From 88d90f0afcee58819acd5294630ad5bf57cb2f2c Mon Sep 17 00:00:00 2001 From: Praveen Yadav Date: Wed, 13 Dec 2023 13:59:52 +0530 Subject: [PATCH] Role assignment for org members, project members & team members (#426) * feat: add ui support to change role * feat: add logic to add/remove policies --- .../components/organization/members/index.tsx | 28 +++-- .../organization/members/member.columns.tsx | 115 ++++++++++++++--- .../organization/members/member.types.tsx | 2 + .../components/organization/project/index.tsx | 4 +- .../organization/project/members/index.tsx | 49 +++++--- .../project/members/member.columns.tsx | 117 +++++++++++++++++- .../organization/project/project.tsx | 30 ++++- .../organization/teams/members/index.tsx | 25 ++-- .../teams/members/member.columns.tsx | 67 +++++++++- .../components/organization/teams/team.tsx | 30 ++++- .../react/hooks/useOrganizationMembers.ts | 29 ++++- sdks/js/packages/core/utils/index.ts | 10 ++ 12 files changed, 440 insertions(+), 66 deletions(-) diff --git a/sdks/js/packages/core/react/components/organization/members/index.tsx b/sdks/js/packages/core/react/components/organization/members/index.tsx index 08cacde43..f4296fb69 100644 --- a/sdks/js/packages/core/react/components/organization/members/index.tsx +++ b/sdks/js/packages/core/react/components/organization/members/index.tsx @@ -10,15 +10,15 @@ import { } from '@raystack/apsara'; import { Outlet, useNavigate, useRouterState } from '@tanstack/react-router'; import { useEffect, useMemo } from 'react'; +import Skeleton from 'react-loading-skeleton'; import { useFrontier } from '~/react/contexts/FrontierContext'; import { useOrganizationMembers } from '~/react/hooks/useOrganizationMembers'; import { usePermissions } from '~/react/hooks/usePermissions'; +import { AuthTooltipMessage } from '~/react/utils'; import { PERMISSIONS, shouldShowComponent } from '~/utils'; import { styles } from '../styles'; import { getColumns } from './member.columns'; import type { MembersTableType } from './member.types'; -import Skeleton from 'react-loading-skeleton'; -import { AuthTooltipMessage } from '~/react/utils'; export default function WorkspaceMembers() { const { activeOrganization: organization } = useFrontier(); @@ -63,10 +63,11 @@ export default function WorkspaceMembers() { }, [permissions, resource]); const { - isFetching: isOrgMembersLoading, + roles, members, memberRoles, - refetch + refetch, + isFetching: isOrgMembersLoading } = useOrganizationMembers({ showInvitations: canCreateInvite }); @@ -89,13 +90,14 @@ export default function WorkspaceMembers() { {organization?.id ? ( ) : null} @@ -122,7 +124,9 @@ const MembersTable = ({ canCreateInvite, canDeleteUser, organizationId, - memberRoles + memberRoles, + roles, + refetch }: MembersTableType) => { let navigate = useNavigate({ from: '/members' }); @@ -133,8 +137,16 @@ const MembersTable = ({ ); const columns = useMemo( - () => getColumns(organizationId, memberRoles, canDeleteUser, isLoading), - [organizationId, memberRoles, canDeleteUser, isLoading] + () => + getColumns( + organizationId, + memberRoles, + roles, + canDeleteUser, + isLoading, + refetch + ), + [organizationId, memberRoles, canDeleteUser, isLoading, roles, refetch] ); return ( diff --git a/sdks/js/packages/core/react/components/organization/members/member.columns.tsx b/sdks/js/packages/core/react/components/organization/members/member.columns.tsx index 562491515..a089b13e4 100644 --- a/sdks/js/packages/core/react/components/organization/members/member.columns.tsx +++ b/sdks/js/packages/core/react/components/organization/members/member.columns.tsx @@ -1,23 +1,42 @@ -import { TrashIcon } from '@radix-ui/react-icons'; -import { Avatar, Flex, Label, Text } from '@raystack/apsara'; +import { + DotsHorizontalIcon, + TrashIcon, + UpdateIcon +} from '@radix-ui/react-icons'; +import { Avatar, DropdownMenu, Flex, Label, Text } from '@raystack/apsara'; import { useNavigate } from '@tanstack/react-router'; import type { ColumnDef } from '@tanstack/react-table'; import Skeleton from 'react-loading-skeleton'; import { toast } from 'sonner'; import { useFrontier } from '~/react/contexts/FrontierContext'; -import { V1Beta1User, V1Beta1Invitation } from '~/src'; +import { + V1Beta1Invitation, + V1Beta1Policy, + V1Beta1Role, + V1Beta1User +} from '~/src'; import { Role } from '~/src/types'; -import { getInitials } from '~/utils'; +import { differenceWith, getInitials, isEqualById } from '~/utils'; +import styles from '../organization.module.css'; export const getColumns: ( id: string, memberRoles: Record, + roles: Role[], canDeleteUser?: boolean, - isLoading?: boolean + isLoading?: boolean, + refetch?: () => void ) => ColumnDef< V1Beta1User & V1Beta1Invitation & { invited?: boolean }, any ->[] = (organizationId, memberRoles = {}, canDeleteUser = false, isLoading) => [ +>[] = ( + organizationId, + memberRoles = {}, + roles = [], + canDeleteUser = false, + isLoading, + refetch = () => null +) => [ { header: '', accessorKey: 'avatar', @@ -98,9 +117,17 @@ export const getColumns: ( ? () => : ({ row }) => ( ( + isEqualById, + roles, + row.original?.id && memberRoles[row.original?.id] + ? memberRoles[row.original?.id] + : [] + )} /> ) } @@ -109,11 +136,15 @@ export const getColumns: ( const MembersActions = ({ member, organizationId, - canDeleteUser + canUpdateGroup, + excludedRoles = [], + refetch = () => null }: { member: V1Beta1User; + canUpdateGroup?: boolean; organizationId: string; - canDeleteUser?: boolean; + excludedRoles: V1Beta1Role[]; + refetch: () => void; }) => { const { client } = useFrontier(); const navigate = useNavigate({ from: '/members' }); @@ -141,14 +172,64 @@ const MembersActions = ({ }); } } + async function updateRole(role: V1Beta1Role) { + try { + const resource = `app/organization:${organizationId}`; + const principal = `app/user:${member?.id}`; + const { + // @ts-ignore + data: { policies = [] } + } = await client?.adminServiceListPolicies({ + orgId: organizationId, + userId: member.id + }); + const deletePromises = policies.map((p: V1Beta1Policy) => + client?.frontierServiceDeletePolicy(p.id as string) + ); + + await Promise.all(deletePromises); + await client?.frontierServiceCreatePolicy({ + roleId: role.id as string, + title: role.name as string, + resource: resource, + principal: principal + }); + refetch(); + toast.success('Member role updated'); + } catch ({ error }: any) { + toast.error('Something went wrong', { + description: error.message + }); + } + } + + return canUpdateGroup ? ( + + + + + + + {excludedRoles.map((role: V1Beta1Role) => ( + +
updateRole(role)} + className={styles.dropdownActionItem} + > + + Make {role.title} +
+
+ ))} - return canDeleteUser ? ( - - - + +
+ + Remove +
+
+
+
+
) : null; }; diff --git a/sdks/js/packages/core/react/components/organization/members/member.types.tsx b/sdks/js/packages/core/react/components/organization/members/member.types.tsx index 5c3e7659f..03c446289 100644 --- a/sdks/js/packages/core/react/components/organization/members/member.types.tsx +++ b/sdks/js/packages/core/react/components/organization/members/member.types.tsx @@ -15,4 +15,6 @@ export type MembersTableType = { canCreateInvite?: boolean; canDeleteUser?: boolean; memberRoles: Record; + roles: Role[]; + refetch?: () => void; }; diff --git a/sdks/js/packages/core/react/components/organization/project/index.tsx b/sdks/js/packages/core/react/components/organization/project/index.tsx index 41f7f590a..ed3e16ab7 100644 --- a/sdks/js/packages/core/react/components/organization/project/index.tsx +++ b/sdks/js/packages/core/react/components/organization/project/index.tsx @@ -11,15 +11,15 @@ import { } from '@raystack/apsara'; import { Outlet, useNavigate, useRouterState } from '@tanstack/react-router'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import Skeleton from 'react-loading-skeleton'; import { useFrontier } from '~/react/contexts/FrontierContext'; import { useOrganizationProjects } from '~/react/hooks/useOrganizationProjects'; import { usePermissions } from '~/react/hooks/usePermissions'; +import { AuthTooltipMessage } from '~/react/utils'; import { V1Beta1Project } from '~/src'; import { PERMISSIONS, shouldShowComponent } from '~/utils'; import { styles } from '../styles'; import { getColumns } from './projects.columns'; -import Skeleton from 'react-loading-skeleton'; -import { AuthTooltipMessage } from '~/react/utils'; const projectsSelectOptions = [ { value: 'my-projects', label: 'My Projects' }, diff --git a/sdks/js/packages/core/react/components/organization/project/members/index.tsx b/sdks/js/packages/core/react/components/organization/project/members/index.tsx index b16e3b67b..b102d1c85 100644 --- a/sdks/js/packages/core/react/components/organization/project/members/index.tsx +++ b/sdks/js/packages/core/react/components/organization/project/members/index.tsx @@ -1,19 +1,34 @@ +import { + CardStackPlusIcon, + MagnifyingGlassIcon, + PlusIcon +} from '@radix-ui/react-icons'; import { Avatar, Button, DataTable, - Popover, EmptyState, Flex, + Popover, + Separator, Text, TextField, - Tooltip, - Separator + Tooltip } from '@raystack/apsara'; -import { useNavigate, useParams } from '@tanstack/react-router'; +import { useParams } from '@tanstack/react-router'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import Skeleton from 'react-loading-skeleton'; +import { toast } from 'sonner'; +import { useFrontier } from '~/react/contexts/FrontierContext'; +import { useOrganizationTeams } from '~/react/hooks/useOrganizationTeams'; import { usePermissions } from '~/react/hooks/usePermissions'; -import { V1Beta1Group, V1Beta1PolicyRequestBody, V1Beta1User } from '~/src'; +import { AuthTooltipMessage } from '~/react/utils'; +import { + V1Beta1Group, + V1Beta1PolicyRequestBody, + V1Beta1Role, + V1Beta1User +} from '~/src'; import { Role } from '~/src/types'; import { PERMISSIONS, @@ -22,21 +37,12 @@ import { shouldShowComponent } from '~/utils'; import { getColumns } from './member.columns'; -import { useFrontier } from '~/react/contexts/FrontierContext'; -import { toast } from 'sonner'; -import { - CardStackPlusIcon, - MagnifyingGlassIcon, - PlusIcon -} from '@radix-ui/react-icons'; import styles from './members.module.css'; -import Skeleton from 'react-loading-skeleton'; -import { AuthTooltipMessage } from '~/react/utils'; -import { useOrganizationTeams } from '~/react/hooks/useOrganizationTeams'; export type MembersProps = { teams?: V1Beta1Group[]; members?: V1Beta1User[]; + roles?: V1Beta1Role[]; memberRoles?: Record; isLoading?: boolean; refetch: () => void; @@ -45,6 +51,7 @@ export type MembersProps = { export const Members = ({ teams = [], members = [], + roles = [], memberRoles, isLoading: isMemberLoading, refetch @@ -87,8 +94,16 @@ export const Members = ({ const isLoading = isMemberLoading || isPermissionsFetching; const columns = useMemo( - () => getColumns(memberRoles, isLoading), - [memberRoles, isLoading] + () => + getColumns( + memberRoles, + roles, + canUpdateProject, + isLoading, + projectId, + refetch + ), + [memberRoles, roles, canUpdateProject, isLoading, projectId, refetch] ); const updatedUsers = useMemo(() => { diff --git a/sdks/js/packages/core/react/components/organization/project/members/member.columns.tsx b/sdks/js/packages/core/react/components/organization/project/members/member.columns.tsx index ed3133543..594a9b9d8 100644 --- a/sdks/js/packages/core/react/components/organization/project/members/member.columns.tsx +++ b/sdks/js/packages/core/react/components/organization/project/members/member.columns.tsx @@ -1,10 +1,15 @@ -import { Avatar, Flex, Label, Text } from '@raystack/apsara'; +import { DotsHorizontalIcon, UpdateIcon } from '@radix-ui/react-icons'; +import { Avatar, DropdownMenu, Flex, Label, Text } from '@raystack/apsara'; +import { useNavigate } from '@tanstack/react-router'; import { ColumnDef } from '@tanstack/react-table'; import Skeleton from 'react-loading-skeleton'; -import { V1Beta1Group, V1Beta1User } from '~/src'; -import { Role } from '~/src/types'; -import { getInitials } from '~/utils'; +import { toast } from 'sonner'; import teamIcon from '~/react/assets/users.svg'; +import { useFrontier } from '~/react/contexts/FrontierContext'; +import { V1Beta1Group, V1Beta1Policy, V1Beta1Role, V1Beta1User } from '~/src'; +import { Role } from '~/src/types'; +import { differenceWith, getInitials, isEqualById } from '~/utils'; +import styles from '../../organization.module.css'; type ColumnType = V1Beta1User & (V1Beta1Group & { isTeam: boolean }); @@ -18,7 +23,11 @@ const teamAvatarStyles: React.CSSProperties = { export const getColumns = ( memberRoles: Record = {}, - isLoading: boolean + roles: V1Beta1Role[] = [], + canUpdateProject: boolean, + isLoading: boolean, + projectId: string, + refetch: () => void ): ColumnDef[] => [ { header: '', @@ -90,5 +99,103 @@ export const getColumns = ( .join(', ')) ?? 'Inherited role'; } + }, + { + header: '', + accessorKey: 'id', + meta: { + style: { + textAlign: 'end' + } + }, + cell: isLoading + ? () => + : ({ row }) => ( + ( + isEqualById, + roles, + row.original?.id && memberRoles[row.original?.id] + ? memberRoles[row.original?.id] + : [] + )} + /> + ) } ]; + +const MembersActions = ({ + projectId, + member, + canUpdateProject, + excludedRoles = [], + refetch = () => null +}: { + projectId: string; + member: V1Beta1User; + canUpdateProject?: boolean; + excludedRoles: V1Beta1Role[]; + refetch: () => void; +}) => { + const { client } = useFrontier(); + const navigate = useNavigate({ from: '/projects' }); + + async function updateRole(role: V1Beta1Role) { + try { + const resource = `app/project:${projectId}`; + const principal = `app/user:${member?.id}`; + const { + // @ts-ignore + data: { policies = [] } + } = await client?.adminServiceListPolicies({ + projectId: projectId, + userId: member.id + }); + + const deletePromises = policies.map((p: V1Beta1Policy) => + client?.frontierServiceDeletePolicy(p.id as string) + ); + + await Promise.all(deletePromises); + await client?.frontierServiceCreatePolicy({ + roleId: role.id as string, + title: role.name as string, + resource: resource, + principal: principal + }); + refetch(); + toast.success('Project member role updated'); + } catch ({ error }: any) { + toast.error('Something went wrong', { + description: error.message + }); + } + } + + return canUpdateProject ? ( + + + + + + + {excludedRoles.map((role: V1Beta1Role) => ( + +
updateRole(role)} + className={styles.dropdownActionItem} + > + + Make {role.title} +
+
+ ))} +
+
+
+ ) : null; +}; diff --git a/sdks/js/packages/core/react/components/organization/project/project.tsx b/sdks/js/packages/core/react/components/organization/project/project.tsx index 1c3a8e483..fba1dffce 100644 --- a/sdks/js/packages/core/react/components/organization/project/project.tsx +++ b/sdks/js/packages/core/react/components/organization/project/project.tsx @@ -11,8 +11,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { toast } from 'sonner'; import backIcon from '~/react/assets/chevron-left.svg'; import { useFrontier } from '~/react/contexts/FrontierContext'; -import { V1Beta1Group, V1Beta1Project, V1Beta1User } from '~/src'; +import { V1Beta1Group, V1Beta1Project, V1Beta1Role, V1Beta1User } from '~/src'; import { Role } from '~/src/types'; +import { PERMISSIONS } from '~/utils'; import { styles } from '../styles'; import { General } from './general'; import { Members } from './members'; @@ -20,6 +21,8 @@ import { Members } from './members'; export const ProjectPage = () => { let { projectId } = useParams({ from: '/projects/$projectId' }); const [isProjectLoading, setIsProjectLoading] = useState(false); + const [isProjectRoleLoading, setIsProjectRoleLoading] = useState(false); + const [roles, setRoles] = useState([]); const [project, setProject] = useState(); const [members, setMembers] = useState([]); const [memberRoles, setMemberRoles] = useState>({}); @@ -101,11 +104,33 @@ export const ProjectPage = () => { } }, [client, isDeleteRoute, organization?.id, projectId]); + const getProjectRoles = useCallback(async () => { + if (!organization?.id || !projectId || isDeleteRoute) return; + try { + setIsProjectRoleLoading(true); + const { + // @ts-ignore + data: { roles } + } = await client?.frontierServiceListRoles({ + state: 'enabled', + scopes: [PERMISSIONS.ProjectNamespace] + }); + setRoles(roles); + } catch (error: any) { + toast.error('Something went wrong', { + description: error?.message + }); + } finally { + setIsProjectRoleLoading(false); + } + }, [client, isDeleteRoute, organization?.id, projectId]); + useEffect(() => { getProjectDetails(); getProjectMembers(); getProjectTeams(); - }, [getProjectDetails, getProjectMembers, getProjectTeams]); + getProjectRoles(); + }, [getProjectDetails, getProjectMembers, getProjectTeams, getProjectRoles]); const isLoading = isProjectLoading || isTeamsLoading || isMembersLoading; @@ -148,6 +173,7 @@ export const ProjectPage = () => { memberRoles={memberRoles} isLoading={isLoading} teams={teams} + roles={roles} refetch={refetchTeamAndMembers} /> diff --git a/sdks/js/packages/core/react/components/organization/teams/members/index.tsx b/sdks/js/packages/core/react/components/organization/teams/members/index.tsx index dbe982413..9fb1dcac8 100644 --- a/sdks/js/packages/core/react/components/organization/teams/members/index.tsx +++ b/sdks/js/packages/core/react/components/organization/teams/members/index.tsx @@ -14,8 +14,12 @@ import { Link, useNavigate, useParams } from '@tanstack/react-router'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { toast } from 'sonner'; +import { MagnifyingGlassIcon, PaperPlaneIcon } from '@radix-ui/react-icons'; +import Skeleton from 'react-loading-skeleton'; +import { useFrontier } from '~/react/contexts/FrontierContext'; import { usePermissions } from '~/react/hooks/usePermissions'; -import { V1Beta1User } from '~/src'; +import { AuthTooltipMessage } from '~/react/utils'; +import { V1Beta1Role, V1Beta1User } from '~/src'; import { Role } from '~/src/types'; import { PERMISSIONS, @@ -24,14 +28,11 @@ import { shouldShowComponent } from '~/utils'; import { getColumns } from './member.columns'; -import Skeleton from 'react-loading-skeleton'; -import { AuthTooltipMessage } from '~/react/utils'; -import { MagnifyingGlassIcon, PaperPlaneIcon } from '@radix-ui/react-icons'; -import { useFrontier } from '~/react/contexts/FrontierContext'; import styles from './members.module.css'; export type MembersProps = { members: V1Beta1User[]; + roles: V1Beta1Role[]; organizationId: string; memberRoles?: Record; isLoading?: boolean; @@ -40,6 +41,7 @@ export type MembersProps = { export const Members = ({ members, + roles = [], organizationId, memberRoles = {}, isLoading: isMemberLoading, @@ -85,12 +87,21 @@ export const Members = ({ const columns = useMemo( () => getColumns({ + roles, organizationId, canUpdateGroup, memberRoles, - isLoading + isLoading, + refetchMembers }), - [organizationId, canUpdateGroup, memberRoles, isLoading] + [ + roles, + organizationId, + canUpdateGroup, + memberRoles, + isLoading, + refetchMembers + ] ); const updatedUsers = useMemo(() => { diff --git a/sdks/js/packages/core/react/components/organization/teams/members/member.columns.tsx b/sdks/js/packages/core/react/components/organization/teams/members/member.columns.tsx index bbf8de870..6a6336a64 100644 --- a/sdks/js/packages/core/react/components/organization/teams/members/member.columns.tsx +++ b/sdks/js/packages/core/react/components/organization/teams/members/member.columns.tsx @@ -9,26 +9,29 @@ import type { ColumnDef } from '@tanstack/react-table'; import Skeleton from 'react-loading-skeleton'; import { toast } from 'sonner'; import { useFrontier } from '~/react/contexts/FrontierContext'; -import { V1Beta1User } from '~/src'; +import { V1Beta1Policy, V1Beta1Role, V1Beta1User } from '~/src'; import { Role } from '~/src/types'; -import { getInitials } from '~/utils'; +import { differenceWith, getInitials, isEqualById } from '~/utils'; import styles from '../../organization.module.css'; -import { useMemo } from 'react'; interface getColumnsOptions { + roles: V1Beta1Role[]; organizationId: string; canUpdateGroup?: boolean; memberRoles?: Record; isLoading?: boolean; + refetchMembers: () => void; } export const getColumns: ( options: getColumnsOptions ) => ColumnDef[] = ({ + roles = [], organizationId, canUpdateGroup = false, memberRoles = {}, - isLoading + isLoading, + refetchMembers }) => [ { header: '', @@ -100,9 +103,17 @@ export const getColumns: ( ? () => : ({ row }) => ( ( + isEqualById, + roles, + row.original?.id && memberRoles[row.original?.id] + ? memberRoles[row.original?.id] + : [] + )} /> ) } @@ -111,11 +122,15 @@ export const getColumns: ( const MembersActions = ({ member, organizationId, - canUpdateGroup + canUpdateGroup, + excludedRoles = [], + refetch = () => null }: { member: V1Beta1User; canUpdateGroup?: boolean; organizationId: string; + excludedRoles: V1Beta1Role[]; + refetch: () => void; }) => { let { teamId } = useParams({ from: '/teams/$teamId' }); const { client } = useFrontier(); @@ -142,6 +157,37 @@ const MembersActions = ({ } } + async function updateRole(role: V1Beta1Role) { + try { + const resource = `app/group:${teamId}`; + const principal = `app/user:${member?.id}`; + const { + // @ts-ignore + data: { policies = [] } + } = await client?.adminServiceListPolicies({ + groupId: teamId, + userId: member.id + }); + + const deletePromises = policies.map((p: V1Beta1Policy) => + client?.frontierServiceDeletePolicy(p.id as string) + ); + + await Promise.all(deletePromises); + await client?.frontierServiceCreatePolicy({ + roleId: role.id as string, + title: role.name as string, + resource: resource, + principal: principal + }); + refetch(); + toast.success('Team member role updated'); + } catch ({ error }: any) { + toast.error('Something went wrong', { + description: error.message + }); + } + } return canUpdateGroup ? ( @@ -149,6 +195,17 @@ const MembersActions = ({ + {excludedRoles.map((role: V1Beta1Role) => ( + +
updateRole(role)} + className={styles.dropdownActionItem} + > + + Make {role.title} +
+
+ ))}
diff --git a/sdks/js/packages/core/react/components/organization/teams/team.tsx b/sdks/js/packages/core/react/components/organization/teams/team.tsx index 6e4afbca0..597dfc687 100644 --- a/sdks/js/packages/core/react/components/organization/teams/team.tsx +++ b/sdks/js/packages/core/react/components/organization/teams/team.tsx @@ -11,8 +11,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { toast } from 'sonner'; import backIcon from '~/react/assets/chevron-left.svg'; import { useFrontier } from '~/react/contexts/FrontierContext'; -import { V1Beta1Group, V1Beta1User } from '~/src'; +import { V1Beta1Group, V1Beta1Role, V1Beta1User } from '~/src'; import { Role } from '~/src/types'; +import { PERMISSIONS } from '~/utils'; import { styles } from '../styles'; import { General } from './general'; import { Members } from './members'; @@ -24,6 +25,8 @@ export const TeamPage = () => { const [memberRoles, setMemberRoles] = useState>({}); const [isTeamLoading, setIsTeamLoading] = useState(false); const [isMembersLoading, setIsMembersLoading] = useState(false); + const [isTeamRoleLoading, setIsTeamRoleLoading] = useState(false); + const [roles, setRoles] = useState([]); const { client, activeOrganization: organization } = useFrontier(); let navigate = useNavigate({ from: '/teams/$teamId' }); @@ -85,9 +88,31 @@ export const TeamPage = () => { } }, [client, isDeleteRoute, organization?.id, teamId]); + const getTeamRoles = useCallback(async () => { + if (!organization?.id || !teamId || isDeleteRoute) return; + try { + setIsTeamRoleLoading(true); + const { + // @ts-ignore + data: { roles } + } = await client?.frontierServiceListRoles({ + state: 'enabled', + scopes: [PERMISSIONS.GroupNamespace] + }); + setRoles(roles); + } catch (error: any) { + toast.error('Something went wrong', { + description: error?.message + }); + } finally { + setIsTeamRoleLoading(false); + } + }, [client, isDeleteRoute, organization?.id, teamId]); + useEffect(() => { getTeamMembers(); - }, [getTeamMembers]); + getTeamRoles(); + }, [getTeamMembers, getTeamRoles]); return ( @@ -120,6 +145,7 @@ export const TeamPage = () => { { const [users, setUsers] = useState([]); + const [roles, setRoles] = useState([]); const [invitations, setInvitations] = useState([]); const [isUsersLoading, setIsUsersLoading] = useState(false); + const [isRolesLoading, setIsRolesLoading] = useState(false); const [isInvitationsLoading, setIsInvitationsLoading] = useState(false); const [memberRoles, setMemberRoles] = useState>({}); @@ -36,6 +39,25 @@ export const useOrganizationMembers = ({ showInvitations = false }) => { } }, [client, organization?.id]); + const fetchOrganizationRoles = useCallback(async () => { + if (!organization?.id) return; + try { + setIsRolesLoading(true); + const { + // @ts-ignore + data: { roles } + } = await client?.frontierServiceListRoles({ + state: 'enabled', + scopes: [PERMISSIONS.OrganizationNamespace] + }); + setRoles(roles); + } catch (err) { + console.error(err); + } finally { + setIsRolesLoading(false); + } + }, [client, organization?.id]); + const fetchInvitations = useCallback(async () => { if (!organization?.id) return; try { @@ -63,6 +85,10 @@ export const useOrganizationMembers = ({ showInvitations = false }) => { fetchOrganizationUser(); }, [fetchOrganizationUser]); + useEffect(() => { + fetchOrganizationRoles(); + }, [fetchOrganizationRoles]); + useEffect(() => { if (showInvitations) { fetchInvitations(); @@ -91,6 +117,7 @@ export const useOrganizationMembers = ({ showInvitations = false }) => { isFetching, members: updatedUsers, memberRoles, + roles, refetch }; }; diff --git a/sdks/js/packages/core/utils/index.ts b/sdks/js/packages/core/utils/index.ts index 4ea70ae57..5edaa20b6 100644 --- a/sdks/js/packages/core/utils/index.ts +++ b/sdks/js/packages/core/utils/index.ts @@ -28,6 +28,16 @@ export const filterUsersfromUsers = ( ); }; +type Predicate = (a: T, b: T) => boolean; +export const isEqualById = (a: any, b: any) => a.id === b.id; +export function differenceWith( + pred: Predicate, + list1: T[], + list2: T[] +): T[] { + return list1.filter(item1 => !list2.some(item2 => pred(item1, item2))); +} + export const PERMISSIONS = { // namespace PlatformNamespace: 'app/platform',