mirror of
https://github.com/grafana/grafana.git
synced 2026-02-18 18:20:52 -05:00
TableNG: Support managed sortBy (#117953)
* feat(TableNG): support managed sortBy
This commit is contained in:
parent
65c290a12f
commit
cb140a5e62
13 changed files with 375 additions and 37 deletions
|
|
@ -155,7 +155,7 @@ test.describe('Panels test: LogsTable', { tag: ['@panels', '@logstable'] }, () =
|
|||
page.getByLabel('Drawer title Inspect value').locator('.view-lines'),
|
||||
'Drawer contains correct log line'
|
||||
).toContainText(
|
||||
`level=info ts=2026-02-06T18:42:42.083508023Z caller=flush.go:253 msg="completing block" userid=29 blockID=73zco`
|
||||
`level=info ts=2026-02-06T18:42:46.211051027Z caller=poller.go:133 msg="blocklist poll complete" seconds=526`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -394,7 +394,7 @@ describe('TableNG', () => {
|
|||
async (desc) => {
|
||||
const { container } = render(
|
||||
<TableNG
|
||||
initialSortBy={[
|
||||
sortBy={[
|
||||
{
|
||||
displayName: 'Category',
|
||||
desc,
|
||||
|
|
@ -418,6 +418,104 @@ describe('TableNG', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('TableNG::sortBy', () => {
|
||||
it.each([true, false])('should set initial sort', async (desc) => {
|
||||
render(
|
||||
<TableNG
|
||||
sortBy={[
|
||||
{
|
||||
displayName: 'Column B',
|
||||
desc,
|
||||
},
|
||||
]}
|
||||
enableVirtualization={false}
|
||||
data={createBasicDataFrame()}
|
||||
width={800}
|
||||
height={600}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTitle('Column B')).toBeVisible();
|
||||
expect(screen.getByTestId(desc ? 'icon-arrow-down' : 'icon-arrow-up')).toBeVisible();
|
||||
});
|
||||
it('should not update sort on rerender if not managed', async () => {
|
||||
const { rerender } = render(
|
||||
<TableNG
|
||||
sortBy={[
|
||||
{
|
||||
displayName: 'Column B',
|
||||
desc: true,
|
||||
},
|
||||
]}
|
||||
enableVirtualization={false}
|
||||
data={createBasicDataFrame()}
|
||||
width={800}
|
||||
height={600}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTitle('Column B')).toBeVisible();
|
||||
expect(screen.getByTestId('icon-arrow-down')).toBeVisible();
|
||||
|
||||
rerender(
|
||||
<TableNG
|
||||
sortBy={[
|
||||
{
|
||||
displayName: 'Column B',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
enableVirtualization={false}
|
||||
data={createBasicDataFrame()}
|
||||
width={800}
|
||||
height={600}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTitle('Column B')).toBeVisible();
|
||||
expect(screen.getByTestId('icon-arrow-down')).toBeVisible();
|
||||
});
|
||||
it('should manage sort', async () => {
|
||||
const { rerender } = render(
|
||||
<TableNG
|
||||
sortBy={[
|
||||
{
|
||||
displayName: 'Column B',
|
||||
desc: true,
|
||||
},
|
||||
]}
|
||||
sortByBehavior={'managed'}
|
||||
enableVirtualization={false}
|
||||
data={createBasicDataFrame()}
|
||||
width={800}
|
||||
height={600}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTitle('Column B')).toBeVisible();
|
||||
expect(screen.getByTestId('icon-arrow-down')).toBeVisible();
|
||||
|
||||
rerender(
|
||||
<TableNG
|
||||
sortBy={[
|
||||
{
|
||||
displayName: 'Column B',
|
||||
desc: false,
|
||||
},
|
||||
]}
|
||||
sortByBehavior={'managed'}
|
||||
enableVirtualization={false}
|
||||
data={createBasicDataFrame()}
|
||||
width={800}
|
||||
height={600}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTitle('Column B')).toBeVisible();
|
||||
expect(screen.getByTestId('icon-arrow-up')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Basic TableNG rendering', () => {
|
||||
it('renders a simple table with columns and rows', async () => {
|
||||
const { container } = render(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import 'react-data-grid/lib/styles.css';
|
|||
|
||||
import { clsx } from 'clsx';
|
||||
import memoize from 'micro-memoize';
|
||||
import { CSSProperties, Key, ReactNode, useCallback, useMemo, useRef, useState, useEffect, type JSX } from 'react';
|
||||
import { CSSProperties, type JSX, Key, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Cell,
|
||||
CellRendererProps,
|
||||
|
|
@ -48,6 +48,7 @@ import {
|
|||
useColWidths,
|
||||
useFilteredRows,
|
||||
useHeaderHeight,
|
||||
useManagedSort,
|
||||
usePaginatedRows,
|
||||
useRowHeight,
|
||||
useScrollbarWidth,
|
||||
|
|
@ -63,19 +64,19 @@ import {
|
|||
getTooltipStyles,
|
||||
} from './styles';
|
||||
import {
|
||||
CellRootRenderer,
|
||||
FromFieldsResult,
|
||||
InspectCellProps,
|
||||
TableCellStyleOptions,
|
||||
TableColumn,
|
||||
TableNGProps,
|
||||
TableRow,
|
||||
TableSummaryRow,
|
||||
TableColumn,
|
||||
InspectCellProps,
|
||||
TableCellStyleOptions,
|
||||
FromFieldsResult,
|
||||
CellRootRenderer,
|
||||
} from './types';
|
||||
import {
|
||||
applySort,
|
||||
canFieldBeColorized,
|
||||
calculateFooterHeight,
|
||||
canFieldBeColorized,
|
||||
createTypographyContext,
|
||||
displayJsonValue,
|
||||
extractPixelValue,
|
||||
|
|
@ -89,15 +90,15 @@ import {
|
|||
getDisplayName,
|
||||
getIsNestedTable,
|
||||
getJustifyContent,
|
||||
getSummaryCellTextAlign,
|
||||
getVisibleFields,
|
||||
IS_SAFARI_26,
|
||||
isCellInspectEnabled,
|
||||
parseStyleJson,
|
||||
predicateByName,
|
||||
rowKeyGetter,
|
||||
shouldTextOverflow,
|
||||
shouldTextWrap,
|
||||
getSummaryCellTextAlign,
|
||||
parseStyleJson,
|
||||
IS_SAFARI_26,
|
||||
rowKeyGetter,
|
||||
} from './utils';
|
||||
|
||||
const EXPANDED_COLUMN_KEY = 'expanded';
|
||||
|
|
@ -114,7 +115,6 @@ export function TableNG(props: TableNGProps) {
|
|||
frozenColumns = 0,
|
||||
getActions = () => [],
|
||||
height,
|
||||
initialSortBy,
|
||||
maxRowHeight: _maxRowHeight,
|
||||
noHeader,
|
||||
onCellFilterAdded,
|
||||
|
|
@ -126,8 +126,9 @@ export function TableNG(props: TableNGProps) {
|
|||
transparent,
|
||||
width,
|
||||
initialRowIndex,
|
||||
sortBy,
|
||||
sortByBehavior = 'initial',
|
||||
} = props;
|
||||
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getGridStyles, enablePagination, transparent);
|
||||
const panelContext = usePanelContext();
|
||||
|
|
@ -182,7 +183,9 @@ export function TableNG(props: TableNGProps) {
|
|||
rows: sortedRows,
|
||||
sortColumns,
|
||||
setSortColumns,
|
||||
} = useSortedRows(filteredRows, data.fields, { hasNestedFrames, initialSortBy });
|
||||
} = useSortedRows(filteredRows, data.fields, { hasNestedFrames, initialSortBy: sortBy });
|
||||
|
||||
useManagedSort({ sortByBehavior, setSortColumns, sortBy });
|
||||
|
||||
const [inspectCell, setInspectCell] = useState<InspectCellProps | null>(null);
|
||||
const [tooltipState, setTooltipState] = useState<DataLinksActionsTooltipState>();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
useHeaderHeight,
|
||||
useRowHeight,
|
||||
useReducerEntries,
|
||||
useManagedSort,
|
||||
} from './hooks';
|
||||
import { TableRow } from './types';
|
||||
import { createTypographyContext } from './utils';
|
||||
|
|
@ -94,6 +95,63 @@ describe('TableNG hooks', () => {
|
|||
it.todo('should handle nested frames');
|
||||
});
|
||||
|
||||
describe('useManagedSort', () => {
|
||||
it('Should not update if sortBy is undefined', () => {
|
||||
const setSortColumns = jest.fn();
|
||||
renderHook(() =>
|
||||
useManagedSort({
|
||||
sortBy: undefined,
|
||||
sortByBehavior: 'managed',
|
||||
setSortColumns,
|
||||
})
|
||||
);
|
||||
|
||||
expect(setSortColumns).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it.each([true, false])('Should not update if behavior is managed', (desc) => {
|
||||
const setSortColumns = jest.fn();
|
||||
renderHook(() =>
|
||||
useManagedSort({
|
||||
sortBy: [
|
||||
{
|
||||
displayName: 'Alice',
|
||||
desc,
|
||||
},
|
||||
],
|
||||
sortByBehavior: 'managed',
|
||||
setSortColumns,
|
||||
})
|
||||
);
|
||||
|
||||
expect(setSortColumns).toHaveBeenCalledTimes(1);
|
||||
expect(setSortColumns).toHaveBeenCalledWith([
|
||||
{
|
||||
columnKey: 'Alice',
|
||||
direction: desc ? 'DESC' : 'ASC',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([true, false])('Should not update if behavior is initial', (desc) => {
|
||||
const setSortColumns = jest.fn();
|
||||
renderHook(() =>
|
||||
useManagedSort({
|
||||
sortBy: [
|
||||
{
|
||||
displayName: 'Alice',
|
||||
desc,
|
||||
},
|
||||
],
|
||||
sortByBehavior: 'initial',
|
||||
setSortColumns,
|
||||
})
|
||||
);
|
||||
|
||||
expect(setSortColumns).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSortedRows', () => {
|
||||
it('should correctly set up the table with an initial sort', () => {
|
||||
const { fields, rows } = setupData();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,15 @@ import { compareArrayValues, Field, FieldType, formattedValueToString, reduceFie
|
|||
import { TableColumnResizeActionCallback } from '../types';
|
||||
|
||||
import { TABLE } from './constants';
|
||||
import { FilterType, FooterFieldState, TableRow, TableSortByFieldState, TableSummaryRow, TypographyCtx } from './types';
|
||||
import {
|
||||
FilterType,
|
||||
FooterFieldState,
|
||||
SortByBehavior,
|
||||
TableRow,
|
||||
TableSortByFieldState,
|
||||
TableSummaryRow,
|
||||
TypographyCtx,
|
||||
} from './types';
|
||||
import {
|
||||
getDisplayName,
|
||||
processNestedTableRows,
|
||||
|
|
@ -98,6 +106,25 @@ export interface SortedRowsResult {
|
|||
setSortColumns: React.Dispatch<React.SetStateAction<SortColumn[]>>;
|
||||
}
|
||||
|
||||
interface ManagedSortProps {
|
||||
sortByBehavior: SortByBehavior;
|
||||
setSortColumns: React.Dispatch<React.SetStateAction<SortColumn[]>>;
|
||||
sortBy?: TableSortByFieldState[];
|
||||
}
|
||||
|
||||
export function useManagedSort({ sortByBehavior, setSortColumns, sortBy }: ManagedSortProps) {
|
||||
useEffect(() => {
|
||||
if (sortByBehavior === 'managed' && sortBy) {
|
||||
setSortColumns(
|
||||
sortBy.map(({ displayName, desc }) => ({
|
||||
columnKey: displayName,
|
||||
direction: desc === true ? 'DESC' : 'ASC',
|
||||
}))
|
||||
);
|
||||
}
|
||||
}, [setSortColumns, sortBy, sortByBehavior]);
|
||||
}
|
||||
|
||||
export function useSortedRows(
|
||||
rows: TableRow[],
|
||||
fields: Field[],
|
||||
|
|
|
|||
|
|
@ -115,6 +115,13 @@ export interface TableSortByFieldState {
|
|||
desc?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls how the `sortBy` prop is applied.
|
||||
* `initial` will only read from the options on initial render,
|
||||
* while `managed` will update the sort order whenever the sortBy array is changed.
|
||||
*/
|
||||
export type SortByBehavior = 'initial' | 'managed';
|
||||
|
||||
export interface BaseTableProps {
|
||||
ariaLabel?: string;
|
||||
data: DataFrame;
|
||||
|
|
@ -126,7 +133,8 @@ export interface BaseTableProps {
|
|||
noHeader?: boolean;
|
||||
showTypeIcons?: boolean;
|
||||
resizable?: boolean;
|
||||
initialSortBy?: TableSortByFieldState[];
|
||||
sortBy?: TableSortByFieldState[];
|
||||
sortByBehavior?: SortByBehavior;
|
||||
onColumnResize?: TableColumnResizeActionCallback;
|
||||
onSortByChange?: TableSortByActionCallback;
|
||||
onCellFilterAdded?: TableFilterActionCallback;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
PanelData,
|
||||
PanelProps,
|
||||
} from '@grafana/data';
|
||||
import type { Options as TableOptions } from '@grafana/schema/dist/esm/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { SETTING_KEY_ROOT } from 'app/features/explore/Logs/utils/logs';
|
||||
import { getDefaultFieldSelectorWidth } from 'app/features/logs/components/fieldSelector/FieldSelector';
|
||||
|
|
@ -30,8 +29,10 @@ import { useExtractFields } from './hooks/useExtractFields';
|
|||
import { useOrganizeFields } from './hooks/useOrganizeFields';
|
||||
import { copyLogsTableDashboardUrl } from './links/copyDashboardUrl';
|
||||
import { getDisplayedFields } from './options/getDisplayedFields';
|
||||
import { onSortOrderChange } from './options/onSortOrderChange';
|
||||
import { Options } from './options/types';
|
||||
import { defaultOptions, Options as LogsTableOptions } from './panelcfg.gen';
|
||||
import { Options as LogsTableOptions } from './panelcfg.gen';
|
||||
import { getInitialRowIndex } from './props/getInitialRowIndex';
|
||||
import { BuildLinkToLogLine, isOnLogsTableOptionsChange, OnLogsTableOptionsChange } from './types';
|
||||
|
||||
interface LogsTablePanelProps extends PanelProps<Options> {}
|
||||
|
|
@ -66,9 +67,7 @@ export const LogsTable = ({
|
|||
const timeFieldName = logsFrame?.timeField.name ?? LOGS_DATAPLANE_TIMESTAMP_NAME;
|
||||
const bodyFieldName = logsFrame?.bodyField.name ?? LOGS_DATAPLANE_BODY_NAME;
|
||||
const permalinkedLogId = getLogsPanelState()?.logs?.id ?? undefined;
|
||||
const initialRowIndex = permalinkedLogId
|
||||
? logsFrame?.idField?.values?.findIndex((id) => id === permalinkedLogId)
|
||||
: undefined;
|
||||
const initialRowIndex = getInitialRowIndex(permalinkedLogId, logsFrame);
|
||||
|
||||
const onLogsTableOptionsChange: OnLogsTableOptionsChange | undefined = isOnLogsTableOptionsChange(onOptionsChange)
|
||||
? onOptionsChange
|
||||
|
|
@ -78,10 +77,11 @@ export const LogsTable = ({
|
|||
|
||||
// Callbacks
|
||||
const handleTableOptionsChange = useCallback(
|
||||
(options: TableOptions) => {
|
||||
onLogsTableOptionsChange?.(options);
|
||||
(newOptions: Options) => {
|
||||
const pendingOptions = onSortOrderChange(newOptions, options.sortOrder, timeFieldName);
|
||||
onLogsTableOptionsChange?.(pendingOptions);
|
||||
},
|
||||
[onLogsTableOptionsChange]
|
||||
[onLogsTableOptionsChange, options.sortOrder, timeFieldName]
|
||||
);
|
||||
|
||||
const handleLogsTableOptionsChange = useCallback(
|
||||
|
|
@ -185,7 +185,11 @@ export const LogsTable = ({
|
|||
id={id}
|
||||
timeRange={timeRange}
|
||||
timeZone={timeZone}
|
||||
options={options}
|
||||
options={{
|
||||
sortOrder: LogsSortOrder.Descending,
|
||||
sortBy: [{ displayName: timeFieldName, desc: true }],
|
||||
...options,
|
||||
}}
|
||||
transparent={transparent}
|
||||
fieldConfig={fieldConfig}
|
||||
renderCounter={renderCounter}
|
||||
|
|
@ -197,7 +201,6 @@ export const LogsTable = ({
|
|||
onChangeTimeRange={onChangeTimeRange}
|
||||
logOptionsStorageKey={SETTING_KEY_ROOT}
|
||||
fieldSelectorWidth={fieldSelectorWidth}
|
||||
sortOrder={options.sortOrder ?? defaultOptions.sortOrder ?? LogsSortOrder.Descending}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ import {
|
|||
import { getAppEvents } from '@grafana/runtime';
|
||||
import type { Options as TableOptions } from '@grafana/schema/dist/esm/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { SETTING_KEY_ROOT } from 'app/features/explore/Logs/utils/logs';
|
||||
import { getDefaultControlsExpandedMode } from 'app/features/logs/components/panel/LogListContext';
|
||||
import { CONTROLS_WIDTH_EXPANDED } from 'app/features/logs/components/panel/LogListControls';
|
||||
import { LogTableControls } from 'app/features/logs/components/panel/LogTableControls';
|
||||
import { LOG_LIST_CONTROLS_WIDTH } from 'app/features/logs/components/panel/virtualization';
|
||||
|
||||
import { SETTING_KEY_ROOT } from '../../../features/explore/Logs/utils/logs';
|
||||
import { TablePanel } from '../table/TablePanel';
|
||||
|
||||
import { Options } from './options/types';
|
||||
|
|
@ -28,7 +28,6 @@ interface Props extends PanelProps<Options> {
|
|||
logOptionsStorageKey: string;
|
||||
containerElement: HTMLDivElement;
|
||||
fieldSelectorWidth: number;
|
||||
sortOrder: LogsSortOrder;
|
||||
}
|
||||
|
||||
export function TableNGWrap({
|
||||
|
|
@ -52,7 +51,6 @@ export function TableNGWrap({
|
|||
initialRowIndex,
|
||||
logOptionsStorageKey,
|
||||
containerElement,
|
||||
sortOrder,
|
||||
}: Props) {
|
||||
const showControls = options.showControls ?? defaultOptions.showControls ?? true;
|
||||
const controlsExpandedFromStore = store.getBool(
|
||||
|
|
@ -74,12 +72,12 @@ export function TableNGWrap({
|
|||
|
||||
const handleSortOrderChange = useCallback(
|
||||
(sortOrder: LogsSortOrder) => {
|
||||
onOptionsChange({ ...options, sortOrder });
|
||||
getAppEvents().publish(
|
||||
new LogSortOrderChangeEvent({
|
||||
order: sortOrder,
|
||||
})
|
||||
);
|
||||
onOptionsChange({ ...options, sortOrder });
|
||||
},
|
||||
[onOptionsChange, options]
|
||||
);
|
||||
|
|
@ -99,13 +97,14 @@ export function TableNGWrap({
|
|||
logOptionsStorageKey={SETTING_KEY_ROOT}
|
||||
controlsExpanded={controlsExpanded}
|
||||
setControlsExpanded={setControlsExpanded}
|
||||
sortOrder={sortOrder}
|
||||
sortOrder={options.sortOrder ?? LogsSortOrder.Descending}
|
||||
setSortOrder={handleSortOrderChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TablePanel
|
||||
sortByBehavior={'managed'}
|
||||
initialRowIndex={initialRowIndex}
|
||||
data={data}
|
||||
width={Math.max(tableWidth - fieldSelectorWidth - controlsWidth, 0)}
|
||||
|
|
@ -113,7 +112,7 @@ export function TableNGWrap({
|
|||
id={id}
|
||||
timeRange={timeRange}
|
||||
timeZone={timeZone}
|
||||
options={options}
|
||||
options={{ ...options }}
|
||||
transparent={transparent}
|
||||
fieldConfig={fieldConfig}
|
||||
renderCounter={renderCounter}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import { LogsSortOrder } from '@grafana/data';
|
||||
|
||||
import { onSortOrderChange } from './onSortOrderChange';
|
||||
import { Options } from './types';
|
||||
|
||||
describe('onSortOrderChange', () => {
|
||||
const timeFieldName = 'timestamp';
|
||||
it('should update', () => {
|
||||
const options = { sortBy: [], sortOrder: LogsSortOrder.Ascending } as unknown as Options;
|
||||
expect(onSortOrderChange(options, LogsSortOrder.Descending, timeFieldName)).toEqual({
|
||||
sortBy: [{ desc: false, displayName: timeFieldName }],
|
||||
sortOrder: LogsSortOrder.Ascending,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle undefined sortOrder', () => {
|
||||
const options = { sortBy: [], sortOrder: undefined } as unknown as Options;
|
||||
expect(onSortOrderChange(options, undefined, timeFieldName)).toEqual({
|
||||
sortBy: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not update', () => {
|
||||
const options = {
|
||||
sortBy: [{ desc: false, displayName: timeFieldName }],
|
||||
sortOrder: LogsSortOrder.Ascending,
|
||||
} as unknown as Options;
|
||||
|
||||
expect(onSortOrderChange(options, LogsSortOrder.Descending, timeFieldName)).toEqual({
|
||||
sortBy: [{ desc: false, displayName: timeFieldName }],
|
||||
sortOrder: LogsSortOrder.Ascending,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create new sortBy array', () => {
|
||||
const options = {
|
||||
sortBy: [{ desc: false, displayName: timeFieldName }],
|
||||
sortOrder: LogsSortOrder.Ascending,
|
||||
} as unknown as Options;
|
||||
const result = onSortOrderChange(options, LogsSortOrder.Descending, timeFieldName);
|
||||
|
||||
// Assert that sort is still ascending
|
||||
expect(result.sortBy).toEqual(options.sortBy);
|
||||
// Assert that return is new reference: if the array is the same reference, the useEffect will not re-render unless the length of the array has changed!
|
||||
expect(result.sortBy).not.toBe(options.sortBy);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { LogsSortOrder } from '@grafana/data';
|
||||
|
||||
import { Options } from './types';
|
||||
|
||||
export const onSortOrderChange = (
|
||||
pendingOptions: Options,
|
||||
currentSortOrder: LogsSortOrder | undefined,
|
||||
timeFieldName: string
|
||||
) => {
|
||||
if (pendingOptions.sortOrder && pendingOptions.sortOrder !== currentSortOrder) {
|
||||
const newSortBy = {
|
||||
desc: pendingOptions.sortOrder === LogsSortOrder.Descending,
|
||||
displayName: timeFieldName,
|
||||
};
|
||||
let sortBy = pendingOptions.sortBy ? [...pendingOptions.sortBy] : undefined;
|
||||
const existingSortByIdx = sortBy?.findIndex((option) => option.displayName === timeFieldName);
|
||||
if (sortBy && existingSortByIdx !== undefined && existingSortByIdx !== -1) {
|
||||
sortBy[existingSortByIdx] = newSortBy;
|
||||
} else {
|
||||
sortBy = [{ ...newSortBy }, ...(sortBy ?? [])];
|
||||
}
|
||||
|
||||
return { ...pendingOptions, sortBy };
|
||||
}
|
||||
|
||||
return pendingOptions;
|
||||
};
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { DataFrameType, FieldType, toDataFrame } from '@grafana/data';
|
||||
|
||||
import {
|
||||
LOGS_DATAPLANE_BODY_NAME,
|
||||
LOGS_DATAPLANE_TIMESTAMP_NAME,
|
||||
parseLogsFrame,
|
||||
} from '../../../../features/logs/logsFrame';
|
||||
|
||||
import { getInitialRowIndex } from './getInitialRowIndex';
|
||||
|
||||
const testLogsDataFrame = [
|
||||
toDataFrame({
|
||||
meta: {
|
||||
type: DataFrameType.LogLines,
|
||||
},
|
||||
fields: [
|
||||
{ name: LOGS_DATAPLANE_TIMESTAMP_NAME, type: FieldType.time, values: [1, 2] },
|
||||
{ name: LOGS_DATAPLANE_BODY_NAME, type: FieldType.string, values: ['log 1', 'log 2'] },
|
||||
{ name: 'id', type: FieldType.string, values: ['abc_123', 'def_456'] },
|
||||
{
|
||||
name: 'labels',
|
||||
type: FieldType.other,
|
||||
values: [
|
||||
{ service: 'frontend', level: 'info' },
|
||||
{ service: 'backend', level: 'error' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
const testLogsFrame = parseLogsFrame(testLogsDataFrame[0]);
|
||||
|
||||
describe('getInitialRowIndex', () => {
|
||||
it('should return undefined', () => {
|
||||
expect(getInitialRowIndex(undefined, null)).toBeUndefined();
|
||||
expect(getInitialRowIndex(undefined, testLogsFrame)).toBeUndefined();
|
||||
expect(getInitialRowIndex('abc_123', null)).toBeUndefined();
|
||||
});
|
||||
it('should return correct index', () => {
|
||||
expect(getInitialRowIndex('abc_123', testLogsFrame)).toEqual(0);
|
||||
expect(getInitialRowIndex('def_456', testLogsFrame)).toEqual(1);
|
||||
});
|
||||
it('should not return -1', () => {
|
||||
expect(getInitialRowIndex('nope', testLogsFrame)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { LogsFrame } from 'app/features/logs/logsFrame';
|
||||
|
||||
export const getInitialRowIndex = (permalinkedLogId: string | undefined, logsFrame: LogsFrame | null) => {
|
||||
if (!permalinkedLogId || !logsFrame) {
|
||||
return undefined;
|
||||
}
|
||||
const initialRowIndex = logsFrame.idField?.values.findIndex((id) => id === permalinkedLogId);
|
||||
return initialRowIndex !== undefined && initialRowIndex > -1 ? initialRowIndex : undefined;
|
||||
};
|
||||
|
|
@ -25,11 +25,23 @@ import { Options } from './panelcfg.gen';
|
|||
|
||||
interface Props extends PanelProps<Options> {
|
||||
initialRowIndex?: number;
|
||||
sortByBehavior?: 'initial' | 'managed';
|
||||
}
|
||||
|
||||
export function TablePanel(props: Props) {
|
||||
const { data, height, width, options, fieldConfig, id, timeRange, replaceVariables, transparent, initialRowIndex } =
|
||||
props;
|
||||
const {
|
||||
data,
|
||||
height,
|
||||
width,
|
||||
options,
|
||||
fieldConfig,
|
||||
id,
|
||||
timeRange,
|
||||
replaceVariables,
|
||||
transparent,
|
||||
initialRowIndex,
|
||||
sortByBehavior = 'initial',
|
||||
} = props;
|
||||
|
||||
useMemo(() => {
|
||||
cacheFieldDisplayNames(data.series);
|
||||
|
|
@ -77,7 +89,8 @@ export function TablePanel(props: Props) {
|
|||
noHeader={!options.showHeader}
|
||||
showTypeIcons={options.showTypeIcons}
|
||||
resizable={true}
|
||||
initialSortBy={options.sortBy}
|
||||
sortByBehavior={sortByBehavior}
|
||||
sortBy={options.sortBy}
|
||||
onSortByChange={(sortBy) => onSortByChange(sortBy, props)}
|
||||
onColumnResize={(displayName, resizedWidth) => onColumnResize(displayName, resizedWidth, props)}
|
||||
onCellFilterAdded={panelContext.onAddAdHocFilter}
|
||||
|
|
|
|||
Loading…
Reference in a new issue