mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
266 lines
7.0 KiB
TypeScript
266 lines
7.0 KiB
TypeScript
import { css, cx } from '@emotion/css';
|
|
import React, { CSSProperties, useCallback, useMemo } from 'react';
|
|
import { SortByFn, useSortBy, useAbsoluteLayout, useTable, CellProps } from 'react-table';
|
|
import { FixedSizeList } from 'react-window';
|
|
|
|
import { GrafanaTheme2 } from '@grafana/data';
|
|
import { Icon, useStyles2, CustomScrollbar } from '@grafana/ui';
|
|
|
|
import { TOP_TABLE_COLUMN_WIDTH } from '../../constants';
|
|
import { ColumnTypes, TopTableData, TopTableValue } from '../types';
|
|
|
|
type Props = {
|
|
width: number;
|
|
height: number;
|
|
data: TopTableData[];
|
|
search: string;
|
|
setSearch: (search: string) => void;
|
|
setTopLevelIndex: (level: number) => void;
|
|
setSelectedBarIndex: (bar: number) => void;
|
|
setRangeMin: (range: number) => void;
|
|
setRangeMax: (range: number) => void;
|
|
};
|
|
|
|
const FlameGraphTopTable = ({
|
|
width,
|
|
height,
|
|
data,
|
|
search,
|
|
setSearch,
|
|
setTopLevelIndex,
|
|
setSelectedBarIndex,
|
|
setRangeMin,
|
|
setRangeMax,
|
|
}: Props) => {
|
|
const styles = useStyles2((theme) => getStyles(theme));
|
|
|
|
const sortSymbols: SortByFn<object> = (a, b, column) => {
|
|
return a.values[column].localeCompare(b.values[column]);
|
|
};
|
|
|
|
const sortUnits: SortByFn<object> = (a, b, column) => {
|
|
return a.values[column].value.toString().localeCompare(b.values[column].value.toString(), 'en', { numeric: true });
|
|
};
|
|
|
|
const columns = useMemo(
|
|
() => [
|
|
{
|
|
accessor: ColumnTypes.Symbol.toLowerCase(),
|
|
header: ColumnTypes.Symbol,
|
|
cell: SymbolCell,
|
|
sortType: sortSymbols,
|
|
width: width - TOP_TABLE_COLUMN_WIDTH * 2,
|
|
},
|
|
{
|
|
accessor: ColumnTypes.Self.toLowerCase(),
|
|
header: ColumnTypes.Self,
|
|
cell: UnitCell,
|
|
sortType: sortUnits,
|
|
width: TOP_TABLE_COLUMN_WIDTH,
|
|
},
|
|
{
|
|
accessor: ColumnTypes.Total.toLowerCase(),
|
|
header: ColumnTypes.Total,
|
|
cell: UnitCell,
|
|
sortType: sortUnits,
|
|
width: TOP_TABLE_COLUMN_WIDTH,
|
|
},
|
|
],
|
|
[width]
|
|
);
|
|
|
|
const options = useMemo(
|
|
() => ({
|
|
columns,
|
|
data,
|
|
initialState: {
|
|
sortBy: [
|
|
{
|
|
id: ColumnTypes.Self.toLowerCase(),
|
|
desc: true,
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
[columns, data]
|
|
);
|
|
|
|
const rowClicked = useCallback(
|
|
(symbol: string) => {
|
|
if (search === symbol) {
|
|
setSearch('');
|
|
} else {
|
|
setSearch(symbol);
|
|
// Reset selected level in flamegraph when selecting row in top table
|
|
setTopLevelIndex(0);
|
|
setSelectedBarIndex(0);
|
|
setRangeMin(0);
|
|
setRangeMax(1);
|
|
}
|
|
},
|
|
[search, setRangeMax, setRangeMin, setSearch, setTopLevelIndex, setSelectedBarIndex]
|
|
);
|
|
|
|
const { headerGroups, rows, prepareRow } = useTable(options, useSortBy, useAbsoluteLayout);
|
|
|
|
const renderRow = React.useCallback(
|
|
({ index, style }: { index: number; style: CSSProperties }) => {
|
|
let row = rows[index];
|
|
prepareRow(row);
|
|
|
|
const symbol = row.values[ColumnTypes.Symbol.toLowerCase()];
|
|
const classNames = cx(symbol === search && styles.matchedRow, styles.row);
|
|
|
|
return (
|
|
<div
|
|
{...row.getRowProps({ style })}
|
|
className={classNames}
|
|
onClick={() => {
|
|
rowClicked(symbol);
|
|
}}
|
|
>
|
|
{row.cells.map((cell) => {
|
|
const { key, ...cellProps } = cell.getCellProps();
|
|
if (cellProps.style) {
|
|
cellProps.style.minWidth = cellProps.style.width;
|
|
}
|
|
return (
|
|
<div key={key} className={styles.cell} {...cellProps}>
|
|
{cell.render('cell')}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
},
|
|
[rows, prepareRow, search, styles.matchedRow, styles.row, styles.cell, rowClicked]
|
|
);
|
|
|
|
return (
|
|
<div className={styles.table(height)} data-testid="topTable">
|
|
{headerGroups.map((headerGroup) => {
|
|
const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps();
|
|
|
|
return (
|
|
<div key={key} className={styles.header} {...headerGroupProps}>
|
|
{headerGroup.headers.map((column) => {
|
|
const { key, ...headerProps } = column.getHeaderProps(
|
|
column.canSort ? column.getSortByToggleProps() : undefined
|
|
);
|
|
|
|
return (
|
|
<div key={key} className={styles.headerCell} {...headerProps}>
|
|
{column.render('header')}
|
|
{column.isSorted && <Icon name={column.isSortedDesc ? 'arrow-down' : 'arrow-up'} />}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{rows.length > 0 ? (
|
|
<CustomScrollbar hideVerticalTrack={true}>
|
|
<FixedSizeList
|
|
height={height}
|
|
itemCount={rows.length}
|
|
itemSize={38}
|
|
width={'100%'}
|
|
style={{ overflow: 'hidden auto' }}
|
|
>
|
|
{renderRow}
|
|
</FixedSizeList>
|
|
</CustomScrollbar>
|
|
) : (
|
|
<div style={{ height: height }} className={styles.noData}>
|
|
No data
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const SymbolCell = ({ cell: { value } }: CellProps<TopTableValue, string>) => {
|
|
return <div>{value}</div>;
|
|
};
|
|
|
|
const UnitCell = ({ cell: { value } }: CellProps<TopTableValue, TopTableValue>) => {
|
|
return <div>{value.unitValue}</div>;
|
|
};
|
|
|
|
const getStyles = (theme: GrafanaTheme2) => ({
|
|
table: (height: number) => {
|
|
return css`
|
|
background-color: ${theme.colors.background.primary};
|
|
height: ${height}px;
|
|
overflow: scroll;
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
`;
|
|
},
|
|
header: css`
|
|
height: 38px;
|
|
|
|
& > :nth-child(2),
|
|
& > :nth-child(3) {
|
|
text-align: right;
|
|
}
|
|
|
|
// needed to keep header row height fixed so header row does not resize with browser
|
|
& > :nth-child(3) {
|
|
position: relative !important;
|
|
}
|
|
`,
|
|
headerCell: css`
|
|
background-color: ${theme.colors.background.secondary};
|
|
color: ${theme.colors.primary.text};
|
|
padding: ${theme.spacing(1)};
|
|
`,
|
|
matchedRow: css`
|
|
& > :nth-child(1),
|
|
& > :nth-child(2),
|
|
& > :nth-child(3) {
|
|
background-color: ${theme.colors.background.secondary} !important;
|
|
}
|
|
`,
|
|
row: css`
|
|
border-top: 1px solid ${theme.components.panel.borderColor};
|
|
|
|
&:hover {
|
|
background-color: ${theme.colors.emphasize(theme.colors.background.primary, 0.03)};
|
|
}
|
|
& > :nth-child(2),
|
|
& > :nth-child(3) {
|
|
text-align: right;
|
|
}
|
|
& > :nth-child(3) {
|
|
border-right: none;
|
|
}
|
|
`,
|
|
cell: css`
|
|
border-right: 1px solid ${theme.components.panel.borderColor};
|
|
padding: ${theme.spacing(1)};
|
|
|
|
div {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
&:hover {
|
|
overflow: visible;
|
|
width: auto !important;
|
|
box-shadow: 0 0 2px ${theme.colors.primary.main};
|
|
background-color: ${theme.colors.emphasize(theme.colors.background.primary, 0.03)};
|
|
z-index: 1;
|
|
}
|
|
`,
|
|
noData: css`
|
|
align-items: center;
|
|
display: flex;
|
|
justify-content: center;
|
|
`,
|
|
});
|
|
|
|
export default FlameGraphTopTable;
|