MM-57014 Extended console log search to all log attributes (#30397)

Этот коммит содержится в:
Clément Collin
2025-05-12 13:43:40 +02:00
коммит произвёл GitHub
родитель bb8aabc15e
Коммит e0f54447b9
3 изменённых файлов: 619 добавлений и 5 удалений

Просмотреть файл

@@ -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 &gt; 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,