You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2018/09/28 15:49:38 UTC
[airavata-django-portal] 01/03: AIRAVATA-2840 Add ShareButton to
credential store UI
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 <ma...@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
+ }
+ }
+};