Skip to content

Commit 12f87cb

Browse files
refactor(site): Show update notification as snackbar (#7546)
1 parent a7f14f8 commit 12f87cb

File tree

7 files changed

+114
-103
lines changed

7 files changed

+114
-103
lines changed

docs/contributing/frontend.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,31 @@ user.click(screen.getByRole("button"))
166166
const form = screen.getByTestId("form")
167167
user.click(within(form).getByRole("button"))
168168
```
169+
170+
#### `jest.spyOn` with the API is not working
171+
172+
For some unknown reason, we figured out the `jest.spyOn` is not able to mock the API function when they are passed directly into the services XState machine configuration.
173+
174+
❌ Does not work
175+
176+
```ts
177+
import { getUpdateCheck } from "api/api"
178+
179+
createMachine({ ... }, {
180+
services: {
181+
getUpdateCheck,
182+
},
183+
})
184+
```
185+
186+
✅ It works
187+
188+
```ts
189+
import { getUpdateCheck } from "api/api"
190+
191+
createMachine({ ... }, {
192+
services: {
193+
getUpdateCheck: () => getUpdateCheck(),
194+
},
195+
})
196+
```
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Route, Routes } from "react-router-dom"
2+
import { renderWithAuth } from "testHelpers/renderHelpers"
3+
import { DashboardLayout } from "./DashboardLayout"
4+
import * as API from "api/api"
5+
import { screen } from "@testing-library/react"
6+
7+
test("Show the new Coder version notification", async () => {
8+
jest.spyOn(API, "getUpdateCheck").mockResolvedValue({
9+
current: false,
10+
version: "v0.12.9",
11+
url: "https://github.com/coder/coder/releases/tag/v0.12.9",
12+
})
13+
renderWithAuth(
14+
<Routes>
15+
<Route element={<DashboardLayout />}>
16+
<Route element={<h1>Test page</h1>} />
17+
</Route>
18+
</Routes>,
19+
)
20+
await screen.findByTestId("update-check-snackbar")
21+
})
Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import { makeStyles } from "@mui/styles"
22
import { useMachine } from "@xstate/react"
3-
import { UpdateCheckResponse } from "api/typesGenerated"
43
import { DeploymentBanner } from "components/DeploymentBanner/DeploymentBanner"
54
import { LicenseBanner } from "components/LicenseBanner/LicenseBanner"
65
import { Loader } from "components/Loader/Loader"
7-
import { Margins } from "components/Margins/Margins"
86
import { ServiceBanner } from "components/ServiceBanner/ServiceBanner"
9-
import { UpdateCheckBanner } from "components/UpdateCheckBanner/UpdateCheckBanner"
107
import { usePermissions } from "hooks/usePermissions"
118
import { FC, Suspense } from "react"
129
import { Outlet } from "react-router-dom"
1310
import { dashboardContentBottomPadding } from "theme/constants"
1411
import { updateCheckMachine } from "xServices/updateCheck/updateCheckXService"
1512
import { Navbar } from "../Navbar/Navbar"
13+
import Snackbar from "@mui/material/Snackbar"
14+
import Link from "@mui/material/Link"
15+
import Box from "@mui/material/Box"
16+
import InfoOutlined from "@mui/icons-material/InfoOutlined"
17+
import Button from "@mui/material/Button"
1618

1719
export const DashboardLayout: FC = () => {
1820
const styles = useStyles()
@@ -22,8 +24,7 @@ export const DashboardLayout: FC = () => {
2224
permissions,
2325
},
2426
})
25-
const { error: updateCheckError, updateCheck } = updateCheckState.context
26-
27+
const { updateCheck } = updateCheckState.context
2728
const canViewDeployment = Boolean(permissions.viewDeploymentValues)
2829

2930
return (
@@ -34,48 +35,80 @@ export const DashboardLayout: FC = () => {
3435
<div className={styles.site}>
3536
<Navbar />
3637

37-
{updateCheckState.matches("show") && (
38-
<div className={styles.updateCheckBanner}>
39-
<Margins>
40-
<UpdateCheckBanner
41-
// We can trust when it is show, the update check is filled
42-
// unfortunately, XState does not has typed state - context yet
43-
updateCheck={updateCheck as UpdateCheckResponse}
44-
error={updateCheckError}
45-
onDismiss={() => updateCheckSend("DISMISS")}
46-
/>
47-
</Margins>
48-
</div>
49-
)}
50-
5138
<div className={styles.siteContent}>
5239
<Suspense fallback={<Loader />}>
5340
<Outlet />
5441
</Suspense>
5542
</div>
5643

5744
<DeploymentBanner />
45+
46+
<Snackbar
47+
data-testid="update-check-snackbar"
48+
open={updateCheckState.matches("show")}
49+
anchorOrigin={{
50+
vertical: "bottom",
51+
horizontal: "right",
52+
}}
53+
ContentProps={{
54+
sx: (theme) => ({
55+
background: theme.palette.background.paper,
56+
color: theme.palette.text.primary,
57+
maxWidth: theme.spacing(55),
58+
flexDirection: "row",
59+
borderColor: theme.palette.info.light,
60+
61+
"& .MuiSnackbarContent-message": {
62+
flex: 1,
63+
},
64+
65+
"& .MuiSnackbarContent-action": {
66+
marginRight: 0,
67+
},
68+
}),
69+
}}
70+
message={
71+
<Box display="flex" gap={2}>
72+
<InfoOutlined
73+
sx={(theme) => ({
74+
fontSize: 16,
75+
height: 20, // 20 is the height of the text line so we can align them
76+
color: theme.palette.info.light,
77+
})}
78+
/>
79+
<Box>
80+
Coder {updateCheck?.version} is now available. View the{" "}
81+
<Link href={updateCheck?.url}>release notes</Link> and{" "}
82+
<Link href="https://coder.com/docs/coder-oss/latest/admin/upgrade">
83+
upgrade instructions
84+
</Link>{" "}
85+
for more information.
86+
</Box>
87+
</Box>
88+
}
89+
action={
90+
<Button
91+
variant="text"
92+
size="small"
93+
onClick={() => updateCheckSend("DISMISS")}
94+
>
95+
Dismiss
96+
</Button>
97+
}
98+
/>
5899
</div>
59100
</>
60101
)
61102
}
62103

63-
const useStyles = makeStyles((theme) => ({
104+
const useStyles = makeStyles({
64105
site: {
65106
display: "flex",
66107
minHeight: "100vh",
67108
flexDirection: "column",
68109
},
69-
updateCheckBanner: {
70-
// Add spacing at the top and remove some from the bottom. Removal
71-
// is necessary to avoid a visual jerk when the banner is dismissed.
72-
// It also give a more pleasant distance to the site content when
73-
// the banner is visible.
74-
marginTop: theme.spacing(2),
75-
marginBottom: theme.spacing(-2),
76-
},
77110
siteContent: {
78111
flex: 1,
79112
paddingBottom: dashboardContentBottomPadding, // Add bottom space since we don't use a footer
80113
},
81-
}))
114+
})

site/src/components/UpdateCheckBanner/UpdateCheckBanner.stories.tsx

Lines changed: 0 additions & 25 deletions
This file was deleted.

site/src/components/UpdateCheckBanner/UpdateCheckBanner.tsx

Lines changed: 0 additions & 47 deletions
This file was deleted.

site/src/theme/theme.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export let dark = createTheme({
4848
dark: colors.green[15],
4949
},
5050
info: {
51+
light: colors.blue[9],
5152
main: colors.blue[11],
5253
dark: colors.blue[15],
5354
contrastText: colors.gray[4],

site/src/xServices/updateCheck/updateCheckXService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ export const updateCheckMachine = createMachine(
7878
},
7979
{
8080
services: {
81-
getUpdateCheck,
81+
// For some reason, when passing values directly, jest.spy does not work.
82+
getUpdateCheck: () => getUpdateCheck(),
8283
},
8384
actions: {
8485
assignUpdateCheck: assign({
@@ -101,7 +102,6 @@ export const updateCheckMachine = createMachine(
101102
shouldShowUpdateCheck: (_, { data }) => {
102103
const isNotDismissed = getDismissedVersionOnLocal() !== data.version
103104
const isOutdated = !data.current
104-
105105
return isNotDismissed && isOutdated
106106
},
107107
},

0 commit comments

Comments
 (0)