Convert Log Events to TypeScript

pull/7605/head
Mark McDowall 2 months ago
parent 9276bd7a16
commit 1e5932d89a
No known key found for this signature in database

@ -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() {
<Route path="/system/updates" component={Updates} />
<Route path="/system/events" component={LogsTableConnector} />
<Route path="/system/events" component={LogsTable} />
<Route path="/system/logs/files" component={Logs} />

@ -0,0 +1,14 @@
import AppSectionState, {
AppSectionFilterState,
PagedAppSectionState,
TableAppSectionState,
} from 'App/State/AppSectionState';
import LogEvent from 'typings/LogEvent';
interface LogsAppState
extends AppSectionState<LogEvent>,
AppSectionFilterState<LogEvent>,
PagedAppSectionState,
TableAppSectionState {}
export default LogsAppState;

@ -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<DiskSpace>;
export type HealthAppState = AppSectionState<Health>;
@ -16,6 +17,7 @@ interface SystemAppState {
backups: BackupAppState;
diskSpace: DiskSpaceAppState;
health: HealthAppState;
logs: LogsAppState;
status: SystemStatusAppState;
tasks: TaskAppState;
updates: UpdateAppState;

@ -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 (
<PageContent title={translate('Logs')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('Refresh')}
iconName={icons.REFRESH}
spinningName={icons.REFRESH}
isSpinning={isFetching}
onPress={onRefreshPress}
/>
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
isSpinning={clearLogExecuting}
onPress={onClearLogsPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
canModifyColumns={false}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
isPopulated && !error && !items.length &&
<Alert kind={kinds.INFO}>
{translate('NoEventsFound')}
</Alert>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
columns={columns}
canModifyColumns={false}
{...otherProps}
>
<TableBody>
{
items.map((item) => {
return (
<LogsTableRow
key={item.id}
columns={columns}
{...item}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
{...otherProps}
/>
</div>
}
</PageContentBody>
</PageContent>
);
}
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;

@ -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 (
<PageContent title={translate('Logs')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('Refresh')}
iconName={icons.REFRESH}
spinningName={icons.REFRESH}
isSpinning={isFetching}
onPress={handleRefreshPress}
/>
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
isSpinning={isClearLogExecuting}
onPress={handleClearLogsPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
canModifyColumns={false}
columns={columns}
pageSize={pageSize}
onTableOptionChange={handleTableOptionChange}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
onFilterSelect={handleFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{isPopulated && !error && !items.length ? (
<Alert kind={kinds.INFO}>{translate('NoEventsFound')}</Alert>
) : null}
{isPopulated && !error && items.length ? (
<div>
<Table
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<LogsTableRow key={item.id} columns={columns} {...item} />
);
})}
</TableBody>
</Table>
<TablePager
page={page}
totalPages={totalPages}
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
/>
</div>
) : null}
</PageContentBody>
</PageContent>
);
}
export default LogsTable;

@ -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 (
<LogsTable
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onTableOptionChange={this.onTableOptionChange}
onRefreshPress={this.onRefreshPress}
onClearLogsPress={this.onClearLogsPress}
{...this.props}
/>
);
}
}
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)
);

@ -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 (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<ModalContent
onModalClose={onModalClose}
>
<ModalHeader>
{translate('Details')}
</ModalHeader>
<ModalBody>
<div>
{translate('Message')}
</div>
<Scroller
className={styles.detailsText}
scrollDirection={scrollDirections.HORIZONTAL}
>
{message}
</Scroller>
{
!!exception &&
<div>
<div>
{translate('Exception')}
</div>
<Scroller
className={styles.detailsText}
scrollDirection={scrollDirections.HORIZONTAL}
>
{exception}
</Scroller>
</div>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
LogsTableDetailsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
message: PropTypes.string.isRequired,
exception: PropTypes.string,
onModalClose: PropTypes.func.isRequired
};
export default LogsTableDetailsModal;

@ -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 (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('Details')}</ModalHeader>
<ModalBody>
<div>{translate('Message')}</div>
<Scroller
className={styles.detailsText}
scrollDirection={scrollDirections.HORIZONTAL}
>
{message}
</Scroller>
{!!exception && (
<div>
<div>{translate('Exception')}</div>
<Scroller
className={styles.detailsText}
scrollDirection={scrollDirections.HORIZONTAL}
>
{exception}
</Scroller>
</div>
)}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
export default LogsTableDetailsModal;

@ -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 (
<TableRowButton
overlayContent={true}
onPress={this.onPress}
>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'level') {
return (
<TableRowCell
key={name}
className={styles.level}
>
<Icon
className={styles[level]}
name={getIconName(level)}
title={level}
/>
</TableRowCell>
);
}
if (name === 'time') {
return (
<RelativeDateCell
key={name}
date={time}
/>
);
}
if (name === 'logger') {
return (
<TableRowCell key={name}>
{logger}
</TableRowCell>
);
}
if (name === 'message') {
return (
<TableRowCell key={name}>
{message}
</TableRowCell>
);
}
if (name === 'actions') {
return (
<TableRowCell
key={name}
className={styles.actions}
/>
);
}
return null;
})
}
<LogsTableDetailsModal
isOpen={this.state.isDetailsModalOpen}
message={message}
exception={exception}
onModalClose={this.onModalClose}
/>
</TableRowButton>
);
}
}
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;

@ -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 (
<TableRowButton overlayContent={true} onPress={handlePress}>
{columns.map((column) => {
const { name, isVisible } = column;
if (!isVisible) {
return null;
}
if (name === 'level') {
return (
<TableRowCell key={name} className={styles.level}>
<Icon className={styles[level]} name={iconName} title={level} />
</TableRowCell>
);
}
if (name === 'time') {
return <RelativeDateCell key={name} date={time} />;
}
if (name === 'logger') {
return <TableRowCell key={name}>{logger}</TableRowCell>;
}
if (name === 'message') {
return <TableRowCell key={name}>{message}</TableRowCell>;
}
if (name === 'actions') {
return <TableRowCell key={name} className={styles.actions} />;
}
return null;
})}
<LogsTableDetailsModal
isOpen={isDetailsModalOpen}
message={message}
exception={exception}
onModalClose={handleDetailsModalClose}
/>
</TableRowButton>
);
}
export default LogsTableRow;

@ -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;
Loading…
Cancel
Save