1
0
mirror of https://github.com/coder/coder.git synced 2025-03-14 10:09:57 +00:00

chore: migrate settings page tables from mui to shadcn ()

Custom Roles
<img width="795" alt="Screenshot 2025-03-12 at 21 04 53"
src="https://github.com/user-attachments/assets/d478e80d-6d11-496c-a37f-87a73a5587b7"
/>

Group Page
<img width="804" alt="Screenshot 2025-03-12 at 21 04 12"
src="https://github.com/user-attachments/assets/eec9749a-7a34-42ca-97a8-c2a624f766bb"
/>

Groups Page
<img width="802" alt="Screenshot 2025-03-12 at 21 04 06"
src="https://github.com/user-attachments/assets/7b88f6ab-9364-4e15-b969-8e422b24085c"
/>

Users Page
<img width="820" alt="Screenshot 2025-03-12 at 21 03 58"
src="https://github.com/user-attachments/assets/195dea6e-c57f-4155-8d71-3adc3a6202bc"
/>
This commit is contained in:
Jaayden Halko
2025-03-13 21:34:00 +00:00
committed by GitHub
parent a1f5468db2
commit 0ea804ccea
9 changed files with 246 additions and 247 deletions
site
e2e/tests
src/pages
DeploymentSettingsPage/IdpOrgSyncPage
GroupsPage
OrganizationSettingsPage
UsersPage/UsersTable

@ -105,8 +105,9 @@ test("change quota settings", async ({ page }) => {
// Go to settings
await login(page, orgUserAdmin);
await page.goto(`/organizations/${org.name}/groups/${group.name}`);
await page.getByRole("button", { name: "Settings", exact: true }).click();
expectUrl(page).toHavePathName(
await page.getByRole("link", { name: "Settings", exact: true }).click();
await expectUrl(page).toHavePathName(
`/organizations/${org.name}/groups/${group.name}/settings`,
);
@ -115,11 +116,11 @@ test("change quota settings", async ({ page }) => {
await page.getByRole("button", { name: /save/i }).click();
// We should get sent back to the group page afterwards
expectUrl(page).toHavePathName(
await expectUrl(page).toHavePathName(
`/organizations/${org.name}/groups/${group.name}`,
);
// ...and that setting should persist if we go back
await page.getByRole("button", { name: "Settings", exact: true }).click();
await page.getByRole("link", { name: "Settings", exact: true }).click();
await expect(page.getByLabel("Quota Allowance")).toHaveValue("100");
});

@ -34,6 +34,7 @@ import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "components/Table/Table";
@ -365,9 +366,9 @@ const IdpMappingTable: FC<IdpMappingTableProps> = ({ isEmpty, children }) => {
<Table>
<TableHeader>
<TableRow>
<TableCell width="45%">IdP organization</TableCell>
<TableCell width="55%">Coder organization</TableCell>
<TableCell width="5%" />
<TableHead className="w-2/5">IdP organization</TableHead>
<TableHead className="w-3/5">Coder organization</TableHead>
<TableHead className="w-auto" />
</TableRow>
</TableHeader>
<TableBody>

@ -4,12 +4,6 @@ import PersonAdd from "@mui/icons-material/PersonAdd";
import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
import LoadingButton from "@mui/lab/LoadingButton";
import Button from "@mui/material/Button";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import { getErrorMessage } from "api/errors";
import {
addMember,
@ -40,6 +34,14 @@ import {
} from "components/MoreMenu/MoreMenu";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "components/Table/Table";
import {
PaginationStatus,
TableToolbar,
@ -111,7 +113,6 @@ export const GroupPage: FC = () => {
{canUpdateGroup && (
<Stack direction="row" spacing={2}>
<Button
role="button"
component={RouterLink}
startIcon={<SettingsOutlined />}
to="settings"
@ -160,53 +161,51 @@ export const GroupPage: FC = () => {
/>
</TableToolbar>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell width="59%">User</TableCell>
<TableCell width="40">Status</TableCell>
<TableCell width="1%" />
</TableRow>
</TableHead>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-2/5">User</TableHead>
<TableHead className="w-3/5">Status</TableHead>
<TableHead className="w-auto" />
</TableRow>
</TableHeader>
<TableBody>
{groupData?.members.length === 0 ? (
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="No members yet"
description="Add a member using the controls above"
/>
</TableCell>
</TableRow>
) : (
groupData?.members.map((member) => (
<GroupMemberRow
member={member}
group={groupData}
key={member.id}
canUpdate={canUpdateGroup}
onRemove={async () => {
try {
await removeMemberMutation.mutateAsync({
groupId: groupData.id,
userId: member.id,
});
await groupQuery.refetch();
displaySuccess("Member removed successfully.");
} catch (error) {
displayError(
getErrorMessage(error, "Failed to remove member."),
);
}
}}
<TableBody>
{groupData?.members.length === 0 ? (
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="No members yet"
description="Add a member using the controls above"
/>
))
)}
</TableBody>
</Table>
</TableContainer>
</TableCell>
</TableRow>
) : (
groupData?.members.map((member) => (
<GroupMemberRow
member={member}
group={groupData}
key={member.id}
canUpdate={canUpdateGroup}
onRemove={async () => {
try {
await removeMemberMutation.mutateAsync({
groupId: groupData.id,
userId: member.id,
});
await groupQuery.refetch();
displaySuccess("Member removed successfully.");
} catch (error) {
displayError(
getErrorMessage(error, "Failed to remove member."),
);
}
}}
/>
))
)}
</TableBody>
</Table>
</Stack>
{groupQuery.data && (

@ -3,12 +3,6 @@ import AddOutlined from "@mui/icons-material/AddOutlined";
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
import AvatarGroup from "@mui/material/AvatarGroup";
import Skeleton from "@mui/material/Skeleton";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import type { Group } from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar";
import { AvatarData } from "components/Avatar/AvatarData";
@ -17,6 +11,14 @@ import { Button } from "components/Button/Button";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Paywall } from "components/Paywall/Paywall";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "components/Table/Table";
import {
TableLoaderSkeleton,
TableRowSkeleton,
@ -51,55 +53,53 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
/>
</Cond>
<Cond>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell width="50%">Name</TableCell>
<TableCell width="49%">Users</TableCell>
<TableCell width="1%" />
</TableRow>
</TableHead>
<TableBody>
<ChooseOne>
<Cond condition={isLoading}>
<TableLoader />
</Cond>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-2/5">Name</TableHead>
<TableHead className="w-3/5">Users</TableHead>
<TableHead className="w-auto" />
</TableRow>
</TableHeader>
<TableBody>
<ChooseOne>
<Cond condition={isLoading}>
<TableLoader />
</Cond>
<Cond condition={isEmpty}>
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="No groups yet"
description={
canCreateGroup
? "Create your first group"
: "You don't have permission to create a group"
}
cta={
canCreateGroup && (
<Button asChild>
<RouterLink to="create">
<AddOutlined />
Create group
</RouterLink>
</Button>
)
}
/>
</TableCell>
</TableRow>
</Cond>
<Cond condition={isEmpty}>
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="No groups yet"
description={
canCreateGroup
? "Create your first group"
: "You don't have permission to create a group"
}
cta={
canCreateGroup && (
<Button asChild>
<RouterLink to="create">
<AddOutlined />
Create group
</RouterLink>
</Button>
)
}
/>
</TableCell>
</TableRow>
</Cond>
<Cond>
{groups?.map((group) => (
<GroupRow key={group.id} group={group} />
))}
</Cond>
</ChooseOne>
</TableBody>
</Table>
</TableContainer>
<Cond>
{groups?.map((group) => (
<GroupRow key={group.id} group={group} />
))}
</Cond>
</ChooseOne>
</TableBody>
</Table>
</Cond>
</ChooseOne>
</>

@ -3,12 +3,6 @@ import AddIcon from "@mui/icons-material/AddOutlined";
import AddOutlined from "@mui/icons-material/AddOutlined";
import Button from "@mui/material/Button";
import Skeleton from "@mui/material/Skeleton";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import type { AssignableRoles, Role } from "api/typesGenerated";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { EmptyState } from "components/EmptyState/EmptyState";
@ -21,6 +15,14 @@ import {
} from "components/MoreMenu/MoreMenu";
import { Paywall } from "components/Paywall/Paywall";
import { Stack } from "components/Stack/Stack";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "components/Table/Table";
import {
TableLoaderSkeleton,
TableRowSkeleton,
@ -123,68 +125,66 @@ const RoleTable: FC<RoleTableProps> = ({
const isLoading = roles === undefined;
const isEmpty = Boolean(roles && roles.length === 0);
return (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell width="40%">Name</TableCell>
<TableCell width="59%">Permissions</TableCell>
<TableCell width="1%" />
</TableRow>
</TableHead>
<TableBody>
<ChooseOne>
<Cond condition={isLoading}>
<TableLoader />
</Cond>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-2/5">Name</TableHead>
<TableHead className="w-3/5">Permissions</TableHead>
<TableHead className="w-auto" />
</TableRow>
</TableHeader>
<TableBody>
<ChooseOne>
<Cond condition={isLoading}>
<TableLoader />
</Cond>
<Cond condition={isEmpty}>
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="No custom roles yet"
description={
canCreateOrgRole && isCustomRolesEnabled
? "Create your first custom role"
: !isCustomRolesEnabled
? "Upgrade to a premium license to create a custom role"
: "You don't have permission to create a custom role"
}
cta={
canCreateOrgRole &&
isCustomRolesEnabled && (
<Button
component={RouterLink}
to="create"
startIcon={<AddOutlined />}
variant="contained"
>
Create custom role
</Button>
)
}
/>
</TableCell>
</TableRow>
</Cond>
<Cond condition={isEmpty}>
<TableRow className="h-14">
<TableCell colSpan={999}>
<EmptyState
message="No custom roles yet"
description={
canCreateOrgRole && isCustomRolesEnabled
? "Create your first custom role"
: !isCustomRolesEnabled
? "Upgrade to a premium license to create a custom role"
: "You don't have permission to create a custom role"
}
cta={
canCreateOrgRole &&
isCustomRolesEnabled && (
<Button
component={RouterLink}
to="create"
startIcon={<AddOutlined />}
variant="contained"
>
Create custom role
</Button>
)
}
/>
</TableCell>
</TableRow>
</Cond>
<Cond>
{roles
?.sort((a, b) => a.name.localeCompare(b.name))
.map((role) => (
<RoleRow
key={role.name}
role={role}
canUpdateOrgRole={canUpdateOrgRole}
canDeleteOrgRole={canDeleteOrgRole}
onDelete={() => onDeleteRole(role)}
/>
))}
</Cond>
</ChooseOne>
</TableBody>
</Table>
</TableContainer>
<Cond>
{roles
?.sort((a, b) => a.name.localeCompare(b.name))
.map((role) => (
<RoleRow
key={role.name}
role={role}
canUpdateOrgRole={canUpdateOrgRole}
canDeleteOrgRole={canDeleteOrgRole}
onDelete={() => onDeleteRole(role)}
/>
))}
</Cond>
</ChooseOne>
</TableBody>
</Table>
);
};
@ -204,7 +204,7 @@ const RoleRow: FC<RoleRowProps> = ({
const navigate = useNavigate();
return (
<TableRow data-testid={`role-${role.name}`}>
<TableRow data-testid={`role-${role.name}`} className="h-14">
<TableCell>{role.display_name || role.name}</TableCell>
<TableCell>

@ -27,9 +27,13 @@ export const IdpMappingTable: FC<IdpMappingTableProps> = ({
<Table>
<TableHeader>
<TableRow>
<TableCell width="45%">IdP {type.toLocaleLowerCase()}</TableCell>
<TableCell width="55%">Coder {type.toLocaleLowerCase()}</TableCell>
<TableCell width="5%" />
<TableCell className="w-2/5">
IdP {type.toLocaleLowerCase()}
</TableCell>
<TableCell className="w-3/5">
Coder {type.toLocaleLowerCase()}
</TableCell>
<TableCell className="w-auto" />
</TableRow>
</TableHeader>
<TableBody>

@ -24,6 +24,7 @@ import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "components/Table/Table";
@ -95,20 +96,20 @@ export const OrganizationMembersPageView: FC<
<Table>
<TableHeader>
<TableRow>
<TableCell width="33%">User</TableCell>
<TableCell width="33%">
<TableHead className="w-2/6">User</TableHead>
<TableHead className="w-2/6">
<Stack direction="row" spacing={1} alignItems="center">
<span>Roles</span>
<TableColumnHelpTooltip variant="roles" />
</Stack>
</TableCell>
<TableCell width="33%">
</TableHead>
<TableHead className="w-2/6">
<Stack direction="row" spacing={1} alignItems="center">
<span>Groups</span>
<TableColumnHelpTooltip variant="groups" />
</Stack>
</TableCell>
<TableCell width="1%" />
</TableHead>
<TableHead className="w-auto" />
</TableRow>
</TableHeader>
<TableBody>

@ -1,12 +1,13 @@
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import type { GroupsByUserId } from "api/queries/groups";
import type * as TypesGen from "api/typesGenerated";
import { Stack } from "components/Stack/Stack";
import {
Table,
TableBody,
TableHead,
TableHeader,
TableRow,
} from "components/Table/Table";
import type { FC } from "react";
import { TableColumnHelpTooltip } from "../../OrganizationSettingsPage/UserTable/TableColumnHelpTooltip";
import { UsersTableBody } from "./UsersTableBody";
@ -65,57 +66,50 @@ export const UsersTable: FC<UsersTableProps> = ({
groupsByUserId,
}) => {
return (
<TableContainer>
<Table data-testid="users-table">
<TableHead>
<TableRow>
<TableCell width="32%">{Language.usernameLabel}</TableCell>
<Table data-testid="users-table">
<TableHeader>
<TableRow>
<TableHead className="w-2/6">{Language.usernameLabel}</TableHead>
<TableHead className="w-2/6">
<Stack direction="row" spacing={1} alignItems="center">
<span>{Language.rolesLabel}</span>
<TableColumnHelpTooltip variant="roles" />
</Stack>
</TableHead>
<TableHead className="w-1/6">
<Stack direction="row" spacing={1} alignItems="center">
<span>{Language.groupsLabel}</span>
<TableColumnHelpTooltip variant="groups" />
</Stack>
</TableHead>
<TableHead className="w-1/6">{Language.loginTypeLabel}</TableHead>
<TableHead className="w-1/6">{Language.statusLabel}</TableHead>
{canEditUsers && <TableHead className="w-auto" />}
</TableRow>
</TableHeader>
<TableCell width="29%">
<Stack direction="row" spacing={1} alignItems="center">
<span>{Language.rolesLabel}</span>
<TableColumnHelpTooltip variant="roles" />
</Stack>
</TableCell>
<TableCell width="13%">
<Stack direction="row" spacing={1} alignItems="center">
<span>{Language.groupsLabel}</span>
<TableColumnHelpTooltip variant="groups" />
</Stack>
</TableCell>
<TableCell width="13%">{Language.loginTypeLabel}</TableCell>
<TableCell width="13%">{Language.statusLabel}</TableCell>
{/* 1% is a trick to make the table cell width fit the content */}
{canEditUsers && <TableCell width="1%" />}
</TableRow>
</TableHead>
<TableBody>
<UsersTableBody
users={users}
roles={roles}
groupsByUserId={groupsByUserId}
isLoading={isLoading}
canEditUsers={canEditUsers}
canViewActivity={canViewActivity}
isUpdatingUserRoles={isUpdatingUserRoles}
onActivateUser={onActivateUser}
onDeleteUser={onDeleteUser}
onListWorkspaces={onListWorkspaces}
onViewActivity={onViewActivity}
onResetUserPassword={onResetUserPassword}
onSuspendUser={onSuspendUser}
onUpdateUserRoles={onUpdateUserRoles}
isNonInitialPage={isNonInitialPage}
actorID={actorID}
oidcRoleSyncEnabled={oidcRoleSyncEnabled}
authMethods={authMethods}
/>
</TableBody>
</Table>
</TableContainer>
<TableBody>
<UsersTableBody
users={users}
roles={roles}
groupsByUserId={groupsByUserId}
isLoading={isLoading}
canEditUsers={canEditUsers}
canViewActivity={canViewActivity}
isUpdatingUserRoles={isUpdatingUserRoles}
onActivateUser={onActivateUser}
onDeleteUser={onDeleteUser}
onListWorkspaces={onListWorkspaces}
onViewActivity={onViewActivity}
onResetUserPassword={onResetUserPassword}
onSuspendUser={onSuspendUser}
onUpdateUserRoles={onUpdateUserRoles}
isNonInitialPage={isNonInitialPage}
actorID={actorID}
oidcRoleSyncEnabled={oidcRoleSyncEnabled}
authMethods={authMethods}
/>
</TableBody>
</Table>
);
};

@ -6,8 +6,6 @@ import PasswordOutlined from "@mui/icons-material/PasswordOutlined";
import ShieldOutlined from "@mui/icons-material/ShieldOutlined";
import Divider from "@mui/material/Divider";
import Skeleton from "@mui/material/Skeleton";
import TableCell from "@mui/material/TableCell";
import TableRow from "@mui/material/TableRow";
import type { GroupsByUserId } from "api/queries/groups";
import type * as TypesGen from "api/typesGenerated";
import { AvatarData } from "components/Avatar/AvatarData";
@ -23,6 +21,7 @@ import {
MoreMenuTrigger,
ThreeDotsButton,
} from "components/MoreMenu/MoreMenu";
import { TableCell, TableRow } from "components/Table/Table";
import {
TableLoaderSkeleton,
TableRowSkeleton,