Repository: SwissDataScienceCenter/renku-ui
Pull request #3528: feat: proper listing of project data connectors
Author: login: olevski
Created: 2025-02-11T15:35:55Z | Merged: 2025-03-21T09:58:45Z

/deploy renku-data-services=build-project-authz-disentangle


Diff:
diff --git a/client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx b/client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
index 38d058c6ae..8e91536fa2 100644
--- a/client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
+++ b/client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
@@ -17,7 +17,7 @@
  */
 
 import cx from "classnames";
-import { useCallback, useState } from "react";
+import { useCallback, useState, useRef } from "react";
 import { Database, PlusLg } from "react-bootstrap-icons";
 import {
   Badge,
@@ -26,12 +26,16 @@ import {
   CardBody,
   CardHeader,
   ListGroup,
+  UncontrolledTooltip,
 } from "reactstrap";
 
 import { Loader } from "../../../../components/Loader";
 import { RtkOrNotebooksError } from "../../../../components/errors/RtkErrorAlert";
 
-import { useGetDataConnectorsByDataConnectorIdQuery } from "../../../dataConnectorsV2/api/data-connectors.api";
+import {
+  useGetDataConnectorsByDataConnectorIdQuery,
+  useGetProjectsByProjectIdInaccessibleDataConnectorLinksQuery,
+} from "../../../dataConnectorsV2/api/data-connectors.api";
 import DataConnectorBoxListDisplay from "../../../dataConnectorsV2/components/DataConnectorsBoxListDisplay";
 import PermissionsGuard from "../../../permissionsV2/PermissionsGuard";
 import type {
@@ -43,6 +47,7 @@ import { useGetProjectsByProjectIdDataConnectorLinksQuery } from "../../../proje
 import useProjectPermissions from "../../utils/useProjectPermissions.hook";
 
 import ProjectConnectDataConnectorsModal from "./ProjectConnectDataConnectorsModal";
+import { ErrorAlert } from "../../../../components/Alert";
 
 interface DataConnectorListDisplayProps {
   project: Project;
@@ -56,22 +61,49 @@ export default function ProjectDataConnectorsBox({
       projectId: project.id,
     });
 
-  if (isLoading) return <DataConnectorLoadingBoxContent />;
+  const {
+    data: inaccessibleDataConnectorsData,
+    isLoading: inaccessibleDataConnectorsIsLoading,
+  } = useGetProjectsByProjectIdInaccessibleDataConnectorLinksQuery({
+    projectId: project.id,
+  });
+
+  if (isLoading || inaccessibleDataConnectorsIsLoading)
+    return <DataConnectorLoadingBoxContent />;
 
-  if (error || data == null) {
+  if (error) {
     return <RtkOrNotebooksError error={error} dismissible={false} />;
   }
 
-  return <ProjectDataConnectorBoxContent data={data} project={project} />;
+  if (data == null) {
+    return (
+      <ErrorAlert>
+        Data connectors could not be loaded from the API, please contact a Renku
+        administrator.
+      </ErrorAlert>
+    );
+  }
+
+  return (
+    <ProjectDataConnectorBoxContent
+      data={data}
+      project={project}
+      inaccessibleDataConnectorsCount={
+        inaccessibleDataConnectorsData?.count || 0
+      }
+    />
+  );
 }
 
 interface ProjectDataConnectorBoxContentProps
   extends DataConnectorListDisplayProps {
   data: GetProjectsByProjectIdDataConnectorLinksApiResponse;
+  inaccessibleDataConnectorsCount: number;
 }
 function ProjectDataConnectorBoxContent({
   data,
   project,
+  inaccessibleDataConnectorsCount,
 }: ProjectDataConnectorBoxContentProps) {
   const [isModalOpen, setModalOpen] = useState(false);
   const toggleOpen = useCallback(() => {
@@ -83,7 +115,8 @@ function ProjectDataConnectorBoxContent({
         <ProjectDataConnectorBoxHeader
           projectId={project.id}
           toggleOpen={toggleOpen}
-          totalConnectors={data.length}
+          accessibleDataConnectorsCount={data.length}
+          inaccessibleDataConnectorsCount={inaccessibleDataConnectorsCount}
         />
         <CardBody>
           {data.length === 0 && (
@@ -95,7 +128,11 @@ function ProjectDataConnectorBoxContent({
           {data != null && data.length > 0 && (
             <ListGroup flush>
               {data.map((dc) => (
-                <DataConnectorLinkDisplay key={dc.id} dataConnectorLink={dc} />
+                <DataConnectorLinkDisplay
+                  key={dc.id}
+                  dataConnectorLink={dc}
+                  projectPath={`${project.namespace}/${project.slug}`}
+                />
               ))}
             </ListGroup>
           )}
@@ -116,13 +153,15 @@ function ProjectDataConnectorBoxContent({
 interface ProjectDataConnectorBoxHeaderProps {
   projectId: Project["id"];
   toggleOpen: () => void;
-  totalConnectors: number;
+  accessibleDataConnectorsCount: number;
+  inaccessibleDataConnectorsCount: number;
 }
 
 function ProjectDataConnectorBoxHeader({
   projectId,
   toggleOpen,
-  totalConnectors,
+  accessibleDataConnectorsCount,
+  inaccessibleDataConnectorsCount,
 }: ProjectDataConnectorBoxHeaderProps) {
   const permissions = useProjectPermissions({ projectId });
 
@@ -135,12 +174,17 @@ function ProjectDataConnectorBoxHeader({
           "justify-content-between"
         )}
       >
-        <div className={cx("align-items-center", "d-flex")}>
-          <h4 className={cx("mb-0", "me-2")}>
+        <div className={cx("align-items-center", "d-flex", "gap-2")}>
+          <h4 className="mb-0">
             <Database className={cx("me-1", "bi")} />
             Data
           </h4>
-          <Badge>{totalConnectors}</Badge>
+          <Badge>{accessibleDataConnectorsCount}</Badge>
+          {inaccessibleDataConnectorsCount > 0 && (
+            <MissingDataConnectorsBadge
+              inaccessibleConnectors={inaccessibleDataConnectorsCount}
+            />
+          )}
         </div>
         <div className="my-auto">
           <PermissionsGuard
@@ -193,9 +237,11 @@ function DataConnectorLoadingBoxContent() {
 
 interface DataConnectorLinkDisplayProps {
   dataConnectorLink: DataConnectorToProjectLink;
+  projectPath: string;
 }
 function DataConnectorLinkDisplay({
   dataConnectorLink,
+  projectPath,
 }: DataConnectorLinkDisplayProps) {
   const { data_connector_id } = dataConnectorLink;
   const { data: dataConnector, isLoading } =
@@ -208,6 +254,41 @@ function DataConnectorLinkDisplay({
     <DataConnectorBoxListDisplay
       dataConnector={dataConnector}
       dataConnectorLink={dataConnectorLink}
+      dataConnectorPotentiallyInaccessible={
+        projectPath != dataConnector.namespace &&
+        dataConnector.visibility == "private"
+      }
     />
   );
 }
+
+interface MissingDataConnectorsBadgeProps {
+  className?: string;
+  inaccessibleConnectors: number;
+}
+
+function MissingDataConnectorsBadge({
+  className,
+  inaccessibleConnectors,
+}: MissingDataConnectorsBadgeProps) {
+  const ref = useRef<HTMLDivElement>(null);
+
+  if (!inaccessibleConnectors) return null;
+  const singular = inaccessibleConnectors === 1;
+  return (
+    <>
+      <Badge
+        className={cx("rounded-pill", className)}
+        color="primary"
+        innerRef={ref}
+      >
+        +{inaccessibleConnectors} hidden
+      </Badge>
+      <UncontrolledTooltip target={ref}>
+        There {singular ? "is" : "are"} {inaccessibleConnectors}
+        data connector{singular ? "" : "s"} linked to this project but not
+        visible to you.
+      </UncontrolledTooltip>
+    </>
+  );
+}
diff --git a/client/src/features/dashboardV2/DashboardV2.tsx b/client/src/features/dashboardV2/DashboardV2.tsx
index 6c108cc7c0..f1f4a9d79f 100644
--- a/client/src/features/dashboardV2/DashboardV2.tsx
+++ b/client/src/features/dashboardV2/DashboardV2.tsx
@@ -297,11 +297,9 @@ function ProjectList({ data, error, isLoading }: ProjectListProps) {
 
 function GroupsDashboard() {
   const { data, error, isLoading } = useGetGroupsQuery({
-    params: {
-      page: 1,
-      per_page: 5,
-      direct_member: true,
-    },
+    page: 1,
+    perPage: 5,
+    directMember: true,
   });
   const hasGroups = data && data?.groups?.length > 0;
   return (
diff --git a/client/src/features/dataConnectorsV2/api/data-connectors.api.ts b/client/src/features/dataConnectorsV2/api/data-connectors.api.ts
index e2667678f0..27cb889c26 100644
--- a/client/src/features/dataConnectorsV2/api/data-connectors.api.ts
+++ b/client/src/features/dataConnectorsV2/api/data-connectors.api.ts
@@ -118,6 +118,22 @@ const injectedRtkApi = api.injectEndpoints({
         method: "DELETE",
       }),
     }),
+    getProjectsByProjectIdDataConnectorLinks: build.query<
+      GetProjectsByProjectIdDataConnectorLinksApiResponse,
+      GetProjectsByProjectIdDataConnectorLinksApiArg
+    >({
+      query: (queryArg) => ({
+        url: `/projects/${queryArg.projectId}/data_connector_links`,
+      }),
+    }),
+    getProjectsByProjectIdInaccessibleDataConnectorLinks: build.query<
+      GetProjectsByProjectIdInaccessibleDataConnectorLinksApiResponse,
+      GetProjectsByProjectIdInaccessibleDataConnectorLinksApiArg
+    >({
+      query: (queryArg) => ({
+        url: `/projects/${queryArg.projectId}/inaccessible_data_connector_links`,
+      }),
+    }),
   }),
   overrideExisting: false,
 });
@@ -206,6 +222,18 @@ export type DeleteDataConnectorsByDataConnectorIdSecretsApiArg = {
   /** the ID of the data connector */
   dataConnectorId: Ulid;
 };
+export type GetProjectsByProjectIdDataConnectorLinksApiResponse =
+  /** status 200 List of data connector to project links */ DataConnectorToProjectLinksList;
+export type GetProjectsByProjectIdDataConnectorLinksApiArg = {
+  /** the ID of the project */
+  projectId: Ulid;
+};
+export type GetProjectsByProjectIdInaccessibleDataConnectorLinksApiResponse =
+  /** status 200 List of data connector to project links */ InaccessibleDataConnectorLinks;
+export type GetProjectsByProjectIdInaccessibleDataConnectorLinksApiArg = {
+  /** the ID of the project */
+  projectId: Ulid;
+};
 export type Ulid = string;
 export type DataConnectorName = string;
 export type Slug = string;
@@ -248,7 +276,17 @@ export type RCloneOption = {
   /** if true, only values from 'examples' can be used */
   exclusive?: boolean;
   /** data type of option value. RClone has more options but they map to the ones listed here. */
-  datatype?: "int" | "bool" | "string" | "Time";
+  type?:
+    | "int"
+    | "bool"
+    | "string"
+    | "Time"
+    | "Duration"
+    | "MultiEncoder"
+    | "SizeSuffix"
+    | "SpaceSepList"
+    | "CommaSepList"
+    | "Tristate";
 };
 export type CloudStorageCore = {
   storage_type: StorageType;
@@ -418,6 +456,10 @@ export type DataConnectorSecretPatch = {
   value: SecretValueNullable;
 };
 export type DataConnectorSecretPatchList = DataConnectorSecretPatch[];
+export type InaccessibleDataConnectorLinks = {
+  /** The number of data links the user does not have access to */
+  count?: number;
+};
 export const {
   useGetDataConnectorsQuery,
   usePostDataConnectorsMutation,
@@ -432,4 +474,6 @@ export const {
   useGetDataConnectorsByDataConnectorIdSecretsQuery,
   usePatchDataConnectorsByDataConnectorIdSecretsMutation,
   useDeleteDataConnectorsByDataConnectorIdSecretsMutation,
+  useGetProjectsByProjectIdDataConnectorLinksQuery,
+  useGetProjectsByProjectIdInaccessibleDataConnectorLinksQuery,
 } = injectedRtkApi;
diff --git a/client/src/features/dataConnectorsV2/api/data-connectors.openapi.json b/client/src/features/dataConnectorsV2/api/data-connectors.openapi.json
index 7fdac4b43c..c7840aa4e6 100644
--- a/client/src/features/dataConnectorsV2/api/data-connectors.openapi.json
+++ b/client/src/features/dataConnectorsV2/api/data-connectors.openapi.json
@@ -473,6 +473,70 @@
         },
         "tags": ["data_connectors"]
       }
+    },
+    "/projects/{project_id}/data_connector_links": {
+      "parameters": [
+        {
+          "in": "path",
+          "name": "project_id",
+          "required": true,
+          "schema": {
+            "$ref": "#/components/schemas/Ulid"
+          },
+          "description": "the ID of the project"
+        }
+      ],
+      "get": {
+        "summary": "Get all links from data connectors to a given project",
+        "responses": {
+          "200": {
+            "description": "List of data connector to project links",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/DataConnectorToProjectLinksList"
+                }
+              }
+            }
+          },
+          "default": {
+            "$ref": "#/components/responses/Error"
+          }
+        },
+        "tags": ["projects"]
+      }
+    },
+    "/projects/{project_id}/inaccessible_data_connector_links": {
+      "parameters": [
+        {
+          "in": "path",
+          "name": "project_id",
+          "required": true,
+          "schema": {
+            "$ref": "#/components/schemas/Ulid"
+          },
+          "description": "the ID of the project"
+        }
+      ],
+      "get": {
+        "summary": "Get the number of links that the user does not have access to in a project",
+        "responses": {
+          "200": {
+            "description": "List of data connector to project links",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/InaccessibleDataConnectorLinks"
+                }
+              }
+            }
+          },
+          "default": {
+            "$ref": "#/components/responses/Error"
+          }
+        },
+        "tags": ["projects"]
+      }
     }
   },
   "components": {
@@ -902,10 +966,21 @@
             "type": "boolean",
             "description": "if true, only values from 'examples' can be used"
           },
-          "datatype": {
+          "type": {
             "type": "string",
             "description": "data type of option value. RClone has more options but they map to the ones listed here.",
-            "enum": ["int", "bool", "string", "Time"]
+            "enum": [
+              "int",
+              "bool",
+              "string",
+              "Time",
+              "Duration",
+              "MultiEncoder",
+              "SizeSuffix",
+              "SpaceSepList",
+              "CommaSepList",
+              "Tristate"
+            ]
           }
         }
       },
@@ -921,9 +996,17 @@
         "type": "string",
         "minLength": 1,
         "maxLength": 99,
-        "pattern": "^(?!.*\\.git$|.*\\.atom$|.*[\\-._][\\-._].*)[a-zA-Z0-9][a-zA-Z0-9\\-_.]*$",
+        "pattern": "^(?!.*\\.git$|.*\\.atom$|.*[\\-._][\\-._].*)[a-z0-9][a-z0-9\\-_.]*$",
         "example": "a-slug-example"
       },
+      "OneOrTwoSlugs": {
+        "description": "A command-line/url friendly name for a single slug or two slugs separated by /",
+        "type": "string",
+        "minLength": 1,
+        "maxLength": 200,
+        "pattern": "^(?!.*\\.git$|.*\\.atom$|.*[\\-._][\\-._].*)[a-z0-9][a-z0-9\\-_.]*(?<!\\.git)(?<!\\.atom)(?:/[a-z0-9][a-z0-9\\-_.]*){0,1}$",
+        "example": "user1/project-1"
+      },
       "CreationDate": {
         "description": "The date and time the resource was created (in UTC and ISO-8601 format)",
         "type": "string",
@@ -1073,6 +1156,17 @@
           }
         },
         "required": ["error"]
+      },
+      "InaccessibleDataConnectorLinks": {
+        "description": "Information about inaccessible data connector links",
+        "type": "object",
+        "properties": {
+          "count": {
+            "type": "integer",
+            "minimum": 0,
+            "description": "The number of data links the user does not have access to"
+          }
+        }
       }
     },
     "responses": {
diff --git a/client/src/features/dataConnectorsV2/components/DataConnectorModal/DataConnectorModalBody.tsx b/client/src/features/dataConnectorsV2/components/DataConnectorModal/DataConnectorModalBody.tsx
index 680c170f92..98869d5e91 100644
--- a/client/src/features/dataConnectorsV2/components/DataConnectorModal/DataConnectorModalBody.tsx
+++ b/client/src/features/dataConnectorsV2/components/DataConnectorModal/DataConnectorModalBody.tsx
@@ -324,9 +324,10 @@ export function DataConnectorMount({ project }: AddOrEditDataConnectorProps) {
                 inputId="namespace-input"
                 onChange={(e) => {
                   field.onChange(e);
-                  onFieldValueChange("namespace", e?.slug ?? "");
+                  onFieldValueChange("namespace", e?.path ?? "");
                 }}
-                project={project}
+                ensureNamespace={project?.namespace}
+                includeProjectNamespaces={true}
               />
             );
           }}
@@ -334,7 +335,7 @@ export function DataConnectorMount({ project }: AddOrEditDataConnectorProps) {
             required: true,
             maxLength: 99,
             pattern:
-              /^(?!.*\.git$|.*\.atom$|.*[-._][-._].*)[a-zA-Z0-9][a-zA-Z0-9\-_.]*$/,
+              /^(?!.*\.git$|.*\.atom$|.*[-._][-._].*)[a-z0-9][a-z0-9\-_.]*(?<!\.git)(?<!\.atom)(?:[a-z0-9][a-z0-9\-_.]*)*$/,
           }}
         />
         <div className="invalid-feedback">
diff --git a/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx b/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
index 6e2e0f39ce..8314fc209d 100644
--- a/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
+++ b/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
@@ -24,6 +24,7 @@ import {
   Key,
   Lock,
   PersonBadge,
+  Globe2,
 } from "react-bootstrap-icons";
 import { Link, generatePath } from "react-router";
 import { Offcanvas, OffcanvasBody, UncontrolledTooltip } from "reactstrap";
@@ -52,6 +53,9 @@ import { useGetDataConnectorsByDataConnectorIdSecretsQuery } from "../api/data-c
 
 import DataConnectorActions from "./DataConnectorActions";
 import useDataConnectorProjects from "./useDataConnectorProjects.hook";
+import { WarnAlert } from "../../../components/Alert";
+import { isProjectNamespace } from "./dataConnector.utils";
+import { DATA_CONNECTORS_VISIBILITY_WARNING } from "./dataConnector.constants";
 
 const SECTION_CLASSES = [
   "border-top",
@@ -82,12 +86,14 @@ interface DataConnectorViewProps {
   dataConnectorLink?: DataConnectorToProjectLink;
   showView: boolean;
   toggleView: () => void;
+  dataConnectorPotentiallyInaccessible?: boolean;
 }
 export default function DataConnectorView({
   dataConnector,
   dataConnectorLink,
   showView,
   toggleView,
+  dataConnectorPotentiallyInaccessible = false,
 }: DataConnectorViewProps) {
   return (
     <Offcanvas
@@ -109,7 +115,12 @@ export default function DataConnectorView({
         <DataConnectorViewHeader
           {...{ dataConnector, dataConnectorLink, toggleView }}
         />
-        <DataConnectorViewMetadata dataConnector={dataConnector} />
+        <DataConnectorViewMetadata
+          dataConnector={dataConnector}
+          dataConnectorPotentiallyInaccessible={
+            dataConnectorPotentiallyInaccessible
+          }
+        />
         <DataConnectorViewConfiguration dataConnector={dataConnector} />
         <DataConnectorViewProjects dataConnector={dataConnector} />
         <DataConnectorViewAccess dataConnector={dataConnector} />
@@ -332,9 +343,15 @@ function DataConnectorViewProjects({
   );
 }
 
+interface DataConnectorViewMetadataProps {
+  dataConnector: DataConnectorRead;
+  dataConnectorPotentiallyInaccessible: boolean;
+}
+
 function DataConnectorViewMetadata({
   dataConnector,
-}: Pick<DataConnectorViewProps, "dataConnector">) {
+  dataConnectorPotentiallyInaccessible,
+}: DataConnectorViewMetadataProps) {
   const storageDefinition = dataConnector.storage;
   const credentialFieldDefinitions = useMemo(
     () =>
@@ -392,10 +409,17 @@ function DataConnectorViewMetadata({
       <DataConnectorPropertyValue title="Owner">
         <div className={cx("d-flex", "align-items-center")}>
           <div className="me-1">
-            <UserAvatar namespace={dataConnector.namespace} />{" "}
+            {isProjectNamespace(dataConnector.namespace) ? (
+              <Folder />
+            ) : (
+              <UserAvatar namespace={dataConnector.namespace} />
+            )}{" "}
           </div>
           {namespaceUrl == null ? (
-            <div className="me-1">@{dataConnector.namespace}</div>
+            <div className="me-1">
+              {isProjectNamespace(dataConnector.namespace) ? "" : "@"}
+              {dataConnector.namespace}
+            </div>
           ) : (
             <div>
               <Link className="me-1" to={namespaceUrl}>
@@ -423,6 +447,24 @@ function DataConnectorViewMetadata({
           </div>
         </div>
       </DataConnectorPropertyValue>
+      <DataConnectorPropertyValue title="Visibility">
+        {dataConnector.visibility === "private" ? (
+          <>
+            <Lock className={cx("bi", "me-1")} />
+            Private
+          </>
+        ) : (
+          <>
+            <Globe2 className={cx("bi", "me-1")} />
+            Public
+          </>
+        )}
+        {dataConnectorPotentiallyInaccessible && (
+          <WarnAlert className="mt-2" timeout={0} dismissible={false}>
+            {DATA_CONNECTORS_VISIBILITY_WARNING}
+          </WarnAlert>
+        )}
+      </DataConnectorPropertyValue>
       {nonRequiredCredentialConfigurationKeys.map((key) => {
         const title =
           key == "provider" && hasAccessMode ? "Mode" : toCapitalized(key);
diff --git a/client/src/features/dataConnectorsV2/components/DataConnectorsBox.tsx b/client/src/features/dataConnectorsV2/components/DataConnectorsBox.tsx
index 9df7a9b1a5..9507b1bbd3 100644
--- a/client/src/features/dataConnectorsV2/components/DataConnectorsBox.tsx
+++ b/client/src/features/dataConnectorsV2/components/DataConnectorsBox.tsx
@@ -34,7 +34,7 @@ import Pagination from "../../../components/Pagination";
 import { RtkOrNotebooksError } from "../../../components/errors/RtkErrorAlert";
 import useGroupPermissions from "../../groupsV2/utils/useGroupPermissions.hook";
 import PermissionsGuard from "../../permissionsV2/PermissionsGuard";
-import type { NamespaceKind } from "../../projectsV2/api/namespace.enhanced-api";
+import type { NamespaceKind } from "../../projectsV2/api/namespace.api";
 import { useGetUserQuery } from "../../usersV2/api/users.api";
 import {
   useGetDataConnectorsQuery,
diff --git a/client/src/features/dataConnectorsV2/components/DataConnectorsBoxListDisplay.tsx b/client/src/features/dataConnectorsV2/components/DataConnectorsBoxListDisplay.tsx
index 5398549c42..7830f2f879 100644
--- a/client/src/features/dataConnectorsV2/components/DataConnectorsBoxListDisplay.tsx
+++ b/client/src/features/dataConnectorsV2/components/DataConnectorsBoxListDisplay.tsx
@@ -17,9 +17,15 @@
  */
 
 import cx from "classnames";
-import { useCallback, useMemo } from "react";
-import { EyeFill, Globe2, Lock, Pencil } from "react-bootstrap-icons";
-import { Col, ListGroupItem, Row } from "reactstrap";
+import { useCallback, useMemo, useRef } from "react";
+import { EyeFill, Globe2, Lock, Pencil, Folder } from "react-bootstrap-icons";
+import {
+  Col,
+  ListGroupItem,
+  Row,
+  UncontrolledTooltip,
+  Badge,
+} from "reactstrap";
 
 import ClampedParagraph from "../../../components/clamped/ClampedParagraph";
 import { TimeCaption } from "../../../components/TimeCaption";
@@ -31,16 +37,20 @@ import type {
 } from "../api/data-connectors.api";
 
 import DataConnectorView from "./DataConnectorView";
+import { isProjectNamespace } from "./dataConnector.utils";
+import { DATA_CONNECTORS_VISIBILITY_WARNING } from "./dataConnector.constants";
 
 interface DataConnectorBoxListDisplayProps {
   dataConnector: DataConnector;
   dataConnectorLink?: DataConnectorToProjectLink;
   extendedPreview?: boolean;
+  dataConnectorPotentiallyInaccessible?: boolean;
 }
 export default function DataConnectorBoxListDisplay({
   dataConnector,
   dataConnectorLink,
   extendedPreview,
+  dataConnectorPotentiallyInaccessible = false,
 }: DataConnectorBoxListDisplayProps) {
   const {
     name,
@@ -97,12 +107,16 @@ export default function DataConnectorBoxListDisplay({
               className={cx(
                 "d-flex",
                 "flex-row",
+                "gap-1",
                 "text-truncate",
-                "gap-2",
                 "align-items-center"
               )}
             >
-              <UserAvatar namespace={namespace} size="sm" />
+              {isProjectNamespace(namespace) ? (
+                <Folder className="bi" />
+              ) : (
+                <UserAvatar namespace={namespace} size="sm" />
+              )}
               <p className={cx("mb-0", "text-truncate", "text-muted")}>
                 {namespace}
               </p>
@@ -114,7 +128,7 @@ export default function DataConnectorBoxListDisplay({
                 "align-items-center",
                 "d-flex",
                 "flex-wrap",
-                "gap-2",
+                "gap-1",
                 "justify-content-between"
               )}
             >
@@ -123,22 +137,27 @@ export default function DataConnectorBoxListDisplay({
                   "align-items-center",
                   "d-flex",
                   "flex-wrap",
-                  "gap-3",
+                  "gap-2",
                   "mt-auto"
                 )}
               >
-                {visibility.toLowerCase() === "private" ? (
-                  <div>
-                    <Lock className={cx("bi", "me-1")} />
-                    Private
-                  </div>
-                ) : (
-                  <div>
-                    <Globe2 className={cx("bi", "me-1")} />
-                    Public
-                  </div>
-                )}
+                <div>
+                  {visibility.toLowerCase() === "private" ? (
+                    <>
+                      <Lock className={cx("bi", "me-1")} />
+                      Private
+                    </>
+                  ) : (
+                    <>
+                      <Globe2 className={cx("bi", "me-1")} />
+                      Public
+                    </>
+                  )}
+                </div>
                 {extendedPreview && readOnly}
+                {dataConnectorPotentiallyInaccessible && (
+                  <DataConnectorNotVisibleToAllUsersBadge />
+                )}
               </div>
               <TimeCaption
                 datetime={creationDate}
@@ -154,7 +173,43 @@ export default function DataConnectorBoxListDisplay({
         dataConnectorLink={dataConnectorLink}
         showView={showDetails}
         toggleView={toggleDetails}
+        dataConnectorPotentiallyInaccessible={
+          dataConnectorPotentiallyInaccessible
+        }
       />
     </>
   );
 }
+
+interface DataConnectorNotVisibleToAllUsersBadgeProps {
+  className?: string;
+  warning?: string;
+}
+
+export function DataConnectorNotVisibleToAllUsersBadge({
+  className,
+}: DataConnectorNotVisibleToAllUsersBadgeProps) {
+  const ref = useRef<HTMLDivElement>(null);
+
+  return (
+    <>
+      <Badge
+        className={cx(
+          "rounded-pill",
+          "border",
+          "bg-warning-subtle",
+          "border-warning",
+          "text-warning-emphasis",
+          className
+        )}
+        color="primary"
+        innerRef={ref}
+      >
+        Visibility warning
+      </Badge>
+      <UncontrolledTooltip target={ref} placement="bottom">
+        {DATA_CONNECTORS_VISIBILITY_WARNING}
+      </UncontrolledTooltip>
+    </>
+  );
+}
diff --git a/client/src/features/dataConnectorsV2/components/dataConnector.constants.ts b/client/src/features/dataConnectorsV2/components/dataConnector.constants.ts
new file mode 100644
index 0000000000..a07549262d
--- /dev/null
+++ b/client/src/features/dataConnectorsV2/components/dataConnector.constants.ts
@@ -0,0 +1,2 @@
+export const DATA_CONNECTORS_VISIBILITY_WARNING =
+  "Some members of this project may not be able to access this data connector";
diff --git a/client/src/features/dataConnectorsV2/components/dataConnector.utils.ts b/client/src/features/dataConnectorsV2/components/dataConnector.utils.ts
index e10ddd2f34..68e4dc473d 100644
--- a/client/src/features/dataConnectorsV2/components/dataConnector.utils.ts
+++ b/client/src/features/dataConnectorsV2/components/dataConnector.utils.ts
@@ -193,3 +193,7 @@ export function hasSchemaAccessMode(schema: CloudStorageSchema) {
     providers?.examples && STORAGES_WITH_ACCESS_MODE.includes(schema.prefix)
   );
 }
+
+export function isProjectNamespace(namespace: string): boolean {
+  return namespace.split("/").length >= 2;
+}
diff --git a/client/src/features/projectsV2/api/namespace.api.ts b/client/src/features/projectsV2/api/namespace.api.ts
index 634f5f279b..3661a0dc21 100644
--- a/client/src/features/projectsV2/api/namespace.api.ts
+++ b/client/src/features/projectsV2/api/namespace.api.ts
@@ -4,7 +4,11 @@ const injectedRtkApi = api.injectEndpoints({
     getGroups: build.query<GetGroupsApiResponse, GetGroupsApiArg>({
       query: (queryArg) => ({
         url: `/groups`,
-        params: { params: queryArg.params },
+        params: {
+          page: queryArg.page,
+          per_page: queryArg.perPage,
+          direct_member: queryArg.directMember,
+        },
       }),
     }),
     postGroups: build.mutation<PostGroupsApiResponse, PostGroupsApiArg>({
@@ -75,7 +79,12 @@ const injectedRtkApi = api.injectEndpoints({
     getNamespaces: build.query<GetNamespacesApiResponse, GetNamespacesApiArg>({
       query: (queryArg) => ({
         url: `/namespaces`,
-        params: { params: queryArg.params },
+        params: {
+          page: queryArg.page,
+          per_page: queryArg.perPage,
+          minimum_role: queryArg.minimumRole,
+          kinds: queryArg.kinds,
+        },
       }),
     }),
     getNamespacesByNamespaceSlug: build.query<
@@ -91,8 +100,12 @@ export { injectedRtkApi as projectAndNamespaceApi };
 export type GetGroupsApiResponse =
   /** status 200 List of groups */ GroupResponseList;
 export type GetGroupsApiArg = {
-  /** query parameters */
-  params?: GroupsGetQuery;
+  /** the current page in paginated response */
+  page?: PaginationRequestPage;
+  /** the number of results per page in a paginated response */
+  perPage?: PaginationRequestPerPage;
+  /** A flag to filter for where the user is a direct member. */
+  directMember?: boolean;
 };
 export type PostGroupsApiResponse =
   /** status 201 The group was created */ GroupResponse;
@@ -141,8 +154,14 @@ export type GetGroupsByGroupSlugPermissionsApiArg = {
 export type GetNamespacesApiResponse =
   /** status 200 List of namespaces */ NamespaceResponseList;
 export type GetNamespacesApiArg = {
-  /** query parameters */
-  params?: NamespaceGetQuery;
+  /** the current page in paginated response */
+  page?: PaginationRequestPage;
+  /** the number of results per page in a paginated response */
+  perPage?: PaginationRequestPerPage;
+  /** The minimum role the user should have in the resources returned */
+  minimumRole?: GroupRole;
+  /** environment kinds query parameter */
+  kinds?: NamespaceGetQueryKind;
 };
 export type GetNamespacesByNamespaceSlugApiResponse =
   /** status 200 The namespace */ NamespaceResponse;
@@ -171,16 +190,8 @@ export type ErrorResponse = {
     message: string;
   };
 };
-export type PaginationRequest = {
-  /** Result's page number starting from 1 */
-  page?: number;
-  /** The number of results per page */
-  per_page?: number;
-};
-export type GroupsGetQuery = PaginationRequest & {
-  /** A flag to filter groups where the user is a direct member. */
-  direct_member?: boolean;
-};
+export type PaginationRequestPage = number;
+export type PaginationRequestPerPage = number;
 export type GroupPostRequest = {
   name: NamespaceName;
   slug: Slug;
@@ -215,7 +226,8 @@ export type GroupPermissions = {
   /** The user can manage group members */
   change_membership?: boolean;
 };
-export type NamespaceKind = "group" | "user";
+export type NamespaceKind = "group" | "user" | "project";
+export type OneOrTwoSlugs = string;
 export type NamespaceResponse = {
   id: Ulid;
   name?: NamespaceName;
@@ -223,12 +235,10 @@ export type NamespaceResponse = {
   creation_date?: CreationDate;
   created_by?: KeycloakId;
   namespace_kind: NamespaceKind;
+  path: OneOrTwoSlugs;
 };
 export type NamespaceResponseList = NamespaceResponse[];
-export type NamespaceGetQuery = PaginationRequest & {
-  /** A minimum role to filter results by. */
-  minimum_role?: GroupRole;
-};
+export type NamespaceGetQueryKind = NamespaceKind[];
 export const {
   useGetGroupsQuery,
   usePostGroupsMutation,
diff --git a/client/src/features/projectsV2/api/namespace.enhanced-api.ts b/client/src/features/projectsV2/api/namespace.enhanced-api.ts
deleted file mode 100644
index 5a248ab5f2..0000000000
--- a/client/src/features/projectsV2/api/namespace.enhanced-api.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import {
-  NamespaceKind as NamespaceKindOrig,
-  NamespaceResponse as NamespaceResponseOrig,
-} from "./namespace.api.ts";
-
-export type NamespaceKind = NamespaceKindOrig | "project";
-export interface NamespaceResponse
-  extends Omit<NamespaceResponseOrig, "namespace_kind"> {
-  namespace_kind: NamespaceKind;
-}
-export type NamespaceResponseList = NamespaceResponse[];
-export type GetNamespacesApiResponse = NamespaceResponseList;
diff --git a/client/src/features/projectsV2/api/namespace.openapi.json b/client/src/features/projectsV2/api/namespace.openapi.json
index cecd6212aa..4d566ef19d 100644
--- a/client/src/features/projectsV2/api/namespace.openapi.json
+++ b/client/src/features/projectsV2/api/namespace.openapi.json
@@ -19,14 +19,13 @@
         "summary": "Get all groups",
         "parameters": [
           {
-            "in": "query",
-            "description": "query parameters",
-            "name": "params",
-            "style": "form",
-            "explode": true,
-            "schema": {
-              "$ref": "#/components/schemas/GroupsGetQuery"
-            }
+            "$ref": "#/components/parameters/PaginationRequestPage"
+          },
+          {
+            "$ref": "#/components/parameters/PaginationRequestPerPage"
+          },
+          {
+            "$ref": "#/components/parameters/OnlyDirectMember"
           }
         ],
         "responses": {
@@ -397,14 +396,16 @@
         "summary": "Get all namespaces",
         "parameters": [
           {
-            "in": "query",
-            "description": "query parameters",
-            "name": "params",
-            "style": "form",
-            "explode": true,
-            "schema": {
-              "$ref": "#/components/schemas/NamespaceGetQuery"
-            }
+            "$ref": "#/components/parameters/PaginationRequestPage"
+          },
+          {
+            "$ref": "#/components/parameters/PaginationRequestPerPage"
+          },
+          {
+            "$ref": "#/components/parameters/MinimumRole"
+          },
+          {
+            "$ref": "#/components/parameters/NamespaceKind"
           }
         ],
         "responses": {
@@ -590,9 +591,17 @@
         "type": "string",
         "minLength": 1,
         "maxLength": 99,
-        "pattern": "^(?!.*\\.git$|.*\\.atom$|.*[\\-._][\\-._].*)[a-zA-Z0-9][a-zA-Z0-9\\-_.]*$",
+        "pattern": "^(?!.*\\.git$|.*\\.atom$|.*[\\-._][\\-._].*)[a-z0-9][a-z0-9\\-_.]*$",
         "example": "a-slug-example"
       },
+      "OneOrTwoSlugs": {
+        "description": "A command-line/url friendly name for a single slug or two slugs separated by /",
+        "type": "string",
+        "minLength": 1,
+        "maxLength": 200,
+        "pattern": "^(?!.*\\.git$|.*\\.atom$|.*[\\-._][\\-._].*)[a-z0-9][a-z0-9\\-_.]*(?<!\\.git)(?<!\\.atom)(?:/[a-z0-9][a-z0-9\\-_.]*){0,1}$",
+        "example": "user1/project-1"
+      },
       "CreationDate": {
         "description": "The date and time the resource was created (in UTC and ISO-8601 format)",
         "type": "string",
@@ -703,7 +712,7 @@
       "NamespaceKind": {
         "description": "Possible kinds of namespaces",
         "type": "string",
-        "enum": ["group", "user"]
+        "enum": ["group", "user", "project"]
       },
       "NamespaceResponseList": {
         "description": "A list of Renku namespaces",
@@ -733,16 +742,20 @@
           },
           "namespace_kind": {
             "$ref": "#/components/schemas/NamespaceKind"
+          },
+          "path": {
+            "$ref": "#/components/schemas/OneOrTwoSlugs"
           }
         },
-        "required": ["id", "namespace_kind", "slug"],
+        "required": ["id", "namespace_kind", "slug", "path"],
         "example": {
           "id": "01AN4Z79ZS5XN0F25N3DB94T4R",
           "name": "R-Project Group",
           "slug": "r-project",
           "created_by": "owner-keycloak-id",
           "creation_date": "2024-03-04T13:04:45Z",
-          "namespace_kind": "group"
+          "namespace_kind": "group",
+          "path": "r-project"
         }
       },
       "UserId": {
@@ -758,38 +771,14 @@
         "minLength": 1,
         "maxLength": 256
       },
-      "NamespaceGetQuery": {
-        "description": "Query params for namespace get request",
-        "allOf": [
-          {
-            "$ref": "#/components/schemas/PaginationRequest"
-          },
-          {
-            "properties": {
-              "minimum_role": {
-                "description": "A minimum role to filter results by.",
-                "$ref": "#/components/schemas/GroupRole"
-              }
-            }
-          }
-        ]
-      },
-      "GroupsGetQuery": {
-        "description": "Query params for namespace get request",
-        "allOf": [
-          {
-            "$ref": "#/components/schemas/PaginationRequest"
-          },
-          {
-            "properties": {
-              "direct_member": {
-                "description": "A flag to filter groups where the user is a direct member.",
-                "type": "boolean",
-                "default": false
-              }
-            }
-          }
-        ]
+      "NamespaceGetQueryKind": {
+        "type": "array",
+        "description": "Which namespace kinds to include in the response",
+        "items": {
+          "$ref": "#/components/schemas/NamespaceKind"
+        },
+        "default": ["user", "group"],
+        "minItems": 1
       },
       "GroupPermissions": {
         "description": "The set of permissions on a group",
@@ -809,24 +798,18 @@
           }
         }
       },
-      "PaginationRequest": {
-        "type": "object",
-        "additionalProperties": false,
-        "properties": {
-          "page": {
-            "description": "Result's page number starting from 1",
-            "type": "integer",
-            "minimum": 1,
-            "default": 1
-          },
-          "per_page": {
-            "description": "The number of results per page",
-            "type": "integer",
-            "minimum": 1,
-            "maximum": 100,
-            "default": 20
-          }
-        }
+      "PaginationRequestPage": {
+        "description": "Result's page number starting from 1",
+        "type": "integer",
+        "minimum": 1,
+        "default": 1
+      },
+      "PaginationRequestPerPage": {
+        "description": "The number of results per page",
+        "type": "integer",
+        "minimum": 1,
+        "maximum": 100,
+        "default": 20
       },
       "ErrorResponse": {
         "type": "object",
@@ -855,6 +838,59 @@
         "required": ["error"]
       }
     },
+    "parameters": {
+      "PaginationRequestPage": {
+        "in": "query",
+        "description": "the current page in paginated response",
+        "name": "page",
+        "style": "form",
+        "explode": true,
+        "schema": {
+          "$ref": "#/components/schemas/PaginationRequestPage"
+        }
+      },
+      "PaginationRequestPerPage": {
+        "in": "query",
+        "description": "the number of results per page in a paginated response",
+        "name": "per_page",
+        "style": "form",
+        "explode": true,
+        "schema": {
+          "$ref": "#/components/schemas/PaginationRequestPerPage"
+        }
+      },
+      "MinimumRole": {
+        "in": "query",
+        "description": "The minimum role the user should have in the resources returned",
+        "name": "minimum_role",
+        "style": "form",
+        "explode": true,
+        "schema": {
+          "$ref": "#/components/schemas/GroupRole"
+        }
+      },
+      "NamespaceKind": {
+        "in": "query",
+        "description": "environment kinds query parameter",
+        "name": "kinds",
+        "style": "form",
+        "explode": true,
+        "schema": {
+          "$ref": "#/components/schemas/NamespaceGetQueryKind"
+        }
+      },
+      "OnlyDirectMember": {
+        "in": "query",
+        "description": "A flag to filter for where the user is a direct member.",
+        "name": "direct_member",
+        "style": "form",
+        "explode": true,
+        "schema": {
+          "type": "boolean",
+          "default": false
+        }
+      }
+    },
     "responses": {
       "Error": {
         "description": "The schema for all 4xx and 5xx responses",
diff --git a/client/src/features/projectsV2/api/projectV2-empty.api.ts b/client/src/features/projectsV2/api/projectV2-empty.api.ts
index 6b8693da4e..fd40fed9e1 100644
--- a/client/src/features/projectsV2/api/projectV2-empty.api.ts
+++ b/client/src/features/projectsV2/api/projectV2-empty.api.ts
@@ -17,10 +17,18 @@
  */
 
 import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react";
+import queryString from "query-string";
 
 // initialize an empty api service that we'll inject endpoints into later as needed
 export const projectV2EmptyApi = createApi({
-  baseQuery: fetchBaseQuery({ baseUrl: "/ui-server/api/data" }),
+  baseQuery: fetchBaseQuery({
+    baseUrl: "/ui-server/api/data",
+    paramsSerializer: (params: Record<string, unknown>) =>
+      // NOTE: arrayFormat: none will serialize arrays by using duplicate keys
+      // like foo: [1, 2, 3] => 'foo=1&foo=2&foo=3' -> this is compatible
+      // with how the backend expects query parameters.
+      queryString.stringify(params, { arrayFormat: "none" }),
+  }),
   endpoints: () => ({}),
   reducerPath: "projectV2Api",
 });
diff --git a/client/src/features/projectsV2/api/projectV2.enhanced-api.ts b/client/src/features/projectsV2/api/projectV2.enhanced-api.ts
index 4b385f0b85..d3dae9597a 100644
--- a/client/src/features/projectsV2/api/projectV2.enhanced-api.ts
+++ b/client/src/features/projectsV2/api/projectV2.enhanced-api.ts
@@ -18,11 +18,9 @@ import type {
   GetGroupsApiResponse as GetGroupsApiResponseOrig,
   GetNamespacesApiArg,
   GroupResponseList,
-} from "./namespace.api";
-import type {
   GetNamespacesApiResponse as GetNamespacesApiResponseOrig,
   NamespaceResponseList,
-} from "./namespace.enhanced-api";
+} from "./namespace.api";
 
 export interface GetGroupsApiResponse extends AbstractKgPaginatedResponse {
   groups: GetGroupsApiResponseOrig;
@@ -54,16 +52,20 @@ export interface GetProjectsApiResponse extends AbstractKgPaginatedResponse {
 const injectedApi = api.injectEndpoints({
   endpoints: (builder) => ({
     getGroupsPaged: builder.query<GetGroupsApiResponse, GetGroupsApiArg>({
-      query: ({ params }) => ({
+      query: (params) => ({
         url: "/groups",
-        params,
+        params: {
+          page: params.page,
+          per_page: params.perPage,
+          direct_member: params.directMember,
+        },
       }),
-      transformResponse: (response, meta, { params }) => {
+      transformResponse: (response, meta, params) => {
         const groups = response as GroupResponseList;
         const headers = meta?.response?.headers;
         const headerResponse = processPaginationHeaders(
           headers,
-          { page: params?.page, perPage: params?.per_page },
+          { page: params?.page, perPage: params?.perPage },
           groups
         );
 
@@ -80,16 +82,21 @@ const injectedApi = api.injectEndpoints({
       GetNamespacesApiResponse,
       GetNamespacesApiArg
     >({
-      query: ({ params }) => ({
+      query: (params) => ({
         url: "/namespaces",
-        params,
+        params: {
+          page: params.page,
+          per_page: params.perPage,
+          minimum_role: params.minimumRole,
+          kinds: params.kinds,
+        },
       }),
-      transformResponse: (response, meta, { params }) => {
+      transformResponse: (response, meta, params) => {
         const namespaces = response as NamespaceResponseList;
         const headers = meta?.response?.headers;
         const headerResponse = processPaginationHeaders(
           headers,
-          { page: params?.page, perPage: params?.per_page },
+          { page: params?.page, perPage: params?.perPage },
           namespaces
         );
 
diff --git a/client/src/features/projectsV2/fields/ProjectNamespaceFormField.tsx b/client/src/features/projectsV2/fields/ProjectNamespaceFormField.tsx
index 21fdce654d..2f94b36fe2 100644
--- a/client/src/features/projectsV2/fields/ProjectNamespaceFormField.tsx
+++ b/client/src/features/projectsV2/fields/ProjectNamespaceFormField.tsx
@@ -38,8 +38,6 @@ import { ErrorAlert } from "../../../components/Alert";
 import { Loader } from "../../../components/Loader";
 import type { PaginatedState } from "../../session/components/options/fetchMore.types";
 import type { GetNamespacesApiResponse } from "../api/projectV2.enhanced-api";
-import type { Project } from "../api/projectV2.api";
-import type { NamespaceResponse } from "../api/namespace.enhanced-api";
 import {
   useGetNamespacesByNamespaceSlugQuery,
   useGetNamespacesQuery,
@@ -150,7 +148,7 @@ function NamespaceSelector({
   onFetchMore,
 }: NamespaceSelectorProps) {
   const currentValue = useMemo(
-    () => namespaces.find(({ slug }) => slug === currentNamespace),
+    () => namespaces.find(({ path }) => path === currentNamespace),
     [namespaces, currentNamespace]
   );
 
@@ -168,7 +166,7 @@ function NamespaceSelector({
       options={namespaces}
       value={currentValue}
       unstyled
-      getOptionValue={(option) => option.id}
+      getOptionValue={(option) => option.path}
       getOptionLabel={(option) => option.slug}
       onChange={onChange}
       classNames={selectClassNames}
@@ -250,7 +248,8 @@ export default function ProjectNamespaceFormField<T extends FieldValues>({
               ensureNamespace={ensureNamespace}
               id={`${entityName}-namespace`}
               inputId={`${entityName}-namespace-input`}
-              onChange={(newValue) => field.onChange(newValue?.slug)}
+              onChange={(newValue) => field.onChange(newValue?.path)}
+              includeProjectNamespaces={false}
             />
           );
         }}
@@ -258,7 +257,7 @@ export default function ProjectNamespaceFormField<T extends FieldValues>({
           required: true,
           maxLength: 99,
           pattern:
-            /^(?!.*\.git$|.*\.atom$|.*[-._][-._].*)[a-zA-Z0-9][a-zA-Z0-9\-_.]*$/,
+            /^(?!.*\.git$|.*\.atom$|.*[-._][-._].*)[a-z0-9][a-z0-9\-_.]*(?<!\.git)(?<!\.atom)(?:[a-z0-9][a-z0-9\-_.]*)*$/,
         }}
       />
       <div className="invalid-feedback">A project must belong to an owner.</div>
@@ -275,7 +274,7 @@ interface ProjectNamespaceControlProps {
   inputId: string;
   onChange: (newValue: SingleValue<ResponseNamespace>) => void;
   value?: string;
-  project?: Project;
+  includeProjectNamespaces: boolean;
 }
 
 export function ProjectNamespaceControl({
@@ -286,14 +285,19 @@ export function ProjectNamespaceControl({
   onChange,
   value,
   "data-cy": dataCy,
-  project,
+  includeProjectNamespaces = false,
 }: ProjectNamespaceControlProps) {
   const {
     data: namespacesFirstPage,
     isError,
     isFetching,
     requestId,
-  } = useGetNamespacesQuery({ params: { minimum_role: "editor" } });
+  } = useGetNamespacesQuery({
+    minimumRole: "editor",
+    kinds: includeProjectNamespaces
+      ? ["group", "user", "project"]
+      : ["group", "user"],
+  });
   const {
     data: specificNamespace,
     isError: specificNamespaceIsError,
@@ -316,17 +320,23 @@ export function ProjectNamespaceControl({
     useLazyGetNamespacesQuery();
   const onFetchMore = useCallback(() => {
     const request = fetchNamespacesPage({
-      params: {
-        page: fetchedPages + 1,
-        per_page: namespacesFirstPage?.perPage,
-        minimum_role: "editor",
-      },
+      page: fetchedPages + 1,
+      perPage: namespacesFirstPage?.perPage,
+      minimumRole: "editor",
+      kinds: includeProjectNamespaces
+        ? ["group", "user", "project"]
+        : ["group", "user"],
     });
     setState((prevState: PaginatedState<ResponseNamespace>) => ({
       ...prevState,
       currentRequestId: request.requestId,
     }));
-  }, [namespacesFirstPage?.perPage, fetchNamespacesPage, fetchedPages]);
+  }, [
+    namespacesFirstPage?.perPage,
+    fetchNamespacesPage,
+    fetchedPages,
+    includeProjectNamespaces,
+  ]);
 
   useEffect(() => {
     if (namespacesFirstPage == null) {
@@ -385,7 +395,7 @@ export function ProjectNamespaceControl({
       return;
     }
     const hasNamespace = allNamespaces.find(
-      ({ slug }) => slug === specificNamespace.slug
+      ({ path }) => path === specificNamespace.path
     );
     if (hasNamespace) {
       return;
@@ -396,23 +406,6 @@ export function ProjectNamespaceControl({
     });
   }, [allNamespaces, specificNamespace, specificNamespaceRequestId]);
 
-  const allNamespacesWithProject = useMemo(() => {
-    if (!project) {
-      return allNamespaces || [];
-    }
-    return [
-      {
-        id: project.id,
-        slug: `${project.namespace}/${project.slug}`,
-        creation_date: project.creation_date,
-        created_by: project.created_by,
-        namespace_kind: "project",
-        name: `${project.namespace}/${project.slug}`,
-      } as NamespaceResponse,
-      ...(allNamespaces || []),
-    ];
-  }, [allNamespaces, project]);
-
   if (isFetching) {
     return (
       <div className={cx(className, "form-control")} id={id}>
@@ -439,7 +432,7 @@ export function ProjectNamespaceControl({
         hasMore={hasMore}
         inputId={inputId}
         isFetchingMore={namespacesPageResult.isFetching}
-        namespaces={allNamespacesWithProject}
+        namespaces={allNamespaces}
         onChange={onChange}
         onFetchMore={onFetchMore}
       />
@@ -456,7 +449,7 @@ function OptionOrSingleValueContent({
 }: OptionOrSingleValueContentProps) {
   return (
     <>
-      <span className={cx(styles.slug)}>{namespace.slug}</span>
+      <span className={cx(styles.slug)}>{namespace.path}</span>
       <span className={cx("fst-italic", "text-body-secondary", styles.kind)}>
         ({namespace.namespace_kind})
       </span>
diff --git a/client/src/features/projectsV2/list/ProjectV2ListDisplay.tsx b/client/src/features/projectsV2/list/ProjectV2ListDisplay.tsx
index f9d205b634..e20189c6d7 100644
--- a/client/src/features/projectsV2/list/ProjectV2ListDisplay.tsx
+++ b/client/src/features/projectsV2/list/ProjectV2ListDisplay.tsx
@@ -28,7 +28,7 @@ import { RtkOrNotebooksError } from "../../../components/errors/RtkErrorAlert";
 import useGroupPermissions from "../../groupsV2/utils/useGroupPermissions.hook";
 import PermissionsGuard from "../../permissionsV2/PermissionsGuard";
 import { useGetUserQuery } from "../../usersV2/api/users.api";
-import { NamespaceKind } from "../api/namespace.enhanced-api";
+import { NamespaceKind } from "../api/namespace.api";
 import { useGetProjectsQuery } from "../api/projectV2.enhanced-api";
 import ProjectShortHandDisplay from "../show/ProjectShortHandDisplay";
 
diff --git a/tests/cypress/fixtures/groupV2/read-groupV2-namespace.json b/tests/cypress/fixtures/groupV2/read-groupV2-namespace.json
index 143dc84292..2d02be2040 100644
--- a/tests/cypress/fixtures/groupV2/read-groupV2-namespace.json
+++ b/tests/cypress/fixtures/groupV2/read-groupV2-namespace.json
@@ -2,6 +2,7 @@
   "id": "namespace-id",
   "name": "test 2 group-v2",
   "slug": "test-2-group-v2",
+  "path": "test-2-group-v2",
   "creation_date": "2023-11-15T09:55:59Z",
   "created_by": "user-id",
   "namespace_kind": "group"
diff --git a/tests/cypress/fixtures/groupV2/update-groupV2-namespace.json b/tests/cypress/fixtures/groupV2/update-groupV2-namespace.json
index 1362eeab63..d05829d082 100644
--- a/tests/cypress/fixtures/groupV2/update-groupV2-namespace.json
+++ b/tests/cypress/fixtures/groupV2/update-groupV2-namespace.json
@@ -2,6 +2,7 @@
   "id": "namespace-id",
   "name": "new name",
   "slug": "new-slug",
+  "path": "new-slug",
   "creation_date": "2023-11-15T09:55:59Z",
   "created_by": "user-id",
   "namespace_kind": "group"
diff --git a/tests/cypress/fixtures/namespaceV2/list-namespaceV2.json b/tests/cypress/fixtures/namespaceV2/list-namespaceV2.json
index 54448c60f8..5b2c5f41ba 100644
--- a/tests/cypress/fixtures/namespaceV2/list-namespaceV2.json
+++ b/tests/cypress/fixtures/namespaceV2/list-namespaceV2.json
@@ -2,12 +2,14 @@
   {
     "id": "AB0134CD43Z3JF9DX88B7XB405N0",
     "slug": "user1-uuid",
+    "path": "user1-uuid",
     "created_by": "user1-uuid",
     "namespace_kind": "user"
   },
   {
     "id": "AB0134CD43Z3JF9DX88B7XB405N0",
     "slug": "e2e",
+    "path": "e2e",
     "created_by": "user1-uuid",
     "namespace_kind": "user"
   },
@@ -15,6 +17,7 @@
     "id": "THEPROJECTULID26CHARACTERS",
     "name": "test 2 group-v2",
     "slug": "test-2-group-v2",
+    "path": "test-2-group-v2",
     "creation_date": "2023-11-15T09:55:59Z",
     "created_by": "user1-uuid",
     "namespace_kind": "group"
@@ -23,6 +26,7 @@
     "id": "01HF96BXZ3JF9DX88B7XB405S5",
     "name": "test 1 group-v2",
     "slug": "test-1-group-v2",
+    "path": "test-1-group-v2",
     "creation_date": "2023-11-15T09:52:59Z",
     "created_by": "user1-uuid",
     "namespace_kind": "group"
diff --git a/tests/cypress/fixtures/namespaceV2/namespaceV2-user1.json b/tests/cypress/fixtures/namespaceV2/namespaceV2-user1.json
index 9cbb7ab835..bba9ade237 100644
--- a/tests/cypress/fixtures/namespaceV2/namespaceV2-user1.json
+++ b/tests/cypress/fixtures/namespaceV2/namespaceV2-user1.json
@@ -1,6 +1,7 @@
 {
   "id": "AB0134CD43Z3JF9DX88B7XB405N0",
   "slug": "user1-uuid",
+  "path": "user1-uuid",
   "created_by": "user1-uuid",
   "namespace_kind": "user",
   "name": "user1"
diff --git a/tests/cypress/support/renkulab-fixtures/namespaceV2.ts b/tests/cypress/support/renkulab-fixtures/namespaceV2.ts
index 90a6087ada..ed7f8b2155 100644
--- a/tests/cypress/support/renkulab-fixtures/namespaceV2.ts
+++ b/tests/cypress/support/renkulab-fixtures/namespaceV2.ts
@@ -84,6 +84,7 @@ function generateNamespaces(numberOfNamespaces: number, start: number) {
       id,
       name: `test ${id} group-v2`,
       slug,
+      path: slug,
       creation_date: "2023-11-15T09:55:59Z",
       created_by: "user1-uuid",
       namespace_kind: "group",


Reviews:
user: login: lorenzo-cavazzi
body: Very nice! :clap: 

The feature work well and the code looks great! I dropped a few comments but it's mostly minor changes.

As discussed offline, the `Select` dropdown shows all elements with a grey background. There should be a function to match the currently selected element, and it probably returns `true` for every entry.

![image](https://github.com/user-attachments/assets/f27d1669-d77d-4fe4-beaa-35b81150e983)

state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-20T16:16:10Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T16:40:05Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T16:41:03Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T16:42:33Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:14:01Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:18:03Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:28:01Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:48:45Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:54:23Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:57:30Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:57:38Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:57:48Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:58:06Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:58:14Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:58:22Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:58:29Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:58:34Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T17:58:40Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T18:03:16Z
user: login: olevski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T18:07:32Z
user: login: lorenzo-cavazzi
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T08:22:57Z
user: login: lorenzo-cavazzi
body: Lgtm! :rocket: 
Very nice contribution to the UI :raised_hands: 

As discussed offline, I pushed a commit to improve consistency in spacing. It's a few changes in Bootstrap spacing classes https://github.com/SwissDataScienceCenter/renku-ui/pull/3528/commits/d82ce875177cbb6f49e9314f56aae8691b3d41e9
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-21T09:37:27Z

Review comments:
diff_hunk: @@ -392,10 +392,17 @@ function DataConnectorViewMetadata({
       <DataConnectorPropertyValue title="Owner">
         <div className={cx("d-flex", "align-items-center")}>
           <div className="me-1">
-            <UserAvatar namespace={dataConnector.namespace} />{" "}
+            {dataConnector.namespace.split("/").length >= 2 ? (
path: client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
user: login: lorenzo-cavazzi
body: `namespace.split("/").length >= 2` is used in a few places and it's not immediately obvious it's used to check for project-scoped data connectors.
I would make a utility function (something like `isProjectScopedDataConnector` to put in `dataConnector.utils.ts`) and use it here and in the other 2 places.
created_at: 2025-03-20T14:37:44Z
author_association: MEMBER
diff_hunk: @@ -392,10 +392,17 @@ function DataConnectorViewMetadata({
       <DataConnectorPropertyValue title="Owner">
         <div className={cx("d-flex", "align-items-center")}>
           <div className="me-1">
-            <UserAvatar namespace={dataConnector.namespace} />{" "}
+            {dataConnector.namespace.split("/").length >= 2 ? (
+              <Folder />
+            ) : (
+              <UserAvatar namespace={dataConnector.namespace} />
+            )}{" "}
           </div>
           {namespaceUrl == null ? (
-            <div className="me-1">@{dataConnector.namespace}</div>
+            <div className="me-1">
+              {dataConnector.namespace.split("/").length >= 2 ? "" : "@"}
path: client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
user: login: lorenzo-cavazzi
body: Use `isProjectScopedDataConnector` (see previous comments)
created_at: 2025-03-20T14:39:21Z
author_association: MEMBER
diff_hunk: @@ -102,7 +102,11 @@ export default function DataConnectorBoxListDisplay({
                 "align-items-center"
               )}
             >
-              <UserAvatar namespace={namespace} size="sm" />
+              {namespace.split("/").length >= 2 ? (
path: client/src/features/dataConnectorsV2/components/DataConnectorsBoxListDisplay.tsx
user: login: lorenzo-cavazzi
body: Use `isProjectScopedDataConnector` (see previous comments)
created_at: 2025-03-20T14:39:36Z
author_association: MEMBER
diff_hunk: @@ -56,22 +60,45 @@ export default function ProjectDataConnectorsBox({
       projectId: project.id,
     });
 
-  if (isLoading) return <DataConnectorLoadingBoxContent />;
+  const {
+    data: dataGetInaccessible,
+    error: errorGetInaccessible,
+    isLoading: isLoadingGetInaccessible,
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: lorenzo-cavazzi
body: I suggest we improve the naming. Ideally, someone who reads the code for the first time should immediately understand what's in there.

```suggestion
    data: inaccessibleDataConnectorsData,
    error: inaccessibleDataConnectorsError,
    isLoading: inaccessibleDataConnectorsIsLoading,
```

It would be great to rename also the variables already fetched by the other API. We usually keep the default `data, error, isLoading` as long as a component uses only one API.
created_at: 2025-03-20T14:50:37Z
author_association: MEMBER
diff_hunk: @@ -56,22 +60,45 @@ export default function ProjectDataConnectorsBox({
       projectId: project.id,
     });
 
-  if (isLoading) return <DataConnectorLoadingBoxContent />;
+  const {
+    data: dataGetInaccessible,
+    error: errorGetInaccessible,
+    isLoading: isLoadingGetInaccessible,
+  } = useGetProjectsByProjectIdInaccessibleDataConnectorLinksQuery({
+    projectId: project.id,
+  });
+
+  if (isLoading || isLoadingGetInaccessible)
+    return <DataConnectorLoadingBoxContent />;
 
   if (error || data == null) {
     return <RtkOrNotebooksError error={error} dismissible={false} />;
   }
 
-  return <ProjectDataConnectorBoxContent data={data} project={project} />;
+  if (errorGetInaccessible || dataGetInaccessible == null) {
+    return (
+      <RtkOrNotebooksError error={errorGetInaccessible} dismissible={false} />
+    );
+  }
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: lorenzo-cavazzi
body: No need to duplicate the error here; you could add the conditions to the previous block

```suggestion
  if (error || errorGetInaccessible) {
    return <RtkOrNotebooksError error={error || errorGetInaccessible} dismissible={false} />;
  }
```

Is `dataGetInaccessible == null` an error condition? If yes, the property shouldn't be optional in the `ProjectDataConnectorBoxContent` component; if not, it should be in an `if` showing an error.

I suggest moving out the cases `data == null` (and `dataGetInaccessible == null` if it's an error), returning an `ErrorAlert` with a message. Currently, it shows a `RtkOrNotebooksError` with no content since it passes `error` but that's empty. 


created_at: 2025-03-20T15:00:00Z
author_association: MEMBER
diff_hunk: @@ -56,22 +60,45 @@ export default function ProjectDataConnectorsBox({
       projectId: project.id,
     });
 
-  if (isLoading) return <DataConnectorLoadingBoxContent />;
+  const {
+    data: dataGetInaccessible,
+    error: errorGetInaccessible,
+    isLoading: isLoadingGetInaccessible,
+  } = useGetProjectsByProjectIdInaccessibleDataConnectorLinksQuery({
+    projectId: project.id,
+  });
+
+  if (isLoading || isLoadingGetInaccessible)
+    return <DataConnectorLoadingBoxContent />;
 
   if (error || data == null) {
     return <RtkOrNotebooksError error={error} dismissible={false} />;
   }
 
-  return <ProjectDataConnectorBoxContent data={data} project={project} />;
+  if (errorGetInaccessible || dataGetInaccessible == null) {
+    return (
+      <RtkOrNotebooksError error={errorGetInaccessible} dismissible={false} />
+    );
+  }
+
+  return (
+    <ProjectDataConnectorBoxContent
+      data={data}
+      project={project}
+      inaccessibleDataConnectors={dataGetInaccessible?.count}
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: lorenzo-cavazzi
body: If `dataGetInaccessible == null` is an error condition, no need for the `?` here.
Can we also improve naming to make it explicit we are passing only the count? Something like `inaccessibleDataConnectorsCount` or `inaccessibleDataConnectorsAmount`
created_at: 2025-03-20T15:01:49Z
author_association: MEMBER
diff_hunk: @@ -83,7 +110,7 @@ function ProjectDataConnectorBoxContent({
         <ProjectDataConnectorBoxHeader
           projectId={project.id}
           toggleOpen={toggleOpen}
-          totalConnectors={data.length}
+          totalConnectors={data.length + (inaccessibleDataConnectors || 0)}
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: lorenzo-cavazzi
body: Instead of adding `|| 0` here, it's best to add a default value in the function arguments as `inaccessibleDataConnectors = 0,`.
created_at: 2025-03-20T15:03:00Z
author_association: MEMBER
diff_hunk: @@ -92,6 +119,15 @@ function ProjectDataConnectorBoxContent({
               cloud storage to read and write custom data.
             </p>
           )}
+          {inaccessibleDataConnectors != null &&
+            inaccessibleDataConnectors > 0 && (
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: lorenzo-cavazzi
body: We can simplify the logic here too after the previous changes
```suggestion
          {inaccessibleDataConnectors > 0 && (
```
created_at: 2025-03-20T15:04:02Z
author_association: MEMBER
diff_hunk: @@ -208,6 +252,50 @@ function DataConnectorLinkDisplay({
     <DataConnectorBoxListDisplay
       dataConnector={dataConnector}
       dataConnectorLink={dataConnectorLink}
+      visibilityWarning={
+        projectPath != dataConnector.namespace &&
+        dataConnector.visibility == "private"
+          ? "Some members of this project may not be able to access this data connector"
+          : undefined
+      }
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: lorenzo-cavazzi
body: This should better be a boolean since you sometimes use it to decide whether to display something or not (i.e. as a boolean) and only once for its text; you could just put the text in that one place and simplify the logic here.
Perhaps rename it to something like `dataConnectorPotentiallyInaccessible` -- feel free to pick a better definition :smile: 

```suggestion
      dataConnectorPotentiallyInaccessible={
        projectPath != dataConnector.namespace &&
        dataConnector.visibility === "private"
      }
```
created_at: 2025-03-20T15:20:41Z
author_association: MEMBER
diff_hunk: @@ -82,12 +84,14 @@ interface DataConnectorViewProps {
   dataConnectorLink?: DataConnectorToProjectLink;
   showView: boolean;
   toggleView: () => void;
+  visibilityWarning?: string;
path: client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
user: login: lorenzo-cavazzi
body: No need to have it potentially undefined. If there is a reason to keep it `?`, then please set a default in the function arguments `visibilityWarning = false`

P.S. please adjust it in the children components too -- I won't mark it everywhere in this review since it drills down quite a few levels

```suggestion
  dataConnectorPotentiallyInaccessible: boolean;
```
created_at: 2025-03-20T15:22:39Z
author_association: MEMBER
diff_hunk: @@ -423,6 +443,24 @@ function DataConnectorViewMetadata({
           </div>
         </div>
       </DataConnectorPropertyValue>
+      <DataConnectorPropertyValue title="Visibility">
+        {dataConnector.visibility.toLowerCase() === "private" ? (
+          <div>
+            <Lock className={cx("bi", "me-1")} />
+            Private
+          </div>
+        ) : (
+          <div>
+            <Globe2 className={cx("bi", "me-1")} />
+            Public
+          </div>
+        )}
+        {visibilityWarning && (
+          <WarnAlert className="mt-2" timeout={0} dismissible={false}>
+            {visibilityWarning}
path: client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
user: login: lorenzo-cavazzi
body: According to the other modifications in the ancestor components, this should now be a boolean, so you should put the text instead

```suggestion
            Some members of this project may not be able to access this data connector
```
created_at: 2025-03-20T15:26:57Z
author_association: MEMBER
diff_hunk: @@ -423,6 +443,24 @@ function DataConnectorViewMetadata({
           </div>
         </div>
       </DataConnectorPropertyValue>
+      <DataConnectorPropertyValue title="Visibility">
+        {dataConnector.visibility.toLowerCase() === "private" ? (
path: client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
user: login: lorenzo-cavazzi
body: `dataConnector.visibility` is typed, there is no need to use `toLowerCase()`

```suggestion
        {dataConnector.visibility === "private" ? (
```
created_at: 2025-03-20T15:30:09Z
author_association: MEMBER
diff_hunk: @@ -423,6 +443,24 @@ function DataConnectorViewMetadata({
           </div>
         </div>
       </DataConnectorPropertyValue>
+      <DataConnectorPropertyValue title="Visibility">
+        {dataConnector.visibility.toLowerCase() === "private" ? (
+          <div>
+            <Lock className={cx("bi", "me-1")} />
+            Private
+          </div>
+        ) : (
+          <div>
+            <Globe2 className={cx("bi", "me-1")} />
+            Public
+          </div>
path: client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
user: login: lorenzo-cavazzi
body: Nitpick: `div`s not necessary here

```suggestion
          <>
            <Lock className={cx("bi", "me-1")} />
            Private
          </>
        ) : (
          <>
            <Globe2 className={cx("bi", "me-1")} />
            Public
          </>
```
created_at: 2025-03-20T15:31:47Z
author_association: MEMBER
diff_hunk: @@ -180,7 +174,15 @@ function ProjectDataConnectorBoxHeader({
             <Database className={cx("me-1", "bi")} />
             Data
           </h4>
-          <Badge>{totalConnectors}</Badge>
+          <Badge>{accessibleConnectors}</Badge>
+          {inaccessibleConnectors > 0 && (
+            <>
+              &nbsp;
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: lorenzo-cavazzi
body: ```suggestion
              {" "}
```
created_at: 2025-03-20T15:38:19Z
author_association: MEMBER
diff_hunk: @@ -208,6 +252,50 @@ function DataConnectorLinkDisplay({
     <DataConnectorBoxListDisplay
       dataConnector={dataConnector}
       dataConnectorLink={dataConnectorLink}
+      visibilityWarning={
+        projectPath != dataConnector.namespace &&
+        dataConnector.visibility == "private"
+          ? "Some members of this project may not be able to access this data connector"
+          : undefined
+      }
     />
   );
 }
+
+interface MissingDataConnectorsBadgeProps {
+  className?: string;
+  inaccessibleConnectors: number;
+}
+
+function MissingDataConnectorsBadge({
+  className,
+  inaccessibleConnectors,
+}: MissingDataConnectorsBadgeProps) {
+  const ref = useRef<HTMLDivElement>(null);
+
+  if (inaccessibleConnectors <= 0) return;
+  return (
+    <>
+      <Badge
+        className={cx("rounded-pill", className)}
+        color="info"
+        innerRef={ref}
+      >
+        {inaccessibleConnectors} hidden
+      </Badge>
+      <UncontrolledTooltip target={ref}>
+        {inaccessibleConnectors == 1 ? (
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: lorenzo-cavazzi
body: You should always prefer `===` to `==` in TypeScript/JavaScript. Weirdly, `==` tries to convert types before making the comparison, which lead to unexpected results especially when dealing with numbers, while `===` actually does what `==` does in most other languages. The only expectation is checking for `== null`

```suggestion
        {inaccessibleConnectors === 1 ? (
```
created_at: 2025-03-20T15:40:35Z
author_association: MEMBER
diff_hunk: @@ -208,6 +252,50 @@ function DataConnectorLinkDisplay({
     <DataConnectorBoxListDisplay
       dataConnector={dataConnector}
       dataConnectorLink={dataConnectorLink}
+      visibilityWarning={
+        projectPath != dataConnector.namespace &&
+        dataConnector.visibility == "private"
+          ? "Some members of this project may not be able to access this data connector"
+          : undefined
+      }
     />
   );
 }
+
+interface MissingDataConnectorsBadgeProps {
+  className?: string;
+  inaccessibleConnectors: number;
+}
+
+function MissingDataConnectorsBadge({
+  className,
+  inaccessibleConnectors,
+}: MissingDataConnectorsBadgeProps) {
+  const ref = useRef<HTMLDivElement>(null);
+
+  if (inaccessibleConnectors <= 0) return;
+  return (
+    <>
+      <Badge
+        className={cx("rounded-pill", className)}
+        color="info"
+        innerRef={ref}
+      >
+        {inaccessibleConnectors} hidden
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: lorenzo-cavazzi
body: ```suggestion
        +{inaccessibleConnectors} hidden
```

Maybe it's only, but it's not immediate to understand the numbers here: I suggest we add a `+` to make it obvious the hidden data connectors are additional to the total number shown.
Compare the following

![image](https://github.com/user-attachments/assets/d66dff00-7b9b-4378-9b79-8f4d39920f8f)

![image](https://github.com/user-attachments/assets/d5107647-8a25-4779-9573-b369218a62b7)
created_at: 2025-03-20T15:42:33Z
author_association: MEMBER
diff_hunk: @@ -208,6 +252,50 @@ function DataConnectorLinkDisplay({
     <DataConnectorBoxListDisplay
       dataConnector={dataConnector}
       dataConnectorLink={dataConnectorLink}
+      visibilityWarning={
+        projectPath != dataConnector.namespace &&
+        dataConnector.visibility == "private"
+          ? "Some members of this project may not be able to access this data connector"
+          : undefined
+      }
     />
   );
 }
+
+interface MissingDataConnectorsBadgeProps {
+  className?: string;
+  inaccessibleConnectors: number;
+}
+
+function MissingDataConnectorsBadge({
+  className,
+  inaccessibleConnectors,
+}: MissingDataConnectorsBadgeProps) {
+  const ref = useRef<HTMLDivElement>(null);
+
+  if (inaccessibleConnectors <= 0) return;
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: lorenzo-cavazzi
body: Nitpick.

```suggestion
  if (!inaccessibleConnectors) return null;
```
created_at: 2025-03-20T15:47:16Z
author_association: MEMBER
diff_hunk: @@ -208,6 +252,50 @@ function DataConnectorLinkDisplay({
     <DataConnectorBoxListDisplay
       dataConnector={dataConnector}
       dataConnectorLink={dataConnectorLink}
+      visibilityWarning={
+        projectPath != dataConnector.namespace &&
+        dataConnector.visibility == "private"
+          ? "Some members of this project may not be able to access this data connector"
+          : undefined
+      }
     />
   );
 }
+
+interface MissingDataConnectorsBadgeProps {
+  className?: string;
+  inaccessibleConnectors: number;
+}
+
+function MissingDataConnectorsBadge({
+  className,
+  inaccessibleConnectors,
+}: MissingDataConnectorsBadgeProps) {
+  const ref = useRef<HTMLDivElement>(null);
+
+  if (inaccessibleConnectors <= 0) return;
+  return (
+    <>
+      <Badge
+        className={cx("rounded-pill", className)}
+        color="info"
+        innerRef={ref}
+      >
+        {inaccessibleConnectors} hidden
+      </Badge>
+      <UncontrolledTooltip target={ref}>
+        {inaccessibleConnectors == 1 ? (
+          <>
+            There is 1 data connector linked to this project but not visible to
+            you.
+          </>
+        ) : (
+          <>
+            There are {{ inaccessibleConnectors }} data connectors linked to
+            this project but not visible to you.
+          </>
+        )}
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: lorenzo-cavazzi
body: This is an error `{{ inaccessibleConnectors }}`.
But... let's simplify!

Add on top `const plural = inaccessibleConnectors === 1;` and then

```suggestion
        There {plural ? "is" : "are"} {inaccessibleConnectors} data connector
        {plural ? "" : "s"} linked to this project but not visible to you.
```
created_at: 2025-03-20T15:49:24Z
author_association: MEMBER
diff_hunk: @@ -208,6 +252,50 @@ function DataConnectorLinkDisplay({
     <DataConnectorBoxListDisplay
       dataConnector={dataConnector}
       dataConnectorLink={dataConnectorLink}
+      visibilityWarning={
+        projectPath != dataConnector.namespace &&
+        dataConnector.visibility == "private"
+          ? "Some members of this project may not be able to access this data connector"
+          : undefined
+      }
     />
   );
 }
+
+interface MissingDataConnectorsBadgeProps {
+  className?: string;
+  inaccessibleConnectors: number;
+}
+
+function MissingDataConnectorsBadge({
+  className,
+  inaccessibleConnectors,
+}: MissingDataConnectorsBadgeProps) {
+  const ref = useRef<HTMLDivElement>(null);
+
+  if (inaccessibleConnectors <= 0) return;
+  return (
+    <>
+      <Badge
+        className={cx("rounded-pill", className)}
+        color="info"
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: lorenzo-cavazzi
body: We don't really use `info` for this kind of information but rather.

```suggestion
        color="primary"
```
created_at: 2025-03-20T15:51:03Z
author_association: MEMBER
diff_hunk: @@ -154,7 +171,40 @@ export default function DataConnectorBoxListDisplay({
         dataConnectorLink={dataConnectorLink}
         showView={showDetails}
         toggleView={toggleDetails}
+        visibilityWarning={visibilityWarning}
       />
     </>
   );
 }
+
+interface DataConnectorNotVisibleToAllUsersBadgeProps {
+  className?: string;
+  warning?: string;
+}
+
+export function DataConnectorNotVisibleToAllUsersBadge({
+  className,
+  warning,
+}: DataConnectorNotVisibleToAllUsersBadgeProps) {
+  const ref = useRef<HTMLDivElement>(null);
+
+  return (
+    <>
+      <Badge
+        className={cx(
+          "rounded-pill border bg-warning-subtle border-warning text-warning-emphasis",
+          className
+        )}
+        color="info"
path: client/src/features/dataConnectorsV2/components/DataConnectorsBoxListDisplay.tsx
user: login: lorenzo-cavazzi
body: See previous comment

```suggestion
        color="primary"
```
created_at: 2025-03-20T15:51:18Z
author_association: MEMBER
diff_hunk: @@ -154,7 +171,40 @@ export default function DataConnectorBoxListDisplay({
         dataConnectorLink={dataConnectorLink}
         showView={showDetails}
         toggleView={toggleDetails}
+        visibilityWarning={visibilityWarning}
       />
     </>
   );
 }
+
+interface DataConnectorNotVisibleToAllUsersBadgeProps {
+  className?: string;
+  warning?: string;
+}
+
+export function DataConnectorNotVisibleToAllUsersBadge({
+  className,
+  warning,
+}: DataConnectorNotVisibleToAllUsersBadgeProps) {
+  const ref = useRef<HTMLDivElement>(null);
+
+  return (
+    <>
+      <Badge
+        className={cx(
+          "rounded-pill border bg-warning-subtle border-warning text-warning-emphasis",
path: client/src/features/dataConnectorsV2/components/DataConnectorsBoxListDisplay.tsx
user: login: lorenzo-cavazzi
body: `cx` requires all the classes to be separated

```suggestion
          "rounded-pill", "border", "bg-warning-subtle", "border-warning", "text-warning-emphasis",
```
created_at: 2025-03-20T15:58:24Z
author_association: MEMBER
diff_hunk: @@ -146,6 +158,11 @@ export default function DataConnectorBoxListDisplay({
                 enableTooltip
               />
             </div>
+            {visibilityWarning && (
+              <div>
+                <DataConnectorNotVisibleToAllUsersBadge />
+              </div>
+            )}
path: client/src/features/dataConnectorsV2/components/DataConnectorsBoxListDisplay.tsx
user: login: lorenzo-cavazzi
body: NitpicK: this works fine, but I wonder if it's best position next to the visibility. Something like this

![image](https://github.com/user-attachments/assets/58154d1e-5003-4b2e-bf9d-98d701f88601)

created_at: 2025-03-20T16:02:55Z
author_association: MEMBER
diff_hunk: @@ -56,22 +60,45 @@ export default function ProjectDataConnectorsBox({
       projectId: project.id,
     });
 
-  if (isLoading) return <DataConnectorLoadingBoxContent />;
+  const {
+    data: dataGetInaccessible,
+    error: errorGetInaccessible,
+    isLoading: isLoadingGetInaccessible,
+  } = useGetProjectsByProjectIdInaccessibleDataConnectorLinksQuery({
+    projectId: project.id,
+  });
+
+  if (isLoading || isLoadingGetInaccessible)
+    return <DataConnectorLoadingBoxContent />;
 
   if (error || data == null) {
     return <RtkOrNotebooksError error={error} dismissible={false} />;
   }
 
-  return <ProjectDataConnectorBoxContent data={data} project={project} />;
+  if (errorGetInaccessible || dataGetInaccessible == null) {
+    return (
+      <RtkOrNotebooksError error={errorGetInaccessible} dismissible={false} />
+    );
+  }
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: olevski
body: Ok here I followed the pattern that was already there. I.e.:

```
 if (error || data == null) {
    return <RtkOrNotebooksError error={error} dismissible={false} />;
  }
```

and this is like that on main.
created_at: 2025-03-20T16:40:05Z
author_association: MEMBER
diff_hunk: @@ -56,22 +60,45 @@ export default function ProjectDataConnectorsBox({
       projectId: project.id,
     });
 
-  if (isLoading) return <DataConnectorLoadingBoxContent />;
+  const {
+    data: dataGetInaccessible,
+    error: errorGetInaccessible,
+    isLoading: isLoadingGetInaccessible,
+  } = useGetProjectsByProjectIdInaccessibleDataConnectorLinksQuery({
+    projectId: project.id,
+  });
+
+  if (isLoading || isLoadingGetInaccessible)
+    return <DataConnectorLoadingBoxContent />;
 
   if (error || data == null) {
     return <RtkOrNotebooksError error={error} dismissible={false} />;
   }
 
-  return <ProjectDataConnectorBoxContent data={data} project={project} />;
+  if (errorGetInaccessible || dataGetInaccessible == null) {
+    return (
+      <RtkOrNotebooksError error={errorGetInaccessible} dismissible={false} />
+    );
+  }
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: olevski
body: But I will make the changes you suggest.
created_at: 2025-03-20T16:41:03Z
author_association: MEMBER
diff_hunk: @@ -56,22 +60,45 @@ export default function ProjectDataConnectorsBox({
       projectId: project.id,
     });
 
-  if (isLoading) return <DataConnectorLoadingBoxContent />;
+  const {
+    data: dataGetInaccessible,
+    error: errorGetInaccessible,
+    isLoading: isLoadingGetInaccessible,
+  } = useGetProjectsByProjectIdInaccessibleDataConnectorLinksQuery({
+    projectId: project.id,
+  });
+
+  if (isLoading || isLoadingGetInaccessible)
+    return <DataConnectorLoadingBoxContent />;
 
   if (error || data == null) {
     return <RtkOrNotebooksError error={error} dismissible={false} />;
   }
 
-  return <ProjectDataConnectorBoxContent data={data} project={project} />;
+  if (errorGetInaccessible || dataGetInaccessible == null) {
+    return (
+      <RtkOrNotebooksError error={errorGetInaccessible} dismissible={false} />
+    );
+  }
+
+  return (
+    <ProjectDataConnectorBoxContent
+      data={data}
+      project={project}
+      inaccessibleDataConnectors={dataGetInaccessible?.count}
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: olevski
body: I dont think we should error out if that is null. We can just assume that if we cannot get the number of inaccessible data connectors then it is `0`. I will make that change. Because the only thing that does is make that blue pill component.
created_at: 2025-03-20T16:42:32Z
author_association: MEMBER
diff_hunk: @@ -56,22 +60,45 @@ export default function ProjectDataConnectorsBox({
       projectId: project.id,
     });
 
-  if (isLoading) return <DataConnectorLoadingBoxContent />;
+  const {
+    data: dataGetInaccessible,
+    error: errorGetInaccessible,
+    isLoading: isLoadingGetInaccessible,
+  } = useGetProjectsByProjectIdInaccessibleDataConnectorLinksQuery({
+    projectId: project.id,
+  });
+
+  if (isLoading || isLoadingGetInaccessible)
+    return <DataConnectorLoadingBoxContent />;
 
   if (error || data == null) {
     return <RtkOrNotebooksError error={error} dismissible={false} />;
   }
 
-  return <ProjectDataConnectorBoxContent data={data} project={project} />;
+  if (errorGetInaccessible || dataGetInaccessible == null) {
+    return (
+      <RtkOrNotebooksError error={errorGetInaccessible} dismissible={false} />
+    );
+  }
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: olevski
body: > Is dataGetInaccessible == null an error condition? If yes, the property shouldn't be optional in the ProjectDataConnectorBoxContent component; if not, it should be in an if showing an error.

I am not sure I follow the second part of this after `if not, it should be in an if showing an error`. This is not an error condition.

So I just made it not null and then replace the value with zero if undefined.
created_at: 2025-03-20T17:14:01Z
author_association: MEMBER
diff_hunk: @@ -83,7 +110,7 @@ function ProjectDataConnectorBoxContent({
         <ProjectDataConnectorBoxHeader
           projectId={project.id}
           toggleOpen={toggleOpen}
-          totalConnectors={data.length}
+          totalConnectors={data.length + (inaccessibleDataConnectors || 0)}
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: olevski
body: This is now different. It is not `totalConnectors` anymore. It just has `accessibleDataConnectorsCount` and `inaccessibleDataConnectorsCount`.
created_at: 2025-03-20T17:18:03Z
author_association: MEMBER
diff_hunk: @@ -208,6 +252,50 @@ function DataConnectorLinkDisplay({
     <DataConnectorBoxListDisplay
       dataConnector={dataConnector}
       dataConnectorLink={dataConnectorLink}
+      visibilityWarning={
+        projectPath != dataConnector.namespace &&
+        dataConnector.visibility == "private"
+          ? "Some members of this project may not be able to access this data connector"
+          : undefined
+      }
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: olevski
body: The text is shown in 2 different places. I will try to pull it out into a constant I guess. I have to figure out where to put the constant though :thinking: .
created_at: 2025-03-20T17:28:01Z
author_association: MEMBER
diff_hunk: @@ -82,12 +84,14 @@ interface DataConnectorViewProps {
   dataConnectorLink?: DataConnectorToProjectLink;
   showView: boolean;
   toggleView: () => void;
+  visibilityWarning?: string;
path: client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
user: login: olevski
body: I kept the `?` and set a default. When I removed the `?` it was needed in a bunch of other places I never touched. So it is better to keep the `?` and add a default.

Please like this were erroring out when I remove the `?`:
```
        <DataConnectorViewHeader
          {...{ dataConnector, dataConnectorLink, toggleView }}
        />

```
created_at: 2025-03-20T17:48:45Z
author_association: MEMBER
diff_hunk: @@ -208,6 +252,50 @@ function DataConnectorLinkDisplay({
     <DataConnectorBoxListDisplay
       dataConnector={dataConnector}
       dataConnectorLink={dataConnectorLink}
+      visibilityWarning={
+        projectPath != dataConnector.namespace &&
+        dataConnector.visibility == "private"
+          ? "Some members of this project may not be able to access this data connector"
+          : undefined
+      }
     />
   );
 }
+
+interface MissingDataConnectorsBadgeProps {
+  className?: string;
+  inaccessibleConnectors: number;
+}
+
+function MissingDataConnectorsBadge({
+  className,
+  inaccessibleConnectors,
+}: MissingDataConnectorsBadgeProps) {
+  const ref = useRef<HTMLDivElement>(null);
+
+  if (inaccessibleConnectors <= 0) return;
+  return (
+    <>
+      <Badge
+        className={cx("rounded-pill", className)}
+        color="info"
+        innerRef={ref}
+      >
+        {inaccessibleConnectors} hidden
+      </Badge>
+      <UncontrolledTooltip target={ref}>
+        {inaccessibleConnectors == 1 ? (
+          <>
+            There is 1 data connector linked to this project but not visible to
+            you.
+          </>
+        ) : (
+          <>
+            There are {{ inaccessibleConnectors }} data connectors linked to
+            this project but not visible to you.
+          </>
+        )}
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: olevski
body: I think your logic is flipped in your suggestion. But I know what you mean. I added `singular = inaccessibleConnectors === 1` and then used that as you suggested.
created_at: 2025-03-20T17:54:23Z
author_association: MEMBER
diff_hunk: @@ -392,10 +392,17 @@ function DataConnectorViewMetadata({
       <DataConnectorPropertyValue title="Owner">
         <div className={cx("d-flex", "align-items-center")}>
           <div className="me-1">
-            <UserAvatar namespace={dataConnector.namespace} />{" "}
+            {dataConnector.namespace.split("/").length >= 2 ? (
path: client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
user: login: olevski
body: Addressed in https://github.com/SwissDataScienceCenter/renku-ui/pull/3528/commits/0b44edb4444219335fba2acb78b890fd1c8dfbbe
created_at: 2025-03-20T17:57:30Z
author_association: MEMBER
diff_hunk: @@ -102,7 +102,11 @@ export default function DataConnectorBoxListDisplay({
                 "align-items-center"
               )}
             >
-              <UserAvatar namespace={namespace} size="sm" />
+              {namespace.split("/").length >= 2 ? (
path: client/src/features/dataConnectorsV2/components/DataConnectorsBoxListDisplay.tsx
user: login: olevski
body: https://github.com/SwissDataScienceCenter/renku-ui/pull/3528/commits/0b44edb4444219335fba2acb78b890fd1c8dfbbe
created_at: 2025-03-20T17:57:38Z
author_association: MEMBER
diff_hunk: @@ -56,22 +60,45 @@ export default function ProjectDataConnectorsBox({
       projectId: project.id,
     });
 
-  if (isLoading) return <DataConnectorLoadingBoxContent />;
+  const {
+    data: dataGetInaccessible,
+    error: errorGetInaccessible,
+    isLoading: isLoadingGetInaccessible,
+  } = useGetProjectsByProjectIdInaccessibleDataConnectorLinksQuery({
+    projectId: project.id,
+  });
+
+  if (isLoading || isLoadingGetInaccessible)
+    return <DataConnectorLoadingBoxContent />;
 
   if (error || data == null) {
     return <RtkOrNotebooksError error={error} dismissible={false} />;
   }
 
-  return <ProjectDataConnectorBoxContent data={data} project={project} />;
+  if (errorGetInaccessible || dataGetInaccessible == null) {
+    return (
+      <RtkOrNotebooksError error={errorGetInaccessible} dismissible={false} />
+    );
+  }
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: olevski
body: Addressed in https://github.com/SwissDataScienceCenter/renku-ui/pull/3528/commits/0b44edb4444219335fba2acb78b890fd1c8dfbbe
created_at: 2025-03-20T17:57:48Z
author_association: MEMBER
diff_hunk: @@ -56,22 +60,45 @@ export default function ProjectDataConnectorsBox({
       projectId: project.id,
     });
 
-  if (isLoading) return <DataConnectorLoadingBoxContent />;
+  const {
+    data: dataGetInaccessible,
+    error: errorGetInaccessible,
+    isLoading: isLoadingGetInaccessible,
+  } = useGetProjectsByProjectIdInaccessibleDataConnectorLinksQuery({
+    projectId: project.id,
+  });
+
+  if (isLoading || isLoadingGetInaccessible)
+    return <DataConnectorLoadingBoxContent />;
 
   if (error || data == null) {
     return <RtkOrNotebooksError error={error} dismissible={false} />;
   }
 
-  return <ProjectDataConnectorBoxContent data={data} project={project} />;
+  if (errorGetInaccessible || dataGetInaccessible == null) {
+    return (
+      <RtkOrNotebooksError error={errorGetInaccessible} dismissible={false} />
+    );
+  }
+
+  return (
+    <ProjectDataConnectorBoxContent
+      data={data}
+      project={project}
+      inaccessibleDataConnectors={dataGetInaccessible?.count}
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: olevski
body: Addressed in https://github.com/SwissDataScienceCenter/renku-ui/pull/3528/commits/0b44edb4444219335fba2acb78b890fd1c8dfbbe
created_at: 2025-03-20T17:58:06Z
author_association: MEMBER
diff_hunk: @@ -208,6 +252,50 @@ function DataConnectorLinkDisplay({
     <DataConnectorBoxListDisplay
       dataConnector={dataConnector}
       dataConnectorLink={dataConnectorLink}
+      visibilityWarning={
+        projectPath != dataConnector.namespace &&
+        dataConnector.visibility == "private"
+          ? "Some members of this project may not be able to access this data connector"
+          : undefined
+      }
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: olevski
body: Addressed in https://github.com/SwissDataScienceCenter/renku-ui/pull/3528/commits/0b44edb4444219335fba2acb78b890fd1c8dfbbe
created_at: 2025-03-20T17:58:14Z
author_association: MEMBER
diff_hunk: @@ -82,12 +84,14 @@ interface DataConnectorViewProps {
   dataConnectorLink?: DataConnectorToProjectLink;
   showView: boolean;
   toggleView: () => void;
+  visibilityWarning?: string;
path: client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
user: login: olevski
body: Addressed in https://github.com/SwissDataScienceCenter/renku-ui/pull/3528/commits/0b44edb4444219335fba2acb78b890fd1c8dfbbe
created_at: 2025-03-20T17:58:22Z
author_association: MEMBER
diff_hunk: @@ -423,6 +443,24 @@ function DataConnectorViewMetadata({
           </div>
         </div>
       </DataConnectorPropertyValue>
+      <DataConnectorPropertyValue title="Visibility">
+        {dataConnector.visibility.toLowerCase() === "private" ? (
+          <div>
+            <Lock className={cx("bi", "me-1")} />
+            Private
+          </div>
+        ) : (
+          <div>
+            <Globe2 className={cx("bi", "me-1")} />
+            Public
+          </div>
+        )}
+        {visibilityWarning && (
+          <WarnAlert className="mt-2" timeout={0} dismissible={false}>
+            {visibilityWarning}
path: client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
user: login: olevski
body: Addressed in https://github.com/SwissDataScienceCenter/renku-ui/pull/3528/commits/0b44edb4444219335fba2acb78b890fd1c8dfbbe
created_at: 2025-03-20T17:58:29Z
author_association: MEMBER
diff_hunk: @@ -208,6 +252,50 @@ function DataConnectorLinkDisplay({
     <DataConnectorBoxListDisplay
       dataConnector={dataConnector}
       dataConnectorLink={dataConnectorLink}
+      visibilityWarning={
+        projectPath != dataConnector.namespace &&
+        dataConnector.visibility == "private"
+          ? "Some members of this project may not be able to access this data connector"
+          : undefined
+      }
     />
   );
 }
+
+interface MissingDataConnectorsBadgeProps {
+  className?: string;
+  inaccessibleConnectors: number;
+}
+
+function MissingDataConnectorsBadge({
+  className,
+  inaccessibleConnectors,
+}: MissingDataConnectorsBadgeProps) {
+  const ref = useRef<HTMLDivElement>(null);
+
+  if (inaccessibleConnectors <= 0) return;
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: olevski
body: Addressed in https://github.com/SwissDataScienceCenter/renku-ui/pull/3528/commits/0b44edb4444219335fba2acb78b890fd1c8dfbbe
created_at: 2025-03-20T17:58:34Z
author_association: MEMBER
diff_hunk: @@ -208,6 +252,50 @@ function DataConnectorLinkDisplay({
     <DataConnectorBoxListDisplay
       dataConnector={dataConnector}
       dataConnectorLink={dataConnectorLink}
+      visibilityWarning={
+        projectPath != dataConnector.namespace &&
+        dataConnector.visibility == "private"
+          ? "Some members of this project may not be able to access this data connector"
+          : undefined
+      }
     />
   );
 }
+
+interface MissingDataConnectorsBadgeProps {
+  className?: string;
+  inaccessibleConnectors: number;
+}
+
+function MissingDataConnectorsBadge({
+  className,
+  inaccessibleConnectors,
+}: MissingDataConnectorsBadgeProps) {
+  const ref = useRef<HTMLDivElement>(null);
+
+  if (inaccessibleConnectors <= 0) return;
+  return (
+    <>
+      <Badge
+        className={cx("rounded-pill", className)}
+        color="info"
+        innerRef={ref}
+      >
+        {inaccessibleConnectors} hidden
+      </Badge>
+      <UncontrolledTooltip target={ref}>
+        {inaccessibleConnectors == 1 ? (
+          <>
+            There is 1 data connector linked to this project but not visible to
+            you.
+          </>
+        ) : (
+          <>
+            There are {{ inaccessibleConnectors }} data connectors linked to
+            this project but not visible to you.
+          </>
+        )}
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: olevski
body: Addressed in https://github.com/SwissDataScienceCenter/renku-ui/pull/3528/commits/0b44edb4444219335fba2acb78b890fd1c8dfbbe
created_at: 2025-03-20T17:58:40Z
author_association: MEMBER
diff_hunk: @@ -146,6 +158,11 @@ export default function DataConnectorBoxListDisplay({
                 enableTooltip
               />
             </div>
+            {visibilityWarning && (
+              <div>
+                <DataConnectorNotVisibleToAllUsersBadge />
+              </div>
+            )}
path: client/src/features/dataConnectorsV2/components/DataConnectorsBoxListDisplay.tsx
user: login: olevski
body: I think @lokijuhy wanted it below from the notes she made on notion.
https://www.notion.so/renku/Project-Disentanglement-Build-Team-17e0df2efafc80fcbaa6f0b363a574f0?pvs=4#19e0df2efafc80e6ae19ce47cef02770

But yeah I dont really care I will move it back up.
created_at: 2025-03-20T18:03:16Z
author_association: MEMBER
diff_hunk: @@ -146,6 +158,11 @@ export default function DataConnectorBoxListDisplay({
                 enableTooltip
               />
             </div>
+            {visibilityWarning && (
+              <div>
+                <DataConnectorNotVisibleToAllUsersBadge />
+              </div>
+            )}
path: client/src/features/dataConnectorsV2/components/DataConnectorsBoxListDisplay.tsx
user: login: olevski
body: Addressed in https://github.com/SwissDataScienceCenter/renku-ui/pull/3528/commits/c2f19b2e826a919dab4b6151a17d6200f849df67
created_at: 2025-03-20T18:07:32Z
author_association: MEMBER
diff_hunk: @@ -56,22 +60,45 @@ export default function ProjectDataConnectorsBox({
       projectId: project.id,
     });
 
-  if (isLoading) return <DataConnectorLoadingBoxContent />;
+  const {
+    data: dataGetInaccessible,
+    error: errorGetInaccessible,
+    isLoading: isLoadingGetInaccessible,
+  } = useGetProjectsByProjectIdInaccessibleDataConnectorLinksQuery({
+    projectId: project.id,
+  });
+
+  if (isLoading || isLoadingGetInaccessible)
+    return <DataConnectorLoadingBoxContent />;
 
   if (error || data == null) {
     return <RtkOrNotebooksError error={error} dismissible={false} />;
   }
 
-  return <ProjectDataConnectorBoxContent data={data} project={project} />;
+  if (errorGetInaccessible || dataGetInaccessible == null) {
+    return (
+      <RtkOrNotebooksError error={errorGetInaccessible} dismissible={false} />
+    );
+  }
path: client/src/features/ProjectPageV2/ProjectPageContent/DataConnectors/ProjectDataConnectorsBox.tsx
user: login: lorenzo-cavazzi
body: You code looks great now :+1: 

About the comment: your choice made sense, but the previous pattern ` if (error || data == null)` wasn't good since `data == null` wouldn't show any error. Not your fault ofc, but it's a good moment to improve it.
The comment about `dataGetInaccessible == null` was a little confusing, apologies. What I mean is that if an API failure should block showing the children component, then you can add that case to the `if (data == null)` making it `if (data == null || inaccessibleDataConnectorsData)` and below just use `inaccessibleDataConnectorsData.count` instead of `inaccessibleDataConnectorsData?.count || 0` since you know `inaccessibleDataConnectorsData` will never be null there. The alternative is what you just did, so allowing `inaccessibleDataConnectorsData` to be null and passing `|| 0` to the children component.
created_at: 2025-03-21T08:22:56Z
author_association: MEMBER

PR comments:
user: login: olevski
created_at: 2025-02-11T15:35:56Z
body: This change is part of the following stack:

- #3525
    - #3528 ◀

<sub>Change managed by [git-spice](https://abhinav.github.io/git-spice/).</sub>
<!-- gs:navigation comment -->

author_association: MEMBER
user: login: RenkuBot
created_at: 2025-03-18T16:47:18Z
body: You can access the deployment of this PR at https://renku-ci-ui-3528.dev.renku.ch
author_association: CONTRIBUTOR
user: login: olevski
created_at: 2025-03-20T16:13:12Z
body: FYI the first 3 commits of this PR have already been reviewed in #3525 
author_association: MEMBER
user: login: olevski
created_at: 2025-03-20T16:34:26Z
body: Here is the followup issue for the namespace selection when creating a data connector:
https://github.com/SwissDataScienceCenter/renku-ui/issues/3606
author_association: MEMBER
user: login: olevski
created_at: 2025-03-20T23:51:42Z
body: The dropdown styling is back to normal.

![Screenshot From 2025-03-21 00-50-49](https://github.com/user-attachments/assets/9897f941-8e1d-4400-8e49-863e5de4f775)

author_association: MEMBER
user: login: RenkuBot
created_at: 2025-03-21T09:59:18Z
body: Tearing down the temporary RenkuLab deplyoment for this PR.
author_association: CONTRIBUTOR