diff --git a/site/src/api/api.ts b/site/src/api/api.ts index d2e32def327b0..397f5e0378d75 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -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 => { const response = await this.axios.patch( - `/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}`, ); }; diff --git a/site/src/api/queries/roles.ts b/site/src/api/queries/roles.ts index 944a4fd4f85f4..b3572efcaed52 100644 --- a/site/src/api/queries/roles.ts +++ b/site/src/api/queries/roles.ts @@ -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), ), }; }; diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx index 46e04f790bf51..9f58b8c3b0f79 100644 --- a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx +++ b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx @@ -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(); const organizationRolesQuery = useQuery(organizationRoles(organizationName)); const filteredRoleData = organizationRolesQuery.data?.filter( (role) => role.built_in === false, @@ -69,9 +76,31 @@ export const CustomRolesPage: FC = () => { + + 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"), + ); + } + }} + /> ); }; diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx index 12404e91dab2d..14dc627b31f6c 100644 --- a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx @@ -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 = ({ roles, + onDeleteRole, canAssignOrgRole, isCustomRolesEnabled, }) => { @@ -53,7 +60,7 @@ export const CustomRolesPageView: FC = ({ Name Permissions - + @@ -91,7 +98,12 @@ export const CustomRolesPageView: FC = ({ {roles?.map((role) => ( - + onDeleteRole(role)} + /> ))} @@ -106,16 +118,15 @@ export const CustomRolesPageView: FC = ({ interface RoleRowProps { role: Role; + onDelete: () => void; + canAssignOrgRole: boolean; } -const RoleRow: FC = ({ role }) => { +const RoleRow: FC = ({ role, onDelete, canAssignOrgRole }) => { const navigate = useNavigate(); - const rowProps = useClickableTableRow({ - onClick: () => navigate(role.name), - }); return ( - + {role.display_name || role.name} @@ -123,9 +134,25 @@ const RoleRow: FC = ({ role }) => { -
- -
+ + + + + + { + navigate(role.name); + }} + > + Edit + + {canAssignOrgRole && ( + + Delete… + + )} + +
); @@ -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, }),