airavata-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From machris...@apache.org
Subject [airavata-django-portal] 28/28: AIRAVATA-2876 App deployment editor: commands, env vars, defaults
Date Tue, 11 Sep 2018 17:12:16 GMT
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 0ae6ff1078a2e8870e2d450ae5d32729064cd88b
Author: Marcus Christie <machrist@iu.edu>
AuthorDate: Tue Sep 11 13:11:36 2018 -0400

    AIRAVATA-2876 App deployment editor: commands, env vars, defaults
---
 .../applications/ApplicationDeploymentEditor.vue   | 108 ++++++++++++-----
 .../applications/CommandObjectsEditor.vue          |  57 +++++++++
 .../components/applications/SetEnvPathsEditor.vue  |  55 +++++++++
 .../src/components/commons/vmodel_mixin.js         |  14 ++-
 .../admin/static/django_airavata_admin/src/main.js |   4 +-
 django_airavata/apps/api/serializers.py            | 133 ++++++++-------------
 .../api/static/django_airavata_api/js/index.js     |   4 +-
 .../js/models/ApplicationDeploymentDescription.js  |   8 +-
 .../js/models/ComputeResourceDescription.js        |   2 +-
 .../js/models/{SetEnvPath.js => SetEnvPaths.js}    |   2 +-
 10 files changed, 264 insertions(+), 123 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentEditor.vue
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentEditor.vue
index 9bbed4b..015f7f5 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentEditor.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentEditor.vue
@@ -14,18 +14,33 @@
         <b-form-group label="Application Deployment Description" label-for="deployment-description">
           <b-form-textarea id="deployment-description" v-model="data.appDeploymentDescription"
:rows="3"></b-form-textarea>
         </b-form-group>
-        <b-card title="Module Load Commands">
-          <b-input-group v-for="moduleLoadCmd in data.moduleLoadCmds" :key="moduleLoadCmd.key"
class="mb-1">
-            <b-form-input type="text" v-model="moduleLoadCmd.command" required ref="moduleLoadCmdInputs"
/>
-            <b-input-group-append>
-              <b-button variant="secondary" @click="deleteModuleLoadCmd(moduleLoadCmd)">
-                <i class="fa fa-trash"></i>
-                <span class="sr-only">Delete</span>
-              </b-button>
-            </b-input-group-append>
-          </b-input-group>
-          <b-button variant="secondary" @click="addModuleLoadCmd">Add Module Load Command</b-button>
-        </b-card>
+        <command-objects-editor title="Module Load Commands" add-button-label="Add Module
Load Command" v-model="data.moduleLoadCmds"
+        />
+        <set-env-paths-editor title="Library Prepend Paths" add-button-label="Add a Library
Prepend Path" v-model="data.libPrependPaths"
+        />
+        <set-env-paths-editor title="Library Append Paths" add-button-label="Add a Library
Append Path" v-model="data.libAppendPaths"
+        />
+        <set-env-paths-editor title="Environment Variables" add-button-label="Add Environment
Variable" v-model="data.setEnvironment"
+        />
+        <command-objects-editor title="Pre Job Commands" add-button-label="Add Pre Job
Command" v-model="data.preJobCommands" />
+        <command-objects-editor title="Post Job Commands" add-button-label="Add Post Job
Command" v-model="data.postJobCommands"
+        />
+        <b-form-group label="Default Queue Name" label-for="default-queue-name">
+          <b-form-select id="default-queue-name" v-model="data.defaultQueueName" :options="queueNameOptions"
@change="defaultQueueChanged">
+            <template slot="first">
+              <option :value="null">Select a Default Queue</option>
+            </template>
+          </b-form-select>
+        </b-form-group>
+        <b-form-group label="Default Node Count" label-for="default-node-count">
+          <b-form-input id="default-node-count" type="number" v-model="data.defaultNodeCount"
min="0" :max="maxNodes" :disabled="defaultQueueAttributesDisabled"></b-form-input>
+        </b-form-group>
+        <b-form-group label="Default CPU Count" label-for="default-cpu-count">
+          <b-form-input id="default-cpu-count" type="number" v-model="data.defaultCPUCount"
min="0" :max="maxCPUCount" :disabled="defaultQueueAttributesDisabled"></b-form-input>
+        </b-form-group>
+        <b-form-group label="Default Walltime" label-for="default-walltime">
+          <b-form-input id="default-walltime" type="number" v-model="data.defaultWalltime"
min="0" :max="maxWalltime" :disabled="defaultQueueAttributesDisabled"></b-form-input>
+        </b-form-group>
       </div>
     </div>
     <div class="row mb-4">
@@ -44,6 +59,8 @@
 <script>
 import { models, services } from "django-airavata-api";
 import vmodel_mixin from "../commons/vmodel_mixin";
+import CommandObjectsEditor from "./CommandObjectsEditor.vue";
+import SetEnvPathsEditor from "./SetEnvPathsEditor.vue";
 
 export default {
   name: "application-deployment-editor",
@@ -57,6 +74,10 @@ export default {
       required: true
     }
   },
+  components: {
+    CommandObjectsEditor,
+    SetEnvPathsEditor
+  },
   data() {
     return {
       computeResource: null
@@ -77,12 +98,52 @@ export default {
           text: parType.name
         };
       });
+    },
+    queueNameOptions() {
+      if (!this.computeResource) {
+        return [];
+      }
+      return this.computeResource.batchQueues.map(queue => {
+        return {
+          value: queue.queueName,
+          text: queue.queueName
+        };
+      });
+    },
+    maxNodes() {
+      const queue = this.computeResource
+        ? this.computeResource.batchQueues.find(
+            q => q.queueName === this.data.defaultQueueName
+          )
+        : null;
+      return queue ? queue.maxNodes : 0;
+    },
+    maxCPUCount() {
+      const queue = this.computeResource
+        ? this.computeResource.batchQueues.find(
+            q => q.queueName === this.data.defaultQueueName
+          )
+        : null;
+      return queue ? queue.maxProcessors : 0;
+    },
+    maxWalltime() {
+      const queue = this.computeResource
+        ? this.computeResource.batchQueues.find(
+            q => q.queueName === this.data.defaultQueueName
+          )
+        : null;
+      return queue ? queue.maxRuntime : 0;
+    },
+    defaultQueueAttributesDisabled() {
+      return !this.data.defaultQueueName;
     }
   },
   created() {
     services.ComputeResourceService.retrieve({
       lookup: this.data.computeHostId
-    }).then(computeResource => (this.computeResource = computeResource));
+    }).then(computeResource => {
+      this.computeResource = computeResource;
+    });
   },
   methods: {
     save() {
@@ -91,22 +152,13 @@ export default {
     cancel() {
       this.$emit("cancel");
     },
-    addModuleLoadCmd() {
-      if (!this.data.moduleLoadCmds) {
-        this.data.moduleLoadCmds = [];
-      }
-      this.data.moduleLoadCmds.push(new models.CommandObject());
-      this.$nextTick(() =>
-        this.$refs.moduleLoadCmdInputs[
-          this.$refs.moduleLoadCmdInputs.length - 1
-        ].focus()
-      );
-    },
-    deleteModuleLoadCmd(moduleLoadCmd) {
-      const index = this.data.moduleLoadCmds.findIndex(
-        cmd => cmd.key === moduleLoadCmd.key
+    defaultQueueChanged(queueName) {
+      const queue = this.computeResource.batchQueues.find(
+        q => q.queueName === queueName
       );
-      this.data.moduleLoadCmds.splice(index, 1);
+      this.data.defaultNodeCount = queue.defaultNodeCount;
+      this.data.defaultCPUCount = queue.defaultCPUCount;
+      this.data.defaultWalltime = queue.defaultWalltime;
     }
   }
 };
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/CommandObjectsEditor.vue
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/CommandObjectsEditor.vue
new file mode 100644
index 0000000..043ac46
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/CommandObjectsEditor.vue
@@ -0,0 +1,57 @@
+<template>
+  <b-card :title="title" title-tag="h5">
+    <b-input-group v-for="commandObject in data" :key="commandObject.key" class="mb-1">
+      <b-form-input type="text" v-model="commandObject.command" required ref="commandObjectInputs"
/>
+      <b-input-group-append>
+        <b-button variant="secondary" @click="deleteCommandObject(commandObject)">
+          <i class="fa fa-trash"></i>
+          <span class="sr-only">Delete</span>
+        </b-button>
+      </b-input-group-append>
+    </b-input-group>
+    <b-button variant="secondary" @click="addCommandObject">{{ addButtonLabel }}</b-button>
+  </b-card>
+</template>
+
+<script>
+import vmodel_mixin from "../commons/vmodel_mixin";
+import { models, services } from "django-airavata-api";
+
+export default {
+  name: "command-objects-editor",
+  mixins: [vmodel_mixin],
+  props: {
+    value: {
+      type: Array
+    },
+    title: {
+      type: String,
+      required: true
+    },
+    addButtonLabel: {
+      type: String,
+      required: true
+    }
+  },
+  methods: {
+    addCommandObject() {
+      if (!this.data) {
+        this.data = [];
+      }
+      this.data.push(new models.CommandObject());
+      this.$nextTick(() =>
+        this.$refs.commandObjectInputs[
+          this.$refs.commandObjectInputs.length - 1
+        ].focus()
+      );
+    },
+    deleteCommandObject(commandObject) {
+      const index = this.data.findIndex(
+        cmd => cmd.key === commandObject.key
+      );
+      this.data.splice(index, 1);
+    }
+  }
+};
+</script>
+
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/SetEnvPathsEditor.vue
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/SetEnvPathsEditor.vue
new file mode 100644
index 0000000..4a5b449
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/SetEnvPathsEditor.vue
@@ -0,0 +1,55 @@
+<template>
+  <b-card :title="title" title-tag="h5">
+    <b-input-group v-for="setEnvPath in data" :key="setEnvPath.key" class="mb-1 align-items-center">
+      <b-form-input type="text" v-model="setEnvPath.name" required placeholder="NAME"
ref="nameInputs" />
+      <font-awesome-icon icon="equals" class="mx-1" />
+      <b-form-input type="text" v-model="setEnvPath.value" required placeholder="VALUE"
/>
+      <b-input-group-append>
+        <b-button variant="secondary" @click="deleteEnvPath(setEnvPath)">
+          <i class="fa fa-trash"></i>
+          <span class="sr-only">Delete</span>
+        </b-button>
+      </b-input-group-append>
+    </b-input-group>
+    <b-button variant="secondary" @click="addEnvPath">{{ addButtonLabel }}</b-button>
+  </b-card>
+</template>
+
+<script>
+import vmodel_mixin from "../commons/vmodel_mixin";
+import { models, services } from "django-airavata-api";
+
+export default {
+  name: "set-env-paths-editor",
+  mixins: [vmodel_mixin],
+  props: {
+    value: {
+      type: Array
+    },
+    title: {
+      type: String,
+      required: true
+    },
+    addButtonLabel: {
+      type: String,
+      required: true
+    }
+  },
+  methods: {
+    addEnvPath() {
+      if (!this.data) {
+        this.data = [];
+      }
+      this.data.push(new models.SetEnvPaths());
+      this.$nextTick(() =>
+        this.$refs.nameInputs[this.$refs.nameInputs.length - 1].focus()
+      );
+    },
+    deleteEnvPath(setEnvPath) {
+      const index = this.data.findIndex(env => env.key === setEnvPath.key);
+      this.data.splice(index, 1);
+    }
+  }
+};
+</script>
+
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/commons/vmodel_mixin.js
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/commons/vmodel_mixin.js
index 94e6834..21cb486 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/commons/vmodel_mixin.js
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/commons/vmodel_mixin.js
@@ -20,11 +20,15 @@ export default {
     }
   },
   methods: {
-    copyValue() {
-      if (this.value instanceof models.BaseModel) {
-        return this.value.clone();
-      } else if (typeof this.value === 'object') {
-        return JSON.parse(JSON.stringify(this.value));
+    copyValue(value) {
+      if (value instanceof Array) {
+        return value.map(item => this.copyValue(item))
+      } else {
+        if (value instanceof models.BaseModel) {
+          return value.clone();
+        } else if (typeof value === 'object') {
+          return JSON.parse(JSON.stringify(value));
+        }
       }
     }
   },
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 3c49b77..f327b95 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
@@ -12,10 +12,10 @@ import BootstrapVue from 'bootstrap-vue'
 import 'bootstrap/dist/css/bootstrap.css'
 import 'bootstrap-vue/dist/bootstrap-vue.css'
 import { library as faLibrary } from '@fortawesome/fontawesome-svg-core'
-import { faGripVertical } from '@fortawesome/free-solid-svg-icons'
+import { faGripVertical, faEquals } from '@fortawesome/free-solid-svg-icons'
 import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
 
-faLibrary.add(faGripVertical)
+faLibrary.add(faGripVertical, faEquals)
 
 Vue.component('font-awesome-icon', FontAwesomeIcon)
 
diff --git a/django_airavata/apps/api/serializers.py b/django_airavata/apps/api/serializers.py
index 9f91207..eb77164 100644
--- a/django_airavata/apps/api/serializers.py
+++ b/django_airavata/apps/api/serializers.py
@@ -140,6 +140,27 @@ class StoredJSONField(serializers.JSONField):
             return value
 
 
+class OrderedListField(serializers.ListField):
+
+    def __init__(self, *args, **kwargs):
+        self.order_by = kwargs.pop('order_by', None)
+        super().__init__(*args, **kwargs)
+
+    def to_representation(self, instance):
+        rep = super().to_representation(instance)
+        if rep is not None:
+            rep.sort(key=lambda item: item[self.order_by])
+        return rep
+
+    def to_internal_value(self, data):
+        validated_data = super().to_internal_value(data)
+        # Update order field based on order in array
+        items = validated_data if validated_data else []
+        for i in range(len(items)):
+            items[i][self.order_by] = i
+        return validated_data
+
+
 class GroupSerializer(serializers.Serializer):
     url = FullyEncodedHyperlinkedIdentityField(view_name='django_airavata_api:group-detail',
lookup_field='id', lookup_url_kwarg='group_id')
     id = serializers.CharField(default=GroupModel.thrift_spec[1][4], allow_null=True)
@@ -236,55 +257,9 @@ class ApplicationModuleSerializer(
         required = ('appModuleName',)
 
 
-class InputDataObjectTypeSerializer(serializers.Serializer):
-    name = serializers.CharField(required=False)
-    value = serializers.CharField(required=False)
-    type = serializers.IntegerField(required=False)
-    applicationArgument = serializers.CharField(required=False)
-    standardInput = serializers.BooleanField(required=False)
-    metaData = StoredJSONField(required=False)
-    inputOrder = serializers.IntegerField(required=False)
-    isRequired = serializers.BooleanField(required=False)
-    requiredToAddedToCommandLine = serializers.BooleanField(required=False)
-    dataStaged = serializers.BooleanField(required=False)
-    storageResourceId = serializers.CharField(required=False)
-    isReadOnly = serializers.BooleanField(required=False)
-
-    def create(self, validated_data):
-        return InputDataObjectType(**validated_data)
-
-    def update(self, instance, validated_data):
-        raise Exception("Not implemented")
-
-
-class OutputDataObjectTypeSerializer(serializers.Serializer):
-    name = serializers.CharField(required=False)
-    value = serializers.CharField(required=False)
-    type = serializers.IntegerField(required=False)
-    applicationArgument = serializers.CharField(required=False)
-    isRequired = serializers.BooleanField(required=False)
-    requiredToAddedToCommandLine = serializers.BooleanField(required=False)
-    dataMovement = serializers.CharField(required=False)
-    location = serializers.CharField(required=False)
-    searchQuery = serializers.CharField(required=False)
-    outputStreaming = serializers.BooleanField(required=False)
-    storageResourceId = serializers.CharField(required=False)
-
-    def create(self, validated_data):
-        return OutputDataObjectType(**validated_data)
-
-    def update(self, instance, validated_data):
-        raise Exception("Not implemented")
-
-
-class CustomSerializer(serializers.Serializer):
-    def process_list_fields(self, validated_data):
-        fields = self.fields
-        params = copy.deepcopy(validated_data)
-        for field_name, serializer in fields.items():
-            if isinstance(serializer, serializers.ListSerializer):
-                   params[field_name] = serializer.create(params[field_name])
-        return params
+class InputDataObjectTypeSerializer(
+        thrift_utils.create_serializer_class(InputDataObjectType)):
+    pass
 
 
 class ApplicationInterfaceDescriptionSerializer(
@@ -294,23 +269,19 @@ class ApplicationInterfaceDescriptionSerializer(
         view_name='django_airavata_api:application-interface-detail',
         lookup_field='applicationInterfaceId',
         lookup_url_kwarg='app_interface_id')
+    applicationInputs = OrderedListField(
+        order_by='inputOrder',
+        child=InputDataObjectTypeSerializer())
 
-    def to_representation(self, instance):
-        rep = super().to_representation(instance)
-        # Sort applicationInputs by 'inputOrder'
-        if rep['applicationInputs'] is not None:
-            rep['applicationInputs'].sort(
-                key=lambda input: input['inputOrder'])
-        return rep
 
-    def to_internal_value(self, data):
-        validated_data = super().to_internal_value(data)
-        # Update application input order based on order in array
-        app_inputs = validated_data.get('applicationInputs', [])
-        if app_inputs is not None:
-            for i in range(len(app_inputs)):
-                app_inputs[i]['inputOrder'] = i
-        return validated_data
+class CommandObjectSerializer(
+        thrift_utils.create_serializer_class(CommandObject)):
+    pass
+
+
+class SetEnvPathsSerializer(
+        thrift_utils.create_serializer_class(SetEnvPaths)):
+    pass
 
 
 class ApplicationDeploymentDescriptionSerializer(
@@ -327,6 +298,24 @@ class ApplicationDeploymentDescriptionSerializer(
         lookup_field='appDeploymentId',
         lookup_url_kwarg='app_deployment_id')
     userHasWriteAccess = serializers.SerializerMethodField()
+    moduleLoadCmds = OrderedListField(
+        order_by='commandOrder',
+        child=CommandObjectSerializer())
+    preJobCommands = OrderedListField(
+        order_by='commandOrder',
+        child=CommandObjectSerializer())
+    postJobCommands = OrderedListField(
+        order_by='commandOrder',
+        child=CommandObjectSerializer())
+    libPrependPaths = OrderedListField(
+        order_by='envPathOrder',
+        child=SetEnvPathsSerializer())
+    libAppendPaths = OrderedListField(
+        order_by='envPathOrder',
+        child=SetEnvPathsSerializer())
+    setEnvironment = OrderedListField(
+        order_by='envPathOrder',
+        child=SetEnvPathsSerializer())
 
     def get_userHasWriteAccess(self, appDeployment):
         request = self.context['request']
@@ -334,22 +323,6 @@ class ApplicationDeploymentDescriptionSerializer(
             request.authz_token, appDeployment.appDeploymentId,
             ResourcePermissionType.WRITE)
 
-    def to_representation(self, instance):
-        rep = super().to_representation(instance)
-        if rep['moduleLoadCmds'] is not None:
-            rep['moduleLoadCmds'].sort(
-                key=lambda cmd: cmd['commandOrder'])
-        return rep
-
-    def to_internal_value(self, data):
-        validated_data = super().to_internal_value(data)
-        # Update application input order based on order in array
-        module_load_cmds = validated_data.get('moduleLoadCmds', [])
-        if module_load_cmds is not None:
-            for i in range(len(module_load_cmds)):
-                module_load_cmds[i]['commandOrder'] = i
-        return validated_data
-
 
 class ComputeResourceDescriptionSerializer(thrift_utils.create_serializer_class(ComputeResourceDescription)):
     pass
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 bfb9569..037201d 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
@@ -23,7 +23,7 @@ import OutputDataObjectType from './models/OutputDataObjectType'
 import ParallelismType from './models/ParallelismType'
 import Project from './models/Project'
 import ResourcePermissionType from './models/ResourcePermissionType'
-import SetEnvPath from './models/SetEnvPath'
+import SetEnvPaths from './models/SetEnvPaths'
 import SharedEntity from './models/SharedEntity'
 import SummaryType from './models/SummaryType'
 import UserPermission from './models/UserPermission'
@@ -76,7 +76,7 @@ exports.models = {
   ParallelismType,
   Project,
   ResourcePermissionType,
-  SetEnvPath,
+  SetEnvPaths,
   SharedEntity,
   SummaryType,
   UserPermission,
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationDeploymentDescription.js
b/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationDeploymentDescription.js
index 1f35bfb..e660cfe 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationDeploymentDescription.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationDeploymentDescription.js
@@ -1,7 +1,7 @@
 import BaseModel from './BaseModel'
 import ParallelismType from './ParallelismType';
 import CommandObject from './CommandObject';
-import SetEnvPath from './SetEnvPath';
+import SetEnvPaths from './SetEnvPaths';
 
 
 const FIELDS = [
@@ -22,17 +22,17 @@ const FIELDS = [
   },
   {
     name: 'libPrependPaths',
-    type: SetEnvPath,
+    type: SetEnvPaths,
     list: true,
   },
   {
     name: 'libAppendPaths',
-    type: SetEnvPath,
+    type: SetEnvPaths,
     list: true,
   },
   {
     name: 'setEnvironment',
-    type: SetEnvPath,
+    type: SetEnvPaths,
     list: true,
   },
   {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ComputeResourceDescription.js
b/django_airavata/apps/api/static/django_airavata_api/js/models/ComputeResourceDescription.js
index f59d770..1ba15bb 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/ComputeResourceDescription.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ComputeResourceDescription.js
@@ -35,7 +35,7 @@ const FIELDS = [
     'defaultWalltime',
 ]
 
-export default class FullExperiment extends BaseModel {
+export default class ComputeResourceDescription extends BaseModel {
     constructor(data = {}) {
         super(FIELDS, data);
     }
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/SetEnvPath.js b/django_airavata/apps/api/static/django_airavata_api/js/models/SetEnvPaths.js
similarity index 83%
rename from django_airavata/apps/api/static/django_airavata_api/js/models/SetEnvPath.js
rename to django_airavata/apps/api/static/django_airavata_api/js/models/SetEnvPaths.js
index 88165c7..2d3595c 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/SetEnvPath.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/SetEnvPaths.js
@@ -8,7 +8,7 @@ const FIELDS = [
   'envPathOrder',
 ];
 
-export default class SetEnvPath extends BaseModel {
+export default class SetEnvPaths extends BaseModel {
 
   constructor(data = {}) {
     super(FIELDS, data);


Mime
View raw message