airavata-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From machris...@apache.org
Subject [airavata-django-portal] 02/02: AIRAVATA-2876 Removing Vuex store, keeping track of unsaved changes
Date Thu, 20 Sep 2018 17:49:56 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 9a1dac485f471cd80163f2063b6f82c71b1f5425
Author: Marcus Christie <machrist@iu.edu>
AuthorDate: Thu Sep 20 13:49:30 2018 -0400

    AIRAVATA-2876 Removing Vuex store, keeping track of unsaved changes
---
 .../applications/ApplicationDeploymentEditor.vue   |  26 +-
 .../applications/ApplicationDeploymentsList.vue    |   6 +-
 .../applications/ApplicationEditorContainer.vue    | 617 ++++++++++++++-------
 .../applications/ApplicationInterfaceEditor.vue    |  10 -
 .../applications/ApplicationModuleEditor.vue       |  13 -
 .../js/errors/UnhandledErrorDispatcher.js          |  37 +-
 .../static/common/js/components/ShareButton.vue    |  16 +-
 7 files changed, 454 insertions(+), 271 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 407d3a3..c898a1d 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
@@ -1,8 +1,5 @@
 <template>
   <div>
-    <confirmation-dialog ref="unsavedChangesDialog" title="You have unsaved changes">
-      You have unsaved changes. Are you sure you want to leave this page?
-    </confirmation-dialog>
     <div class="row">
       <div class="col">
         <h1 class="h4 mb-4">
@@ -49,16 +46,6 @@
         </b-form-group>
       </div>
     </div>
-    <div class="row mb-4">
-      <div class="col">
-        <b-button variant="primary" @click="save" :disabled="readonly">
-          Save
-        </b-button>
-        <b-button variant="secondary" @click="cancel">
-          Cancel
-        </b-button>
-      </div>
-    </div>
   </div>
 </template>
 
@@ -92,8 +79,7 @@ export default {
   components: {
     CommandObjectsEditor,
     SetEnvPathsEditor,
-    "share-button": components.ShareButton,
-    "confirmation-dialog": components.ConfirmationDialog
+    "share-button": components.ShareButton
   },
   data() {
     return {
@@ -193,21 +179,13 @@ export default {
     },
     sharingChanged(newSharedEntity) {
       this.dirty = true;
-      this.$emit("sharing-changed", newSharedEntity);
+      this.$emit("sharing-changed", newSharedEntity, this.data);
     }
   },
   watch: {
     sharedEntity(newValue, oldValue) {
       this.localSharedEntity = newValue.clone();
     }
-  },
-  beforeRouteLeave(to, from, next) {
-    if (this.dirty) {
-      this.$refs.unsavedChangesDialog.show();
-      this.$refs.unsavedChangesDialog.$on("ok", next);
-    } else {
-      next();
-    }
   }
 };
 </script>
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentsList.vue
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentsList.vue
index bb1f1f9..ff33354 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentsList.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentsList.vue
@@ -9,7 +9,11 @@
               View
               <i class="fa fa-eye" aria-hidden="true"></i>
             </router-link>
-            <router-link v-if="data.item.userHasWriteAccess" :to="{name: 'application_deployment',
params: {id: id, deployment_id: data.item.appDeploymentId, readonly: false}}">
+            <router-link v-if="data.item.userHasWriteAccess && data.item.appDeploymentId"
:to="{name: 'application_deployment', params: {id: id, deployment_id: data.item.appDeploymentId,
readonly: false}}">
+              Edit
+              <i class="fa fa-edit" aria-hidden="true"></i>
+            </router-link>
+            <router-link v-if="data.item.userHasWriteAccess && !data.item.appDeploymentId"
:to="{name: 'new_application_deployment', params: {id: id, hostId: data.item.computeHostId,
readonly: false}}">
               Edit
               <i class="fa fa-edit" aria-hidden="true"></i>
             </router-link>
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue
index 58bb9b5..b83bc1c 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue
@@ -18,51 +18,69 @@
           <b-nav-item exact-active-class="active" exact :to="{name: 'application_interface',
params: {id: id}}" :disabled="!id">Interface</b-nav-item>
           <b-nav-item active-class="active" :to="{name: 'application_deployments', params:
{id: id}}" :disabled="!id">Deployments</b-nav-item>
         </b-nav>
-        <router-view name="module" v-if="module" v-model="module" @save="saveModule" @cancel="cancelModule"
@delete="deleteApplication"
-          @input="moduleIsDirty = true" :readonly="!module.userHasWriteAccess" />
-        <router-view name="interface" v-if="appInterface" v-model="appInterface" @save="saveInterface"
@cancel="cancelInterface"
-          @input="interfaceIsDirty = true" :readonly="!appInterface.userHasWriteAccess" />
-        <router-view name="deployments" v-if="deployments" :deployments="deployments"
@new="createNewDeployment" @delete="deleteDeployment"
+        <router-view name="module" v-if="appModule" v-model="appModule" @input="appModuleIsDirty
= true" :readonly="!appModule.userHasWriteAccess"
         />
-        <router-view name="deployment" v-if="deployment" v-model="deployment" :shared-entity="deploymentSharedEntity"
@sharing-changed="deploymentSharingChanged"
-          @input="deploymentIsDirty = true" @save="saveDeployment" @cancel="cancelDeployment"
/>
+        <router-view name="interface" v-if="appInterface" v-model="appInterface" @input="appInterfaceIsDirty
= true" :readonly="!appInterface.userHasWriteAccess"
+        />
+        <router-view name="deployments" v-if="appDeployments" :deployments="appDeployments"
@new="createNewDeployment" @delete="deleteApplicationDeployment"
+        />
+        <router-view name="deployment" v-if="currentDeployment" v-model="currentDeployment"
:shared-entity="currentDeploymentSharedEntity"
+          @sharing-changed="deploymentSharingChanged" @input="currentDeploymentChanged" />
+      </div>
+    </div>
+    <div class="row">
+      <div class="col">
+        <b-button variant="primary" @click="saveAll" :disabled="readonly">
+          Save
+        </b-button>
+        <delete-button v-if="id" :disabled="readonly" @delete="deleteApplication">
+          Are you sure you want to delete the {{ appModule ? appModule.appModuleName : ""
}} application?
+        </delete-button>
+        <b-button variant="secondary" @click="cancel">
+          Cancel
+        </b-button>
       </div>
     </div>
   </div>
 </template>
 
 <script>
-import { mapActions, mapState } from "vuex";
-import { models, services } from "django-airavata-api";
+import { errors, models, services } from "django-airavata-api";
 import { components, notifications } from "django-airavata-common-ui";
 
 export default {
   name: "application-editor-container",
   props: {
     id: String,
+    // TODO: camelCase this
     deployment_id: String,
     hostId: String
   },
   components: {
     "unsaved-changes-guard": components.UnsavedChangesGuard,
-    "confirmation-dialog": components.ConfirmationDialog
+    "confirmation-dialog": components.ConfirmationDialog,
+    "delete-button": components.DeleteButton
   },
   data: function() {
     return {
-      module: null,
+      appModule: null,
       appInterface: null,
-      deployment: null,
-      deploymentSharedEntity: null,
-      moduleIsDirty: false,
-      interfaceIsDirty: false,
-      deploymentIsDirty: false
+      appDeployments: [],
+      // Map key is computeHostId, value is SharedEntity
+      appDeploymentsSharedEntities: {},
+      currentDeployment: null,
+      currentDeploymentSharedEntity: null,
+      appModuleIsDirty: false,
+      appInterfaceIsDirty: false,
+      dirtyAppDeploymentComputeHostIds: [],
+      dirtyAppDeploymentSharedEntityComputeHostIds: []
     };
   },
   computed: {
     title: function() {
       if (this.id) {
-        return this.module && this.module.appModuleName
-          ? this.module.appModuleName
+        return this.appModule && this.appModule.appModuleName
+          ? this.appModule.appModuleName
           : "";
       } else {
         return "Create a New Application";
@@ -70,221 +88,423 @@ export default {
     },
     isDirty() {
       return (
-        this.moduleIsDirty || this.interfaceIsDirty || this.deploymentIsDirty
+        this.appModuleIsDirty ||
+        this.appInterfaceIsDirty ||
+        this.dirtyAppDeploymentComputeHostIds.length > 0 ||
+        this.dirtyAppDeploymentSharedEntityComputeHostIds.length > 0
       );
     },
-    ...mapState("applications/modules", ["currentModule"]),
-    ...mapState("applications/interfaces", ["currentInterface"]),
-    ...mapState("applications/deployments", [
-      "currentDeployment",
-      "deployments"
-    ])
+    readonly() {
+      return this.appModule && !this.appModule.userHasWriteAccess;
+    }
   },
   created() {
     this.initialize();
-    if (this.deployment_id) {
-      this.loadApplicationDeployment(this.deployment_id);
-      services.SharedEntityService.retrieve({
-        lookup: this.deployment_id
-      }).then(sharedEntity => (this.deploymentSharedEntity = sharedEntity));
-    } else if (this.hostId) {
-      this.createNewDeployment(this.hostId);
-    }
   },
   methods: {
-    ...mapActions("applications/modules", [
-      "loadApplicationModule",
-      "createApplicationModule",
-      "updateApplicationModule"
-    ]),
-    ...mapActions("applications/interfaces", [
-      "loadApplicationInterface",
-      "createApplicationInterface",
-      "updateApplicationInterface"
-    ]),
-    ...mapActions("applications/deployments", [
-      "loadApplicationDeployments",
-      "loadApplicationDeployment",
-      "createApplicationDeployment",
-      "updateApplicationDeployment",
-      "deleteApplicationDeployment"
-    ]),
     initialize() {
-      // TODO: move this to applications store?
-      if (this.currentModule && this.currentModule.appModuleId === this.id) {
-        this.module = this.currentModule.clone();
-        this.loadApplicationInterface(this.id).catch(error => {
-          notifications.NotificationList.addError(error);
-        });
-        this.loadApplicationDeployments(this.id);
-      } else if (this.id) {
+      if (this.id) {
         this.loadApplicationModule(this.id);
-        this.loadApplicationInterface(this.id).catch(error => {
-          notifications.NotificationList.addError(error);
+        this.loadApplicationInterface(this.id);
+        this.loadApplicationDeployments(this.id).then(appDeployments => {
+          this.initializeDeploymentEditing();
         });
-        this.loadApplicationDeployments(this.id);
       } else {
-        this.module = new models.ApplicationModule({
+        this.appModule = new models.ApplicationModule({
           userHasWriteAccess: true
         });
       }
     },
-    saveModule() {
-      if (this.id) {
-        this.updateApplicationModule(this.module).then(() => {
-          this.moduleIsDirty = false;
-          this.$router.push({ path: "/applications" });
-        });
-      } else {
-        this.createApplicationModule(this.module).then(appModule => {
-          this.moduleIsDirty = false;
-          this.$router.push({
-            name: "application_module",
-            params: { id: appModule.appModuleId }
-          });
-        });
+    initializeDeploymentEditing() {
+      if (this.deployment_id) {
+        this.startEditingExistingDeployment(this.deployment_id);
+      } else if (this.hostId) {
+        this.startEditingNewDeployment(this.hostId);
       }
     },
-    saveInterface() {
-      // Copy name and description from module
-      this.appInterface.applicationName = this.module.appModuleName;
-      this.appInterface.applicationDescription = this.module.appModuleDescription;
-
-      this.updateApplicationModule(this.module)
-        .then(appModule => {
-          if (this.appInterface.applicationInterfaceId) {
-            return this.updateApplicationInterface(this.appInterface).then(
-              () => {
-                this.interfaceIsDirty = false;
-                this.$router.push({ path: "/applications" });
-              }
-            );
+    startEditingExistingDeployment(deploymentId) {
+      this.setCurrentDeploymentFromAppDeploymentId(deploymentId).then(
+        appDeployment =>
+          this.setCurrentApplicationDeploymentSharedEntity(appDeployment)
+      );
+    },
+    startEditingNewDeployment(computeHostId) {
+      this.setCurrentDeploymentFromComputeHostId(computeHostId).then(
+        appDeployment =>
+          this.setCurrentApplicationDeploymentSharedEntity(appDeployment)
+      );
+    },
+    loadApplicationModule(appModuleId) {
+      return services.ApplicationModuleService.retrieve({
+        lookup: appModuleId
+      }).then(appModule => {
+        this.appModuleIsDirty = false;
+        this.appModule = appModule;
+      });
+    },
+    createApplicationModule(appModule) {
+      return services.ApplicationModuleService.create({ data: appModule }).then(
+        appModule => {
+          this.appModuleIsDirty = false;
+          this.appModule = appModule;
+          return appModule;
+        }
+      );
+    },
+    updateApplicationModule(appModule) {
+      return services.ApplicationModuleService.update({
+        lookup: appModule.appModuleId,
+        data: appModule
+      }).then(appModule => {
+        this.appModuleIsDirty = false;
+        this.appModule = appModule;
+        return appModule;
+      });
+    },
+    saveApplicationModule(appModule) {
+      return this.id
+        ? this.updateApplicationModule(appModule)
+        : this.createApplicationModule(appModule);
+    },
+    deleteApplicationModule(appModule) {
+      const deleteModule = this.id
+        ? services.ApplicationModuleService.delete({
+            lookup: this.id
+          })
+        : Promise.resolve(null);
+      return deleteModule.then(() => {
+        this.appModuleIsDirty = false;
+        this.appModule = null;
+      });
+    },
+    loadApplicationInterface(appModuleId) {
+      return services.ApplicationModuleService.getApplicationInterface(
+        { lookup: appModuleId },
+        { ignoreErrors: true }
+      )
+        .then(appInterface => {
+          this.appInterfaceIsDirty = false;
+          this.appInterface = appInterface;
+          return appInterface;
+        })
+        .catch(error => {
+          if (error.details.status === 404) {
+            // If there is no interface, just create a new instance
+            const appInterface = new models.ApplicationInterfaceDefinition({
+              userHasWriteAccess: true
+            });
+            appInterface.addStandardOutAndStandardErrorOutputs();
+            this.appInterface = appInterface;
+            return Promise.resolve(null);
           } else {
-            this.appInterface.applicationModules = [this.id];
-            return this.createApplicationInterface(this.appInterface).then(
-              () => {
-                this.interfaceIsDirty = false;
-                this.$router.push({ path: "/applications" });
-              }
-            );
+            throw error;
           }
         })
-        .catch(error => notifications.NotificationList.addError(error));
+        .catch(errors.UnhandledErrorDispatcher.reportUnhandledError);
+    },
+    createApplicationInterface(appInterface) {
+      return services.ApplicationInterfaceService.create({
+        data: appInterface
+      }).then(appInterface => {
+        this.appInterfaceIsDirty = false;
+        this.appInterface = appInterface;
+        return appInterface;
+      });
+    },
+    updateApplicationInterface(appInterface) {
+      return services.ApplicationInterfaceService.update({
+        lookup: appInterface.applicationInterfaceId,
+        data: appInterface
+      }).then(appInterface => {
+        this.appInterfaceIsDirty = false;
+        this.appInterface = appInterface;
+        return appInterface;
+      });
+    },
+    saveApplicationInterface(appInterface) {
+      appInterface.applicationName = this.appModule.appModuleName;
+      appInterface.applicationDescription = this.appModule.appModuleDescription;
+      appInterface.applicationModules = [this.id];
+      return appInterface.applicationInterfaceId
+        ? this.updateApplicationInterface(appInterface)
+        : this.createApplicationInterface(appInterface);
+    },
+    deleteApplicationInterface(appInterface) {
+      if (appInterface.applicationInterfaceId) {
+        return services.ApplicationInterfaceService.delete({
+          lookup: appInterface.applicationInterfaceId
+        }).then(() => (this.appInterfaceIsDirty = false));
+      } else {
+        this.appInterfaceIsDirty = false;
+        this.appInterface = null;
+        return Promise.resolve(null);
+      }
+    },
+    loadApplicationDeployments(appModuleId) {
+      return services.ApplicationModuleService.getApplicationDeployments({
+        lookup: appModuleId
+      }).then(appDeployments => {
+        this.dirtyAppDeploymentComputeHostIds = [];
+        this.appDeployments = appDeployments;
+        return appDeployments;
+      });
+    },
+    loadApplicationDeployment(appDeploymentId) {
+      return services.ApplicationDeploymentService.retrieve({
+        lookup: appDeploymentId
+      }).then(appDeployment => {
+        this.currentDeployment = appDeployment;
+        return appDeployment;
+      });
+    },
+    createApplicationDeployment(appDeployment) {
+      return services.ApplicationDeploymentService.create({
+        data: appDeployment
+      }).then(appDeployment => {
+        this.removeDirtyAppDeploymentComputeHostId(appDeployment);
+        this.replaceAppDeployment(appDeployment);
+        return appDeployment;
+      });
+    },
+    updateApplicationDeployment(appDeployment) {
+      return services.ApplicationDeploymentService.update({
+        lookup: appDeployment.appDeploymentId,
+        data: appDeployment
+      }).then(appDeployment => {
+        this.removeDirtyAppDeploymentComputeHostId(appDeployment);
+        this.replaceAppDeployment(appDeployment);
+        return appDeployment;
+      });
+    },
+    saveApplicationDeployment(appDeployment) {
+      return appDeployment.appDeploymentId
+        ? this.updateApplicationDeployment(appDeployment)
+        : this.createApplicationDeployment(appDeployment);
+    },
+    deleteApplicationDeployment(appDeployment) {
+      if (appDeployment.appDeploymentId) {
+        return services.ApplicationDeploymentService.delete({
+          lookup: appDeployment.appDeploymentId
+        }).then(() => {
+          this.removeDirtyAppDeploymentComputeHostId(appDeployment);
+          return this.loadApplicationDeployments(this.id);
+        });
+      } else {
+        const depIndex = this.appDeployments.findIndex(
+          dep => dep.computeHostId === appDeployment.computeHostId
+        );
+        this.appDeployments.splice(depIndex, 1);
+        this.removeDirtyAppDeploymentComputeHostId(appDeployment);
+        return Promise.resolve(this.appDeployments);
+      }
+    },
+    currentDeploymentChanged(appDeployment) {
+      this.replaceAppDeployment(appDeployment);
+      this.setApplicationDeploymentDirty(appDeployment);
+    },
+    replaceAppDeployment(appDeployment) {
+      const depIndex = this.appDeployments.findIndex(
+        dep => dep.computeHostId === appDeployment.computeHostId
+      );
+      this.appDeployments.splice(depIndex, 1, appDeployment);
+    },
+    setApplicationDeploymentDirty(appDeployment) {
+      if (
+        !this.dirtyAppDeploymentComputeHostIds.includes(
+          appDeployment.computeHostId
+        )
+      ) {
+        this.dirtyAppDeploymentComputeHostIds.push(appDeployment.computeHostId);
+      }
+    },
+    removeDirtyAppDeploymentComputeHostId(appDeployment) {
+      const hostIdIndex = this.dirtyAppDeploymentComputeHostIds.indexOf(
+        appDeployment.computeHostId
+      );
+      if (hostIdIndex >= 0) {
+        this.dirtyAppDeploymentComputeHostIds.splice(hostIdIndex, 1);
+      }
     },
     createNewDeployment(computeHostId) {
-      const deployment = new models.ApplicationDeploymentDescription();
-      deployment.appModuleId = this.id;
-      deployment.computeHostId = computeHostId;
-      this.deployment = deployment;
-      this.deploymentSharedEntity = new models.SharedEntity();
       this.$router.push({
         name: "new_application_deployment",
         params: { id: this.id, hostId: computeHostId }
       });
     },
-    saveDeployment() {
-      return this.saveAll()
-        .then(() => {
-          return this.loadApplicationDeployments(this.id);
-        })
-        .then(() => {
-          this.$router.push({
-            name: "application_deployments",
-            params: { id: this.id }
-          });
-        });
+    loadApplicationDeploymentSharedEntity(appDeployment) {
+      return services.SharedEntityService.retrieve({
+        lookup: appDeployment.appDeploymentId
+      }).then(sharedEntity => {
+        this.appDeploymentsSharedEntities[
+          appDeployment.computeHostId
+        ] = sharedEntity;
+        return sharedEntity;
+      });
     },
-    saveAll() {
-      const moduleSave = this.id
-        ? this.updateApplicationModule(this.module)
-        : this.createApplicationModule(this.module);
-      return moduleSave
-        .then(appModule => {
-          this.moduleIsDirty = false;
-          this.appInterface.applicationName = appModule.appModuleName;
-          this.appInterface.applicationDescription =
-            appModule.appModuleDescription;
-
-          if (this.appInterface.applicationInterfaceId) {
-            return this.updateApplicationInterface(this.appInterface);
-          } else {
-            this.appInterface.applicationModules = [this.id];
-            return this.createApplicationInterface(this.appInterface);
-          }
-        })
-        .then(appInterface => {
-          this.interfaceIsDirty = false;
-          if (this.deployment) {
-            if (this.deployment.appDeploymentId) {
-              return this.updateApplicationDeployment(this.deployment);
-            } else {
-              return this.createApplicationDeployment(this.deployment).then(
-                deployment => {
-                  return services.SharedEntityService.merge({
-                    data: this.deploymentSharedEntity,
-                    lookup: deployment.appDeploymentId
-                  });
-                }
-              );
-            }
-          } else {
-            return Promise.resolve(null);
-          }
-        })
-        .then(() => (this.deploymentIsDirty = false));
+    setCurrentApplicationDeploymentSharedEntity(appDeployment) {
+      if (appDeployment.computeHostId in this.appDeploymentsSharedEntities) {
+        this.currentDeploymentSharedEntity = this.appDeploymentsSharedEntities[
+          appDeployment.computeHostId
+        ];
+        return Promise.resolve(this.currentDeploymentSharedEntity);
+      } else if (appDeployment.appDeploymentId) {
+        return this.loadApplicationDeploymentSharedEntity(appDeployment).then(
+          sharedEntity => (this.currentDeploymentSharedEntity = sharedEntity)
+        );
+      } else {
+        throw new Error(
+          "Could not find shared entity in local map and cannot fetch"
+        );
+      }
     },
-    cancelModule() {
-      this.moduleIsDirty = false;
-      this.$router.push({ path: "/applications" });
+    deploymentSharingChanged(deploymentSharedEntity, appDeployment) {
+      this.currentDeploymentSharedEntity = deploymentSharedEntity;
+      this.replaceAppDeploymentSharedEntity(
+        deploymentSharedEntity,
+        appDeployment
+      );
+      this.setApplicationDeploymentSharedEntityDirty(
+        deploymentSharedEntity,
+        appDeployment
+      );
     },
-    cancelInterface() {
-      this.interfaceIsDirty = false;
-      this.$router.push({ path: "/applications" });
+    mergeSharedEntity(sharedEntity, appDeployment) {
+      return services.SharedEntityService.merge({
+        data: sharedEntity,
+        lookup: appDeployment.appDeploymentId
+      }).then(sharedEntity => {
+        this.replaceAppDeploymentSharedEntity(sharedEntity, appDeployment);
+        this.removeAppDeploymentSharedEntityDirty(sharedEntity, appDeployment);
+        return sharedEntity;
+      });
     },
-    cancelDeployment() {
-      this.deploymentIsDirty = false;
-      this.$router.push({
-        name: "application_deployments",
-        params: { id: this.id }
+    updateSharedEntity(sharedEntity, appDeployment) {
+      return services.SharedEntityService.update({
+        data: sharedEntity,
+        lookup: appDeployment.appDeploymentId
+      }).then(sharedEntity => {
+        this.replaceAppDeploymentSharedEntity(sharedEntity, appDeployment);
+        this.removeAppDeploymentSharedEntityDirty(sharedEntity, appDeployment);
+        return sharedEntity;
       });
     },
-    deleteDeployment(deployment) {
-      return this.deleteApplicationDeployment(deployment)
+    saveSharedEntity(sharedEntity, appDeployment) {
+      return sharedEntity.entityId
+        ? this.updateSharedEntity(sharedEntity, appDeployment)
+        : this.mergeSharedEntity(sharedEntity, appDeployment);
+    },
+    setApplicationDeploymentSharedEntityDirty(sharedEntity, appDeployment) {
+      if (
+        !this.dirtyAppDeploymentSharedEntityComputeHostIds.includes(
+          appDeployment.computeHostId
+        )
+      ) {
+        this.dirtyAppDeploymentSharedEntityComputeHostIds.push(
+          appDeployment.computeHostId
+        );
+      }
+    },
+    removeAppDeploymentSharedEntityDirty(sharedEntity, appDeployment) {
+      const hostIdIndex = this.dirtyAppDeploymentSharedEntityComputeHostIds.indexOf(
+        appDeployment.computeHostId
+      );
+      if (hostIdIndex >= 0) {
+        this.dirtyAppDeploymentSharedEntityComputeHostIds.splice(
+          hostIdIndex,
+          1
+        );
+      }
+    },
+    replaceAppDeploymentSharedEntity(sharedEntity, appDeployment) {
+      this.appDeploymentsSharedEntities[
+        appDeployment.computeHostId
+      ] = sharedEntity;
+    },
+    setCurrentDeploymentFromAppDeploymentId(appDeploymentId) {
+      this.currentDeployment = this.appDeployments.find(
+        dep => dep.appDeploymentId === appDeploymentId
+      );
+      if (!this.currentDeployment) {
+        throw new Error(
+          "Unable to find deployment from appDeploymentId=" + appDeploymentId
+        );
+      }
+      return Promise.resolve(this.currentDeployment);
+      this.setCurrentApplicationDeploymentSharedEntity(this.currentDeployment);
+    },
+    setCurrentDeploymentFromComputeHostId(computeHostId) {
+      this.currentDeployment = this.appDeployments.find(
+        dep => dep.computeHostId === computeHostId
+      );
+      if (!this.currentDeployment) {
+        // Create a new deployment
+        const deployment = new models.ApplicationDeploymentDescription({
+          userHasWriteAccess: true
+        });
+        deployment.appModuleId = this.id;
+        deployment.computeHostId = computeHostId;
+        this.currentDeployment = deployment;
+        this.appDeployments.push(deployment);
+        this.setApplicationDeploymentDirty(deployment);
+        this.appDeploymentsSharedEntities[
+          computeHostId
+        ] = new models.SharedEntity();
+      }
+      return Promise.resolve(this.currentDeployment);
+    },
+    saveAll() {
+      // TODO: catch errors and navigate to the route showing the errors
+      const moduleSave = this.appModuleIsDirty
+        ? this.saveApplicationModule(this.appModule)
+        : Promise.resolve(this.appModule);
+      const interfaceSave = moduleSave.then(
+        this.appInterfaceIsDirty
+          ? this.saveApplicationInterface(this.appInterface)
+          : Promise.resolve(this.appInterface)
+      );
+      const deploymentsSave = interfaceSave
         .then(() => {
-          this.deploymentIsDirty = false;
-          return this.loadApplicationDeployments(this.id);
+          return Promise.all(
+            this.dirtyAppDeploymentComputeHostIds.map(computeHostId => {
+              const deployment = this.appDeployments.find(
+                dep => dep.computeHostId === computeHostId
+              );
+              return this.saveApplicationDeployment(deployment);
+            })
+          );
         })
         .then(() => {
-          this.$router.push({
-            name: "application_deployments",
-            params: { id: this.id }
-          });
+          return Promise.all(
+            this.dirtyAppDeploymentSharedEntityComputeHostIds.map(
+              computeHostId => {
+                const sharedEntity = this.appDeploymentsSharedEntities[
+                  computeHostId
+                ];
+                const deployment = this.appDeployments.find(
+                  dep => dep.computeHostId === computeHostId
+                );
+                return this.saveSharedEntity(sharedEntity, deployment);
+              }
+            )
+          );
+        })
+        .then(() => {
+          // Reinitialize deployment editing so that deployment being edited is
+          // the saved instance
+          this.initializeDeploymentEditing();
         });
     },
-    deploymentSharingChanged(deploymentSharedEntity) {
-      this.deploymentSharedEntity = deploymentSharedEntity;
+    cancel() {
+      this.$router.push({ path: "/applications" });
     },
     deleteApplication(appModule) {
-      const deleteAllDeployments = this.deployments.map(dep =>
-        services.ApplicationDeploymentService.delete({
-          lookup: dep.appDeploymentId
-        })
+      const deleteAllDeployments = this.appDeployments.map(dep =>
+        this.deleteApplicationDeployment(dep)
       );
       return Promise.all(deleteAllDeployments)
+        .then(() => this.deleteApplicationInterface(this.appInterface))
+        .then(() => this.deleteApplicationModule(this.appModule))
         .then(() => {
-          this.deploymentIsDirty = false;
-          if (this.appInterface && this.appInterface.applicationInterfaceId) {
-            return services.ApplicationInterfaceService.delete({
-              lookup: this.appInterface.applicationInterfaceId
-            });
-          }
-        })
-        .then(() => {
-          this.interfaceIsDirty = false;
-          return services.ApplicationModuleService.delete({ lookup: this.id });
-        })
-        .then(() => {
-          this.deploymentIsDirty = false;
           this.$router.push({ path: "/applications" });
         });
     }
@@ -294,22 +514,7 @@ export default {
       if (to.params.id !== from.params.id) {
         this.initialize();
       }
-      if (this.deployment_id) {
-        this.loadApplicationDeployment(this.deployment_id);
-        services.SharedEntityService.retrieve({
-          lookup: this.deployment_id
-        }).then(sharedEntity => (this.deploymentSharedEntity = sharedEntity));
-      }
-    },
-    currentModule: function(newModule) {
-      // Clone the module from the store so we can modify it locally
-      this.module = newModule.clone();
-    },
-    currentInterface: function(newInterface) {
-      this.appInterface = newInterface.clone();
-    },
-    currentDeployment: function(newDeployment) {
-      this.deployment = newDeployment ? newDeployment.clone() : null;
+      this.initializeDeploymentEditing();
     }
   },
   beforeRouteLeave(to, from, next) {
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue
index 2186bfd..00db14f 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue
@@ -55,16 +55,6 @@
         </b-button>
       </div>
     </div>
-    <div class="row mb-4">
-      <div class="col">
-        <b-button variant="primary" @click="save" :disabled="readonly">
-          Save
-        </b-button>
-        <b-button variant="secondary" @click="cancel">
-          Cancel
-        </b-button>
-      </div>
-    </div>
   </div>
 </template>
 
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationModuleEditor.vue
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationModuleEditor.vue
index baf5c7f..8964c71 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationModuleEditor.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationModuleEditor.vue
@@ -16,19 +16,6 @@
         </b-form-group>
       </div>
     </div>
-    <div class="row">
-      <div class="col">
-        <b-button variant="primary" @click="save" :disabled="readonly">
-          Save
-        </b-button>
-        <delete-button v-if="data.appModuleId" :disabled="readonly" @delete="deleteApplicationModule">
-          Are you sure you want to delete the {{ data.appModuleName }} application?
-        </delete-button>
-        <b-button variant="secondary" @click="cancel">
-          Cancel
-        </b-button>
-      </div>
-    </div>
   </div>
 </template>
 
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/errors/UnhandledErrorDispatcher.js
b/django_airavata/apps/api/static/django_airavata_api/js/errors/UnhandledErrorDispatcher.js
index 1a8cee5..32f434d 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/errors/UnhandledErrorDispatcher.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/errors/UnhandledErrorDispatcher.js
@@ -1,18 +1,33 @@
-import ErrorReporter from './ErrorReporter'
-import UnhandledError from './UnhandledError'
-import UnhandledErrorList from './UnhandledErrorDisplayList';
+import ErrorReporter from "./ErrorReporter";
+import UnhandledError from "./UnhandledError";
+import UnhandledErrorList from "./UnhandledErrorDisplayList";
 
 class UnhandledErrorDispatcher {
+  reportError({
+    message = null,
+    error = null,
+    details = null,
+    suppressDisplay = false,
+    suppressLogging = false
+  }) {
+    const unhandledError = new UnhandledError({
+      message,
+      error,
+      details,
+      suppressDisplay,
+      suppressLogging
+    });
+    this.reportUnhandledError(unhandledError);
+  }
 
-    reportError({ message = null, error = null, details = null, suppressDisplay = false,
suppressLogging = false }) {
-        const unhandledError = new UnhandledError({message, error, details, suppressDisplay,
suppressLogging});
-        if (!unhandledError.suppressDisplay) {
-            UnhandledErrorList.add(unhandledError);
-        }
-        if (!unhandledError.suppressLogging) {
-            ErrorReporter.reportUnhandledError(unhandledError);
-        }
+  reportUnhandledError(unhandledError) {
+    if (!unhandledError.suppressDisplay) {
+      UnhandledErrorList.add(unhandledError);
     }
+    if (!unhandledError.suppressLogging) {
+      ErrorReporter.reportUnhandledError(unhandledError);
+    }
+  }
 }
 
 export default new UnhandledErrorDispatcher();
diff --git a/django_airavata/static/common/js/components/ShareButton.vue b/django_airavata/static/common/js/components/ShareButton.vue
index 251a17b..3a93278 100644
--- a/django_airavata/static/common/js/components/ShareButton.vue
+++ b/django_airavata/static/common/js/components/ShareButton.vue
@@ -244,12 +244,16 @@ export default {
       if (!this.sharedEntity.groupPermissions) {
         this.sharedEntity.groupPermissions = [];
       }
-      this.sharedEntity.groupPermissions.push(
-        new models.GroupPermission({
-          group: group,
-          permissionType: models.ResourcePermissionType.READ
-        })
-      );
+      if (
+        !this.sharedEntity.groupPermissions.find(gp => gp.group.id === group.id)
+      ) {
+        this.sharedEntity.groupPermissions.push(
+          new models.GroupPermission({
+            group: group,
+            permissionType: models.ResourcePermissionType.READ
+          })
+        );
+      }
     },
     saveSharedEntity: function(event) {
       this.emitValueChanged();


Mime
View raw message