Skip to content

Commit

Permalink
fix: hide the edit button when the user does not have permissions (#3462
Browse files Browse the repository at this point in the history
)

Closes #3446.

Update the actions buttons on the project / group / user pages to be presented only when the corresponding action is available to the user.

---------

Co-authored-by: Lorenzo Cavazzi <[email protected]>
  • Loading branch information
leafty and lorenzo-cavazzi authored Jan 7, 2025
1 parent 1a27e1d commit a75f163
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 162 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -317,31 +321,41 @@ function CodeRepositoryActions({
);

return (
<>
<ButtonWithMenuV2
color="outline-primary"
default={defaultAction}
preventPropagation
size="sm"
>
<DropdownItem data-cy="code-repository-delete" onClick={toggleDelete}>
<Trash className={cx("bi", "me-1")} />
Remove
</DropdownItem>
</ButtonWithMenuV2>
<CodeRepositoryDeleteModal
repositoryUrl={url}
isOpen={isDeleteOpen}
toggleModal={toggleDelete}
project={project}
/>
<EditCodeRepositoryModal
toggleModal={toggleEdit}
isOpen={isEditOpen}
project={project}
repositoryUrl={url}
/>
</>
<PermissionsGuard
disabled={null}
enabled={
<>
<ButtonWithMenuV2
color="outline-primary"
default={defaultAction}
preventPropagation
size="sm"
>
<DropdownItem
data-cy="code-repository-delete"
onClick={toggleDelete}
>
<Trash className={cx("bi", "me-1")} />
Remove
</DropdownItem>
</ButtonWithMenuV2>
<CodeRepositoryDeleteModal
repositoryUrl={url}
isOpen={isDeleteOpen}
toggleModal={toggleDelete}
project={project}
/>
<EditCodeRepositoryModal
toggleModal={toggleEdit}
isOpen={isEditOpen}
project={project}
repositoryUrl={url}
/>
</>
}
requestedPermission="write"
userPermissions={permissions}
/>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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]);
Expand Down
217 changes: 144 additions & 73 deletions client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ 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";
import { projectV2Api } from "../../projectsV2/api/projectV2.enhanced-api";
import {
projectV2Api,
useGetNamespacesByNamespaceProjectsAndSlugQuery,
} from "../../projectsV2/api/projectV2.enhanced-api";

import type {
DataConnectorRead,
Expand All @@ -50,11 +54,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;
Expand Down Expand Up @@ -359,15 +362,19 @@ function DataConnectorRemoveUnlinkModal({
);
}

export default function DataConnectorActions({
function DataConnectorActionsInner({
dataConnector,
dataConnectorLink,
toggleView,
}: {
dataConnector: DataConnectorRead;
dataConnectorLink?: DataConnectorToProjectLink;
toggleView: () => void;
}) {
}: DataConnectorActionsProps) {
const { id: dataConnectorId } = dataConnector;
const { permissions } = useDataConnectorPermissions({ dataConnectorId });

const { project_id: projectId } = dataConnectorLink ?? {};
const projectPermissions = useProjectPermissions({
projectId: projectId ?? "",
});

const location = useLocation();
const pathMatch = matchPath(
ABSOLUTE_ROUTES.v2.projects.show.root,
Expand All @@ -376,7 +383,7 @@ export default function DataConnectorActions({
const namespace = pathMatch?.params?.namespace;
const slug = pathMatch?.params?.slug;
const removeMode =
pathMatch === null ||
pathMatch == null ||
namespace == null ||
slug == null ||
dataConnectorLink == null
Expand All @@ -399,84 +406,148 @@ export default function DataConnectorActions({
setIsEditOpen((open) => !open);
}, []);

const defaultAction = (
<Button
className="text-nowrap"
color="outline-primary"
data-cy="data-connector-edit"
onClick={toggleEdit}
size="sm"
>
<Pencil className={cx("bi", "me-1")} />
Edit
</Button>
);
const actions = [
...(permissions.write
? [
{
key: "data-connector-edit",
onClick: toggleEdit,
content: (
<>
<Pencil className={cx("bi", "me-1")} />
Edit
</>
),
},
]
: []),
{
key: "data-connector-credentials",
onClick: toggleCredentials,
content: (
<>
<Lock className={cx("bi", "me-1")} />
Credentials
</>
),
},
...(permissions.delete && removeMode === "delete"
? [
{
key: "data-connector-delete",
onClick: toggleDelete,
content: (
<>
<Trash className={cx("bi", "me-1")} />
Remove
</>
),
},
]
: []),
...(projectPermissions.write && removeMode === "unlink"
? [
{
key: "data-connector-delete",
onClick: toggleDelete,
content: (
<>
<NodeMinus className={cx("bi", "me-1")} />
Unlink
</>
),
},
]
: []),
];

const removeModal =
removeMode == "delete" ? (
<DataConnectorRemoveDeleteModal
dataConnector={dataConnector}
dataConnectorLink={dataConnectorLink}
isOpen={isDeleteOpen}
onDelete={onDelete}
toggleModal={toggleDelete}
/>
) : (
<DataConnectorRemoveUnlinkModal
dataConnector={dataConnector}
dataConnectorLink={dataConnectorLink!}
isOpen={isDeleteOpen}
onDelete={onDelete}
projectNamespace={namespace!}
projectSlug={slug!}
toggleModal={toggleDelete}
/>
);
if (actions.length < 1) {
return null;
}

return (
<>
const actionsContent =
actions.length == 1 ? (
<Button
color="outline-primary"
data-cy={actions[0].key}
onClick={actions[0].onClick}
size="sm"
>
{actions[0].content}
</Button>
) : (
<ButtonWithMenuV2
color="outline-primary"
default={defaultAction}
preventPropagation
default={
<Button
color="outline-primary"
data-cy={actions[0].key}
onClick={actions[0].onClick}
size="sm"
>
{actions[0].content}
</Button>
}
size="sm"
>
<DropdownItem
data-cy="data-connector-credentials"
onClick={toggleCredentials}
>
<Lock className={cx("bi", "me-1")} />
Credentials
</DropdownItem>
<DropdownItem data-cy="data-connector-delete" onClick={toggleDelete}>
{removeMode === "delete" ? (
<span>
<Trash className={cx("bi", "me-1")} />
Remove
</span>
) : (
<span>
<NodeMinus className={cx("bi", "me-1")} />
Unlink
</span>
)}
</DropdownItem>
{actions.slice(1).map(({ key, onClick, content }) => (
<DropdownItem key={key} data-cy={key} onClick={onClick}>
{content}
</DropdownItem>
))}
</ButtonWithMenuV2>
{/* This component needs to be always be in the DOM for some reason... */}
);

return (
<>
{actionsContent}
<DataConnectorModal
dataConnector={dataConnector}
isOpen={isEditOpen}
namespace={dataConnector.namespace}
toggle={toggleEdit}
/>
<DataConnectorCredentialsModal
dataConnector={dataConnector}
setOpen={setCredentialsOpen}
isOpen={isCredentialsOpen}
/>
{isDeleteOpen && removeModal}
{isEditOpen && (
<DataConnectorModal
<DataConnectorRemoveDeleteModal
dataConnector={dataConnector}
dataConnectorLink={dataConnectorLink}
isOpen={isDeleteOpen}
onDelete={onDelete}
toggleModal={toggleDelete}
/>
{dataConnectorLink && (
<DataConnectorRemoveUnlinkModal
dataConnector={dataConnector}
isOpen={isEditOpen}
namespace={dataConnector.namespace}
toggle={toggleEdit}
dataConnectorLink={dataConnectorLink}
isOpen={isDeleteOpen}
onDelete={onDelete}
projectNamespace={namespace!}
projectSlug={slug!}
toggleModal={toggleDelete}
/>
)}
</>
);
}

interface DataConnectorActionsProps {
dataConnector: DataConnectorRead;
dataConnectorLink?: DataConnectorToProjectLink;
toggleView: () => void;
}

export default function DataConnectorActions(props: DataConnectorActionsProps) {
const userLogged = useLegacySelector<boolean>(
(state) => state.stateModel.user.logged
);

if (!userLogged) {
return null;
}

return <DataConnectorActionsInner {...props} />;
}
Loading

0 comments on commit a75f163

Please sign in to comment.