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}]({`/images/integrations/${APP_CONNECTION_MAP[app].image}`})
-
{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}]({`/images/integrations/${APP_CONNECTION_MAP[app].image}`})
+
{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";