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

Этот коммит содержится в:
M-ZubairAhmed
2024-02-06 12:14:44 +00:00
коммит произвёл GitHub
родитель 9ff26f7b4f
Коммит e7f537e502
8 изменённых файлов: 130 добавлений и 112 удалений

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

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

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

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

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

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

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

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

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

@@ -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>
);
}
}

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

@@ -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>
);
}

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

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

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

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