diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts
index 076b33e54..b63550d2c 100644
--- a/backend/src/server/routes/v1/admin-router.ts
+++ b/backend/src/server/routes/v1/admin-router.ts
@@ -118,7 +118,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
querystring: z.object({
searchTerm: z.string().default(""),
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: {
200: z.object({
diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts
index 166f73317..8efdfd7f9 100644
--- a/backend/src/services/super-admin/super-admin-service.ts
+++ b/backend/src/services/super-admin/super-admin-service.ts
@@ -271,12 +271,13 @@ export const superAdminServiceFactory = ({
return { token, user: userInfo, organization };
};
- const getUsers = ({ offset, limit, searchTerm }: TAdminGetUsersDTO) => {
+ const getUsers = ({ offset, limit, searchTerm, adminsOnly }: TAdminGetUsersDTO) => {
return userDAL.getUsersByFilter({
limit,
offset,
searchTerm,
- sortBy: "username"
+ sortBy: "username",
+ adminsOnly
});
};
diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts
index 2d10941b4..d6de67e59 100644
--- a/backend/src/services/super-admin/super-admin-types.ts
+++ b/backend/src/services/super-admin/super-admin-types.ts
@@ -20,6 +20,7 @@ export type TAdminGetUsersDTO = {
offset: number;
limit: number;
searchTerm: string;
+ adminsOnly: boolean;
};
export enum LoginMethod {
diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts
index 99f403e84..eba497f0f 100644
--- a/backend/src/services/user/user-dal.ts
+++ b/backend/src/services/user/user-dal.ts
@@ -23,15 +23,18 @@ export const userDALFactory = (db: TDbClient) => {
limit,
offset,
searchTerm,
- sortBy
+ sortBy,
+ adminsOnly
}: {
limit: number;
offset: number;
searchTerm: string;
sortBy?: keyof TUsers;
+ adminsOnly: boolean;
}) => {
try {
let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false);
+
if (searchTerm) {
query = query.where((qb) => {
void qb
@@ -42,6 +45,10 @@ export const userDALFactory = (db: TDbClient) => {
});
}
+ if (adminsOnly) {
+ query = query.where("superAdmin", true);
+ }
+
if (sortBy) {
query = query.orderBy(sortBy);
}
diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts
index e5d281fc4..80e3edc92 100644
--- a/frontend/src/hooks/api/admin/types.ts
+++ b/frontend/src/hooks/api/admin/types.ts
@@ -50,6 +50,7 @@ export type TUpdateAdminSlackConfigDTO = {
export type AdminGetUsersFilters = {
limit: number;
searchTerm: string;
+ adminsOnly: boolean;
};
export type AdminSlackConfig = {
diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
index 4a255acad..3cc104f48 100644
--- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
+++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx
@@ -30,10 +30,13 @@ import {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
+ Modal,
+ ModalContent,
Tooltip
} from "@app/components/v2";
import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context";
+import { isInfisicalCloud } from "@app/helpers/platform";
import { usePopUp, useToggle } from "@app/hooks";
import {
useGetOrganizations,
@@ -50,6 +53,7 @@ import { ProjectType } from "@app/hooks/api/workspace/types";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { MenuIconButton } from "../MenuIconButton";
+import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
const getPlan = (subscription: SubscriptionPlan) => {
if (subscription.dynamicSecret) return "Enterprise Plan";
@@ -77,6 +81,11 @@ export const INFISICAL_SUPPORT_OPTIONS = [
,
"Email Support",
"mailto:support@infisical.com"
+ ],
+ [
+ ,
+ "Instance Admins",
+ "server-admins"
]
];
@@ -89,6 +98,7 @@ export const MinimizedOrgSidebar = () => {
const [openSupport, setOpenSupport] = useState(false);
const [openUser, setOpenUser] = useState(false);
const [openOrg, setOpenOrg] = useState(false);
+ const [showAdminsModal, setShowAdminsModal] = useState(false);
const { user } = useUser();
const { mutateAsync } = useGetOrgTrialUrl();
@@ -410,21 +420,39 @@ export const MinimizedOrgSidebar = () => {
side="right"
className="p-1"
>
- {INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
-
-
-
-
-
- ))}
+ {INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => {
+ if (url === "server-admins" && isInfisicalCloud()) {
+ return null;
+ }
+ return (
+
+ {url === "server-admins" ? (
+
+ ) : (
+
+
+
+ )}
+
+ );
+ })}
{envConfig.PLATFORM_VERSION && (
@@ -540,6 +568,13 @@ export const MinimizedOrgSidebar = () => {
+
+
+
+
+
+
+
handlePopUpToggle("createOrg", false)}
diff --git a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx
new file mode 100644
index 000000000..c834daad8
--- /dev/null
+++ b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx
@@ -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 (
+
+
+ setSearchUserFilter(e.target.value)}
+ leftIcon={}
+ placeholder="Search server admins..."
+ className="w-full"
+ />
+
+
+
+
+
+
+ | Name |
+ Email |
+
+
+
+ {isPending && }
+ {!isPending &&
+ adminUsers?.map(({ user }) => {
+ const name =
+ user.firstName || user.lastName
+ ? `${user.firstName} ${user.lastName}`
+ : user.username;
+ return (
+
+ | {name} |
+ {user.email} |
+
+ );
+ })}
+
+
+ {isEmpty && (
+
+ No server administrators found
+
+ )}
+
+
+
+ );
+};
diff --git a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx
index df51216a6..6654439de 100644
--- a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx
+++ b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx
@@ -1,6 +1,14 @@
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 { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
@@ -8,7 +16,13 @@ import {
Badge,
Button,
DeleteActionModal,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuTrigger,
EmptyState,
+ IconButton,
Input,
Table,
TableContainer,
@@ -17,11 +31,7 @@ import {
Td,
Th,
THead,
- Tr,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger
+ Tr
} from "@app/components/v2";
import { useSubscription, useUser } from "@app/context";
import { useDebounce, usePopUp } from "@app/hooks";
@@ -48,6 +58,7 @@ const UserPanelTable = ({
) => void;
}) => {
const [searchUserFilter, setSearchUserFilter] = useState("");
+ const [adminsOnly, setAdminsOnly] = useState(false);
const { user } = useUser();
const userId = user?.id || "";
const [debounedSearchTerm] = useDebounce(searchUserFilter, 500);
@@ -55,18 +66,55 @@ const UserPanelTable = ({
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({
limit: 20,
- searchTerm: debounedSearchTerm
+ searchTerm: debounedSearchTerm,
+ adminsOnly
});
const isEmpty = !isPending && !data?.pages?.[0].length;
+ const isTableFiltered = Boolean(adminsOnly);
+
return (
<>
- setSearchUserFilter(e.target.value)}
- leftIcon={}
- placeholder="Search users..."
- />
+
+
setSearchUserFilter(e.target.value)}
+ leftIcon={
}
+ placeholder="Search users..."
+ className="flex-1"
+ />
+
+
+
+
+
+
+
+ Filter By
+ {
+ e.preventDefault();
+ setAdminsOnly(!adminsOnly);
+ }}
+ icon={adminsOnly && }
+ iconPos="right"
+ >
+
+
+ Server Admins
+
+
+
+
+