From abe338033d5bf0b5e92c79f4ffc35ae319fbee17 Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Fri, 3 Mar 2023 22:43:07 +0100 Subject: [PATCH 01/20] refactor: shared logging types --- .../src/common/context/LoggingContext.tsx | 23 ++++--------------- .../src/definitions/runtime/Logger.type.ts | 14 +++++++++++ packages/types/src/index.ts | 3 +++ 3 files changed, 22 insertions(+), 18 deletions(-) create mode 100644 packages/types/src/definitions/runtime/Logger.type.ts diff --git a/apps/client/src/common/context/LoggingContext.tsx b/apps/client/src/common/context/LoggingContext.tsx index b646e0fc04..b2d841a114 100644 --- a/apps/client/src/common/context/LoggingContext.tsx +++ b/apps/client/src/common/context/LoggingContext.tsx @@ -1,23 +1,10 @@ import { createContext, ReactNode, useCallback, useEffect, useState } from 'react'; +import { Log, LogLevel } from 'ontime-types'; import { generateId } from 'ontime-utils'; import socket from '../utils/socket'; import { nowInMillis, stringFromMillis } from '../utils/time'; -export enum LOG_LEVEL { - INFO = 'INFO', - WARN = 'WARN', - ERROR = 'ERROR', -} - -export type Log = { - id: string; - origin: string; - time: string; - level: LOG_LEVEL; - text: string; -}; - interface LoggingProviderState { logData: Log[]; emitInfo: (text: string) => void; @@ -70,7 +57,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => { * @private */ const _send = useCallback( - (text: string, level: LOG_LEVEL) => { + (text: string, level: LogLevel) => { if (socket != null) { const newLogMessage: Log = { id: generateId(), @@ -95,7 +82,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => { */ const emitInfo = useCallback( (text: string) => { - _send(text, LOG_LEVEL.INFO); + _send(text, LogLevel.Info); }, [_send], ); @@ -106,7 +93,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => { */ const emitWarning = useCallback( (text: string) => { - _send(text, LOG_LEVEL.WARN); + _send(text, LogLevel.Warn); }, [_send], ); @@ -117,7 +104,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => { */ const emitError = useCallback( (text: string) => { - _send(text, LOG_LEVEL.ERROR); + _send(text, LogLevel.Error); }, [_send], ); diff --git a/packages/types/src/definitions/runtime/Logger.type.ts b/packages/types/src/definitions/runtime/Logger.type.ts new file mode 100644 index 0000000000..9e2acf8111 --- /dev/null +++ b/packages/types/src/definitions/runtime/Logger.type.ts @@ -0,0 +1,14 @@ +export enum LogLevel { + Info = 'INFO', + Warn = 'WARN', + Error = 'ERROR', +} + +export type Log = { + id: string; + origin: string; + time: string; + level: LogLevel; + text: string; +}; + diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 5e757f583a..d7ffd4a5ab 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -13,6 +13,7 @@ import { OntimeRundown, OntimeRundownEntry } from './definitions/core/Rundown.ty import { OSCSettings, OscSubscription } from './definitions/core/OscSettings.type.js'; import { Playback } from './definitions/runtime/Playback.type.js'; import { Loaded } from './definitions/runtime/Playlist.type.js'; +import { Log, LogLevel } from './definitions/runtime/Logger.type.js'; import { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js'; import { Settings } from './definitions/core/Settings.type.js'; import { TimerLifeCycle } from './definitions/core/TimerLifecycle.type.js'; @@ -52,6 +53,8 @@ export type { OscSubscription, OSCSettings }; // ---> HTTP // SERVER RUNTIME +export { LogLevel }; +export type { Log }; export { Playback }; export { TimerLifeCycle }; From 788b9a3109a1387c828c6e24b95c8133652ecb6f Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Fri, 3 Mar 2023 22:43:53 +0100 Subject: [PATCH 02/20] refactor: simplify message service consumption --- .../src/services/message-service/MessageService.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/server/src/services/message-service/MessageService.ts b/apps/server/src/services/message-service/MessageService.ts index 9478b3b57f..0d129e4bec 100644 --- a/apps/server/src/services/message-service/MessageService.ts +++ b/apps/server/src/services/message-service/MessageService.ts @@ -88,10 +88,14 @@ class MessageService { } /** - * @description set state of onAir + * @description set state of onAir, toggles if parameters are offered */ - setOnAir(status: boolean) { - this.onAir = status; + setOnAir(status?: boolean) { + if (!status) { + this.onAir = !this.onAir; + } else { + this.onAir = status; + } this.updateStore(); return this.getAll(); } From e3464edd11f083f60c1df42ace58a99640ee80bd Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Fri, 3 Mar 2023 23:20:46 +0100 Subject: [PATCH 03/20] refactor: create discrete logging system --- apps/server/src/classes/Logger.ts | 110 ++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 apps/server/src/classes/Logger.ts diff --git a/apps/server/src/classes/Logger.ts b/apps/server/src/classes/Logger.ts new file mode 100644 index 0000000000..ba4806292b --- /dev/null +++ b/apps/server/src/classes/Logger.ts @@ -0,0 +1,110 @@ +import { Log, LogLevel } from 'ontime-types'; +import { generateId } from 'ontime-utils'; + +import { stringFromMillis } from '../utils/time.js'; +import { clock } from '../services/Clock.js'; +import { isProduction } from '../setup.js'; + +type LogMessage = { + type: 'ontime-log'; + payload: Log; +}; + +class Logger { + private push: (log: LogMessage) => void | null; + private queue: Log[]; + + constructor(emitCallback?: (message) => void) { + this.push = emitCallback; + this.queue = []; + } + + /** + * Enabling setup logger after init + * @param emitCallback + */ + init(emitCallback: (message) => void) { + this.push = emitCallback; + this.queue.forEach((log) => { + this._push(log); + }); + this.queue = []; + } + + /** + * Internal safe push method, adds log to queue if callback not available + * @param log + */ + private _push(log: Log) { + if (!isProduction) { + console.log(`[${log.level}] \t ${log.origin} \t ${log.text}`); + } + + if (this.push) { + this.push({ + type: 'ontime-log', + payload: log, + }); + } else { + this.queue.push(log); + if (this.queue.length > 100) { + this.queue.pop(); + } + } + } + + /** + * Emits logging message + * @param level + * @param origin + * @param text + */ + emit(level, origin: string, text: string) { + const log = { + id: generateId(), + level, + origin, + text, + time: stringFromMillis(clock.getSystemTime() || 0), + }; + this._push(log); + } + + /** + * Utility to emit logging message of type INFO + * @param origin + * @param text + */ + info(origin: string, text: string) { + this.emit(LogLevel.Info, origin, text); + } + + /** + * Utility to emit logging message of type WARN + * @param origin + * @param text + */ + warning(origin: string, text: string) { + this.emit(LogLevel.Warn, origin, text); + } + + /** + * Utility to emit logging message of type ERROR + * @param origin + * @param text + */ + error(origin: string, text: string) { + this.emit(LogLevel.Error, origin, text); + } + + /** + * Shutdown logger + */ + shutdown() { + console.log('Shutting down logger'); + this.push = null; + this.queue = []; + } +} + +export const logger = new Logger(); From e53fa47b68184f7405fdd4b9e1079179dbdef80d Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Fri, 3 Mar 2023 23:21:32 +0100 Subject: [PATCH 04/20] refactor: move socket.io > websocket --- apps/client/package.json | 2 +- apps/server/package.json | 3 +- pnpm-lock.yaml | 140 ++++++++------------------------------- 3 files changed, 29 insertions(+), 116 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index a062b66edb..5014c95ab3 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -29,7 +29,7 @@ "react-qr-code": "^2.0.11", "react-router-dom": "^6.3.0", "react-table": "^7.7.0", - "socket.io-client": "^4.5.4", + "react-use-websocket": "^4.3.1", "typeface-open-sans": "^1.1.13", "web-vitals": "^3.1.1" }, diff --git a/apps/server/package.json b/apps/server/package.json index 3192b820e7..bd9c088ec4 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -21,12 +21,13 @@ "ontime-utils": "workspace:*", "passport": "^0.6.0", "passport-local": "~1.0.0", - "socket.io": "^4.5.4" + "ws": "^8.12.1" }, "devDependencies": { "@types/express": "^4.17.17", "@types/node": "^16.11.7", "@types/node-osc": "^6.0.0", + "@types/websocket": "^1.0.5", "@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/parser": "^5.48.1", "esbuild": "^0.17.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b68c55e57..3909684320 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,8 +83,8 @@ importers: react-qr-code: ^2.0.11 react-router-dom: ^6.3.0 react-table: ^7.7.0 + react-use-websocket: ^4.3.1 sass: ^1.57.1 - socket.io-client: ^4.5.4 stylelint: ^14.16.1 stylelint-config-prettier: ^9.0.4 stylelint-config-standard-scss: ^6.1.0 @@ -121,7 +121,7 @@ importers: react-qr-code: 2.0.11_react@18.2.0 react-router-dom: 6.6.2_biqbaboplfbrettd7655fr4n2y react-table: 7.8.0_react@18.2.0 - socket.io-client: 4.5.4 + react-use-websocket: 4.3.1_biqbaboplfbrettd7655fr4n2y typeface-open-sans: 1.1.13 web-vitals: 3.1.1 devDependencies: @@ -182,6 +182,7 @@ importers: '@types/express': ^4.17.17 '@types/node': ^16.11.7 '@types/node-osc': ^6.0.0 + '@types/websocket': ^1.0.5 '@typescript-eslint/eslint-plugin': ^5.48.1 '@typescript-eslint/parser': ^5.48.1 body-parser: ^1.20.0 @@ -205,10 +206,10 @@ importers: passport-local: ~1.0.0 prettier: ^2.8.3 shx: ^0.3.4 - socket.io: ^4.5.4 ts-node: ^10.9.1 typescript: ^4.9.4 vitest: ^0.27.1 + ws: ^8.12.1 dependencies: '@sentry/node': 7.30.0 '@sentry/tracing': 7.30.0 @@ -226,11 +227,12 @@ importers: ontime-utils: link:../../packages/utils passport: 0.6.0 passport-local: 1.0.0 - socket.io: 4.5.4 + ws: 8.12.1 devDependencies: '@types/express': 4.17.17 '@types/node': 16.18.11 '@types/node-osc': 6.0.0 + '@types/websocket': 1.0.5 '@typescript-eslint/eslint-plugin': 5.48.1_3jon24igvnqaqexgwtxk6nkpse '@typescript-eslint/parser': 5.48.1_iukboom6ndih5an6iafl45j2fe esbuild: 0.17.5 @@ -2608,10 +2610,6 @@ packages: engines: {node: '>=6'} dev: true - /@socket.io/component-emitter/3.1.0: - resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} - dev: false - /@svgr/babel-plugin-add-jsx-attribute/6.5.1_@babel+core@7.20.12: resolution: {integrity: sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==} engines: {node: '>=10'} @@ -2906,16 +2904,6 @@ packages: '@types/node': 18.11.18 dev: true - /@types/cookie/0.4.1: - resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} - dev: false - - /@types/cors/2.8.13: - resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==} - dependencies: - '@types/node': 18.11.18 - dev: false - /@types/debug/4.1.7: resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} dependencies: @@ -3035,6 +3023,7 @@ packages: /@types/node/18.11.18: resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==} + dev: true /@types/normalize-package-data/2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} @@ -3127,6 +3116,12 @@ packages: dev: true optional: true + /@types/websocket/1.0.5: + resolution: {integrity: sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ==} + dependencies: + '@types/node': 18.11.18 + dev: true + /@types/yargs-parser/21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} dev: true @@ -3764,11 +3759,6 @@ packages: requiresBuild: true dev: true - /base64id/2.0.0: - resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} - engines: {node: ^4.5.0 || >= 5.9} - dev: false - /binary-extensions/2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -4667,45 +4657,6 @@ packages: once: 1.4.0 dev: true - /engine.io-client/6.2.3: - resolution: {integrity: sha512-aXPtgF1JS3RuuKcpSrBtimSjYvrbhKW9froICH4s0F3XQWLxsKNxqzG39nnvQZQnva4CMvUK63T7shevxRyYHw==} - dependencies: - '@socket.io/component-emitter': 3.1.0 - debug: 4.3.4 - engine.io-parser: 5.0.5 - ws: 8.2.3 - xmlhttprequest-ssl: 2.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - - /engine.io-parser/5.0.5: - resolution: {integrity: sha512-mjEyaa4zhuuRhaSLOdjEb57X0XPP9JEsnXI4E+ivhwT0GgzUogARx4MqoY1jQyB+4Bkz3BUOmzL7t9RMKmlG3g==} - engines: {node: '>=10.0.0'} - dev: false - - /engine.io/6.2.1: - resolution: {integrity: sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==} - engines: {node: '>=10.0.0'} - dependencies: - '@types/cookie': 0.4.1 - '@types/cors': 2.8.13 - '@types/node': 18.11.18 - accepts: 1.3.8 - base64id: 2.0.0 - cookie: 0.4.2 - cors: 2.8.5 - debug: 4.3.4 - engine.io-parser: 5.0.5 - ws: 8.2.3 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /entities/4.4.0: resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} engines: {node: '>=0.12'} @@ -7613,6 +7564,16 @@ packages: react: 18.2.0 dev: false + /react-use-websocket/4.3.1_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-zHPLWrgcqydJaak2O5V9hiz4q2dwkwqNQqpgFVmSuPxLZdsZlnDs8DVHy3WtHH+A6ms/8aHIyX7+7ulOcrnR0Q==} + peerDependencies: + react: '>= 18.0.0' + react-dom: '>= 18.0.0' + dependencies: + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: false + /react/18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -8025,50 +7986,6 @@ packages: dev: true optional: true - /socket.io-adapter/2.4.0: - resolution: {integrity: sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==} - dev: false - - /socket.io-client/4.5.4: - resolution: {integrity: sha512-ZpKteoA06RzkD32IbqILZ+Cnst4xewU7ZYK12aS1mzHftFFjpoMz69IuhP/nL25pJfao/amoPI527KnuhFm01g==} - engines: {node: '>=10.0.0'} - dependencies: - '@socket.io/component-emitter': 3.1.0 - debug: 4.3.4 - engine.io-client: 6.2.3 - socket.io-parser: 4.2.1 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - - /socket.io-parser/4.2.1: - resolution: {integrity: sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==} - engines: {node: '>=10.0.0'} - dependencies: - '@socket.io/component-emitter': 3.1.0 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: false - - /socket.io/4.5.4: - resolution: {integrity: sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==} - engines: {node: '>=10.0.0'} - dependencies: - accepts: 1.3.8 - base64id: 2.0.0 - debug: 4.3.4 - engine.io: 6.2.1 - socket.io-adapter: 2.4.0 - socket.io-parser: 4.2.1 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /source-map-js/1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -9307,12 +9224,12 @@ packages: optional: true dev: true - /ws/8.2.3: - resolution: {integrity: sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==} + /ws/8.12.1: + resolution: {integrity: sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 + utf-8-validate: '>=5.0.2' peerDependenciesMeta: bufferutil: optional: true @@ -9349,11 +9266,6 @@ packages: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} dev: true - /xmlhttprequest-ssl/2.0.0: - resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} - engines: {node: '>=0.4.0'} - dev: false - /xtend/4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} From 5b977790b052e883033efa101e0f047800855ca0 Mon Sep 17 00:00:00 2001 From: Fabian Posenau Date: Sat, 4 Mar 2023 22:40:19 +0100 Subject: [PATCH 05/20] Feat/update check (#303) * feat: show latest version in settings modal --------- Co-authored-by: Fabian Posenau --- apps/client/src/common/api/apiConstants.ts | 3 + apps/client/src/common/api/ontimeApi.ts | 11 +++- .../src/features/modals/AppSettingsModal.jsx | 61 +++++++++++++------ 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/apps/client/src/common/api/apiConstants.ts b/apps/client/src/common/api/apiConstants.ts index 287e4d8009..070e4a0f33 100644 --- a/apps/client/src/common/api/apiConstants.ts +++ b/apps/client/src/common/api/apiConstants.ts @@ -19,6 +19,9 @@ export const FEAT_PLAYBACKCONTROL = 'feat-playbackcontrol'; export const FEAT_RUNDOWN = 'feat-rundown'; export const TIMER = 'timer'; +// external stuff +export const githubURL = 'https://api.github.com/repos/cpvalente/ontime/releases/latest'; + /** * @description finds server path given the current location, it * @return {*} diff --git a/apps/client/src/common/api/ontimeApi.ts b/apps/client/src/common/api/ontimeApi.ts index 9f90da5d07..9c0b3fa113 100644 --- a/apps/client/src/common/api/ontimeApi.ts +++ b/apps/client/src/common/api/ontimeApi.ts @@ -3,7 +3,7 @@ import { Alias, OSCSettings, Settings, UserFields, ViewSettings } from 'ontime-t import { InfoType } from '../models/Info'; -import { ontimeURL } from './apiConstants'; +import { githubURL, ontimeURL } from './apiConstants'; /** * @description HTTP request to retrieve application settings @@ -151,3 +151,12 @@ export const uploadData = async (file: string, setProgress: (value: number) => v }) .then((response) => response.data.id); }; + +/** + * @description HTTP request to get the latest version and url from github + * @return {Promise} + */ +export async function getLatestVersion(): Promise { + const res = await axios.get(`${githubURL}`); + return { url: res.data.html_url, version: res.data.tag_name }; +} diff --git a/apps/client/src/features/modals/AppSettingsModal.jsx b/apps/client/src/features/modals/AppSettingsModal.jsx index 8ecc394347..69d62f63db 100644 --- a/apps/client/src/features/modals/AppSettingsModal.jsx +++ b/apps/client/src/features/modals/AppSettingsModal.jsx @@ -1,6 +1,7 @@ import { useContext, useEffect, useState } from 'react'; import isEqual from 'react-fast-compare'; import { + Button, Checkbox, FormControl, FormLabel, @@ -16,7 +17,7 @@ import { FiX } from '@react-icons/all-files/fi/FiX'; import { useAtom } from 'jotai'; import { version } from '../../../package.json'; -import { postSettings } from '../../common/api/ontimeApi'; +import { getLatestVersion, postSettings } from '../../common/api/ontimeApi'; import { eventSettingsAtom } from '../../common/atoms/LocalEventSettings'; import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn'; import { LoggingContext } from '../../common/context/LoggingContext'; @@ -39,6 +40,9 @@ export default function AppSettingsModal() { const [eventSettings, saveEventSettings] = useAtom(eventSettingsAtom); const [formSettings, setFormSettings] = useState(eventSettings); + const [updateMessage, setUpdateMessage] = useState(Using ontime version: {version}); + const [isFetching, setIsFetching] = useState(false); + /** * Set formdata from server state */ @@ -49,6 +53,9 @@ export default function AppSettingsModal() { pinCode: data.pinCode, timeFormat: data.timeFormat, }); + // getLatestVersion().then((data) => { + // setVersionData(data); + // }); }, [changed, data]); /** @@ -59,7 +66,7 @@ export default function AppSettingsModal() { setSubmitting(true); const validation = { isValid: false, message: '' }; - const hasChanged = !isEqual(formSettings,eventSettings); + const hasChanged = !isEqual(formSettings, eventSettings); if (hasChanged) { saveEventSettings(formSettings); validation.isValid = true; @@ -93,7 +100,7 @@ export default function AppSettingsModal() { try { await postSettings(formData); } catch (error) { - emitError(`Error saving settings: ${error}`) + emitError(`Error saving settings: ${error}`); } finally { await refetch(); resetChange = true; @@ -128,6 +135,32 @@ export default function AppSettingsModal() { setChanged(true); }; + /** + * Handles version comparison and returns component with message + */ + const versionCheck = async () => { + let message = Using latest version; + setIsFetching(true); + getLatestVersion() + .then((data) => { + const remoteVersion = data; + if (!remoteVersion.version.includes(version)) { + message = ( + + Update to version {remoteVersion.version} available + + ); + } + }) + .catch(function () { + message = Error reaching server; + }) + .finally(function () { + setUpdateMessage(message); + setIsFetching(false); + }); + }; + const disableModal = status !== 'success'; return ( @@ -137,7 +170,6 @@ export default function AppSettingsModal() {
🔥 Changes take effect on save 🔥

-

{`Running ontime version ${version}`}

General App Settings
@@ -150,13 +182,7 @@ export default function AppSettingsModal() { Ontime is available at port - + @@ -255,13 +281,14 @@ export default function AppSettingsModal() { Event default public
+
+ {updateMessage} + +
- + ); From 5de5ecc03fc02957a298334805bd4875e58ca1c5 Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Sun, 5 Mar 2023 21:13:14 +0100 Subject: [PATCH 06/20] feat: add logging websockets --- apps/client/src/common/stores/createStore.ts | 15 ++ apps/client/src/common/stores/logger.ts | 112 +++++++++++ apps/client/src/features/info/InfoLogger.tsx | 20 +- apps/server/src/adapters/IAdapter.ts | 3 + apps/server/src/adapters/OscAdapter.ts | 54 +++++ apps/server/src/adapters/WebsocketAdapter.ts | 120 +++++++++++ apps/server/src/app.ts | 53 +++-- apps/server/src/classes/Logger.ts | 27 ++- .../src/controllers/integrationController.ts | 188 ++++++++++++++++++ .../src/definitions/runtime/Logger.type.ts | 4 + packages/types/src/index.ts | 4 +- 11 files changed, 559 insertions(+), 41 deletions(-) create mode 100644 apps/client/src/common/stores/createStore.ts create mode 100644 apps/client/src/common/stores/logger.ts create mode 100644 apps/server/src/adapters/IAdapter.ts create mode 100644 apps/server/src/adapters/OscAdapter.ts create mode 100644 apps/server/src/adapters/WebsocketAdapter.ts create mode 100644 apps/server/src/controllers/integrationController.ts diff --git a/apps/client/src/common/stores/createStore.ts b/apps/client/src/common/stores/createStore.ts new file mode 100644 index 0000000000..fe5d4136a3 --- /dev/null +++ b/apps/client/src/common/stores/createStore.ts @@ -0,0 +1,15 @@ +export default function createStore(initialState: T) { + let currentState = initialState; + const listeners = new Set<(state: T) => void>(); + + return { + get: () => currentState, + set: (newState: T) => { + currentState = newState; + }, + subscribe: (listener: (state: T) => void) => { + listeners.add(listener); + return () => listeners.delete(listener); + }, + }; +} diff --git a/apps/client/src/common/stores/logger.ts b/apps/client/src/common/stores/logger.ts new file mode 100644 index 0000000000..d519b5c533 --- /dev/null +++ b/apps/client/src/common/stores/logger.ts @@ -0,0 +1,112 @@ +import { useCallback, useSyncExternalStore } from 'react'; +import useWebSocket from 'react-use-websocket'; +import { Log, LogLevel, LogMessage } from 'ontime-types'; +import { generateId } from 'ontime-utils'; + +import { nowInMillis, stringFromMillis } from '../utils/time'; + +import createStore from './createStore'; + +const logger = createStore([]); +const MAX_MESSAGES = 100; + +export function useEmitLog() { + const { sendJsonMessage } = useWebSocket('ws://localhost:4001/ws', { + share: true, + shouldReconnect: () => true, + }); + + const _addToLogger = (log: Log) => { + const state = logger.get(); + console.log('DEBUG', state); + state.push(log); + if (state.length > MAX_MESSAGES) { + state.slice(1); + } + logger.set(state); + }; + + /** + * Utility function sends message over socket + * @param text + * @param level + * @private + */ + const _emit = (text: string, level: LogLevel) => { + const log = { + id: generateId(), + origin: 'CLIENT', + time: stringFromMillis(nowInMillis()), + level, + text, + }; + + const logMessage: LogMessage = { + type: 'ontime-log', + payload: log, + }; + _addToLogger(log); + sendJsonMessage(logMessage); + }; + + /** + * Sends a message with level INFO + * @param text + */ + const emitInfo = useCallback( + (text: string) => { + _emit(text, LogLevel.Info); + }, + [_emit], + ); + + /** + * Sends a message with level WARN + * @param text + */ + const emitWarning = useCallback( + (text: string) => { + _emit(text, LogLevel.Warn); + }, + [_emit], + ); + + /** + * Sends a message with level ERROR + * @param text + */ + const emitError = useCallback( + (text: string) => { + _emit(text, LogLevel.Error); + }, + [_emit], + ); + + const clearLog = useCallback(() => { + throw new Error('NOT IMPLEMENTED CLEAR LOG'); + }, []); + + return { + emitInfo, + emitWarning, + emitError, + clearLog, + }; +} + +// export function useSyncExternalLogger() { +// const { lastMessage, lastJsonMessage } = useWebSocket('ws://localhost:4001/ws', { +// share: true, +// shouldReconnect: () => true, +// onClose: () => console.log('closed socket connection'), +// onError: () => console.log('error in socket connection'), +// }); +// +// useEffect(() => { +// console.log('DEBUG LOGGER SYNC', lastMessage, lastJsonMessage); +// }, [lastMessage, lastJsonMessage]); +// } +// +export const useLogData = () => { + return useSyncExternalStore(logger.subscribe, () => logger.get()); +}; diff --git a/apps/client/src/features/info/InfoLogger.tsx b/apps/client/src/features/info/InfoLogger.tsx index 9ded93754d..b3e8d134b5 100644 --- a/apps/client/src/features/info/InfoLogger.tsx +++ b/apps/client/src/features/info/InfoLogger.tsx @@ -1,8 +1,9 @@ -import { useCallback, useContext, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Button } from '@chakra-ui/react'; +import { Log } from 'ontime-types'; import CollapseBar from '../../common/components/collapse-bar/CollapseBar'; -import { Log, LoggingContext } from '../../common/context/LoggingContext'; +import { useLogData } from '../../common/stores/logger'; import style from './InfoLogger.module.scss'; @@ -16,7 +17,8 @@ enum LOG_FILTER { } export default function InfoLogger() { - const { logData, clearLog } = useContext(LoggingContext); + const logData = useLogData(); + // const { logData, clearLog } = useContext(LoggingContext); const [data, setData] = useState([]); const [collapsed, setCollapsed] = useState(false); const [showClient, setShowClient] = useState(true); @@ -26,6 +28,10 @@ export default function InfoLogger() { const [showPlayback, setShowPlayback] = useState(true); const [showUser, setShowUser] = useState(true); + const clearLog = () => console.log('CALLED CLEAR'); + + console.log('DEBUG INFO', logData) + useEffect(() => { if (!logData) { return; @@ -65,7 +71,7 @@ export default function InfoLogger() { }, []); return ( -
+
setCollapsed((prev) => !prev)} /> {!collapsed && ( <> @@ -124,11 +130,7 @@ export default function InfoLogger() { > TX -
diff --git a/apps/server/src/adapters/IAdapter.ts b/apps/server/src/adapters/IAdapter.ts new file mode 100644 index 0000000000..2b58f4edf1 --- /dev/null +++ b/apps/server/src/adapters/IAdapter.ts @@ -0,0 +1,3 @@ +export interface IAdapter { + shutdown: () => void; +} diff --git a/apps/server/src/adapters/OscAdapter.ts b/apps/server/src/adapters/OscAdapter.ts new file mode 100644 index 0000000000..0d9688ec59 --- /dev/null +++ b/apps/server/src/adapters/OscAdapter.ts @@ -0,0 +1,54 @@ +import { Server } from 'node-osc'; +import { OSCSettings } from 'ontime-types'; + +import { IAdapter } from './IAdapter.js'; +import { dispatchFromAdapter } from '../controllers/integrationController.js'; +import { logger } from '../classes/Logger.js'; + +export class OscServer implements IAdapter { + private osc: Server; + + constructor(config: OSCSettings) { + this.osc = new Server(config.portIn, '0.0.0.0'); + + this.osc.on('error', console.error); + + this.osc.on('message', (msg) => { + // message should look like /ontime/{path} {args} where + // ontime: fixed message for app + // path: command to be called + // args: extra data, only used on some API entries (delay, goto) + + // split message + const [, address, path] = msg[0].split('/'); + const args = msg[1]; + + // get first part before (ontime) + if (address !== 'ontime') { + logger.error('RX', `OSC IN: OSC messages to ontime must start with /ontime/, received: ${msg}`); + return; + } + + // get second part (command) + if (!path) { + logger.error('RX', 'OSC IN: No path found'); + return; + } + + try { + const reply = dispatchFromAdapter(path, args, 'osc'); + if (reply) { + const { topic, payload } = reply; + this.osc.emit(topic, payload); + } + } catch (error) { + logger.error('RX', `OSC IN: ${error}`); + } + }); + } + + shutdown() { + console.log('Shutting down OSC Server'); + this.osc?.close(); + } +} diff --git a/apps/server/src/adapters/WebsocketAdapter.ts b/apps/server/src/adapters/WebsocketAdapter.ts new file mode 100644 index 0000000000..7ae071a1d1 --- /dev/null +++ b/apps/server/src/adapters/WebsocketAdapter.ts @@ -0,0 +1,120 @@ +/** + * DESIGN BY CONTRACT + * =================== + * All websocket calls are expected to follow the defined format, + * otherwise they will be ignored by Ontime server + * + * Messages should be in JSON format with two top level objects + * { + * type: ... + * payload: ... + * } + * + * Type: describes the action to be performed as enumerated in the API design + * Payload: adds necessary payload for the request to be completed + */ + +import { WebSocket, WebSocketServer } from 'ws'; + +import getRandomName from '../utils/getRandomName.js'; +import { IAdapter } from './IAdapter.js'; +import { eventStore } from '../stores/EventStore.js'; +import { dispatchFromAdapter } from '../controllers/integrationController.js'; +import { logger } from '../classes/Logger.js'; + +let instance; + +export class SocketServer implements IAdapter { + private readonly MAX_PAYLOAD = 1024 * 256; // 256Kb + + private wss: WebSocketServer | null; + private clientIds: Set; + + constructor(server) { + if (instance) { + throw new Error('There can be only one'); + } + + // eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton + instance = this; + this.clientIds = new Set(); + + this.wss = new WebSocketServer({ path: '/ws', server }); + + this.wss.on('connection', (ws) => { + const clientId = getRandomName(); + this.clientIds.add(clientId); + logger.info('RX', `${this.wss.clients.size} Connections with new: ${clientId}`); + console.log('DEBUG OPENED', this.wss.clients.size); + + // send store payload on connect + ws.send( + JSON.stringify({ + type: 'ontime', + payload: eventStore.poll(), + }), + ); + + ws.on('error', console.error); + + ws.on('close', () => { + logger.info('RX', `${this.wss.clients.size} Connections with disconnected: ${clientId}`); + this.clientIds.delete(clientId); + }); + + ws.on('message', (data) => { + if (data.length > this.MAX_PAYLOAD) { + ws.close(); + } + + // TODO: protocol specific stuff should be handled here + // eg: rename-client + // socket.on('rename-client', (newName) => { + // if (newName) { + // const previousName = this._clientNames[socket.id]; + // this._clientNames[socket.id] = newName; + // this.info('CLIENT', `Client ${previousName} renamed to ${newName}`); + // } + // }); + + try { + const message = JSON.parse(data); + const { type, payload } = message; + + if (type === 'hello') { + ws.send('hi'); + } + + if (type === 'ontime-log') { + console.log('attempted adding to log'); + } + + try { + const reply = dispatchFromAdapter(type, payload, 'ws'); + if (reply) { + const { topic, payload } = reply; + ws.send(topic, payload); + } + } catch (error) { + logger.error('RX', `WS IN: ${error}`); + } + } catch (_) { + return; + } + }); + }); + } + + // message is any serializable value + send(message: any) { + console.log('DEBUG SEND', this.wss); + this.wss?.clients.forEach((client) => { + if (client !== this.wss && client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(message)); + } + }); + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + shutdown() {} +} diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index d97cdb6af4..31b132ed61 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -6,7 +6,6 @@ import cors from 'cors'; // import utils import { join, resolve } from 'path'; -import { initiateOSC, shutdownOSCServer } from './controllers/OscController.js'; import { initSentry } from './modules/sentry.js'; import { currentDirectory, environment, isProduction, resolvedPath } from './setup.js'; import { ONTIME_VERSION } from './ONTIME_VERSION.js'; @@ -18,13 +17,17 @@ import { router as eventDataRouter } from './routes/eventDataRouter.js'; import { router as ontimeRouter } from './routes/ontimeRouter.js'; import { router as playbackRouter } from './routes/playbackRouter.js'; -// Services +// Import adapters +import { OscServer } from './adapters/OscAdapter.js'; +import { SocketServer } from './adapters/WebsocketAdapter.js'; import { DataProvider } from './classes/data-provider/DataProvider.js'; -import { socketProvider } from './classes/socket/SocketController.js'; -import { eventTimer } from './services/TimerService.js'; import { dbLoadingProcess } from './modules/loadDb.js'; + +// Services +import { eventTimer } from './services/TimerService.js'; import { integrationService } from './services/integration-service/IntegrationService.js'; import { OscIntegration } from './services/integration-service/OscIntegration.js'; +import { logger } from './classes/Logger.js'; console.log(`Starting Ontime version ${ONTIME_VERSION}`); @@ -35,9 +38,6 @@ if (!isProduction) { initSentry(environment); -// import socket provider -const socketServer = socketProvider; - // Create express APP const app = express(); app.disable('x-powered-by'); @@ -100,6 +100,9 @@ enum OntimeStartOrder { } let step = OntimeStartOrder.InitDB; +let expressServer = null; +let oscServer = null; + const checkStart = (currentState: OntimeStartOrder) => { if (step !== currentState) { step = OntimeStartOrder.Error; @@ -116,9 +119,6 @@ export const startDb = async () => { await dbLoadingProcess; }; -// create HTTP server -const expressServer = http.createServer(app); - /** * Starts servers * @return {Promise} @@ -128,11 +128,20 @@ export const startServer = async () => { const serverPort = 4001; // hardcoded for now const returnMessage = `Ontime is listening on port ${serverPort}`; + + expressServer = http.createServer(app); + + const socket = new SocketServer(expressServer); + expressServer.listen(serverPort, '0.0.0.0'); - socketServer.initServer(expressServer); - socketServer.info('SERVER', returnMessage); - socketServer.startListener(); + logger.init(socket.send); + + let i = 1; + setInterval(() => { + logger.info('RX', `TESTING ${i}`); + i++; + }, 2000); return returnMessage; }; @@ -149,7 +158,7 @@ export const startOSCServer = async (overrideConfig = null) => { const { osc } = DataProvider.getData(); if (!osc.enabledIn) { - socketServer.info('RX', 'OSC Input Disabled'); + logger.info('RX', 'OSC Input Disabled'); return; } @@ -160,8 +169,8 @@ export const startOSCServer = async (overrideConfig = null) => { }; // Start OSC Server - socketServer.info('RX', `Starting OSC Server on port: ${oscSettings.portIn}`); - initiateOSC(oscSettings); + logger.info('RX', `Starting OSC Server on port: ${oscSettings.portIn}`); + oscServer = new OscServer(oscSettings); }; /** @@ -178,7 +187,7 @@ export const startIntegrations = async (config?: { osc: OSCSettings }) => { const oscIntegration = new OscIntegration(); const { success, message } = oscIntegration.init(osc); - socketServer.info('RX', message); + logger.info('RX', message); if (success) { integrationService.register(oscIntegration); @@ -193,10 +202,10 @@ export const startIntegrations = async (config?: { osc: OSCSettings }) => { export const shutdown = async (exitCode = 0) => { console.log(`Ontime shutting down with code ${exitCode}`); - expressServer.close(); - shutdownOSCServer(); + expressServer?.close(); + oscServer?.shutdown(); eventTimer.shutdown(); - socketServer.shutdown(); + logger.shutdown(); integrationService.shutdown(); process.exit(exitCode); }; @@ -205,13 +214,13 @@ process.on('exit', (code) => console.log(`Ontime exited with code: ${code}`)); process.on('unhandledRejection', async (error, promise) => { console.error(error, 'Error: unhandled rejection', promise); - socketServer.error('SERVER', 'Error: unhandled rejection'); + logger.error('SERVER', 'Error: unhandled rejection'); await shutdown(1); }); process.on('uncaughtException', async (error, promise) => { console.error(error, 'Error: uncaught exception', promise); - socketServer.error('SERVER', 'Error: uncaught exception'); + logger.error('SERVER', 'Error: uncaught exception'); await shutdown(1); }); diff --git a/apps/server/src/classes/Logger.ts b/apps/server/src/classes/Logger.ts index ba4806292b..a8a07b4697 100644 --- a/apps/server/src/classes/Logger.ts +++ b/apps/server/src/classes/Logger.ts @@ -31,6 +31,13 @@ class Logger { this.queue = []; } + private addToQueue(log: Log) { + this.queue.push(log); + if (this.queue.length > 100) { + this.queue.pop(); + } + } + /** * Internal safe push method, adds log to queue if callback not available * @param log @@ -41,15 +48,19 @@ class Logger { } if (this.push) { - this.push({ - type: 'ontime-log', - payload: log, - }); - } else { - this.queue.push(log); - if (this.queue.length > 100) { - this.queue.pop(); + try { + this.push({ + type: 'ontime-log', + payload: log, + }); + console.log('DEBUG FAILED LOGGER SHOULDVE SENT') + + } catch (_e) { + console.log('DEBUG FAILED LOGGER SEND', _e) + this.addToQueue(log); } + } else { + this.addToQueue(log); } } diff --git a/apps/server/src/controllers/integrationController.ts b/apps/server/src/controllers/integrationController.ts new file mode 100644 index 0000000000..f75e67c0df --- /dev/null +++ b/apps/server/src/controllers/integrationController.ts @@ -0,0 +1,188 @@ +import { messageService } from '../services/message-service/MessageService.js'; +import { PlaybackService } from '../services/PlaybackService.js'; +import { eventStore } from '../stores/EventStore.js'; + +export function dispatchFromAdapter(type: string, payload: unknown, source?: 'osc' | 'ws') { + switch (type.toLowerCase()) { + case 'test-ontime': { + return { topic: 'hello' }; + } + + case 'ontime-poll': { + return { + topic: 'poll', + payload: eventStore.poll(), + }; + } + + case 'set-onair': { + if (payload) { + messageService.setOnAir(Boolean(payload)); + } + break; + } + + case 'onair': { + messageService.setOnAir(true); + break; + } + + case 'offair': { + messageService.setOnAir(false); + break; + } + + case 'timer-message-text': { + if (typeof payload !== 'string') { + throw new Error('Unable to parse payload'); + } + messageService.setTimerText(payload); + break; + } + case 'timer-message-visibility': { + if (!payload) { + throw new Error('Unable to parse payload'); + } + messageService.setTimerVisibility(Boolean(payload)); + break; + } + + case 'public-message-text': { + if (typeof payload !== 'string') { + throw new Error('Unable to parse payload'); + } + messageService.setPublicText(payload); + break; + } + case 'public-message-visibility': { + if (!payload) { + throw new Error('Unable to parse payload'); + } + messageService.setPublicVisibility(Boolean(payload)); + break; + } + + case 'lower-message-text': { + if (typeof payload !== 'string') { + throw new Error('Unable to parse payload'); + } + messageService.setLowerText(payload); + break; + } + case 'lower-message-visibility': { + if (!payload) { + throw new Error('Unable to parse payload'); + } + messageService.setLowerVisibility(Boolean(payload)); + break; + } + + case 'start': { + PlaybackService.start(); + break; + } + + case 'startindex': { + const eventIndex = Number(payload); + if (isNaN(eventIndex) || eventIndex <= 0) { + throw new Error(`Event index not recognised or out of range ${eventIndex}`); + } + + try { + // Indexes in frontend are 1 based + PlaybackService.startByIndex(eventIndex - 1); + } catch (error) { + throw new Error(`Error loading event:: ${error}`); + } + break; + } + + case 'startid': { + if (!payload) { + throw new Error(`Event ID not recognised: ${payload}`); + } + PlaybackService.startById(payload); + break; + } + case 'pause': { + PlaybackService.pause(); + break; + } + case 'previous': { + PlaybackService.loadPrevious(); + break; + } + case 'next': { + PlaybackService.loadNext(); + break; + } + case 'unload': + case 'stop': { + PlaybackService.stop(); + break; + } + case 'reload': { + PlaybackService.reload(); + break; + } + case 'roll': { + PlaybackService.roll(); + break; + } + case 'delay': { + const delayTime = Number(payload); + if (isNaN(delayTime)) { + throw new Error(`Delay time not recognised ${payload}`); + } + + try { + PlaybackService.setDelay(delayTime); + } catch (error) { + throw new Error(`Could not add delay: ${error}`); + } + break; + } + case 'gotoindex': + case 'loadindex': { + const eventIndex = Number(payload); + if (isNaN(eventIndex) || eventIndex <= 0) { + throw new Error(`Event index not recognised or out of range ${eventIndex}`); + } + + try { + // Indexes in frontend are 1 based + PlaybackService.loadByIndex(eventIndex - 1); + } catch (error) { + throw new Error(`Event index not recognised or out of range ${error}`); + } + break; + } + case 'gotoid': + case 'loadid': { + if (!payload) { + throw new Error(`Event ID not recognised: ${payload}`); + } + + try { + PlaybackService.loadById(payload.toString().toLowerCase()); + } catch (error) { + throw new Error(`OSC IN: error calling goto ${error}`); + } + break; + } + + case 'get-playback': { + const playback = eventStore.get('playback'); + return { topic: 'playback', payload: playback }; + } + + case 'get-timer': { + const timer = eventStore.get('timer'); + return { topic: 'timer', payload: timer }; + } + + default: { + throw new Error(`Unhandled message ${type}`); + } + } +} diff --git a/packages/types/src/definitions/runtime/Logger.type.ts b/packages/types/src/definitions/runtime/Logger.type.ts index 9e2acf8111..1ff25e82fb 100644 --- a/packages/types/src/definitions/runtime/Logger.type.ts +++ b/packages/types/src/definitions/runtime/Logger.type.ts @@ -12,3 +12,7 @@ export type Log = { text: string; }; +export type LogMessage = { + type: 'ontime-log'; + payload: Log; +}; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index d7ffd4a5ab..a01ff53f56 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -13,7 +13,7 @@ import { OntimeRundown, OntimeRundownEntry } from './definitions/core/Rundown.ty import { OSCSettings, OscSubscription } from './definitions/core/OscSettings.type.js'; import { Playback } from './definitions/runtime/Playback.type.js'; import { Loaded } from './definitions/runtime/Playlist.type.js'; -import { Log, LogLevel } from './definitions/runtime/Logger.type.js'; +import { Log, LogLevel, LogMessage } from './definitions/runtime/Logger.type.js'; import { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js'; import { Settings } from './definitions/core/Settings.type.js'; import { TimerLifeCycle } from './definitions/core/TimerLifecycle.type.js'; @@ -54,7 +54,7 @@ export type { OscSubscription, OSCSettings }; // SERVER RUNTIME export { LogLevel }; -export type { Log }; +export type { Log, LogMessage }; export { Playback }; export { TimerLifeCycle }; From 9565ab7d65e34cdd74ef6d2bce907d8dfccf085e Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Sun, 5 Mar 2023 21:13:33 +0100 Subject: [PATCH 07/20] wip: cleanup --- apps/client/src/App.tsx | 53 ++-- .../src/common/context/LoggingContext.tsx | 21 +- apps/client/src/common/utils/socket.ts | 9 +- apps/client/src/common/utils/wss.ts | 15 + .../src/classes/socket/SocketController.js | 297 ------------------ apps/server/src/controllers/OscController.ts | 159 ---------- apps/server/src/services/PlaybackService.ts | 36 +-- apps/server/src/stores/EventStore.ts | 7 +- 8 files changed, 75 insertions(+), 522 deletions(-) create mode 100644 apps/client/src/common/utils/wss.ts delete mode 100644 apps/server/src/classes/socket/SocketController.js delete mode 100644 apps/server/src/controllers/OscController.ts diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index f8f02b3a7e..1a6a6a3883 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -6,11 +6,11 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import ErrorBoundary from './common/components/error-boundary/ErrorBoundary'; import { AppContextProvider } from './common/context/AppContext'; -import { LoggingProvider } from './common/context/LoggingContext'; import useElectronEvent from './common/hooks/useElectronEvent'; import { ontimeQueryClient } from './common/queryClient'; import theme from './theme/theme'; import AppRouter from './AppRouter'; +// import { useSyncExternalLogger } from './common/stores/logger'; // Load Open Sans typeface // @ts-expect-error no types from font import @@ -18,18 +18,19 @@ import('typeface-open-sans'); function App() { const { isElectron, sendToElectron } = useElectronEvent(); + // useSyncExternalLogger(); - const handleKeyPress = (event:KeyboardEvent) => { - // handle held key - if (event.repeat) return; - // check if the alt key is pressed - if (event.altKey) { - if (event.code === 'KeyT') { - // ask to see debug - sendToElectron('set-window', 'show-dev'); - } + const handleKeyPress = (event: KeyboardEvent) => { + // handle held key + if (event.repeat) return; + // check if the alt key is pressed + if (event.altKey) { + if (event.code === 'KeyT') { + // ask to see debug + sendToElectron('set-window', 'show-dev'); } - }; + } + }; useEffect(() => { if (isElectron) { @@ -44,22 +45,20 @@ function App() { return ( - - - - -
- - - - - - -
-
-
-
-
+ + + +
+ + + + + + +
+
+
+
); } diff --git a/apps/client/src/common/context/LoggingContext.tsx b/apps/client/src/common/context/LoggingContext.tsx index b2d841a114..39be67d2b4 100644 --- a/apps/client/src/common/context/LoggingContext.tsx +++ b/apps/client/src/common/context/LoggingContext.tsx @@ -29,6 +29,7 @@ export const LoggingContext = createContext({ clearLog: notInitialised, }); +// TODO: Logger should probably implement its own store? export const LoggingProvider = ({ children }: LoggingProviderProps) => { const MAX_MESSAGES = 100; const [logData, setLogData] = useState([]); @@ -38,16 +39,16 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => { // todo: useSubscription or feature // handle incoming messages useEffect(() => { - socket.emit('get-logger'); + // socket.send('get-logger'); - socket.on('logger', (data: Log) => { - setLogData((currentLog) => [data, ...currentLog]); - }); - - // Clear listener - return () => { - socket.off('logger'); - }; + // socket.on('logger', (data: Log) => { + // setLogData((currentLog) => [data, ...currentLog]); + // }); + // + // // Clear listener + // return () => { + // socket.off('logger'); + // }; }, []); /** @@ -67,7 +68,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => { text, }; setLogData((currentLog) => [newLogMessage, ...currentLog]); - socket.emit('logger', newLogMessage); + socket.send('logger', newLogMessage); } if (logData.length > MAX_MESSAGES) { setLogData((currentLog) => currentLog.slice(1)); diff --git a/apps/client/src/common/utils/socket.ts b/apps/client/src/common/utils/socket.ts index 632b3ec7be..4fff1ad5d3 100644 --- a/apps/client/src/common/utils/socket.ts +++ b/apps/client/src/common/utils/socket.ts @@ -1,7 +1,4 @@ -import { serverURL } from '../api/apiConstants'; -import { io } from 'socket.io-client'; - -const socket = io(serverURL, { transports: ['websocket'] }); +const socket = new WebSocket('ws://localhost:4001/ws'); const subscriptions = new Set(); export function subscribeOnce(key: string, callback: (data: T) => void, requestString?: string) { @@ -10,8 +7,8 @@ export function subscribeOnce(key: string, callback: (data: T) => void, reque } subscriptions.add(key); - requestString ? socket.emit(requestString) : socket.emit(`get-${key}`); - socket.on(key, callback); + // requestString ? socket.send(requestString) : socket.send(`get-${key}`); + // socket.on(key, callback); } export default socket; diff --git a/apps/client/src/common/utils/wss.ts b/apps/client/src/common/utils/wss.ts new file mode 100644 index 0000000000..64848291d2 --- /dev/null +++ b/apps/client/src/common/utils/wss.ts @@ -0,0 +1,15 @@ +import useWebSocket from 'react-use-websocket'; + +export default function useSocketClient() { + const { sendJsonMessage, lastMessage, lastJsonMessage, readyState } = useWebSocket('_emit:4001', { + share: true, + shouldReconnect: () => true, + onClose: () => console.log('closed socket connection'), + onError: () => console.log('error in socket connection'), + }); + + console.log('debug messages', lastMessage); + console.log('debug JSON messages', lastJsonMessage); + + return { sendJsonMessage, lastJsonMessage, lastMessage, readyState }; +} diff --git a/apps/server/src/classes/socket/SocketController.js b/apps/server/src/classes/socket/SocketController.js deleted file mode 100644 index ce6a14cb8b..0000000000 --- a/apps/server/src/classes/socket/SocketController.js +++ /dev/null @@ -1,297 +0,0 @@ -import { Server } from 'socket.io'; -import { generateId } from 'ontime-utils'; - -import getRandomName from '../../utils/getRandomName.js'; -import { stringFromMillis } from '../../utils/time.js'; -import { messageService } from '../../services/message-service/MessageService.js'; -import { PlaybackService } from '../../services/PlaybackService.js'; - -import { eventTimer } from '../../services/TimerService.js'; -import { clock } from '../../services/Clock.js'; -import { eventStore } from '../../stores/EventStore.js'; -import { isProduction } from '../../setup.js'; - -class SocketController { - constructor() { - this.numClients = 0; - this.messageStack = []; - this._MAX_MESSAGES = 100; - this._clientNames = {}; - this.socket = null; - } - - initServer(httpServer) { - this.socket = new Server(httpServer, { - cors: { - origin: '*', - methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', - preflightContinue: false, - optionsSuccessStatus: 204, - }, - }); - } - - startListener() { - this._socketMessageHandler(); - } - - shutdown() { - this.info('SERVER', 'Shutting down ontime'); - if (this.socket) { - this.info('TX', '... Closing socket server'); - this.socket.close(); - } - } - - /** - * Handle socket io connections - * @private - */ - _socketMessageHandler() { - this.socket.on('connection', (socket) => { - /*******************************/ - /*** HANDLE NEW CONNECTION ***/ - /*** --------------------- ***/ - /*******************************/ - // keep track of connections - this.numClients++; - this._clientNames[socket.id] = getRandomName(); - const message = `${this.numClients} Clients with new connection: ${this._clientNames[socket.id]}`; - this.info('CLIENT', message); - - /** - * @description handle disconnecting a user - */ - socket.on('disconnect', () => { - this.numClients--; - const message = `${this.numClients} Clients with disconnection: ${this._clientNames[socket.id]}`; - delete this._clientNames[socket.id]; - this.info('CLIENT', message); - }); - - /** - * @description utility for renaming a user - */ - socket.on('rename-client', (newName) => { - if (newName) { - const previousName = this._clientNames[socket.id]; - this._clientNames[socket.id] = newName; - this.info('CLIENT', `Client ${previousName} renamed to ${newName}`); - } - }); - - /***************************************/ - /*** TIMER STATE GETTERS / SETTERS ***/ - /*** ------- WEBSOCKET API ------- ***/ - /*** ----------------------------- ***/ - /***************************************/ - - /*******************************************/ - socket.on('ontime-test', () => { - socket.emit('hello', socket.id); - }); - - socket.on('set-start', () => { - PlaybackService.start(); - }); - - socket.on('set-startid', (data) => { - if (data) { - PlaybackService.startById(data); - } - }); - - socket.on('set-startindex', (data) => { - const eventIndex = Number(data); - if (!isNaN(eventIndex)) { - PlaybackService.startByIndex(eventIndex); - } - }); - - socket.on('set-loadid', (data) => { - if (data) { - PlaybackService.loadById(data); - } - }); - - socket.on('set-loadindex', (data) => { - const eventIndex = Number(data); - if (!isNaN(eventIndex)) { - PlaybackService.loadByIndex(eventIndex - 1); - } - }); - - socket.on('set-pause', () => { - PlaybackService.pause(); - }); - - socket.on('set-stop', () => { - PlaybackService.stop(); - }); - - socket.on('set-reload', () => { - PlaybackService.reload(); - }); - - socket.on('set-previous', () => { - PlaybackService.loadPrevious(); - }); - - socket.on('set-next', () => { - PlaybackService.loadNext(); - }); - - socket.on('set-roll', () => { - PlaybackService.roll(); - }); - - socket.on('set-delay', (data) => { - const delayTime = Number(data); - if (!isNaN(delayTime)) { - PlaybackService.setDelay(delayTime); - } - }); - - /*******************************************/ - // general playback state, useful for external sync - // Todo: add delayed value (will come from rundownService) - socket.on('ontime-poll', () => { - const timerPoll = eventTimer.timer; - const isDelayed = false; - const colour = ''; - socket.emit('ontime-poll', { isDelayed, colour, ...timerPoll }); - }); - - // On Air - socket.on('set-onAir', (data) => { - if (typeof data === 'boolean') { - try { - const featureData = messageService.setOnAir(data); - this.info('PLAYBACK', featureData.onAir ? 'Going On Air' : 'Going Off Air'); - } catch (error) { - this.error('RX', `Failed to parse message ${data} : ${error}`); - } - } - }); - - // Presenter message - socket.on('set-timer-message-text', (data) => { - if (typeof data !== 'string') { - return; - } - messageService.setTimerText(data); - }); - - socket.on('set-timer-message-visible', (data) => { - if (typeof data !== 'boolean') { - return; - } - messageService.setTimerVisibility(data); - }); - - /*******************************************/ - // Public message - socket.on('set-public-message-text', (data) => { - if (typeof data !== 'string') { - return; - } - messageService.setPublicText(data); - }); - - socket.on('set-public-message-visible', (data) => { - if (typeof data !== 'boolean') { - return; - } - messageService.setPublicVisibility(data); - }); - - /*******************************************/ - // Lower third message - socket.on('set-lower-message-text', (data) => { - if (typeof data !== 'string') { - return; - } - messageService.setLowerText(data); - }); - - socket.on('set-lower-message-visible', (data) => { - if (typeof data !== 'boolean') { - return; - } - messageService.setLowerVisibility(data); - }); - - socket.on('get-timer', () => { - const timer = eventStore.get('timer'); - socket.emit('timer', timer); - }); - }); - } - - send(topic, payload) { - this.socket?.emit(topic, payload); - } - - /****************************************************************************/ - - /** - * Logger logic - * ------------- - */ - - /** - * Utility method, sends message and pushes into stack - * @param {string} level - * @param {string} origin - * @param {string} text - */ - _push(level, origin, text) { - const logMessage = { - id: generateId(), - level, - origin, - text, - time: stringFromMillis(clock.getSystemTime() || 0), - }; - - this.messageStack.unshift(logMessage); - this.socket?.emit('logger', logMessage); - - if (!isProduction) { - console.log(`[${logMessage.level}] \t ${logMessage.origin} \t ${logMessage.text}`); - } - - if (this.messageStack.length > this._MAX_MESSAGES) { - this.messageStack.pop(); - } - } - - /** - * Sends a message with level LOG - * @param {string} origin - * @param {string} text - */ - info(origin, text) { - this._push('INFO', origin, text); - } - - /** - * Sends a message with level WARN - * @param {string} origin - * @param {string} text - */ - warning(origin, text) { - this._push('WARN', origin, text); - } - - /** - * Sends a message with level ERROR - * @param {string} origin - * @param {string} text - */ - error(origin, text) { - this._push('ERROR', origin, text); - } -} - -export const socketProvider = new SocketController(); diff --git a/apps/server/src/controllers/OscController.ts b/apps/server/src/controllers/OscController.ts deleted file mode 100644 index 120a3b3754..0000000000 --- a/apps/server/src/controllers/OscController.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { Server } from 'node-osc'; -import { OSCSettings } from 'ontime-types'; - -import { PlaybackService } from '../services/PlaybackService.js'; -import { messageService } from '../services/message-service/MessageService.js'; -import { socketProvider } from '../classes/socket/SocketController.js'; - -let oscServer = null; - -/** - * @description utility function to shut down osc server - */ -export const shutdownOSCServer = () => { - if (oscServer != null) oscServer.close(); -}; - -/** - * Initialises OSC server - */ -export const initiateOSC = (config: OSCSettings) => { - oscServer = new Server(config.portIn, '0.0.0.0'); - - oscServer.on('error', console.error); - - oscServer.on('message', function (msg) { - // message should look like /ontime/{path} {args} where - // ontime: fixed message for app - // path: command to be called - // args: extra data, only used on some API entries (delay, goto) - - // split message - const [, address, path] = msg[0].split('/'); - const args = msg[1]; - - // get first part before (ontime) - if (address !== 'ontime') { - console.error('RX', `OSC IN: Message address ${address} not recognised`, msg); - return; - } - - // get second part (command) - if (!path) { - console.error('RX', 'OSC IN: No path found'); - return; - } - - switch (path.toLowerCase()) { - case 'onair': { - messageService.setOnAir(true); - break; - } - case 'offair': { - messageService.setOnAir(false); - break; - } - case 'play': { - PlaybackService.start(); - break; - } - case 'start': { - try { - const eventIndex = Number(args); - if (isNaN(eventIndex)) { - socketProvider.error('RX', `OSC IN: event index not recognised ${args}`); - return; - } - PlaybackService.startByIndex(eventIndex); - } catch (error) { - console.log('Error loading event: ', error); - } - break; - } - case 'startid': { - if (!args) { - socketProvider.error('RX', `OSC IN: No ID in request`); - return; - } - PlaybackService.loadById(args); - break; - } - case 'pause': { - PlaybackService.pause(); - break; - } - case 'prev': { - PlaybackService.loadPrevious(); - break; - } - case 'next': { - PlaybackService.loadNext(); - break; - } - case 'unload': - case 'stop': { - PlaybackService.stop(); - break; - } - case 'reload': { - PlaybackService.reload(); - break; - } - case 'roll': { - PlaybackService.roll(); - break; - } - case 'delay': { - try { - const delayTime = Number(args); - if (isNaN(delayTime)) { - socketProvider.error('RX', `OSC IN: delay time not recognised ${args}`); - return; - } - PlaybackService.setDelay(delayTime); - } catch (error) { - console.log('Error adding delay: ', error); - } - break; - } - case 'goto': - case 'load': { - try { - const eventIndex = Number(args); - if (isNaN(eventIndex) || eventIndex <= 0) { - socketProvider.error('RX', `OSC IN: event index not recognised or out of range ${eventIndex}`); - } else { - PlaybackService.loadByIndex(eventIndex - 1); - } - } catch (error) { - socketProvider.error('RX', `OSC IN: error calling goto ${error}`); - } - break; - } - case 'gotoid': - case 'loadid': { - if (!args) { - socketProvider.error('RX', `OSC IN: event ID not recognised: ${args}}`); - return; - } - try { - PlaybackService.loadById(args.toString().toLowerCase()); - } catch (error) { - socketProvider.error('RX', `OSC IN: error calling goto ${error}`); - } - break; - } - - case 'get-playback': { - const playback = global.timer.state; - global.timer.sendOsc('playback', playback); - break; - } - - default: { - socketProvider.warning('RX', `OSC IN: unhandled message ${path}`); - break; - } - } - }); -}; diff --git a/apps/server/src/services/PlaybackService.ts b/apps/server/src/services/PlaybackService.ts index ebddebf50c..0e1705d1b2 100644 --- a/apps/server/src/services/PlaybackService.ts +++ b/apps/server/src/services/PlaybackService.ts @@ -1,12 +1,10 @@ -/** - * starts loaded timer - */ import { Playback } from 'ontime-types'; -import { socketProvider } from '../classes/socket/SocketController.js'; + import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js'; import { eventStore } from '../stores/EventStore.js'; import { eventTimer } from './TimerService.js'; import { clock } from './Clock.js'; +import { logger } from '../classes/Logger.js'; /** * Service manages playback status of app @@ -21,9 +19,9 @@ export class PlaybackService { static loadEvent(event) { let success = false; if (!event) { - socketProvider.error('PLAYBACK', 'No event found'); + logger.error('PLAYBACK', 'No event found'); } else if (event.skip) { - socketProvider.warning('PLAYBACK', `Refused playback of skipped event ID ${event.id}`); + logger.warning('PLAYBACK', `Refused playback of skipped event ID ${event.id}`); } else { eventLoader.loadEvent(event); eventTimer.load(event); @@ -42,7 +40,7 @@ export class PlaybackService { const event = EventLoader.getEventWithId(eventId); const success = PlaybackService.loadEvent(event); if (success) { - socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`); + logger.info('PLAYBACK', `Loaded event with ID ${event.id}`); PlaybackService.start(); } return success; @@ -57,7 +55,7 @@ export class PlaybackService { const event = EventLoader.getEventAtIndex(eventIndex); const success = PlaybackService.loadEvent(event); if (success) { - socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`); + logger.info('PLAYBACK', `Loaded event with ID ${event.id}`); PlaybackService.start(); } return success; @@ -72,7 +70,7 @@ export class PlaybackService { const event = EventLoader.getEventWithId(eventId); const success = PlaybackService.loadEvent(event); if (success) { - socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`); + logger.info('PLAYBACK', `Loaded event with ID ${event.id}`); } return success; } @@ -86,7 +84,7 @@ export class PlaybackService { const event = EventLoader.getEventAtIndex(eventIndex); const success = PlaybackService.loadEvent(event); if (success) { - socketProvider.info('PLAYBACK', `Loaded event with ID ${event.id}`); + logger.info('PLAYBACK', `Loaded event with ID ${event.id}`); } return success; } @@ -99,7 +97,7 @@ export class PlaybackService { if (previousEvent) { const success = PlaybackService.loadEvent(previousEvent); if (success) { - socketProvider.info('PLAYBACK', `Loaded event with ID ${previousEvent.id}`); + logger.info('PLAYBACK', `Loaded event with ID ${previousEvent.id}`); } } } @@ -112,7 +110,7 @@ export class PlaybackService { if (nextEvent) { const success = PlaybackService.loadEvent(nextEvent); if (success) { - socketProvider.info('PLAYBACK', `Loaded event with ID ${nextEvent.id}`); + logger.info('PLAYBACK', `Loaded event with ID ${nextEvent.id}`); } } } @@ -124,7 +122,7 @@ export class PlaybackService { if (eventTimer.loadedTimerId) { eventTimer.start(); const newState = eventTimer.playback; - socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`); + logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`); } } @@ -135,7 +133,7 @@ export class PlaybackService { if (eventTimer.loadedTimerId) { eventTimer.pause(); const newState = eventTimer.playback; - socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`); + logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`); } } @@ -147,7 +145,7 @@ export class PlaybackService { eventLoader.reset(); eventTimer.stop(); const newState = eventTimer.playback; - socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`); + logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`); } } @@ -169,14 +167,14 @@ export class PlaybackService { // nothing to play if (rollTimers === null) { - socketProvider.error('SERVER', 'Roll: no events found'); + logger.error('SERVER', 'Roll: no events found'); PlaybackService.stop(); return; } const { currentEvent, nextEvent, timers } = rollTimers; if (!currentEvent && !nextEvent) { - socketProvider.error('SERVER', 'Roll: no events found'); + logger.error('SERVER', 'Roll: no events found'); PlaybackService.stop(); return; } @@ -184,7 +182,7 @@ export class PlaybackService { eventTimer.roll(currentEvent, nextEvent, timers); const newState = eventTimer.playback; - socketProvider.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`); + logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`); } } @@ -196,7 +194,7 @@ export class PlaybackService { if (eventTimer.loadedTimerId) { const delayInMs = delayTime * 1000 * 60; eventTimer.delay(delayInMs); - socketProvider.info('PLAYBACK', `Added ${delayTime} min delay`); + logger.info('PLAYBACK', `Added ${delayTime} min delay`); } } } diff --git a/apps/server/src/stores/EventStore.ts b/apps/server/src/stores/EventStore.ts index df644adba5..71b01204b6 100644 --- a/apps/server/src/stores/EventStore.ts +++ b/apps/server/src/stores/EventStore.ts @@ -1,24 +1,23 @@ import { RuntimeStore } from 'ontime-types'; -import { socketProvider } from '../classes/socket/SocketController.js'; - const store: Partial = {}; /** * A runtime store that broadcasts its payload */ +// TODO: misses callback to send stuff export const eventStore = { get(key: T) { return store[key]; }, set(key: T, value: RuntimeStore[T]) { store[key] = value; - socketProvider.send(key, value); + // socketProvider.send(key, value); }, poll() { return store; }, broadcast() { - socketProvider.send(store); + // socketProvider.send(store); }, }; From 11648ee5466eeba756bac04d08e380b62ac90a5e Mon Sep 17 00:00:00 2001 From: Fabian Posenau Date: Mon, 6 Mar 2023 19:47:09 +0100 Subject: [PATCH 08/20] improve time input (#307) * add the ability to parse time like 2h20m --------- Co-authored-by: Fabian Posenau --- .../common/utils/__tests__/dateConfig.test.js | 9 ++ .../utils/{dateConfig.js => dateConfig.ts} | 91 ++++++++++--------- 2 files changed, 59 insertions(+), 41 deletions(-) rename apps/client/src/common/utils/{dateConfig.js => dateConfig.ts} (58%) diff --git a/apps/client/src/common/utils/__tests__/dateConfig.test.js b/apps/client/src/common/utils/__tests__/dateConfig.test.js index 9cf803cd39..61b669bb24 100644 --- a/apps/client/src/common/utils/__tests__/dateConfig.test.js +++ b/apps/client/src/common/utils/__tests__/dateConfig.test.js @@ -299,6 +299,15 @@ describe('test forgivingStringToMillis()', () => { { value: '010000', expect: 1000 * 60 * 60 }, { value: '230000', expect: 1000 * 60 * 60 * 23 }, { value: '121212', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 }, + { value: '0h0m0s', expect: 0 }, + { value: '0h0m1s', expect: 1000 }, + { value: '0h1m0s', expect: 1000 * 60 }, + { value: '1h0m0s', expect: 1000 * 60 * 60 }, + { value: '23h0m0s', expect: 1000 * 60 * 60 * 23 }, + { value: '12h12m12s', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 }, + { value: '2m', expect: 2 * 60 * 1000 }, + { value: '1h5s', expect: 1000 * 60 * 60 + 1000 * 5 }, + { value: '1h2m', expect: 1000 * 60 * 60 + 1000 * 60 * 2 }, ]; for (const s of testData) { diff --git a/apps/client/src/common/utils/dateConfig.js b/apps/client/src/common/utils/dateConfig.ts similarity index 58% rename from apps/client/src/common/utils/dateConfig.js rename to apps/client/src/common/utils/dateConfig.ts index 47a7c49166..41f24e21af 100644 --- a/apps/client/src/common/utils/dateConfig.js +++ b/apps/client/src/common/utils/dateConfig.ts @@ -10,13 +10,13 @@ export const timeFormatSeconds = 'HH:mm:ss'; * @param {boolean} [hideZero] - whether to show hours in case its 00 * @returns {string} String representing absolute time 00:12:02 */ -export function formatDisplay(seconds, hideZero = false) { +export function formatDisplay(seconds: number | null, hideZero = false): string { if (typeof seconds !== 'number') { return hideZero ? '00:00' : '00:00:00'; } // add an extra 0 if necessary - const format = (val) => `0${Math.floor(val)}`.slice(-2); + const format = (val: number) => `0${Math.floor(val)}`.slice(-2); const s = Math.abs(seconds); const hours = Math.floor((s / 3600) % 24); @@ -31,7 +31,7 @@ export function formatDisplay(seconds, hideZero = false) { * @param {number | null} millis - time in seconds * @returns {number} Amount in seconds */ -export const millisToSeconds = (millis) => { +export const millisToSeconds = (millis: number | null): number => { if (millis === null) { return 0; } @@ -43,7 +43,7 @@ export const millisToSeconds = (millis) => { * @param {number} millis - time in seconds * @returns {number} Amount in seconds */ -export const millisToMinutes = (millis) => { +export const millisToMinutes = (millis: number): number => { return millis < 0 ? Math.ceil(millis / mtm) : Math.floor(millis / mtm); }; @@ -52,7 +52,7 @@ export const millisToMinutes = (millis) => { * @param {string} string - time string "23:00:12" * @returns {number} Amount in milliseconds */ -export const timeStringToMillis = (string) => { +export const timeStringToMillis = (string: string): number => { if (typeof string !== 'string') return 0; const time = string.split(':'); if (time.length === 1) return Math.abs(time[0] * mts); @@ -66,7 +66,7 @@ export const timeStringToMillis = (string) => { * @param {string} string - time string "23:00:12" * @returns {boolean} string represents time */ -export const isTimeString = (string) => { +export const isTimeString = (string: string): boolean => { // ^ # Start of string // (?: # Try to match... // (?: # Try to match... @@ -83,10 +83,10 @@ export const isTimeString = (string) => { /** * @description safe parse string to int - * @param valueAsString + * @param {string} valueAsString * @return {number} */ -const parse = (valueAsString) => { +const parse = (valueAsString: string): number => { const parsed = parseInt(valueAsString, 10); if (isNaN(parsed)) { return 0; @@ -100,44 +100,53 @@ const parse = (valueAsString) => { * @param {boolean} fillLeft - autofill left = hours / right = seconds * @returns {number} - time string in millis */ -export const forgivingStringToMillis = (value, fillLeft = true) => { +export const forgivingStringToMillis = (value: string, fillLeft = true): number => { let millis = 0; - // split string at known separators : , . - const separatorRegex = /[\s,:.]+/; - const [first, second, third] = value.split(separatorRegex); - - if (first != null && second != null && third != null) { - // if string has three sections, treat as [hours] [minutes] [seconds] - millis = parse(first) * mth; - millis += parse(second) * mtm; - millis += parse(third) * mts; - } else if (first != null && second == null && third == null) { - // if string has one section, - // could be a complete string like 121010 - 12:10:10 - if (first.length === 6) { - const hours = first.substring(0, 2); - const minutes = first.substring(2, 4); - const seconds = first.substring(4); - millis = parse(hours) * mth; - millis += parse(minutes) * mtm; - millis += parse(seconds) * mts; - } else { - // otherwise lets treat as [minutes] - millis = parse(first) * mtm; - } - } - if (first != null && second != null && third == null) { - // if string has two sections - if (fillLeft) { - // treat as [hours] [minutes] + const hours = parseInt(value.match(/(\d+)h/)?.[0] ?? 0, 10); + const minutes = parseInt(value.match(/(\d+)m/)?.[0] ?? 0, 10); + const seconds = parseInt(value.match(/(\d+)s/)?.[0] ?? 0, 10); + + if (hours > 0 || minutes > 0 || seconds > 0) { + millis = hours * mth + minutes * mtm + seconds * mts; + } else { + // split string at known separators : , . + const separatorRegex = /[\s,:.]+/; + const [first, second, third] = value.split(separatorRegex); + + if (first != null && second != null && third != null) { + // if string has three sections, treat as [hours] [minutes] [seconds] millis = parse(first) * mth; millis += parse(second) * mtm; - } else { - // treat as [minutes] [seconds] - millis = parse(first) * mtm; - millis += parse(second) * mts; + millis += parse(third) * mts; + } else if (first != null && second == null && third == null) { + // if string has one section, + // could be a complete string like 121010 - 12:10:10 + if (first.length === 6) { + const hours = first.substring(0, 2); + const minutes = first.substring(2, 4); + const seconds = first.substring(4); + millis = parse(hours) * mth; + millis += parse(minutes) * mtm; + millis += parse(seconds) * mts; + } else { + // otherwise lets treat as [minutes] + millis = parse(first) * mtm; + } + } + if (first != null && second != null && third == null) { + // if string has two sections + if (fillLeft) { + // treat as [hours] [minutes] + millis = parse(first) * mth; + millis += parse(second) * mtm; + } else { + // treat as [minutes] [seconds] + millis = parse(first) * mtm; + millis += parse(second) * mts; + } } } + return millis; }; From e60fa1e6d657a6daafcd9ec07595835ca530041b Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Tue, 7 Mar 2023 18:15:46 +0100 Subject: [PATCH 09/20] refactor: re-write logger implementation --- .../common/components/buttons/QuitIconBtn.tsx | 6 +- .../error-boundary/ErrorBoundary.jsx | 3 - .../components/input/time-input/TimeInput.tsx | 6 +- .../components/upload-modal/UploadModal.tsx | 6 +- .../src/common/context/LoggingContext.tsx | 125 ------------------ .../client/src/common/hooks/useEventAction.ts | 6 +- apps/client/src/common/stores/logger.ts | 20 +-- .../src/features/event-editor/EventEditor.tsx | 6 +- .../src/features/modals/AliasesModal.jsx | 110 +++++++-------- .../src/features/modals/AppSettingsModal.jsx | 7 +- .../features/modals/EventSettingsModal.jsx | 7 +- .../src/features/modals/TableOptionsModal.jsx | 7 +- .../features/modals/ViewsSettingsModal.jsx | 7 +- .../OscIntegrationSettings.tsx | 5 +- .../integration-modal/OscSettingsModal.jsx | 22 ++- .../src/features/rundown/RundownEntry.tsx | 4 +- .../composite/EventBlockTimers.jsx | 7 +- .../rundown/quick-add-block/QuickAddBlock.tsx | 6 +- 18 files changed, 105 insertions(+), 255 deletions(-) delete mode 100644 apps/client/src/common/context/LoggingContext.tsx diff --git a/apps/client/src/common/components/buttons/QuitIconBtn.tsx b/apps/client/src/common/components/buttons/QuitIconBtn.tsx index 78281928ae..2a398afa43 100644 --- a/apps/client/src/common/components/buttons/QuitIconBtn.tsx +++ b/apps/client/src/common/components/buttons/QuitIconBtn.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { AlertDialog, AlertDialogBody, @@ -12,8 +12,8 @@ import { } from '@chakra-ui/react'; import { FiPower } from '@react-icons/all-files/fi/FiPower'; -import { LoggingContext } from '../../context/LoggingContext'; import { Size } from '../../models/Util.type'; +import { useEmitLog } from '../../stores/logger'; interface QuitIconBtnProps { clickHandler: () => void; @@ -39,7 +39,7 @@ const quitBtnStyle = { export default function QuitIconBtn(props: QuitIconBtnProps) { const { clickHandler, size = 'lg', ...rest } = props; const [isOpen, setIsOpen] = useState(false); - const { emitInfo } = useContext(LoggingContext); + const { emitInfo } = useEmitLog(); const onClose = () => setIsOpen(false); const cancelRef = useRef(null); diff --git a/apps/client/src/common/components/error-boundary/ErrorBoundary.jsx b/apps/client/src/common/components/error-boundary/ErrorBoundary.jsx index 9240ea7a69..dac1aa50ab 100644 --- a/apps/client/src/common/components/error-boundary/ErrorBoundary.jsx +++ b/apps/client/src/common/components/error-boundary/ErrorBoundary.jsx @@ -2,12 +2,9 @@ import React from 'react'; import * as Sentry from '@sentry/react'; -import { LoggingContext } from '../../context/LoggingContext'; - import style from './ErrorBoundary.module.scss'; class ErrorBoundary extends React.Component { - static contextType = LoggingContext; reportContent = ''; constructor(props) { diff --git a/apps/client/src/common/components/input/time-input/TimeInput.tsx b/apps/client/src/common/components/input/time-input/TimeInput.tsx index fc13e3871a..736f68e844 100644 --- a/apps/client/src/common/components/input/time-input/TimeInput.tsx +++ b/apps/client/src/common/components/input/time-input/TimeInput.tsx @@ -1,9 +1,9 @@ -import { KeyboardEvent, useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react'; import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react'; import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor'; import { tooltipDelayFast } from '../../../../ontimeConfig'; -import { LoggingContext } from '../../../context/LoggingContext'; +import { useEmitLog } from '../../../stores/logger'; import { forgivingStringToMillis } from '../../../utils/dateConfig'; import { stringFromMillis } from '../../../utils/time'; import { TimeEntryField } from '../../../utils/timesManager'; @@ -24,7 +24,7 @@ export default function TimeInput(props: TimeInputProps) { const { name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0, } = props; - const { emitError } = useContext(LoggingContext); + const { emitError } = useEmitLog(); const inputRef = useRef(null); const [value, setValue] = useState(''); diff --git a/apps/client/src/common/components/upload-modal/UploadModal.tsx b/apps/client/src/common/components/upload-modal/UploadModal.tsx index 1df81a5aac..66a1109a20 100644 --- a/apps/client/src/common/components/upload-modal/UploadModal.tsx +++ b/apps/client/src/common/components/upload-modal/UploadModal.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useCallback, useContext, useRef, useState } from 'react'; +import { ChangeEvent, useCallback, useRef, useState } from 'react'; import { Button, Checkbox, @@ -21,7 +21,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { RUNDOWN_TABLE } from '../../api/apiConstants'; import { uploadData } from '../../api/ontimeApi'; -import { LoggingContext } from '../../context/LoggingContext'; +import { useEmitLog } from '../../stores/logger'; import TooltipActionBtn from '../buttons/TooltipActionBtn'; import { validateFile } from './utils'; @@ -35,7 +35,7 @@ interface UploadModalProps { export default function UploadModal({ onClose, isOpen }: UploadModalProps) { const queryClient = useQueryClient(); - const { emitError } = useContext(LoggingContext); + const { emitError } = useEmitLog(); const [errors, setErrors] = useState([]); const [file, setFile] = useState(null); const [progress, setProgress] = useState(0); diff --git a/apps/client/src/common/context/LoggingContext.tsx b/apps/client/src/common/context/LoggingContext.tsx deleted file mode 100644 index 39be67d2b4..0000000000 --- a/apps/client/src/common/context/LoggingContext.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { createContext, ReactNode, useCallback, useEffect, useState } from 'react'; -import { Log, LogLevel } from 'ontime-types'; -import { generateId } from 'ontime-utils'; - -import socket from '../utils/socket'; -import { nowInMillis, stringFromMillis } from '../utils/time'; - -interface LoggingProviderState { - logData: Log[]; - emitInfo: (text: string) => void; - emitWarning: (text: string) => void; - emitError: (text: string) => void; - clearLog: () => void; -} - -type LoggingProviderProps = { - children: ReactNode; -}; - -const notInitialised = () => { - throw new Error('Not initialised'); -}; - -export const LoggingContext = createContext({ - logData: [], - emitInfo: notInitialised, - emitWarning: notInitialised, - emitError: notInitialised, - clearLog: notInitialised, -}); - -// TODO: Logger should probably implement its own store? -export const LoggingProvider = ({ children }: LoggingProviderProps) => { - const MAX_MESSAGES = 100; - const [logData, setLogData] = useState([]); - const origin = 'USER'; - - // todo: use react-query store - // todo: useSubscription or feature - // handle incoming messages - useEffect(() => { - // socket.send('get-logger'); - - // socket.on('logger', (data: Log) => { - // setLogData((currentLog) => [data, ...currentLog]); - // }); - // - // // Clear listener - // return () => { - // socket.off('logger'); - // }; - }, []); - - /** - * Utility function sends message over socket - * @param text - * @param level - * @private - */ - const _send = useCallback( - (text: string, level: LogLevel) => { - if (socket != null) { - const newLogMessage: Log = { - id: generateId(), - origin, - time: stringFromMillis(nowInMillis()), - level, - text, - }; - setLogData((currentLog) => [newLogMessage, ...currentLog]); - socket.send('logger', newLogMessage); - } - if (logData.length > MAX_MESSAGES) { - setLogData((currentLog) => currentLog.slice(1)); - } - }, - [logData.length, setLogData], - ); - - /** - * Sends a message with level INFO - * @param text - */ - const emitInfo = useCallback( - (text: string) => { - _send(text, LogLevel.Info); - }, - [_send], - ); - - /** - * Sends a message with level WARN - * @param text - */ - const emitWarning = useCallback( - (text: string) => { - _send(text, LogLevel.Warn); - }, - [_send], - ); - - /** - * Sends a message with level ERROR - * @param text - */ - const emitError = useCallback( - (text: string) => { - _send(text, LogLevel.Error); - }, - [_send], - ); - - /** - * Clears running log - */ - const clearLog = useCallback(() => { - setLogData([]); - }, [setLogData]); - - return ( - - {children} - - ); -}; diff --git a/apps/client/src/common/hooks/useEventAction.ts b/apps/client/src/common/hooks/useEventAction.ts index f89dcbfb66..0a9bd6ef8f 100644 --- a/apps/client/src/common/hooks/useEventAction.ts +++ b/apps/client/src/common/hooks/useEventAction.ts @@ -1,4 +1,4 @@ -import { useCallback, useContext } from 'react'; +import { useCallback } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import axios, { AxiosError } from 'axios'; import { useAtomValue } from 'jotai'; @@ -15,14 +15,14 @@ import { requestReorderEvent, } from '../api/eventsApi'; import { defaultPublicAtom, startTimeIsLastEndAtom } from '../atoms/LocalEventSettings'; -import { LoggingContext } from '../context/LoggingContext'; +import { useEmitLog } from '../stores/logger'; /** * @description Set of utilities for events */ export const useEventAction = () => { const queryClient = useQueryClient(); - const { emitError } = useContext(LoggingContext); + const { emitError } = useEmitLog(); const defaultPublic = useAtomValue(defaultPublicAtom); const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom); diff --git a/apps/client/src/common/stores/logger.ts b/apps/client/src/common/stores/logger.ts index d519b5c533..20adddbc45 100644 --- a/apps/client/src/common/stores/logger.ts +++ b/apps/client/src/common/stores/logger.ts @@ -7,10 +7,11 @@ import { nowInMillis, stringFromMillis } from '../utils/time'; import createStore from './createStore'; -const logger = createStore([]); -const MAX_MESSAGES = 100; +export const logger = createStore([]); +export const LOGGER_MAX_MESSAGES = 100; export function useEmitLog() { + // should I just make my own ? const { sendJsonMessage } = useWebSocket('ws://localhost:4001/ws', { share: true, shouldReconnect: () => true, @@ -20,7 +21,7 @@ export function useEmitLog() { const state = logger.get(); console.log('DEBUG', state); state.push(log); - if (state.length > MAX_MESSAGES) { + if (state.length > LOGGER_MAX_MESSAGES) { state.slice(1); } logger.set(state); @@ -94,19 +95,6 @@ export function useEmitLog() { }; } -// export function useSyncExternalLogger() { -// const { lastMessage, lastJsonMessage } = useWebSocket('ws://localhost:4001/ws', { -// share: true, -// shouldReconnect: () => true, -// onClose: () => console.log('closed socket connection'), -// onError: () => console.log('error in socket connection'), -// }); -// -// useEffect(() => { -// console.log('DEBUG LOGGER SYNC', lastMessage, lastJsonMessage); -// }, [lastMessage, lastJsonMessage]); -// } -// export const useLogData = () => { return useSyncExternalStore(logger.subscribe, () => logger.get()); }; diff --git a/apps/client/src/features/event-editor/EventEditor.tsx b/apps/client/src/features/event-editor/EventEditor.tsx index 5ffc5d8c2d..3068d2e512 100644 --- a/apps/client/src/features/event-editor/EventEditor.tsx +++ b/apps/client/src/features/event-editor/EventEditor.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Button, Select, Switch } from '@chakra-ui/react'; import { IoBan } from '@react-icons/all-files/io5/IoBan'; import { useAtom } from 'jotai'; @@ -9,9 +9,9 @@ import CopyTag from '../../common/components/copy-tag/CopyTag'; import ColourInput from '../../common/components/input/colour-input/ColourInput'; import TextInput from '../../common/components/input/text-input/TextInput'; import TimeInput from '../../common/components/input/time-input/TimeInput'; -import { LoggingContext } from '../../common/context/LoggingContext'; import { useEventAction } from '../../common/hooks/useEventAction'; import useRundown from '../../common/hooks-query/useRundown'; +import { useEmitLog } from '../../common/stores/logger'; import { millisToMinutes } from '../../common/utils/dateConfig'; import getDelayTo from '../../common/utils/getDelayTo'; import { stringFromMillis } from '../../common/utils/time'; @@ -25,7 +25,7 @@ export type EventEditorSubmitActions = keyof OntimeEvent | 'durationOverride'; export default function EventEditor() { const [openId] = useAtom(editorEventId); const { data } = useRundown(); - const { emitWarning, emitError } = useContext(LoggingContext); + const { emitWarning, emitError } = useEmitLog(); const { updateEvent } = useEventAction(); const [event, setEvent] = useState(null); const [delay, setDelay] = useState(0); diff --git a/apps/client/src/features/modals/AliasesModal.jsx b/apps/client/src/features/modals/AliasesModal.jsx index c7957750e2..92222d9f14 100644 --- a/apps/client/src/features/modals/AliasesModal.jsx +++ b/apps/client/src/features/modals/AliasesModal.jsx @@ -1,13 +1,14 @@ /* eslint-disable jsx-a11y/anchor-has-content */ -import { useCallback, useContext, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Button, IconButton, Input, ModalBody, Tooltip } from '@chakra-ui/react'; import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline'; import { IoRemove } from '@react-icons/all-files/io5/IoRemove'; import { IoSunny } from '@react-icons/all-files/io5/IoSunny'; +import { useEmitLog } from '@/common/stores/logger'; + import { viewerLocations } from '../../appConstants'; import { postAliases } from '../../common/api/ontimeApi'; -import { LoggingContext } from '../../common/context/LoggingContext'; import useAliases from '../../common/hooks-query/useAliases'; import { validateAlias } from '../../common/utils/aliases'; import { handleLinks, host } from '../../common/utils/linkUtils'; @@ -19,7 +20,7 @@ import style from './Modals.module.scss'; export default function AliasesModal() { const { data, status, refetch } = useAliases(); - const { emitError } = useContext(LoggingContext); + const { emitError } = useEmitLog(); const [changed, setChanged] = useState(false); const [submitting, setSubmitting] = useState(false); const [aliases, setAliases] = useState([]); @@ -111,29 +112,32 @@ export default function AliasesModal() { * @param {string} id - object id * @param {boolean} isEnabled - whether to enable / disable flag */ - const setEnabled = useCallback((id, isEnabled) => { - const aliasesState = [...aliases]; - for (const a of aliasesState) { - if (a.id === id) { - if (isEnabled) { - if (a.alias === '' || a.pathAndParams === '') { - emitError('Alias incomplete'); - break; - } + const setEnabled = useCallback( + (id, isEnabled) => { + const aliasesState = [...aliases]; + for (const a of aliasesState) { + if (a.id === id) { + if (isEnabled) { + if (a.alias === '' || a.pathAndParams === '') { + emitError('Alias incomplete'); + break; + } - const isRepeated = aliases.some((r) => a.alias === r.alias && r.enabled); - if (isRepeated) { - emitError('There is already an alias with this name'); - break; + const isRepeated = aliases.some((r) => a.alias === r.alias && r.enabled); + if (isRepeated) { + emitError('There is already an alias with this name'); + break; + } } + a.enabled = isEnabled; + break; } - a.enabled = isEnabled; - break; } - } - setChanged(true); - setAliases(aliasesState); - }, [aliases, emitError]); + setChanged(true); + setAliases(aliasesState); + }, + [aliases, emitError], + ); /** * Reverts local state equals to server state @@ -194,16 +198,16 @@ export default function AliasesModal() { eg. a lower third url with some custom parameters - - - - - - - - + + + + + + + +
- Alias - Page URL
mylowerlower?bg=ff2&text=f00&size=0.6&transition=5
+ Alias + Page URL
mylowerlower?bg=ff2&text=f00&size=0.6&transition=5

@@ -212,16 +216,16 @@ export default function AliasesModal() { eg. an unattended screen that you would need to change route from the app - - - - - - - - + + + + + + + +
- Alias - Page URL
thirdfloorpublic
+ Alias + Page URL
thirdfloorpublic
@@ -254,12 +258,7 @@ export default function AliasesModal() { onChange={(event) => handleChange(index, 'pathAndParams', event.target.value)} /> - handleLinks(e, alias.pathAndParams)} - /> + handleLinks(e, alias.pathAndParams)} /> - {alias.aliasError ? ( -
{`Alias error: ${alias.aliasError}`}
- ) : null} - {alias.urlError ? ( -
{`URL error: ${alias.urlError}`}
- ) : null} + {alias.aliasError ?
{`Alias error: ${alias.aliasError}`}
: null} + {alias.urlError ?
{`URL error: ${alias.urlError}`}
: null} ))} @@ -296,12 +291,7 @@ export default function AliasesModal() { - + ); diff --git a/apps/client/src/features/modals/AppSettingsModal.jsx b/apps/client/src/features/modals/AppSettingsModal.jsx index 8ecc394347..e54a8f8f47 100644 --- a/apps/client/src/features/modals/AppSettingsModal.jsx +++ b/apps/client/src/features/modals/AppSettingsModal.jsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import isEqual from 'react-fast-compare'; import { Checkbox, @@ -15,11 +15,12 @@ import { FiEye } from '@react-icons/all-files/fi/FiEye'; import { FiX } from '@react-icons/all-files/fi/FiX'; import { useAtom } from 'jotai'; +import { useEmitLog } from '@/common/stores/logger'; + import { version } from '../../../package.json'; import { postSettings } from '../../common/api/ontimeApi'; import { eventSettingsAtom } from '../../common/atoms/LocalEventSettings'; import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn'; -import { LoggingContext } from '../../common/context/LoggingContext'; import useSettings from '../../common/hooks-query/useSettings'; import { ontimePlaceholderSettings } from '../../common/models/OntimeSettings'; @@ -30,7 +31,7 @@ import style from './Modals.module.scss'; export default function AppSettingsModal() { const { data, status, refetch } = useSettings(); - const { emitError, emitWarning } = useContext(LoggingContext); + const { emitError, emitWarning } = useEmitLog(); const [formData, setFormData] = useState(ontimePlaceholderSettings); const [changed, setChanged] = useState(false); const [submitting, setSubmitting] = useState(false); diff --git a/apps/client/src/features/modals/EventSettingsModal.jsx b/apps/client/src/features/modals/EventSettingsModal.jsx index b6f15c3afe..d449ecc1d4 100644 --- a/apps/client/src/features/modals/EventSettingsModal.jsx +++ b/apps/client/src/features/modals/EventSettingsModal.jsx @@ -1,8 +1,9 @@ -import { useCallback, useContext, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { FormLabel, Input, ModalBody, Textarea } from '@chakra-ui/react'; +import { useEmitLog } from '@/common/stores/logger'; + import { postEventData } from '../../common/api/eventDataApi'; -import { LoggingContext } from '../../common/context/LoggingContext'; import useEventData from '../../common/hooks-query/useEventData'; import { eventDataPlaceholder } from '../../common/models/EventData'; @@ -13,7 +14,7 @@ import style from './Modals.module.scss'; export default function SettingsModal() { const { data, status, refetch } = useEventData(); - const { emitError } = useContext(LoggingContext); + const { emitError } = useEmitLog(); const [formData, setFormData] = useState(eventDataPlaceholder); const [changed, setChanged] = useState(false); const [submitting, setSubmitting] = useState(false); diff --git a/apps/client/src/features/modals/TableOptionsModal.jsx b/apps/client/src/features/modals/TableOptionsModal.jsx index fd7193a9d8..bd8df82814 100644 --- a/apps/client/src/features/modals/TableOptionsModal.jsx +++ b/apps/client/src/features/modals/TableOptionsModal.jsx @@ -1,9 +1,10 @@ -import { useCallback, useContext, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Input, ModalBody } from '@chakra-ui/react'; import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline'; +import { useEmitLog } from '@/common/stores/logger'; + import { postUserFields } from '../../common/api/ontimeApi'; -import { LoggingContext } from '../../common/context/LoggingContext'; import useUserFields from '../../common/hooks-query/useUserFields'; import { userFieldsPlaceholder } from '../../common/models/UserFields'; import { handleLinks, host } from '../../common/utils/linkUtils'; @@ -14,7 +15,7 @@ import style from './Modals.module.scss'; export default function TableOptionsModal() { const { data, status, refetch } = useUserFields(); - const { emitError } = useContext(LoggingContext); + const { emitError } = useEmitLog(); const [changed, setChanged] = useState(false); const [submitting, setSubmitting] = useState(false); const [userFields, setUserFields] = useState(userFieldsPlaceholder); diff --git a/apps/client/src/features/modals/ViewsSettingsModal.jsx b/apps/client/src/features/modals/ViewsSettingsModal.jsx index 5b2ab4f92b..bfca2194b5 100644 --- a/apps/client/src/features/modals/ViewsSettingsModal.jsx +++ b/apps/client/src/features/modals/ViewsSettingsModal.jsx @@ -1,11 +1,12 @@ -import { useCallback, useContext, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { FormControl, FormLabel, ModalBody } from '@chakra-ui/react'; import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp'; import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline'; +import { useEmitLog } from '@/common/stores/logger'; + import { postView } from '../../common/api/ontimeApi'; import EnableBtn from '../../common/components/buttons/EnableBtn'; -import { LoggingContext } from '../../common/context/LoggingContext'; import useViewSettings from '../../common/hooks-query/useViewSettings'; import { viewsSettingsPlaceholder } from '../../common/models/ViewSettings.type'; import { openLink } from '../../common/utils/linkUtils'; @@ -17,7 +18,7 @@ import style from './Modals.module.scss'; export default function ViewsSettingsModal() { const { data, status, refetch } = useViewSettings(); - const { emitError } = useContext(LoggingContext); + const { emitError } = useEmitLog(); const [formData, setFormData] = useState(viewsSettingsPlaceholder); const [changed, setChanged] = useState(false); const [submitting, setSubmitting] = useState(false); diff --git a/apps/client/src/features/modals/integration-modal/OscIntegrationSettings.tsx b/apps/client/src/features/modals/integration-modal/OscIntegrationSettings.tsx index 917e7d02bc..7e98b74a49 100644 --- a/apps/client/src/features/modals/integration-modal/OscIntegrationSettings.tsx +++ b/apps/client/src/features/modals/integration-modal/OscIntegrationSettings.tsx @@ -1,18 +1,17 @@ -import { useContext } from 'react'; import { useForm } from 'react-hook-form'; import { Button, FormControl, Input, ModalBody, ModalFooter, Switch } from '@chakra-ui/react'; import { postOSC } from '../../../common/api/ontimeApi'; -import { LoggingContext } from '../../../common/context/LoggingContext'; import useOscSettings from '../../../common/hooks-query/useOscSettings'; import { PlaceholderSettings } from '../../../common/models/OscSettings'; +import { useEmitLog } from '../../../common/stores/logger'; import { isIPAddress, isOnlyNumbers } from '../../../common/utils/regex'; import styles from '../Modal.module.scss'; export default function OscIntegrationSettings() { const { data } = useOscSettings(); - const { emitError } = useContext(LoggingContext); + const { emitError } = useEmitLog(); const { handleSubmit, register, diff --git a/apps/client/src/features/modals/integration-modal/OscSettingsModal.jsx b/apps/client/src/features/modals/integration-modal/OscSettingsModal.jsx index a6d4f64e26..e65702a683 100644 --- a/apps/client/src/features/modals/integration-modal/OscSettingsModal.jsx +++ b/apps/client/src/features/modals/integration-modal/OscSettingsModal.jsx @@ -1,10 +1,11 @@ -import { useCallback, useContext, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { FormControl, FormLabel, Input, ModalBody } from '@chakra-ui/react'; import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline'; +import { useEmitLog } from '@/common/stores/logger'; + import { postOSC } from '../../../common/api/ontimeApi'; import EnableBtn from '../../../common/components/buttons/EnableBtn'; -import { LoggingContext } from '../../../common/context/LoggingContext'; import useOscSettings from '../../../common/hooks-query/useOscSettings'; import { oscPlaceholderSettings } from '../../../common/models/OscSettings'; import { inputProps, portInputProps } from '../modalHelper'; @@ -76,7 +77,7 @@ const oscTriggerEndpoints = [ export default function OscSettingsModal() { const { data, status, refetch } = useOscSettings(); - const { emitError } = useContext(LoggingContext); + const { emitError } = useEmitLog(); const [formData, setFormData] = useState(oscPlaceholderSettings); const [changed, setChanged] = useState(false); const [submitting, setSubmitting] = useState(false); @@ -122,8 +123,8 @@ export default function OscSettingsModal() { } else { try { await postOSC(formData); - } catch (error){ - emitError(`Error setting OSC: ${error}`) + } catch (error) { + emitError(`Error setting OSC: ${error}`); } finally { await refetch(); setChanged(false); @@ -131,7 +132,7 @@ export default function OscSettingsModal() { } setSubmitting(false); }, - [emitError, formData, refetch] + [emitError, formData, refetch], ); /** @@ -154,7 +155,7 @@ export default function OscSettingsModal() { setFormData(temp); setChanged(true); }, - [formData] + [formData], ); return ( @@ -283,12 +284,7 @@ export default function OscSettingsModal() { - + ); diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx index 534ac88a58..3eb0f1efac 100644 --- a/apps/client/src/features/rundown/RundownEntry.tsx +++ b/apps/client/src/features/rundown/RundownEntry.tsx @@ -4,8 +4,8 @@ import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontim import { defaultPublicAtom, editorEventId, startTimeIsLastEndAtom } from '../../common/atoms/LocalEventSettings'; import { CursorContext } from '../../common/context/CursorContext'; -import { LoggingContext } from '../../common/context/LoggingContext'; import { useEventAction } from '../../common/hooks/useEventAction'; +import { useEmitLog } from '../../common/stores/logger'; import { cloneEvent } from '../../common/utils/eventsManager'; import { calculateDuration } from '../../common/utils/timesManager'; @@ -31,7 +31,7 @@ interface RundownEntryProps { export default function RundownEntry(props: RundownEntryProps) { const { index, eventIndex, data, selected, hasCursor, next, delay, previousEnd, previousEventId, playback } = props; - const { emitError } = useContext(LoggingContext); + const { emitError } = useEmitLog(); const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom); const defaultPublic = useAtomValue(defaultPublicAtom); const { addEvent, updateEvent, deleteEvent } = useEventAction(); diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.jsx b/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.jsx index 11d8afb580..cf5c2a3cca 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.jsx +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.jsx @@ -1,8 +1,9 @@ -import { useCallback, useContext } from 'react'; +import { useCallback } from 'react'; import PropTypes from 'prop-types'; +import { useEmitLog } from '@/common/stores/logger'; + import TimeInput from '../../../../common/components/input/time-input/TimeInput'; -import { LoggingContext } from '../../../../common/context/LoggingContext'; import { millisToMinutes } from '../../../../common/utils/dateConfig'; import { stringFromMillis } from '../../../../common/utils/time'; import { validateEntry } from '../../../../common/utils/timesManager'; @@ -11,7 +12,7 @@ import style from '../EventBlock.module.scss'; export default function EventBlockTimers(props) { const { timeStart, timeEnd, duration, delay, actionHandler, previousEnd } = props; - const { emitWarning } = useContext(LoggingContext); + const { emitWarning } = useEmitLog(); const delayTime = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`; const newTime = stringFromMillis(timeStart + delay); diff --git a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx index cb534c0c35..bb287f0d24 100644 --- a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx +++ b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx @@ -1,11 +1,11 @@ -import { useCallback, useContext, useRef } from 'react'; +import { useCallback, useRef } from 'react'; import { Button, Checkbox, Tooltip } from '@chakra-ui/react'; import { useAtomValue } from 'jotai'; import { SupportedEvent } from 'ontime-types'; import { defaultPublicAtom, startTimeIsLastEndAtom } from '../../../common/atoms/LocalEventSettings'; -import { LoggingContext } from '../../../common/context/LoggingContext'; import { useEventAction } from '../../../common/hooks/useEventAction'; +import { useEmitLog } from '../../../common/stores/logger'; import { tooltipDelayMid } from '../../../ontimeConfig'; import style from './QuickAddBlock.module.scss'; @@ -21,7 +21,7 @@ interface QuickAddBlockProps { export default function QuickAddBlock(props: QuickAddBlockProps) { const { showKbd, eventId, previousEventId, disableAddDelay = true, disableAddBlock } = props; const { addEvent } = useEventAction(); - const { emitError } = useContext(LoggingContext); + const { emitError } = useEmitLog(); const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom); const defaultPublic = useAtomValue(defaultPublicAtom); const doStartTime = useRef(null); From 84baf820a200e3badcc76bb6c9bd9c5e35f1e0d2 Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Wed, 8 Mar 2023 20:22:35 +0100 Subject: [PATCH 10/20] chore: add sentry reporting in backend --- apps/server/src/app.ts | 11 +++++------ apps/server/src/modules/sentry.js | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 31b132ed61..2d0689b724 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -6,7 +6,6 @@ import cors from 'cors'; // import utils import { join, resolve } from 'path'; -import { initSentry } from './modules/sentry.js'; import { currentDirectory, environment, isProduction, resolvedPath } from './setup.js'; import { ONTIME_VERSION } from './ONTIME_VERSION.js'; import { OSCSettings } from 'ontime-types'; @@ -36,7 +35,7 @@ if (!isProduction) { console.log(`Ontime directory at ${currentDirectory} `); } -initSentry(environment); +initSentry(isProduction); // Create express APP const app = express(); @@ -212,14 +211,14 @@ export const shutdown = async (exitCode = 0) => { process.on('exit', (code) => console.log(`Ontime exited with code: ${code}`)); -process.on('unhandledRejection', async (error, promise) => { - console.error(error, 'Error: unhandled rejection', promise); +process.on('unhandledRejection', async (error) => { + reportSentryException(error); logger.error('SERVER', 'Error: unhandled rejection'); await shutdown(1); }); -process.on('uncaughtException', async (error, promise) => { - console.error(error, 'Error: uncaught exception', promise); +process.on('uncaughtException', async (error) => { + reportSentryException(error); logger.error('SERVER', 'Error: uncaught exception'); await shutdown(1); }); diff --git a/apps/server/src/modules/sentry.js b/apps/server/src/modules/sentry.js index 6ccfe231a2..fb4a8cf43a 100644 --- a/apps/server/src/modules/sentry.js +++ b/apps/server/src/modules/sentry.js @@ -2,8 +2,8 @@ import * as Sentry from '@sentry/node'; let shouldReport; -export function initSentry(environment) { - shouldReport = environment === 'production'; +export function initSentry(doReport) { + shouldReport = doReport; Sentry.init({ dsn: 'https://ceb6abdce7374857bb50b65636cbaed1@o4504288369836032.ingest.sentry.io/4504288555565056', tracesSampleRate: 1.0, From 5c162b01896c08c03986eb11a052d92fffc6ec87 Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Sat, 11 Mar 2023 10:53:35 +0100 Subject: [PATCH 11/20] wip: message control --- .../control/message/MessageControl.tsx | 26 +++---- .../message-service/MessageService.ts | 70 +++++++++---------- .../runtime/MessageControl.type.ts | 6 -- .../definitions/runtime/RuntimeStore.type.ts | 6 +- packages/types/src/index.ts | 3 +- 5 files changed, 53 insertions(+), 58 deletions(-) diff --git a/apps/client/src/features/control/message/MessageControl.tsx b/apps/client/src/features/control/message/MessageControl.tsx index f62906fe3e..20d7549745 100644 --- a/apps/client/src/features/control/message/MessageControl.tsx +++ b/apps/client/src/features/control/message/MessageControl.tsx @@ -9,40 +9,40 @@ import InputRow from './InputRow'; import style from './MessageControl.module.scss'; export default function MessageControl() { - const { data } = useMessageControl(); + const data = useMessageControl(); return (
setMessage.presenterText(newValue)} - actionHandler={() => setMessage.presenterVisible(!data?.messages.presenter.visible)} + actionHandler={() => setMessage.presenterVisible(!data.timerMessage.visible)} /> setMessage.publicText(newValue)} - actionHandler={() => setMessage.publicVisible(!data?.messages.public.visible)} + actionHandler={() => setMessage.publicVisible(!data.publicMessage.visible)} /> setMessage.lowerText(newValue)} - actionHandler={() => setMessage.lowerVisible(!data?.messages.lower.visible)} + actionHandler={() => setMessage.lowerVisible(!data.lowerMessage.visible)} />
diff --git a/apps/server/src/services/message-service/MessageService.ts b/apps/server/src/services/message-service/MessageService.ts index 0d129e4bec..91c4189509 100644 --- a/apps/server/src/services/message-service/MessageService.ts +++ b/apps/server/src/services/message-service/MessageService.ts @@ -1,11 +1,13 @@ -import { MessageControl } from 'ontime-types'; +import { Message } from 'ontime-types'; import { eventStore } from '../../stores/EventStore.js'; let instance; class MessageService { - messages: MessageControl; + timerMessage: Message; + publicMessage: Message; + lowerMessage: Message; onAir: boolean; constructor() { @@ -16,20 +18,21 @@ class MessageService { // eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton instance = this; - this.messages = { - presenter: { - text: '', - visible: false, - }, - public: { - text: '', - visible: false, - }, - lower: { - text: '', - visible: false, - }, + this.timerMessage = { + text: '', + visible: false, }; + + this.publicMessage = { + text: '', + visible: false, + }; + + this.lowerMessage = { + text: '', + visible: false, + }; + this.onAir = false; } @@ -37,8 +40,8 @@ class MessageService { * @description sets message on stage timer screen */ setTimerText(payload: string) { - this.messages.presenter.text = payload; - this.updateStore(); + this.timerMessage.text = payload; + eventStore.set('timerMessage', this.timerMessage); return this.getAll(); } @@ -46,8 +49,8 @@ class MessageService { * @description sets message visibility on stage timer screen */ setTimerVisibility(status: boolean) { - this.messages.presenter.visible = status; - this.updateStore(); + this.timerMessage.visible = status; + eventStore.set('timerMessage', this.timerMessage); return this.getAll(); } @@ -55,8 +58,8 @@ class MessageService { * @description sets message on public screen */ setPublicText(payload: string) { - this.messages.public.text = payload; - this.updateStore(); + this.publicMessage.text = payload; + eventStore.set('publicMessage', this.publicMessage); return this.getAll(); } @@ -64,8 +67,8 @@ class MessageService { * @description sets message visibility on public screen */ setPublicVisibility(status: boolean) { - this.messages.public.visible = status; - this.updateStore(); + this.publicMessage.visible = status; + eventStore.set('publicMessage', this.publicMessage); return this.getAll(); } @@ -73,8 +76,8 @@ class MessageService { * @description sets message on lower third screen */ setLowerText(payload: string) { - this.messages.lower.text = payload; - this.updateStore(); + this.lowerMessage.text = payload; + eventStore.set('lowerMessage', this.lowerMessage); return this.getAll(); } @@ -82,8 +85,8 @@ class MessageService { * @description sets message visibility on lower third screen */ setLowerVisibility(status: boolean) { - this.messages.lower.visible = status; - this.updateStore(); + this.lowerMessage.visible = status; + eventStore.set('lowerMessage', this.lowerMessage); return this.getAll(); } @@ -91,18 +94,13 @@ class MessageService { * @description set state of onAir, toggles if parameters are offered */ setOnAir(status?: boolean) { - if (!status) { + if (typeof status === 'undefined') { this.onAir = !this.onAir; } else { this.onAir = status; } - this.updateStore(); - return this.getAll(); - } - - private updateStore() { - eventStore.set('messages', this.messages); eventStore.set('onAir', this.onAir); + return this.getAll(); } /** @@ -110,7 +108,9 @@ class MessageService { */ getAll() { return { - messages: this.messages, + timerMessage: this.timerMessage, + publicMessage: this.publicMessage, + lowerMessage: this.lowerMessage, onAir: this.onAir, }; } diff --git a/packages/types/src/definitions/runtime/MessageControl.type.ts b/packages/types/src/definitions/runtime/MessageControl.type.ts index 0b2d6bf2cd..bbedf6828e 100644 --- a/packages/types/src/definitions/runtime/MessageControl.type.ts +++ b/packages/types/src/definitions/runtime/MessageControl.type.ts @@ -2,9 +2,3 @@ export type Message = { text: string; visible: boolean; }; - -export type MessageControl = { - presenter: Message; - public: Message; - lower: Message; -}; diff --git a/packages/types/src/definitions/runtime/RuntimeStore.type.ts b/packages/types/src/definitions/runtime/RuntimeStore.type.ts index 7846c197ab..f5cba42d6d 100644 --- a/packages/types/src/definitions/runtime/RuntimeStore.type.ts +++ b/packages/types/src/definitions/runtime/RuntimeStore.type.ts @@ -1,5 +1,5 @@ import { Playback } from './Playback.type.js'; -import { MessageControl } from './MessageControl.type.js'; +import { Message } from './MessageControl.type.js'; import { TimerState } from './TimerState.type.js'; import { TitleBlock } from './TitleBlock.type.js'; import { Loaded } from './Playlist.type.js'; @@ -10,7 +10,9 @@ export type RuntimeStore = { playback: Playback; // messages service - messages: MessageControl; + timerMessage: Message; + publicMessage: Message; + lowerMessage: Message; onAir: boolean; // event loader diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index a01ff53f56..d3aeab9be1 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,7 +1,7 @@ import { Alias } from './definitions/core/Alias.type.js'; import { DatabaseModel } from './definitions/DataModel.type.js'; import { EventData } from './definitions/core/EventData.type.js'; -import { Message, MessageControl } from './definitions/runtime/MessageControl.type.js'; +import { Message } from './definitions/runtime/MessageControl.type.js'; import { OntimeBaseEvent, OntimeBlock, @@ -59,7 +59,6 @@ export { Playback }; export { TimerLifeCycle }; export type { Message }; -export type { MessageControl }; export type { Loaded }; export type { RuntimeStore }; export type { TimerState }; From 6ba3b0ca086b91c7096ef9ec424c90d4d6d08426 Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Sat, 11 Mar 2023 11:16:29 +0100 Subject: [PATCH 12/20] wip: info --- apps/client/src/common/stores/logger.ts | 19 ++++------------ .../src/features/info/CollapsableInfo.tsx | 8 ++----- .../src/features/info/{Info.jsx => Info.tsx} | 18 +++++++-------- apps/client/src/features/info/InfoLogger.tsx | 22 ++++++------------- .../src/classes/event-loader/EventLoader.ts | 17 +++++++++++--- apps/server/src/services/RundownService.ts | 20 ++++++++++------- 6 files changed, 48 insertions(+), 56 deletions(-) rename apps/client/src/features/info/{Info.jsx => Info.tsx} (69%) diff --git a/apps/client/src/common/stores/logger.ts b/apps/client/src/common/stores/logger.ts index 20adddbc45..39d984b62b 100644 --- a/apps/client/src/common/stores/logger.ts +++ b/apps/client/src/common/stores/logger.ts @@ -1,8 +1,8 @@ import { useCallback, useSyncExternalStore } from 'react'; -import useWebSocket from 'react-use-websocket'; -import { Log, LogLevel, LogMessage } from 'ontime-types'; +import { Log, LogLevel } from 'ontime-types'; import { generateId } from 'ontime-utils'; +import { socketSendJson } from '../utils/socket'; import { nowInMillis, stringFromMillis } from '../utils/time'; import createStore from './createStore'; @@ -11,15 +11,8 @@ export const logger = createStore([]); export const LOGGER_MAX_MESSAGES = 100; export function useEmitLog() { - // should I just make my own ? - const { sendJsonMessage } = useWebSocket('ws://localhost:4001/ws', { - share: true, - shouldReconnect: () => true, - }); - const _addToLogger = (log: Log) => { const state = logger.get(); - console.log('DEBUG', state); state.push(log); if (state.length > LOGGER_MAX_MESSAGES) { state.slice(1); @@ -42,12 +35,8 @@ export function useEmitLog() { text, }; - const logMessage: LogMessage = { - type: 'ontime-log', - payload: log, - }; _addToLogger(log); - sendJsonMessage(logMessage); + socketSendJson('ontime-log', log); }; /** @@ -84,7 +73,7 @@ export function useEmitLog() { ); const clearLog = useCallback(() => { - throw new Error('NOT IMPLEMENTED CLEAR LOG'); + logger.set([]); }, []); return { diff --git a/apps/client/src/features/info/CollapsableInfo.tsx b/apps/client/src/features/info/CollapsableInfo.tsx index 1a2e4929ab..f076a6343b 100644 --- a/apps/client/src/features/info/CollapsableInfo.tsx +++ b/apps/client/src/features/info/CollapsableInfo.tsx @@ -9,7 +9,7 @@ type TitleShape = { presenter: string; subtitle: string; note: string; -} +}; interface CollapsableInfoProps { title: string; @@ -22,11 +22,7 @@ export default function CollapsableInfo(props: CollapsableInfoProps) { return (
- setCollapsed((prev) => !prev)} - /> + setCollapsed((prev) => !prev)} /> {!collapsed && (
diff --git a/apps/client/src/features/info/Info.jsx b/apps/client/src/features/info/Info.tsx similarity index 69% rename from apps/client/src/features/info/Info.jsx rename to apps/client/src/features/info/Info.tsx index 4ba685ff67..7201dea9ff 100644 --- a/apps/client/src/features/info/Info.jsx +++ b/apps/client/src/features/info/Info.tsx @@ -7,20 +7,20 @@ import InfoNif from './InfoNif'; import style from './Info.module.scss'; export default function Info() { - const { data } = useInfoPanel(); + const data = useInfoPanel(); const titlesNow = { - title: data.titles.titleNow, - subtitle: data.titles.subtitleNow, - presenter: data.titles.presenterNow, - note: data.titles.noteNow, + title: data.titles.titleNow || '', + subtitle: data.titles.subtitleNow || '', + presenter: data.titles.presenterNow || '', + note: data.titles.noteNow || '', }; const titlesNext = { - title: data.titles.titleNext, - subtitle: data.titles.subtitleNext, - presenter: data.titles.presenterNext, - note: data.titles.noteNext, + title: data.titles.titleNext || '', + subtitle: data.titles.subtitleNext || '', + presenter: data.titles.presenterNext || '', + note: data.titles.noteNext || '', }; const selected = !data.numEvents diff --git a/apps/client/src/features/info/InfoLogger.tsx b/apps/client/src/features/info/InfoLogger.tsx index b3e8d134b5..e7d8d640f2 100644 --- a/apps/client/src/features/info/InfoLogger.tsx +++ b/apps/client/src/features/info/InfoLogger.tsx @@ -3,7 +3,7 @@ import { Button } from '@chakra-ui/react'; import { Log } from 'ontime-types'; import CollapseBar from '../../common/components/collapse-bar/CollapseBar'; -import { useLogData } from '../../common/stores/logger'; +import { useEmitLog, useLogData } from '../../common/stores/logger'; import style from './InfoLogger.module.scss'; @@ -18,8 +18,11 @@ enum LOG_FILTER { export default function InfoLogger() { const logData = useLogData(); + const { clearLog } = useEmitLog(); // const { logData, clearLog } = useContext(LoggingContext); - const [data, setData] = useState([]); + // TODO: derived data shouldn't be in state + const data: Log[] = []; + const setData = (newData) => console.log('tried setting data'); const [collapsed, setCollapsed] = useState(false); const [showClient, setShowClient] = useState(true); const [showServer, setShowServer] = useState(true); @@ -28,15 +31,6 @@ export default function InfoLogger() { const [showPlayback, setShowPlayback] = useState(true); const [showUser, setShowUser] = useState(true); - const clearLog = () => console.log('CALLED CLEAR'); - - console.log('DEBUG INFO', logData) - - useEffect(() => { - if (!logData) { - return; - } - const matchers: LOG_FILTER[] = []; if (showUser) { matchers.push(LOG_FILTER.USER); @@ -57,9 +51,7 @@ export default function InfoLogger() { matchers.push(LOG_FILTER.PLAYBACK); } - const filteredData = logData.filter((entry) => matchers.some((match) => entry.origin === match)); - setData(filteredData); - }, [logData, showUser, showClient, showServer, showPlayback, showRx, showTx]); + const filteredData = logData.filter((entry) => matchers.some((match) => entry.origin === match)); const disableOthers = useCallback((toEnable: LOG_FILTER) => { toEnable === LOG_FILTER.USER ? setShowUser(true) : setShowUser(false); @@ -135,7 +127,7 @@ export default function InfoLogger() {
    - {data.map((logEntry) => ( + {filteredData.map((logEntry) => (
  • {logEntry.time} {logEntry.origin} diff --git a/apps/server/src/classes/event-loader/EventLoader.ts b/apps/server/src/classes/event-loader/EventLoader.ts index dd9a81e536..7660ed28d0 100644 --- a/apps/server/src/classes/event-loader/EventLoader.ts +++ b/apps/server/src/classes/event-loader/EventLoader.ts @@ -22,7 +22,10 @@ export class EventLoader { // eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton instance = this; - this.reset(false); + } + + init() { + this.reset(); this.loadedEvent = null; } @@ -179,10 +182,18 @@ export class EventLoader { }; } + /** + * Forces event loader to update the event count + */ + updateNumEvents() { + this.loaded.numEvents = EventLoader.getPlayableEvents().length; + eventStore.set('loaded', this.loaded); + } + /** * Resets instance state */ - reset(emit?: boolean) { + reset(emit = true) { this.loadedEvent = null; this.loaded = { selectedEventIndex: null, @@ -190,7 +201,7 @@ export class EventLoader { selectedPublicEventId: null, nextEventId: null, nextPublicEventId: null, - numEvents: 0, + numEvents: EventLoader.getPlayableEvents().length, }; this.titles = { titleNow: null, diff --git a/apps/server/src/services/RundownService.ts b/apps/server/src/services/RundownService.ts index a2d0527c90..1887fc50e1 100644 --- a/apps/server/src/services/RundownService.ts +++ b/apps/server/src/services/RundownService.ts @@ -5,7 +5,6 @@ import { block as blockDef, delay as delayDef, event as eventDef } from '../mode import { MAX_EVENTS } from '../settings.js'; import { EventLoader, eventLoader } from '../classes/event-loader/EventLoader.js'; import { eventTimer } from './TimerService.js'; -import { eventStore } from '../stores/EventStore.js'; /** * Checks if a list of IDs is in the current selection @@ -112,7 +111,7 @@ export function updateTimer(affectedIds?: string[]) { * @param {object} eventData * @return {unknown[]} */ -export async function addEvent(eventData) { +export async function addEvent(eventData: Partial | Partial | Partial) { const numEvents = DataProvider.getRundownLength(); if (numEvents > MAX_EVENTS) { throw new Error(`ERROR: Reached limit number of ${MAX_EVENTS} events`); @@ -145,7 +144,7 @@ export async function addEvent(eventData) { throw new Error(error); } updateTimer([id]); - eventStore.broadcast(); + updateChangeNumEvents(); return newEvent; } @@ -157,7 +156,6 @@ export async function editEvent(eventData) { } const newEvent = await DataProvider.updateEventById(eventId, eventData); updateTimer([eventId]); - eventStore.broadcast(); return newEvent; } @@ -169,7 +167,7 @@ export async function editEvent(eventData) { export async function deleteEvent(eventId) { await DataProvider.deleteEvent(eventId); updateTimer([eventId]); - eventStore.broadcast(); + updateChangeNumEvents(); } /** @@ -179,7 +177,7 @@ export async function deleteEvent(eventId) { export async function deleteAllEvents() { await DataProvider.clearRundown(); updateTimer(); - eventStore.broadcast(); + updateChangeNumEvents(); } /** @@ -204,7 +202,6 @@ export async function reorderEvent(eventId, from, to) { // save rundown await DataProvider.setRundown(rundown); updateTimer(); - return reorderedItem; } @@ -260,5 +257,12 @@ export async function applyDelay(eventId) { // update rundown await DataProvider.setRundown(rundown); updateTimer(); - eventStore.broadcast(); +} + +/** + * Forces update in the store + * Called when we make changes to the rundown object + */ +function updateChangeNumEvents() { + eventLoader.updateNumEvents(); } From 4c05e7c223f910c54d35cc99eab51fd396439926 Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Sat, 11 Mar 2023 21:34:55 +0100 Subject: [PATCH 13/20] refactor: string manipulation --- .../components/input/time-input/TimeInput.tsx | 6 +- apps/client/src/common/stores/logger.ts | 6 +- .../common/utils/__tests__/dateConfig.test.js | 3 +- apps/client/src/common/utils/time.js | 86 ------------------- apps/client/src/common/utils/time.ts | 60 +++++++++++++ .../control/playback/PlaybackTimer.tsx | 18 ++-- .../src/features/event-editor/EventEditor.tsx | 6 +- .../composite/EventBlockTimers.jsx | 4 +- apps/client/src/features/table/columns.jsx | 9 +- apps/client/src/features/table/utils.js | 4 +- .../features/viewers/studio/StudioClock.jsx | 5 +- apps/server/src/utils/time.js | 31 ------- packages/utils/index.ts | 1 + packages/utils/package.json | 2 + .../src/date-utils/millisToString.test.ts | 71 +++++++++++++++ .../utils/src/date-utils/millisToString.ts | 19 ++++ 16 files changed, 183 insertions(+), 148 deletions(-) delete mode 100644 apps/client/src/common/utils/time.js create mode 100644 apps/client/src/common/utils/time.ts create mode 100644 packages/utils/src/date-utils/millisToString.test.ts create mode 100644 packages/utils/src/date-utils/millisToString.ts diff --git a/apps/client/src/common/components/input/time-input/TimeInput.tsx b/apps/client/src/common/components/input/time-input/TimeInput.tsx index 736f68e844..dd79147159 100644 --- a/apps/client/src/common/components/input/time-input/TimeInput.tsx +++ b/apps/client/src/common/components/input/time-input/TimeInput.tsx @@ -1,11 +1,11 @@ import { KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react'; import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react'; +import { millisToString } from 'ontime-utils'; import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor'; import { tooltipDelayFast } from '../../../../ontimeConfig'; import { useEmitLog } from '../../../stores/logger'; import { forgivingStringToMillis } from '../../../utils/dateConfig'; -import { stringFromMillis } from '../../../utils/time'; import { TimeEntryField } from '../../../utils/timesManager'; import style from './TimeInput.module.scss'; @@ -34,7 +34,7 @@ export default function TimeInput(props: TimeInputProps) { const resetValue = useCallback(() => { // Todo: check if change is necessary try { - setValue(stringFromMillis(time + delay)); + setValue(millisToString(time + delay)); } catch (error) { emitError(`Unable to parse date: ${error}`); } @@ -97,7 +97,7 @@ export default function TimeInput(props: TimeInputProps) { const success = handleSubmit(newValue); if (success) { const ms = forgivingStringToMillis(newValue); - setValue(stringFromMillis(ms + delay)); + setValue(millisToString(ms + delay)); } else { resetValue(); } diff --git a/apps/client/src/common/stores/logger.ts b/apps/client/src/common/stores/logger.ts index 39d984b62b..456cff7f7e 100644 --- a/apps/client/src/common/stores/logger.ts +++ b/apps/client/src/common/stores/logger.ts @@ -1,9 +1,9 @@ import { useCallback, useSyncExternalStore } from 'react'; import { Log, LogLevel } from 'ontime-types'; -import { generateId } from 'ontime-utils'; +import { generateId, millisToString } from 'ontime-utils'; import { socketSendJson } from '../utils/socket'; -import { nowInMillis, stringFromMillis } from '../utils/time'; +import { nowInMillis } from '../utils/time'; import createStore from './createStore'; @@ -30,7 +30,7 @@ export function useEmitLog() { const log = { id: generateId(), origin: 'CLIENT', - time: stringFromMillis(nowInMillis()), + time: millisToString(nowInMillis()), level, text, }; diff --git a/apps/client/src/common/utils/__tests__/dateConfig.test.js b/apps/client/src/common/utils/__tests__/dateConfig.test.js index 9cf803cd39..fb8baa1fd4 100644 --- a/apps/client/src/common/utils/__tests__/dateConfig.test.js +++ b/apps/client/src/common/utils/__tests__/dateConfig.test.js @@ -6,7 +6,6 @@ import { millisToSeconds, timeStringToMillis, } from '../dateConfig'; -import { stringFromMillis } from '../time'; describe('test string from formatDisplay function', () => { it('test with null values', () => { @@ -58,7 +57,7 @@ describe('test string from formatDisplay function', () => { describe('test formatDisplay handles partial secs', () => { it('test with 1795829', () => { const t = { val: 1795829, result: '00:29:55' }; - expect(stringFromMillis(t.val)).toBe(t.result); + expect(formatDisplay(t.val)).toBe(t.result); }); }); diff --git a/apps/client/src/common/utils/time.js b/apps/client/src/common/utils/time.js deleted file mode 100644 index 76df9c98e8..0000000000 --- a/apps/client/src/common/utils/time.js +++ /dev/null @@ -1,86 +0,0 @@ -import { DateTime } from 'luxon'; - -import { APP_SETTINGS } from '../api/apiConstants'; -import { ontimeQueryClient } from '../queryClient'; - -import { mth, mtm, mts } from './timeConstants'; - - -/** - * Returns current time in milliseconds - * @returns {number} - */ -export const nowInMillis = () => { - const now = new Date(); - - // extract milliseconds since midnight - let elapsed = now.getHours() * 3600000; - elapsed += now.getMinutes() * 60000; - elapsed += now.getSeconds() * 1000; - elapsed += now.getMilliseconds(); - - return elapsed; -}; - -/** - * @description Converts milliseconds to string representing time - * @param {number | null} ms - time in milliseconds - * @param {boolean} showSeconds - weather to show the seconds - * @param {string} delim - character between HH MM SS - * @param {string} ifNull - what to return if value is null - * @returns {string} String representing time 00:12:02 - */ -export const stringFromMillis = (ms, showSeconds = true, delim = ':', ifNull = '...') => { - if (ms == null || isNaN(ms)) return ifNull; - const isNegative = ms < 0 ? '-' : ''; - const millis = Math.abs(ms); - - /** - * @description ensures value is double digit - * @param value - * @return {string|*} - */ - const showWith0 = (value) => (value < 10 ? `0${value}` : value); - const hours = showWith0(Math.floor(((millis / mth) % 60) % 24)); - const minutes = showWith0(Math.floor((millis / mtm) % 60)); - const seconds = showWith0(Math.floor((millis / mts) % 60)); - - return showSeconds - ? `${isNegative}${ - parseInt(hours, 10) ? `${hours}${delim}` : `00${delim}` - }${minutes}${delim}${seconds}` - : `${isNegative}${parseInt(hours, 10) ? `${hours}` : '00'}${delim}${minutes}`; -}; - -/** - * @description Resolves format from url and store - * @return {string|undefined} - */ -export const resolveTimeFormat = () => { - const params = new URL(document.location).searchParams; - const urlOptions = params.get('format'); - const settings = ontimeQueryClient.getQueryData(APP_SETTINGS); - - return urlOptions || settings?.timeFormat; -}; - -/** - /** - * @description utility function to format a date in 12 or 24 hour format - * @param {number} milliseconds - * @param {object} [options] - * @param {boolean} [options.showSeconds] - * @param {string} [options.format] - * @param {function} resolver - * @return {string} - */ -export const formatTime = (milliseconds, options, resolver = resolveTimeFormat) => { - if (milliseconds === null) { - return '...'; - } - const timeFormat = resolver(); - const { showSeconds = false, format: formatString = 'hh:mm a' } = options || {}; - return timeFormat === '12' - ? DateTime.fromMillis(milliseconds).toUTC().toFormat(formatString) - : stringFromMillis(milliseconds, showSeconds); -}; diff --git a/apps/client/src/common/utils/time.ts b/apps/client/src/common/utils/time.ts new file mode 100644 index 0000000000..6aeaa50dba --- /dev/null +++ b/apps/client/src/common/utils/time.ts @@ -0,0 +1,60 @@ +import { DateTime } from 'luxon'; +import { Settings } from 'ontime-types'; +import { millisToString } from 'ontime-utils'; + +import { APP_SETTINGS } from '../api/apiConstants'; +import { ontimeQueryClient } from '../queryClient'; + +/** + * Returns current time in milliseconds + * @returns {number} + */ +export const nowInMillis = () => { + const now = new Date(); + + // extract milliseconds since midnight + let elapsed = now.getHours() * 3600000; + elapsed += now.getMinutes() * 60000; + elapsed += now.getSeconds() * 1000; + elapsed += now.getMilliseconds(); + + return elapsed; +}; + +/** + * @description Resolves format from url and store + * @return {string|undefined} + */ +export const resolveTimeFormat = () => { + const params = new URL(document.location.href).searchParams; + const urlOptions = params.get('format'); + const settings: Settings | undefined = ontimeQueryClient.getQueryData(APP_SETTINGS); + + return urlOptions || settings?.timeFormat; +}; + +type FormatOptions = { + showSeconds?: boolean; + format?: string; +}; + +/** + /** + * @description utility function to format a date in 12 or 24 hour format + * @param {number | null} milliseconds + * @param {object} [options] + * @param {boolean} [options.showSeconds] + * @param {string} [options.format] + * @param {function} resolver + * @return {string} + */ +export const formatTime = (milliseconds: number | null, options: FormatOptions, resolver = resolveTimeFormat) => { + if (milliseconds === null) { + return '...'; + } + const timeFormat = resolver(); + const { showSeconds = false, format: formatString = 'hh:mm a' } = options || {}; + return timeFormat === '12' + ? DateTime.fromMillis(milliseconds).toUTC().toFormat(formatString) + : millisToString(milliseconds, showSeconds); +}; diff --git a/apps/client/src/features/control/playback/PlaybackTimer.tsx b/apps/client/src/features/control/playback/PlaybackTimer.tsx index 9fcfb8d154..2c382eafcf 100644 --- a/apps/client/src/features/control/playback/PlaybackTimer.tsx +++ b/apps/client/src/features/control/playback/PlaybackTimer.tsx @@ -4,12 +4,12 @@ import { Playback } from 'ontime-types'; import TimerDisplay from '../../../common/components/timer-display/TimerDisplay'; import { setPlayback, useTimer } from '../../../common/hooks/useSocket'; import { millisToMinutes } from '../../../common/utils/dateConfig'; -import { stringFromMillis } from '../../../common/utils/time'; import { tooltipDelayMid } from '../../../ontimeConfig'; import TapButton from './TapButton'; import style from './PlaybackControl.module.scss'; +import { millisToString } from 'ontime-utils'; interface PlaybackTimerProps { playback: Playback; @@ -17,20 +17,20 @@ interface PlaybackTimerProps { export default function PlaybackTimer(props: PlaybackTimerProps) { const { playback } = props; - const { data: timerData } = useTimer(); + const data = useTimer(); // TODO: checkout typescript in utilities - const started = stringFromMillis(timerData?.startedAt, true); - const finish = stringFromMillis(timerData.expectedFinish, true); + const started = millisToString(data.timer.startedAt); + const finish = millisToString(data.timer.expectedFinish); const isRolling = playback === Playback.Roll; const isStopped = playback === Playback.Stop; - const isWaiting = timerData.secondaryTimer !== null && timerData.secondaryTimer > 0 && timerData.current === null; + const isWaiting = data.timer.secondaryTimer !== null && data.timer.secondaryTimer > 0 && data.timer.current === null; const disableButtons = isStopped || isRolling; - const isOvertime = timerData.current !== null && timerData.current < 0; - const hasAddedTime = Boolean(timerData.addedTime); + const isOvertime = data.timer.current !== null && data.timer.current < 0; + const hasAddedTime = Boolean(data.timer.addedTime); const rollLabel = isRolling ? 'Roll mode active' : ''; - const addedTimeLabel = hasAddedTime ? `Added ${millisToMinutes(timerData.addedTime)} minutes` : ''; + const addedTimeLabel = hasAddedTime ? `Added ${millisToMinutes(data.timer.addedTime)} minutes` : ''; return (
    @@ -44,7 +44,7 @@ export default function PlaybackTimer(props: PlaybackTimerProps) {
    - +
    {isWaiting ? (
    diff --git a/apps/client/src/features/event-editor/EventEditor.tsx b/apps/client/src/features/event-editor/EventEditor.tsx index 3068d2e512..2c5e185690 100644 --- a/apps/client/src/features/event-editor/EventEditor.tsx +++ b/apps/client/src/features/event-editor/EventEditor.tsx @@ -3,6 +3,7 @@ import { Button, Select, Switch } from '@chakra-ui/react'; import { IoBan } from '@react-icons/all-files/io5/IoBan'; import { useAtom } from 'jotai'; import { OntimeEvent, TimerType } from 'ontime-types'; +import { millisToString } from 'ontime-utils'; import { editorEventId } from '../../common/atoms/LocalEventSettings'; import CopyTag from '../../common/components/copy-tag/CopyTag'; @@ -14,7 +15,6 @@ import useRundown from '../../common/hooks-query/useRundown'; import { useEmitLog } from '../../common/stores/logger'; import { millisToMinutes } from '../../common/utils/dateConfig'; import getDelayTo from '../../common/utils/getDelayTo'; -import { stringFromMillis } from '../../common/utils/time'; import { calculateDuration, TimeEntryField, validateEntry } from '../../common/utils/timesManager'; import style from './EventEditor.module.scss'; @@ -121,8 +121,8 @@ export default function EventEditor() { const delayed = delay !== 0; const addedTime = delayed ? `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))} minutes` : null; - const newStart = delayed ? `New start ${stringFromMillis(event.timeStart + delay)}` : null; - const newEnd = delayed ? `New end ${stringFromMillis(event.timeEnd + delay)}` : null; + const newStart = delayed ? `New start ${millisToString(event.timeStart + delay)}` : null; + const newEnd = delayed ? `New end ${millisToString(event.timeEnd + delay)}` : null; return (
    diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.jsx b/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.jsx index cf5c2a3cca..0d2c24f612 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.jsx +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.jsx @@ -1,11 +1,11 @@ import { useCallback } from 'react'; +import { millisToString } from 'ontime-utils'; import PropTypes from 'prop-types'; import { useEmitLog } from '@/common/stores/logger'; import TimeInput from '../../../../common/components/input/time-input/TimeInput'; import { millisToMinutes } from '../../../../common/utils/dateConfig'; -import { stringFromMillis } from '../../../../common/utils/time'; import { validateEntry } from '../../../../common/utils/timesManager'; import style from '../EventBlock.module.scss'; @@ -15,7 +15,7 @@ export default function EventBlockTimers(props) { const { emitWarning } = useEmitLog(); const delayTime = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`; - const newTime = stringFromMillis(timeStart + delay); + const newTime = millisToString(timeStart + delay); /** * @description Validates a time input against its pair diff --git a/apps/client/src/features/table/columns.jsx b/apps/client/src/features/table/columns.jsx index 52792e9956..081a01a3b4 100644 --- a/apps/client/src/features/table/columns.jsx +++ b/apps/client/src/features/table/columns.jsx @@ -1,10 +1,9 @@ import { FiCheck } from '@react-icons/all-files/fi/FiCheck'; -import { stringFromMillis } from '../../common/utils/time.js'; - import EditableCell from './tableElements/EditableCell'; import style from './Table.module.scss'; +import { millisToString } from 'ontime-utils'; /** * React - Table column object @@ -22,19 +21,19 @@ export const makeColumns = (sizes, userFields) => { { Header: 'Start', accessor: 'timeStart', - Cell: ({ cell: { value, delayed } }) => stringFromMillis(delayed || value), + Cell: ({ cell: { value, delayed } }) => millisToString(delayed || value), width: sizes?.timeStart || 90, }, { Header: 'End', accessor: 'timeEnd', - Cell: ({ cell: { value, delayed } }) => stringFromMillis(delayed || value), + Cell: ({ cell: { value, delayed } }) => millisToString(delayed || value), width: sizes?.timeEnd || 90, }, { Header: 'Duration', accessor: 'duration', - Cell: ({ cell: { value } }) => stringFromMillis(value), + Cell: ({ cell: { value } }) => millisToString(value), width: sizes?.duration || 90, }, { Header: 'Title', accessor: 'title', width: sizes?.title || 400 }, diff --git a/apps/client/src/features/table/utils.js b/apps/client/src/features/table/utils.js index 28d9c5cd54..1d9968e17d 100644 --- a/apps/client/src/features/table/utils.js +++ b/apps/client/src/features/table/utils.js @@ -1,4 +1,5 @@ import { stringify } from 'csv-stringify/browser/esm/sync'; +import { millisToString } from 'ontime-utils'; /** * @description parses a field for export @@ -6,14 +7,13 @@ import { stringify } from 'csv-stringify/browser/esm/sync'; * @param {*} data * @return {string} */ -import { stringFromMillis } from '../../common/utils/time'; export const parseField = (field, data) => { let val; switch (field) { case 'timeStart': case 'timeEnd': - val = stringFromMillis(data); + val = millisToString(data); break; case 'isPublic': val = data ? 'x' : ''; diff --git a/apps/client/src/features/viewers/studio/StudioClock.jsx b/apps/client/src/features/viewers/studio/StudioClock.jsx index fc64b2c9c0..35e323cf1d 100644 --- a/apps/client/src/features/viewers/studio/StudioClock.jsx +++ b/apps/client/src/features/viewers/studio/StudioClock.jsx @@ -10,9 +10,10 @@ import useFitText from '../../../common/hooks/useFitText'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { formatDisplay } from '../../../common/utils/dateConfig'; import { formatEventList, getEventsWithDelay, trimEventlist } from '../../../common/utils/eventsManager'; -import { formatTime, stringFromMillis } from '../../../common/utils/time'; +import { formatTime } from '../../../common/utils/time'; import './StudioClock.scss'; +import { millisToString } from 'ontime-utils'; const formatOptions = { showSeconds: false, @@ -68,7 +69,7 @@ export default function StudioClock(props) { }, [backstageEvents, nextId, selectedId]); const clock = formatTime(time.clock, formatOptions); - const [, , secondsNow] = stringFromMillis(time.clock).split(':'); + const [, , secondsNow] = millisToString(time.clock).split(':'); const isNegative = (time.current ?? 0) < 0; return ( diff --git a/apps/server/src/utils/time.js b/apps/server/src/utils/time.js index e4cedadd35..0ad2124517 100644 --- a/apps/server/src/utils/time.js +++ b/apps/server/src/utils/time.js @@ -26,37 +26,6 @@ export const isTimeString = (string) => { return regex.test(string); }; -/** - * @description Converts milliseconds to string representing time - * @param {number} ms - time in milliseconds - * @param {boolean} showSeconds - weather to show the seconds - * @param {string} delim - character between HH MM SS - * @param {string} ifNull - what to return if value is null - * @returns {string} String representing time 00:12:02 - */ - -export const stringFromMillis = (ms, showSeconds = true, delim = ':', ifNull = '...') => { - if (ms == null || isNaN(ms)) return ifNull; - const isNegative = ms < 0 ? '-' : ''; - const millis = Math.abs(ms); - - /** - * @description ensures value is double digit - * @param value - * @return {string|*} - */ - const showWith0 = (value) => (value < 10 ? `0${value}` : value); - const hours = showWith0(Math.floor(((millis / mth) % 60) % 24)); - const minutes = showWith0(Math.floor((millis / mtm) % 60)); - const seconds = showWith0(Math.floor((millis / mts) % 60)); - - return showSeconds - ? `${isNegative}${ - parseInt(hours, 10) ? `${hours}${delim}` : `00${delim}` - }${minutes}${delim}${seconds}` - : `${isNegative}${parseInt(hours, 10) ? `${hours}` : '00'}${delim}${minutes}`; -}; - /** * @description Converts an excel date to milliseconds * @argument {string} date - excel string date diff --git a/packages/utils/index.ts b/packages/utils/index.ts index f7ea126caa..dbf9e00edf 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -1 +1,2 @@ +export { millisToString } from './src/date-utils/millisToString.js'; export { generateId } from './src/generate-id/generateId.js'; diff --git a/packages/utils/package.json b/packages/utils/package.json index 84ca4d9c04..02774b23e7 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -12,9 +12,11 @@ "cleanup": "rm -rf .turbo && rm -rf node_modules" }, "dependencies": { + "luxon": "^3.3.0", "nanoid": "^4.0.0" }, "devDependencies": { + "@types/luxon": "^3.2.0", "@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/parser": "^5.48.1", "eslint": "^8.31.0", diff --git a/packages/utils/src/date-utils/millisToString.test.ts b/packages/utils/src/date-utils/millisToString.test.ts new file mode 100644 index 0000000000..915d6c71aa --- /dev/null +++ b/packages/utils/src/date-utils/millisToString.test.ts @@ -0,0 +1,71 @@ +import { expect } from 'vitest'; + +import { millisToString } from './millisToString'; + +describe('millisToString()', () => { + it('returns fallback if millis is null', () => { + const fallback = 'testFallback'; + expect(millisToString(null, true, fallback)).toBe(fallback); + }); + + it('returns 00:00:00 if 0 is passed', () => { + expect(millisToString(0)).toBe('00:00:00'); + }); + + it('shows negative timers', () => { + const testScenarios = [ + { millis: -300, expected: '-00:00:00' }, + { millis: -1000, expected: '-00:00:01' }, + { millis: -1500, expected: '-00:00:01' }, + { millis: -60000, expected: '-00:01:00' }, + { millis: -600000, expected: '-00:10:00' }, + { millis: -3600000, expected: '-01:00:00' }, + { millis: -36000000, expected: '-10:00:00' }, + { millis: -86399000, expected: '-23:59:59' }, + { millis: -86400000, expected: '-00:00:00' }, + { millis: -86401000, expected: '-00:00:01' }, + ]; + + testScenarios.forEach((scenario) => { + expect(millisToString(scenario.millis)).toBe(scenario.expected); + }); + }); + + test('random properties', () => { + const testScenarios = [ + { millis: 300, expected: '00:00:00' }, + { millis: 1000, expected: '00:00:01' }, + { millis: 1500, expected: '00:00:01' }, + { millis: 60000, expected: '00:01:00' }, + { millis: 600000, expected: '00:10:00' }, + { millis: 3600000, expected: '01:00:00' }, + { millis: 36000000, expected: '10:00:00' }, + { millis: 86399000, expected: '23:59:59' }, + { millis: 86400000, expected: '00:00:00' }, + { millis: 86401000, expected: '00:00:01' }, + ]; + + testScenarios.forEach((scenario) => { + expect(millisToString(scenario.millis)).toBe(scenario.expected); + }); + }); + + test('random properties without seconds', () => { + const testScenarios = [ + { millis: 300, expected: '00:00' }, + { millis: 1000, expected: '00:00' }, + { millis: 1500, expected: '00:00' }, + { millis: 60000, expected: '00:01' }, + { millis: 600000, expected: '00:10' }, + { millis: 3600000, expected: '01:00' }, + { millis: 36000000, expected: '10:00' }, + { millis: 86399000, expected: '23:59' }, + { millis: 86400000, expected: '00:00' }, + { millis: 86401000, expected: '00:00' }, + ]; + + testScenarios.forEach((scenario) => { + expect(millisToString(scenario.millis, false)).toBe(scenario.expected); + }); + }); +}); diff --git a/packages/utils/src/date-utils/millisToString.ts b/packages/utils/src/date-utils/millisToString.ts new file mode 100644 index 0000000000..7820eba17c --- /dev/null +++ b/packages/utils/src/date-utils/millisToString.ts @@ -0,0 +1,19 @@ +import { DateTime } from 'luxon'; + +/** + * @description Converts milliseconds to string representing time + * @param {number | null} millis - time in milliseconds + * @param {boolean} showSeconds - weather to show the seconds + * @param {string} fallback - what to return if value is null + * @returns {string} String representing time 00:12:02 + */ +export function millisToString(millis: number | null, showSeconds = true, fallback = '...') { + if (millis === null) { + return fallback; + } + + const isNegative = millis < 0; + + const format = `HH:mm${showSeconds ? ':ss' : ''}`; + return `${isNegative ? '-' : ''}${DateTime.fromMillis(Math.abs(millis)).toUTC().toFormat(format)}`; +} From 93783c49aa6ca5a30f42612a7b67eaff7f7aeafe Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Sat, 11 Mar 2023 21:56:48 +0100 Subject: [PATCH 14/20] wip: playback --- .../src/features/control/playback/PlaybackControl.tsx | 2 +- .../src/features/control/playback/PlaybackDisplay.tsx | 7 ++++++- apps/server/src/services/PlaybackService.ts | 10 +++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/client/src/features/control/playback/PlaybackControl.tsx b/apps/client/src/features/control/playback/PlaybackControl.tsx index 398111c7f0..4277e16187 100644 --- a/apps/client/src/features/control/playback/PlaybackControl.tsx +++ b/apps/client/src/features/control/playback/PlaybackControl.tsx @@ -8,7 +8,7 @@ import PlaybackTimer from './PlaybackTimer'; import style from './PlaybackControl.module.scss'; export default function PlaybackControl() { - const { data } = usePlaybackControl(); + const data = usePlaybackControl(); return (
    diff --git a/apps/client/src/features/control/playback/PlaybackDisplay.tsx b/apps/client/src/features/control/playback/PlaybackDisplay.tsx index 01aec4dfeb..d2000022ac 100644 --- a/apps/client/src/features/control/playback/PlaybackDisplay.tsx +++ b/apps/client/src/features/control/playback/PlaybackDisplay.tsx @@ -42,7 +42,12 @@ export default function PlaybackDisplay(props: PlaybackProps) { - setPlayback.roll()} disabled={noEvents} theme={Playback.Roll} active={isRolling}> + setPlayback.roll()} + disabled={!isStopped || noEvents} + theme={Playback.Roll} + active={isRolling} + >
    diff --git a/apps/server/src/services/PlaybackService.ts b/apps/server/src/services/PlaybackService.ts index 0e1705d1b2..ca3929afd7 100644 --- a/apps/server/src/services/PlaybackService.ts +++ b/apps/server/src/services/PlaybackService.ts @@ -119,7 +119,7 @@ export class PlaybackService { * Starts playback on selected event */ static start() { - if (eventTimer.loadedTimerId) { + if (eventTimer.playback === Playback.Armed || eventTimer.playback === Playback.Pause) { eventTimer.start(); const newState = eventTimer.playback; logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`); @@ -130,7 +130,7 @@ export class PlaybackService { * Pauses playback on selected event */ static pause() { - if (eventTimer.loadedTimerId) { + if (eventTimer.playback === Playback.Play) { eventTimer.pause(); const newState = eventTimer.playback; logger.info('PLAYBACK', `Play Mode ${newState.toUpperCase()}`); @@ -141,7 +141,7 @@ export class PlaybackService { * Stops timer and unloads any events */ static stop() { - if (eventTimer.loadedTimerId || eventTimer.playback === Playback.Roll) { + if (eventTimer.playback !== Playback.Stop) { eventLoader.reset(); eventTimer.stop(); const newState = eventTimer.playback; @@ -167,14 +167,14 @@ export class PlaybackService { // nothing to play if (rollTimers === null) { - logger.error('SERVER', 'Roll: no events found'); + logger.warning('SERVER', 'Roll: no events found'); PlaybackService.stop(); return; } const { currentEvent, nextEvent, timers } = rollTimers; if (!currentEvent && !nextEvent) { - logger.error('SERVER', 'Roll: no events found'); + logger.warning('SERVER', 'Roll: no events found'); PlaybackService.stop(); return; } From 1809e65fa7592d52c04a5fb035a9de0923ab9874 Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Sat, 11 Mar 2023 22:10:51 +0100 Subject: [PATCH 15/20] feat: implement websocket --- apps/client/package.json | 4 +- apps/client/src/App.tsx | 5 +- apps/client/src/common/api/apiConstants.ts | 11 +- apps/client/src/common/hooks/useSocket.ts | 185 ++++++++---------- .../src/common/hooks/useSubscription.tsx | 22 --- apps/client/src/common/stores/createStore.ts | 1 + apps/client/src/common/stores/runtime.ts | 77 ++++++++ apps/client/src/common/utils/socket.ts | 151 ++++++++++++-- apps/client/src/common/utils/wss.ts | 15 -- apps/client/src/features/rundown/Rundown.tsx | 2 +- apps/server/src/adapters/WebsocketAdapter.ts | 16 +- apps/server/src/app.ts | 18 +- apps/server/src/classes/Logger.ts | 39 ++-- .../src/controllers/integrationController.ts | 32 +-- apps/server/src/stores/EventStore.ts | 14 +- pnpm-lock.yaml | 25 ++- 16 files changed, 381 insertions(+), 236 deletions(-) delete mode 100644 apps/client/src/common/hooks/useSubscription.tsx create mode 100644 apps/client/src/common/stores/runtime.ts delete mode 100644 apps/client/src/common/utils/wss.ts diff --git a/apps/client/package.json b/apps/client/package.json index 5014c95ab3..cc27befce2 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -18,9 +18,10 @@ "axios": "^1.2.0", "color": "^4.2.3", "csv-stringify": "^6.2.3", + "deepmerge": "^4.3.0", "framer-motion": "^8.0.2", "jotai": "^1.10.0", - "luxon": "^3.1.0", + "luxon": "^3.3.0", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", @@ -63,6 +64,7 @@ "@testing-library/react": "^13.1.1", "@testing-library/user-event": "^14.1.1", "@types/color": "^3.0.3", + "@types/luxon": "^3.2.0", "@types/prop-types": "^15.7.5", "@types/react": "^18.0.26", "@types/react-beautiful-dnd": "^13.1.3", diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index 1a6a6a3883..d2b0c1daa6 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -8,17 +8,18 @@ import ErrorBoundary from './common/components/error-boundary/ErrorBoundary'; import { AppContextProvider } from './common/context/AppContext'; import useElectronEvent from './common/hooks/useElectronEvent'; import { ontimeQueryClient } from './common/queryClient'; +import { connectSocket } from './common/utils/socket'; import theme from './theme/theme'; import AppRouter from './AppRouter'; -// import { useSyncExternalLogger } from './common/stores/logger'; // Load Open Sans typeface // @ts-expect-error no types from font import import('typeface-open-sans'); +connectSocket(); + function App() { const { isElectron, sendToElectron } = useElectronEvent(); - // useSyncExternalLogger(); const handleKeyPress = (event: KeyboardEvent) => { // handle held key diff --git a/apps/client/src/common/api/apiConstants.ts b/apps/client/src/common/api/apiConstants.ts index 287e4d8009..bb77f60913 100644 --- a/apps/client/src/common/api/apiConstants.ts +++ b/apps/client/src/common/api/apiConstants.ts @@ -10,14 +10,7 @@ export const APP_INFO = ['appinfo']; export const OSC_SETTINGS = ['oscSettings']; export const APP_SETTINGS = ['appSettings']; export const VIEW_SETTINGS = ['viewSettings']; - -// websocket stuff -export const FEAT_CUESHEET = 'feat-cuesheet'; -export const FEAT_INFO = 'feat-info'; -export const FEAT_MESSAGECONTROL = 'feat-messagecontrol'; -export const FEAT_PLAYBACKCONTROL = 'feat-playbackcontrol'; -export const FEAT_RUNDOWN = 'feat-rundown'; -export const TIMER = 'timer'; +export const RUNTIME = ['runtimeStore']; /** * @description finds server path given the current location, it @@ -26,6 +19,8 @@ export const TIMER = 'timer'; export const calculateServer = () => (import.meta.env.DEV ? `http://localhost:${STATIC_PORT}` : window.location.origin); export const serverURL = calculateServer(); +export const websocketUrl = `ws://${window.location.hostname}:${STATIC_PORT}/ws`; + export const eventURL = `${serverURL}/eventdata`; export const rundownURL = `${serverURL}/eventlist`; export const ontimeURL = `${serverURL}/ontime`; diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 75b70f787f..56b354fc73 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -1,141 +1,110 @@ -import { useQuery } from '@tanstack/react-query'; -import { Playback } from 'ontime-types'; +import { useMemo } from 'react'; -import { - FEAT_CUESHEET, - FEAT_INFO, - FEAT_MESSAGECONTROL, - FEAT_PLAYBACKCONTROL, - FEAT_RUNDOWN, - TIMER, -} from '../api/apiConstants'; -import { ontimeQueryClient as queryClient } from '../queryClient'; -import socket, { subscribeOnce } from '../utils/socket'; +import { useRuntimeStore } from '../stores/runtime'; +import { socketSendJson } from '../utils/socket'; -function createSocketHook(key: string, defaultValue: T | null = null) { - subscribeOnce(key, (data) => queryClient.setQueryData([key], data)); +export const useRundownEditor = () => { + const state = useRuntimeStore(); - // retrieves data from the cache or null if non-existent - // we need the null because useQuery can't receive undefined - const fetcher = () => (queryClient.getQueryData([key]) ?? defaultValue) as T | null; - - return () => useQuery({ queryKey: [key], queryFn: fetcher, placeholderData: defaultValue }); -} - -interface IRundown { - selectedEventId: string | null; - nextEventId: string | null; - playback: Playback | null; -} - -const emptyRundown: IRundown = { - selectedEventId: null, - nextEventId: null, - playback: null, + return useMemo(() => { + return { + selectedEventId: state.loaded.selectedEventId, + nextEventId: state.loaded.nextEventId, + playback: state.playback, + }; + }, [state.loaded.selectedEventId, state.loaded.nextEventId, state.playback]); }; -export const useRundownEditor = createSocketHook(FEAT_RUNDOWN, emptyRundown); - -const emptyMessageControl = { - messages: { - presenter: { - text: '', - visible: false, - }, - public: { - text: '', - visible: false, - }, - lower: { - text: '', - visible: false, - }, - }, - onAir: false, +export const useMessageControl = () => { + const state = useRuntimeStore(); + return useMemo(() => { + return { + timerMessage: state.timerMessage, + publicMessage: state.publicMessage, + lowerMessage: state.lowerMessage, + onAir: state.onAir, + }; + }, [state.timerMessage, state.publicMessage, state.lowerMessage, state.onAir]); }; -export const useMessageControl = createSocketHook(FEAT_MESSAGECONTROL, emptyMessageControl); export const setMessage = { - presenterText: (payload: string) => socket.emit('set-timer-message-text', payload), - presenterVisible: (payload: boolean) => socket.emit('set-timer-message-visible', payload), - publicText: (payload: string) => socket.emit('set-public-message-text', payload), - publicVisible: (payload: boolean) => socket.emit('set-public-message-visible', payload), - lowerText: (payload: string) => socket.emit('set-lower-message-text', payload), - lowerVisible: (payload: boolean) => socket.emit('set-lower-message-visible', payload), - onAir: (payload: boolean) => socket.emit('set-onAir', payload), + presenterText: (payload: string) => socketSendJson('set-timer-message-text', payload), + presenterVisible: (payload: boolean) => socketSendJson('set-timer-message-visible', payload), + publicText: (payload: string) => socketSendJson('set-public-message-text', payload), + publicVisible: (payload: boolean) => socketSendJson('set-public-message-visible', payload), + lowerText: (payload: string) => socketSendJson('set-lower-message-text', payload), + lowerVisible: (payload: boolean) => socketSendJson('set-lower-message-visible', payload), + onAir: (payload: boolean) => socketSendJson('set-onAir', payload), }; -export const emptyPlaybackControl = { - playback: 'stop', - numEvents: 0, -}; -export const usePlaybackControl = createSocketHook(FEAT_PLAYBACKCONTROL, emptyPlaybackControl); -export const resetPlayback = () => { - const cacheData = queryClient.getQueryData([FEAT_PLAYBACKCONTROL]) as Record; - queryClient.setQueryData([FEAT_PLAYBACKCONTROL], { - ...cacheData, - playback: 'stop', - }); +export const usePlaybackControl = () => { + const state = useRuntimeStore(); + + return useMemo(() => { + return { + playback: state.playback, + numEvents: state.loaded.numEvents, + }; + }, [state.playback, state.loaded.numEvents]); }; + export const setPlayback = { - start: () => socket.emit('set-start'), - pause: () => socket.emit('set-pause'), - roll: () => socket.emit('set-roll'), + start: () => socketSendJson('start'), + pause: () => socketSendJson('pause'), + roll: () => socketSendJson('roll'), previous: () => { - socket.emit('set-previous'); + socketSendJson('previous'); }, next: () => { - socket.emit('set-next'); + socketSendJson('next'); }, stop: () => { - socket.emit('set-stop'); + socketSendJson('stop'); }, reload: () => { - socket.emit('set-reload'); + socketSendJson('reload'); }, delay: (amount: number) => { - socket.emit('set-delay', amount); + socketSendJson('delay', amount); }, }; -export const emptyInfo = { - titles: { - titleNow: '', - subtitleNow: '', - presenterNow: '', - noteNow: '', - titleNext: '', - subtitleNext: '', - presenterNext: '', - noteNext: '', - }, - playback: 'stop', - selectedEventIndex: null, - numEvents: 0, +export const useInfoPanel = () => { + const state = useRuntimeStore(); + + return useMemo(() => { + return { + titles: state.titles, + playback: state.playback, + selectedEventIndex: state.loaded.selectedEventIndex, + numEvents: state.loaded.numEvents, + }; + }, [state.titles, state.playback, state.loaded.selectedEventIndex, state.loaded.numEvents]); }; -export const useInfoPanel = createSocketHook(FEAT_INFO, emptyInfo); +export const useCuesheet = () => { + const state = useRuntimeStore(); -export const emptyCuesheet = { - selectedEventId: null, - titleNow: '', + return useMemo(() => { + return { + selectedEventIndex: state.loaded.selectedEventId, + titleNow: state.titles.titleNow, + }; + }, [state.loaded.selectedEventId, state.titles.titleNow]); }; -export const useCuesheet = createSocketHook(FEAT_CUESHEET, emptyCuesheet); - export const setEventPlayback = { - loadEvent: (eventId: string) => socket.emit('set-loadid', eventId), - startEvent: (eventId: string) => socket.emit('set-startid', eventId), - pause: () => socket.emit('set-pause'), + loadEvent: (eventId: string) => socketSendJson('loadid', eventId), + startEvent: (eventId: string) => socketSendJson('startid', eventId), + pause: () => socketSendJson('pause'), }; -const emptyTimer = { - clock: 0, - current: 0, - secondaryTimer: null, - duration: null, - startedAt: null, - expectedFinish: null, -}; +export const useTimer = () => { + const state = useRuntimeStore(); -export const useTimer = createSocketHook(TIMER, emptyTimer); + return useMemo(() => { + return { + timer: state.timer, + }; + }, [state.timer]); +}; diff --git a/apps/client/src/common/hooks/useSubscription.tsx b/apps/client/src/common/hooks/useSubscription.tsx deleted file mode 100644 index cda26fa183..0000000000 --- a/apps/client/src/common/hooks/useSubscription.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffect, useState } from 'react'; - -import socket from '../utils/socket'; - -export default function useSubscription(topic: string, initialState: T, requestString?: string) { - const [state, setState] = useState(initialState); - - useEffect(() => { - if (requestString) { - socket.emit(requestString); - } else { - socket.emit(`get-${topic}`); - } - socket.on(topic, setState); - - return () => { - socket.off(topic); - }; - }, [requestString, topic]); - - return [state, setState] as const; -}; diff --git a/apps/client/src/common/stores/createStore.ts b/apps/client/src/common/stores/createStore.ts index fe5d4136a3..10df84379e 100644 --- a/apps/client/src/common/stores/createStore.ts +++ b/apps/client/src/common/stores/createStore.ts @@ -6,6 +6,7 @@ export default function createStore(initialState: T) { get: () => currentState, set: (newState: T) => { currentState = newState; + listeners.forEach((listener) => listener(currentState)); }, subscribe: (listener: (state: T) => void) => { listeners.add(listener); diff --git a/apps/client/src/common/stores/runtime.ts b/apps/client/src/common/stores/runtime.ts new file mode 100644 index 0000000000..542dce8006 --- /dev/null +++ b/apps/client/src/common/stores/runtime.ts @@ -0,0 +1,77 @@ +import { useSyncExternalStore } from 'react'; +import { Playback, RuntimeStore } from 'ontime-types'; + +import { RUNTIME } from '../api/apiConstants'; +import { ontimeQueryClient } from '../queryClient'; + +import createStore from './createStore'; + +export const runtimeStorePlaceholder = { + timer: { + clock: 0, + current: null, + elapsed: null, + expectedFinish: null, + addedTime: 0, + startedAt: null, + finishedAt: null, + secondaryTimer: null, + selectedEventId: null, + duration: null, + timerType: null, + }, + playback: Playback.Stop, + timerMessage: { + text: '', + visible: false, + }, + publicMessage: { + text: '', + visible: false, + }, + lowerMessage: { + text: '', + visible: false, + }, + onAir: false, + loaded: { + numEvents: 0, + selectedEventIndex: null, + selectedEventId: null, + selectedPublicEventId: null, + nextEventId: null, + nextPublicEventId: null, + }, + titles: { + titleNow: null, + subtitleNow: null, + presenterNow: null, + noteNow: null, + titleNext: null, + subtitleNext: null, + presenterNext: null, + noteNext: null, + }, + titlesPublic: { + titleNow: null, + subtitleNow: null, + presenterNow: null, + noteNow: null, + titleNext: null, + subtitleNext: null, + presenterNext: null, + noteNext: null, + }, +}; + +export const runtime = createStore(runtimeStorePlaceholder); + +export const useRuntimeStore = () => { + const data = useSyncExternalStore(runtime.subscribe, runtime.get); + + // inject the data to react query to leverage dev tools for debugging + if (import.meta.env.DEV) { + ontimeQueryClient.setQueryData(RUNTIME, data); + } + return data; +}; diff --git a/apps/client/src/common/utils/socket.ts b/apps/client/src/common/utils/socket.ts index 4fff1ad5d3..fb1144270f 100644 --- a/apps/client/src/common/utils/socket.ts +++ b/apps/client/src/common/utils/socket.ts @@ -1,14 +1,143 @@ -const socket = new WebSocket('ws://localhost:4001/ws'); -const subscriptions = new Set(); +import deepmerge from 'deepmerge'; +import { Log } from 'ontime-types'; -export function subscribeOnce(key: string, callback: (data: T) => void, requestString?: string) { - if (subscriptions.has(key)) { - return; - } - subscriptions.add(key); +import { websocketUrl } from '../api/apiConstants'; +import { logger, LOGGER_MAX_MESSAGES } from '../stores/logger'; +import { runtime } from '../stores/runtime'; + +export let websocket: WebSocket | null = null; +let reconnectTimeout: NodeJS.Timeout | null = null; +const reconnectInterval = 1000; +let shouldReconnect = true; + +export const connectSocket = () => { + websocket = new WebSocket(websocketUrl); + + websocket.onopen = () => { + clearTimeout(reconnectTimeout as NodeJS.Timeout); + }; + + websocket.onclose = () => { + console.warn('WebSocket disconnected'); + if (shouldReconnect) { + reconnectTimeout = setTimeout(() => { + console.warn('WebSocket: attempting reconnect'); + if (websocket && websocket.readyState === WebSocket.CLOSED) { + connectSocket(); + } + }, reconnectInterval); + } + }; + + websocket.onerror = (error) => { + console.error('WebSocket error:', error); + }; + + websocket.onmessage = (event) => { + try { + const data = JSON.parse(event.data); - // requestString ? socket.send(requestString) : socket.send(`get-${key}`); - // socket.on(key, callback); -} + const { type, payload } = data; + + if (!type) { + return; + } + + // TODO: implement partial store updates + switch (type) { + case 'ontime-log': { + const state = logger.get(); + state.unshift(payload as Log); + + if (state.length > LOGGER_MAX_MESSAGES) { + state.splice(LOGGER_MAX_MESSAGES + 1, state.length - LOGGER_MAX_MESSAGES - 1); + } + logger.set(state); + break; + } + case 'ontime': { + const storeState = runtime.get(); + const newState = deepmerge(storeState, payload); + runtime.set(newState); + break; + } + case 'ontime-playback': { + const state = runtime.get(); + state.playback = payload; + runtime.set(state); + break; + } + case 'ontime-timer': { + const state = runtime.get(); + state.timer = payload; + runtime.set(state); + break; + } + case 'ontime-loaded': { + const state = runtime.get(); + state.loaded = payload; + runtime.set(state); + break; + } + case 'ontime-titles': { + const state = runtime.get(); + state.titles = payload; + runtime.set(state); + break; + } + case 'ontime-titlesPublic': { + const state = runtime.get(); + state.titlesPublic = payload; + runtime.set(state); + break; + } + case 'ontime-timerMessage': { + const state = runtime.get(); + state.timerMessage = payload; + runtime.set(state); + break; + } + case 'ontime-publicMessage': { + const state = runtime.get(); + state.publicMessage = payload; + runtime.set(state); + break; + } + case 'ontime-lowerMessage': { + const state = runtime.get(); + state.lowerMessage = payload; + runtime.set(state); + break; + } + case 'ontime-onAir': { + const state = runtime.get(); + state.onAir = payload; + runtime.set(state); + break; + } + } + } catch (_) { + // ignore unhandled + } + }; +}; + +export const disconnectSocket = () => { + shouldReconnect = false; + websocket?.close(); +}; + +export const socketSend = (message: any) => { + if (websocket && websocket.readyState === WebSocket.OPEN) { + websocket.send(message); + } +}; -export default socket; +export const socketSendJson = (type: string, payload?: any) => { + socketSend( + JSON.stringify({ + type, + payload, + }), + ); +}; diff --git a/apps/client/src/common/utils/wss.ts b/apps/client/src/common/utils/wss.ts deleted file mode 100644 index 64848291d2..0000000000 --- a/apps/client/src/common/utils/wss.ts +++ /dev/null @@ -1,15 +0,0 @@ -import useWebSocket from 'react-use-websocket'; - -export default function useSocketClient() { - const { sendJsonMessage, lastMessage, lastJsonMessage, readyState } = useWebSocket('_emit:4001', { - share: true, - shouldReconnect: () => true, - onClose: () => console.log('closed socket connection'), - onError: () => console.log('error in socket connection'), - }); - - console.log('debug messages', lastMessage); - console.log('debug JSON messages', lastJsonMessage); - - return { sendJsonMessage, lastJsonMessage, lastMessage, readyState }; -} diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 862341f837..355ef1b1c3 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -23,7 +23,7 @@ interface RundownProps { export default function Rundown(props: RundownProps) { const { entries } = props; - const { data } = useRundownEditor(); + const data = useRundownEditor(); const { cursor, moveCursorUp, moveCursorDown, moveCursorTo, isCursorLocked } = useContext(CursorContext); const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom); const defaultPublic = useAtomValue(defaultPublicAtom); diff --git a/apps/server/src/adapters/WebsocketAdapter.ts b/apps/server/src/adapters/WebsocketAdapter.ts index 7ae071a1d1..04c0531712 100644 --- a/apps/server/src/adapters/WebsocketAdapter.ts +++ b/apps/server/src/adapters/WebsocketAdapter.ts @@ -30,7 +30,7 @@ export class SocketServer implements IAdapter { private wss: WebSocketServer | null; private clientIds: Set; - constructor(server) { + constructor() { if (instance) { throw new Error('There can be only one'); } @@ -38,14 +38,16 @@ export class SocketServer implements IAdapter { // eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton instance = this; this.clientIds = new Set(); + this.wss = null; + } + init(server) { this.wss = new WebSocketServer({ path: '/ws', server }); this.wss.on('connection', (ws) => { const clientId = getRandomName(); this.clientIds.add(clientId); logger.info('RX', `${this.wss.clients.size} Connections with new: ${clientId}`); - console.log('DEBUG OPENED', this.wss.clients.size); // send store payload on connect ws.send( @@ -99,7 +101,7 @@ export class SocketServer implements IAdapter { logger.error('RX', `WS IN: ${error}`); } } catch (_) { - return; + // we ignore unknown } }); }); @@ -107,7 +109,6 @@ export class SocketServer implements IAdapter { // message is any serializable value send(message: any) { - console.log('DEBUG SEND', this.wss); this.wss?.clients.forEach((client) => { if (client !== this.wss && client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(message)); @@ -115,6 +116,9 @@ export class SocketServer implements IAdapter { }); } - // eslint-disable-next-line @typescript-eslint/no-empty-function - shutdown() {} + shutdown() { + this.wss?.close(); + } } + +export const socket = new SocketServer(); diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 2d0689b724..6900f4c164 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -6,6 +6,7 @@ import cors from 'cors'; // import utils import { join, resolve } from 'path'; +import { initSentry, reportSentryException } from './modules/sentry.js'; import { currentDirectory, environment, isProduction, resolvedPath } from './setup.js'; import { ONTIME_VERSION } from './ONTIME_VERSION.js'; import { OSCSettings } from 'ontime-types'; @@ -18,7 +19,7 @@ import { router as playbackRouter } from './routes/playbackRouter.js'; // Import adapters import { OscServer } from './adapters/OscAdapter.js'; -import { SocketServer } from './adapters/WebsocketAdapter.js'; +import { socket } from './adapters/WebsocketAdapter.js'; import { DataProvider } from './classes/data-provider/DataProvider.js'; import { dbLoadingProcess } from './modules/loadDb.js'; @@ -27,6 +28,7 @@ import { eventTimer } from './services/TimerService.js'; import { integrationService } from './services/integration-service/IntegrationService.js'; import { OscIntegration } from './services/integration-service/OscIntegration.js'; import { logger } from './classes/Logger.js'; +import { eventLoader } from './classes/event-loader/EventLoader.js'; console.log(`Starting Ontime version ${ONTIME_VERSION}`); @@ -130,18 +132,11 @@ export const startServer = async () => { expressServer = http.createServer(app); - const socket = new SocketServer(expressServer); + socket.init(expressServer); + eventLoader.init(); expressServer.listen(serverPort, '0.0.0.0'); - logger.init(socket.send); - - let i = 1; - setInterval(() => { - logger.info('RX', `TESTING ${i}`); - i++; - }, 2000); - return returnMessage; }; @@ -204,8 +199,9 @@ export const shutdown = async (exitCode = 0) => { expressServer?.close(); oscServer?.shutdown(); eventTimer.shutdown(); - logger.shutdown(); integrationService.shutdown(); + logger.shutdown(); + socket.shutdown(); process.exit(exitCode); }; diff --git a/apps/server/src/classes/Logger.ts b/apps/server/src/classes/Logger.ts index a8a07b4697..d4318a1271 100644 --- a/apps/server/src/classes/Logger.ts +++ b/apps/server/src/classes/Logger.ts @@ -1,30 +1,21 @@ import { Log, LogLevel } from 'ontime-types'; -import { generateId } from 'ontime-utils'; +import { generateId, millisToString } from 'ontime-utils'; -import { stringFromMillis } from '../utils/time.js'; import { clock } from '../services/Clock.js'; import { isProduction } from '../setup.js'; - -type LogMessage = { - type: 'ontime-log'; - payload: Log; -}; +import { socket } from '../adapters/WebsocketAdapter.js'; class Logger { - private push: (log: LogMessage) => void | null; private queue: Log[]; - constructor(emitCallback?: (message) => void) { - this.push = emitCallback; + constructor() { this.queue = []; } /** * Enabling setup logger after init - * @param emitCallback */ - init(emitCallback: (message) => void) { - this.push = emitCallback; + init() { this.queue.forEach((log) => { this._push(log); }); @@ -47,19 +38,12 @@ class Logger { console.log(`[${log.level}] \t ${log.origin} \t ${log.text}`); } - if (this.push) { - try { - this.push({ - type: 'ontime-log', - payload: log, - }); - console.log('DEBUG FAILED LOGGER SHOULDVE SENT') - - } catch (_e) { - console.log('DEBUG FAILED LOGGER SEND', _e) - this.addToQueue(log); - } - } else { + try { + socket.send({ + type: 'ontime-log', + payload: log, + }); + } catch (_e) { this.addToQueue(log); } } @@ -76,7 +60,7 @@ class Logger { level, origin, text, - time: stringFromMillis(clock.getSystemTime() || 0), + time: millisToString(clock.getSystemTime() || 0), }; this._push(log); } @@ -113,7 +97,6 @@ class Logger { */ shutdown() { console.log('Shutting down logger'); - this.push = null; this.queue = []; } } diff --git a/apps/server/src/controllers/integrationController.ts b/apps/server/src/controllers/integrationController.ts index f75e67c0df..0ad2dc5139 100644 --- a/apps/server/src/controllers/integrationController.ts +++ b/apps/server/src/controllers/integrationController.ts @@ -16,7 +16,7 @@ export function dispatchFromAdapter(type: string, payload: unknown, source?: 'os } case 'set-onair': { - if (payload) { + if (typeof payload !== 'undefined') { messageService.setOnAir(Boolean(payload)); } break; @@ -32,46 +32,46 @@ export function dispatchFromAdapter(type: string, payload: unknown, source?: 'os break; } - case 'timer-message-text': { + case 'set-timer-message-text': { if (typeof payload !== 'string') { - throw new Error('Unable to parse payload'); + throw new Error(`Unable to parse payload: ${payload}`); } messageService.setTimerText(payload); break; } - case 'timer-message-visibility': { - if (!payload) { - throw new Error('Unable to parse payload'); + case 'set-timer-message-visible': { + if (typeof payload === 'undefined') { + throw new Error(`Unable to parse payload: ${payload}`); } messageService.setTimerVisibility(Boolean(payload)); break; } - case 'public-message-text': { + case 'set-public-message-text': { if (typeof payload !== 'string') { - throw new Error('Unable to parse payload'); + throw new Error(`Unable to parse payload: ${payload}`); } messageService.setPublicText(payload); break; } - case 'public-message-visibility': { - if (!payload) { - throw new Error('Unable to parse payload'); + case 'set-public-message-visible': { + if (typeof payload === 'undefined') { + throw new Error(`Unable to parse payload: ${payload}`); } messageService.setPublicVisibility(Boolean(payload)); break; } - case 'lower-message-text': { + case 'set-lower-message-text': { if (typeof payload !== 'string') { - throw new Error('Unable to parse payload'); + throw new Error(`Unable to parse payload: ${payload}`); } messageService.setLowerText(payload); break; } - case 'lower-message-visibility': { - if (!payload) { - throw new Error('Unable to parse payload'); + case 'set-lower-message-visible': { + if (typeof payload === 'undefined') { + throw new Error(`Unable to parse payload: ${payload}`); } messageService.setLowerVisibility(Boolean(payload)); break; diff --git a/apps/server/src/stores/EventStore.ts b/apps/server/src/stores/EventStore.ts index 71b01204b6..f6ba159835 100644 --- a/apps/server/src/stores/EventStore.ts +++ b/apps/server/src/stores/EventStore.ts @@ -1,23 +1,31 @@ import { RuntimeStore } from 'ontime-types'; +import { socket } from '../adapters/WebsocketAdapter.js'; const store: Partial = {}; /** * A runtime store that broadcasts its payload */ -// TODO: misses callback to send stuff export const eventStore = { get(key: T) { return store[key]; }, set(key: T, value: RuntimeStore[T]) { store[key] = value; - // socketProvider.send(key, value); + // TODO: Partial updates seems to cause issues on the client + // socket.send({ + // type: `ontime-${key}`, + // payload: value, + // }); + this.broadcast(); }, poll() { return store; }, broadcast() { - // socketProvider.send(store); + socket.send({ + type: 'ontime', + payload: store, + }); }, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3909684320..f14fd6babd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,7 @@ importers: '@testing-library/react': ^13.1.1 '@testing-library/user-event': ^14.1.1 '@types/color': ^3.0.3 + '@types/luxon': ^3.2.0 '@types/prop-types': ^15.7.5 '@types/react': ^18.0.26 '@types/react-beautiful-dnd': ^13.1.3 @@ -59,6 +60,7 @@ importers: axios: ^1.2.0 color: ^4.2.3 csv-stringify: ^6.2.3 + deepmerge: ^4.3.0 eslint: ^8.31.0 eslint-config-prettier: ^8.6.0 eslint-plugin-jest: ^27.1.7 @@ -70,7 +72,7 @@ importers: framer-motion: ^8.0.2 jotai: ^1.10.0 jsdom: ^21.1.0 - luxon: ^3.1.0 + luxon: ^3.3.0 ontime-types: workspace:* ontime-utils: workspace:* prettier: ^2.8.3 @@ -110,9 +112,10 @@ importers: axios: 1.2.2 color: 4.2.3 csv-stringify: 6.2.3 + deepmerge: 4.3.0 framer-motion: 8.4.3_biqbaboplfbrettd7655fr4n2y jotai: 1.13.0_react@18.2.0 - luxon: 3.2.1 + luxon: 3.3.0 react: 18.2.0 react-beautiful-dnd: 13.1.1_biqbaboplfbrettd7655fr4n2y react-dom: 18.2.0_react@18.2.0 @@ -131,6 +134,7 @@ importers: '@testing-library/react': 13.4.0_biqbaboplfbrettd7655fr4n2y '@testing-library/user-event': 14.4.3 '@types/color': 3.0.3 + '@types/luxon': 3.2.0 '@types/prop-types': 15.7.5 '@types/react': 18.0.26 '@types/react-beautiful-dnd': 13.1.3 @@ -260,19 +264,23 @@ importers: packages/utils: specifiers: + '@types/luxon': ^3.2.0 '@typescript-eslint/eslint-plugin': ^5.48.1 '@typescript-eslint/parser': ^5.48.1 eslint: ^8.31.0 eslint-config-prettier: ^8.6.0 eslint-plugin-prettier: ^4.2.1 eslint-plugin-simple-import-sort: ^8.0.0 + luxon: ^3.3.0 nanoid: ^4.0.0 prettier: ^2.8.3 typescript: ^4.9.4 vitest: ^0.27.1 dependencies: + luxon: 3.3.0 nanoid: 4.0.0 devDependencies: + '@types/luxon': 3.2.0 '@typescript-eslint/eslint-plugin': 5.48.1_3jon24igvnqaqexgwtxk6nkpse '@typescript-eslint/parser': 5.48.1_iukboom6ndih5an6iafl45j2fe eslint: 8.31.0 @@ -2996,6 +3004,10 @@ packages: resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} dev: false + /@types/luxon/3.2.0: + resolution: {integrity: sha512-lGmaGFoaXHuOLXFvuju2bfvZRqxAqkHPx9Y9IQdQABrinJJshJwfNCKV+u7rR3kJbiqfTF/NhOkcxxAFrObyaA==} + dev: true + /@types/mime/3.0.1: resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} dev: true @@ -4413,6 +4425,11 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /deepmerge/4.3.0: + resolution: {integrity: sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==} + engines: {node: '>=0.10.0'} + dev: false + /defer-to-connect/1.1.3: resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} dev: true @@ -6485,8 +6502,8 @@ packages: /lru_map/0.3.3: resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==} - /luxon/3.2.1: - resolution: {integrity: sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==} + /luxon/3.3.0: + resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==} engines: {node: '>=12'} dev: false From 8dbfc667f095115ed77cf4d843e55686959c9ced Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Sat, 11 Mar 2023 22:31:02 +0100 Subject: [PATCH 16/20] refactor: migrate viewwrapper --- apps/client/src/AppRouter.tsx | 18 ++-- .../{ViewWrapper.jsx => ViewWrapper.tsx} | 84 +++++-------------- 2 files changed, 32 insertions(+), 70 deletions(-) rename apps/client/src/features/viewers/{ViewWrapper.jsx => ViewWrapper.tsx} (54%) diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx index 846ed2fd02..d17a4da4fc 100644 --- a/apps/client/src/AppRouter.tsx +++ b/apps/client/src/AppRouter.tsx @@ -2,7 +2,7 @@ import { lazy, useEffect } from 'react'; import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import useAliases from './common/hooks-query/useAliases'; -import withSocket from './features/viewers/ViewWrapper'; +import withData from './features/viewers/ViewWrapper'; const Editor = lazy(() => import('./features/editors/ProtectedEditor')); const Table = lazy(() => import('./features/table/ProtectedTable')); @@ -17,14 +17,14 @@ const Public = lazy(() => import('./features/viewers/public/Public')); const Lower = lazy(() => import('./features/viewers/lower-thirds/LowerWrapper')); const StudioClock = lazy(() => import('./features/viewers/studio/StudioClock')); -const STimer = withSocket(TimerView); -const SMinimalTimer = withSocket(MinimalTimerView); -const SClock = withSocket(ClockView); -const SCountdown = withSocket(Countdown); -const SBackstage = withSocket(Backstage); -const SPublic = withSocket(Public); -const SLowerThird = withSocket(Lower); -const SStudio = withSocket(StudioClock); +const STimer = withData(TimerView); +const SMinimalTimer = withData(MinimalTimerView); +const SClock = withData(ClockView); +const SCountdown = withData(Countdown); +const SBackstage = withData(Backstage); +const SPublic = withData(Public); +const SLowerThird = withData(Lower); +const SStudio = withData(StudioClock); const FeatureWrapper = lazy(() => import('./features/FeatureWrapper')); const RundownPanel = lazy(() => import('./features/rundown/RundownExport')); diff --git a/apps/client/src/features/viewers/ViewWrapper.jsx b/apps/client/src/features/viewers/ViewWrapper.tsx similarity index 54% rename from apps/client/src/features/viewers/ViewWrapper.jsx rename to apps/client/src/features/viewers/ViewWrapper.tsx index 3c01454ae8..88419f3614 100644 --- a/apps/client/src/features/viewers/ViewWrapper.jsx +++ b/apps/client/src/features/viewers/ViewWrapper.tsx @@ -1,68 +1,33 @@ -/* eslint-disable react/display-name */ -import { useEffect, useMemo, useState } from 'react'; +import { ReactNode, useMemo } from 'react'; +import { Playback } from 'ontime-types'; -import { useMessageControl } from '../../common/hooks/useSocket'; -import useSubscription from '../../common/hooks/useSubscription'; import useEventData from '../../common/hooks-query/useEventData'; import useRundown from '../../common/hooks-query/useRundown'; import useViewSettings from '../../common/hooks-query/useViewSettings'; -import socket from '../../common/utils/socket'; +import { useRuntimeStore } from '../../common/stores/runtime'; -const withSocket = (Component) => { +const withData = (Component: ReactNode) => { return (props) => { + + // HTTP API data const { data: eventsData } = useRundown(); const { data: genData } = useEventData(); const { data: viewSettings } = useViewSettings(); - const { data: messageControl } = useMessageControl(); - - const [publicSelectedId, setPublicSelectedId] = useState(null); - - const [timer] = useSubscription('timer', { - clock: null, - current: null, - elapsed: null , - expectedFinish: null, - addedTime: 0, - startedAt: null, - finishedAt: null, - secondaryTimer: null, - }); - const [titles] = useSubscription('titles', { - titleNow: '', - subtitleNow: '', - presenterNow: '', - titleNext: '', - subtitleNext: '', - presenterNext: '', - }); - const [publicTitles] = useSubscription('titlesPublic', { - titleNow: '', - subtitleNow: '', - presenterNow: '', - titleNext: '', - subtitleNext: '', - presenterNext: '', - }); - const [selectedId] = useSubscription('selected-id', null); - const [nextId] = useSubscription('next-id', null); - const [playback] = useSubscription('playback', null); - - // Ask for update on load - useEffect(() => { - // todo: remove - socket.on('publicselected-id', (data) => { - setPublicSelectedId(data); - }); - }, []); - const publicEvents = useMemo(() => { if (Array.isArray(eventsData)) { - return eventsData.filter((d) => d.type === 'event' && d.title !== '' && d.isPublic); + return eventsData.filter((e) => e.type === 'event' && e.title && e.isPublic); } return []; }, [eventsData]); + // websocket data + const data = useRuntimeStore(); + const { timer, titles, titlesPublic, publicMessage, timerMessage, lowerMessage, playback, onAir } = data; + const publicSelectedId = data.loaded.selectedPublicEventId; + const selectedId = data.loaded.selectedEventId; + const nextId = data.loaded.nextEventId; + /********************************************/ /*** + titleManager ***/ /*** WRAP INFORMATION RELATED TO TITLES ***/ @@ -85,16 +50,14 @@ const withSocket = (Component) => { /********************************************/ // is there a now field? let showPublicNow = true; - if (!publicTitles.titleNow && !publicTitles.subtitleNow && !publicTitles.presenterNow) - showPublicNow = false; + if (!titlesPublic.titleNow && !titlesPublic.subtitleNow && !titlesPublic.presenterNow) showPublicNow = false; // is there a next field? let showPublicNext = true; - if (!publicTitles.titleNext && !publicTitles.subtitleNext && !publicTitles.presenterNext) - showPublicNext = false; + if (!titlesPublic.titleNext && !titlesPublic.subtitleNext && !titlesPublic.presenterNext) showPublicNext = false; const publicTitleManager = { - ...publicTitles, + ...titlesPublic, showNow: showPublicNow, showNext: showPublicNext, }; @@ -110,7 +73,7 @@ const withSocket = (Component) => { // get clock string const TimeManagerType = { ...timer, - finished: playback === 'play' && timer.current < 0 && timer.startedAt, + finished: playback === Playback.Play && (timer.current ?? 0) < 0 && timer.startedAt, playback, }; @@ -119,13 +82,12 @@ const withSocket = (Component) => { return null; } - Component.displayName = 'ComponentWithData'; return ( { viewSettings={viewSettings} nextId={nextId} general={genData} - onAir={messageControl.onAir} + onAir={onAir} /> ); }; }; -export default withSocket; +export default withData; From e052b60f91d4c6ec716cc3b8cb757f72a8e06591 Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Sat, 11 Mar 2023 22:35:57 +0100 Subject: [PATCH 17/20] fix: update migration --- apps/server/src/controllers/playbackController.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/server/src/controllers/playbackController.js b/apps/server/src/controllers/playbackController.js index e9f76e68e8..fe71f19c17 100644 --- a/apps/server/src/controllers/playbackController.js +++ b/apps/server/src/controllers/playbackController.js @@ -1,11 +1,10 @@ -// Create controller for GET request to '/playback' -// Returns ACK message import { PlaybackService } from '../services/PlaybackService.js'; +import { eventStore } from '../stores/EventStore.js'; // Create controller for POST request to '/playback' // Returns playback state export const pbGet = async (req, res) => { - res.send({ playback: global.timer.state }); + res.send({ playback: eventStore.get('playback') }); }; // Create controller for POST request to '/playback/start' From 3d3855a76617e5aa90fb84227d9a3bfc5ce44425 Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Sun, 12 Mar 2023 22:15:42 +0100 Subject: [PATCH 18/20] feat: zustand to manage stores --- apps/client/package.json | 3 +- apps/client/src/common/hooks/useSocket.ts | 93 ++++++++++------------- apps/client/src/common/stores/runtime.ts | 26 +++---- apps/client/src/common/utils/socket.ts | 57 +++++++------- pnpm-lock.yaml | 18 +++++ 5 files changed, 98 insertions(+), 99 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index cc27befce2..9a9ca8d298 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -32,7 +32,8 @@ "react-table": "^7.7.0", "react-use-websocket": "^4.3.1", "typeface-open-sans": "^1.1.13", - "web-vitals": "^3.1.1" + "web-vitals": "^3.1.1", + "zustand": "^4.3.6" }, "scripts": { "addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js", diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 56b354fc73..51c31c0324 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -1,30 +1,27 @@ -import { useMemo } from 'react'; +import { RuntimeStore } from 'ontime-types'; -import { useRuntimeStore } from '../stores/runtime'; +import { deepCompare, useRuntimeStore } from '../stores/runtime'; import { socketSendJson } from '../utils/socket'; export const useRundownEditor = () => { - const state = useRuntimeStore(); - - return useMemo(() => { - return { - selectedEventId: state.loaded.selectedEventId, - nextEventId: state.loaded.nextEventId, - playback: state.playback, - }; - }, [state.loaded.selectedEventId, state.loaded.nextEventId, state.playback]); + const featureSelector = (state: RuntimeStore) => ({ + playback: state.playback, + selectedEventId: state.loaded.selectedEventId, + nextEventId: state.loaded.nextEventId, + }); + + return useRuntimeStore(featureSelector, deepCompare); }; export const useMessageControl = () => { - const state = useRuntimeStore(); - return useMemo(() => { - return { - timerMessage: state.timerMessage, - publicMessage: state.publicMessage, - lowerMessage: state.lowerMessage, - onAir: state.onAir, - }; - }, [state.timerMessage, state.publicMessage, state.lowerMessage, state.onAir]); + const featureSelector = (state: RuntimeStore) => ({ + timerMessage: state.timerMessage, + publicMessage: state.publicMessage, + lowerMessage: state.lowerMessage, + onAir: state.onAir, + }); + + return useRuntimeStore(featureSelector, deepCompare); }; export const setMessage = { @@ -38,14 +35,12 @@ export const setMessage = { }; export const usePlaybackControl = () => { - const state = useRuntimeStore(); - - return useMemo(() => { - return { - playback: state.playback, - numEvents: state.loaded.numEvents, - }; - }, [state.playback, state.loaded.numEvents]); + const featureSelector = (state: RuntimeStore) => ({ + playback: state.playback, + numEvents: state.loaded.numEvents, + }); + + return useRuntimeStore(featureSelector, deepCompare); }; export const setPlayback = { @@ -70,27 +65,23 @@ export const setPlayback = { }; export const useInfoPanel = () => { - const state = useRuntimeStore(); - - return useMemo(() => { - return { - titles: state.titles, - playback: state.playback, - selectedEventIndex: state.loaded.selectedEventIndex, - numEvents: state.loaded.numEvents, - }; - }, [state.titles, state.playback, state.loaded.selectedEventIndex, state.loaded.numEvents]); + const featureSelector = (state: RuntimeStore) => ({ + titles: state.titles, + playback: state.playback, + selectedEventIndex: state.loaded.selectedEventIndex, + numEvents: state.loaded.numEvents, + }); + + return useRuntimeStore(featureSelector, deepCompare); }; export const useCuesheet = () => { - const state = useRuntimeStore(); - - return useMemo(() => { - return { - selectedEventIndex: state.loaded.selectedEventId, - titleNow: state.titles.titleNow, - }; - }, [state.loaded.selectedEventId, state.titles.titleNow]); + const featureSelector = (state: RuntimeStore) => ({ + selectedEventIndex: state.loaded.selectedEventId, + titleNow: state.titles.titleNow, + }); + + return useRuntimeStore(featureSelector, deepCompare); }; export const setEventPlayback = { @@ -100,11 +91,9 @@ export const setEventPlayback = { }; export const useTimer = () => { - const state = useRuntimeStore(); + const featureSelector = (state: RuntimeStore) => ({ + timer: state.timer, + }); - return useMemo(() => { - return { - timer: state.timer, - }; - }, [state.timer]); + return useRuntimeStore(featureSelector, deepCompare); }; diff --git a/apps/client/src/common/stores/runtime.ts b/apps/client/src/common/stores/runtime.ts index 542dce8006..bf2f0767b2 100644 --- a/apps/client/src/common/stores/runtime.ts +++ b/apps/client/src/common/stores/runtime.ts @@ -1,10 +1,7 @@ -import { useSyncExternalStore } from 'react'; +import isEqual from 'react-fast-compare'; import { Playback, RuntimeStore } from 'ontime-types'; - -import { RUNTIME } from '../api/apiConstants'; -import { ontimeQueryClient } from '../queryClient'; - -import createStore from './createStore'; +import { useStore } from 'zustand'; +import { createStore } from 'zustand/vanilla'; export const runtimeStorePlaceholder = { timer: { @@ -64,14 +61,13 @@ export const runtimeStorePlaceholder = { }, }; -export const runtime = createStore(runtimeStorePlaceholder); +export const runtime = createStore(() => ({ + ...runtimeStorePlaceholder, +})); -export const useRuntimeStore = () => { - const data = useSyncExternalStore(runtime.subscribe, runtime.get); +export const deepCompare = (a: T, b: T) => isEqual(a, b); - // inject the data to react query to leverage dev tools for debugging - if (import.meta.env.DEV) { - ontimeQueryClient.setQueryData(RUNTIME, data); - } - return data; -}; +export const useRuntimeStore = ( + selector: (state: RuntimeStore) => T, + equalityFn?: (a: unknown, b: unknown) => boolean, +) => useStore(runtime, selector, equalityFn); diff --git a/apps/client/src/common/utils/socket.ts b/apps/client/src/common/utils/socket.ts index fb1144270f..437f3660f8 100644 --- a/apps/client/src/common/utils/socket.ts +++ b/apps/client/src/common/utils/socket.ts @@ -1,8 +1,8 @@ -import deepmerge from 'deepmerge'; import { Log } from 'ontime-types'; -import { websocketUrl } from '../api/apiConstants'; -import { logger, LOGGER_MAX_MESSAGES } from '../stores/logger'; +import { RUNTIME, websocketUrl } from '../api/apiConstants'; +import { ontimeQueryClient } from '../queryClient'; +import { addLog } from '../stores/logger'; import { runtime } from '../stores/runtime'; export let websocket: WebSocket | null = null; @@ -46,73 +46,68 @@ export const connectSocket = () => { // TODO: implement partial store updates switch (type) { case 'ontime-log': { - const state = logger.get(); - state.unshift(payload as Log); - - if (state.length > LOGGER_MAX_MESSAGES) { - state.splice(LOGGER_MAX_MESSAGES + 1, state.length - LOGGER_MAX_MESSAGES - 1); - } - logger.set(state); + addLog(payload as Log); break; } case 'ontime': { - const storeState = runtime.get(); - const newState = deepmerge(storeState, payload); - runtime.set(newState); + runtime.setState(payload); + if (import.meta.env.DEV) { + ontimeQueryClient.setQueryData(RUNTIME, data.payload); + } break; } case 'ontime-playback': { - const state = runtime.get(); + const state = runtime.getState(); state.playback = payload; - runtime.set(state); + runtime.setState(state); break; } case 'ontime-timer': { - const state = runtime.get(); + const state = runtime.getState(); state.timer = payload; - runtime.set(state); + runtime.setState(state); break; } case 'ontime-loaded': { - const state = runtime.get(); + const state = runtime.getState(); state.loaded = payload; - runtime.set(state); + runtime.setState(state); break; } case 'ontime-titles': { - const state = runtime.get(); + const state = runtime.getState(); state.titles = payload; - runtime.set(state); + runtime.setState(state); break; } case 'ontime-titlesPublic': { - const state = runtime.get(); + const state = runtime.getState(); state.titlesPublic = payload; - runtime.set(state); + runtime.setState(state); break; } case 'ontime-timerMessage': { - const state = runtime.get(); + const state = runtime.getState(); state.timerMessage = payload; - runtime.set(state); + runtime.setState(state); break; } case 'ontime-publicMessage': { - const state = runtime.get(); + const state = runtime.getState(); state.publicMessage = payload; - runtime.set(state); + runtime.setState(state); break; } case 'ontime-lowerMessage': { - const state = runtime.get(); + const state = runtime.getState(); state.lowerMessage = payload; - runtime.set(state); + runtime.setState(state); break; } case 'ontime-onAir': { - const state = runtime.get(); + const state = runtime.getState(); state.onAir = payload; - runtime.set(state); + runtime.setState(state); break; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f14fd6babd..3247152526 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,7 @@ importers: vite-plugin-svgr: ^2.4.0 vite-tsconfig-paths: ^4.0.3 web-vitals: ^3.1.1 + zustand: ^4.3.6 dependencies: '@chakra-ui/react': 2.4.8_loo4skotrnm7icurwgkplqpnwq '@dnd-kit/core': 6.0.7_biqbaboplfbrettd7655fr4n2y @@ -127,6 +128,7 @@ importers: react-use-websocket: 4.3.1_biqbaboplfbrettd7655fr4n2y typeface-open-sans: 1.1.13 web-vitals: 3.1.1 + zustand: 4.3.6_react@18.2.0 devDependencies: '@sentry/vite-plugin': 0.3.0 '@tanstack/eslint-plugin-query': 4.21.0 @@ -9344,3 +9346,19 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + /zustand/4.3.6_react@18.2.0: + resolution: {integrity: sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==} + engines: {node: '>=12.7.0'} + peerDependencies: + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + immer: + optional: true + react: + optional: true + dependencies: + react: 18.2.0 + use-sync-external-store: 1.2.0_react@18.2.0 + dev: false From 76ead2037815ea9de2cab4914481344958a3e7ab Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Wed, 15 Mar 2023 20:56:28 +0100 Subject: [PATCH 19/20] refactor: cleanup logging store --- apps/client/src/common/stores/logger.ts | 46 ++-- .../src/features/info/CollapsableInfo.tsx | 35 +-- apps/client/src/features/info/Info.tsx | 21 +- .../src/features/info/InfoLogger.module.scss | 8 +- apps/client/src/features/info/InfoLogger.tsx | 220 +++++++++--------- apps/client/src/features/info/InfoNif.tsx | 22 +- apps/client/src/features/info/InfoTitles.tsx | 36 +++ 7 files changed, 189 insertions(+), 199 deletions(-) create mode 100644 apps/client/src/features/info/InfoTitles.tsx diff --git a/apps/client/src/common/stores/logger.ts b/apps/client/src/common/stores/logger.ts index 456cff7f7e..dd57139c19 100644 --- a/apps/client/src/common/stores/logger.ts +++ b/apps/client/src/common/stores/logger.ts @@ -1,32 +1,37 @@ -import { useCallback, useSyncExternalStore } from 'react'; +import { useCallback } from 'react'; import { Log, LogLevel } from 'ontime-types'; import { generateId, millisToString } from 'ontime-utils'; +import { useStore } from 'zustand'; +import { createStore } from 'zustand/vanilla'; import { socketSendJson } from '../utils/socket'; import { nowInMillis } from '../utils/time'; -import createStore from './createStore'; +type LogStore = { + logs: Log[]; +}; -export const logger = createStore([]); -export const LOGGER_MAX_MESSAGES = 100; +export const logger = createStore(() => ({ + logs: [], +})); -export function useEmitLog() { - const _addToLogger = (log: Log) => { - const state = logger.get(); - state.push(log); - if (state.length > LOGGER_MAX_MESSAGES) { - state.slice(1); - } - logger.set(state); - }; +export const useLogData = () => useStore(logger); +export const addLog = (log: Log) => + logger.setState((state) => ({ + logs: [...state.logs, log], + })); + +export const clearLogs = () => logger.setState({ logs: [] }); + +export function useEmitLog() { /** * Utility function sends message over socket * @param text * @param level * @private */ - const _emit = (text: string, level: LogLevel) => { + const _emit = useCallback((text: string, level: LogLevel) => { const log = { id: generateId(), origin: 'CLIENT', @@ -35,9 +40,9 @@ export function useEmitLog() { text, }; - _addToLogger(log); + addLog(log); socketSendJson('ontime-log', log); - }; + }, []); /** * Sends a message with level INFO @@ -72,18 +77,9 @@ export function useEmitLog() { [_emit], ); - const clearLog = useCallback(() => { - logger.set([]); - }, []); - return { emitInfo, emitWarning, emitError, - clearLog, }; } - -export const useLogData = () => { - return useSyncExternalStore(logger.subscribe, () => logger.get()); -}; diff --git a/apps/client/src/features/info/CollapsableInfo.tsx b/apps/client/src/features/info/CollapsableInfo.tsx index f076a6343b..207052c4b4 100644 --- a/apps/client/src/features/info/CollapsableInfo.tsx +++ b/apps/client/src/features/info/CollapsableInfo.tsx @@ -1,48 +1,21 @@ -import { useState } from 'react'; +import { PropsWithChildren, useState } from 'react'; import CollapseBar from '../../common/components/collapse-bar/CollapseBar'; import style from './Info.module.scss'; -type TitleShape = { - title: string; - presenter: string; - subtitle: string; - note: string; -}; - interface CollapsableInfoProps { title: string; - data: TitleShape; } -export default function CollapsableInfo(props: CollapsableInfoProps) { - const { title, data } = props; +export default function CollapsableInfo(props: PropsWithChildren) { + const { title, children } = props; const [collapsed, setCollapsed] = useState(false); return (
    setCollapsed((prev) => !prev)} /> - {!collapsed && ( -
    -
    - Title: - {data.title} -
    -
    - Presenter: - {data.presenter} -
    -
    - Subtitle: - {data.subtitle} -
    -
    - Note: - {data.note} -
    -
    - )} + {!collapsed && children}
    ); } diff --git a/apps/client/src/features/info/Info.tsx b/apps/client/src/features/info/Info.tsx index 7201dea9ff..406ed85c6a 100644 --- a/apps/client/src/features/info/Info.tsx +++ b/apps/client/src/features/info/Info.tsx @@ -1,8 +1,9 @@ import { useInfoPanel } from '../../common/hooks/useSocket'; -import InfoTitle from './CollapsableInfo'; +import CollapsableInfo from './CollapsableInfo'; import InfoLogger from './InfoLogger'; import InfoNif from './InfoNif'; +import InfoTitles from './InfoTitles'; import style from './Info.module.scss'; @@ -25,7 +26,7 @@ export default function Info() { const selected = !data.numEvents ? 'No events' - : `Event ${data.selectedEventIndex != null ? data.selectedEventIndex + 1 : '-'} / ${ + : `Event ${data.selectedEventIndex !== null ? data.selectedEventIndex + 1 : '-'} / ${ data.numEvents ? data.numEvents : '-' }`; @@ -35,10 +36,18 @@ export default function Info() { Ontime running on port 4001 {selected}
    - - - - + + + + + + + + + + + + ); } diff --git a/apps/client/src/features/info/InfoLogger.module.scss b/apps/client/src/features/info/InfoLogger.module.scss index d1e86e4a9a..33ba388606 100644 --- a/apps/client/src/features/info/InfoLogger.module.scss +++ b/apps/client/src/features/info/InfoLogger.module.scss @@ -6,12 +6,7 @@ $info-hover: $section-white; .infoLoggerContainer { max-height: 80%; - margin-top: 32px; - - &.expanded { - min-height: 50%; - height: 100% - } + height: 100% } .log { @@ -24,6 +19,7 @@ $info-hover: $section-white; .logEntry { display: flex; margin-bottom: 2px; + &.INFO { color: $info-gray; } diff --git a/apps/client/src/features/info/InfoLogger.tsx b/apps/client/src/features/info/InfoLogger.tsx index e7d8d640f2..5abab2adab 100644 --- a/apps/client/src/features/info/InfoLogger.tsx +++ b/apps/client/src/features/info/InfoLogger.tsx @@ -1,29 +1,22 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; import { Button } from '@chakra-ui/react'; -import { Log } from 'ontime-types'; -import CollapseBar from '../../common/components/collapse-bar/CollapseBar'; -import { useEmitLog, useLogData } from '../../common/stores/logger'; +import { clearLogs, useLogData } from '../../common/stores/logger'; import style from './InfoLogger.module.scss'; -enum LOG_FILTER { - USER = 'USER', - CLIENT = 'CLIENT', - SERVER = 'SERVER', +enum LogFilter { + User = 'USER', + Client = 'CLIENT', + Server = 'SERVER', RX = 'RX', TX = 'TX', - PLAYBACK = 'PLAYBACK', + Playback = 'PLAYBACK', } export default function InfoLogger() { - const logData = useLogData(); - const { clearLog } = useEmitLog(); - // const { logData, clearLog } = useContext(LoggingContext); - // TODO: derived data shouldn't be in state - const data: Log[] = []; - const setData = (newData) => console.log('tried setting data'); - const [collapsed, setCollapsed] = useState(false); + const { logs: logData } = useLogData(); + const [showClient, setShowClient] = useState(true); const [showServer, setShowServer] = useState(true); const [showRx, setShowRx] = useState(true); @@ -31,112 +24,107 @@ export default function InfoLogger() { const [showPlayback, setShowPlayback] = useState(true); const [showUser, setShowUser] = useState(true); - const matchers: LOG_FILTER[] = []; - if (showUser) { - matchers.push(LOG_FILTER.USER); - } - if (showClient) { - matchers.push(LOG_FILTER.CLIENT); - } - if (showServer) { - matchers.push(LOG_FILTER.SERVER); - } - if (showRx) { - matchers.push(LOG_FILTER.RX); - } - if (showTx) { - matchers.push(LOG_FILTER.TX); - } - if (showPlayback) { - matchers.push(LOG_FILTER.PLAYBACK); - } + const matchers: LogFilter[] = []; + if (showUser) { + matchers.push(LogFilter.User); + } + if (showClient) { + matchers.push(LogFilter.Client); + } + if (showServer) { + matchers.push(LogFilter.Server); + } + if (showRx) { + matchers.push(LogFilter.RX); + } + if (showTx) { + matchers.push(LogFilter.TX); + } + if (showPlayback) { + matchers.push(LogFilter.Playback); + } const filteredData = logData.filter((entry) => matchers.some((match) => entry.origin === match)); - const disableOthers = useCallback((toEnable: LOG_FILTER) => { - toEnable === LOG_FILTER.USER ? setShowUser(true) : setShowUser(false); - toEnable === LOG_FILTER.CLIENT ? setShowClient(true) : setShowClient(false); - toEnable === LOG_FILTER.SERVER ? setShowServer(true) : setShowServer(false); - toEnable === LOG_FILTER.RX ? setShowRx(true) : setShowRx(false); - toEnable === LOG_FILTER.TX ? setShowTx(true) : setShowTx(false); - toEnable === LOG_FILTER.PLAYBACK ? setShowPlayback(true) : setShowPlayback(false); + const disableOthers = useCallback((toEnable: LogFilter) => { + toEnable === LogFilter.User ? setShowUser(true) : setShowUser(false); + toEnable === LogFilter.Client ? setShowClient(true) : setShowClient(false); + toEnable === LogFilter.Server ? setShowServer(true) : setShowServer(false); + toEnable === LogFilter.RX ? setShowRx(true) : setShowRx(false); + toEnable === LogFilter.TX ? setShowTx(true) : setShowTx(false); + toEnable === LogFilter.Playback ? setShowPlayback(true) : setShowPlayback(false); }, []); return ( -
    - setCollapsed((prev) => !prev)} /> - {!collapsed && ( - <> -
    - - - - - - - -
    -
      - {filteredData.map((logEntry) => ( -
    • - {logEntry.time} - {logEntry.origin} - {logEntry.text} -
    • - ))} -
    - - )} +
    +
    + + + + + + + +
    +
      + {filteredData.map((logEntry) => ( +
    • + {logEntry.time} + {logEntry.origin} + {logEntry.text} +
    • + ))} +
    ); } diff --git a/apps/client/src/features/info/InfoNif.tsx b/apps/client/src/features/info/InfoNif.tsx index 907c5c04ce..3b33aba7cb 100644 --- a/apps/client/src/features/info/InfoNif.tsx +++ b/apps/client/src/features/info/InfoNif.tsx @@ -1,7 +1,5 @@ -import { useState } from 'react'; import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp'; -import CollapseBar from '../../common/components/collapse-bar/CollapseBar'; import useInfo from '../../common/hooks-query/useInfo'; import { openLink } from '../../common/utils/linkUtils'; @@ -9,7 +7,6 @@ import style from './Info.module.scss'; export default function InfoNif() { const { data } = useInfo(); - const [collapsed, setCollapsed] = useState(false); const handleClick = (address: string) => { const baseURL = 'http://__IP__:4001'; @@ -17,18 +14,13 @@ export default function InfoNif() { }; return ( -
    - setCollapsed((prev) => !prev)} /> - {!collapsed && ( -
    - {data?.networkInterfaces.map((nif) => ( - handleClick(nif.address)} className={style.interface}> - {`${nif.name} - ${nif.address}`} - - - ))} -
    - )} +
    + {data?.networkInterfaces.map((nif) => ( + handleClick(nif.address)} className={style.interface}> + {`${nif.name} - ${nif.address}`} + + + ))}
    ); } diff --git a/apps/client/src/features/info/InfoTitles.tsx b/apps/client/src/features/info/InfoTitles.tsx new file mode 100644 index 0000000000..20b7bdb39d --- /dev/null +++ b/apps/client/src/features/info/InfoTitles.tsx @@ -0,0 +1,36 @@ +import style from './Info.module.scss'; + +type TitleShape = { + title: string; + presenter: string; + subtitle: string; + note: string; +}; + +interface InfoTitleProps { + data: TitleShape; +} + +export default function InfoTitles(props: InfoTitleProps) { + const { data } = props; + return ( +
    +
    + Title: + {data.title} +
    +
    + Presenter: + {data.presenter} +
    +
    + Subtitle: + {data.subtitle} +
    +
    + Note: + {data.note} +
    +
    + ); +} From 35ae34040593929f5eb36e1209d30478a3f2aa91 Mon Sep 17 00:00:00 2001 From: cv <34649812+cpvalente@users.noreply.github.com> Date: Wed, 15 Mar 2023 21:43:08 +0100 Subject: [PATCH 20/20] fix: adding blocks and delays after events --- .../client/src/common/hooks/useEventAction.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/client/src/common/hooks/useEventAction.ts b/apps/client/src/common/hooks/useEventAction.ts index 0a9bd6ef8f..9a447a1543 100644 --- a/apps/client/src/common/hooks/useEventAction.ts +++ b/apps/client/src/common/hooks/useEventAction.ts @@ -39,23 +39,24 @@ export const useEventAction = () => { networkMode: 'always', }); - type AddOptions = { + type BaseOptions = { + after?: string; + }; + + type EventOptions = BaseOptions & { defaultPublic?: boolean; - startTimeIsLastEnd?: boolean; lastEventId?: string; - after?: string; + startTimeIsLastEnd?: boolean; }; /** * Adds an event to rundown */ const addEvent = useCallback( - async (event: Partial, options?: AddOptions) => { + async (event: Partial, options?: EventOptions) => { const newEvent: Partial = { ...event }; - // ************* CHECK OPTIONS - // there is an option to pass an index of an array to use as start time - // only events have options + // ************* CHECK OPTIONS specific to events if (newEvent.type === SupportedEvent.Event) { const applicationOptions = { defaultPublic: options?.defaultPublic ?? defaultPublic, @@ -81,13 +82,15 @@ export const useEventAction = () => { if (applicationOptions.defaultPublic) { newEvent.isPublic = true; } + } - if (applicationOptions?.after) { - newEvent.after = applicationOptions.after; - } + // handle adding options that concern all event type + if (options?.after) { + newEvent.after = options.after; } try { + // @ts-expect-error -- we know that the object is well formed now await _addEventMutation.mutateAsync(newEvent); } catch (error) { if (!axios.isAxiosError(error)) {