mirror of
https://github.com/grafana/grafana.git
synced 2025-02-09 23:16:16 -06:00
Explore: Sort order of log results (#26669)
* Create sorting button and functionality * Set up logs ordering * Add tests * Refactor * Refactor * Replace new button with old * Move SortOrder enum to grafana/data * Update SortOrder in test * Update context based on sort order of logs * Update used method for panel, update tests * Rename prop to logsSortOrder * Memoize resuults * Add title too button * Add disablinng of button for 1sec * Update wordiing * Update packages/grafana-data/src/utils/logs.ts Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com> * Update packages/grafana-data/src/utils/logs.ts Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com> * Update test by reordering logs * Clear timers, add button flipping title Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
parent
4334e34366
commit
4a523c3248
@ -33,6 +33,11 @@ export enum LogsMetaKind {
|
||||
LabelsMap,
|
||||
}
|
||||
|
||||
export enum LogsSortOrder {
|
||||
Descending = 'Descending',
|
||||
Ascending = 'Ascending',
|
||||
}
|
||||
|
||||
export interface LogsMetaItem {
|
||||
label: string;
|
||||
value: string | number | Labels;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { LogLevel } from '../types/logs';
|
||||
import { LogLevel, LogsModel, LogRowModel, LogsSortOrder } from '../types/logs';
|
||||
import { MutableDataFrame } from '../dataframe/MutableDataFrame';
|
||||
import {
|
||||
getLogLevel,
|
||||
calculateLogsLabelStats,
|
||||
@ -7,6 +8,7 @@ import {
|
||||
LogsParsers,
|
||||
calculateStats,
|
||||
getLogLevelFromKey,
|
||||
sortLogsResult,
|
||||
} from './logs';
|
||||
|
||||
describe('getLoglevel()', () => {
|
||||
@ -284,3 +286,69 @@ describe('getParser()', () => {
|
||||
expect(getParser('{"foo": "bar", "baz": "41 + 1"}')).toEqual(LogsParsers.JSON);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortLogsResult', () => {
|
||||
const firstRow: LogRowModel = {
|
||||
rowIndex: 0,
|
||||
entryFieldIndex: 0,
|
||||
dataFrame: new MutableDataFrame(),
|
||||
entry: '',
|
||||
hasAnsi: false,
|
||||
labels: {},
|
||||
logLevel: LogLevel.info,
|
||||
raw: '',
|
||||
timeEpochMs: 0,
|
||||
timeEpochNs: '0',
|
||||
timeFromNow: '',
|
||||
timeLocal: '',
|
||||
timeUtc: '',
|
||||
uid: '1',
|
||||
};
|
||||
const sameAsFirstRow = firstRow;
|
||||
const secondRow: LogRowModel = {
|
||||
rowIndex: 1,
|
||||
entryFieldIndex: 0,
|
||||
dataFrame: new MutableDataFrame(),
|
||||
entry: '',
|
||||
hasAnsi: false,
|
||||
labels: {},
|
||||
logLevel: LogLevel.info,
|
||||
raw: '',
|
||||
timeEpochMs: 10,
|
||||
timeEpochNs: '10000000',
|
||||
timeFromNow: '',
|
||||
timeLocal: '',
|
||||
timeUtc: '',
|
||||
uid: '2',
|
||||
};
|
||||
|
||||
describe('when called with LogsSortOrder.Descending', () => {
|
||||
it('then it should sort descending', () => {
|
||||
const logsResult: LogsModel = {
|
||||
rows: [firstRow, sameAsFirstRow, secondRow],
|
||||
hasUniqueLabels: false,
|
||||
};
|
||||
const result = sortLogsResult(logsResult, LogsSortOrder.Descending);
|
||||
|
||||
expect(result).toEqual({
|
||||
rows: [secondRow, firstRow, sameAsFirstRow],
|
||||
hasUniqueLabels: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with LogsSortOrder.Ascending', () => {
|
||||
it('then it should sort ascending', () => {
|
||||
const logsResult: LogsModel = {
|
||||
rows: [secondRow, firstRow, sameAsFirstRow],
|
||||
hasUniqueLabels: false,
|
||||
};
|
||||
const result = sortLogsResult(logsResult, LogsSortOrder.Ascending);
|
||||
|
||||
expect(result).toEqual({
|
||||
rows: [firstRow, sameAsFirstRow, secondRow],
|
||||
hasUniqueLabels: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { countBy, chain, escapeRegExp } from 'lodash';
|
||||
|
||||
import { LogLevel, LogRowModel, LogLabelStatsModel, LogsParser } from '../types/logs';
|
||||
import { LogLevel, LogRowModel, LogLabelStatsModel, LogsParser, LogsModel, LogsSortOrder } from '../types/logs';
|
||||
import { DataFrame, FieldType } from '../types/index';
|
||||
import { ArrayVector } from '../vector/ArrayVector';
|
||||
|
||||
@ -158,3 +158,55 @@ export function getParser(line: string): LogsParser | undefined {
|
||||
|
||||
return parser;
|
||||
}
|
||||
|
||||
export const sortInAscendingOrder = (a: LogRowModel, b: LogRowModel) => {
|
||||
// compare milliseconds
|
||||
if (a.timeEpochMs < b.timeEpochMs) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.timeEpochMs > b.timeEpochMs) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// if milliseconds are equal, compare nanoseconds
|
||||
if (a.timeEpochNs < b.timeEpochNs) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.timeEpochNs > b.timeEpochNs) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const sortInDescendingOrder = (a: LogRowModel, b: LogRowModel) => {
|
||||
// compare milliseconds
|
||||
if (a.timeEpochMs > b.timeEpochMs) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.timeEpochMs < b.timeEpochMs) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// if milliseconds are equal, compare nanoseconds
|
||||
if (a.timeEpochNs > b.timeEpochNs) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.timeEpochNs < b.timeEpochNs) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const sortLogsResult = (logsResult: LogsModel | null, sortOrder: LogsSortOrder): LogsModel => {
|
||||
const rows = logsResult ? sortLogRows(logsResult.rows, sortOrder) : [];
|
||||
return logsResult ? { ...logsResult, rows } : { hasUniqueLabels: false, rows };
|
||||
};
|
||||
|
||||
export const sortLogRows = (logRows: LogRowModel[], sortOrder: LogsSortOrder) =>
|
||||
sortOrder === LogsSortOrder.Ascending ? logRows.sort(sortInAscendingOrder) : logRows.sort(sortInDescendingOrder);
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
Field,
|
||||
LinkModel,
|
||||
LogRowModel,
|
||||
LogsSortOrder,
|
||||
TimeZone,
|
||||
DataQueryResponse,
|
||||
GrafanaTheme,
|
||||
@ -38,6 +39,7 @@ interface Props extends Themeable {
|
||||
wrapLogMessage: boolean;
|
||||
timeZone: TimeZone;
|
||||
allowDetails?: boolean;
|
||||
logsSortOrder?: LogsSortOrder | null;
|
||||
getRows: () => LogRowModel[];
|
||||
onClickFilterLabel?: (key: string, value: string) => void;
|
||||
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||
@ -206,11 +208,16 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { showContext } = this.state;
|
||||
const { logsSortOrder } = this.props;
|
||||
|
||||
if (showContext) {
|
||||
return (
|
||||
<>
|
||||
<LogRowContextProvider row={this.props.row} getRowContext={this.props.getRowContext}>
|
||||
<LogRowContextProvider
|
||||
row={this.props.row}
|
||||
getRowContext={this.props.getRowContext}
|
||||
logsSortOrder={logsSortOrder}
|
||||
>
|
||||
{({ result, errors, hasMoreContextRows, updateLimit }) => {
|
||||
return <>{this.renderLogRow(result, errors, hasMoreContextRows, updateLimit)}</>;
|
||||
}}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { LogRowModel, toDataFrame, Field, FieldCache } from '@grafana/data';
|
||||
import { LogRowModel, toDataFrame, Field, FieldCache, LogsSortOrder } from '@grafana/data';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
@ -30,6 +30,7 @@ interface ResultType {
|
||||
|
||||
interface LogRowContextProviderProps {
|
||||
row: LogRowModel;
|
||||
logsSortOrder?: LogsSortOrder | null;
|
||||
getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>;
|
||||
children: (props: {
|
||||
result: LogRowContextRows;
|
||||
@ -43,7 +44,8 @@ interface LogRowContextProviderProps {
|
||||
export const getRowContexts = async (
|
||||
getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>,
|
||||
row: LogRowModel,
|
||||
limit: number
|
||||
limit: number,
|
||||
logsSortOrder?: LogsSortOrder | null
|
||||
) => {
|
||||
const promises = [
|
||||
getRowContext(row, {
|
||||
@ -58,62 +60,66 @@ export const getRowContexts = async (
|
||||
|
||||
const results: Array<DataQueryResponse | DataQueryError> = await Promise.all(promises.map(p => p.catch(e => e)));
|
||||
|
||||
return {
|
||||
data: results.map(result => {
|
||||
const dataResult: DataQueryResponse = result as DataQueryResponse;
|
||||
if (!dataResult.data) {
|
||||
return [];
|
||||
}
|
||||
const data = results.map(result => {
|
||||
const dataResult: DataQueryResponse = result as DataQueryResponse;
|
||||
if (!dataResult.data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data: any[] = [];
|
||||
for (let index = 0; index < dataResult.data.length; index++) {
|
||||
const dataFrame = toDataFrame(dataResult.data[index]);
|
||||
const fieldCache = new FieldCache(dataFrame);
|
||||
const timestampField: Field<string> = fieldCache.getFieldByName('ts')!;
|
||||
const idField: Field<string> | undefined = fieldCache.getFieldByName('id');
|
||||
const data: any[] = [];
|
||||
for (let index = 0; index < dataResult.data.length; index++) {
|
||||
const dataFrame = toDataFrame(dataResult.data[index]);
|
||||
const fieldCache = new FieldCache(dataFrame);
|
||||
const timestampField: Field<string> = fieldCache.getFieldByName('ts')!;
|
||||
const idField: Field<string> | undefined = fieldCache.getFieldByName('id');
|
||||
|
||||
for (let fieldIndex = 0; fieldIndex < timestampField.values.length; fieldIndex++) {
|
||||
// TODO: this filtering is datasource dependant so it will make sense to move it there so the API is
|
||||
// to return correct list of lines handling inclusive ranges or how to filter the correct line on the
|
||||
// datasource.
|
||||
for (let fieldIndex = 0; fieldIndex < timestampField.values.length; fieldIndex++) {
|
||||
// TODO: this filtering is datasource dependant so it will make sense to move it there so the API is
|
||||
// to return correct list of lines handling inclusive ranges or how to filter the correct line on the
|
||||
// datasource.
|
||||
|
||||
// Filter out the row that is the one used as a focal point for the context as we will get it in one of the
|
||||
// requests.
|
||||
if (idField) {
|
||||
// For Loki this means we filter only the one row. Issue is we could have other rows logged at the same
|
||||
// ns which came before but they come in the response that search for logs after. This means right now
|
||||
// we will show those as if they came after. This is not strictly correct but seems better than losing them
|
||||
// and making this correct would mean quite a bit of complexity to shuffle things around and messing up
|
||||
//counts.
|
||||
if (idField.values.get(fieldIndex) === row.uid) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to timestamp. This should not happen right now as this feature is implemented only for loki
|
||||
// and that has ID. Later this branch could be used in other DS but mind that this could also filter out
|
||||
// logs which were logged in the same timestamp and that can be a problem depending on the precision.
|
||||
if (parseInt(timestampField.values.get(fieldIndex), 10) === row.timeEpochMs) {
|
||||
continue;
|
||||
}
|
||||
// Filter out the row that is the one used as a focal point for the context as we will get it in one of the
|
||||
// requests.
|
||||
if (idField) {
|
||||
// For Loki this means we filter only the one row. Issue is we could have other rows logged at the same
|
||||
// ns which came before but they come in the response that search for logs after. This means right now
|
||||
// we will show those as if they came after. This is not strictly correct but seems better than losing them
|
||||
// and making this correct would mean quite a bit of complexity to shuffle things around and messing up
|
||||
//counts.
|
||||
if (idField.values.get(fieldIndex) === row.uid) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Fallback to timestamp. This should not happen right now as this feature is implemented only for loki
|
||||
// and that has ID. Later this branch could be used in other DS but mind that this could also filter out
|
||||
// logs which were logged in the same timestamp and that can be a problem depending on the precision.
|
||||
if (parseInt(timestampField.values.get(fieldIndex), 10) === row.timeEpochMs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lineField: Field<string> = dataFrame.fields.filter(field => field.name === 'line')[0];
|
||||
const line = lineField.values.get(fieldIndex); // assuming that both fields have same length
|
||||
|
||||
data.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}),
|
||||
errors: results.map(result => {
|
||||
const errorResult: DataQueryError = result as DataQueryError;
|
||||
if (!errorResult.message) {
|
||||
return '';
|
||||
}
|
||||
const lineField: Field<string> = dataFrame.fields.filter(field => field.name === 'line')[0];
|
||||
const line = lineField.values.get(fieldIndex); // assuming that both fields have same length
|
||||
|
||||
return errorResult.message;
|
||||
}),
|
||||
data.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return logsSortOrder === LogsSortOrder.Ascending ? data.reverse() : data;
|
||||
});
|
||||
|
||||
const errors = results.map(result => {
|
||||
const errorResult: DataQueryError = result as DataQueryError;
|
||||
if (!errorResult.message) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return errorResult.message;
|
||||
});
|
||||
|
||||
return {
|
||||
data: logsSortOrder === LogsSortOrder.Ascending ? data.reverse() : data,
|
||||
errors: logsSortOrder === LogsSortOrder.Ascending ? errors.reverse() : errors,
|
||||
};
|
||||
};
|
||||
|
||||
@ -121,6 +127,7 @@ export const LogRowContextProvider: React.FunctionComponent<LogRowContextProvide
|
||||
getRowContext,
|
||||
row,
|
||||
children,
|
||||
logsSortOrder,
|
||||
}) => {
|
||||
// React Hook that creates a number state value called limit to component state and a setter function called setLimit
|
||||
// The initial value for limit is 10
|
||||
@ -144,7 +151,7 @@ export const LogRowContextProvider: React.FunctionComponent<LogRowContextProvide
|
||||
// First promise fetches limit number of rows backwards in time from a specific point in time
|
||||
// Second promise fetches limit number of rows forwards in time from a specific point in time
|
||||
const { value } = useAsync(async () => {
|
||||
return await getRowContexts(getRowContext, row, limit); // Moved it to a separate function for debugging purposes
|
||||
return await getRowContexts(getRowContext, row, limit, logsSortOrder); // Moved it to a separate function for debugging purposes
|
||||
}, [limit]);
|
||||
|
||||
// React Hook that performs a side effect every time the value (from useAsync hook) prop changes
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { range } from 'lodash';
|
||||
import { LogRows, PREVIEW_LIMIT } from './LogRows';
|
||||
import { mount } from 'enzyme';
|
||||
import { LogLevel, LogRowModel, LogsDedupStrategy, MutableDataFrame } from '@grafana/data';
|
||||
import { LogLevel, LogRowModel, LogsDedupStrategy, MutableDataFrame, LogsSortOrder } from '@grafana/data';
|
||||
import { LogRow } from './LogRow';
|
||||
|
||||
describe('LogRows', () => {
|
||||
@ -93,10 +93,88 @@ describe('LogRows', () => {
|
||||
|
||||
expect(wrapper.find(LogRow).length).toBe(100);
|
||||
});
|
||||
|
||||
it('renders asc ordered rows if order and function supplied', () => {
|
||||
const rows: LogRowModel[] = [
|
||||
makeLog({ uid: '1', timeEpochMs: 1 }),
|
||||
makeLog({ uid: '3', timeEpochMs: 3 }),
|
||||
makeLog({ uid: '2', timeEpochMs: 2 }),
|
||||
];
|
||||
const wrapper = mount(
|
||||
<LogRows
|
||||
logRows={rows}
|
||||
dedupStrategy={LogsDedupStrategy.none}
|
||||
highlighterExpressions={[]}
|
||||
showLabels={false}
|
||||
showTime={false}
|
||||
wrapLogMessage={true}
|
||||
timeZone={'utc'}
|
||||
logsSortOrder={LogsSortOrder.Ascending}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find(LogRow)
|
||||
.at(0)
|
||||
.text()
|
||||
).toBe('log message 1');
|
||||
expect(
|
||||
wrapper
|
||||
.find(LogRow)
|
||||
.at(1)
|
||||
.text()
|
||||
).toBe('log message 2');
|
||||
expect(
|
||||
wrapper
|
||||
.find(LogRow)
|
||||
.at(2)
|
||||
.text()
|
||||
).toBe('log message 3');
|
||||
});
|
||||
it('renders desc ordered rows if order and function supplied', () => {
|
||||
const rows: LogRowModel[] = [
|
||||
makeLog({ uid: '1', timeEpochMs: 1 }),
|
||||
makeLog({ uid: '3', timeEpochMs: 3 }),
|
||||
makeLog({ uid: '2', timeEpochMs: 2 }),
|
||||
];
|
||||
const wrapper = mount(
|
||||
<LogRows
|
||||
logRows={rows}
|
||||
dedupStrategy={LogsDedupStrategy.none}
|
||||
highlighterExpressions={[]}
|
||||
showLabels={false}
|
||||
showTime={false}
|
||||
wrapLogMessage={true}
|
||||
timeZone={'utc'}
|
||||
logsSortOrder={LogsSortOrder.Descending}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find(LogRow)
|
||||
.at(0)
|
||||
.text()
|
||||
).toBe('log message 3');
|
||||
expect(
|
||||
wrapper
|
||||
.find(LogRow)
|
||||
.at(1)
|
||||
.text()
|
||||
).toBe('log message 2');
|
||||
expect(
|
||||
wrapper
|
||||
.find(LogRow)
|
||||
.at(2)
|
||||
.text()
|
||||
).toBe('log message 1');
|
||||
});
|
||||
});
|
||||
|
||||
const makeLog = (overrides: Partial<LogRowModel>): LogRowModel => {
|
||||
const uid = overrides.uid || '1';
|
||||
const timeEpochMs = overrides.timeEpochMs || 1;
|
||||
const entry = `log message ${uid}`;
|
||||
return {
|
||||
entryFieldIndex: 0,
|
||||
@ -110,8 +188,8 @@ const makeLog = (overrides: Partial<LogRowModel>): LogRowModel => {
|
||||
labels: {},
|
||||
raw: entry,
|
||||
timeFromNow: '',
|
||||
timeEpochMs: 1,
|
||||
timeEpochNs: '1000000',
|
||||
timeEpochMs,
|
||||
timeEpochNs: (timeEpochMs * 1000000).toString(),
|
||||
timeLocal: '',
|
||||
timeUtc: '',
|
||||
searchWords: [],
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { TimeZone, LogsDedupStrategy, LogRowModel, Field, LinkModel } from '@grafana/data';
|
||||
import { TimeZone, LogsDedupStrategy, LogRowModel, Field, LinkModel, LogsSortOrder, sortLogRows } from '@grafana/data';
|
||||
|
||||
import { Themeable } from '../../types/theme';
|
||||
import { withTheme } from '../../themes/index';
|
||||
@ -23,6 +23,7 @@ export interface Props extends Themeable {
|
||||
showTime: boolean;
|
||||
wrapLogMessage: boolean;
|
||||
timeZone: TimeZone;
|
||||
logsSortOrder?: LogsSortOrder | null;
|
||||
rowLimit?: number;
|
||||
allowDetails?: boolean;
|
||||
previewLimit?: number;
|
||||
@ -70,10 +71,14 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
makeGetRows = memoizeOne((processedRows: LogRowModel[]) => {
|
||||
return () => processedRows;
|
||||
makeGetRows = memoizeOne((orderedRows: LogRowModel[]) => {
|
||||
return () => orderedRows;
|
||||
});
|
||||
|
||||
sortLogs = memoizeOne((logRows: LogRowModel[], logsSortOrder: LogsSortOrder): LogRowModel[] =>
|
||||
sortLogRows(logRows, logsSortOrder)
|
||||
);
|
||||
|
||||
render() {
|
||||
const {
|
||||
dedupStrategy,
|
||||
@ -93,6 +98,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
previewLimit,
|
||||
getFieldLinks,
|
||||
disableCustomHorizontalScroll,
|
||||
logsSortOrder,
|
||||
} = this.props;
|
||||
const { renderAll } = this.state;
|
||||
const { logsRowsTable, logsRowsHorizontalScroll } = getLogRowStyles(theme);
|
||||
@ -109,12 +115,13 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
|
||||
// Staged rendering
|
||||
const processedRows = dedupedRows ? dedupedRows : [];
|
||||
const firstRows = processedRows.slice(0, previewLimit!);
|
||||
const rowCount = Math.min(processedRows.length, rowLimit!);
|
||||
const lastRows = processedRows.slice(previewLimit!, rowCount);
|
||||
const orderedRows = logsSortOrder ? this.sortLogs(processedRows, logsSortOrder) : processedRows;
|
||||
const firstRows = orderedRows.slice(0, previewLimit!);
|
||||
const rowCount = Math.min(orderedRows.length, rowLimit!);
|
||||
const lastRows = orderedRows.slice(previewLimit!, rowCount);
|
||||
|
||||
// React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead
|
||||
const getRows = this.makeGetRows(processedRows);
|
||||
const getRows = this.makeGetRows(orderedRows);
|
||||
const getRowContext = this.props.getRowContext ? this.props.getRowContext : () => Promise.resolve([]);
|
||||
|
||||
return (
|
||||
@ -139,6 +146,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||
getFieldLinks={getFieldLinks}
|
||||
logsSortOrder={logsSortOrder}
|
||||
/>
|
||||
))}
|
||||
{hasData &&
|
||||
@ -159,6 +167,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||
getFieldLinks={getFieldLinks}
|
||||
logsSortOrder={logsSortOrder}
|
||||
/>
|
||||
))}
|
||||
{hasData && !renderAll && (
|
||||
|
@ -28,10 +28,11 @@ import {
|
||||
textUtil,
|
||||
dateTime,
|
||||
AbsoluteTimeRange,
|
||||
sortInAscendingOrder,
|
||||
} from '@grafana/data';
|
||||
import { getThemeColor } from 'app/core/utils/colors';
|
||||
|
||||
import { sortInAscendingOrder, deduplicateLogRowsById } from 'app/core/utils/explore';
|
||||
import { deduplicateLogRowsById } from 'app/core/utils/explore';
|
||||
import { decimalSIPrefix } from '@grafana/data/src/valueFormats/symbolFormatters';
|
||||
|
||||
export const LogLevelColor = {
|
||||
|
@ -8,23 +8,12 @@ import {
|
||||
hasNonEmptyQuery,
|
||||
parseUrlState,
|
||||
refreshIntervalToSortOrder,
|
||||
sortLogsResult,
|
||||
SortOrder,
|
||||
updateHistory,
|
||||
getExploreUrl,
|
||||
GetExploreUrlArguments,
|
||||
} from './explore';
|
||||
import store from 'app/core/store';
|
||||
import {
|
||||
DataQueryError,
|
||||
dateTime,
|
||||
LogLevel,
|
||||
LogRowModel,
|
||||
LogsDedupStrategy,
|
||||
LogsModel,
|
||||
MutableDataFrame,
|
||||
ExploreUrlState,
|
||||
} from '@grafana/data';
|
||||
import { DataQueryError, dateTime, LogsDedupStrategy, ExploreUrlState, LogsSortOrder } from '@grafana/data';
|
||||
import { RefreshPicker } from '@grafana/ui';
|
||||
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
||||
|
||||
@ -392,7 +381,7 @@ describe('refreshIntervalToSortOrder', () => {
|
||||
it('then it should return ascending', () => {
|
||||
const result = refreshIntervalToSortOrder(RefreshPicker.liveOption.value);
|
||||
|
||||
expect(result).toBe(SortOrder.Ascending);
|
||||
expect(result).toBe(LogsSortOrder.Ascending);
|
||||
});
|
||||
});
|
||||
|
||||
@ -400,7 +389,7 @@ describe('refreshIntervalToSortOrder', () => {
|
||||
it('then it should return descending', () => {
|
||||
const result = refreshIntervalToSortOrder(RefreshPicker.offOption.value);
|
||||
|
||||
expect(result).toBe(SortOrder.Descending);
|
||||
expect(result).toBe(LogsSortOrder.Descending);
|
||||
});
|
||||
});
|
||||
|
||||
@ -408,7 +397,7 @@ describe('refreshIntervalToSortOrder', () => {
|
||||
it('then it should return descending', () => {
|
||||
const result = refreshIntervalToSortOrder('5s');
|
||||
|
||||
expect(result).toBe(SortOrder.Descending);
|
||||
expect(result).toBe(LogsSortOrder.Descending);
|
||||
});
|
||||
});
|
||||
|
||||
@ -416,102 +405,31 @@ describe('refreshIntervalToSortOrder', () => {
|
||||
it('then it should return descending', () => {
|
||||
const result = refreshIntervalToSortOrder(undefined);
|
||||
|
||||
expect(result).toBe(SortOrder.Descending);
|
||||
expect(result).toBe(LogsSortOrder.Descending);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortLogsResult', () => {
|
||||
const firstRow: LogRowModel = {
|
||||
rowIndex: 0,
|
||||
entryFieldIndex: 0,
|
||||
dataFrame: new MutableDataFrame(),
|
||||
entry: '',
|
||||
hasAnsi: false,
|
||||
labels: {},
|
||||
logLevel: LogLevel.info,
|
||||
raw: '',
|
||||
timeEpochMs: 0,
|
||||
timeEpochNs: '0',
|
||||
timeFromNow: '',
|
||||
timeLocal: '',
|
||||
timeUtc: '',
|
||||
uid: '1',
|
||||
};
|
||||
const sameAsFirstRow = firstRow;
|
||||
const secondRow: LogRowModel = {
|
||||
rowIndex: 1,
|
||||
entryFieldIndex: 0,
|
||||
dataFrame: new MutableDataFrame(),
|
||||
entry: '',
|
||||
hasAnsi: false,
|
||||
labels: {},
|
||||
logLevel: LogLevel.info,
|
||||
raw: '',
|
||||
timeEpochMs: 10,
|
||||
timeEpochNs: '10000000',
|
||||
timeFromNow: '',
|
||||
timeLocal: '',
|
||||
timeUtc: '',
|
||||
uid: '2',
|
||||
};
|
||||
|
||||
describe('when called with SortOrder.Descending', () => {
|
||||
it('then it should sort descending', () => {
|
||||
const logsResult: LogsModel = {
|
||||
rows: [firstRow, sameAsFirstRow, secondRow],
|
||||
hasUniqueLabels: false,
|
||||
};
|
||||
const result = sortLogsResult(logsResult, SortOrder.Descending);
|
||||
|
||||
expect(result).toEqual({
|
||||
rows: [secondRow, firstRow, sameAsFirstRow],
|
||||
hasUniqueLabels: false,
|
||||
});
|
||||
});
|
||||
describe('when buildQueryTransaction', () => {
|
||||
it('it should calculate interval based on time range', () => {
|
||||
const queries = [{ refId: 'A' }];
|
||||
const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
|
||||
const range = { from: dateTime().subtract(1, 'd'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
||||
const transaction = buildQueryTransaction(queries, queryOptions, range, false);
|
||||
expect(transaction.request.intervalMs).toEqual(60000);
|
||||
});
|
||||
|
||||
describe('when called with SortOrder.Ascending', () => {
|
||||
it('then it should sort ascending', () => {
|
||||
const logsResult: LogsModel = {
|
||||
rows: [secondRow, firstRow, sameAsFirstRow],
|
||||
hasUniqueLabels: false,
|
||||
};
|
||||
const result = sortLogsResult(logsResult, SortOrder.Ascending);
|
||||
|
||||
expect(result).toEqual({
|
||||
rows: [firstRow, sameAsFirstRow, secondRow],
|
||||
hasUniqueLabels: false,
|
||||
});
|
||||
});
|
||||
it('it should calculate interval taking minInterval into account', () => {
|
||||
const queries = [{ refId: 'A' }];
|
||||
const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
|
||||
const range = { from: dateTime().subtract(1, 'm'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
||||
const transaction = buildQueryTransaction(queries, queryOptions, range, false);
|
||||
expect(transaction.request.intervalMs).toEqual(15000);
|
||||
});
|
||||
|
||||
describe('when buildQueryTransaction', () => {
|
||||
it('it should calculate interval based on time range', () => {
|
||||
const queries = [{ refId: 'A' }];
|
||||
const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
|
||||
const range = { from: dateTime().subtract(1, 'd'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
||||
const transaction = buildQueryTransaction(queries, queryOptions, range, false);
|
||||
|
||||
expect(transaction.request.intervalMs).toEqual(60000);
|
||||
});
|
||||
|
||||
it('it should calculate interval taking minInterval into account', () => {
|
||||
const queries = [{ refId: 'A' }];
|
||||
const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
|
||||
const range = { from: dateTime().subtract(1, 'm'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
||||
const transaction = buildQueryTransaction(queries, queryOptions, range, false);
|
||||
|
||||
expect(transaction.request.intervalMs).toEqual(15000);
|
||||
});
|
||||
|
||||
it('it should calculate interval taking maxDataPoints into account', () => {
|
||||
const queries = [{ refId: 'A' }];
|
||||
const queryOptions = { maxDataPoints: 10, minInterval: '15s' };
|
||||
const range = { from: dateTime().subtract(1, 'd'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
||||
const transaction = buildQueryTransaction(queries, queryOptions, range, false);
|
||||
|
||||
expect(transaction.request.interval).toEqual('2h');
|
||||
});
|
||||
it('it should calculate interval taking maxDataPoints into account', () => {
|
||||
const queries = [{ refId: 'A' }];
|
||||
const queryOptions = { maxDataPoints: 10, minInterval: '15s' };
|
||||
const range = { from: dateTime().subtract(1, 'd'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
||||
const transaction = buildQueryTransaction(queries, queryOptions, range, false);
|
||||
expect(transaction.request.interval).toEqual('2h');
|
||||
});
|
||||
});
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
IntervalValues,
|
||||
LogRowModel,
|
||||
LogsDedupStrategy,
|
||||
LogsModel,
|
||||
LogsSortOrder,
|
||||
RawTimeRange,
|
||||
TimeFragment,
|
||||
TimeRange,
|
||||
@ -464,67 +464,8 @@ export const getRefIds = (value: any): string[] => {
|
||||
return _.uniq(_.flatten(refIds));
|
||||
};
|
||||
|
||||
export const sortInAscendingOrder = (a: LogRowModel, b: LogRowModel) => {
|
||||
// compare milliseconds
|
||||
if (a.timeEpochMs < b.timeEpochMs) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.timeEpochMs > b.timeEpochMs) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// if milliseonds are equal, compare nanoseconds
|
||||
if (a.timeEpochNs < b.timeEpochNs) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.timeEpochNs > b.timeEpochNs) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const sortInDescendingOrder = (a: LogRowModel, b: LogRowModel) => {
|
||||
// compare milliseconds
|
||||
if (a.timeEpochMs > b.timeEpochMs) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.timeEpochMs < b.timeEpochMs) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// if milliseonds are equal, compare nanoseconds
|
||||
if (a.timeEpochNs > b.timeEpochNs) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.timeEpochNs < b.timeEpochNs) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export enum SortOrder {
|
||||
Descending = 'Descending',
|
||||
Ascending = 'Ascending',
|
||||
DatasourceAZ = 'Datasource A-Z',
|
||||
DatasourceZA = 'Datasource Z-A',
|
||||
}
|
||||
|
||||
export const refreshIntervalToSortOrder = (refreshInterval?: string) =>
|
||||
RefreshPicker.isLive(refreshInterval) ? SortOrder.Ascending : SortOrder.Descending;
|
||||
|
||||
export const sortLogsResult = (logsResult: LogsModel | null, sortOrder: SortOrder): LogsModel => {
|
||||
const rows = logsResult ? logsResult.rows : [];
|
||||
sortOrder === SortOrder.Ascending ? rows.sort(sortInAscendingOrder) : rows.sort(sortInDescendingOrder);
|
||||
const result: LogsModel = logsResult ? { ...logsResult, rows } : { hasUniqueLabels: false, rows };
|
||||
|
||||
return result;
|
||||
};
|
||||
RefreshPicker.isLive(refreshInterval) ? LogsSortOrder.Ascending : LogsSortOrder.Descending;
|
||||
|
||||
export const convertToWebSocketUrl = (url: string) => {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
filterAndSortQueries,
|
||||
} from './richHistory';
|
||||
import store from 'app/core/store';
|
||||
import { SortOrder } from './explore';
|
||||
import { SortOrder } from './richHistory';
|
||||
import { dateTime, DataQuery } from '@grafana/data';
|
||||
|
||||
const mock: any = {
|
||||
|
@ -5,7 +5,6 @@ import _ from 'lodash';
|
||||
import { DataQuery, DataSourceApi, dateTimeFormat, AppEvents, urlUtil, ExploreUrlState } from '@grafana/data';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import store from 'app/core/store';
|
||||
import { SortOrder } from './explore';
|
||||
import { getExploreDatasources } from '../../features/explore/state/selectors';
|
||||
|
||||
// Types
|
||||
@ -21,6 +20,13 @@ export const RICH_HISTORY_SETTING_KEYS = {
|
||||
datasourceFilters: 'grafana.explore.richHistory.datasourceFilters',
|
||||
};
|
||||
|
||||
export enum SortOrder {
|
||||
Descending = 'Descending',
|
||||
Ascending = 'Ascending',
|
||||
DatasourceAZ = 'Datasource A-Z',
|
||||
DatasourceZA = 'Datasource Z-A',
|
||||
}
|
||||
|
||||
/*
|
||||
* Add queries to rich history. Save only queries within the retention period, or that are starred.
|
||||
* Side-effect: store history in local storage
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
|
||||
import {
|
||||
rangeUtil,
|
||||
@ -11,6 +12,7 @@ import {
|
||||
LogRowModel,
|
||||
LogsDedupDescription,
|
||||
LogsMetaItem,
|
||||
LogsSortOrder,
|
||||
GraphSeriesXY,
|
||||
LinkModel,
|
||||
Field,
|
||||
@ -72,13 +74,39 @@ interface State {
|
||||
showLabels: boolean;
|
||||
showTime: boolean;
|
||||
wrapLogMessage: boolean;
|
||||
logsSortOrder: LogsSortOrder | null;
|
||||
isFlipping: boolean;
|
||||
}
|
||||
|
||||
export class Logs extends PureComponent<Props, State> {
|
||||
flipOrderTimer: NodeJS.Timeout;
|
||||
cancelFlippingTimer: NodeJS.Timeout;
|
||||
|
||||
state = {
|
||||
showLabels: store.getBool(SETTINGS_KEYS.showLabels, false),
|
||||
showTime: store.getBool(SETTINGS_KEYS.showTime, true),
|
||||
wrapLogMessage: store.getBool(SETTINGS_KEYS.wrapLogMessage, true),
|
||||
logsSortOrder: null,
|
||||
isFlipping: false,
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.flipOrderTimer);
|
||||
clearTimeout(this.cancelFlippingTimer);
|
||||
}
|
||||
|
||||
onChangeLogsSortOrder = () => {
|
||||
this.setState({ isFlipping: true });
|
||||
// we are using setTimeout here to make sure that disabled button is rendered before the rendering of reordered logs
|
||||
this.flipOrderTimer = setTimeout(() => {
|
||||
this.setState(prevState => {
|
||||
if (prevState.logsSortOrder === null || prevState.logsSortOrder === LogsSortOrder.Descending) {
|
||||
return { logsSortOrder: LogsSortOrder.Ascending };
|
||||
}
|
||||
return { logsSortOrder: LogsSortOrder.Descending };
|
||||
});
|
||||
}, 0);
|
||||
this.cancelFlippingTimer = setTimeout(() => this.setState({ isFlipping: false }), 1000);
|
||||
};
|
||||
|
||||
onChangeDedup = (dedup: LogsDedupStrategy) => {
|
||||
@ -166,7 +194,7 @@ export class Logs extends PureComponent<Props, State> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { showLabels, showTime, wrapLogMessage } = this.state;
|
||||
const { showLabels, showTime, wrapLogMessage, logsSortOrder, isFlipping } = this.state;
|
||||
const { dedupStrategy } = this.props;
|
||||
const hasData = logRows && logRows.length > 0;
|
||||
const dedupCount = dedupedRows
|
||||
@ -214,23 +242,39 @@ export class Logs extends PureComponent<Props, State> {
|
||||
</div>
|
||||
<div className="logs-panel-options">
|
||||
<div className="logs-panel-controls">
|
||||
<Switch label="Time" checked={showTime} onChange={this.onChangeTime} transparent />
|
||||
<Switch label="Unique labels" checked={showLabels} onChange={this.onChangeLabels} transparent />
|
||||
<Switch label="Wrap lines" checked={wrapLogMessage} onChange={this.onChangewrapLogMessage} transparent />
|
||||
<ToggleButtonGroup label="Dedup" transparent={true}>
|
||||
{Object.keys(LogsDedupStrategy).map((dedupType: string, i) => (
|
||||
<ToggleButton
|
||||
key={i}
|
||||
value={dedupType}
|
||||
onChange={this.onChangeDedup}
|
||||
selected={dedupStrategy === dedupType}
|
||||
// @ts-ignore
|
||||
tooltip={LogsDedupDescription[dedupType]}
|
||||
>
|
||||
{dedupType}
|
||||
</ToggleButton>
|
||||
))}
|
||||
</ToggleButtonGroup>
|
||||
<div className="logs-panel-controls-main">
|
||||
<Switch label="Time" checked={showTime} onChange={this.onChangeTime} transparent />
|
||||
<Switch label="Unique labels" checked={showLabels} onChange={this.onChangeLabels} transparent />
|
||||
<Switch label="Wrap lines" checked={wrapLogMessage} onChange={this.onChangewrapLogMessage} transparent />
|
||||
<ToggleButtonGroup label="Dedup" transparent={true}>
|
||||
{Object.keys(LogsDedupStrategy).map((dedupType: string, i) => (
|
||||
<ToggleButton
|
||||
key={i}
|
||||
value={dedupType}
|
||||
onChange={this.onChangeDedup}
|
||||
selected={dedupStrategy === dedupType}
|
||||
// @ts-ignore
|
||||
tooltip={LogsDedupDescription[dedupType]}
|
||||
>
|
||||
{dedupType}
|
||||
</ToggleButton>
|
||||
))}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<button
|
||||
disabled={isFlipping}
|
||||
title={logsSortOrder === LogsSortOrder.Ascending ? 'Change to newest first' : 'Change to oldest first'}
|
||||
aria-label="Flip results order"
|
||||
className={cx(
|
||||
'gf-form-label gf-form-label--btn',
|
||||
css`
|
||||
margin-top: 4px;
|
||||
`
|
||||
)}
|
||||
onClick={this.onChangeLogsSortOrder}
|
||||
>
|
||||
<span className="btn-title">{isFlipping ? 'Flipping...' : 'Flip results order'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -260,6 +304,7 @@ export class Logs extends PureComponent<Props, State> {
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
timeZone={timeZone}
|
||||
getFieldLinks={getFieldLinks}
|
||||
logsSortOrder={logsSortOrder}
|
||||
/>
|
||||
|
||||
{!loading && !hasData && !scanning && (
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
//Services & Utils
|
||||
import { SortOrder } from 'app/core/utils/explore';
|
||||
import { RICH_HISTORY_SETTING_KEYS } from 'app/core/utils/richHistory';
|
||||
import { RICH_HISTORY_SETTING_KEYS, SortOrder } from 'app/core/utils/richHistory';
|
||||
import store from 'app/core/store';
|
||||
import { withTheme, TabbedContainer, TabConfig } from '@grafana/ui';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { ExploreId } from '../../../types/explore';
|
||||
import { SortOrder } from 'app/core/utils/explore';
|
||||
import { SortOrder } from 'app/core/utils/richHistory';
|
||||
import { RichHistoryQueriesTab, Props } from './RichHistoryQueriesTab';
|
||||
import { Slider } from '@grafana/ui';
|
||||
|
||||
|
@ -9,8 +9,8 @@ import { RichHistoryQuery, ExploreId } from 'app/types/explore';
|
||||
import { stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
||||
|
||||
import { SortOrder } from 'app/core/utils/explore';
|
||||
import {
|
||||
SortOrder,
|
||||
mapNumbertoTimeInSlider,
|
||||
mapQueriesToHeadings,
|
||||
createDatasourcesList,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { ExploreId } from '../../../types/explore';
|
||||
import { SortOrder } from 'app/core/utils/explore';
|
||||
import { SortOrder } from 'app/core/utils/richHistory';
|
||||
import { RichHistoryStarredTab, Props } from './RichHistoryStarredTab';
|
||||
|
||||
jest.mock('../state/selectors', () => ({ getExploreDatasources: jest.fn() }));
|
||||
|
@ -8,9 +8,7 @@ import { RichHistoryQuery, ExploreId } from 'app/types/explore';
|
||||
// Utils
|
||||
import { stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
||||
|
||||
import { SortOrder } from '../../../core/utils/explore';
|
||||
import { filterAndSortQueries, createDatasourcesList } from '../../../core/utils/richHistory';
|
||||
import { filterAndSortQueries, createDatasourcesList, SortOrder } from 'app/core/utils/richHistory';
|
||||
|
||||
// Components
|
||||
import RichHistoryCard from './RichHistoryCard';
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
toLegacyResponseData,
|
||||
ExploreMode,
|
||||
LogsDedupStrategy,
|
||||
sortLogsResult,
|
||||
} from '@grafana/data';
|
||||
import { RefreshPicker } from '@grafana/ui';
|
||||
import { LocationUpdate } from '@grafana/runtime';
|
||||
@ -23,7 +24,6 @@ import {
|
||||
getQueryKeys,
|
||||
parseUrlState,
|
||||
refreshIntervalToSortOrder,
|
||||
sortLogsResult,
|
||||
stopQueryState,
|
||||
} from 'app/core/utils/explore';
|
||||
import { ExploreId, ExploreItemState, ExploreState, ExploreUpdateState } from 'app/types/explore';
|
||||
|
@ -7,9 +7,10 @@ import {
|
||||
getDisplayProcessor,
|
||||
PreferredVisualisationType,
|
||||
standardTransformers,
|
||||
sortLogsResult,
|
||||
} from '@grafana/data';
|
||||
import { ExploreItemState } from 'app/types/explore';
|
||||
import { sortLogsResult, refreshIntervalToSortOrder } from 'app/core/utils/explore';
|
||||
import { refreshIntervalToSortOrder } from 'app/core/utils/explore';
|
||||
import { dataFrameToLogsModel } from 'app/core/logs_model';
|
||||
import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel';
|
||||
import { config } from 'app/core/config';
|
||||
|
@ -3,7 +3,6 @@ import { LogRows, CustomScrollbar } from '@grafana/ui';
|
||||
import { LogsDedupStrategy, PanelProps } from '@grafana/data';
|
||||
import { Options } from './types';
|
||||
import { dataFrameToLogsModel } from 'app/core/logs_model';
|
||||
import { sortLogsResult } from 'app/core/utils/explore';
|
||||
|
||||
interface LogsPanelProps extends PanelProps<Options> {}
|
||||
|
||||
@ -22,12 +21,11 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
|
||||
}
|
||||
|
||||
const newResults = data ? dataFrameToLogsModel(data.series, data.request?.intervalMs, timeZone) : null;
|
||||
const sortedNewResults = sortLogsResult(newResults, sortOrder);
|
||||
|
||||
return (
|
||||
<CustomScrollbar autoHide>
|
||||
<LogRows
|
||||
logRows={sortedNewResults.rows}
|
||||
logRows={newResults?.rows || []}
|
||||
dedupStrategy={LogsDedupStrategy.none}
|
||||
highlighterExpressions={[]}
|
||||
showLabels={showLabels}
|
||||
@ -36,6 +34,7 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
|
||||
timeZone={timeZone}
|
||||
allowDetails={true}
|
||||
disableCustomHorizontalScroll={true}
|
||||
logsSortOrder={sortOrder}
|
||||
/>
|
||||
</CustomScrollbar>
|
||||
);
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
import { PanelPlugin, LogsSortOrder } from '@grafana/data';
|
||||
import { Options } from './types';
|
||||
import { LogsPanel } from './LogsPanel';
|
||||
import { SortOrder } from '../../../core/utils/explore';
|
||||
|
||||
export const plugin = new PanelPlugin<Options>(LogsPanel).setPanelOptions(builder => {
|
||||
builder
|
||||
@ -29,10 +28,10 @@ export const plugin = new PanelPlugin<Options>(LogsPanel).setPanelOptions(builde
|
||||
description: '',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: SortOrder.Descending, label: 'Descending' },
|
||||
{ value: SortOrder.Ascending, label: 'Ascending' },
|
||||
{ value: LogsSortOrder.Descending, label: 'Descending' },
|
||||
{ value: LogsSortOrder.Ascending, label: 'Ascending' },
|
||||
],
|
||||
},
|
||||
defaultValue: SortOrder.Descending,
|
||||
defaultValue: LogsSortOrder.Descending,
|
||||
});
|
||||
});
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { SortOrder } from 'app/core/utils/explore';
|
||||
import { LogsSortOrder } from '@grafana/data';
|
||||
|
||||
export interface Options {
|
||||
showLabels: boolean;
|
||||
showTime: boolean;
|
||||
wrapLogMessage: boolean;
|
||||
sortOrder: SortOrder;
|
||||
sortOrder: LogsSortOrder;
|
||||
}
|
||||
|
@ -12,12 +12,18 @@ $column-horizontal-spacing: 10px;
|
||||
|
||||
.logs-panel-controls {
|
||||
display: flex;
|
||||
justify-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
.logs-panel-controls-main {
|
||||
display: flex;
|
||||
justify-items: flex-start;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> * {
|
||||
margin-right: $spacer * 2;
|
||||
> * {
|
||||
margin-right: $spacer * 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user