mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Nested folders: Unify visual styles between tree + search view (#70814)
* unify search table styles with browse * add a skeleton state when switching to search view * show all column headers * use isItemLoaded * extract number of placeholder rows into variable + add comment * fix all selection toggle behaviour * tidy up * fix unit test
This commit is contained in:
parent
0bbf011ca8
commit
703bf4afcc
@ -38,8 +38,8 @@ interface DashboardsTreeProps {
|
|||||||
requestLoadMore: (folderUid: string | undefined) => void;
|
requestLoadMore: (folderUid: string | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HEADER_HEIGHT = 35;
|
const HEADER_HEIGHT = 36;
|
||||||
const ROW_HEIGHT = 35;
|
const ROW_HEIGHT = 36;
|
||||||
|
|
||||||
export function DashboardsTree({
|
export function DashboardsTree({
|
||||||
items,
|
items,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useCallback } from 'react';
|
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 { Trans } from 'app/core/internationalization';
|
||||||
import { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKeyboardSelection';
|
import { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKeyboardSelection';
|
||||||
import { SearchResultsProps, SearchResultsTable } from 'app/features/search/page/components/SearchResultsTable';
|
import { SearchResultsProps, SearchResultsTable } from 'app/features/search/page/components/SearchResultsTable';
|
||||||
@ -16,6 +17,30 @@ interface SearchViewProps {
|
|||||||
canSelect: boolean;
|
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) {
|
export function SearchView({ width, height, canSelect }: SearchViewProps) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const selectedItems = useSelector((wholeState) => wholeState.browseDashboards.selectedItems);
|
const selectedItems = useSelector((wholeState) => wholeState.browseDashboards.selectedItems);
|
||||||
@ -24,7 +49,7 @@ export function SearchView({ width, height, canSelect }: SearchViewProps) {
|
|||||||
const { keyboardEvents } = useKeyNavigationListener();
|
const { keyboardEvents } = useKeyNavigationListener();
|
||||||
const [searchState, stateManager] = useSearchStateManager();
|
const [searchState, stateManager] = useSearchStateManager();
|
||||||
|
|
||||||
const value = searchState.result;
|
const value = searchState.result ?? initialLoadingView;
|
||||||
|
|
||||||
const selectionChecker = useCallback(
|
const selectionChecker = useCallback(
|
||||||
(kind: string | undefined, uid: string): boolean => {
|
(kind: string | undefined, uid: string): boolean => {
|
||||||
@ -61,14 +86,6 @@ export function SearchView({ width, height, canSelect }: SearchViewProps) {
|
|||||||
[selectionChecker, dispatch]
|
[selectionChecker, dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return (
|
|
||||||
<div style={{ width }}>
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.totalRows === 0) {
|
if (value.totalRows === 0) {
|
||||||
return (
|
return (
|
||||||
<div style={{ width }}>
|
<div style={{ width }}>
|
||||||
|
@ -43,7 +43,7 @@ describe('SearchResultsTable', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mockSearchResult: QueryResponse = {
|
const mockSearchResult: QueryResponse = {
|
||||||
isItemLoaded: jest.fn(),
|
isItemLoaded: jest.fn().mockReturnValue(true),
|
||||||
loadMoreItems: jest.fn(),
|
loadMoreItems: jest.fn(),
|
||||||
totalRows: searchData.length,
|
totalRows: searchData.length,
|
||||||
view: new DataFrameView<DashboardQueryResult>(dataFrames[0]),
|
view: new DataFrameView<DashboardQueryResult>(dataFrames[0]),
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable react/jsx-no-undef */
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { useEffect, useMemo, useRef, useCallback, useState, CSSProperties } from 'react';
|
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 { FixedSizeList } from 'react-window';
|
||||||
import InfiniteLoader from 'react-window-infinite-loader';
|
import InfiniteLoader from 'react-window-infinite-loader';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
@ -11,6 +10,7 @@ import { TableCellHeight } from '@grafana/schema';
|
|||||||
import { useStyles2, useTheme2 } from '@grafana/ui';
|
import { useStyles2, useTheme2 } from '@grafana/ui';
|
||||||
import { TableCell } from '@grafana/ui/src/components/Table/TableCell';
|
import { TableCell } from '@grafana/ui/src/components/Table/TableCell';
|
||||||
import { useTableStyles } from '@grafana/ui/src/components/Table/styles';
|
import { useTableStyles } from '@grafana/ui/src/components/Table/styles';
|
||||||
|
import { useCustomFlexLayout } from 'app/features/browse-dashboards/components/customFlexTableLayout';
|
||||||
|
|
||||||
import { useSearchKeyboardNavigation } from '../../hooks/useSearchKeyboardSelection';
|
import { useSearchKeyboardNavigation } from '../../hooks/useSearchKeyboardSelection';
|
||||||
import { QueryResponse } from '../../service';
|
import { QueryResponse } from '../../service';
|
||||||
@ -35,7 +35,7 @@ export type TableColumn = Column & {
|
|||||||
field?: Field;
|
field?: Field;
|
||||||
};
|
};
|
||||||
|
|
||||||
const HEADER_HEIGHT = 36; // pixels
|
const ROW_HEIGHT = 36; // pixels
|
||||||
|
|
||||||
export const SearchResultsTable = React.memo(
|
export const SearchResultsTable = React.memo(
|
||||||
({
|
({
|
||||||
@ -101,7 +101,7 @@ export const SearchResultsTable = React.memo(
|
|||||||
[memoizedColumns, memoizedData]
|
[memoizedColumns, memoizedData]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(options, useAbsoluteLayout);
|
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(options, useCustomFlexLayout);
|
||||||
|
|
||||||
const handleLoadMore = useCallback(
|
const handleLoadMore = useCallback(
|
||||||
async (startIndex: number, endIndex: number) => {
|
async (startIndex: number, endIndex: number) => {
|
||||||
@ -162,24 +162,24 @@ export const SearchResultsTable = React.memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...getTableProps()} aria-label="Search results table" role="table">
|
<div {...getTableProps()} aria-label="Search results table" role="table">
|
||||||
<div>
|
{headerGroups.map((headerGroup) => {
|
||||||
{headerGroups.map((headerGroup) => {
|
const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps({
|
||||||
const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps();
|
style: { width },
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} {...headerGroupProps} className={styles.headerRow}>
|
<div key={key} {...headerGroupProps} className={styles.headerRow}>
|
||||||
{headerGroup.headers.map((column) => {
|
{headerGroup.headers.map((column) => {
|
||||||
const { key, ...headerProps } = column.getHeaderProps();
|
const { key, ...headerProps } = column.getHeaderProps();
|
||||||
return (
|
return (
|
||||||
<div key={key} {...headerProps} role="columnheader" className={styles.headerCell}>
|
<div key={key} {...headerProps} role="columnheader" className={styles.headerCell}>
|
||||||
{column.render('Header')}
|
{column.render('Header')}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div {...getTableBodyProps()}>
|
<div {...getTableBodyProps()}>
|
||||||
<InfiniteLoader
|
<InfiniteLoader
|
||||||
@ -195,10 +195,10 @@ export const SearchResultsTable = React.memo(
|
|||||||
setListEl(innerRef);
|
setListEl(innerRef);
|
||||||
}}
|
}}
|
||||||
onItemsRendered={onItemsRendered}
|
onItemsRendered={onItemsRendered}
|
||||||
height={height - HEADER_HEIGHT}
|
height={height - ROW_HEIGHT}
|
||||||
itemCount={rows.length}
|
itemCount={rows.length}
|
||||||
itemSize={tableStyles.rowHeight}
|
itemSize={tableStyles.rowHeight}
|
||||||
width="100%"
|
width={width}
|
||||||
style={{ overflow: 'hidden auto' }}
|
style={{ overflow: 'hidden auto' }}
|
||||||
>
|
>
|
||||||
{RenderRow}
|
{RenderRow}
|
||||||
@ -224,18 +224,25 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
`,
|
`,
|
||||||
headerCell: css`
|
headerCell: css`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
overflo: hidden;
|
||||||
padding: ${theme.spacing(1)};
|
padding: ${theme.spacing(1)};
|
||||||
`,
|
`,
|
||||||
headerRow: css`
|
headerRow: css`
|
||||||
background-color: ${theme.colors.background.secondary};
|
background-color: ${theme.colors.background.secondary};
|
||||||
height: ${HEADER_HEIGHT}px;
|
display: flex;
|
||||||
align-items: center;
|
gap: ${theme.spacing(1)};
|
||||||
|
height: ${ROW_HEIGHT}px;
|
||||||
`,
|
`,
|
||||||
selectedRow: css`
|
selectedRow: css`
|
||||||
background-color: ${rowHoverBg};
|
background-color: ${rowHoverBg};
|
||||||
box-shadow: inset 3px 0px ${theme.colors.primary.border};
|
box-shadow: inset 3px 0px ${theme.colors.primary.border};
|
||||||
`,
|
`,
|
||||||
rowContainer: css`
|
rowContainer: css`
|
||||||
|
display: flex;
|
||||||
|
gap: ${theme.spacing(1)};
|
||||||
|
height: ${ROW_HEIGHT}px;
|
||||||
label: row;
|
label: row;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: ${rowHoverBg};
|
background-color: ${rowHoverBg};
|
||||||
@ -253,27 +260,22 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
// CSS for columns from react table
|
// CSS for columns from react table
|
||||||
const getColumnStyles = (theme: GrafanaTheme2) => {
|
const getColumnStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
|
cell: css({
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
overflow: 'hidden', // Required so flex children can do text-overflow: ellipsis
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}),
|
||||||
nameCellStyle: css`
|
nameCellStyle: css`
|
||||||
border-right: none;
|
|
||||||
padding: ${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(2)};
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
&:hover {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
headerNameStyle: css`
|
typeCell: css({
|
||||||
padding-left: ${theme.spacing(1)};
|
gap: theme.spacing(0.5),
|
||||||
`,
|
}),
|
||||||
|
|
||||||
typeIcon: css`
|
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};
|
fill: ${theme.colors.text.secondary};
|
||||||
`,
|
`,
|
||||||
datasourceItem: css`
|
datasourceItem: css`
|
||||||
@ -291,41 +293,24 @@ const getColumnStyles = (theme: GrafanaTheme2) => {
|
|||||||
color: ${theme.colors.error.main};
|
color: ${theme.colors.error.main};
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
`,
|
`,
|
||||||
typeText: css`
|
locationContainer: css({
|
||||||
color: ${theme.colors.text.secondary};
|
display: 'flex',
|
||||||
padding-top: ${theme.spacing(1)};
|
flexWrap: 'nowrap',
|
||||||
`,
|
gap: theme.spacing(1),
|
||||||
|
overflow: 'hidden',
|
||||||
|
}),
|
||||||
locationItem: css`
|
locationItem: css`
|
||||||
|
align-items: center;
|
||||||
color: ${theme.colors.text.secondary};
|
color: ${theme.colors.text.secondary};
|
||||||
margin-right: 12px;
|
display: flex;
|
||||||
`,
|
flex-wrap: nowrap;
|
||||||
sortedHeader: css`
|
gap: 4px;
|
||||||
text-align: right;
|
overflow: hidden;
|
||||||
padding-right: ${theme.spacing(2)};
|
|
||||||
`,
|
|
||||||
sortedItems: css`
|
|
||||||
text-align: right;
|
|
||||||
padding: ${theme.spacing(1)} ${theme.spacing(3)} ${theme.spacing(1)} ${theme.spacing(1)};
|
|
||||||
`,
|
`,
|
||||||
explainItem: css`
|
explainItem: css`
|
||||||
text-align: right;
|
|
||||||
padding: ${theme.spacing(1)} ${theme.spacing(3)} ${theme.spacing(1)} ${theme.spacing(1)};
|
|
||||||
cursor: pointer;
|
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`
|
tagList: css`
|
||||||
padding-top: ${theme.spacing(0.5)};
|
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
`,
|
`,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DisplayProcessor,
|
DisplayProcessor,
|
||||||
@ -10,7 +11,8 @@ import {
|
|||||||
getFieldDisplayName,
|
getFieldDisplayName,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config, getDataSourceSrv } from '@grafana/runtime';
|
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 appEvents from 'app/core/app_events';
|
||||||
import { t } from 'app/core/internationalization';
|
import { t } from 'app/core/internationalization';
|
||||||
import { PluginIconName } from 'app/features/plugins/admin/types';
|
import { PluginIconName } from 'app/features/plugins/admin/types';
|
||||||
@ -57,38 +59,30 @@ export const generateColumns = (
|
|||||||
|
|
||||||
let width = 50;
|
let width = 50;
|
||||||
if (selection && selectionToggle) {
|
if (selection && selectionToggle) {
|
||||||
width = 30;
|
width = 0;
|
||||||
columns.push({
|
columns.push({
|
||||||
id: `column-checkbox`,
|
id: `column-checkbox`,
|
||||||
width,
|
width,
|
||||||
Header: () => {
|
Header: () => {
|
||||||
if (selection('*', '*')) {
|
|
||||||
return (
|
|
||||||
<div className={styles.checkboxHeader}>
|
|
||||||
<IconButton name="check-square" onClick={clearSelection} tooltip="Clear selection" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.checkboxHeader}>
|
<Checkbox
|
||||||
<Checkbox
|
indeterminate={selection('*', '*')}
|
||||||
checked={false}
|
checked={false}
|
||||||
onChange={(e) => {
|
disabled={!response}
|
||||||
e.stopPropagation();
|
onChange={(e) => {
|
||||||
e.preventDefault();
|
const { view } = response;
|
||||||
const { view } = response;
|
const count = Math.min(view.length, 50);
|
||||||
const count = Math.min(view.length, 50);
|
const hasSelection = selection('*', '*');
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const item = view.get(i);
|
const item = view.get(i);
|
||||||
if (item.uid && item.kind) {
|
if (item.uid && item.kind) {
|
||||||
if (!selection(item.kind, item.uid)) {
|
if (hasSelection === selection(item.kind, item.uid)) {
|
||||||
selectionToggle(item.kind, item.uid);
|
selectionToggle(item.kind, item.uid);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
Cell: (p) => {
|
Cell: (p) => {
|
||||||
@ -97,16 +91,14 @@ export const generateColumns = (
|
|||||||
const selected = selection(kind, uid);
|
const selected = selection(kind, uid);
|
||||||
const hasUID = uid != null; // Panels don't have UID! Likely should not be shown on pages with manage options
|
const hasUID = uid != null; // Panels don't have UID! Likely should not be shown on pages with manage options
|
||||||
return (
|
return (
|
||||||
<div {...p.cellProps}>
|
<div {...p.cellProps} className={styles.cell}>
|
||||||
<div className={styles.checkbox}>
|
<Checkbox
|
||||||
<Checkbox
|
disabled={!hasUID}
|
||||||
disabled={!hasUID}
|
value={selected && hasUID}
|
||||||
value={selected && hasUID}
|
onChange={(e) => {
|
||||||
onChange={(e) => {
|
selectionToggle(kind, uid);
|
||||||
selectionToggle(kind, uid);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -127,20 +119,26 @@ export const generateColumns = (
|
|||||||
classNames += ' ' + styles.missingTitleText;
|
classNames += ' ' + styles.missingTitleText;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<a {...p.cellProps} href={p.userProps.href} onClick={p.userProps.onClick} className={classNames} title={name}>
|
<div className={styles.cell} {...p.cellProps}>
|
||||||
{name}
|
{!response.isItemLoaded(p.row.index) ? (
|
||||||
</a>
|
<Skeleton width={200} />
|
||||||
|
) : (
|
||||||
|
<a href={p.userProps.href} onClick={p.userProps.onClick} className={classNames} title={name}>
|
||||||
|
{name}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
id: `column-name`,
|
id: `column-name`,
|
||||||
field: access.name!,
|
field: access.name!,
|
||||||
Header: () => <div className={styles.headerNameStyle}>{t('search.results-table.name-header', 'Name')}</div>,
|
Header: () => <div>{t('search.results-table.name-header', 'Name')}</div>,
|
||||||
width,
|
width,
|
||||||
});
|
});
|
||||||
availableWidth -= width;
|
availableWidth -= width;
|
||||||
|
|
||||||
width = TYPE_COLUMN_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;
|
availableWidth -= width;
|
||||||
|
|
||||||
// Show datasources if we have any
|
// Show datasources if we have any
|
||||||
@ -168,20 +166,29 @@ export const generateColumns = (
|
|||||||
Cell: (p) => {
|
Cell: (p) => {
|
||||||
const parts = (access.location?.values[p.row.index] ?? '').split('/');
|
const parts = (access.location?.values[p.row.index] ?? '').split('/');
|
||||||
return (
|
return (
|
||||||
<div {...p.cellProps} className={cx(styles.locationCellStyle)}>
|
<div {...p.cellProps} className={styles.cell}>
|
||||||
{parts.map((p) => {
|
{!response.isItemLoaded(p.row.index) ? (
|
||||||
let info = meta.locationInfo[p];
|
<Skeleton width={150} />
|
||||||
if (!info && p === 'general') {
|
) : (
|
||||||
info = { kind: 'folder', url: '/dashboards', name: 'General' };
|
<div className={styles.locationContainer}>
|
||||||
}
|
{parts.map((p) => {
|
||||||
return info ? (
|
let info = meta.locationInfo[p];
|
||||||
<a key={p} href={info.url} className={styles.locationItem}>
|
if (!info && p === 'general') {
|
||||||
<Icon name={getIconForKind(info.kind)} /> {info.name}
|
info = { kind: 'folder', url: '/dashboards', name: 'General' };
|
||||||
</a>
|
}
|
||||||
) : (
|
return info ? (
|
||||||
<span key={p}>{p}</span>
|
<a key={p} href={info.url} className={styles.locationItem}>
|
||||||
);
|
<Icon name={getIconForKind(info.kind)} />
|
||||||
})}
|
<Span variant="body" truncate>
|
||||||
|
{info.name}
|
||||||
|
</Span>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span key={p}>{p}</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -193,17 +200,17 @@ export const generateColumns = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (availableWidth > 0 && showTags) {
|
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) {
|
if (sortField && sortFieldWith) {
|
||||||
const disp = sortField.display ?? getDisplayProcessor({ field: sortField, theme: config.theme2 });
|
const disp = sortField.display ?? getDisplayProcessor({ field: sortField, theme: config.theme2 });
|
||||||
|
|
||||||
columns.push({
|
columns.push({
|
||||||
Header: () => <div className={styles.sortedHeader}>{getFieldDisplayName(sortField)}</div>,
|
Header: getFieldDisplayName(sortField),
|
||||||
Cell: (p) => {
|
Cell: (p) => {
|
||||||
return (
|
return (
|
||||||
<div {...p.cellProps} className={styles.sortedItems}>
|
<div {...p.cellProps} className={styles.cell}>
|
||||||
{getDisplayValue({
|
{getDisplayValue({
|
||||||
sortField,
|
sortField,
|
||||||
getDisplay: disp,
|
getDisplay: disp,
|
||||||
@ -239,7 +246,11 @@ export const generateColumns = (
|
|||||||
Header: () => <div className={styles.sortedHeader}>Score</div>,
|
Header: () => <div className={styles.sortedHeader}>Score</div>,
|
||||||
Cell: (p) => {
|
Cell: (p) => {
|
||||||
return (
|
return (
|
||||||
<div {...p.cellProps} className={styles.explainItem} onClick={() => showExplainPopup(p.row.index)}>
|
<div
|
||||||
|
{...p.cellProps}
|
||||||
|
className={cx(styles.cell, styles.explainItem)}
|
||||||
|
onClick={() => showExplainPopup(p.row.index)}
|
||||||
|
>
|
||||||
{vals[p.row.index]}
|
{vals[p.row.index]}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -314,6 +325,7 @@ function makeDataSourceColumn(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeTypeColumn(
|
function makeTypeColumn(
|
||||||
|
response: QueryResponse,
|
||||||
kindField: Field<string>,
|
kindField: Field<string>,
|
||||||
typeField: Field<string>,
|
typeField: Field<string>,
|
||||||
width: number,
|
width: number,
|
||||||
@ -366,9 +378,15 @@ function makeTypeColumn(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div {...p.cellProps} className={styles.typeText}>
|
<div {...p.cellProps} className={cx(styles.cell, styles.typeCell)}>
|
||||||
<Icon name={icon} size="sm" title={txt} className={styles.typeIcon} />
|
{!response.isItemLoaded(p.row.index) ? (
|
||||||
{txt}
|
<Skeleton width={100} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon name={icon} size="sm" title={txt} className={styles.typeIcon} />
|
||||||
|
{txt}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -377,19 +395,24 @@ function makeTypeColumn(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeTagsColumn(
|
function makeTagsColumn(
|
||||||
|
response: QueryResponse,
|
||||||
field: Field<string[]>,
|
field: Field<string[]>,
|
||||||
width: number,
|
width: number,
|
||||||
tagListClass: string,
|
styles: Record<string, string>,
|
||||||
onTagSelected: (tag: string) => void
|
onTagSelected: (tag: string) => void
|
||||||
): TableColumn {
|
): TableColumn {
|
||||||
return {
|
return {
|
||||||
Cell: (p) => {
|
Cell: (p) => {
|
||||||
const tags = field.values[p.row.index];
|
const tags = field.values[p.row.index];
|
||||||
return tags ? (
|
return (
|
||||||
<div {...p.cellProps}>
|
<div {...p.cellProps} className={styles.cell}>
|
||||||
<TagList className={tagListClass} tags={tags} onClick={onTagSelected} />
|
{!response.isItemLoaded(p.row.index) ? (
|
||||||
|
<TagList.Skeleton />
|
||||||
|
) : (
|
||||||
|
<>{tags ? <TagList className={styles.tagList} tags={tags} onClick={onTagSelected} /> : null}</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
);
|
||||||
},
|
},
|
||||||
id: `column-tags`,
|
id: `column-tags`,
|
||||||
field: field,
|
field: field,
|
||||||
|
Loading…
Reference in New Issue
Block a user