diff --git a/public/app/features/browse-dashboards/components/DashboardsTree.tsx b/public/app/features/browse-dashboards/components/DashboardsTree.tsx
index 9a9713574b6..a44ef0b3a0b 100644
--- a/public/app/features/browse-dashboards/components/DashboardsTree.tsx
+++ b/public/app/features/browse-dashboards/components/DashboardsTree.tsx
@@ -38,8 +38,8 @@ interface DashboardsTreeProps {
requestLoadMore: (folderUid: string | undefined) => void;
}
-const HEADER_HEIGHT = 35;
-const ROW_HEIGHT = 35;
+const HEADER_HEIGHT = 36;
+const ROW_HEIGHT = 36;
export function DashboardsTree({
items,
diff --git a/public/app/features/browse-dashboards/components/SearchView.tsx b/public/app/features/browse-dashboards/components/SearchView.tsx
index a181d192a55..52ede56baea 100644
--- a/public/app/features/browse-dashboards/components/SearchView.tsx
+++ b/public/app/features/browse-dashboards/components/SearchView.tsx
@@ -1,6 +1,7 @@
import React, { useCallback } from 'react';
-import { Button, Card, Spinner } from '@grafana/ui';
+import { DataFrameView, toDataFrame } from '@grafana/data';
+import { Button, Card } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKeyboardSelection';
import { SearchResultsProps, SearchResultsTable } from 'app/features/search/page/components/SearchResultsTable';
@@ -16,6 +17,30 @@ interface SearchViewProps {
canSelect: boolean;
}
+const NUM_PLACEHOLDER_ROWS = 50;
+const initialLoadingView = {
+ view: new DataFrameView(
+ toDataFrame({
+ fields: [
+ { name: 'uid', display: true, values: Array(NUM_PLACEHOLDER_ROWS).fill(null) },
+ { name: 'kind', display: true, values: Array(NUM_PLACEHOLDER_ROWS).fill('dashboard') },
+ { name: 'name', display: true, values: Array(NUM_PLACEHOLDER_ROWS).fill('') },
+ { name: 'location', display: true, values: Array(NUM_PLACEHOLDER_ROWS).fill('') },
+ { name: 'tags', display: true, values: Array(NUM_PLACEHOLDER_ROWS).fill([]) },
+ ],
+ meta: {
+ custom: {
+ locationInfo: [],
+ },
+ },
+ })
+ ),
+ loadMoreItems: () => Promise.resolve(),
+ // this is key and controls whether to show the skeleton in generateColumns
+ isItemLoaded: () => false,
+ totalRows: NUM_PLACEHOLDER_ROWS,
+};
+
export function SearchView({ width, height, canSelect }: SearchViewProps) {
const dispatch = useDispatch();
const selectedItems = useSelector((wholeState) => wholeState.browseDashboards.selectedItems);
@@ -24,7 +49,7 @@ export function SearchView({ width, height, canSelect }: SearchViewProps) {
const { keyboardEvents } = useKeyNavigationListener();
const [searchState, stateManager] = useSearchStateManager();
- const value = searchState.result;
+ const value = searchState.result ?? initialLoadingView;
const selectionChecker = useCallback(
(kind: string | undefined, uid: string): boolean => {
@@ -61,14 +86,6 @@ export function SearchView({ width, height, canSelect }: SearchViewProps) {
[selectionChecker, dispatch]
);
- if (!value) {
- return (
-
-
-
- );
- }
-
if (value.totalRows === 0) {
return (
diff --git a/public/app/features/search/page/components/SearchResultsTable.test.tsx b/public/app/features/search/page/components/SearchResultsTable.test.tsx
index 979865b9f6a..1231ee39a5b 100644
--- a/public/app/features/search/page/components/SearchResultsTable.test.tsx
+++ b/public/app/features/search/page/components/SearchResultsTable.test.tsx
@@ -43,7 +43,7 @@ describe('SearchResultsTable', () => {
});
const mockSearchResult: QueryResponse = {
- isItemLoaded: jest.fn(),
+ isItemLoaded: jest.fn().mockReturnValue(true),
loadMoreItems: jest.fn(),
totalRows: searchData.length,
view: new DataFrameView
(dataFrames[0]),
diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx
index 6ab728be20c..f6d4e9ed962 100644
--- a/public/app/features/search/page/components/SearchResultsTable.tsx
+++ b/public/app/features/search/page/components/SearchResultsTable.tsx
@@ -1,7 +1,6 @@
-/* eslint-disable react/jsx-no-undef */
import { css } from '@emotion/css';
import React, { useEffect, useMemo, useRef, useCallback, useState, CSSProperties } from 'react';
-import { useTable, Column, TableOptions, Cell, useAbsoluteLayout } from 'react-table';
+import { useTable, Column, TableOptions, Cell } from 'react-table';
import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { Observable } from 'rxjs';
@@ -11,6 +10,7 @@ import { TableCellHeight } from '@grafana/schema';
import { useStyles2, useTheme2 } from '@grafana/ui';
import { TableCell } from '@grafana/ui/src/components/Table/TableCell';
import { useTableStyles } from '@grafana/ui/src/components/Table/styles';
+import { useCustomFlexLayout } from 'app/features/browse-dashboards/components/customFlexTableLayout';
import { useSearchKeyboardNavigation } from '../../hooks/useSearchKeyboardSelection';
import { QueryResponse } from '../../service';
@@ -35,7 +35,7 @@ export type TableColumn = Column & {
field?: Field;
};
-const HEADER_HEIGHT = 36; // pixels
+const ROW_HEIGHT = 36; // pixels
export const SearchResultsTable = React.memo(
({
@@ -101,7 +101,7 @@ export const SearchResultsTable = React.memo(
[memoizedColumns, memoizedData]
);
- const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(options, useAbsoluteLayout);
+ const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(options, useCustomFlexLayout);
const handleLoadMore = useCallback(
async (startIndex: number, endIndex: number) => {
@@ -162,24 +162,24 @@ export const SearchResultsTable = React.memo(
return (
-
- {headerGroups.map((headerGroup) => {
- const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps();
+ {headerGroups.map((headerGroup) => {
+ const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps({
+ style: { width },
+ });
- return (
-
- {headerGroup.headers.map((column) => {
- const { key, ...headerProps } = column.getHeaderProps();
- return (
-
- {column.render('Header')}
-
- );
- })}
-
- );
- })}
-
+ return (
+
+ {headerGroup.headers.map((column) => {
+ const { key, ...headerProps } = column.getHeaderProps();
+ return (
+
+ {column.render('Header')}
+
+ );
+ })}
+
+ );
+ })}
{RenderRow}
@@ -224,18 +224,25 @@ const getStyles = (theme: GrafanaTheme2) => {
height: 100%;
`,
headerCell: css`
+ align-items: center;
+ display: flex;
+ overflo: hidden;
padding: ${theme.spacing(1)};
`,
headerRow: css`
background-color: ${theme.colors.background.secondary};
- height: ${HEADER_HEIGHT}px;
- align-items: center;
+ display: flex;
+ gap: ${theme.spacing(1)};
+ height: ${ROW_HEIGHT}px;
`,
selectedRow: css`
background-color: ${rowHoverBg};
box-shadow: inset 3px 0px ${theme.colors.primary.border};
`,
rowContainer: css`
+ display: flex;
+ gap: ${theme.spacing(1)};
+ height: ${ROW_HEIGHT}px;
label: row;
&:hover {
background-color: ${rowHoverBg};
@@ -253,27 +260,22 @@ const getStyles = (theme: GrafanaTheme2) => {
// CSS for columns from react table
const getColumnStyles = (theme: GrafanaTheme2) => {
return {
+ cell: css({
+ padding: theme.spacing(1),
+ overflow: 'hidden', // Required so flex children can do text-overflow: ellipsis
+ display: 'flex',
+ alignItems: '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)};
- `,
-
+ typeCell: css({
+ gap: theme.spacing(0.5),
+ }),
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`
@@ -291,41 +293,24 @@ const getColumnStyles = (theme: GrafanaTheme2) => {
color: ${theme.colors.error.main};
text-decoration: line-through;
`,
- typeText: css`
- color: ${theme.colors.text.secondary};
- padding-top: ${theme.spacing(1)};
- `,
+ locationContainer: css({
+ display: 'flex',
+ flexWrap: 'nowrap',
+ gap: theme.spacing(1),
+ overflow: 'hidden',
+ }),
locationItem: css`
+ align-items: center;
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)};
+ display: flex;
+ flex-wrap: nowrap;
+ gap: 4px;
+ overflow: hidden;
`,
explainItem: css`
- text-align: right;
- padding: ${theme.spacing(1)} ${theme.spacing(3)} ${theme.spacing(1)} ${theme.spacing(1)};
cursor: pointer;
`,
- 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;
- `,
tagList: css`
- padding-top: ${theme.spacing(0.5)};
justify-content: flex-start;
flex-wrap: nowrap;
`,
diff --git a/public/app/features/search/page/components/columns.tsx b/public/app/features/search/page/components/columns.tsx
index 87153f2af91..cdd45f9e0f6 100644
--- a/public/app/features/search/page/components/columns.tsx
+++ b/public/app/features/search/page/components/columns.tsx
@@ -1,5 +1,6 @@
import { cx } from '@emotion/css';
import React from 'react';
+import Skeleton from 'react-loading-skeleton';
import {
DisplayProcessor,
@@ -10,7 +11,8 @@ import {
getFieldDisplayName,
} from '@grafana/data';
import { config, getDataSourceSrv } from '@grafana/runtime';
-import { Checkbox, Icon, IconButton, IconName, TagList } from '@grafana/ui';
+import { Checkbox, Icon, IconName, TagList } from '@grafana/ui';
+import { Span } from '@grafana/ui/src/unstable';
import appEvents from 'app/core/app_events';
import { t } from 'app/core/internationalization';
import { PluginIconName } from 'app/features/plugins/admin/types';
@@ -57,38 +59,30 @@ export const generateColumns = (
let width = 50;
if (selection && selectionToggle) {
- width = 30;
+ width = 0;
columns.push({
id: `column-checkbox`,
width,
Header: () => {
- if (selection('*', '*')) {
- return (
-
-
-
- );
- }
return (
-
- {
- e.stopPropagation();
- e.preventDefault();
- const { view } = response;
- const count = Math.min(view.length, 50);
- for (let i = 0; i < count; i++) {
- const item = view.get(i);
- if (item.uid && item.kind) {
- if (!selection(item.kind, item.uid)) {
- selectionToggle(item.kind, item.uid);
- }
+ {
+ const { view } = response;
+ const count = Math.min(view.length, 50);
+ const hasSelection = selection('*', '*');
+ for (let i = 0; i < count; i++) {
+ const item = view.get(i);
+ if (item.uid && item.kind) {
+ if (hasSelection === selection(item.kind, item.uid)) {
+ selectionToggle(item.kind, item.uid);
}
}
- }}
- />
-
+ }
+ }}
+ />
);
},
Cell: (p) => {
@@ -97,16 +91,14 @@ export const generateColumns = (
const selected = selection(kind, uid);
const hasUID = uid != null; // Panels don't have UID! Likely should not be shown on pages with manage options
return (
-
-
- {
- selectionToggle(kind, uid);
- }}
- />
-
+
+ {
+ selectionToggle(kind, uid);
+ }}
+ />
);
},
@@ -127,20 +119,26 @@ export const generateColumns = (
classNames += ' ' + styles.missingTitleText;
}
return (
-
- {name}
-
+
+ {!response.isItemLoaded(p.row.index) ? (
+
+ ) : (
+
+ {name}
+
+ )}
+
);
},
id: `column-name`,
field: access.name!,
- Header: () =>
{t('search.results-table.name-header', 'Name')}
,
+ Header: () =>
{t('search.results-table.name-header', 'Name')}
,
width,
});
availableWidth -= width;
width = TYPE_COLUMN_WIDTH;
- columns.push(makeTypeColumn(access.kind, access.panel_type, width, styles));
+ columns.push(makeTypeColumn(response, access.kind, access.panel_type, width, styles));
availableWidth -= width;
// Show datasources if we have any
@@ -168,20 +166,29 @@ export const generateColumns = (
Cell: (p) => {
const parts = (access.location?.values[p.row.index] ?? '').split('/');
return (
-
- {parts.map((p) => {
- let info = meta.locationInfo[p];
- if (!info && p === 'general') {
- info = { kind: 'folder', url: '/dashboards', name: 'General' };
- }
- return info ? (
-
- {info.name}
-
- ) : (
-
{p}
- );
- })}
+
+ {!response.isItemLoaded(p.row.index) ? (
+
+ ) : (
+
+ {parts.map((p) => {
+ let info = meta.locationInfo[p];
+ if (!info && p === 'general') {
+ info = { kind: 'folder', url: '/dashboards', name: 'General' };
+ }
+ return info ? (
+
+
+
+ {info.name}
+
+
+ ) : (
+
{p}
+ );
+ })}
+
+ )}
);
},
@@ -193,17 +200,17 @@ export const generateColumns = (
}
if (availableWidth > 0 && showTags) {
- columns.push(makeTagsColumn(access.tags, availableWidth, styles.tagList, onTagSelected));
+ columns.push(makeTagsColumn(response, access.tags, availableWidth, styles, onTagSelected));
}
if (sortField && sortFieldWith) {
const disp = sortField.display ?? getDisplayProcessor({ field: sortField, theme: config.theme2 });
columns.push({
- Header: () =>
{getFieldDisplayName(sortField)}
,
+ Header: getFieldDisplayName(sortField),
Cell: (p) => {
return (
-
+
{getDisplayValue({
sortField,
getDisplay: disp,
@@ -239,7 +246,11 @@ export const generateColumns = (
Header: () =>
Score
,
Cell: (p) => {
return (
-
showExplainPopup(p.row.index)}>
+
showExplainPopup(p.row.index)}
+ >
{vals[p.row.index]}
);
@@ -314,6 +325,7 @@ function makeDataSourceColumn(
}
function makeTypeColumn(
+ response: QueryResponse,
kindField: Field
,
typeField: Field,
width: number,
@@ -366,9 +378,15 @@ function makeTypeColumn(
}
}
return (
-
-
- {txt}
+
+ {!response.isItemLoaded(p.row.index) ? (
+
+ ) : (
+ <>
+
+ {txt}
+ >
+ )}
);
},
@@ -377,19 +395,24 @@ function makeTypeColumn(
}
function makeTagsColumn(
+ response: QueryResponse,
field: Field
,
width: number,
- tagListClass: string,
+ styles: Record,
onTagSelected: (tag: string) => void
): TableColumn {
return {
Cell: (p) => {
const tags = field.values[p.row.index];
- return tags ? (
-
-
+ return (
+
+ {!response.isItemLoaded(p.row.index) ? (
+
+ ) : (
+ <>{tags ? : null}>
+ )}
- ) : null;
+ );
},
id: `column-tags`,
field: field,