Skip to content

Commit

Permalink
Role assignment for org members, project members & team members (#426)
Browse files Browse the repository at this point in the history
* feat: add ui support to change role

* feat: add logic to add/remove policies
  • Loading branch information
Praveen Yadav authored Dec 13, 2023
1 parent 924c9a8 commit 88d90f0
Show file tree
Hide file tree
Showing 12 changed files with 440 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -63,10 +63,11 @@ export default function WorkspaceMembers() {
}, [permissions, resource]);

const {
isFetching: isOrgMembersLoading,
roles,
members,
memberRoles,
refetch
refetch,
isFetching: isOrgMembersLoading
} = useOrganizationMembers({
showInvitations: canCreateInvite
});
Expand All @@ -89,13 +90,14 @@ export default function WorkspaceMembers() {
<ManageMembers />
{organization?.id ? (
<MembersTable
// @ts-ignore
roles={roles}
users={members}
organizationId={organization?.id}
isLoading={isLoading}
canCreateInvite={canCreateInvite}
canDeleteUser={canDeleteUser}
memberRoles={memberRoles}
refetch={refetch}
/>
) : null}
</Flex>
Expand All @@ -122,7 +124,9 @@ const MembersTable = ({
canCreateInvite,
canDeleteUser,
organizationId,
memberRoles
memberRoles,
roles,
refetch
}: MembersTableType) => {
let navigate = useNavigate({ from: '/members' });

Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, Role[]>,
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',
Expand Down Expand Up @@ -98,9 +117,17 @@ export const getColumns: (
? () => <Skeleton />
: ({ row }) => (
<MembersActions
refetch={refetch}
member={row.original as V1Beta1User}
organizationId={organizationId}
canDeleteUser={canDeleteUser}
canUpdateGroup={canDeleteUser}
excludedRoles={differenceWith<V1Beta1Role>(
isEqualById,
roles,
row.original?.id && memberRoles[row.original?.id]
? memberRoles[row.original?.id]
: []
)}
/>
)
}
Expand All @@ -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' });
Expand Down Expand Up @@ -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 ? (
<DropdownMenu style={{ padding: '0 !important' }}>
<DropdownMenu.Trigger asChild style={{ cursor: 'pointer' }}>
<DotsHorizontalIcon />
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Group style={{ padding: 0 }}>
{excludedRoles.map((role: V1Beta1Role) => (
<DropdownMenu.Item style={{ padding: 0 }} key={role.id}>
<div
onClick={() => updateRole(role)}
className={styles.dropdownActionItem}
>
<UpdateIcon />
Make {role.title}
</div>
</DropdownMenu.Item>
))}

return canDeleteUser ? (
<Flex align="center" justify="end" gap="large">
<TrashIcon
onClick={deleteMember}
color="var(--foreground-danger)"
style={{ cursor: 'pointer' }}
/>
</Flex>
<DropdownMenu.Item style={{ padding: 0 }}>
<div onClick={deleteMember} className={styles.dropdownActionItem}>
<TrashIcon />
Remove
</div>
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu>
) : null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ export type MembersTableType = {
canCreateInvite?: boolean;
canDeleteUser?: boolean;
memberRoles: Record<string, Role[]>;
roles: Role[];
refetch?: () => void;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<string, Role[]>;
isLoading?: boolean;
refetch: () => void;
Expand All @@ -45,6 +51,7 @@ export type MembersProps = {
export const Members = ({
teams = [],
members = [],
roles = [],
memberRoles,
isLoading: isMemberLoading,
refetch
Expand Down Expand Up @@ -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(() => {
Expand Down
Loading

0 comments on commit 88d90f0

Please sign in to comment.