mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 15:27:46 +00:00
improvement: standardize and improve org access control
This commit is contained in:
@@ -392,7 +392,12 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityOrgMembership}.identityId`)
|
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityOrgMembership}.identityId`)
|
||||||
.where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
|
.where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
|
||||||
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||||
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection)
|
.orderBy(
|
||||||
|
orderBy === OrgIdentityOrderBy.Role
|
||||||
|
? `${TableName.IdentityOrgMembership}.${orderBy}`
|
||||||
|
: `${TableName.Identity}.${orderBy}`,
|
||||||
|
orderDirection
|
||||||
|
)
|
||||||
.select(`${TableName.IdentityOrgMembership}.id`)
|
.select(`${TableName.IdentityOrgMembership}.id`)
|
||||||
.select<{ id: string; total_count: string }>(
|
.select<{ id: string; total_count: string }>(
|
||||||
db.raw(
|
db.raw(
|
||||||
@@ -523,6 +528,23 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
if (orderBy === OrgIdentityOrderBy.Name) {
|
if (orderBy === OrgIdentityOrderBy.Name) {
|
||||||
void query.orderBy("identityName", orderDirection);
|
void query.orderBy("identityName", orderDirection);
|
||||||
|
} else if (orderBy === OrgIdentityOrderBy.Role) {
|
||||||
|
void query.orderByRaw(
|
||||||
|
`
|
||||||
|
CASE
|
||||||
|
WHEN ??.role = ?
|
||||||
|
THEN ??.slug
|
||||||
|
ELSE ??.role
|
||||||
|
END ?
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
TableName.IdentityOrgMembership,
|
||||||
|
"custom",
|
||||||
|
TableName.OrgRoles,
|
||||||
|
TableName.IdentityOrgMembership,
|
||||||
|
db.raw(orderDirection)
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const docs = await query;
|
const docs = await query;
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ export type TListOrgIdentitiesByOrgIdDTO = {
|
|||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
|
||||||
export enum OrgIdentityOrderBy {
|
export enum OrgIdentityOrderBy {
|
||||||
Name = "name"
|
Name = "name",
|
||||||
// Role = "role"
|
Role = "role"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TSearchOrgIdentitiesByOrgIdDAL = {
|
export type TSearchOrgIdentitiesByOrgIdDAL = {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const Pagination = ({
|
|||||||
<FontAwesomeIcon className="text-xs" icon={faCaretDown} />
|
<FontAwesomeIcon className="text-xs" icon={faCaretDown} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="min-w-fit">
|
<DropdownMenuContent sideOffset={2} className="min-w-fit">
|
||||||
{perPageList.map((perPageOption) => (
|
{perPageList.map((perPageOption) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={`pagination-per-page-options-${perPageOption}`}
|
key={`pagination-per-page-options-${perPageOption}`}
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ export const useSearchIdentities = (dto: TSearchIdentitiesDTO) => {
|
|||||||
search
|
search
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}
|
},
|
||||||
|
placeholderData: (previousData) => previousData
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -154,6 +154,6 @@ export type TOrgIdentitiesList = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export enum OrgIdentityOrderBy {
|
export enum OrgIdentityOrderBy {
|
||||||
Name = "name"
|
Name = "name",
|
||||||
// Role = "role"
|
Role = "role"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -56,12 +56,12 @@ export const OrgGroupsSection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Groups</p>
|
<p className="text-xl font-semibold text-mineshaft-100">Groups</p>
|
||||||
<OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}>
|
<OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="primary"
|
colorSchema="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => handleAddGroupModal()}
|
onClick={() => handleAddGroupModal()}
|
||||||
|
|||||||
+24
-24
@@ -2,14 +2,17 @@ import { useMemo } from "react";
|
|||||||
import {
|
import {
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
faArrowUp,
|
faArrowUp,
|
||||||
faEllipsis,
|
faCopy,
|
||||||
|
faEdit,
|
||||||
|
faEllipsisV,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faSearch,
|
faSearch,
|
||||||
|
faTrash,
|
||||||
|
faUserGroup,
|
||||||
faUsers
|
faUsers
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
@@ -261,7 +264,8 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
<Select
|
<Select
|
||||||
value={role === "custom" ? (customRole?.slug as string) : role}
|
value={role === "custom" ? (customRole?.slug as string) : role}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
className="w-48 bg-mineshaft-600"
|
className="h-8 w-48 bg-mineshaft-700"
|
||||||
|
position="popper"
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
onValueChange={(selectedRole) =>
|
onValueChange={(selectedRole) =>
|
||||||
handleChangeRole({
|
handleChangeRole({
|
||||||
@@ -282,13 +286,19 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<DropdownMenuTrigger asChild>
|
||||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
<IconButton
|
||||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
ariaLabel="Options"
|
||||||
</div>
|
className="w-6"
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEllipsisV} />
|
||||||
|
</IconButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
<DropdownMenuContent sideOffset={2} align="end">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -306,10 +316,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={twMerge(
|
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||||
!isAllowed &&
|
|
||||||
"pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handlePopUpOpen("group", {
|
handlePopUpOpen("group", {
|
||||||
@@ -320,7 +327,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
customRole
|
customRole
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
disabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Edit Group
|
Edit Group
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -332,10 +339,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={twMerge(
|
icon={<FontAwesomeIcon icon={faUserGroup} />}
|
||||||
!isAllowed &&
|
|
||||||
"pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate({
|
navigate({
|
||||||
to: "/organization/groups/$groupId",
|
to: "/organization/groups/$groupId",
|
||||||
@@ -344,7 +348,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Manage Members
|
Manage Members
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -356,11 +360,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={twMerge(
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
isAllowed
|
|
||||||
? "hover:!bg-red-500 hover:!text-white"
|
|
||||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handlePopUpOpen("deleteGroup", {
|
handlePopUpOpen("deleteGroup", {
|
||||||
@@ -368,7 +368,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
name
|
name
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
disabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Delete Group
|
Delete Group
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
+14
-12
@@ -1,4 +1,4 @@
|
|||||||
import { faArrowUpRightFromSquare, faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
@@ -71,20 +71,22 @@ export const IdentitySection = withPermission(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
<div className="flex items-center gap-1">
|
||||||
<div className="flex w-full justify-end pr-4">
|
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||||
<a
|
<a
|
||||||
|
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
|
||||||
className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white"
|
|
||||||
>
|
>
|
||||||
Documentation{" "}
|
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
icon={faArrowUpRightFromSquare}
|
<span>Docs</span>
|
||||||
className="mb-[0.06rem] ml-1 text-xs"
|
<FontAwesomeIcon
|
||||||
/>
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
@@ -93,7 +95,7 @@ export const IdentitySection = withPermission(
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="primary"
|
colorSchema="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
+103
-97
@@ -1,12 +1,15 @@
|
|||||||
import { useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import {
|
import {
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
faArrowUp,
|
faArrowUp,
|
||||||
faEllipsis,
|
faCheckCircle,
|
||||||
|
faChevronRight,
|
||||||
|
faEdit,
|
||||||
|
faEllipsisV,
|
||||||
faFilter,
|
faFilter,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faServer
|
faServer,
|
||||||
|
faTrash
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
@@ -15,19 +18,18 @@ import { twMerge } from "tailwind-merge";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
|
DropdownSubMenu,
|
||||||
|
DropdownSubMenuContent,
|
||||||
|
DropdownSubMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
FormControl,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Pagination,
|
Pagination,
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
Spinner,
|
Spinner,
|
||||||
@@ -38,7 +40,6 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tooltip,
|
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
@@ -63,6 +64,10 @@ type Props = {
|
|||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Filter = {
|
||||||
|
roles: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
@@ -90,7 +95,9 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
setUserTablePreference("identityTable", PreferenceKey.PerPage, newPerPage);
|
setUserTablePreference("identityTable", PreferenceKey.PerPage, newPerPage);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [filteredRoles, setFilteredRoles] = useState<string[]>([]);
|
const [filter, setFilter] = useState<Filter>({
|
||||||
|
roles: []
|
||||||
|
});
|
||||||
|
|
||||||
const organizationId = currentOrg?.id || "";
|
const organizationId = currentOrg?.id || "";
|
||||||
|
|
||||||
@@ -103,7 +110,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
orderBy,
|
orderBy,
|
||||||
search: {
|
search: {
|
||||||
name: debouncedSearch ? { $contains: debouncedSearch } : undefined,
|
name: debouncedSearch ? { $contains: debouncedSearch } : undefined,
|
||||||
role: filteredRoles?.length ? { $in: filteredRoles } : undefined
|
role: filter.roles?.length ? { $in: filter.roles } : undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,7 +120,6 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
offset,
|
offset,
|
||||||
setPage
|
setPage
|
||||||
});
|
});
|
||||||
const filterForm = useForm<{ roles: string }>();
|
|
||||||
|
|
||||||
const { data: roles } = useGetOrgRoles(organizationId);
|
const { data: roles } = useGetOrgRoles(organizationId);
|
||||||
|
|
||||||
@@ -153,79 +159,80 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRoleToggle = useCallback(
|
||||||
|
(roleSlug: string) =>
|
||||||
|
setFilter((state) => {
|
||||||
|
const currentRoles = state.roles || [];
|
||||||
|
|
||||||
|
if (currentRoles.includes(roleSlug)) {
|
||||||
|
return { ...state, roles: currentRoles.filter((role) => role !== roleSlug) };
|
||||||
|
}
|
||||||
|
return { ...state, roles: [...currentRoles, roleSlug] };
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isTableFiltered = Boolean(filter.roles.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4 flex items-center space-x-2">
|
<div className="mb-4 flex items-center space-x-2">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="Filter Identities"
|
||||||
|
variant="plain"
|
||||||
|
size="sm"
|
||||||
|
className={twMerge(
|
||||||
|
"flex h-[2.375rem] w-[2.6rem] items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||||
|
isTableFiltered && "border-primary/50 text-primary"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faFilter} />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-0">
|
||||||
|
<DropdownMenuLabel>Filter By</DropdownMenuLabel>
|
||||||
|
<DropdownSubMenu>
|
||||||
|
<DropdownSubMenuTrigger
|
||||||
|
iconPos="right"
|
||||||
|
icon={<FontAwesomeIcon icon={faChevronRight} size="sm" />}
|
||||||
|
>
|
||||||
|
Roles
|
||||||
|
</DropdownSubMenuTrigger>
|
||||||
|
<DropdownSubMenuContent className="thin-scrollbar max-h-[20rem] overflow-y-auto rounded-l-none">
|
||||||
|
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||||
|
Apply Roles to Filter Identities
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{roles?.map(({ id, slug, name }) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
handleRoleToggle(slug);
|
||||||
|
}}
|
||||||
|
key={id}
|
||||||
|
icon={filter.roles.includes(slug) && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div
|
||||||
|
className="mr-2 h-2 w-2 rounded-full"
|
||||||
|
style={{ background: "#bec2c8" }}
|
||||||
|
/>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownSubMenuContent>
|
||||||
|
</DropdownSubMenu>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
<Input
|
<Input
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
placeholder="Search identities by name..."
|
placeholder="Search identities by name..."
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="filter"
|
|
||||||
variant="outline_bg"
|
|
||||||
className={filteredRoles?.length ? "border-primary" : ""}
|
|
||||||
>
|
|
||||||
<Tooltip content="Advance Filter">
|
|
||||||
<FontAwesomeIcon icon={faFilter} />
|
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl">
|
|
||||||
<div className="mb-4 border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
|
|
||||||
Advance Filter
|
|
||||||
</div>
|
|
||||||
<form
|
|
||||||
onSubmit={filterForm.handleSubmit((el) => {
|
|
||||||
setFilteredRoles(el.roles?.split(",")?.filter(Boolean) || []);
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={filterForm.control}
|
|
||||||
name="roles"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Roles"
|
|
||||||
helperText="Eg: admin,viewer"
|
|
||||||
isError={Boolean(error?.message)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
size="xs"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
className="mt-4"
|
|
||||||
>
|
|
||||||
Apply Filter
|
|
||||||
</Button>
|
|
||||||
{Boolean(filteredRoles.length) && (
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant="link"
|
|
||||||
className="ml-4 mt-4"
|
|
||||||
onClick={() => {
|
|
||||||
filterForm.reset({ roles: "" });
|
|
||||||
setFilteredRoles([]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
@@ -251,8 +258,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</Th>
|
</Th>
|
||||||
<Th>Role</Th>
|
<Th>
|
||||||
{/* <Th>
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
Role
|
Role
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -271,7 +277,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</Th> */}
|
</Th>
|
||||||
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
@@ -303,7 +309,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
<Select
|
<Select
|
||||||
value={role === "custom" ? (customRole?.slug as string) : role}
|
value={role === "custom" ? (customRole?.slug as string) : role}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
className="w-48 bg-mineshaft-600"
|
className="h-8 w-48 bg-mineshaft-700"
|
||||||
|
position="popper"
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
onValueChange={(selectedRole) =>
|
onValueChange={(selectedRole) =>
|
||||||
handleChangeRole({
|
handleChangeRole({
|
||||||
@@ -324,21 +331,24 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<DropdownMenuTrigger asChild>
|
||||||
<div className="flex justify-center hover:text-primary-400 data-[state=open]:text-primary-400">
|
<IconButton
|
||||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
ariaLabel="Options"
|
||||||
</div>
|
className="w-6"
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEllipsisV} />
|
||||||
|
</IconButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="mt-3 p-1">
|
<DropdownMenuContent sideOffset={2} align="end">
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionIdentityActions.Edit}
|
I={OrgPermissionIdentityActions.Edit}
|
||||||
a={OrgPermissionSubjects.Identity}
|
a={OrgPermissionSubjects.Identity}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={twMerge(
|
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
navigate({
|
navigate({
|
||||||
@@ -348,7 +358,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
disabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Edit Identity
|
Edit Identity
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -360,11 +370,6 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={twMerge(
|
|
||||||
isAllowed
|
|
||||||
? "hover:!bg-red-500 hover:!text-white"
|
|
||||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handlePopUpOpen("deleteIdentity", {
|
handlePopUpOpen("deleteIdentity", {
|
||||||
@@ -372,7 +377,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
name
|
name
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
disabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
>
|
>
|
||||||
Delete Identity
|
Delete Identity
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -398,7 +404,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
{!isPending && data && data?.identities.length === 0 && (
|
{!isPending && data && data?.identities.length === 0 && (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title={
|
title={
|
||||||
debouncedSearch.trim().length > 0 || filteredRoles?.length > 0
|
debouncedSearch.trim().length > 0 || filter.roles?.length > 0
|
||||||
? "No identities match search filter"
|
? "No identities match search filter"
|
||||||
: "No identities have been created in this organization"
|
: "No identities have been created in this organization"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -115,12 +115,12 @@ export const OrgMembersSection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
|
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="primary"
|
colorSchema="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => handleAddMemberModal()}
|
onClick={() => handleAddMemberModal()}
|
||||||
|
|||||||
+144
-121
@@ -4,11 +4,14 @@ import {
|
|||||||
faArrowUp,
|
faArrowUp,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
faEllipsis,
|
faEdit,
|
||||||
|
faEllipsisV,
|
||||||
faFilter,
|
faFilter,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faSearch,
|
faSearch,
|
||||||
faUsers
|
faUsers,
|
||||||
|
faUserSlash,
|
||||||
|
faUserXmark
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
@@ -79,7 +82,8 @@ type Props = {
|
|||||||
|
|
||||||
enum OrgMembersOrderBy {
|
enum OrgMembersOrderBy {
|
||||||
Name = "firstName",
|
Name = "firstName",
|
||||||
Email = "email"
|
Email = "email",
|
||||||
|
Role = "role"
|
||||||
}
|
}
|
||||||
|
|
||||||
type Filter = {
|
type Filter = {
|
||||||
@@ -229,6 +233,16 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
|||||||
valueOne = memberOne.user.email || memberOne.inviteEmail;
|
valueOne = memberOne.user.email || memberOne.inviteEmail;
|
||||||
valueTwo = memberTwo.user.email || memberTwo.inviteEmail;
|
valueTwo = memberTwo.user.email || memberTwo.inviteEmail;
|
||||||
break;
|
break;
|
||||||
|
case OrgMembersOrderBy.Role:
|
||||||
|
valueOne =
|
||||||
|
memberOne.role === "custom"
|
||||||
|
? findRoleFromId(memberOne.roleId)!.slug
|
||||||
|
: memberOne.role;
|
||||||
|
valueTwo =
|
||||||
|
memberTwo.role === "custom"
|
||||||
|
? findRoleFromId(memberTwo.roleId)!.slug
|
||||||
|
: memberTwo.role;
|
||||||
|
break;
|
||||||
case OrgMembersOrderBy.Name:
|
case OrgMembersOrderBy.Name:
|
||||||
default:
|
default:
|
||||||
valueOne = memberOne.user.firstName;
|
valueOne = memberOne.user.firstName;
|
||||||
@@ -284,7 +298,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
|||||||
variant="plain"
|
variant="plain"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
"flex h-[2.375rem] w-[2.6rem] items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||||
isTableFiltered && "border-primary/50 text-primary"
|
isTableFiltered && "border-primary/50 text-primary"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -378,7 +392,26 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</Th>
|
</Th>
|
||||||
<Th>Role</Th>
|
<Th>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Role
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={`ml-2 ${orderBy === OrgMembersOrderBy.Role ? "" : "opacity-30"}`}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(OrgMembersOrderBy.Role)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
orderDirection === OrderByDirection.DESC &&
|
||||||
|
orderBy === OrgMembersOrderBy.Role
|
||||||
|
? faArrowUp
|
||||||
|
: faArrowDown
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
@@ -398,7 +431,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
|||||||
isActive
|
isActive
|
||||||
}) => {
|
}) => {
|
||||||
const name =
|
const name =
|
||||||
u && u.firstName ? `${u.firstName} ${u.lastName ?? ""}`.trim() : "-";
|
u && u.firstName ? `${u.firstName} ${u.lastName ?? ""}`.trim() : null;
|
||||||
const email = u?.email || inviteEmail;
|
const email = u?.email || inviteEmail;
|
||||||
const username = u?.username ?? inviteEmail ?? "-";
|
const username = u?.username ?? inviteEmail ?? "-";
|
||||||
return (
|
return (
|
||||||
@@ -415,7 +448,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Td className={isActive ? "" : "text-mineshaft-400"}>
|
<Td className={isActive ? "" : "text-mineshaft-400"}>
|
||||||
{name}
|
{name ?? <span className="text-mineshaft-400">Not Set</span>}
|
||||||
{u.superAdmin && (
|
{u.superAdmin && (
|
||||||
<Badge variant="primary" className="ml-2">
|
<Badge variant="primary" className="ml-2">
|
||||||
Server Admin
|
Server Admin
|
||||||
@@ -445,7 +478,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
|||||||
<Select
|
<Select
|
||||||
value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
|
value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
|
||||||
isDisabled={userId === u?.id || !isAllowed}
|
isDisabled={userId === u?.id || !isAllowed}
|
||||||
className="w-48 bg-mineshaft-600"
|
className="h-8 w-48 bg-mineshaft-700"
|
||||||
|
position="popper"
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
onValueChange={(selectedRole) =>
|
onValueChange={(selectedRole) =>
|
||||||
onRoleChange(orgMembershipId, selectedRole)
|
onRoleChange(orgMembershipId, selectedRole)
|
||||||
@@ -468,7 +502,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
|||||||
serverDetails?.emailConfigured && (
|
serverDetails?.emailConfigured && (
|
||||||
<Button
|
<Button
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
className="w-48"
|
className="h-8 w-48 border-mineshaft-600 bg-mineshaft-700 font-normal"
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -476,7 +510,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Resend invite
|
Resend Invite
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -484,124 +518,113 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
|||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{userId !== u?.id && (
|
<DropdownMenu>
|
||||||
<DropdownMenu>
|
<DropdownMenuTrigger asChild>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<IconButton
|
||||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
ariaLabel="Options"
|
||||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
colorSchema="secondary"
|
||||||
</div>
|
className={twMerge("w-6", userId === u?.id && "opacity-50")}
|
||||||
</DropdownMenuTrigger>
|
variant="plain"
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
isDisabled={userId === u?.id}
|
||||||
<OrgPermissionCan
|
>
|
||||||
I={OrgPermissionActions.Edit}
|
<FontAwesomeIcon icon={faEllipsisV} />
|
||||||
a={OrgPermissionSubjects.Member}
|
</IconButton>
|
||||||
>
|
</DropdownMenuTrigger>
|
||||||
{(isAllowed) => (
|
<DropdownMenuContent sideOffset={2} align="end">
|
||||||
<DropdownMenuItem
|
<OrgPermissionCan
|
||||||
className={twMerge(
|
I={OrgPermissionActions.Edit}
|
||||||
!isAllowed &&
|
a={OrgPermissionSubjects.Member}
|
||||||
"pointer-events-none cursor-not-allowed opacity-50"
|
>
|
||||||
)}
|
{(isAllowed) => (
|
||||||
onClick={(e) => {
|
<DropdownMenuItem
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
navigate({
|
e.stopPropagation();
|
||||||
to: "/organization/members/$membershipId" as const,
|
navigate({
|
||||||
params: {
|
to: "/organization/members/$membershipId" as const,
|
||||||
membershipId: orgMembershipId
|
params: {
|
||||||
}
|
membershipId: orgMembershipId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||||
|
>
|
||||||
|
Edit User
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
a={OrgPermissionSubjects.Member}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
icon={<FontAwesomeIcon icon={faUserSlash} />}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (currentOrg?.scimEnabled) {
|
||||||
|
createNotification({
|
||||||
|
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
|
||||||
|
type: "error"
|
||||||
});
|
});
|
||||||
}}
|
return;
|
||||||
disabled={!isAllowed}
|
|
||||||
>
|
|
||||||
Edit User
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Delete}
|
|
||||||
a={OrgPermissionSubjects.Member}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className={
|
|
||||||
isActive
|
|
||||||
? twMerge(
|
|
||||||
isAllowed
|
|
||||||
? "hover:!bg-red-500 hover:!text-white"
|
|
||||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)
|
|
||||||
: ""
|
|
||||||
}
|
}
|
||||||
onClick={async (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (currentOrg?.scimEnabled) {
|
if (!isActive) {
|
||||||
createNotification({
|
// activate user
|
||||||
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
|
await updateOrgMembership({
|
||||||
type: "error"
|
organizationId: orgId,
|
||||||
});
|
membershipId: orgMembershipId,
|
||||||
return;
|
isActive: true
|
||||||
}
|
|
||||||
|
|
||||||
if (!isActive) {
|
|
||||||
// activate user
|
|
||||||
await updateOrgMembership({
|
|
||||||
organizationId: orgId,
|
|
||||||
membershipId: orgMembershipId,
|
|
||||||
isActive: true
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// deactivate user
|
|
||||||
handlePopUpOpen("deactivateMember", {
|
|
||||||
orgMembershipId,
|
|
||||||
username
|
|
||||||
});
|
});
|
||||||
}}
|
|
||||||
disabled={!isAllowed}
|
|
||||||
>
|
|
||||||
{`${isActive ? "Deactivate" : "Activate"} User`}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Delete}
|
|
||||||
a={OrgPermissionSubjects.Member}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className={twMerge(
|
|
||||||
isAllowed
|
|
||||||
? "hover:!bg-red-500 hover:!text-white"
|
|
||||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (currentOrg?.scimEnabled && isActive) {
|
return;
|
||||||
createNotification({
|
}
|
||||||
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("removeMember", {
|
// deactivate user
|
||||||
orgMembershipId,
|
handlePopUpOpen("deactivateMember", {
|
||||||
username
|
orgMembershipId,
|
||||||
|
username
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
{`${isActive ? "Deactivate" : "Activate"} User`}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
a={OrgPermissionSubjects.Member}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (currentOrg?.scimEnabled && isActive) {
|
||||||
|
createNotification({
|
||||||
|
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
|
||||||
|
type: "error"
|
||||||
});
|
});
|
||||||
}}
|
return;
|
||||||
disabled={!isAllowed}
|
}
|
||||||
>
|
|
||||||
Remove User
|
handlePopUpOpen("removeMember", {
|
||||||
</DropdownMenuItem>
|
orgMembershipId,
|
||||||
)}
|
username
|
||||||
</OrgPermissionCan>
|
});
|
||||||
</DropdownMenuContent>
|
}}
|
||||||
</DropdownMenu>
|
isDisabled={!isAllowed}
|
||||||
)}
|
icon={<FontAwesomeIcon icon={faUserXmark} />}
|
||||||
|
>
|
||||||
|
Remove User
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
|
|||||||
+3
-3
@@ -6,10 +6,10 @@ export const OrgRoleTabSection = () => {
|
|||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="role-list"
|
key="role-list"
|
||||||
transition={{ duration: 0.1 }}
|
transition={{ duration: 0.15 }}
|
||||||
initial={{ opacity: 0, translateX: -30 }}
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
animate={{ opacity: 1, translateX: 0 }}
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
exit={{ opacity: 0, translateX: -30 }}
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
>
|
>
|
||||||
<OrgRoleTable />
|
<OrgRoleTable />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
+200
-32
@@ -1,4 +1,17 @@
|
|||||||
import { faEllipsis, faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { useMemo } from "react";
|
||||||
|
import {
|
||||||
|
faArrowDown,
|
||||||
|
faArrowUp,
|
||||||
|
faCopy,
|
||||||
|
faEdit,
|
||||||
|
faEllipsisV,
|
||||||
|
faEye,
|
||||||
|
faIdBadge,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faPlus,
|
||||||
|
faSearch,
|
||||||
|
faTrash
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
@@ -14,6 +27,10 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Pagination,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -30,13 +47,25 @@ import {
|
|||||||
useOrganization,
|
useOrganization,
|
||||||
useSubscription
|
useSubscription
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { isCustomOrgRole } from "@app/helpers/roles";
|
import { isCustomOrgRole, isCustomProjectRole } from "@app/helpers/roles";
|
||||||
import { usePopUp } from "@app/hooks";
|
import {
|
||||||
|
getUserTablePreference,
|
||||||
|
PreferenceKey,
|
||||||
|
setUserTablePreference
|
||||||
|
} from "@app/helpers/userTablePreferences";
|
||||||
|
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||||
import { useDeleteOrgRole, useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
|
import { useDeleteOrgRole, useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { TOrgRole } from "@app/hooks/api/roles/types";
|
import { TOrgRole } from "@app/hooks/api/roles/types";
|
||||||
import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal";
|
import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal";
|
||||||
import { RoleModal } from "@app/pages/organization/RoleByIDPage/components/RoleModal";
|
import { RoleModal } from "@app/pages/organization/RoleByIDPage/components/RoleModal";
|
||||||
|
|
||||||
|
enum RolesOrderBy {
|
||||||
|
Name = "name",
|
||||||
|
Slug = "slug",
|
||||||
|
Type = "type"
|
||||||
|
}
|
||||||
|
|
||||||
export const OrgRoleTable = () => {
|
export const OrgRoleTable = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
@@ -93,14 +122,89 @@ export const OrgRoleTable = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
orderDirection,
|
||||||
|
toggleOrderDirection,
|
||||||
|
orderBy,
|
||||||
|
setOrderDirection,
|
||||||
|
setOrderBy,
|
||||||
|
search,
|
||||||
|
setSearch,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
setPerPage,
|
||||||
|
setPage,
|
||||||
|
offset
|
||||||
|
} = usePagination<RolesOrderBy>(RolesOrderBy.Type, {
|
||||||
|
initPerPage: getUserTablePreference("orgRolesTable", PreferenceKey.PerPage, 20)
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
|
setPerPage(newPerPage);
|
||||||
|
setUserTablePreference("orgRolesTable", PreferenceKey.PerPage, newPerPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredRoles = useMemo(
|
||||||
|
() =>
|
||||||
|
roles
|
||||||
|
?.filter((role) => {
|
||||||
|
const { slug, name } = role;
|
||||||
|
|
||||||
|
const searchValue = search.trim().toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
name.toLowerCase().includes(searchValue) || slug.toLowerCase().includes(searchValue)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const [roleOne, roleTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||||
|
|
||||||
|
switch (orderBy) {
|
||||||
|
case RolesOrderBy.Slug:
|
||||||
|
return roleOne.slug.toLowerCase().localeCompare(roleTwo.slug.toLowerCase());
|
||||||
|
case RolesOrderBy.Type: {
|
||||||
|
const roleOneValue = isCustomOrgRole(roleOne.slug) ? -1 : 1;
|
||||||
|
const roleTwoValue = isCustomOrgRole(roleTwo.slug) ? -1 : 1;
|
||||||
|
|
||||||
|
return roleTwoValue - roleOneValue;
|
||||||
|
}
|
||||||
|
case RolesOrderBy.Name:
|
||||||
|
default:
|
||||||
|
return roleOne.name.toLowerCase().localeCompare(roleTwo.name.toLowerCase());
|
||||||
|
}
|
||||||
|
}) ?? [],
|
||||||
|
[roles, orderDirection, search, orderBy]
|
||||||
|
);
|
||||||
|
|
||||||
|
useResetPageHelper({
|
||||||
|
totalCount: filteredRoles.length,
|
||||||
|
offset,
|
||||||
|
setPage
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSort = (column: RolesOrderBy) => {
|
||||||
|
if (column === orderBy) {
|
||||||
|
toggleOrderDirection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrderBy(column);
|
||||||
|
setOrderDirection(OrderByDirection.ASC);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClassName = (col: RolesOrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||||
|
|
||||||
|
const getColSortIcon = (col: RolesOrderBy) =>
|
||||||
|
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Organization Roles</p>
|
<p className="text-xl font-semibold text-mineshaft-100">Organization Roles</p>
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
|
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="primary"
|
colorSchema="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -113,18 +217,63 @@ export const OrgRoleTable = () => {
|
|||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
|
<Input
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search roles..."
|
||||||
|
className="flex-1"
|
||||||
|
containerClassName="mb-4"
|
||||||
|
/>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Name</Th>
|
<Th>
|
||||||
<Th>Slug</Th>
|
<div className="flex items-center">
|
||||||
|
Name
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={getClassName(RolesOrderBy.Name)}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(RolesOrderBy.Name)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Name)} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Slug
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={getClassName(RolesOrderBy.Slug)}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(RolesOrderBy.Slug)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Slug)} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Type
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={getClassName(RolesOrderBy.Type)}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(RolesOrderBy.Type)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Type)} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
<Th aria-label="actions" className="w-5" />
|
<Th aria-label="actions" className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isRolesLoading && <TableSkeleton columns={3} innerKey="org-roles" />}
|
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
|
||||||
{roles?.map((role) => {
|
{filteredRoles?.slice(offset, perPage * page).map((role) => {
|
||||||
const { id, name, slug } = role;
|
const { id, name, slug } = role;
|
||||||
const isNonMutatable = ["owner", "admin", "member", "no-access"].includes(slug);
|
const isNonMutatable = ["owner", "admin", "member", "no-access"].includes(slug);
|
||||||
const isDefaultOrgRole = isCustomOrgRole(slug)
|
const isDefaultOrgRole = isCustomOrgRole(slug)
|
||||||
@@ -162,23 +311,30 @@ export const OrgRoleTable = () => {
|
|||||||
<Td className="max-w-md overflow-hidden text-ellipsis whitespace-nowrap">
|
<Td className="max-w-md overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
{slug}
|
{slug}
|
||||||
</Td>
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Badge className="w-min whitespace-nowrap bg-mineshaft-400/50 text-bunker-200">
|
||||||
|
{isCustomProjectRole(slug) ? "Custom" : "Default"}
|
||||||
|
</Badge>
|
||||||
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<DropdownMenuTrigger asChild>
|
||||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
<IconButton
|
||||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
ariaLabel="Options"
|
||||||
</div>
|
colorSchema="secondary"
|
||||||
|
className="w-6"
|
||||||
|
variant="plain"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEllipsisV} />
|
||||||
|
</IconButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
<DropdownMenuContent className="min-w-[12rem]" sideOffset={2} align="end">
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Edit}
|
I={OrgPermissionActions.Edit}
|
||||||
a={OrgPermissionSubjects.Role}
|
a={OrgPermissionSubjects.Role}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={twMerge(
|
|
||||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
navigate({
|
navigate({
|
||||||
@@ -188,7 +344,8 @@ export const OrgRoleTable = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
disabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={isNonMutatable ? faEye : faEdit} />}
|
||||||
>
|
>
|
||||||
{`${isNonMutatable ? "View" : "Edit"} Role`}
|
{`${isNonMutatable ? "View" : "Edit"} Role`}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -200,14 +357,12 @@ export const OrgRoleTable = () => {
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={twMerge(
|
|
||||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handlePopUpOpen("duplicateRole", role);
|
handlePopUpOpen("duplicateRole", role);
|
||||||
}}
|
}}
|
||||||
disabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||||
>
|
>
|
||||||
Duplicate Role
|
Duplicate Role
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -220,14 +375,12 @@ export const OrgRoleTable = () => {
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={twMerge(
|
isDisabled={!isAllowed}
|
||||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
disabled={!isAllowed}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleSetRoleAsDefault(slug);
|
handleSetRoleAsDefault(slug);
|
||||||
}}
|
}}
|
||||||
|
icon={<FontAwesomeIcon icon={faIdBadge} />}
|
||||||
>
|
>
|
||||||
Set as Default Role
|
Set as Default Role
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -250,16 +403,12 @@ export const OrgRoleTable = () => {
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={twMerge(
|
|
||||||
isAllowed && !isDefaultOrgRole
|
|
||||||
? "hover:!bg-red-500 hover:!text-white"
|
|
||||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handlePopUpOpen("deleteRole", role);
|
handlePopUpOpen("deleteRole", role);
|
||||||
}}
|
}}
|
||||||
disabled={!isAllowed || isDefaultOrgRole}
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
isDisabled={!isAllowed || isDefaultOrgRole}
|
||||||
>
|
>
|
||||||
Delete Role
|
Delete Role
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -276,6 +425,25 @@ export const OrgRoleTable = () => {
|
|||||||
})}
|
})}
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
{Boolean(filteredRoles?.length) && (
|
||||||
|
<Pagination
|
||||||
|
count={filteredRoles!.length}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={setPage}
|
||||||
|
onChangePerPage={handlePerPageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!filteredRoles?.length && !isRolesLoading && (
|
||||||
|
<EmptyState
|
||||||
|
title={
|
||||||
|
roles?.length
|
||||||
|
? "No roles match search..."
|
||||||
|
: "This organization does not have any roles"
|
||||||
|
}
|
||||||
|
icon={roles?.length ? faSearch : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
|
|||||||
+4
-3
@@ -197,7 +197,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
variant="plain"
|
variant="plain"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
"flex h-[2.375rem] w-[2.6rem] items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||||
isTableFiltered && "border-primary/50 text-primary"
|
isTableFiltered && "border-primary/50 text-primary"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -298,7 +298,8 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
{!isMembersLoading &&
|
{!isMembersLoading &&
|
||||||
filteredUsers.slice(offset, perPage * page).map((projectMember) => {
|
filteredUsers.slice(offset, perPage * page).map((projectMember) => {
|
||||||
const { user: u, inviteEmail, id: membershipId, roles } = projectMember;
|
const { user: u, inviteEmail, id: membershipId, roles } = projectMember;
|
||||||
const name = u.firstName || u.lastName ? `${u.firstName} ${u.lastName || ""}` : "-";
|
const name =
|
||||||
|
u.firstName || u.lastName ? `${u.firstName} ${u.lastName || ""}` : null;
|
||||||
const email = u?.email || inviteEmail;
|
const email = u?.email || inviteEmail;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -328,7 +329,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Td>{name}</Td>
|
<Td>{name ?? <span className="text-mineshaft-400">Not Set</span>}</Td>
|
||||||
<Td>{email}</Td>
|
<Td>{email}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|||||||
+1
-1
@@ -236,7 +236,7 @@ export const ProjectRoleList = () => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isRolesLoading && <TableSkeleton columns={3} innerKey="project-roles" />}
|
{isRolesLoading && <TableSkeleton columns={4} innerKey="project-roles" />}
|
||||||
{filteredRoles?.slice(offset, perPage * page).map((role) => {
|
{filteredRoles?.slice(offset, perPage * page).map((role) => {
|
||||||
const { id, name, slug } = role;
|
const { id, name, slug } = role;
|
||||||
const isNonMutatable = Object.values(ProjectMembershipRole).includes(
|
const isNonMutatable = Object.values(ProjectMembershipRole).includes(
|
||||||
|
|||||||
Reference in New Issue
Block a user