mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 16:28:11 +00:00
Merge pull request #3201 from Infisical/feat/addMoreVisibilityToServerAdmins
Add is-admin filter to Server Admin Console and add a component to sh…
This commit is contained in:
@@ -118,7 +118,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
searchTerm: z.string().default(""),
|
searchTerm: z.string().default(""),
|
||||||
offset: z.coerce.number().default(0),
|
offset: z.coerce.number().default(0),
|
||||||
limit: z.coerce.number().max(100).default(20)
|
limit: z.coerce.number().max(100).default(20),
|
||||||
|
// TODO: remove this once z.coerce.boolean() is supported
|
||||||
|
adminsOnly: z
|
||||||
|
.string()
|
||||||
|
.transform((val) => val === "true")
|
||||||
|
.default("false")
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -271,12 +271,13 @@ export const superAdminServiceFactory = ({
|
|||||||
return { token, user: userInfo, organization };
|
return { token, user: userInfo, organization };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUsers = ({ offset, limit, searchTerm }: TAdminGetUsersDTO) => {
|
const getUsers = ({ offset, limit, searchTerm, adminsOnly }: TAdminGetUsersDTO) => {
|
||||||
return userDAL.getUsersByFilter({
|
return userDAL.getUsersByFilter({
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
sortBy: "username"
|
sortBy: "username",
|
||||||
|
adminsOnly
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export type TAdminGetUsersDTO = {
|
|||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
|
adminsOnly: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum LoginMethod {
|
export enum LoginMethod {
|
||||||
|
|||||||
@@ -23,15 +23,18 @@ export const userDALFactory = (db: TDbClient) => {
|
|||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
sortBy
|
sortBy,
|
||||||
|
adminsOnly
|
||||||
}: {
|
}: {
|
||||||
limit: number;
|
limit: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
sortBy?: keyof TUsers;
|
sortBy?: keyof TUsers;
|
||||||
|
adminsOnly: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false);
|
let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false);
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
query = query.where((qb) => {
|
query = query.where((qb) => {
|
||||||
void qb
|
void qb
|
||||||
@@ -42,6 +45,10 @@ export const userDALFactory = (db: TDbClient) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (adminsOnly) {
|
||||||
|
query = query.where("superAdmin", true);
|
||||||
|
}
|
||||||
|
|
||||||
if (sortBy) {
|
if (sortBy) {
|
||||||
query = query.orderBy(sortBy);
|
query = query.orderBy(sortBy);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export type TUpdateAdminSlackConfigDTO = {
|
|||||||
export type AdminGetUsersFilters = {
|
export type AdminGetUsersFilters = {
|
||||||
limit: number;
|
limit: number;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
|
adminsOnly: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminSlackConfig = {
|
export type AdminSlackConfig = {
|
||||||
|
|||||||
+50
-15
@@ -30,10 +30,13 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { envConfig } from "@app/config/env";
|
import { envConfig } from "@app/config/env";
|
||||||
import { useOrganization, useSubscription, useUser } from "@app/context";
|
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||||
|
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useGetOrganizations,
|
useGetOrganizations,
|
||||||
@@ -50,6 +53,7 @@ import { ProjectType } from "@app/hooks/api/workspace/types";
|
|||||||
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
|
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
|
||||||
|
|
||||||
import { MenuIconButton } from "../MenuIconButton";
|
import { MenuIconButton } from "../MenuIconButton";
|
||||||
|
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
|
||||||
|
|
||||||
const getPlan = (subscription: SubscriptionPlan) => {
|
const getPlan = (subscription: SubscriptionPlan) => {
|
||||||
if (subscription.dynamicSecret) return "Enterprise Plan";
|
if (subscription.dynamicSecret) return "Enterprise Plan";
|
||||||
@@ -77,6 +81,11 @@ export const INFISICAL_SUPPORT_OPTIONS = [
|
|||||||
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
|
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
|
||||||
"Email Support",
|
"Email Support",
|
||||||
"mailto:[email protected]"
|
"mailto:[email protected]"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
<FontAwesomeIcon key={5} className="pr-4 text-sm" icon={faUsers} />,
|
||||||
|
"Instance Admins",
|
||||||
|
"server-admins"
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -89,6 +98,7 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
const [openSupport, setOpenSupport] = useState(false);
|
const [openSupport, setOpenSupport] = useState(false);
|
||||||
const [openUser, setOpenUser] = useState(false);
|
const [openUser, setOpenUser] = useState(false);
|
||||||
const [openOrg, setOpenOrg] = useState(false);
|
const [openOrg, setOpenOrg] = useState(false);
|
||||||
|
const [showAdminsModal, setShowAdminsModal] = useState(false);
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { mutateAsync } = useGetOrgTrialUrl();
|
const { mutateAsync } = useGetOrgTrialUrl();
|
||||||
@@ -410,21 +420,39 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
side="right"
|
side="right"
|
||||||
className="p-1"
|
className="p-1"
|
||||||
>
|
>
|
||||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
|
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => {
|
||||||
<DropdownMenuItem key={url as string}>
|
if (url === "server-admins" && isInfisicalCloud()) {
|
||||||
<a
|
return null;
|
||||||
target="_blank"
|
}
|
||||||
rel="noopener noreferrer"
|
return (
|
||||||
href={String(url)}
|
<DropdownMenuItem key={url as string}>
|
||||||
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
|
{url === "server-admins" ? (
|
||||||
>
|
<button
|
||||||
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
|
type="button"
|
||||||
{icon}
|
onClick={() => setShowAdminsModal(true)}
|
||||||
<div className="text-sm">{text}</div>
|
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
|
||||||
</div>
|
>
|
||||||
</a>
|
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
|
||||||
</DropdownMenuItem>
|
{icon}
|
||||||
))}
|
<div className="text-sm">{text}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={String(url)}
|
||||||
|
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
|
||||||
|
>
|
||||||
|
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
|
||||||
|
{icon}
|
||||||
|
<div className="text-sm">{text}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
{envConfig.PLATFORM_VERSION && (
|
{envConfig.PLATFORM_VERSION && (
|
||||||
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
|
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
|
||||||
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
|
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
|
||||||
@@ -540,6 +568,13 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
<Modal isOpen={showAdminsModal} onOpenChange={setShowAdminsModal}>
|
||||||
|
<ModalContent title="Server Administrators" subTitle="View all server administrators">
|
||||||
|
<div className="mb-2">
|
||||||
|
<ServerAdminsPanel />
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
<CreateOrgModal
|
<CreateOrgModal
|
||||||
isOpen={popUp?.createOrg?.isOpen}
|
isOpen={popUp?.createOrg?.isOpen}
|
||||||
onClose={() => handlePopUpToggle("createOrg", false)}
|
onClose={() => handlePopUpToggle("createOrg", false)}
|
||||||
|
|||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import { useDebounce } from "@app/hooks";
|
||||||
|
import { useGetOrgUsers } from "@app/hooks/api";
|
||||||
|
|
||||||
|
export const ServerAdminsPanel = () => {
|
||||||
|
const [searchUserFilter, setSearchUserFilter] = useState("");
|
||||||
|
const [debounedSearchTerm] = useDebounce(searchUserFilter, 500);
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
|
const { data: orgUsers, isPending } = useGetOrgUsers(currentOrg?.id || "");
|
||||||
|
|
||||||
|
const adminUsers = orgUsers?.filter((orgUser) => {
|
||||||
|
const isSuperAdmin = orgUser.user.superAdmin;
|
||||||
|
const matchesSearch = debounedSearchTerm
|
||||||
|
? orgUser.user.email?.toLowerCase().includes(debounedSearchTerm.toLowerCase()) ||
|
||||||
|
orgUser.user.firstName?.toLowerCase().includes(debounedSearchTerm.toLowerCase()) ||
|
||||||
|
orgUser.user.lastName?.toLowerCase().includes(debounedSearchTerm.toLowerCase())
|
||||||
|
: true;
|
||||||
|
return isSuperAdmin && matchesSearch;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isEmpty = !isPending && (!adminUsers || adminUsers.length === 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
<div className="mb-4 px-4">
|
||||||
|
<Input
|
||||||
|
value={searchUserFilter}
|
||||||
|
onChange={(e) => setSearchUserFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search server admins..."
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 px-2">
|
||||||
|
<TableContainer className="flex max-h-[30vh] flex-col overflow-auto">
|
||||||
|
<Table className="w-full table-fixed">
|
||||||
|
<THead className="sticky top-0 bg-bunker-800">
|
||||||
|
<Tr>
|
||||||
|
<Th className="w-1/2">Name</Th>
|
||||||
|
<Th className="w-1/2">Email</Th>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isPending && <TableSkeleton columns={2} innerKey="admins" />}
|
||||||
|
{!isPending &&
|
||||||
|
adminUsers?.map(({ user }) => {
|
||||||
|
const name =
|
||||||
|
user.firstName || user.lastName
|
||||||
|
? `${user.firstName} ${user.lastName}`
|
||||||
|
: user.username;
|
||||||
|
return (
|
||||||
|
<Tr key={`admin-${user.id}`}>
|
||||||
|
<Td className="w-1/2 break-words">{name}</Td>
|
||||||
|
<Td className="w-1/2 break-words">{user.email}</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{isEmpty && (
|
||||||
|
<div className="flex h-32 items-center justify-center text-sm text-mineshaft-400">
|
||||||
|
No server administrators found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { faMagnifyingGlass, faUsers, faEllipsis } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faCheckCircle,
|
||||||
|
faEllipsis,
|
||||||
|
faFilter,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faUsers,
|
||||||
|
faUserShield
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
@@ -8,7 +16,13 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
@@ -17,11 +31,7 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr,
|
Tr
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger
|
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useSubscription, useUser } from "@app/context";
|
import { useSubscription, useUser } from "@app/context";
|
||||||
import { useDebounce, usePopUp } from "@app/hooks";
|
import { useDebounce, usePopUp } from "@app/hooks";
|
||||||
@@ -48,6 +58,7 @@ const UserPanelTable = ({
|
|||||||
) => void;
|
) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [searchUserFilter, setSearchUserFilter] = useState("");
|
const [searchUserFilter, setSearchUserFilter] = useState("");
|
||||||
|
const [adminsOnly, setAdminsOnly] = useState(false);
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const userId = user?.id || "";
|
const userId = user?.id || "";
|
||||||
const [debounedSearchTerm] = useDebounce(searchUserFilter, 500);
|
const [debounedSearchTerm] = useDebounce(searchUserFilter, 500);
|
||||||
@@ -55,18 +66,55 @@ const UserPanelTable = ({
|
|||||||
|
|
||||||
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({
|
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({
|
||||||
limit: 20,
|
limit: 20,
|
||||||
searchTerm: debounedSearchTerm
|
searchTerm: debounedSearchTerm,
|
||||||
|
adminsOnly
|
||||||
});
|
});
|
||||||
|
|
||||||
const isEmpty = !isPending && !data?.pages?.[0].length;
|
const isEmpty = !isPending && !data?.pages?.[0].length;
|
||||||
|
const isTableFiltered = Boolean(adminsOnly);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Input
|
<div className="flex gap-2">
|
||||||
value={searchUserFilter}
|
<Input
|
||||||
onChange={(e) => setSearchUserFilter(e.target.value)}
|
value={searchUserFilter}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
onChange={(e) => setSearchUserFilter(e.target.value)}
|
||||||
placeholder="Search users..."
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
/>
|
placeholder="Search users..."
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="Filter Users"
|
||||||
|
variant="plain"
|
||||||
|
size="sm"
|
||||||
|
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",
|
||||||
|
isTableFiltered && "border-primary/50 text-primary"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faFilter} />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="p-0">
|
||||||
|
<DropdownMenuLabel>Filter By</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setAdminsOnly(!adminsOnly);
|
||||||
|
}}
|
||||||
|
icon={adminsOnly && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FontAwesomeIcon icon={faUserShield} className="text-yellow-700" />
|
||||||
|
<span>Server Admins</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
|
|||||||
Reference in New Issue
Block a user