mirror of
https://github.com/grafana/grafana.git
synced 2025-01-27 16:57:14 -06:00
Search: Reduce unnecessary child component re-rendering (#49013)
This commit is contained in:
parent
a2ebdf2bc2
commit
e2e9616c87
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user