Search: Reduce unnecessary child component re-rendering (#49013)

This commit is contained in:
kay delaney 2022-05-16 16:36:34 +01:00 committed by GitHub
parent a2ebdf2bc2
commit e2e9616c87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 127 additions and 134 deletions

View File

@ -25,20 +25,20 @@ export const useSearchQuery = (defaults: Partial<DashboardQuery>) => {
const initialState = { ...defaultQuery, ...defaults, ...queryParams };
const [query, dispatch] = useReducer(queryReducer, initialState);
const onQueryChange = (query: string) => {
const onQueryChange = useCallback((query: string) => {
dispatch({ type: QUERY_CHANGE, payload: query });
updateLocation({ query });
};
}, []);
const onTagFilterChange = (tags: string[]) => {
const onTagFilterChange = useCallback((tags: string[]) => {
dispatch({ type: SET_TAGS, payload: tags });
updateLocation({ tag: tags });
};
}, []);
const onDatasourceChange = (datasource?: string) => {
const onDatasourceChange = useCallback((datasource?: string) => {
dispatch({ type: DATASOURCE_CHANGE, payload: datasource });
updateLocation({ datasource });
};
}, []);
const onTagAdd = useCallback(
(tag: string) => {
@ -48,30 +48,30 @@ export const useSearchQuery = (defaults: Partial<DashboardQuery>) => {
[query.tag]
);
const onClearFilters = () => {
const onClearFilters = useCallback(() => {
dispatch({ type: CLEAR_FILTERS });
updateLocation(defaultQueryParams);
};
}, []);
const onStarredFilterChange = (e: FormEvent<HTMLInputElement>) => {
const onStarredFilterChange = useCallback((e: FormEvent<HTMLInputElement>) => {
const starred = (e.target as HTMLInputElement).checked;
dispatch({ type: TOGGLE_STARRED, payload: starred });
updateLocation({ starred: starred || null });
};
}, []);
const onSortChange = (sort: SelectableValue | null) => {
const onSortChange = useCallback((sort: SelectableValue | null) => {
dispatch({ type: TOGGLE_SORT, payload: sort });
updateLocation({ sort: sort?.value, layout: SearchLayout.List });
};
}, []);
const onLayoutChange = (layout: SearchLayout) => {
const onLayoutChange = useCallback((layout: SearchLayout) => {
dispatch({ type: LAYOUT_CHANGE, payload: layout });
if (layout === SearchLayout.Folders) {
updateLocation({ layout, sort: null });
return;
}
updateLocation({ layout });
};
}, []);
return {
query,

View File

@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { useAsync, useDebounce } from 'react-use';
import AutoSizer from 'react-virtualized-auto-sizer';
@ -31,9 +31,8 @@ const node: NavModelItem = {
export default function SearchPage() {
const styles = useStyles2(getStyles);
const { query, onQueryChange, onTagFilterChange, onDatasourceChange, onSortChange, onLayoutChange } = useSearchQuery(
{}
);
const { query, onQueryChange, onTagFilterChange, onTagAdd, onDatasourceChange, onSortChange, onLayoutChange } =
useSearchQuery({});
const [showManage, setShowManage] = useState(false); // grid vs list view
const [searchSelection, setSearchSelection] = useState(newSearchSelection());
@ -62,6 +61,17 @@ export default function SearchPage() {
useDebounce(() => onQueryChange(inputValue), 200, [inputValue]);
const toggleSelection = useCallback(
(kind: string, uid: string) => {
const current = searchSelection.isSelected(kind, uid);
if (kind === 'folder') {
// ??? also select all children?
}
setSearchSelection(updateSearchSelection(searchSelection, !current, kind, [uid]));
},
[searchSelection]
);
if (!config.featureToggles.panelTitleSearch) {
return <div className={styles.unsupported}>Unsupported</div>;
}
@ -76,18 +86,6 @@ export default function SearchPage() {
return getGrafanaSearcher().tags(q);
};
const onTagSelected = (tag: string) => {
onTagFilterChange([...new Set(query.tag as string[]).add(tag)]);
};
const toggleSelection = (kind: string, uid: string) => {
const current = searchSelection.isSelected(kind, uid);
if (kind === 'folder') {
// ??? also select all children?
}
setSearchSelection(updateSearchSelection(searchSelection, !current, kind, [uid]));
};
// function to update items when dashboards or folders are moved or deleted
const onChangeItemsList = async () => {
// clean up search selection
@ -130,7 +128,7 @@ export default function SearchPage() {
const selection = showManage ? searchSelection.isSelected : undefined;
if (layout === SearchLayout.Folders) {
return <FolderView selection={selection} selectionToggle={toggleSelection} onTagSelected={onTagSelected} />;
return <FolderView selection={selection} selectionToggle={toggleSelection} onTagSelected={onTagAdd} />;
}
return (
@ -143,7 +141,7 @@ export default function SearchPage() {
selectionToggle: toggleSelection,
width: width,
height: height,
onTagSelected: onTagSelected,
onTagSelected: onTagAdd,
onDatasourceChange: query.datasource ? onDatasourceChange : undefined,
};

View File

@ -31,118 +31,113 @@ export type TableColumn = Column & {
const HEADER_HEIGHT = 36; // pixels
export const SearchResultsTable = ({
response,
width,
height,
selection,
selectionToggle,
onTagSelected,
onDatasourceChange,
}: SearchResultsProps) => {
const styles = useStyles2(getStyles);
const tableStyles = useStyles2(getTableStyles);
export const SearchResultsTable = React.memo(
({ response, width, height, selection, selectionToggle, onTagSelected, onDatasourceChange }: SearchResultsProps) => {
const styles = useStyles2(getStyles);
const tableStyles = useStyles2(getTableStyles);
const memoizedData = useMemo(() => {
if (!response?.view?.dataFrame.fields.length) {
return [];
const memoizedData = useMemo(() => {
if (!response?.view?.dataFrame.fields.length) {
return [];
}
// as we only use this to fake the length of our data set for react-table we need to make sure we always return an array
// filled with values at each index otherwise we'll end up trying to call accessRow for null|undefined value in
// https://github.com/tannerlinsley/react-table/blob/7be2fc9d8b5e223fc998af88865ae86a88792fdb/src/hooks/useTable.js#L585
return Array(response.totalRows).fill(0);
}, [response]);
// React-table column definitions
const memoizedColumns = useMemo(() => {
return generateColumns(response, width, selection, selectionToggle, styles, onTagSelected, onDatasourceChange);
}, [response, width, styles, selection, selectionToggle, onTagSelected, onDatasourceChange]);
const options: TableOptions<{}> = useMemo(
() => ({
columns: memoizedColumns,
data: memoizedData,
}),
[memoizedColumns, memoizedData]
);
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(options, useAbsoluteLayout);
const RenderRow = React.useCallback(
({ index: rowIndex, style }) => {
const row = rows[rowIndex];
prepareRow(row);
const url = response.view.fields.url?.values.get(rowIndex);
return (
<div {...row.getRowProps({ style })} className={styles.rowContainer}>
{row.cells.map((cell: Cell, index: number) => {
return (
<TableCell
key={index}
tableStyles={tableStyles}
cell={cell}
columnIndex={index}
columnCount={row.cells.length}
userProps={{ href: url }}
/>
);
})}
</div>
);
},
[rows, prepareRow, response.view.fields.url?.values, styles.rowContainer, tableStyles]
);
if (!rows.length) {
return <div className={styles.noData}>No data</div>;
}
// as we only use this to fake the length of our data set for react-table we need to make sure we always return an array
// filled with values at each index otherwise we'll end up trying to call accessRow for null|undefined value in
// https://github.com/tannerlinsley/react-table/blob/7be2fc9d8b5e223fc998af88865ae86a88792fdb/src/hooks/useTable.js#L585
return Array(response.totalRows).fill(0);
}, [response]);
// React-table column definitions
const memoizedColumns = useMemo(() => {
return generateColumns(response, width, selection, selectionToggle, styles, onTagSelected, onDatasourceChange);
}, [response, width, styles, selection, selectionToggle, onTagSelected, onDatasourceChange]);
return (
<div {...getTableProps()} aria-label="Search result table" role="table">
<div>
{headerGroups.map((headerGroup) => {
const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps();
const options: TableOptions<{}> = useMemo(
() => ({
columns: memoizedColumns,
data: memoizedData,
}),
[memoizedColumns, memoizedData]
);
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(options, useAbsoluteLayout);
const RenderRow = React.useCallback(
({ index: rowIndex, style }) => {
const row = rows[rowIndex];
prepareRow(row);
const url = response.view.fields.url?.values.get(rowIndex);
return (
<div {...row.getRowProps({ style })} className={styles.rowContainer}>
{row.cells.map((cell: Cell, index: number) => {
return (
<TableCell
key={index}
tableStyles={tableStyles}
cell={cell}
columnIndex={index}
columnCount={row.cells.length}
userProps={{ href: url }}
/>
<div key={key} {...headerGroupProps} className={styles.headerRow}>
{headerGroup.headers.map((column) => {
const { key, ...headerProps } = column.getHeaderProps();
return (
<div key={key} {...headerProps} role="columnheader" className={styles.headerCell}>
{column.render('Header')}
</div>
);
})}
</div>
);
})}
</div>
);
},
[rows, prepareRow, response.view.fields.url?.values, styles.rowContainer, tableStyles]
);
if (!rows.length) {
return <div className={styles.noData}>No data</div>;
<div {...getTableBodyProps()}>
<InfiniteLoader
isItemLoaded={response.isItemLoaded}
itemCount={rows.length}
loadMoreItems={response.loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
ref={ref}
onItemsRendered={onItemsRendered}
height={height - HEADER_HEIGHT}
itemCount={rows.length}
itemSize={tableStyles.rowHeight}
width="100%"
style={{ overflow: 'hidden auto' }}
>
{RenderRow}
</FixedSizeList>
)}
</InfiniteLoader>
</div>
</div>
);
}
return (
<div {...getTableProps()} aria-label="Search result table" role="table">
<div>
{headerGroups.map((headerGroup) => {
const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps();
return (
<div key={key} {...headerGroupProps} className={styles.headerRow}>
{headerGroup.headers.map((column) => {
const { key, ...headerProps } = column.getHeaderProps();
return (
<div key={key} {...headerProps} role="columnheader" className={styles.headerCell}>
{column.render('Header')}
</div>
);
})}
</div>
);
})}
</div>
<div {...getTableBodyProps()}>
<InfiniteLoader
isItemLoaded={response.isItemLoaded}
itemCount={rows.length}
loadMoreItems={response.loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
ref={ref}
onItemsRendered={onItemsRendered}
height={height - HEADER_HEIGHT}
itemCount={rows.length}
itemSize={tableStyles.rowHeight}
width="100%"
style={{ overflow: 'hidden auto' }}
>
{RenderRow}
</FixedSizeList>
)}
</InfiniteLoader>
</div>
</div>
);
};
);
SearchResultsTable.displayName = 'SearchResultsTable';
const getStyles = (theme: GrafanaTheme2) => {
const rowHoverBg = theme.colors.emphasize(theme.colors.background.primary, 0.03);