mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: UI change for log row details (#20034)
Add LogDetail section that is shown when log line is clicked and expanded. Contains labels/fields and actions to show stats and add/remove label filter.
This commit is contained in:
parent
750e8d27bf
commit
a5e8e0e291
@ -89,7 +89,15 @@ describe('LogsParsers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should return parsed fields', () => {
|
test('should return parsed fields', () => {
|
||||||
expect(parser.getFields('foo=bar baz="42 + 1"')).toEqual(['foo=bar', 'baz="42 + 1"']);
|
expect(
|
||||||
|
parser.getFields(
|
||||||
|
'foo=bar baz="42 + 1" msg="[resolver] received A record \\"127.0.0.1\\" for \\"localhost.\\" from udp:192.168.65.1"'
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
'foo=bar',
|
||||||
|
'baz="42 + 1"',
|
||||||
|
'msg="[resolver] received A record \\"127.0.0.1\\" for \\"localhost.\\" from udp:192.168.65.1"',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return label for field', () => {
|
test('should return label for field', () => {
|
||||||
@ -98,6 +106,11 @@ describe('LogsParsers', () => {
|
|||||||
|
|
||||||
test('should return value for field', () => {
|
test('should return value for field', () => {
|
||||||
expect(parser.getValueFromField('foo=bar')).toBe('bar');
|
expect(parser.getValueFromField('foo=bar')).toBe('bar');
|
||||||
|
expect(
|
||||||
|
parser.getValueFromField(
|
||||||
|
'msg="[resolver] received A record \\"127.0.0.1\\" for \\"localhost.\\" from udp:192.168.65.1"'
|
||||||
|
)
|
||||||
|
).toBe('"[resolver] received A record \\"127.0.0.1\\" for \\"localhost.\\" from udp:192.168.65.1"');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should build a valid value matcher', () => {
|
test('should build a valid value matcher', () => {
|
||||||
@ -117,7 +130,7 @@ describe('LogsParsers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should return parsed fields', () => {
|
test('should return parsed fields', () => {
|
||||||
expect(parser.getFields('{ "foo" : "bar", "baz" : 42 }')).toEqual(['"foo" : "bar"', '"baz" : 42']);
|
expect(parser.getFields('{ "foo" : "bar", "baz" : 42 }')).toEqual(['"foo":"bar"', '"baz":42']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return parsed fields for nested quotes', () => {
|
test('should return parsed fields for nested quotes', () => {
|
||||||
@ -126,6 +139,7 @@ describe('LogsParsers', () => {
|
|||||||
|
|
||||||
test('should return label for field', () => {
|
test('should return label for field', () => {
|
||||||
expect(parser.getLabelFromField('"foo" : "bar"')).toBe('foo');
|
expect(parser.getLabelFromField('"foo" : "bar"')).toBe('foo');
|
||||||
|
expect(parser.getLabelFromField('"docker.memory.fail.count":0')).toBe('docker.memory.fail.count');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return value for field', () => {
|
test('should return value for field', () => {
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import { countBy, chain, map, escapeRegExp } from 'lodash';
|
import { countBy, chain } from 'lodash';
|
||||||
|
|
||||||
import { LogLevel, LogRowModel, LogLabelStatsModel, LogsParser } from '../types/logs';
|
import { LogLevel, LogRowModel, LogLabelStatsModel, LogsParser } 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';
|
||||||
|
|
||||||
const LOGFMT_REGEXP = /(?:^|\s)(\w+)=("[^"]*"|\S+)/;
|
// This matches:
|
||||||
|
// first a label from start of the string or first white space, then any word chars until "="
|
||||||
|
// second either an empty quotes, or anything that starts with quote and ends with unescaped quote,
|
||||||
|
// or any non whitespace chars that do not start with qoute
|
||||||
|
const LOGFMT_REGEXP = /(?:^|\s)(\w+)=(""|(?:".*?[^\\]"|[^"\s]\S*))/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the log level of a log line.
|
* Returns the log level of a log line.
|
||||||
@ -79,21 +83,15 @@ export const LogsParsers: { [name: string]: LogsParser } = {
|
|||||||
JSON: {
|
JSON: {
|
||||||
buildMatcher: label => new RegExp(`(?:{|,)\\s*"${label}"\\s*:\\s*"?([\\d\\.]+|[^"]*)"?`),
|
buildMatcher: label => new RegExp(`(?:{|,)\\s*"${label}"\\s*:\\s*"?([\\d\\.]+|[^"]*)"?`),
|
||||||
getFields: line => {
|
getFields: line => {
|
||||||
const fields: string[] = [];
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(line);
|
const parsed = JSON.parse(line);
|
||||||
map(parsed, (value, key) => {
|
return Object.keys(parsed).map(key => {
|
||||||
const fieldMatcher = new RegExp(`"${key}"\\s*:\\s*"?${escapeRegExp(JSON.stringify(value))}"?`);
|
return `"${key}":${JSON.stringify(parsed[key])}`;
|
||||||
|
|
||||||
const match = line.match(fieldMatcher);
|
|
||||||
if (match) {
|
|
||||||
fields.push(match[0]);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
return fields;
|
return [];
|
||||||
},
|
},
|
||||||
getLabelFromField: field => (field.match(/^"(\w+)"\s*:/) || [])[1],
|
getLabelFromField: field => (field.match(/^"([^"]+)"\s*:/) || [])[1],
|
||||||
getValueFromField: field => (field.match(/:\s*(.*)$/) || [])[1],
|
getValueFromField: field => (field.match(/:\s*(.*)$/) || [])[1],
|
||||||
test: line => {
|
test: line => {
|
||||||
try {
|
try {
|
||||||
|
88
packages/grafana-ui/src/components/Logs/LogDetails.test.tsx
Normal file
88
packages/grafana-ui/src/components/Logs/LogDetails.test.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { LogDetails, Props } from './LogDetails';
|
||||||
|
import { LogRowModel, LogLevel, GrafanaTheme } from '@grafana/data';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: Props = {
|
||||||
|
theme: {} as GrafanaTheme,
|
||||||
|
row: {
|
||||||
|
logLevel: 'error' as LogLevel,
|
||||||
|
timeFromNow: '',
|
||||||
|
timeEpochMs: 1546297200000,
|
||||||
|
timeLocal: '',
|
||||||
|
timeUtc: '',
|
||||||
|
hasAnsi: false,
|
||||||
|
entry: '',
|
||||||
|
raw: '',
|
||||||
|
timestamp: '',
|
||||||
|
uid: '0',
|
||||||
|
} as LogRowModel,
|
||||||
|
getRows: () => [],
|
||||||
|
onClickFilterLabel: () => {},
|
||||||
|
onClickFilterOutLabel: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
const wrapper = mount(<LogDetails {...props} />);
|
||||||
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('LogDetails', () => {
|
||||||
|
describe('when labels are present', () => {
|
||||||
|
it('should render heading', () => {
|
||||||
|
const wrapper = setup({ row: { labels: { key1: 'label1', key2: 'label2' } } });
|
||||||
|
expect(wrapper.find({ 'aria-label': 'Log labels' })).toHaveLength(1);
|
||||||
|
}),
|
||||||
|
it('should render labels', () => {
|
||||||
|
const wrapper = setup({ row: { labels: { key1: 'label1', key2: 'label2' } } });
|
||||||
|
expect(wrapper.text().includes('key1label1key2label2')).toBe(true);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
describe('when row entry has parsable fields', () => {
|
||||||
|
it('should render heading ', () => {
|
||||||
|
const wrapper = setup({ row: { entry: 'test=successful' } });
|
||||||
|
expect(wrapper.find({ 'aria-label': 'Parsed fields' })).toHaveLength(1);
|
||||||
|
}),
|
||||||
|
it('should render parsed fields', () => {
|
||||||
|
const wrapper = setup({
|
||||||
|
row: { entry: 'test=successful' },
|
||||||
|
parser: {
|
||||||
|
getLabelFromField: () => 'test',
|
||||||
|
getValueFromField: () => 'successful',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(wrapper.text().includes('testsuccessful')).toBe(true);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
describe('when row entry have parsable fields and labels are present', () => {
|
||||||
|
it('should render all headings', () => {
|
||||||
|
const wrapper = setup({ row: { entry: 'test=successful', labels: { key: 'label' } } });
|
||||||
|
expect(wrapper.find({ 'aria-label': 'Log labels' })).toHaveLength(1);
|
||||||
|
expect(wrapper.find({ 'aria-label': 'Parsed fields' })).toHaveLength(1);
|
||||||
|
}),
|
||||||
|
it('should render all labels and parsed fields', () => {
|
||||||
|
const wrapper = setup({
|
||||||
|
row: { entry: 'test=successful', labels: { key: 'label' } },
|
||||||
|
parser: {
|
||||||
|
getLabelFromField: () => 'test',
|
||||||
|
getValueFromField: () => 'successful',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(wrapper.text().includes('keylabel')).toBe(true);
|
||||||
|
expect(wrapper.text().includes('testsuccessful')).toBe(true);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
describe('when row entry and labels are not present', () => {
|
||||||
|
it('should render no details available message', () => {
|
||||||
|
const wrapper = setup({ parsedFields: [] });
|
||||||
|
expect(wrapper.text().includes('No details available')).toBe(true);
|
||||||
|
}),
|
||||||
|
it('should not render headings', () => {
|
||||||
|
const wrapper = setup({ parsedFields: [] });
|
||||||
|
expect(wrapper.find({ 'aria-label': 'Log labels' })).toHaveLength(0);
|
||||||
|
expect(wrapper.find({ 'aria-label': 'Parsed fields' })).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
98
packages/grafana-ui/src/components/Logs/LogDetails.tsx
Normal file
98
packages/grafana-ui/src/components/Logs/LogDetails.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import memoizeOne from 'memoize-one';
|
||||||
|
import { getParser, LogRowModel, LogsParser } from '@grafana/data';
|
||||||
|
|
||||||
|
import { Themeable } from '../../types/theme';
|
||||||
|
import { withTheme } from '../../themes/index';
|
||||||
|
import { getLogRowStyles } from './getLogRowStyles';
|
||||||
|
|
||||||
|
//Components
|
||||||
|
import { LogDetailsRow } from './LogDetailsRow';
|
||||||
|
|
||||||
|
export interface Props extends Themeable {
|
||||||
|
row: LogRowModel;
|
||||||
|
getRows: () => LogRowModel[];
|
||||||
|
onClickFilterLabel?: (key: string, value: string) => void;
|
||||||
|
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnThemedLogDetails extends PureComponent<Props> {
|
||||||
|
parseMessage = memoizeOne(
|
||||||
|
(rowEntry): { parsedFields: string[]; parser?: LogsParser } => {
|
||||||
|
const parser = getParser(rowEntry);
|
||||||
|
if (!parser) {
|
||||||
|
return { parsedFields: [] };
|
||||||
|
}
|
||||||
|
// Use parser to highlight detected fields
|
||||||
|
const parsedFields = parser.getFields(rowEntry);
|
||||||
|
return { parsedFields, parser };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { row, theme, onClickFilterOutLabel, onClickFilterLabel, getRows } = this.props;
|
||||||
|
const style = getLogRowStyles(theme, row.logLevel);
|
||||||
|
const labels = row.labels ? row.labels : {};
|
||||||
|
const labelsAvailable = Object.keys(labels).length > 0;
|
||||||
|
const { parsedFields, parser } = this.parseMessage(row.entry);
|
||||||
|
const parsedFieldsAvailable = parsedFields && parsedFields.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.logsRowDetailsTable}>
|
||||||
|
{labelsAvailable && (
|
||||||
|
<div className={style.logsRowDetailsSectionTable}>
|
||||||
|
<div className={style.logsRowDetailsHeading} aria-label="Log labels">
|
||||||
|
Log Labels:
|
||||||
|
</div>
|
||||||
|
{Object.keys(labels).map(key => {
|
||||||
|
const value = labels[key];
|
||||||
|
const field = `${key}=${value}`;
|
||||||
|
return (
|
||||||
|
<LogDetailsRow
|
||||||
|
key={`${key}=${value}`}
|
||||||
|
parsedKey={key}
|
||||||
|
parsedValue={value}
|
||||||
|
field={field}
|
||||||
|
row={row}
|
||||||
|
getRows={getRows}
|
||||||
|
isLabel={true}
|
||||||
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||||
|
onClickFilterLabel={onClickFilterLabel}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{parsedFieldsAvailable && (
|
||||||
|
<div className={style.logsRowDetailsSectionTable}>
|
||||||
|
<div className={style.logsRowDetailsHeading} aria-label="Parsed fields">
|
||||||
|
Parsed fields:
|
||||||
|
</div>
|
||||||
|
{parsedFields &&
|
||||||
|
parsedFields.map(field => {
|
||||||
|
const key = parser!.getLabelFromField(field);
|
||||||
|
const value = parser!.getValueFromField(field);
|
||||||
|
return (
|
||||||
|
<LogDetailsRow
|
||||||
|
key={`${key}=${value}`}
|
||||||
|
parsedKey={key}
|
||||||
|
parsedValue={value}
|
||||||
|
field={field}
|
||||||
|
row={row}
|
||||||
|
isLabel={false}
|
||||||
|
getRows={getRows}
|
||||||
|
parser={parser}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!parsedFieldsAvailable && !labelsAvailable && <div aria-label="No details">No details available</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogDetails = withTheme(UnThemedLogDetails);
|
||||||
|
LogDetails.displayName = 'LogDetails';
|
@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { LogDetailsRow, Props } from './LogDetailsRow';
|
||||||
|
import { LogRowModel, LogsParser, GrafanaTheme } from '@grafana/data';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: Props = {
|
||||||
|
theme: {} as GrafanaTheme,
|
||||||
|
parsedValue: '',
|
||||||
|
parsedKey: '',
|
||||||
|
field: '',
|
||||||
|
isLabel: true,
|
||||||
|
parser: {} as LogsParser,
|
||||||
|
row: {} as LogRowModel,
|
||||||
|
getRows: () => [],
|
||||||
|
onClickFilterLabel: () => {},
|
||||||
|
onClickFilterOutLabel: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
const wrapper = mount(<LogDetailsRow {...props} />);
|
||||||
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('LogDetailsRow', () => {
|
||||||
|
it('should render parsed key', () => {
|
||||||
|
const wrapper = setup({ parsedKey: 'test key' });
|
||||||
|
expect(wrapper.text().includes('test key')).toBe(true);
|
||||||
|
}),
|
||||||
|
it('should render parsed value', () => {
|
||||||
|
const wrapper = setup({ parsedValue: 'test value' });
|
||||||
|
expect(wrapper.text().includes('test value')).toBe(true);
|
||||||
|
});
|
||||||
|
it('should render metrics button', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
expect(wrapper.find('i.fa-signal')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
describe('if props is a label', () => {
|
||||||
|
it('should render filter label button', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
expect(wrapper.find('i.fa-search-plus')).toHaveLength(1);
|
||||||
|
}),
|
||||||
|
it('should render filte out label button', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
expect(wrapper.find('i.fa-search-minus')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
141
packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx
Normal file
141
packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import {
|
||||||
|
LogRowModel,
|
||||||
|
LogsParser,
|
||||||
|
LogLabelStatsModel,
|
||||||
|
calculateFieldStats,
|
||||||
|
calculateLogsLabelStats,
|
||||||
|
} from '@grafana/data';
|
||||||
|
|
||||||
|
import { Themeable } from '../../types/theme';
|
||||||
|
import { withTheme } from '../../themes/index';
|
||||||
|
import { getLogRowStyles } from './getLogRowStyles';
|
||||||
|
|
||||||
|
//Components
|
||||||
|
import { LogLabelStats } from './LogLabelStats';
|
||||||
|
|
||||||
|
export interface Props extends Themeable {
|
||||||
|
parsedValue: string;
|
||||||
|
parsedKey: string;
|
||||||
|
field: string;
|
||||||
|
row: LogRowModel;
|
||||||
|
isLabel: boolean;
|
||||||
|
parser?: LogsParser;
|
||||||
|
getRows: () => LogRowModel[];
|
||||||
|
onClickFilterLabel?: (key: string, value: string) => void;
|
||||||
|
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
showFieldsStats: boolean;
|
||||||
|
fieldCount: number;
|
||||||
|
fieldLabel: string | null;
|
||||||
|
fieldStats: LogLabelStatsModel[] | null;
|
||||||
|
fieldValue: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
showFieldsStats: false,
|
||||||
|
fieldCount: 0,
|
||||||
|
fieldLabel: null,
|
||||||
|
fieldStats: null,
|
||||||
|
fieldValue: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
filterLabel = () => {
|
||||||
|
const { onClickFilterLabel, parsedKey, parsedValue } = this.props;
|
||||||
|
if (onClickFilterLabel) {
|
||||||
|
onClickFilterLabel(parsedKey, parsedValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
filterOutLabel = () => {
|
||||||
|
const { onClickFilterOutLabel, parsedKey, parsedValue } = this.props;
|
||||||
|
if (onClickFilterOutLabel) {
|
||||||
|
onClickFilterOutLabel(parsedKey, parsedValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
showStats = () => {
|
||||||
|
const { showFieldsStats } = this.state;
|
||||||
|
if (!showFieldsStats) {
|
||||||
|
this.createStatsForLabels();
|
||||||
|
}
|
||||||
|
this.toggleFieldsStats();
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleFieldsStats() {
|
||||||
|
this.setState(state => {
|
||||||
|
return {
|
||||||
|
showFieldsStats: !state.showFieldsStats,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createStatsForLabels() {
|
||||||
|
const { getRows, parser, parsedKey, parsedValue, isLabel } = this.props;
|
||||||
|
const allRows = getRows();
|
||||||
|
const fieldLabel = parsedKey;
|
||||||
|
const fieldValue = parsedValue;
|
||||||
|
let fieldStats = [];
|
||||||
|
if (isLabel) {
|
||||||
|
fieldStats = calculateLogsLabelStats(allRows, parsedKey);
|
||||||
|
} else {
|
||||||
|
const matcher = parser!.buildMatcher(fieldLabel);
|
||||||
|
fieldStats = calculateFieldStats(allRows, matcher);
|
||||||
|
}
|
||||||
|
const fieldCount = fieldStats.reduce((sum, stat) => sum + stat.count, 0);
|
||||||
|
this.setState({ fieldCount, fieldLabel, fieldStats, fieldValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { theme, parsedKey, parsedValue, isLabel } = this.props;
|
||||||
|
const { showFieldsStats, fieldStats, fieldLabel, fieldValue, fieldCount } = this.state;
|
||||||
|
const style = getLogRowStyles(theme);
|
||||||
|
return (
|
||||||
|
<div className={style.logsRowDetailsValue}>
|
||||||
|
{/* Action buttons - show stats/filter results */}
|
||||||
|
<div onClick={this.showStats} className={style.logsRowDetailsIcon}>
|
||||||
|
<i className={'fa fa-signal'} />
|
||||||
|
</div>
|
||||||
|
{isLabel ? (
|
||||||
|
<div onClick={() => this.filterLabel()} className={style.logsRowDetailsIcon}>
|
||||||
|
<i className={'fa fa-search-plus'} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={style.logsRowDetailsIcon} />
|
||||||
|
)}
|
||||||
|
{isLabel ? (
|
||||||
|
<div onClick={() => this.filterOutLabel()} className={style.logsRowDetailsIcon}>
|
||||||
|
<i className={'fa fa-search-minus'} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={style.logsRowDetailsIcon} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Key - value columns */}
|
||||||
|
<div className={style.logsRowDetailsLabel}>
|
||||||
|
<span>{parsedKey}</span>
|
||||||
|
</div>
|
||||||
|
<div className={style.logsRowCell}>
|
||||||
|
<span>{parsedValue}</span>
|
||||||
|
{showFieldsStats && (
|
||||||
|
<div className={style.logsRowCell}>
|
||||||
|
<LogLabelStats
|
||||||
|
stats={fieldStats!}
|
||||||
|
label={fieldLabel!}
|
||||||
|
value={fieldValue!}
|
||||||
|
rowCount={fieldCount}
|
||||||
|
isLabel={isLabel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogDetailsRow = withTheme(UnThemedLogDetailsRow);
|
||||||
|
LogDetailsRow.displayName = 'LogDetailsRow';
|
@ -110,13 +110,7 @@ class UnThemedLogLabel extends PureComponent<Props, State> {
|
|||||||
)}
|
)}
|
||||||
{showStats && (
|
{showStats && (
|
||||||
<span className={cx([styles.logsLabelStats])}>
|
<span className={cx([styles.logsLabelStats])}>
|
||||||
<LogLabelStats
|
<LogLabelStats stats={stats} rowCount={getRows().length} label={label} value={value} isLabel={true} />
|
||||||
stats={stats}
|
|
||||||
rowCount={getRows().length}
|
|
||||||
label={label}
|
|
||||||
value={value}
|
|
||||||
onClickClose={this.onClickClose}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,48 +1,56 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { css, cx } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { LogLabelStatsModel } from '@grafana/data';
|
import { LogLabelStatsModel, GrafanaTheme } from '@grafana/data';
|
||||||
|
|
||||||
import { LogLabelStatsRow } from './LogLabelStatsRow';
|
|
||||||
import { Themeable } from '../../types/theme';
|
import { Themeable } from '../../types/theme';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { stylesFactory } from '../../themes';
|
||||||
import { selectThemeVariant } from '../../themes/selectThemeVariant';
|
import { selectThemeVariant } from '../../themes/selectThemeVariant';
|
||||||
import { withTheme } from '../../themes/index';
|
import { withTheme } from '../../themes/index';
|
||||||
|
|
||||||
|
//Components
|
||||||
|
import { LogLabelStatsRow } from './LogLabelStatsRow';
|
||||||
|
|
||||||
const STATS_ROW_LIMIT = 5;
|
const STATS_ROW_LIMIT = 5;
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme) => ({
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
logsStats: css`
|
const borderColor = selectThemeVariant(
|
||||||
label: logs-stats;
|
{
|
||||||
background-color: ${selectThemeVariant({ light: theme.colors.pageBg, dark: theme.colors.dark2 }, theme.type)};
|
light: theme.colors.gray5,
|
||||||
color: ${theme.colors.text};
|
dark: theme.colors.dark9,
|
||||||
border: 1px solid ${selectThemeVariant({ light: theme.colors.gray5, dark: theme.colors.dark9 }, theme.type)};
|
},
|
||||||
border-radius: ${theme.border.radius.md};
|
theme.type
|
||||||
max-width: 500px;
|
);
|
||||||
`,
|
return {
|
||||||
logsStatsHeader: css`
|
logsStats: css`
|
||||||
label: logs-stats__header;
|
label: logs-stats;
|
||||||
background: ${selectThemeVariant({ light: theme.colors.gray5, dark: theme.colors.dark9 }, theme.type)};
|
display: table-cell;
|
||||||
padding: 6px 10px;
|
column-span: 2;
|
||||||
display: flex;
|
background: inherit;
|
||||||
`,
|
color: ${theme.colors.text};
|
||||||
logsStatsTitle: css`
|
`,
|
||||||
label: logs-stats__title;
|
logsStatsHeader: css`
|
||||||
font-weight: ${theme.typography.weight.semibold};
|
label: logs-stats__header;
|
||||||
padding-right: ${theme.spacing.d};
|
border-bottom: 1px solid ${borderColor};
|
||||||
overflow: hidden;
|
display: flex;
|
||||||
display: inline-block;
|
`,
|
||||||
white-space: nowrap;
|
logsStatsTitle: css`
|
||||||
text-overflow: ellipsis;
|
label: logs-stats__title;
|
||||||
flex-grow: 1;
|
font-weight: ${theme.typography.weight.semibold};
|
||||||
`,
|
padding-right: ${theme.spacing.d};
|
||||||
logsStatsClose: css`
|
display: inline-block;
|
||||||
label: logs-stats__close;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
text-overflow: ellipsis;
|
||||||
`,
|
flex-grow: 1;
|
||||||
logsStatsBody: css`
|
`,
|
||||||
label: logs-stats__body;
|
logsStatsClose: css`
|
||||||
padding: 20px 10px 10px 10px;
|
label: logs-stats__close;
|
||||||
`,
|
cursor: pointer;
|
||||||
|
`,
|
||||||
|
logsStatsBody: css`
|
||||||
|
label: logs-stats__body;
|
||||||
|
padding: 5px 0;
|
||||||
|
`,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Props extends Themeable {
|
interface Props extends Themeable {
|
||||||
@ -50,12 +58,12 @@ interface Props extends Themeable {
|
|||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
rowCount: number;
|
rowCount: number;
|
||||||
onClickClose: () => void;
|
isLabel: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UnThemedLogLabelStats extends PureComponent<Props> {
|
class UnThemedLogLabelStats extends PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { label, rowCount, stats, value, onClickClose, theme } = this.props;
|
const { label, rowCount, stats, value, theme, isLabel } = this.props;
|
||||||
const style = getStyles(theme);
|
const style = getStyles(theme);
|
||||||
const topRows = stats.slice(0, STATS_ROW_LIMIT);
|
const topRows = stats.slice(0, STATS_ROW_LIMIT);
|
||||||
let activeRow = topRows.find(row => row.value === value);
|
let activeRow = topRows.find(row => row.value === value);
|
||||||
@ -74,14 +82,13 @@ class UnThemedLogLabelStats extends PureComponent<Props> {
|
|||||||
const otherProportion = otherCount / total;
|
const otherProportion = otherCount / total;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx([style.logsStats])}>
|
<div className={style.logsStats}>
|
||||||
<div className={cx([style.logsStatsHeader])}>
|
<div className={style.logsStatsHeader}>
|
||||||
<span className={cx([style.logsStatsTitle])}>
|
<div className={style.logsStatsTitle}>
|
||||||
{label}: {total} of {rowCount} rows have that label
|
{label}: {total} of {rowCount} rows have that {isLabel ? 'label' : 'field'}
|
||||||
</span>
|
</div>
|
||||||
<span className={cx([style.logsStatsClose, 'fa fa-remove'])} onClick={onClickClose} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className={cx([style.logsStatsBody])}>
|
<div className={style.logsStatsBody}>
|
||||||
{topRows.map(stat => (
|
{topRows.map(stat => (
|
||||||
<LogLabelStatsRow key={stat.value} {...stat} active={stat.value === value} />
|
<LogLabelStatsRow key={stat.value} {...stat} active={stat.value === value} />
|
||||||
))}
|
))}
|
||||||
|
@ -13,14 +13,6 @@ const getStyles = (theme: GrafanaTheme) => ({
|
|||||||
label: logs-stats-row--active;
|
label: logs-stats-row--active;
|
||||||
color: ${theme.colors.blue};
|
color: ${theme.colors.blue};
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
::after {
|
|
||||||
display: inline;
|
|
||||||
content: '*';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -8px;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
logsStatsRowLabel: css`
|
logsStatsRowLabel: css`
|
||||||
label: logs-stats-row__label;
|
label: logs-stats-row__label;
|
||||||
|
@ -1,33 +1,37 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { LogRowModel, TimeZone, DataQueryResponse } from '@grafana/data';
|
import { LogRowModel, TimeZone, DataQueryResponse } from '@grafana/data';
|
||||||
import { cx } from 'emotion';
|
|
||||||
import {
|
import {
|
||||||
LogRowContextRows,
|
LogRowContextRows,
|
||||||
LogRowContextQueryErrors,
|
LogRowContextQueryErrors,
|
||||||
HasMoreContextRows,
|
HasMoreContextRows,
|
||||||
LogRowContextProvider,
|
LogRowContextProvider,
|
||||||
} from './LogRowContextProvider';
|
} from './LogRowContextProvider';
|
||||||
import { LogLabels } from './LogLabels';
|
|
||||||
import { Themeable } from '../../types/theme';
|
import { Themeable } from '../../types/theme';
|
||||||
import { withTheme } from '../../themes/index';
|
import { withTheme } from '../../themes/index';
|
||||||
import { getLogRowStyles } from './getLogRowStyles';
|
import { getLogRowStyles } from './getLogRowStyles';
|
||||||
|
|
||||||
|
//Components
|
||||||
|
import { LogDetails } from './LogDetails';
|
||||||
import { LogRowMessage } from './LogRowMessage';
|
import { LogRowMessage } from './LogRowMessage';
|
||||||
|
|
||||||
interface Props extends Themeable {
|
interface Props extends Themeable {
|
||||||
highlighterExpressions?: string[];
|
highlighterExpressions?: string[];
|
||||||
row: LogRowModel;
|
row: LogRowModel;
|
||||||
showDuplicates: boolean;
|
showDuplicates: boolean;
|
||||||
showLabels: boolean;
|
|
||||||
showTime: boolean;
|
showTime: boolean;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
|
isLogsPanel?: boolean;
|
||||||
getRows: () => LogRowModel[];
|
getRows: () => LogRowModel[];
|
||||||
onClickLabel?: (label: string, value: string) => void;
|
onClickFilterLabel?: (key: string, value: string) => void;
|
||||||
|
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||||
onContextClick?: () => void;
|
onContextClick?: () => void;
|
||||||
getRowContext: (row: LogRowModel, options?: any) => Promise<DataQueryResponse>;
|
getRowContext: (row: LogRowModel, options?: any) => Promise<DataQueryResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
showContext: boolean;
|
showContext: boolean;
|
||||||
|
showDetails: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,6 +44,7 @@ interface State {
|
|||||||
class UnThemedLogRow extends PureComponent<Props, State> {
|
class UnThemedLogRow extends PureComponent<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
showContext: false,
|
showContext: false,
|
||||||
|
showDetails: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleContext = () => {
|
toggleContext = () => {
|
||||||
@ -50,6 +55,14 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toggleDetails = () => {
|
||||||
|
this.setState(state => {
|
||||||
|
return {
|
||||||
|
showDetails: !state.showDetails,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
renderLogRow(
|
renderLogRow(
|
||||||
context?: LogRowContextRows,
|
context?: LogRowContextRows,
|
||||||
errors?: LogRowContextQueryErrors,
|
errors?: LogRowContextQueryErrors,
|
||||||
@ -58,57 +71,66 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
|||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
getRows,
|
getRows,
|
||||||
|
onClickFilterLabel,
|
||||||
|
onClickFilterOutLabel,
|
||||||
highlighterExpressions,
|
highlighterExpressions,
|
||||||
onClickLabel,
|
isLogsPanel,
|
||||||
row,
|
row,
|
||||||
showDuplicates,
|
showDuplicates,
|
||||||
showLabels,
|
|
||||||
timeZone,
|
timeZone,
|
||||||
showTime,
|
showTime,
|
||||||
theme,
|
theme,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { showContext } = this.state;
|
const { showDetails, showContext } = this.state;
|
||||||
const style = getLogRowStyles(theme, row.logLevel);
|
const style = getLogRowStyles(theme, row.logLevel);
|
||||||
const showUtc = timeZone === 'utc';
|
const showUtc = timeZone === 'utc';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx([style.logsRow])}>
|
<div className={style.logsRow}>
|
||||||
{showDuplicates && (
|
{showDuplicates && (
|
||||||
<div className={cx([style.logsRowDuplicates])}>
|
<div className={style.logsRowDuplicates}>
|
||||||
{row.duplicates && row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
|
{row.duplicates && row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={cx([style.logsRowLevel])} />
|
<div className={style.logsRowLevel} />
|
||||||
{showTime && showUtc && (
|
{!isLogsPanel && (
|
||||||
<div className={cx([style.logsRowLocalTime])} title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
|
<div title="See log details" onClick={this.toggleDetails} className={style.logsRowToggleDetails}>
|
||||||
{row.timeUtc}
|
<i className={showDetails ? 'fa fa-chevron-up' : 'fa fa-chevron-down'} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showTime && !showUtc && (
|
<div>
|
||||||
<div className={cx([style.logsRowLocalTime])} title={`${row.timeUtc} (${row.timeFromNow})`}>
|
<div>
|
||||||
{row.timeLocal}
|
{showTime && showUtc && (
|
||||||
</div>
|
<div className={style.logsRowLocalTime} title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
|
||||||
)}
|
{row.timeUtc}
|
||||||
{showLabels && (
|
</div>
|
||||||
<div className={cx([style.logsRowLabels])}>
|
)}
|
||||||
<LogLabels
|
{showTime && !showUtc && (
|
||||||
|
<div className={style.logsRowLocalTime} title={`${row.timeUtc} (${row.timeFromNow})`}>
|
||||||
|
{row.timeLocal}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<LogRowMessage
|
||||||
|
highlighterExpressions={highlighterExpressions}
|
||||||
|
row={row}
|
||||||
getRows={getRows}
|
getRows={getRows}
|
||||||
labels={row.uniqueLabels ? row.uniqueLabels : {}}
|
errors={errors}
|
||||||
onClickLabel={onClickLabel}
|
hasMoreContextRows={hasMoreContextRows}
|
||||||
|
updateLimit={updateLimit}
|
||||||
|
context={context}
|
||||||
|
showContext={showContext}
|
||||||
|
onToggleContext={this.toggleContext}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{this.state.showDetails && (
|
||||||
<LogRowMessage
|
<LogDetails
|
||||||
highlighterExpressions={highlighterExpressions}
|
onClickFilterLabel={onClickFilterLabel}
|
||||||
row={row}
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||||
getRows={getRows}
|
getRows={getRows}
|
||||||
errors={errors}
|
row={row}
|
||||||
hasMoreContextRows={hasMoreContextRows}
|
/>
|
||||||
updateLimit={updateLimit}
|
)}
|
||||||
context={context}
|
</div>
|
||||||
showContext={showContext}
|
|
||||||
onToggleContext={this.toggleContext}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,68 +1,35 @@
|
|||||||
import React, { PureComponent, FunctionComponent, useContext } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
// @ts-ignore
|
|
||||||
import Highlighter from 'react-highlight-words';
|
|
||||||
import {
|
|
||||||
LogRowModel,
|
|
||||||
LogLabelStatsModel,
|
|
||||||
LogsParser,
|
|
||||||
calculateFieldStats,
|
|
||||||
getParser,
|
|
||||||
findHighlightChunksInText,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { css, cx } from 'emotion';
|
import { css, cx } from 'emotion';
|
||||||
import { selectThemeVariant, ThemeContext } from '../../index';
|
import { LogRowModel, findHighlightChunksInText, GrafanaTheme } from '@grafana/data';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
|
||||||
|
// @ts-ignore
|
||||||
|
import Highlighter from 'react-highlight-words';
|
||||||
import { LogRowContextQueryErrors, HasMoreContextRows, LogRowContextRows } from './LogRowContextProvider';
|
import { LogRowContextQueryErrors, HasMoreContextRows, LogRowContextRows } from './LogRowContextProvider';
|
||||||
import { LogRowContext } from './LogRowContext';
|
import { selectThemeVariant } from '../../index';
|
||||||
import { LogMessageAnsi } from './LogMessageAnsi';
|
|
||||||
import { LogLabelStats } from './LogLabelStats';
|
|
||||||
import { Themeable } from '../../types/theme';
|
import { Themeable } from '../../types/theme';
|
||||||
import { withTheme } from '../../themes/index';
|
import { withTheme } from '../../themes/index';
|
||||||
import { getLogRowStyles } from './getLogRowStyles';
|
import { getLogRowStyles } from './getLogRowStyles';
|
||||||
import { stylesFactory } from '../../themes/stylesFactory';
|
import { stylesFactory } from '../../themes/stylesFactory';
|
||||||
|
|
||||||
|
//Components
|
||||||
|
import { LogRowContext } from './LogRowContext';
|
||||||
|
import { LogMessageAnsi } from './LogMessageAnsi';
|
||||||
|
|
||||||
interface Props extends Themeable {
|
interface Props extends Themeable {
|
||||||
highlighterExpressions?: string[];
|
|
||||||
row: LogRowModel;
|
row: LogRowModel;
|
||||||
getRows: () => LogRowModel[];
|
|
||||||
errors?: LogRowContextQueryErrors;
|
|
||||||
hasMoreContextRows?: HasMoreContextRows;
|
hasMoreContextRows?: HasMoreContextRows;
|
||||||
updateLimit?: () => void;
|
|
||||||
context?: LogRowContextRows;
|
|
||||||
showContext: boolean;
|
showContext: boolean;
|
||||||
|
errors?: LogRowContextQueryErrors;
|
||||||
|
context?: LogRowContextRows;
|
||||||
|
highlighterExpressions?: string[];
|
||||||
|
getRows: () => LogRowModel[];
|
||||||
onToggleContext: () => void;
|
onToggleContext: () => void;
|
||||||
|
updateLimit?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {}
|
||||||
fieldCount: number;
|
|
||||||
fieldLabel: string | null;
|
|
||||||
fieldStats: LogLabelStatsModel[] | null;
|
|
||||||
fieldValue: string | null;
|
|
||||||
parsed: boolean;
|
|
||||||
parser?: LogsParser;
|
|
||||||
parsedFieldHighlights: string[];
|
|
||||||
showFieldStats: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a highlighted field.
|
|
||||||
* When hovering, a stats icon is shown.
|
|
||||||
*/
|
|
||||||
const FieldHighlight = (onClick: any): FunctionComponent<any> => (props: any) => {
|
|
||||||
const theme = useContext(ThemeContext);
|
|
||||||
const style = getLogRowStyles(theme);
|
|
||||||
return (
|
|
||||||
<span className={props.className} style={props.style}>
|
|
||||||
{props.children}
|
|
||||||
<span
|
|
||||||
className={cx([style, 'logs-row__field-highlight--icon', 'fa fa-signal'])}
|
|
||||||
onClick={() => onClick(props.children)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
const outlineColor = selectThemeVariant(
|
const outlineColor = selectThemeVariant(
|
||||||
@ -86,98 +53,14 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
.setAlpha(0.7)
|
.setAlpha(0.7)
|
||||||
.toRgbString()};
|
.toRgbString()};
|
||||||
`,
|
`,
|
||||||
|
whiteSpacePreWrap: css`
|
||||||
|
label: whiteSpacePreWrap;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
class UnThemedLogRowMessage extends PureComponent<Props, State> {
|
class UnThemedLogRowMessage extends PureComponent<Props, State> {
|
||||||
mouseMessageTimer: number | null = null;
|
|
||||||
|
|
||||||
state: State = {
|
|
||||||
fieldCount: 0,
|
|
||||||
fieldLabel: null,
|
|
||||||
fieldStats: null,
|
|
||||||
fieldValue: null,
|
|
||||||
parsed: false,
|
|
||||||
parser: undefined,
|
|
||||||
parsedFieldHighlights: [],
|
|
||||||
showFieldStats: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.clearMouseMessageTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickClose = () => {
|
|
||||||
this.setState({ showFieldStats: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onClickHighlight = (fieldText: string) => {
|
|
||||||
const { getRows } = this.props;
|
|
||||||
const { parser } = this.state;
|
|
||||||
const allRows = getRows();
|
|
||||||
|
|
||||||
// Build value-agnostic row matcher based on the field label
|
|
||||||
const fieldLabel = parser!.getLabelFromField(fieldText);
|
|
||||||
const fieldValue = parser!.getValueFromField(fieldText);
|
|
||||||
const matcher = parser!.buildMatcher(fieldLabel);
|
|
||||||
const fieldStats = calculateFieldStats(allRows, matcher);
|
|
||||||
const fieldCount = fieldStats.reduce((sum, stat) => sum + stat.count, 0);
|
|
||||||
|
|
||||||
this.setState({ fieldCount, fieldLabel, fieldStats, fieldValue, showFieldStats: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onMouseOverMessage = () => {
|
|
||||||
if (this.props.showContext || this.isTextSelected()) {
|
|
||||||
// When showing context we don't want to the LogRow rerender as it will mess up state of context block
|
|
||||||
// making the "after" context to be scrolled to the top, what is desired only on open
|
|
||||||
// The log row message needs to be refactored to separate component that encapsulates parsing and parsed message state
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Don't parse right away, user might move along
|
|
||||||
this.mouseMessageTimer = window.setTimeout(this.parseMessage, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMouseOutMessage = () => {
|
|
||||||
if (this.props.showContext) {
|
|
||||||
// See comment in onMouseOverMessage method
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.clearMouseMessageTimer();
|
|
||||||
this.setState({ parsed: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
clearMouseMessageTimer = () => {
|
|
||||||
if (this.mouseMessageTimer) {
|
|
||||||
clearTimeout(this.mouseMessageTimer);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
parseMessage = () => {
|
|
||||||
if (!this.state.parsed) {
|
|
||||||
const { row } = this.props;
|
|
||||||
const parser = getParser(row.entry);
|
|
||||||
if (parser) {
|
|
||||||
// Use parser to highlight detected fields
|
|
||||||
const parsedFieldHighlights = parser.getFields(this.props.row.entry);
|
|
||||||
this.setState({ parsedFieldHighlights, parsed: true, parser });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
isTextSelected() {
|
|
||||||
if (!window.getSelection) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selection = window.getSelection();
|
|
||||||
|
|
||||||
if (!selection) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return selection.anchorNode !== null && selection.isCollapsed === false;
|
|
||||||
}
|
|
||||||
|
|
||||||
onContextToggle = (e: React.SyntheticEvent<HTMLElement>) => {
|
onContextToggle = (e: React.SyntheticEvent<HTMLElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.props.onToggleContext();
|
this.props.onToggleContext();
|
||||||
@ -195,17 +78,10 @@ class UnThemedLogRowMessage extends PureComponent<Props, State> {
|
|||||||
showContext,
|
showContext,
|
||||||
onToggleContext,
|
onToggleContext,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {} = this.state;
|
||||||
fieldCount,
|
|
||||||
fieldLabel,
|
|
||||||
fieldStats,
|
|
||||||
fieldValue,
|
|
||||||
parsed,
|
|
||||||
parsedFieldHighlights,
|
|
||||||
showFieldStats,
|
|
||||||
} = this.state;
|
|
||||||
const style = getLogRowStyles(theme, row.logLevel);
|
const style = getLogRowStyles(theme, row.logLevel);
|
||||||
const { entry, hasAnsi, raw } = row;
|
const { entry, hasAnsi, raw } = row;
|
||||||
|
|
||||||
const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
|
const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
|
||||||
const highlights = previewHighlights ? highlighterExpressions : row.searchWords;
|
const highlights = previewHighlights ? highlighterExpressions : row.searchWords;
|
||||||
const needsHighlighter = highlights && highlights.length > 0 && highlights[0] && highlights[0].length > 0;
|
const needsHighlighter = highlights && highlights.length > 0 && highlights[0] && highlights[0].length > 0;
|
||||||
@ -213,13 +89,8 @@ class UnThemedLogRowMessage extends PureComponent<Props, State> {
|
|||||||
? cx([style.logsRowMatchHighLight, style.logsRowMatchHighLightPreview])
|
? cx([style.logsRowMatchHighLight, style.logsRowMatchHighLightPreview])
|
||||||
: cx([style.logsRowMatchHighLight]);
|
: cx([style.logsRowMatchHighLight]);
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={style.logsRowMessage}>
|
||||||
className={cx([style.logsRowMessage])}
|
|
||||||
onMouseEnter={this.onMouseOverMessage}
|
|
||||||
onMouseLeave={this.onMouseOutMessage}
|
|
||||||
>
|
|
||||||
<div className={styles.positionRelative}>
|
<div className={styles.positionRelative}>
|
||||||
{showContext && context && (
|
{showContext && context && (
|
||||||
<LogRowContext
|
<LogRowContext
|
||||||
@ -236,37 +107,18 @@ class UnThemedLogRowMessage extends PureComponent<Props, State> {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className={cx(styles.positionRelative, { [styles.rowWithContext]: showContext })}>
|
<span className={cx(styles.positionRelative, { [styles.rowWithContext]: showContext })}>
|
||||||
{parsed && (
|
{needsHighlighter ? (
|
||||||
<Highlighter
|
<Highlighter
|
||||||
style={{ whiteSpace: 'pre-wrap' }}
|
style={styles.whiteSpacePreWrap}
|
||||||
autoEscape
|
|
||||||
highlightTag={FieldHighlight(this.onClickHighlight)}
|
|
||||||
textToHighlight={entry}
|
|
||||||
searchWords={parsedFieldHighlights}
|
|
||||||
highlightClassName={cx([style.logsRowFieldHighLight])}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!parsed && needsHighlighter && (
|
|
||||||
<Highlighter
|
|
||||||
style={{ whiteSpace: 'pre-wrap' }}
|
|
||||||
textToHighlight={entry}
|
textToHighlight={entry}
|
||||||
searchWords={highlights}
|
searchWords={highlights}
|
||||||
findChunks={findHighlightChunksInText}
|
findChunks={findHighlightChunksInText}
|
||||||
highlightClassName={highlightClassName}
|
highlightClassName={highlightClassName}
|
||||||
/>
|
/>
|
||||||
)}
|
) : hasAnsi ? (
|
||||||
{hasAnsi && !parsed && !needsHighlighter && <LogMessageAnsi value={raw} />}
|
<LogMessageAnsi value={raw} />
|
||||||
{!hasAnsi && !parsed && !needsHighlighter && entry}
|
) : (
|
||||||
{showFieldStats && (
|
entry
|
||||||
<div className={cx([style.logsRowStats])}>
|
|
||||||
<LogLabelStats
|
|
||||||
stats={fieldStats!}
|
|
||||||
label={fieldLabel!}
|
|
||||||
value={fieldValue!}
|
|
||||||
onClickClose={this.onClickClose}
|
|
||||||
rowCount={fieldCount}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{row.searchWords && row.searchWords.length > 0 && (
|
{row.searchWords && row.searchWords.length > 0 && (
|
||||||
|
@ -17,7 +17,6 @@ describe('LogRows', () => {
|
|||||||
dedupStrategy={LogsDedupStrategy.none}
|
dedupStrategy={LogsDedupStrategy.none}
|
||||||
highlighterExpressions={[]}
|
highlighterExpressions={[]}
|
||||||
showTime={false}
|
showTime={false}
|
||||||
showLabels={false}
|
|
||||||
timeZone={'utc'}
|
timeZone={'utc'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -40,7 +39,6 @@ describe('LogRows', () => {
|
|||||||
dedupStrategy={LogsDedupStrategy.none}
|
dedupStrategy={LogsDedupStrategy.none}
|
||||||
highlighterExpressions={[]}
|
highlighterExpressions={[]}
|
||||||
showTime={false}
|
showTime={false}
|
||||||
showLabels={false}
|
|
||||||
timeZone={'utc'}
|
timeZone={'utc'}
|
||||||
previewLimit={1}
|
previewLimit={1}
|
||||||
/>
|
/>
|
||||||
@ -75,7 +73,6 @@ describe('LogRows', () => {
|
|||||||
dedupStrategy={LogsDedupStrategy.none}
|
dedupStrategy={LogsDedupStrategy.none}
|
||||||
highlighterExpressions={[]}
|
highlighterExpressions={[]}
|
||||||
showTime={false}
|
showTime={false}
|
||||||
showLabels={false}
|
|
||||||
timeZone={'utc'}
|
timeZone={'utc'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -97,7 +94,6 @@ describe('LogRows', () => {
|
|||||||
dedupStrategy={LogsDedupStrategy.none}
|
dedupStrategy={LogsDedupStrategy.none}
|
||||||
highlighterExpressions={[]}
|
highlighterExpressions={[]}
|
||||||
showTime={false}
|
showTime={false}
|
||||||
showLabels={false}
|
|
||||||
timeZone={'utc'}
|
timeZone={'utc'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -121,6 +117,7 @@ const makeLog = (overides: Partial<LogRowModel>): LogRowModel => {
|
|||||||
timeEpochMs: 1,
|
timeEpochMs: 1,
|
||||||
timeLocal: '',
|
timeLocal: '',
|
||||||
timeUtc: '',
|
timeUtc: '',
|
||||||
|
searchWords: [],
|
||||||
...overides,
|
...overides,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { cx } from 'emotion';
|
import memoizeOne from 'memoize-one';
|
||||||
import { LogsModel, TimeZone, LogsDedupStrategy, LogRowModel } from '@grafana/data';
|
import { LogsModel, TimeZone, LogsDedupStrategy, LogRowModel } from '@grafana/data';
|
||||||
|
|
||||||
import { LogRow } from './LogRow';
|
|
||||||
import { Themeable } from '../../types/theme';
|
import { Themeable } from '../../types/theme';
|
||||||
import { withTheme } from '../../themes/index';
|
import { withTheme } from '../../themes/index';
|
||||||
import { getLogRowStyles } from './getLogRowStyles';
|
import { getLogRowStyles } from './getLogRowStyles';
|
||||||
import memoizeOne from 'memoize-one';
|
|
||||||
|
//Components
|
||||||
|
import { LogRow } from './LogRow';
|
||||||
|
|
||||||
export const PREVIEW_LIMIT = 100;
|
export const PREVIEW_LIMIT = 100;
|
||||||
export const RENDER_LIMIT = 500;
|
export const RENDER_LIMIT = 500;
|
||||||
@ -16,13 +17,14 @@ export interface Props extends Themeable {
|
|||||||
dedupStrategy: LogsDedupStrategy;
|
dedupStrategy: LogsDedupStrategy;
|
||||||
highlighterExpressions: string[];
|
highlighterExpressions: string[];
|
||||||
showTime: boolean;
|
showTime: boolean;
|
||||||
showLabels: boolean;
|
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
deduplicatedData?: LogsModel;
|
deduplicatedData?: LogsModel;
|
||||||
onClickLabel?: (label: string, value: string) => void;
|
|
||||||
getRowContext?: (row: LogRowModel, options?: any) => Promise<any>;
|
|
||||||
rowLimit?: number;
|
rowLimit?: number;
|
||||||
|
isLogsPanel?: boolean;
|
||||||
previewLimit?: number;
|
previewLimit?: number;
|
||||||
|
onClickFilterLabel?: (key: string, value: string) => void;
|
||||||
|
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||||
|
getRowContext?: (row: LogRowModel, options?: any) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -71,17 +73,17 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
|||||||
data,
|
data,
|
||||||
deduplicatedData,
|
deduplicatedData,
|
||||||
highlighterExpressions,
|
highlighterExpressions,
|
||||||
showLabels,
|
|
||||||
timeZone,
|
timeZone,
|
||||||
onClickLabel,
|
onClickFilterLabel,
|
||||||
|
onClickFilterOutLabel,
|
||||||
rowLimit,
|
rowLimit,
|
||||||
theme,
|
theme,
|
||||||
|
isLogsPanel,
|
||||||
previewLimit,
|
previewLimit,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { renderAll } = this.state;
|
const { renderAll } = this.state;
|
||||||
const dedupedData = deduplicatedData ? deduplicatedData : data;
|
const dedupedData = deduplicatedData ? deduplicatedData : data;
|
||||||
const hasData = data && data.rows && data.rows.length > 0;
|
const hasData = data && data.rows && data.rows.length > 0;
|
||||||
const hasLabel = hasData && dedupedData && dedupedData.hasUniqueLabels ? true : false;
|
|
||||||
const dedupCount = dedupedData
|
const dedupCount = dedupedData
|
||||||
? dedupedData.rows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0)
|
? dedupedData.rows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0)
|
||||||
: 0;
|
: 0;
|
||||||
@ -99,7 +101,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
|||||||
const { logsRows } = getLogRowStyles(theme);
|
const { logsRows } = getLogRowStyles(theme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx([logsRows])}>
|
<div className={logsRows}>
|
||||||
{hasData &&
|
{hasData &&
|
||||||
firstRows.map((row, index) => (
|
firstRows.map((row, index) => (
|
||||||
<LogRow
|
<LogRow
|
||||||
@ -109,10 +111,11 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
|||||||
highlighterExpressions={highlighterExpressions}
|
highlighterExpressions={highlighterExpressions}
|
||||||
row={row}
|
row={row}
|
||||||
showDuplicates={showDuplicates}
|
showDuplicates={showDuplicates}
|
||||||
showLabels={showLabels && hasLabel}
|
|
||||||
showTime={showTime}
|
showTime={showTime}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
onClickLabel={onClickLabel}
|
isLogsPanel={isLogsPanel}
|
||||||
|
onClickFilterLabel={onClickFilterLabel}
|
||||||
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{hasData &&
|
{hasData &&
|
||||||
@ -124,10 +127,11 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
|||||||
getRowContext={getRowContext}
|
getRowContext={getRowContext}
|
||||||
row={row}
|
row={row}
|
||||||
showDuplicates={showDuplicates}
|
showDuplicates={showDuplicates}
|
||||||
showLabels={showLabels && hasLabel}
|
|
||||||
showTime={showTime}
|
showTime={showTime}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
onClickLabel={onClickLabel}
|
isLogsPanel={isLogsPanel}
|
||||||
|
onClickFilterLabel={onClickFilterLabel}
|
||||||
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{hasData && !renderAll && <span>Rendering {rowCount - previewLimit!} rows...</span>}
|
{hasData && !renderAll && <span>Rendering {rowCount - previewLimit!} rows...</span>}
|
||||||
|
@ -7,6 +7,7 @@ import { stylesFactory } from '../../themes';
|
|||||||
|
|
||||||
export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: LogLevel) => {
|
export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: LogLevel) => {
|
||||||
let logColor = selectThemeVariant({ light: theme.colors.gray5, dark: theme.colors.gray2 }, theme.type);
|
let logColor = selectThemeVariant({ light: theme.colors.gray5, dark: theme.colors.gray2 }, theme.type);
|
||||||
|
const bgColor = selectThemeVariant({ light: theme.colors.gray5, dark: theme.colors.gray2 }, theme.type);
|
||||||
switch (logLevel) {
|
switch (logLevel) {
|
||||||
case LogLevel.crit:
|
case LogLevel.crit:
|
||||||
case LogLevel.critical:
|
case LogLevel.critical:
|
||||||
@ -32,34 +33,13 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
logsRowFieldHighLight: css`
|
|
||||||
label: logs-row__field-highlight;
|
|
||||||
background: inherit;
|
|
||||||
padding: inherit;
|
|
||||||
border-bottom: 1px dotted ${theme.colors.yellow};
|
|
||||||
|
|
||||||
.logs-row__field-highlight--icon {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
cursor: pointer;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: ${theme.colors.yellow};
|
|
||||||
border-bottom-style: solid;
|
|
||||||
|
|
||||||
.logs-row__field-highlight--icon {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
logsRowMatchHighLight: css`
|
logsRowMatchHighLight: css`
|
||||||
label: logs-row__match-highlight;
|
label: logs-row__match-highlight;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
padding: inherit;
|
padding: inherit;
|
||||||
|
|
||||||
color: ${theme.colors.yellow};
|
color: ${theme.colors.yellow};
|
||||||
border-bottom: 1px solid ${theme.colors.yellow};
|
border-bottom: ${theme.border.width.sm} solid ${theme.colors.yellow};
|
||||||
background-color: rgba(${theme.colors.yellow}, 0.1);
|
background-color: rgba(${theme.colors.yellow}, 0.1);
|
||||||
`,
|
`,
|
||||||
logsRowMatchHighLightPreview: css`
|
logsRowMatchHighLightPreview: css`
|
||||||
@ -81,9 +61,9 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
|
|||||||
|
|
||||||
> div {
|
> div {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
padding-right: 10px;
|
padding-right: ${theme.spacing.sm};
|
||||||
border-top: 1px solid transparent;
|
border-top: ${theme.border.width.sm} solid transparent;
|
||||||
border-bottom: 1px solid transparent;
|
border-bottom: ${theme.border.width.sm} solid transparent;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,24 +91,87 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
|
|||||||
background-color: ${logColor};
|
background-color: ${logColor};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
logsRowCell: css`
|
||||||
|
label: logs-row-cell;
|
||||||
|
display: table-cell;
|
||||||
|
word-break: break-all;
|
||||||
|
`,
|
||||||
|
logsRowToggleDetails: css`
|
||||||
|
label: logs-row-toggle-details__level;
|
||||||
|
position: relative;
|
||||||
|
width: 15px;
|
||||||
|
padding-right: ${theme.spacing.sm};
|
||||||
|
font-size: 9px;
|
||||||
|
cursor: pointer;
|
||||||
|
`,
|
||||||
logsRowLocalTime: css`
|
logsRowLocalTime: css`
|
||||||
label: logs-row__localtime;
|
label: logs-row__localtime;
|
||||||
|
display: table-cell;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 12.5em;
|
width: 12.5em;
|
||||||
`,
|
`,
|
||||||
logsRowLabels: css`
|
|
||||||
label: logs-row__labels;
|
|
||||||
width: 20%;
|
|
||||||
line-height: 1.2;
|
|
||||||
position: relative;
|
|
||||||
`,
|
|
||||||
logsRowMessage: css`
|
logsRowMessage: css`
|
||||||
label: logs-row__message;
|
label: logs-row__message;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
display: table-cell;
|
||||||
`,
|
`,
|
||||||
logsRowStats: css`
|
logsRowStats: css`
|
||||||
label: logs-row__stats;
|
label: logs-row__stats;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
`,
|
`,
|
||||||
|
//Log details sepcific CSS
|
||||||
|
logsRowDetailsTable: css`
|
||||||
|
label: logs-row-details-table;
|
||||||
|
display: table;
|
||||||
|
border: 1px solid ${bgColor};
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: ${theme.spacing.sm};
|
||||||
|
width: 100%;
|
||||||
|
`,
|
||||||
|
logsRowDetailsSectionTable: css`
|
||||||
|
label: logs-row-details-table__section;
|
||||||
|
display: table;
|
||||||
|
table-layout: fixed;
|
||||||
|
margin: 5px 0;
|
||||||
|
width: 100%;
|
||||||
|
`,
|
||||||
|
logsRowDetailsIcon: css`
|
||||||
|
label: logs-row-details__icon;
|
||||||
|
display: table-cell;
|
||||||
|
position: relative;
|
||||||
|
width: 22px;
|
||||||
|
padding-right: ${theme.spacing.sm};
|
||||||
|
color: ${theme.colors.gray3};
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: ${theme.colors.yellow};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
logsRowDetailsLabel: css`
|
||||||
|
label: logs-row-details__label;
|
||||||
|
display: table-cell;
|
||||||
|
padding: 0 ${theme.spacing.md} 0 ${theme.spacing.md};
|
||||||
|
width: 12.5em;
|
||||||
|
word-break: break-all;
|
||||||
|
`,
|
||||||
|
logsRowDetailsHeading: css`
|
||||||
|
label: logs-row-details__heading;
|
||||||
|
display: table-caption;
|
||||||
|
margin: 5px 0 7px;
|
||||||
|
font-weight: ${theme.typography.weight.bold};
|
||||||
|
`,
|
||||||
|
logsRowDetailsValue: css`
|
||||||
|
label: logs-row-details__row;
|
||||||
|
display: table-row;
|
||||||
|
line-height: 2;
|
||||||
|
padding: 0 ${theme.spacing.xl} 0 ${theme.spacing.md};
|
||||||
|
position: relative;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ${theme.colors.yellow};
|
||||||
|
}
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -192,10 +192,14 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
this.props.setQueries(this.props.exploreId, [query]);
|
this.props.setQueries(this.props.exploreId, [query]);
|
||||||
};
|
};
|
||||||
|
|
||||||
onClickLabel = (key: string, value: string) => {
|
onClickFilterLabel = (key: string, value: string) => {
|
||||||
this.onModifyQueries({ type: 'ADD_FILTER', key, value });
|
this.onModifyQueries({ type: 'ADD_FILTER', key, value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onClickFilterOutLabel = (key: string, value: string) => {
|
||||||
|
this.onModifyQueries({ type: 'ADD_FILTER_OUT', key, value });
|
||||||
|
};
|
||||||
|
|
||||||
onModifyQueries = (action: any, index?: number) => {
|
onModifyQueries = (action: any, index?: number) => {
|
||||||
const { datasourceInstance } = this.props;
|
const { datasourceInstance } = this.props;
|
||||||
if (datasourceInstance && datasourceInstance.modifyQuery) {
|
if (datasourceInstance && datasourceInstance.modifyQuery) {
|
||||||
@ -307,14 +311,15 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{mode === ExploreMode.Metrics && (
|
{mode === ExploreMode.Metrics && (
|
||||||
<TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />
|
<TableContainer exploreId={exploreId} onClickCell={this.onClickFilterLabel} />
|
||||||
)}
|
)}
|
||||||
{mode === ExploreMode.Logs && (
|
{mode === ExploreMode.Logs && (
|
||||||
<LogsContainer
|
<LogsContainer
|
||||||
width={width}
|
width={width}
|
||||||
exploreId={exploreId}
|
exploreId={exploreId}
|
||||||
syncedTimes={syncedTimes}
|
syncedTimes={syncedTimes}
|
||||||
onClickLabel={this.onClickLabel}
|
onClickFilterLabel={this.onClickFilterLabel}
|
||||||
|
onClickFilterOutLabel={this.onClickFilterOutLabel}
|
||||||
onStartScanning={this.onStartScanning}
|
onStartScanning={this.onStartScanning}
|
||||||
onStopScanning={this.onStopScanning}
|
onStopScanning={this.onStopScanning}
|
||||||
/>
|
/>
|
||||||
|
@ -39,7 +39,8 @@ interface Props {
|
|||||||
scanRange?: RawTimeRange;
|
scanRange?: RawTimeRange;
|
||||||
dedupStrategy: LogsDedupStrategy;
|
dedupStrategy: LogsDedupStrategy;
|
||||||
onChangeTime: (range: AbsoluteTimeRange) => void;
|
onChangeTime: (range: AbsoluteTimeRange) => void;
|
||||||
onClickLabel?: (label: string, value: string) => void;
|
onClickFilterLabel?: (key: string, value: string) => void;
|
||||||
|
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||||
onStartScanning?: () => void;
|
onStartScanning?: () => void;
|
||||||
onStopScanning?: () => void;
|
onStopScanning?: () => void;
|
||||||
onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void;
|
onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void;
|
||||||
@ -48,13 +49,11 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
showLabels: boolean;
|
|
||||||
showTime: boolean;
|
showTime: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Logs extends PureComponent<Props, State> {
|
export class Logs extends PureComponent<Props, State> {
|
||||||
state = {
|
state = {
|
||||||
showLabels: false,
|
|
||||||
showTime: true,
|
showTime: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -66,15 +65,6 @@ export class Logs extends PureComponent<Props, State> {
|
|||||||
return onDedupStrategyChange(dedup);
|
return onDedupStrategyChange(dedup);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeLabels = (event?: React.SyntheticEvent) => {
|
|
||||||
const target = event && (event.target as HTMLInputElement);
|
|
||||||
if (target) {
|
|
||||||
this.setState({
|
|
||||||
showLabels: target.checked,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onChangeTime = (event?: React.SyntheticEvent) => {
|
onChangeTime = (event?: React.SyntheticEvent) => {
|
||||||
const target = event && (event.target as HTMLInputElement);
|
const target = event && (event.target as HTMLInputElement);
|
||||||
if (target) {
|
if (target) {
|
||||||
@ -108,7 +98,8 @@ export class Logs extends PureComponent<Props, State> {
|
|||||||
data,
|
data,
|
||||||
highlighterExpressions,
|
highlighterExpressions,
|
||||||
loading = false,
|
loading = false,
|
||||||
onClickLabel,
|
onClickFilterLabel,
|
||||||
|
onClickFilterOutLabel,
|
||||||
timeZone,
|
timeZone,
|
||||||
scanning,
|
scanning,
|
||||||
scanRange,
|
scanRange,
|
||||||
@ -122,7 +113,7 @@ export class Logs extends PureComponent<Props, State> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { showLabels, showTime } = this.state;
|
const { showTime } = this.state;
|
||||||
const { dedupStrategy } = this.props;
|
const { dedupStrategy } = this.props;
|
||||||
const hasData = data && data.rows && data.rows.length > 0;
|
const hasData = data && data.rows && data.rows.length > 0;
|
||||||
const dedupCount = dedupedData
|
const dedupCount = dedupedData
|
||||||
@ -163,7 +154,6 @@ export class Logs extends PureComponent<Props, State> {
|
|||||||
<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 />
|
<Switch label="Time" checked={showTime} onChange={this.onChangeTime} transparent />
|
||||||
<Switch label="Labels" checked={showLabels} onChange={this.onChangeLabels} transparent />
|
|
||||||
<ToggleButtonGroup label="Dedup" transparent={true}>
|
<ToggleButtonGroup label="Dedup" transparent={true}>
|
||||||
{Object.keys(LogsDedupStrategy).map((dedupType: string, i) => (
|
{Object.keys(LogsDedupStrategy).map((dedupType: string, i) => (
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
@ -198,9 +188,9 @@ export class Logs extends PureComponent<Props, State> {
|
|||||||
dedupStrategy={dedupStrategy}
|
dedupStrategy={dedupStrategy}
|
||||||
getRowContext={this.props.getRowContext}
|
getRowContext={this.props.getRowContext}
|
||||||
highlighterExpressions={highlighterExpressions}
|
highlighterExpressions={highlighterExpressions}
|
||||||
onClickLabel={onClickLabel}
|
onClickFilterLabel={onClickFilterLabel}
|
||||||
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||||
rowLimit={data ? data.rows.length : undefined}
|
rowLimit={data ? data.rows.length : undefined}
|
||||||
showLabels={showLabels}
|
|
||||||
showTime={showTime}
|
showTime={showTime}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
/>
|
/>
|
||||||
|
@ -35,7 +35,8 @@ interface LogsContainerProps {
|
|||||||
logsHighlighterExpressions?: string[];
|
logsHighlighterExpressions?: string[];
|
||||||
logsResult?: LogsModel;
|
logsResult?: LogsModel;
|
||||||
dedupedResult?: LogsModel;
|
dedupedResult?: LogsModel;
|
||||||
onClickLabel: (key: string, value: string) => void;
|
onClickFilterLabel?: (key: string, value: string) => void;
|
||||||
|
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||||
onStartScanning: () => void;
|
onStartScanning: () => void;
|
||||||
onStopScanning: () => void;
|
onStopScanning: () => void;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
@ -87,7 +88,8 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
|||||||
logsHighlighterExpressions,
|
logsHighlighterExpressions,
|
||||||
logsResult,
|
logsResult,
|
||||||
dedupedResult,
|
dedupedResult,
|
||||||
onClickLabel,
|
onClickFilterLabel,
|
||||||
|
onClickFilterOutLabel,
|
||||||
onStartScanning,
|
onStartScanning,
|
||||||
onStopScanning,
|
onStopScanning,
|
||||||
absoluteRange,
|
absoluteRange,
|
||||||
@ -126,7 +128,8 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
|||||||
highlighterExpressions={logsHighlighterExpressions}
|
highlighterExpressions={logsHighlighterExpressions}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onChangeTime={this.onChangeTime}
|
onChangeTime={this.onChangeTime}
|
||||||
onClickLabel={onClickLabel}
|
onClickFilterLabel={onClickFilterLabel}
|
||||||
|
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||||
onStartScanning={onStartScanning}
|
onStartScanning={onStartScanning}
|
||||||
onStopScanning={onStopScanning}
|
onStopScanning={onStopScanning}
|
||||||
onDedupStrategyChange={this.handleDedupStrategyChange}
|
onDedupStrategyChange={this.handleDedupStrategyChange}
|
||||||
|
@ -262,6 +262,10 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
selector = addLabelToSelector(selector, action.key, action.value);
|
selector = addLabelToSelector(selector, action.key, action.value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'ADD_FILTER_OUT': {
|
||||||
|
selector = addLabelToSelector(selector, action.key, action.value, '!=');
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -31,8 +31,8 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
|
|||||||
dedupStrategy={LogsDedupStrategy.none}
|
dedupStrategy={LogsDedupStrategy.none}
|
||||||
highlighterExpressions={[]}
|
highlighterExpressions={[]}
|
||||||
showTime={showTime}
|
showTime={showTime}
|
||||||
showLabels={false}
|
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
|
isLogsPanel={true}
|
||||||
/>
|
/>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user