You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2019/06/17 14:23:13 UTC

[airavata-django-portal] branch master updated (de77232 -> d6441a8)

This is an automated email from the ASF dual-hosted git repository.

machristie pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git.


    from de77232  Adding webpack build to make API JS available to non-vuejs Django apps
     new 5e4527c  AIRAVATA-2990 Display of experiment statistics counts
     new 706d839  AIRAVATA-2990 Display experiments for selected status category
     new 0ad795d  AIRAVATA-2990 Adding simple response cache mechanism
     new 1ce5892  AIRAVATA-2990 Add UI component for picking date range for stats
     new c9375a2  AIRAVATA-2990 Past day and past week quick filter options
     new f5c6b68  AIRAVATA-2990 Username filtering
     new 9530a93  AIRAVATA-2990 Adding application and hostname filtering
     new 8461738  AIRAVATA-2990 Removing hours/minutes from range dates
     new 6d71829  AIRAVATA-2990 Tabbed layout
     new d447ed9  AIRAVATA-2990 Experiment details tab view
     new d6441a8  AIRAVATA-2990 Process level details added to ExperimentDetailsView

The 11 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 django_airavata/apps/admin/package.json            |   5 +-
 .../statistics/ExperimentDetailsView.vue           | 443 ++++++++++++++++++
 .../statistics/ExperimentStatisticsCard.vue        |  69 +++
 .../statistics/ExperimentStatisticsContainer.vue   | 511 +++++++++++++++++++++
 .../admin/static/django_airavata_admin/src/main.js |   4 +
 .../static/django_airavata_admin/src/router.js     |   6 +
 .../apps/admin/templates/admin/admin_base.html     |   5 +
 django_airavata/apps/admin/urls.py                 |   2 +
 django_airavata/apps/admin/views.py                |   6 +
 django_airavata/apps/api/serializers.py            |  11 +
 .../api/static/django_airavata_api/js/index.js     |   3 +
 .../js/models/ExperimentStatistics.js              |  43 ++
 .../django_airavata_api/js/models/ProcessModel.js  |  94 +++-
 .../django_airavata_api/js/models/ProcessState.js  |  19 +
 .../js/models/{JobStatus.js => ProcessStatus.js}   |  10 +-
 .../js/models/ProcessWorkflow.js                   |  17 +
 .../static/django_airavata_api/js/models/Task.js   |  55 +++
 .../django_airavata_api/js/models/TaskState.js     |   4 +
 .../js/models/{JobStatus.js => TaskStatus.js}      |  11 +-
 .../django_airavata_api/js/models/TaskTypes.js     |  11 +
 .../django_airavata_api/js/service_config.js       |  20 +-
 .../js/services/ServiceFactory.js                  |   8 +-
 .../static/django_airavata_api/js/utils/Cache.js   |  53 +++
 .../django_airavata_api/js/utils/FetchUtils.js     |  33 +-
 django_airavata/apps/api/urls.py                   |   6 +-
 django_airavata/apps/api/view_utils.py             |  16 +
 django_airavata/apps/api/views.py                  |  32 +-
 .../experiment/ExperimentStatusBadge.vue           |  33 --
 .../js/components/experiment/ExperimentSummary.vue |  21 +-
 .../experiment/input-editors/FileInputEditor.vue   |   3 +-
 .../output-displays/DownloadOutputDisplay.vue      |   5 +-
 .../output-displays/OutputDisplayContainer.vue     |   3 +-
 .../js/containers/ExperimentListContainer.vue      |   4 +-
 .../js/containers/RecentExperimentsContainer.vue   |   3 +-
 .../common/js/components/ApplicationName.vue       |  40 ++
 .../common/js/components/ComputeResourceName.vue   |  48 ++
 .../common/js/components}/DataProductViewer.vue    |   0
 .../common/js/components/ExperimentStatusBadge.vue |  38 ++
 django_airavata/static/common/js/index.js          |  18 +-
 39 files changed, 1618 insertions(+), 95 deletions(-)
 create mode 100644 django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentDetailsView.vue
 create mode 100644 django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsCard.vue
 create mode 100644 django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
 create mode 100644 django_airavata/apps/api/static/django_airavata_api/js/models/ExperimentStatistics.js
 create mode 100644 django_airavata/apps/api/static/django_airavata_api/js/models/ProcessState.js
 copy django_airavata/apps/api/static/django_airavata_api/js/models/{JobStatus.js => ProcessStatus.js} (55%)
 create mode 100644 django_airavata/apps/api/static/django_airavata_api/js/models/ProcessWorkflow.js
 create mode 100644 django_airavata/apps/api/static/django_airavata_api/js/models/Task.js
 create mode 100644 django_airavata/apps/api/static/django_airavata_api/js/models/TaskState.js
 copy django_airavata/apps/api/static/django_airavata_api/js/models/{JobStatus.js => TaskStatus.js} (56%)
 create mode 100644 django_airavata/apps/api/static/django_airavata_api/js/models/TaskTypes.js
 create mode 100644 django_airavata/apps/api/static/django_airavata_api/js/utils/Cache.js
 delete mode 100644 django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentStatusBadge.vue
 create mode 100644 django_airavata/static/common/js/components/ApplicationName.vue
 create mode 100644 django_airavata/static/common/js/components/ComputeResourceName.vue
 rename django_airavata/{apps/workspace/static/django_airavata_workspace/js/components/experiment => static/common/js/components}/DataProductViewer.vue (100%)
 create mode 100644 django_airavata/static/common/js/components/ExperimentStatusBadge.vue


[airavata-django-portal] 11/11: AIRAVATA-2990 Process level details added to ExperimentDetailsView

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit d6441a853a04c2975a6fe8e5f55a37e956cd9aed
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Fri Jun 14 12:47:31 2019 -0400

    AIRAVATA-2990 Process level details added to ExperimentDetailsView
---
 .../statistics/ExperimentDetailsView.vue           | 193 ++++++++++++++++++++-
 .../api/static/django_airavata_api/js/index.js     |   2 +
 .../django_airavata_api/js/models/ProcessModel.js  |  94 +++++++++-
 .../django_airavata_api/js/models/ProcessState.js  |  19 ++
 .../django_airavata_api/js/models/ProcessStatus.js |  21 +++
 .../js/models/ProcessWorkflow.js                   |  17 ++
 .../static/django_airavata_api/js/models/Task.js   |  55 ++++++
 .../django_airavata_api/js/models/TaskState.js     |   4 +
 .../django_airavata_api/js/models/TaskStatus.js    |  22 +++
 .../django_airavata_api/js/models/TaskTypes.js     |  11 ++
 .../js/components/experiment/ExperimentSummary.vue |  18 +-
 .../output-displays/DownloadOutputDisplay.vue      |   2 +-
 12 files changed, 437 insertions(+), 21 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentDetailsView.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentDetailsView.vue
index 6cf4ad6..471555a 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentDetailsView.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentDetailsView.vue
@@ -73,6 +73,7 @@
           <th>Status</th>
           <th>Creation Time</th>
           </thead>
+      <tbody>
         <tr
           v-for="(jobDetail, index) in fullExperiment.jobDetails"
           :key="jobDetail.jobId"
@@ -84,6 +85,7 @@
             <span :title="jobDetail.creationTime.toString()">{{ jobCreationTimes[index] }}</span>
           </td>
         </tr>
+      </tbody>
     </table>
     </td>
     </tr>
@@ -104,6 +106,18 @@
         </div>
       </td>
     </tr>
+    <tr v-if="fullExperiment.jobDetails && fullExperiment.jobDetails.length > 0">
+      <th scope="row">Job Description</th>
+      <td>
+        <b-card
+          v-for="jobDetail in fullExperiment.jobDetails"
+          :key="jobDetail.jobId"
+          :header="jobDetail.jobName"
+        >
+          <pre>{{ jobDetail.jobDescription }}</pre>
+        </b-card>
+      </td>
+    </tr>
     <tr>
       <th scope="row">Creation Time</th>
       <td>
@@ -149,7 +163,6 @@
               v-else-if="input.type.isFileValueType"
               :data-product="dp"
               :input-file="true"
-              class="data-product"
               :key="dp.productUri"
             />
           </li>
@@ -157,12 +170,150 @@
       </td>
     </tr>
     <tr>
-      <!-- TODO -->
+      <th scope="row">Outputs</th>
+      <td>
+        <ul>
+          <li
+            v-for="output in experiment.experimentOutputs"
+            :key="output.name"
+          >
+            {{ output.name }}:
+            <template v-if="output.type.isSimpleValueType">
+              <span class="text-break">{{ output.value }}</span>
+            </template>
+            <data-product-viewer
+              v-for="dp in outputDataProducts[output.name]"
+              v-else-if="output.type.isFileValueType"
+              :data-product="dp"
+              :key="dp.productUri"
+            />
+          </li>
+        </ul>
+      </td>
+    </tr>
+    <tr v-if="storageDirLink">
+      <th scope="row">Storage Directory</th>
+      <td>
+        <b-link :href="storageDirLink">Open</b-link>
+      </td>
+    </tr>
+    <tr>
       <th scope="row">Errors</th>
-      <td></td>
+      <td>
+        <b-card
+          v-for="error in experiment.errors"
+          :key="error.errorId"
+          header="Error"
+        >
+          <p>{{error.userFriendlyMessage}}</p>
+          <pre class="pre-scrollable">{{error.actualErrorMessage}}</pre>
+        </b-card>
+      </td>
     </tr>
+    <template v-if="failedJobs.length > 0">
+
+      <tr
+        v-for="job in failedJobs"
+        :key="job.jobId"
+      >
+        <th scope="row">Job Submission Response</th>
+        <td>
+          <b-card
+            v-if="job.stdOut"
+            :header="job.jobName + ' STDOUT'"
+          >
+            <pre class="pre-scrollable">{{job.stdOut}}</pre>
+          </b-card>
+          <b-card
+            v-if="job.stdErr"
+            :header="job.jobName + ' STDERR'"
+          >
+            <pre class="pre-scrollable">{{job.stdErr}}</pre>
+          </b-card>
+        </td>
+      </tr>
+    </template>
     </tbody>
     </table>
+    <h2 class="h5 mb-3">Process Details</h2>
+    <b-card
+      v-for="process in experiment.processes"
+      :key="process.processId"
+      :header="process.processId"
+    >
+      <b-card
+        v-for="task in process.sortedTasks"
+        :key="task.taskId"
+        :header="task.taskId"
+      >
+        <table class="table table-sm">
+          <tbody>
+            <tr>
+              <th scope="row">Task Id</th>
+              <td>{{task.taskId}}</td>
+            </tr>
+            <tr>
+              <th scope="row">Task Type</th>
+              <td>{{task.taskType.name}}</td>
+            </tr>
+            <tr>
+              <th scope="row">Task Status</th>
+              <td>{{task.latestStatus.state.name}}</td>
+            </tr>
+            <tr>
+              <th scope="row">Task Status Time</th>
+              <td>
+                <human-date :date="task.latestStatus.timeOfStateChange" />
+              </td>
+            </tr>
+            <tr>
+              <th scope="row">Task Status Reason</th>
+              <td>{{ task.latestStatus.reason }}</td>
+            </tr>
+            <template v-if="task.taskErrors && task.taskErrors.length > 0">
+
+              <tr>
+                <th scope="row">Task Errors</th>
+                <td>
+                  <b-card
+                    v-for="error in task.taskErrors"
+                    :key="error.errorId"
+                    :header="error.errorId"
+                  >
+                    <p>{{error.userFriendlyMessage}}</p>
+                    <pre class="pre-scrollable">{{error.actualErrorMessage}}</pre>
+                  </b-card>
+                </td>
+              </tr>
+            </template>
+            <template v-if="task.jobs && task.jobs.length > 0">
+              <tr>
+                <th scope="row">Jobs</th>
+                <td>
+                  <b-card
+                    v-for="job in task.jobs"
+                    :key="job.jobId"
+                    :header="job.jobName"
+                  >
+                    <pre>{{ job.jobDescription }}</pre>
+                  </b-card>
+                </td>
+              </tr>
+            </template>
+          </tbody>
+        </table>
+      </b-card>
+
+      <b-card
+        v-for="error in process.processErrors"
+        :key="error.errorId"
+        :header="'Process Error ' + error.errorId"
+      >
+        <p>{{error.userFriendlyMessage}}</p>
+        <pre class="pre-scrollable">{{error.actualErrorMessage}}</pre>
+      </b-card>
+    </b-card>
+
   </div>
 </template>
 
@@ -182,7 +333,8 @@ export default {
   },
   components: {
     "clipboard-copy-link": components.ClipboardCopyLink,
-    "data-product-viewer": components.DataProductViewer
+    "data-product-viewer": components.DataProductViewer,
+    "human-date": components.HumanDate
   },
   data() {
     return {
@@ -226,6 +378,25 @@ export default {
       return this.fullExperiment.jobDetails.map(jobDetail =>
         moment(jobDetail.creationTime).fromNow()
       );
+    },
+    storageDirLink() {
+      if (this.experiment.relativeExperimentDataDir) {
+        return this.storageDirectory(this.experiment.relativeExperimentDataDir);
+      } else {
+        return null;
+      }
+    },
+    failedJobs() {
+      if (this.fullExperiment && this.fullExperiment.jobDetails) {
+        return this.fullExperiment.jobDetails.filter(
+          job =>
+            this.experiment.latestStatus.state ===
+              models.ExperimentState.FAILED ||
+            job.latestJobStatus.jobState === models.JobState.FAILED
+        );
+      } else {
+        return [];
+      }
     }
   },
   created() {
@@ -251,8 +422,22 @@ export default {
         );
       }
       return dataProducts ? dataProducts.filter(dp => (dp ? true : false)) : [];
+    },
+    storageDirectory(relativePath) {
+      if (relativePath.startsWith("/")) {
+        relativePath = relativePath.substring(1);
+      }
+      return "/workspace/storage/~/" + relativePath;
     }
   }
 };
 </script>
 
+<style scoped>
+.table {
+  table-layout: fixed;
+}
+.table th[scope="row"] {
+  width: 20%;
+}
+</style>
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/index.js b/django_airavata/apps/api/static/django_airavata_api/js/index.js
index 1729443..a43fecf 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/index.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/index.js
@@ -22,6 +22,7 @@ import GroupComputeResourcePreference from "./models/GroupComputeResourcePrefere
 import GroupPermission from "./models/GroupPermission";
 import GroupResourceProfile from "./models/GroupResourceProfile";
 import InputDataObjectType from "./models/InputDataObjectType";
+import JobState from "./models/JobState";
 import ManagedUserProfile from "./models/ManagedUserProfile";
 import OutputDataObjectType from "./models/OutputDataObjectType";
 import ParallelismType from "./models/ParallelismType";
@@ -77,6 +78,7 @@ const models = {
   GroupPermission,
   GroupResourceProfile,
   InputDataObjectType,
+  JobState,
   ManagedUserProfile,
   OutputDataObjectType,
   ParallelismType,
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessModel.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessModel.js
index ee37636..fd962a3 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessModel.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessModel.js
@@ -1,13 +1,93 @@
-import BaseModel from './BaseModel';
+import BaseModel from "./BaseModel";
+import ProcessStatus from "./ProcessStatus";
+import InputDataObjectType from "./InputDataObjectType";
+import OutputDataObjectType from "./OutputDataObjectType";
+import ComputationalResourceSchedulingModel from "./ComputationalResourceSchedulingModel";
+import Task from "./Task";
+import ErrorModel from "./ErrorModel";
+import ProcessWorkflow from "./ProcessWorkflow";
 
 const FIELDS = [
-    'processId',
-    'experimentId',
-    // TODO: finish mapping fields
+  "processId",
+  "experimentId",
+  {
+    name: "creationTime",
+    type: Date
+  },
+  {
+    name: "lastUpdateTime",
+    type: Date
+  },
+  {
+    name: "processStatuses",
+    type: ProcessStatus,
+    list: true
+  },
+  "processDetail",
+  "applicationInterfaceId",
+  "applicationDeploymentId",
+  "computeResourceId",
+  {
+    name: "processInputs",
+    type: InputDataObjectType,
+    list: true
+  },
+  {
+    name: "processOutputs",
+    type: OutputDataObjectType,
+    list: true
+  },
+  {
+    name: "processResourceSchedule",
+    type: ComputationalResourceSchedulingModel
+  },
+  {
+    name: "tasks",
+    type: Task,
+    list: true
+  },
+  "taskDag",
+  {
+    name: "processErrors",
+    type: ErrorModel,
+    list: true
+  },
+  "gatewayExecutionId",
+  "enableEmailNotification",
+  "emailAddresses",
+  "storageResourceId",
+  "userDn",
+  "generateCert",
+  "experimentDataDir",
+  "userName",
+  "useUserCRPref",
+  "groupResourceProfileId",
+  {
+    name: "processWorkflows",
+    type: ProcessWorkflow,
+    list: true
+  }
 ];
 
 export default class ProcessModel extends BaseModel {
-    constructor(data = {}) {
-        super(FIELDS, data);
-    }
+  constructor(data = {}) {
+    super(FIELDS, data);
+  }
+
+  /**
+   * Return tasks sorted by task DAG order.
+   */
+  get sortedTasks() {
+    const tasksArrCopy = this.tasks.slice();
+    tasksArrCopy.sort((a, b) => {
+      const aIndex = this.taskDagArray.findIndex(t => t === a.taskId);
+      const bIndex = this.taskDagArray.findIndex(t => t === b.taskId);
+      return aIndex - bIndex;
+    });
+    return tasksArrCopy;
+  }
+
+  get taskDagArray() {
+    return this.taskDag ? this.taskDag.split(",") : [];
+  }
 }
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessState.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessState.js
new file mode 100644
index 0000000..7f59e72
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessState.js
@@ -0,0 +1,19 @@
+import BaseEnum from "./BaseEnum";
+
+export default class ProcessState extends BaseEnum {}
+ProcessState.init([
+  "CREATED",
+  "VALIDATED",
+  "STARTED",
+  "PRE_PROCESSING",
+  "CONFIGURING_WORKSPACE",
+  "INPUT_DATA_STAGING",
+  "EXECUTING",
+  "MONITORING",
+  "OUTPUT_DATA_STAGING",
+  "POST_PROCESSING",
+  "COMPLETED",
+  "FAILED",
+  "CANCELLING",
+  "CANCELED"
+]);
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessStatus.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessStatus.js
new file mode 100644
index 0000000..767b170
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessStatus.js
@@ -0,0 +1,21 @@
+import BaseModel from "./BaseModel";
+import ProcessState from "./ProcessState";
+
+const FIELDS = [
+  {
+    name: "state",
+    type: ProcessState
+  },
+  {
+    name: "timeOfStateChange",
+    type: Date
+  },
+  "reason",
+  "statusId"
+];
+
+export default class ProcessStatus extends BaseModel {
+  constructor(data = {}) {
+    super(FIELDS, data);
+  }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessWorkflow.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessWorkflow.js
new file mode 100644
index 0000000..4ccd59e
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ProcessWorkflow.js
@@ -0,0 +1,17 @@
+import BaseModel from "./BaseModel";
+
+const FIELDS = [
+  "processId",
+  "workflowId",
+  {
+    name: "creationTime",
+    type: Date
+  },
+  "type"
+];
+
+export default class ProcessWorkflow extends BaseModel {
+  constructor(data = {}) {
+    super(FIELDS, data);
+  }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/Task.js b/django_airavata/apps/api/static/django_airavata_api/js/models/Task.js
new file mode 100644
index 0000000..7c7acd4
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/Task.js
@@ -0,0 +1,55 @@
+import BaseModel from "./BaseModel";
+import ErrorModel from "./ErrorModel";
+import Job from "./Job";
+import TaskTypes from "./TaskTypes";
+import TaskStatus from "./TaskStatus";
+
+const FIELDS = [
+  "taskId",
+  {
+    name: "taskType",
+    type: TaskTypes
+  },
+  "parentProcessId",
+  {
+    name: "creationTime",
+    type: Date
+  },
+  {
+    name: "lastUpdateTime",
+    type: Date
+  },
+  {
+    name: "taskStatuses",
+    type: TaskStatus,
+    list: true
+  },
+  "taskDetail",
+  "subTaskModel",
+  {
+    name: "taskErrors",
+    type: ErrorModel,
+    list: true
+  },
+  {
+    name: "jobs",
+    type: Job,
+    list: true
+  },
+  "maxRetry",
+  "currentRetry"
+];
+
+export default class Task extends BaseModel {
+  constructor(data = {}) {
+    super(FIELDS, data);
+  }
+
+  get latestStatus() {
+    if (this.taskStatuses && this.taskStatuses.length > 0) {
+      return this.taskStatuses[this.taskStatuses.length - 1];
+    } else {
+      return null;
+    }
+  }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/TaskState.js b/django_airavata/apps/api/static/django_airavata_api/js/models/TaskState.js
new file mode 100644
index 0000000..9c860fe
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/TaskState.js
@@ -0,0 +1,4 @@
+import BaseEnum from "./BaseEnum";
+
+export default class TaskState extends BaseEnum {}
+TaskState.init(["CREATED", "EXECUTING", "COMPLETED", "FAILED", "CANCELED"]);
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/TaskStatus.js b/django_airavata/apps/api/static/django_airavata_api/js/models/TaskStatus.js
new file mode 100644
index 0000000..bbcc5ef
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/TaskStatus.js
@@ -0,0 +1,22 @@
+
+import BaseModel from "./BaseModel";
+import TaskState from "./TaskState";
+
+const FIELDS = [
+  {
+    name: "state",
+    type: TaskState,
+  },
+  {
+    name: "timeOfStateChange",
+    type: Date
+  },
+  "reason",
+  "statusId"
+];
+
+export default class TaskStatus extends BaseModel {
+  constructor(data = {}) {
+    super(FIELDS, data);
+  }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/TaskTypes.js b/django_airavata/apps/api/static/django_airavata_api/js/models/TaskTypes.js
new file mode 100644
index 0000000..0a3918a
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/TaskTypes.js
@@ -0,0 +1,11 @@
+import BaseEnum from "./BaseEnum";
+
+export default class TaskTypes extends BaseEnum {}
+TaskTypes.init([
+  "ENV_SETUP",
+  "DATA_STAGING",
+  "JOB_SUBMISSION",
+  "ENV_CLEANUP",
+  "MONITORING",
+  "OUTPUT_FETCHING"
+]);
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue
index 97b1336..9ba38e8 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue
@@ -198,7 +198,6 @@
                       v-else-if="input.type.isFileValueType"
                       :data-product="dp"
                       :input-file="true"
-                      class="data-product"
                       :key="dp.productUri"
                     />
                   </li>
@@ -206,9 +205,16 @@
               </td>
             </tr>
             <tr>
-              <!-- TODO -->
               <th scope="row">Errors</th>
-              <td></td>
+              <td>
+                <b-card
+                  v-for="error in experiment.errors"
+                  :key="error.errorId"
+                  header="Error"
+                >
+                  <p>{{error.userFriendlyMessage}}</p>
+                </b-card>
+              </td>
             </tr>
             </tbody>
             </table>
@@ -376,9 +382,3 @@ export default {
   }
 };
 </script>
-
-<style scoped>
-.data-product + .data-product {
-  margin-left: 0.5em;
-}
-</style>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/DownloadOutputDisplay.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/DownloadOutputDisplay.vue
index 603b35a..0234033 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/DownloadOutputDisplay.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/DownloadOutputDisplay.vue
@@ -1,7 +1,7 @@
 <template>
   <div>
     <data-product-viewer v-for="dp in dataProducts"
-      :data-product="dp" class="data-product" :key="dp.productUri"/>
+      :data-product="dp" :key="dp.productUri"/>
   </div>
 </template>
 


[airavata-django-portal] 10/11: AIRAVATA-2990 Experiment details tab view

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit d447ed96710d80838f19cafb2113861d268baa17
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Jun 13 09:56:47 2019 -0400

    AIRAVATA-2990 Experiment details tab view
---
 .../statistics/ExperimentDetailsView.vue           | 258 +++++++++++++++++++++
 .../statistics/ExperimentStatisticsContainer.vue   |  35 ++-
 .../js/components/experiment/ExperimentSummary.vue |   3 +-
 .../experiment/input-editors/FileInputEditor.vue   |   3 +-
 .../output-displays/DownloadOutputDisplay.vue      |   3 +-
 .../output-displays/OutputDisplayContainer.vue     |   3 +-
 .../common/js/components}/DataProductViewer.vue    |   0
 django_airavata/static/common/js/index.js          |   2 +
 8 files changed, 296 insertions(+), 11 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentDetailsView.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentDetailsView.vue
new file mode 100644
index 0000000..6cf4ad6
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentDetailsView.vue
@@ -0,0 +1,258 @@
+<template>
+  <div>
+    <table
+      class="table"
+      v-if="fullExperiment"
+    >
+      <tbody>
+        <tr>
+          <th scope="row">Name</th>
+          <td>
+            <div :title="experiment.experimentId">{{ experiment.experimentName }}</div>
+            <small class="text-muted">
+              ID: {{ experiment.experimentId }}
+              (<clipboard-copy-link
+                :text="experiment.experimentId"
+                :link-classes="['text-reset']"
+              >
+                copy
+                <span slot="icon"></span>
+                <span slot="tooltip">Copied ID!</span>
+              </clipboard-copy-link>)
+            </small>
+          </td>
+        </tr>
+        <tr>
+          <th scope="row">Description</th>
+          <td>{{ experiment.description }}</td>
+        </tr>
+        <tr>
+          <th scope="row">Project</th>
+          <td v-if="fullExperiment.project">{{ fullExperiment.projectName }}</td>
+          <td v-else>
+            <em>You don't have access to this project.</em>
+          </td>
+        </tr>
+        <tr>
+          <th scope="row">Owner</th>
+          <td>{{ experiment.userName }}</td>
+        </tr>
+        <tr>
+          <th scope="row">Application</th>
+          <td v-if="fullExperiment.applicationName">{{ fullExperiment.applicationName }}</td>
+          <td
+            v-else
+            class="font-italic text-muted"
+          >Unable to load interface {{ fullExperiment.experiment.executionId }}</td>
+        </tr>
+        <tr>
+          <th scope="row">Compute Resource</th>
+          <td v-if="fullExperiment.computeHostName">{{ fullExperiment.computeHostName }}</td>
+          <td
+            v-else
+            class="font-italic text-muted"
+          >Unable to load compute resource {{ fullExperiment.resourceHostId }}</td>
+        </tr>
+        <tr>
+          <th scope="row">Experiment Status</th>
+          <td>
+            <template v-if="fullExperiment.experiment.isProgressing">
+              <i class="fa fa-sync-alt fa-spin"></i>
+              <span class="sr-only">Progressing...</span>
+            </template>
+            {{ fullExperiment.experimentStatusName }}
+          </td>
+        </tr>
+        <tr v-if="fullExperiment.jobDetails && fullExperiment.jobDetails.length > 0">
+          <th scope="row">Job</th>
+          <td>
+            <table class="table">
+              <thead>
+          <th>Name</th>
+          <th>ID</th>
+          <th>Status</th>
+          <th>Creation Time</th>
+          </thead>
+        <tr
+          v-for="(jobDetail, index) in fullExperiment.jobDetails"
+          :key="jobDetail.jobId"
+        >
+          <td>{{ jobDetail.jobName }}</td>
+          <td>{{ jobDetail.jobId }}</td>
+          <td>{{ jobDetail.jobStatusStateName }}</td>
+          <td>
+            <span :title="jobDetail.creationTime.toString()">{{ jobCreationTimes[index] }}</span>
+          </td>
+        </tr>
+    </table>
+    </td>
+    </tr>
+    <tr>
+      <th scope="row">Notification List</th>
+      <td>{{ experiment.emailAddresses
+        ? experiment.emailAddresses.join(", ")
+        : '' }}</td>
+    </tr>
+    <tr v-if="fullExperiment.jobDetails && fullExperiment.jobDetails.length > 0">
+      <th scope="row">Working Dir</th>
+      <td>
+        <div
+          v-for="jobDetail in fullExperiment.jobDetails"
+          :key="jobDetail.jobId"
+        >
+          {{ jobDetail.jobName }}: {{ jobDetail.workingDir }}
+        </div>
+      </td>
+    </tr>
+    <tr>
+      <th scope="row">Creation Time</th>
+      <td>
+        <span :title="experiment.creationTime.toString()">{{ creationTime }}</span>
+      </td>
+    </tr>
+    <tr>
+      <th scope="row">Last Modified Time</th>
+      <td>
+        <span :title="fullExperiment.experimentStatus.timeOfStateChange.toString()">{{ lastModifiedTime }}</span>
+      </td>
+    </tr>
+    <tr>
+      <th scope="row">Wall Time Limit</th>
+      <td>{{ experiment.userConfigurationData.computationalResourceScheduling.wallTimeLimit }} minutes</td>
+    </tr>
+    <tr>
+      <th scope="row">CPU Count</th>
+      <td>{{ experiment.userConfigurationData.computationalResourceScheduling.totalCPUCount }}</td>
+    </tr>
+    <tr>
+      <th scope="row">Node Count</th>
+      <td>{{ experiment.userConfigurationData.computationalResourceScheduling.nodeCount }}</td>
+    </tr>
+    <tr>
+      <th scope="row">Queue</th>
+      <td>{{ experiment.userConfigurationData.computationalResourceScheduling.queueName }}</td>
+    </tr>
+    <tr>
+      <th scope="row">Inputs</th>
+      <td>
+        <ul>
+          <li
+            v-for="input in experiment.experimentInputs"
+            :key="input.name"
+          >
+            {{ input.name }}:
+            <template v-if="input.type.isSimpleValueType">
+              <span class="text-break">{{ input.value }}</span>
+            </template>
+            <data-product-viewer
+              v-for="dp in inputDataProducts[input.name]"
+              v-else-if="input.type.isFileValueType"
+              :data-product="dp"
+              :input-file="true"
+              class="data-product"
+              :key="dp.productUri"
+            />
+          </li>
+        </ul>
+      </td>
+    </tr>
+    <tr>
+      <!-- TODO -->
+      <th scope="row">Errors</th>
+      <td></td>
+    </tr>
+    </tbody>
+    </table>
+  </div>
+</template>
+
+<script>
+import { models, services } from "django-airavata-api";
+import { components } from "django-airavata-common-ui";
+
+import moment from "moment";
+
+export default {
+  name: "experiment-details-view",
+  props: {
+    experiment: {
+      type: models.Experiment,
+      required: true
+    }
+  },
+  components: {
+    "clipboard-copy-link": components.ClipboardCopyLink,
+    "data-product-viewer": components.DataProductViewer
+  },
+  data() {
+    return {
+      fullExperiment: null
+    };
+  },
+  computed: {
+    inputDataProducts() {
+      const result = {};
+      if (this.fullExperiment && this.fullExperiment.inputDataProducts) {
+        this.fullExperiment.experiment.experimentInputs.forEach(input => {
+          result[input.name] = this.getDataProducts(
+            input,
+            this.fullExperiment.inputDataProducts
+          );
+        });
+      }
+      return result;
+    },
+    outputDataProducts() {
+      const result = {};
+      if (this.fullExperiment && this.fullExperiment.outputDataProducts) {
+        this.fullExperiment.experiment.experimentOutputs.forEach(output => {
+          result[output.name] = this.getDataProducts(
+            output,
+            this.fullExperiment.outputDataProducts
+          );
+        });
+      }
+      return result;
+    },
+    creationTime: function() {
+      return moment(this.fullExperiment.experiment.creationTime).fromNow();
+    },
+    lastModifiedTime: function() {
+      return moment(
+        this.fullExperiment.experimentStatus.timeOfStateChange
+      ).fromNow();
+    },
+    jobCreationTimes: function() {
+      return this.fullExperiment.jobDetails.map(jobDetail =>
+        moment(jobDetail.creationTime).fromNow()
+      );
+    }
+  },
+  created() {
+    services.FullExperimentService.retrieve({
+      lookup: this.experiment.experimentId
+    }).then(fullExperiment => (this.fullExperiment = fullExperiment));
+  },
+  methods: {
+    getDataProducts(io, collection) {
+      if (!io.value || !collection) {
+        return [];
+      }
+      let dataProducts = null;
+      if (io.type === models.DataType.URI_COLLECTION) {
+        const dataProductURIs = io.value.split(",");
+        dataProducts = dataProductURIs.map(uri =>
+          collection.find(dp => dp.productUri === uri)
+        );
+      } else {
+        const dataProductURI = io.value;
+        dataProducts = collection.filter(
+          dp => dp.productUri === dataProductURI
+        );
+      }
+      return dataProducts ? dataProducts.filter(dp => (dp ? true : false)) : [];
+    }
+  }
+};
+</script>
+
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
index cf784ae..1030d49 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
@@ -228,8 +228,23 @@
                 >
                   <experiment-status-badge :status-name="data.value.name" />
                 </template>
+                <template
+                  slot="actions"
+                  slot-scope="data"
+                >
+                  <b-link @click="showExperimentDetails(data.item.experimentId)">
+                    View Details
+                    <i
+                      class="far fa-chart-bar"
+                      aria-hidden="true"
+                    ></i>
+                  </b-link>
+                </template>
               </b-table>
             </b-tab>
+            <b-tab v-for="experimentDetail in experimentDetails" :key="experimentDetail.experimentId" :title="experimentDetail.experimentName">
+              <experiment-details-view :experiment="experimentDetail"/>
+            </b-tab>
           </b-tabs>
         </b-card>
       </div>
@@ -240,6 +255,7 @@
 import { models, services, utils } from "django-airavata-api";
 import { components } from "django-airavata-common-ui";
 import ExperimentStatisticsCard from "./ExperimentStatisticsCard";
+import ExperimentDetailsView from "./ExperimentDetailsView";
 
 import moment from "moment";
 
@@ -267,7 +283,8 @@ export default {
       hostnameFilterEnabled: false,
       hostnameFilter: null,
       appInterfaces: null,
-      computeResourceNames: null
+      computeResourceNames: null,
+      experimentDetails: []
     };
   },
   created() {
@@ -276,6 +293,7 @@ export default {
     this.loadComputeResources();
   },
   components: {
+    ExperimentDetailsView,
     ExperimentStatisticsCard,
     "application-name": components.ApplicationName,
     "compute-resource-name": components.ComputeResourceName,
@@ -395,9 +413,13 @@ export default {
         return "Created Experiments";
       } else if (this.selectedExperimentSummariesKey === "runningExperiments") {
         return "Running Experiments";
-      } else if (this.selectedExperimentSummariesKey === "completedExperiments") {
+      } else if (
+        this.selectedExperimentSummariesKey === "completedExperiments"
+      ) {
         return "Completed Experiments";
-      } else if (this.selectedExperimentSummariesKey === "cancelledExperiments") {
+      } else if (
+        this.selectedExperimentSummariesKey === "cancelledExperiments"
+      ) {
         return "Cancelled Experiments";
       } else if (this.selectedExperimentSummariesKey === "failedExperiments") {
         return "Failed Experiments";
@@ -476,6 +498,13 @@ export default {
       this.hostnameFilter = null;
       this.hostnameFilterEnabled = false;
       this.loadStatistics();
+    },
+    showExperimentDetails(experimentId) {
+      // TODO: if experiment details already loaded, select its tab
+      // TODO: maybe don't need to load the experiment first since ExperimentDetailsView will load FullExperiment?
+      services.ExperimentService.retrieve({
+        lookup: experimentId
+      }).then(exp => this.experimentDetails.push(exp));
     }
   }
 };
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue
index 0339172..97b1336 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue
@@ -222,7 +222,6 @@
 <script>
 import { models, services } from "django-airavata-api";
 import { components } from "django-airavata-common-ui";
-import DataProductViewer from "./DataProductViewer.vue";
 import OutputDisplayContainer from "./output-displays/OutputDisplayContainer";
 import urls from "../../utils/urls";
 
@@ -246,7 +245,7 @@ export default {
     };
   },
   components: {
-    DataProductViewer,
+    "data-product-viewer": components.DataProductViewer,
     "clipboard-copy-link": components.ClipboardCopyLink,
     "share-button": components.ShareButton,
     OutputDisplayContainer
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/FileInputEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/FileInputEditor.vue
index 2c65b9f..c1daef4 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/FileInputEditor.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/input-editors/FileInputEditor.vue
@@ -56,7 +56,6 @@
 <script>
 import { models, services, utils } from "django-airavata-api";
 import { InputEditorMixin } from "django-airavata-workspace-plugin-api";
-import DataProductViewer from "../DataProductViewer.vue";
 import { components } from "django-airavata-common-ui";
 import UserStorageFileSelectionContainer from "../../storage/UserStorageFileSelectionContainer";
 
@@ -64,7 +63,7 @@ export default {
   name: "file-input-editor",
   mixins: [InputEditorMixin],
   components: {
-    DataProductViewer,
+    "data-product-viewer": components.DataProductViewer,
     "delete-link": components.DeleteLink,
     UserStorageFileSelectionContainer
   },
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/DownloadOutputDisplay.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/DownloadOutputDisplay.vue
index 51657c5..603b35a 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/DownloadOutputDisplay.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/DownloadOutputDisplay.vue
@@ -7,7 +7,6 @@
 
 <script>
 import { models } from "django-airavata-api"
-import DataProductViewer from "../DataProductViewer.vue";
 
 export default {
   name: "download-output-viewer",
@@ -25,7 +24,7 @@ export default {
     }
   },
   components: {
-    DataProductViewer
+    "data-product-viewer": components.DataProductViewer
   }
 }
 </script>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/OutputDisplayContainer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/OutputDisplayContainer.vue
index 0b2d400..ed8bbe0 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/OutputDisplayContainer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/output-displays/OutputDisplayContainer.vue
@@ -24,7 +24,6 @@
 import { models } from "django-airavata-api";
 import DownloadOutputDisplay from "./DownloadOutputDisplay";
 import LinkDisplay from "./LinkDisplay";
-import DataProductViewer from "../DataProductViewer";
 
 export default {
   name: "output-viewer-container",
@@ -44,7 +43,7 @@ export default {
     }
   },
   components: {
-    DataProductViewer,
+    "data-product-viewer": components.DataProductViewer,
     DownloadOutputDisplay,
     LinkDisplay
   },
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/DataProductViewer.vue b/django_airavata/static/common/js/components/DataProductViewer.vue
similarity index 100%
rename from django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/DataProductViewer.vue
rename to django_airavata/static/common/js/components/DataProductViewer.vue
diff --git a/django_airavata/static/common/js/index.js b/django_airavata/static/common/js/index.js
index e7eda5d..48a040c 100644
--- a/django_airavata/static/common/js/index.js
+++ b/django_airavata/static/common/js/index.js
@@ -5,6 +5,7 @@ import ClipboardCopyButton from "./components/ClipboardCopyButton.vue";
 import ClipboardCopyLink from "./components/ClipboardCopyLink.vue";
 import ComputeResourceName from "./components/ComputeResourceName";
 import ConfirmationDialog from "./components/ConfirmationDialog.vue";
+import DataProductViewer from "./components/DataProductViewer";
 import DeleteButton from "./components/DeleteButton.vue";
 import DeleteLink from "./components/DeleteLink.vue";
 import ExperimentStatusBadge from "./components/ExperimentStatusBadge";
@@ -40,6 +41,7 @@ const components = {
   ClipboardCopyLink,
   ComputeResourceName,
   ConfirmationDialog,
+  DataProductViewer,
   DeleteButton,
   DeleteLink,
   ExperimentStatusBadge,


[airavata-django-portal] 08/11: AIRAVATA-2990 Removing hours/minutes from range dates

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 84617387690043dc64497ff49980cb30b73c0ea8
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Wed Jun 12 11:40:04 2019 -0400

    AIRAVATA-2990 Removing hours/minutes from range dates
    
    I was hitting some sort of bug when rolling over to the next
    minute only the fromTime would be updated.
---
 .../src/components/statistics/ExperimentStatisticsContainer.vue     | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
index 40dfc63..92d6057 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
@@ -256,7 +256,7 @@ export default {
       dateConfig: {
         mode: "range",
         wrap: true,
-        dateFormat: "Y-m-d H:i",
+        dateFormat: "Y-m-d",
         maxDate: new Date()
       },
       usernameFilterEnabled: false,
@@ -437,8 +437,8 @@ export default {
     },
     updateDateRange() {
       this.dateRange = [
-        moment(this.fromTime).format("YYYY-MM-DD HH:mm"),
-        moment(this.toTime).format("YYYY-MM-DD HH:mm")
+        moment(this.fromTime).format("YYYY-MM-DD"),
+        moment(this.toTime).format("YYYY-MM-DD")
       ];
     },
     daysAgo(days) {


[airavata-django-portal] 01/11: AIRAVATA-2990 Display of experiment statistics counts

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 5e4527c8a5410ac72175a9434a71418e90a8c574
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Wed Jun 5 13:01:35 2019 -0400

    AIRAVATA-2990 Display of experiment statistics counts
---
 django_airavata/apps/admin/package.json            |   4 +-
 .../statistics/ExperimentStatisticsCard.vue        |  69 ++++++++++++
 .../statistics/ExperimentStatisticsContainer.vue   | 121 +++++++++++++++++++++
 .../static/django_airavata_admin/src/router.js     |   6 +
 .../apps/admin/templates/admin/admin_base.html     |   5 +
 django_airavata/apps/admin/urls.py                 |   2 +
 django_airavata/apps/admin/views.py                |   6 +
 django_airavata/apps/api/serializers.py            |  11 ++
 .../api/static/django_airavata_api/js/index.js     |   1 +
 .../js/models/ExperimentStatistics.js              |  43 ++++++++
 .../django_airavata_api/js/service_config.js       |  14 ++-
 django_airavata/apps/api/urls.py                   |   6 +-
 django_airavata/apps/api/views.py                  |  21 ++++
 13 files changed, 304 insertions(+), 5 deletions(-)

diff --git a/django_airavata/apps/admin/package.json b/django_airavata/apps/admin/package.json
index 21d49f4..f1f904c 100644
--- a/django_airavata/apps/admin/package.json
+++ b/django_airavata/apps/admin/package.json
@@ -11,8 +11,8 @@
     "lint:visualstudio": "vue-cli-service lint --format visualstudio ./static/django_airavata_admin/src/"
   },
   "dependencies": {
-    "bootstrap": "^4.0.0",
-    "bootstrap-vue": "^2.0.0-rc.11",
+    "bootstrap": "^4.3.1",
+    "bootstrap-vue": "^2.0.0-rc.22",
     "django-airavata-api": "file:../api",
     "django-airavata-common-ui": "file:../../static/common",
     "moment": "^2.22.2",
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsCard.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsCard.vue
new file mode 100644
index 0000000..63dc8ed
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsCard.vue
@@ -0,0 +1,69 @@
+<template>
+
+  <b-card
+    :bg-variant="bgVariant"
+    body-bg-variant="light"
+    :header-text-variant="headerTextVariant"
+    class="statistics-card"
+  >
+    <div
+      slot="header"
+      class="text-right"
+    >
+      <div class="statistic-count">{{ count }}</div>
+      <div>{{ title }}</div>
+    </div>
+    <b-link
+      :class="'text-decoration-none text-' + linkVariant"
+      @click="$emit('click')"
+    >
+      <slot name="link-text">
+        <div
+          v-for="state in states"
+          :key="state.value"
+        >{{ state.name }}</div>
+      </slot>
+    </b-link>
+  </b-card>
+</template>
+
+<script>
+export default {
+  name: "experiment-statistics-card",
+  props: {
+    bgVariant: {
+      type: String,
+      default: "light"
+    },
+    headerTextVariant: {
+      type: String,
+      default: "dark"
+    },
+    linkVariant: {
+      type: String,
+      default: "primary"
+    },
+    count: {
+      type: Number,
+      required: true
+    },
+    title: {
+      type: String,
+      required: true
+    },
+    states: {
+      type: Array,
+      default: () => []
+    }
+  }
+};
+</script>
+
+<style scoped>
+.statistic-count {
+  font-size: 3rem;
+}
+.statistics-card {
+  height: calc(100% - 30px);
+}
+</style>
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
new file mode 100644
index 0000000..803460a
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
@@ -0,0 +1,121 @@
+<template>
+  <div>
+    <div class="row">
+      <div class="col">
+        <h1 class="h4 mb-4">Experiment Statistics</h1>
+      </div>
+    </div>
+    <div class="row">
+      <div class="col-lg-2 col-md-4">
+        <experiment-statistics-card
+          bg-variant="primary"
+          header-text-variant="white"
+          :count="experimentStatistics.allExperimentCount || 0"
+          title="Total Experiments"
+        >
+          <span slot="link-text">All</span>
+        </experiment-statistics-card>
+      </div>
+      <div class="col-lg-2 col-md-4">
+        <experiment-statistics-card
+          bg-variant="light"
+          :count="experimentStatistics.createdExperimentCount || 0"
+          :states="createdStates"
+          title="Created Experiments"
+        >
+        </experiment-statistics-card>
+      </div>
+      <div class="col-lg-2 col-md-4">
+        <experiment-statistics-card
+          bg-variant="light"
+          header-text-variant="success"
+          :count="experimentStatistics.runningExperimentCount || 0"
+          :states="runningStates"
+          title="Running Experiments"
+        >
+        </experiment-statistics-card>
+
+      </div>
+      <div class="col-lg-2 col-md-4">
+        <experiment-statistics-card
+          bg-variant="success"
+          header-text-variant="white"
+          link-variant="success"
+          :count="experimentStatistics.completedExperimentCount || 0"
+          :states="completedStates"
+          title="Completed Experiments"
+        >
+        </experiment-statistics-card>
+      </div>
+      <div class="col-lg-2 col-md-4">
+        <experiment-statistics-card
+          bg-variant="warning"
+          header-text-variant="white"
+          link-variant="warning"
+          :count="experimentStatistics.cancelledExperimentCount || 0"
+          :states="canceledStates"
+          title="Cancelled Experiments"
+        >
+        </experiment-statistics-card>
+      </div>
+      <div class="col-lg-2 col-md-4">
+        <experiment-statistics-card
+          bg-variant="danger"
+          header-text-variant="white"
+          link-variant="danger"
+          :count="experimentStatistics.failedExperimentCount || 0"
+          :states="failedStates"
+          title="Failed Experiments"
+        >
+        </experiment-statistics-card>
+
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import { models, services } from "django-airavata-api";
+import ExperimentStatisticsCard from "./ExperimentStatisticsCard";
+
+export default {
+  name: "experiment-statistics-container",
+  data() {
+    return {
+      experimentStatistics: {}
+    };
+  },
+  created() {
+    services.ExperimentStatisticsService.get().then(
+      stats => (this.experimentStatistics = stats)
+    );
+  },
+  components: {
+    ExperimentStatisticsCard
+  },
+  computed: {
+    createdStates() {
+      // TODO: moved to ExperimentStatistics model
+      return [models.ExperimentState.CREATED, models.ExperimentState.VALIDATED];
+    },
+    runningStates() {
+      return [
+        models.ExperimentState.SCHEDULED,
+        models.ExperimentState.LAUNCHED,
+        models.ExperimentState.EXECUTING
+      ];
+    },
+    completedStates() {
+      return [models.ExperimentState.COMPLETED];
+    },
+    canceledStates() {
+      return [
+        models.ExperimentState.CANCELING,
+        models.ExperimentState.CANCELED
+      ];
+    },
+    failedStates() {
+      return [models.ExperimentState.FAILED];
+    }
+  }
+};
+</script>
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/router.js b/django_airavata/apps/admin/static/django_airavata_admin/src/router.js
index be3b2c0..b4070d4 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/router.js
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/router.js
@@ -6,6 +6,7 @@ import ApplicationModuleEditor from "./components/applications/ApplicationModule
 import ApplicationsDashboard from "./components/dashboards/ApplicationsDashboard.vue";
 import ComputePreference from "./components/admin/group_resource_preferences/ComputePreference";
 import ComputeResourcePreferenceDashboard from "./components/dashboards/ComputeResourcePreferenceDashboard";
+import ExperimentStatisticsContainer from "./components/statistics/ExperimentStatisticsContainer";
 import CredentialStoreDashboard from "./components/dashboards/CredentialStoreDashboard";
 import GatewayResourceProfileEditorContainer from "./components/gatewayprofile/GatewayResourceProfileEditorContainer.vue";
 import GroupComputeResourcePreference from "./components/admin/group_resource_preferences/GroupComputeResourcePreference";
@@ -122,6 +123,11 @@ const routes = [
     path: "/users",
     component: UserManagementContainer,
     name: "users"
+  },
+  {
+    path: "/experiment-statistics",
+    component: ExperimentStatisticsContainer,
+    name: "experiment-statistics"
   }
 ];
 export default new VueRouter({
diff --git a/django_airavata/apps/admin/templates/admin/admin_base.html b/django_airavata/apps/admin/templates/admin/admin_base.html
index e883735..9b48f21 100644
--- a/django_airavata/apps/admin/templates/admin/admin_base.html
+++ b/django_airavata/apps/admin/templates/admin/admin_base.html
@@ -19,6 +19,11 @@
             <i class="fa fa-users"></i> <span class=sr-only>Manage Users</span>
         </a>
         {% endif %}
+        {% if request.is_gateway_admin or request.is_read_only_gateway_admin %}
+        <a href="{% url 'django_airavata_admin:experiment-statistics' %}" class="c-nav__item {% if request.active_nav_item == 'experiment-statistics' %}is-active{% endif %}" data-toggle=tooltip data-placement=right title="Experiment Statistics">
+            <i class="fa fa-chart-bar"></i> <span class=sr-only>Experiment Statistics</span>
+        </a>
+        {% endif %}
         <a href="{% url 'django_airavata_admin:credential_store' %}" class="c-nav__item {% if request.active_nav_item == 'credential_store' %}is-active{% endif %}" data-toggle=tooltip data-placement=right title="Credential Store">
             <i class="fa fa-lock"></i> <span class=sr-only>Credential Store</span>
         </a>
diff --git a/django_airavata/apps/admin/urls.py b/django_airavata/apps/admin/urls.py
index d706078..c56da01 100644
--- a/django_airavata/apps/admin/urls.py
+++ b/django_airavata/apps/admin/urls.py
@@ -7,6 +7,8 @@ urlpatterns = [
     url(r'^$', views.home, name='home'),
     url(r'^applications/', views.app_catalog, name='app_catalog'),
     url(r'^credentials/', views.credential_store, name='credential_store'),
+    url(r'^experiment-statistics/', views.experiment_statistics,
+        name="experiment-statistics"),
     url(r'^group-resource-profiles/', views.group_resource_profile,
         name='group_resource_profile'),
     url(r'^gateway-resource-profile/', views.gateway_resource_profile,
diff --git a/django_airavata/apps/admin/views.py b/django_airavata/apps/admin/views.py
index 627febf..18908fc 100644
--- a/django_airavata/apps/admin/views.py
+++ b/django_airavata/apps/admin/views.py
@@ -45,3 +45,9 @@ def gateway_resource_profile(request):
 def users(request):
     request.active_nav_item = 'users'
     return render(request, 'admin/admin_base.html')
+
+
+@login_required
+def experiment_statistics(request):
+    request.active_nav_item = 'experiment-statistics'
+    return render(request, 'admin/admin_base.html')
diff --git a/django_airavata/apps/api/serializers.py b/django_airavata/apps/api/serializers.py
index 90fd2ed..a33287d 100644
--- a/django_airavata/apps/api/serializers.py
+++ b/django_airavata/apps/api/serializers.py
@@ -47,6 +47,7 @@ from airavata.model.data.replica.ttypes import (
 )
 from airavata.model.experiment.ttypes import (
     ExperimentModel,
+    ExperimentStatistics,
     ExperimentSummaryModel
 )
 from airavata.model.group.ttypes import GroupModel, ResourcePermissionType
@@ -852,3 +853,13 @@ class ManagedUserProfile(serializers.Serializer):
         instance['_removed_group_ids'] = list(
             set(existing_group_ids) - set(new_group_ids))
         return instance
+
+
+class ExperimentStatisticsSerializer(
+        thrift_utils.create_serializer_class(ExperimentStatistics)):
+    allExperiments = ExperimentSummarySerializer(many=True)
+    completedExperiments = ExperimentSummarySerializer(many=True)
+    failedExperiments = ExperimentSummarySerializer(many=True)
+    cancelledExperiments = ExperimentSummarySerializer(many=True)
+    createdExperiments = ExperimentSummarySerializer(many=True)
+    runningExperiments = ExperimentSummarySerializer(many=True)
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/index.js b/django_airavata/apps/api/static/django_airavata_api/js/index.js
index 67a26d5..1729443 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/index.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/index.js
@@ -101,6 +101,7 @@ const services = {
   DataProductService: ServiceFactory.service("DataProducts"),
   ExperimentSearchService: ServiceFactory.service("ExperimentSearch"),
   ExperimentService: ServiceFactory.service("Experiments"),
+  ExperimentStatisticsService: ServiceFactory.service("ExperimentStatistics"),
   FullExperimentService: ServiceFactory.service("FullExperiments"),
   GatewayResourceProfileService: ServiceFactory.service(
     "GatewayResourceProfiles"
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ExperimentStatistics.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ExperimentStatistics.js
new file mode 100644
index 0000000..ffad82c
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ExperimentStatistics.js
@@ -0,0 +1,43 @@
+import BaseModel from "./BaseModel";
+import ExperimentSummary from "./ExperimentSummary";
+
+const FIELDS = [
+  "allExperimentCount",
+  "completedExperimentCount",
+  "cancelledExperimentCount",
+  "failedExperimentCount",
+  "createdExperimentCount",
+  "runningExperimentCount",
+  {
+    name: "allExperiments",
+    type: ExperimentSummary,
+    list: true
+  },
+  {
+    name: "completedExperiments",
+    type: ExperimentSummary,
+    list: true
+  },
+  {
+    name: "failedExperiments",
+    type: ExperimentSummary,
+    list: true
+  },
+  {
+    name: "cancelledExperiments",
+    type: ExperimentSummary,
+    list: true
+  },
+  {
+    name: "createdExperiments",
+    type: ExperimentSummary,
+    list: true
+  },
+  { name: "runningExperiments", type: ExperimentSummary, list: true }
+];
+
+export default class ExperimentStatistics extends BaseModel {
+  constructor(data = {}) {
+    super(FIELDS, data);
+  }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
index ad2e322..43551c7 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
@@ -7,6 +7,7 @@ import CredentialSummary from "./models/CredentialSummary";
 import DataProduct from "./models/DataProduct";
 import Experiment from "./models/Experiment";
 import ExperimentSearchFields from "./models/ExperimentSearchFields";
+import ExperimentStatistics from "./models/ExperimentStatistics";
 import ExperimentSummary from "./models/ExperimentSummary";
 import FullExperiment from "./models/FullExperiment";
 import GatewayResourceProfile from "./models/GatewayResourceProfile";
@@ -187,6 +188,17 @@ export default {
       ExperimentSearchFields.values.map(f => f.name)
     )
   },
+  ExperimentStatistics: {
+    url: "/api/experiment-statistics",
+    methods: {
+      get: {
+        url: "/api/experiment-statistics",
+        requestType: "get",
+        queryParams: ["fromDate", "toDate"],
+        modelClass: ExperimentStatistics
+      }
+    }
+  },
   FullExperiments: {
     url: "/api/full-experiments",
     viewSet: [
@@ -293,7 +305,7 @@ export default {
         modelClass: UserStoragePath,
         encodePathParams: false
       }
-    },
+    }
   },
   WorkspacePreferences: {
     url: "/api/workspace-preferences",
diff --git a/django_airavata/apps/api/urls.py b/django_airavata/apps/api/urls.py
index 1810513..267da92 100644
--- a/django_airavata/apps/api/urls.py
+++ b/django_airavata/apps/api/urls.py
@@ -76,10 +76,12 @@ urlpatterns = [
     url(r'^workspace-preferences',
         views.WorkspacePreferencesView.as_view(),
         name="workspace-preferences"),
-    # url(r'^user-storage/~/(?P<path>.*)/$',
     url(r'^user-storage/~/(?P<path>.*)$',
         views.UserStoragePathView.as_view(),
-        name="user-storage-items")
+        name="user-storage-items"),
+    url(r'^experiment-statistics',
+        views.ExperimentStatisticsView.as_view(),
+        name="experiment-statistics")
 ]
 
 if logger.isEnabledFor(logging.DEBUG):
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index 0fe5192..3e20e17 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -1,5 +1,6 @@
 import logging
 import os
+from datetime import datetime, timedelta
 
 from django.conf import settings
 from django.contrib.auth.decorators import login_required
@@ -1440,3 +1441,23 @@ class ManagedUserViewSet(mixins.CreateModelMixin,
             'creationTime': user_profile.creationTime,
             'groups': groups
         }
+
+
+class ExperimentStatisticsView(APIView):
+    # TODO: restrict to only Admins or Read Only Admins group members
+    serializer_class = serializers.ExperimentStatisticsSerializer
+
+    def get(self, request, format=None):
+        # TODO: convert from ISO-8601 to posix timestamp
+        from_time = request.GET.get(
+            'fromTime',
+            (datetime.utcnow() - timedelta(days=1)).timestamp() * 1000)
+        to_time = request.GET.get(
+            'toTime',
+            datetime.utcnow().timestamp() * 1000)
+        statistics = request.airavata_client.getExperimentStatistics(
+            request.authz_token, settings.GATEWAY_ID, from_time, to_time,
+            None, None, None)
+        serializer = self.serializer_class(
+            statistics, context={'request': request})
+        return Response(serializer.data)


[airavata-django-portal] 02/11: AIRAVATA-2990 Display experiments for selected status category

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 706d83983e165381e4bd9fba51d01893c5e1a3cf
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Jun 6 15:48:05 2019 -0400

    AIRAVATA-2990 Display experiments for selected status category
---
 .../statistics/ExperimentStatisticsContainer.vue   | 96 +++++++++++++++++++++-
 django_airavata/apps/api/views.py                  |  2 +-
 .../experiment/ExperimentStatusBadge.vue           | 33 --------
 .../js/containers/ExperimentListContainer.vue      |  4 +-
 .../js/containers/RecentExperimentsContainer.vue   |  3 +-
 .../common/js/components/ApplicationName.vue       | 40 +++++++++
 .../common/js/components/ComputeResourceName.vue   | 38 +++++++++
 .../common/js/components/ExperimentStatusBadge.vue | 38 +++++++++
 django_airavata/static/common/js/index.js          | 16 ++--
 9 files changed, 220 insertions(+), 50 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
index 803460a..0a20b76 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
@@ -12,6 +12,7 @@
           header-text-variant="white"
           :count="experimentStatistics.allExperimentCount || 0"
           title="Total Experiments"
+          @click="selectExperiments(experimentStatistics.allExperiments)"
         >
           <span slot="link-text">All</span>
         </experiment-statistics-card>
@@ -22,6 +23,7 @@
           :count="experimentStatistics.createdExperimentCount || 0"
           :states="createdStates"
           title="Created Experiments"
+          @click="selectExperiments(experimentStatistics.createdExperiments)"
         >
         </experiment-statistics-card>
       </div>
@@ -32,6 +34,7 @@
           :count="experimentStatistics.runningExperimentCount || 0"
           :states="runningStates"
           title="Running Experiments"
+          @click="selectExperiments(experimentStatistics.runningExperiments)"
         >
         </experiment-statistics-card>
 
@@ -44,6 +47,7 @@
           :count="experimentStatistics.completedExperimentCount || 0"
           :states="completedStates"
           title="Completed Experiments"
+          @click="selectExperiments(experimentStatistics.completedExperiments)"
         >
         </experiment-statistics-card>
       </div>
@@ -55,6 +59,7 @@
           :count="experimentStatistics.cancelledExperimentCount || 0"
           :states="canceledStates"
           title="Cancelled Experiments"
+          @click="selectExperiments(experimentStatistics.cancelledExperiments)"
         >
         </experiment-statistics-card>
       </div>
@@ -66,22 +71,61 @@
           :count="experimentStatistics.failedExperimentCount || 0"
           :states="failedStates"
           title="Failed Experiments"
+          @click="selectExperiments(experimentStatistics.failedExperiments)"
         >
         </experiment-statistics-card>
 
       </div>
     </div>
+    <div
+      class="row"
+      v-if="items.length"
+    >
+      <div class="col">
+        <b-table
+          :fields="fields"
+          :items="items"
+        >
+          <template
+            slot="executionId"
+            slot-scope="data"
+          >
+            <application-name :application-interface-id="data.value" />
+          </template>
+          <template
+            slot="resourceHostId"
+            slot-scope="data"
+          >
+            <compute-resource-name :compute-resource-id="data.value" />
+          </template>
+          <template
+            slot="creationTime"
+            slot-scope="data"
+          >
+            <human-date :date="data.value" />
+          </template>
+          <template
+            slot="experimentStatus"
+            slot-scope="data"
+          >
+            <experiment-status-badge :status-name="data.value.name" />
+          </template>
+        </b-table>
+      </div>
+    </div>
   </div>
 </template>
 <script>
 import { models, services } from "django-airavata-api";
+import { components } from "django-airavata-common-ui";
 import ExperimentStatisticsCard from "./ExperimentStatisticsCard";
 
 export default {
   name: "experiment-statistics-container",
   data() {
     return {
-      experimentStatistics: {}
+      experimentStatistics: {},
+      selectedExperimentSummaries: null
     };
   },
   created() {
@@ -90,7 +134,11 @@ export default {
     );
   },
   components: {
-    ExperimentStatisticsCard
+    ExperimentStatisticsCard,
+    "application-name": components.ApplicationName,
+    "compute-resource-name": components.ComputeResourceName,
+    "human-date": components.HumanDate,
+    "experiment-status-badge": components.ExperimentStatusBadge
   },
   computed: {
     createdStates() {
@@ -115,6 +163,50 @@ export default {
     },
     failedStates() {
       return [models.ExperimentState.FAILED];
+    },
+    fields() {
+      return [
+        {
+          key: "name",
+          label: "Name"
+        },
+        {
+          key: "userName",
+          label: "Owner"
+        },
+        {
+          key: "executionId",
+          label: "Application"
+        },
+        {
+          key: "resourceHostId",
+          label: "Resource"
+        },
+        {
+          key: "creationTime",
+          label: "Creation Time"
+        },
+        {
+          key: "experimentStatus",
+          label: "Status"
+        },
+        {
+          key: "actions",
+          label: "Actions"
+        }
+      ];
+    },
+    items() {
+      if (this.selectedExperimentSummaries) {
+        return this.selectedExperimentSummaries;
+      } else {
+        return [];
+      }
+    }
+  },
+  methods: {
+    selectExperiments(experiments) {
+      this.selectedExperimentSummaries = experiments;
     }
   }
 };
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index 3e20e17..6bd8a93 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -1451,7 +1451,7 @@ class ExperimentStatisticsView(APIView):
         # TODO: convert from ISO-8601 to posix timestamp
         from_time = request.GET.get(
             'fromTime',
-            (datetime.utcnow() - timedelta(days=1)).timestamp() * 1000)
+            (datetime.utcnow() - timedelta(days=7)).timestamp() * 1000)
         to_time = request.GET.get(
             'toTime',
             datetime.utcnow().timestamp() * 1000)
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentStatusBadge.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentStatusBadge.vue
deleted file mode 100644
index bea963c..0000000
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentStatusBadge.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<template>
-    <b-badge :variant="badgeVariant">{{ statusName }}</b-badge>
-</template>
-
-<script>
-import { models } from 'django-airavata-api'
-
-export default {
-    name: 'experiment-status-badge',
-    props: {
-        statusName: {
-            type: String,
-            required: true
-        }
-    },
-    computed: {
-        experimentState: function() {
-            return models.ExperimentState.byName(this.statusName);
-        },
-        badgeVariant: function() {
-            if (this.experimentState.isProgressing) {
-                return "secondary";
-            } else if (this.experimentState === models.ExperimentState.COMPLETED) {
-                return "success";
-            } else if (this.experimentState === models.ExperimentState.FAILED) {
-                return "danger";
-            } else {
-                return "info";
-            }
-        }
-    }
-}
-</script>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/ExperimentListContainer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/ExperimentListContainer.vue
index 90170ee..9f1c678 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/ExperimentListContainer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/ExperimentListContainer.vue
@@ -86,8 +86,6 @@
 import { models, services } from "django-airavata-api";
 import { components as comps } from "django-airavata-common-ui";
 
-import ExperimentStatusBadge from "../components/experiment/ExperimentStatusBadge.vue";
-
 import moment from "moment";
 import urls from "../utils/urls";
 
@@ -102,7 +100,7 @@ export default {
   },
   components: {
     pager: comps.Pager,
-    "experiment-status-badge": ExperimentStatusBadge
+    "experiment-status-badge": comps.ExperimentStatusBadge
   },
   methods: {
     nextExperiments: function() {
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/RecentExperimentsContainer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/RecentExperimentsContainer.vue
index 95a6fd9..d3a5850 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/RecentExperimentsContainer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/RecentExperimentsContainer.vue
@@ -20,7 +20,6 @@
 </template>
 
 <script>
-import ExperimentStatusBadge from "../components/experiment/ExperimentStatusBadge.vue";
 import urls from "../utils/urls";
 import { models, services } from "django-airavata-api";
 import { components } from "django-airavata-common-ui";
@@ -34,7 +33,7 @@ export default {
     sidebar: components.Sidebar,
     "sidebar-header": components.SidebarHeader,
     "sidebar-feed": components.SidebarFeed,
-    ExperimentStatusBadge
+    "experiment-status-badge": components.ExperimentStatusBadge
   },
   created() {
     this.pollExperiments();
diff --git a/django_airavata/static/common/js/components/ApplicationName.vue b/django_airavata/static/common/js/components/ApplicationName.vue
new file mode 100644
index 0000000..161adae
--- /dev/null
+++ b/django_airavata/static/common/js/components/ApplicationName.vue
@@ -0,0 +1,40 @@
+<template>
+  <span :class="{'font-italic': notAvailable}">{{ applicationName }}</span>
+</template>
+<script>
+import { services } from "django-airavata-api";
+export default {
+  name: "application-name",
+  props: {
+    applicationInterfaceId: {
+      type: String,
+      required: true
+    }
+  },
+  data() {
+    return {
+      applicationInterface: null,
+      notAvailable: false
+    };
+  },
+  created() {
+    services.ApplicationInterfaceService.retrieve(
+      { lookup: this.applicationInterfaceId },
+      { ignoreErrors: true, cache: true }
+    )
+      .then(appInterface => (this.applicationInterface = appInterface))
+      .catch(() => (this.notAvailable = true));
+  },
+  computed: {
+    applicationName() {
+      if (this.notAvailable) {
+        return "N/A";
+      } else {
+        return this.applicationInterface
+          ? this.applicationInterface.applicationName
+          : "";
+      }
+    }
+  }
+};
+</script>
diff --git a/django_airavata/static/common/js/components/ComputeResourceName.vue b/django_airavata/static/common/js/components/ComputeResourceName.vue
new file mode 100644
index 0000000..ddb89c7
--- /dev/null
+++ b/django_airavata/static/common/js/components/ComputeResourceName.vue
@@ -0,0 +1,38 @@
+<template>
+  <span :class="{'font-italic': notAvailable}">{{ name }}</span>
+</template>
+<script>
+import { services } from "django-airavata-api";
+export default {
+  name: "compute-resource-name",
+  props: {
+    computeResourceId: {
+      type: String,
+      required: true
+    }
+  },
+  data() {
+    return {
+      computeResource: null,
+      notAvailable: false
+    };
+  },
+  created() {
+    services.ComputeResourceService.retrieve(
+      { lookup: this.computeResourceId },
+      { ignoreErrors: true, cache: true }
+    )
+      .then(computeResource => (this.computeResource = computeResource))
+      .catch(() => (this.notAvailable = true));
+  },
+  computed: {
+    name() {
+      if (this.notAvailable) {
+        return "N/A";
+      } else {
+        return this.computeResource ? this.computeResource.hostName : "";
+      }
+    }
+  }
+};
+</script>
diff --git a/django_airavata/static/common/js/components/ExperimentStatusBadge.vue b/django_airavata/static/common/js/components/ExperimentStatusBadge.vue
new file mode 100644
index 0000000..54b7829
--- /dev/null
+++ b/django_airavata/static/common/js/components/ExperimentStatusBadge.vue
@@ -0,0 +1,38 @@
+<template>
+  <b-badge :variant="badgeVariant">{{ statusName }}</b-badge>
+</template>
+
+<script>
+import { models } from "django-airavata-api";
+
+export default {
+  name: "experiment-status-badge",
+  props: {
+    statusName: {
+      type: String,
+      required: true
+    }
+  },
+  computed: {
+    experimentState: function() {
+      return models.ExperimentState.byName(this.statusName);
+    },
+    badgeVariant: function() {
+      if (this.experimentState.isProgressing) {
+        return "secondary";
+      } else if (this.experimentState === models.ExperimentState.COMPLETED) {
+        return "success";
+      } else if (
+        this.experimentState === models.ExperimentState.CANCELING ||
+        this.experimentState === models.ExperimentState.CANCELED
+      ) {
+        return "warning";
+      } else if (this.experimentState === models.ExperimentState.FAILED) {
+        return "danger";
+      } else {
+        return "info";
+      }
+    }
+  }
+};
+</script>
diff --git a/django_airavata/static/common/js/index.js b/django_airavata/static/common/js/index.js
index 74c1d3e..e7eda5d 100644
--- a/django_airavata/static/common/js/index.js
+++ b/django_airavata/static/common/js/index.js
@@ -1,10 +1,13 @@
 import ApplicationCard from "./components/ApplicationCard.vue";
+import ApplicationName from "./components/ApplicationName";
 import AutocompleteTextInput from "./components/AutocompleteTextInput.vue";
 import ClipboardCopyButton from "./components/ClipboardCopyButton.vue";
 import ClipboardCopyLink from "./components/ClipboardCopyLink.vue";
+import ComputeResourceName from "./components/ComputeResourceName";
 import ConfirmationDialog from "./components/ConfirmationDialog.vue";
 import DeleteButton from "./components/DeleteButton.vue";
 import DeleteLink from "./components/DeleteLink.vue";
+import ExperimentStatusBadge from "./components/ExperimentStatusBadge";
 import HumanDate from "./components/HumanDate.vue";
 import MainLayout from "./components/MainLayout.vue";
 import Pager from "./components/Pager.vue";
@@ -31,12 +34,15 @@ import entry from "./entry";
 const components = {
   Pager,
   ApplicationCard,
+  ApplicationName,
   AutocompleteTextInput,
   ClipboardCopyButton,
   ClipboardCopyLink,
+  ComputeResourceName,
   ConfirmationDialog,
   DeleteButton,
   DeleteLink,
+  ExperimentStatusBadge,
   HumanDate,
   MainLayout,
   ShareButton,
@@ -74,12 +80,4 @@ export default {
   utils
 };
 
-export {
-  components,
-  entry,
-  errors,
-  layouts,
-  mixins,
-  notifications,
-  utils
-};
+export { components, entry, errors, layouts, mixins, notifications, utils };


[airavata-django-portal] 03/11: AIRAVATA-2990 Adding simple response cache mechanism

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 0ad795d5453b6c61f2f4f32159a98aefb5ba7848
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Jun 6 15:49:09 2019 -0400

    AIRAVATA-2990 Adding simple response cache mechanism
---
 .../js/services/ServiceFactory.js                  |  8 ++--
 .../static/django_airavata_api/js/utils/Cache.js   | 53 ++++++++++++++++++++++
 .../django_airavata_api/js/utils/FetchUtils.js     | 33 ++++++++++++--
 3 files changed, 87 insertions(+), 7 deletions(-)

diff --git a/django_airavata/apps/api/static/django_airavata_api/js/services/ServiceFactory.js b/django_airavata/apps/api/static/django_airavata_api/js/services/ServiceFactory.js
index 855d600..94db8b2 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/services/ServiceFactory.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/services/ServiceFactory.js
@@ -211,9 +211,10 @@ class ServiceFactory {
       let queryParamsMapping = parseQueryMapping(config.queryParams);
       serviceObj[functionName] = function(
         params = {},
-        { ignoreErrors, showSpinner } = {
+        { ignoreErrors, showSpinner, cache } = {
           ignoreErrors: false,
-          showSpinner: true
+          showSpinner: true,
+          cache: false
         }
       ) {
         let url = config.url;
@@ -287,7 +288,8 @@ class ServiceFactory {
             } else {
               return FetchUtils.get(url, queryParams, {
                 ignoreErrors,
-                showSpinner
+                showSpinner,
+                cache
               }).then(paginationHandler);
             }
           case putKey:
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/utils/Cache.js b/django_airavata/apps/api/static/django_airavata_api/js/utils/Cache.js
new file mode 100644
index 0000000..da6728b
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/utils/Cache.js
@@ -0,0 +1,53 @@
+const DEFAULT_EXPIRATION_TIME_MS = 5 * 60 * 1000;
+
+class CacheEntry {
+  constructor(value, expireDate) {
+    this._value = value;
+    this._expireDate = expireDate;
+  }
+
+  get value() {
+    return this._value;
+  }
+
+  get isExpired() {
+    return this._expireDate.getTime() < Date.now();
+  }
+}
+
+export default class Cache {
+  constructor() {
+    this._cache = {};
+  }
+
+  get(key) {
+    if (this.has(key)) {
+      const cacheEntry = this._cache[key];
+      return cacheEntry.value;
+    } else {
+      return null;
+    }
+  }
+
+  put({
+    key,
+    value,
+    expireDate = new Date(Date.now() + DEFAULT_EXPIRATION_TIME_MS)
+  }) {
+    this._cache[key] = new CacheEntry(value, expireDate);
+  }
+
+  has(key) {
+    if (this._cache.hasOwnProperty(key)) {
+      const cacheEntry = this._cache[key];
+      if (cacheEntry.isExpired) {
+        delete this._cache[key];
+        return false;
+      } else {
+        return true;
+      }
+    } else {
+      return false;
+    }
+  }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js b/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js
index 11efe6b..d863345 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js
@@ -1,4 +1,5 @@
 import UnhandledErrorDispatcher from "../errors/UnhandledErrorDispatcher";
+import Cache from "./Cache";
 
 var count = 0;
 const parseQueryParams = function(url, queryParams = "") {
@@ -37,6 +38,8 @@ const decrementCount = function() {
   }
 };
 
+const responseCache = new Cache();
+
 export default {
   enableSpinner: function() {},
   disableSpinner: function() {},
@@ -70,7 +73,11 @@ export default {
     url,
     body,
     queryParams = "",
-    { mediaType = "application/json", ignoreErrors = false, showSpinner = true } = {}
+    {
+      mediaType = "application/json",
+      ignoreErrors = false,
+      showSpinner = true
+    } = {}
   ) {
     var headers = this.createHeaders(mediaType);
     // Browsers automatically handle content type for FormData request bodies
@@ -93,7 +100,11 @@ export default {
   put: function(
     url,
     body,
-    { mediaType = "application/json", ignoreErrors = false, showSpinner = true } = {}
+    {
+      mediaType = "application/json",
+      ignoreErrors = false,
+      showSpinner = true
+    } = {}
   ) {
     var headers = this.createHeaders(mediaType);
     return this.processFetch(url, {
@@ -111,7 +122,12 @@ export default {
   get: function(
     url,
     queryParams = "",
-    { mediaType = "application/json", ignoreErrors = false, showSpinner = true } = {}
+    {
+      mediaType = "application/json",
+      ignoreErrors = false,
+      showSpinner = true,
+      cache = false
+    } = {}
   ) {
     if (queryParams && typeof queryParams != "string") {
       queryParams = Object.keys(queryParams)
@@ -124,14 +140,23 @@ export default {
     if (queryParams) {
       url = url + "?" + queryParams;
     }
+    if (cache) {
+      if (responseCache.has(url)) {
+        return responseCache.get(url);
+      }
+    }
     var headers = this.createHeaders(mediaType);
-    return this.processFetch(url, {
+    const fetchRequest = this.processFetch(url, {
       method: "get",
       headers: headers,
       credentials: "same-origin",
       ignoreErrors,
       showSpinner
     });
+    if (cache) {
+      responseCache.put({ key: url, value: fetchRequest });
+    }
+    return fetchRequest;
   },
   delete: function(url, { ignoreErrors = false, showSpinner = true } = {}) {
     var headers = this.createHeaders();


[airavata-django-portal] 09/11: AIRAVATA-2990 Tabbed layout

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 6d7182974d564d9940b1275ca254a85c737a145e
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Wed Jun 12 15:30:26 2019 -0400

    AIRAVATA-2990 Tabbed layout
---
 .../statistics/ExperimentStatisticsContainer.vue   | 86 +++++++++++++---------
 1 file changed, 52 insertions(+), 34 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
index 92d6057..cf784ae 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
@@ -195,41 +195,42 @@
 
       </div>
     </div>
-    <div
-      class="row"
-      v-if="items.length"
-    >
+    <div class="row">
       <div class="col">
-        <b-card>
-          <b-table
-            :fields="fields"
-            :items="items"
-          >
-            <template
-              slot="executionId"
-              slot-scope="data"
-            >
-              <application-name :application-interface-id="data.value" />
-            </template>
-            <template
-              slot="resourceHostId"
-              slot-scope="data"
-            >
-              <compute-resource-name :compute-resource-id="data.value" />
-            </template>
-            <template
-              slot="creationTime"
-              slot-scope="data"
-            >
-              <human-date :date="data.value" />
-            </template>
-            <template
-              slot="experimentStatus"
-              slot-scope="data"
-            >
-              <experiment-status-badge :status-name="data.value.name" />
-            </template>
-          </b-table>
+        <b-card no-body>
+          <b-tabs card>
+            <b-tab :title="selectedExperimentsTabTitle">
+              <b-table
+                :fields="fields"
+                :items="items"
+              >
+                <template
+                  slot="executionId"
+                  slot-scope="data"
+                >
+                  <application-name :application-interface-id="data.value" />
+                </template>
+                <template
+                  slot="resourceHostId"
+                  slot-scope="data"
+                >
+                  <compute-resource-name :compute-resource-id="data.value" />
+                </template>
+                <template
+                  slot="creationTime"
+                  slot-scope="data"
+                >
+                  <human-date :date="data.value" />
+                </template>
+                <template
+                  slot="experimentStatus"
+                  slot-scope="data"
+                >
+                  <experiment-status-badge :status-name="data.value.name" />
+                </template>
+              </b-table>
+            </b-tab>
+          </b-tabs>
         </b-card>
       </div>
     </div>
@@ -386,6 +387,23 @@ export default {
       } else {
         return [];
       }
+    },
+    selectedExperimentsTabTitle() {
+      if (this.selectedExperimentSummariesKey === "allExperiments") {
+        return "All Experiments";
+      } else if (this.selectedExperimentSummariesKey === "createdExperiments") {
+        return "Created Experiments";
+      } else if (this.selectedExperimentSummariesKey === "runningExperiments") {
+        return "Running Experiments";
+      } else if (this.selectedExperimentSummariesKey === "completedExperiments") {
+        return "Completed Experiments";
+      } else if (this.selectedExperimentSummariesKey === "cancelledExperiments") {
+        return "Cancelled Experiments";
+      } else if (this.selectedExperimentSummariesKey === "failedExperiments") {
+        return "Failed Experiments";
+      } else {
+        return "Experiments";
+      }
     }
   },
   methods: {


[airavata-django-portal] 05/11: AIRAVATA-2990 Past day and past week quick filter options

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit c9375a2d29edcbd64e917993cef4399175c2243c
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Mon Jun 10 12:09:55 2019 -0400

    AIRAVATA-2990 Past day and past week quick filter options
---
 .../statistics/ExperimentStatisticsContainer.vue   | 54 +++++++++++++++++++---
 1 file changed, 47 insertions(+), 7 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
index dcbe586..0a59ff1 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
@@ -2,17 +2,27 @@
   <div>
     <div class="row">
       <div class="col">
-        <h1 class="h4 mb-4">Experiment Statistics</h1>
+        <h1 class="h4 mb-4">Experiment Statistics from {{fromTimeDisplay}} to {{toTimeDisplay}}</h1>
       </div>
     </div>
     <div class="row">
       <div class="col">
-        <b-card>
-          <flat-pickr
-            v-model="dateRange"
-            :config="dateConfig"
-            @on-change="dateRangeChanged"
-          />
+        <b-card header="Filter">
+          <b-input-group class="w-100">
+            <b-input-group-prepend is-text>
+              <i class="fa fa-calendar-week" aria-hidden="true"></i>
+            </b-input-group-prepend>
+            <flat-pickr
+              :value="dateRange"
+              :config="dateConfig"
+              @on-change="dateRangeChanged"
+              class="form-control"
+            />
+            <b-input-group-append>
+              <b-button @click="getPast24Hours" variant="outline-secondary">Past 24 Hours</b-button>
+              <b-button @click="getPastWeek" variant="outline-secondary">Past Week</b-button>
+            </b-input-group-append>
+          </b-input-group>
         </b-card>
       </div>
     </div>
@@ -133,6 +143,8 @@ import { models, services } from "django-airavata-api";
 import { components } from "django-airavata-common-ui";
 import ExperimentStatisticsCard from "./ExperimentStatisticsCard";
 
+import moment from "moment";
+
 export default {
   name: "experiment-statistics-container",
   data() {
@@ -146,6 +158,7 @@ export default {
       dateRange: [fromTime, toTime],
       dateConfig: {
         mode: "range",
+        wrap: true,
         maxDate: new Date()
       }
     };
@@ -222,6 +235,12 @@ export default {
       } else {
         return [];
       }
+    },
+    fromTimeDisplay() {
+      return moment(this.fromTime).format("MMM Do YYYY");
+    },
+    toTimeDisplay() {
+      return moment(this.toTime).format("MMM Do YYYY");
     }
   },
   methods: {
@@ -239,6 +258,27 @@ export default {
         fromTime: this.fromTime.toJSON(),
         toTime: this.toTime.toJSON()
       }).then(stats => (this.experimentStatistics = stats));
+    },
+    getPast24Hours() {
+      this.fromTime = this.daysAgo(1);
+      this.toTime = new Date();
+      this.updateDateRange();
+      this.loadStatistics();
+    },
+    getPastWeek() {
+      this.fromTime = this.daysAgo(7);
+      this.toTime = new Date();
+      this.updateDateRange();
+      this.loadStatistics();
+    },
+    updateDateRange() {
+      this.dateRange = [
+        moment(this.fromTime).format("YYYY-MM-DD"),
+        moment(this.toTime).format("YYYY-MM-DD")
+      ];
+    },
+    daysAgo(days) {
+      return new Date(Date.now() - days * 24 * 60 * 60 * 1000);
     }
   }
 };


[airavata-django-portal] 06/11: AIRAVATA-2990 Username filtering

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit f5c6b685d5dba874465ee1c2c6717e1db77769c3
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Tue Jun 11 16:29:47 2019 -0400

    AIRAVATA-2990 Username filtering
---
 .../statistics/ExperimentStatisticsContainer.vue   | 103 +++++++++++++++++----
 .../django_airavata_api/js/service_config.js       |   8 +-
 django_airavata/apps/api/views.py                  |   2 +-
 3 files changed, 92 insertions(+), 21 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
index 0a59ff1..9aa9ff8 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
@@ -8,9 +8,12 @@
     <div class="row">
       <div class="col">
         <b-card header="Filter">
-          <b-input-group class="w-100">
+          <b-input-group class="w-100 mb-2">
             <b-input-group-prepend is-text>
-              <i class="fa fa-calendar-week" aria-hidden="true"></i>
+              <i
+                class="fa fa-calendar-week"
+                aria-hidden="true"
+              ></i>
             </b-input-group-prepend>
             <flat-pickr
               :value="dateRange"
@@ -19,10 +22,50 @@
               class="form-control"
             />
             <b-input-group-append>
-              <b-button @click="getPast24Hours" variant="outline-secondary">Past 24 Hours</b-button>
-              <b-button @click="getPastWeek" variant="outline-secondary">Past Week</b-button>
+              <b-button
+                @click="getPast24Hours"
+                variant="outline-secondary"
+              >Past 24 Hours</b-button>
+              <b-button
+                @click="getPastWeek"
+                variant="outline-secondary"
+              >Past Week</b-button>
             </b-input-group-append>
           </b-input-group>
+          <b-dropdown
+            text="Add Filters"
+            class="mb-2"
+          >
+            <b-dropdown-item
+              v-if="!usernameFilterEnabled"
+              @click="usernameFilterEnabled=true"
+            >Username</b-dropdown-item>
+            <b-dropdown-item>Application Name</b-dropdown-item>
+            <b-dropdown-item>Hostname</b-dropdown-item>
+          </b-dropdown>
+          <b-input-group
+            v-if="usernameFilterEnabled"
+            class="mb-2"
+          >
+            <b-form-input
+              v-model="usernameFilter"
+              placeholder="Username"
+              @keydown.native.enter="loadStatistics"
+            />
+            <b-input-group-append>
+              <b-button @click="removeUsernameFilter"><i class="fa fa-times"></i><span class="sr-only">Remove username
+                  filter</span></b-button>
+            </b-input-group-append>
+          </b-input-group>
+          <template slot="footer">
+            <div class="d-flex justify-content-end">
+              <b-button
+                @click="loadStatistics"
+                class="ml-auto"
+                variant="primary"
+              >Get Statistics</b-button>
+            </div>
+          </template>
         </b-card>
       </div>
     </div>
@@ -33,7 +76,7 @@
           header-text-variant="white"
           :count="experimentStatistics.allExperimentCount || 0"
           title="Total Experiments"
-          @click="selectExperiments(experimentStatistics.allExperiments)"
+          @click="selectedExperimentSummariesKey = 'allExperiments'"
         >
           <span slot="link-text">All</span>
         </experiment-statistics-card>
@@ -44,7 +87,7 @@
           :count="experimentStatistics.createdExperimentCount || 0"
           :states="createdStates"
           title="Created Experiments"
-          @click="selectExperiments(experimentStatistics.createdExperiments)"
+          @click="selectedExperimentSummariesKey = 'createdExperiments'"
         >
         </experiment-statistics-card>
       </div>
@@ -55,7 +98,7 @@
           :count="experimentStatistics.runningExperimentCount || 0"
           :states="runningStates"
           title="Running Experiments"
-          @click="selectExperiments(experimentStatistics.runningExperiments)"
+          @click="selectedExperimentSummariesKey = 'runningExperiments'"
         >
         </experiment-statistics-card>
 
@@ -68,7 +111,7 @@
           :count="experimentStatistics.completedExperimentCount || 0"
           :states="completedStates"
           title="Completed Experiments"
-          @click="selectExperiments(experimentStatistics.completedExperiments)"
+          @click="selectedExperimentSummariesKey = 'completedExperiments'"
         >
         </experiment-statistics-card>
       </div>
@@ -80,7 +123,7 @@
           :count="experimentStatistics.cancelledExperimentCount || 0"
           :states="canceledStates"
           title="Cancelled Experiments"
-          @click="selectExperiments(experimentStatistics.cancelledExperiments)"
+          @click="selectedExperimentSummariesKey = 'cancelledExperiments'"
         >
         </experiment-statistics-card>
       </div>
@@ -92,7 +135,7 @@
           :count="experimentStatistics.failedExperimentCount || 0"
           :states="failedStates"
           title="Failed Experiments"
-          @click="selectExperiments(experimentStatistics.failedExperiments)"
+          @click="selectedExperimentSummariesKey = 'failedExperiments'"
         >
         </experiment-statistics-card>
 
@@ -152,15 +195,22 @@ export default {
     const toTime = new Date();
     return {
       experimentStatistics: {},
-      selectedExperimentSummaries: null,
+      selectedExperimentSummariesKey: null,
       fromTime: fromTime,
       toTime: toTime,
       dateRange: [fromTime, toTime],
       dateConfig: {
         mode: "range",
         wrap: true,
+        dateFormat: "Y-m-d H:i",
         maxDate: new Date()
-      }
+      },
+      usernameFilterEnabled: false,
+      usernameFilter: null,
+      applicationNameFilterEnabled: false,
+      applicationNameFilter: null,
+      hostnameFilterEnabled: false,
+      hostnameFilter: null
     };
   },
   created() {
@@ -241,12 +291,16 @@ export default {
     },
     toTimeDisplay() {
       return moment(this.toTime).format("MMM Do YYYY");
+    },
+    selectedExperimentSummaries() {
+      if (this.selectedExperimentSummariesKey && this.experimentStatistics && this.selectedExperimentSummariesKey in this.experimentStatistics) {
+        return this.experimentStatistics[this.selectedExperimentSummariesKey];
+      } else {
+        return []
+      }
     }
   },
   methods: {
-    selectExperiments(experiments) {
-      this.selectedExperimentSummaries = experiments;
-    },
     dateRangeChanged(selectedDates) {
       [this.fromTime, this.toTime] = selectedDates;
       if (this.fromTime && this.toTime) {
@@ -254,10 +308,16 @@ export default {
       }
     },
     loadStatistics() {
-      services.ExperimentStatisticsService.get({
+      const requestData = {
         fromTime: this.fromTime.toJSON(),
         toTime: this.toTime.toJSON()
-      }).then(stats => (this.experimentStatistics = stats));
+      };
+      if (this.usernameFilterEnabled && this.usernameFilter) {
+        requestData["userName"] = this.usernameFilter;
+      }
+      services.ExperimentStatisticsService.get(requestData).then(
+        stats => (this.experimentStatistics = stats)
+      );
     },
     getPast24Hours() {
       this.fromTime = this.daysAgo(1);
@@ -273,12 +333,17 @@ export default {
     },
     updateDateRange() {
       this.dateRange = [
-        moment(this.fromTime).format("YYYY-MM-DD"),
-        moment(this.toTime).format("YYYY-MM-DD")
+        moment(this.fromTime).format("YYYY-MM-DD HH:mm"),
+        moment(this.toTime).format("YYYY-MM-DD HH:mm")
       ];
     },
     daysAgo(days) {
       return new Date(Date.now() - days * 24 * 60 * 60 * 1000);
+    },
+    removeUsernameFilter() {
+      this.usernameFilter = null;
+      this.usernameFilterEnabled = false;
+      this.loadStatistics();
     }
   }
 };
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
index 0770e81..ef94c66 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
@@ -194,7 +194,13 @@ export default {
       get: {
         url: "/api/experiment-statistics",
         requestType: "get",
-        queryParams: ["fromTime", "toTime"],
+        queryParams: [
+          "fromTime",
+          "toTime",
+          "userName",
+          "applicationName",
+          "resourceHostName"
+        ],
         modelClass: ExperimentStatistics
       }
     }
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index e58326e..637f3f7 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -1463,7 +1463,7 @@ class ExperimentStatisticsView(APIView):
             to_time = datetime.utcnow().timestamp() * 1000
         username = request.GET.get('userName', None)
         application_name = request.GET.get('applicationName', None)
-        resource_hostname = request.GET.get('requestHostName', None)
+        resource_hostname = request.GET.get('resourceHostName', None)
         statistics = request.airavata_client.getExperimentStatistics(
             request.authz_token, settings.GATEWAY_ID, from_time, to_time,
             username, application_name, resource_hostname)


[airavata-django-portal] 07/11: AIRAVATA-2990 Adding application and hostname filtering

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 9530a93ffb792a8ac6b9d8928b578e39d3d6bbc5
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Wed Jun 12 11:34:43 2019 -0400

    AIRAVATA-2990 Adding application and hostname filtering
---
 .../statistics/ExperimentStatisticsContainer.vue   | 132 +++++++++++++++++++--
 .../common/js/components/ComputeResourceName.vue   |  22 +++-
 2 files changed, 139 insertions(+), 15 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
index 9aa9ff8..40dfc63 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
@@ -7,7 +7,7 @@
     </div>
     <div class="row">
       <div class="col">
-        <b-card header="Filter">
+        <b-card header="Filter Options">
           <b-input-group class="w-100 mb-2">
             <b-input-group-prepend is-text>
               <i
@@ -40,8 +40,14 @@
               v-if="!usernameFilterEnabled"
               @click="usernameFilterEnabled=true"
             >Username</b-dropdown-item>
-            <b-dropdown-item>Application Name</b-dropdown-item>
-            <b-dropdown-item>Hostname</b-dropdown-item>
+            <b-dropdown-item
+              v-if="!applicationNameFilterEnabled"
+              @click="applicationNameFilterEnabled=true"
+            >Application Name</b-dropdown-item>
+            <b-dropdown-item
+              v-if="!hostnameFilterEnabled"
+              @click="hostnameFilterEnabled=true"
+            >Hostname</b-dropdown-item>
           </b-dropdown>
           <b-input-group
             v-if="usernameFilterEnabled"
@@ -53,8 +59,56 @@
               @keydown.native.enter="loadStatistics"
             />
             <b-input-group-append>
-              <b-button @click="removeUsernameFilter"><i class="fa fa-times"></i><span class="sr-only">Remove username
-                  filter</span></b-button>
+              <b-button @click="removeUsernameFilter">
+                <i class="fa fa-times"></i>
+                <span class="sr-only">Remove username filter</span>
+              </b-button>
+            </b-input-group-append>
+          </b-input-group>
+          <b-input-group
+            v-if="applicationNameFilterEnabled"
+            class="mb-2"
+          >
+            <b-form-select
+              v-model="applicationNameFilter"
+              :options="applicationNameOptions"
+              @input="loadStatistics"
+            >
+              <template slot="first">
+                <option
+                  :value="null"
+                  disabled
+                >Select an application to filter on</option>
+              </template>
+            </b-form-select>
+            <b-input-group-append>
+              <b-button @click="removeApplicationNameFilter">
+                <i class="fa fa-times"></i>
+                <span class="sr-only">Remove application name filter</span>
+              </b-button>
+            </b-input-group-append>
+          </b-input-group>
+          <b-input-group
+            v-if="hostnameFilterEnabled"
+            class="mb-2"
+          >
+            <b-form-select
+              v-model="hostnameFilter"
+              :options="hostnameOptions"
+              @input="loadStatistics"
+            >
+              <template slot="first">
+                <option
+                  :value="null"
+                  disabled
+                >Select compute resource to filter on</option>
+              </template>
+            </b-form-select>
+            <b-input-group-append>
+              <b-button @click="removeHostnameFilter">
+                <i class="fa fa-times"></i>
+                <span class="sr-only">Remove hostname filter</span>
+              </b-button>
             </b-input-group-append>
           </b-input-group>
           <template slot="footer">
@@ -182,7 +236,7 @@
   </div>
 </template>
 <script>
-import { models, services } from "django-airavata-api";
+import { models, services, utils } from "django-airavata-api";
 import { components } from "django-airavata-common-ui";
 import ExperimentStatisticsCard from "./ExperimentStatisticsCard";
 
@@ -210,11 +264,15 @@ export default {
       applicationNameFilterEnabled: false,
       applicationNameFilter: null,
       hostnameFilterEnabled: false,
-      hostnameFilter: null
+      hostnameFilter: null,
+      appInterfaces: null,
+      computeResourceNames: null
     };
   },
   created() {
     this.loadStatistics();
+    this.loadApplicationInterfaces();
+    this.loadComputeResources();
   },
   components: {
     ExperimentStatisticsCard,
@@ -293,10 +351,40 @@ export default {
       return moment(this.toTime).format("MMM Do YYYY");
     },
     selectedExperimentSummaries() {
-      if (this.selectedExperimentSummariesKey && this.experimentStatistics && this.selectedExperimentSummariesKey in this.experimentStatistics) {
+      if (
+        this.selectedExperimentSummariesKey &&
+        this.experimentStatistics &&
+        this.selectedExperimentSummariesKey in this.experimentStatistics
+      ) {
         return this.experimentStatistics[this.selectedExperimentSummariesKey];
       } else {
-        return []
+        return [];
+      }
+    },
+    applicationNameOptions() {
+      if (this.appInterfaces) {
+        const options = this.appInterfaces.map(appInterface => {
+          return {
+            value: appInterface.applicationInterfaceId,
+            text: appInterface.applicationName
+          };
+        });
+        return utils.StringUtils.sortIgnoreCase(options, o => o.text);
+      } else {
+        return [];
+      }
+    },
+    hostnameOptions() {
+      if (this.computeResourceNames) {
+        const options = this.computeResourceNames.map(name => {
+          return {
+            value: name.host_id,
+            text: name.host
+          };
+        });
+        return utils.StringUtils.sortIgnoreCase(options, o => o.text);
+      } else {
+        return [];
       }
     }
   },
@@ -307,6 +395,16 @@ export default {
         this.loadStatistics();
       }
     },
+    loadApplicationInterfaces() {
+      return services.ApplicationInterfaceService.list().then(
+        appInterfaces => (this.appInterfaces = appInterfaces)
+      );
+    },
+    loadComputeResources() {
+      return services.ComputeResourceService.namesList().then(
+        names => (this.computeResourceNames = names)
+      );
+    },
     loadStatistics() {
       const requestData = {
         fromTime: this.fromTime.toJSON(),
@@ -315,6 +413,12 @@ export default {
       if (this.usernameFilterEnabled && this.usernameFilter) {
         requestData["userName"] = this.usernameFilter;
       }
+      if (this.applicationNameFilterEnabled && this.applicationNameFilter) {
+        requestData["applicationName"] = this.applicationNameFilter;
+      }
+      if (this.hostnameFilterEnabled && this.hostnameFilter) {
+        requestData["resourceHostName"] = this.hostnameFilter;
+      }
       services.ExperimentStatisticsService.get(requestData).then(
         stats => (this.experimentStatistics = stats)
       );
@@ -344,6 +448,16 @@ export default {
       this.usernameFilter = null;
       this.usernameFilterEnabled = false;
       this.loadStatistics();
+    },
+    removeApplicationNameFilter() {
+      this.applicationNameFilter = null;
+      this.applicationNameFilterEnabled = false;
+      this.loadStatistics();
+    },
+    removeHostnameFilter() {
+      this.hostnameFilter = null;
+      this.hostnameFilterEnabled = false;
+      this.loadStatistics();
     }
   }
 };
diff --git a/django_airavata/static/common/js/components/ComputeResourceName.vue b/django_airavata/static/common/js/components/ComputeResourceName.vue
index ddb89c7..9395e8e 100644
--- a/django_airavata/static/common/js/components/ComputeResourceName.vue
+++ b/django_airavata/static/common/js/components/ComputeResourceName.vue
@@ -18,12 +18,17 @@ export default {
     };
   },
   created() {
-    services.ComputeResourceService.retrieve(
-      { lookup: this.computeResourceId },
-      { ignoreErrors: true, cache: true }
-    )
-      .then(computeResource => (this.computeResource = computeResource))
-      .catch(() => (this.notAvailable = true));
+    this.loadComputeResource();
+  },
+  methods: {
+    loadComputeResource() {
+      services.ComputeResourceService.retrieve(
+        { lookup: this.computeResourceId },
+        { ignoreErrors: true, cache: true }
+      )
+        .then(computeResource => (this.computeResource = computeResource))
+        .catch(() => (this.notAvailable = true));
+    }
   },
   computed: {
     name() {
@@ -33,6 +38,11 @@ export default {
         return this.computeResource ? this.computeResource.hostName : "";
       }
     }
+  },
+  watch: {
+    computeResourceId() {
+      this.loadComputeResource();
+    }
   }
 };
 </script>


[airavata-django-portal] 04/11: AIRAVATA-2990 Add UI component for picking date range for stats

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 1ce5892dcdc47db7967d2ee4ea480a0096adc67d
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Fri Jun 7 16:51:30 2019 -0400

    AIRAVATA-2990 Add UI component for picking date range for stats
---
 django_airavata/apps/admin/package.json            |  1 +
 .../statistics/ExperimentStatisticsCard.vue        |  2 +-
 .../statistics/ExperimentStatisticsContainer.vue   | 96 ++++++++++++++--------
 .../admin/static/django_airavata_admin/src/main.js |  4 +
 .../django_airavata_api/js/service_config.js       |  2 +-
 django_airavata/apps/api/view_utils.py             | 16 ++++
 django_airavata/apps/api/views.py                  | 27 ++++--
 7 files changed, 105 insertions(+), 43 deletions(-)

diff --git a/django_airavata/apps/admin/package.json b/django_airavata/apps/admin/package.json
index f1f904c..d6b9048 100644
--- a/django_airavata/apps/admin/package.json
+++ b/django_airavata/apps/admin/package.json
@@ -17,6 +17,7 @@
     "django-airavata-common-ui": "file:../../static/common",
     "moment": "^2.22.2",
     "vue": "^2.5.22",
+    "vue-flatpickr-component": "^8.1.2",
     "vue-resource": "^1.3.4",
     "vue-router": "^2.7.0",
     "vuedraggable": "^2.16.0",
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsCard.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsCard.vue
index 63dc8ed..db8a464 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsCard.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsCard.vue
@@ -2,7 +2,7 @@
 
   <b-card
     :bg-variant="bgVariant"
-    body-bg-variant="light"
+    body-bg-variant="white"
     :header-text-variant="headerTextVariant"
     class="statistics-card"
   >
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
index 0a20b76..dcbe586 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
@@ -6,6 +6,17 @@
       </div>
     </div>
     <div class="row">
+      <div class="col">
+        <b-card>
+          <flat-pickr
+            v-model="dateRange"
+            :config="dateConfig"
+            @on-change="dateRangeChanged"
+          />
+        </b-card>
+      </div>
+    </div>
+    <div class="row">
       <div class="col-lg-2 col-md-4">
         <experiment-statistics-card
           bg-variant="primary"
@@ -82,35 +93,37 @@
       v-if="items.length"
     >
       <div class="col">
-        <b-table
-          :fields="fields"
-          :items="items"
-        >
-          <template
-            slot="executionId"
-            slot-scope="data"
-          >
-            <application-name :application-interface-id="data.value" />
-          </template>
-          <template
-            slot="resourceHostId"
-            slot-scope="data"
+        <b-card>
+          <b-table
+            :fields="fields"
+            :items="items"
           >
-            <compute-resource-name :compute-resource-id="data.value" />
-          </template>
-          <template
-            slot="creationTime"
-            slot-scope="data"
-          >
-            <human-date :date="data.value" />
-          </template>
-          <template
-            slot="experimentStatus"
-            slot-scope="data"
-          >
-            <experiment-status-badge :status-name="data.value.name" />
-          </template>
-        </b-table>
+            <template
+              slot="executionId"
+              slot-scope="data"
+            >
+              <application-name :application-interface-id="data.value" />
+            </template>
+            <template
+              slot="resourceHostId"
+              slot-scope="data"
+            >
+              <compute-resource-name :compute-resource-id="data.value" />
+            </template>
+            <template
+              slot="creationTime"
+              slot-scope="data"
+            >
+              <human-date :date="data.value" />
+            </template>
+            <template
+              slot="experimentStatus"
+              slot-scope="data"
+            >
+              <experiment-status-badge :status-name="data.value.name" />
+            </template>
+          </b-table>
+        </b-card>
       </div>
     </div>
   </div>
@@ -123,15 +136,22 @@ import ExperimentStatisticsCard from "./ExperimentStatisticsCard";
 export default {
   name: "experiment-statistics-container",
   data() {
+    const fromTime = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago
+    const toTime = new Date();
     return {
       experimentStatistics: {},
-      selectedExperimentSummaries: null
+      selectedExperimentSummaries: null,
+      fromTime: fromTime,
+      toTime: toTime,
+      dateRange: [fromTime, toTime],
+      dateConfig: {
+        mode: "range",
+        maxDate: new Date()
+      }
     };
   },
   created() {
-    services.ExperimentStatisticsService.get().then(
-      stats => (this.experimentStatistics = stats)
-    );
+    this.loadStatistics();
   },
   components: {
     ExperimentStatisticsCard,
@@ -207,6 +227,18 @@ export default {
   methods: {
     selectExperiments(experiments) {
       this.selectedExperimentSummaries = experiments;
+    },
+    dateRangeChanged(selectedDates) {
+      [this.fromTime, this.toTime] = selectedDates;
+      if (this.fromTime && this.toTime) {
+        this.loadStatistics();
+      }
+    },
+    loadStatistics() {
+      services.ExperimentStatisticsService.get({
+        fromTime: this.fromTime.toJSON(),
+        toTime: this.toTime.toJSON()
+      }).then(stats => (this.experimentStatistics = stats));
     }
   }
 };
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/main.js b/django_airavata/apps/admin/static/django_airavata_admin/src/main.js
index 60cd37a..31a3644 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/main.js
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/main.js
@@ -1,14 +1,18 @@
 import { components, entry } from "django-airavata-common-ui";
 import VueResource from "vue-resource";
 import VueRouter from "vue-router";
+import VueFlatPickr from 'vue-flatpickr-component';
 import App from "./App.vue";
 import router from "./router";
 
+import 'flatpickr/dist/flatpickr.css';
+
 entry(Vue => {
   Vue.config.productionTip = false;
 
   Vue.use(VueResource);
   Vue.use(VueRouter);
+  Vue.use(VueFlatPickr);
 
   new Vue({
     render: h => h(components.MainLayout, [h(App)]),
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
index 43551c7..0770e81 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
@@ -194,7 +194,7 @@ export default {
       get: {
         url: "/api/experiment-statistics",
         requestType: "get",
-        queryParams: ["fromDate", "toDate"],
+        queryParams: ["fromTime", "toTime"],
         modelClass: ExperimentStatistics
       }
     }
diff --git a/django_airavata/apps/api/view_utils.py b/django_airavata/apps/api/view_utils.py
index 1046684..8e932da 100644
--- a/django_airavata/apps/api/view_utils.py
+++ b/django_airavata/apps/api/view_utils.py
@@ -1,5 +1,8 @@
+import logging
 from collections.__init__ import OrderedDict
+from datetime import datetime
 
+import pytz
 from django.conf import settings
 from django.http import Http404
 from rest_framework import mixins, pagination
@@ -8,6 +11,8 @@ from rest_framework.reverse import reverse
 from rest_framework.utils.urls import remove_query_param, replace_query_param
 from rest_framework.viewsets import GenericViewSet
 
+logger = logging.getLogger(__name__)
+
 
 class GenericAPIBackedViewSet(GenericViewSet):
     # Make lookup_value_regex to any set of non-forward-slash characters. Many
@@ -178,3 +183,14 @@ class APIResultPagination(pagination.LimitOffsetPagination):
             return self.request.build_absolute_uri(reverse(self.viewname))
         else:
             return self.request.build_absolute_uri()
+
+
+def convert_utc_iso8601_to_date(iso8601_utc_string):
+    # This is meant to convert a JavaScript `new Date().toJSON()` into a
+    # datetime instance
+    timestamp = datetime.strptime(
+        iso8601_utc_string, "%Y-%m-%dT%H:%M:%S.%fZ")
+    timestamp = timestamp.replace(tzinfo=pytz.UTC)
+    logger.debug("convert_utc_iso8601_to_date({})={}".format(
+        iso8601_utc_string, timestamp))
+    return timestamp
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index 6bd8a93..e58326e 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -2,6 +2,7 @@ import logging
 import os
 from datetime import datetime, timedelta
 
+import pytz
 from django.conf import settings
 from django.contrib.auth.decorators import login_required
 from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
@@ -47,7 +48,8 @@ from . import (
     models,
     output_views,
     serializers,
-    thrift_utils
+    thrift_utils,
+    view_utils
 )
 
 READ_PERMISSION_TYPE = '{}:READ'
@@ -1448,16 +1450,23 @@ class ExperimentStatisticsView(APIView):
     serializer_class = serializers.ExperimentStatisticsSerializer
 
     def get(self, request, format=None):
-        # TODO: convert from ISO-8601 to posix timestamp
-        from_time = request.GET.get(
-            'fromTime',
-            (datetime.utcnow() - timedelta(days=7)).timestamp() * 1000)
-        to_time = request.GET.get(
-            'toTime',
-            datetime.utcnow().timestamp() * 1000)
+        if 'fromTime' in request.GET:
+            from_time = view_utils.convert_utc_iso8601_to_date(
+                request.GET['fromTime']).timestamp() * 1000
+        else:
+            from_time = (datetime.utcnow() -
+                         timedelta(days=7)).timestamp() * 1000
+        if 'toTime' in request.GET:
+            to_time = view_utils.convert_utc_iso8601_to_date(
+                request.GET['toTime']).timestamp() * 1000
+        else:
+            to_time = datetime.utcnow().timestamp() * 1000
+        username = request.GET.get('userName', None)
+        application_name = request.GET.get('applicationName', None)
+        resource_hostname = request.GET.get('requestHostName', None)
         statistics = request.airavata_client.getExperimentStatistics(
             request.authz_token, settings.GATEWAY_ID, from_time, to_time,
-            None, None, None)
+            username, application_name, resource_hostname)
         serializer = self.serializer_class(
             statistics, context={'request': request})
         return Response(serializer.data)