Merge pull request #4333 from Infisical/ENG-3451

feat(org-admin): Remove organization admin console
This commit is contained in:
x032205
2025-08-08 13:45:52 -04:00
committed by GitHub
13 changed files with 59 additions and 346 deletions

View File

@@ -849,8 +849,6 @@ export const registerRoutes = async (
projectDAL,
permissionService,
projectUserMembershipRoleDAL,
projectBotDAL,
projectKeyDAL,
projectMembershipDAL
});

View File

@@ -6,8 +6,6 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
@@ -20,8 +18,6 @@ type TOrgAdminServiceFactoryDep = {
TProjectMembershipDALFactory,
"findOne" | "create" | "transaction" | "delete" | "findAllProjectMembers"
>;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
smtpService: Pick<TSmtpService, "sendMail">;
};
@@ -32,8 +28,6 @@ export const orgAdminServiceFactory = ({
permissionService,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
projectBotDAL,
projectUserMembershipRoleDAL,
smtpService
}: TOrgAdminServiceFactoryDep) => {
@@ -119,28 +113,6 @@ export const orgAdminServiceFactory = ({
return { isExistingMember: true, membership: projectMembership };
}
// missing membership thus add admin back as admin to project
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
if (!ghostUser) {
throw new NotFoundError({
message: `Project owner of project with ID '${projectId}' not found`
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
if (!ghostUserLatestKey) {
throw new NotFoundError({
message: `Project owner's latest key of project with ID '${projectId}' not found`
});
}
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) {
throw new NotFoundError({
message: `Project bot for project with ID '${projectId}' not found`
});
}
const updatedMembership = await projectMembershipDAL.transaction(async (tx) => {
const newProjectMembership = await projectMembershipDAL.create(
{

View File

@@ -1,2 +1 @@
export { useOrgAdminAccessProject } from "./mutation";
export { useOrgAdminGetProjects } from "./queries";

View File

@@ -1,30 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { Workspace } from "../types";
import { TOrgAdminGetProjectsDTO } from "./types";
export const orgAdminQueryKeys = {
getProjects: (filter: TOrgAdminGetProjectsDTO) => ["org-admin-projects", filter] as const
};
export const useOrgAdminGetProjects = ({ search, offset, limit = 50 }: TOrgAdminGetProjectsDTO) => {
return useQuery({
queryKey: orgAdminQueryKeys.getProjects({ search, offset, limit }),
queryFn: async () => {
const { data } = await apiRequest.get<{ projects: Workspace[]; count: number }>(
"/api/v1/organization-admin/projects",
{
params: {
limit,
offset,
search
}
}
);
return data;
}
});
};

View File

@@ -1,9 +1,3 @@
export type TOrgAdminGetProjectsDTO = {
limit?: number;
offset?: number;
search?: string;
};
export type TOrgAdminAccessProjectDTO = {
projectId: string;
};

View File

@@ -7,7 +7,6 @@ import {
faPlug,
faShare,
faTable,
faUserCog,
faUsers,
faUserTie
} from "@fortawesome/free-solid-svg-icons";
@@ -181,18 +180,6 @@ export const OrgSidebar = ({ isHidden }: Props) => {
Share Secret
</MenuItem>
</Link>
<Link to="/organization/admin">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faUserCog} />
</div>
}
>
Organization Admin
</MenuItem>
</Link>
{user.superAdmin && (
<Link to="/admin">
<MenuItem

View File

@@ -1,39 +0,0 @@
import { useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgAdminProjects } from "./components/OrgAdminProjects";
enum TabSections {
Projects = "projects"
}
export const AdminPage = () => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<TabSections>(TabSections.Projects);
return (
<>
<Helmet>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
title="Organization Admin Console"
description="View and manage resources across your organization."
/>
<Tabs value={activeTab} onValueChange={(el) => setActiveTab(el as TabSections)}>
<TabList>
<Tab value={TabSections.Projects}>Projects</Tab>
</TabList>
<TabPanel value={TabSections.Projects}>
<OrgAdminProjects />
</TabPanel>
</Tabs>
</div>
</div>
</>
);
};

View File

@@ -1,169 +0,0 @@
import { useState } from "react";
import { faEllipsis, faMagnifyingGlass, faSignIn } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { format } from "date-fns";
import { motion } from "framer-motion";
import { createNotification } from "@app/components/notifications";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Input,
Pagination,
Spinner,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import {
OrgPermissionAdminConsoleAction,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { getProjectHomePage } from "@app/helpers/project";
import { withPermission } from "@app/hoc";
import { useDebounce } from "@app/hooks";
import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const OrgAdminProjects = withPermission(
() => {
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebounce(search);
const [perPage, setPerPage] = useState(25);
const navigate = useNavigate();
const orgAdminAccessProject = useOrgAdminAccessProject();
const { data, isPending: isProjectsLoading } = useOrgAdminGetProjects({
offset: (page - 1) * perPage,
limit: perPage,
search: debouncedSearch || undefined
});
const projects = data?.projects || [];
const projectCount = data?.count || 0;
const isEmpty = !isProjectsLoading && projects.length === 0;
const handleAccessProject = async (type: ProjectType, projectId: string) => {
try {
await orgAdminAccessProject.mutateAsync({
projectId
});
await navigate({
to: getProjectHomePage(type),
params: {
projectId
}
});
} catch {
createNotification({
text: "Failed to access project",
type: "error"
});
}
};
return (
<motion.div
key="panel-projects"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Projects</p>
</div>
<div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search by project name"
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th>Created At</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isProjectsLoading && <TableSkeleton columns={4} innerKey="projects" />}
{!isProjectsLoading &&
projects?.map(({ name, slug, createdAt, id, type }) => (
<Tr key={`project-${id}`} className="group w-full">
<Td>{name}</Td>
<Td>{slug}</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd, hh:mm aaa")}</Td>
<Td>
<div>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<Button
variant="link"
className="text-bunker-300 hover:text-primary-400 data-[state=open]:text-primary-400"
>
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleAccessProject(type, id);
}}
icon={<FontAwesomeIcon icon={faSignIn} />}
disabled={
orgAdminAccessProject.variables?.projectId === id &&
orgAdminAccessProject.isPending
}
>
Access{" "}
{orgAdminAccessProject.variables?.projectId === id &&
orgAdminAccessProject.isPending && <Spinner size="xs" />}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Td>
</Tr>
))}
</TBody>
</Table>
{!isProjectsLoading && (
<Pagination
count={projectCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{isEmpty && <EmptyState title="No projects found" />}
</TableContainer>
</div>
</div>
</motion.div>
);
},
{
action: OrgPermissionAdminConsoleAction.AccessAllProjects,
subject: OrgPermissionSubjects.AdminConsole
}
);

View File

@@ -1 +0,0 @@
export { OrgAdminProjects } from "./OrgAdminProjects";

View File

@@ -1,16 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { AdminPage } from "./AdminPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/admin"
)({
component: AdminPage,
context: () => ({
breadcrumbs: [
{
label: "Admin Console"
}
]
})
});

View File

@@ -35,6 +35,7 @@ import {
Tooltip
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { OrgPermissionAdminConsoleAction } from "@app/context/OrgPermissionContext/types";
import { getProjectHomePage, getProjectLottieIcon, getProjectTitle } from "@app/helpers/project";
import {
getUserTablePreference,
@@ -42,7 +43,11 @@ import {
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useRequestProjectAccess, useSearchProjects } from "@app/hooks/api";
import {
useOrgAdminAccessProject,
useRequestProjectAccess,
useSearchProjects
} from "@app/hooks/api";
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
type Props = {
@@ -120,6 +125,8 @@ export const AllProjectView = ({
initPerPage: getUserTablePreference("allProjectsTable", PreferenceKey.PerPage, 50)
});
const orgAdminAccessProject = useOrgAdminAccessProject();
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
setUserTablePreference("allProjectsTable", PreferenceKey.PerPage, newPerPage);
@@ -137,6 +144,25 @@ export const AllProjectView = ({
type: projectTypeFilter
});
const handleAccessProject = async (type: ProjectType, projectId: string) => {
try {
await orgAdminAccessProject.mutateAsync({
projectId
});
await navigate({
to: getProjectHomePage(type),
params: {
projectId
}
});
} catch {
createNotification({
text: "Failed to access project",
type: "error"
});
}
};
useResetPageHelper({
setPage,
offset,
@@ -325,15 +351,38 @@ export const AllProjectView = ({
<span>Joined</span>
</Badge>
) : (
<div className="opacity-0 transition-all group-hover:opacity-100">
<Button
size="xs"
variant="outline_bg"
onClick={() => handlePopUpOpen("requestAccessConfirmation", workspace)}
>
Request Access
</Button>
</div>
<OrgPermissionCan
I={OrgPermissionAdminConsoleAction.AccessAllProjects}
an={OrgPermissionSubjects.AdminConsole}
>
{(isAllowed) =>
isAllowed ? (
<Button
size="xs"
variant="outline_bg"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleAccessProject(workspace.type, workspace.id);
}}
disabled={
orgAdminAccessProject.variables?.projectId === workspace.id &&
orgAdminAccessProject.isPending
}
>
Join as Admin
</Button>
) : (
<Button
size="xs"
variant="outline_bg"
onClick={() => handlePopUpOpen("requestAccessConfirmation", workspace)}
>
Request Access
</Button>
)
}
</OrgPermissionCan>
)}
</div>
))}

View File

@@ -50,7 +50,6 @@ import { Route as adminAuthenticationPageRouteImport } from './pages/admin/Authe
import { Route as organizationProjectsPageRouteImport } from './pages/organization/ProjectsPage/route'
import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route'
import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route'
import { Route as organizationAdminPageRouteImport } from './pages/organization/AdminPage/route'
import { Route as organizationAccessManagementPageRouteImport } from './pages/organization/AccessManagementPage/route'
import { Route as adminGeneralPageRouteImport } from './pages/admin/GeneralPage/route'
import { Route as secretManagerRedirectsRedirectApprovalPageImport } from './pages/secret-manager/redirects/redirect-approval-page'
@@ -631,15 +630,6 @@ const organizationAuditLogsPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const organizationAdminPageRouteRoute = organizationAdminPageRouteImport.update(
{
id: '/admin',
path: '/admin',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any,
)
const organizationAccessManagementPageRouteRoute =
organizationAccessManagementPageRouteImport.update({
id: '/access-management',
@@ -2293,13 +2283,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationAccessManagementPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/admin': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/admin'
path: '/admin'
fullPath: '/organization/admin'
preLoaderRoute: typeof organizationAdminPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs'
path: '/audit-logs'
@@ -3758,7 +3741,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute
organizationAdminPageRouteRoute: typeof organizationAdminPageRouteRoute
organizationAuditLogsPageRouteRoute: typeof organizationAuditLogsPageRouteRoute
organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute
organizationProjectsPageRouteRoute: typeof organizationProjectsPageRouteRoute
@@ -3776,7 +3758,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
{
organizationAccessManagementPageRouteRoute:
organizationAccessManagementPageRouteRoute,
organizationAdminPageRouteRoute: organizationAdminPageRouteRoute,
organizationAuditLogsPageRouteRoute: organizationAuditLogsPageRouteRoute,
organizationBillingPageRouteRoute: organizationBillingPageRouteRoute,
organizationProjectsPageRouteRoute: organizationProjectsPageRouteRoute,
@@ -4668,7 +4649,6 @@ export interface FileRoutesByFullPath {
'/organization': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteWithChildren
'/admin/': typeof adminGeneralPageRouteRoute
'/organization/access-management': typeof organizationAccessManagementPageRouteRoute
'/organization/admin': typeof organizationAdminPageRouteRoute
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/organization/billing': typeof organizationBillingPageRouteRoute
'/organization/projects': typeof organizationProjectsPageRouteRoute
@@ -4887,7 +4867,6 @@ export interface FileRoutesByTo {
'/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutIntegrationsRouteWithChildren
'/organization': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteWithChildren
'/organization/access-management': typeof organizationAccessManagementPageRouteRoute
'/organization/admin': typeof organizationAdminPageRouteRoute
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/organization/billing': typeof organizationBillingPageRouteRoute
'/organization/projects': typeof organizationProjectsPageRouteRoute
@@ -5106,7 +5085,6 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/admin/_admin-layout': typeof adminLayoutRouteWithChildren
'/_authenticate/_inject-org-details/admin/_admin-layout/': typeof adminGeneralPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/access-management': typeof organizationAccessManagementPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/admin': typeof organizationAdminPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/projects': typeof organizationProjectsPageRouteRoute
@@ -5336,7 +5314,6 @@ export interface FileRouteTypes {
| '/organization'
| '/admin/'
| '/organization/access-management'
| '/organization/admin'
| '/organization/audit-logs'
| '/organization/billing'
| '/organization/projects'
@@ -5554,7 +5531,6 @@ export interface FileRouteTypes {
| '/integrations'
| '/organization'
| '/organization/access-management'
| '/organization/admin'
| '/organization/audit-logs'
| '/organization/billing'
| '/organization/projects'
@@ -5771,7 +5747,6 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/admin/_admin-layout'
| '/_authenticate/_inject-org-details/admin/_admin-layout/'
| '/_authenticate/_inject-org-details/_org-layout/organization/access-management'
| '/_authenticate/_inject-org-details/_org-layout/organization/admin'
| '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/organization/billing'
| '/_authenticate/_inject-org-details/_org-layout/organization/projects'
@@ -6202,7 +6177,6 @@ export const routeTree = rootRoute
"parent": "/_authenticate/_inject-org-details/_org-layout",
"children": [
"/_authenticate/_inject-org-details/_org-layout/organization/access-management",
"/_authenticate/_inject-org-details/_org-layout/organization/admin",
"/_authenticate/_inject-org-details/_org-layout/organization/audit-logs",
"/_authenticate/_inject-org-details/_org-layout/organization/billing",
"/_authenticate/_inject-org-details/_org-layout/organization/projects",
@@ -6239,10 +6213,6 @@ export const routeTree = rootRoute
"filePath": "organization/AccessManagementPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
},
"/_authenticate/_inject-org-details/_org-layout/organization/admin": {
"filePath": "organization/AdminPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
},
"/_authenticate/_inject-org-details/_org-layout/organization/audit-logs": {
"filePath": "organization/AuditLogsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"

View File

@@ -20,7 +20,6 @@ const adminRoute = route("/admin", [
const organizationRoutes = route("/organization", [
route("/projects", "organization/ProjectsPage/route.tsx"),
route("/access-management", "organization/AccessManagementPage/route.tsx"),
route("/admin", "organization/AdminPage/route.tsx"),
route("/audit-logs", "organization/AuditLogsPage/route.tsx"),
route("/billing", "organization/BillingPage/route.tsx"),
route("/secret-sharing", [