mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore(site): remove update check service (#10355)
This commit is contained in:
@@ -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");
|
||||
};
|
||||
Reference in New Issue
Block a user