From 09d4d1ba8ffab08aa3ea98b08552a3d4664ec82f Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Fri, 20 Dec 2024 10:18:27 +0100 Subject: [PATCH 1/7] Hide the edit button when the user does not have permissions Closes #3446. --- .../CodeRepositoryDisplay.tsx | 64 +++++++++++-------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/client/src/features/ProjectPageV2/ProjectPageContent/CodeRepositories/CodeRepositoryDisplay.tsx b/client/src/features/ProjectPageV2/ProjectPageContent/CodeRepositories/CodeRepositoryDisplay.tsx index 0d27112a70..bb99d53c40 100644 --- a/client/src/features/ProjectPageV2/ProjectPageContent/CodeRepositories/CodeRepositoryDisplay.tsx +++ b/client/src/features/ProjectPageV2/ProjectPageContent/CodeRepositories/CodeRepositoryDisplay.tsx @@ -63,12 +63,14 @@ import { useGetOauth2ProvidersQuery, } from "../../../connectedServices/api/connectedServices.api"; import { INTERNAL_GITLAB_PROVIDER_ID } from "../../../connectedServices/connectedServices.constants"; +import PermissionsGuard from "../../../permissionsV2/PermissionsGuard"; import { Project } from "../../../projectsV2/api/projectV2.api"; import { usePatchProjectsByProjectIdMutation } from "../../../projectsV2/api/projectV2.enhanced-api"; import repositoriesApi, { useGetRepositoryMetadataQuery, useGetRepositoryProbeQuery, } from "../../../repositories/repositories.api"; +import useProjectPermissions from "../../utils/useProjectPermissions.hook"; interface EditCodeRepositoryModalProps { project: Project; @@ -293,6 +295,8 @@ function CodeRepositoryActions({ url: string; project: Project; }) { + const permissions = useProjectPermissions({ projectId: project.id }); + const [isDeleteOpen, setIsDeleteOpen] = useState(false); const toggleDelete = useCallback(() => { setIsDeleteOpen((open) => !open); @@ -317,31 +321,41 @@ function CodeRepositoryActions({ ); return ( - <> - - - - Remove - - - - - + + + + + Remove + + + + + + } + requestedPermission="write" + userPermissions={permissions} + /> ); } From a2d9ac523789ab3cddee4ec6a6c1730d497921f4 Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Fri, 20 Dec 2024 10:41:29 +0100 Subject: [PATCH 2/7] fix for data connector offcanvas --- .../components/DataConnectorActions.tsx | 36 ++++++++++++---- .../utils/useDataConnectorPermissions.hook.ts | 43 ++++++++++++++----- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx b/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx index 5893466a6c..0a287ffb19 100644 --- a/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx +++ b/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx @@ -39,7 +39,10 @@ import useAppDispatch from "../../../utils/customHooks/useAppDispatch.hook"; import PermissionsGuard from "../../permissionsV2/PermissionsGuard"; import useProjectPermissions from "../../ProjectPageV2/utils/useProjectPermissions.hook"; -import { projectV2Api } from "../../projectsV2/api/projectV2.enhanced-api"; +import { + projectV2Api, + useGetNamespacesByNamespaceProjectsAndSlugQuery, +} from "../../projectsV2/api/projectV2.enhanced-api"; import type { DataConnectorRead, @@ -50,11 +53,10 @@ import { useDeleteDataConnectorsByDataConnectorIdProjectLinksAndLinkIdMutation, useGetDataConnectorsByDataConnectorIdProjectLinksQuery, } from "../api/data-connectors.enhanced-api"; +import useDataConnectorPermissions from "../utils/useDataConnectorPermissions.hook"; import DataConnectorCredentialsModal from "./DataConnectorCredentialsModal"; import DataConnectorModal from "./DataConnectorModal"; -import { useGetNamespacesByNamespaceProjectsAndSlugQuery } from "../../projectsV2/api/projectV2.enhanced-api"; -import useDataConnectorPermissions from "../utils/useDataConnectorPermissions.hook"; interface DataConnectorRemoveModalProps { dataConnector: DataConnectorRead; @@ -359,15 +361,11 @@ function DataConnectorRemoveUnlinkModal({ ); } -export default function DataConnectorActions({ +function DataConnectorActionsInner({ dataConnector, dataConnectorLink, toggleView, -}: { - dataConnector: DataConnectorRead; - dataConnectorLink?: DataConnectorToProjectLink; - toggleView: () => void; -}) { +}: DataConnectorActionsProps) { const location = useLocation(); const pathMatch = matchPath( ABSOLUTE_ROUTES.v2.projects.show.root, @@ -480,3 +478,23 @@ export default function DataConnectorActions({ ); } + +interface DataConnectorActionsProps { + dataConnector: DataConnectorRead; + dataConnectorLink?: DataConnectorToProjectLink; + toggleView: () => void; +} + +export default function DataConnectorActions(props: DataConnectorActionsProps) { + const { id: dataConnectorId } = props.dataConnector; + const { permissions } = useDataConnectorPermissions({ dataConnectorId }); + + return ( + } + requestedPermission="write" + userPermissions={permissions} + /> + ); +} diff --git a/client/src/features/dataConnectorsV2/utils/useDataConnectorPermissions.hook.ts b/client/src/features/dataConnectorsV2/utils/useDataConnectorPermissions.hook.ts index a7e349d272..7982ed7d63 100644 --- a/client/src/features/dataConnectorsV2/utils/useDataConnectorPermissions.hook.ts +++ b/client/src/features/dataConnectorsV2/utils/useDataConnectorPermissions.hook.ts @@ -20,30 +20,51 @@ import { skipToken } from "@reduxjs/toolkit/query"; import { DEFAULT_PERMISSIONS } from "../../permissionsV2/permissions.constants"; import type { Permissions } from "../../permissionsV2/permissions.types"; -import { useGetDataConnectorsByDataConnectorIdPermissionsQuery } from "../api/data-connectors.enhanced-api"; +import { + useGetDataConnectorsByDataConnectorIdPermissionsQuery, + dataConnectorsApi, +} from "../api/data-connectors.enhanced-api"; +import { useEffect } from "react"; interface UseDataConnectorPermissionsArgs { dataConnectorId: string; } +type UseQueryStateResult = ReturnType< + typeof useGetDataConnectorsByDataConnectorIdPermissionsQuery +>; +type Result = Omit & { + permissions: Permissions; +}; export default function useDataConnectorPermissions({ dataConnectorId, -}: UseDataConnectorPermissionsArgs): { - permissions: Permissions; - isLoading: boolean; -} { - const { data, isLoading, isError } = - useGetDataConnectorsByDataConnectorIdPermissionsQuery( +}: UseDataConnectorPermissionsArgs): Result { + const { currentData, isLoading, isError, isUninitialized, ...result } = + dataConnectorsApi.endpoints.getDataConnectorsByDataConnectorIdPermissions.useQueryState( dataConnectorId ? { dataConnectorId } : skipToken ); + const [fetchPermissions] = + dataConnectorsApi.endpoints.getDataConnectorsByDataConnectorIdPermissions.useLazyQuery(); + + useEffect(() => { + if (dataConnectorId && isUninitialized) { + fetchPermissions({ dataConnectorId }); + } + }, [dataConnectorId, fetchPermissions, isUninitialized]); - if (isLoading || isError || !data) { - return { permissions: DEFAULT_PERMISSIONS, isLoading }; + if (isLoading || isError || !currentData) { + return { + permissions: DEFAULT_PERMISSIONS, + isLoading, + isError, + isUninitialized, + ...result, + }; } const permissions: Permissions = { ...DEFAULT_PERMISSIONS, - ...data, + ...currentData, }; - return { permissions, isLoading }; + return { permissions, isLoading, isError, isUninitialized, ...result }; } From fc9f83f53b5cffceaa4378814bd7453898604558 Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Fri, 20 Dec 2024 11:34:31 +0100 Subject: [PATCH 3/7] fix for data connector offcanvas --- .../utils/useProjectPermissions.hook.ts | 9 +- .../components/DataConnectorActions.tsx | 208 +++++++++++------- 2 files changed, 139 insertions(+), 78 deletions(-) diff --git a/client/src/features/ProjectPageV2/utils/useProjectPermissions.hook.ts b/client/src/features/ProjectPageV2/utils/useProjectPermissions.hook.ts index d7c96ea64f..782649ba6c 100644 --- a/client/src/features/ProjectPageV2/utils/useProjectPermissions.hook.ts +++ b/client/src/features/ProjectPageV2/utils/useProjectPermissions.hook.ts @@ -16,6 +16,7 @@ * limitations under the License. */ +import { skipToken } from "@reduxjs/toolkit/query"; import { useEffect } from "react"; import { DEFAULT_PERMISSIONS } from "../../permissionsV2/permissions.constants"; @@ -30,14 +31,14 @@ export default function useProjectPermissions({ projectId, }: UseProjectPermissionsArgs): Permissions { const { currentData, isLoading, isError, isUninitialized } = - projectV2Api.endpoints.getProjectsByProjectIdPermissions.useQueryState({ - projectId, - }); + projectV2Api.endpoints.getProjectsByProjectIdPermissions.useQueryState( + projectId ? { projectId } : skipToken + ); const [fetchPermissions] = projectV2Api.endpoints.getProjectsByProjectIdPermissions.useLazyQuery(); useEffect(() => { - if (isUninitialized) { + if (projectId && isUninitialized) { fetchPermissions({ projectId }); } }, [fetchPermissions, isUninitialized, projectId]); diff --git a/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx b/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx index 0a287ffb19..24d9056360 100644 --- a/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx +++ b/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx @@ -36,6 +36,7 @@ import { RtkOrNotebooksError } from "../../../components/errors/RtkErrorAlert"; import { Loader } from "../../../components/Loader"; import { ABSOLUTE_ROUTES } from "../../../routing/routes.constants"; import useAppDispatch from "../../../utils/customHooks/useAppDispatch.hook"; +import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook"; import PermissionsGuard from "../../permissionsV2/PermissionsGuard"; import useProjectPermissions from "../../ProjectPageV2/utils/useProjectPermissions.hook"; @@ -366,6 +367,21 @@ function DataConnectorActionsInner({ dataConnectorLink, toggleView, }: DataConnectorActionsProps) { + const { id: dataConnectorId } = dataConnector; + const { permissions } = useDataConnectorPermissions({ dataConnectorId }); + + const { project_id: projectId } = dataConnectorLink ?? {}; + const projectPermissions = useProjectPermissions({ + projectId: projectId ?? "", + }); + + // const { data: project, isLoading: isLoadingProject } = + // useGetNamespacesByNamespaceProjectsAndSlugQuery({ + // namespace: projectNamespace, + // slug: projectSlug, + // }); + // const permissions = useProjectPermissions({ projectId: project?.id ?? "" }); + const location = useLocation(); const pathMatch = matchPath( ABSOLUTE_ROUTES.v2.projects.show.root, @@ -374,7 +390,7 @@ function DataConnectorActionsInner({ const namespace = pathMatch?.params?.namespace; const slug = pathMatch?.params?.slug; const removeMode = - pathMatch === null || + pathMatch == null || namespace == null || slug == null || dataConnectorLink == null @@ -397,82 +413,128 @@ function DataConnectorActionsInner({ setIsEditOpen((open) => !open); }, []); - const defaultAction = ( - - ); + const actions = [ + ...(permissions.write + ? [ + { + key: "data-connector-edit", + onClick: toggleEdit, + content: ( + <> + + Edit + + ), + }, + ] + : []), + { + key: "data-connector-credentials", + onClick: toggleCredentials, + content: ( + <> + + Credentials + + ), + }, + ...(permissions.delete && removeMode === "delete" + ? [ + { + key: "data-connector-delete", + onClick: toggleDelete, + content: ( + <> + + Remove + + ), + }, + ] + : []), + ...(projectPermissions.write && removeMode === "unlink" + ? [ + { + key: "data-connector-delete", + onClick: toggleDelete, + content: ( + <> + + Unlink + + ), + }, + ] + : []), + ]; - const removeModal = - removeMode == "delete" ? ( - - ) : ( - - ); + if (actions.length < 1) { + return null; + } - return ( - <> + const actionsContent = + actions.length == 1 ? ( + + ) : ( + {actions[0].content} + + } size="sm" > - - - Credentials - - - {removeMode === "delete" ? ( - - - Remove - - ) : ( - - - Unlink - - )} - + {actions.slice(1).map(({ key, onClick, content }) => ( + + {content} + + ))} - {/* This component needs to be always be in the DOM for some reason... */} + ); + + return ( + <> + {actionsContent} + - {isDeleteOpen && removeModal} - {isEditOpen && ( - + {dataConnectorLink && ( + )} @@ -486,15 +548,13 @@ interface DataConnectorActionsProps { } export default function DataConnectorActions(props: DataConnectorActionsProps) { - const { id: dataConnectorId } = props.dataConnector; - const { permissions } = useDataConnectorPermissions({ dataConnectorId }); - - return ( - } - requestedPermission="write" - userPermissions={permissions} - /> + const userLogged = useLegacySelector( + (state) => state.stateModel.user.logged ); + + if (!userLogged) { + return null; + } + + return ; } From 35499c0db790e9353d3d32526ba1e62210fb9dba Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Fri, 20 Dec 2024 11:39:52 +0100 Subject: [PATCH 4/7] fix for session launchers --- client/src/features/sessionsV2/SessionsV2.tsx | 61 +++++++++++-------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/client/src/features/sessionsV2/SessionsV2.tsx b/client/src/features/sessionsV2/SessionsV2.tsx index e41723e4c4..e6ac47c726 100644 --- a/client/src/features/sessionsV2/SessionsV2.tsx +++ b/client/src/features/sessionsV2/SessionsV2.tsx @@ -180,6 +180,9 @@ export function SessionV2Actions({ launcher, sessionsLength, }: SessionV2ActionsProps) { + const { project_id: projectId } = launcher; + const permissions = useProjectPermissions({ projectId }); + const [isUpdateOpen, setIsUpdateOpen] = useState(false); const [isDeleteOpen, setIsDeleteOpen] = useState(false); @@ -204,30 +207,40 @@ export function SessionV2Actions({ ); return ( - <> - - - - Delete - - - - - + + + + + Delete + + {" "} + + + + } + requestedPermission="write" + userPermissions={permissions} + /> ); } From f3a5a57436d63fdc95932ed2f76fe928322c0bb2 Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Fri, 20 Dec 2024 12:01:28 +0100 Subject: [PATCH 5/7] fix e2e --- tests/cypress/e2e/groupV2.spec.ts | 13 ++++------- .../groupV2DataConnectorCredentials.spec.ts | 1 - tests/cypress/e2e/projectV2setup.spec.ts | 23 +++++++------------ 3 files changed, 12 insertions(+), 25 deletions(-) diff --git a/tests/cypress/e2e/groupV2.spec.ts b/tests/cypress/e2e/groupV2.spec.ts index 00e6828811..4a96ec8819 100644 --- a/tests/cypress/e2e/groupV2.spec.ts +++ b/tests/cypress/e2e/groupV2.spec.ts @@ -467,10 +467,8 @@ describe("Work with group data connectors, missing permissions", () => { cy.contains("test 2 group-v2").should("be.visible").click(); cy.wait("@readGroupV2"); cy.contains("public-storage").should("be.visible").click(); - cy.getDataCy("data-connector-edit").should("be.visible").click(); - cy.contains( - "You do not have the required permissions to modify this data connector" - ).should("be.visible"); + cy.getDataCy("data-connector-credentials").should("be.visible"); + cy.getDataCy("data-connector-edit").should("not.exist"); }); it("delete a group data connector", () => { @@ -481,10 +479,7 @@ describe("Work with group data connectors, missing permissions", () => { cy.contains("test 2 group-v2").should("be.visible").click(); cy.wait("@readGroupV2"); cy.contains("public-storage").should("be.visible").click(); - cy.getDataCy("button-with-menu-dropdown").should("be.visible").click(); - cy.getDataCy("data-connector-delete").should("be.visible").click(); - cy.contains( - "You do not have the required permissions to delete this data connector" - ).should("be.visible"); + cy.getDataCy("data-connector-credentials").should("be.visible"); + cy.getDataCy("data-connector-delete").should("not.exist"); }); }); diff --git a/tests/cypress/e2e/groupV2DataConnectorCredentials.spec.ts b/tests/cypress/e2e/groupV2DataConnectorCredentials.spec.ts index fc6d4fa6b2..81ee8e76e1 100644 --- a/tests/cypress/e2e/groupV2DataConnectorCredentials.spec.ts +++ b/tests/cypress/e2e/groupV2DataConnectorCredentials.spec.ts @@ -367,7 +367,6 @@ describe("Set up data connectors with credentials", () => { ); // clear credentials - openDataConnectorMenu(); cy.getDataCy("data-connector-credentials").click(); cy.getDataCy("data-connector-credentials-modal") .contains("The saved credentials for this data connector are incomplete") diff --git a/tests/cypress/e2e/projectV2setup.spec.ts b/tests/cypress/e2e/projectV2setup.spec.ts index abdfe8228f..ac7c4cd36c 100644 --- a/tests/cypress/e2e/projectV2setup.spec.ts +++ b/tests/cypress/e2e/projectV2setup.spec.ts @@ -18,14 +18,6 @@ import fixtures from "../support/renkulab-fixtures"; -function openDataConnectorMenu() { - cy.getDataCy("data-connector-edit") - .parent() - .find("[data-cy=button-with-menu-dropdown]") - .first() - .click(); -} - describe("Set up project components", () => { beforeEach(() => { fixtures @@ -89,6 +81,7 @@ describe("Set up project components", () => { }).as("getSessionsV2"); fixtures .readProjectV2({ fixture: "projectV2/read-projectV2-empty.json" }) + .getProjectV2Permissions({ projectId: "01HYJE5FR1JV4CWFMBFJQFQ4RM" }) .listProjectDataConnectors() .getDataConnector() .sessionLaunchers() @@ -292,7 +285,11 @@ describe("Set up data connectors", () => { cy.wait("@listProjectDataConnectors"); cy.contains("example storage").should("be.visible").click(); - openDataConnectorMenu(); + cy.getDataCy("data-connector-credentials") + .should("be.visible") + .siblings() + .get("[data-cy=button-with-menu-dropdown]") + .click(); cy.getDataCy("data-connector-delete").should("be.visible").click(); cy.wait("@getProjectV2Permissions"); cy.contains("Are you sure you want to unlink the data connector").should( @@ -319,12 +316,8 @@ describe("Set up data connectors", () => { cy.wait("@listProjectDataConnectors"); cy.contains("example storage").should("be.visible").click(); - openDataConnectorMenu(); - cy.getDataCy("data-connector-delete").should("be.visible").click(); - cy.contains( - "You do not have the required permissions to unlink this data connector." - ).should("be.visible"); - cy.getDataCy("delete-data-connector-modal-button").should("not.exist"); + cy.getDataCy("data-connector-credentials").should("be.visible"); + cy.getDataCy("data-connector-delete").should("not.exist"); }); it("should clear state after a data connector has been created", () => { From 0096d4f3e0330591c88abf89356bae89ee2a1e0f Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Mon, 6 Jan 2025 09:29:05 +0100 Subject: [PATCH 6/7] Update client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx --- .../dataConnectorsV2/components/DataConnectorActions.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx b/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx index 24d9056360..fc62e4d07e 100644 --- a/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx +++ b/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx @@ -375,13 +375,6 @@ function DataConnectorActionsInner({ projectId: projectId ?? "", }); - // const { data: project, isLoading: isLoadingProject } = - // useGetNamespacesByNamespaceProjectsAndSlugQuery({ - // namespace: projectNamespace, - // slug: projectSlug, - // }); - // const permissions = useProjectPermissions({ projectId: project?.id ?? "" }); - const location = useLocation(); const pathMatch = matchPath( ABSOLUTE_ROUTES.v2.projects.show.root, From 004c0efc9ebafa783204231aedb110d9baee2c59 Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Tue, 7 Jan 2025 10:26:31 +0100 Subject: [PATCH 7/7] fix e2e --- tests/cypress/e2e/projectV2setup.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cypress/e2e/projectV2setup.spec.ts b/tests/cypress/e2e/projectV2setup.spec.ts index ac7c4cd36c..3f5b71a5d7 100644 --- a/tests/cypress/e2e/projectV2setup.spec.ts +++ b/tests/cypress/e2e/projectV2setup.spec.ts @@ -287,8 +287,8 @@ describe("Set up data connectors", () => { cy.contains("example storage").should("be.visible").click(); cy.getDataCy("data-connector-credentials") .should("be.visible") - .siblings() - .get("[data-cy=button-with-menu-dropdown]") + .parent() + .find("[data-cy=button-with-menu-dropdown]") .click(); cy.getDataCy("data-connector-delete").should("be.visible").click(); cy.wait("@getProjectV2Permissions");