Skip to content

Commit 0a213a6

Browse files
refactor(site): improve the overall user table design (#9342)
1 parent 14f769d commit 0a213a6

File tree

9 files changed

+198
-80
lines changed

9 files changed

+198
-80
lines changed

site/src/components/EditRolesButton/EditRolesButton.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const Option: React.FC<{
3737
onChange(e.currentTarget.value)
3838
}}
3939
/>
40-
<Stack spacing={0.5}>
40+
<Stack spacing={0}>
4141
<strong>{name}</strong>
4242
<span className={styles.optionDescription}>{description}</span>
4343
</Stack>
@@ -142,7 +142,7 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
142142
<div className={styles.footer}>
143143
<Stack direction="row" alignItems="flex-start">
144144
<UserIcon className={styles.userIcon} />
145-
<Stack spacing={0.5}>
145+
<Stack spacing={0}>
146146
<strong>{t("member")}</strong>
147147
<span className={styles.optionDescription}>
148148
{t("roleDescription.member")}
@@ -182,14 +182,15 @@ const useStyles = makeStyles((theme) => ({
182182
padding: 0,
183183

184184
"&:disabled": {
185-
opacity: 0.5,
185+
opacity: 0,
186186
},
187187
},
188188
options: {
189189
padding: theme.spacing(3),
190190
},
191191
option: {
192192
cursor: "pointer",
193+
fontSize: 14,
193194
},
194195
checkbox: {
195196
padding: 0,
@@ -202,13 +203,15 @@ const useStyles = makeStyles((theme) => ({
202203
},
203204
},
204205
optionDescription: {
205-
fontSize: 12,
206+
fontSize: 13,
206207
color: theme.palette.text.secondary,
208+
lineHeight: "160%",
207209
},
208210
footer: {
209211
padding: theme.spacing(3),
210212
backgroundColor: theme.palette.background.paper,
211213
borderTop: `1px solid ${theme.palette.divider}`,
214+
fontSize: 14,
212215
},
213216
userIcon: {
214217
width: theme.spacing(2.5), // Same as the checkbox
Lines changed: 60 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,80 @@
1-
import { ComponentMeta, Story } from "@storybook/react"
21
import {
32
MockUser,
43
MockUser2,
54
MockAssignableSiteRoles,
5+
MockAuthMethods,
66
} from "testHelpers/entities"
7-
import { UsersTable, UsersTableProps } from "./UsersTable"
7+
import { UsersTable } from "./UsersTable"
8+
import type { Meta, StoryObj } from "@storybook/react"
89

9-
export default {
10+
const meta: Meta<typeof UsersTable> = {
1011
title: "components/UsersTable",
1112
component: UsersTable,
1213
args: {
1314
isNonInitialPage: false,
15+
authMethods: MockAuthMethods,
1416
},
15-
} as ComponentMeta<typeof UsersTable>
17+
}
1618

17-
const Template: Story<UsersTableProps> = (args) => <UsersTable {...args} />
19+
export default meta
20+
type Story = StoryObj<typeof UsersTable>
1821

19-
export const Example = Template.bind({})
20-
Example.args = {
21-
users: [MockUser, MockUser2],
22-
roles: MockAssignableSiteRoles,
23-
canEditUsers: false,
22+
export const Example: Story = {
23+
args: {
24+
users: [MockUser, MockUser2],
25+
roles: MockAssignableSiteRoles,
26+
canEditUsers: false,
27+
},
2428
}
2529

26-
export const Editable = Template.bind({})
27-
Editable.args = {
28-
users: [
29-
MockUser,
30-
MockUser2,
31-
{
32-
...MockUser,
33-
username: "John Doe",
34-
email: "john.doe@coder.com",
35-
roles: [],
36-
status: "dormant",
37-
},
38-
{
39-
...MockUser,
40-
username: "Roger Moore",
41-
email: "roger.moore@coder.com",
42-
roles: [],
43-
status: "suspended",
44-
},
45-
{
46-
...MockUser,
47-
username: "OIDC User",
48-
email: "oidc.user@coder.com",
49-
roles: [],
50-
status: "active",
51-
login_type: "oidc",
52-
},
53-
],
54-
roles: MockAssignableSiteRoles,
55-
canEditUsers: true,
56-
canViewActivity: true,
30+
export const Editable: Story = {
31+
args: {
32+
users: [
33+
MockUser,
34+
MockUser2,
35+
{
36+
...MockUser,
37+
username: "John Doe",
38+
email: "john.doe@coder.com",
39+
roles: [],
40+
status: "dormant",
41+
},
42+
{
43+
...MockUser,
44+
username: "Roger Moore",
45+
email: "roger.moore@coder.com",
46+
roles: [],
47+
status: "suspended",
48+
},
49+
{
50+
...MockUser,
51+
username: "OIDC User",
52+
email: "oidc.user@coder.com",
53+
roles: [],
54+
status: "active",
55+
login_type: "oidc",
56+
},
57+
],
58+
roles: MockAssignableSiteRoles,
59+
canEditUsers: true,
60+
canViewActivity: true,
61+
},
5762
}
5863

59-
export const Empty = Template.bind({})
60-
Empty.args = {
61-
users: [],
62-
roles: MockAssignableSiteRoles,
64+
export const Empty: Story = {
65+
args: {
66+
users: [],
67+
roles: MockAssignableSiteRoles,
68+
},
6369
}
6470

65-
export const Loading = Template.bind({})
66-
Loading.args = {
67-
users: [],
68-
roles: MockAssignableSiteRoles,
69-
isLoading: true,
70-
}
71-
Loading.parameters = {
72-
chromatic: { pauseAnimationAtEnd: true },
71+
export const Loading: Story = {
72+
args: {
73+
users: [],
74+
roles: MockAssignableSiteRoles,
75+
isLoading: true,
76+
},
77+
parameters: {
78+
chromatic: { pauseAnimationAtEnd: true },
79+
},
7380
}

site/src/components/UsersTable/UsersTable.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface UsersTableProps {
3838
isNonInitialPage: boolean
3939
actorID: string
4040
oidcRoleSyncEnabled: boolean
41+
authMethods?: TypesGen.AuthMethods
4142
}
4243

4344
export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
@@ -57,6 +58,7 @@ export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
5758
isNonInitialPage,
5859
actorID,
5960
oidcRoleSyncEnabled,
61+
authMethods,
6062
}) => {
6163
return (
6264
<TableContainer>
@@ -70,10 +72,8 @@ export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
7072
<UserRoleHelpTooltip />
7173
</Stack>
7274
</TableCell>
73-
<TableCell width="10%">{Language.loginTypeLabel}</TableCell>
74-
<TableCell width="10%">{Language.statusLabel}</TableCell>
75-
<TableCell width="10%">{Language.lastSeenLabel}</TableCell>
76-
75+
<TableCell width="15%">{Language.loginTypeLabel}</TableCell>
76+
<TableCell width="15%">{Language.statusLabel}</TableCell>
7777
{/* 1% is a trick to make the table cell width fit the content */}
7878
{canEditUsers && <TableCell width="1%" />}
7979
</TableRow>
@@ -96,6 +96,7 @@ export const UsersTable: FC<React.PropsWithChildren<UsersTableProps>> = ({
9696
isNonInitialPage={isNonInitialPage}
9797
actorID={actorID}
9898
oidcRoleSyncEnabled={oidcRoleSyncEnabled}
99+
authMethods={authMethods}
99100
/>
100101
</TableBody>
101102
</Table>

site/src/components/UsersTable/UsersTableBody.tsx

Lines changed: 104 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import Box from "@mui/material/Box"
2-
import { makeStyles } from "@mui/styles"
1+
import Box, { BoxProps } from "@mui/material/Box"
2+
import { makeStyles, useTheme } from "@mui/styles"
33
import TableCell from "@mui/material/TableCell"
44
import TableRow from "@mui/material/TableRow"
55
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
6-
import { LastUsed } from "components/LastUsed/LastUsed"
76
import { Pill } from "components/Pill/Pill"
87
import { FC } from "react"
98
import { useTranslation } from "react-i18next"
@@ -16,6 +15,16 @@ import { TableRowMenu } from "../TableRowMenu/TableRowMenu"
1615
import { EditRolesButton } from "components/EditRolesButton/EditRolesButton"
1716
import { Stack } from "components/Stack/Stack"
1817
import { EnterpriseBadge } from "components/DeploySettingsLayout/Badges"
18+
import dayjs from "dayjs"
19+
import { SxProps, Theme } from "@mui/material/styles"
20+
import HideSourceOutlined from "@mui/icons-material/HideSourceOutlined"
21+
import KeyOutlined from "@mui/icons-material/KeyOutlined"
22+
import GitHub from "@mui/icons-material/GitHub"
23+
import PasswordOutlined from "@mui/icons-material/PasswordOutlined"
24+
import relativeTime from "dayjs/plugin/relativeTime"
25+
import ShieldOutlined from "@mui/icons-material/ShieldOutlined"
26+
27+
dayjs.extend(relativeTime)
1928

2029
const isOwnerRole = (role: TypesGen.Role): boolean => {
2130
return role.name === "owner"
@@ -31,6 +40,7 @@ const sortRoles = (roles: TypesGen.Role[]) => {
3140

3241
interface UsersTableBodyProps {
3342
users?: TypesGen.User[]
43+
authMethods?: TypesGen.AuthMethods
3444
roles?: TypesGen.AssignableRoles[]
3545
isUpdatingUserRoles?: boolean
3646
canEditUsers?: boolean
@@ -58,6 +68,7 @@ export const UsersTableBody: FC<
5868
React.PropsWithChildren<UsersTableBodyProps>
5969
> = ({
6070
users,
71+
authMethods,
6172
roles,
6273
onSuspendUser,
6374
onDeleteUser,
@@ -80,7 +91,7 @@ export const UsersTableBody: FC<
8091
return (
8192
<ChooseOne>
8293
<Cond condition={Boolean(isLoading)}>
83-
<TableLoaderSkeleton columns={5} useAvatarData />
94+
<TableLoaderSkeleton columns={canEditUsers ? 5 : 4} useAvatarData />
8495
</Cond>
8596
<Cond condition={!users || users.length === 0}>
8697
<ChooseOne>
@@ -156,7 +167,10 @@ export const UsersTableBody: FC<
156167
</Stack>
157168
</TableCell>
158169
<TableCell>
159-
<pre>{user.login_type}</pre>
170+
<LoginType
171+
authMethods={authMethods!}
172+
value={user.login_type}
173+
/>
160174
</TableCell>
161175
<TableCell
162176
className={combineClasses([
@@ -166,11 +180,10 @@ export const UsersTableBody: FC<
166180
: undefined,
167181
])}
168182
>
169-
{user.status}
170-
</TableCell>
171-
<TableCell>
172-
<LastUsed lastUsedAt={user.last_seen_at} />
183+
<Box>{user.status}</Box>
184+
<LastSeen value={user.last_seen_at} sx={{ fontSize: 12 }} />
173185
</TableCell>
186+
174187
{canEditUsers && (
175188
<TableCell>
176189
<TableRowMenu
@@ -236,6 +249,88 @@ export const UsersTableBody: FC<
236249
)
237250
}
238251

252+
const LoginType = ({
253+
authMethods,
254+
value,
255+
}: {
256+
authMethods: TypesGen.AuthMethods
257+
value: TypesGen.LoginType
258+
}) => {
259+
let displayName = value as string
260+
let icon = <></>
261+
const iconStyles: SxProps = { width: 14, height: 14 }
262+
263+
if (value === "password") {
264+
displayName = "Password"
265+
icon = <PasswordOutlined sx={iconStyles} />
266+
} else if (value === "none") {
267+
displayName = "None"
268+
icon = <HideSourceOutlined sx={iconStyles} />
269+
} else if (value === "github") {
270+
displayName = "GitHub"
271+
icon = <GitHub sx={iconStyles} />
272+
} else if (value === "token") {
273+
displayName = "Token"
274+
icon = <KeyOutlined sx={iconStyles} />
275+
} else if (value === "oidc") {
276+
displayName =
277+
authMethods.oidc.signInText === "" ? "OIDC" : authMethods.oidc.signInText
278+
icon =
279+
authMethods.oidc.iconUrl === "" ? (
280+
<ShieldOutlined sx={iconStyles} />
281+
) : (
282+
<Box
283+
component="img"
284+
alt="Open ID Connect icon"
285+
src={authMethods.oidc.iconUrl}
286+
sx={iconStyles}
287+
/>
288+
)
289+
}
290+
291+
return (
292+
<Box sx={{ display: "flex", alignItems: "center", gap: 1, fontSize: 14 }}>
293+
{icon}
294+
{displayName}
295+
</Box>
296+
)
297+
}
298+
299+
const LastSeen = ({ value, ...boxProps }: { value: string } & BoxProps) => {
300+
const theme: Theme = useTheme()
301+
const t = dayjs(value)
302+
const now = dayjs()
303+
304+
let message = t.fromNow()
305+
let color = theme.palette.text.secondary
306+
307+
if (t.isAfter(now.subtract(1, "hour"))) {
308+
color = theme.palette.success.light
309+
// Since the agent reports on a 10m interval,
310+
// the last_used_at can be inaccurate when recent.
311+
message = "Now"
312+
} else if (t.isAfter(now.subtract(3, "day"))) {
313+
color = theme.palette.text.secondary
314+
} else if (t.isAfter(now.subtract(1, "month"))) {
315+
color = theme.palette.warning.light
316+
} else if (t.isAfter(now.subtract(100, "year"))) {
317+
color = theme.palette.error.light
318+
} else {
319+
message = "Never"
320+
}
321+
322+
return (
323+
<Box
324+
component="span"
325+
data-chromatic="ignore"
326+
{...boxProps}
327+
sx={{ color, ...boxProps.sx }}
328+
>
329+
{message}
330+
</Box>
331+
)
332+
}
333+
239334
const useStyles = makeStyles((theme) => ({
240335
status: {
241336
textTransform: "capitalize",

0 commit comments

Comments
 (0)