ambari-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From yus...@apache.org
Subject git commit: AMBARI-5980. Usability: add JDK check in Host Checks UI. (xiwang via yusaku)
Date Fri, 06 Jun 2014 04:37:46 GMT
Repository: ambari
Updated Branches:
  refs/heads/trunk 81b755c60 -> 9f9ea5458


AMBARI-5980. Usability: add JDK check in Host Checks UI. (xiwang via yusaku)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/9f9ea545
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/9f9ea545
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/9f9ea545

Branch: refs/heads/trunk
Commit: 9f9ea545841638fcbbcecfcde1db82cb60ba7bae
Parents: 81b755c
Author: Yusaku Sako <yusaku@hortonworks.com>
Authored: Thu Jun 5 21:37:06 2014 -0700
Committer: Yusaku Sako <yusaku@hortonworks.com>
Committed: Thu Jun 5 21:37:06 2014 -0700

----------------------------------------------------------------------
 .../app/controllers/wizard/step3_controller.js  | 151 +++++++++++++++++--
 ambari-web/app/messages.js                      |   8 +-
 ambari-web/app/utils/ajax/ajax.js               |  33 +++-
 .../wizard/step3/hostWarningPopupBody_view.js   |  38 ++++-
 ambari-web/app/views/wizard/step3_view.js       |   4 +-
 5 files changed, 210 insertions(+), 24 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/9f9ea545/ambari-web/app/controllers/wizard/step3_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/wizard/step3_controller.js b/ambari-web/app/controllers/wizard/step3_controller.js
index 397bc39..7d2a1ce 100644
--- a/ambari-web/app/controllers/wizard/step3_controller.js
+++ b/ambari-web/app/controllers/wizard/step3_controller.js
@@ -32,15 +32,15 @@ App.WizardStep3Controller = Em.Controller.extend({
 
   registeredHosts: [],
 
+  hostCheckWarnings: [],
   repoCategoryWarnings: [],
-
   diskCategoryWarnings: [],
+  jdkCategoryWarnings: null,
 
   registrationStartedAt: null,
 
   requestId: 0,
 
-  hostCheckWarnings: [],
   /**
    * Timeout for registration
    * Based on <code>installOptions.manualInstall</code>
@@ -119,7 +119,9 @@ App.WizardStep3Controller = Em.Controller.extend({
    * Are hosts warnings loaded
    * @type {bool}
    */
-  isWarningsLoaded: false,
+  isWarningsLoaded: function () {
+    return this.get('isJDKWarningsLoaded') && this.get('isHostsWarningsLoaded');
+  }.property('isJDKWarningsLoaded', 'isHostsWarningsLoaded'),
 
   /**
    * Check are hosts have any warnings
@@ -735,12 +737,129 @@ App.WizardStep3Controller = Em.Controller.extend({
   },
 
   /**
+   * Get JDK name from server to determine if user had setup a customized JDK path when doing
'ambari-server setup'.
+   * The Ambari properties are different from default ambari-server setup, property 'jdk.name'
will be missing if a customized jdk path is applied.
+   * @return {$.ajax}
+   * @method getJDKName
+   */
+  getJDKName: function () {
+    return App.ajax.send({
+      name: 'ambari.service.load_jdk_name',
+      sender: this,
+      success: 'getJDKNameSuccessCallback'
+    });
+  },
+
+  /**
+    * Success callback for JDK name, property 'jdk.name' will be missing if a customized
jdk path is applied
+    * @param {object} data
+    * @method getJDKNameSuccessCallback
+    */
+  getJDKNameSuccessCallback: function (data) {
+    this.set('needJDKCheckOnHosts', !data.RootServiceComponents.properties["jdk.name"]);
+    this.set('jdkLocation', Em.get(data, "RootServiceComponents.properties.jdk_location"));
+    this.set('javaHome', data.RootServiceComponents.properties["java.home"]);
+  },
+
+  doCheckJDK: function () {
+    var hostsNames = this.get('bootHosts').getEach('name').join(",");
+    var javaHome = this.get('javaHome');
+    var jdkLocation = this.get('jdkLocation');
+    App.ajax.send({
+      name: 'wizard.step3.jdk_check',
+      sender: this,
+      data: {
+        host_names: hostsNames,
+        java_home: javaHome,
+        jdk_location: jdkLocation
+      },
+      success: 'doCheckJDKsuccessCallback',
+      error: 'doCheckJDKerrorCallback'
+    });
+  },
+
+  doCheckJDKsuccessCallback: function (data) {
+    var requestIndex = data.href.split('/')[data.href.split('/').length - 1];
+    var self = this;
+    // keep getting JDK check results data until all hosts jdk check completed
+    var myVar = setInterval( function(){
+      if (self.get('jdkCategoryWarnings') == null) {
+        // get jdk check results for all hosts
+        App.ajax.send({
+          name: 'wizard.step3.jdk_check.get_results',
+          sender: self,
+          data: {
+            requestIndex: requestIndex
+          },
+          success: 'parseJDKCheckResults'
+        })
+      } else {
+        clearInterval(myVar);
+        self.set('isJDKWarningsLoaded', true);
+      }
+    },
+    1000);
+  },
+  doCheckJDKerrorCallback: function () {
+    console.log('INFO: Doing JDK check for host failed');
+    this.set('isJDKWarningsLoaded', true);
+  },
+  parseJDKCheckResults: function (data) {
+    var jdkWarnings = [], hostsJDKContext = [], hostsJDKNames = [];
+    // check if the request ended
+    if (data.Requests.end_time > 0 && data.tasks) {
+      data.tasks.forEach( function(task) {
+        // generate warning context
+        if (Em.get(task, "Tasks.structured_out.java_home_check.exit_code") == 1){
+          var jdkContext = Em.I18n.t('installer.step3.hostWarningsPopup.jdk.context').format(task.Tasks.host_name);
+          hostsJDKContext.push(jdkContext);
+          hostsJDKNames.push(task.Tasks.host_name);
+        }
+      });
+      if (hostsJDKContext.length > 0) { // java jdk warning exist
+        var invalidJavaHome = this.get('javaHome');
+        jdkWarnings.push({
+          name: Em.I18n.t('installer.step3.hostWarningsPopup.jdk.name').format(invalidJavaHome),
+          hosts: hostsJDKContext,
+          hostsNames: hostsJDKNames,
+          category: 'jdk',
+          onSingleHost: false
+        });
+      }
+      this.set('jdkCategoryWarnings', jdkWarnings);
+    } else {
+      // still doing JDK check, data not ready to be parsed
+      this.set('jdkCategoryWarnings', null);
+    }
+  },
+
+  /**
+   * Check JDK issues on registered hosts.
+   */
+  checkHostJDK: function () {
+    this.set('isJDKWarningsLoaded', false);
+    this.set('jdkCategoryWarnings', null);
+    var self = this;
+    this.getJDKName().done( function() {
+      if (self.get('needJDKCheckOnHosts')) {
+        // need to do JDK check on each host
+       self.doCheckJDK();
+      } else {
+        // no customized JDK path, so no need to check jdk
+        self.set('jdkCategoryWarnings', []);
+        self.set('isJDKWarningsLoaded', true);
+      }
+    });
+  },
+
+  /**
    * Get disk info and cpu count of booted hosts from server
    * @return {$.ajax}
    * @method getHostInfo
    */
   getHostInfo: function () {
-    this.set('isWarningsLoaded', false);
+    this.set('isHostsWarningsLoaded', false);
+    // begin JDK check for each host
     return App.ajax.send({
       name: 'wizard.step3.host_info',
       sender: this,
@@ -752,7 +871,8 @@ App.WizardStep3Controller = Em.Controller.extend({
   
   startHostcheck: function() {
     this.set('isWarningsLoaded', false);
-    this.getHostNameResolution();   
+    this.getHostNameResolution();
+    this.checkHostJDK();
   },
 
   getHostNameResolution: function () {
@@ -880,14 +1000,11 @@ App.WizardStep3Controller = Em.Controller.extend({
   getHostInfoSuccessCallback: function (jsonData) {
     var hosts = this.get('bootHosts'),
       self = this,
-      repoWarnings = [],
-      hostsContext = [],
-      diskWarnings = [],
-      hostsDiskContext = [],
-      hostsDiskNames = [],
-      hostsRepoNames = [];
-    this.parseWarnings(jsonData);
+      repoWarnings = [], hostsRepoNames = [], hostsContext = [],
+      diskWarnings = [], hostsDiskContext = [], hostsDiskNames = [];
 
+    // parse host checks warning
+    this.parseWarnings(jsonData);
     hosts.forEach(function (_host) {
       var host = (App.get('testMode')) ? jsonData.items[0] : jsonData.items.findProperty('Hosts.host_name',
_host.name);
       if (App.get('skipBootstrap')) {
@@ -896,8 +1013,8 @@ App.WizardStep3Controller = Em.Controller.extend({
       else {
         if (host) {
           self._setHostDataFromLoadedHostInfo(_host, host);
-
           var host_name = Em.get(host, 'Hosts.host_name');
+
           var context = self.checkHostOSType(host.Hosts.os_type, host_name);
           if (context) {
             hostsContext.push(context);
@@ -911,7 +1028,7 @@ App.WizardStep3Controller = Em.Controller.extend({
         }
       }
     });
-    if (hostsContext.length > 0) { // warning exist
+    if (hostsContext.length > 0) { // repository warning exist
       repoWarnings.push({
         name: Em.I18n.t('installer.step3.hostWarningsPopup.repositories.name'),
         hosts: hostsContext,
@@ -981,7 +1098,7 @@ App.WizardStep3Controller = Em.Controller.extend({
    */
   getHostInfoErrorCallback: function () {
     console.log('INFO: Getting host information(cpu_count and total_mem) from the server
failed');
-    this.set('isWarningsLoaded', true);
+    this.set('isHostsWarningsLoaded', true);
     this.registerErrPopup(Em.I18n.t('installer.step3.hostInformation.popup.header'), Em.I18n.t('installer.step3.hostInformation.popup.body'));
   },
 
@@ -1146,6 +1263,8 @@ App.WizardStep3Controller = Em.Controller.extend({
   rerunChecks: function () {
     var self = this;
     var currentProgress = 0;
+    this.getHostNameResolution();
+    this.checkHostJDK();
     var interval = setInterval(function () {
       currentProgress += 100000 / self.get('warningsTimeInterval');
       if (currentProgress < 100) {
@@ -1447,7 +1566,7 @@ App.WizardStep3Controller = Em.Controller.extend({
     });
     this.set('warnings', warnings);
     this.set('warningsByHost', hosts);
-    this.set('isWarningsLoaded', true);
+    this.set('isHostsWarningsLoaded', true);
   },
 
   /**

http://git-wip-us.apache.org/repos/asf/ambari/blob/9f9ea545/ambari-web/app/messages.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js
index 69bd3ab..e5be25b 100644
--- a/ambari-web/app/messages.js
+++ b/ambari-web/app/messages.js
@@ -467,6 +467,10 @@ Em.I18n.translations = {
     '<br><div class="code-snippet">python /usr/lib/python2.6/site-packages/ambari_agent/HostCleanup.py
--silent --skip=users</div>' +
     '<div class="alert alert-warn"><b>Note</b>: To clean up in interactive
mode, remove <b>--silent</b> option. To clean up all resources, including <i>users</i>,
remove <b>--skip=users</b> option. Use <b>--help</b> for a list of
available options.</div>',
   'installer.step3.hostWarningsPopup.summary':'{0} on {1}',
+  'installer.step3.hostWarningsPopup.jdk':'JDK Issues',
+  'installer.step3.hostWarningsPopup.jdk.name':'JDK not found at <i>{0}</i>',
+  'installer.step3.hostWarningsPopup.jdk.context':'{0}',
+  'installer.step3.hostWarningsPopup.jdk.message':'The following registered hosts have issues
related to JDK',
   'installer.step3.hostWarningsPopup.repositories':'Repository Issues',
   'installer.step3.hostWarningsPopup.repositories.name':'Repository for OS not available',
   'installer.step3.hostWarningsPopup.repositories.context':'Host ({0}) is {1} OS type, but
the repositories chosen in "Select Stack" step was {2}. Selected repositories dont support
such host OS type.',
@@ -509,12 +513,13 @@ Em.I18n.translations = {
   'installer.step3.hostWarningsPopup.empty.firewall':'firewalls running',
   'installer.step3.hostWarningsPopup.empty.repositories':'repositories OS type mis-match
with registered hosts',
   'installer.step3.hostWarningsPopup.empty.disk':'disk space issues',
+  'installer.step3.hostWarningsPopup.empty.jdk':'JDK issues',
   'installer.step3.hostWarningsPopup.reverseLookup.name': 'Reverse Lookup validation failed
on',
   'installer.step3.hostWarningsPopup.reverseLookup': 'Reverse Lookup Issues',
   'installer.step3.hostWarningsPopup.reverseLookup.message': 'The hostname was not found
in the reverse DNS lookup. This may result in incorrect behavior. Please check the DNS setup
and fix the issue.',
   'installer.step3.hostWarningsPopup.reverseLookup.empty': 'reverse DNS lookup issues.',
   'installer.step3.hostWarningsPopup.resolution.validation.name': 'Hostname Resolution Issues',
-  'installer.step3.hostWarningsPopup.resolution.validation.error': 'Hostname resolution failed
on',
+  'installer.step3.hostWarningsPopup.resolution.validation.error': 'Hostname resolution',
   'installer.step3.hostWarningsPopup.resolution.validation': 'Hostname resolution validation',
   'installer.step3.hostWarningsPopup.resolution.validation.message': 'Not all hosts could
resolve hostnames of other hosts. Make sure that host resolution works properly on all hosts
before continuing.',
   'installer.step3.hostWarningsPopup.resolution.validation.empty': 'hostname resolution issues',
@@ -523,6 +528,7 @@ Em.I18n.translations = {
   'installer.step3.hostWarningsPopup.action.installed':'Installed on',
   'installer.step3.hostWarningsPopup.action.running':'Running on',
   'installer.step3.hostWarningsPopup.action.invalid':'Invalid on',
+  'installer.step3.hostWarningsPopup.action.failed':'Failed on',
   'installer.step3.hostWarningsPopup.host':'host',
   'installer.step3.hostWarningsPopup.hosts':'hosts',
   'installer.step3.hostWarningsPopup.moreHosts':'{0} more hosts...<br>Click on link
to view all hosts.',

http://git-wip-us.apache.org/repos/asf/ambari/blob/9f9ea545/ambari-web/app/utils/ajax/ajax.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/ajax/ajax.js b/ambari-web/app/utils/ajax/ajax.js
index 3bc98c3..7997249 100644
--- a/ambari-web/app/utils/ajax/ajax.js
+++ b/ambari-web/app/utils/ajax/ajax.js
@@ -1591,7 +1591,34 @@ var urls = {
       }
     }
   },
-
+  'wizard.step3.jdk_check': {
+    'real': '/requests',
+    'mock': '',
+    'format': function (data) {
+      return {
+        type: 'POST',
+        data: JSON.stringify({
+          "RequestInfo": {
+            "context": "Check hosts",
+            "action": "check_host",
+            "parameters" : {
+              "threshold" : "60",
+              "java_home" : data.java_home,
+              "jdk_location": data.jdk_location,
+              "check_execute_list" : "java_home_check"
+            }
+          },
+          "Requests/resource_filters": [{
+            "hosts": data.host_names
+          }]
+        })
+      }
+    }
+  },
+  'wizard.step3.jdk_check.get_results': {
+    'real': '/requests/{requestIndex}?fields=*,tasks/Tasks/host_name,tasks/Tasks/status,tasks/Tasks/structured_out',
+    'mock': '/data/requests/host_check/jdk_check_results.json'
+  },
   'wizard.step3.host_info': {
     'real': '/hosts?fields=Hosts/total_mem,Hosts/cpu_count,Hosts/disk_info,Hosts/last_agent_env,Hosts/host_name,Hosts/os_type,Hosts/os_arch,Hosts/ip',
     'mock': '/data/wizard/bootstrap/two_hosts_information.json',
@@ -1707,6 +1734,10 @@ var urls = {
       };
     }
   },
+  'ambari.service.load_jdk_name': {
+    'real': '/services/AMBARI/components/AMBARI_SERVER?fields=RootServiceComponents/properties/jdk.name,RootServiceComponents/properties/java.home,RootServiceComponents/properties/jdk_location',
+    'mock': '/data/requests/host_check/jdk_name.json'
+  },
   'ambari.service.load_server_version': {
     'real': '/services/AMBARI/components/AMBARI_SERVER?fields=RootServiceComponents/component_version',
     'mock': ''

http://git-wip-us.apache.org/repos/asf/ambari/blob/9f9ea545/ambari-web/app/views/wizard/step3/hostWarningPopupBody_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/wizard/step3/hostWarningPopupBody_view.js b/ambari-web/app/views/wizard/step3/hostWarningPopupBody_view.js
index e01bcf3..06ce91b 100644
--- a/ambari-web/app/views/wizard/step3/hostWarningPopupBody_view.js
+++ b/ambari-web/app/views/wizard/step3/hostWarningPopupBody_view.js
@@ -142,12 +142,23 @@ App.WizardStep3HostWarningPopupBody = Em.View.extend({
    * @type {Ember.Object[]}
    */
   content: function () {
+    var hostCheckWarnings = this.get('bodyController.hostCheckWarnings');
     var repoCategoryWarnings = this.get('bodyController.repoCategoryWarnings');
     var diskCategoryWarnings = this.get('bodyController.diskCategoryWarnings');
-    var hostCheckWarnings = this.get('bodyController.hostCheckWarnings');
+    var jdkCategoryWarnings = this.get('bodyController.jdkCategoryWarnings') || [];
     var categoryWarnings = this.get('categoryWarnings');
     return [
       Em.Object.create({
+        warnings: jdkCategoryWarnings,
+        title: Em.I18n.t('installer.step3.hostWarningsPopup.jdk'),
+        message: Em.I18n.t('installer.step3.hostWarningsPopup.jdk.message'),
+        type: Em.I18n.t('common.issues'),
+        emptyName: Em.I18n.t('installer.step3.hostWarningsPopup.empty.jdk'),
+        action: Em.I18n.t('installer.step3.hostWarningsPopup.action.exists'),
+        category: 'jdk',
+        isCollapsed: true
+      }),
+      Em.Object.create({
         warnings: diskCategoryWarnings,
         title: Em.I18n.t('installer.step3.hostWarningsPopup.disk'),
         message: Em.I18n.t('installer.step3.hostWarningsPopup.disk.message'),
@@ -259,24 +270,27 @@ App.WizardStep3HostWarningPopupBody = Em.View.extend({
         warnings: hostCheckWarnings,
         title: Em.I18n.t('installer.step3.hostWarningsPopup.resolution.validation.name'),
         message: Em.I18n.t('installer.step3.hostWarningsPopup.resolution.validation.message'),
+        type: Em.I18n.t('common.issues'),
         emptyName: Em.I18n.t('installer.step3.hostWarningsPopup.resolution.validation.empty'),
+        action: Em.I18n.t('installer.step3.hostWarningsPopup.action.failed'),
         category: 'hostNameResolution',
         isCollapsed: true
       })
     ]
-  }.property('category', 'warningsByHost'),
+  }.property('category', 'warningsByHost', 'bodyController.jdkCategoryWarnings', 'bodyController.repoCategoryWarnings',
'bodyController.diskCategoryWarnings', 'bodyController.hostCheckWarnings'),
 
   /**
    * Message with info about warnings
    * @return {string}
    */
   warningsNotice: function () {
-    var issuesNumber = this.get('warnings.length') + this.get('bodyController.repoCategoryWarnings.length')
+ this.get('bodyController.diskCategoryWarnings.length');
+    var issuesNumber = this.get('warnings.length') + this.get('bodyController.repoCategoryWarnings.length')
+ this.get('bodyController.diskCategoryWarnings.length')
+      + this.get('bodyController.jdkCategoryWarnings.length') + this.get('bodyController.hostCheckWarnings.length');
     var issues = issuesNumber + ' ' + (issuesNumber.length === 1 ? Em.I18n.t('installer.step3.hostWarningsPopup.issue')
: Em.I18n.t('installer.step3.hostWarningsPopup.issues'));
     var hostsCnt = this.warningHostsNamesCount();
     var hosts = hostsCnt + ' ' + (hostsCnt === 1 ? Em.I18n.t('installer.step3.hostWarningsPopup.host')
: Em.I18n.t('installer.step3.hostWarningsPopup.hosts'));
     return Em.I18n.t('installer.step3.hostWarningsPopup.summary').format(issues, hosts);
-  }.property('warnings', 'warningsByHost'),
+  }.property('warnings', 'warningsByHost', 'bodyController.jdkCategoryWarnings', 'bodyController.repoCategoryWarnings',
'bodyController.diskCategoryWarnings', 'bodyController.hostCheckWarnings'),
 
   /**
    * Detailed content to show it in new window
@@ -381,6 +395,8 @@ App.WizardStep3HostWarningPopupBody = Em.View.extend({
     });
     var repoCategoryWarnings = this.get('bodyController.repoCategoryWarnings');
     var diskCategoryWarnings = this.get('bodyController.diskCategoryWarnings');
+    var jdkCategoryWarnings = this.get('bodyController.jdkCategoryWarnings');
+    var hostResolutionWarnings = this.get('bodyController.hostCheckWarnings');
 
     if (repoCategoryWarnings.length) {
       repoCategoryWarnings[0].hostsNames.forEach(function (_hostName) {
@@ -396,6 +412,20 @@ App.WizardStep3HostWarningPopupBody = Em.View.extend({
         }
       });
     }
+    if (jdkCategoryWarnings && jdkCategoryWarnings.length) {
+      jdkCategoryWarnings[0].hostsNames.forEach(function (_hostName) {
+        if (!hostNameMap[_hostName]) {
+          hostNameMap[_hostName] = true;
+        }
+      });
+    }
+    if (hostResolutionWarnings && hostResolutionWarnings.length) {
+      hostResolutionWarnings[0].hosts.forEach(function (_hostName) {
+        if (!hostNameMap[_hostName]) {
+          hostNameMap[_hostName] = true;
+        }
+      });
+    }
     return Em.keys(hostNameMap).length;
   },
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/9f9ea545/ambari-web/app/views/wizard/step3_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/wizard/step3_view.js b/ambari-web/app/views/wizard/step3_view.js
index 1466d2c..705048f 100644
--- a/ambari-web/app/views/wizard/step3_view.js
+++ b/ambari-web/app/views/wizard/step3_view.js
@@ -276,7 +276,7 @@ App.WizardStep3View = App.TableView.extend({
         this.set('message', Em.I18n.t('installer.step3.warning.loading'));
       }
       else {
-        if (this.get('controller.isHostHaveWarnings') || this.get('controller.repoCategoryWarnings.length')
|| this.get('controller.diskCategoryWarnings.length') || this.get('controller.hostCheckWarnings'))
{
+        if (this.get('controller.isHostHaveWarnings') || this.get('controller.repoCategoryWarnings.length')
|| this.get('controller.diskCategoryWarnings.length') || this.get('controller.jdkCategoryWarnings.length')
|| this.get('controller.hostCheckWarnings.length')) {
           this.set('status', 'alert-warn');
           this.set('linkText', Em.I18n.t('installer.step3.warnings.linkText'));
           this.set('message', Em.I18n.t('installer.step3.warnings.fails').format(hosts.length
- failedHosts));
@@ -303,8 +303,8 @@ App.WizardStep3View = App.TableView.extend({
         }
       }
     }
-  }.observes('controller.isWarningsLoaded', 'controller.isHostHaveWarnings', 'controller.repoCategoryWarnings',
'controller.diskCategoryWarnings', 'controller.hostCheckWarnings')
 
+  }.observes('controller.isWarningsLoaded','controller.hostCheckWarnings', 'controller.isHostHaveWarnings',
'controller.repoCategoryWarnings', 'controller.diskCategoryWarnings', 'controller.jdkCategoryWarnings')
 });
 
 //todo: move it inside WizardStep3View


Mime
View raw message