From 1f4b50a6e0305d062874b7165e658a72aa6ee67b Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 15 Sep 2025 10:58:57 -0700 Subject: [PATCH] wip --- .../src/server/routes/v1/project-router.ts | 1 + backend/src/services/project/project-dal.ts | 6 + .../src/services/project/project-service.ts | 4 +- backend/src/services/project/project-types.ts | 1 + .../projects/RequestProjectAccessModal.tsx | 88 ++++++++++++++ frontend/src/components/projects/index.tsx | 1 + frontend/src/hooks/api/workspace/types.ts | 1 + .../components/AppConnectionsTable.tsx | 74 ++++++------ .../components/AllProjectView.tsx | 73 +---------- .../src/pages/public/ErrorPage/ErrorPage.tsx | 10 ++ .../components/ProjectAccessError.tsx | 113 ++++++++++++++++++ .../public/ErrorPage/components/index.ts | 1 + 12 files changed, 269 insertions(+), 104 deletions(-) create mode 100644 frontend/src/components/projects/RequestProjectAccessModal.tsx create mode 100644 frontend/src/pages/public/ErrorPage/components/ProjectAccessError.tsx create mode 100644 frontend/src/pages/public/ErrorPage/components/index.ts diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 5e7dce76e..5c5af7523 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -1064,6 +1064,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { type: z.nativeEnum(ProjectType).optional(), orderBy: z.nativeEnum(SearchProjectSortBy).optional().default(SearchProjectSortBy.NAME), orderDirection: z.nativeEnum(SortDirection).optional().default(SortDirection.ASC), + projectIds: z.string().trim().array().optional(), name: z .string() .trim() diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 2766db379..d64977f8b 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -399,6 +399,7 @@ export const projectDALFactory = (db: TDbClient) => { name?: string; sortBy?: SearchProjectSortBy; sortDir?: SortDirection; + projectIds?: string[]; }) => { const { limit = 20, offset = 0, sortBy = SearchProjectSortBy.NAME, sortDir = SortDirection.ASC } = dto; @@ -454,6 +455,11 @@ export const projectDALFactory = (db: TDbClient) => { if (dto.name) { void query.whereILike(`${TableName.Project}.name`, `%${dto.name}%`); } + + if (dto.projectIds?.length) { + void query.whereIn(`${TableName.Project}.id`, dto.projectIds); + } + const docs = await query; return { docs, totalCount: Number(docs?.[0]?.count ?? 0) }; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index d59f20bc6..2232a0411 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1806,7 +1806,8 @@ export const projectServiceFactory = ({ limit, type, orderBy, - orderDirection + orderDirection, + projectIds }: TSearchProjectsDTO) => { // check user belong to org await permissionService.getOrgPermission( @@ -1822,6 +1823,7 @@ export const projectServiceFactory = ({ offset, name, type, + projectIds, orgId: permission.orgId, actor: permission.type, actorId: permission.id, diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index ceef78f6a..bf7b633a9 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -221,6 +221,7 @@ export type TSearchProjectsDTO = { limit?: number; offset?: number; orderBy?: SearchProjectSortBy; + projectIds?: string[]; orderDirection?: SortDirection; }; diff --git a/frontend/src/components/projects/RequestProjectAccessModal.tsx b/frontend/src/components/projects/RequestProjectAccessModal.tsx new file mode 100644 index 000000000..a0c5de76f --- /dev/null +++ b/frontend/src/components/projects/RequestProjectAccessModal.tsx @@ -0,0 +1,88 @@ +import { useForm } from "react-hook-form"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, Modal, ModalClose, ModalContent } from "@app/components/v2"; +import { useRequestProjectAccess } from "@app/hooks/api"; +import { Workspace } from "@app/hooks/api/workspace/types"; + +type ContentProps = { + projectId: string; + onComplete: () => void; +}; + +const Content = ({ projectId, onComplete }: ContentProps) => { + const form = useForm<{ note: string }>(); + + const requestProjectAccess = useRequestProjectAccess(); + + const onFormSubmit = ({ note }: { note: string }) => { + if (requestProjectAccess.isPending) return; + requestProjectAccess.mutate( + { + comment: note, + projectId + }, + { + onSuccess: () => { + createNotification({ + type: "success", + title: "Project Access Request Sent", + text: "Project admins will receive an email of your request" + }); + onComplete(); + } + } + ); + }; + + return ( +
+ + + +
+ + + + +
+
+ ); +}; + +type RequestProjectAccessModalProps = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + project?: Workspace; + onComplete?: () => void; +}; + +export const RequestProjectAccessModal = ({ + isOpen, + onOpenChange, + project, + onComplete +}: RequestProjectAccessModalProps) => { + if (!project) return null; + + return ( + + + { + onOpenChange(false); + if (onComplete) onComplete(); + }} + projectId={project?.id} + /> + + + ); +}; diff --git a/frontend/src/components/projects/index.tsx b/frontend/src/components/projects/index.tsx index 1fb78225d..a2dc754ad 100644 --- a/frontend/src/components/projects/index.tsx +++ b/frontend/src/components/projects/index.tsx @@ -1 +1,2 @@ export { NewProjectModal } from "./NewProjectModal"; +export * from "./RequestProjectAccessModal"; diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 33eb0909f..e87bc439e 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -186,6 +186,7 @@ export type TSearchProjectsDTO = { name?: string; limit?: number; offset?: number; + projectIds?: string[]; type?: ProjectType; options?: { enabled?: boolean }; orderBy?: ProjectIdentityOrderBy; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionsTable.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionsTable.tsx index 18ae6034c..63f757114 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionsTable.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionsTable.tsx @@ -38,11 +38,7 @@ import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context"; import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types"; import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types"; import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; -import { - getUserTablePreference, - PreferenceKey, - setUserTablePreference -} from "@app/helpers/userTablePreferences"; +import { getUserTablePreference, PreferenceKey, setUserTablePreference } from "@app/helpers/userTablePreferences"; import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { TAppConnection, useListAppConnections } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; @@ -311,38 +307,46 @@ export const AppConnectionsTable = ({ projectId, projectType }: Props) => { - + Filter by Apps {appConnections.length ? ( - [...new Set(appConnections.map(({ app }) => app))].map((app) => ( - { - e.preventDefault(); - setFilters((prev) => ({ - ...prev, - apps: prev.apps.includes(app) - ? prev.apps.filter((a) => a !== app) - : [...prev.apps, app] - })); - }} - key={app} - icon={ - filters.apps.includes(app) && ( - - ) - } - iconPos="right" - > -
- {`${APP_CONNECTION_MAP[app].name} - {APP_CONNECTION_MAP[app].name} -
-
- )) + [...new Set(appConnections.map(({ app }) => app))] + .sort((a, b) => { + return a.toLowerCase().localeCompare(b.toLowerCase()); + }) + .map((app) => ( + { + e.preventDefault(); + setFilters((prev) => ({ + ...prev, + apps: prev.apps.includes(app) + ? prev.apps.filter((a) => a !== app) + : [...prev.apps, app] + })); + }} + key={app} + icon={ + filters.apps.includes(app) && ( + + ) + } + iconPos="right" + > +
+ {`${APP_CONNECTION_MAP[app].name} + {APP_CONNECTION_MAP[app].name} +
+
+ )) ) : ( No Connections Configured )} diff --git a/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx b/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx index 7b066a099..d5c91d810 100644 --- a/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx +++ b/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx @@ -1,5 +1,4 @@ import { useState } from "react"; -import { useForm } from "react-hook-form"; import { faArrowDownAZ, faBorderAll, @@ -16,6 +15,7 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; +import { RequestProjectAccessModal } from "@app/components/projects/RequestProjectAccessModal"; import { Badge, Button, @@ -24,12 +24,9 @@ import { DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, - FormControl, IconButton, Input, Lottie, - Modal, - ModalContent, Pagination, Skeleton, Tooltip @@ -43,11 +40,7 @@ import { setUserTablePreference } from "@app/helpers/userTablePreferences"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; -import { - useOrgAdminAccessProject, - useRequestProjectAccess, - useSearchProjects -} from "@app/hooks/api"; +import { useOrgAdminAccessProject, useSearchProjects } from "@app/hooks/api"; import { ProjectType, Workspace, WorkspaceEnv } from "@app/hooks/api/workspace/types"; import { ProjectListToggle, @@ -62,53 +55,6 @@ type Props = { onProjectListViewChange: (value: ProjectListView) => void; }; -type RequestAccessModalProps = { - projectId: string; - onPopUpToggle: () => void; -}; - -const RequestAccessModal = ({ projectId, onPopUpToggle }: RequestAccessModalProps) => { - const form = useForm<{ note: string }>(); - - const requestProjectAccess = useRequestProjectAccess(); - - const onFormSubmit = ({ note }: { note: string }) => { - if (requestProjectAccess.isPending) return; - requestProjectAccess.mutate( - { - comment: note, - projectId - }, - { - onSuccess: () => { - createNotification({ - type: "success", - title: "Project Access Request Sent", - text: "Project admins will receive an email of your request" - }); - onPopUpToggle(); - } - } - ); - }; - - return ( -
- - - -
- - -
-
- ); -}; - export const AllProjectView = ({ onAddNewProject, onUpgradePlan, @@ -419,20 +365,11 @@ export const AllProjectView = ({
No Projects Found
)} - handlePopUpToggle("requestAccessConfirmation", isOpen)} - > - - handlePopUpToggle("requestAccessConfirmation")} - projectId={requestedWorkspaceDetails?.id} - /> - - + project={requestedWorkspaceDetails} + /> ); }; diff --git a/frontend/src/pages/public/ErrorPage/ErrorPage.tsx b/frontend/src/pages/public/ErrorPage/ErrorPage.tsx index 1910eb891..ef6a837ae 100644 --- a/frontend/src/pages/public/ErrorPage/ErrorPage.tsx +++ b/frontend/src/pages/public/ErrorPage/ErrorPage.tsx @@ -5,7 +5,17 @@ import { AxiosError } from "axios"; import { Button } from "@app/components/v2"; +import { ProjectAccessError } from "./components"; + export const ErrorPage = ({ error }: ErrorComponentProps) => { + if ( + error instanceof AxiosError && + error.status === 403 && + error.response?.data?.error === "User not a part of the specified project" + ) { + return ; + } + return (
diff --git a/frontend/src/pages/public/ErrorPage/components/ProjectAccessError.tsx b/frontend/src/pages/public/ErrorPage/components/ProjectAccessError.tsx new file mode 100644 index 000000000..95a7d3729 --- /dev/null +++ b/frontend/src/pages/public/ErrorPage/components/ProjectAccessError.tsx @@ -0,0 +1,113 @@ +import { faHome } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Link, useNavigate, useParams } from "@tanstack/react-router"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { RequestProjectAccessModal } from "@app/components/projects"; +import { AccessRestrictedBanner, Button } from "@app/components/v2"; +import { OrgPermissionSubjects } from "@app/context"; +import { OrgPermissionAdminConsoleAction } from "@app/context/OrgPermissionContext/types"; +import { getProjectHomePage } from "@app/helpers/project"; +import { usePopUp } from "@app/hooks"; +import { useOrgAdminAccessProject, useSearchProjects } from "@app/hooks/api"; + +export const ProjectAccessError = () => { + const orgAdminAccessProject = useOrgAdminAccessProject(); + + const navigate = useNavigate(); + + const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([ + "requestAccessConfirmation" + ] as const); + + const { projectId } = useParams({ + strict: false + }); + + const { data, isPending: isProjectLoading } = useSearchProjects({ + projectIds: projectId ? [projectId] : [], + options: { + enabled: Boolean(projectId) + } + }); + + const [project] = data?.projects ?? []; + + const handleAccessProject = async () => { + if (!project) return; + try { + await orgAdminAccessProject.mutateAsync({ + projectId: project.id + }); + await navigate({ + to: getProjectHomePage(project.type, project.environments), + params: { + projectId: project.id + } + }); + } catch { + createNotification({ + text: "Failed to access project", + type: "error" + }); + } + }; + + return ( +
+ + You are not currently a member of this project. Request access to join project. +
+ + + + + {(isAllowed) => + isAllowed ? ( + + ) : ( + + ) + } + +
+ handlePopUpToggle("requestAccessConfirmation", isOpen)} + project={project} + onComplete={() => { + navigate({ + to: "/organization/projects" + }); + }} + /> + + } + /> +
+ ); +}; diff --git a/frontend/src/pages/public/ErrorPage/components/index.ts b/frontend/src/pages/public/ErrorPage/components/index.ts new file mode 100644 index 000000000..1940497e9 --- /dev/null +++ b/frontend/src/pages/public/ErrorPage/components/index.ts @@ -0,0 +1 @@ +export * from "./ProjectAccessError";