From 214f837041057d33eef5f31a63b38e8ed3432d26 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Fri, 7 Mar 2025 11:42:15 -0300 Subject: [PATCH 1/4] Add is-admin filter to Server Admin Console and add a component to show the server admins on side panel --- backend/src/server/routes/v1/admin-router.ts | 7 +- .../super-admin/super-admin-service.ts | 5 +- .../services/super-admin/super-admin-types.ts | 1 + backend/src/services/user/user-dal.ts | 9 +- frontend/src/hooks/api/admin/types.ts | 1 + .../MinimizedOrgSidebar.tsx | 27 ++++++ .../ServerAdminsPanel/ServerAdminsPanel.tsx | 85 +++++++++++++++++++ .../OverviewPage/components/UserPanel.tsx | 79 ++++++++++++++--- 8 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx 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..c1c4297ca 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -50,6 +50,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"; @@ -89,6 +90,7 @@ export const MinimizedOrgSidebar = () => { const [openSupport, setOpenSupport] = useState(false); const [openUser, setOpenUser] = useState(false); const [openOrg, setOpenOrg] = useState(false); + const [openAdmins, setOpenAdmins] = useState(false); const { user } = useUser(); const { mutateAsync } = useGetOrgTrialUrl(); @@ -392,6 +394,31 @@ export const MinimizedOrgSidebar = () => { : "mb-4" } flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`} > + + setOpenAdmins(true)} + onMouseLeave={() => setOpenAdmins(false)} + asChild + > +
+ + Admins + +
+
+ setOpenAdmins(true)} + onMouseLeave={() => setOpenAdmins(false)} + align="start" + side="right" + className="mb-2 w-[60vh] p-1" + > + Server Administrators +
+ +
+
+
setOpenSupport(true)} 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..69eecec40 --- /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" + /> +
+
+ + + + + + + + + + {isPending && } + {!isPending && + adminUsers?.map(({ user }) => { + const name = + user.firstName || user.lastName + ? `${user.firstName} ${user.lastName}` + : user.username; + return ( + + + + + ); + })} + +
NameEmail
{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..9f6524cf7 100644 --- a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx @@ -1,6 +1,13 @@ 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 { twMerge } from "tailwind-merge"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; @@ -8,8 +15,15 @@ import { Badge, Button, DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, EmptyState, + IconButton, Input, + Switch, Table, TableContainer, TableSkeleton, @@ -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,60 @@ 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 Users + { + e.preventDefault(); + setAdminsOnly(!adminsOnly); + }} + icon={adminsOnly && } + iconPos="right" + > +
+ Admins Only + setAdminsOnly(checked)} + className="data-[state=checked]:bg-primary-400" + /> +
+
+
+
+
From 1dea0248809e158e051c8cdb0a0c223bba0641ae Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Fri, 7 Mar 2025 19:19:55 -0300 Subject: [PATCH 2/4] Improvement on admin visibility UI components --- .../MinimizedOrgSidebar.tsx | 80 ++++++++++--------- .../ServerAdminsPanel/ServerAdminsPanel.tsx | 18 ++--- .../OverviewPage/components/UserPanel.tsx | 21 ++--- 3 files changed, 60 insertions(+), 59 deletions(-) diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index c1c4297ca..95e254ccb 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -30,6 +30,8 @@ import { DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, + Modal, + ModalContent, Tooltip } from "@app/components/v2"; import { envConfig } from "@app/config/env"; @@ -78,6 +80,11 @@ export const INFISICAL_SUPPORT_OPTIONS = [ , "Email Support", "mailto:support@infisical.com" + ], + [ + , + "Instance Admins", + "server-admins" ] ]; @@ -90,7 +97,7 @@ export const MinimizedOrgSidebar = () => { const [openSupport, setOpenSupport] = useState(false); const [openUser, setOpenUser] = useState(false); const [openOrg, setOpenOrg] = useState(false); - const [openAdmins, setOpenAdmins] = useState(false); + const [showAdminsModal, setShowAdminsModal] = useState(false); const { user } = useUser(); const { mutateAsync } = useGetOrgTrialUrl(); @@ -394,31 +401,6 @@ export const MinimizedOrgSidebar = () => { : "mb-4" } flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`} > - - setOpenAdmins(true)} - onMouseLeave={() => setOpenAdmins(false)} - asChild - > -
- - Admins - -
-
- setOpenAdmins(true)} - onMouseLeave={() => setOpenAdmins(false)} - align="start" - side="right" - className="mb-2 w-[60vh] p-1" - > - Server Administrators -
- -
-
-
setOpenSupport(true)} @@ -439,17 +421,30 @@ export const MinimizedOrgSidebar = () => { > {INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => ( - -
- {icon} -
{text}
-
-
+ {url === "server-admins" ? ( + + ) : ( + +
+ {icon} +
{text}
+
+
+ )}
))} {envConfig.PLATFORM_VERSION && ( @@ -567,6 +562,17 @@ 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 index 69eecec40..fed2a5a8b 100644 --- a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx @@ -37,7 +37,7 @@ export const ServerAdminsPanel = () => { const isEmpty = !isPending && (!adminUsers || adminUsers.length === 0); return ( -
+
{ className="w-full" />
-
- -
- +
+ +
+ - + {isPending && } {!isPending && adminUsers?.map(({ user }) => { @@ -65,9 +65,9 @@ export const ServerAdminsPanel = () => { ? `${user.firstName} ${user.lastName}` : user.username; return ( - - - + + + ); })} diff --git a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx index 9f6524cf7..6654439de 100644 --- a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx @@ -4,7 +4,8 @@ import { faEllipsis, faFilter, faMagnifyingGlass, - faUsers + faUsers, + faUserShield } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; @@ -23,7 +24,6 @@ import { EmptyState, IconButton, Input, - Switch, Table, TableContainer, TableSkeleton, @@ -97,24 +97,19 @@ const UserPanelTable = ({ - - Filter Users + + Filter By { e.preventDefault(); setAdminsOnly(!adminsOnly); }} - icon={adminsOnly && } + icon={adminsOnly && } iconPos="right" > -
- Admins Only - setAdminsOnly(checked)} - className="data-[state=checked]:bg-primary-400" - /> +
+ + Server Admins
From 66817a40db59f55a2c33e4782e984b380c712e69 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Mon, 10 Mar 2025 08:31:19 -0300 Subject: [PATCH 3/4] Adjust modal width to match the rest of the modals --- .../MinimizedOrgSidebar/MinimizedOrgSidebar.tsx | 8 ++------ .../components/ServerAdminsPanel/ServerAdminsPanel.tsx | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index 95e254ccb..453a080ad 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -563,12 +563,8 @@ export const MinimizedOrgSidebar = () => { - -
+ +
diff --git a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx index fed2a5a8b..c834daad8 100644 --- a/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/ServerAdminsPanel/ServerAdminsPanel.tsx @@ -48,7 +48,7 @@ export const ServerAdminsPanel = () => { />
- +
Name Email
{name}{user.email}
{name}{user.email}
From a814f459aba5776279a5655f249c5474f042bc2e Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Mon, 10 Mar 2025 10:58:39 -0300 Subject: [PATCH 4/4] Add condition to hide Instance Admins on cloud instances --- .../MinimizedOrgSidebar.tsx | 62 ++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index 453a080ad..3cc104f48 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -36,6 +36,7 @@ import { } 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, @@ -419,34 +420,39 @@ export const MinimizedOrgSidebar = () => { side="right" className="p-1" > - {INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => ( - - {url === "server-admins" ? ( - - ) : ( - -
- {icon} -
{text}
-
-
- )} -
- ))} + {INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => { + if (url === "server-admins" && isInfisicalCloud()) { + return null; + } + return ( + + {url === "server-admins" ? ( + + ) : ( + +
+ {icon} +
{text}
+
+
+ )} +
+ ); + })} {envConfig.PLATFORM_VERSION && (