tez-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From s...@apache.org
Subject tez git commit: TEZ-2780. Tez UI: Update All Tasks page while in progress (sree)
Date Fri, 11 Sep 2015 18:44:39 GMT
Repository: tez
Updated Branches:
  refs/heads/branch-0.7 39fb968c5 -> e3f10d2e0


TEZ-2780. Tez UI: Update All Tasks page while in progress (sree)

(cherry picked from commit 9554c6455900b18c6a30779b2fcc1ced96299f6e)


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

Branch: refs/heads/branch-0.7
Commit: e3f10d2e0ae77199ac4b848152e31c093024e97b
Parents: 39fb968
Author: Sreenath Somarajapuram <sree@apache.org>
Authored: Fri Sep 11 23:56:10 2015 +0530
Committer: Sreenath Somarajapuram <sree@apache.org>
Committed: Sat Sep 12 00:12:11 2015 +0530

----------------------------------------------------------------------
 CHANGES.txt                                     |   1 +
 tez-ui/src/main/webapp/app/scripts/app.js       |  22 +++-
 .../basic-table/basic-table-component.js        |   6 +-
 .../scripts/components/basic-table/cell-view.js |  39 ++++++-
 .../app/scripts/controllers/dag_controller.js   |   2 +-
 .../webapp/app/scripts/controllers/dag_tasks.js |  59 +++++++++++
 .../controllers/table-page-controller.js        |  14 +++
 .../scripts/helpers/entity-array-pollster.js    | 103 +++++++++++++++++++
 .../src/main/webapp/app/scripts/helpers/misc.js |   9 ++
 .../main/webapp/app/scripts/helpers/pollster.js |   2 +-
 .../app/scripts/models/TimelineRestAdapter.js   |  13 +++
 .../src/main/webapp/app/scripts/models/dag.js   |   9 +-
 tez-ui/src/main/webapp/app/scripts/router.js    |  10 +-
 .../main/webapp/app/templates/common/table.hbs  |   2 +
 14 files changed, 282 insertions(+), 9 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tez/blob/e3f10d2e/CHANGES.txt
----------------------------------------------------------------------
diff --git a/CHANGES.txt b/CHANGES.txt
index f842f7d..d907ede 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -6,6 +6,7 @@ Release 0.7.1: Unreleased
 INCOMPATIBLE CHANGES
 
 ALL CHANGES:
+  TEZ-2780. Tez UI: Update All Tasks page while in progress
   TEZ-2792. addendum fix build failure for java 6
   TEZ-2792. Add AM web service API for tasks
   TEZ-2807. Log data in the finish event instead of the start event

http://git-wip-us.apache.org/repos/asf/tez/blob/e3f10d2e/tez-ui/src/main/webapp/app/scripts/app.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/app.js b/tez-ui/src/main/webapp/app/scripts/app.js
index 8e32874..13c08cb 100644
--- a/tez-ui/src/main/webapp/app/scripts/app.js
+++ b/tez-ui/src/main/webapp/app/scripts/app.js
@@ -218,6 +218,25 @@ App.ready = function () {
       return 'verticesInfo?dagID=%@';
     }
   });
+
+  App.TaskInfoAdapter = App.AMInfoAdapter.extend({
+    namespace: App.Configs.restNamespace.aminfoV2,
+    findQuery: function(store, type, query) {
+      var record = query.metadata;
+      delete query.metadata;
+      return this.ajax(
+        this.buildURL(Ember.String.pluralize(type.typeKey),
+          record.taskID, Em.Object.create(record)), 'GET', { data: query});
+    },
+    buildURL: function(type, id, record) {
+      var url = this._super(type, undefined, record);
+      return url.replace('__app_id__', record.get('appID'))
+        .fmt(record.get('dagID'), id);
+    },
+    pathForType: function(typeName) {
+      return 'tasksInfo?dagID=%@&taskID=%@';
+    }
+  });
 };
 
 $.ajaxPrefilter(function(options, originalOptions, jqXHR) {
@@ -232,8 +251,9 @@ $.ajaxSetup({
 require('scripts/default-configs');
 
 require('scripts/translations');
-require('scripts/mixins/*');
+require('scripts/helpers/pollster');
 require('scripts/helpers/*');
+require('scripts/mixins/*');
 
 require('scripts/router');
 require('scripts/views/**/*');

http://git-wip-us.apache.org/repos/asf/tez/blob/e3f10d2e/tez-ui/src/main/webapp/app/scripts/components/basic-table/basic-table-component.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/components/basic-table/basic-table-component.js
b/tez-ui/src/main/webapp/app/scripts/components/basic-table/basic-table-component.js
index e56b5d8..2ea9cc5 100644
--- a/tez-ui/src/main/webapp/app/scripts/components/basic-table/basic-table-component.js
+++ b/tez-ui/src/main/webapp/app/scripts/components/basic-table/basic-table-component.js
@@ -138,8 +138,10 @@ App.BasicTableComponent = Em.Component.extend({
   }.property('columns'),
 
   _rows: function () {
-    var startIndex = (this.get('pageNum') - 1) * this.get('rowCount');
-    return this.get('_searchedRows').slice(startIndex, startIndex + this.get('rowCount'));
+    var startIndex = (this.get('pageNum') - 1) * this.get('rowCount'),
+        rows = this.get('_searchedRows').slice(startIndex, startIndex + this.get('rowCount'));
+    this.sendAction('rowsChanged', rows);
+    return rows;
   }.property('_searchedRows.@each', 'rowCount', 'pageNum'),
 
   _searchObserver: function () {

http://git-wip-us.apache.org/repos/asf/tez/blob/e3f10d2e/tez-ui/src/main/webapp/app/scripts/components/basic-table/cell-view.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/components/basic-table/cell-view.js b/tez-ui/src/main/webapp/app/scripts/components/basic-table/cell-view.js
index 80a4c56..fa8d1f7 100644
--- a/tez-ui/src/main/webapp/app/scripts/components/basic-table/cell-view.js
+++ b/tez-ui/src/main/webapp/app/scripts/components/basic-table/cell-view.js
@@ -27,16 +27,47 @@ function stringifyNumbers(content) {
 }
 
 App.BasicTableComponent.CellView = Ember.View.extend({
-  templateName: 'components/basic-table/basic-cell',
+  templateName: function () {
+    var template = this.get('column.observePath') ? 'bounded-basic-cell' : 'basic-cell';
+    return 'components/basic-table/' + template;
+  }.property('column.observePath'),
 
   classNames: ['cell-content'],
 
+  value: null,
+  observedPath: null,
+
+  _addObserver: function (path) {
+    this._removeObserver();
+    this.get('row').addObserver(path, this, this._onValueChange);
+    this.set('observedPath', path);
+  },
+
+  _removeObserver: function (path) {
+    var path = this.get('observedPath');
+    if(path) {
+      this.get('row').removeObserver(path, this, this._onValueChange);
+      this.set('observedPath', null);
+    }
+  },
+
   _normalizeContent: function (content) {
     return stringifyNumbers(content && typeof content == 'object' ? content : {
       displayText: content
     });
   },
 
+  _pathObserver: function () {
+    var path = this.get('column.contentPath');
+    if(path && this.get('column.observePath')) {
+      this._addObserver(path);
+    }
+  }.observes('row', 'column.contentPath', 'column.observePath').on('init'),
+
+  _onValueChange: function (row, path) {
+    this.set('value', row.get(path));
+  },
+
   cellContent: function () {
     var cellContent = this.get('column').getCellContent(this.get('row'));
 
@@ -47,5 +78,9 @@ App.BasicTableComponent.CellView = Ember.View.extend({
     }
 
     return this._normalizeContent(cellContent);
-  }.property('row', 'column')
+  }.property('row', 'column', 'value'),
+
+  willDestroy: function () {
+    this._removeObserver();
+  }
 });
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tez/blob/e3f10d2e/tez-ui/src/main/webapp/app/scripts/controllers/dag_controller.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/controllers/dag_controller.js b/tez-ui/src/main/webapp/app/scripts/controllers/dag_controller.js
index af22918..6260e06 100644
--- a/tez-ui/src/main/webapp/app/scripts/controllers/dag_controller.js
+++ b/tez-ui/src/main/webapp/app/scripts/controllers/dag_controller.js
@@ -143,7 +143,7 @@ App.DagController = Em.ObjectController.extend(App.Helpers.DisplayHelper,
{
         that = this;
 
     if (Em.isNone(amInfoUpdateService)) {
-      amInfoUpdateService = App.Helpers.pollster.create({
+      amInfoUpdateService = App.Helpers.Pollster.create({
         onPoll: function() {
           that.updateAMDagInfo();
           that.updateAMVerticesInfo();

http://git-wip-us.apache.org/repos/asf/tez/blob/e3f10d2e/tez-ui/src/main/webapp/app/scripts/controllers/dag_tasks.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/controllers/dag_tasks.js b/tez-ui/src/main/webapp/app/scripts/controllers/dag_tasks.js
index 81e4277..0bd9df0 100644
--- a/tez-ui/src/main/webapp/app/scripts/controllers/dag_tasks.js
+++ b/tez-ui/src/main/webapp/app/scripts/controllers/dag_tasks.js
@@ -27,6 +27,57 @@ App.DagTasksController = App.TablePageController.extend({
 
   cacheDomain: Ember.computed.alias('controllers.dag.id'),
 
+  pollster: App.Helpers.EntityArrayPollster.create(),
+
+  init: function () {
+    this._super();
+    this.get('pollster').setProperties({
+      entityType: 'taskInfo',
+      mergeProperties: ['status', 'progress'],
+      store: this.get('store')
+    });
+  },
+
+  pollsterControl: function () {
+    if(this.get('controllers.dag.status') == 'RUNNING' &&
+        this.get('controllers.dag.amWebServiceVersion') != '1' &&
+        !this.get('loading') &&
+        this.get('isActive') &&
+        this. get('rowsDisplayed.length') > 0) {
+      this.get('pollster').start();
+    }
+    else {
+      this.get('pollster').stop();
+    }
+  }.observes('controllers.dag.status',
+      'controllers.dag.amWebServiceVersion',
+      'rowsDisplayed',
+      'loading',
+      'isActive'),
+
+  reset: function () {
+    this.set('pollster.options', null);
+  },
+
+  pollsterOptionsObserver: function () {
+    var rows = this.get('rowsDisplayed');
+    this.set('pollster.targetRecords', rows);
+
+    rows = rows.filter(function (row) {
+      return row.get('status') != 'SUCCEEDED';
+    });
+
+    this.set('pollster.options', (rows && rows.length) ? {
+      appID: this.get('controllers.dag.model.applicationId'),
+      dagID: this.get('controllers.dag.model.idx'),
+      taskID: rows.map(function (row) {
+          var taskIndex = App.Helpers.misc.getIndexFromId(row.get('id')),
+          vertexIndex = App.Helpers.misc.getIndexFromId(row.get('vertexID'));
+          return '%@_%@'.fmt(vertexIndex, taskIndex);
+        }).join(',')
+    } : null);
+  }.observes('controllers.dag.model.applicationId', 'controllers.dag.model.idx', 'rowsDisplayed'),
+
   beforeLoad: function () {
     var dagController = this.get('controllers.dag'),
         model = dagController.get('model');
@@ -113,6 +164,7 @@ App.DagTasksController = App.TablePageController.extend({
         headerCellName: 'Status',
         templateName: 'components/basic-table/status-cell',
         contentPath: 'status',
+        observePath: true,
         getCellContent: function(row) {
           var status = row.get('status');
           return {
@@ -123,6 +175,13 @@ App.DagTasksController = App.TablePageController.extend({
         }
       },
       {
+        id: 'progress',
+        headerCellName: 'Progress',
+        contentPath: 'progress',
+        observePath: true,
+        templateName: 'components/basic-table/progress-cell'
+      },
+      {
         id: 'startTime',
         headerCellName: 'Start Time',
         contentPath: 'startTime',

http://git-wip-us.apache.org/repos/asf/tez/blob/e3f10d2e/tez-ui/src/main/webapp/app/scripts/controllers/table-page-controller.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/controllers/table-page-controller.js b/tez-ui/src/main/webapp/app/scripts/controllers/table-page-controller.js
index 7e17c0e..dc66d7d 100644
--- a/tez-ui/src/main/webapp/app/scripts/controllers/table-page-controller.js
+++ b/tez-ui/src/main/webapp/app/scripts/controllers/table-page-controller.js
@@ -28,9 +28,20 @@ App.TablePageController = Em.ObjectController.extend(
       rowCount: 25,
 
       searchText: '',
+      rowsDisplayed: [],
 
       isRefreshable: true,
 
+      // -- TODO: TEZ-2785 : Following 3 must be moved to a parent class
+      isActive: false,
+
+      setup: function () {
+        this.set('isActive', true);
+      },
+      reset: function () {
+        this.set('isActive', false);
+      },
+
       statusMessage: function () {
         return this.get('loading') ? "Loading all records..." : null;
       }.property('loading'),
@@ -38,6 +49,9 @@ App.TablePageController = Em.ObjectController.extend(
       actions: {
         refresh: function () {
           this.loadData(true);
+        },
+        tableRowsChanged: function (rows) {
+          this.set('rowsDisplayed', rows);
         }
       }
     }

http://git-wip-us.apache.org/repos/asf/tez/blob/e3f10d2e/tez-ui/src/main/webapp/app/scripts/helpers/entity-array-pollster.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/helpers/entity-array-pollster.js b/tez-ui/src/main/webapp/app/scripts/helpers/entity-array-pollster.js
new file mode 100644
index 0000000..040ec8f
--- /dev/null
+++ b/tez-ui/src/main/webapp/app/scripts/helpers/entity-array-pollster.js
@@ -0,0 +1,103 @@
+/**
+ * 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.
+ */
+
+App.Helpers.EntityArrayPollster = App.Helpers.Pollster.extend({
+  entityType: null, // Entity type to be polled
+  store: null,
+  mergeProperties: [],
+  options: null,
+
+  isRunning: false,
+  isWaiting: false,
+
+  polledRecords: null,
+  targetRecords: [],
+
+  _ready: function () {
+    return this.get('entityType') && this.get('store') && this.get('options');
+  }.property('entityType', 'store', 'options'),
+
+  start: function(interval) {
+    if(!this.get('isRunning')) {
+      if (!!interval && interval > 1000) {
+        this.set('_interval', interval)
+      }
+
+      this.set('isRunning', true);
+
+      this.onPoll();
+      this.set('timer', this.schedule(this.onPoll, true));
+    }
+  },
+
+  stop: function() {
+    if(this.get('isRunning')) {
+      Ember.run.cancel(this.get('timer'));
+      this.set('isRunning', false);
+    }
+  },
+
+  onPoll: function(){
+    if(!this.get('isWaiting') && this.get('_ready')) {
+      this.set('isWaiting', true);
+
+      return this.store.findQuery(this.get('entityType'), {
+        metadata: this.get('options')
+      }).then(this._callIfRunning(this, 'onResponse')).
+      catch(this._callIfRunning(this, 'onFailure')).
+      finally(this._final.bind(this));
+    }
+  },
+
+  _callIfRunning: function (that, funName) {
+    return function (data) {
+      var fun = that.get(funName);
+      if(fun && that.get('isRunning')) {
+        fun.call(that, data);
+      }
+    };
+  },
+
+  onResponse: function (data) {
+    this.set('polledRecords', data);
+    this.mergeToTarget();
+  },
+
+  onFailure: function (err) {
+    // Implement based on requirement
+  },
+
+  _final: function () {
+    this.set('isWaiting', false);
+  },
+
+  mergeToTarget: function () {
+    var polledRecords = this.get('polledRecords'),
+        targetRecords = this.get('targetRecords'),
+        mergeProperties = this.get('mergeProperties') || [];
+
+    if(polledRecords && targetRecords) {
+      targetRecords.forEach(function (row) {
+        var info = polledRecords.findBy('id', row.get('id'));
+        if(info) {
+          row.setProperties(info.getProperties.apply(info, mergeProperties));
+        }
+      });
+    }
+  }.observes('targetRecords').on('init')
+});

http://git-wip-us.apache.org/repos/asf/tez/blob/e3f10d2e/tez-ui/src/main/webapp/app/scripts/helpers/misc.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/helpers/misc.js b/tez-ui/src/main/webapp/app/scripts/helpers/misc.js
index 639c9e8..223b686 100644
--- a/tez-ui/src/main/webapp/app/scripts/helpers/misc.js
+++ b/tez-ui/src/main/webapp/app/scripts/helpers/misc.js
@@ -360,6 +360,15 @@ App.Helpers.misc = {
     return dagId.split('_').splice(-1).pop();
   },
 
+  /*
+   * Return index for the given id
+   * @param id {string}
+   * @return index {Number}
+   */
+  getIndexFromId: function (id) {
+    return parseInt(id.split('_').splice(-1).pop());
+  },
+
   /**
    * Remove the specific record from store
    * @param store {DS.Store}

http://git-wip-us.apache.org/repos/asf/tez/blob/e3f10d2e/tez-ui/src/main/webapp/app/scripts/helpers/pollster.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/helpers/pollster.js b/tez-ui/src/main/webapp/app/scripts/helpers/pollster.js
index 5728b81..af8dd70 100644
--- a/tez-ui/src/main/webapp/app/scripts/helpers/pollster.js
+++ b/tez-ui/src/main/webapp/app/scripts/helpers/pollster.js
@@ -15,7 +15,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-App.Helpers.pollster = Ember.Object.extend({
+App.Helpers.Pollster = Ember.Object.extend({
   interval: function() {
     return this.get('_interval') || 10000; // Time between polls (in ms)
   }.property().readOnly(),

http://git-wip-us.apache.org/repos/asf/tez/blob/e3f10d2e/tez-ui/src/main/webapp/app/scripts/models/TimelineRestAdapter.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/models/TimelineRestAdapter.js b/tez-ui/src/main/webapp/app/scripts/models/TimelineRestAdapter.js
index a21fd67..051a984 100644
--- a/tez-ui/src/main/webapp/app/scripts/models/TimelineRestAdapter.js
+++ b/tez-ui/src/main/webapp/app/scripts/models/TimelineRestAdapter.js
@@ -192,6 +192,11 @@ var timelineJsonToTaskMap = {
   vertexID: 'primaryfilters.TEZ_VERTEX_ID.0',
   endTime: 'otherinfo.endTime',
   status: 'otherinfo.status',
+  progress: {
+    custom: function(source) {
+      return Em.get(source, 'otherinfo.status') == 'SUCCEEDED' ? 1 : null;
+    }
+  },
   numFailedTaskAttempts: 'otherinfo.numFailedTaskAttempts',
   diagnostics: 'otherinfo.diagnostics',
   counterGroups: 'otherinfo.counters.counterGroups',
@@ -543,3 +548,11 @@ App.VertexInfoSerializer = DS.RESTSerializer.extend({
     }
   }
 });
+
+App.TaskInfoSerializer = DS.RESTSerializer.extend({
+  normalizePayload: function(rawPayload) {
+    return {
+      taskInfo : rawPayload.tasks
+    }
+  }
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tez/blob/e3f10d2e/tez-ui/src/main/webapp/app/scripts/models/dag.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/models/dag.js b/tez-ui/src/main/webapp/app/scripts/models/dag.js
index 011c181..1cbee7c 100644
--- a/tez-ui/src/main/webapp/app/scripts/models/dag.js
+++ b/tez-ui/src/main/webapp/app/scripts/models/dag.js
@@ -337,7 +337,7 @@ App.TezApp = App.AbstractEntity.extend({
 
 
 App.Task = App.AbstractEntity.extend({
-  status: DS.attr('status'),
+  status: DS.attr('string'),
 
   index: function () {
     var id = this.get('id'),
@@ -347,6 +347,8 @@ App.Task = App.AbstractEntity.extend({
 
   dagID: DS.attr('string'),
 
+  progress: DS.attr('number'),
+
   successfulAttemptId: DS.attr('string'),
 
   attempts: DS.attr('array'),
@@ -426,6 +428,11 @@ App.VertexInfo = DS.Model.extend({
   }.property('totalTasks', 'runningTasks', 'succeededTasks')
 });
 
+App.TaskInfo = DS.Model.extend({
+  progress: DS.attr('number'),
+  status: DS.attr('string'),
+});
+
 App.KVDatum = DS.Model.extend({
   key: DS.attr('string'),
   value: DS.attr('string'),

http://git-wip-us.apache.org/repos/asf/tez/blob/e3f10d2e/tez-ui/src/main/webapp/app/scripts/router.js
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/scripts/router.js b/tez-ui/src/main/webapp/app/scripts/router.js
index c5c4687..77fb6ef 100644
--- a/tez-ui/src/main/webapp/app/scripts/router.js
+++ b/tez-ui/src/main/webapp/app/scripts/router.js
@@ -106,6 +106,10 @@ function setupControllerFactory(format) {
     }
 
     this._super(controller, model);
+    if(controller.setup) {
+      controller.setup();
+    }
+
     if(controller.loadData) {
       controller.loadData();
     }
@@ -274,13 +278,17 @@ App.TezAppConfigsRoute = Em.Route.extend({
 });
 
 /* --- Shared routes --- */
-
 App.DagTasksRoute =
     App.DagVerticesRoute =
     App.DagTaskAttemptsRoute =
     App.VertexTasksRoute =
     App.VertexTaskAttemptsRoute =
     Em.Route.extend({
+      resetController: function () {
+        if(this.controller.reset) {
+          this.controller.reset();
+        }
+      },
       renderTemplate: renderTable,
       setupController: setupControllerFactory()
     });

http://git-wip-us.apache.org/repos/asf/tez/blob/e3f10d2e/tez-ui/src/main/webapp/app/templates/common/table.hbs
----------------------------------------------------------------------
diff --git a/tez-ui/src/main/webapp/app/templates/common/table.hbs b/tez-ui/src/main/webapp/app/templates/common/table.hbs
index 462089d..0b1ab8d 100644
--- a/tez-ui/src/main/webapp/app/templates/common/table.hbs
+++ b/tez-ui/src/main/webapp/app/templates/common/table.hbs
@@ -34,6 +34,8 @@
     enablePagination=true
     enableSort=true
 
+    rowsChanged='tableRowsChanged'
+
     pageNumBinding='pageNum'
     rowCountBinding='rowCount'
 


Mime
View raw message