ambari-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From akovale...@apache.org
Subject ambari git commit: AMBARI-8398. Alerts UI: Details page thresholds API integration. (akovalenko)
Date Thu, 20 Nov 2014 17:37:21 GMT
Repository: ambari
Updated Branches:
  refs/heads/trunk 6f2875cee -> f543cc6c0


AMBARI-8398. Alerts UI: Details page thresholds API integration. (akovalenko)


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

Branch: refs/heads/trunk
Commit: f543cc6c08909b3ed157fda4ba23827a745a7881
Parents: 6f2875c
Author: Aleksandr Kovalenko <akovalenko@hortonworks.com>
Authored: Thu Nov 20 18:38:09 2014 +0200
Committer: Aleksandr Kovalenko <akovalenko@hortonworks.com>
Committed: Thu Nov 20 18:38:09 2014 +0200

----------------------------------------------------------------------
 .../main/alert_definitions_controller.js        |   6 +-
 .../alerts/definition_configs_controller.js     |  48 ++++++++-
 .../mappers/alert_definition_summary_mapper.js  |   8 +-
 .../app/mappers/alert_definitions_mapper.js     |  42 +++++---
 ambari-web/app/models/alert_config.js           | 102 +++++++++++++++----
 ambari-web/app/models/alert_definition.js       |  21 ++++
 .../main/alerts/definition_details.hbs          |   2 +-
 .../definitions_configs_controller_test.js      |  94 ++++++++++++++++-
 .../mappers/alert_definitions_mapper_test.js    |   2 +
 9 files changed, 274 insertions(+), 51 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/f543cc6c/ambari-web/app/controllers/main/alert_definitions_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/main/alert_definitions_controller.js b/ambari-web/app/controllers/main/alert_definitions_controller.js
index 7ada3ba..df02e0e 100644
--- a/ambari-web/app/controllers/main/alert_definitions_controller.js
+++ b/ambari-web/app/controllers/main/alert_definitions_controller.js
@@ -42,11 +42,7 @@ App.MainAlertDefinitionsController = Em.ArrayController.extend({
    * @type {App.AlertDefinition[]}
    */
   content: function() {
-    return Array.prototype.concat.call(Array.prototype, App.PortAlertDefinition.find().toArray(),
-      App.MetricsAlertDefinition.find().toArray(),
-      App.WebAlertDefinition.find().toArray(),
-      App.AggregateAlertDefinition.find().toArray(),
-      App.ScriptAlertDefinition.find().toArray());
+    return App.AlertDefinition.getAllDefinitions();
   }.property('mapperTimestamp'),
 
   /**

http://git-wip-us.apache.org/repos/asf/ambari/blob/f543cc6c/ambari-web/app/controllers/main/alerts/definition_configs_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/main/alerts/definition_configs_controller.js b/ambari-web/app/controllers/main/alerts/definition_configs_controller.js
index 12ab94e..dc57b09 100644
--- a/ambari-web/app/controllers/main/alerts/definition_configs_controller.js
+++ b/ambari-web/app/controllers/main/alerts/definition_configs_controller.js
@@ -174,10 +174,12 @@ App.MainAlertDefinitionConfigsController = Em.Controller.extend({
         })
       }),
       App.AlertConfigProperties.Metrics.create({
-        value: alertDefinition.get('jmx.propertyList') ? alertDefinition.get('jmx.propertyList').join('\n')
: alertDefinition.get('ganglia.propertyList').join('\n')
+        value: alertDefinition.get('jmx.propertyList') ? alertDefinition.get('jmx.propertyList').join(',\n')
: alertDefinition.get('ganglia.propertyList').join(',\n'),
+        isJMXMetric: !!alertDefinition.get('jmx.propertyList')
       }),
       App.AlertConfigProperties.FormatString.create({
-        value: alertDefinition.get('jmx.value') ? alertDefinition.get('jmx.value') : alertDefinition.get('ganglia.value')
+        value: alertDefinition.get('jmx.value') ? alertDefinition.get('jmx.value') : alertDefinition.get('ganglia.value'),
+        isJMXMetric: !!alertDefinition.get('jmx.value')
       })
     ];
   },
@@ -306,9 +308,49 @@ App.MainAlertDefinitionConfigsController = Em.Controller.extend({
    * Save edit configs button handler
    */
   saveConfigs: function () {
-    //todo: write logic for saving alert definition properties to server
     this.get('configs').setEach('isDisabled', true);
     this.set('canEdit', false);
+
+    var propertiesToUpdate = this.getPropertiesToUpdate();
+
+    App.ajax.send({
+      name: 'alerts.update_alert_definition',
+      sender: this,
+      data: {
+        id: this.get('content.id'),
+        data: propertiesToUpdate
+      }
+    });
+  },
+
+  /**
+   * Create object with new values to put it on server
+   * @returns {Object}
+   */
+  getPropertiesToUpdate: function () {
+    var propertiesToUpdate = {};
+    this.get('configs').filterProperty('wasChanged').forEach(function (property) {
+      if (property.get('apiProperty').contains('source.')) {
+        if (!propertiesToUpdate['AlertDefinition/source']) {
+          propertiesToUpdate['AlertDefinition/source'] = this.get('content.rawSourceData');
+        }
+
+        var sourcePath = propertiesToUpdate['AlertDefinition/source'];
+        property.get('apiProperty').replace('source.', '').split('.').forEach(function (path,
index, array) {
+          // check if it is last path
+          if (array.length - index === 1) {
+            sourcePath[path] = property.get('apiFormattedValue');
+          } else {
+            sourcePath = sourcePath[path];
+          }
+        });
+
+      } else {
+        propertiesToUpdate['AlertDefinition/' + property.get('apiProperty')] = property.get('apiFormattedValue');
+      }
+    }, this);
+
+    return propertiesToUpdate;
   }
 
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/f543cc6c/ambari-web/app/mappers/alert_definition_summary_mapper.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mappers/alert_definition_summary_mapper.js b/ambari-web/app/mappers/alert_definition_summary_mapper.js
index 8b9680d..9e18b0d 100644
--- a/ambari-web/app/mappers/alert_definition_summary_mapper.js
+++ b/ambari-web/app/mappers/alert_definition_summary_mapper.js
@@ -23,13 +23,7 @@ App.alertDefinitionSummaryMapper = App.QuickDataMapper.create({
 
   map: function(data) {
     if (!data.alerts_summary_grouped) return;
-    var alertDefinitions = Array.prototype.concat.call(
-      Array.prototype, App.PortAlertDefinition.find().toArray(),
-      App.MetricsAlertDefinition.find().toArray(),
-      App.WebAlertDefinition.find().toArray(),
-      App.AggregateAlertDefinition.find().toArray(),
-      App.ScriptAlertDefinition.find().toArray()
-    );
+    var alertDefinitions = App.AlertDefinition.getAllDefinitions();
     data.alerts_summary_grouped.forEach(function(alertDefinitionSummary) {
       var alertDefinition = alertDefinitions.findProperty('id', alertDefinitionSummary.definition_id);
       if (alertDefinition) {

http://git-wip-us.apache.org/repos/asf/ambari/blob/f543cc6c/ambari-web/app/mappers/alert_definitions_mapper.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mappers/alert_definitions_mapper.js b/ambari-web/app/mappers/alert_definitions_mapper.js
index 2b817d9..7ca8c41 100644
--- a/ambari-web/app/mappers/alert_definitions_mapper.js
+++ b/ambari-web/app/mappers/alert_definitions_mapper.js
@@ -74,21 +74,16 @@ App.alertDefinitionsMapper = App.QuickDataMapper.create({
     if (json && json.items) {
 
       var portAlertDefinitions = [],
-        metricsAlertDefinitions = [],
-        webAlertDefinitions = [],
-        aggregateAlertDefinitions = [],
-        scriptAlertDefinitions = [],
-        alertReportDefinitions = [],
-        alertMetricsSourceDefinitions = [],
-        alertMetricsUriDefinitions = [],
-        alertGroupsMap = App.cache['previousAlertGroupsMap'],
-        alertDefinitions = Array.prototype.concat.call(
-          Array.prototype, App.PortAlertDefinition.find().toArray(),
-          App.MetricsAlertDefinition.find().toArray(),
-          App.WebAlertDefinition.find().toArray(),
-          App.AggregateAlertDefinition.find().toArray(),
-          App.ScriptAlertDefinition.find().toArray()
-        );
+          metricsAlertDefinitions = [],
+          webAlertDefinitions = [],
+          aggregateAlertDefinitions = [],
+          scriptAlertDefinitions = [],
+          alertReportDefinitions = [],
+          alertMetricsSourceDefinitions = [],
+          alertMetricsUriDefinitions = [],
+          alertGroupsMap = App.cache['previousAlertGroupsMap'],
+          alertDefinitions = App.AlertDefinition.getAllDefinitions(),
+          rawSourceData = {};
 
       json.items.forEach(function (item) {
         var convertedReportDefinitions = [];
@@ -106,6 +101,9 @@ App.alertDefinitionsMapper = App.QuickDataMapper.create({
 
         alertReportDefinitions = alertReportDefinitions.concat(convertedReportDefinitions);
         item.reporting = convertedReportDefinitions;
+
+        rawSourceData[item.AlertDefinition.id] = item.AlertDefinition.source;
+
         var alertDefinition = this.parseIt(item, this.get('config'));
 
         if (alertGroupsMap[alertDefinition.id]) {
@@ -179,8 +177,9 @@ App.alertDefinitionsMapper = App.QuickDataMapper.create({
       App.store.loadMany(this.get('webModel'), webAlertDefinitions);
       App.store.loadMany(this.get('aggregateModel'), aggregateAlertDefinitions);
       App.store.loadMany(this.get('scriptModel'), scriptAlertDefinitions);
+      this.setAlertDefinitionsRawSourceData(rawSourceData);
       if (App.router.get('mainAlertDefinitionsController')) {
-         App.router.set('mainAlertDefinitionsController.mapperTimestamp', (new Date()).getTime());
+        App.router.set('mainAlertDefinitionsController.mapperTimestamp', (new Date()).getTime());
       }
     }
   },
@@ -194,5 +193,16 @@ App.alertDefinitionsMapper = App.QuickDataMapper.create({
     data.forEach(function (record) {
       model.find().findProperty('id', record.id).set('propertyList', record.property_list);
     });
+  },
+
+  /**
+   * set rawSourceDate properties for <code>App.AlertDefinition</code> records
+   * @param rawSourceData
+   */
+  setAlertDefinitionsRawSourceData: function (rawSourceData) {
+    var allDefinitions = App.AlertDefinition.getAllDefinitions();
+    for (var alertDefinitionId in rawSourceData) {
+      allDefinitions.findProperty('id', +alertDefinitionId).set('rawSourceData', rawSourceData[alertDefinitionId]);
+    }
   }
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/f543cc6c/ambari-web/app/models/alert_config.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models/alert_config.js b/ambari-web/app/models/alert_config.js
index 7d9a720..804eec4 100644
--- a/ambari-web/app/models/alert_config.js
+++ b/ambari-web/app/models/alert_config.js
@@ -70,6 +70,36 @@ App.AlertConfigProperty = Ember.Object.extend({
   classNames: '',
 
   /**
+   * name of property related to config
+   * @type {String}
+   */
+  apiProperty: '',
+
+  /**
+   * for some metrics properties may be set true or false
+   * depending on what property is related to (JMX or Ganglia)
+   */
+  isJMXMetric: null,
+
+  /**
+   * value converted to appropriate format for sending to server
+   * should be computed property
+   * should be defined in child class
+   * @type {*}
+   */
+  apiFormattedValue: function () {
+    return this.get('value');
+  }.property('value'),
+
+  /**
+   * define if property was changed by user
+   * @type {Boolean}
+   */
+  wasChanged: function () {
+    return this.get('previousValue') !== null && this.get('value') !== this.get('previousValue');
+  }.property('value', 'previousValue'),
+
+  /**
    * view class according to <code>displayType</code>
    * @type {Em.View}
    */
@@ -96,35 +126,52 @@ App.AlertConfigProperties = {
   AlertName: App.AlertConfigProperty.extend({
     label: 'Alert Name',
     displayType: 'textField',
-    classNames: 'alert-text-input'
+    classNames: 'alert-text-input',
+    apiProperty: 'name'
   }),
   AlertNameSelected: App.AlertConfigProperty.extend({
     label: 'Alert Name',
-    displayType: 'select'
+    displayType: 'select',
+    apiProperty: 'name'
   }),
   Service: App.AlertConfigProperty.extend({
     label: 'Service',
-    displayType: 'select'
+    displayType: 'select',
+    apiProperty: 'service_name',
+    apiFormattedValue: function () {
+      return App.StackService.find().findProperty('displayName', this.get('value')).get('serviceName');
+    }.property('value')
   }),
   Component: App.AlertConfigProperty.extend({
     label: 'Component',
-    displayType: 'select'
+    displayType: 'select',
+    apiProperty: 'component_name',
+    apiFormattedValue: function () {
+      return App.StackServiceComponent.find().findProperty('displayName', this.get('value')).get('componentName');
+    }.property('value')
   }),
   Scope: App.AlertConfigProperty.extend({
     label: 'Scope',
     options: ['Any', 'Host', 'Service'],
-    displayType: 'select'
+    displayType: 'select',
+    apiProperty: 'scope',
+    apiFormattedValue: function () {
+      return this.get('value').toUpperCase();
+    }.property('value')
   }),
   Description: App.AlertConfigProperty.extend({
     label: 'Description',
     displayType: 'textArea',
-    classNames: 'alert-config-text-area'
+    classNames: 'alert-config-text-area',
+    // todo: check value after API will be provided
+    apiProperty: 'description'
   }),
   Interval: App.AlertConfigProperty.extend({
     label: 'Interval',
     displayType: 'textField',
     unit: 'Second',
-    classNames: 'alert-interval-input'
+    classNames: 'alert-interval-input',
+    apiProperty: 'interval'
   }),
   Thresholds: App.AlertConfigProperty.extend({
     label: 'Thresholds',
@@ -133,6 +180,8 @@ App.AlertConfigProperties = {
     from: '',
     to: '',
     value: '',
+    // todo: check value after API will be provided
+    apiProperty: 'thresholds',
 
     setFromTo: function () {
       this.set('doNotChangeValue', true);
@@ -153,35 +202,54 @@ App.AlertConfigProperties = {
   URI: App.AlertConfigProperty.extend({
     label: 'URI',
     displayType: 'textField',
-    classNames: 'alert-text-input'
+    classNames: 'alert-text-input',
+    apiProperty: 'source.uri'
   }),
   URIExtended: App.AlertConfigProperty.extend({
     label: 'URI',
     displayType: 'textArea',
-    classNames: 'alert-config-text-area'
+    classNames: 'alert-config-text-area',
+    apiProperty: 'source.uri',
+    apiFormattedValue: function () {
+      var result = {};
+      try {
+        result = JSON.parse(this.get('value'));
+      } catch (e) {
+        console.error('Wrong format of URI');
+      }
+      return result;
+    }.property('value')
   }),
   DefaultPort: App.AlertConfigProperty.extend({
     label: 'Default Port',
     displayType: 'textField',
-    classNames: 'alert-port-input'
+    classNames: 'alert-port-input',
+    apiProperty: 'source.default_port'
   }),
   Path: App.AlertConfigProperty.extend({
     label: 'Path',
     displayType: 'textField',
-    classNames: 'alert-text-input'
+    classNames: 'alert-text-input',
+    apiProperty: 'source.path'
   }),
   Metrics: App.AlertConfigProperty.extend({
     label: 'JMX/Ganglia Metrics',
     displayType: 'textArea',
-    classNames: 'alert-config-text-area'
+    classNames: 'alert-config-text-area',
+    apiProperty: function () {
+      return this.get('isJMXMetric') ? 'source.jmx.property_list' : 'source.ganglia.property_list'
+    }.property('isJMXMetric'),
+    apiFormattedValue: function () {
+      return this.get('value').split(',\n');
+    }.property('value')
   }),
   FormatString: App.AlertConfigProperty.extend({
     label: 'Format String',
     displayType: 'textArea',
-    classNames: 'alert-config-text-area'
+    classNames: 'alert-config-text-area',
+    apiProperty: function () {
+      return this.get('isJMXMetric') ? 'source.jmx.value' : 'source.ganglia.value'
+    }.property('isJMXMetric')
   })
 
-};
-
-
-
+};
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/f543cc6c/ambari-web/app/models/alert_definition.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models/alert_definition.js b/ambari-web/app/models/alert_definition.js
index 302fa65..2005595 100644
--- a/ambari-web/app/models/alert_definition.js
+++ b/ambari-web/app/models/alert_definition.js
@@ -34,6 +34,13 @@ App.AlertDefinition = DS.Model.extend({
   lastTriggered: DS.attr('number'),
 
   /**
+   * Raw data from AlertDefinition/source
+   * used to format request content for updating alert definition
+   * @type {Object}
+   */
+  rawSourceData: {},
+
+  /**
    * Counts of alert grouped by their status
    * Format:
    * <code>
@@ -95,6 +102,20 @@ App.AlertDefinition = DS.Model.extend({
   thresholds: '5-10'
 });
 
+App.AlertDefinition.reopenClass({
+
+  getAllDefinitions: function () {
+    return Array.prototype.concat.call(
+        Array.prototype, App.PortAlertDefinition.find().toArray(),
+        App.MetricsAlertDefinition.find().toArray(),
+        App.WebAlertDefinition.find().toArray(),
+        App.AggregateAlertDefinition.find().toArray(),
+        App.ScriptAlertDefinition.find().toArray()
+    )
+  }
+
+});
+
 App.AlertReportDefinition = DS.Model.extend({
   type: DS.attr('string'),
   text: DS.attr('string'),

http://git-wip-us.apache.org/repos/asf/ambari/blob/f543cc6c/ambari-web/app/templates/main/alerts/definition_details.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/main/alerts/definition_details.hbs b/ambari-web/app/templates/main/alerts/definition_details.hbs
index 8c128b5..ab58561 100644
--- a/ambari-web/app/templates/main/alerts/definition_details.hbs
+++ b/ambari-web/app/templates/main/alerts/definition_details.hbs
@@ -113,7 +113,7 @@
           </tr>
           </tbody>
         </table>
-        <span>{{t alerts.table.header.lastTriggered}} : {{controller.content.lastTriggered}}</span>
+        <span>{{t alerts.table.header.lastTriggered}} : {{controller.content.lastTriggeredFormatted}}</span>
       </div>
     </div>
   </div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/f543cc6c/ambari-web/test/controllers/main/alerts/definitions_configs_controller_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/controllers/main/alerts/definitions_configs_controller_test.js
b/ambari-web/test/controllers/main/alerts/definitions_configs_controller_test.js
index a5d9040..2c31f4d 100644
--- a/ambari-web/test/controllers/main/alerts/definitions_configs_controller_test.js
+++ b/ambari-web/test/controllers/main/alerts/definitions_configs_controller_test.js
@@ -163,7 +163,7 @@ describe('App.MainAlertDefinitionConfigsController', function () {
       expect(result.someProperty('value', 60)).to.be.true;
       expect(result.someProperty('value', '10-20')).to.be.true;
       expect(result.someProperty('value', '{\"http\":\"{{mapred-site/mapreduce.jobhistory.webapp.address}}\",\"https\":\"{{mapred-site/mapreduce.jobhistory.webapp.https.address}}\"}')).to.be.true;
-      expect(result.someProperty('value', 'property1\nproperty2')).to.be.true;
+      expect(result.someProperty('value', 'property1,\nproperty2')).to.be.true;
       expect(result.someProperty('value', 'jmxValue')).to.be.true;
     });
 
@@ -297,6 +297,14 @@ describe('App.MainAlertDefinitionConfigsController', function () {
 
   describe('#saveConfigs()', function () {
 
+    beforeEach(function () {
+      sinon.spy(App.ajax, 'send');
+    });
+
+    afterEach(function () {
+      App.ajax.send.restore();
+    });
+
     it('should set previousValue, isDisabled for each config and change canEdit flag', function
() {
 
       controller.set('configs', [
@@ -311,8 +319,90 @@ describe('App.MainAlertDefinitionConfigsController', function () {
 
       expect(controller.get('configs').someProperty('isDisabled', false)).to.be.false;
       expect(controller.get('canEdit')).to.be.false;
+      expect(App.ajax.send.calledOnce).to.be.true;
+    });
+
+  });
+
+  describe('#getPropertiesToUpdate()', function () {
+
+    beforeEach(function () {
+      controller.set('content', {
+        rawSourceData: {
+          path1: 'value',
+          path2: {
+            path3: 'value'
+          }
+        }
+      });
     });
 
+    var testCases = [
+      {
+        m: 'should ignore configs with wasChanged false',
+        configs: [
+          Em.Object.create({
+            wasChanged: false,
+            apiProperty: 'name1',
+            apiFormattedValue: 'test1'
+          }),
+          Em.Object.create({
+            wasChanged: true,
+            apiProperty: 'name2',
+            apiFormattedValue: 'test2'
+          }),
+          Em.Object.create({
+            wasChanged: false,
+            apiProperty: 'name3',
+            apiFormattedValue: 'test3'
+          })
+        ],
+        result: {
+          'AlertDefinition/name2': 'test2'
+        }
+      },
+      {
+        m: 'should correctly map deep source properties',
+        configs: [
+          Em.Object.create({
+            wasChanged: true,
+            apiProperty: 'name1',
+            apiFormattedValue: 'test1'
+          }),
+          Em.Object.create({
+            wasChanged: true,
+            apiProperty: 'source.path1',
+            apiFormattedValue: 'value1'
+          }),
+          Em.Object.create({
+            wasChanged: true,
+            apiProperty: 'source.path2.path3',
+            apiFormattedValue: 'value2'
+          })
+        ],
+        result: {
+          'AlertDefinition/name1': 'test1',
+          'AlertDefinition/source': {
+            path1: 'value1',
+            path2: {
+              path3: 'value2'
+            }
+          }
+        }
+      }
+    ];
+
+    testCases.forEach(function (testCase) {
+
+      it(testCase.m, function () {
+
+        controller.set('configs', testCase.configs);
+        var result = controller.getPropertiesToUpdate();
+
+        expect(result).to.eql(testCase.result);
+      });
+    });
   });
 
-});
+})
+;

http://git-wip-us.apache.org/repos/asf/ambari/blob/f543cc6c/ambari-web/test/mappers/alert_definitions_mapper_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/mappers/alert_definitions_mapper_test.js b/ambari-web/test/mappers/alert_definitions_mapper_test.js
index 12535a5..802fd96 100644
--- a/ambari-web/test/mappers/alert_definitions_mapper_test.js
+++ b/ambari-web/test/mappers/alert_definitions_mapper_test.js
@@ -210,6 +210,7 @@ describe('App.alertDefinitionsMapper', function () {
       App.cache['previousAlertGroupsMap'] = {};
 
       sinon.stub(App.alertDefinitionsMapper, 'setMetricsSourcePropertyLists', Em.K);
+      sinon.stub(App.alertDefinitionsMapper, 'setAlertDefinitionsRawSourceData', Em.K);
 
     });
 
@@ -242,6 +243,7 @@ describe('App.alertDefinitionsMapper', function () {
       App.cache['previousAlertGroupsMap'] = {};
 
       App.alertDefinitionsMapper.setMetricsSourcePropertyLists.restore();
+      App.alertDefinitionsMapper.setAlertDefinitionsRawSourceData.restore();
 
     });
 


Mime
View raw message