diff --git a/public/app/features/search/hooks/useSearchQuery.ts b/public/app/features/search/hooks/useSearchQuery.ts index 9b425e1066d..359ca90a8a1 100644 --- a/public/app/features/search/hooks/useSearchQuery.ts +++ b/public/app/features/search/hooks/useSearchQuery.ts @@ -25,20 +25,20 @@ export const useSearchQuery = (defaults: Partial) => { 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) => { [query.tag] ); - const onClearFilters = () => { + const onClearFilters = useCallback(() => { dispatch({ type: CLEAR_FILTERS }); updateLocation(defaultQueryParams); - }; + }, []); - const onStarredFilterChange = (e: FormEvent) => { + const onStarredFilterChange = useCallback((e: FormEvent) => { 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, diff --git a/public/app/features/search/page/SearchPage.tsx b/public/app/features/search/page/SearchPage.tsx index 66b5a8c6ac7..2a83b0b11a2 100644 --- a/public/app/features/search/page/SearchPage.tsx +++ b/public/app/features/search/page/SearchPage.tsx @@ -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
Unsupported
; } @@ -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 ; + return ; } return ( @@ -143,7 +141,7 @@ export default function SearchPage() { selectionToggle: toggleSelection, width: width, height: height, - onTagSelected: onTagSelected, + onTagSelected: onTagAdd, onDatasourceChange: query.datasource ? onDatasourceChange : undefined, }; diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index b6e3173f3c7..ee1ab1dbf74 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -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 ( +
+ {row.cells.map((cell: Cell, index: number) => { + return ( + + ); + })} +
+ ); + }, + [rows, prepareRow, response.view.fields.url?.values, styles.rowContainer, tableStyles] + ); + + if (!rows.length) { + return
No data
; } - // 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 ( +
+
+ {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 ( -
- {row.cells.map((cell: Cell, index: number) => { return ( - +
+ {headerGroup.headers.map((column) => { + const { key, ...headerProps } = column.getHeaderProps(); + return ( +
+ {column.render('Header')} +
+ ); + })} +
); })}
- ); - }, - [rows, prepareRow, response.view.fields.url?.values, styles.rowContainer, tableStyles] - ); - if (!rows.length) { - return
No data
; +
+ + {({ onItemsRendered, ref }) => ( + + {RenderRow} + + )} + +
+
+ ); } - - return ( -
-
- {headerGroups.map((headerGroup) => { - const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps(); - - return ( -
- {headerGroup.headers.map((column) => { - const { key, ...headerProps } = column.getHeaderProps(); - return ( -
- {column.render('Header')} -
- ); - })} -
- ); - })} -
- -
- - {({ onItemsRendered, ref }) => ( - - {RenderRow} - - )} - -
-
- ); -}; +); +SearchResultsTable.displayName = 'SearchResultsTable'; const getStyles = (theme: GrafanaTheme2) => { const rowHoverBg = theme.colors.emphasize(theme.colors.background.primary, 0.03);