Skip to content

Commit

Permalink
web/satellite: added functionality to update project member's role
Browse files Browse the repository at this point in the history
Added new API request and dialog responsible for updating project member's role.

Issue:
storj/storj-private#569

Change-Id: I098f062567d01c3a6e83274c28a872b348858e97
  • Loading branch information
VitaliiShpital authored and Storj Robot committed May 7, 2024
1 parent bb35101 commit 034770a
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 12 deletions.
16 changes: 10 additions & 6 deletions web/satellite/src/api/accessGrants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,17 @@ export class AccessGrantsHttpApi implements AccessGrantsApi {
const path = `${this.ROOT_PATH}/delete-by-ids`;
const response = await this.client.delete(path, JSON.stringify({ ids }));

if (!response.ok) {
throw new APIError({
status: response.status,
message: 'Can not delete access grants',
requestID: response.headers.get('x-request-id'),
});
if (response.ok) {
return;
}

const result = await response.json();

throw new APIError({
status: response.status,
message: result.error || 'Can not delete access grants',
requestID: response.headers.get('x-request-id'),
});
}

/**
Expand Down
39 changes: 38 additions & 1 deletion web/satellite/src/api/projectMembers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.

import { ProjectInvitationItemModel, ProjectMember, ProjectMemberCursor, ProjectMembersApi, ProjectMembersPage } from '@/types/projectMembers';
import {
ProjectInvitationItemModel,
ProjectMember,
ProjectMemberCursor,
ProjectMembersApi,
ProjectMembersPage,
ProjectRole,
} from '@/types/projectMembers';
import { HttpClient } from '@/utils/httpClient';
import { APIError } from '@/utils/error';

Expand Down Expand Up @@ -68,6 +75,35 @@ export class ProjectMembersHttpApi implements ProjectMembersApi {
});
}

/**
* Handles updating project member's role.
*
* @throws Error
*/
public async updateRole(projectID: string, memberID: string, role: ProjectRole): Promise<ProjectMember> {
const path = `${this.ROOT_PATH}/${projectID}/members/${memberID}`;
const body = role === ProjectRole.Admin ? 0 : 1;
const response = await this.http.patch(path, body.toString());

const result = await response.json();
if (!response.ok) {
throw new APIError({
status: response.status,
message: result.error || `Failed update member's role`,
requestID: response.headers.get('x-request-id'),
});
}

return new ProjectMember(
result.fullName,
result.shortName,
result.email,
new Date(result.joinedAt),
result.id,
result.role,
);
}

/**
* Handles resending invitations to project.
*
Expand Down Expand Up @@ -125,6 +161,7 @@ export class ProjectMembersHttpApi implements ProjectMembersApi {
key.email,
new Date(key.joinedAt),
key.id,
key.role,
));
projectMembersPage.projectInvitations = projectMembers.projectInvitations.map(key => new ProjectInvitationItemModel(
key.email,
Expand Down
37 changes: 36 additions & 1 deletion web/satellite/src/components/TeamTableComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@
<v-menu activator="parent">
<v-list class="pa-1">
<v-list-item
v-if="item.role === ProjectRole.Member || item.role === ProjectRole.Admin"
density="comfortable"
link
rounded="lg"
@click="() => showChangeRoleDialog(item)"
>
<template #prepend>
<icon-member />
</template>
<v-list-item-title class="pl-2 text-body-2 font-weight-medium">
Change Role
</v-list-item-title>
</v-list-item>
<v-list-item
v-if="item.role === ProjectRole.Invited || item.role === ProjectRole.InviteExpired"
density="comfortable"
link
rounded="lg"
Expand Down Expand Up @@ -101,6 +116,12 @@
@deleted="onPostDelete"
/>

<change-member-role-dialog
v-model="isChangeMembersRoleShown"
:email="memberToUpdate?.email"
:member-id="memberToUpdate?.id"
/>
<v-snackbar
rounded="lg"
variant="elevated"
Expand Down Expand Up @@ -176,10 +197,13 @@ import { ROUTES } from '@/router';
import RemoveProjectMemberDialog from '@/components/dialogs/RemoveProjectMemberDialog.vue';
import IconUpload from '@/components/icons/IconUpload.vue';
import IconMember from '@/components/icons/IconMember.vue';
import IconCopy from '@/components/icons/IconCopy.vue';
import IconRemove from '@/components/icons/IconRemove.vue';
import ChangeMemberRoleDialog from '@/components/dialogs/ChangeMemberRoleDialog.vue';
type RenderedItem = {
id: string,
name: string,
email: string,
role: ProjectRole,
Expand All @@ -199,10 +223,12 @@ const notify = useNotify();
const { isLoading, withLoading } = useLoading();
const isRemoveMembersDialogShown = ref<boolean>(false);
const isChangeMembersRoleShown = ref<boolean>(false);
const search = ref<string>('');
const searchTimer = ref<NodeJS.Timeout>();
const selectedMembers = ref<string[]>([]);
const memberToDelete = ref<string>();
const memberToUpdate = ref<RenderedItem>();
const headers = ref([
{
Expand Down Expand Up @@ -236,7 +262,7 @@ const projectMembers = computed((): RenderedItem[] => {
projectOwner && projectMembersToReturn.unshift(projectOwner);
return projectMembersToReturn.map(member => {
let role = ProjectRole.Member;
let role = member.getRole();
if (member.getUserID() === projectOwner?.getUserID()) {
role = ProjectRole.Owner;
} else if (member.isPending()) {
Expand All @@ -248,6 +274,7 @@ const projectMembers = computed((): RenderedItem[] => {
}
return {
id: member.getUserID(),
name: member.getName(),
email: member.getEmail(),
role,
Expand Down Expand Up @@ -385,6 +412,14 @@ async function fetch(page = FIRST_PAGE, limit = DEFAULT_PAGE_LIMIT): Promise<voi
});
}
/**
* Makes change project member role dialog visible.
*/
function showChangeRoleDialog(item: RenderedItem): void {
memberToUpdate.value = item;
isChangeMembersRoleShown.value = true;
}
/**
* Makes delete project members dialog visible.
*/
Expand Down
133 changes: 133 additions & 0 deletions web/satellite/src/components/dialogs/ChangeMemberRoleDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (C) 2024 Storj Labs, Inc.
// See LICENSE for copying information.

<template>
<v-dialog
v-model="model"
width="auto"
min-width="400px"
max-width="450px"
transition="fade-transition"
:persistent="isLoading"
>
<v-card rounded="xlg">
<v-sheet>
<v-card-item class="py-4 pl-6">
<template #prepend>
<icon-member-color class="mt-1" :size="40" />
</template>
<v-card-title class="font-weight-bold">
Change Role
</v-card-title>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
@click="model = false"
/>
</template>
</v-card-item>
</v-sheet>

<v-divider />

<v-row>
<v-col class="pa-6 mx-3">
<p class="my-2">
Selected team member:
</p>
<v-chip class="font-weight-bold text-wrap py-2">{{ email }}</v-chip>
<v-select
v-model="selectedRole"
chips
label="Role"
:items="[ProjectRole.Member, ProjectRole.Admin]"
class="mt-8"
/>
<v-alert color="info" border variant="tonal" class="my-4">
{{ selectedRole === ProjectRole.Member ?
'Members can only delete API keys and buckets they personally created; they cannot invite new users or remove existing users from the project.' :
'Admins can invite new users, remove existing users (except the project owner), and delete any API keys and buckets, regardless of who created them.' }}
</v-alert>
</v-col>
</v-row>

<v-divider />

<v-card-actions class="pa-6">
<v-row>
<v-col>
<v-btn variant="outlined" color="default" block @click="model = false">Cancel</v-btn>
</v-col>
<v-col>
<v-btn color="primary" variant="flat" block :loading="isLoading" @click="updateRole">
Change Role
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import {
VAlert,
VBtn,
VCard,
VCardActions,
VCardTitle,
VChip,
VCol,
VDialog,
VDivider,
VRow,
VSelect,
} from 'vuetify/components';
import { ProjectRole } from '@/types/projectMembers';
import { useLoading } from '@/composables/useLoading';
import { useNotify } from '@/utils/hooks';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import IconMemberColor from '@/components/icons/IconMemberColor.vue';
const props = withDefaults(defineProps<{
memberId?: string
email?: string
}>(), {
memberId: '',
email: '',
});
const pmStore = useProjectMembersStore();
const projectsStore = useProjectsStore();
const model = defineModel<boolean>({ required: true });
const selectedRole = ref<ProjectRole>(ProjectRole.Member);
const { isLoading, withLoading } = useLoading();
const notify = useNotify();
/**
* Resends project invitation to current project.
*/
async function updateRole(): Promise<void> {
await withLoading(async () => {
try {
await pmStore.updateRole(projectsStore.state.selectedProject.id, props.memberId, selectedRole.value);
notify.success('Member role was updated successfully');
model.value = false;
} catch (error) {
error.message = `Error updating role. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.PROJECT_MEMBERS_PAGE);
}
});
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ async function onDeleteClick(): Promise<void> {
emit('deleted');
model.value = false;
} catch (error) {
error.message = `Error deleting access grant${props.accesses.length > 1 ? 's' : ''}. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.CONFIRM_DELETE_AG_MODAL);
}
});
Expand Down
16 changes: 16 additions & 0 deletions web/satellite/src/components/icons/IconMember.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (C) 2024 Storj Labs, Inc.
// See LICENSE for copying information.

<template>
<svg :width="size" :height="size" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0772 1.5C13.0087 1.5 15.3851 3.87642 15.3851 6.80788C15.3851 8.56688 14.5295 10.126 13.2117 11.0919C15.8554 12.0628 17.9374 14.1988 18.8374 16.8784C18.8652 16.9612 18.8918 17.0445 18.9174 17.1283L18.9548 17.2543C19.1299 17.8598 18.781 18.4927 18.1754 18.6678C18.0724 18.6976 17.9657 18.7127 17.8584 18.7127H2.13186C1.50675 18.7127 1 18.2059 1 17.5808C1 17.4927 1.0103 17.4049 1.03065 17.3192L1.04406 17.2681C1.08085 17.1401 1.12031 17.0133 1.16236 16.8876C2.07214 14.1692 4.19808 12.0091 6.89322 11.0549C5.60362 10.0868 4.76936 8.54476 4.76936 6.80788C4.76936 3.87642 7.14578 1.5 10.0772 1.5ZM10.0014 12.1916C6.85328 12.1916 4.07232 14.1131 2.91359 16.96L2.87976 17.0445H17.1231L17.1225 17.0431C15.9962 14.1805 13.2381 12.2325 10.1006 12.1922L10.0014 12.1916ZM10.0772 3.16819C8.06709 3.16819 6.43755 4.79773 6.43755 6.80788C6.43755 8.81802 8.06709 10.4476 10.0772 10.4476C12.0874 10.4476 13.7169 8.81802 13.7169 6.80788C13.7169 4.79773 12.0874 3.16819 10.0772 3.16819Z" fill="currentColor" />
</svg>
</template>

<script setup lang="ts">
withDefaults(defineProps<{
size?: number;
}>(), {
size: 18,
});
</script>
17 changes: 17 additions & 0 deletions web/satellite/src/components/icons/IconMemberColor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (C) 2024 Storj Labs, Inc.
// See LICENSE for copying information.

<template>
<svg :width="size" :height="size" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="31" height="31" rx="7.5" fill="white" stroke="#EBEEF1" />
<path d="M16.0695 8.34998C18.7078 8.34998 20.8466 10.4888 20.8466 13.1271C20.8466 14.7102 20.0765 16.1134 18.8905 16.9826C21.2698 17.8565 23.1437 19.7789 23.9536 22.1905C23.9786 22.265 24.0026 22.34 24.0256 22.4154L24.0593 22.5289C24.2169 23.0738 23.9029 23.6434 23.3579 23.801C23.2651 23.8278 23.1691 23.8414 23.0725 23.8414H8.91866C8.35607 23.8414 7.89999 23.3853 7.89999 22.8227C7.89999 22.7434 7.90926 22.6644 7.92758 22.5873L7.93965 22.5412C7.97276 22.4261 8.00827 22.3119 8.04612 22.1988C8.86492 19.7523 10.7783 17.8081 13.2039 16.9494C12.0432 16.0781 11.2924 14.6903 11.2924 13.1271C11.2924 10.4888 13.4312 8.34998 16.0695 8.34998ZM16.0013 17.9724C13.1679 17.9724 10.6651 19.7017 9.62223 22.264L9.59178 22.34H22.4107L22.4102 22.3388C21.3965 19.7624 18.9143 18.0092 16.0905 17.973L16.0013 17.9724ZM16.0695 9.85135C14.2604 9.85135 12.7938 11.3179 12.7938 13.1271C12.7938 14.9362 14.2604 16.4028 16.0695 16.4028C17.8786 16.4028 19.3452 14.9362 19.3452 13.1271C19.3452 11.3179 17.8786 9.85135 16.0695 9.85135Z" fill="black" />
</svg>
</template>

<script setup lang="ts">
withDefaults(defineProps<{
size?: number;
}>(), {
size: 32,
});
</script>
5 changes: 2 additions & 3 deletions web/satellite/src/components/icons/IconRemove.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
// See LICENSE for copying information.

<template>
<!-- Remove Icon -->
<svg :width="size" :height="size" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path :fill="color" d="M10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1ZM10 2.65C5.94071 2.65 2.65 5.94071 2.65 10C2.65 14.0593 5.94071 17.35 10 17.35C14.0593 17.35 17.35 14.0593 17.35 10C17.35 5.94071 14.0593 2.65 10 2.65ZM12.4475 10.873H7.552C7.09637 10.873 6.727 10.5036 6.727 10.048C6.727 9.60348 7.07857 9.24107 7.51882 9.22366L7.552 9.223H12.3926C12.8529 9.223 13.2302 9.58799 13.2456 10.048C13.2602 10.4887 12.9148 10.8579 12.4741 10.8726C12.4652 10.8729 12.4564 10.873 12.4475 10.873Z" />
</svg>
</template>

<script setup lang="ts">
const props = withDefaults(defineProps<{
color: string;
withDefaults(defineProps<{
color?: string;
size: number | string;
}>(), {
color: 'currentColor',
Expand Down

0 comments on commit 034770a

Please sign in to comment.