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