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 LogList from './log_list';
|
||||||
import PlainLogList from './plain_log_list';
|
import PlainLogList from './plain_log_list';
|
||||||
|
|
||||||
|
type LogObjectWithAdditionalInfo = LogObject & {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
logs: LogObject[];
|
logs: LogObjectWithAdditionalInfo[];
|
||||||
plainLogs: string[];
|
plainLogs: string[];
|
||||||
isPlainLogs: boolean;
|
isPlainLogs: boolean;
|
||||||
actions: {
|
actions: {
|
||||||
@@ -121,13 +125,25 @@ export default class Logs extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
performSearch = debounce(() => {
|
performSearch = debounce(() => {
|
||||||
const {search} = this.state;
|
const {search} = this.state;
|
||||||
const filteredLogs = this.props.logs.filter((log) => {
|
|
||||||
// to be improved
|
// Excluding level and timestamp from search
|
||||||
return `${log.caller}${log.msg}${log.worker}${log.worker}`.toLowerCase().includes(search.toLowerCase());
|
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});
|
this.setState({filteredLogs});
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
this.performSearch.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
onFiltersChange = ({
|
onFiltersChange = ({
|
||||||
dateFrom,
|
dateFrom,
|
||||||
dateTo,
|
dateTo,
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user