You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ak...@apache.org on 2016/09/09 03:26:59 UTC

[31/52] ignite git commit: Web Console beta-3.

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/services/FormUtils.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/FormUtils.service.js b/modules/web-console/frontend/app/services/FormUtils.service.js
new file mode 100644
index 0000000..5e7943a
--- /dev/null
+++ b/modules/web-console/frontend/app/services/FormUtils.service.js
@@ -0,0 +1,435 @@
+/*
+ * 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 ['IgniteFormUtils', ['$window', 'IgniteFocus', ($window, Focus) => {
+    function ensureActivePanel(ui, pnl, focusId) {
+        if (ui) {
+            const collapses = $('div.panel-collapse');
+
+            ui.loadPanel(pnl);
+
+            const idx = _.findIndex(collapses, function(collapse) {
+                return collapse.id === pnl;
+            });
+
+            if (idx >= 0) {
+                const activePanels = ui.activePanels;
+
+                if (!_.includes(ui.topPanels, idx)) {
+                    ui.expanded = true;
+
+                    const customExpanded = ui[pnl];
+
+                    if (customExpanded)
+                        ui[customExpanded] = true;
+                }
+
+                if (!activePanels || activePanels.length < 1)
+                    ui.activePanels = [idx];
+                else if (!_.includes(activePanels, idx)) {
+                    const newActivePanels = angular.copy(activePanels);
+
+                    newActivePanels.push(idx);
+
+                    ui.activePanels = newActivePanels;
+                }
+            }
+
+            if (!_.isNil(focusId))
+                Focus.move(focusId);
+        }
+    }
+
+    let context = null;
+
+    /**
+     * Calculate width of specified text in body's font.
+     *
+     * @param text Text to calculate width.
+     * @returns {Number} Width of text in pixels.
+     */
+    function measureText(text) {
+        if (!context) {
+            const canvas = document.createElement('canvas');
+
+            context = canvas.getContext('2d');
+
+            const style = window.getComputedStyle(document.getElementsByTagName('body')[0]);
+
+            context.font = style.fontSize + ' ' + style.fontFamily;
+        }
+
+        return context.measureText(text).width;
+    }
+
+    /**
+     * Compact java full class name by max number of characters.
+     *
+     * @param names Array of class names to compact.
+     * @param nameLength Max available width in characters for simple name.
+     * @returns {*} Array of compacted class names.
+     */
+    function compactByMaxCharts(names, nameLength) {
+        for (let nameIx = 0; nameIx < names.length; nameIx++) {
+            const s = names[nameIx];
+
+            if (s.length > nameLength) {
+                let totalLength = s.length;
+
+                const packages = s.split('.');
+
+                const packageCnt = packages.length - 1;
+
+                for (let i = 0; i < packageCnt && totalLength > nameLength; i++) {
+                    if (packages[i].length > 0) {
+                        totalLength -= packages[i].length - 1;
+
+                        packages[i] = packages[i][0];
+                    }
+                }
+
+                if (totalLength > nameLength) {
+                    const className = packages[packageCnt];
+
+                    const classNameLen = className.length;
+
+                    let remains = Math.min(nameLength - totalLength + classNameLen, classNameLen);
+
+                    if (remains < 3)
+                        remains = Math.min(3, classNameLen);
+
+                    packages[packageCnt] = className.substring(0, remains) + '...';
+                }
+
+                let result = packages[0];
+
+                for (let i = 1; i < packages.length; i++)
+                    result += '.' + packages[i];
+
+                names[nameIx] = result;
+            }
+        }
+
+        return names;
+    }
+
+    /**
+     * Compact java full class name by max number of pixels.
+     *
+     * @param names Array of class names to compact.
+     * @param nameLength Max available width in characters for simple name. Used for calculation optimization.
+     * @param nameWidth Maximum available width in pixels for simple name.
+     * @returns {*} Array of compacted class names.
+     */
+    function compactByMaxPixels(names, nameLength, nameWidth) {
+        if (nameWidth <= 0)
+            return names;
+
+        const fitted = [];
+
+        const widthByName = [];
+
+        const len = names.length;
+
+        let divideTo = len;
+
+        for (let nameIx = 0; nameIx < len; nameIx++) {
+            fitted[nameIx] = false;
+
+            widthByName[nameIx] = nameWidth;
+        }
+
+        // Try to distribute space from short class names to long class names.
+        let remains = 0;
+
+        do {
+            for (let nameIx = 0; nameIx < len; nameIx++) {
+                if (!fitted[nameIx]) {
+                    const curNameWidth = measureText(names[nameIx]);
+
+                    if (widthByName[nameIx] > curNameWidth) {
+                        fitted[nameIx] = true;
+
+                        remains += widthByName[nameIx] - curNameWidth;
+
+                        divideTo -= 1;
+
+                        widthByName[nameIx] = curNameWidth;
+                    }
+                }
+            }
+
+            const remainsByName = remains / divideTo;
+
+            for (let nameIx = 0; nameIx < len; nameIx++) {
+                if (!fitted[nameIx])
+                    widthByName[nameIx] += remainsByName;
+            }
+        }
+        while (remains > 0);
+
+        // Compact class names to available for each space.
+        for (let nameIx = 0; nameIx < len; nameIx++) {
+            const s = names[nameIx];
+
+            if (s.length > (nameLength / 2 | 0)) {
+                let totalWidth = measureText(s);
+
+                if (totalWidth > widthByName[nameIx]) {
+                    const packages = s.split('.');
+
+                    const packageCnt = packages.length - 1;
+
+                    for (let i = 0; i < packageCnt && totalWidth > widthByName[nameIx]; i++) {
+                        if (packages[i].length > 1) {
+                            totalWidth -= measureText(packages[i].substring(1, packages[i].length));
+
+                            packages[i] = packages[i][0];
+                        }
+                    }
+
+                    let shortPackage = '';
+
+                    for (let i = 0; i < packageCnt; i++)
+                        shortPackage += packages[i] + '.';
+
+                    const className = packages[packageCnt];
+
+                    const classLen = className.length;
+
+                    let minLen = Math.min(classLen, 3);
+
+                    totalWidth = measureText(shortPackage + className);
+
+                    // Compact class name if shorten package path is very long.
+                    if (totalWidth > widthByName[nameIx]) {
+                        let maxLen = classLen;
+                        let middleLen = (minLen + (maxLen - minLen) / 2 ) | 0;
+
+                        while (middleLen !== minLen && middleLen !== maxLen) {
+                            const middleLenPx = measureText(shortPackage + className.substr(0, middleLen) + '...');
+
+                            if (middleLenPx > widthByName[nameIx])
+                                maxLen = middleLen;
+                            else
+                                minLen = middleLen;
+
+                            middleLen = (minLen + (maxLen - minLen) / 2 ) | 0;
+                        }
+
+                        names[nameIx] = shortPackage + className.substring(0, middleLen) + '...';
+                    }
+                    else
+                        names[nameIx] = shortPackage + className;
+                }
+            }
+        }
+
+        return names;
+    }
+
+    /**
+     * Compact any string by max number of pixels.
+     *
+     * @param label String to compact.
+     * @param nameWidth Maximum available width in pixels for simple name.
+     * @returns {*} Compacted string.
+     */
+    function compactLabelByPixels(label, nameWidth) {
+        if (nameWidth <= 0)
+            return label;
+
+        const totalWidth = measureText(label);
+
+        if (totalWidth > nameWidth) {
+            let maxLen = label.length;
+            let minLen = Math.min(maxLen, 3);
+            let middleLen = (minLen + (maxLen - minLen) / 2 ) | 0;
+
+            while (middleLen !== minLen && middleLen !== maxLen) {
+                const middleLenPx = measureText(label.substr(0, middleLen) + '...');
+
+                if (middleLenPx > nameWidth)
+                    maxLen = middleLen;
+                else
+                    minLen = middleLen;
+
+                middleLen = (minLen + (maxLen - minLen) / 2 ) | 0;
+            }
+
+            return label.substring(0, middleLen) + '...';
+        }
+
+        return label;
+    }
+
+    /**
+     * Calculate available width for text in link to edit element.
+     *
+     * @param index Showed index of element for calculation of maximum width in pixels.
+     * @param id Id of contains link table.
+     * @returns {*[]} First element is length of class for single value, second element is length for pair vlaue.
+     */
+    function availableWidth(index, id) {
+        const idElem = $('#' + id);
+
+        let width = 0;
+
+        switch (idElem.prop('tagName')) {
+            // Detection of available width in presentation table row.
+            case 'TABLE':
+                const cont = $(idElem.find('tr')[index - 1]).find('td')[0];
+
+                width = cont.clientWidth;
+
+                if (width > 0) {
+                    const children = $(cont).children(':not("a")');
+
+                    _.forEach(children, function(child) {
+                        if ('offsetWidth' in child)
+                            width -= $(child).outerWidth(true);
+                    });
+                }
+
+                break;
+
+            // Detection of available width in dropdown row.
+            case 'A':
+                width = idElem.width();
+
+                $(idElem).children(':not("span")').each(function(ix, child) {
+                    if ('offsetWidth' in child)
+                        width -= child.offsetWidth;
+                });
+
+                break;
+
+            default:
+        }
+
+        return width | 0;
+    }
+
+    return {
+        /**
+         * Cut class name by width in pixel or width in symbol count.
+         *
+         * @param id Id of parent table.
+         * @param index Row number in table.
+         * @param maxLength Maximum length in symbols for all names.
+         * @param names Array of class names to compact.
+         * @param divider String to visualy divide items.
+         * @returns {*} Array of compacted class names.
+         */
+        compactJavaName(id, index, maxLength, names, divider) {
+            divider = ' ' + divider + ' ';
+
+            const prefix = index + ') ';
+
+            const nameCnt = names.length;
+
+            const nameLength = ((maxLength - 3 * (nameCnt - 1)) / nameCnt) | 0;
+
+            try {
+                const nameWidth = (availableWidth(index, id) - measureText(prefix) - (nameCnt - 1) * measureText(divider)) /
+                    nameCnt | 0;
+
+                // HTML5 calculation of showed message width.
+                names = compactByMaxPixels(names, nameLength, nameWidth);
+            }
+            catch (err) {
+                names = compactByMaxCharts(names, nameLength);
+            }
+
+            let result = prefix + names[0];
+
+            for (let nameIx = 1; nameIx < names.length; nameIx++)
+                result += divider + names[nameIx];
+
+            return result;
+        },
+        /**
+         * Compact text by width in pixels or symbols count.
+         *
+         * @param id Id of parent table.
+         * @param index Row number in table.
+         * @param maxLength Maximum length in symbols for all names.
+         * @param label Text to compact.
+         * @returns Compacted label text.
+         */
+        compactTableLabel(id, index, maxLength, label) {
+            label = index + ') ' + label;
+
+            try {
+                const nameWidth = availableWidth(index, id) | 0;
+
+                // HTML5 calculation of showed message width.
+                label = compactLabelByPixels(label, nameWidth);
+            }
+            catch (err) {
+                const nameLength = maxLength - 3 | 0;
+
+                label = label.length > maxLength ? label.substr(0, nameLength) + '...' : label;
+            }
+
+            return label;
+        },
+        widthIsSufficient(id, index, text) {
+            try {
+                const available = availableWidth(index, id);
+
+                const required = measureText(text);
+
+                return !available || available >= Math.floor(required);
+            }
+            catch (err) {
+                return true;
+            }
+        },
+        ensureActivePanel(panels, id, focusId) {
+            ensureActivePanel(panels, id, focusId);
+        },
+        confirmUnsavedChanges(dirty, selectFunc) {
+            if (dirty) {
+                if ($window.confirm('You have unsaved changes.\n\nAre you sure you want to discard them?'))
+                    selectFunc();
+            }
+            else
+                selectFunc();
+        },
+        saveBtnTipText(dirty, objectName) {
+            if (dirty)
+                return 'Save ' + objectName;
+
+            return 'Nothing to save';
+        },
+        formUI() {
+            return {
+                ready: false,
+                expanded: false,
+                loadedPanels: [],
+                loadPanel(pnl) {
+                    if (!_.includes(this.loadedPanels, pnl))
+                        this.loadedPanels.push(pnl);
+                },
+                isPanelLoaded(pnl) {
+                    return _.includes(this.loadedPanels, pnl);
+                }
+            };
+        }
+    };
+}]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/services/InetAddress.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/InetAddress.service.js b/modules/web-console/frontend/app/services/InetAddress.service.js
new file mode 100644
index 0000000..abdd8a3
--- /dev/null
+++ b/modules/web-console/frontend/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/6af6560a/modules/web-console/frontend/app/services/JavaTypes.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/JavaTypes.service.js b/modules/web-console/frontend/app/services/JavaTypes.service.js
new file mode 100644
index 0000000..e8d4903
--- /dev/null
+++ b/modules/web-console/frontend/app/services/JavaTypes.service.js
@@ -0,0 +1,93 @@
+/*
+ * 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 '../data/java-classes.json';
+
+// Java build-in primitive.
+import JAVA_PRIMITIVES from '../data/java-primitives.json';
+
+import JAVA_KEYWORDS from '../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 valid Java UUID value.
+         */
+        validUUID(value) {
+            const regexp = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/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/6af6560a/modules/web-console/frontend/app/services/LegacyTable.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/LegacyTable.service.js b/modules/web-console/frontend/app/services/LegacyTable.service.js
new file mode 100644
index 0000000..5d9ec9d
--- /dev/null
+++ b/modules/web-console/frontend/app/services/LegacyTable.service.js
@@ -0,0 +1,209 @@
+/*
+ * 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.
+ */
+
+// TODO: Refactor this service for legacy tables with more than one input field.
+export default ['IgniteLegacyTable',
+    ['IgniteLegacyUtils', 'IgniteFocus', 'IgniteErrorPopover', (LegacyUtils, Focus, ErrorPopover) => {
+        function _model(item, field) {
+            return LegacyUtils.getModel(item, field);
+        }
+
+        const table = {name: 'none', editIndex: -1};
+
+        function _tableReset() {
+            delete table.field;
+            table.name = 'none';
+            table.editIndex = -1;
+
+            ErrorPopover.hide();
+        }
+
+        function _tableSaveAndReset() {
+            const field = table.field;
+
+            const save = LegacyUtils.isDefined(field) && LegacyUtils.isDefined(field.save);
+
+            if (!save || !LegacyUtils.isDefined(field) || field.save(field, table.editIndex, true)) {
+                _tableReset();
+
+                return true;
+            }
+
+            return false;
+        }
+
+        function _tableState(field, editIndex, specName) {
+            table.field = field;
+            table.name = specName || field.model;
+            table.editIndex = editIndex;
+        }
+
+        function _tableUI(field) {
+            const ui = field.ui;
+
+            return ui ? ui : field.type;
+        }
+
+        function _tableFocus(focusId, index) {
+            Focus.move((index < 0 ? 'new' : 'cur') + focusId + (index >= 0 ? index : ''));
+        }
+
+        function _tablePairValue(filed, index) {
+            return index < 0 ? {key: filed.newKey, value: filed.newValue} : {
+                key: filed.curKey,
+                value: filed.curValue
+            };
+        }
+
+        function _tableStartEdit(item, tbl, index, save) {
+            _tableState(tbl, index);
+
+            const val = _.get(_model(item, tbl), tbl.model)[index];
+
+            const ui = _tableUI(tbl);
+
+            tbl.save = save;
+
+            if (ui === 'table-pair') {
+                tbl.curKey = val[tbl.keyName];
+                tbl.curValue = val[tbl.valueName];
+
+                _tableFocus('Key' + tbl.focusId, index);
+            }
+            else if (ui === 'table-db-fields') {
+                tbl.curDatabaseFieldName = val.databaseFieldName;
+                tbl.curDatabaseFieldType = val.databaseFieldType;
+                tbl.curJavaFieldName = val.javaFieldName;
+                tbl.curJavaFieldType = val.javaFieldType;
+
+                _tableFocus('DatabaseFieldName' + tbl.focusId, index);
+            }
+            else if (ui === 'table-indexes') {
+                tbl.curIndexName = val.name;
+                tbl.curIndexType = val.indexType;
+                tbl.curIndexFields = val.fields;
+
+                _tableFocus(tbl.focusId, index);
+            }
+        }
+
+        function _tableNewItem(tbl) {
+            _tableState(tbl, -1);
+
+            const ui = _tableUI(tbl);
+
+            if (ui === 'table-pair') {
+                tbl.newKey = null;
+                tbl.newValue = null;
+
+                _tableFocus('Key' + tbl.focusId, -1);
+            }
+            else if (ui === 'table-db-fields') {
+                tbl.newDatabaseFieldName = null;
+                tbl.newDatabaseFieldType = null;
+                tbl.newJavaFieldName = null;
+                tbl.newJavaFieldType = null;
+
+                _tableFocus('DatabaseFieldName' + tbl.focusId, -1);
+            }
+            else if (ui === 'table-indexes') {
+                tbl.newIndexName = null;
+                tbl.newIndexType = 'SORTED';
+                tbl.newIndexFields = null;
+
+                _tableFocus(tbl.focusId, -1);
+            }
+        }
+
+        return {
+            tableState: _tableState,
+            tableReset: _tableReset,
+            tableSaveAndReset: _tableSaveAndReset,
+            tableNewItem: _tableNewItem,
+            tableNewItemActive(tbl) {
+                return table.name === tbl.model && table.editIndex < 0;
+            },
+            tableEditing(tbl, index) {
+                return table.name === tbl.model && table.editIndex === index;
+            },
+            tableEditedRowIndex() {
+                return table.editIndex;
+            },
+            tableField() {
+                return table.field;
+            },
+            tableStartEdit: _tableStartEdit,
+            tableRemove(item, field, index) {
+                _tableReset();
+
+                _.get(_model(item, field), field.model).splice(index, 1);
+            },
+            tablePairValue: _tablePairValue,
+            tablePairSave(pairValid, item, field, index, stopEdit) {
+                const valid = pairValid(item, field, index, stopEdit);
+
+                if (valid) {
+                    const pairValue = _tablePairValue(field, index);
+
+                    let pairModel = {};
+
+                    const container = _.get(item, field.model);
+
+                    if (index < 0) {
+                        pairModel[field.keyName] = pairValue.key;
+                        pairModel[field.valueName] = pairValue.value;
+
+                        if (container)
+                            container.push(pairModel);
+                        else
+                            _.set(item, field.model, [pairModel]);
+
+                        if (!stopEdit)
+                            _tableNewItem(field);
+                    }
+                    else {
+                        pairModel = container[index];
+
+                        pairModel[field.keyName] = pairValue.key;
+                        pairModel[field.valueName] = pairValue.value;
+
+                        if (!stopEdit) {
+                            if (index < container.length - 1)
+                                _tableStartEdit(item, field, index + 1);
+                            else
+                                _tableNewItem(field);
+                        }
+                    }
+                }
+
+                return valid;
+            },
+            tablePairSaveVisible(field, index) {
+                const pairValue = _tablePairValue(field, index);
+
+                return !LegacyUtils.isEmptyString(pairValue.key) && !LegacyUtils.isEmptyString(pairValue.value);
+            },
+            tableFocusInvalidField(index, id) {
+                _tableFocus(id, index);
+
+                return false;
+            },
+            tableFieldId(index, id) {
+                return (index < 0 ? 'new' : 'cur') + id + (index >= 0 ? index : '');
+            }
+        };
+    }]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/services/LegacyUtils.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/LegacyUtils.service.js b/modules/web-console/frontend/app/services/LegacyUtils.service.js
new file mode 100644
index 0000000..ed555a1
--- /dev/null
+++ b/modules/web-console/frontend/app/services/LegacyUtils.service.js
@@ -0,0 +1,572 @@
+/*
+ * 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.
+ */
+
+// TODO: Refactor this service for legacy tables with more than one input field.
+export default ['IgniteLegacyUtils', ['IgniteErrorPopover', (ErrorPopover) => {
+    function isDefined(v) {
+        return !_.isNil(v);
+    }
+
+    function isEmptyString(s) {
+        if (isDefined(s))
+            return s.trim().length === 0;
+
+        return true;
+    }
+
+    const javaBuiltInClasses = [
+        'BigDecimal',
+        'Boolean',
+        'Byte',
+        'Date',
+        'Double',
+        'Float',
+        'Integer',
+        'Long',
+        'Object',
+        'Short',
+        'String',
+        'Time',
+        'Timestamp',
+        'UUID'
+    ];
+
+    const javaBuiltInTypes = [
+        'BigDecimal',
+        'boolean',
+        'Boolean',
+        'byte',
+        'Byte',
+        'Date',
+        'double',
+        'Double',
+        'float',
+        'Float',
+        'int',
+        'Integer',
+        'long',
+        'Long',
+        'Object',
+        'short',
+        'Short',
+        'String',
+        'Time',
+        'Timestamp',
+        'UUID'
+    ];
+
+    const javaBuiltInFullNameClasses = [
+        'java.math.BigDecimal',
+        'java.lang.Boolean',
+        'java.lang.Byte',
+        'java.sql.Date',
+        'java.lang.Double',
+        'java.lang.Float',
+        'java.lang.Integer',
+        'java.lang.Long',
+        'java.lang.Object',
+        'java.lang.Short',
+        'java.lang.String',
+        'java.sql.Time',
+        'java.sql.Timestamp',
+        'java.util.UUID'
+    ];
+
+    /**
+     * @param clsName Class name to check.
+     * @returns {Boolean} 'true' if given class name is a java build-in type.
+     */
+    function isJavaBuiltInClass(clsName) {
+        if (isEmptyString(clsName))
+            return false;
+
+        return _.includes(javaBuiltInClasses, clsName) || _.includes(javaBuiltInFullNameClasses, clsName);
+    }
+
+    const SUPPORTED_JDBC_TYPES = [
+        'BIGINT',
+        'BIT',
+        'BOOLEAN',
+        'BLOB',
+        'CHAR',
+        'CLOB',
+        'DATE',
+        'DECIMAL',
+        'DOUBLE',
+        'FLOAT',
+        'INTEGER',
+        'LONGNVARCHAR',
+        'LONGVARCHAR',
+        'NCHAR',
+        'NUMERIC',
+        'NVARCHAR',
+        'REAL',
+        'SMALLINT',
+        'TIME',
+        'TIMESTAMP',
+        'TINYINT',
+        'VARCHAR'
+    ];
+
+    const ALL_JDBC_TYPES = [
+        {dbName: 'BIT', dbType: -7, javaType: 'Boolean', primitiveType: 'boolean'},
+        {dbName: 'TINYINT', dbType: -6, javaType: 'Byte', primitiveType: 'byte'},
+        {dbName: 'SMALLINT', dbType: 5, javaType: 'Short', primitiveType: 'short'},
+        {dbName: 'INTEGER', dbType: 4, javaType: 'Integer', primitiveType: 'int'},
+        {dbName: 'BIGINT', dbType: -5, javaType: 'Long', primitiveType: 'long'},
+        {dbName: 'FLOAT', dbType: 6, javaType: 'Float', primitiveType: 'float'},
+        {dbName: 'REAL', dbType: 7, javaType: 'Double', primitiveType: 'double'},
+        {dbName: 'DOUBLE', dbType: 8, javaType: 'Double', primitiveType: 'double'},
+        {dbName: 'NUMERIC', dbType: 2, javaType: 'BigDecimal'},
+        {dbName: 'DECIMAL', dbType: 3, javaType: 'BigDecimal'},
+        {dbName: 'CHAR', dbType: 1, javaType: 'String'},
+        {dbName: 'VARCHAR', dbType: 12, javaType: 'String'},
+        {dbName: 'LONGVARCHAR', dbType: -1, javaType: 'String'},
+        {dbName: 'DATE', dbType: 91, javaType: 'Date'},
+        {dbName: 'TIME', dbType: 92, javaType: 'Time'},
+        {dbName: 'TIMESTAMP', dbType: 93, javaType: 'Timestamp'},
+        {dbName: 'BINARY', dbType: -2, javaType: 'Object'},
+        {dbName: 'VARBINARY', dbType: -3, javaType: 'Object'},
+        {dbName: 'LONGVARBINARY', dbType: -4, javaType: 'Object'},
+        {dbName: 'NULL', dbType: 0, javaType: 'Object'},
+        {dbName: 'OTHER', dbType: 1111, javaType: 'Object'},
+        {dbName: 'JAVA_OBJECT', dbType: 2000, javaType: 'Object'},
+        {dbName: 'DISTINCT', dbType: 2001, javaType: 'Object'},
+        {dbName: 'STRUCT', dbType: 2002, javaType: 'Object'},
+        {dbName: 'ARRAY', dbType: 2003, javaType: 'Object'},
+        {dbName: 'BLOB', dbType: 2004, javaType: 'Object'},
+        {dbName: 'CLOB', dbType: 2005, javaType: 'String'},
+        {dbName: 'REF', dbType: 2006, javaType: 'Object'},
+        {dbName: 'DATALINK', dbType: 70, javaType: 'Object'},
+        {dbName: 'BOOLEAN', dbType: 16, javaType: 'Boolean', primitiveType: 'boolean'},
+        {dbName: 'ROWID', dbType: -8, javaType: 'Object'},
+        {dbName: 'NCHAR', dbType: -15, javaType: 'String'},
+        {dbName: 'NVARCHAR', dbType: -9, javaType: 'String'},
+        {dbName: 'LONGNVARCHAR', dbType: -16, javaType: 'String'},
+        {dbName: 'NCLOB', dbType: 2011, javaType: 'String'},
+        {dbName: 'SQLXML', dbType: 2009, javaType: 'Object'}
+    ];
+
+    /*eslint-disable */
+    const JAVA_KEYWORDS = [
+        'abstract',
+        'assert',
+        'boolean',
+        'break',
+        'byte',
+        'case',
+        'catch',
+        'char',
+        'class',
+        'const',
+        'continue',
+        'default',
+        'do',
+        'double',
+        'else',
+        'enum',
+        'extends',
+        'false',
+        'final',
+        'finally',
+        'float',
+        'for',
+        'goto',
+        'if',
+        'implements',
+        'import',
+        'instanceof',
+        'int',
+        'interface',
+        'long',
+        'native',
+        'new',
+        'null',
+        'package',
+        'private',
+        'protected',
+        'public',
+        'return',
+        'short',
+        'static',
+        'strictfp',
+        'super',
+        'switch',
+        'synchronized',
+        'this',
+        'throw',
+        'throws',
+        'transient',
+        'true',
+        'try',
+        'void',
+        'volatile',
+        'while'
+    ];
+    /*eslint-enable */
+
+    const VALID_JAVA_IDENTIFIER = new RegExp('^[a-zA-Z_$][a-zA-Z\\d_$]*$');
+
+    function isValidJavaIdentifier(msg, ident, elemId, panels, panelId) {
+        if (isEmptyString(ident))
+            return ErrorPopover.show(elemId, msg + ' is invalid!', panels, panelId);
+
+        if (_.includes(JAVA_KEYWORDS, ident))
+            return ErrorPopover.show(elemId, msg + ' could not contains reserved java keyword: "' + ident + '"!', panels, panelId);
+
+        if (!VALID_JAVA_IDENTIFIER.test(ident))
+            return ErrorPopover.show(elemId, msg + ' contains invalid identifier: "' + ident + '"!', panels, panelId);
+
+        return true;
+    }
+
+    function getModel(obj, field) {
+        let path = field.path;
+
+        if (!isDefined(path) || !isDefined(obj))
+            return obj;
+
+        path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
+        path = path.replace(/^\./, '');           // strip a leading dot
+
+        const segs = path.split('.');
+        let root = obj;
+
+        while (segs.length > 0) {
+            const pathStep = segs.shift();
+
+            if (typeof root[pathStep] === 'undefined')
+                root[pathStep] = {};
+
+            root = root[pathStep];
+        }
+
+        return root;
+    }
+
+    /**
+     * Extract datasource from cache or cluster.
+     *
+     * @param object Cache or cluster to extract datasource.
+     * @returns {*} Datasource object or null if not set.
+     */
+    function extractDataSource(object) {
+        // Extract from cluster object
+        if (_.get(object, 'discovery.kind') === 'Jdbc') {
+            const datasource = object.discovery.Jdbc;
+
+            if (datasource.dataSourceBean && datasource.dialect)
+                return datasource;
+        } // Extract from cache object
+        else if (_.get(object, 'cacheStoreFactory.kind')) {
+            const storeFactory = object.cacheStoreFactory[object.cacheStoreFactory.kind];
+
+            if (storeFactory.dialect || (storeFactory.connectVia === 'DataSource'))
+                return storeFactory;
+        }
+
+        return null;
+    }
+
+    const cacheStoreJdbcDialects = [
+        {value: 'Generic', label: 'Generic JDBC'},
+        {value: 'Oracle', label: 'Oracle'},
+        {value: 'DB2', label: 'IBM DB2'},
+        {value: 'SQLServer', label: 'Microsoft SQL Server'},
+        {value: 'MySQL', label: 'MySQL'},
+        {value: 'PostgreSQL', label: 'PostgreSQL'},
+        {value: 'H2', label: 'H2 database'}
+    ];
+
+    function domainForStoreConfigured(domain) {
+        const isEmpty = !isDefined(domain) || (isEmptyString(domain.databaseSchema) &&
+            isEmptyString(domain.databaseTable) &&
+            _.isEmpty(domain.keyFields) &&
+            _.isEmpty(domain.valueFields));
+
+        return !isEmpty;
+    }
+
+    const DS_CHECK_SUCCESS = {checked: true};
+
+    /**
+     * Compare datasources of caches or clusters.
+     *
+     * @param firstObj First cache or cluster.
+     * @param secondObj Second cache or cluster.
+     * @returns {*} Check result object.
+     */
+    function compareDataSources(firstObj, secondObj) {
+        const firstDs = extractDataSource(firstObj);
+        const secondDs = extractDataSource(secondObj);
+
+        if (firstDs && secondDs) {
+            const firstDB = firstDs.dialect;
+            const secondDB = secondDs.dialect;
+
+            if (firstDs.dataSourceBean === secondDs.dataSourceBean && firstDB !== secondDB)
+                return {checked: false, firstObj, firstDB, secondObj, secondDB};
+        }
+
+        return DS_CHECK_SUCCESS;
+    }
+
+    function compareSQLSchemaNames(firstCache, secondCache) {
+        const firstName = firstCache.sqlSchema;
+        const secondName = secondCache.sqlSchema;
+
+        if (firstName && secondName && (firstName === secondName))
+            return {checked: false, firstCache, secondCache};
+
+        return DS_CHECK_SUCCESS;
+    }
+
+    function toJavaName(prefix, name) {
+        const javaName = name ? name.replace(/[^A-Za-z_0-9]+/g, '_') : 'dflt';
+
+        return prefix + javaName.charAt(0).toLocaleUpperCase() + javaName.slice(1);
+    }
+
+    return {
+        getModel,
+        mkOptions(options) {
+            return _.map(options, (option) => {
+                return {value: option, label: isDefined(option) ? option : 'Not set'};
+            });
+        },
+        isDefined,
+        hasProperty(obj, props) {
+            for (const propName in props) {
+                if (props.hasOwnProperty(propName)) {
+                    if (obj[propName])
+                        return true;
+                }
+            }
+
+            return false;
+        },
+        isEmptyString,
+        SUPPORTED_JDBC_TYPES,
+        findJdbcType(jdbcType) {
+            const res = _.find(ALL_JDBC_TYPES, (item) => item.dbType === jdbcType);
+
+            return res ? res : {dbName: 'Unknown', javaType: 'Unknown'};
+        },
+        javaBuiltInClasses,
+        javaBuiltInTypes,
+        isJavaBuiltInClass,
+        isValidJavaIdentifier,
+        isValidJavaClass(msg, ident, allowBuiltInClass, elemId, packageOnly, panels, panelId) {
+            if (isEmptyString(ident))
+                return ErrorPopover.show(elemId, msg + ' could not be empty!', panels, panelId);
+
+            const parts = ident.split('.');
+
+            const len = parts.length;
+
+            if (!allowBuiltInClass && isJavaBuiltInClass(ident))
+                return ErrorPopover.show(elemId, msg + ' should not be the Java build-in class!', panels, panelId);
+
+            if (len < 2 && !isJavaBuiltInClass(ident) && !packageOnly)
+                return ErrorPopover.show(elemId, msg + ' does not have package specified!', panels, panelId);
+
+            for (let i = 0; i < parts.length; i++) {
+                const part = parts[i];
+
+                if (!isValidJavaIdentifier(msg, part, elemId, panels, panelId))
+                    return false;
+            }
+
+            return true;
+        },
+        domainForQueryConfigured(domain) {
+            const isEmpty = !isDefined(domain) || (_.isEmpty(domain.fields) &&
+                _.isEmpty(domain.aliases) &&
+                _.isEmpty(domain.indexes));
+
+            return !isEmpty;
+        },
+        domainForStoreConfigured,
+        download(type, name, data) {
+            const file = document.createElement('a');
+
+            file.setAttribute('href', 'data:' + type + ';charset=utf-8,' + data);
+            file.setAttribute('download', name);
+            file.setAttribute('target', '_self');
+
+            file.style.display = 'none';
+
+            document.body.appendChild(file);
+
+            file.click();
+
+            document.body.removeChild(file);
+        },
+        getQueryVariable(name) {
+            const attrs = window.location.search.substring(1).split('&');
+            const attr = _.find(attrs, (a) => a === name || (a.indexOf('=') >= 0 && a.substr(0, a.indexOf('=')) === name));
+
+            if (!isDefined(attr))
+                return null;
+
+            if (attr === name)
+                return true;
+
+            return attr.substr(attr.indexOf('=') + 1);
+        },
+        cacheStoreJdbcDialects,
+        cacheStoreJdbcDialectsLabel(dialect) {
+            const found = _.find(cacheStoreJdbcDialects, (dialectVal) => dialectVal.value === dialect);
+
+            return found ? found.label : null;
+        },
+        checkDataSources(cluster, caches, checkCacheExt) {
+            let res = DS_CHECK_SUCCESS;
+
+            _.find(caches, (curCache, curIx) => {
+                res = compareDataSources(curCache, cluster);
+
+                if (!res.checked)
+                    return true;
+
+                if (isDefined(checkCacheExt)) {
+                    if (checkCacheExt._id !== curCache._id) {
+                        res = compareDataSources(checkCacheExt, curCache);
+
+                        return !res.checked;
+                    }
+
+                    return false;
+                }
+
+                return _.find(caches, (checkCache, checkIx) => {
+                    if (checkIx < curIx) {
+                        res = compareDataSources(checkCache, curCache);
+
+                        return !res.checked;
+                    }
+
+                    return false;
+                });
+            });
+
+            return res;
+        },
+        checkCacheSQLSchemas(caches, checkCacheExt) {
+            let res = DS_CHECK_SUCCESS;
+
+            _.find(caches, (curCache, curIx) => {
+                if (isDefined(checkCacheExt)) {
+                    if (checkCacheExt._id !== curCache._id) {
+                        res = compareSQLSchemaNames(checkCacheExt, curCache);
+
+                        return !res.checked;
+                    }
+
+                    return false;
+                }
+
+                return _.find(caches, (checkCache, checkIx) => {
+                    if (checkIx < curIx) {
+                        res = compareSQLSchemaNames(checkCache, curCache);
+
+                        return !res.checked;
+                    }
+
+                    return false;
+                });
+            });
+
+            return res;
+        },
+        autoCacheStoreConfiguration(cache, domains) {
+            const cacheStoreFactory = isDefined(cache.cacheStoreFactory) &&
+                isDefined(cache.cacheStoreFactory.kind);
+
+            if (!cacheStoreFactory && _.findIndex(domains, domainForStoreConfigured) >= 0) {
+                const dflt = !cache.readThrough && !cache.writeThrough;
+
+                return {
+                    cacheStoreFactory: {
+                        kind: 'CacheJdbcPojoStoreFactory',
+                        CacheJdbcPojoStoreFactory: {
+                            dataSourceBean: toJavaName('ds', cache.name),
+                            dialect: 'Generic'
+                        },
+                        CacheJdbcBlobStoreFactory: {connectVia: 'DataSource'}
+                    },
+                    readThrough: dflt || cache.readThrough,
+                    writeThrough: dflt || cache.writeThrough
+                };
+            }
+        },
+        autoClusterSwapSpiConfiguration(cluster, caches) {
+            const swapConfigured = cluster.swapSpaceSpi && cluster.swapSpaceSpi.kind;
+
+            if (!swapConfigured && _.find(caches, (cache) => cache.swapEnabled))
+                return {swapSpaceSpi: {kind: 'FileSwapSpaceSpi'}};
+
+            return null;
+        },
+        randomString(len) {
+            const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+            const possibleLen = possible.length;
+
+            let res = '';
+
+            for (let i = 0; i < len; i++)
+                res += possible.charAt(Math.floor(Math.random() * possibleLen));
+
+            return res;
+        },
+        checkFieldValidators(ui) {
+            const form = ui.inputForm;
+            const errors = form.$error;
+            const errKeys = Object.keys(errors);
+
+            if (errKeys && errKeys.length > 0) {
+                const firstErrorKey = errKeys[0];
+
+                const firstError = errors[firstErrorKey][0];
+                const actualError = firstError.$error[firstErrorKey][0];
+
+                const errNameFull = actualError.$name;
+                const errNameShort = errNameFull.endsWith('TextInput') ? errNameFull.substring(0, errNameFull.length - 9) : errNameFull;
+
+                const extractErrorMessage = (errName) => {
+                    try {
+                        return errors[firstErrorKey][0].$errorMessages[errName][firstErrorKey];
+                    }
+                    catch (ignored) {
+                        try {
+                            return form[firstError.$name].$errorMessages[errName][firstErrorKey];
+                        }
+                        catch (ignited) {
+                            return false;
+                        }
+                    }
+                };
+
+                const msg = extractErrorMessage(errNameFull) || extractErrorMessage(errNameShort) || 'Invalid value!';
+
+                return ErrorPopover.show(errNameFull, msg, ui, firstError.$name);
+            }
+
+            return true;
+        }
+    };
+}]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/services/Messages.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/Messages.service.js b/modules/web-console/frontend/app/services/Messages.service.js
new file mode 100644
index 0000000..e679488
--- /dev/null
+++ b/modules/web-console/frontend/app/services/Messages.service.js
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+// Service to show various information and error messages.
+export default ['IgniteMessages', ['$alert', ($alert) => {
+    // Common instance of alert modal.
+    let msgModal;
+
+    const errorMessage = (prefix, err) => {
+        prefix = prefix || '';
+
+        if (err) {
+            if (err.hasOwnProperty('message'))
+                return prefix + err.message;
+
+            return prefix + err;
+        }
+
+        return prefix + 'Internal error.';
+    };
+
+    const hideAlert = () => {
+        if (msgModal)
+            msgModal.hide();
+    };
+
+    const _showMessage = (err, type, duration, icon) => {
+        hideAlert();
+
+        const title = errorMessage(null, err);
+
+        msgModal = $alert({type, title, duration});
+
+        msgModal.$scope.icon = icon;
+    };
+
+    return {
+        errorMessage,
+        hideAlert,
+        showError(err) {
+            _showMessage(err, 'danger', 10, 'fa-exclamation-triangle');
+
+            return false;
+        },
+        showInfo(err) {
+            _showMessage(err, 'success', 3, 'fa-check-circle-o');
+        }
+    };
+}]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/services/ModelNormalizer.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/ModelNormalizer.service.js b/modules/web-console/frontend/app/services/ModelNormalizer.service.js
new file mode 100644
index 0000000..4c7052b
--- /dev/null
+++ b/modules/web-console/frontend/app/services/ModelNormalizer.service.js
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+// Service to normalize objects for dirty checks.
+export default ['IgniteModelNormalizer', () => {
+    /**
+     * Normalize object for dirty checks.
+     *
+     * @param original
+     * @param dest
+     * @returns {*}
+     */
+    const normalize = (original, dest) => {
+        if (_.isUndefined(original))
+            return dest;
+
+        if (_.isObject(original)) {
+            _.forOwn(original, (value, key) => {
+                if (/\$\$hashKey/.test(key))
+                    return;
+
+                const attr = normalize(value);
+
+                if (!_.isNil(attr)) {
+                    dest = dest || {};
+                    dest[key] = attr;
+                }
+            });
+        } else if (_.isBoolean(original) && original === true)
+            dest = original;
+        else if ((_.isString(original) && original.length) || _.isNumber(original))
+            dest = original;
+        else if (_.isArray(original) && original.length)
+            dest = _.map(original, (value) => normalize(value, {}));
+
+        return dest;
+    };
+
+    return {
+        normalize,
+        isEqual(prev, cur) {
+            return _.isEqual(prev, normalize(cur));
+        }
+    };
+}];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/services/UnsavedChangesGuard.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/UnsavedChangesGuard.service.js b/modules/web-console/frontend/app/services/UnsavedChangesGuard.service.js
new file mode 100644
index 0000000..91244b0
--- /dev/null
+++ b/modules/web-console/frontend/app/services/UnsavedChangesGuard.service.js
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+const MSG = 'You have unsaved changes.\n\nAre you sure you want to discard them?';
+
+// Service that show confirmation about unsaved changes on user change location.
+export default ['IgniteUnsavedChangesGuard', ['$rootScope', ($root) => {
+    return {
+        install(scope, customDirtyCheck = () => scope.ui.inputForm.$dirty) {
+            scope.$on('$destroy', () => window.onbeforeunload = null);
+
+            const unbind = $root.$on('$stateChangeStart', (event) => {
+                if (_.get(scope, 'ui.inputForm', false) && customDirtyCheck()) {
+                    if (!confirm(MSG)) // eslint-disable-line no-alert
+                        event.preventDefault();
+                    else
+                        unbind();
+                }
+            });
+
+            window.onbeforeunload = () => _.get(scope, 'ui.inputForm.$dirty', false) ? MSG : null;
+        }
+    };
+}]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/vendor.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/vendor.js b/modules/web-console/frontend/app/vendor.js
new file mode 100644
index 0000000..0322887
--- /dev/null
+++ b/modules/web-console/frontend/app/vendor.js
@@ -0,0 +1,55 @@
+/*
+ * 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 'jquery';
+import 'angular';
+import 'angular-acl';
+import 'angular-animate';
+import 'angular-sanitize';
+import 'angular-strap';
+import 'angular-strap/dist/angular-strap.tpl';
+import 'angular-socket-io';
+import 'angular-retina';
+import 'angular-ui-router';
+import 'ui-router-metatags/dist/ui-router-metatags';
+import 'angular-smart-table';
+import 'angular-ui-grid/ui-grid';
+import 'angular-drag-and-drop-lists';
+import 'angular-nvd3';
+import 'angular-tree-control';
+import 'angular-gridster';
+import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
+import 'bootstrap-sass/assets/javascripts/bootstrap/carousel';
+import 'brace';
+import 'brace/mode/xml';
+import 'brace/mode/sql';
+import 'brace/mode/java';
+import 'brace/mode/dockerfile';
+import 'brace/mode/snippets';
+import 'brace/theme/chrome';
+import 'brace/ext/language_tools';
+import 'brace/ext/searchbox';
+import 'file-saver';
+import 'jszip';
+import 'nvd3';
+import 'query-command-supported';
+import 'angular-gridster/dist/angular-gridster.min.css';
+import 'angular-tree-control/css/tree-control-attribute.css';
+import 'angular-tree-control/css/tree-control.css';
+import 'angular-ui-grid/ui-grid.css';
+import 'angular-motion/dist/angular-motion.css';
+import 'nvd3/build/nv.d3.css';

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/controllers/admin-controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/controllers/admin-controller.js b/modules/web-console/frontend/controllers/admin-controller.js
new file mode 100644
index 0000000..57a39b2
--- /dev/null
+++ b/modules/web-console/frontend/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.
+export default ['adminController', [
+    '$rootScope', '$scope', '$http', '$q', '$state', 'IgniteMessages', 'IgniteConfirm', 'User', 'IgniteCountries',
+    ($rootScope, $scope, $http, $q, $state, Messages, Confirm, User, Countries) => {
+        $scope.users = null;
+
+        const _reloadUsers = () => {
+            $http.post('/api/v1/admin/list')
+                .success((users) => {
+                    $scope.users = users;
+
+                    _.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.countryCode || '');
+                    });
+                })
+                .error(Messages.showError);
+        };
+
+        _reloadUsers();
+
+        $scope.becomeUser = function(user) {
+            $http.get('/api/v1/admin/become', { params: {viewedUserId: user._id}})
+                .catch(({data}) => Promise.reject(data))
+                .then(User.load)
+                .then((becomeUser) => {
+                    $rootScope.$broadcast('user', becomeUser);
+
+                    $state.go('base.configuration.clusters');
+                })
+                .catch(Messages.showError);
+        };
+
+        $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);
+
+                            Messages.showInfo('User has been removed: "' + user.userName + '"');
+                        })
+                        .error((err, status) => {
+                            if (status === 503)
+                                Messages.showInfo(err);
+                            else
+                                Messages.showError(Messages.errorMessage('Failed to remove user: ', err));
+                        });
+                });
+        };
+
+        $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;
+
+                    Messages.showInfo('Admin right was successfully toggled for user: "' + user.userName + '"');
+                })
+                .error((err) => {
+                    Messages.showError(Messages.errorMessage('Failed to toggle admin right for user: ', err));
+                })
+                .finally(() => user.adminChanging = false);
+        };
+    }
+]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/controllers/caches-controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/controllers/caches-controller.js b/modules/web-console/frontend/controllers/caches-controller.js
new file mode 100644
index 0000000..9873051
--- /dev/null
+++ b/modules/web-console/frontend/controllers/caches-controller.js
@@ -0,0 +1,524 @@
+/*
+ * 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.
+export default ['cachesController', [
+    '$scope', '$http', '$state', '$filter', '$timeout', 'IgniteLegacyUtils', 'IgniteMessages', 'IgniteConfirm', 'IgniteClone', 'IgniteLoading', 'IgniteModelNormalizer', 'IgniteUnsavedChangesGuard', 'igniteConfigurationResource', 'IgniteErrorPopover', 'IgniteFormUtils',
+    function($scope, $http, $state, $filter, $timeout, LegacyUtils, Messages, Confirm, Clone, Loading, ModelNormalizer, UnsavedChangesGuard, Resource, ErrorPopover, FormUtils) {
+        UnsavedChangesGuard.install($scope);
+
+        const emptyCache = {empty: true};
+
+        let __original_value;
+
+        const blank = {
+            evictionPolicy: {},
+            cacheStoreFactory: {
+                CacheHibernateBlobStoreFactory: {
+                    hibernateProperties: []
+                }
+            },
+            nearConfiguration: {},
+            sqlFunctionClasses: []
+        };
+
+        // We need to initialize backupItem with empty object in order to properly used from angular directives.
+        $scope.backupItem = emptyCache;
+
+        $scope.ui = FormUtils.formUI();
+        $scope.ui.activePanels = [0];
+        $scope.ui.topPanels = [0, 1, 2, 3];
+
+        $scope.saveBtnTipText = FormUtils.saveBtnTipText;
+        $scope.widthIsSufficient = FormUtils.widthIsSufficient;
+        $scope.offHeapMode = 'DISABLED';
+
+        $scope.contentVisible = function() {
+            const item = $scope.backupItem;
+
+            return !item.empty && (!item._id || _.find($scope.displayedRows, {_id: item._id}));
+        };
+
+        $scope.toggleExpanded = function() {
+            $scope.ui.expanded = !$scope.ui.expanded;
+
+            ErrorPopover.hide();
+        };
+
+        $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;
+            }, []);
+        }
+
+        const setOffHeapMode = (item) => {
+            if (_.isNil(item.offHeapMaxMemory))
+                return;
+
+            return item.offHeapMode = Math.sign(item.offHeapMaxMemory);
+        };
+
+        const setOffHeapMaxMemory = (value) => {
+            const item = $scope.backupItem;
+
+            if (_.isNil(value) || value <= 0)
+                return item.offHeapMaxMemory = value;
+
+            item.offHeapMaxMemory = item.offHeapMaxMemory > 0 ? item.offHeapMaxMemory : null;
+        };
+
+        Loading.start('loadingCachesScreen');
+
+        // When landing on the page, get caches and show them.
+        Resource.read()
+            .then(({spaces, clusters, caches, domains, igfss}) => {
+                const validFilter = $filter('domainsValidation');
+
+                $scope.spaces = spaces;
+                $scope.caches = caches;
+                $scope.igfss = _.map(igfss, (igfs) => ({
+                    label: igfs.name,
+                    value: igfs._id,
+                    igfs
+                }));
+
+                _.forEach($scope.caches, (cache) => cache.label = _cacheLbl(cache));
+
+                $scope.clusters = _.map(clusters, (cluster) => ({
+                    value: cluster._id,
+                    label: cluster.name,
+                    discovery: cluster.discovery,
+                    caches: cluster.caches
+                }));
+
+                $scope.domains = _.sortBy(_.map(validFilter(domains, true, false), (domain) => ({
+                    label: domain.valueType,
+                    value: domain._id,
+                    kind: domain.kind,
+                    meta: domain
+                })), 'label');
+
+                if ($state.params.linkId)
+                    $scope.createItem($state.params.linkId);
+                else {
+                    const lastSelectedCache = angular.fromJson(sessionStorage.lastSelectedCache);
+
+                    if (lastSelectedCache) {
+                        const 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 && ModelNormalizer.isEqual(__original_value, $scope.backupItem))
+                        $scope.ui.inputForm.$dirty = false;
+                });
+
+                $scope.$watch('backupItem', function(val) {
+                    if (!$scope.ui.inputForm)
+                        return;
+
+                    const form = $scope.ui.inputForm;
+
+                    if (form.$valid && ModelNormalizer.isEqual(__original_value, val))
+                        form.$setPristine();
+                    else
+                        form.$setDirty();
+                }, true);
+
+                $scope.$watch('backupItem.offHeapMode', setOffHeapMaxMemory);
+
+                $scope.$watch('ui.activePanels.length', () => {
+                    ErrorPopover.hide();
+                });
+            })
+            .catch(Messages.showError)
+            .then(() => {
+                $scope.ui.ready = true;
+                $scope.ui.inputForm && $scope.ui.inputForm.$setPristine();
+
+                Loading.finish('loadingCachesScreen');
+            });
+
+        $scope.selectItem = function(item, backup) {
+            function selectItem() {
+                $scope.selectedItem = item;
+
+                if (item && !_.get(item.cacheStoreFactory.CacheJdbcBlobStoreFactory, 'connectVia'))
+                    _.set(item.cacheStoreFactory, 'CacheJdbcBlobStoreFactory.connectVia', 'DataSource');
+
+                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);
+
+                if ($scope.ui.inputForm) {
+                    $scope.ui.inputForm.$error = {};
+                    $scope.ui.inputForm.$setPristine();
+                }
+
+                setOffHeapMode($scope.backupItem);
+
+                __original_value = ModelNormalizer.normalize($scope.backupItem);
+
+                if (LegacyUtils.getQueryVariable('new'))
+                    $state.go('base.configuration.caches');
+            }
+
+            FormUtils.confirmUnsavedChanges($scope.backupItem && $scope.ui.inputForm && $scope.ui.inputForm.$dirty, selectItem);
+        };
+
+        $scope.linkId = () => $scope.backupItem._id ? $scope.backupItem._id : 'create';
+
+        function prepareNewItem(linkId) {
+            return {
+                space: $scope.spaces[0]._id,
+                cacheMode: 'PARTITIONED',
+                atomicityMode: 'ATOMIC',
+                readFromBackup: true,
+                copyOnRead: true,
+                clusters: linkId && _.find($scope.clusters, {value: linkId})
+                    ? [linkId] : _.map($scope.clusters, function(cluster) { return cluster.value; }),
+                domains: linkId && _.find($scope.domains, { value: linkId }) ? [linkId] : [],
+                cacheStoreFactory: {CacheJdbcBlobStoreFactory: {connectVia: 'DataSource'}}
+            };
+        }
+
+        // Add new cache.
+        $scope.createItem = function(linkId) {
+            $timeout(() => FormUtils.ensureActivePanel($scope.ui, 'general', 'cacheNameInput'));
+
+            $scope.selectItem(null, prepareNewItem(linkId));
+        };
+
+        function cacheClusters() {
+            return _.filter($scope.clusters, (cluster) => _.includes($scope.backupItem.clusters, cluster.value));
+        }
+
+        function clusterCaches(cluster) {
+            const caches = _.filter($scope.caches,
+                (cache) => cache._id !== $scope.backupItem._id && _.includes(cluster.caches, cache._id));
+
+            caches.push($scope.backupItem);
+
+            return caches;
+        }
+
+        function checkDataSources() {
+            const clusters = cacheClusters();
+
+            let checkRes = {checked: true};
+
+            const failCluster = _.find(clusters, (cluster) => {
+                const caches = clusterCaches(cluster);
+
+                checkRes = LegacyUtils.checkDataSources(cluster, caches, $scope.backupItem);
+
+                return !checkRes.checked;
+            });
+
+            if (!checkRes.checked) {
+                if (_.get(checkRes.secondObj, 'discovery.kind') === 'Jdbc') {
+                    return ErrorPopover.show(checkRes.firstObj.cacheStoreFactory.kind === 'CacheJdbcPojoStoreFactory' ? 'pojoDialectInput' : 'blobDialectInput',
+                        'Found cluster "' + failCluster.label + '" with the same data source bean name "' +
+                        checkRes.secondObj.discovery.Jdbc.dataSourceBean + '" and different database: "' +
+                        LegacyUtils.cacheStoreJdbcDialectsLabel(checkRes.firstDB) + '" in current cache and "' +
+                        LegacyUtils.cacheStoreJdbcDialectsLabel(checkRes.secondDB) + '" in"' + checkRes.secondObj.label + '" cluster',
+                        $scope.ui, 'store', 10000);
+                }
+
+                return ErrorPopover.show(checkRes.firstObj.cacheStoreFactory.kind === 'CacheJdbcPojoStoreFactory' ? 'pojoDialectInput' : 'blobDialectInput',
+                    'Found cache "' + checkRes.secondObj.name + '" in cluster "' + failCluster.label + '" ' +
+                    'with the same data source bean name "' + checkRes.firstObj.cacheStoreFactory[checkRes.firstObj.cacheStoreFactory.kind].dataSourceBean +
+                    '" and different database: "' + LegacyUtils.cacheStoreJdbcDialectsLabel(checkRes.firstDB) + '" in current cache and "' +
+                    LegacyUtils.cacheStoreJdbcDialectsLabel(checkRes.secondDB) + '" in "' + checkRes.secondObj.name + '" cache',
+                    $scope.ui, 'store', 10000);
+            }
+
+            return true;
+        }
+
+        function checkSQLSchemas() {
+            const clusters = cacheClusters();
+
+            let checkRes = {checked: true};
+
+            const failCluster = _.find(clusters, (cluster) => {
+                const caches = clusterCaches(cluster);
+
+                checkRes = LegacyUtils.checkCacheSQLSchemas(caches, $scope.backupItem);
+
+                return !checkRes.checked;
+            });
+
+            if (!checkRes.checked) {
+                return ErrorPopover.show('sqlSchemaInput',
+                    'Found cache "' + checkRes.secondCache.name + '" in cluster "' + failCluster.label + '" ' +
+                    'with the same SQL schema name "' + checkRes.firstCache.sqlSchema + '"',
+                    $scope.ui, 'query', 10000);
+            }
+
+            return true;
+        }
+
+        function checkStoreFactoryBean(storeFactory, beanFieldId) {
+            if (!LegacyUtils.isValidJavaIdentifier('Data source bean', storeFactory.dataSourceBean, beanFieldId, $scope.ui, 'store'))
+                return false;
+
+            return checkDataSources();
+        }
+
+        function checkStoreFactory(item) {
+            const cacheStoreFactorySelected = item.cacheStoreFactory && item.cacheStoreFactory.kind;
+
+            if (cacheStoreFactorySelected) {
+                const storeFactory = item.cacheStoreFactory[item.cacheStoreFactory.kind];
+
+                if (item.cacheStoreFactory.kind === 'CacheJdbcPojoStoreFactory' && !checkStoreFactoryBean(storeFactory, 'pojoDataSourceBean'))
+                    return false;
+
+                if (item.cacheStoreFactory.kind === 'CacheJdbcBlobStoreFactory' && storeFactory.connectVia !== 'URL'
+                    && !checkStoreFactoryBean(storeFactory, 'blobDataSourceBean'))
+                    return false;
+            }
+
+            if ((item.readThrough || item.writeThrough) && !cacheStoreFactorySelected)
+                return ErrorPopover.show('cacheStoreFactoryInput', (item.readThrough ? 'Read' : 'Write') + ' through are enabled but store is not configured!', $scope.ui, 'store');
+
+            if (item.writeBehindEnabled && !cacheStoreFactorySelected)
+                return ErrorPopover.show('cacheStoreFactoryInput', 'Write behind enabled but store is not configured!', $scope.ui, 'store');
+
+            if (cacheStoreFactorySelected && !item.readThrough && !item.writeThrough)
+                return ErrorPopover.show('readThroughLabel', 'Store is configured but read/write through are not enabled!', $scope.ui, 'store');
+
+            return true;
+        }
+
+        // Check cache logical consistency.
+        function validate(item) {
+            ErrorPopover.hide();
+
+            if (LegacyUtils.isEmptyString(item.name))
+                return ErrorPopover.show('cacheNameInput', 'Cache name should not be empty!', $scope.ui, 'general');
+
+            if (item.memoryMode === 'ONHEAP_TIERED' && item.offHeapMaxMemory > 0 && !LegacyUtils.isDefined(item.evictionPolicy.kind))
+                return ErrorPopover.show('evictionPolicyKindInput', 'Eviction policy should be configured!', $scope.ui, 'memory');
+
+            if (!LegacyUtils.checkFieldValidators($scope.ui))
+                return false;
+
+            if (item.memoryMode === 'OFFHEAP_VALUES' && !_.isEmpty(item.domains))
+                return ErrorPopover.show('memoryModeInput', 'Query indexing could not be enabled while values are stored off-heap!', $scope.ui, 'memory');
+
+            if (item.memoryMode === 'OFFHEAP_TIERED' && item.offHeapMaxMemory === -1)
+                return ErrorPopover.show('offHeapModeInput', 'Invalid value!', $scope.ui, 'memory');
+
+            if (!checkSQLSchemas())
+                return false;
+
+            if (!checkStoreFactory(item))
+                return false;
+
+            if (item.writeBehindFlushSize === 0 && item.writeBehindFlushFrequency === 0)
+                return ErrorPopover.show('writeBehindFlushSizeInput', 'Both "Flush frequency" and "Flush size" are not allowed as 0!', $scope.ui, 'store');
+
+            if (item.nodeFilter && item.nodeFilter.kind === 'OnNodes' && _.isEmpty(item.nodeFilter.OnNodes.nodeIds))
+                return ErrorPopover.show('nodeFilter-title', 'At least one node ID should be specified!', $scope.ui, 'nodeFilter');
+
+            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();
+
+                    const 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);
+                    }
+
+                    _.forEach($scope.clusters, (cluster) => {
+                        if (_.includes(item.clusters, cluster.value))
+                            cluster.caches = _.union(cluster.caches, [_id]);
+                        else
+                            _.remove(cluster.caches, (id) => id === _id);
+                    });
+
+                    _.forEach($scope.domains, (domain) => {
+                        if (_.includes(item.domains, domain.value))
+                            domain.meta.caches = _.union(domain.meta.caches, [_id]);
+                        else
+                            _.remove(domain.meta.caches, (id) => id === _id);
+                    });
+
+                    $scope.selectItem(item);
+
+                    Messages.showInfo('Cache "' + item.name + '" saved.');
+                })
+                .error(Messages.showError);
+        }
+
+        // Save cache.
+        $scope.saveItem = function() {
+            const item = $scope.backupItem;
+
+            angular.extend(item, LegacyUtils.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) {
+                    const item = angular.copy($scope.backupItem);
+
+                    delete item._id;
+
+                    item.name = newName;
+
+                    delete item.sqlSchema;
+
+                    save(item);
+                });
+            }
+        };
+
+        // Remove cache from db.
+        $scope.removeItem = function() {
+            const selectedItem = $scope.selectedItem;
+
+            Confirm.confirm('Are you sure you want to remove cache: "' + selectedItem.name + '"?')
+                .then(function() {
+                    const _id = selectedItem._id;
+
+                    $http.post('/api/v1/configuration/caches/remove', {_id})
+                        .success(function() {
+                            Messages.showInfo('Cache has been removed: ' + selectedItem.name);
+
+                            const caches = $scope.caches;
+
+                            const idx = _.findIndex(caches, function(cache) {
+                                return cache._id === _id;
+                            });
+
+                            if (idx >= 0) {
+                                caches.splice(idx, 1);
+
+                                $scope.ui.inputForm.$setPristine();
+
+                                if (caches.length > 0)
+                                    $scope.selectItem(caches[0]);
+                                else
+                                    $scope.backupItem = emptyCache;
+
+                                _.forEach($scope.clusters, (cluster) => _.remove(cluster.caches, (id) => id === _id));
+                                _.forEach($scope.domains, (domain) => _.remove(domain.meta.caches, (id) => id === _id));
+                            }
+                        })
+                        .error(Messages.showError);
+                });
+        };
+
+        // 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() {
+                            Messages.showInfo('All caches have been removed');
+
+                            $scope.caches = [];
+
+                            _.forEach($scope.clusters, (cluster) => cluster.caches = []);
+                            _.forEach($scope.domains, (domain) => domain.meta.caches = []);
+
+                            $scope.backupItem = emptyCache;
+                            $scope.ui.inputForm.$error = {};
+                            $scope.ui.inputForm.$setPristine();
+                        })
+                        .error(Messages.showError);
+                });
+        };
+
+        $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.$error = {};
+                    $scope.ui.inputForm.$setPristine();
+                });
+        };
+    }
+]];