From 23bc6a157c094f89a39f5beaf9f6df1586fd115c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 24 Dec 2024 20:10:38 -0800 Subject: [PATCH] Convert Log Events to TypeScript --- frontend/src/App/AppRoutes.tsx | 4 +- frontend/src/App/State/LogsAppState.ts | 14 ++ frontend/src/App/State/SystemAppState.ts | 2 + frontend/src/System/Events/LogsTable.js | 141 ------------ frontend/src/System/Events/LogsTable.tsx | 205 ++++++++++++++++++ .../src/System/Events/LogsTableConnector.js | 148 ------------- .../System/Events/LogsTableDetailsModal.js | 79 ------- .../System/Events/LogsTableDetailsModal.tsx | 62 ++++++ frontend/src/System/Events/LogsTableRow.js | 158 -------------- frontend/src/System/Events/LogsTableRow.tsx | 102 +++++++++ frontend/src/typings/LogEvent.ts | 18 ++ 11 files changed, 405 insertions(+), 528 deletions(-) create mode 100644 frontend/src/App/State/LogsAppState.ts delete mode 100644 frontend/src/System/Events/LogsTable.js create mode 100644 frontend/src/System/Events/LogsTable.tsx delete mode 100644 frontend/src/System/Events/LogsTableConnector.js delete mode 100644 frontend/src/System/Events/LogsTableDetailsModal.js create mode 100644 frontend/src/System/Events/LogsTableDetailsModal.tsx delete mode 100644 frontend/src/System/Events/LogsTableRow.js create mode 100644 frontend/src/System/Events/LogsTableRow.tsx create mode 100644 frontend/src/typings/LogEvent.ts diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx index 3ed7bcde0..c0dcecc8a 100644 --- a/frontend/src/App/AppRoutes.tsx +++ b/frontend/src/App/AppRoutes.tsx @@ -25,7 +25,7 @@ import Settings from 'Settings/Settings'; import TagSettings from 'Settings/Tags/TagSettings'; import UISettingsConnector from 'Settings/UI/UISettingsConnector'; import Backups from 'System/Backup/Backups'; -import LogsTableConnector from 'System/Events/LogsTableConnector'; +import LogsTable from 'System/Events/LogsTable'; import Logs from 'System/Logs/Logs'; import Status from 'System/Status/Status'; import Tasks from 'System/Tasks/Tasks'; @@ -151,7 +151,7 @@ function AppRoutes() { - + diff --git a/frontend/src/App/State/LogsAppState.ts b/frontend/src/App/State/LogsAppState.ts new file mode 100644 index 000000000..3eca35496 --- /dev/null +++ b/frontend/src/App/State/LogsAppState.ts @@ -0,0 +1,14 @@ +import AppSectionState, { + AppSectionFilterState, + PagedAppSectionState, + TableAppSectionState, +} from 'App/State/AppSectionState'; +import LogEvent from 'typings/LogEvent'; + +interface LogsAppState + extends AppSectionState, + AppSectionFilterState, + PagedAppSectionState, + TableAppSectionState {} + +export default LogsAppState; diff --git a/frontend/src/App/State/SystemAppState.ts b/frontend/src/App/State/SystemAppState.ts index c4f63da67..81be2ab24 100644 --- a/frontend/src/App/State/SystemAppState.ts +++ b/frontend/src/App/State/SystemAppState.ts @@ -5,6 +5,7 @@ import Task from 'typings/Task'; import Update from 'typings/Update'; import AppSectionState, { AppSectionItemState } from './AppSectionState'; import BackupAppState from './BackupAppState'; +import LogsAppState from './LogsAppState'; export type DiskSpaceAppState = AppSectionState; export type HealthAppState = AppSectionState; @@ -16,6 +17,7 @@ interface SystemAppState { backups: BackupAppState; diskSpace: DiskSpaceAppState; health: HealthAppState; + logs: LogsAppState; status: SystemStatusAppState; tasks: TaskAppState; updates: UpdateAppState; diff --git a/frontend/src/System/Events/LogsTable.js b/frontend/src/System/Events/LogsTable.js deleted file mode 100644 index 1c37a03ba..000000000 --- a/frontend/src/System/Events/LogsTable.js +++ /dev/null @@ -1,141 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Alert from 'Components/Alert'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import FilterMenu from 'Components/Menu/FilterMenu'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; -import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; -import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; -import TablePager from 'Components/Table/TablePager'; -import { align, icons, kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import LogsTableRow from './LogsTableRow'; - -function LogsTable(props) { - const { - isFetching, - isPopulated, - error, - items, - columns, - selectedFilterKey, - filters, - totalRecords, - clearLogExecuting, - onRefreshPress, - onClearLogsPress, - onFilterSelect, - ...otherProps - } = props; - - return ( - - - - - - - - - - - - - - - - - - - { - isFetching && !isPopulated && - - } - - { - isPopulated && !error && !items.length && - - {translate('NoEventsFound')} - - } - - { - isPopulated && !error && !!items.length && -
- - - { - items.map((item) => { - return ( - - ); - }) - } - -
- - -
- } -
-
- ); -} - -LogsTable.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - selectedFilterKey: PropTypes.string.isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, - totalRecords: PropTypes.number, - clearLogExecuting: PropTypes.bool.isRequired, - onFilterSelect: PropTypes.func.isRequired, - onRefreshPress: PropTypes.func.isRequired, - onClearLogsPress: PropTypes.func.isRequired -}; - -export default LogsTable; diff --git a/frontend/src/System/Events/LogsTable.tsx b/frontend/src/System/Events/LogsTable.tsx new file mode 100644 index 000000000..893d6b2d8 --- /dev/null +++ b/frontend/src/System/Events/LogsTable.tsx @@ -0,0 +1,205 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import * as commandNames from 'Commands/commandNames'; +import Alert from 'Components/Alert'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; +import TablePager from 'Components/Table/TablePager'; +import usePaging from 'Components/Table/usePaging'; +import useCurrentPage from 'Helpers/Hooks/useCurrentPage'; +import { align, icons, kinds } from 'Helpers/Props'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { + fetchLogs, + gotoLogsFirstPage, + gotoLogsPage, + setLogsFilter, + setLogsSort, + setLogsTableOption, +} from 'Store/Actions/systemActions'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import { TableOptionsChangePayload } from 'typings/Table'; +import translate from 'Utilities/String/translate'; +import LogsTableRow from './LogsTableRow'; + +function LogsTable() { + const dispatch = useDispatch(); + const requestCurrentPage = useCurrentPage(); + + const { + isFetching, + isPopulated, + error, + items, + columns, + page, + pageSize, + totalPages, + totalRecords, + sortKey, + sortDirection, + filters, + selectedFilterKey, + } = useSelector((state: AppState) => state.system.logs); + + const isClearLogExecuting = useSelector( + createCommandExecutingSelector(commandNames.CLEAR_LOGS) + ); + + const { + handleFirstPagePress, + handlePreviousPagePress, + handleNextPagePress, + handleLastPagePress, + handlePageSelect, + } = usePaging({ + page, + totalPages, + gotoPage: gotoLogsPage, + }); + + const handleFilterSelect = useCallback( + (selectedFilterKey: string | number) => { + dispatch(setLogsFilter({ selectedFilterKey })); + }, + [dispatch] + ); + + const handleSortPress = useCallback( + (sortKey: string) => { + dispatch(setLogsSort({ sortKey })); + }, + [dispatch] + ); + + const handleTableOptionChange = useCallback( + (payload: TableOptionsChangePayload) => { + dispatch(setLogsTableOption(payload)); + + if (payload.pageSize) { + dispatch(gotoLogsFirstPage({ page: 1 })); + } + }, + [dispatch] + ); + + const handleRefreshPress = useCallback(() => { + dispatch(gotoLogsFirstPage()); + }, [dispatch]); + + const handleClearLogsPress = useCallback(() => { + dispatch( + executeCommand({ + name: commandNames.CLEAR_LOGS, + commandFinished: () => { + dispatch(gotoLogsFirstPage()); + }, + }) + ); + }, [dispatch]); + + useEffect(() => { + if (requestCurrentPage) { + dispatch(fetchLogs()); + } else { + dispatch(gotoLogsFirstPage({ page: 1 })); + } + }, [requestCurrentPage, dispatch]); + + return ( + + + + + + + + + + + + + + + + + + + {isFetching && !isPopulated ? : null} + + {isPopulated && !error && !items.length ? ( + {translate('NoEventsFound')} + ) : null} + + {isPopulated && !error && items.length ? ( +
+ + + {items.map((item) => { + return ( + + ); + })} + +
+ + +
+ ) : null} +
+
+ ); +} + +export default LogsTable; diff --git a/frontend/src/System/Events/LogsTableConnector.js b/frontend/src/System/Events/LogsTableConnector.js deleted file mode 100644 index a717cba15..000000000 --- a/frontend/src/System/Events/LogsTableConnector.js +++ /dev/null @@ -1,148 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import * as commandNames from 'Commands/commandNames'; -import withCurrentPage from 'Components/withCurrentPage'; -import { executeCommand } from 'Store/Actions/commandActions'; -import * as systemActions from 'Store/Actions/systemActions'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import LogsTable from './LogsTable'; - -function createMapStateToProps() { - return createSelector( - (state) => state.system.logs, - createCommandExecutingSelector(commandNames.CLEAR_LOGS), - (logs, clearLogExecuting) => { - return { - clearLogExecuting, - ...logs - }; - } - ); -} - -const mapDispatchToProps = { - executeCommand, - ...systemActions -}; - -class LogsTableConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - useCurrentPage, - fetchLogs, - gotoLogsFirstPage - } = this.props; - - if (useCurrentPage) { - fetchLogs(); - } else { - gotoLogsFirstPage(); - } - } - - componentDidUpdate(prevProps) { - if (prevProps.clearLogExecuting && !this.props.clearLogExecuting) { - this.props.gotoLogsFirstPage(); - } - } - - // - // Listeners - - onFirstPagePress = () => { - this.props.gotoLogsFirstPage(); - }; - - onPreviousPagePress = () => { - this.props.gotoLogsPreviousPage(); - }; - - onNextPagePress = () => { - this.props.gotoLogsNextPage(); - }; - - onLastPagePress = () => { - this.props.gotoLogsLastPage(); - }; - - onPageSelect = (page) => { - this.props.gotoLogsPage({ page }); - }; - - onSortPress = (sortKey) => { - this.props.setLogsSort({ sortKey }); - }; - - onFilterSelect = (selectedFilterKey) => { - this.props.setLogsFilter({ selectedFilterKey }); - }; - - onTableOptionChange = (payload) => { - this.props.setLogsTableOption(payload); - - if (payload.pageSize) { - this.props.gotoLogsFirstPage(); - } - }; - - onRefreshPress = () => { - this.props.gotoLogsFirstPage(); - }; - - onClearLogsPress = () => { - this.props.executeCommand({ - name: commandNames.CLEAR_LOGS, - commandFinished: this.onCommandFinished - }); - }; - - onCommandFinished = () => { - this.props.gotoLogsFirstPage(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -LogsTableConnector.propTypes = { - useCurrentPage: PropTypes.bool.isRequired, - clearLogExecuting: PropTypes.bool.isRequired, - fetchLogs: PropTypes.func.isRequired, - gotoLogsFirstPage: PropTypes.func.isRequired, - gotoLogsPreviousPage: PropTypes.func.isRequired, - gotoLogsNextPage: PropTypes.func.isRequired, - gotoLogsLastPage: PropTypes.func.isRequired, - gotoLogsPage: PropTypes.func.isRequired, - setLogsSort: PropTypes.func.isRequired, - setLogsFilter: PropTypes.func.isRequired, - setLogsTableOption: PropTypes.func.isRequired, - executeCommand: PropTypes.func.isRequired -}; - -export default withCurrentPage( - connect(createMapStateToProps, mapDispatchToProps)(LogsTableConnector) -); diff --git a/frontend/src/System/Events/LogsTableDetailsModal.js b/frontend/src/System/Events/LogsTableDetailsModal.js deleted file mode 100644 index 13329f17b..000000000 --- a/frontend/src/System/Events/LogsTableDetailsModal.js +++ /dev/null @@ -1,79 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Button from 'Components/Link/Button'; -import Modal from 'Components/Modal/Modal'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import Scroller from 'Components/Scroller/Scroller'; -import { scrollDirections } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import styles from './LogsTableDetailsModal.css'; - -function LogsTableDetailsModal(props) { - const { - isOpen, - message, - exception, - onModalClose - } = props; - - return ( - - - - {translate('Details')} - - - -
- {translate('Message')} -
- - - {message} - - - { - !!exception && -
-
- {translate('Exception')} -
- - {exception} - -
- } -
- - - - -
-
- ); -} - -LogsTableDetailsModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - message: PropTypes.string.isRequired, - exception: PropTypes.string, - onModalClose: PropTypes.func.isRequired -}; - -export default LogsTableDetailsModal; diff --git a/frontend/src/System/Events/LogsTableDetailsModal.tsx b/frontend/src/System/Events/LogsTableDetailsModal.tsx new file mode 100644 index 000000000..640f34514 --- /dev/null +++ b/frontend/src/System/Events/LogsTableDetailsModal.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import Button from 'Components/Link/Button'; +import Modal from 'Components/Modal/Modal'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import Scroller from 'Components/Scroller/Scroller'; +import { scrollDirections } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './LogsTableDetailsModal.css'; + +interface LogsTableDetailsModalProps { + isOpen: boolean; + message: string; + exception?: string; + onModalClose: (...args: unknown[]) => unknown; +} + +function LogsTableDetailsModal({ + isOpen, + message, + exception, + onModalClose, +}: LogsTableDetailsModalProps) { + return ( + + + {translate('Details')} + + +
{translate('Message')}
+ + + {message} + + + {!!exception && ( +
+
{translate('Exception')}
+ + {exception} + +
+ )} +
+ + + + +
+
+ ); +} + +export default LogsTableDetailsModal; diff --git a/frontend/src/System/Events/LogsTableRow.js b/frontend/src/System/Events/LogsTableRow.js deleted file mode 100644 index 2c38ea10c..000000000 --- a/frontend/src/System/Events/LogsTableRow.js +++ /dev/null @@ -1,158 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRowButton from 'Components/Table/TableRowButton'; -import { icons } from 'Helpers/Props'; -import LogsTableDetailsModal from './LogsTableDetailsModal'; -import styles from './LogsTableRow.css'; - -function getIconName(level) { - switch (level) { - case 'trace': - case 'debug': - case 'info': - return icons.INFO; - case 'warn': - return icons.DANGER; - case 'error': - return icons.BUG; - case 'fatal': - return icons.FATAL; - default: - return icons.UNKNOWN; - } -} - -class LogsTableRow extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isDetailsModalOpen: false - }; - } - - // - // Listeners - - onPress = () => { - // Don't re-open the modal if it's already open - if (!this.state.isDetailsModalOpen) { - this.setState({ isDetailsModalOpen: true }); - } - }; - - onModalClose = () => { - this.setState({ isDetailsModalOpen: false }); - }; - - // - // Render - - render() { - const { - level, - time, - logger, - message, - exception, - columns - } = this.props; - - return ( - - { - columns.map((column) => { - const { - name, - isVisible - } = column; - - if (!isVisible) { - return null; - } - - if (name === 'level') { - return ( - - - - ); - } - - if (name === 'time') { - return ( - - ); - } - - if (name === 'logger') { - return ( - - {logger} - - ); - } - - if (name === 'message') { - return ( - - {message} - - ); - } - - if (name === 'actions') { - return ( - - ); - } - - return null; - }) - } - - - - ); - } - -} - -LogsTableRow.propTypes = { - level: PropTypes.string.isRequired, - time: PropTypes.string.isRequired, - logger: PropTypes.string.isRequired, - message: PropTypes.string.isRequired, - exception: PropTypes.string, - columns: PropTypes.arrayOf(PropTypes.object).isRequired -}; - -export default LogsTableRow; diff --git a/frontend/src/System/Events/LogsTableRow.tsx b/frontend/src/System/Events/LogsTableRow.tsx new file mode 100644 index 000000000..a4e539c67 --- /dev/null +++ b/frontend/src/System/Events/LogsTableRow.tsx @@ -0,0 +1,102 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import Icon from 'Components/Icon'; +import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Column from 'Components/Table/Column'; +import TableRowButton from 'Components/Table/TableRowButton'; +import { icons } from 'Helpers/Props'; +import { LogEventLevel } from 'typings/LogEvent'; +import LogsTableDetailsModal from './LogsTableDetailsModal'; +import styles from './LogsTableRow.css'; + +interface LogsTableRowProps { + level: LogEventLevel; + time: string; + logger: string; + message: string; + exception?: string; + columns: Column[]; +} + +function LogsTableRow({ + level, + time, + logger, + message, + exception, + columns, +}: LogsTableRowProps) { + const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false); + + const iconName = useMemo(() => { + switch (level) { + case 'trace': + case 'debug': + case 'info': + return icons.INFO; + case 'warn': + return icons.DANGER; + case 'error': + return icons.BUG; + case 'fatal': + return icons.FATAL; + default: + return icons.UNKNOWN; + } + }, [level]); + + const handlePress = useCallback(() => { + setIsDetailsModalOpen(true); + }, []); + + const handleDetailsModalClose = useCallback(() => { + setIsDetailsModalOpen(false); + }, []); + + return ( + + {columns.map((column) => { + const { name, isVisible } = column; + + if (!isVisible) { + return null; + } + + if (name === 'level') { + return ( + + + + ); + } + + if (name === 'time') { + return ; + } + + if (name === 'logger') { + return {logger}; + } + + if (name === 'message') { + return {message}; + } + + if (name === 'actions') { + return ; + } + + return null; + })} + + + + ); +} + +export default LogsTableRow; diff --git a/frontend/src/typings/LogEvent.ts b/frontend/src/typings/LogEvent.ts new file mode 100644 index 000000000..f46adf2a9 --- /dev/null +++ b/frontend/src/typings/LogEvent.ts @@ -0,0 +1,18 @@ +import ModelBase from 'App/ModelBase'; + +export type LogEventLevel = + | 'trace' + | 'debug' + | 'info' + | 'warn' + | 'error' + | 'fatal'; + +interface LogEvent extends ModelBase { + time: string; + level: LogEventLevel; + logger: string; + message: string; +} + +export default LogEvent;