Implement functionality to update project member permissions in settings (#56)

* Add mutation to update permissions of project member

* Use update project member permission client method in UI

* Handle review changes

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-01-31 17:09:29 +05:30 committed by Ashwin Phatak
parent b5e7554c26
commit ace27c7eae
9 changed files with 134 additions and 45 deletions

View File

@ -165,13 +165,24 @@ export class Database {
return environmentVariables; return environmentVariables;
} }
async removeProjectMemberById (memberId: string): Promise<boolean> { async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
const projectMemberRepository = this.dataSource.getRepository(ProjectMember); const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
const deleted = await projectMemberRepository.delete(memberId); const deleteResult = await projectMemberRepository.delete({ id: Number(projectMemberId) });
if (deleted.affected) { if (deleteResult.affected) {
return deleted.affected > 0; return deleteResult.affected > 0;
} else {
return false;
}
}
async updateProjectMemberById (projectMemberId: string, data: DeepPartial<ProjectMember>): Promise<boolean> {
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
const updateResult = await projectMemberRepository.update({ id: Number(projectMemberId) }, data);
if (updateResult.affected) {
return updateResult.affected > 0;
} else { } else {
return false; return false;
} }
@ -223,7 +234,7 @@ export class Database {
} }
} }
async getProjectMemberByMemberId (memberId: string): Promise<ProjectMember> { async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
const projectMemberRepository = this.dataSource.getRepository(ProjectMember); const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
const projectMemberWithProject = await projectMemberRepository.find({ const projectMemberWithProject = await projectMemberRepository.find({
@ -234,7 +245,7 @@ export class Database {
member: true member: true
}, },
where: { where: {
id: Number(memberId) id: Number(projectMemberId)
} }
} }
); );

View File

@ -11,7 +11,7 @@ import {
import { Project } from './Project'; import { Project } from './Project';
import { User } from './User'; import { User } from './User';
enum Permissions { export enum Permission {
View = 'View', View = 'View',
Edit = 'Edit' Edit = 'Edit'
} }
@ -32,7 +32,7 @@ export class ProjectMember {
@Column({ @Column({
type: 'simple-array' type: 'simple-array'
}) })
permissions!: Permissions[]; permissions!: Permission[];
@CreateDateColumn() @CreateDateColumn()
createdAt!: Date; createdAt!: Date;

View File

@ -6,6 +6,7 @@ import { OAuthApp } from '@octokit/oauth-app';
import { Database } from './database'; import { Database } from './database';
import { deploymentToGqlType, projectMemberToGqlType, projectToGqlType, environmentVariableToGqlType, isUserOwner } from './utils'; import { deploymentToGqlType, projectMemberToGqlType, projectToGqlType, environmentVariableToGqlType, isUserOwner } from './utils';
import { Environment } from './entity/Deployment'; import { Environment } from './entity/Deployment';
import { Permission } from './entity/ProjectMember';
const log = debug('snowball:database'); const log = debug('snowball:database');
@ -108,9 +109,9 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
}, },
Mutation: { Mutation: {
removeMember: async (_: any, { memberId }: { memberId: string }, context: any) => { removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => {
try { try {
const member = await db.getProjectMemberByMemberId(memberId); const member = await db.getProjectMemberById(projectMemberId);
if (member.member.id === context.userId) { if (member.member.id === context.userId) {
throw new Error('Invalid operation: cannot remove self'); throw new Error('Invalid operation: cannot remove self');
@ -120,7 +121,7 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
assert(memberProject); assert(memberProject);
if (isUserOwner(String(context.userId), String(memberProject.owner.id))) { if (isUserOwner(String(context.userId), String(memberProject.owner.id))) {
return db.removeProjectMemberById(memberId); return db.removeProjectMemberById(projectMemberId);
} else { } else {
throw new Error('Invalid operation: not authorized'); throw new Error('Invalid operation: not authorized');
} }
@ -130,6 +131,20 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
} }
}, },
updateProjectMember: async (_: any, { projectMemberId, data }: {
projectMemberId: string,
data: {
permissions: Permission[]
}
}) => {
try {
return db.updateProjectMemberById(projectMemberId, data);
} catch (err) {
log(err);
return false;
}
},
addEnvironmentVariables: async (_: any, { projectId, environmentVariables }: { projectId: string, environmentVariables: { environments: string[], key: string, value: string}[] }) => { addEnvironmentVariables: async (_: any, { projectId, environmentVariables }: { projectId: string, environmentVariables: { environments: string[], key: string, value: string}[] }) => {
try { try {
return db.addEnvironmentVariablesByProjectId(projectId, environmentVariables); return db.addEnvironmentVariablesByProjectId(projectId, environmentVariables);

View File

@ -131,7 +131,8 @@ type AuthResult {
} }
type Mutation { type Mutation {
removeMember(memberId: String!): Boolean! removeProjectMember(projectMemberId: String!): Boolean!
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean!
addEnvironmentVariables(projectId: String!, environmentVariables: [AddEnvironmentVariableInput!]): Boolean! addEnvironmentVariables(projectId: String!, environmentVariables: [AddEnvironmentVariableInput!]): Boolean!
removeEnvironmentVariable(environmentVariableId: String!): Boolean! removeEnvironmentVariable(environmentVariableId: String!): Boolean!
updateEnvironmentVariable(environmentVariableId: String!, environmentVariable: UpdateEnvironmentVariableInput!): Boolean! updateEnvironmentVariable(environmentVariableId: String!, environmentVariable: UpdateEnvironmentVariableInput!): Boolean!
@ -170,3 +171,7 @@ input UpdateEnvironmentVariableInput {
key: String key: String
value: String value: String
} }
input UpdateProjectMemberInput {
permissions: [Permission]
}

View File

@ -1,4 +1,5 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Permission } from 'gql-client';
import { import {
Select, Select,
@ -33,8 +34,11 @@ interface MemberCardProps {
isOwner: boolean; isOwner: boolean;
isPending: boolean; isPending: boolean;
permissions: string[]; permissions: string[];
handleDeletePendingMember: (id: string) => void; handleDeletePendingMember?: (id: string) => void;
removeMemberHandler: () => Promise<void>; onRemoveProjectMember?: () => Promise<void>;
onUpdateProjectMember?: (data: {
permissions: Permission[];
}) => Promise<void>;
} }
const MemberCard = ({ const MemberCard = ({
@ -44,7 +48,8 @@ const MemberCard = ({
isPending, isPending,
permissions, permissions,
handleDeletePendingMember, handleDeletePendingMember,
removeMemberHandler, onRemoveProjectMember,
onUpdateProjectMember,
}: MemberCardProps) => { }: MemberCardProps) => {
const [selectedPermission, setSelectedPermission] = useState( const [selectedPermission, setSelectedPermission] = useState(
permissions.join('+'), permissions.join('+'),
@ -52,7 +57,7 @@ const MemberCard = ({
const [removeMemberDialogOpen, setRemoveMemberDialogOpen] = useState(false); const [removeMemberDialogOpen, setRemoveMemberDialogOpen] = useState(false);
const handlePermissionChange = useCallback( const handlePermissionChange = useCallback(
(value: string) => { async (value: string) => {
setSelectedPermission(value); setSelectedPermission(value);
if (value === 'remove') { if (value === 'remove') {
@ -61,6 +66,11 @@ const MemberCard = ({
setTimeout(() => { setTimeout(() => {
setSelectedPermission(selectedPermission); setSelectedPermission(selectedPermission);
}); });
} else {
if (onUpdateProjectMember) {
const permissions = value.split('+') as Permission[];
await onUpdateProjectMember({ permissions });
}
} }
}, },
[removeMemberDialogOpen, selectedPermission], [removeMemberDialogOpen, selectedPermission],
@ -112,7 +122,9 @@ const MemberCard = ({
size="sm" size="sm"
className="rounded-full" className="rounded-full"
onClick={() => { onClick={() => {
handleDeletePendingMember(member.id); if (handleDeletePendingMember) {
handleDeletePendingMember(member.id);
}
}} }}
> >
D D
@ -128,7 +140,9 @@ const MemberCard = ({
confirmButtonTitle="Yes, Remove member" confirmButtonTitle="Yes, Remove member"
handleConfirm={() => { handleConfirm={() => {
setRemoveMemberDialogOpen((preVal) => !preVal); setRemoveMemberDialogOpen((preVal) => !preVal);
removeMemberHandler(); if (onRemoveProjectMember) {
onRemoveProjectMember();
}
}} }}
color="red" color="red"
> >

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import toast, { Toaster } from 'react-hot-toast'; import toast, { Toaster } from 'react-hot-toast';
import { Project } from 'gql-client'; import { Permission, Project } from 'gql-client';
import { Chip, Button, Typography } from '@material-tailwind/react'; import { Chip, Button, Typography } from '@material-tailwind/react';
@ -30,17 +30,32 @@ const MembersTabPanel = ({ project }: { project: Project }) => {
}, [project.id]); }, [project.id]);
const removeMemberHandler = async (projectMemberId: string) => { const removeMemberHandler = async (projectMemberId: string) => {
const { removeMember: isMemberRemoved } = const { removeProjectMember: isMemberRemoved } =
await client.removeMember(projectMemberId); await client.removeProjectMember(projectMemberId);
if (isMemberRemoved) { if (isMemberRemoved) {
toast.success('Member removed from project');
await fetchProjectMembers(); await fetchProjectMembers();
toast.success('Member removed from project');
} else { } else {
toast.error('Not able to remove member'); toast.error('Not able to remove member');
} }
}; };
const updateProjectMemberHandler = useCallback(
async (projectMemberId: string, data: { permissions: Permission[] }) => {
const { updateProjectMember: isProjectMemberUpdated } =
await client.updateProjectMember(projectMemberId, data);
if (isProjectMemberUpdated) {
await fetchProjectMembers();
toast.success('Project member permission updated');
} else {
toast.error('Project member permission not updated');
}
},
[],
);
useEffect(() => { useEffect(() => {
fetchProjectMembers(); fetchProjectMembers();
}, [project.id, fetchProjectMembers]); }, [project.id, fetchProjectMembers]);
@ -73,8 +88,6 @@ const MembersTabPanel = ({ project }: { project: Project }) => {
isOwner={true} isOwner={true}
isPending={false} isPending={false}
permissions={[]} permissions={[]}
handleDeletePendingMember={() => {}}
removeMemberHandler={async () => {}}
/> />
{projectMembers.map((projectMember, index) => { {projectMembers.map((projectMember, index) => {
return ( return (
@ -92,7 +105,12 @@ const MembersTabPanel = ({ project }: { project: Project }) => {
), ),
); );
}} }}
removeMemberHandler={() => removeMemberHandler(projectMember.id)} onRemoveProjectMember={async () =>
await removeMemberHandler(projectMember.id)
}
onUpdateProjectMember={async (data) => {
await updateProjectMemberHandler(projectMember.id, data);
}}
/> />
); );
})} })}

View File

@ -1,8 +1,8 @@
import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client';
import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject, getDomains, getProjectsInOrganization } from './queries'; import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject, getDomains, getProjectsInOrganization } from './queries';
import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse, RollbackDeploymentResponse, AddDomainInput, AddDomainResponse, GetDomainsResponse, UpdateDomainInput, UpdateDomainResponse, AuthenticateGithubResponse, UpdateEnvironmentVariableResponse, UpdateEnvironmentVariableInput, RemoveEnvironmentVariableResponse } from './types'; import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse, RollbackDeploymentResponse, AddDomainInput, AddDomainResponse, GetDomainsResponse, UpdateDomainInput, UpdateDomainResponse, AuthenticateGithubResponse, UpdateEnvironmentVariableResponse, UpdateEnvironmentVariableInput, RemoveEnvironmentVariableResponse, UpdateProjectMemberInput, RemoveProjectMemberResponse, UpdateProjectMemberResponse } from './types';
import { removeMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGithub, updateEnvironmentVariable, removeEnvironmentVariable } from './mutations'; import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGithub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember } from './mutations';
export interface GraphQLConfig { export interface GraphQLConfig {
gqlEndpoint: string; gqlEndpoint: string;
@ -91,26 +91,38 @@ export class GQLClient {
return data; return data;
} }
async removeMember (memberId: string): Promise<RemoveMemberResponse> {
const { data } = await this.client.mutate({
mutation: removeMember,
variables: {
memberId
}
});
return data;
}
async getProjectMembers (projectId: string) : Promise<GetProjectMembersResponse> { async getProjectMembers (projectId: string) : Promise<GetProjectMembersResponse> {
const { data } = await this.client.query({ const result = await this.client.query({
query: getProjectMembers, query: getProjectMembers,
variables: { variables: {
projectId projectId
} }
}); });
return data; return result.data;
}
async updateProjectMember (projectMemberId: string, data: UpdateProjectMemberInput): Promise<UpdateProjectMemberResponse> {
const result = await this.client.mutate({
mutation: updateProjectMember,
variables: {
projectMemberId,
data
}
});
return result.data;
}
async removeProjectMember (projectMemberId: string): Promise<RemoveProjectMemberResponse> {
const result = await this.client.mutate({
mutation: removeProjectMember,
variables: {
projectMemberId
}
});
return result.data;
} }
async searchProjects (searchText: string) : Promise<SearchProjectsResponse> { async searchProjects (searchText: string) : Promise<SearchProjectsResponse> {

View File

@ -1,8 +1,14 @@
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
export const removeMember = gql` export const removeProjectMember = gql`
mutation ($memberId: String!) { mutation ($projectMemberId: String!) {
removeMember(memberId: $memberId) removeProjectMember(projectMemberId: $projectMemberId)
}
`;
export const updateProjectMember = gql`
mutation ($projectMemberId: String!, $data: UpdateProjectMemberInput) {
updateProjectMember(projectMemberId: $projectMemberId, data: $data)
} }
`; `;

View File

@ -134,8 +134,12 @@ export type GetProjectMembersResponse = {
projectMembers: ProjectMember[] projectMembers: ProjectMember[]
} }
export type RemoveMemberResponse = { export type RemoveProjectMemberResponse = {
removeMember: boolean; removeProjectMember: boolean;
}
export type UpdateProjectMemberResponse = {
updateProjectMember: boolean;
} }
export type GetDeploymentsResponse = { export type GetDeploymentsResponse = {
@ -185,6 +189,10 @@ export type UpdateEnvironmentVariableInput = {
value: string; value: string;
} }
export type UpdateProjectMemberInput = {
permissions: Permission[];
}
export type UpdateEnvironmentVariableResponse = { export type UpdateEnvironmentVariableResponse = {
updateEnvironmentVariable: boolean; updateEnvironmentVariable: boolean;
} }