Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: hide the edit button when the user does not have permissions #3462

Merged
merged 10 commits into from
Jan 7, 2025
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
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
Loading