mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 23:27:35 +00:00
Add is-admin filter to Server Admin Console and add a component to show the server admins on side panel
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 = {
|
||||||
|
|||||||
+27
@@ -50,6 +50,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";
|
||||||
@@ -89,6 +90,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 [openAdmins, setOpenAdmins] = useState(false);
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { mutateAsync } = useGetOrgTrialUrl();
|
const { mutateAsync } = useGetOrgTrialUrl();
|
||||||
@@ -392,6 +394,31 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
: "mb-4"
|
: "mb-4"
|
||||||
} flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`}
|
} flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`}
|
||||||
>
|
>
|
||||||
|
<DropdownMenu open={openAdmins} onOpenChange={setOpenAdmins}>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
onMouseEnter={() => setOpenAdmins(true)}
|
||||||
|
onMouseLeave={() => setOpenAdmins(false)}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<div className="w-full">
|
||||||
|
<MenuIconButton lottieIconMode="reverse" icon="groups" isSelected={openAdmins}>
|
||||||
|
Admins
|
||||||
|
</MenuIconButton>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
onMouseEnter={() => setOpenAdmins(true)}
|
||||||
|
onMouseLeave={() => setOpenAdmins(false)}
|
||||||
|
align="start"
|
||||||
|
side="right"
|
||||||
|
className="mb-2 w-[60vh] p-1"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel>Server Administrators</DropdownMenuLabel>
|
||||||
|
<div className="h-[30vh]">
|
||||||
|
<ServerAdminsPanel />
|
||||||
|
</div>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
<DropdownMenu open={openSupport} onOpenChange={setOpenSupport}>
|
<DropdownMenu open={openSupport} onOpenChange={setOpenSupport}>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
onMouseEnter={() => setOpenSupport(true)}
|
onMouseEnter={() => setOpenSupport(true)}
|
||||||
|
|||||||
+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 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="mb-2 w-full px-2">
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th className="w-1/2">Name</Th>
|
||||||
|
<Th className="w-1/2">Email</Th>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody className="h-full">
|
||||||
|
{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}`} className="w-full">
|
||||||
|
<Td className="w-1/2 truncate">{name}</Td>
|
||||||
|
<Td className="w-1/2 truncate">{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,13 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { faMagnifyingGlass, faUsers, faEllipsis } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faCheckCircle,
|
||||||
|
faEllipsis,
|
||||||
|
faFilter,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faUsers
|
||||||
|
} 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,8 +15,15 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
|
Switch,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -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,60 @@ 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="w-56">
|
||||||
|
<DropdownMenuLabel>Filter Users</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setAdminsOnly(!adminsOnly);
|
||||||
|
}}
|
||||||
|
icon={adminsOnly && <FontAwesomeIcon className="text-primary" icon={faCheckCircle} />}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<span>Admins Only</span>
|
||||||
|
<Switch
|
||||||
|
id="admins-only"
|
||||||
|
isChecked={adminsOnly}
|
||||||
|
onCheckedChange={(checked) => setAdminsOnly(checked)}
|
||||||
|
className="data-[state=checked]:bg-primary-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
|
|||||||
Reference in New Issue
Block a user