Memoize SearchResults (#35851)

This commit is contained in:
Alex Khomenko 2021-06-17 11:19:33 +03:00 committed by GitHub
parent 5f07381796
commit 91e9ef232d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 97 additions and 92 deletions

View File

@ -1,4 +1,4 @@
import React, { FC } from 'react'; import React, { FC, memo } from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { FixedSizeList } from 'react-window'; import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
@ -22,82 +22,78 @@ export interface Props {
const { section: sectionLabel, items: itemsLabel } = selectors.components.Search; const { section: sectionLabel, items: itemsLabel } = selectors.components.Search;
export const SearchResults: FC<Props> = ({ export const SearchResults: FC<Props> = memo(
editable, ({ editable, loading, onTagSelected, onToggleChecked, onToggleSection, results, layout }) => {
loading, const theme = useTheme();
onTagSelected, const styles = getSectionStyles(theme);
onToggleChecked, const itemProps = { editable, onToggleChecked, onTagSelected };
onToggleSection, const renderFolders = () => {
results, return (
layout, <div className={styles.wrapper}>
}) => { {results.map((section) => {
const theme = useTheme(); return (
const styles = getSectionStyles(theme); <div aria-label={sectionLabel} className={styles.section} key={section.id || section.title}>
const itemProps = { editable, onToggleChecked, onTagSelected }; <SectionHeader onSectionClick={onToggleSection} {...{ onToggleChecked, editable, section }} />
const renderFolders = () => { {section.expanded && (
return ( <div aria-label={itemsLabel} className={styles.sectionItems}>
<div className={styles.wrapper}> {section.items.map((item) => (
{results.map((section) => { <SearchItem key={item.id} {...itemProps} item={item} />
return ( ))}
<div aria-label={sectionLabel} className={styles.section} key={section.id || section.title}>
<SectionHeader onSectionClick={onToggleSection} {...{ onToggleChecked, editable, section }} />
{section.expanded && (
<div aria-label={itemsLabel} className={styles.sectionItems}>
{section.items.map((item) => (
<SearchItem key={item.id} {...itemProps} item={item} />
))}
</div>
)}
</div>
);
})}
</div>
);
};
const renderDashboards = () => {
const items = results[0]?.items;
return (
<div className={styles.listModeWrapper}>
<AutoSizer disableWidth>
{({ height }) => (
<FixedSizeList
aria-label="Search items"
className={styles.wrapper}
innerElementType="ul"
itemSize={SEARCH_ITEM_HEIGHT + SEARCH_ITEM_MARGIN}
height={height}
itemCount={items.length}
width="100%"
>
{({ index, style }) => {
const item = items[index];
// The wrapper div is needed as the inner SearchItem has margin-bottom spacing
// And without this wrapper there is no room for that margin
return (
<div style={style}>
<SearchItem key={item.id} {...itemProps} item={item} />
</div> </div>
); )}
}} </div>
</FixedSizeList> );
)} })}
</AutoSizer> </div>
);
};
const renderDashboards = () => {
const items = results[0]?.items;
return (
<div className={styles.listModeWrapper}>
<AutoSizer disableWidth>
{({ height }) => (
<FixedSizeList
aria-label="Search items"
className={styles.wrapper}
innerElementType="ul"
itemSize={SEARCH_ITEM_HEIGHT + SEARCH_ITEM_MARGIN}
height={height}
itemCount={items.length}
width="100%"
>
{({ index, style }) => {
const item = items[index];
// The wrapper div is needed as the inner SearchItem has margin-bottom spacing
// And without this wrapper there is no room for that margin
return (
<div style={style}>
<SearchItem key={item.id} {...itemProps} item={item} />
</div>
);
}}
</FixedSizeList>
)}
</AutoSizer>
</div>
);
};
if (loading) {
return <Spinner className={styles.spinner} />;
} else if (!results || !results.length) {
return <div className={styles.noResults}>No dashboards matching your query were found.</div>;
}
return (
<div className={styles.resultsContainer}>
{layout === SearchLayout.Folders ? renderFolders() : renderDashboards()}
</div> </div>
); );
};
if (loading) {
return <Spinner className={styles.spinner} />;
} else if (!results || !results.length) {
return <div className={styles.noResults}>No dashboards matching your query were found.</div>;
} }
);
return ( SearchResults.displayName = 'SearchResults';
<div className={styles.resultsContainer}>
{layout === SearchLayout.Folders ? renderFolders() : renderDashboards()}
</div>
);
};
const getSectionStyles = stylesFactory((theme: GrafanaTheme) => { const getSectionStyles = stylesFactory((theme: GrafanaTheme) => {
const { md } = theme.spacing; const { md } = theme.spacing;

View File

@ -1,4 +1,4 @@
import { useMemo, useReducer } from 'react'; import { useCallback, useMemo, useReducer } from 'react';
import { FolderDTO } from 'app/types'; import { FolderDTO } from 'app/types';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { DashboardQuery, DashboardSection, OnDeleteItems, OnMoveItems, OnToggleChecked } from '../types'; import { DashboardQuery, DashboardSection, OnDeleteItems, OnMoveItems, OnToggleChecked } from '../types';
@ -23,9 +23,12 @@ export const useManageDashboards = (
dispatch, dispatch,
} = useSearch<ManageDashboardsState>(query, reducer, {}); } = useSearch<ManageDashboardsState>(query, reducer, {});
const onToggleChecked: OnToggleChecked = (item) => { const onToggleChecked: OnToggleChecked = useCallback(
dispatch({ type: TOGGLE_CHECKED, payload: item }); (item) => {
}; dispatch({ type: TOGGLE_CHECKED, payload: item });
},
[dispatch]
);
const onToggleAllChecked = () => { const onToggleAllChecked = () => {
dispatch({ type: TOGGLE_ALL_CHECKED }); dispatch({ type: TOGGLE_ALL_CHECKED });

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useDebounce } from 'react-use'; import { useDebounce } from 'react-use';
import { SearchSrv } from 'app/core/services/search_srv'; import { SearchSrv } from 'app/core/services/search_srv';
import { backendSrv } from 'app/core/services/backend_srv'; import { backendSrv } from 'app/core/services/backend_srv';
@ -35,17 +35,20 @@ export const useSearch: UseSearch = (query, reducer, params = {}) => {
useDebounce(search, 300, [query, queryParsing]); useDebounce(search, 300, [query, queryParsing]);
const onToggleSection = (section: DashboardSection) => { const onToggleSection = useCallback(
if (hasId(section.title) && !section.items.length) { (section: DashboardSection) => {
dispatch({ type: FETCH_ITEMS_START, payload: section.id }); if (hasId(section.title) && !section.items.length) {
backendSrv.search({ folderIds: [section.id] }).then((items) => { dispatch({ type: FETCH_ITEMS_START, payload: section.id });
dispatch({ type: FETCH_ITEMS, payload: { section, items } }); backendSrv.search({ folderIds: [section.id] }).then((items) => {
dispatch({ type: FETCH_ITEMS, payload: { section, items } });
dispatch({ type: TOGGLE_SECTION, payload: section });
});
} else {
dispatch({ type: TOGGLE_SECTION, payload: section }); dispatch({ type: TOGGLE_SECTION, payload: section });
}); }
} else { },
dispatch({ type: TOGGLE_SECTION, payload: section }); [dispatch]
} );
};
return { state, dispatch, onToggleSection }; return { state, dispatch, onToggleSection };
}; };

View File

@ -1,4 +1,4 @@
import { FormEvent, useReducer } from 'react'; import { FormEvent, useCallback, useReducer } from 'react';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { locationService } from '@grafana/runtime'; import { locationService } from '@grafana/runtime';
@ -32,10 +32,13 @@ export const useSearchQuery = (defaults: Partial<DashboardQuery>) => {
updateLocation({ tag: tags }); updateLocation({ tag: tags });
}; };
const onTagAdd = (tag: string) => { const onTagAdd = useCallback(
dispatch({ type: ADD_TAG, payload: tag }); (tag: string) => {
updateLocation({ tag: [...query.tag, tag] }); dispatch({ type: ADD_TAG, payload: tag });
}; updateLocation({ tag: [...query.tag, tag] });
},
[query.tag]
);
const onClearFilters = () => { const onClearFilters = () => {
dispatch({ type: CLEAR_FILTERS }); dispatch({ type: CLEAR_FILTERS });