diff --git a/apps/web-portal-client/lib/api/index.jsx b/apps/web-portal-client/lib/api/index.jsx index 24f12c95..c5a6e87d 100644 --- a/apps/web-portal-client/lib/api/index.jsx +++ b/apps/web-portal-client/lib/api/index.jsx @@ -1,3 +1,6 @@ -import {createClient} from '@util-web-api-client' +import {ClientTypes, createClient} from '@util-web-api-client' -export const client = createClient() +export const client = createClient({ + type: ClientTypes.Axios, + options: {baseURL: 'http://localhost:7777'}, +}) diff --git a/apps/web-portal-client/lib/app/AppContainer.jsx b/apps/web-portal-client/lib/app/AppContainer.jsx index eee099c2..9bbe8a6b 100644 --- a/apps/web-portal-client/lib/app/AppContainer.jsx +++ b/apps/web-portal-client/lib/app/AppContainer.jsx @@ -1,6 +1,5 @@ import React from 'react' import {Switch, Route} from 'react-router-dom' - import {App} from './App' import {LoginPage, PrivateRoute} from '../features/auth/containers' diff --git a/apps/web-portal-client/lib/components/Button.jsx b/apps/web-portal-client/lib/components/Button.jsx index 0ca63fa5..03d46371 100644 --- a/apps/web-portal-client/lib/components/Button.jsx +++ b/apps/web-portal-client/lib/components/Button.jsx @@ -2,7 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import className from 'classnames' -import {childrenPropTypes} from '../utils/childrenPropTypes' +import {childrenPropTypes} from '../utils' import './button.css' diff --git a/apps/web-portal-client/lib/components/ErrorBoundary.jsx b/apps/web-portal-client/lib/components/ErrorBoundary.jsx new file mode 100644 index 00000000..966781bb --- /dev/null +++ b/apps/web-portal-client/lib/components/ErrorBoundary.jsx @@ -0,0 +1,40 @@ +import React, {Component} from 'react' +import {AppContext} from '../context' +import {childrenPropTypes} from '../utils' + +export class ErrorBoundary extends Component { + constructor(props) { + super(props) + this.state = {hasError: false} + } + + static getDerivedStateFromError() { + return {hasError: true} + } + + componentDidCatch(error) { + const {logger} = this.context + logger.error({message: error.message, stack: JSON.stringify(error.stack)}) + } + + render() { + const {hasError} = this.state + const {children} = this.props + + if (hasError) { + return

Something went wrong.

+ } + + return children + } +} + +ErrorBoundary.contextType = AppContext + +ErrorBoundary.defaultProps = { + children: childrenPropTypes, +} + +ErrorBoundary.propTypes = { + children: null, +} diff --git a/apps/web-portal-client/lib/components/Popover.jsx b/apps/web-portal-client/lib/components/Popover.jsx index 5d78da57..8ecdc1cc 100644 --- a/apps/web-portal-client/lib/components/Popover.jsx +++ b/apps/web-portal-client/lib/components/Popover.jsx @@ -4,7 +4,7 @@ import Tippy from '@tippy.js/react' import 'tippy.js/dist/tippy.css' import 'tippy.js/themes/light-border.css' -import {childrenPropTypes} from '../utils/childrenPropTypes' +import {childrenPropTypes} from '../utils' export function Popover({title, text, duration, delay, children}) { const [visible, setVisible] = useState(false) diff --git a/apps/web-portal-client/lib/components/dialog/ConfirmDialog.jsx b/apps/web-portal-client/lib/components/dialog/ConfirmDialog.jsx index 93dd4698..f27cd0b8 100644 --- a/apps/web-portal-client/lib/components/dialog/ConfirmDialog.jsx +++ b/apps/web-portal-client/lib/components/dialog/ConfirmDialog.jsx @@ -1,6 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' -import {childrenPropTypes} from '../../utils/childrenPropTypes' +import {childrenPropTypes} from '../../utils' import {Dialog} from './Dialog' import {Button} from '../Button' diff --git a/apps/web-portal-client/lib/components/dialog/Dialog.jsx b/apps/web-portal-client/lib/components/dialog/Dialog.jsx index 851c3a20..65e71042 100644 --- a/apps/web-portal-client/lib/components/dialog/Dialog.jsx +++ b/apps/web-portal-client/lib/components/dialog/Dialog.jsx @@ -1,6 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' -import {childrenPropTypes} from '../../utils/childrenPropTypes' +import {childrenPropTypes} from '../../utils' import {Button} from '../Button' import {Image, ImageShapes} from '../Image' diff --git a/apps/web-portal-client/lib/components/layout/main-menu/DesktopMenu.jsx b/apps/web-portal-client/lib/components/layout/main-menu/DesktopMenu.jsx index 15db5a9d..76a07c5d 100644 --- a/apps/web-portal-client/lib/components/layout/main-menu/DesktopMenu.jsx +++ b/apps/web-portal-client/lib/components/layout/main-menu/DesktopMenu.jsx @@ -1,5 +1,5 @@ import React from 'react' -import {childrenPropTypes} from '../../../utils/childrenPropTypes' +import {childrenPropTypes} from '../../../utils' export function DesktopMenu({children}) { return ( diff --git a/apps/web-portal-client/lib/components/layout/main-menu/MobileMenu.jsx b/apps/web-portal-client/lib/components/layout/main-menu/MobileMenu.jsx index 50dc2e06..76e2bc60 100644 --- a/apps/web-portal-client/lib/components/layout/main-menu/MobileMenu.jsx +++ b/apps/web-portal-client/lib/components/layout/main-menu/MobileMenu.jsx @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' import className from 'classnames' -import {childrenPropTypes} from '../../../utils/childrenPropTypes' +import {childrenPropTypes} from '../../../utils' export function MobileMenu({isShown, children}) { return ( diff --git a/apps/web-portal-client/lib/components/layout/sidebar/Sidebar.jsx b/apps/web-portal-client/lib/components/layout/sidebar/Sidebar.jsx index 7e00bf5d..cd17db38 100644 --- a/apps/web-portal-client/lib/components/layout/sidebar/Sidebar.jsx +++ b/apps/web-portal-client/lib/components/layout/sidebar/Sidebar.jsx @@ -1,6 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' -import {childrenPropTypes} from '../../../utils/childrenPropTypes' +import {childrenPropTypes} from '../../../utils' export function Sidebar({title, children}) { return ( diff --git a/apps/web-portal-client/lib/context/AppContext.js b/apps/web-portal-client/lib/context/AppContext.js index 9e7bdf93..8a4454c8 100644 --- a/apps/web-portal-client/lib/context/AppContext.js +++ b/apps/web-portal-client/lib/context/AppContext.js @@ -2,4 +2,5 @@ import {createContext} from 'react' export const AppContext = createContext({ api: null, + logger: null, }) diff --git a/apps/web-portal-client/lib/context/AppProvider.js b/apps/web-portal-client/lib/context/AppProvider.js index 8b363173..e3867de1 100644 --- a/apps/web-portal-client/lib/context/AppProvider.js +++ b/apps/web-portal-client/lib/context/AppProvider.js @@ -1,11 +1,12 @@ import React from 'react' import PropTypes from 'prop-types' import {client} from '../api' +import {logger} from '../logger' import {AppContext} from './AppContext' export function AppProvider(props) { const {children} = props - return {children} + return {children} } AppProvider.propTypes = { diff --git a/apps/web-portal-client/lib/features/examples/components/ValidatorExample.jsx b/apps/web-portal-client/lib/features/examples/components/ValidatorExample.jsx index 67dcd1f9..f987f646 100644 --- a/apps/web-portal-client/lib/features/examples/components/ValidatorExample.jsx +++ b/apps/web-portal-client/lib/features/examples/components/ValidatorExample.jsx @@ -21,6 +21,5 @@ export function ValidatorExample() { const validator = createValidator(rules) const {error, result} = validator.parseSync(value) - return !error ?
{JSON.stringify(result)}
:
{error.message}
} diff --git a/apps/web-portal-client/lib/index.jsx b/apps/web-portal-client/lib/index.jsx index 0d6dbc9c..10ca363f 100644 --- a/apps/web-portal-client/lib/index.jsx +++ b/apps/web-portal-client/lib/index.jsx @@ -5,13 +5,16 @@ import {Provider} from 'react-redux' import {AppProvider} from './context' import store from './store' import {AppContainer} from './app/AppContainer' +import {ErrorBoundary} from './components/ErrorBoundary' import './index.css' ReactDOM.render( - + + + , diff --git a/apps/web-portal-client/lib/logger/index.jsx b/apps/web-portal-client/lib/logger/index.jsx new file mode 100644 index 00000000..27c9ef26 --- /dev/null +++ b/apps/web-portal-client/lib/logger/index.jsx @@ -0,0 +1,19 @@ +import {createLogger} from '@zorko-io/util-logger' +import {MessageQueue} from '../utils' +import {client} from '../api' + +const LOGS_MAX_SIZE = 10 +const messageQueue = new MessageQueue(LOGS_MAX_SIZE) + +export const logger = createLogger({ + context: { + browser: { + write: async (message) => { + messageQueue.push(message) + if (message.level >= 50) { + await client.log.save(messageQueue.messages) + } + }, + }, + }, +}) diff --git a/apps/web-portal-client/lib/store/index.js b/apps/web-portal-client/lib/store/index.js index 013a921a..395fdd63 100644 --- a/apps/web-portal-client/lib/store/index.js +++ b/apps/web-portal-client/lib/store/index.js @@ -1,8 +1,15 @@ import {configureStore} from '@reduxjs/toolkit' +import {logger} from '../logger' import authReducer from '../features/auth/slices' +const loggerMiddleware = () => (next) => (action) => { + logger.info(action) + return next(action) +} + export default configureStore({ reducer: { auth: authReducer, }, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(loggerMiddleware), }) diff --git a/apps/web-portal-client/lib/utils/MessageQueue.mjs b/apps/web-portal-client/lib/utils/MessageQueue.mjs new file mode 100644 index 00000000..fd123008 --- /dev/null +++ b/apps/web-portal-client/lib/utils/MessageQueue.mjs @@ -0,0 +1,34 @@ +export class MessageQueue { + #messages = [] + + constructor(size) { + this.size = size + } + + enqueue(message) { + this.#messages.push(message) + } + + dequeue() { + this.#messages.shift() + } + + get isEmpty() { + return !!this.#messages.length + } + + get queueLength() { + return this.#messages.length + } + + push(message) { + if (!this.isEmpty && this.queueLength >= this.size) { + this.dequeue() + } + this.enqueue(message) + } + + get messages() { + return this.#messages + } +} diff --git a/apps/web-portal-client/lib/utils/index.mjs b/apps/web-portal-client/lib/utils/index.mjs new file mode 100644 index 00000000..3be3229c --- /dev/null +++ b/apps/web-portal-client/lib/utils/index.mjs @@ -0,0 +1,2 @@ +export * from './childrenPropTypes' +export * from './MessageQueue' diff --git a/apps/web-portal/lib/rest-api-v1/index.mjs b/apps/web-portal/lib/rest-api-v1/index.mjs index e5d9398b..5864716a 100644 --- a/apps/web-portal/lib/rest-api-v1/index.mjs +++ b/apps/web-portal/lib/rest-api-v1/index.mjs @@ -1,5 +1,6 @@ import preview from './preview' import auth from './auth' +import log from './log' // TODO: Provide helpers/utilities for Rest API // - jsdocs @@ -10,9 +11,11 @@ export function route(deps) { const router = deps.createRouter() const previewController = preview(deps) const authController = auth(deps) + const logController = log(deps) router.get('/previews', previewController.list) router.post('/auth/login', authController.login) + router.post('/log', logController.save) return router } diff --git a/apps/web-portal/lib/rest-api-v1/log.mjs b/apps/web-portal/lib/rest-api-v1/log.mjs new file mode 100644 index 00000000..58d5f63c --- /dev/null +++ b/apps/web-portal/lib/rest-api-v1/log.mjs @@ -0,0 +1,9 @@ +import {LogSave} from '../use-cases/log' + +export default ({makeRunner}) => { + return { + save: makeRunner(LogSave, { + toParams: (req) => ({...req.body}), + }), + } +} diff --git a/apps/web-portal/lib/use-cases/log/LogSave.mjs b/apps/web-portal/lib/use-cases/log/LogSave.mjs new file mode 100644 index 00000000..c30543e3 --- /dev/null +++ b/apps/web-portal/lib/use-cases/log/LogSave.mjs @@ -0,0 +1,11 @@ +import {UseCase} from '@zorko-io/util-use-case' + +// TODO: add integration tests for api/v1/log endpoint +export class LogSave extends UseCase { + // eslint-disable-next-line no-unused-vars + async run(logs) { + // TODO: wire with log 'pino' instance + console.log('logs ', logs) + return {} + } +} diff --git a/apps/web-portal/lib/use-cases/log/index.mjs b/apps/web-portal/lib/use-cases/log/index.mjs new file mode 100644 index 00000000..fd18d28a --- /dev/null +++ b/apps/web-portal/lib/use-cases/log/index.mjs @@ -0,0 +1 @@ +export * from './LogSave' diff --git a/packages/util-logger/lib/createLogger.mjs b/packages/util-logger/lib/createLogger.mjs index cfb57ffb..53e8844e 100644 --- a/packages/util-logger/lib/createLogger.mjs +++ b/packages/util-logger/lib/createLogger.mjs @@ -17,9 +17,10 @@ const cache = {} /** * Creates Logger * @param {Object} options - * @param {LoggerTypes} [options.type] - logger type, PINO by default + * @param {} [options.type] - logger type, PINO by default * @param {Boolean} [options.isPrettyPrint] - turn on/off pretty print * @param {Boolean} [options.shared] - create a shared, singleton instance, true by default + * @param {Object} [options.context] - contain logger context * @returns {PinoLogger|MockLogger|ConsoleLogger|*} */ @@ -28,9 +29,10 @@ export function createLogger( isPrettyPrint: false, type: LoggerTypes.Pino, shared: true, + context: {}, } ) { - let {type, shared} = options + let {type, shared, context} = options let logger type = type || LoggerTypes.Pino @@ -46,6 +48,7 @@ export function createLogger( if (type === LoggerTypes.Pino) { logger = new PinoLogger({ isPrettyPrint: options.isPrettyPrint, + ...context, }) } else if (type === LoggerTypes.Console) { logger = new ConsoleLogger() diff --git a/packages/util-logger/lib/types/MockLogger.mjs b/packages/util-logger/lib/types/MockLogger.mjs index 9f80541d..c5df70f5 100644 --- a/packages/util-logger/lib/types/MockLogger.mjs +++ b/packages/util-logger/lib/types/MockLogger.mjs @@ -1,5 +1,5 @@ /* eslint-disable no-unused-vars */ -import {CoreLogger} from '../..' +import {CoreLogger} from '../core' export class MockLogger extends CoreLogger { info(...args) {} diff --git a/packages/util-logger/lib/types/PinoLogger.mjs b/packages/util-logger/lib/types/PinoLogger.mjs index 39924bb9..b6d6e6f3 100644 --- a/packages/util-logger/lib/types/PinoLogger.mjs +++ b/packages/util-logger/lib/types/PinoLogger.mjs @@ -1,5 +1,5 @@ import pino from 'pino' -import {CoreLogger} from '../..' +import {CoreLogger} from '../core' export class PinoLogger extends CoreLogger { #pino = null @@ -30,6 +30,7 @@ export class PinoLogger extends CoreLogger { }, // TODO: configure log level over env vars level: context.level || 'info', + browser: context.browser || null, } if (context.isPrettyPrint) { diff --git a/packages/util-web-api-client/lib/axios/AxiosClientApi.mjs b/packages/util-web-api-client/lib/axios/AxiosClientApi.mjs index bb2b03b1..c6351e1d 100644 --- a/packages/util-web-api-client/lib/axios/AxiosClientApi.mjs +++ b/packages/util-web-api-client/lib/axios/AxiosClientApi.mjs @@ -4,12 +4,15 @@ import {v4 as uuid} from 'uuid' import {ClientApi} from '../core' import {AxiosAuthApi} from './AxiosAuthApi' import {AxiosPreviewApi} from './AxiosPreviewApi' +import {AxiosLogApi} from './AxiosLogApi' export class AxiosClientApi extends ClientApi { #auth = null #preview = null + #log = null + constructor(options) { super() const instance = axios.create({ @@ -23,6 +26,7 @@ export class AxiosClientApi extends ClientApi { this.#auth = new AxiosAuthApi(instance) this.#preview = new AxiosPreviewApi(instance) + this.#log = new AxiosLogApi(instance) } get auth() { @@ -32,4 +36,8 @@ export class AxiosClientApi extends ClientApi { get preview() { return this.#preview } + + get log() { + return this.#log + } } diff --git a/packages/util-web-api-client/lib/axios/AxiosLogApi.mjs b/packages/util-web-api-client/lib/axios/AxiosLogApi.mjs new file mode 100644 index 00000000..6c43b8cd --- /dev/null +++ b/packages/util-web-api-client/lib/axios/AxiosLogApi.mjs @@ -0,0 +1,19 @@ +/* eslint-disable no-unused-vars */ +import {LogApi} from '../core' + +export class AxiosLogApi extends LogApi { + #http = null + /** + * @param http - Axios instance + */ + + constructor(http) { + super() + this.#http = http + } + + async save(logs) { + const response = await this.#http.post(`/api/v1/log`, logs) + return response ? response.data : {status: 1} + } +} diff --git a/packages/util-web-api-client/lib/axios/index.mjs b/packages/util-web-api-client/lib/axios/index.mjs index 2893add3..3efd3bd9 100644 --- a/packages/util-web-api-client/lib/axios/index.mjs +++ b/packages/util-web-api-client/lib/axios/index.mjs @@ -1,3 +1,4 @@ export * from './AxiosClientApi' export * from './AxiosAuthApi' export * from './AxiosPreviewApi' +export * from './AxiosLogApi' diff --git a/packages/util-web-api-client/lib/core/LogApi.mjs b/packages/util-web-api-client/lib/core/LogApi.mjs new file mode 100644 index 00000000..7f222676 --- /dev/null +++ b/packages/util-web-api-client/lib/core/LogApi.mjs @@ -0,0 +1,18 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable class-methods-use-this */ +import {NotYetImplementedError} from '@zorko-io/util-error' + +export class LogApi { + /** + * @typedef {Array} Log + */ + + /** + * Upload logs to the server + * @param {Array} logs - the list of logs + * @returns {Promise} + */ + async save(logs) { + throw new NotYetImplementedError() + } +} diff --git a/packages/util-web-api-client/lib/core/index.mjs b/packages/util-web-api-client/lib/core/index.mjs index 61928656..75a1e573 100644 --- a/packages/util-web-api-client/lib/core/index.mjs +++ b/packages/util-web-api-client/lib/core/index.mjs @@ -1,3 +1,4 @@ export * from './ClientApi' export * from './AuthApi' export * from './PreviewApi' +export * from './LogApi' diff --git a/packages/util-web-api-client/lib/mock/MockClientApi.mjs b/packages/util-web-api-client/lib/mock/MockClientApi.mjs index c8523007..179b279b 100644 --- a/packages/util-web-api-client/lib/mock/MockClientApi.mjs +++ b/packages/util-web-api-client/lib/mock/MockClientApi.mjs @@ -1,6 +1,7 @@ import {ClientApi} from '../core' import {MockAuthApi} from './MockAuthApi' import {MockPreviewApi} from './MockPreviewApi' +import {MockLogApi} from './MockLogApi' import mock from './mock.json' export class MockClientApi extends ClientApi { @@ -8,10 +9,13 @@ export class MockClientApi extends ClientApi { #preview = null + #log = null + constructor() { super() this.#auth = new MockAuthApi(mock.auth) this.#preview = new MockPreviewApi(mock.preview) + this.#log = new MockLogApi() } get auth() { @@ -21,4 +25,8 @@ export class MockClientApi extends ClientApi { get preview() { return this.#preview } + + get log() { + return this.#log + } } diff --git a/packages/util-web-api-client/lib/mock/MockLogApi.mjs b/packages/util-web-api-client/lib/mock/MockLogApi.mjs new file mode 100644 index 00000000..61ddcfbc --- /dev/null +++ b/packages/util-web-api-client/lib/mock/MockLogApi.mjs @@ -0,0 +1,15 @@ +/* eslint-disable no-unused-vars */ +import {LogApi} from '../core' + +export class MockLogApi extends LogApi { + #mock = null + + constructor(mock) { + super() + this.#mock = mock + } + + async save(logs) { + return undefined + } +} diff --git a/packages/util-web-api-client/lib/mock/index.mjs b/packages/util-web-api-client/lib/mock/index.mjs index 8ca522bd..9aea7e95 100644 --- a/packages/util-web-api-client/lib/mock/index.mjs +++ b/packages/util-web-api-client/lib/mock/index.mjs @@ -1,3 +1,4 @@ export * from './MockAuthApi' export * from './MockClientApi' export * from './MockPreviewApi' +export * from './MockLogApi'