Skip to content

Commit e7b8318

Browse files
refactor(site): Normalize avatar components (#5860)
1 parent 233492b commit e7b8318

File tree

26 files changed

+221
-311
lines changed

26 files changed

+221
-311
lines changed

site/.eslintrc.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ rules:
9696
message:
9797
"Use path imports to avoid pulling in unused modules. See:
9898
https://material-ui.com/guides/minimizing-bundle-size/"
99+
- name: "@material-ui/core/Avatar"
100+
message:
101+
"You should use the Avatar component provided on
102+
components/Avatar/Avatar"
99103
no-unused-vars: "off"
100104
"object-curly-spacing": "off"
101105
react-hooks/exhaustive-deps: warn
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Story } from "@storybook/react"
2+
import { Avatar, AvatarIcon, AvatarProps } from "./Avatar"
3+
import PauseIcon from "@material-ui/icons/PauseOutlined"
4+
5+
export default {
6+
title: "components/Avatar",
7+
component: Avatar,
8+
}
9+
10+
const Template: Story<AvatarProps> = (args: AvatarProps) => <Avatar {...args} />
11+
12+
export const Letter = Template.bind({})
13+
Letter.args = {
14+
children: "Coder",
15+
}
16+
17+
export const LetterXL = Template.bind({})
18+
LetterXL.args = {
19+
children: "Coder",
20+
size: "xl",
21+
}
22+
23+
export const LetterDarken = Template.bind({})
24+
LetterDarken.args = {
25+
children: "Coder",
26+
colorScheme: "darken",
27+
}
28+
29+
export const Image = Template.bind({})
30+
Image.args = {
31+
src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
32+
}
33+
34+
export const ImageXL = Template.bind({})
35+
ImageXL.args = {
36+
src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
37+
size: "xl",
38+
}
39+
40+
export const MuiIcon = Template.bind({})
41+
MuiIcon.args = {
42+
children: <PauseIcon />,
43+
}
44+
45+
export const MuiIconDarken = Template.bind({})
46+
MuiIconDarken.args = {
47+
children: <PauseIcon />,
48+
colorScheme: "darken",
49+
}
50+
51+
export const MuiIconXL = Template.bind({})
52+
MuiIconXL.args = {
53+
children: <PauseIcon />,
54+
size: "xl",
55+
}
56+
57+
export const AvatarIconDarken = Template.bind({})
58+
AvatarIconDarken.args = {
59+
children: <AvatarIcon src="/icon/database.svg" />,
60+
colorScheme: "darken",
61+
}

site/src/components/Avatar/Avatar.tsx

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// This is the only place MuiAvatar can be used
2+
// eslint-disable-next-line no-restricted-imports -- Read above
3+
import MuiAvatar, {
4+
AvatarProps as MuiAvatarProps,
5+
} from "@material-ui/core/Avatar"
6+
import { makeStyles } from "@material-ui/core/styles"
7+
import { FC } from "react"
8+
import { combineClasses } from "util/combineClasses"
9+
import { firstLetter } from "./firstLetter"
10+
11+
export type AvatarProps = MuiAvatarProps & {
12+
size?: "md" | "xl"
13+
colorScheme?: "light" | "darken"
14+
fitImage?: boolean
15+
}
16+
17+
export const Avatar: FC<AvatarProps> = ({
18+
size = "md",
19+
colorScheme = "light",
20+
fitImage,
21+
className,
22+
children,
23+
...muiProps
24+
}) => {
25+
const styles = useStyles()
26+
27+
return (
28+
<MuiAvatar
29+
{...muiProps}
30+
className={combineClasses([
31+
className,
32+
styles[size],
33+
styles[colorScheme],
34+
fitImage && styles.fitImage,
35+
])}
36+
>
37+
{/* If the children is a string, we always want to render the first letter */}
38+
{typeof children === "string" ? firstLetter(children) : children}
39+
</MuiAvatar>
40+
)
41+
}
42+
43+
/**
44+
* Use it to make an img element behaves like a MaterialUI Icon component
45+
*/
46+
export const AvatarIcon: FC<{ src: string }> = ({ src }) => {
47+
const styles = useStyles()
48+
return <img src={src} alt="" className={styles.avatarIcon} />
49+
}
50+
51+
const useStyles = makeStyles((theme) => ({
52+
// Size styles
53+
// Just use the default value from theme
54+
md: {},
55+
xl: {
56+
width: theme.spacing(6),
57+
height: theme.spacing(6),
58+
fontSize: theme.spacing(3),
59+
},
60+
// Colors
61+
// Just use the default value from theme
62+
light: {},
63+
darken: {
64+
background: theme.palette.divider,
65+
color: theme.palette.text.primary,
66+
},
67+
// Avatar icon
68+
avatarIcon: {
69+
maxWidth: "50%",
70+
},
71+
// Fit image
72+
fitImage: {
73+
"& .MuiAvatar-img": {
74+
objectFit: "contain",
75+
},
76+
},
77+
}))

site/src/components/AvatarData/AvatarData.stories.tsx

+3-10
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,9 @@ Example.args = {
1616
subtitle: "coder@coder.com",
1717
}
1818

19-
export const WithHighlightTitle = Template.bind({})
20-
WithHighlightTitle.args = {
19+
export const WithImage = Template.bind({})
20+
WithImage.args = {
2121
title: "coder",
2222
subtitle: "coder@coder.com",
23-
highlightTitle: true,
24-
}
25-
26-
export const WithLink = Template.bind({})
27-
WithLink.args = {
28-
title: "coder",
29-
subtitle: "coder@coder.com",
30-
link: "/users/coder",
23+
src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
3124
}
+23-44
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,50 @@
1-
import Avatar from "@material-ui/core/Avatar"
2-
import Link from "@material-ui/core/Link"
3-
import { makeStyles } from "@material-ui/core/styles"
1+
import { Avatar } from "components/Avatar/Avatar"
42
import { FC, PropsWithChildren } from "react"
5-
import { Link as RouterLink } from "react-router-dom"
6-
import { firstLetter } from "../../util/firstLetter"
7-
import {
8-
TableCellData,
9-
TableCellDataPrimary,
10-
TableCellDataSecondary,
11-
} from "../TableCellData/TableCellData"
3+
import { Stack } from "components/Stack/Stack"
4+
import { makeStyles } from "@material-ui/core/styles"
125

136
export interface AvatarDataProps {
147
title: string
158
subtitle?: string
16-
highlightTitle?: boolean
17-
link?: string
9+
src?: string
1810
avatar?: React.ReactNode
1911
}
2012

2113
export const AvatarData: FC<PropsWithChildren<AvatarDataProps>> = ({
2214
title,
2315
subtitle,
24-
link,
25-
highlightTitle,
16+
src,
2617
avatar,
2718
}) => {
2819
const styles = useStyles()
2920

3021
if (!avatar) {
31-
avatar = <Avatar>{firstLetter(title)}</Avatar>
22+
avatar = <Avatar src={src}>{title}</Avatar>
3223
}
3324

3425
return (
35-
<div className={styles.root}>
36-
<div className={styles.avatarWrapper}>{avatar}</div>
26+
<Stack spacing={1.5} direction="row" alignItems="center">
27+
{avatar}
3728

38-
{link ? (
39-
<Link to={link} underline="none" component={RouterLink}>
40-
<TableCellData>
41-
<TableCellDataPrimary highlight={highlightTitle}>
42-
{title}
43-
</TableCellDataPrimary>
44-
{subtitle && (
45-
<TableCellDataSecondary>{subtitle}</TableCellDataSecondary>
46-
)}
47-
</TableCellData>
48-
</Link>
49-
) : (
50-
<TableCellData>
51-
<TableCellDataPrimary highlight={highlightTitle}>
52-
{title}
53-
</TableCellDataPrimary>
54-
{subtitle && (
55-
<TableCellDataSecondary>{subtitle}</TableCellDataSecondary>
56-
)}
57-
</TableCellData>
58-
)}
59-
</div>
29+
<Stack spacing={0}>
30+
<span className={styles.title}>{title}</span>
31+
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
32+
</Stack>
33+
</Stack>
6034
)
6135
}
6236

6337
const useStyles = makeStyles((theme) => ({
64-
root: {
65-
display: "flex",
66-
alignItems: "center",
38+
title: {
39+
color: theme.palette.text.primary,
40+
fontWeight: 600,
6741
},
68-
avatarWrapper: {
69-
marginRight: theme.spacing(1.5),
42+
43+
subtitle: {
44+
fontSize: 12,
45+
color: theme.palette.text.secondary,
46+
lineHeight: "140%",
47+
marginTop: 2,
48+
maxWidth: 540,
7049
},
7150
}))

site/src/components/BuildsTable/BuildAvatar.tsx

+5-23
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import Avatar from "@material-ui/core/Avatar"
21
import Badge from "@material-ui/core/Badge"
32
import { Theme, useTheme, withStyles } from "@material-ui/core/styles"
43
import { FC } from "react"
@@ -8,6 +7,7 @@ import DeleteOutlined from "@material-ui/icons/DeleteOutlined"
87
import { WorkspaceBuild, WorkspaceTransition } from "api/typesGenerated"
98
import { getDisplayWorkspaceBuildStatus } from "util/workspace"
109
import { PaletteIndex } from "theme/palettes"
10+
import { Avatar, AvatarProps } from "components/Avatar/Avatar"
1111

1212
interface StylesBadgeProps {
1313
type: PaletteIndex
@@ -25,27 +25,9 @@ const StyledBadge = withStyles((theme) => ({
2525
},
2626
}))(Badge)
2727

28-
interface StyledAvatarProps {
29-
size?: number
30-
}
31-
32-
const StyledAvatar = withStyles((theme) => ({
33-
root: {
34-
background: theme.palette.divider,
35-
color: theme.palette.text.primary,
36-
border: `2px solid ${theme.palette.divider}`,
37-
width: ({ size }: StyledAvatarProps) => size,
38-
height: ({ size }: StyledAvatarProps) => size,
39-
40-
"& svg": {
41-
width: ({ size }: StyledAvatarProps) => (size ? size / 2 : 18),
42-
height: ({ size }: StyledAvatarProps) => (size ? size / 2 : 18),
43-
},
44-
},
45-
}))(Avatar)
46-
47-
export interface BuildAvatarProps extends StyledAvatarProps {
28+
export interface BuildAvatarProps {
4829
build: WorkspaceBuild
30+
size?: AvatarProps["size"]
4931
}
5032

5133
const iconByTransition: Record<WorkspaceTransition, JSX.Element> = {
@@ -71,9 +53,9 @@ export const BuildAvatar: FC<BuildAvatarProps> = ({ build, size }) => {
7153
}}
7254
badgeContent={<div></div>}
7355
>
74-
<StyledAvatar size={size}>
56+
<Avatar size={size} colorScheme="darken">
7557
{iconByTransition[build.transition]}
76-
</StyledAvatar>
58+
</Avatar>
7759
</StyledBadge>
7860
)
7961
}

site/src/components/GroupAvatar/GroupAvatar.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import Avatar from "@material-ui/core/Avatar"
1+
import { Avatar } from "components/Avatar/Avatar"
22
import Badge from "@material-ui/core/Badge"
33
import { withStyles } from "@material-ui/core/styles"
44
import Group from "@material-ui/icons/Group"
55
import { FC } from "react"
6-
import { firstLetter } from "util/firstLetter"
76

87
const StyledBadge = withStyles((theme) => ({
98
badge: {
@@ -38,7 +37,7 @@ export const GroupAvatar: FC<GroupAvatarProps> = ({ name, avatarURL }) => {
3837
}}
3938
badgeContent={<Group />}
4039
>
41-
<Avatar src={avatarURL}>{firstLetter(name)}</Avatar>
40+
<Avatar src={avatarURL}>{name}</Avatar>
4241
</StyledBadge>
4342
)
4443
}

site/src/components/Resources/ResourceAvatar.tsx

+6-16
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import Avatar from "@material-ui/core/Avatar"
2-
import { makeStyles } from "@material-ui/core/styles"
1+
import { Avatar, AvatarIcon } from "components/Avatar/Avatar"
32
import { FC } from "react"
43
import { WorkspaceResource } from "../../api/typesGenerated"
54

65
const FALLBACK_ICON = "/icon/widgets.svg"
76

8-
// NOTE @jsjoeio, @BrunoQuaresma
97
// These resources (i.e. docker_image, kubernetes_deployment) map to Terraform
108
// resource types. These are the most used ones and are based on user usage.
119
// We may want to update from time-to-time.
@@ -37,18 +35,10 @@ export type ResourceAvatarProps = { resource: WorkspaceResource }
3735
export const ResourceAvatar: FC<ResourceAvatarProps> = ({ resource }) => {
3836
const hasIcon = resource.icon && resource.icon !== ""
3937
const avatarSrc = hasIcon ? resource.icon : getIconPathResource(resource.type)
40-
const styles = useStyles()
4138

42-
return <Avatar className={styles.resourceAvatar} src={avatarSrc} />
39+
return (
40+
<Avatar colorScheme="darken">
41+
<AvatarIcon src={avatarSrc} />
42+
</Avatar>
43+
)
4344
}
44-
45-
const useStyles = makeStyles((theme) => ({
46-
resourceAvatar: {
47-
backgroundColor: theme.palette.divider,
48-
49-
"& img": {
50-
width: 18,
51-
height: 18,
52-
},
53-
},
54-
}))

0 commit comments

Comments
 (0)