chore(site): remove update check service (#10355)

This commit is contained in:
Bruno Quaresma
2023-10-20 09:41:34 -03:00
committed by GitHub
parent 1ba5169109
commit 3d9bfdd5dc
7 changed files with 194 additions and 282 deletions
+8
View File
@@ -0,0 +1,8 @@
import * as API from "api/api";
export const updateCheck = () => {
return {
queryKey: ["updateCheck"],
queryFn: () => API.getUpdateCheck(),
};
};
@@ -1,14 +1,22 @@
import { renderWithAuth } from "testHelpers/renderHelpers";
import { DashboardLayout } from "./DashboardLayout";
import * as API from "api/api";
import { screen } from "@testing-library/react";
import { rest } from "msw";
import { server } from "testHelpers/server";
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",
});
server.use(
rest.get("/api/v2/updatecheck", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
current: false,
version: "v0.12.9",
url: "https://github.com/coder/coder/releases/tag/v0.12.9",
}),
);
}),
);
renderWithAuth(<DashboardLayout />, {
children: [{ element: <h1>Test page</h1> }],
});
@@ -1,4 +1,3 @@
import { useMachine } from "@xstate/react";
import { DeploymentBanner } from "./DeploymentBanner/DeploymentBanner";
import { LicenseBanner } from "components/Dashboard/LicenseBanner/LicenseBanner";
import { Loader } from "components/Loader/Loader";
@@ -7,7 +6,6 @@ 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";
@@ -15,15 +13,11 @@ import Box, { BoxProps } from "@mui/material/Box";
import InfoOutlined from "@mui/icons-material/InfoOutlined";
import Button from "@mui/material/Button";
import { docs } from "utils/docs";
import { useUpdateCheck } from "./useUpdateCheck";
export const DashboardLayout: FC = () => {
const permissions = usePermissions();
const [updateCheckState, updateCheckSend] = useMachine(updateCheckMachine, {
context: {
permissions,
},
});
const { updateCheck } = updateCheckState.context;
const updateCheck = useUpdateCheck(permissions.viewUpdateCheck);
const canViewDeployment = Boolean(permissions.viewDeploymentValues);
return (
@@ -57,7 +51,7 @@ export const DashboardLayout: FC = () => {
<Snackbar
data-testid="update-check-snackbar"
open={updateCheckState.matches("show")}
open={updateCheck.isVisible}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
@@ -89,19 +83,15 @@ export const DashboardLayout: FC = () => {
})}
/>
<Box>
Coder {updateCheck?.version} is now available. View the{" "}
<Link href={updateCheck?.url}>release notes</Link> and{" "}
Coder {updateCheck.data?.version} is now available. View the{" "}
<Link href={updateCheck.data?.url}>release notes</Link> and{" "}
<Link href={docs("/admin/upgrade")}>upgrade instructions</Link>{" "}
for more information.
</Box>
</Box>
}
action={
<Button
variant="text"
size="small"
onClick={() => updateCheckSend("DISMISS")}
>
<Button variant="text" size="small" onClick={updateCheck.dismiss}>
Dismiss
</Button>
}
@@ -0,0 +1,121 @@
import { act, renderHook, waitFor } from "@testing-library/react";
import { useUpdateCheck } from "./useUpdateCheck";
import { QueryClient, QueryClientProvider } from "react-query";
import { ReactNode } from "react";
import { rest } from "msw";
import { MockUpdateCheck } from "testHelpers/entities";
import { server } from "testHelpers/server";
const createWrapper = () => {
const queryClient = new QueryClient();
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
beforeEach(() => {
window.localStorage.clear();
});
it("is dismissed when does not have permission to see it", () => {
const { result } = renderHook(() => useUpdateCheck(false), {
wrapper: createWrapper(),
});
expect(result.current.isVisible).toBeFalsy();
});
it("is dismissed when it is already using current version", async () => {
server.use(
rest.get("/api/v2/updatecheck", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
...MockUpdateCheck,
current: true,
}),
);
}),
);
const { result } = renderHook(() => useUpdateCheck(true), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isVisible).toBeFalsy();
});
});
it("is dismissed when it was dismissed previously", async () => {
server.use(
rest.get("/api/v2/updatecheck", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
...MockUpdateCheck,
current: false,
}),
);
}),
);
window.localStorage.setItem("dismissedVersion", MockUpdateCheck.version);
const { result } = renderHook(() => useUpdateCheck(true), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isVisible).toBeFalsy();
});
});
it("shows when has permission and is outdated", async () => {
server.use(
rest.get("/api/v2/updatecheck", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
...MockUpdateCheck,
current: false,
}),
);
}),
);
const { result } = renderHook(() => useUpdateCheck(true), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isVisible).toBeTruthy();
});
});
it("shows when has permission and is outdated", async () => {
server.use(
rest.get("/api/v2/updatecheck", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
...MockUpdateCheck,
current: false,
}),
);
}),
);
const { result } = renderHook(() => useUpdateCheck(true), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isVisible).toBeTruthy();
});
act(() => {
result.current.dismiss();
});
await waitFor(() => {
expect(result.current.isVisible).toBeFalsy();
});
expect(localStorage.getItem("dismissedVersion")).toEqual(
MockUpdateCheck.version,
);
});
@@ -0,0 +1,45 @@
import { updateCheck } from "api/queries/updateCheck";
import { useMemo, useState } from "react";
import { useQuery } from "react-query";
export const useUpdateCheck = (enabled: boolean) => {
const [dismissedVersion, setDismissedVersion] = useState(() =>
getDismissedVersionOnLocal(),
);
const updateCheckQuery = useQuery({
...updateCheck(),
enabled,
});
const isVisible: boolean = useMemo(() => {
if (!updateCheckQuery.data) {
return false;
}
const isNotDismissed = dismissedVersion !== updateCheckQuery.data.version;
const isOutdated = !updateCheckQuery.data.current;
return isNotDismissed && isOutdated ? true : false;
}, [dismissedVersion, updateCheckQuery.data]);
const dismiss = () => {
if (!updateCheckQuery.data) {
return;
}
setDismissedVersion(updateCheckQuery.data.version);
saveDismissedVersionOnLocal(updateCheckQuery.data.version);
};
return {
isVisible,
dismiss,
data: updateCheckQuery.data,
};
};
const saveDismissedVersionOnLocal = (version: string): void => {
window.localStorage.setItem("dismissedVersion", version);
};
const getDismissedVersionOnLocal = (): string | undefined => {
return localStorage.getItem("dismissedVersion") ?? undefined;
};
@@ -1,138 +0,0 @@
import { waitFor } from "@testing-library/react";
import { MockPermissions, MockUpdateCheck } from "testHelpers/entities";
import { interpret } from "xstate";
import {
clearDismissedVersionOnLocal,
getDismissedVersionOnLocal,
saveDismissedVersionOnLocal,
updateCheckMachine,
} from "./updateCheckXService";
describe("updateCheckMachine", () => {
beforeEach(() => {
clearDismissedVersionOnLocal();
});
it("is dismissed when does not have permission to see it", () => {
const machine = updateCheckMachine.withContext({
permissions: {
...MockPermissions,
viewUpdateCheck: false,
},
});
const updateCheckService = interpret(machine);
updateCheckService.start();
expect(updateCheckService.state.matches("dismissed")).toBeTruthy();
});
it("is dismissed when it is already using current version", async () => {
const machine = updateCheckMachine
.withContext({
permissions: {
...MockPermissions,
viewUpdateCheck: true,
},
})
.withConfig({
services: {
getUpdateCheck: () =>
Promise.resolve({
...MockUpdateCheck,
current: true,
}),
},
});
const updateCheckService = interpret(machine);
updateCheckService.start();
await waitFor(() => {
expect(updateCheckService.state.matches("dismissed")).toBeTruthy();
});
});
it("is dismissed when it was dismissed previously", async () => {
const machine = updateCheckMachine
.withContext({
permissions: {
...MockPermissions,
viewUpdateCheck: true,
},
})
.withConfig({
services: {
getUpdateCheck: () =>
Promise.resolve({
...MockUpdateCheck,
current: false,
}),
},
});
saveDismissedVersionOnLocal(MockUpdateCheck.version);
const updateCheckService = interpret(machine);
updateCheckService.start();
await waitFor(() => {
expect(updateCheckService.state.matches("dismissed")).toBeTruthy();
});
});
it("shows when has permission and is outdated", async () => {
const machine = updateCheckMachine
.withContext({
permissions: {
...MockPermissions,
viewUpdateCheck: true,
},
})
.withConfig({
services: {
getUpdateCheck: () =>
Promise.resolve({
...MockUpdateCheck,
current: false,
}),
},
});
const updateCheckService = interpret(machine);
updateCheckService.start();
await waitFor(() => {
expect(updateCheckService.state.matches("show")).toBeTruthy();
});
});
it("it is dismissed when the DISMISS event happens", async () => {
const machine = updateCheckMachine
.withContext({
permissions: {
...MockPermissions,
viewUpdateCheck: true,
},
})
.withConfig({
services: {
getUpdateCheck: () =>
Promise.resolve({
...MockUpdateCheck,
current: false,
}),
},
});
const updateCheckService = interpret(machine);
updateCheckService.start();
await waitFor(() => {
expect(updateCheckService.state.matches("show")).toBeTruthy();
});
updateCheckService.send("DISMISS");
await waitFor(() => {
expect(updateCheckService.state.matches("dismissed")).toBeTruthy();
});
expect(getDismissedVersionOnLocal()).toEqual(MockUpdateCheck.version);
});
});
@@ -1,122 +0,0 @@
import { assign, createMachine } from "xstate";
import { getUpdateCheck } from "api/api";
import { AuthorizationResponse, UpdateCheckResponse } from "api/typesGenerated";
import { checks, Permissions } from "components/AuthProvider/permissions";
export interface UpdateCheckContext {
permissions: Permissions;
updateCheck?: UpdateCheckResponse;
error?: unknown;
}
export type UpdateCheckEvent = { type: "DISMISS" };
export const updateCheckMachine = createMachine(
{
id: "updateCheckState",
predictableActionArguments: true,
tsTypes: {} as import("./updateCheckXService.typegen").Typegen0,
schema: {
context: {} as UpdateCheckContext,
events: {} as UpdateCheckEvent,
services: {} as {
checkPermissions: {
data: AuthorizationResponse;
};
getUpdateCheck: {
data: UpdateCheckResponse;
};
},
},
initial: "checkingPermissions",
states: {
checkingPermissions: {
always: [
{
target: "fetchingUpdateCheck",
cond: "canViewUpdateCheck",
},
{
target: "dismissed",
},
],
},
fetchingUpdateCheck: {
invoke: {
src: "getUpdateCheck",
id: "getUpdateCheck",
onDone: [
{
actions: ["assignUpdateCheck"],
target: "show",
cond: "shouldShowUpdateCheck",
},
{
target: "dismissed",
},
],
onError: [
{
actions: ["assignError"],
target: "dismissed",
},
],
},
},
show: {
on: {
DISMISS: {
actions: ["setDismissedVersion"],
target: "dismissed",
},
},
},
dismissed: {
type: "final",
},
},
},
{
services: {
// For some reason, when passing values directly, jest.spy does not work.
getUpdateCheck: () => getUpdateCheck(),
},
actions: {
assignUpdateCheck: assign({
updateCheck: (_, event) => event.data,
}),
assignError: assign({
error: (_, event) => event.data,
}),
setDismissedVersion: ({ updateCheck }) => {
if (!updateCheck) {
throw new Error("Update check is not set");
}
saveDismissedVersionOnLocal(updateCheck.version);
},
},
guards: {
canViewUpdateCheck: ({ permissions }) =>
permissions[checks.viewUpdateCheck] || false,
shouldShowUpdateCheck: (_, { data }) => {
const isNotDismissed = getDismissedVersionOnLocal() !== data.version;
const isOutdated = !data.current;
return isNotDismissed && isOutdated;
},
},
},
);
// Exporting to be used in the tests
export const saveDismissedVersionOnLocal = (version: string): void => {
window.localStorage.setItem("dismissedVersion", version);
};
export const getDismissedVersionOnLocal = (): string | undefined => {
return localStorage.getItem("dismissedVersion") ?? undefined;
};
export const clearDismissedVersionOnLocal = (): void => {
localStorage.removeItem("dismissedVersion");
};