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 }, },