diff --git a/docs/contributing/frontend.md b/docs/contributing/frontend.md
index 194b1dfa78e9a..03ebd41fa28a0 100644
--- a/docs/contributing/frontend.md
+++ b/docs/contributing/frontend.md
@@ -166,3 +166,31 @@ user.click(screen.getByRole("button"))
const form = screen.getByTestId("form")
user.click(within(form).getByRole("button"))
```
+
+#### `jest.spyOn` with the API is not working
+
+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.
+
+❌ Does not work
+
+```ts
+import { getUpdateCheck } from "api/api"
+
+createMachine({ ... }, {
+ services: {
+ getUpdateCheck,
+ },
+})
+```
+
+✅ It works
+
+```ts
+import { getUpdateCheck } from "api/api"
+
+createMachine({ ... }, {
+ services: {
+ getUpdateCheck: () => getUpdateCheck(),
+ },
+})
+```
diff --git a/site/src/components/Dashboard/DashboardLayout.test.tsx b/site/src/components/Dashboard/DashboardLayout.test.tsx
new file mode 100644
index 0000000000000..49ca5e30576c0
--- /dev/null
+++ b/site/src/components/Dashboard/DashboardLayout.test.tsx
@@ -0,0 +1,21 @@
+import { Route, Routes } from "react-router-dom"
+import { renderWithAuth } from "testHelpers/renderHelpers"
+import { DashboardLayout } from "./DashboardLayout"
+import * as API from "api/api"
+import { screen } from "@testing-library/react"
+
+test("Show the new Coder version notification", async () => {
+ jest.spyOn(API, "getUpdateCheck").mockResolvedValue({
+ current: false,
+ version: "v0.12.9",
+ url: "https://github.com/coder/coder/releases/tag/v0.12.9",
+ })
+ renderWithAuth(
+
+ }>
+ Test page} />
+
+ ,
+ )
+ await screen.findByTestId("update-check-snackbar")
+})
diff --git a/site/src/components/Dashboard/DashboardLayout.tsx b/site/src/components/Dashboard/DashboardLayout.tsx
index a7ce6b500ef6c..6aa8db92785ed 100644
--- a/site/src/components/Dashboard/DashboardLayout.tsx
+++ b/site/src/components/Dashboard/DashboardLayout.tsx
@@ -1,18 +1,20 @@
import { makeStyles } from "@mui/styles"
import { useMachine } from "@xstate/react"
-import { UpdateCheckResponse } from "api/typesGenerated"
import { DeploymentBanner } from "components/DeploymentBanner/DeploymentBanner"
import { LicenseBanner } from "components/LicenseBanner/LicenseBanner"
import { Loader } from "components/Loader/Loader"
-import { Margins } from "components/Margins/Margins"
import { ServiceBanner } from "components/ServiceBanner/ServiceBanner"
-import { UpdateCheckBanner } from "components/UpdateCheckBanner/UpdateCheckBanner"
import { usePermissions } from "hooks/usePermissions"
import { FC, Suspense } from "react"
import { Outlet } from "react-router-dom"
import { dashboardContentBottomPadding } from "theme/constants"
import { updateCheckMachine } from "xServices/updateCheck/updateCheckXService"
import { Navbar } from "../Navbar/Navbar"
+import Snackbar from "@mui/material/Snackbar"
+import Link from "@mui/material/Link"
+import Box from "@mui/material/Box"
+import InfoOutlined from "@mui/icons-material/InfoOutlined"
+import Button from "@mui/material/Button"
export const DashboardLayout: FC = () => {
const styles = useStyles()
@@ -22,8 +24,7 @@ export const DashboardLayout: FC = () => {
permissions,
},
})
- const { error: updateCheckError, updateCheck } = updateCheckState.context
-
+ const { updateCheck } = updateCheckState.context
const canViewDeployment = Boolean(permissions.viewDeploymentValues)
return (
@@ -34,20 +35,6 @@ export const DashboardLayout: FC = () => {
- {updateCheckState.matches("show") && (
-
-
- updateCheckSend("DISMISS")}
- />
-
-
- )}
-
}>
@@ -55,27 +42,73 @@ export const DashboardLayout: FC = () => {
+
+
({
+ background: theme.palette.background.paper,
+ color: theme.palette.text.primary,
+ maxWidth: theme.spacing(55),
+ flexDirection: "row",
+ borderColor: theme.palette.info.light,
+
+ "& .MuiSnackbarContent-message": {
+ flex: 1,
+ },
+
+ "& .MuiSnackbarContent-action": {
+ marginRight: 0,
+ },
+ }),
+ }}
+ message={
+
+ ({
+ fontSize: 16,
+ height: 20, // 20 is the height of the text line so we can align them
+ color: theme.palette.info.light,
+ })}
+ />
+
+ Coder {updateCheck?.version} is now available. View the{" "}
+ release notes and{" "}
+
+ upgrade instructions
+ {" "}
+ for more information.
+
+
+ }
+ action={
+
+ }
+ />
>
)
}
-const useStyles = makeStyles((theme) => ({
+const useStyles = makeStyles({
site: {
display: "flex",
minHeight: "100vh",
flexDirection: "column",
},
- updateCheckBanner: {
- // Add spacing at the top and remove some from the bottom. Removal
- // is necessary to avoid a visual jerk when the banner is dismissed.
- // It also give a more pleasant distance to the site content when
- // the banner is visible.
- marginTop: theme.spacing(2),
- marginBottom: theme.spacing(-2),
- },
siteContent: {
flex: 1,
paddingBottom: dashboardContentBottomPadding, // Add bottom space since we don't use a footer
},
-}))
+})
diff --git a/site/src/components/UpdateCheckBanner/UpdateCheckBanner.stories.tsx b/site/src/components/UpdateCheckBanner/UpdateCheckBanner.stories.tsx
deleted file mode 100644
index dc963c8e11a10..0000000000000
--- a/site/src/components/UpdateCheckBanner/UpdateCheckBanner.stories.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { ComponentMeta, Story } from "@storybook/react"
-import { UpdateCheckBanner, UpdateCheckBannerProps } from "./UpdateCheckBanner"
-
-export default {
- title: "components/UpdateCheckBanner",
- component: UpdateCheckBanner,
-} as ComponentMeta
-
-const Template: Story = (args) => (
-
-)
-
-export const UpdateAvailable = Template.bind({})
-UpdateAvailable.args = {
- updateCheck: {
- current: false,
- version: "v0.12.9",
- url: "https://github.com/coder/coder/releases/tag/v0.12.9",
- },
-}
-
-export const UpdateCheckError = Template.bind({})
-UpdateCheckError.args = {
- error: new Error("Something went wrong."),
-}
diff --git a/site/src/components/UpdateCheckBanner/UpdateCheckBanner.tsx b/site/src/components/UpdateCheckBanner/UpdateCheckBanner.tsx
deleted file mode 100644
index c3511180c8765..0000000000000
--- a/site/src/components/UpdateCheckBanner/UpdateCheckBanner.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import Link from "@mui/material/Link"
-import { AlertBanner } from "components/AlertBanner/AlertBanner"
-import { Trans, useTranslation } from "react-i18next"
-import * as TypesGen from "api/typesGenerated"
-import { FC } from "react"
-
-export interface UpdateCheckBannerProps {
- updateCheck: TypesGen.UpdateCheckResponse
- error?: unknown
- onDismiss: () => void
-}
-
-export const UpdateCheckBanner: FC<
- React.PropsWithChildren
-> = ({ updateCheck, error, onDismiss }) => {
- const { t } = useTranslation("common")
-
- return (
-
- <>
- {error ? (
- t("updateCheck.error")
- ) : (
-
-
- Coder {"{{version}}"} is now available. View the{" "}
- release notes and{" "}
-
- upgrade instructions
- {" "}
- for more information.
-
-
- )}
- >
-
- )
-}
diff --git a/site/src/theme/theme.ts b/site/src/theme/theme.ts
index 3dccdd5b9c2b3..12f029bffa9f0 100644
--- a/site/src/theme/theme.ts
+++ b/site/src/theme/theme.ts
@@ -48,6 +48,7 @@ export let dark = createTheme({
dark: colors.green[15],
},
info: {
+ light: colors.blue[9],
main: colors.blue[11],
dark: colors.blue[15],
contrastText: colors.gray[4],
diff --git a/site/src/xServices/updateCheck/updateCheckXService.ts b/site/src/xServices/updateCheck/updateCheckXService.ts
index e90d73c2eda23..c280ff306a0f4 100644
--- a/site/src/xServices/updateCheck/updateCheckXService.ts
+++ b/site/src/xServices/updateCheck/updateCheckXService.ts
@@ -78,7 +78,8 @@ export const updateCheckMachine = createMachine(
},
{
services: {
- getUpdateCheck,
+ // For some reason, when passing values directly, jest.spy does not work.
+ getUpdateCheck: () => getUpdateCheck(),
},
actions: {
assignUpdateCheck: assign({
@@ -101,7 +102,6 @@ export const updateCheckMachine = createMachine(
shouldShowUpdateCheck: (_, { data }) => {
const isNotDismissed = getDismissedVersionOnLocal() !== data.version
const isOutdated = !data.current
-
return isNotDismissed && isOutdated
},
},