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 2021/06/18 19:38:28 UTC

[airavata-django-portal] 11/20: AIRAVATA-3453 Initial version of resource selection components

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

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

commit 0c18ce115ad233a4afe4e84e1d2316cc4384a8fc
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Tue May 11 16:36:16 2021 -0400

    AIRAVATA-3453 Initial version of resource selection components
---
 .../api/static/django_airavata_api/js/index.js     |   6 +-
 .../js/web-components/ComputeResourceSelector.vue  |  84 +++++++++++
 .../js/web-components/ExperimentEditor.vue         |  18 +--
 .../js/web-components/QueueSettingsEditor.vue      |  13 ++
 .../js/web-components/ResourceSelectionEditor.vue  | 160 +++++++++++++++++++++
 .../js/web-components/store.js                     |  35 ++++-
 6 files changed, 306 insertions(+), 10 deletions(-)

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 f0c74e8..c0804f1 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
@@ -35,6 +35,7 @@ import SetEnvPaths from "./models/SetEnvPaths";
 import SharedEntity from "./models/SharedEntity";
 import StoragePreference from "./models/StoragePreference";
 import SummaryType from "./models/SummaryType";
+import UserConfigurationData from "./models/UserConfigurationData";
 import UserPermission from "./models/UserPermission";
 
 import CloudJobSubmissionService from "./services/CloudJobSubmissionService";
@@ -93,6 +94,7 @@ const models = {
   SharedEntity,
   StoragePreference,
   SummaryType,
+  UserConfigurationData,
   UserPermission,
 };
 
@@ -110,7 +112,9 @@ const services = {
   ExperimentSearchService: ServiceFactory.service("ExperimentSearch"),
   ExperimentService: ServiceFactory.service("Experiments"),
   ExperimentStatisticsService: ServiceFactory.service("ExperimentStatistics"),
-  ExperimentStoragePathService: ServiceFactory.service("ExperimentStoragePaths"),
+  ExperimentStoragePathService: ServiceFactory.service(
+    "ExperimentStoragePaths"
+  ),
   FullExperimentService: ServiceFactory.service("FullExperiments"),
   GatewayResourceProfileService: ServiceFactory.service(
     "GatewayResourceProfile"
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ComputeResourceSelector.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ComputeResourceSelector.vue
new file mode 100644
index 0000000..3b28d83
--- /dev/null
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ComputeResourceSelector.vue
@@ -0,0 +1,84 @@
+<template>
+  <b-form-group label="Compute Resource" label-for="compute-resource">
+    <b-form-select
+      id="compute-resource"
+      v-model="resourceHostId"
+      :options="computeResourceOptions"
+      required
+      @input="computeResourceChanged"
+    >
+      <template slot="first">
+        <option :value="null" disabled>Select a Compute Resource</option>
+      </template>
+    </b-form-select>
+  </b-form-group>
+</template>
+
+<script>
+import { getComputeResourceNames } from "./store";
+export default {
+  name: "compute-resource-selector",
+  props: {
+    value: {
+      // compute resource host id
+      type: String,
+      default: null,
+    },
+    computeResources: {
+      type: Array, // of compute resource host ids
+      default: () => [],
+    },
+  },
+  data() {
+    return {
+      resourceHostId: this.value,
+      computeResourceNames: {},
+    };
+  },
+  created() {
+    this.loadComputeResourceNames();
+  },
+  computed: {
+    computeResourceOptions: function () {
+      const computeResourceOptions = this.computeResources.map(
+        (computeHostId) => {
+          return {
+            value: computeHostId,
+            text:
+              computeHostId in this.computeResourceNames
+                ? this.computeResourceNames[computeHostId]
+                : "",
+          };
+        }
+      );
+      computeResourceOptions.sort((a, b) => a.text.localeCompare(b.text));
+      return computeResourceOptions;
+    },
+  },
+  methods: {
+    async loadComputeResourceNames() {
+      this.computeResourceNames = await getComputeResourceNames();
+    },
+    computeResourceChanged() {
+      this.emitValueChanged();
+    },
+    emitValueChanged: function () {
+      const inputEvent = new CustomEvent("input", {
+        detail: [this.resourceHostId],
+        composed: true,
+        bubbles: true,
+      });
+      this.$el.dispatchEvent(inputEvent);
+    },
+  },
+  watch: {
+    value() {
+      this.resourceHostId = this.value;
+    },
+  },
+};
+</script>
+
+<style>
+@import "./styles.css";
+</style>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ExperimentEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ExperimentEditor.vue
index 25d77d8..e7acb23 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ExperimentEditor.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ExperimentEditor.vue
@@ -24,9 +24,9 @@
       <!-- programmatically define slots as native slots (not Vue slots), see #mounted() -->
       </div>
     </template>
-    <div @input="updateGroupResourceProfileId">
-      <slot name="experiment-group-resource-profile">
-        <adpf-group-resource-profile-selector :value="experiment.userConfigurationData.groupResourceProfileId"/>
+    <div @input="updateUserConfigurationData">
+      <slot name="experiment-resource-selection">
+        <adpf-resource-selection-editor ref="resourceSelectionEditor" />
       </slot>
     </div>
     <slot name="save-button">
@@ -45,6 +45,7 @@ import {
 
 export default {
   props: {
+    // TODO: rename to applicationModuleId?
     applicationId: {
       type: String,
       required: true,
@@ -79,6 +80,9 @@ export default {
         // TODO: add support for other input types
         this.$refs[input.name][0].append(slot);
       }
+      // Can't set objects via attributes, must set as prop
+      this.$refs.resourceSelectionEditor.value = this.experiment.userConfigurationData;
+      this.$refs.resourceSelectionEditor.applicationModuleId = this.applicationId;
     });
   },
   data() {
@@ -100,9 +104,9 @@ export default {
       const [projectId] = event.detail;
       this.experiment.projectId = projectId;
     },
-    updateGroupResourceProfileId(event) {
-      const [groupResourceProfileId] = event.detail;
-      this.experiment.userConfigurationData.groupResourceProfileId = groupResourceProfileId;
+    updateUserConfigurationData(event) {
+      const [userConfigurationData] = event.detail;
+      this.experiment.userConfigurationData = userConfigurationData;
     },
     onSubmit(event) {
       // console.log(event);
@@ -137,8 +141,6 @@ export default {
           this.applicationModule.appModuleName +
           " on " +
           new Date().toLocaleString();
-        experiment.userConfigurationData.computationalResourceScheduling.resourceHostId =
-          "js-169-51.jetstream-cloud.org_6672e8fe-8d63-4bbe-8bf8-4ea04092e72f";
         this.$emit("loaded", experiment);
         return experiment;
       }
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/QueueSettingsEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/QueueSettingsEditor.vue
new file mode 100644
index 0000000..a02d2bb
--- /dev/null
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/QueueSettingsEditor.vue
@@ -0,0 +1,13 @@
+<template>
+  <div>QueueSettingsEditor</div>
+</template>
+
+<script>
+export default {
+
+}
+</script>
+
+<style>
+
+</style>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ResourceSelectionEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ResourceSelectionEditor.vue
new file mode 100644
index 0000000..d6824ec
--- /dev/null
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ResourceSelectionEditor.vue
@@ -0,0 +1,160 @@
+<template>
+  <div v-if="userConfigurationData">
+    <div @input.stop="updateGroupResourceProfileId">
+      <adpf-group-resource-profile-selector
+        slot="resource-selection-grp"
+        :value="userConfigurationData.groupResourceProfileId"
+      />
+    </div>
+    <div @input.stop="updateComputeResourceHostId">
+      <adpf-compute-resource-selector
+        ref="computeResourceSelector"
+        slot="resource-selection-compute-resource"
+        :value="
+          userConfigurationData.computationalResourceScheduling.resourceHostId
+        "
+      />
+    </div>
+    <div @input.stop="updateComputationalResourceScheduling">
+      <adpf-queue-settings-editor
+        slot="resource-selection-queue-settings"
+        :value="userConfigurationData.computationalResourceScheduling"
+        :queues="queues"
+        :max-allowed-nodes="maxAllowedNodes"
+        :max-allowed-cores="maxAllowedCores"
+        :max-allowed-walltime="maxAllowedWalltime"
+        :max-memory="maxMemory"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import { models } from "django-airavata-api";
+import {
+  getApplicationDeployments,
+  getDefaultComputeResourceId,
+} from "./store";
+export default {
+  // TODO: better name? UserConfigurationDataEditor?
+  name: "resource-selection-editor",
+  props: {
+    value: {
+      type: models.UserConfigurationData,
+    },
+    applicationModuleId: {
+      type: String,
+    },
+  },
+  data() {
+    return {
+      userConfigurationData: this.cloneValue(),
+      applicationDeployments: [],
+      queues: [],
+      maxAllowedNodes: 0,
+      maxAllowedCores: 0,
+      maxAllowedWalltime: 0,
+      maxMemory: 0,
+      defaultComputeResourceId: null,
+    };
+  },
+  computed: {
+    computeResources() {
+      return this.applicationDeployments.map((dep) => dep.computeHostId);
+    },
+    groupResourceProfileId() {
+      return this.userConfigurationData
+        ? this.userConfigurationData.groupResourceProfileId
+        : null;
+    },
+    resourceHostId() {
+      return this.userConfigurationData &&
+        this.userConfigurationData.computationalResourceScheduling
+        ? this.userConfigurationData.computationalResourceScheduling
+            .resourceHostId
+        : null;
+    },
+  },
+  methods: {
+    emitValueChanged: function () {
+      const inputEvent = new CustomEvent("input", {
+        detail: [this.userConfigurationData],
+        composed: true,
+        bubbles: true,
+      });
+      this.$el.dispatchEvent(inputEvent);
+    },
+    updateGroupResourceProfileId(event) {
+      const [groupResourceProfileId] = event.detail;
+      this.userConfigurationData.groupResourceProfileId = groupResourceProfileId;
+      this.emitValueChanged();
+      this.loadApplicationDeployments();
+    },
+    updateComputeResourceHostId(event) {
+      const [computeResourceHostId] = event.detail;
+      this.userConfigurationData.computationalResourceScheduling.resourceHostId = computeResourceHostId;
+      this.emitValueChanged();
+      // TODO: recalculate queues for the selected host
+    },
+    updateComputationalResourceScheduling(event) {
+      const [computationalResourceScheduling] = event.detail;
+      this.userConfigurationData.computationalResourceScheduling = computationalResourceScheduling;
+      this.emitValueChanged();
+      // TODO: recalculate maxes for the selected queue, etc.
+    },
+    async loadApplicationDeployments() {
+      this.applicationDeployments = await getApplicationDeployments(
+        this.applicationModuleId,
+        this.groupResourceProfileId
+      );
+      if (
+        !this.userConfigurationData.computationalResourceScheduling
+          .computeHostId
+      ) {
+        this.userConfigurationData.computationalResourceScheduling.resourceHostId = this.getDefaultResourceHostId();
+      }
+    },
+    cloneValue() {
+      return this.value ? new models.UserConfigurationData(this.value) : null;
+    },
+    async loadData() {
+      if (this.groupResourceProfileId) {
+        this.loadApplicationDeployments();
+      }
+      this.loadDefaultComputeResourceId();
+    },
+    async loadDefaultComputeResourceId() {
+      this.defaultComputeResourceId = await getDefaultComputeResourceId();
+    },
+    getDefaultResourceHostId() {
+      if (
+        this.defaultComputeResourceId &&
+        this.computeResources.find(
+          (crid) => crid === this.defaultComputeResourceId
+        )
+      ) {
+        return this.defaultComputeResourceId;
+      } else if (this.computeResources.length > 0) {
+        // Just pick the first one
+        return this.computeResources[0];
+      }
+    },
+    bindWebComponentProps() {
+      this.$nextTick(() => {
+        this.$refs.computeResourceSelector.computeResources = this.computeResources;
+        this.$refs.computeResourceSelector.value = this.resourceHostId;
+      });
+    },
+  },
+  watch: {
+    value() {
+      this.userConfigurationData = this.cloneValue();
+      this.loadData();
+    },
+    computeResources: "bindWebComponentProps",
+    resourceHostId: "bindWebComponentProps",
+  },
+};
+</script>
+
+<style></style>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/store.js b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/store.js
index 387ca5a..866fcea 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/store.js
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/store.js
@@ -1,4 +1,4 @@
-import { services } from "django-airavata-api";
+import { errors, services, utils } from "django-airavata-api";
 const CACHE = {
   APPLICATION_MODULES: {},
   APPLICATION_INTERFACES: {},
@@ -16,6 +16,8 @@ export async function getApplicationModule(applicationId) {
 }
 
 export async function getApplicationInterfaceForModule(applicationId) {
+  // TODO: I'm not sure this is the right pattern. Perhaps the promise should be
+  // put in the cache and the cache entry should be 'await'-ed.
   if (applicationId in CACHE.APPLICATION_INTERFACES) {
     return CACHE.APPLICATION_INTERFACES[applicationId];
   }
@@ -54,6 +56,11 @@ export async function getDefaultGroupResourceProfileId() {
   return prefs.most_recent_group_resource_profile_id;
 }
 
+export async function getDefaultComputeResourceId() {
+  const prefs = await getWorkspacePreferences();
+  return prefs.most_recent_compute_resource_id;
+}
+
 export async function getExperiment(experimentId) {
   return await services.ExperimentService.retrieve({ lookup: experimentId });
 }
@@ -65,3 +72,29 @@ export async function getProjects() {
 export async function getGroupResourceProfiles() {
   return await services.GroupResourceProfileService.list();
 }
+
+export async function getApplicationDeployments(
+  applicationId,
+  groupResourceProfileId
+) {
+  return await services.ApplicationDeploymentService.list(
+    {
+      appModuleId: applicationId,
+      groupResourceProfileId: groupResourceProfileId,
+    },
+    { ignoreErrors: true }
+  )
+    .catch((error) => {
+      // Ignore unauthorized errors, force user to pick another GroupResourceProfile
+      if (!errors.ErrorUtils.isUnauthorizedError(error)) {
+        return Promise.reject(error);
+      }
+    })
+    // Report all other error types
+    .catch(utils.FetchUtils.reportError);
+}
+
+export async function getComputeResourceNames() {
+  // TODO: cache these
+  return await services.ComputeResourceService.names();
+}