Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id 2DD96200B81 for ; Tue, 13 Sep 2016 11:53:02 +0200 (CEST) Received: by cust-asf.ponee.io (Postfix) id 2CACD160AD9; Tue, 13 Sep 2016 09:53:02 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id 97952160AED for ; Tue, 13 Sep 2016 11:52:57 +0200 (CEST) Received: (qmail 97316 invoked by uid 500); 13 Sep 2016 09:52:56 -0000 Mailing-List: contact commits-help@ignite.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@ignite.apache.org Delivered-To: mailing list commits@ignite.apache.org Received: (qmail 96059 invoked by uid 99); 13 Sep 2016 09:52:54 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 13 Sep 2016 09:52:54 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id CFA5FE3607; Tue, 13 Sep 2016 09:52:54 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: vozerov@apache.org To: commits@ignite.apache.org Date: Tue, 13 Sep 2016 09:53:23 -0000 Message-Id: In-Reply-To: References: X-Mailer: ASF-Git Admin Mailer Subject: [31/69] [abbrv] ignite git commit: Web Console beta-3. archived-at: Tue, 13 Sep 2016 09:53:02 -0000 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(); + }); + }; + } +]];