mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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,
|
LabelsMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum LogsSortOrder {
|
||||||
|
Descending = 'Descending',
|
||||||
|
Ascending = 'Ascending',
|
||||||
|
}
|
||||||
|
|
||||||
export interface LogsMetaItem {
|
export interface LogsMetaItem {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number | Labels;
|
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 {
|
import {
|
||||||
getLogLevel,
|
getLogLevel,
|
||||||
calculateLogsLabelStats,
|
calculateLogsLabelStats,
|
||||||
@ -7,6 +8,7 @@ import {
|
|||||||
LogsParsers,
|
LogsParsers,
|
||||||
calculateStats,
|
calculateStats,
|
||||||
getLogLevelFromKey,
|
getLogLevelFromKey,
|
||||||
|
sortLogsResult,
|
||||||
} from './logs';
|
} from './logs';
|
||||||
|
|
||||||
describe('getLoglevel()', () => {
|
describe('getLoglevel()', () => {
|
||||||
@ -284,3 +286,69 @@ describe('getParser()', () => {
|
|||||||
expect(getParser('{"foo": "bar", "baz": "41 + 1"}')).toEqual(LogsParsers.JSON);
|
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 { 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 { DataFrame, FieldType } from '../types/index';
|
||||||
import { ArrayVector } from '../vector/ArrayVector';
|
import { ArrayVector } from '../vector/ArrayVector';
|
||||||
|
|
||||||
@ -158,3 +158,55 @@ export function getParser(line: string): LogsParser | undefined {
|
|||||||
|
|
||||||
return parser;
|
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,
|
Field,
|
||||||
LinkModel,
|
LinkModel,
|
||||||
LogRowModel,
|
LogRowModel,
|
||||||
|
LogsSortOrder,
|
||||||
TimeZone,
|
TimeZone,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
GrafanaTheme,
|
GrafanaTheme,
|
||||||
@ -38,6 +39,7 @@ interface Props extends Themeable {
|
|||||||
wrapLogMessage: boolean;
|
wrapLogMessage: boolean;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
allowDetails?: boolean;
|
allowDetails?: boolean;
|
||||||
|
logsSortOrder?: LogsSortOrder | null;
|
||||||
getRows: () => LogRowModel[];
|
getRows: () => LogRowModel[];
|
||||||
onClickFilterLabel?: (key: string, value: string) => void;
|
onClickFilterLabel?: (key: string, value: string) => void;
|
||||||
onClickFilterOutLabel?: (key: string, value: string) => void;
|
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||||
@ -206,11 +208,16 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { showContext } = this.state;
|
const { showContext } = this.state;
|
||||||
|
const { logsSortOrder } = this.props;
|
||||||
|
|
||||||
if (showContext) {
|
if (showContext) {
|
||||||
return (
|
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 }) => {
|
{({ result, errors, hasMoreContextRows, updateLimit }) => {
|
||||||
return <>{this.renderLogRow(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 React, { useState, useEffect } from 'react';
|
||||||
import useAsync from 'react-use/lib/useAsync';
|
import useAsync from 'react-use/lib/useAsync';
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ interface ResultType {
|
|||||||
|
|
||||||
interface LogRowContextProviderProps {
|
interface LogRowContextProviderProps {
|
||||||
row: LogRowModel;
|
row: LogRowModel;
|
||||||
|
logsSortOrder?: LogsSortOrder | null;
|
||||||
getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>;
|
getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>;
|
||||||
children: (props: {
|
children: (props: {
|
||||||
result: LogRowContextRows;
|
result: LogRowContextRows;
|
||||||
@ -43,7 +44,8 @@ interface LogRowContextProviderProps {
|
|||||||
export const getRowContexts = async (
|
export const getRowContexts = async (
|
||||||
getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>,
|
getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>,
|
||||||
row: LogRowModel,
|
row: LogRowModel,
|
||||||
limit: number
|
limit: number,
|
||||||
|
logsSortOrder?: LogsSortOrder | null
|
||||||
) => {
|
) => {
|
||||||
const promises = [
|
const promises = [
|
||||||
getRowContext(row, {
|
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)));
|
const results: Array<DataQueryResponse | DataQueryError> = await Promise.all(promises.map(p => p.catch(e => e)));
|
||||||
|
|
||||||
return {
|
const data = results.map(result => {
|
||||||
data: results.map(result => {
|
const dataResult: DataQueryResponse = result as DataQueryResponse;
|
||||||
const dataResult: DataQueryResponse = result as DataQueryResponse;
|
if (!dataResult.data) {
|
||||||
if (!dataResult.data) {
|
return [];
|
||||||
return [];
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const data: any[] = [];
|
const data: any[] = [];
|
||||||
for (let index = 0; index < dataResult.data.length; index++) {
|
for (let index = 0; index < dataResult.data.length; index++) {
|
||||||
const dataFrame = toDataFrame(dataResult.data[index]);
|
const dataFrame = toDataFrame(dataResult.data[index]);
|
||||||
const fieldCache = new FieldCache(dataFrame);
|
const fieldCache = new FieldCache(dataFrame);
|
||||||
const timestampField: Field<string> = fieldCache.getFieldByName('ts')!;
|
const timestampField: Field<string> = fieldCache.getFieldByName('ts')!;
|
||||||
const idField: Field<string> | undefined = fieldCache.getFieldByName('id');
|
const idField: Field<string> | undefined = fieldCache.getFieldByName('id');
|
||||||
|
|
||||||
for (let fieldIndex = 0; fieldIndex < timestampField.values.length; fieldIndex++) {
|
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
|
// 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
|
// to return correct list of lines handling inclusive ranges or how to filter the correct line on the
|
||||||
// datasource.
|
// 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
|
// 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.
|
// requests.
|
||||||
if (idField) {
|
if (idField) {
|
||||||
// For Loki this means we filter only the one row. Issue is we could have other rows logged at the same
|
// 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
|
// 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
|
// 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
|
// and making this correct would mean quite a bit of complexity to shuffle things around and messing up
|
||||||
//counts.
|
//counts.
|
||||||
if (idField.values.get(fieldIndex) === row.uid) {
|
if (idField.values.get(fieldIndex) === row.uid) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback to timestamp. This should not happen right now as this feature is implemented only for loki
|
// 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
|
// 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.
|
// 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) {
|
if (parseInt(timestampField.values.get(fieldIndex), 10) === row.timeEpochMs) {
|
||||||
continue;
|
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;
|
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
|
||||||
errors: results.map(result => {
|
|
||||||
const errorResult: DataQueryError = result as DataQueryError;
|
|
||||||
if (!errorResult.message) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
getRowContext,
|
||||||
row,
|
row,
|
||||||
children,
|
children,
|
||||||
|
logsSortOrder,
|
||||||
}) => {
|
}) => {
|
||||||
// React Hook that creates a number state value called limit to component state and a setter function called setLimit
|
// 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
|
// 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
|
// 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
|
// Second promise fetches limit number of rows forwards in time from a specific point in time
|
||||||
const { value } = useAsync(async () => {
|
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]);
|
}, [limit]);
|
||||||
|
|
||||||
// React Hook that performs a side effect every time the value (from useAsync hook) prop changes
|
// 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 { range } from 'lodash';
|
||||||
import { LogRows, PREVIEW_LIMIT } from './LogRows';
|
import { LogRows, PREVIEW_LIMIT } from './LogRows';
|
||||||
import { mount } from 'enzyme';
|
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';
|
import { LogRow } from './LogRow';
|
||||||
|
|
||||||
describe('LogRows', () => {
|
describe('LogRows', () => {
|
||||||
@ -93,10 +93,88 @@ describe('LogRows', () => {
|
|||||||
|
|
||||||
expect(wrapper.find(LogRow).length).toBe(100);
|
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 makeLog = (overrides: Partial<LogRowModel>): LogRowModel => {
|
||||||
const uid = overrides.uid || '1';
|
const uid = overrides.uid || '1';
|
||||||
|
const timeEpochMs = overrides.timeEpochMs || 1;
|
||||||
const entry = `log message ${uid}`;
|
const entry = `log message ${uid}`;
|
||||||
return {
|
return {
|
||||||
entryFieldIndex: 0,
|
entryFieldIndex: 0,
|
||||||
@ -110,8 +188,8 @@ const makeLog = (overrides: Partial<LogRowModel>): LogRowModel => {
|
|||||||
labels: {},
|
labels: {},
|
||||||
raw: entry,
|
raw: entry,
|
||||||
timeFromNow: '',
|
timeFromNow: '',
|
||||||
timeEpochMs: 1,
|
timeEpochMs,
|
||||||
timeEpochNs: '1000000',
|
timeEpochNs: (timeEpochMs * 1000000).toString(),
|
||||||
timeLocal: '',
|
timeLocal: '',
|
||||||
timeUtc: '',
|
timeUtc: '',
|
||||||
searchWords: [],
|
searchWords: [],
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import memoizeOne from 'memoize-one';
|
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 { Themeable } from '../../types/theme';
|
||||||
import { withTheme } from '../../themes/index';
|
import { withTheme } from '../../themes/index';
|
||||||
@ -23,6 +23,7 @@ export interface Props extends Themeable {
|
|||||||
showTime: boolean;
|
showTime: boolean;
|
||||||
wrapLogMessage: boolean;
|
wrapLogMessage: boolean;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
|
logsSortOrder?: LogsSortOrder | null;
|
||||||
rowLimit?: number;
|
rowLimit?: number;
|
||||||
allowDetails?: boolean;
|
allowDetails?: boolean;
|
||||||
previewLimit?: number;
|
previewLimit?: number;
|
||||||
@ -70,10 +71,14 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
makeGetRows = memoizeOne((processedRows: LogRowModel[]) => {
|
makeGetRows = memoizeOne((orderedRows: LogRowModel[]) => {
|
||||||
return () => processedRows;
|
return () => orderedRows;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
sortLogs = memoizeOne((logRows: LogRowModel[], logsSortOrder: LogsSortOrder): LogRowModel[] =>
|
||||||
|
sortLogRows(logRows, logsSortOrder)
|
||||||
|
);
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
dedupStrategy,
|
dedupStrategy,
|
||||||
@ -93,6 +98,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
|||||||
previewLimit,
|
previewLimit,
|
||||||
getFieldLinks,
|
getFieldLinks,
|
||||||
disableCustomHorizontalScroll,
|
disableCustomHorizontalScroll,
|
||||||
|
logsSortOrder,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { renderAll } = this.state;
|
const { renderAll } = this.state;
|
||||||
const { logsRowsTable, logsRowsHorizontalScroll } = getLogRowStyles(theme);
|
const { logsRowsTable, logsRowsHorizontalScroll } = getLogRowStyles(theme);
|
||||||
@ -109,12 +115,13 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
// Staged rendering
|
// Staged rendering
|
||||||
const processedRows = dedupedRows ? dedupedRows : [];
|
const processedRows = dedupedRows ? dedupedRows : [];
|
||||||
const firstRows = processedRows.slice(0, previewLimit!);
|
const orderedRows = logsSortOrder ? this.sortLogs(processedRows, logsSortOrder) : processedRows;
|
||||||
const rowCount = Math.min(processedRows.length, rowLimit!);
|
const firstRows = orderedRows.slice(0, previewLimit!);
|
||||||
const lastRows = processedRows.slice(previewLimit!, rowCount);
|
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
|
// 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([]);
|
const getRowContext = this.props.getRowContext ? this.props.getRowContext : () => Promise.resolve([]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -139,6 +146,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
|||||||
onClickFilterLabel={onClickFilterLabel}
|
onClickFilterLabel={onClickFilterLabel}
|
||||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||||
getFieldLinks={getFieldLinks}
|
getFieldLinks={getFieldLinks}
|
||||||
|
logsSortOrder={logsSortOrder}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{hasData &&
|
{hasData &&
|
||||||
@ -159,6 +167,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
|||||||
onClickFilterLabel={onClickFilterLabel}
|
onClickFilterLabel={onClickFilterLabel}
|
||||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||||
getFieldLinks={getFieldLinks}
|
getFieldLinks={getFieldLinks}
|
||||||
|
logsSortOrder={logsSortOrder}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{hasData && !renderAll && (
|
{hasData && !renderAll && (
|
||||||
|
@ -28,10 +28,11 @@ import {
|
|||||||
textUtil,
|
textUtil,
|
||||||
dateTime,
|
dateTime,
|
||||||
AbsoluteTimeRange,
|
AbsoluteTimeRange,
|
||||||
|
sortInAscendingOrder,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getThemeColor } from 'app/core/utils/colors';
|
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';
|
import { decimalSIPrefix } from '@grafana/data/src/valueFormats/symbolFormatters';
|
||||||
|
|
||||||
export const LogLevelColor = {
|
export const LogLevelColor = {
|
||||||
|
@ -8,23 +8,12 @@ import {
|
|||||||
hasNonEmptyQuery,
|
hasNonEmptyQuery,
|
||||||
parseUrlState,
|
parseUrlState,
|
||||||
refreshIntervalToSortOrder,
|
refreshIntervalToSortOrder,
|
||||||
sortLogsResult,
|
|
||||||
SortOrder,
|
|
||||||
updateHistory,
|
updateHistory,
|
||||||
getExploreUrl,
|
getExploreUrl,
|
||||||
GetExploreUrlArguments,
|
GetExploreUrlArguments,
|
||||||
} from './explore';
|
} from './explore';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import {
|
import { DataQueryError, dateTime, LogsDedupStrategy, ExploreUrlState, LogsSortOrder } from '@grafana/data';
|
||||||
DataQueryError,
|
|
||||||
dateTime,
|
|
||||||
LogLevel,
|
|
||||||
LogRowModel,
|
|
||||||
LogsDedupStrategy,
|
|
||||||
LogsModel,
|
|
||||||
MutableDataFrame,
|
|
||||||
ExploreUrlState,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { RefreshPicker } from '@grafana/ui';
|
import { RefreshPicker } from '@grafana/ui';
|
||||||
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
||||||
|
|
||||||
@ -392,7 +381,7 @@ describe('refreshIntervalToSortOrder', () => {
|
|||||||
it('then it should return ascending', () => {
|
it('then it should return ascending', () => {
|
||||||
const result = refreshIntervalToSortOrder(RefreshPicker.liveOption.value);
|
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', () => {
|
it('then it should return descending', () => {
|
||||||
const result = refreshIntervalToSortOrder(RefreshPicker.offOption.value);
|
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', () => {
|
it('then it should return descending', () => {
|
||||||
const result = refreshIntervalToSortOrder('5s');
|
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', () => {
|
it('then it should return descending', () => {
|
||||||
const result = refreshIntervalToSortOrder(undefined);
|
const result = refreshIntervalToSortOrder(undefined);
|
||||||
|
|
||||||
expect(result).toBe(SortOrder.Descending);
|
expect(result).toBe(LogsSortOrder.Descending);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sortLogsResult', () => {
|
describe('when buildQueryTransaction', () => {
|
||||||
const firstRow: LogRowModel = {
|
it('it should calculate interval based on time range', () => {
|
||||||
rowIndex: 0,
|
const queries = [{ refId: 'A' }];
|
||||||
entryFieldIndex: 0,
|
const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
|
||||||
dataFrame: new MutableDataFrame(),
|
const range = { from: dateTime().subtract(1, 'd'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
||||||
entry: '',
|
const transaction = buildQueryTransaction(queries, queryOptions, range, false);
|
||||||
hasAnsi: false,
|
expect(transaction.request.intervalMs).toEqual(60000);
|
||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
it('it should calculate interval taking minInterval into account', () => {
|
||||||
describe('when called with SortOrder.Ascending', () => {
|
const queries = [{ refId: 'A' }];
|
||||||
it('then it should sort ascending', () => {
|
const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
|
||||||
const logsResult: LogsModel = {
|
const range = { from: dateTime().subtract(1, 'm'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
||||||
rows: [secondRow, firstRow, sameAsFirstRow],
|
const transaction = buildQueryTransaction(queries, queryOptions, range, false);
|
||||||
hasUniqueLabels: false,
|
expect(transaction.request.intervalMs).toEqual(15000);
|
||||||
};
|
|
||||||
const result = sortLogsResult(logsResult, SortOrder.Ascending);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
rows: [firstRow, sameAsFirstRow, secondRow],
|
|
||||||
hasUniqueLabels: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
it('it should calculate interval taking maxDataPoints into account', () => {
|
||||||
describe('when buildQueryTransaction', () => {
|
const queries = [{ refId: 'A' }];
|
||||||
it('it should calculate interval based on time range', () => {
|
const queryOptions = { maxDataPoints: 10, minInterval: '15s' };
|
||||||
const queries = [{ refId: 'A' }];
|
const range = { from: dateTime().subtract(1, 'd'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
||||||
const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
|
const transaction = buildQueryTransaction(queries, queryOptions, range, false);
|
||||||
const range = { from: dateTime().subtract(1, 'd'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
expect(transaction.request.interval).toEqual('2h');
|
||||||
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');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
IntervalValues,
|
IntervalValues,
|
||||||
LogRowModel,
|
LogRowModel,
|
||||||
LogsDedupStrategy,
|
LogsDedupStrategy,
|
||||||
LogsModel,
|
LogsSortOrder,
|
||||||
RawTimeRange,
|
RawTimeRange,
|
||||||
TimeFragment,
|
TimeFragment,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
@ -464,67 +464,8 @@ export const getRefIds = (value: any): string[] => {
|
|||||||
return _.uniq(_.flatten(refIds));
|
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) =>
|
export const refreshIntervalToSortOrder = (refreshInterval?: string) =>
|
||||||
RefreshPicker.isLive(refreshInterval) ? SortOrder.Ascending : SortOrder.Descending;
|
RefreshPicker.isLive(refreshInterval) ? LogsSortOrder.Ascending : LogsSortOrder.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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const convertToWebSocketUrl = (url: string) => {
|
export const convertToWebSocketUrl = (url: string) => {
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
filterAndSortQueries,
|
filterAndSortQueries,
|
||||||
} from './richHistory';
|
} from './richHistory';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { SortOrder } from './explore';
|
import { SortOrder } from './richHistory';
|
||||||
import { dateTime, DataQuery } from '@grafana/data';
|
import { dateTime, DataQuery } from '@grafana/data';
|
||||||
|
|
||||||
const mock: any = {
|
const mock: any = {
|
||||||
|
@ -5,7 +5,6 @@ import _ from 'lodash';
|
|||||||
import { DataQuery, DataSourceApi, dateTimeFormat, AppEvents, urlUtil, ExploreUrlState } from '@grafana/data';
|
import { DataQuery, DataSourceApi, dateTimeFormat, AppEvents, urlUtil, ExploreUrlState } from '@grafana/data';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { SortOrder } from './explore';
|
|
||||||
import { getExploreDatasources } from '../../features/explore/state/selectors';
|
import { getExploreDatasources } from '../../features/explore/state/selectors';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
@ -21,6 +20,13 @@ export const RICH_HISTORY_SETTING_KEYS = {
|
|||||||
datasourceFilters: 'grafana.explore.richHistory.datasourceFilters',
|
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.
|
* Add queries to rich history. Save only queries within the retention period, or that are starred.
|
||||||
* Side-effect: store history in local storage
|
* Side-effect: store history in local storage
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
import { css, cx } from 'emotion';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
rangeUtil,
|
rangeUtil,
|
||||||
@ -11,6 +12,7 @@ import {
|
|||||||
LogRowModel,
|
LogRowModel,
|
||||||
LogsDedupDescription,
|
LogsDedupDescription,
|
||||||
LogsMetaItem,
|
LogsMetaItem,
|
||||||
|
LogsSortOrder,
|
||||||
GraphSeriesXY,
|
GraphSeriesXY,
|
||||||
LinkModel,
|
LinkModel,
|
||||||
Field,
|
Field,
|
||||||
@ -72,13 +74,39 @@ interface State {
|
|||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
showTime: boolean;
|
showTime: boolean;
|
||||||
wrapLogMessage: boolean;
|
wrapLogMessage: boolean;
|
||||||
|
logsSortOrder: LogsSortOrder | null;
|
||||||
|
isFlipping: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Logs extends PureComponent<Props, State> {
|
export class Logs extends PureComponent<Props, State> {
|
||||||
|
flipOrderTimer: NodeJS.Timeout;
|
||||||
|
cancelFlippingTimer: NodeJS.Timeout;
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
showLabels: store.getBool(SETTINGS_KEYS.showLabels, false),
|
showLabels: store.getBool(SETTINGS_KEYS.showLabels, false),
|
||||||
showTime: store.getBool(SETTINGS_KEYS.showTime, true),
|
showTime: store.getBool(SETTINGS_KEYS.showTime, true),
|
||||||
wrapLogMessage: store.getBool(SETTINGS_KEYS.wrapLogMessage, 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) => {
|
onChangeDedup = (dedup: LogsDedupStrategy) => {
|
||||||
@ -166,7 +194,7 @@ export class Logs extends PureComponent<Props, State> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { showLabels, showTime, wrapLogMessage } = this.state;
|
const { showLabels, showTime, wrapLogMessage, logsSortOrder, isFlipping } = this.state;
|
||||||
const { dedupStrategy } = this.props;
|
const { dedupStrategy } = this.props;
|
||||||
const hasData = logRows && logRows.length > 0;
|
const hasData = logRows && logRows.length > 0;
|
||||||
const dedupCount = dedupedRows
|
const dedupCount = dedupedRows
|
||||||
@ -214,23 +242,39 @@ export class Logs extends PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
<div className="logs-panel-options">
|
<div className="logs-panel-options">
|
||||||
<div className="logs-panel-controls">
|
<div className="logs-panel-controls">
|
||||||
<Switch label="Time" checked={showTime} onChange={this.onChangeTime} transparent />
|
<div className="logs-panel-controls-main">
|
||||||
<Switch label="Unique labels" checked={showLabels} onChange={this.onChangeLabels} transparent />
|
<Switch label="Time" checked={showTime} onChange={this.onChangeTime} transparent />
|
||||||
<Switch label="Wrap lines" checked={wrapLogMessage} onChange={this.onChangewrapLogMessage} transparent />
|
<Switch label="Unique labels" checked={showLabels} onChange={this.onChangeLabels} transparent />
|
||||||
<ToggleButtonGroup label="Dedup" transparent={true}>
|
<Switch label="Wrap lines" checked={wrapLogMessage} onChange={this.onChangewrapLogMessage} transparent />
|
||||||
{Object.keys(LogsDedupStrategy).map((dedupType: string, i) => (
|
<ToggleButtonGroup label="Dedup" transparent={true}>
|
||||||
<ToggleButton
|
{Object.keys(LogsDedupStrategy).map((dedupType: string, i) => (
|
||||||
key={i}
|
<ToggleButton
|
||||||
value={dedupType}
|
key={i}
|
||||||
onChange={this.onChangeDedup}
|
value={dedupType}
|
||||||
selected={dedupStrategy === dedupType}
|
onChange={this.onChangeDedup}
|
||||||
// @ts-ignore
|
selected={dedupStrategy === dedupType}
|
||||||
tooltip={LogsDedupDescription[dedupType]}
|
// @ts-ignore
|
||||||
>
|
tooltip={LogsDedupDescription[dedupType]}
|
||||||
{dedupType}
|
>
|
||||||
</ToggleButton>
|
{dedupType}
|
||||||
))}
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
))}
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -260,6 +304,7 @@ export class Logs extends PureComponent<Props, State> {
|
|||||||
wrapLogMessage={wrapLogMessage}
|
wrapLogMessage={wrapLogMessage}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
getFieldLinks={getFieldLinks}
|
getFieldLinks={getFieldLinks}
|
||||||
|
logsSortOrder={logsSortOrder}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!loading && !hasData && !scanning && (
|
{!loading && !hasData && !scanning && (
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
//Services & Utils
|
//Services & Utils
|
||||||
import { SortOrder } from 'app/core/utils/explore';
|
import { RICH_HISTORY_SETTING_KEYS, SortOrder } from 'app/core/utils/richHistory';
|
||||||
import { RICH_HISTORY_SETTING_KEYS } from 'app/core/utils/richHistory';
|
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { withTheme, TabbedContainer, TabConfig } from '@grafana/ui';
|
import { withTheme, TabbedContainer, TabConfig } from '@grafana/ui';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { ExploreId } from '../../../types/explore';
|
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 { RichHistoryQueriesTab, Props } from './RichHistoryQueriesTab';
|
||||||
import { Slider } from '@grafana/ui';
|
import { Slider } from '@grafana/ui';
|
||||||
|
|
||||||
|
@ -9,8 +9,8 @@ import { RichHistoryQuery, ExploreId } from 'app/types/explore';
|
|||||||
import { stylesFactory, useTheme } from '@grafana/ui';
|
import { stylesFactory, useTheme } from '@grafana/ui';
|
||||||
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
import { SortOrder } from 'app/core/utils/explore';
|
|
||||||
import {
|
import {
|
||||||
|
SortOrder,
|
||||||
mapNumbertoTimeInSlider,
|
mapNumbertoTimeInSlider,
|
||||||
mapQueriesToHeadings,
|
mapQueriesToHeadings,
|
||||||
createDatasourcesList,
|
createDatasourcesList,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { ExploreId } from '../../../types/explore';
|
import { ExploreId } from '../../../types/explore';
|
||||||
import { SortOrder } from 'app/core/utils/explore';
|
import { SortOrder } from 'app/core/utils/richHistory';
|
||||||
import { RichHistoryStarredTab, Props } from './RichHistoryStarredTab';
|
import { RichHistoryStarredTab, Props } from './RichHistoryStarredTab';
|
||||||
|
|
||||||
jest.mock('../state/selectors', () => ({ getExploreDatasources: jest.fn() }));
|
jest.mock('../state/selectors', () => ({ getExploreDatasources: jest.fn() }));
|
||||||
|
@ -8,9 +8,7 @@ import { RichHistoryQuery, ExploreId } from 'app/types/explore';
|
|||||||
// Utils
|
// Utils
|
||||||
import { stylesFactory, useTheme } from '@grafana/ui';
|
import { stylesFactory, useTheme } from '@grafana/ui';
|
||||||
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
||||||
|
import { filterAndSortQueries, createDatasourcesList, SortOrder } from 'app/core/utils/richHistory';
|
||||||
import { SortOrder } from '../../../core/utils/explore';
|
|
||||||
import { filterAndSortQueries, createDatasourcesList } from '../../../core/utils/richHistory';
|
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import RichHistoryCard from './RichHistoryCard';
|
import RichHistoryCard from './RichHistoryCard';
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
toLegacyResponseData,
|
toLegacyResponseData,
|
||||||
ExploreMode,
|
ExploreMode,
|
||||||
LogsDedupStrategy,
|
LogsDedupStrategy,
|
||||||
|
sortLogsResult,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { RefreshPicker } from '@grafana/ui';
|
import { RefreshPicker } from '@grafana/ui';
|
||||||
import { LocationUpdate } from '@grafana/runtime';
|
import { LocationUpdate } from '@grafana/runtime';
|
||||||
@ -23,7 +24,6 @@ import {
|
|||||||
getQueryKeys,
|
getQueryKeys,
|
||||||
parseUrlState,
|
parseUrlState,
|
||||||
refreshIntervalToSortOrder,
|
refreshIntervalToSortOrder,
|
||||||
sortLogsResult,
|
|
||||||
stopQueryState,
|
stopQueryState,
|
||||||
} from 'app/core/utils/explore';
|
} from 'app/core/utils/explore';
|
||||||
import { ExploreId, ExploreItemState, ExploreState, ExploreUpdateState } from 'app/types/explore';
|
import { ExploreId, ExploreItemState, ExploreState, ExploreUpdateState } from 'app/types/explore';
|
||||||
|
@ -7,9 +7,10 @@ import {
|
|||||||
getDisplayProcessor,
|
getDisplayProcessor,
|
||||||
PreferredVisualisationType,
|
PreferredVisualisationType,
|
||||||
standardTransformers,
|
standardTransformers,
|
||||||
|
sortLogsResult,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { ExploreItemState } from 'app/types/explore';
|
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 { dataFrameToLogsModel } from 'app/core/logs_model';
|
||||||
import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel';
|
import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
|
@ -3,7 +3,6 @@ import { LogRows, CustomScrollbar } from '@grafana/ui';
|
|||||||
import { LogsDedupStrategy, PanelProps } from '@grafana/data';
|
import { LogsDedupStrategy, PanelProps } from '@grafana/data';
|
||||||
import { Options } from './types';
|
import { Options } from './types';
|
||||||
import { dataFrameToLogsModel } from 'app/core/logs_model';
|
import { dataFrameToLogsModel } from 'app/core/logs_model';
|
||||||
import { sortLogsResult } from 'app/core/utils/explore';
|
|
||||||
|
|
||||||
interface LogsPanelProps extends PanelProps<Options> {}
|
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 newResults = data ? dataFrameToLogsModel(data.series, data.request?.intervalMs, timeZone) : null;
|
||||||
const sortedNewResults = sortLogsResult(newResults, sortOrder);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomScrollbar autoHide>
|
<CustomScrollbar autoHide>
|
||||||
<LogRows
|
<LogRows
|
||||||
logRows={sortedNewResults.rows}
|
logRows={newResults?.rows || []}
|
||||||
dedupStrategy={LogsDedupStrategy.none}
|
dedupStrategy={LogsDedupStrategy.none}
|
||||||
highlighterExpressions={[]}
|
highlighterExpressions={[]}
|
||||||
showLabels={showLabels}
|
showLabels={showLabels}
|
||||||
@ -36,6 +34,7 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
|
|||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
allowDetails={true}
|
allowDetails={true}
|
||||||
disableCustomHorizontalScroll={true}
|
disableCustomHorizontalScroll={true}
|
||||||
|
logsSortOrder={sortOrder}
|
||||||
/>
|
/>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { PanelPlugin } from '@grafana/data';
|
import { PanelPlugin, LogsSortOrder } from '@grafana/data';
|
||||||
import { Options } from './types';
|
import { Options } from './types';
|
||||||
import { LogsPanel } from './LogsPanel';
|
import { LogsPanel } from './LogsPanel';
|
||||||
import { SortOrder } from '../../../core/utils/explore';
|
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<Options>(LogsPanel).setPanelOptions(builder => {
|
export const plugin = new PanelPlugin<Options>(LogsPanel).setPanelOptions(builder => {
|
||||||
builder
|
builder
|
||||||
@ -29,10 +28,10 @@ export const plugin = new PanelPlugin<Options>(LogsPanel).setPanelOptions(builde
|
|||||||
description: '',
|
description: '',
|
||||||
settings: {
|
settings: {
|
||||||
options: [
|
options: [
|
||||||
{ value: SortOrder.Descending, label: 'Descending' },
|
{ value: LogsSortOrder.Descending, label: 'Descending' },
|
||||||
{ value: SortOrder.Ascending, label: 'Ascending' },
|
{ 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 {
|
export interface Options {
|
||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
showTime: boolean;
|
showTime: boolean;
|
||||||
wrapLogMessage: boolean;
|
wrapLogMessage: boolean;
|
||||||
sortOrder: SortOrder;
|
sortOrder: LogsSortOrder;
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,18 @@ $column-horizontal-spacing: 10px;
|
|||||||
|
|
||||||
.logs-panel-controls {
|
.logs-panel-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-items: flex-start;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: baseline;
|
||||||
flex-wrap: wrap;
|
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