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 { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
@ -22,82 +22,78 @@ export interface Props {
const { section: sectionLabel, items: itemsLabel } = selectors.components.Search;
export const SearchResults: FC<Props> = ({
editable,
loading,
onTagSelected,
onToggleChecked,
onToggleSection,
results,
layout,
}) => {
const theme = useTheme();
const styles = getSectionStyles(theme);
const itemProps = { editable, onToggleChecked, onTagSelected };
const renderFolders = () => {
return (
<div className={styles.wrapper}>
{results.map((section) => {
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} />
export const SearchResults: FC<Props> = memo(
({ editable, loading, onTagSelected, onToggleChecked, onToggleSection, results, layout }) => {
const theme = useTheme();
const styles = getSectionStyles(theme);
const itemProps = { editable, onToggleChecked, onTagSelected };
const renderFolders = () => {
return (
<div className={styles.wrapper}>
{results.map((section) => {
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>
);
}}
</FixedSizeList>
)}
</AutoSizer>
)}
</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>
);
}}
</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>
);
};
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>
);
};
SearchResults.displayName = 'SearchResults';
const getSectionStyles = stylesFactory((theme: GrafanaTheme) => {
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 { contextSrv } from 'app/core/services/context_srv';
import { DashboardQuery, DashboardSection, OnDeleteItems, OnMoveItems, OnToggleChecked } from '../types';
@ -23,9 +23,12 @@ export const useManageDashboards = (
dispatch,
} = useSearch<ManageDashboardsState>(query, reducer, {});
const onToggleChecked: OnToggleChecked = (item) => {
dispatch({ type: TOGGLE_CHECKED, payload: item });
};
const onToggleChecked: OnToggleChecked = useCallback(
(item) => {
dispatch({ type: TOGGLE_CHECKED, payload: item });
},
[dispatch]
);
const onToggleAllChecked = () => {
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 { SearchSrv } from 'app/core/services/search_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]);
const onToggleSection = (section: DashboardSection) => {
if (hasId(section.title) && !section.items.length) {
dispatch({ type: FETCH_ITEMS_START, payload: section.id });
backendSrv.search({ folderIds: [section.id] }).then((items) => {
dispatch({ type: FETCH_ITEMS, payload: { section, items } });
const onToggleSection = useCallback(
(section: DashboardSection) => {
if (hasId(section.title) && !section.items.length) {
dispatch({ type: FETCH_ITEMS_START, payload: section.id });
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 });
});
} else {
dispatch({ type: TOGGLE_SECTION, payload: section });
}
};
}
},
[dispatch]
);
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 { SelectableValue } from '@grafana/data';
import { locationService } from '@grafana/runtime';
@ -32,10 +32,13 @@ export const useSearchQuery = (defaults: Partial<DashboardQuery>) => {
updateLocation({ tag: tags });
};
const onTagAdd = (tag: string) => {
dispatch({ type: ADD_TAG, payload: tag });
updateLocation({ tag: [...query.tag, tag] });
};
const onTagAdd = useCallback(
(tag: string) => {
dispatch({ type: ADD_TAG, payload: tag });
updateLocation({ tag: [...query.tag, tag] });
},
[query.tag]
);
const onClearFilters = () => {
dispatch({ type: CLEAR_FILTERS });