mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
feat: add delete custom role context menu button and modal (#14228)
* feat: delete custom role * fix: add doc comment
This commit is contained in:
+10
-4
@@ -600,21 +600,27 @@ class ApiMethods {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param organization Can be the organization's ID or name
|
||||
*/
|
||||
patchOrganizationRole = async (
|
||||
organizationId: string,
|
||||
organization: string,
|
||||
role: TypesGen.Role,
|
||||
): Promise<TypesGen.Role> => {
|
||||
const response = await this.axios.patch<TypesGen.Role>(
|
||||
`/api/v2/organizations/${organizationId}/members/roles`,
|
||||
`/api/v2/organizations/${organization}/members/roles`,
|
||||
role,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
deleteOrganizationRole = async (organizationId: string, roleName: string) => {
|
||||
/**
|
||||
* @param organization Can be the organization's ID or name
|
||||
*/
|
||||
deleteOrganizationRole = async (organization: string, roleName: string) => {
|
||||
await this.axios.delete(
|
||||
`/api/v2/organizations/${organizationId}/members/roles/${roleName}`,
|
||||
`/api/v2/organizations/${organization}/members/roles/${roleName}`,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -16,36 +16,37 @@ export const roles = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const organizationRoles = (organizationId: string) => {
|
||||
export const organizationRoles = (organization: string) => {
|
||||
return {
|
||||
queryKey: ["organization", organizationId, "roles"],
|
||||
queryFn: () => API.getOrganizationRoles(organizationId),
|
||||
queryKey: ["organization", organization, "roles"],
|
||||
queryFn: () => API.getOrganizationRoles(organization),
|
||||
};
|
||||
};
|
||||
|
||||
export const patchOrganizationRole = (
|
||||
queryClient: QueryClient,
|
||||
organizationId: string,
|
||||
organization: string,
|
||||
) => {
|
||||
return {
|
||||
mutationFn: (request: Role) =>
|
||||
API.patchOrganizationRole(organizationId, request),
|
||||
API.patchOrganizationRole(organization, request),
|
||||
onSuccess: async (updatedRole: Role) =>
|
||||
await queryClient.invalidateQueries(
|
||||
getRoleQueryKey(organizationId, updatedRole.name),
|
||||
getRoleQueryKey(organization, updatedRole.name),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteRole = (
|
||||
export const deleteOrganizationRole = (
|
||||
queryClient: QueryClient,
|
||||
organizationId: string,
|
||||
organization: string,
|
||||
) => {
|
||||
return {
|
||||
mutationFn: API.deleteOrganizationRole,
|
||||
mutationFn: (roleName: string) =>
|
||||
API.deleteOrganizationRole(organization, roleName),
|
||||
onSuccess: async (_: void, roleName: string) =>
|
||||
await queryClient.invalidateQueries(
|
||||
getRoleQueryKey(organizationId, roleName),
|
||||
getRoleQueryKey(organization, roleName),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import AddIcon from "@mui/icons-material/AddOutlined";
|
||||
import Button from "@mui/material/Button";
|
||||
import { type FC, useEffect } from "react";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useQuery } from "react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { Link as RouterLink, useParams } from "react-router-dom";
|
||||
import { getErrorMessage } from "api/errors";
|
||||
import { organizationPermissions } from "api/queries/organizations";
|
||||
import { organizationRoles } from "api/queries/roles";
|
||||
import { displayError } from "components/GlobalSnackbar/utils";
|
||||
import { deleteOrganizationRole, organizationRoles } from "api/queries/roles";
|
||||
import type { Role } from "api/typesGenerated";
|
||||
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
|
||||
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
@@ -17,6 +19,7 @@ import { useOrganizationSettings } from "../ManagementSettingsLayout";
|
||||
import CustomRolesPageView from "./CustomRolesPageView";
|
||||
|
||||
export const CustomRolesPage: FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { custom_roles: isCustomRolesEnabled } = useFeatureVisibility();
|
||||
const { organization: organizationName } = useParams() as {
|
||||
organization: string;
|
||||
@@ -24,6 +27,10 @@ export const CustomRolesPage: FC = () => {
|
||||
const { organizations } = useOrganizationSettings();
|
||||
const organization = organizations?.find((o) => o.name === organizationName);
|
||||
const permissionsQuery = useQuery(organizationPermissions(organization?.id));
|
||||
const deleteRoleMutation = useMutation(
|
||||
deleteOrganizationRole(queryClient, organizationName),
|
||||
);
|
||||
const [roleToDelete, setRoleToDelete] = useState<Role>();
|
||||
const organizationRolesQuery = useQuery(organizationRoles(organizationName));
|
||||
const filteredRoleData = organizationRolesQuery.data?.filter(
|
||||
(role) => role.built_in === false,
|
||||
@@ -69,9 +76,31 @@ export const CustomRolesPage: FC = () => {
|
||||
|
||||
<CustomRolesPageView
|
||||
roles={filteredRoleData}
|
||||
onDeleteRole={setRoleToDelete}
|
||||
canAssignOrgRole={permissions.assignOrgRole}
|
||||
isCustomRolesEnabled={isCustomRolesEnabled}
|
||||
/>
|
||||
|
||||
<DeleteDialog
|
||||
key={roleToDelete?.name}
|
||||
isOpen={roleToDelete !== undefined}
|
||||
confirmLoading={deleteRoleMutation.isLoading}
|
||||
name={roleToDelete?.name ?? ""}
|
||||
entity="role"
|
||||
onCancel={() => setRoleToDelete(undefined)}
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await deleteRoleMutation.mutateAsync(roleToDelete!.name);
|
||||
setRoleToDelete(undefined);
|
||||
await organizationRolesQuery.refetch();
|
||||
displaySuccess("Custom role deleted successfully!");
|
||||
} catch (error) {
|
||||
displayError(
|
||||
getErrorMessage(error, "Failed to delete custom role"),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import AddOutlined from "@mui/icons-material/AddOutlined";
|
||||
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
|
||||
import Button from "@mui/material/Button";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import Table from "@mui/material/Table";
|
||||
@@ -14,22 +13,30 @@ import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||
import type { Role } from "api/typesGenerated";
|
||||
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
import {
|
||||
MoreMenu,
|
||||
MoreMenuContent,
|
||||
MoreMenuItem,
|
||||
MoreMenuTrigger,
|
||||
ThreeDotsButton,
|
||||
} from "components/MoreMenu/MoreMenu";
|
||||
import { Paywall } from "components/Paywall/Paywall";
|
||||
import {
|
||||
TableLoaderSkeleton,
|
||||
TableRowSkeleton,
|
||||
} from "components/TableLoader/TableLoader";
|
||||
import { useClickableTableRow } from "hooks";
|
||||
import { docs } from "utils/docs";
|
||||
|
||||
export type CustomRolesPageViewProps = {
|
||||
roles: Role[] | undefined;
|
||||
onDeleteRole: (role: Role) => void;
|
||||
canAssignOrgRole: boolean;
|
||||
isCustomRolesEnabled: boolean;
|
||||
};
|
||||
|
||||
export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
|
||||
roles,
|
||||
onDeleteRole,
|
||||
canAssignOrgRole,
|
||||
isCustomRolesEnabled,
|
||||
}) => {
|
||||
@@ -53,7 +60,7 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
|
||||
<TableRow>
|
||||
<TableCell width="50%">Name</TableCell>
|
||||
<TableCell width="49%">Permissions</TableCell>
|
||||
<TableCell width="1%"></TableCell>
|
||||
<TableCell width="1%" />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -91,7 +98,12 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
|
||||
|
||||
<Cond>
|
||||
{roles?.map((role) => (
|
||||
<RoleRow key={role.name} role={role} />
|
||||
<RoleRow
|
||||
key={role.name}
|
||||
role={role}
|
||||
canAssignOrgRole={canAssignOrgRole}
|
||||
onDelete={() => onDeleteRole(role)}
|
||||
/>
|
||||
))}
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
@@ -106,16 +118,15 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
|
||||
|
||||
interface RoleRowProps {
|
||||
role: Role;
|
||||
onDelete: () => void;
|
||||
canAssignOrgRole: boolean;
|
||||
}
|
||||
|
||||
const RoleRow: FC<RoleRowProps> = ({ role }) => {
|
||||
const RoleRow: FC<RoleRowProps> = ({ role, onDelete, canAssignOrgRole }) => {
|
||||
const navigate = useNavigate();
|
||||
const rowProps = useClickableTableRow({
|
||||
onClick: () => navigate(role.name),
|
||||
});
|
||||
|
||||
return (
|
||||
<TableRow data-testid={`role-${role.name}`} {...rowProps}>
|
||||
<TableRow data-testid={`role-${role.name}`}>
|
||||
<TableCell>{role.display_name || role.name}</TableCell>
|
||||
|
||||
<TableCell css={styles.secondary}>
|
||||
@@ -123,9 +134,25 @@ const RoleRow: FC<RoleRowProps> = ({ role }) => {
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div css={styles.arrowCell}>
|
||||
<KeyboardArrowRight css={styles.arrowRight} />
|
||||
</div>
|
||||
<MoreMenu>
|
||||
<MoreMenuTrigger>
|
||||
<ThreeDotsButton />
|
||||
</MoreMenuTrigger>
|
||||
<MoreMenuContent>
|
||||
<MoreMenuItem
|
||||
onClick={() => {
|
||||
navigate(role.name);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</MoreMenuItem>
|
||||
{canAssignOrgRole && (
|
||||
<MoreMenuItem danger onClick={onDelete}>
|
||||
Delete…
|
||||
</MoreMenuItem>
|
||||
)}
|
||||
</MoreMenuContent>
|
||||
</MoreMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
@@ -150,14 +177,6 @@ const TableLoader = () => {
|
||||
};
|
||||
|
||||
const styles = {
|
||||
arrowRight: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}),
|
||||
arrowCell: {
|
||||
display: "flex",
|
||||
},
|
||||
secondary: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user