From cab44cdb7a2d9b9874170d52ea1b359c15c2e732 Mon Sep 17 00:00:00 2001 From: Alexandre Rousseau Date: Wed, 18 Dec 2024 12:15:08 +0100 Subject: [PATCH] chore(ui): introduce a logger proxy - WF-85 Introduce a simple abstraction to use logger in the application. For the moment, it's just a proxy to `console`, but it can be plugged to any library later. It's also a good way to prevent that logs are not displayed in the tests. --- .../components/core/content/CoreDataframe.vue | 4 +-- .../core/content/CoreDataframe/useJobs.ts | 5 ++-- .../core/content/CoreDataframeLegacy.vue | 4 +-- .../src/components/core/embed/CoreMapbox.vue | 4 +-- .../core/other/CoreWebcamCapture.vue | 9 ++++--- .../components/shared/SharedControlBar.vue | 5 ++-- src/ui/src/composables/useLogger.ts | 16 ++++++++++++ src/ui/src/core/auditAndFix.ts | 21 ++++++++++----- src/ui/src/core/index.ts | 26 +++++++++---------- src/ui/src/core/loadExtensions.ts | 10 +++---- src/ui/src/main.ts | 14 +++++++--- 11 files changed, 74 insertions(+), 44 deletions(-) create mode 100644 src/ui/src/composables/useLogger.ts diff --git a/src/ui/src/components/core/content/CoreDataframe.vue b/src/ui/src/components/core/content/CoreDataframe.vue index fa9470da4..58c1a08e8 100644 --- a/src/ui/src/components/core/content/CoreDataframe.vue +++ b/src/ui/src/components/core/content/CoreDataframe.vue @@ -134,6 +134,7 @@ import { UNNAMED_INDEX_COLUMN_PATTERN, } from "./CoreDataframe/constants"; import WdsButton from "@/wds/WdsButton.vue"; +import { useLogger } from "@/composables/useLogger"; const description = "A component to display Pandas DataFrames."; @@ -515,8 +516,7 @@ async function loadData() { [ARQUERO_INTERNAL_ID]: () => aq.op.row_number(), }); } catch (e) { - // eslint-disable-next-line no-console - console.error("Couldn't load dataframe from Arrow URL.", e); + useLogger().error("Couldn't load dataframe from Arrow URL.", e); } finally { isLoadingData.value = false; } diff --git a/src/ui/src/components/core/content/CoreDataframe/useJobs.ts b/src/ui/src/components/core/content/CoreDataframe/useJobs.ts index 7d715ec41..437efa948 100644 --- a/src/ui/src/components/core/content/CoreDataframe/useJobs.ts +++ b/src/ui/src/components/core/content/CoreDataframe/useJobs.ts @@ -1,3 +1,4 @@ +import { useLogger } from "@/composables/useLogger"; import { shallowRef, ref, readonly } from "vue"; /** @@ -6,6 +7,7 @@ import { shallowRef, ref, readonly } from "vue"; export function useJobs(handler: (value: T) => Promise) { const jobs = shallowRef([]); const isRunning = ref(false); + const logger = useLogger(); async function run() { if (isRunning.value) return; @@ -18,8 +20,7 @@ export function useJobs(handler: (value: T) => Promise) { try { await handler(job); } catch (error) { - // eslint-disable-next-line no-console - console.error("Error during handling job", job, error); + logger.error("Error during handling job", job, error); } finally { jobs.value = rest; } diff --git a/src/ui/src/components/core/content/CoreDataframeLegacy.vue b/src/ui/src/components/core/content/CoreDataframeLegacy.vue index cddbe3073..c4912f6a5 100644 --- a/src/ui/src/components/core/content/CoreDataframeLegacy.vue +++ b/src/ui/src/components/core/content/CoreDataframeLegacy.vue @@ -125,6 +125,7 @@ import { onUnmounted } from "vue"; import WdsTextInput from "@/wds/WdsTextInput.vue"; import WdsControl from "@/wds/WdsControl.vue"; import BaseMarkdown from "../base/BaseMarkdown.vue"; +import { useLogger } from "@/composables/useLogger"; const description = "A component to display Pandas DataFrames."; const defaultDataframe = `data:application/vnd.apache.arrow.file;base64,QVJST1cxAAD/////iAMAABAAAAAAAAoADgAGAAUACAAKAAAAAAEEABAAAAAAAAoADAAAAAQACAAKAAAAlAIAAAQAAAABAAAADAAAAAgADAAEAAgACAAAAGwCAAAEAAAAXwIAAHsiaW5kZXhfY29sdW1ucyI6IFsiX19pbmRleF9sZXZlbF8wX18iXSwgImNvbHVtbl9pbmRleGVzIjogW3sibmFtZSI6IG51bGwsICJmaWVsZF9uYW1lIjogbnVsbCwgInBhbmRhc190eXBlIjogInVuaWNvZGUiLCAibnVtcHlfdHlwZSI6ICJvYmplY3QiLCAibWV0YWRhdGEiOiB7ImVuY29kaW5nIjogIlVURi04In19XSwgImNvbHVtbnMiOiBbeyJuYW1lIjogImNvbF9hIiwgImZpZWxkX25hbWUiOiAiY29sX2EiLCAicGFuZGFzX3R5cGUiOiAiaW50NjQiLCAibnVtcHlfdHlwZSI6ICJpbnQ2NCIsICJtZXRhZGF0YSI6IG51bGx9LCB7Im5hbWUiOiAiY29sX2IiLCAiZmllbGRfbmFtZSI6ICJjb2xfYiIsICJwYW5kYXNfdHlwZSI6ICJpbnQ2NCIsICJudW1weV90eXBlIjogImludDY0IiwgIm1ldGFkYXRhIjogbnVsbH0sIHsibmFtZSI6IG51bGwsICJmaWVsZF9uYW1lIjogIl9faW5kZXhfbGV2ZWxfMF9fIiwgInBhbmRhc190eXBlIjogImludDY0IiwgIm51bXB5X3R5cGUiOiAiaW50NjQiLCAibWV0YWRhdGEiOiBudWxsfV0sICJjcmVhdG9yIjogeyJsaWJyYXJ5IjogInB5YXJyb3ciLCAidmVyc2lvbiI6ICIxMi4wLjAifSwgInBhbmRhc192ZXJzaW9uIjogIjEuNS4zIn0ABgAAAHBhbmRhcwAAAwAAAIgAAABEAAAABAAAAJT///8AAAECEAAAACQAAAAEAAAAAAAAABEAAABfX2luZGV4X2xldmVsXzBfXwAAAJD///8AAAABQAAAAND///8AAAECEAAAABgAAAAEAAAAAAAAAAUAAABjb2xfYgAAAMD///8AAAABQAAAABAAFAAIAAYABwAMAAAAEAAQAAAAAAABAhAAAAAgAAAABAAAAAAAAAAFAAAAY29sX2EAAAAIAAwACAAHAAgAAAAAAAABQAAAAAAAAAD/////6AAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAMAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAfAAAABAAAAACAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAABAAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAIAAAAAAAAAAwAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAD/////AAAAABAAAAAMABQABgAIAAwAEAAMAAAAAAAEADwAAAAoAAAABAAAAAEAAACYAwAAAAAAAPAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAACgAMAAAABAAIAAoAAACUAgAABAAAAAEAAAAMAAAACAAMAAQACAAIAAAAbAIAAAQAAABfAgAAeyJpbmRleF9jb2x1bW5zIjogWyJfX2luZGV4X2xldmVsXzBfXyJdLCAiY29sdW1uX2luZGV4ZXMiOiBbeyJuYW1lIjogbnVsbCwgImZpZWxkX25hbWUiOiBudWxsLCAicGFuZGFzX3R5cGUiOiAidW5pY29kZSIsICJudW1weV90eXBlIjogIm9iamVjdCIsICJtZXRhZGF0YSI6IHsiZW5jb2RpbmciOiAiVVRGLTgifX1dLCAiY29sdW1ucyI6IFt7Im5hbWUiOiAiY29sX2EiLCAiZmllbGRfbmFtZSI6ICJjb2xfYSIsICJwYW5kYXNfdHlwZSI6ICJpbnQ2NCIsICJudW1weV90eXBlIjogImludDY0IiwgIm1ldGFkYXRhIjogbnVsbH0sIHsibmFtZSI6ICJjb2xfYiIsICJmaWVsZF9uYW1lIjogImNvbF9iIiwgInBhbmRhc190eXBlIjogImludDY0IiwgIm51bXB5X3R5cGUiOiAiaW50NjQiLCAibWV0YWRhdGEiOiBudWxsfSwgeyJuYW1lIjogbnVsbCwgImZpZWxkX25hbWUiOiAiX19pbmRleF9sZXZlbF8wX18iLCAicGFuZGFzX3R5cGUiOiAiaW50NjQiLCAibnVtcHlfdHlwZSI6ICJpbnQ2NCIsICJtZXRhZGF0YSI6IG51bGx9XSwgImNyZWF0b3IiOiB7ImxpYnJhcnkiOiAicHlhcnJvdyIsICJ2ZXJzaW9uIjogIjEyLjAuMCJ9LCAicGFuZGFzX3ZlcnNpb24iOiAiMS41LjMifQAGAAAAcGFuZGFzAAADAAAAiAAAAEQAAAAEAAAAlP///wAAAQIQAAAAJAAAAAQAAAAAAAAAEQAAAF9faW5kZXhfbGV2ZWxfMF9fAAAAkP///wAAAAFAAAAA0P///wAAAQIQAAAAGAAAAAQAAAAAAAAABQAAAGNvbF9iAAAAwP///wAAAAFAAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAECEAAAACAAAAAEAAAAAAAAAAUAAABjb2xfYQAAAAgADAAIAAcACAAAAAAAAAFAAAAAsAMAAEFSUk9XMQ==`; @@ -418,8 +419,7 @@ async function loadData() { baseTable = aqTable; table.value = baseTable; } catch (e) { - // eslint-disable-next-line no-console - console.error("Couldn't load dataframe from Arrow URL.", e); + useLogger().error("Couldn't load dataframe from Arrow URL.", e); } } diff --git a/src/ui/src/components/core/embed/CoreMapbox.vue b/src/ui/src/components/core/embed/CoreMapbox.vue index 5f335bb11..358e2af5b 100644 --- a/src/ui/src/components/core/embed/CoreMapbox.vue +++ b/src/ui/src/components/core/embed/CoreMapbox.vue @@ -12,6 +12,7 @@ diff --git a/src/ui/src/composables/useLogger.ts b/src/ui/src/composables/useLogger.ts new file mode 100644 index 000000000..de9c449a4 --- /dev/null +++ b/src/ui/src/composables/useLogger.ts @@ -0,0 +1,16 @@ +/* eslint-disable no-console */ + +/** + * A simple abstraction to use logger in the application. For the moment, it's just a proxy to `console`, but it can be plugged to any library later. + */ +export function useLogger(): Pick< + typeof console, + "log" | "warn" | "info" | "error" +> { + return { + log: console.log, + warn: console.warn, + info: console.info, + error: console.error, + }; +} diff --git a/src/ui/src/core/auditAndFix.ts b/src/ui/src/core/auditAndFix.ts index 13ecd74b6..d194c2485 100644 --- a/src/ui/src/core/auditAndFix.ts +++ b/src/ui/src/core/auditAndFix.ts @@ -4,6 +4,7 @@ import { WriterComponentDefinition, } from "@/writerTypes"; import { getComponentDefinition } from "./templateMap"; +import { useLogger } from "@/composables/useLogger"; /** * Audits integrity of ComponentMap. Applies automatic fixes if necessary. @@ -23,9 +24,11 @@ export function auditAndFixComponents(components: ComponentMap): boolean { } export function auditAndFixComponent(component: Component): boolean { + const logger = useLogger(); + const def = getComponentDefinition(component.type); if (!def || def.category == "Fallback") { - console.error( + logger.error( `Component ${component.id} (${component.type}). Invalid component type.`, ); return false; @@ -53,6 +56,7 @@ function traverseComponentTree( } function auditOrphanComponents(components: ComponentMap) { + const logger = useLogger(); const visited = Object.fromEntries( Object.entries(components).map(([componentId]) => [componentId, false]), ); @@ -60,7 +64,7 @@ function auditOrphanComponents(components: ComponentMap) { traverseComponentTree("workflows_root", components, visited); Object.entries(visited).forEach(([componentId, isVisited]) => { if (!isVisited) { - console.warn( + logger.warn( `Component ${componentId} (${components[componentId].type}). Orphan component.`, ); } @@ -71,11 +75,12 @@ function auditComponentFieldKeys( component: Component, def: WriterComponentDefinition, ) { + const logger = useLogger(); const fieldKeys = Object.keys(def.fields ?? {}); if (!component.content) return; Object.keys(component.content).forEach((contentFieldKey) => { if (fieldKeys.includes(contentFieldKey)) return; - console.warn( + logger.warn( `Component ${component.id} (${component.type}). Field key "${contentFieldKey}" is defined in the component but not in the template.`, ); }); @@ -93,7 +98,8 @@ function auditComponentBinding( def.events[boundEventType].bindable ) return; - console.warn( + const logger = useLogger(); + logger.warn( `Component ${component.id} (${component.type}). The component is bound to event "${component.binding.eventType}" but the template doesn't define that event or it's not bindable.`, ); } @@ -110,14 +116,15 @@ function auditAndFixPositions( component: Component, components: ComponentMap, ): boolean { + const logger = useLogger(); let isFixApplied = false; if (component.id == "root") { if (component.position !== 0) { - console.error("Root must be at position 0."); + logger.error("Root must be at position 0."); } } if (component.position == -1) { - console.error( + logger.error( `Component ${component.id} (${component.type}). Invalid position.`, ); } @@ -136,7 +143,7 @@ function auditAndFixPositions( const arithmeticProgression = ((positionfulChildren.length - 1) * positionfulChildren.length) / 2; if (arithmeticProgression !== positionSum) { - console.error( + logger.error( `Component ${component.id} (${component.type}). Invalid children positions. Automated fix will be applied.`, ); fixPositions(positionfulChildren); diff --git a/src/ui/src/core/index.ts b/src/ui/src/core/index.ts index 7a1e33f70..34ae5a72a 100644 --- a/src/ui/src/core/index.ts +++ b/src/ui/src/core/index.ts @@ -20,6 +20,7 @@ import { auditAndFixComponents } from "./auditAndFix"; import { parseAccessor } from "./parsing"; import { loadExtensions } from "./loadExtensions"; import { bigIntReplacer } from "./serializer"; +import { useLogger } from "@/composables/useLogger"; const RECONNECT_DELAY_MS = 1000; const KEEP_ALIVE_DELAY_MS = 60000; @@ -191,6 +192,8 @@ export function generateCore() { async function startSync(): Promise { if (webSocket) return; // Open WebSocket exists + const logger = useLogger(); + const url = new URL("./api/stream", window.location.href); url.protocol = url.protocol.replace("https", "wss"); url.protocol = url.protocol.replace("http", "ws"); @@ -199,7 +202,7 @@ export function generateCore() { webSocket.onopen = () => { clearFrontendMap(); syncHealth.value = "connected"; - console.log("WebSocket connected. Initialising stream..."); + logger.log("WebSocket connected. Initialising stream..."); sendFrontendMessage("streamInit", { sessionId }); }; @@ -236,7 +239,7 @@ export function generateCore() { // Connection established correctly but closed due to invalid session. // Do not attempt to reconnect, the session will remain invalid. Initialise a new session. - console.error("Invalid session. Reinitialising..."); + logger.error("Invalid session. Reinitialising..."); // Take care of pending event resolutions and fail them. await initSession(); @@ -245,12 +248,12 @@ export function generateCore() { // Connection lost due to some other reason. Try to reconnect. - console.error("WebSocket closed. Attempting to reconnect..."); + logger.error("WebSocket closed. Attempting to reconnect..."); setTimeout(async () => { try { await startSync(); } catch { - console.error("Couldn't reconnect."); + logger.error("Couldn't reconnect."); } }, RECONNECT_DELAY_MS); }; @@ -336,11 +339,11 @@ export function generateCore() { /** * Sends a message to be hashed in the backend using the relevant keys. * Due to security reasons, it works only in edit mode. - * + * * @param message Messaged to be hashed * @returns The hashed message */ - async function hashMessage(message: string):Promise { + async function hashMessage(message: string): Promise { return new Promise((resolve, reject) => { const messageCallback = (r: { ok: boolean; @@ -353,13 +356,8 @@ export function generateCore() { resolve(r.payload?.message); }; - sendFrontendMessage( - "hashRequest", - { message }, - messageCallback, - ); + sendFrontendMessage("hashRequest", { message }, messageCallback); }); - } async function sendCodeSaveRequest(newCode: string): Promise { @@ -440,6 +438,7 @@ export function generateCore() { callback?.({ ok: false }); return; } + const logger = useLogger(); const trackingId = frontendMessageCounter++; try { if (callback || track) { @@ -468,8 +467,7 @@ export function generateCore() { } webSocket.send(JSON.stringify(wsData, bigIntReplacer)); } catch (error) { - // eslint-disable-next-line no-console - console.error("sendFrontendMessage error", error); + logger.error("sendFrontendMessage error", error); callback?.({ ok: false }); } } diff --git a/src/ui/src/core/loadExtensions.ts b/src/ui/src/core/loadExtensions.ts index 80a72fedb..268ce2871 100644 --- a/src/ui/src/core/loadExtensions.ts +++ b/src/ui/src/core/loadExtensions.ts @@ -1,3 +1,4 @@ +import { useLogger } from "@/composables/useLogger"; import { registerComponentTemplate } from "./templateMap"; const CUSTOM_COMPONENTS_GLOBAL_VAR = "WriterCustomComponentTemplates"; @@ -16,15 +17,16 @@ export async function loadExtensions(extensionPaths: string[]) { } async function importCustomComponentTemplate(path: string) { - console.log(`Importing custom component templates at "${path}"...`); + const logger = useLogger(); + logger.log(`Importing custom component templates at "${path}"...`); await import(/* @vite-ignore */ getRelativeExtensionsPath() + path); Object.entries(window[CUSTOM_COMPONENTS_GLOBAL_VAR])?.forEach( ([key, template]) => { if (checkComponentKey(key)) { registerComponentTemplate(`custom_${key}`, template); - console.log(`Registering template for "${key}".`); + logger.log(`Registering template for "${key}".`); } else { - console.warn( + logger.warn( `custom component '${key}' is ignored. A custom component should be declared using only alphanumeric lowercase and _.`, ); } @@ -62,5 +64,3 @@ function getRelativeExtensionsPath() { return `${pathname}extensions/`; } - - diff --git a/src/ui/src/main.ts b/src/ui/src/main.ts index 71893929c..f39ca0183 100644 --- a/src/ui/src/main.ts +++ b/src/ui/src/main.ts @@ -6,13 +6,19 @@ import { generateCore } from "./core"; import "./fonts"; import injectionKeys from "./injectionKeys"; import { setCaptureTabsDirective } from "./directives.js"; +import { useLogger } from "./composables/useLogger.js"; const wf = generateCore(); +// eslint-disable-next-line no-undef globalThis.vue = vue; +// eslint-disable-next-line no-undef globalThis.injectionKeys = injectionKeys; +// eslint-disable-next-line no-undef globalThis.core = wf; +const logger = useLogger(); + async function load() { await wf.init(); const mode = wf.mode.value; @@ -22,7 +28,7 @@ async function load() { wf.addMailSubscription("logEntry", ssbm.handleLogEntry); } - console.log(`Mounting app in mode ${mode}...`); + logger.log(`Mounting app in mode ${mode}...`); const { default: componentRenderer } = mode === "run" && (await import("./renderer/ComponentRenderer.vue")); @@ -38,12 +44,12 @@ async function load() { app.mount("#app"); } -console.log("Initialising Writer Framework core..."); +logger.log("Initialising Writer Framework core..."); load() .then(async () => { - console.log("Core initialised."); + logger.log("Core initialised."); }) .catch((reason) => { - console.error("Core initialisation failed.", reason); + logger.error("Core initialisation failed.", reason); document.write(reason); });