airavata-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From machris...@apache.org
Subject [airavata-django-portal] 01/03: AIRAVATA-2840 Add ShareButton to credential store UI
Date Fri, 28 Sep 2018 15:49:38 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 ab611d30bbe8713637f4e037769e1632d484869a
Author: Marcus Christie <machrist@iu.edu>
AuthorDate: Thu Sep 27 16:37:47 2018 -0400

    AIRAVATA-2840 Add ShareButton to credential store UI
---
 .../GroupComputeResourcePreference.vue             |  17 +-
 .../applications/ApplicationDeploymentEditor.vue   |   9 +-
 .../applications/ApplicationEditorContainer.vue    |  18 +-
 .../dashboards/CredentialStoreDashboard.vue        |   4 +-
 .../django_airavata_api/js/models/SharedEntity.js  |  40 ++-
 .../static/common/js/components/ShareButton.vue    | 313 +++++++--------------
 .../common/js/components/SharedEntityEditor.vue    | 223 +++++++++++++++
 django_airavata/static/common/js/index.js          |   6 +
 .../static/common/js/mixins/VModelMixin.js         |  56 ++++
 9 files changed, 440 insertions(+), 246 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/GroupComputeResourcePreference.vue
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/GroupComputeResourcePreference.vue
index 74a85ea..419dd2f 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/GroupComputeResourcePreference.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/GroupComputeResourcePreference.vue
@@ -13,7 +13,7 @@
               <b-form-input id="profile-name" type="text" v-model="data.groupResourceProfileName"
required placeholder="Name of this Group Resource Profile">
               </b-form-input>
             </b-form-group>
-            <share-button ref="shareButton" v-model="sharedEntity" @save="saveSharedEntity"
/>
+            <share-button ref="shareButton" :entity-id="id" />
           </div>
         </div>
       </div>
@@ -86,9 +86,6 @@ export default {
           .retrieve({ lookup: this.id })
           .then(grp => (this.data = grp));
       }
-      services.SharedEntityService.retrieve({ lookup: this.id }).then(
-        sharedEntity => (this.sharedEntity = sharedEntity)
-      );
     }
   },
   data: function() {
@@ -96,7 +93,6 @@ export default {
     return {
       data: data,
       service: services.ServiceFactory.service("GroupResourceProfiles"),
-      sharedEntity: null,
       computePreferencesFields: [
         {
           label: "Name",
@@ -155,22 +151,13 @@ export default {
           // Merge sharing settings with default sharing settings created when
           // Group Resource Profile was created
           const groupResourceProfileId = data.groupResourceProfileId;
-          return services.SharedEntityService.merge({
-            data: this.sharedEntity,
-            lookup: groupResourceProfileId
-          });
+          return this.$refs.shareButton.mergeAndSave(groupResourceProfileId);
         });
       }
       persist.then(data => {
         this.$router.push("/group-resource-profiles");
       });
     },
-    saveSharedEntity: function(sharedEntity) {
-      return services.SharedEntityService.update({
-        data: sharedEntity,
-        lookup: sharedEntity.entityId
-      });
-    },
     getComputeResourceName: function(computeResourceId) {
       // TODO: load compute resources to get the real name
       return computeResourceId && computeResourceId.indexOf("_") > 0
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 700d176..46807ce 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
@@ -5,7 +5,7 @@
         <h1 class="h4 mb-4">
           {{ name }}
         </h1>
-        <share-button v-model="localSharedEntity" @input="sharingChanged" />
+        <share-button :shared-entity="localSharedEntity" @saved="savedSharedEntity" @unsaved="unsavedSharedEntity"
/>
         <b-form-group label="Application Executable Path" label-for="executable-path">
           <b-form-input id="executable-path" type="text" v-model="data.executablePath"
required :disabled="readonly"></b-form-input>
         </b-form-group>
@@ -177,9 +177,12 @@ export default {
       this.data.defaultCPUCount = queue.defaultCPUCount;
       this.data.defaultWalltime = queue.defaultWalltime;
     },
-    sharingChanged(newSharedEntity) {
+    savedSharedEntity(newSharedEntity) {
+      this.$emit("sharing-changed", newSharedEntity, this.data, false);
+    },
+    unsavedSharedEntity(newSharedEntity) {
       this.dirty = true;
-      this.$emit("sharing-changed", newSharedEntity, this.data);
+      this.$emit("sharing-changed", newSharedEntity, this.data, true);
     }
   },
   watch: {
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 d8c1426..762e0d1 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
@@ -350,6 +350,7 @@ export default {
         this.appDeploymentsSharedEntities[
           appDeployment.computeHostId
         ] = sharedEntity;
+        this.removeAppDeploymentSharedEntityDirty(sharedEntity, appDeployment);
         return sharedEntity;
       });
     },
@@ -369,16 +370,23 @@ export default {
         );
       }
     },
-    deploymentSharingChanged(deploymentSharedEntity, appDeployment) {
+    deploymentSharingChanged(deploymentSharedEntity, appDeployment, dirty) {
       this.currentDeploymentSharedEntity = deploymentSharedEntity;
       this.replaceAppDeploymentSharedEntity(
         deploymentSharedEntity,
         appDeployment
       );
-      this.setApplicationDeploymentSharedEntityDirty(
-        deploymentSharedEntity,
-        appDeployment
-      );
+      if (dirty) {
+        this.setApplicationDeploymentSharedEntityDirty(
+          deploymentSharedEntity,
+          appDeployment
+        );
+      } else {
+        this.removeAppDeploymentSharedEntityDirty(
+          deploymentSharedEntity,
+          appDeployment
+        );
+      }
     },
     mergeSharedEntity(sharedEntity, appDeployment) {
       return services.SharedEntityService.merge({
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/dashboards/CredentialStoreDashboard.vue
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/dashboards/CredentialStoreDashboard.vue
index 30798ff..7af3803 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/dashboards/CredentialStoreDashboard.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/dashboards/CredentialStoreDashboard.vue
@@ -5,6 +5,7 @@
 
         <b-table striped hover :fields="fields" :items="slotProps.items">
           <template slot="action" slot-scope="data">
+            <share-button :entity-id="data.item.token" />
             <clipboard-copy-link :text="data.item.publicKey" class="mr-1" />
             <delete-link v-if="data.item.userHasWriteAccess" @delete="deleteSSHCredential(data.item)">
               Are you sure you want to delete this SSH credential?
@@ -29,7 +30,8 @@ export default {
     "delete-link": components.DeleteLink,
     "list-layout": layouts.ListLayout,
     ClipboardCopyLink,
-    "new-ssh-credential-modal": NewSSHCredentialModal
+    "new-ssh-credential-modal": NewSSHCredentialModal,
+    "share-button": components.ShareButton
   },
   created: function() {
     this.fetchSSHKeys();
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/SharedEntity.js
b/django_airavata/apps/api/static/django_airavata_api/js/models/SharedEntity.js
index bc556b0..464de39 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/SharedEntity.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/SharedEntity.js
@@ -1,32 +1,46 @@
-import BaseModel from './BaseModel';
-import GroupPermission from './GroupPermission';
-import UserPermission from './UserPermission';
-import UserProfile from './UserProfile';
-
+import BaseModel from "./BaseModel";
+import GroupPermission from "./GroupPermission";
+import UserPermission from "./UserPermission";
+import UserProfile from "./UserProfile";
+import ResourcePermissionType from "./ResourcePermissionType";
 
 const FIELDS = [
-  'entityId',
+  "entityId",
   {
-    name: 'userPermissions',
+    name: "userPermissions",
     type: UserPermission,
     list: true,
-    default: BaseModel.defaultNewInstance(Array),
+    default: BaseModel.defaultNewInstance(Array)
   },
   {
-    name: 'groupPermissions',
+    name: "groupPermissions",
     type: GroupPermission,
     list: true,
-    default: BaseModel.defaultNewInstance(Array),
+    default: BaseModel.defaultNewInstance(Array)
   },
   {
-    name: 'owner',
-    type: UserProfile,
+    name: "owner",
+    type: UserProfile
   },
-  'isOwner',
+  "isOwner"
 ];
 
 export default class SharedEntity extends BaseModel {
   constructor(data = {}) {
     super(FIELDS, data);
   }
+
+  addGroup(group) {
+    if (!this.groupPermissions) {
+      this.groupPermissions = [];
+    }
+    if (!this.groupPermissions.find(gp => gp.group.id === group.id)) {
+      this.groupPermissions.push(
+        new GroupPermission({
+          group: group,
+          permissionType: ResourcePermissionType.READ
+        })
+      );
+    }
+  }
 }
diff --git a/django_airavata/static/common/js/components/ShareButton.vue b/django_airavata/static/common/js/components/ShareButton.vue
index 3a93278..e5de53a 100644
--- a/django_airavata/static/common/js/components/ShareButton.vue
+++ b/django_airavata/static/common/js/components/ShareButton.vue
@@ -1,92 +1,40 @@
 <template>
-  <div id="share-button">
-    <b-button v-b-modal.modal-share-settings :variant="'outline-primary'" :title="title"
:disabled="!shareButtonEnabled">
+  <div class="share-button">
+    <b-button :variant="'outline-primary'" :title="title" :disabled="!shareButtonEnabled"
@click="openSharingSettingsModal">
       Share
       <b-badge>{{ totalCount }}</b-badge>
     </b-button>
-    <b-modal id="modal-share-settings" title="Sharing Settings" ref="modalSharingSettings"
ok-title="Save" @ok="saveSharedEntity"
+    <b-modal clas="modal-share-settings" title="Sharing Settings" ref="sharingSettingsModal"
ok-title="Save" @ok="saveSharedEntity"
       @cancel="cancelEditSharedEntity" no-close-on-esc no-close-on-backdrop hide-header-close
@show="showSharingSettingsModal">
-      <b-form-group label="Search for users/groups" labelFor="user-groups-autocomplete">
-        <autocomplete-text-input id="user-groups-autocomplete" :suggestions="usersAndGroupsSuggestions"
@selected="suggestionSelected">
-          <template slot="suggestion" slot-scope="slotProps">
-            <span v-if="slotProps.suggestion.type == 'group'">
-              <i class="fa fa-users"></i> {{ slotProps.suggestion.name }}
-            </span>
-            <span v-if="slotProps.suggestion.type == 'user'">
-              <i class="fa fa-user"></i>
-              {{ slotProps.suggestion.user.firstName }} {{ slotProps.suggestion.user.lastName
}} ({{ slotProps.suggestion.user.userId }})
-              - {{ slotProps.suggestion.user.email }}
-            </span>
-          </template>
-        </autocomplete-text-input>
-      </b-form-group>
-      <h5 v-if="totalCount > 0">Currently Shared With</h5>
-      <b-table v-if="usersCount > 0" id="modal-user-table" hover :items="sharedEntity.userPermissions"
:fields="userFields">
-        <template slot="name" slot-scope="data">
-          <span :title="data.item.user.userId">{{data.item.user.firstName}} {{data.item.user.lastName}}</span>
-        </template>
-        <template slot="email" slot-scope="data">
-          {{data.item.user.email}}
-        </template>
-        <template slot="permission" slot-scope="data">
-          <b-form-select v-model="data.item.permissionType" :options="permissionOptions"
/>
-        </template>
-        <template slot="remove" slot-scope="data">
-          <a href="#" @click.prevent="removeUser(data.item.user)">
-            <span class="fa fa-trash"></span>
-          </a>
-        </template>
-      </b-table>
-      <b-table v-if="groupsCount > 0" id="modal-group-table" hover :items="filteredGroupPermissions"
:fields="groupFields">
-        <template slot="name" slot-scope="data">
-          {{data.item.group.name}}
-        </template>
-        <template slot="permission" slot-scope="data">
-          <b-form-select v-model="data.item.permissionType" :options="permissionOptions"
/>
-        </template>
-        <template slot="remove" slot-scope="data">
-          <a href="#" @click.prevent="removeGroup(data.item.group)">
-            <span class="fa fa-trash"></span>
-          </a>
-        </template>
-      </b-table>
+      <shared-entity-editor v-if="localSharedEntity" v-model="localSharedEntity" />
     </b-modal>
   </div>
 </template>
 
 <script>
 import { models, services } from "django-airavata-api";
-import AutocompleteTextInput from "./AutocompleteTextInput.vue";
+import SharedEntityEditor from "./SharedEntityEditor.vue";
 
 export default {
   name: "share-button",
   props: {
-    value: models.SharedEntity,
+    entityId: String,
+    sharedEntity: models.SharedEntity,
     autoAddDefaultGatewayUsersGroup: {
       type: Boolean,
       default: true
     }
   },
   components: {
-    AutocompleteTextInput
+    SharedEntityEditor
   },
   data: function() {
     return {
-      sharedEntity: this.cloneSharedEntity(this.value),
-      userFields: [
-        { key: "name", label: "User Name" },
-        { key: "email", label: "Email" },
-        { key: "permission", label: "Permission" },
-        { key: "remove", label: "Remove" }
-      ],
-      groupFields: [
-        { key: "name", label: "Group Name" },
-        { key: "permission", label: "Permission" },
-        { key: "remove", label: "Remove" }
-      ],
+      localSharedEntity: null,
       users: [],
       groups: [],
-      sharedEntityCopy: null
+      sharedEntityCopy: null,
+      defaultGatewayUsersGroup: null
     };
   },
   computed: {
@@ -103,20 +51,22 @@ export default {
       );
     },
     usersCount: function() {
-      return this.sharedEntity && this.sharedEntity.userPermissions
-        ? this.sharedEntity.userPermissions.length
+      return this.localSharedEntity && this.localSharedEntity.userPermissions
+        ? this.localSharedEntity.userPermissions.length
         : 0;
     },
     userNames: function() {
-      return this.sharedEntity && this.sharedEntity.userPermissions
-        ? this.sharedEntity.userPermissions.map(
+      return this.localSharedEntity && this.localSharedEntity.userPermissions
+        ? this.localSharedEntity.userPermissions.map(
             userPerm => userPerm.user.firstName + " " + userPerm.user.lastName
           )
         : null;
     },
     filteredGroupPermissions: function() {
-      return this.sharedEntity && this.sharedEntity.groupPermissions
-        ? this.sharedEntity.groupPermissions.filter(
+      // TODO: make this an option? Maybe don't filter the admin groups but add
+      // an option to make it so that they can't be removed
+      return this.localSharedEntity && this.localSharedEntity.groupPermissions
+        ? this.localSharedEntity.groupPermissions.filter(
             grp =>
               !grp.group.isGatewayAdminsGroup &&
               !grp.group.isReadOnlyGatewayAdminsGroup
@@ -134,71 +84,61 @@ export default {
     totalCount: function() {
       return this.usersCount + this.groupsCount;
     },
-    permissionOptions: function() {
-      return [
-        models.ResourcePermissionType.READ,
-        models.ResourcePermissionType.WRITE
-      ].map(perm => {
-        return {
-          value: perm,
-          text: perm.name
-        };
-      });
-    },
-    groupSuggestions: function() {
-      // filter out already selected groups
-      const currentGroupIds = this.filteredGroupPermissions.map(
-        groupPerm => groupPerm.group.id
-      );
-      return this.groups
-        .filter(
-          group =>
-            currentGroupIds.indexOf(group.id) < 0 &&
-            !group.isGatewayAdminsGroup &&
-            !group.isReadOnlyGatewayAdminsGroup
-        )
-        .map(group => {
-          return {
-            id: group.id,
-            name: group.name,
-            type: "group"
-          };
-        });
-    },
-    userSuggestions: function() {
-      // filter out already selected users
-      const currentUserIds = this.sharedEntity.userPermissions
-        ? this.sharedEntity.userPermissions.map(
-            userPerm => userPerm.user.airavataInternalUserId
-          )
-        : [];
-      return this.users
-        .filter(user => currentUserIds.indexOf(user.airavataInternalUserId) < 0)
-        .map(user => {
-          return {
-            id: user.airavataInternalUserId,
-            name:
-              user.firstName +
-              " " +
-              user.lastName +
-              " (" +
-              user.userId +
-              ") " +
-              user.email,
-            user: user,
-            type: "user"
-          };
-        });
-    },
-    usersAndGroupsSuggestions: function() {
-      return this.userSuggestions.concat(this.groupSuggestions);
-    },
     shareButtonEnabled: function() {
       // Enable share button if new entity or user is the entity's owner
-      return !this.sharedEntity.entityId || this.sharedEntity.isOwner;
+      return (
+        this.localSharedEntity &&
+        (!this.localSharedEntity.entityId || this.localSharedEntity.isOwner)
+      );
     }
   },
   methods: {
+    initialize: function() {
+      // First loaded needed data and then process it. This is to prevent one
+      // call to initialize clobbering a later call to initialize. That is, do
+      // all of the async stuff first and then make decisions based on the
+      // values of the props.
+      const promises = [];
+      let loadedSharedEntity = null;
+      if (this.entityId) {
+        promises.push(
+          services.SharedEntityService.retrieve({ lookup: this.entityId }).then(
+            sharedEntity => (loadedSharedEntity = sharedEntity)
+          )
+        );
+      }
+      if (
+        !this.entityId &&
+        (!this.sharedEntity || !this.sharedEntity.entityId) &&
+        !this.defaultGatewayUsersGroup
+      ) {
+        promises.push(
+          services.ServiceFactory.service("Groups")
+            .list({ limit: -1 })
+            .then(groups => {
+              // If a new sharedEntity, automatically add the defaultGatewayUsersGroup
+              groups
+                .filter(group => group.isDefaultGatewayUsersGroup)
+                .forEach(group => (this.defaultGatewayUsersGroup = group));
+            })
+        );
+      }
+      Promise.all(promises).then(() => {
+        if (this.sharedEntity) {
+          this.localSharedEntity = this.sharedEntity.clone();
+        } else if (this.entityId) {
+          this.localSharedEntity = loadedSharedEntity;
+        } else {
+          this.localSharedEntity = new models.SharedEntity();
+        }
+        if (
+          !this.localSharedEntity.entityId &&
+          this.autoAddDefaultGatewayUsersGroup
+        ) {
+          this.localSharedEntity.addGroup(this.defaultGatewayUsersGroup);
+        }
+      });
+    },
     /**
      * Merge the persisted SharedEntity with the local SharedEntity
      * instance and save it, returning a Promise.
@@ -206,99 +146,54 @@ export default {
     mergeAndSave: function(entityId) {
       return services.SharedEntityService.merge({
         lookup: entityId,
-        data: this.sharedEntity
-      }).then(sharedEntity => (this.sharedEntity = sharedEntity));
-    },
-    removeUser: function(user) {
-      this.sharedEntity.userPermissions = this.sharedEntity.userPermissions.filter(
-        userPermission =>
-          userPermission.user.airavataInternalUserId !==
-          user.airavataInternalUserId
-      );
-    },
-    removeGroup: function(group) {
-      this.sharedEntity.groupPermissions = this.sharedEntity.groupPermissions.filter(
-        groupPermission => groupPermission.group.id !== group.id
-      );
+        data: this.localSharedEntity
+      }).then(sharedEntity => {
+        this.localSharedEntity = sharedEntity;
+        this.emitSavedEvent();
+      });
     },
-    suggestionSelected: function(suggestion) {
-      if (suggestion.type === "group") {
-        const group = this.groups.find(group => group.id === suggestion.id);
-        this.addGroup(group);
-      } else if (suggestion.type === "user") {
-        const user = this.users.find(
-          user => user.airavataInternalUserId === suggestion.id
-        );
-        if (!this.sharedEntity.userPermissions) {
-          this.sharedEntity.userPermissions = [];
-        }
-        this.sharedEntity.userPermissions.push(
-          new models.UserPermission({
-            user: user,
-            permissionType: models.ResourcePermissionType.READ
-          })
-        );
+    saveSharedEntity: function(event) {
+      // If we don't have an entityId we can't create a SharedEntity. Instead,
+      // we'll just emit 'unsaved' to let parent know that sharing has changed.
+      // It will be up to parent to call `mergeAndSave(entityId)` once there is
+      // an entityId or merge the sharedEntity itself.
+      if (this.localSharedEntity.entityId) {
+        services.SharedEntityService.update({
+          data: this.localSharedEntity,
+          lookup: this.localSharedEntity.entityId
+        }).then(sharedEntity => {
+          this.localSharedEntity = sharedEntity;
+          this.emitSavedEvent();
+        });
+      } else {
+        this.emitUnsavedEvent();
       }
     },
-    addGroup: function(group) {
-      if (!this.sharedEntity.groupPermissions) {
-        this.sharedEntity.groupPermissions = [];
-      }
-      if (
-        !this.sharedEntity.groupPermissions.find(gp => gp.group.id === group.id)
-      ) {
-        this.sharedEntity.groupPermissions.push(
-          new models.GroupPermission({
-            group: group,
-            permissionType: models.ResourcePermissionType.READ
-          })
-        );
-      }
+    emitSavedEvent() {
+      this.$emit("saved", this.localSharedEntity);
     },
-    saveSharedEntity: function(event) {
-      this.emitValueChanged();
-      this.$emit("save", this.sharedEntity);
+    emitUnsavedEvent() {
+      this.$emit("unsaved", this.localSharedEntity);
     },
     cancelEditSharedEntity: function(event) {
-      this.sharedEntity = this.sharedEntityCopy;
-    },
-    emitValueChanged: function() {
-      this.$emit("input", this.sharedEntity);
+      this.localSharedEntity = this.sharedEntityCopy;
     },
-    cloneSharedEntity: function(sharedEntity) {
-      return sharedEntity ? sharedEntity.clone() : new models.SharedEntity();
+    openSharingSettingsModal: function(event) {
+      this.$refs.sharingSettingsModal.show();
     },
     showSharingSettingsModal: function(event) {
-      this.sharedEntityCopy = this.cloneSharedEntity(this.sharedEntity);
+      this.sharedEntityCopy = this.localSharedEntity.clone();
     }
   },
   mounted: function() {
-    // Load all of the groups and users
-    services.ServiceFactory.service("Groups")
-      .list({ limit: -1 })
-      .then(groups => {
-        this.groups = groups;
-        // If a new sharedEntity, automatically add the defaultGatewayUsersGroup
-        if (
-          !this.sharedEntity.entityId &&
-          this.autoAddDefaultGatewayUsersGroup
-        ) {
-          this.groups
-            .filter(group => group.isDefaultGatewayUsersGroup)
-            .forEach(this.addGroup);
-          // Since this is a new sharedEntity and we're implicitly modifying it,
-          // need to emitValueChanged so parent component sees the added
-          // defaultGatewayUsersGroup
-          this.emitValueChanged();
-        }
-      });
-    services.ServiceFactory.service("UserProfiles")
-      .list()
-      .then(users => (this.users = users));
+    this.initialize();
   },
   watch: {
-    value: function(newValue) {
-      this.sharedEntity = this.cloneSharedEntity(newValue);
+    sharedEntity(newSharedEntity) {
+      this.initialize();
+    },
+    entityId(newEntityId) {
+      this.initialize();
     }
   }
 };
@@ -308,7 +203,7 @@ export default {
 button {
   background-color: white;
 }
-#share-button >>> #modal-share-settings .modal-body {
+.share-button >>> .modal-share-settings .modal-body {
   max-height: 50vh;
   min-height: 300px;
   overflow: auto;
diff --git a/django_airavata/static/common/js/components/SharedEntityEditor.vue b/django_airavata/static/common/js/components/SharedEntityEditor.vue
new file mode 100644
index 0000000..a0cf510
--- /dev/null
+++ b/django_airavata/static/common/js/components/SharedEntityEditor.vue
@@ -0,0 +1,223 @@
+<template>
+  <div>
+    <b-form-group label="Search for users/groups" labelFor="user-groups-autocomplete">
+      <autocomplete-text-input id="user-groups-autocomplete" :suggestions="usersAndGroupsSuggestions"
@selected="suggestionSelected">
+        <template slot="suggestion" slot-scope="slotProps">
+          <span v-if="slotProps.suggestion.type == 'group'">
+            <i class="fa fa-users"></i> {{ slotProps.suggestion.name }}
+          </span>
+          <span v-if="slotProps.suggestion.type == 'user'">
+            <i class="fa fa-user"></i>
+            {{ slotProps.suggestion.user.firstName }} {{ slotProps.suggestion.user.lastName
}} ({{ slotProps.suggestion.user.userId }})
+            - {{ slotProps.suggestion.user.email }}
+          </span>
+        </template>
+      </autocomplete-text-input>
+    </b-form-group>
+    <h5 v-if="totalCount > 0">Currently Shared With</h5>
+    <b-table v-if="usersCount > 0" id="modal-user-table" hover :items="data.userPermissions"
:fields="userFields">
+      <template slot="name" slot-scope="data">
+        <span :title="data.item.user.userId">{{data.item.user.firstName}} {{data.item.user.lastName}}</span>
+      </template>
+      <template slot="email" slot-scope="data">
+        {{data.item.user.email}}
+      </template>
+      <template slot="permission" slot-scope="data">
+        <b-form-select v-model="data.item.permissionType" :options="permissionOptions"
/>
+      </template>
+      <template slot="remove" slot-scope="data">
+        <a href="#" @click.prevent="removeUser(data.item.user)">
+          <span class="fa fa-trash"></span>
+        </a>
+      </template>
+    </b-table>
+    <b-table v-if="groupsCount > 0" id="modal-group-table" hover :items="filteredGroupPermissions"
:fields="groupFields">
+      <template slot="name" slot-scope="data">
+        {{data.item.group.name}}
+      </template>
+      <template slot="permission" slot-scope="data">
+        <b-form-select v-model="data.item.permissionType" :options="permissionOptions"
/>
+      </template>
+      <template slot="remove" slot-scope="data">
+        <a href="#" @click.prevent="removeGroup(data.item.group)">
+          <span class="fa fa-trash"></span>
+        </a>
+      </template>
+    </b-table>
+  </div>
+</template>
+
+<script>
+import { models, services } from "django-airavata-api";
+import AutocompleteTextInput from "./AutocompleteTextInput.vue";
+import VModelMixin from "../mixins/VModelMixin";
+
+export default {
+  name: "shared-entity-editor",
+  mixins: [VModelMixin],
+  props: {
+    value: {
+      type: models.SharedEntity
+    }
+  },
+  components: {
+    AutocompleteTextInput
+  },
+  data: function() {
+    return {
+      userFields: [
+        { key: "name", label: "User Name" },
+        { key: "email", label: "Email" },
+        { key: "permission", label: "Permission" },
+        { key: "remove", label: "Remove" }
+      ],
+      groupFields: [
+        { key: "name", label: "Group Name" },
+        { key: "permission", label: "Permission" },
+        { key: "remove", label: "Remove" }
+      ],
+      users: [],
+      groups: []
+    };
+  },
+  computed: {
+    usersCount: function() {
+      return this.data && this.data.userPermissions
+        ? this.data.userPermissions.length
+        : 0;
+    },
+    filteredGroupPermissions: function() {
+      return this.data && this.data.groupPermissions
+        ? this.data.groupPermissions.filter(
+            grp =>
+              !grp.group.isGatewayAdminsGroup &&
+              !grp.group.isReadOnlyGatewayAdminsGroup
+          )
+        : [];
+    },
+    groupsCount: function() {
+      return this.filteredGroupPermissions.length;
+    },
+    totalCount: function() {
+      return this.usersCount + this.groupsCount;
+    },
+    permissionOptions: function() {
+      return [
+        models.ResourcePermissionType.READ,
+        models.ResourcePermissionType.WRITE
+      ].map(perm => {
+        return {
+          value: perm,
+          text: perm.name
+        };
+      });
+    },
+    groupSuggestions: function() {
+      // filter out already selected groups
+      const currentGroupIds = this.filteredGroupPermissions.map(
+        groupPerm => groupPerm.group.id
+      );
+      return this.groups
+        .filter(
+          group =>
+            currentGroupIds.indexOf(group.id) < 0 &&
+            !group.isGatewayAdminsGroup &&
+            !group.isReadOnlyGatewayAdminsGroup
+        )
+        .map(group => {
+          return {
+            id: group.id,
+            name: group.name,
+            type: "group"
+          };
+        });
+    },
+    userSuggestions: function() {
+      // filter out already selected users
+      const currentUserIds = this.data.userPermissions
+        ? this.data.userPermissions.map(
+            userPerm => userPerm.user.airavataInternalUserId
+          )
+        : [];
+      return this.users
+        .filter(user => currentUserIds.indexOf(user.airavataInternalUserId) < 0)
+        .map(user => {
+          return {
+            id: user.airavataInternalUserId,
+            name:
+              user.firstName +
+              " " +
+              user.lastName +
+              " (" +
+              user.userId +
+              ") " +
+              user.email,
+            user: user,
+            type: "user"
+          };
+        });
+    },
+    usersAndGroupsSuggestions: function() {
+      return this.userSuggestions.concat(this.groupSuggestions);
+    }
+  },
+  methods: {
+    removeUser: function(user) {
+      this.data.userPermissions = this.data.userPermissions.filter(
+        userPermission =>
+          userPermission.user.airavataInternalUserId !==
+          user.airavataInternalUserId
+      );
+    },
+    removeGroup: function(group) {
+      this.data.groupPermissions = this.data.groupPermissions.filter(
+        groupPermission => groupPermission.group.id !== group.id
+      );
+    },
+    suggestionSelected: function(suggestion) {
+      if (suggestion.type === "group") {
+        const group = this.groups.find(group => group.id === suggestion.id);
+        this.addGroup(group);
+      } else if (suggestion.type === "user") {
+        const user = this.users.find(
+          user => user.airavataInternalUserId === suggestion.id
+        );
+        if (!this.data.userPermissions) {
+          this.data.userPermissions = [];
+        }
+        this.data.userPermissions.push(
+          new models.UserPermission({
+            user: user,
+            permissionType: models.ResourcePermissionType.READ
+          })
+        );
+      }
+    },
+    addGroup: function(group) {
+      if (!this.data.groupPermissions) {
+        this.data.groupPermissions = [];
+      }
+      if (!this.data.groupPermissions.find(gp => gp.group.id === group.id)) {
+        this.data.groupPermissions.push(
+          new models.GroupPermission({
+            group: group,
+            permissionType: models.ResourcePermissionType.READ
+          })
+        );
+      }
+    }
+  },
+  mounted: function() {
+    // Load all of the groups and users
+    services.ServiceFactory.service("Groups")
+      .list({ limit: -1 })
+      .then(groups => {
+        this.groups = groups;
+      });
+    services.ServiceFactory.service("UserProfiles")
+      .list()
+      .then(users => (this.users = users));
+  }
+};
+</script>
+
diff --git a/django_airavata/static/common/js/index.js b/django_airavata/static/common/js/index.js
index 8b7ce4b..2cd1d48 100644
--- a/django_airavata/static/common/js/index.js
+++ b/django_airavata/static/common/js/index.js
@@ -14,6 +14,8 @@ import ValidationErrors from "./errors/ValidationErrors";
 
 import ListLayout from "./layouts/ListLayout.vue";
 
+import VModelMixin from "./mixins/VModelMixin";
+
 import Notification from "./notifications/Notification";
 import NotificationList from "./notifications/NotificationList";
 
@@ -41,6 +43,10 @@ exports.layouts = {
   ListLayout
 };
 
+exports.mixins = {
+  VModelMixin
+};
+
 exports.notifications = {
   Notification,
   NotificationList
diff --git a/django_airavata/static/common/js/mixins/VModelMixin.js b/django_airavata/static/common/js/mixins/VModelMixin.js
new file mode 100644
index 0000000..abf777b
--- /dev/null
+++ b/django_airavata/static/common/js/mixins/VModelMixin.js
@@ -0,0 +1,56 @@
+import { models } from "django-airavata-api";
+
+export default {
+  watch: {
+    data: {
+      handler: function(newValue, oldValue) {
+        // Only emit 'input' for objects when one of their deep properties has
+        // changed to prevent infinite loop since 'data' is recloned whenever
+        // 'value' changes
+        if (typeof this.value === "object" && newValue === oldValue) {
+          this.$emit("input", newValue);
+        } else if (
+          (this.value === null || typeof this.value !== "object") &&
+          newValue !== oldValue
+        ) {
+          this.$emit("input", newValue);
+        }
+      },
+      deep: true
+    },
+    value: {
+      handler: function(newValue) {
+        this.data = this.copyValue(newValue);
+      },
+      deep: true
+    }
+  },
+  methods: {
+    copyValue(value) {
+      if (value instanceof Array) {
+        return value.map(item => this.copyValue(item));
+      } else {
+        if (value === null) {
+          return null;
+        } else if (value instanceof models.BaseModel) {
+          return value.clone();
+        } else if (typeof value === "object") {
+          return JSON.parse(JSON.stringify(value));
+        } else {
+          // Must be number, boolean or string
+          return value;
+        }
+      }
+    }
+  },
+  data: function() {
+    return {
+      data: this.copyValue(this.value)
+    };
+  },
+  props: {
+    value: {
+      required: true
+    }
+  }
+};


Mime
View raw message