Commit c3b67f3a by David Kaltschmidt

Explore: Logging label filtering

- adds a custom label renderer to Logs viewer in Explore
- labels are no longer treated as strings, they are passed as parsed objects to the log row
- label renderer supports onClick handler for an action
- renamed Explore's `onClickTableCell` to `onClickLabel` and wired up log label renderers
- reuse Prometheus `addLabelToSelector` to modify Logging queries via click on label
- added tests to `addLabelToSelector`, changed to include the surrounding `{}`
- use label render also for common labels in the controls panel
- logging meta data section has now a custom renderer that can render numbers, strings, and labels
- style adjustments
parent 8830c133
...@@ -35,19 +35,26 @@ export interface LogRow { ...@@ -35,19 +35,26 @@ export interface LogRow {
duplicates?: number; duplicates?: number;
entry: string; entry: string;
key: string; // timestamp + labels key: string; // timestamp + labels
labels: string; labels: LogsStreamLabels;
logLevel: LogLevel; logLevel: LogLevel;
searchWords?: string[]; searchWords?: string[];
timestamp: string; // ISO with nanosec precision timestamp: string; // ISO with nanosec precision
timeFromNow: string; timeFromNow: string;
timeEpochMs: number; timeEpochMs: number;
timeLocal: string; timeLocal: string;
uniqueLabels?: string; uniqueLabels?: LogsStreamLabels;
}
export enum LogsMetaKind {
Number,
String,
LabelsMap,
} }
export interface LogsMetaItem { export interface LogsMetaItem {
label: string; label: string;
value: string; value: string | number | LogsStreamLabels;
kind: LogsMetaKind;
} }
export interface LogsModel { export interface LogsModel {
...@@ -61,7 +68,7 @@ export interface LogsStream { ...@@ -61,7 +68,7 @@ export interface LogsStream {
entries: LogsStreamEntry[]; entries: LogsStreamEntry[];
search?: string; search?: string;
parsedLabels?: LogsStreamLabels; parsedLabels?: LogsStreamLabels;
uniqueLabels?: string; uniqueLabels?: LogsStreamLabels;
} }
export interface LogsStreamEntry { export interface LogsStreamEntry {
......
...@@ -429,8 +429,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> { ...@@ -429,8 +429,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
); );
}; };
onClickTableCell = (columnKey: string, rowValue: string) => { onClickLabel = (key: string, value: string) => {
this.onModifyQueries({ type: 'ADD_FILTER', key: columnKey, value: rowValue }); this.onModifyQueries({ type: 'ADD_FILTER', key, value });
}; };
onModifyQueries = (action, index?: number) => { onModifyQueries = (action, index?: number) => {
...@@ -931,7 +931,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> { ...@@ -931,7 +931,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
isOpen={showingTable} isOpen={showingTable}
onToggle={this.onClickTableButton} onToggle={this.onClickTableButton}
> >
<Table data={tableResult} loading={tableLoading} onClickCell={this.onClickTableCell} /> <Table data={tableResult} loading={tableLoading} onClickCell={this.onClickLabel} />
</Panel> </Panel>
)} )}
{supportsLogs && ( {supportsLogs && (
...@@ -941,6 +941,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> { ...@@ -941,6 +941,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
loading={logsLoading} loading={logsLoading}
position={position} position={position}
onChangeTime={this.onChangeTime} onChangeTime={this.onChangeTime}
onClickLabel={this.onClickLabel}
onStartScanning={this.onStartScanning} onStartScanning={this.onStartScanning}
onStopScanning={this.onStopScanning} onStopScanning={this.onStopScanning}
range={range} range={range}
......
import _ from 'lodash';
import React, { Fragment, PureComponent } from 'react'; import React, { Fragment, PureComponent } from 'react';
import Highlighter from 'react-highlight-words'; import Highlighter from 'react-highlight-words';
import * as rangeUtil from 'app/core/utils/rangeutil'; import * as rangeUtil from 'app/core/utils/rangeutil';
import { RawTimeRange } from 'app/types/series'; import { RawTimeRange } from 'app/types/series';
import { LogsDedupStrategy, LogsModel, dedupLogRows, filterLogLevels, LogLevel } from 'app/core/logs_model'; import {
LogsDedupStrategy,
LogsModel,
dedupLogRows,
filterLogLevels,
LogLevel,
LogsStreamLabels,
LogsMetaKind,
} from 'app/core/logs_model';
import { findHighlightChunksInText } from 'app/core/utils/text'; import { findHighlightChunksInText } from 'app/core/utils/text';
import { Switch } from 'app/core/components/Switch/Switch'; import { Switch } from 'app/core/components/Switch/Switch';
...@@ -23,6 +32,51 @@ const graphOptions = { ...@@ -23,6 +32,51 @@ const graphOptions = {
}, },
}; };
function renderMetaItem(value: any, kind: LogsMetaKind) {
if (kind === LogsMetaKind.LabelsMap) {
return (
<span className="logs-meta-item__value-labels">
<Labels labels={value} />
</span>
);
}
return value;
}
class Label extends PureComponent<{
label: string;
value: string;
onClickLabel?: (label: string, value: string) => void;
}> {
onClickLabel = () => {
const { onClickLabel, label, value } = this.props;
if (onClickLabel) {
onClickLabel(label, value);
}
};
render() {
const { label, value } = this.props;
const tooltip = `${label}: ${value}`;
return (
<span className="logs-label" title={tooltip} onClick={this.onClickLabel}>
{value}
</span>
);
}
}
class Labels extends PureComponent<{
labels: LogsStreamLabels;
onClickLabel?: (label: string, value: string) => void;
}> {
render() {
const { labels, onClickLabel } = this.props;
return Object.keys(labels).map(key => (
<Label key={key} label={key} value={labels[key]} onClickLabel={onClickLabel} />
));
}
}
interface LogsProps { interface LogsProps {
className?: string; className?: string;
data: LogsModel; data: LogsModel;
...@@ -32,6 +86,7 @@ interface LogsProps { ...@@ -32,6 +86,7 @@ interface LogsProps {
scanning?: boolean; scanning?: boolean;
scanRange?: RawTimeRange; scanRange?: RawTimeRange;
onChangeTime?: (range: RawTimeRange) => void; onChangeTime?: (range: RawTimeRange) => void;
onClickLabel?: (label: string, value: string) => void;
onStartScanning?: () => void; onStartScanning?: () => void;
onStopScanning?: () => void; onStopScanning?: () => void;
} }
...@@ -39,7 +94,7 @@ interface LogsProps { ...@@ -39,7 +94,7 @@ interface LogsProps {
interface LogsState { interface LogsState {
dedup: LogsDedupStrategy; dedup: LogsDedupStrategy;
hiddenLogLevels: Set<LogLevel>; hiddenLogLevels: Set<LogLevel>;
showLabels: boolean; showLabels: boolean | null; // Tristate: null means auto
showLocalTime: boolean; showLocalTime: boolean;
showUtc: boolean; showUtc: boolean;
} }
...@@ -48,7 +103,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> { ...@@ -48,7 +103,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
state = { state = {
dedup: LogsDedupStrategy.none, dedup: LogsDedupStrategy.none,
hiddenLogLevels: new Set(), hiddenLogLevels: new Set(),
showLabels: true, showLabels: null,
showLocalTime: true, showLocalTime: true,
showUtc: false, showUtc: false,
}; };
...@@ -99,9 +154,12 @@ export default class Logs extends PureComponent<LogsProps, LogsState> { ...@@ -99,9 +154,12 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
}; };
render() { render() {
const { className = '', data, loading = false, position, range, scanning, scanRange } = this.props; const { className = '', data, loading = false, onClickLabel, position, range, scanning, scanRange } = this.props;
const { dedup, hiddenLogLevels, showLabels, showLocalTime, showUtc } = this.state; const { dedup, hiddenLogLevels, showLocalTime, showUtc } = this.state;
let { showLabels } = this.state;
const hasData = data && data.rows && data.rows.length > 0; const hasData = data && data.rows && data.rows.length > 0;
// Filtering
const filteredData = filterLogLevels(data, hiddenLogLevels); const filteredData = filterLogLevels(data, hiddenLogLevels);
const dedupedData = dedupLogRows(filteredData, dedup); const dedupedData = dedupLogRows(filteredData, dedup);
const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0); const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0);
...@@ -109,9 +167,17 @@ export default class Logs extends PureComponent<LogsProps, LogsState> { ...@@ -109,9 +167,17 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
if (dedup !== LogsDedupStrategy.none) { if (dedup !== LogsDedupStrategy.none) {
meta.push({ meta.push({
label: 'Dedup count', label: 'Dedup count',
value: String(dedupCount), value: dedupCount,
kind: LogsMetaKind.Number,
}); });
} }
// Check for labels
if (showLabels === null && hasData) {
showLabels = data.rows.some(row => _.size(row.uniqueLabels) > 0);
}
// Grid options
const cssColumnSizes = ['3px']; // Log-level indicator line const cssColumnSizes = ['3px']; // Log-level indicator line
if (showUtc) { if (showUtc) {
cssColumnSizes.push('minmax(100px, max-content)'); cssColumnSizes.push('minmax(100px, max-content)');
...@@ -177,7 +243,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> { ...@@ -177,7 +243,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
{meta.map(item => ( {meta.map(item => (
<div className="logs-meta-item" key={item.label}> <div className="logs-meta-item" key={item.label}>
<span className="logs-meta-item__label">{item.label}:</span> <span className="logs-meta-item__label">{item.label}:</span>
<span className="logs-meta-item__value">{item.value}</span> <span className="logs-meta-item__value">{renderMetaItem(item.value, item.kind)}</span>
</div> </div>
))} ))}
</div> </div>
...@@ -201,8 +267,8 @@ export default class Logs extends PureComponent<LogsProps, LogsState> { ...@@ -201,8 +267,8 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
{showUtc && <div title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>{row.timestamp}</div>} {showUtc && <div title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>{row.timestamp}</div>}
{showLocalTime && <div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>} {showLocalTime && <div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>}
{showLabels && ( {showLabels && (
<div className="max-width" title={row.labels}> <div className="logs-row-labels">
{row.labels} <Labels labels={row.uniqueLabels} onClickLabel={onClickLabel} />
</div> </div>
)} )}
<div> <div>
......
...@@ -3,9 +3,11 @@ import _ from 'lodash'; ...@@ -3,9 +3,11 @@ import _ from 'lodash';
import * as dateMath from 'app/core/utils/datemath'; import * as dateMath from 'app/core/utils/datemath';
import { LogsStream, LogsModel, makeSeriesForLogs } from 'app/core/logs_model'; import { LogsStream, LogsModel, makeSeriesForLogs } from 'app/core/logs_model';
import { PluginMeta, DataQuery } from 'app/types'; import { PluginMeta, DataQuery } from 'app/types';
import { addLabelToSelector } from 'app/plugins/datasource/prometheus/add_label_to_query';
import LanguageProvider from './language_provider'; import LanguageProvider from './language_provider';
import { mergeStreamsToLogs } from './result_transformer'; import { mergeStreamsToLogs } from './result_transformer';
import { formatQuery, parseQuery } from './query_utils';
export const DEFAULT_LIMIT = 1000; export const DEFAULT_LIMIT = 1000;
...@@ -16,20 +18,6 @@ const DEFAULT_QUERY_PARAMS = { ...@@ -16,20 +18,6 @@ const DEFAULT_QUERY_PARAMS = {
query: '', query: '',
}; };
const selectorRegexp = /{[^{]*}/g;
export function parseQuery(input: string) {
const match = input.match(selectorRegexp);
let query = '';
let regexp = input;
if (match) {
query = match[0];
regexp = input.replace(selectorRegexp, '').trim();
}
return { query, regexp };
}
function serializeParams(data: any) { function serializeParams(data: any) {
return Object.keys(data) return Object.keys(data)
.map(k => { .map(k => {
...@@ -114,6 +102,21 @@ export default class LoggingDatasource { ...@@ -114,6 +102,21 @@ export default class LoggingDatasource {
}); });
} }
modifyQuery(query: DataQuery, action: any): DataQuery {
const parsed = parseQuery(query.expr || '');
let selector = parsed.query;
switch (action.type) {
case 'ADD_FILTER': {
selector = addLabelToSelector(selector, action.key, action.value);
break;
}
default:
break;
}
const expression = formatQuery(selector, parsed.regexp);
return { ...query, expr: expression };
}
getTime(date, roundUp) { getTime(date, roundUp) {
if (_.isString(date)) { if (_.isString(date)) {
date = dateMath.parse(date, roundUp); date = dateMath.parse(date, roundUp);
......
import { parseQuery } from './datasource'; import { parseQuery } from './query_utils';
describe('parseQuery', () => { describe('parseQuery', () => {
it('returns empty for empty string', () => { it('returns empty for empty string', () => {
......
const selectorRegexp = /{[^{]*}/g;
export function parseQuery(input: string) {
const match = input.match(selectorRegexp);
let query = '';
let regexp = input;
if (match) {
query = match[0];
regexp = input.replace(selectorRegexp, '').trim();
}
return { query, regexp };
}
export function formatQuery(selector: string, search: string): string {
return `${selector || ''} ${search || ''}`.trim();
}
...@@ -41,7 +41,7 @@ describe('parseLabels()', () => { ...@@ -41,7 +41,7 @@ describe('parseLabels()', () => {
}); });
it('returns labels on labels string', () => { it('returns labels on labels string', () => {
expect(parseLabels('{foo="bar", baz="42"}')).toEqual({ foo: '"bar"', baz: '"42"' }); expect(parseLabels('{foo="bar", baz="42"}')).toEqual({ foo: 'bar', baz: '42' });
}); });
}); });
...@@ -52,7 +52,7 @@ describe('formatLabels()', () => { ...@@ -52,7 +52,7 @@ describe('formatLabels()', () => {
}); });
it('returns label string on label set', () => { it('returns label string on label set', () => {
expect(formatLabels({ foo: '"bar"', baz: '"42"' })).toEqual('{baz="42", foo="bar"}'); expect(formatLabels({ foo: 'bar', baz: '42' })).toEqual('{baz="42", foo="bar"}');
}); });
}); });
...@@ -63,14 +63,14 @@ describe('findCommonLabels()', () => { ...@@ -63,14 +63,14 @@ describe('findCommonLabels()', () => {
}); });
it('returns no common labels on differing sets', () => { it('returns no common labels on differing sets', () => {
expect(findCommonLabels([{ foo: '"bar"' }, {}])).toEqual({}); expect(findCommonLabels([{ foo: 'bar' }, {}])).toEqual({});
expect(findCommonLabels([{}, { foo: '"bar"' }])).toEqual({}); expect(findCommonLabels([{}, { foo: 'bar' }])).toEqual({});
expect(findCommonLabels([{ baz: '42' }, { foo: '"bar"' }])).toEqual({}); expect(findCommonLabels([{ baz: '42' }, { foo: 'bar' }])).toEqual({});
expect(findCommonLabels([{ foo: '42', baz: '"bar"' }, { foo: '"bar"' }])).toEqual({}); expect(findCommonLabels([{ foo: '42', baz: 'bar' }, { foo: 'bar' }])).toEqual({});
}); });
it('returns the single labels set as common labels', () => { it('returns the single labels set as common labels', () => {
expect(findCommonLabels([{ foo: '"bar"' }])).toEqual({ foo: '"bar"' }); expect(findCommonLabels([{ foo: 'bar' }])).toEqual({ foo: 'bar' });
}); });
}); });
...@@ -106,10 +106,10 @@ describe('mergeStreamsToLogs()', () => { ...@@ -106,10 +106,10 @@ describe('mergeStreamsToLogs()', () => {
expect(mergeStreamsToLogs([stream1]).rows).toMatchObject([ expect(mergeStreamsToLogs([stream1]).rows).toMatchObject([
{ {
entry: 'WARN boooo', entry: 'WARN boooo',
labels: '{foo="bar"}', labels: { foo: 'bar' },
key: 'EK1970-01-01T00:00:00Z{foo="bar"}', key: 'EK1970-01-01T00:00:00Z{foo="bar"}',
logLevel: 'warning', logLevel: 'warning',
uniqueLabels: '', uniqueLabels: {},
}, },
]); ]);
}); });
...@@ -140,21 +140,21 @@ describe('mergeStreamsToLogs()', () => { ...@@ -140,21 +140,21 @@ describe('mergeStreamsToLogs()', () => {
expect(mergeStreamsToLogs([stream1, stream2]).rows).toMatchObject([ expect(mergeStreamsToLogs([stream1, stream2]).rows).toMatchObject([
{ {
entry: 'INFO 2', entry: 'INFO 2',
labels: '{foo="bar", baz="2"}', labels: { foo: 'bar', baz: '2' },
logLevel: 'info', logLevel: 'info',
uniqueLabels: '{baz="2"}', uniqueLabels: { baz: '2' },
}, },
{ {
entry: 'WARN boooo', entry: 'WARN boooo',
labels: '{foo="bar", baz="1"}', labels: { foo: 'bar', baz: '1' },
logLevel: 'warning', logLevel: 'warning',
uniqueLabels: '{baz="1"}', uniqueLabels: { baz: '1' },
}, },
{ {
entry: 'INFO 1', entry: 'INFO 1',
labels: '{foo="bar", baz="2"}', labels: { foo: 'bar', baz: '2' },
logLevel: 'info', logLevel: 'info',
uniqueLabels: '{baz="2"}', uniqueLabels: { baz: '2' },
}, },
]); ]);
}); });
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
LogsStream, LogsStream,
LogsStreamEntry, LogsStreamEntry,
LogsStreamLabels, LogsStreamLabels,
LogsMetaKind,
} from 'app/core/logs_model'; } from 'app/core/logs_model';
import { DEFAULT_LIMIT } from './datasource'; import { DEFAULT_LIMIT } from './datasource';
...@@ -40,7 +41,7 @@ export function getLogLevel(line: string): LogLevel { ...@@ -40,7 +41,7 @@ export function getLogLevel(line: string): LogLevel {
/** /**
* Regexp to extract Prometheus-style labels * Regexp to extract Prometheus-style labels
*/ */
const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g; const labelRegexp = /\b(\w+)(!?=~?)"([^"\n]*?)"/g;
/** /**
* Returns a map of label keys to value from an input selector string. * Returns a map of label keys to value from an input selector string.
...@@ -104,11 +105,17 @@ export function formatLabels(labels: LogsStreamLabels, defaultValue = ''): strin ...@@ -104,11 +105,17 @@ export function formatLabels(labels: LogsStreamLabels, defaultValue = ''): strin
return defaultValue; return defaultValue;
} }
const labelKeys = Object.keys(labels).sort(); const labelKeys = Object.keys(labels).sort();
const cleanSelector = labelKeys.map(key => `${key}=${labels[key]}`).join(', '); const cleanSelector = labelKeys.map(key => `${key}="${labels[key]}"`).join(', ');
return ['{', cleanSelector, '}'].join(''); return ['{', cleanSelector, '}'].join('');
} }
export function processEntry(entry: LogsStreamEntry, labels: string, uniqueLabels: string, search: string): LogRow { export function processEntry(
entry: LogsStreamEntry,
labels: string,
parsedLabels: LogsStreamLabels,
uniqueLabels: LogsStreamLabels,
search: string
): LogRow {
const { line, timestamp } = entry; const { line, timestamp } = entry;
// Assumes unique-ness, needs nanosec precision for timestamp // Assumes unique-ness, needs nanosec precision for timestamp
const key = `EK${timestamp}${labels}`; const key = `EK${timestamp}${labels}`;
...@@ -120,13 +127,13 @@ export function processEntry(entry: LogsStreamEntry, labels: string, uniqueLabel ...@@ -120,13 +127,13 @@ export function processEntry(entry: LogsStreamEntry, labels: string, uniqueLabel
return { return {
key, key,
labels,
logLevel, logLevel,
timeFromNow, timeFromNow,
timeEpochMs, timeEpochMs,
timeLocal, timeLocal,
uniqueLabels, uniqueLabels,
entry: line, entry: line,
labels: parsedLabels,
searchWords: search ? [search] : [], searchWords: search ? [search] : [],
timestamp: timestamp, timestamp: timestamp,
}; };
...@@ -141,7 +148,7 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_LIMIT) ...@@ -141,7 +148,7 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_LIMIT)
const commonLabels = findCommonLabels(streams.map(model => model.parsedLabels)); const commonLabels = findCommonLabels(streams.map(model => model.parsedLabels));
streams = streams.map(stream => ({ streams = streams.map(stream => ({
...stream, ...stream,
uniqueLabels: formatLabels(findUniqueLabels(stream.parsedLabels, commonLabels)), uniqueLabels: findUniqueLabels(stream.parsedLabels, commonLabels),
})); }));
// Merge stream entries into single list of log rows // Merge stream entries into single list of log rows
...@@ -149,7 +156,9 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_LIMIT) ...@@ -149,7 +156,9 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_LIMIT)
.reduce( .reduce(
(acc: LogRow[], stream: LogsStream) => [ (acc: LogRow[], stream: LogsStream) => [
...acc, ...acc,
...stream.entries.map(entry => processEntry(entry, stream.labels, stream.uniqueLabels, stream.search)), ...stream.entries.map(entry =>
processEntry(entry, stream.labels, stream.parsedLabels, stream.uniqueLabels, stream.search)
),
], ],
[] []
) )
...@@ -162,13 +171,15 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_LIMIT) ...@@ -162,13 +171,15 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_LIMIT)
if (_.size(commonLabels) > 0) { if (_.size(commonLabels) > 0) {
meta.push({ meta.push({
label: 'Common labels', label: 'Common labels',
value: formatLabels(commonLabels), value: commonLabels,
kind: LogsMetaKind.LabelsMap,
}); });
} }
if (limit) { if (limit) {
meta.push({ meta.push({
label: 'Limit', label: 'Limit',
value: `${limit} (${sortedRows.length} returned)`, value: `${limit} (${sortedRows.length} returned)`,
kind: LogsMetaKind.String,
}); });
} }
......
...@@ -49,7 +49,7 @@ export function addLabelToQuery(query: string, key: string, value: string, opera ...@@ -49,7 +49,7 @@ export function addLabelToQuery(query: string, key: string, value: string, opera
const selectorWithLabel = addLabelToSelector(selector, key, value, operator); const selectorWithLabel = addLabelToSelector(selector, key, value, operator);
lastIndex = match.index + match[1].length + 2; lastIndex = match.index + match[1].length + 2;
suffix = query.slice(match.index + match[0].length); suffix = query.slice(match.index + match[0].length);
parts.push(prefix, '{', selectorWithLabel, '}'); parts.push(prefix, selectorWithLabel);
match = selectorRegexp.exec(query); match = selectorRegexp.exec(query);
} }
...@@ -59,7 +59,7 @@ export function addLabelToQuery(query: string, key: string, value: string, opera ...@@ -59,7 +59,7 @@ export function addLabelToQuery(query: string, key: string, value: string, opera
const labelRegexp = /(\w+)\s*(=|!=|=~|!~)\s*("[^"]*")/g; const labelRegexp = /(\w+)\s*(=|!=|=~|!~)\s*("[^"]*")/g;
function addLabelToSelector(selector: string, labelKey: string, labelValue: string, labelOperator?: string) { export function addLabelToSelector(selector: string, labelKey: string, labelValue: string, labelOperator?: string) {
const parsedLabels = []; const parsedLabels = [];
// Split selector into labels // Split selector into labels
...@@ -76,13 +76,15 @@ function addLabelToSelector(selector: string, labelKey: string, labelValue: stri ...@@ -76,13 +76,15 @@ function addLabelToSelector(selector: string, labelKey: string, labelValue: stri
parsedLabels.push({ key: labelKey, operator: operatorForLabelKey, value: `"${labelValue}"` }); parsedLabels.push({ key: labelKey, operator: operatorForLabelKey, value: `"${labelValue}"` });
// Sort labels by key and put them together // Sort labels by key and put them together
return _.chain(parsedLabels) const formatted = _.chain(parsedLabels)
.uniqWith(_.isEqual) .uniqWith(_.isEqual)
.compact() .compact()
.sortBy('key') .sortBy('key')
.map(({ key, operator, value }) => `${key}${operator}${value}`) .map(({ key, operator, value }) => `${key}${operator}${value}`)
.value() .value()
.join(','); .join(',');
return `{${formatted}}`;
} }
function isPositionInsideChars(text: string, position: number, openChar: string, closeChar: string) { function isPositionInsideChars(text: string, position: number, openChar: string, closeChar: string) {
......
import addLabelToQuery from '../add_label_to_query'; import { addLabelToQuery, addLabelToSelector } from '../add_label_to_query';
describe('addLabelToQuery()', () => { describe('addLabelToQuery()', () => {
it('should add label to simple query', () => { it('should add label to simple query', () => {
...@@ -56,3 +56,16 @@ describe('addLabelToQuery()', () => { ...@@ -56,3 +56,16 @@ describe('addLabelToQuery()', () => {
); );
}); });
}); });
describe('addLabelToSelector()', () => {
test('should add a label to an empty selector', () => {
expect(addLabelToSelector('{}', 'foo', 'bar')).toBe('{foo="bar"}');
expect(addLabelToSelector('', 'foo', 'bar')).toBe('{foo="bar"}');
});
test('should add a label to a selector', () => {
expect(addLabelToSelector('{foo="bar"}', 'baz', '42')).toBe('{baz="42",foo="bar"}');
});
test('should add a label to a selector with custom operator', () => {
expect(addLabelToSelector('{}', 'baz', '42', '!=')).toBe('{baz!="42"}');
});
});
...@@ -261,6 +261,8 @@ ...@@ -261,6 +261,8 @@
border-radius: $border-radius; border-radius: $border-radius;
margin: 2*$panel-margin 0; margin: 2*$panel-margin 0;
border: $panel-border; border: $panel-border;
justify-items: flex-start;
align-items: flex-start;
> * { > * {
margin-right: 1em; margin-right: 1em;
...@@ -276,11 +278,11 @@ ...@@ -276,11 +278,11 @@
.logs-meta { .logs-meta {
flex: 1; flex: 1;
color: $text-color-weak; color: $text-color-weak;
padding: 2px 0; // Align first line with controls labels
margin-top: -2px;
} }
.logs-meta-item { .logs-meta-item {
display: inline-block;
margin-right: 1em; margin-right: 1em;
} }
...@@ -294,6 +296,12 @@ ...@@ -294,6 +296,12 @@
font-family: $font-family-monospace; font-family: $font-family-monospace;
} }
.logs-meta-item__value-labels {
// compensate for the labels padding
position: relative;
top: 4px;
}
.logs-row-match-highlight { .logs-row-match-highlight {
// Undoing mark styling // Undoing mark styling
background: inherit; background: inherit;
...@@ -356,6 +364,25 @@ ...@@ -356,6 +364,25 @@
background-color: #1f78c1; background-color: #1f78c1;
margin: 0 1px 1px 0; margin: 0 1px 1px 0;
} }
.logs-label {
display: inline-block;
padding: 0 2px;
background-color: $btn-inverse-bg;
border-radius: $border-radius;
margin-right: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.logs-row-labels {
line-height: 1.2;
.logs-label {
cursor: pointer;
}
}
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment