diff --git a/public/app/features/search/page/components/SearchResultsTable.test.tsx b/public/app/features/search/page/components/SearchResultsTable.test.tsx new file mode 100644 index 00000000000..eba70c2b4d1 --- /dev/null +++ b/public/app/features/search/page/components/SearchResultsTable.test.tsx @@ -0,0 +1,162 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { Subject } from 'rxjs'; + +import { + applyFieldOverrides, + ArrayVector, + createTheme, + DataFrame, + DataFrameView, + FieldType, + toDataFrame, +} from '@grafana/data'; + +import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from '../../service'; +import { DashboardSearchItemType } from '../../types'; + +import { SearchResultsTable } from './SearchResultsTable'; + +describe('SearchResultsTable', () => { + const mockOnTagSelected = jest.fn(); + const mockClearSelection = jest.fn(); + const mockSelectionToggle = jest.fn(); + const mockSelection = jest.fn(); + const mockKeyboardEvents = new Subject(); + + describe('when there is data', () => { + const searchData = toDataFrame({ + name: 'A', + fields: [ + { name: 'kind', type: FieldType.string, config: {}, values: [DashboardSearchItemType.DashDB] }, + { name: 'uid', type: FieldType.string, config: {}, values: ['my-dashboard-1'] }, + { name: 'name', type: FieldType.string, config: {}, values: ['My dashboard 1'] }, + { name: 'panel_type', type: FieldType.string, config: {}, values: [''] }, + { name: 'url', type: FieldType.string, config: {}, values: ['/my-dashboard-1'] }, + { name: 'tags', type: FieldType.other, config: {}, values: [['foo', 'bar']] }, + { name: 'ds_uid', type: FieldType.other, config: {}, values: [''] }, + { name: 'location', type: FieldType.string, config: {}, values: ['/my-dashboard-1'] }, + ], + }); + const dataFrames = applyFieldOverrides({ + data: [searchData], + fieldConfig: { + defaults: {}, + overrides: [], + }, + replaceVariables: (value, vars, format) => { + return vars && value === '${__value.text}' ? vars['__value'].value.text : value; + }, + theme: createTheme(), + }); + + const mockSearchResult: QueryResponse = { + isItemLoaded: jest.fn(), + loadMoreItems: jest.fn(), + totalRows: searchData.length, + view: new DataFrameView(dataFrames[0]), + }; + + beforeAll(() => { + jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue(mockSearchResult); + }); + + it('shows the table with the correct accessible label', () => { + render( + + ); + expect(screen.getByRole('table', { name: 'Search results table' })).toBeInTheDocument(); + }); + + it('has the correct row headers', async () => { + render( + + ); + expect(screen.getByRole('columnheader', { name: 'Name' })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: 'Type' })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: 'Tags' })).toBeInTheDocument(); + }); + + it('displays the data correctly in the table', () => { + render( + + ); + + const rows = screen.getAllByRole('row'); + + expect(rows).toHaveLength(2); + expect(screen.getByText('My dashboard 1')).toBeInTheDocument(); + expect(screen.getByText('foo')).toBeInTheDocument(); + expect(screen.getByText('bar')).toBeInTheDocument(); + }); + }); + + describe('when there is no data', () => { + const emptySearchData: DataFrame = { + fields: [ + { name: 'kind', type: FieldType.string, config: {}, values: new ArrayVector([]) }, + { name: 'name', type: FieldType.string, config: {}, values: new ArrayVector([]) }, + { name: 'uid', type: FieldType.string, config: {}, values: new ArrayVector([]) }, + { name: 'url', type: FieldType.string, config: {}, values: new ArrayVector([]) }, + { name: 'tags', type: FieldType.other, config: {}, values: new ArrayVector([]) }, + { name: 'location', type: FieldType.string, config: {}, values: new ArrayVector([]) }, + ], + length: 0, + }; + + const mockEmptySearchResult: QueryResponse = { + isItemLoaded: jest.fn(), + loadMoreItems: jest.fn(), + totalRows: emptySearchData.length, + view: new DataFrameView(emptySearchData), + }; + + beforeAll(() => { + jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue(mockEmptySearchResult); + }); + + it('shows a "No data" message', () => { + render( + + ); + expect(screen.queryByRole('table', { name: 'Search results table' })).not.toBeInTheDocument(); + expect(screen.getByText('No data')).toBeInTheDocument(); + }); + }); +}); diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index 0732e3f3120..093ac1202d9 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/jsx-no-undef */ import { css } from '@emotion/css'; -import React, { useEffect, useMemo, useRef, useCallback } from 'react'; +import React, { useEffect, useMemo, useRef, useCallback, useState } from 'react'; import { useTable, Column, TableOptions, Cell, useAbsoluteLayout } from 'react-table'; import { FixedSizeList } from 'react-window'; import InfiniteLoader from 'react-window-infinite-loader'; @@ -50,7 +50,7 @@ export const SearchResultsTable = React.memo( const styles = useStyles2(getStyles); const tableStyles = useStyles2(getTableStyles); const infiniteLoaderRef = useRef(null); - const listRef = useRef(null); + const [listEl, setListEl] = useState(null); const highlightIndex = useSearchKeyboardNavigation(keyboardEvents, 0, response); const memoizedData = useMemo(() => { @@ -69,10 +69,10 @@ export const SearchResultsTable = React.memo( if (infiniteLoaderRef.current) { infiniteLoaderRef.current.resetloadMoreItemsCache(); } - if (listRef.current) { - listRef.current.scrollTo(0); + if (listEl) { + listEl.scrollTo(0); } - }, [memoizedData]); + }, [memoizedData, listEl]); // React-table column definitions const memoizedColumns = useMemo(() => { @@ -135,7 +135,7 @@ export const SearchResultsTable = React.memo( } return ( -
+
{headerGroups.map((headerGroup) => { const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps(); @@ -162,9 +162,12 @@ export const SearchResultsTable = React.memo( itemCount={rows.length} loadMoreItems={response.loadMoreItems} > - {({ onItemsRendered }) => ( + {({ onItemsRendered, ref }) => ( { + ref(innerRef); + setListEl(innerRef); + }} onItemsRendered={onItemsRendered} height={height - HEADER_HEIGHT} itemCount={rows.length} @@ -194,27 +197,6 @@ const getStyles = (theme: GrafanaTheme2) => { justify-content: center; height: 100%; `, - table: css` - width: 100%; - `, - cellIcon: css` - display: flex; - align-items: center; - `, - nameCellStyle: css` - border-right: none; - padding: ${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(2)}; - overflow: hidden; - text-overflow: ellipsis; - user-select: text; - white-space: nowrap; - &:hover { - box-shadow: none; - } - `, - headerNameStyle: css` - padding-left: ${theme.spacing(1)}; - `, headerCell: css` padding: ${theme.spacing(1)}; `, @@ -239,67 +221,5 @@ const getStyles = (theme: GrafanaTheme2) => { text-overflow: ellipsis; } `, - typeIcon: css` - margin-left: 5px; - margin-right: 9.5px; - vertical-align: middle; - display: inline-block; - margin-bottom: ${theme.v1.spacing.xxs}; - fill: ${theme.colors.text.secondary}; - `, - datasourceItem: css` - span { - &:hover { - color: ${theme.colors.text.link}; - } - } - `, - missingTitleText: css` - color: ${theme.colors.text.disabled}; - font-style: italic; - `, - invalidDatasourceItem: css` - color: ${theme.colors.error.main}; - text-decoration: line-through; - `, - typeText: css` - color: ${theme.colors.text.secondary}; - padding-top: ${theme.spacing(1)}; - `, - locationItem: css` - color: ${theme.colors.text.secondary}; - margin-right: 12px; - `, - sortedHeader: css` - text-align: right; - padding-right: ${theme.spacing(2)}; - `, - sortedItems: css` - text-align: right; - padding: ${theme.spacing(1)} ${theme.spacing(3)} ${theme.spacing(1)} ${theme.spacing(1)}; - `, - locationCellStyle: css` - padding-top: ${theme.spacing(1)}; - padding-right: ${theme.spacing(1)}; - `, - checkboxHeader: css` - margin-left: 2px; - `, - checkbox: css` - margin-left: 10px; - margin-right: 10px; - margin-top: 5px; - `, - infoWrap: css` - color: ${theme.colors.text.secondary}; - span { - margin-right: 10px; - } - `, - tagList: css` - padding-top: ${theme.spacing(0.5)}; - justify-content: flex-start; - flex-wrap: nowrap; - `, }; };