From e89fb3688d91e23fc765745b5429b90f9c2ab844 Mon Sep 17 00:00:00 2001 From: David Vitor Antonio Date: Thu, 2 Nov 2023 16:44:44 -0300 Subject: [PATCH] feat(studio): add logs filter (#415) --- .../Layout/BottomPanel/Logs/Filter.tsx | 146 ++++++++++++++++++ .../Layout/BottomPanel/Logs/index.tsx | 54 ++++++- .../studio-ui/src/web/translations/en.json | 4 +- .../studio-ui/src/web/translations/es.json | 4 +- .../studio-ui/src/web/translations/fr.json | 4 +- 5 files changed, 202 insertions(+), 10 deletions(-) create mode 100644 packages/studio-ui/src/web/components/Layout/BottomPanel/Logs/Filter.tsx diff --git a/packages/studio-ui/src/web/components/Layout/BottomPanel/Logs/Filter.tsx b/packages/studio-ui/src/web/components/Layout/BottomPanel/Logs/Filter.tsx new file mode 100644 index 00000000..0e69b1bb --- /dev/null +++ b/packages/studio-ui/src/web/components/Layout/BottomPanel/Logs/Filter.tsx @@ -0,0 +1,146 @@ +import { Button, Intent } from '@blueprintjs/core' +import { lang } from 'botpress/shared' +import cx from 'classnames' +import React from 'react' +import CheckboxTree from 'react-checkbox-tree' +import 'react-checkbox-tree/lib/react-checkbox-tree.css' +import { + FaCheckSquare, + FaChevronDown, + FaChevronRight, + FaFile, + FaFolder, + FaFolderOpen, + FaMinusSquare, + FaPlusSquare, + FaSquare +} from 'react-icons/fa' + +import style from '../style.scss' + +interface Props { + setLogFilter: (checked: string[]) => any + ref?: any +} + +interface State { + nodes: any + checked: any + expanded: any +} + +export const LOG_FILTER_STORAGE_KEY = 'logsFilter' +export const LOG_FILTER_DEFAULT_VALUE = [ + 'severity', + 'debug', + 'info', + 'warn', + 'error', + 'critical', + 'botLevel', + 'global', + 'currentBot' +] + +export default class Debug extends React.Component { + state = { + nodes: undefined, + checked: undefined, + expanded: ['severity', 'botLevel'] + } + + async componentDidMount() { + await this.loadConfiguration() + } + + loadConfiguration = async () => { + let logFilter = LOG_FILTER_DEFAULT_VALUE + try { + const logFilterString = localStorage.getItem('logsFilter') + const persistedLogFilter = JSON.parse(logFilterString) + + if (logFilterString?.length && Array.isArray(persistedLogFilter)) { + logFilter = persistedLogFilter + } + } catch (e) {} + + this.setState({ + nodes: [ + { + value: 'severity', + label: 'Severity', + children: [ + { value: 'debug', label: 'Debug' }, + { value: 'info', label: 'Info' }, + { value: 'warn', label: 'Warning' }, + { value: 'error', label: 'Error' }, + { value: 'critical', label: 'Critical' } + ] + }, + { + value: 'botLevel', + label: 'Bot Level', + children: [ + { value: 'global', label: 'Global' }, + { value: 'currentBot', label: 'Current Bot' } + ] + } + ], + checked: logFilter + }) + + this.props.setLogFilter(logFilter) + } + + saveConfiguration = async () => { + localStorage.setItem(LOG_FILTER_STORAGE_KEY, JSON.stringify(this.state.checked)) + this.props.setLogFilter(this.state.checked) + } + + render() { + if (!this.state.nodes) { + return null + } + + return ( +
+
+ this.setState({ checked })} + icons={{ + check: , + uncheck: , + halfCheck: , + expandClose: , + expandOpen: , + expandAll: , + collapseAll: , + parentClose: , + parentOpen: , + leaf: + }} + /> +
+
+
+ {lang.tr('bottomPanel.logs.botLevelOBS')} +
+
+
+
+
+ ) + } +} diff --git a/packages/studio-ui/src/web/components/Layout/BottomPanel/Logs/index.tsx b/packages/studio-ui/src/web/components/Layout/BottomPanel/Logs/index.tsx index ae1325fa..1cdffa16 100644 --- a/packages/studio-ui/src/web/components/Layout/BottomPanel/Logs/index.tsx +++ b/packages/studio-ui/src/web/components/Layout/BottomPanel/Logs/index.tsx @@ -16,6 +16,7 @@ import EventBus from '~/util/EventBus' import style from '../style.scss' import Debug from './Debug' +import Filter, { LOG_FILTER_DEFAULT_VALUE } from './Filter' import logStyle from './style.scss' const INITIAL_LOGS_LIMIT = 200 @@ -35,12 +36,14 @@ interface State { followLogs: boolean selectedPanel: string initialLogs: LogEntry[] + logFilter: string[] } interface LogEntry { level: string message: string args: any + botId: string ts: string } @@ -56,6 +59,14 @@ class BottomPanel extends React.Component { private debounceRefreshLogs: typeof this.forceUpdate private logs: LogEntry[] private debugRef = React.createRef() + private filterRef = React.createRef() + + state = { + followLogs: true, + selectedPanel: 'logs', + initialLogs: [], + logFilter: LOG_FILTER_DEFAULT_VALUE + } constructor(props: Props) { super(props) @@ -77,6 +88,7 @@ class BottomPanel extends React.Component { return { ts: log.timestamp, level: log.level, + botId: log.type?.replace?.('logs::', ''), message: this.ansiiToSafeHTML(log.message), args: this.ansiiToSafeHTML(log.metadata || log.args || '') } @@ -97,12 +109,6 @@ class BottomPanel extends React.Component { }) } - state = { - followLogs: true, - selectedPanel: 'logs', - initialLogs: [] - } - queryLogs = async () => { const { data } = await axios.get(`${window.API_PATH}/admin/logs/bots/${window.BOT_ID}`, { params: { @@ -182,13 +188,22 @@ class BottomPanel extends React.Component { return null } const allLogs = [...this.state.initialLogs, ...this.logs] + const logFilter = this.state.logFilter const LogsPanel = (
    - {allLogs.map((e) => this.renderEntry(e))} + {allLogs + .filter((log) => logFilter.includes(log.level)) + .filter( + (log) => + !log.botId || + (logFilter.includes('global') && log.botId === '*') || + (logFilter.includes('currentBot') && log.botId !== '*') + ) + .map((e) => this.renderEntry(e))}
  • {lang.tr('bottomPanel.logs.endOfLogs')}
) @@ -197,6 +212,12 @@ class BottomPanel extends React.Component { } /> + this.setState({ logFilter })} ref={this.filterRef} />} + /> {this.state.selectedPanel === 'logs' && ( @@ -244,6 +265,25 @@ class BottomPanel extends React.Component { )} + + {this.state.selectedPanel === 'filter' && ( + + +
+ + +