AMBARI-11958. Hive View Enahancements (Erik Bergenholtz via rlevas)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/2580916a Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/2580916a Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/2580916a Branch: refs/heads/branch-2.1 Commit: 2580916a6f3707d0f41c03bcd10015bbe3d21da6 Parents: c33e7d7 Author: Erik Bergenholtz <[email protected]> Authored: Wed Jun 17 14:38:07 2015 -0400 Committer: Robert Levas <[email protected]> Committed: Wed Jun 17 14:38:13 2015 -0400 ---------------------------------------------------------------------- .../ui/hive-web/app/components/udf-tr-view.js | 81 +++++ .../ui/hive-web/app/controllers/index.js | 113 +++--- .../ui/hive-web/app/controllers/job-progress.js | 129 ------- .../ui/hive-web/app/controllers/open-queries.js | 21 +- .../ui/hive-web/app/controllers/query-tabs.js | 12 +- .../ui/hive-web/app/controllers/settings.js | 360 ++----------------- .../ui/hive-web/app/controllers/udf.js | 132 ------- .../ui/hive-web/app/controllers/udfs.js | 76 +++- .../hive-web/app/controllers/visual-explain.js | 14 +- .../ui/hive-web/app/initializers/i18n.js | 3 +- .../resources/ui/hive-web/app/models/job.js | 1 + .../ui/hive-web/app/services/job-progress.js | 102 ++++++ .../ui/hive-web/app/services/session.js | 48 +++ .../ui/hive-web/app/services/settings.js | 175 +++++++++ .../resources/ui/hive-web/app/styles/app.scss | 27 +- .../app/templates/components/udf-tr-view.hbs | 77 ++++ .../ui/hive-web/app/templates/index.hbs | 8 +- .../hive-web/app/templates/settings-global.hbs | 57 --- .../hive-web/app/templates/settings-query.hbs | 72 ---- .../ui/hive-web/app/templates/settings.hbs | 49 ++- .../ui/hive-web/app/templates/udfs.hbs | 89 +---- .../ui/hive-web/app/utils/constants.js | 14 +- .../ui/hive-web/app/views/visual-explain.js | 2 + .../tests/unit/controllers/index-test.js | 13 +- .../tests/unit/controllers/insert-udfs-test.js | 2 +- .../tests/unit/controllers/settings-test.js | 156 ++++---- .../tests/unit/controllers/tez-ui-test.js | 6 +- .../hive-web/tests/unit/controllers/udf-test.js | 92 ----- .../tests/unit/services/settings-test.js | 155 ++++++++ .../ambari/view/utils/ambari/AmbariApi.java | 15 + .../ambari/view/utils/ambari/Services.java | 45 +++ 31 files changed, 1045 insertions(+), 1101 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/components/udf-tr-view.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/components/udf-tr-view.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/components/udf-tr-view.js new file mode 100644 index 0000000..f019578 --- /dev/null +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/components/udf-tr-view.js @@ -0,0 +1,81 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Ember from 'ember'; +import constants from 'hive/utils/constants'; + +export default Ember.Component.extend({ + tagName: 'tr', + + didInsertElement: function () { + this._super(); + + if (this.get('udf.isNew')) { + this.set('udf.isEditing', true); + } + }, + + setfileBackup: function () { + if (!this.get('udf.isDirty')) { + this.set('fileBackup', this.get('udf.fileResource')); + } + }.observes('udf.isDirty').on('didInsertElement'), + + actions: { + editUdf: function () { + this.set('udf.isEditing', true); + }, + + deleteUdf: function () { + this.sendAction('onDeleteUdf', this.get('udf')); + }, + + addFileResource: function () { + this.sendAction('onAddFileResource', this.get('udf')); + }, + + editFileResource: function (file) { + this.set('udf.fileResource', file); + this.set('udf.isEditingResource', true); + }, + + deleteFileResource: function (file) { + this.sendAction('onDeleteFileResource', file); + }, + + save: function () { + this.sendAction('onSaveUdf', this.get('udf')); + }, + + cancel: function () { + var self = this; + + this.set('udf.isEditing', false); + this.set('udf.isEditingResource', false); + + this.udf.get('fileResource').then(function (file) { + if (file) { + file.rollback(); + } + + self.udf.rollback(); + self.udf.set('fileResource', self.get('fileBackup')); + }); + } + } +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/index.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/index.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/index.js index ea2ed5d..b3cd127 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/index.js +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/index.js @@ -21,33 +21,47 @@ import utils from 'hive/utils/functions'; export default Ember.Controller.extend({ jobService: Ember.inject.service(constants.namingConventions.job), + jobProgressService: Ember.inject.service(constants.namingConventions.jobProgress), databaseService: Ember.inject.service(constants.namingConventions.database), notifyService: Ember.inject.service(constants.namingConventions.notify), - - needs: [ constants.namingConventions.openQueries, - constants.namingConventions.udfs, - constants.namingConventions.jobLogs, - constants.namingConventions.jobResults, - constants.namingConventions.jobExplain, - constants.namingConventions.settings, - constants.namingConventions.visualExplain, - constants.namingConventions.tezUI, - constants.namingConventions.jobProgress, - ], - - openQueries: Ember.computed.alias('controllers.' + constants.namingConventions.openQueries), - udfs: Ember.computed.alias('controllers.' + constants.namingConventions.udfs + '.udfs'), - logs: Ember.computed.alias('controllers.' + constants.namingConventions.jobLogs), - results: Ember.computed.alias('controllers.' + constants.namingConventions.jobResults), - explain: Ember.computed.alias('controllers.' + constants.namingConventions.jobExplain), - settings: Ember.computed.alias('controllers.' + constants.namingConventions.settings), - visualExplain: Ember.computed.alias('controllers.' + constants.namingConventions.visualExplain), - tezUI: Ember.computed.alias('controllers.' + constants.namingConventions.tezUI), - jobProgress: Ember.computed.alias('controllers.' + constants.namingConventions.jobProgress), + session: Ember.inject.service(constants.namingConventions.session), + settingsService: Ember.inject.service(constants.namingConventions.settings), + + openQueries : Ember.inject.controller(constants.namingConventions.openQueries), + udfs : Ember.inject.controller(constants.namingConventions.udfs), + logs : Ember.inject.controller(constants.namingConventions.jobLogs), + results : Ember.inject.controller(constants.namingConventions.jobResults), + explain : Ember.inject.controller(constants.namingConventions.jobExplain), + settings : Ember.inject.controller(constants.namingConventions.settings), + visualExplain : Ember.inject.controller(constants.namingConventions.visualExplain), + tezUI : Ember.inject.controller(constants.namingConventions.tezUI), selectedDatabase: Ember.computed.alias('databaseService.selectedDatabase'), - isDatabaseExplorerVisible: true, + canKillSession: Ember.computed.and('model.sessionTag', 'model.sessionActive'), + + queryProcessTabs: [ + Ember.Object.create({ + name: Ember.I18n.t('menus.logs'), + path: constants.namingConventions.subroutes.jobLogs + }), + Ember.Object.create({ + name: Ember.I18n.t('menus.results'), + path: constants.namingConventions.subroutes.jobResults + }), + Ember.Object.create({ + name: Ember.I18n.t('menus.explain'), + path: constants.namingConventions.subroutes.jobExplain + }) + ], + + queryPanelActions: [ + Ember.Object.create({ + icon: 'fa-expand', + action: 'toggleDatabaseExplorerVisibility', + tooltip: Ember.I18n.t('tooltips.expand') + }) + ], init: function () { this._super(); @@ -163,7 +177,7 @@ export default Ember.Controller.extend({ finalQuery = query; finalQuery = this.bindQueryParams(finalQuery); - finalQuery = this.prependQuerySettings(finalQuery); + finalQuery = this.prependGlobalSettings(finalQuery, job); job.set('forcedContent', finalQuery); @@ -171,7 +185,7 @@ export default Ember.Controller.extend({ return this.getVisualExplainJson(job, originalModel); } - return this.saveQuery(job, originalModel); + return this.createJob(job, originalModel); }, getVisualExplainJson: function (job, originalModel) { @@ -191,7 +205,7 @@ export default Ember.Controller.extend({ return defer.promise; }, - saveQuery: function (job, originalModel) { + createJob: function (job, originalModel) { var defer = Ember.RSVP.defer(), self = this, openQueries = this.get('openQueries'); @@ -205,6 +219,7 @@ export default Ember.Controller.extend({ job.save().then(function () { //convert tab for current model since the execution will create a new job, and navigate to the new job route. openQueries.convertTabToJob(originalModel, job).then(function () { + self.get('jobProgressService').setupProgress(job); self.set('jobSaveSucceeded', true); //reset flag on the original model @@ -214,8 +229,6 @@ export default Ember.Controller.extend({ }, function (err) { handleError(err); }); - - self.get('settings').updateSettingsId(originalModel.get('id'), job.get('id')); }, function (err) { handleError(err); }); @@ -223,28 +236,17 @@ export default Ember.Controller.extend({ return defer.promise; }, - prependQuerySettings: function (query) { - var validSettings = this.get('settings').getCurrentValidSettings(); - var regex = new RegExp(utils.regexes.setSetting); - var existingSettings = query.match(regex); + prependGlobalSettings: function (query, job) { + var jobGlobalSettings = job.get('globalSettings'); + var currentGlobalSettings = this.get('settingsService').getSettings(); - //clear previously added settings - if (existingSettings) { - existingSettings.forEach(function (setting) { - query = query.replace(setting, ''); - }); + // remove old globals + if (jobGlobalSettings) { + query.replace(jobGlobalSettings, ''); } - query = query.trim(); - - //update with the current settings - if (validSettings.get('length')) { - query = '\n' + query; - - validSettings.forEach(function (setting) { - query = setting + '\n' + query; - }); - } + job.set('globalSettings', currentGlobalSettings); + query = currentGlobalSettings + query; return query; }, @@ -457,6 +459,10 @@ export default Ember.Controller.extend({ return Ember.I18n.t('titles.query.process') + ' (' + Ember.I18n.t('titles.query.status') + this.get('content.status') + ')'; }.property('content.status'), + updateSessionStatus: function() { + this.get('session').updateSessionStatus(this.get('model')); + }.observes('model', 'model.status'), + actions: { stopCurrentJob: function () { this.get('jobService').stopJob(this.get('model')); @@ -628,6 +634,21 @@ export default Ember.Controller.extend({ toggleDatabaseExplorerVisibility: function () { this.toggleProperty('isDatabaseExplorerVisible'); + }, + + killSession: function() { + var self = this; + var model = this.get('model'); + + this.get('session').killSession(model) + .catch(function (response) { + if ([200, 404].contains(response.status)) { + model.set('sessionActive', false); + self.notify.success(Ember.I18n.t('alerts.success.sessions.deleted')); + } else { + self.notify.error(response); + } + }); } } }); http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/job-progress.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/job-progress.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/job-progress.js deleted file mode 100644 index 3181f90..0000000 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/job-progress.js +++ /dev/null @@ -1,129 +0,0 @@ -/** - * 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 Ember from 'ember'; -import constants from 'hive/utils/constants'; - -export default Ember.Controller.extend({ - needs: [ constants.namingConventions.index ], - - jobs: [], - - index: Ember.computed.alias('controllers.' + constants.namingConventions.index), - - modelChanged: function () { - var model = this.get('index.model'); - var job; - - if (!this.isJob(model)) { - return; - } - - job = this.jobs.findBy('model', model); - - if (!job) { - job = this.jobs.pushObject(Ember.Object.create({ - model: model, - stages: [], - totalProgress: 0, - retrievingProgress: false, - })); - } - - this.set('currentJob', job); - }.observes('index.model'), - - updateProgress: function () { - var job = this.get('currentJob'); - - if (!job.get('model.dagId')) { - return; - } - - if (this.get('totalProgress') < 100 && !job.get('retrievingProgress')) { - this.reloadProgress(job); - } - }.observes('currentJob.model.dagId'), - - totalProgress: function () { - if (!this.isJob(this.get('index.model'))) { - return; - } - - return this.get('currentJob.totalProgress'); - }.property('index.model', 'currentJob.totalProgress'), - - stages: function () { - if (!this.isJob(this.get('index.model'))) { - return; - } - - return this.get('currentJob.stages'); - }.property('index.model', '[email protected]'), - - reloadProgress: function (job) { - var self = this; - var url = '%@/%@/%@/progress'.fmt(this.container.lookup('adapter:application').buildURL(), - constants.namingConventions.jobs, - job.get('model.id')); - - job.set('retrievingProgress', true); - - Ember.$.getJSON(url).then(function (data) { - var total = 0; - var length = Object.keys(data.vertexProgresses).length; - - if (!job.get('stages.length')) { - data.vertexProgresses.forEach(function (vertexProgress) { - var progress = vertexProgress.progress * 100; - - job.get('stages').pushObject(Ember.Object.create({ - name: vertexProgress.name, - value: progress - })); - - total += progress; - }); - } else { - data.vertexProgresses.forEach(function (vertexProgress) { - var progress = vertexProgress.progress * 100; - - job.get('stages').findBy('name', vertexProgress.name).set('value', progress); - - total += progress; - }); - } - - total /= length; - - job.set('totalProgress', total); - - if (job.get('model.isRunning') && total < 100) { - Ember.run.later(function () { - self.reloadProgress(job); - }, 1000); - } else { - job.set('retrievingProgress'); - } - }); - }, - - isJob: function (model) { - return model.get('constructor.typeKey') === constants.namingConventions.job; - } -}); http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/open-queries.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/open-queries.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/open-queries.js index 484df16..5ab46f6 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/open-queries.js +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/open-queries.js @@ -26,14 +26,12 @@ export default Ember.ArrayController.extend({ needs: [ constants.namingConventions.jobResults, constants.namingConventions.jobExplain, - constants.namingConventions.index, - constants.namingConventions.settings + constants.namingConventions.index ], jobResults: Ember.computed.alias('controllers.' + constants.namingConventions.jobResults), jobExplain: Ember.computed.alias('controllers.' + constants.namingConventions.jobExplain), index: Ember.computed.alias('controllers.' + constants.namingConventions.index), - settings: Ember.computed.alias('controllers.' + constants.namingConventions.settings), selectedTables: Ember.computed.alias('databaseService.selectedTables'), selectedDatabase: Ember.computed.alias('databaseService.selectedDatabase'), @@ -155,8 +153,7 @@ export default Ember.ArrayController.extend({ self = this, wasNew, defer = Ember.RSVP.defer(), - jobModel = model, - originalId = model.get('id'); + jobModel = model; if (!query) { query = this.getQueryForModel(model); @@ -203,11 +200,9 @@ export default Ember.ArrayController.extend({ //update query tab path with saved model id if its a new record if (wasNew) { - self.get('settings').updateSettingsId(originalId, updatedModel.get('id')); tab.set('id', updatedModel.get('id')); self.get('fileService').loadFile(updatedModel.get('queryFile')).then(function (file) { - file.set('blockSettingsParser', true); file.set('fileContent', content); file.save().then(function (updatedFile) { self.removeObject(query); @@ -215,8 +210,6 @@ export default Ember.ArrayController.extend({ self.set('currentQuery', updatedFile); defer.resolve(updatedModel.get('id')); - self.get('settings').parseQuerySettings(false); - query.set('blockSettingsParser', false); }, function (err) { defer.reject(err); }); @@ -224,14 +217,11 @@ export default Ember.ArrayController.extend({ defer.reject(err); }); } else { - query.set('blockSettingsParser', true); query.set('fileContent', content); query.save().then(function () { self.toggleProperty('tabUpdated'); defer.resolve(updatedModel.get('id')); - self.get('settings').parseQuerySettings(false); - query.set('blockSettingsParser', false); }, function (err) { defer.reject(err); }); @@ -271,14 +261,11 @@ export default Ember.ArrayController.extend({ return defer.promise; }, - keepOriginalQuery: function (jobId) { + keepOriginalQuery: function () { var selected = this.get('highlightedText'); var hasQueryParams = this.get('index.queryParams.length'); - var hasSettings = this.get('settings').hasSettings(jobId); - return selected && selected[0] !== "" || - hasQueryParams || - hasSettings; + return selected && selected[0] !== "" || hasQueryParams; }, isDirty: function (model) { http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/query-tabs.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/query-tabs.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/query-tabs.js index 7b6c222..4f5176c 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/query-tabs.js +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/query-tabs.js @@ -24,8 +24,9 @@ export default Ember.Controller.extend({ tabs: [ Ember.Object.create({ - iconClass: 'fa-code', + iconClass: 'text-icon', id: 'query-icon', + text: 'SQL', action: 'setDefaultActive', name: constants.namingConventions.index, tooltip: Ember.I18n.t('tooltips.query') @@ -144,15 +145,6 @@ export default Ember.Controller.extend({ } }, - flashSettings: function() { - var settingsTab = this.get('tabs').findBy('id', 'settings-icon'); - settingsTab.set('flash', true); - - Ember.run.later(function() { - settingsTab.set('flash', false); - }, 1000); - }, - actions: { toggleOverlay: function (tab) { if (tab !== this.get('default') && tab.get('active')) { http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/settings.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/settings.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/settings.js index 787f42b..77250b4 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/settings.js +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/settings.js @@ -17,367 +17,53 @@ */ import Ember from 'ember'; -import constants from 'hive/utils/constants'; -import utils from 'hive/utils/functions'; -export default Ember.ArrayController.extend({ - notifyService: Ember.inject.service(constants.namingConventions.notify), +export default Ember.Controller.extend({ + openQueries: Ember.inject.controller(), + index: Ember.inject.controller(), - needs: [ - constants.namingConventions.index, - constants.namingConventions.openQueries, - constants.namingConventions.queryTabs - ], + settingsService: Ember.inject.service('settings'), - index: Ember.computed.alias('controllers.' + constants.namingConventions.index), - openQueries: Ember.computed.alias('controllers.' + constants.namingConventions.openQueries), - queryTabs: Ember.computed.alias('controllers.' + constants.namingConventions.queryTabs), + predefinedSettings: Ember.computed.alias('settingsService.predefinedSettings'), + settings: Ember.computed.alias('settingsService.settings'), - sessionTag: Ember.computed.alias('index.model.sessionTag'), - sessionActive: Ember.computed.alias('index.model.sessionActive'), + init: function() { + this._super(); - createDefaultsSettings: function(settings) { - var globalSettings = []; - var newSetting; - - for (var key in settings) { - newSetting = this.createSetting(key, settings[key]); - globalSettings.push(newSetting); - } - - this.get('globalSettings').setObjects(globalSettings); - }, - - loadDefaultSettings: function() { - var adapter = this.container.lookup('adapter:application'); - var url = adapter.buildURL() + '/savedQueries/defaultSettings'; - var self = this; - - adapter.ajax(url) - .then(function(response) { - self.createDefaultsSettings(response.settings); - }) - .catch(function(error) { - self.get('notifyService').error(error); - }); - }.on('init'), - - setSettingsTabs: function() { - this.set('settingsTabs', Ember.ArrayProxy.create({ content: [ - Ember.Object.create({ - name: 'Query Settings', - view: constants.namingConventions.settingsQuery, - visible: true - }), - Ember.Object.create({ - name: 'Global Settings', - view: constants.namingConventions.settingsGlobal, - visible: true - }) - ]})); - - this.set('selectedTab', this.get('selectTab.firstObject')); - }.on('init'), - - canInvalidateSession: Ember.computed.and('sessionTag', 'sessionActive'), - - predefinedSettings: constants.hiveParameters, - - selectedSettings: function () { - var predefined = this.get('predefinedSettings'); - var current = this.get('currentSettings.settings'); - - return predefined.filter(function (setting) { - return current.findBy('key.name', setting.name); - }); - }.property('[email protected]'), - - currentSettings: function () { - var currentId = this.get('index.model.id'); - var targetSettings = this.findBy('id', currentId); - - if (!targetSettings) { - targetSettings = this.pushObject(Ember.Object.create({ - id: currentId, - settings: [] - })); - } - - return targetSettings; - }.property('index.model.id'), - - settingsSets: [ - Ember.Object.create({ name: 'Set 1' }), - Ember.Object.create({ name: 'Set 2' }), - Ember.Object.create({ name: 'Set 3' }) - ], - - globalSettings: Ember.ArrayProxy.create({ content: []}), - - updateSettingsId: function (oldId, newId) { - this.filterBy('id', oldId).setEach('id', newId); + this.get('settingsService').loadDefaultSettings(); }, - getCurrentValidSettings: function () { - var currentSettings = this.get('currentSettings'); - var globalSettings = this.get('globalSettings'); - var validSettings = []; - var settings = Ember.copy(currentSettings.get('settings')); + excluded: function() { + var settings = this.get('settings'); - globalSettings.map(function(setting) { - if (!settings.findBy('key.name', setting.get('key.name'))) { - settings.pushObject(setting); - } + return this.get('predefinedSettings').filter(function(setting) { + return settings.findBy('key.name', setting.name); }); + }.property('[email protected]'), - if (!currentSettings && !globalSettings) { - return ''; - } - - settings.map(function (setting) { - if (setting.get('valid')) { - validSettings.pushObject('set %@ = %@;'.fmt(setting.get('key.name'), setting.get('value'))); - } - }); - - return validSettings; - }, - - hasSettings: function (id) { - var settings; - var settingId = id ? id : this.get('index.model.id'); - - settings = this.findBy('id', settingId); - - return settings && settings.get('settings.length'); - }, - - createSetting: function(name, value) { - var self = this; - var setting = Ember.Object.createWithMixins({ - valid : true, - value : Ember.computed.alias('selection.value'), - selection : Ember.Object.create(), - - isInGlobal: function() { - return self.get('globalSettings').mapProperty('key.name').contains(this.get('key.name')); - }.property('key.name') - }); - - if (name) { - setting.set('key', Ember.Object.create({ name: name })); - } - - if (value) { - setting.set('selection.value', value); - } - - return setting; - }, - - parseQuerySettings: function(notify) { - var self = this; - var query = this.get('openQueries.currentQuery'); - var content = query.get('fileContent'); - var regex = new RegExp(utils.regexes.setSetting); - var settings = content.match(regex); - var targetSettings = this.get('currentSettings'); - - if (!query || !settings) { - return; - } - - var parsedSettings = []; - settings.forEach(function (setting) { - var KeyValue = setting.split('='); - var name = KeyValue[0].replace('set', '').trim(); - var value = KeyValue[1].replace(';', '').trim(); - - if (!self.get('predefinedSettings').findBy('name', name)) { - self.get('predefinedSettings').pushObject({ - name: name - }); - } - - if (self.get('globalSettings').findBy('key.name', name)) { - return false; - } - - parsedSettings.push(self.createSetting(name, value)); - }); - - if (notify) { - this.get('notifyService').info(Ember.I18n.t('settings.parsed')); - this.get('queryTabs').flashSettings(); - } - - query.set('fileContent', content.replace(regex, '').trim()); - targetSettings.set('settings', parsedSettings); - }, - - parseQuerySettingsObserver: function () { - var query; - - Ember.run(this, function () { - query = this.get('openQueries.currentQuery'); - }); - - if (query.get('blockSettingsParser')) { - return; - } - - this.parseQuerySettings(true); - }.observes('openQueries.currentQuery', 'openQueries.currentQuery.fileContent', 'openQueries.tabUpdated'), - - validate: function () { - var settings = this.get('currentSettings.settings') || []; - var predefinedSettings = this.get('predefinedSettings'); - - settings.forEach(function (setting) { - var predefined = predefinedSettings.findBy('name', setting.get('key.name')); - - if (!predefined) { - return; - } - - if (predefined.values && predefined.values.contains(setting.get('value'))) { - setting.set('valid', true); - return; - } - - if (predefined.validate && predefined.validate.test(setting.get('value'))) { - setting.set('valid', true); - return; - } - - if (!predefined.validate) { - setting.set('valid', true); - return; - } - - setting.set('valid', false); - }); - }.observes('currentSettings.[]', 'currentSettings.settings.[]', '[email protected]', '[email protected]'), - - currentSettingsAreValid: function () { - var currentSettings = this.get('currentSettings.settings'); - var invalid = currentSettings.filterProperty('valid', false); - - return invalid.length ? false : true; - }.property('[email protected]', '[email protected]'), - - loadSessionStatus: function () { - var model = this.get('index.model'); - var sessionActive = this.get('sessionActive'); - var sessionTag = this.get('sessionTag'); - var adapter = this.container.lookup('adapter:application'); - var url = adapter.buildURL() + '/jobs/sessions/' + sessionTag; - - if (sessionTag && sessionActive === undefined) { - adapter.ajax(url, 'GET') - .then(function (response) { - model.set('sessionActive', response.session.actual); - }) - .catch(function () { - model.set('sessionActive', false); - }); - } - }.observes('index.model', 'index.model.status'), + parseGlobalSettings: function () { + this.get('settingsService').parseGlobalSettings(this.get('openQueries.currentQuery'), this.get('index.model')); + }.observes('openQueries.currentQuery', 'openQueries.currentQuery.fileContent', 'openQueries.tabUpdated').on('init'), actions: { add: function () { - this.get('currentSettings.settings').pushObject(this.createSetting()); + this.get('settingsService').add(); }, remove: function (setting) { - var currentQuery = this.get('openQueries.currentQuery'); - var currentQueryContent = currentQuery.get('fileContent'); - var keyValue = 'set %@ = %@;\n'.fmt(setting.get('key.name'), setting.get('value')); - - this.get('currentSettings.settings').removeObject(setting); - - if (currentQueryContent.indexOf(keyValue) > -1) { - currentQuery.set('fileContent', currentQueryContent.replace(keyValue, '')); - } + this.get('settingsService').remove(setting); }, - addKey: function (param) { - var newKey = this.get('predefinedSettings').pushObject({ - name: param - }); - - this.get('currentSettings.settings').findBy('key', null).set('key', newKey); + addKey: function (name) { + this.get('settingsService').createKey(name); }, removeAll: function () { - var currentQuery = this.get('openQueries.currentQuery'), - currentQueryContent = currentQuery.get('fileContent'), - regex = new RegExp(utils.regexes.setSetting), - settings = currentQueryContent.match(regex); - - currentQuery.set('fileContent', currentQueryContent.replace(settings, '')); - this.get('currentSettings').set('settings', []); - }, - - invalidateSession: function () { - var self = this; - var sessionTag = this.get('sessionTag'); - var adapter = this.container.lookup('adapter:application'); - var url = adapter.buildURL() + '/jobs/sessions/' + sessionTag; - var model = this.get('index.model'); - - // @TODO: Split this into then/catch once the BE is fixed - adapter.ajax(url, 'DELETE').catch(function (response) { - if ([200, 404].contains(response.status)) { - model.set('sessionActive', false); - self.get('notifyService').success(Ember.I18n.t('alerts.success.sessions.deleted')); - } else { - self.get('notifyService').error(response); - } - }); - }, - - makeSettingGlobal: function(setting) { - // @TODO: should remove from all query settings? - // @TODO: validate setting? maybe its not needed bc it was already validated? - this.get('globalSettings').pushObject(setting); - this.get('currentSettings.settings').removeObject(setting); - }, - - removeGlobal: function(setting) { - this.get('globalSettings').removeObject(setting); - }, - - overwriteGlobalValue: function(setting) { - var globalSetting = this.get('globalSettings').findBy('key.name', setting.get('key.name')); - - if (globalSetting) { - globalSetting.set('value', setting.get('value')); - this.get('currentSettings.settings').removeObject(setting); - } + this.get('settingsService').removeAll(); }, saveDefaultSettings: function() { - var self = this; - var data = {}; - var adapter = this.container.lookup('adapter:application'); - var url = adapter.buildURL() + '/savedQueries/defaultSettings'; - var settings = this.get('globalSettings'); - - settings.forEach(function(setting) { - data[ setting.get('key.name') ] = setting.get('value'); - }); - - adapter.ajax(url, 'POST', { - data: {settings: data } - }) - .then(function(response) { - if (response && response.settings) { - self.get('notifyService').success(Ember.I18n.t('alerts.success.settings.saved')); - } else { - self.get('notifyService').error(response); - } - }); + this.get('settingsService').saveDefaultSettings(); } } }); http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/udf.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/udf.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/udf.js deleted file mode 100644 index ced29ca..0000000 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/udf.js +++ /dev/null @@ -1,132 +0,0 @@ -/** - * 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 Ember from 'ember'; -import constants from 'hive/utils/constants'; - -export default Ember.ObjectController.extend({ - init: function () { - this._super(); - - if (this.get('model.isNew')) { - this.set('isEditing', true); - } - - // we need this because model.rollback doesnt roll back secondary relations - this.set('fileBackup', this.get('model.fileResource')); - }, - - actions: { - executeAction: function (action) { - switch (action) { - case 'buttons.edit': - this.set('isEditing', true); - break; - case 'buttons.delete': - var defer = Ember.RSVP.defer(), - self = this; - - this.send('openModal', - 'modal-delete', - { - heading: 'modals.delete.heading', - text: 'modals.delete.message', - defer: defer - }); - - defer.promise.then(function () { - self.get('model').destroyRecord(); - }); - break; - } - }, - - save: function () { - var self = this, - saveUdf = function () { - self.get('model').save().then(function (updatedModel) { - self.set('isEditing', false); - self.set('isEditingResource', false); - self.set('fileBackup', updatedModel.get('fileResource')); - }); - }; - - //replace with a validation system if needed. - if (!this.get('model.name') || !this.get('model.classname')) { - return; - } - - this.get('model.fileResource').then(function (file) { - if (file) { - if (!file.get('name') || !file.get('path')) { - return; - } - - file.save().then(function () { - saveUdf(); - }); - } else { - saveUdf(); - } - }); - }, - - cancel: function () { - var self = this; - - this.set('isEditing', false); - this.set('isEditingResource', false); - - this.model.get('fileResource').then(function (file) { - if (file) { - file.rollback(); - } - - self.model.rollback(); - self.model.set('fileResource', self.get('fileBackup')); - }); - }, - - addFileResource: function () { - var file = this.store.createRecord(constants.namingConventions.fileResource); - this.set('isEditingResource', true); - this.model.set('fileResource', file); - }, - - editFileResource: function (file) { - this.set('isEditingResource', true); - this.model.set('fileResource', file); - }, - - removeFileResource: function (file) { - var defer = Ember.RSVP.defer(); - - this.send('openModal', - 'modal-delete', - { - heading: Ember.I18n.translations.modals.delete.heading, - text: Ember.I18n.translations.modals.delete.message, - defer: defer - }); - - defer.promise.then(function () { - file.destroyRecord(); - }); - } - } -}); http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/udfs.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/udfs.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/udfs.js index 437c9d0..6804260 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/udfs.js +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/udfs.js @@ -20,19 +20,12 @@ import Ember from 'ember'; import FilterableMixin from 'hive/mixins/filterable'; import constants from 'hive/utils/constants'; -export default Ember.ArrayController.extend(FilterableMixin, { - itemController: constants.namingConventions.udf, +export default Ember.Controller.extend(FilterableMixin, { fileResources: [], sortAscending: true, sortProperties: [], - //row buttons - links: [ - 'buttons.edit', - 'buttons.delete' - ], - columns: [ Ember.Object.create({ caption: 'placeholders.udfs.name', @@ -49,6 +42,73 @@ export default Ember.ArrayController.extend(FilterableMixin, { }.property('udfs', 'filters.@each'), actions: { + handleAddFileResource: function (udf) { + var file = this.store.createRecord(constants.namingConventions.fileResource); + udf.set('fileResource', file); + udf.set('isEditingResource', true); + }, + + handleDeleteFileResource: function (file) { + var defer = Ember.RSVP.defer(); + + this.send('openModal', + 'modal-delete', + { + heading: 'modals.delete.heading', + text: 'modals.delete.message', + defer: defer + }); + + defer.promise.then(function () { + file.destroyRecord(); + }); + }, + + handleSaveUdf: function (udf) { + var self = this, + saveUdf = function () { + udf.save().then(function () { + udf.set('isEditing', false); + udf.set('isEditingResource', false); + }); + }; + + //replace with a validation system if needed. + if (!udf.get('name') || !udf.get('classname')) { + return; + } + + udf.get('fileResource').then(function (file) { + if (file) { + if (!file.get('name') || !file.get('path')) { + return; + } + + file.save().then(function () { + saveUdf(); + }); + } else { + saveUdf(); + } + }); + }, + + handleDeleteUdf: function (udf) { + var defer = Ember.RSVP.defer(); + + this.send('openModal', + 'modal-delete', + { + heading: 'modals.delete.heading', + text: 'modals.delete.message', + defer: defer + }); + + defer.promise.then(function () { + udf.destroyRecord(); + }); + }, + sort: function (property) { //if same column has been selected, toggle flag, else default it to true if (this.get('sortProperties').objectAt(0) === property) { http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/visual-explain.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/visual-explain.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/visual-explain.js index 8401388..d6ae8c4 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/visual-explain.js +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/visual-explain.js @@ -20,19 +20,13 @@ import Ember from 'ember'; import constants from 'hive/utils/constants'; export default Ember.Controller.extend({ + jobProgressService: Ember.inject.service(constants.namingConventions.jobProgress), notifyService: Ember.inject.service(constants.namingConventions.notify), - needs: [ constants.namingConventions.index, - constants.namingConventions.openQueries, - constants.namingConventions.jobProgress ], + index: Ember.inject.controller(), + openQueries: Ember.inject.controller(), - index: Ember.computed.alias('controllers.' + constants.namingConventions.index), - jobProgress: Ember.computed.alias('controllers.' + constants.namingConventions.jobProgress), - openQueries: Ember.computed.alias('controllers.' + constants.namingConventions.openQueries), - - updateProgress: function () { - this.set('verticesProgress', this.get('jobProgress.stages')); - }.observes('jobProgress.stages', '[email protected]'), + verticesProgress: Ember.computed.alias('jobProgressService.currentJob.stages'), observeCurrentQuery: function () { this.set('shouldChangeGraph', true); http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/initializers/i18n.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/initializers/i18n.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/initializers/i18n.js index ab73c63..af5e3a7 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/initializers/i18n.js +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/initializers/i18n.js @@ -198,7 +198,8 @@ TRANSLATIONS = { loadMore: 'Load more...', saveHdfs: 'Save to HDFS', saveCsv: 'Download as CSV', - runOnTez: 'Run on Tez' + runOnTez: 'Run on Tez', + killSession: 'Kill Session' }, labels: { http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/models/job.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/models/job.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/models/job.js index 45393db..9079b5a 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/models/job.js +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/models/job.js @@ -38,6 +38,7 @@ export default DS.Model.extend({ applicationId: DS.attr(), referrer: DS.attr('string'), confFile: DS.attr('string'), + globalSettings: DS.attr('string'), dateSubmittedTimestamp: function () { var date = this.get('dateSubmitted'); http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/services/job-progress.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/services/job-progress.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/services/job-progress.js new file mode 100644 index 0000000..1e0b96b --- /dev/null +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/services/job-progress.js @@ -0,0 +1,102 @@ +/** + * 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 Ember from 'ember'; +import constants from 'hive/utils/constants'; + +export default Ember.Service.extend({ + jobs: [], + + setupProgress: function (currentModel) { + var job = this.jobs.findBy('model', currentModel); + + if (!job) { + job = this.jobs.pushObject(Ember.Object.create({ + model: currentModel, + stages: [], + totalProgress: 0, + retrievingProgress: false, + })); + } + + this.set('currentJob', job); + }, + + updateProgress: function () { + var job = this.get('currentJob'); + + if (!job.get('model.dagId')) { + return; + } + + if (job.get('totalProgress') < 100 && !job.get('retrievingProgress')) { + this.reloadProgress(job); + } + }.observes('currentJob.model.dagId'), + + reloadProgress: function (job) { + var self = this; + var url = '%@/%@/%@/progress'.fmt(this.container.lookup('adapter:application').buildURL(), + constants.namingConventions.jobs, + job.get('model.id')); + + job.set('retrievingProgress', true); + + Ember.$.getJSON(url).then(function (data) { + var total = 0; + var length = Object.keys(data.vertexProgresses).length; + + if (!job.get('stages.length')) { + data.vertexProgresses.forEach(function (vertexProgress) { + var progress = vertexProgress.progress * 100; + + job.get('stages').pushObject(Ember.Object.create({ + name: vertexProgress.name, + value: progress + })); + + total += progress; + }); + } else { + data.vertexProgresses.forEach(function (vertexProgress) { + var progress = vertexProgress.progress * 100; + + job.get('stages').findBy('name', vertexProgress.name).set('value', progress); + + total += progress; + }); + } + + total /= length; + + job.set('totalProgress', total); + + if (job.get('model.isRunning') && total < 100) { + Ember.run.later(function () { + self.reloadProgress(job); + }, 1000); + } else { + job.set('retrievingProgress'); + } + }); + }, + + isJob: function (model) { + return model.get('constructor.typeKey') === constants.namingConventions.job; + } +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/services/session.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/services/session.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/services/session.js new file mode 100644 index 0000000..d7d448d --- /dev/null +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/services/session.js @@ -0,0 +1,48 @@ +/** + * 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 Ember from 'ember'; + +export default Ember.Service.extend({ + + updateSessionStatus: function (model) { + var sessionActive = model.get('sessionActive'); + var sessionTag = model.get('sessionTag'); + var adapter = this.container.lookup('adapter:application'); + var url = adapter.buildURL() + '/jobs/sessions/' + sessionTag; + + if (sessionTag && sessionActive === undefined) { + adapter.ajax(url, 'GET') + .then(function (response) { + model.set('sessionActive', response.session.actual); + }) + .catch(function () { + model.set('sessionActive', false); + }); + } + }, + + killSession: function (model) { + var sessionTag = model.get('sessionTag'); + var adapter = this.container.lookup('adapter:application'); + var url = adapter.buildURL() + '/jobs/sessions/' + sessionTag; + + return adapter.ajax(url, 'DELETE'); + } + +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/services/settings.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/services/settings.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/services/settings.js new file mode 100644 index 0000000..b813bbf --- /dev/null +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/services/settings.js @@ -0,0 +1,175 @@ +/** + * 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 Ember from 'ember'; +import constants from 'hive/utils/constants'; + +export default Ember.Service.extend({ + + notifyService: Ember.inject.service('notify'), + + settings: Ember.ArrayProxy.create({ content: [] }), + predefinedSettings: constants.hiveParameters, + + _createSetting: function(name, value) { + var setting = Ember.Object.createWithMixins({ + valid : true, + value : Ember.computed.alias('selection.value'), + selection : Ember.Object.create() + }); + + if (name) { + setting.set('key', Ember.Object.create({ name: name })); + } + + if (value) { + setting.set('selection.value', value); + } + + return setting; + }, + + _createDefaultSettings: function(settings) { + if (!settings) { + return; + } + + for (var key in settings) { + this.get('settings').pushObject(this._createSetting(key, settings[key])); + } + }, + + _validate: function () { + var settings = this.get('settings'); + var predefinedSettings = this.get('predefinedSettings'); + + settings.forEach(function (setting) { + var predefined = predefinedSettings.findBy('name', setting.get('key.name')); + + if (!predefined) { + return; + } + + if (predefined.values && predefined.values.contains(setting.get('value'))) { + setting.set('valid', true); + return; + } + + if (predefined.validate && predefined.validate.test(setting.get('value'))) { + setting.set('valid', true); + return; + } + + if (!predefined.validate) { + setting.set('valid', true); + return; + } + + setting.set('valid', false); + }); + }.observes('[email protected]', '[email protected]'), + + add: function() { + this.get('settings').pushObject(this._createSetting()); + }, + + createKey: function(name) { + var key = { name: name }; + this.get('predefinedSettings').pushObject(key); + + this.get('settings').findBy('key', null).set('key', key); + }, + + remove: function(setting) { + this.get('settings').removeObject(setting); + }, + + removeAll: function() { + this.get('settings').clear(); + }, + + loadDefaultSettings: function() { + var adapter = this.container.lookup('adapter:application'); + var url = adapter.buildURL() + '/savedQueries/defaultSettings'; + var self = this; + + adapter.ajax(url) + .then(function(response) { + self._createDefaultSettings(response.settings); + }) + .catch(function(error) { + self.get('notifyService').error(error); + }); + }, + + saveDefaultSettings: function() { + var self = this; + var data = {}; + var adapter = this.container.lookup('adapter:application'); + var url = adapter.buildURL() + '/savedQueries/defaultSettings'; + var settings = this.get('settings'); + + settings.forEach(function(setting) { + data[ setting.get('key.name') ] = setting.get('value'); + }); + + adapter.ajax(url, 'POST', { + data: {settings: data } + }) + .then(function(response) { + if (response && response.settings) { + self.get('notifyService').success(Ember.I18n.t('alerts.success.settings.saved')); + } else { + self.get('notifyService').error(response); + } + }); + }, + + getSettings: function() { + var settings = this.get('settings'); + var asString = ""; + + if (!settings.get('length')) { + return asString; + } + + settings.forEach(function(setting) { + asString += "set %@=%@;\n".fmt(setting.get('key.name'), setting.get('value')); + }); + + asString += constants.globalSettings.comment; + + return asString; + }, + + parseGlobalSettings: function(query, model) { + if (!query || !model || !model.get('globalSettings')) { + return; + } + + var globals = model.get('globalSettings'); + var content = query.get('fileContent'); + + if (globals !== this.getSettings()) { + return; + } + + query.set('fileContent', content.replace(globals, '')); + } + +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/styles/app.scss ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/styles/app.scss b/contrib/views/hive/src/main/resources/ui/hive-web/app/styles/app.scss index 7be5dcc..6507d6b 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/styles/app.scss +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/styles/app.scss @@ -407,9 +407,6 @@ body { cursor: pointer; } -.settings-container { -} - .settings-container .close-settings { float: right; font-size: 18px; @@ -434,40 +431,20 @@ body { } } -.setting .remove, .setting .makeGlobal { +.setting .remove { line-height: 30px; font-size: 18px; cursor: pointer; - // position: absolute; - // right: -5px; - // top: -10px; -} - -.setting .makeGlobal.overwriteGlobal { - color: #ff3322; } .setting .setting-input-value { - width: calc(100% - 50px); + width: calc(100% - 30px); display: inline-block; } .setting .global-setting-value { width: calc(100% - 25px); } -.setting .makeGlobal { - top: 30px; -} - -.settings-set { - margin: 10px 0; -} -.settings-set h3 { - display: inline-block; - margin: 0 10px 0 0; - vertical-align: top; - line-height: 35px; -} .settings-set .settings-set-selector { display: inline-block; width: 300px; http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/udf-tr-view.hbs ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/udf-tr-view.hbs b/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/udf-tr-view.hbs new file mode 100644 index 0000000..4b5cd61 --- /dev/null +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/udf-tr-view.hbs @@ -0,0 +1,77 @@ +{{! +* 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. +}} + +<td> + {{#if udf.isEditing}} + {{#if udf.isEditingResource}} + {{extended-input type="text" + class="pull-left form-control halfed input-sm" + placeholderTranslation="placeholders.fileResource.name" + value=udf.fileResource.name}} + {{extended-input type="text" + class="pull-left form-control halfed input-sm" + placeholderTranslation="placeholders.fileResource.path" + value=udf.fileResource.path}} + {{else}} + {{select-widget items=fileResources + selectedValue=udf.fileResource + labelPath="name" + defaultLabelTranslation="placeholders.select.file" + itemAdded="addFileResource" + itemEdited="editFileResource" + itemRemoved="deleteFileResource" + canAdd=true + canEdit=true}} + {{/if}} + {{else}} + {{#if udf.fileResource}} + {{udf.fileResource.name}} ({{udf.fileResource.path}}) + {{/if}} + {{/if}} +</td> +{{#each column in columns}} + <td> + {{#if udf.isEditing}} + {{extended-input type="text" + class="pull-left form-control input-sm" + placeholderTranslation=column.caption + dynamicContextBinding="udf" + dynamicValueBinding="column.property"}} + {{else}} + {{path-binding udf column.property}} + {{/if}} + </td> +{{/each}} +<td> + {{#if udf.isEditing}} + <div class="pull-right"> + <button type="button" class="btn btn-sm btn-warning" {{action "cancel"}}>{{t "buttons.cancel"}}</button> + <button type="button" class="btn btn-sm btn-success" {{action "save"}}>{{t "buttons.save"}}</button> + </div> + {{else}} + <div class="btn-group pull-right"> + <span data-toggle="dropdown"> + <a class="fa fa-gear"></a> + </span> + <ul class="dropdown-menu" role="menu"> + <li {{action 'editUdf'}}><a>{{t 'buttons.edit'}}</a></li> + <li {{action 'deleteUdf'}}><a>{{t 'buttons.delete'}}</a></li> + </ul> + </div> + {{/if}} +</td> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/index.hbs ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/index.hbs b/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/index.hbs index 2fdf0ce..81d6ecf 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/index.hbs +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/index.hbs @@ -50,13 +50,17 @@ {{render 'insert-udfs'}} + {{#if canKillSession}} + <button type="button" class="btn btn-sm btn-danger kill-session" {{action "killSession"}}>{{t "buttons.killSession"}}</button> + {{/if}} + <button type="button" class="btn btn-sm btn-primary pull-right" {{action "addQuery"}}>{{t "buttons.newQuery"}}</button> </div> {{/panel-widget}} {{#if displayJobTabs}} - {{#if jobProgress.stages.length}} - {{#progress-widget value=jobProgress.totalProgress}} + {{#if jobProgressService.currentJob.stages.length}} + {{#progress-widget value=jobProgressService.currentJob.totalProgress}} {{/progress-widget}} {{/if}} {{/if}} http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings-global.hbs ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings-global.hbs b/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings-global.hbs deleted file mode 100644 index 0712ec2..0000000 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings-global.hbs +++ /dev/null @@ -1,57 +0,0 @@ -{{! -* 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. -}} - -<div class='settings-controls'> - <button class="btn btn-success btn-xs" {{action 'saveDefaultSettings'}}><i class="fa fa-plus"></i> Save Default Settings</button> -</div> - -{{#each setting in globalSettings}} - <div class="setting col-md-12 col-sm-12"> - <form> - <div class="form-group"> - <div class="input-group"> - <div class="input-group-addon"> - {{typeahead-widget - options=predefinedSettings - excluded=selectedSettings - optionLabelPath="name" - optionValuePath="name" - selection=setting.key - }} - </div> - <div {{bind-attr class=":input-group-addon setting.valid::has-error"}}> - - <div class="setting-input-value global-setting-value"> - {{#if setting.key.values}} - {{select-widget items=setting.key.values - labelPath="value" - selectedValue=setting.selection - defaultLabelTranslation="placeholders.select.value" - }} - {{else}} - {{input class="input-sm form-control" placeholderTranslation="placeholders.select.value" value=setting.selection.value}} - {{/if}} - </div> - - <span class="fa fa-times-circle remove pull-right" {{action 'removeGlobal' setting}}></span> - </div> - </div> - </div> - </form> - </div> -{{/each}} http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings-query.hbs ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings-query.hbs b/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings-query.hbs deleted file mode 100644 index 3d28d9b..0000000 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings-query.hbs +++ /dev/null @@ -1,72 +0,0 @@ -{{! -* 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. -}} - -<div class='settings-controls'> - <button class="btn btn-success btn-xs" {{action 'add'}}><i class="fa fa-plus"></i> Add</button> - {{#if currentSettings.settings}} - <button class="btn btn-danger btn-xs" {{action 'removeAll'}}><i class="fa fa-minus"></i> Remove All</button> - {{/if}} - - - {{#if canInvalidateSession}} - <button class="btn btn-danger btn-xs pull-right" {{action 'invalidateSession'}}><i class="fa fa-times"></i> Invalidate Session</button> - {{/if}} -</div> - -{{#each setting in currentSettings.settings}} - <div class="setting col-md-12 col-sm-12"> - <form> - <div class="form-group"> - <div class="input-group"> - <div class="input-group-addon"> - {{typeahead-widget - options=predefinedSettings - excluded=selectedSettings - optionLabelPath="name" - optionValuePath="name" - selection=setting.key - create="addKey" - }} - </div> - <div {{bind-attr class=":input-group-addon setting.valid::has-error"}}> - - <div class="setting-input-value"> - {{#if setting.key.values}} - {{select-widget items=setting.key.values - labelPath="value" - selectedValue=setting.selection - defaultLabelTranslation="placeholders.select.value" - }} - {{else}} - {{input class="input-sm form-control" placeholderTranslation="placeholders.select.value" value=setting.selection.value}} - {{/if}} - </div> - - <span class="fa fa-times-circle remove pull-right" {{action 'remove' setting}}></span> - - {{#if setting.isInGlobal}} - <span class="fa fa-globe makeGlobal overwriteGlobal pull-right" title="Overwrite Global Setting" {{action 'overwriteGlobalValue' setting}}></span> - {{else}} - <span class="fa fa-globe makeGlobal pull-right" title="Make Global" {{action 'makeSettingGlobal' setting}}></span> - {{/if}} - </div> - </div> - </div> - </form> - </div> -{{/each}} http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings.hbs ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings.hbs b/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings.hbs index 31b9cff..55b659a 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings.hbs +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings.hbs @@ -17,7 +17,50 @@ }} <div class="editor-overlay settings-container fadeIn"> - {{#tabs-widget tabs=settingsTabs selectedTab=selectedTab}} - {{partial selectedTab.view}} - {{/tabs-widget}} + <div class='settings-controls'> + <button class="btn btn-success btn-xs" {{action 'add'}}><i class="fa fa-plus"></i> Add</button> + + {{#if settings.length}} + <button class="btn btn-danger btn-xs" {{action 'removeAll'}}><i class="fa fa-minus"></i> Remove All</button> + {{/if}} + + <button class="btn btn-success btn-xs pull-right" {{action 'saveDefaultSettings'}}><i class="fa fa-plus"></i> Save Default Settings</button> + </div> + + {{#each setting in settings}} + <div class="setting col-md-12 col-sm-12"> + <form> + <div class="form-group"> + <div class="input-group"> + <div class="input-group-addon"> + {{typeahead-widget + options=predefinedSettings + excluded=excluded + optionLabelPath="name" + optionValuePath="name" + selection=setting.key + create="addKey" + }} + </div> + <div {{bind-attr class=":input-group-addon setting.valid::has-error"}}> + + <div class="setting-input-value"> + {{#if setting.key.values}} + {{select-widget items=setting.key.values + labelPath="value" + selectedValue=setting.selection + defaultLabelTranslation="placeholders.select.value" + }} + {{else}} + {{input class="input-sm form-control" placeholderTranslation="placeholders.select.value" value=setting.selection.value}} + {{/if}} + </div> + + <span class="fa fa-times-circle remove pull-right" {{action 'remove' setting}}></span> + </div> + </div> + </div> + </form> + </div> + {{/each}} </div> http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/udfs.hbs ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/udfs.hbs b/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/udfs.hbs index 9d057db..4cd1a97 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/udfs.hbs +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/templates/udfs.hbs @@ -22,17 +22,13 @@ <th>{{t "columns.fileResource"}}</th> {{#each column in columns}} <th> - {{#if column.caption}} - {{column-filter-widget class="pull-left" - column=column - filterValue=column.filterValue - sortAscending=controller.sortAscending - sortProperties=controller.sortProperties - columnSorted="sort" - columnFiltered="filter"}} - {{else}} - {{column.caption}} - {{/if}} + {{column-filter-widget class="pull-left" + column=column + filterValue=column.filterValue + sortAscending=controller.sortAscending + sortProperties=controller.sortProperties + columnSorted="sort" + columnFiltered="filter"}} </th> {{/each}} <th> @@ -44,69 +40,14 @@ </tr> </thead> <tbody> - {{#each udf in this}} - <tr> - <td> - {{#if udf.isEditing}} - {{#if udf.isEditingResource}} - {{extended-input type="text" - class="pull-left form-control halfed input-sm" - placeholderTranslation="placeholders.fileResource.name" - value=udf.fileResource.name}} - {{extended-input type="text" - class="pull-left form-control halfed input-sm" - placeholderTranslation="placeholders.fileResource.path" - value=udf.fileResource.path}} - {{else}} - {{select-widget items=fileResources - selectedValue=udf.fileResource - labelPath="name" - defaultLabelTranslation="placeholders.select.file" - itemAdded="addFileResource" - itemEdited="editFileResource" - itemRemoved="removeFileResource" - canAdd=true - canEdit=true}} - {{/if}} - {{else}} - {{#if udf.fileResource}} - {{udf.fileResource.name}} ({{udf.fileResource.path}}) - {{/if}} - {{/if}} - </td> - {{#each column in columns}} - <td> - {{#if udf.isEditing}} - {{extended-input type="text" - class="pull-left form-control input-sm" - placeholderTranslation=column.caption - dynamicContextBinding="udf" - dynamicValueBinding="column.property"}} - {{else}} - {{path-binding udf column.property}} - {{/if}} - </td> - {{/each}} - <td> - {{#if udf.isEditing}} - <div class="pull-right"> - <button type="button" class="btn btn-sm btn-warning" {{action "cancel"}}>{{t "buttons.cancel"}}</button> - <button type="button" class="btn btn-sm btn-success" {{action "save"}}>{{t "buttons.save"}}</button> - </div> - {{else}} - <div class="btn-group pull-right"> - <span data-toggle="dropdown"> - <a class="fa fa-gear"></a> - </span> - <ul class="dropdown-menu" role="menu"> - {{#each link in links}} - <li {{action 'executeAction' link}}><a>{{tb-helper link}}</a></li> - {{/each}} - </ul> - </div> - {{/if}} - </td> - </tr> + {{#each udf in model}} + {{udf-tr-view udf=udf + fileResources=fileResources + columns=columns + onAddFileResource="handleAddFileResource" + onDeleteFileResource="handleDeleteFileResource" + onSaveUdf="handleSaveUdf" + onDeleteUdf='handleDeleteUdf'}} {{/each}} </tbody> </table> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/utils/constants.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/utils/constants.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/utils/constants.js index 0539463..e4e445a 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/utils/constants.js +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/utils/constants.js @@ -78,10 +78,9 @@ export default Ember.Object.create({ databaseTree: 'databases-tree', databaseSearch: 'databases-search-results', settings: 'settings', - settingsQuery: 'settings-query', - settingsGlobal: 'settings-global', jobProgress: 'job-progress', - queryTabs: 'query-tabs' + queryTabs: 'query-tabs', + session: 'session' }, hiveParameters: [ @@ -190,16 +189,13 @@ export default Ember.Object.create({ //this can be replaced by a string.format implementation adapter: { - version: '0.4.0', + version: '1.0.0', instance: 'Hive', apiPrefix: '/api/v1/views/HIVE/versions/', instancePrefix: '/instances/', resourcePrefix: 'resources/' }, - settings: { - executionEngine: 'hive.execution.engine' - }, sampleDataQuery: 'SELECT * FROM %@ LIMIT 100;', notify: { @@ -219,5 +215,9 @@ export default Ember.Object.create({ typeClass : 'alert-info', typeIcon : 'fa-info' } + }, + + globalSettings: { + comment: "--Global Settings--\n\n" } }); http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/app/views/visual-explain.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/app/views/visual-explain.js b/contrib/views/hive/src/main/resources/ui/hive-web/app/views/visual-explain.js index d2a800c..8d85e20 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/app/views/visual-explain.js +++ b/contrib/views/hive/src/main/resources/ui/hive-web/app/views/visual-explain.js @@ -440,6 +440,8 @@ export default Ember.View.extend({ return array; }; + this.set('edges', []); + // Create a new directed graph var g = this.get('graph'); http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/tests/unit/controllers/index-test.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/tests/unit/controllers/index-test.js b/contrib/views/hive/src/main/resources/ui/hive-web/tests/unit/controllers/index-test.js index 3ac9d8a..d66c899 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/tests/unit/controllers/index-test.js +++ b/contrib/views/hive/src/main/resources/ui/hive-web/tests/unit/controllers/index-test.js @@ -23,31 +23,24 @@ moduleFor('controller:index', 'IndexController', { needs: [ 'controller:open-queries', 'controller:udfs', - // 'controller:insert-udfs', 'controller:index/history-query/logs', 'controller:index/history-query/results', 'controller:index/history-query/explain', 'controller:settings', - 'controller:job-progress', 'controller:visual-explain', 'controller:tez-ui', 'service:job', 'service:file', 'service:database', 'service:notify', + 'service:job-progress', + 'service:session', + 'service:settings', 'adapter:application', 'adapter:database' ] }); -test('when initialized, controller sets the queryProcessTabs.', function () { - expect(1); - - var controller = this.subject(); - - ok(controller.get('queryProcessTabs', 'queryProcessTabs is initialized.')); -}); - test('modelChanged calls update on the open-queries cotnroller.', function () { expect(1); http://git-wip-us.apache.org/repos/asf/ambari/blob/2580916a/contrib/views/hive/src/main/resources/ui/hive-web/tests/unit/controllers/insert-udfs-test.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive/src/main/resources/ui/hive-web/tests/unit/controllers/insert-udfs-test.js b/contrib/views/hive/src/main/resources/ui/hive-web/tests/unit/controllers/insert-udfs-test.js index 6f20024..e770bdd 100644 --- a/contrib/views/hive/src/main/resources/ui/hive-web/tests/unit/controllers/insert-udfs-test.js +++ b/contrib/views/hive/src/main/resources/ui/hive-web/tests/unit/controllers/insert-udfs-test.js @@ -20,7 +20,7 @@ import Ember from 'ember'; import { moduleFor, test } from 'ember-qunit'; moduleFor('controller:insert-udfs', 'InsertUdfsController', { - needs: ['controller:udf', 'controller:udfs' ] + needs: 'controller:udfs' }); test('controller is initialized correctly', function () {
