[MM-56758] Enhance search implementation for Admin console (#26099)

This commit is contained in:
M-ZubairAhmed 2024-02-06 12:14:44 +00:00 committed by GitHub
parent 9ff26f7b4f
commit e7f537e502
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 130 additions and 112 deletions

View File

@ -12,6 +12,7 @@ import type {Role} from '@mattermost/types/roles';
import type {ActionResult} from 'mattermost-redux/types/actions';
import SchemaAdminSettings from 'components/admin_console/schema_admin_settings';
import SearchKeywordMarking from 'components/admin_console/search_keyword_marking';
import AnnouncementBarController from 'components/announcement_bar';
import BackstageNavbar from 'components/backstage/components/backstage_navbar';
import DiscardChangesModal from 'components/discard_changes_modal';
@ -23,7 +24,6 @@ import {applyTheme, resetTheme} from 'utils/utils';
import {LhsItemType} from 'types/store/lhs';
import AdminSidebar from './admin_sidebar';
import Highlight from './highlight';
import type {AdminDefinitionSubSection, AdminDefinitionSection} from './types';
import type {PropsFromRedux} from './index';
@ -31,7 +31,7 @@ import type {PropsFromRedux} from './index';
export type Props = PropsFromRedux & RouteComponentProps;
type State = {
filter: string;
search: string;
}
// not every page in the system console will need the license and config, but the vast majority will
@ -48,11 +48,11 @@ type ExtraProps = {
isCurrentUserSystemAdmin: boolean;
}
export default class AdminConsole extends React.PureComponent<Props, State> {
class AdminConsole extends React.PureComponent<Props, State> {
public constructor(props: Props) {
super(props);
this.state = {
filter: '',
search: '',
};
}
@ -73,8 +73,8 @@ export default class AdminConsole extends React.PureComponent<Props, State> {
applyTheme(this.props.currentTheme);
}
private onFilterChange = (filter: string) => {
this.setState({filter});
private handleSearchChange = (search: string) => {
this.setState({search});
};
private mainRolesLoaded(roles: Record<string, Role>) {
@ -192,14 +192,6 @@ export default class AdminConsole extends React.PureComponent<Props, State> {
);
}
const discardChangesModal: JSX.Element = (
<DiscardChangesModal
show={showNavigationPrompt}
onConfirm={confirmNavigation}
onCancel={cancelNavigation}
/>
);
const extraProps: ExtraProps = {
enterpriseReady: this.props.buildEnterpriseReady,
license,
@ -212,23 +204,33 @@ export default class AdminConsole extends React.PureComponent<Props, State> {
cloud: this.props.cloud,
isCurrentUserSystemAdmin: this.props.isCurrentUserSystemAdmin,
};
return (
<>
<AnnouncementBarController/>
<SystemNotice/>
<BackstageNavbar team={this.props.team}/>
<AdminSidebar onFilterChange={this.onFilterChange}/>
<AdminSidebar onSearchChange={this.handleSearchChange}/>
<div
className='admin-console__wrapper admin-console'
id='adminConsoleWrapper'
>
<Highlight filter={this.state.filter}>
<SearchKeywordMarking
keyword={this.state.search}
pathname={this.props.location.pathname}
>
{this.renderRoutes(extraProps)}
</Highlight>
</SearchKeywordMarking>
</div>
{discardChangesModal}
<DiscardChangesModal
show={showNavigationPrompt}
onConfirm={confirmNavigation}
onCancel={cancelNavigation}
/>
<ModalController/>
</>
);
}
}
export default AdminConsole;

View File

@ -43,8 +43,8 @@ exports[`components/AdminSidebar Plugins should filter plugins 1`] = `
<div
className="nav-pills__container"
>
<Highlight
filter="autolink"
<SearchKeywordMarking
keyword="autolink"
>
<ul
className="nav nav-pills nav-stacked"
@ -74,7 +74,7 @@ exports[`components/AdminSidebar Plugins should filter plugins 1`] = `
/>
</AdminSidebarCategory>
</ul>
</Highlight>
</SearchKeywordMarking>
</div>
</Scrollbars>
</div>
@ -123,8 +123,8 @@ exports[`components/AdminSidebar Plugins should match snapshot 1`] = `
<div
className="nav-pills__container"
>
<Highlight
filter=""
<SearchKeywordMarking
keyword=""
>
<ul
className="nav nav-pills nav-stacked"
@ -481,7 +481,7 @@ exports[`components/AdminSidebar Plugins should match snapshot 1`] = `
/>
</AdminSidebarCategory>
</ul>
</Highlight>
</SearchKeywordMarking>
</div>
</Scrollbars>
</div>
@ -530,8 +530,8 @@ exports[`components/AdminSidebar should match snapshot 1`] = `
<div
className="nav-pills__container"
>
<Highlight
filter=""
<SearchKeywordMarking
keyword=""
>
<ul
className="nav nav-pills nav-stacked"
@ -1175,7 +1175,7 @@ exports[`components/AdminSidebar should match snapshot 1`] = `
/>
</AdminSidebarCategory>
</ul>
</Highlight>
</SearchKeywordMarking>
</div>
</Scrollbars>
</div>
@ -1224,8 +1224,8 @@ exports[`components/AdminSidebar should match snapshot with workspace optimizati
<div
className="nav-pills__container"
>
<Highlight
filter=""
<SearchKeywordMarking
keyword=""
>
<ul
className="nav nav-pills nav-stacked"
@ -1869,7 +1869,7 @@ exports[`components/AdminSidebar should match snapshot with workspace optimizati
/>
</AdminSidebarCategory>
</ul>
</Highlight>
</SearchKeywordMarking>
</div>
</Scrollbars>
</div>
@ -1918,13 +1918,13 @@ exports[`components/AdminSidebar should match snapshot, no access 1`] = `
<div
className="nav-pills__container"
>
<Highlight
filter=""
<SearchKeywordMarking
keyword=""
>
<ul
className="nav nav-pills nav-stacked"
/>
</Highlight>
</SearchKeywordMarking>
</div>
</Scrollbars>
</div>
@ -1973,8 +1973,8 @@ exports[`components/AdminSidebar should match snapshot, not prevent the console
<div
className="nav-pills__container"
>
<Highlight
filter=""
<SearchKeywordMarking
keyword=""
>
<ul
className="nav nav-pills nav-stacked"
@ -2618,7 +2618,7 @@ exports[`components/AdminSidebar should match snapshot, not prevent the console
/>
</AdminSidebarCategory>
</ul>
</Highlight>
</SearchKeywordMarking>
</div>
</Scrollbars>
</div>
@ -2667,8 +2667,8 @@ exports[`components/AdminSidebar should match snapshot, render plugins without a
<div
className="nav-pills__container"
>
<Highlight
filter=""
<SearchKeywordMarking
keyword=""
>
<ul
className="nav nav-pills nav-stacked"
@ -3312,7 +3312,7 @@ exports[`components/AdminSidebar should match snapshot, render plugins without a
/>
</AdminSidebarCategory>
</ul>
</Highlight>
</SearchKeywordMarking>
</div>
</Scrollbars>
</div>
@ -3361,8 +3361,8 @@ exports[`components/AdminSidebar should match snapshot, with license (with all f
<div
className="nav-pills__container"
>
<Highlight
filter=""
<SearchKeywordMarking
keyword=""
>
<ul
className="nav nav-pills nav-stacked"
@ -4168,7 +4168,7 @@ exports[`components/AdminSidebar should match snapshot, with license (with all f
/>
</AdminSidebarCategory>
</ul>
</Highlight>
</SearchKeywordMarking>
</div>
</Scrollbars>
</div>
@ -4217,8 +4217,8 @@ exports[`components/AdminSidebar should match snapshot, with license (without an
<div
className="nav-pills__container"
>
<Highlight
filter=""
<SearchKeywordMarking
keyword=""
>
<ul
className="nav nav-pills nav-stacked"
@ -5121,7 +5121,7 @@ exports[`components/AdminSidebar should match snapshot, with license (without an
/>
</AdminSidebarCategory>
</ul>
</Highlight>
</SearchKeywordMarking>
</div>
</Scrollbars>
</div>

View File

@ -61,7 +61,7 @@ describe('components/AdminSidebar', () => {
webapp: {bundle_path: 'webapp/dist/main.js'},
},
},
onFilterChange: jest.fn(),
onSearchChange: jest.fn(),
actions: {
getPlugins: jest.fn(),
},
@ -175,7 +175,7 @@ describe('components/AdminSidebar', () => {
webapp: {bundle_path: 'webapp/dist/main.js'},
},
},
onFilterChange: jest.fn(),
onSearchChange: jest.fn(),
actions: {
getPlugins: jest.fn(),
},
@ -221,7 +221,7 @@ describe('components/AdminSidebar', () => {
webapp: {bundle_path: 'webapp/dist/main.js'},
},
},
onFilterChange: jest.fn(),
onSearchChange: jest.fn(),
actions: {
getPlugins: jest.fn(),
},
@ -269,7 +269,7 @@ describe('components/AdminSidebar', () => {
webapp: {bundle_path: 'webapp/dist/main.js'},
},
},
onFilterChange: jest.fn(),
onSearchChange: jest.fn(),
actions: {
getPlugins: jest.fn(),
},
@ -345,7 +345,7 @@ describe('components/AdminSidebar', () => {
webapp: {bundle_path: 'webapp/dist/main.js'},
},
},
onFilterChange: jest.fn(),
onSearchChange: jest.fn(),
actions: {
getPlugins: jest.fn(),
},
@ -379,7 +379,7 @@ describe('components/AdminSidebar', () => {
plugins: {
'mattermost-autolink': samplePlugin1,
},
onFilterChange: jest.fn(),
onSearchChange: jest.fn(),
actions: {
getPlugins: jest.fn(),
},
@ -467,7 +467,7 @@ describe('components/AdminSidebar', () => {
plugins: {
'mattermost-autolink': samplePlugin1,
},
onFilterChange: jest.fn(),
onSearchChange: jest.fn(),
actions: {
getPlugins: jest.fn(),
},

View File

@ -13,7 +13,7 @@ import type {PluginRedux} from '@mattermost/types/plugins';
import AdminSidebarCategory from 'components/admin_console/admin_sidebar/admin_sidebar_category';
import AdminSidebarSection from 'components/admin_console/admin_sidebar/admin_sidebar_section';
import AdminSidebarHeader from 'components/admin_console/admin_sidebar_header';
import Highlight from 'components/admin_console/highlight';
import SearchKeywordMarking from 'components/admin_console/search_keyword_marking';
import QuickInput from 'components/quick_input';
import SearchIcon from 'components/widgets/icons/search_icon';
@ -27,7 +27,7 @@ import type {PropsFromRedux} from './index';
export interface Props extends PropsFromRedux {
intl: IntlShape;
onFilterChange: (term: string) => void;
onSearchChange: (term: string) => void;
}
type State = {
@ -94,11 +94,11 @@ class AdminSidebar extends React.PureComponent<Props, State> {
}
}
onFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const filter = e.target.value;
if (filter === '') {
this.setState({sections: null, filter});
this.props.onFilterChange(filter);
this.props.onSearchChange(filter);
return;
}
@ -115,7 +115,7 @@ class AdminSidebar extends React.PureComponent<Props, State> {
}
const sections = this.idx.search(query);
this.setState({sections, filter});
this.props.onFilterChange(filter);
this.props.onSearchChange(filter);
if (this.props.navigationBlocked) {
return;
@ -286,7 +286,7 @@ class AdminSidebar extends React.PureComponent<Props, State> {
handleClearFilter = () => {
this.setState({sections: null, filter: ''});
this.props.onFilterChange('');
this.props.onSearchChange('');
};
render() {
@ -302,7 +302,7 @@ class AdminSidebar extends React.PureComponent<Props, State> {
<QuickInput
className={'filter ' + (this.state.filter ? 'active' : '')}
type='text'
onChange={this.onFilterChange}
onChange={this.handleSearchChange}
value={this.state.filter}
placeholder={this.props.intl.formatMessage({id: 'admin.sidebar.filter', defaultMessage: 'Find settings'})}
ref={this.searchRef}
@ -320,11 +320,11 @@ class AdminSidebar extends React.PureComponent<Props, State> {
renderView={renderScrollView}
>
<div className='nav-pills__container'>
<Highlight filter={this.state.filter}>
<SearchKeywordMarking keyword={this.state.filter}>
<ul className={classNames('nav nav-pills nav-stacked', {'task-list-shown': showTaskList})}>
{this.renderRootMenu(this.props.adminDefinition)}
</ul>
</Highlight>
</SearchKeywordMarking>
</div>
</Scrollbars>
</div>

View File

@ -1,49 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import debounce from 'lodash/debounce';
import Mark from 'mark.js';
import React from 'react';
type Props = {
filter: string;
children: React.ReactNode;
}
export default class Highlight extends React.PureComponent<Props> {
private markInstance?: Mark;
private ref: React.RefObject<HTMLDivElement>;
public constructor(props: Props) {
super(props);
this.ref = React.createRef<HTMLDivElement>();
}
private redrawHighlight = debounce(() => {
if (this.markInstance) {
this.markInstance.unmark();
}
if (!this.props.filter) {
return;
}
if (!this.ref.current) {
return;
}
// Is necesary to recreate the instances to get again the DOM elements after the re-render
this.markInstance = new Mark(this.ref.current);
this.markInstance.mark(this.props.filter, {accuracy: 'complementary'});
}, 100, {leading: true, trailing: true});
public render() {
// Run on next frame
setTimeout(this.redrawHighlight, 0);
return (
<div ref={this.ref}>
{this.props.children}
</div>
);
}
}

View File

@ -0,0 +1,65 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import debounce from 'lodash/debounce';
import Mark from 'mark.js';
import React, {useEffect, useMemo, useRef} from 'react';
import type {ReactNode} from 'react';
type Props = {
keyword?: string;
pathname?: string;
children: ReactNode;
}
const DEBOUNCE_WAIT_TIME = 200;
export default function SearchKeywordMarking({
keyword = '',
pathname,
children,
}: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const markJsRef = useRef<Mark>();
function doMark(keyword: string, container: HTMLDivElement) {
markJsRef.current = new Mark(container);
markJsRef.current.mark(keyword, {
accuracy: 'complementary',
exclude: ['.ignore-marking *'],
});
}
const debouncedDoMark = useMemo(() => debounce((keywordToMark?: string, markJs?: Mark, container?: HTMLDivElement | null) => {
if (!keywordToMark || !container) {
return;
}
if (markJs) {
// We need to mark again only after its 'done' callback is called
// if we dont then there is a possiblity of creating multiple marks in the same container
markJs.unmark({done: () => doMark(keywordToMark, container)});
} else {
// If there's no previous instance, just create a new one
doMark(keywordToMark, container);
}
}, DEBOUNCE_WAIT_TIME), []);
useEffect(() => {
debouncedDoMark(keyword, markJsRef.current, containerRef.current);
return (() => {
debouncedDoMark.cancel();
if (markJsRef.current) {
markJsRef.current.unmark();
}
});
}, [keyword, pathname]);
return (
<div ref={containerRef}>
{children}
</div>
);
}

View File

@ -17,7 +17,7 @@ exports[`components/admin_console/system_users should match default snapshot 1`]
<RevokeSessionsButton />
</AdminHeader>
<div
className="admin-console__wrapper"
className="admin-console__wrapper ignore-marking"
>
<div
className="admin-console__content"

View File

@ -315,7 +315,7 @@ export class SystemUsers extends React.PureComponent<Props, State> {
/>
<RevokeSessionsButton/>
</AdminHeader>
<div className='admin-console__wrapper'>
<div className='admin-console__wrapper ignore-marking'>
<div className='admin-console__content'>
<div className='more-modal__list member-list-holder'>
<div className='system-users__filter-row'>