MM-57014 Extended console log search to all log attributes (#30397)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
bb8aabc15e
Коммит
e0f54447b9
@@ -0,0 +1,510 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/admin_console/server_logs/Logs should display the logs correctly after loading 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="wrapper--admin"
|
||||
>
|
||||
<div
|
||||
class="admin-console__header"
|
||||
>
|
||||
Server Logs
|
||||
</div>
|
||||
<div
|
||||
class="admin-console__wrapper"
|
||||
>
|
||||
<div
|
||||
class="admin-logs-content admin-console__content"
|
||||
>
|
||||
<div
|
||||
class="logs-banner"
|
||||
>
|
||||
<div
|
||||
class="banner"
|
||||
>
|
||||
<div
|
||||
class="banner__content"
|
||||
>
|
||||
To look up users by User ID or Token ID, go to User Management > Users and paste the ID into the search filter.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="banner-buttons"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="admin.logs.LogFormat.legend"
|
||||
class="banner-buttons__log-format"
|
||||
id="admin.logs.LogFormat"
|
||||
role="radiogroup"
|
||||
>
|
||||
<span
|
||||
id="admin.logs.LogFormat.legend"
|
||||
>
|
||||
Log Format:
|
||||
</span>
|
||||
<label>
|
||||
<input
|
||||
checked=""
|
||||
id="admin.logs.LogFormat.json"
|
||||
name="log-format"
|
||||
type="radio"
|
||||
value="json"
|
||||
/>
|
||||
JSON
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
id="admin.logs.LogFormat.plain"
|
||||
name="log-format"
|
||||
type="radio"
|
||||
value="plain"
|
||||
/>
|
||||
Plain text
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="submit"
|
||||
>
|
||||
Reload Logs
|
||||
</button>
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
href="/api/v4/logs/download"
|
||||
location="download_logs"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Download Logs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="LogTable"
|
||||
>
|
||||
<div
|
||||
class="DataGrid"
|
||||
>
|
||||
<div
|
||||
class="DataGrid_search"
|
||||
>
|
||||
<div
|
||||
class="DataGrid_searchBar"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="DataGrid_searchIcon"
|
||||
>
|
||||
<i
|
||||
class="fa fa-search"
|
||||
title="Search Icon"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
data-testid="searchInput"
|
||||
placeholder="Search"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
class="DataGrid_clearButton fa fa-times-circle hidden"
|
||||
data-testid="clear-search"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="Filter"
|
||||
>
|
||||
<button
|
||||
class="Filter_button"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="Icon icon-filter-variant"
|
||||
/>
|
||||
Filters
|
||||
</button>
|
||||
<div
|
||||
class="Filter_content"
|
||||
>
|
||||
<div
|
||||
class="Filter_header"
|
||||
>
|
||||
<div
|
||||
class="Filter_title"
|
||||
>
|
||||
Filter by
|
||||
</div>
|
||||
<a
|
||||
class="Filter_reset"
|
||||
>
|
||||
Reset filters
|
||||
</a>
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
class="Filter_lists"
|
||||
>
|
||||
<div
|
||||
class="FilterList"
|
||||
>
|
||||
<div
|
||||
class="FilterList_name"
|
||||
>
|
||||
Levels
|
||||
</div>
|
||||
<div
|
||||
class="FilterList_item"
|
||||
>
|
||||
<div
|
||||
class="FilterList_checkbox"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
checked=""
|
||||
id="all"
|
||||
name="all"
|
||||
type="checkbox"
|
||||
/>
|
||||
All levels
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="FilterList_item"
|
||||
>
|
||||
<div
|
||||
class="FilterList_checkbox"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
id="error"
|
||||
name="error"
|
||||
type="checkbox"
|
||||
/>
|
||||
Error
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="FilterList_item"
|
||||
>
|
||||
<div
|
||||
class="FilterList_checkbox"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
id="info"
|
||||
name="info"
|
||||
type="checkbox"
|
||||
/>
|
||||
Info
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="FilterList_item"
|
||||
>
|
||||
<div
|
||||
class="FilterList_checkbox"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
id="debug"
|
||||
name="debug"
|
||||
type="checkbox"
|
||||
/>
|
||||
Debug
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary btn-sm Filter_apply"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-tertiary btn-sm ml-2"
|
||||
>
|
||||
Show last 0 errors
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_header"
|
||||
>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 1.5;"
|
||||
>
|
||||
<div
|
||||
class="timestamp"
|
||||
>
|
||||
Timestamp
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="18"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 0.5;"
|
||||
>
|
||||
Level
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 2.5;"
|
||||
>
|
||||
Message
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 1.5;"
|
||||
>
|
||||
Caller
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 1;"
|
||||
>
|
||||
Options
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_rows"
|
||||
style="min-height: 120px;"
|
||||
>
|
||||
<div
|
||||
class="DataGrid_row"
|
||||
>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 1.5; text-align: left;"
|
||||
>
|
||||
<span
|
||||
class="group-name overflow--ellipsis row-content"
|
||||
data-testid="timestamp"
|
||||
>
|
||||
<span
|
||||
class="group-description row-content"
|
||||
>
|
||||
timestamp 1
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 0.5; text-align: left;"
|
||||
>
|
||||
<span
|
||||
class="group-description adjusted row-content"
|
||||
>
|
||||
info
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 2.5; text-align: left;"
|
||||
>
|
||||
<span
|
||||
class="group-description row-content"
|
||||
title="msg 1"
|
||||
>
|
||||
msg 1
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 1.5; text-align: left;"
|
||||
>
|
||||
<span
|
||||
class="group-description row-content"
|
||||
>
|
||||
caller 1
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 1; text-align: left;"
|
||||
>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm"
|
||||
type="submit"
|
||||
>
|
||||
Full Log event
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_row"
|
||||
>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 1.5; text-align: left;"
|
||||
>
|
||||
<span
|
||||
class="group-name overflow--ellipsis row-content"
|
||||
data-testid="timestamp"
|
||||
>
|
||||
<span
|
||||
class="group-description row-content"
|
||||
>
|
||||
timestamp 2
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 0.5; text-align: left;"
|
||||
>
|
||||
<span
|
||||
class="group-description adjusted row-content"
|
||||
>
|
||||
info
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 2.5; text-align: left;"
|
||||
>
|
||||
<span
|
||||
class="group-description row-content"
|
||||
title="msg 2"
|
||||
>
|
||||
msg 2
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 1.5; text-align: left;"
|
||||
>
|
||||
<span
|
||||
class="group-description row-content"
|
||||
>
|
||||
caller 2
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 1; text-align: left;"
|
||||
>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm"
|
||||
type="submit"
|
||||
>
|
||||
Full Log event
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_row"
|
||||
>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 1.5; text-align: left;"
|
||||
>
|
||||
<span
|
||||
class="group-name overflow--ellipsis row-content"
|
||||
data-testid="timestamp"
|
||||
>
|
||||
<span
|
||||
class="group-description row-content"
|
||||
>
|
||||
filtered
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 0.5; text-align: left;"
|
||||
>
|
||||
<span
|
||||
class="group-description adjusted row-content"
|
||||
>
|
||||
info
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 2.5; text-align: left;"
|
||||
>
|
||||
<span
|
||||
class="group-description row-content"
|
||||
title="filtered message"
|
||||
>
|
||||
filtered message
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 1.5; text-align: left;"
|
||||
>
|
||||
<span
|
||||
class="group-description row-content"
|
||||
>
|
||||
filtered
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
style="flex-grow: 1; text-align: left;"
|
||||
>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm"
|
||||
type="submit"
|
||||
>
|
||||
Full Log event
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="DataGrid_footer"
|
||||
>
|
||||
<div
|
||||
class="DataGrid_cell"
|
||||
>
|
||||
1 - 3 of 3
|
||||
<button
|
||||
class="btn btn-quaternary btn-icon btn-sm ml-2 prev disabled"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="icon icon-chevron-left"
|
||||
title="Previous Icon"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-quaternary btn-icon btn-sm next disabled"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="icon icon-chevron-right"
|
||||
title="Next Icon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {LogLevelEnum} from '@mattermost/types/admin';
|
||||
|
||||
import {renderWithContext, userEvent, screen, waitFor} from 'tests/react_testing_utils';
|
||||
|
||||
import Logs from './logs';
|
||||
|
||||
describe('components/admin_console/server_logs/Logs', () => {
|
||||
// Log dataset
|
||||
const logs = [{
|
||||
caller: 'caller 1',
|
||||
job_id: 'job_id 1',
|
||||
level: LogLevelEnum.INFO,
|
||||
msg: 'msg 1',
|
||||
timestamp: 'timestamp 1',
|
||||
worker: 'worker 1',
|
||||
whatever: 'whatever 1',
|
||||
}, {
|
||||
caller: 'caller 2',
|
||||
job_id: 'job_id 2',
|
||||
level: LogLevelEnum.INFO,
|
||||
msg: 'msg 2',
|
||||
timestamp: 'timestamp 2',
|
||||
worker: 'worker 2',
|
||||
whatever: 'whatever 2',
|
||||
}, {
|
||||
caller: 'filtered',
|
||||
job_id: 'filtered',
|
||||
level: LogLevelEnum.INFO,
|
||||
msg: 'filtered message',
|
||||
timestamp: 'filtered',
|
||||
worker: 'filtered',
|
||||
whatever: 'filtered',
|
||||
}];
|
||||
let container: HTMLElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Mount server log screen
|
||||
container = renderWithContext(
|
||||
<Logs
|
||||
logs={logs}
|
||||
plainLogs={[]}
|
||||
isPlainLogs={false}
|
||||
actions={{getLogs: jest.fn(), getPlainLogs: jest.fn()}}
|
||||
/>,
|
||||
).container;
|
||||
|
||||
// Wait for the logs to be displayed
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('msg 1')).toBeInTheDocument();
|
||||
expect(screen.queryByText('msg 2')).toBeInTheDocument();
|
||||
expect(screen.queryByText('filtered message')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should display the logs correctly after loading', () => {
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test.each(['caller', 'msg', 'worker', 'job_id', 'whatever'])('should search input be performed on %s attribute',
|
||||
async (searchString: string) => {
|
||||
const searchInput = screen.getByTestId('searchInput');
|
||||
userEvent.type(searchInput, searchString);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('msg 1')).toBeInTheDocument();
|
||||
expect(screen.queryByText('msg 2')).toBeInTheDocument();
|
||||
expect(screen.queryByText('filtered message')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test.each(['level', 'timestamp'])('should search input not be performed on %s attribute',
|
||||
async (searchString: string) => {
|
||||
const searchInput = screen.getByTestId('searchInput');
|
||||
userEvent.type(searchInput, searchString);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('msg 1')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('msg 2')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('filtered message')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,8 +20,12 @@ import AdminHeader from 'components/widgets/admin_console/admin_header';
|
||||
import LogList from './log_list';
|
||||
import PlainLogList from './plain_log_list';
|
||||
|
||||
type LogObjectWithAdditionalInfo = LogObject & {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
logs: LogObject[];
|
||||
logs: LogObjectWithAdditionalInfo[];
|
||||
plainLogs: string[];
|
||||
isPlainLogs: boolean;
|
||||
actions: {
|
||||
@@ -121,13 +125,25 @@ export default class Logs extends React.PureComponent<Props, State> {
|
||||
|
||||
performSearch = debounce(() => {
|
||||
const {search} = this.state;
|
||||
const filteredLogs = this.props.logs.filter((log) => {
|
||||
// to be improved
|
||||
return `${log.caller}${log.msg}${log.worker}${log.worker}`.toLowerCase().includes(search.toLowerCase());
|
||||
});
|
||||
|
||||
// Excluding level and timestamp from search
|
||||
const excludedKeys = new Set(['level', 'timestamp']);
|
||||
|
||||
const filteredLogs = this.props.logs.filter((log) =>
|
||||
Object.entries(log).some(([key, value]) => {
|
||||
if (excludedKeys.has(key)) {
|
||||
return false;
|
||||
}
|
||||
return String(value).toLowerCase().includes(search.toLowerCase());
|
||||
}),
|
||||
);
|
||||
this.setState({filteredLogs});
|
||||
}, 200);
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.performSearch.cancel();
|
||||
}
|
||||
|
||||
onFiltersChange = ({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
|
||||
Ссылка в новой задаче
Block a user