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:14 UTC

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

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)