diff --git a/package.json b/package.json index 8bcec67cd..a1fbfc0ca 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "fuse.js": "7.0.0", "lodash.isequal": "4.5.0", "lodash.mergewith": "4.6.2", - "loglevel": "^1.9.2", + "loglevel": "1.9.2", "prism-react-renderer": "2.4.1", "prismjs": "1.29.0", "react": "18.3.1", diff --git a/src/components/App/App.test.tsx b/src/components/App/App.test.tsx index 8794a1706..9c546435c 100644 --- a/src/components/App/App.test.tsx +++ b/src/components/App/App.test.tsx @@ -1,5 +1,4 @@ import { render, screen } from "@testing-library/react"; -import log from "loglevel"; import * as reactGA from "react-ga"; import { Provider } from "react-redux"; import configureStore from "redux-mock-store"; @@ -27,21 +26,9 @@ vi.mock("react-router", async () => { }; }); -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - const mockStore = configureStore([]); describe("App", () => { - beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); - }); - afterEach(() => { vi.resetAllMocks(); vi.unstubAllEnvs(); diff --git a/src/components/LogIn/UserPassForm/UserPassForm.test.tsx b/src/components/LogIn/UserPassForm/UserPassForm.test.tsx index d816db4dd..0cfae1653 100644 --- a/src/components/LogIn/UserPassForm/UserPassForm.test.tsx +++ b/src/components/LogIn/UserPassForm/UserPassForm.test.tsx @@ -1,6 +1,5 @@ import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import log from "loglevel"; import { vi } from "vitest"; import { thunks as appThunks } from "store/app"; @@ -10,27 +9,9 @@ import { generalStateFactory } from "testing/factories/general"; import { rootStateFactory } from "testing/factories/root"; import { renderComponent } from "testing/utils"; -import { Label } from "../types"; - import UserPassForm from "./UserPassForm"; -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - describe("UserPassForm", () => { - beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - it("should log in", async () => { // Mock the result of the thunk to be a normal action so that it can be tested // for. This is necessary because we don't have a full store set up which @@ -70,31 +51,4 @@ describe("UserPassForm", () => { type: "connectAndStartPolling", }); }); - - it("should display console error when trying to log in", async () => { - vi.spyOn(appThunks, "connectAndStartPolling").mockImplementation( - vi.fn().mockReturnValue({ type: "connectAndStartPolling" }), - ); - vi.spyOn(dashboardStore, "useAppDispatch").mockImplementation( - vi - .fn() - .mockReturnValue((action: unknown) => - action instanceof Object && - "type" in action && - action.type === "connectAndStartPolling" - ? Promise.reject( - new Error("Error while dispatching connectAndStartPolling!"), - ) - : null, - ), - ); - - renderComponent(); - await userEvent.click(screen.getByRole("button")); - expect(appThunks.connectAndStartPolling).toHaveBeenCalledTimes(1); - expect(log.error).toHaveBeenCalledWith( - Label.POLLING_ERROR, - new Error("Error while dispatching connectAndStartPolling!"), - ); - }); }); diff --git a/src/components/LogIn/UserPassForm/UserPassForm.tsx b/src/components/LogIn/UserPassForm/UserPassForm.tsx index 04cbf36d5..2e7fbccf4 100644 --- a/src/components/LogIn/UserPassForm/UserPassForm.tsx +++ b/src/components/LogIn/UserPassForm/UserPassForm.tsx @@ -1,5 +1,4 @@ import { unwrapResult } from "@reduxjs/toolkit"; -import log from "loglevel"; import type { FormEvent } from "react"; import bakery from "juju/bakery"; @@ -7,6 +6,7 @@ import { thunks as appThunks } from "store/app"; import { actions as generalActions } from "store/general"; import { getWSControllerURL } from "store/general/selectors"; import { useAppDispatch, useAppSelector } from "store/store"; +import { logger } from "utils/logger"; import { Label } from "../types"; @@ -36,7 +36,7 @@ const UserPassForm = () => { if (bakery) { dispatch(appThunks.connectAndStartPolling()) .then(unwrapResult) - .catch((error) => log.error(Label.POLLING_ERROR, error)); + .catch((error) => logger.error(Label.POLLING_ERROR, error)); } } diff --git a/src/components/ShareCard/ShareCard.test.tsx b/src/components/ShareCard/ShareCard.test.tsx index a6271dd89..7a189b08e 100644 --- a/src/components/ShareCard/ShareCard.test.tsx +++ b/src/components/ShareCard/ShareCard.test.tsx @@ -1,24 +1,11 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import log from "loglevel"; import { vi } from "vitest"; import ShareCard from "./ShareCard"; import { Label } from "./types"; -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - describe("Share Card", () => { - beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); - }); - it("should display appropriate text", () => { render( { await userEvent.selectOptions(screen.getByRole("combobox"), "write"); expect(accessSelectChangeFn).toHaveBeenCalled(); }); - - it("should log error when trying to change access", async () => { - const removeUserFn = vi.fn(); - const accessSelectChangeFn = vi.fn(() => Promise.reject(new Error())); - render( - , - ); - await userEvent.selectOptions(screen.getByRole("combobox"), "write"); - expect(log.error).toHaveBeenCalledWith( - Label.ACCESS_CHANGE_ERROR, - new Error(), - ); - }); }); diff --git a/src/components/ShareCard/ShareCard.tsx b/src/components/ShareCard/ShareCard.tsx index c18de08a7..422f6852e 100644 --- a/src/components/ShareCard/ShareCard.tsx +++ b/src/components/ShareCard/ShareCard.tsx @@ -1,11 +1,11 @@ import type { ErrorResults } from "@canonical/jujulib/dist/api/facades/model-manager/ModelManagerV9"; import { Button, Select } from "@canonical/react-components"; -import log from "loglevel"; import { useEffect, useState } from "react"; import SlideDownFadeOut from "animations/SlideDownFadeOut"; import TruncatedTooltip from "components/TruncatedTooltip"; import { formatFriendlyDateToNow } from "components/utils"; +import { logger } from "utils/logger"; import "./_share-card.scss"; import { Label } from "./types"; @@ -118,7 +118,7 @@ export default function ShareCard({ return; }) .catch((error) => - log.error(Label.ACCESS_CHANGE_ERROR, error), + logger.error(Label.ACCESS_CHANGE_ERROR, error), ); }} value={access} diff --git a/src/hooks/useLocalStorage.test.ts b/src/hooks/useLocalStorage.test.ts index def992eec..9604fe7b2 100644 --- a/src/hooks/useLocalStorage.test.ts +++ b/src/hooks/useLocalStorage.test.ts @@ -1,22 +1,8 @@ import { act, renderHook } from "@testing-library/react"; -import log from "loglevel"; -import { vi } from "vitest"; import useLocalStorage from "./useLocalStorage"; -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - describe("useLocalStorage", () => { - beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); - }); - afterEach(() => { localStorage.clear(); }); @@ -43,10 +29,6 @@ describe("useLocalStorage", () => { const { result } = renderHook(() => useLocalStorage("test-key", "init-val"), ); - expect(log.error).toHaveBeenCalledWith( - "Unable to parse local storage:", - expect.any(Error), - ); const [value] = result.current; expect(value).toBe("init-val"); }); @@ -76,7 +58,6 @@ describe("useLocalStorage", () => { act(() => { setValue(circular as unknown as string); }); - expect(log.error).toHaveBeenCalledWith(expect.any(Error)); const [value] = result.current; expect(value).toBe("init-val"); expect(JSON.parse(localStorage.getItem("test-key") ?? "")).toBe("init-val"); diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index d75e78579..d5cf66082 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -1,6 +1,7 @@ -import log from "loglevel"; import { useState } from "react"; +import { logger } from "utils/logger"; + function useLocalStorage( key: string, initialValue: V, @@ -13,7 +14,7 @@ function useLocalStorage( return item ? JSON.parse(item) : initialValue; } catch (error) { // Not shown in UI. Logged for debugging purposes. - log.error("Unable to parse local storage:", error); + logger.error("Unable to parse local storage:", error); return initialValue; } }); @@ -27,7 +28,7 @@ function useLocalStorage( window.localStorage.setItem(key, stringified); } catch (error) { // Not shown in UI. Logged for debugging purposes. - log.error(error); + logger.error(error); } }; diff --git a/src/hooks/useLogout.test.tsx b/src/hooks/useLogout.test.tsx index 9e607d11a..b4a5c3cb8 100644 --- a/src/hooks/useLogout.test.tsx +++ b/src/hooks/useLogout.test.tsx @@ -1,6 +1,5 @@ import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import log from "loglevel"; import { vi } from "vitest"; import { Label } from "hooks/useLogout"; @@ -10,19 +9,7 @@ import { renderComponent, renderWrappedHook } from "testing/utils"; import useLogout from "./useLogout"; -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - describe("useLogout", () => { - beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); - }); - it("should logout", async () => { vi.spyOn(appThunks, "logOut").mockImplementation( vi.fn().mockReturnValue({ type: "logOut", catch: vi.fn() }), diff --git a/src/hooks/useLogout.tsx b/src/hooks/useLogout.tsx index ffa5effcc..ed849e943 100644 --- a/src/hooks/useLogout.tsx +++ b/src/hooks/useLogout.tsx @@ -1,11 +1,11 @@ import { Button } from "@canonical/react-components"; import { unwrapResult } from "@reduxjs/toolkit"; -import log from "loglevel"; import reactHotToast from "react-hot-toast"; import ToastCard from "components/ToastCard"; import { thunks as appThunks } from "store/app"; import { useAppDispatch } from "store/store"; +import { logger } from "utils/logger"; export enum Label { LOGOUT_ERROR = "Error when trying to logout.", @@ -31,7 +31,7 @@ const useLogout = () => { )); - log.error(Label.LOGOUT_ERROR, error); + logger.error(Label.LOGOUT_ERROR, error); }); }; }; diff --git a/src/index.test.tsx b/src/index.test.tsx index a86b263d8..9b8b540aa 100644 --- a/src/index.test.tsx +++ b/src/index.test.tsx @@ -1,6 +1,4 @@ import { screen, waitFor } from "@testing-library/dom"; -import log from "loglevel"; -import type { UnknownAction } from "redux"; import { vi } from "vitest"; import * as storeModule from "store"; @@ -34,14 +32,6 @@ vi.mock("store", () => { }; }); -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - const appVersion = packageJSON.version; describe("renderApp", () => { @@ -54,8 +44,6 @@ describe("renderApp", () => { enumerable: true, value: new URL(window.location.href), }); - // Hide the config errors from the test output. - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); rootNode = document.createElement("div"); rootNode.setAttribute("id", ROOT_ID); document.body.appendChild(rootNode); @@ -257,10 +245,6 @@ describe("renderApp", () => { }); describe("getControllerAPIEndpointErrors", () => { - beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); - }); - it("should handle secure protocol", () => { expect( getControllerAPIEndpointErrors("wss://example.com:80/api"), @@ -306,34 +290,4 @@ describe("getControllerAPIEndpointErrors", () => { "controllerAPIEndpoint (example.com:80/api) must be an absolute path or begin with ws:// or wss://.", ); }); - - it("should show console error when dispatching connectAndStartPolling", async () => { - vi.spyOn(appThunks, "connectAndStartPolling").mockImplementation( - vi.fn().mockReturnValue({ type: "connectAndStartPolling" }), - ); - vi.spyOn(storeModule.default, "dispatch").mockImplementation( - (action) => - (action instanceof Object && - "type" in action && - action.type === "connectAndStartPolling" - ? Promise.reject( - new Error("Error while dispatching connectAndStartPolling!"), - ) - : null) as unknown as UnknownAction, - ); - const config = configFactory.build({ - baseControllerURL: null, - identityProviderURL: "/candid", - isJuju: true, - }); - window.jujuDashboardConfig = config; - renderApp(); - expect(appThunks.connectAndStartPolling).toHaveBeenCalledTimes(1); - await waitFor(() => - expect(log.error).toHaveBeenCalledWith( - Label.POLLING_ERROR, - new Error("Error while dispatching connectAndStartPolling!"), - ), - ); - }); }); diff --git a/src/index.tsx b/src/index.tsx index c29c5f275..179304647 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,6 @@ import { Notification, Strip } from "@canonical/react-components"; import { unwrapResult } from "@reduxjs/toolkit"; import * as Sentry from "@sentry/browser"; -import log from "loglevel"; import { StrictMode } from "react"; import type { Root } from "react-dom/client"; import { createRoot } from "react-dom/client"; @@ -14,6 +13,7 @@ import { thunks as appThunks } from "store/app"; import { actions as generalActions } from "store/general"; import { AuthMethod } from "store/general/types"; import type { WindowConfig } from "types"; +import { logger } from "utils/logger"; import packageJSON from "../package.json"; @@ -125,7 +125,7 @@ function bootstrap() { , ); - log.error(error); + logger.error(error); return; } // It's possible that the charm is generating a relative path for the @@ -160,7 +160,7 @@ function bootstrap() { reduxStore .dispatch(appThunks.connectAndStartPolling()) .then(unwrapResult) - .catch((error) => log.error(Label.POLLING_ERROR, error)); + .catch((error) => logger.error(Label.POLLING_ERROR, error)); } getRoot()?.render( diff --git a/src/juju/api.test.ts b/src/juju/api.test.ts index 5ce448447..be3452409 100644 --- a/src/juju/api.test.ts +++ b/src/juju/api.test.ts @@ -2,7 +2,6 @@ import type { Client, Connection } from "@canonical/jujulib"; import * as jujuLib from "@canonical/jujulib"; import * as jujuLibVersions from "@canonical/jujulib/dist/api/versions"; import { waitFor } from "@testing-library/react"; -import log from "loglevel"; import { vi } from "vitest"; import { actions as generalActions } from "store/general"; @@ -68,17 +67,8 @@ vi.mock("@canonical/jujulib/dist/api/versions", () => ({ jujuUpdateAvailable: vi.fn(), })); -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - describe("Juju API", () => { beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); vi.useFakeTimers(); }); @@ -379,11 +369,6 @@ describe("Juju API", () => { await expect(response).rejects.toStrictEqual( new Error(Label.LOGIN_TIMEOUT_ERROR), ); - expect(log.error).toHaveBeenCalledWith( - "Error connecting to model:", - "abc123", - new Error(Label.LOGIN_TIMEOUT_ERROR), - ); }); it("can fetch the status", async () => { @@ -554,11 +539,6 @@ describe("Juju API", () => { await expect(response).rejects.toStrictEqual( new Error("Unable to fetch the status. Uh oh!"), ); - expect(log.error).toHaveBeenCalledWith( - "Error connecting to model:", - "abc123", - new Error("Unable to fetch the status. Uh oh!"), - ); }); }); @@ -691,11 +671,6 @@ describe("Juju API", () => { await expect(response).rejects.toStrictEqual( new Error("Unable to fetch the status. Status not returned."), ); - expect(log.error).toHaveBeenCalledWith( - "Error connecting to model:", - "abc123", - new Error("Unable to fetch the status. Status not returned."), - ); expect(dispatch).not.toHaveBeenCalled(); }); }); @@ -879,12 +854,6 @@ describe("Juju API", () => { () => state, ); expect(dispatch).toHaveBeenCalledTimes(2); - await waitFor(() => - expect(log.error).toHaveBeenCalledWith( - "Error when trying to add controller cloud and region data.", - new Error("Error while trying to dispatch!"), - ), - ); }); it("should return a rejected promise when retrieving data for some models fails", async () => { diff --git a/src/juju/api.ts b/src/juju/api.ts index 5ce66fe9d..48c4f5636 100644 --- a/src/juju/api.ts +++ b/src/juju/api.ts @@ -17,7 +17,6 @@ import { jujuUpdateAvailable } from "@canonical/jujulib/dist/api/versions"; import type { ValueOf } from "@canonical/react-components"; import { unwrapResult } from "@reduxjs/toolkit"; import Limiter from "async-limiter"; -import log from "loglevel"; import type { Dispatch } from "redux"; import bakery from "juju/bakery"; @@ -41,6 +40,7 @@ import type { import { ModelsError } from "store/middleware/model-poller"; import type { RootState, Store } from "store/store"; import { toErrorString } from "utils"; +import { logger } from "utils/logger"; import { getModelByUUID } from "../store/juju/selectors"; @@ -124,7 +124,7 @@ function startPingerLoop(conn: ConnectionWithFacades) { conn.facades.pinger?.ping(null).catch((e) => { // If the pinger fails for whatever reason then cancel the ping. // Not shown in UI. Logged for debugging purposes. - log.error("pinger stopped,", e); + logger.error("pinger stopped,", e); stopPingerLoop(intervalId); }); }, PING_TIME); @@ -271,7 +271,7 @@ export async function fetchModelStatus( } logout(); } catch (error) { - log.error("Error connecting to model:", modelUUID, error); + logger.error("Error connecting to model:", modelUUID, error); throw error; } } @@ -390,7 +390,7 @@ export async function fetchAllModelStatuses( .then(unwrapResult) .catch((error) => // Not shown in UI. Logged for debugging purposes. - log.error( + logger.error( "Error when trying to add controller cloud and region data.", error, ), diff --git a/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.test.tsx b/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.test.tsx index b3cd96fa0..70c5e827e 100644 --- a/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.test.tsx +++ b/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.test.tsx @@ -1,7 +1,6 @@ import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { add } from "date-fns"; -import log from "loglevel"; import { vi } from "vitest"; import * as actionsHooks from "juju/api-hooks/actions"; @@ -23,6 +22,7 @@ import { modelDataInfoFactory, } from "testing/factories/juju/juju"; import { renderComponent } from "testing/utils"; +import { logger } from "utils/logger"; import ActionLogs from "./ActionLogs"; import { Label, Output } from "./types"; @@ -103,21 +103,13 @@ vi.mock("juju/api-hooks/actions", () => { }; }); -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - describe("Action Logs", () => { let state: RootState; const path = "/models/:userName/:modelName"; const url = "/models/eggman@external/group-test?activeView=action-logs"; beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); + vi.spyOn(logger, "error").mockImplementation(() => vi.fn()); vi.spyOn(actionsHooks, "useQueryOperationsList").mockImplementation(() => vi.fn().mockImplementation(() => Promise.resolve(mockOperationResults)), ); @@ -377,7 +369,7 @@ describe("Action Logs", () => { renderComponent(, { path, url, state }); expect(queryOperationsListSpy).toHaveBeenCalledTimes(1); await waitFor(() => { - expect(log.error).toHaveBeenCalledWith( + expect(logger.error).toHaveBeenCalledWith( Label.FETCH_ERROR, new Error("Error while querying operations list."), ); diff --git a/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.tsx b/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.tsx index c4cddd1a6..6b6bed198 100644 --- a/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.tsx +++ b/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.tsx @@ -8,7 +8,6 @@ import { Icon, ModularTable, } from "@canonical/react-components"; -import log from "loglevel"; import type { ReactNode } from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useSelector } from "react-redux"; @@ -28,6 +27,7 @@ import { getModelStatus, getModelUUID } from "store/juju/selectors"; import type { ModelData } from "store/juju/types"; import type { RootState } from "store/store"; import urls from "urls"; +import { logger } from "utils/logger"; import "./_action-logs.scss"; import ActionPayloadModal from "./ActionPayloadModal"; @@ -114,7 +114,10 @@ const generateApplicationRow = ( const appName = parts && parts[1]; if (!appName) { // Not shown in UI. Logged for debugging purposes. - log.error("Unable to parse action receiver", actionData.action?.receiver); + logger.error( + "Unable to parse action receiver", + actionData.action?.receiver, + ); return null; } return { @@ -193,7 +196,7 @@ export default function ActionLogs() { setInlineErrors(InlineErrors.FETCH, null); } catch (error) { setInlineErrors(InlineErrors.FETCH, Label.FETCH_ERROR); - log.error(Label.FETCH_ERROR, error); + logger.error(Label.FETCH_ERROR, error); } finally { setFetchedOperations(true); } diff --git a/src/pages/ModelDetails/ModelDetails.test.tsx b/src/pages/ModelDetails/ModelDetails.test.tsx index f748bb0c8..ac19e68f4 100644 --- a/src/pages/ModelDetails/ModelDetails.test.tsx +++ b/src/pages/ModelDetails/ModelDetails.test.tsx @@ -1,7 +1,6 @@ import type { Connection } from "@canonical/jujulib"; import * as jujuLib from "@canonical/jujulib"; import { screen, waitFor } from "@testing-library/react"; -import log from "loglevel"; import { vi } from "vitest"; import * as juju from "juju/api"; @@ -16,7 +15,6 @@ import { renderComponent } from "testing/utils"; import urls from "urls"; import ModelDetails from "./ModelDetails"; -import { Label as ModelDetailsLabel } from "./types"; vi.mock("pages/EntityDetails/App", () => { return { default: () =>
}; @@ -38,14 +36,6 @@ vi.mock("@canonical/jujulib", () => ({ connectAndLogin: vi.fn(), })); -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - describe("ModelDetails", () => { let state: RootState; let client: { @@ -59,7 +49,6 @@ describe("ModelDetails", () => { }); beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); state = rootStateFactory.withGeneralConfig().build({ juju: jujuStateFactory.build({ models: { @@ -242,9 +231,5 @@ describe("ModelDetails", () => { ); unmount(); await waitFor(() => expect(juju.stopModelWatcher).toHaveBeenCalledTimes(1)); - expect(log.error).toHaveBeenCalledWith( - ModelDetailsLabel.MODEL_WATCHER_ERROR, - new Error("Failed to stop model watcher!"), - ); }); }); diff --git a/src/pages/ModelDetails/ModelDetails.tsx b/src/pages/ModelDetails/ModelDetails.tsx index f971eb06e..6886c5b95 100644 --- a/src/pages/ModelDetails/ModelDetails.tsx +++ b/src/pages/ModelDetails/ModelDetails.tsx @@ -1,5 +1,4 @@ import type { AllWatcherId } from "@canonical/jujulib/dist/api/facades/client/ClientV6"; -import log from "loglevel"; import { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { Route, Routes, useParams } from "react-router"; @@ -17,6 +16,7 @@ import { getModelUUIDFromList } from "store/juju/selectors"; import { useAppStore } from "store/store"; import urls from "urls"; import { getMajorMinorVersion, toErrorString } from "utils"; +import { logger } from "utils/logger"; import { Label } from "./types"; @@ -71,7 +71,7 @@ export default function ModelDetails() { ).catch((error) => // Error doesn’t interfere with the user’s interaction with the // dashboard. Not shown in UI. Logged for debugging purposes. - log.error(Label.MODEL_WATCHER_ERROR, error), + logger.error(Label.MODEL_WATCHER_ERROR, error), ); } }; diff --git a/src/panels/ActionsPanel/ActionsPanel.test.tsx b/src/panels/ActionsPanel/ActionsPanel.test.tsx index f71c0a0d8..dc6718b7e 100644 --- a/src/panels/ActionsPanel/ActionsPanel.test.tsx +++ b/src/panels/ActionsPanel/ActionsPanel.test.tsx @@ -1,6 +1,5 @@ import { screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import log from "loglevel"; import { vi } from "vitest"; import * as actionsHooks from "juju/api-hooks/actions"; @@ -61,14 +60,6 @@ vi.mock("juju/api-hooks/actions", () => { }; }); -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - describe("ActionsPanel", () => { let state: RootState; const path = "/models/:userName/:modelName/app/:appName"; @@ -76,7 +67,6 @@ describe("ActionsPanel", () => { "/models/user-eggman@external/group-test/app/kubernetes-master?panel=execute-action&units=ceph%2F0,ceph%2F1"; beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); const getActionsForApplicationSpy = vi .fn() .mockImplementation(() => Promise.resolve(mockResponse)); @@ -335,10 +325,6 @@ describe("ActionsPanel", () => { await waitFor(() => expect(getActionsForApplicationSpy).toHaveBeenCalledTimes(1), ); - expect(log.error).toHaveBeenCalledWith( - ActionsPanelLabel.GET_ACTIONS_ERROR, - new Error("Error while trying to get actions for application!"), - ); await waitFor(() => expect( screen.getByText(ActionsPanelLabel.GET_ACTIONS_ERROR, { exact: false }), diff --git a/src/panels/ActionsPanel/ActionsPanel.tsx b/src/panels/ActionsPanel/ActionsPanel.tsx index 537996682..3176b86c7 100644 --- a/src/panels/ActionsPanel/ActionsPanel.tsx +++ b/src/panels/ActionsPanel/ActionsPanel.tsx @@ -1,5 +1,4 @@ import { ActionButton, Button } from "@canonical/react-components"; -import log from "loglevel"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useSelector } from "react-redux"; import { useParams } from "react-router"; @@ -18,6 +17,7 @@ import { getModelUUID } from "store/juju/selectors"; import { pluralize } from "store/juju/utils/models"; import type { RootState } from "store/store"; import { useAppStore } from "store/store"; +import { logger } from "utils/logger"; import ActionOptions from "./ActionOptions"; import ConfirmationDialog from "./ConfirmationDialog"; @@ -101,7 +101,7 @@ export default function ActionsPanel(): JSX.Element { }) .catch((error) => { setInlineErrors(InlineErrors.GET_ACTION, Label.GET_ACTIONS_ERROR); - log.error(Label.GET_ACTIONS_ERROR, error); + logger.error(Label.GET_ACTIONS_ERROR, error); }) .finally(() => { setFetchingActionData(false); diff --git a/src/panels/ActionsPanel/ConfirmationDialog/ConfirmationDialog.test.tsx b/src/panels/ActionsPanel/ConfirmationDialog/ConfirmationDialog.test.tsx index 4a3858de0..d356dae9c 100644 --- a/src/panels/ActionsPanel/ConfirmationDialog/ConfirmationDialog.test.tsx +++ b/src/panels/ActionsPanel/ConfirmationDialog/ConfirmationDialog.test.tsx @@ -1,6 +1,5 @@ import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import log from "loglevel"; import { vi } from "vitest"; import * as actionsHooks from "juju/api-hooks/actions"; @@ -12,23 +11,11 @@ import { InlineErrors } from "../types"; import ConfirmationDialog from "./ConfirmationDialog"; import { Label } from "./types"; -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - describe("ConfirmationDialog", () => { const path = "/models/:userName/:modelName/app/:appName"; const url = "/models/user-eggman@external/group-test/app/kubernetes-master?panel=execute-action&units=ceph%2F0,ceph%2F1"; - beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); - }); - afterEach(() => { vi.resetModules(); vi.restoreAllMocks(); @@ -146,9 +133,5 @@ describe("ConfirmationDialog", () => { InlineErrors.EXECUTE_ACTION, Label.EXECUTE_ACTION_ERROR, ); - expect(log.error).toHaveBeenCalledWith( - Label.EXECUTE_ACTION_ERROR, - new Error("mock error"), - ); }); }); diff --git a/src/panels/ActionsPanel/ConfirmationDialog/ConfirmationDialog.tsx b/src/panels/ActionsPanel/ConfirmationDialog/ConfirmationDialog.tsx index d43632525..7f427dfba 100644 --- a/src/panels/ActionsPanel/ConfirmationDialog/ConfirmationDialog.tsx +++ b/src/panels/ActionsPanel/ConfirmationDialog/ConfirmationDialog.tsx @@ -1,5 +1,4 @@ import { ConfirmationModal } from "@canonical/react-components"; -import log from "loglevel"; import { useParams } from "react-router"; import usePortal from "react-useportal"; @@ -8,6 +7,7 @@ import { type SetError } from "hooks/useInlineErrors"; import { useExecuteActionOnUnits } from "juju/api-hooks"; import type { ConfirmTypes } from "panels/types"; import { ConfirmType } from "panels/types"; +import { logger } from "utils/logger"; import { InlineErrors, type ActionOptionValue } from "../types"; @@ -68,7 +68,7 @@ const ConfirmationDialog = ({ Label.EXECUTE_ACTION_ERROR, ); setIsExecutingAction(false); - log.error(Label.EXECUTE_ACTION_ERROR, error); + logger.error(Label.EXECUTE_ACTION_ERROR, error); }); }} close={() => setConfirmType(null)} diff --git a/src/panels/CharmsAndActionsPanel/CharmsAndActionsPanel.test.tsx b/src/panels/CharmsAndActionsPanel/CharmsAndActionsPanel.test.tsx index 90bab568c..26072e488 100644 --- a/src/panels/CharmsAndActionsPanel/CharmsAndActionsPanel.test.tsx +++ b/src/panels/CharmsAndActionsPanel/CharmsAndActionsPanel.test.tsx @@ -1,6 +1,5 @@ import { screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import log from "loglevel"; import { vi } from "vitest"; import * as juju from "juju/api"; @@ -26,14 +25,6 @@ import { CharmsPanelLabel } from "../CharmsPanel"; import CharmsAndActionsPanel from "./CharmsAndActionsPanel"; import { Label as CharmsAndActionsPanelLabel } from "./types"; -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - describe("CharmsAndActionsPanel", () => { let state: RootState; const path = urls.model.index(null); @@ -43,7 +34,6 @@ describe("CharmsAndActionsPanel", () => { }); beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); vi.resetAllMocks(); state = rootStateFactory.build({ @@ -167,14 +157,10 @@ describe("CharmsAndActionsPanel", () => { } = renderComponent(, { path, url, state }); expect(juju.getCharmsURLFromApplications).toHaveBeenCalledTimes(1); await waitFor(() => - expect(log.error).toHaveBeenCalledWith( - CharmsAndActionsPanelLabel.GET_URL_ERROR, - new Error("Error while calling getCharmsURLFromApplications"), + expect(container.querySelector(".p-panel__title")).toContainHTML( + CharmsPanelLabel.PANEL_TITLE, ), ); - expect(container.querySelector(".p-panel__title")).toContainHTML( - CharmsPanelLabel.PANEL_TITLE, - ); const getCharmsURLErrorNotification = screen.getByText( CharmsAndActionsPanelLabel.GET_URL_ERROR, { exact: false }, diff --git a/src/panels/CharmsAndActionsPanel/CharmsAndActionsPanel.tsx b/src/panels/CharmsAndActionsPanel/CharmsAndActionsPanel.tsx index 747015c92..26681aa83 100644 --- a/src/panels/CharmsAndActionsPanel/CharmsAndActionsPanel.tsx +++ b/src/panels/CharmsAndActionsPanel/CharmsAndActionsPanel.tsx @@ -1,5 +1,4 @@ import { Button } from "@canonical/react-components"; -import log from "loglevel"; import { useCallback, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router"; @@ -16,6 +15,7 @@ import { getSelectedApplications, } from "store/juju/selectors"; import { useAppStore } from "store/store"; +import { logger } from "utils/logger"; import { Label } from "./types"; @@ -77,7 +77,7 @@ const CharmsAndActionsPanel = () => { }) .catch((error) => { setInlineErrors(InlineErrors.GET_URL, Label.GET_URL_ERROR); - log.error(Label.GET_URL_ERROR, error); + logger.error(Label.GET_URL_ERROR, error); }); }, [dispatch, getState, modelUUID, selectedApplications, setInlineErrors]); diff --git a/src/panels/ConfigPanel/ConfigPanel.test.tsx b/src/panels/ConfigPanel/ConfigPanel.test.tsx index a143ab627..7794842db 100644 --- a/src/panels/ConfigPanel/ConfigPanel.test.tsx +++ b/src/panels/ConfigPanel/ConfigPanel.test.tsx @@ -1,6 +1,5 @@ import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import log from "loglevel"; import type { Mock } from "vitest"; import { vi } from "vitest"; @@ -51,14 +50,6 @@ vi.mock("juju/api-hooks/secrets", () => { }; }); -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - describe("ConfigPanel", () => { let state: RootState; const params = new URLSearchParams({ @@ -72,7 +63,6 @@ describe("ConfigPanel", () => { let getApplicationConfig: Mock; beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); vi.resetModules(); vi.spyOn(secretHooks, "useListSecrets").mockImplementation(() => vi.fn()); state = rootStateFactory.build({ @@ -401,13 +391,7 @@ describe("ConfigPanel", () => { ); renderComponent(, { state, path, url }); expect(getApplicationConfig).toHaveBeenCalledTimes(1); - await waitFor(() => { - expect(log.error).toHaveBeenCalledWith( - ConfigPanelLabel.GET_CONFIG_ERROR, - new Error("Error while calling getApplicationConfig"), - ); - }); - const configErrorNotification = screen.getByText( + const configErrorNotification = await screen.findByText( ConfigPanelLabel.GET_CONFIG_ERROR, { exact: false, @@ -457,12 +441,6 @@ describe("ConfigPanel", () => { name: ConfirmationDialogLabel.SAVE_CONFIRM_CONFIRM_BUTTON, }), ); - await waitFor(() => - expect(log.error).toHaveBeenCalledWith( - ConfirmationDialogLabel.SUBMIT_TO_JUJU_ERROR, - new Error("Error while trying to save"), - ), - ); expect( screen.getByText(ConfirmationDialogLabel.SUBMIT_TO_JUJU_ERROR, { exact: false, diff --git a/src/panels/ConfigPanel/ConfigPanel.tsx b/src/panels/ConfigPanel/ConfigPanel.tsx index 4641c947b..8f6abe93b 100644 --- a/src/panels/ConfigPanel/ConfigPanel.tsx +++ b/src/panels/ConfigPanel/ConfigPanel.tsx @@ -2,7 +2,6 @@ import type { ListSecretResult } from "@canonical/jujulib/dist/api/facades/secre import { ActionButton, Button } from "@canonical/react-components"; import classnames from "classnames"; import cloneDeep from "clone-deep"; -import log from "loglevel"; import type { MouseEvent } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; import { useParams } from "react-router"; @@ -24,6 +23,7 @@ import { actions as jujuActions } from "store/juju"; import { getModelSecrets, getModelByUUID } from "store/juju/selectors"; import { useAppSelector, useAppDispatch } from "store/store"; import { secretIsAppOwned } from "utils"; +import { logger } from "utils/logger"; import BooleanConfig from "./BooleanConfig"; import type { SetNewValue, SetSelectedConfig } from "./ConfigField"; @@ -362,7 +362,7 @@ function getConfig( }) .catch((error) => { setInlineError(InlineErrors.GET_CONFIG, Label.GET_CONFIG_ERROR); - log.error(Label.GET_CONFIG_ERROR, error); + logger.error(Label.GET_CONFIG_ERROR, error); }) .finally(() => setIsLoading(false)); } diff --git a/src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.test.tsx b/src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.test.tsx index 81c3292a9..79e3e09d2 100644 --- a/src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.test.tsx +++ b/src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.test.tsx @@ -1,6 +1,5 @@ -import { screen, waitFor } from "@testing-library/react"; +import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import log from "loglevel"; import type { Mock } from "vitest"; import { vi } from "vitest"; @@ -24,14 +23,6 @@ import { ConfigConfirmType } from "../types"; import ConfirmationDialog from "./ConfirmationDialog"; import { InlineErrors, Label, Label as ConfirmationDialogLabel } from "./types"; -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - describe("ConfirmationDialog", () => { let state: RootState; const params = new URLSearchParams({ @@ -79,7 +70,6 @@ describe("ConfirmationDialog", () => { }; beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); mockSetConfirmType = vi.fn(); mockSetInlineError = vi.fn(); mockHandleRemovePanelQueryParams = vi.fn(); @@ -177,7 +167,6 @@ describe("ConfirmationDialog", () => { default: "eggman", }), }); - expect(log.error).not.toHaveBeenCalled(); expect(mockHandleRemovePanelQueryParams).toHaveBeenCalledOnce(); }); @@ -229,7 +218,6 @@ describe("ConfirmationDialog", () => { default: "eggman", }), }); - expect(log.error).not.toHaveBeenCalled(); expect(mockSetConfirmType).toHaveBeenCalledWith(ConfigConfirmType.GRANT); }); @@ -275,12 +263,6 @@ describe("ConfirmationDialog", () => { default: "eggman", }), }); - await waitFor(() => - expect(log.error).toHaveBeenCalledWith( - Label.SUBMIT_TO_JUJU_ERROR, - new Error("Error while trying to save"), - ), - ); expect(mockSetInlineError).toHaveBeenCalledWith( InlineErrors.SUBMIT_TO_JUJU, Label.SUBMIT_TO_JUJU_ERROR, @@ -375,7 +357,6 @@ describe("ConfirmationDialog", () => { expect(mockSetConfirmType).toHaveBeenCalledWith(null); expect(mockSetInlineError).toHaveBeenCalledWith(InlineErrors.FORM, null); expect(grantSecret).toHaveBeenCalledWith("secret:aabbccdd", ["easyrsa"]); - expect(log.error).not.toHaveBeenCalled(); expect(mockHandleRemovePanelQueryParams).toHaveBeenCalledOnce(); }); @@ -418,12 +399,6 @@ describe("ConfirmationDialog", () => { expect(mockSetConfirmType).toHaveBeenCalledWith(null); expect(mockSetInlineError).toHaveBeenCalledWith(InlineErrors.FORM, null); expect(grantSecret).toHaveBeenCalledWith("secret:aabbccdd", ["easyrsa"]); - await waitFor(() => - expect(log.error).toHaveBeenCalledWith( - Label.GRANT_ERROR, - new Error("Caught error"), - ), - ); expect(mockSetInlineError).toHaveBeenCalledWith( InlineErrors.SUBMIT_TO_JUJU, Label.GRANT_ERROR, diff --git a/src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.tsx b/src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.tsx index 069504e8e..574ba3863 100644 --- a/src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.tsx +++ b/src/panels/ConfigPanel/ConfirmationDialog/ConfirmationDialog.tsx @@ -1,5 +1,4 @@ import { ConfirmationModal } from "@canonical/react-components"; -import log from "loglevel"; import { useParams } from "react-router"; import usePortal from "react-useportal"; @@ -13,6 +12,7 @@ import type { usePanelQueryParams } from "panels/hooks"; import { ConfirmType as DefaultConfirmType } from "panels/types"; import { getModelSecrets } from "store/juju/selectors"; import { useAppSelector } from "store/store"; +import { logger } from "utils/logger"; import ChangedKeyValues from "../ChangedKeyValues"; import type { Config, ConfigQueryParams, ConfirmTypes } from "../types"; @@ -110,7 +110,7 @@ const ConfirmationDialog = ({ InlineErrors.SUBMIT_TO_JUJU, Label.SUBMIT_TO_JUJU_ERROR, ); - log.error(Label.SUBMIT_TO_JUJU_ERROR, error); + logger.error(Label.SUBMIT_TO_JUJU_ERROR, error); }); }} close={() => setConfirmType(null)} @@ -150,7 +150,7 @@ const ConfirmationDialog = ({ handleRemovePanelQueryParams(); } catch (error) { setInlineError(InlineErrors.SUBMIT_TO_JUJU, Label.GRANT_ERROR); - log.error(Label.GRANT_ERROR, error); + logger.error(Label.GRANT_ERROR, error); } })(); }} diff --git a/src/store/app/thunks.test.ts b/src/store/app/thunks.test.ts index 876feeb8a..5c24fb3fd 100644 --- a/src/store/app/thunks.test.ts +++ b/src/store/app/thunks.test.ts @@ -1,4 +1,3 @@ -import log from "loglevel"; import { vi } from "vitest"; import { pollWhoamiStop } from "juju/jimm/listeners"; @@ -21,19 +20,10 @@ import type { WindowConfig } from "types"; import { logOut, connectAndStartPolling } from "./thunks"; -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - describe("thunks", () => { let state: RootState; beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); window.jujuDashboardConfig = { controllerAPIEndpoint: "wss://example.com/api", } as WindowConfig; @@ -161,10 +151,6 @@ describe("thunks", () => { isJuju: true, }), ); - expect(log.error).toHaveBeenCalledWith( - "Error while triggering the connection and polling of models.", - "Error while dispatching connectAndPollControllers!", - ); expect(dispatch).toHaveBeenCalledWith( generalActions.storeConnectionError( "Unable to connect: Error while dispatching connectAndPollControllers!", @@ -192,10 +178,6 @@ describe("thunks", () => { isJuju: true, }), ); - expect(log.error).toHaveBeenCalledWith( - "Error while triggering the connection and polling of models.", - new Error("Error while dispatching connectAndPollControllers!"), - ); expect(dispatch).toHaveBeenCalledWith( generalActions.storeConnectionError( "Unable to connect: Error while dispatching connectAndPollControllers!", @@ -224,10 +206,6 @@ describe("thunks", () => { isJuju: true, }), ); - expect(log.error).toHaveBeenCalledWith( - "Error while triggering the connection and polling of models.", - ["Unknown error."], - ); expect(dispatch).toHaveBeenCalledWith( generalActions.storeConnectionError( "Unable to connect: Something went wrong. View the console log for more details.", diff --git a/src/store/app/thunks.ts b/src/store/app/thunks.ts index cae2977f6..ae724b7af 100644 --- a/src/store/app/thunks.ts +++ b/src/store/app/thunks.ts @@ -1,5 +1,4 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; -import log from "loglevel"; import bakery from "juju/bakery"; import { pollWhoamiStop } from "juju/jimm/listeners"; @@ -16,6 +15,7 @@ import { import { AuthMethod } from "store/general/types"; import { actions as jujuActions } from "store/juju"; import type { RootState } from "store/store"; +import { logger } from "utils/logger"; import type { ControllerArgs } from "./actions"; @@ -85,7 +85,7 @@ export const connectAndStartPolling = createAsyncThunk< } catch (error) { // a common error logged to the console by this is: // Error while triggering the connection and polling of models. cannot send request {"type":"ModelManager","request":"ListModels","version":5,"params":...}: connection state 3 is not open - log.error( + logger.error( "Error while triggering the connection and polling of models.", error, ); diff --git a/src/store/middleware/check-auth.test.ts b/src/store/middleware/check-auth.test.ts index 398e1a265..30c6cf2bc 100644 --- a/src/store/middleware/check-auth.test.ts +++ b/src/store/middleware/check-auth.test.ts @@ -1,4 +1,3 @@ -import log from "loglevel"; import type { UnknownAction, MiddlewareAPI } from "redux"; import type { Mock } from "vitest"; import { vi } from "vitest"; @@ -15,14 +14,6 @@ import { import { checkAuthMiddleware } from "./check-auth"; -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - describe("model poller", () => { let fakeStore: MiddlewareAPI; let next: Mock; @@ -31,7 +22,6 @@ describe("model poller", () => { let state: RootState; beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); console.log = vi.fn(); vi.useFakeTimers(); next = vi.fn(); diff --git a/src/store/middleware/check-auth.ts b/src/store/middleware/check-auth.ts index ae9bb9a38..df0dcd104 100644 --- a/src/store/middleware/check-auth.ts +++ b/src/store/middleware/check-auth.ts @@ -3,7 +3,6 @@ has been allowed. */ -import log from "loglevel"; import { isAction, type Middleware } from "redux"; import * as jimmListeners from "juju/jimm/listeners"; @@ -15,10 +14,11 @@ import { actions as jujuActions } from "store/juju"; import { addControllerCloudRegion } from "store/juju/thunks"; import type { RootState, Store } from "store/store"; import { isPayloadAction } from "types"; +import { logger } from "utils/logger"; function error(name: string, wsControllerURL?: string | null) { // Not shown in UI. Logged for debugging purposes. - log.error( + logger.error( "Unable to perform action: ", name, wsControllerURL @@ -33,7 +33,7 @@ function error(name: string, wsControllerURL?: string | null) { export const checkLoggedIn = (state: RootState, wsControllerURL: string) => { if (!wsControllerURL) { // Not shown in UI. Logged for debugging purposes. - log.error( + logger.error( "Unable to determine logged in status. " + "'wsControllerURL' was not provided in the action that was dispatched.", ); diff --git a/src/store/middleware/model-poller.test.ts b/src/store/middleware/model-poller.test.ts index 3bb3d25fb..c5e94e574 100644 --- a/src/store/middleware/model-poller.test.ts +++ b/src/store/middleware/model-poller.test.ts @@ -1,5 +1,4 @@ import type { Client, Connection, Transport } from "@canonical/jujulib"; -import log from "loglevel"; import type { UnknownAction, MiddlewareAPI } from "redux"; import type { Mock } from "vitest"; import { vi } from "vitest"; @@ -41,14 +40,6 @@ vi.mock("juju/jimm/api", () => ({ findAuditEvents: vi.fn(), })); -vi.mock("loglevel", async () => { - const actual = await vi.importActual("loglevel"); - return { - ...actual, - error: vi.fn(), - }; -}); - describe("model poller", () => { let fakeStore: MiddlewareAPI; let next: Mock; @@ -93,7 +84,6 @@ describe("model poller", () => { }); beforeEach(() => { - vi.spyOn(log, "error").mockImplementation(() => vi.fn()); vi.useFakeTimers(); next = vi.fn(); fakeStore = { @@ -495,10 +485,6 @@ describe("model poller", () => { new Error(ModelsError.LOAD_ALL_MODELS), ); await runMiddleware(); - expect(log.error).toHaveBeenCalledWith( - ModelsError.LOAD_ALL_MODELS, - new Error(ModelsError.LOAD_ALL_MODELS), - ); expect(fakeStore.dispatch).toHaveBeenCalledWith( jujuActions.updateModelsError({ modelsError: ModelsError.LOAD_ALL_MODELS, @@ -518,10 +504,6 @@ describe("model poller", () => { new Error(ModelsError.LOAD_SOME_MODELS), ); await runMiddleware(); - expect(log.error).toHaveBeenCalledWith( - ModelsError.LOAD_SOME_MODELS, - new Error(ModelsError.LOAD_SOME_MODELS), - ); expect(fakeStore.dispatch).toHaveBeenCalledWith( jujuActions.updateModelsError({ modelsError: ModelsError.LOAD_SOME_MODELS, @@ -549,14 +531,10 @@ describe("model poller", () => { ); runMiddleware({ payload: { controllers, isJuju: true, poll: 1 }, - }).catch(log.error); + }).catch(() => vi.fn()); vi.advanceTimersByTime(30000); // Resolve the async calls again. await vi.runAllTimersAsync(); - expect(log.error).toHaveBeenCalledWith( - ModelsError.LOAD_LATEST_MODELS, - new Error(ModelsError.LOAD_SOME_MODELS), - ); expect(fakeStore.dispatch).toHaveBeenCalledWith( jujuActions.updateModelsError({ modelsError: ModelsError.LOAD_LATEST_MODELS, @@ -578,10 +556,6 @@ describe("model poller", () => { }), ); await runMiddleware(); - expect(log.error).toHaveBeenCalledWith( - ModelsError.LIST_OR_UPDATE_MODELS, - new Error(ModelsError.LIST_OR_UPDATE_MODELS), - ); expect(fakeStore.dispatch).toHaveBeenCalledWith( jujuActions.updateModelsError({ modelsError: ModelsError.LIST_OR_UPDATE_MODELS, @@ -599,7 +573,7 @@ describe("model poller", () => { juju, })); runMiddleware({ payload: { controllers, isJuju: true, poll: 1 } }).catch( - log.error, + () => vi.fn(), ); vi.advanceTimersByTime(30000); // Resolve the async calls again. @@ -736,10 +710,6 @@ describe("model poller", () => { expect(fakeStore.dispatch).toHaveBeenCalledWith( jujuActions.updateAuditEventsErrors("Uh oh!"), ); - expect(log.error).toHaveBeenCalledWith( - "Could not fetch audit events.", - new Error("Uh oh!"), - ); }); it("handles fetching cross model query results", async () => { @@ -826,10 +796,6 @@ describe("model poller", () => { query: ".", }); await middleware(next)(action); - expect(log.error).toHaveBeenCalledWith( - "Could not perform cross model query.", - new Error("Uh oh!"), - ); expect(fakeStore.dispatch).toHaveBeenCalledWith( jujuActions.updateCrossModelQueryErrors("Uh oh!"), ); @@ -850,10 +816,6 @@ describe("model poller", () => { query: ".", }); await middleware(next)(action); - expect(log.error).toHaveBeenCalledWith( - "Could not perform cross model query.", - "Uh oh!", - ); expect(fakeStore.dispatch).toHaveBeenCalledWith( jujuActions.updateCrossModelQueryErrors( "Unable to perform search. Please try again later.", diff --git a/src/store/middleware/model-poller.ts b/src/store/middleware/model-poller.ts index d3c755a95..e505ec385 100644 --- a/src/store/middleware/model-poller.ts +++ b/src/store/middleware/model-poller.ts @@ -1,7 +1,6 @@ import type { Client } from "@canonical/jujulib"; import { unwrapResult } from "@reduxjs/toolkit"; import * as Sentry from "@sentry/browser"; -import log from "loglevel"; import { isAction, type Middleware } from "redux"; import { @@ -23,6 +22,7 @@ import { actions as jujuActions } from "store/juju"; import type { RootState, Store } from "store/store"; import { isSpecificAction } from "types"; import { toErrorString } from "utils"; +import { logger } from "utils/logger"; export enum LoginError { LOG = "Unable to log into controller.", @@ -236,7 +236,7 @@ export const modelPollerMiddleware: Middleware< } else { errorMessage = ModelsError.LIST_OR_UPDATE_MODELS; } - log.error(errorMessage, error); + logger.error(errorMessage, error); reduxStore.dispatch( jujuActions.updateModelsError({ modelsError: errorMessage, @@ -306,7 +306,7 @@ export const modelPollerMiddleware: Middleware< const auditEvents = await findAuditEvents(conn, params); reduxStore.dispatch(jujuActions.updateAuditEvents(auditEvents.events)); } catch (error) { - log.error("Could not fetch audit events.", error); + logger.error("Could not fetch audit events.", error); reduxStore.dispatch( jujuActions.updateAuditEventsErrors(toErrorString(error)), ); @@ -342,7 +342,7 @@ export const modelPollerMiddleware: Middleware< ), ); } catch (error) { - log.error("Could not perform cross model query.", error); + logger.error("Could not perform cross model query.", error); const errorMessage = error instanceof Error ? error.message diff --git a/src/store/store.ts b/src/store/store.ts index b51377709..2016e2ebc 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -1,6 +1,5 @@ import type { UnknownAction } from "@reduxjs/toolkit"; import { combineReducers, configureStore } from "@reduxjs/toolkit"; -import log from "loglevel"; import { useCallback } from "react"; import type { TypedUseSelectorHook } from "react-redux"; import { useDispatch, useSelector, useStore } from "react-redux"; @@ -9,6 +8,7 @@ import generalReducer from "store/general"; import jujuReducer from "store/juju"; import checkAuth from "store/middleware/check-auth"; import { modelPollerMiddleware } from "store/middleware/model-poller"; +import { logger } from "utils/logger"; import { listenerMiddleware } from "./listenerMiddleware"; @@ -17,7 +17,7 @@ if (!import.meta.env.PROD && process.env.VITE_APP_MOCK_STORE) { try { preloadedState = JSON.parse(process.env.VITE_APP_MOCK_STORE); } catch (error) { - log.error("VITE_APP_MOCK_STORE could not be parsed"); + logger.error("VITE_APP_MOCK_STORE could not be parsed"); } } diff --git a/src/testing/setup.ts b/src/testing/setup.ts index c43293aa3..4485e8993 100644 --- a/src/testing/setup.ts +++ b/src/testing/setup.ts @@ -3,6 +3,8 @@ import type { DetachedWindowAPI } from "happy-dom"; import { vi } from "vitest"; import createFetchMock from "vitest-fetch-mock"; +import { logger } from "utils/logger"; + vi.mock("react-ga"); const fetchMocker = createFetchMock(vi); @@ -17,6 +19,8 @@ declare global { var jest: object; } +logger.setDefaultLevel(logger.levels.SILENT); + // Fix for RTL using fake timers: // https://github.com/testing-library/user-event/issues/1115#issuecomment-1565730917 globalThis.jest = { diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 000000000..b2c8232b7 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,3 @@ +import log from "loglevel"; + +export const logger = log.getLogger("JujuDashboard"); diff --git a/yarn.lock b/yarn.lock index 7d799b909..4d9427077 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8357,7 +8357,7 @@ __metadata: jest-websocket-mock: "npm:2.5.0" lodash.isequal: "npm:4.5.0" lodash.mergewith: "npm:4.6.2" - loglevel: "npm:^1.9.2" + loglevel: "npm:1.9.2" npm-package-json-lint: "npm:8.0.0" postcss: "npm:8.4.49" prettier: "npm:3.4.2" @@ -8535,7 +8535,7 @@ __metadata: languageName: node linkType: hard -"loglevel@npm:1.9.2, loglevel@npm:^1.9.2": +"loglevel@npm:1.9.2": version: 1.9.2 resolution: "loglevel@npm:1.9.2" checksum: 10c0/1e317fa4648fe0b4a4cffef6de037340592cee8547b07d4ce97a487abe9153e704b98451100c799b032c72bb89c9366d71c9fb8192ada8703269263ae77acdc7