mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
8830c133d1
commit
c3b67f3a13
@ -35,19 +35,26 @@ export interface LogRow {
|
||||
duplicates?: number;
|
||||
entry: string;
|
||||
key: string; // timestamp + labels
|
||||
labels: string;
|
||||
labels: LogsStreamLabels;
|
||||
logLevel: LogLevel;
|
||||
searchWords?: string[];
|
||||
timestamp: string; // ISO with nanosec precision
|
||||
timeFromNow: string;
|
||||
timeEpochMs: number;
|
||||
timeLocal: string;
|
||||
uniqueLabels?: string;
|
||||
uniqueLabels?: LogsStreamLabels;
|
||||
}
|
||||
|
||||
export enum LogsMetaKind {
|
||||
Number,
|
||||
String,
|
||||
LabelsMap,
|
||||
}
|
||||
|
||||
export interface LogsMetaItem {
|
||||
label: string;
|
||||
value: string;
|
||||
value: string | number | LogsStreamLabels;
|
||||
kind: LogsMetaKind;
|
||||
}
|
||||
|
||||
export interface LogsModel {
|
||||
@ -61,7 +68,7 @@ export interface LogsStream {
|
||||
entries: LogsStreamEntry[];
|
||||
search?: string;
|
||||
parsedLabels?: LogsStreamLabels;
|
||||
uniqueLabels?: string;
|
||||
uniqueLabels?: LogsStreamLabels;
|
||||
}
|
||||
|
||||
export interface LogsStreamEntry {
|
||||
|
@ -429,8 +429,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
);
|
||||
};
|
||||
|
||||
onClickTableCell = (columnKey: string, rowValue: string) => {
|
||||
this.onModifyQueries({ type: 'ADD_FILTER', key: columnKey, value: rowValue });
|
||||
onClickLabel = (key: string, value: string) => {
|
||||
this.onModifyQueries({ type: 'ADD_FILTER', key, value });
|
||||
};
|
||||
|
||||
onModifyQueries = (action, index?: number) => {
|
||||
@ -931,7 +931,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
isOpen={showingTable}
|
||||
onToggle={this.onClickTableButton}
|
||||
>
|
||||
<Table data={tableResult} loading={tableLoading} onClickCell={this.onClickTableCell} />
|
||||
<Table data={tableResult} loading={tableLoading} onClickCell={this.onClickLabel} />
|
||||
</Panel>
|
||||
)}
|
||||
{supportsLogs && (
|
||||
@ -941,6 +941,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
loading={logsLoading}
|
||||
position={position}
|
||||
onChangeTime={this.onChangeTime}
|
||||
onClickLabel={this.onClickLabel}
|
||||
onStartScanning={this.onStartScanning}
|
||||
onStopScanning={this.onStopScanning}
|
||||
range={range}
|
||||
|
@ -1,9 +1,18 @@
|
||||
import _ from 'lodash';
|
||||
import React, { Fragment, PureComponent } from 'react';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
|
||||
import * as rangeUtil from 'app/core/utils/rangeutil';
|
||||
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 { Switch } from 'app/core/components/Switch/Switch';
|
||||
|
||||
@ -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 {
|
||||
className?: string;
|
||||
data: LogsModel;
|
||||
@ -32,6 +86,7 @@ interface LogsProps {
|
||||
scanning?: boolean;
|
||||
scanRange?: RawTimeRange;
|
||||
onChangeTime?: (range: RawTimeRange) => void;
|
||||
onClickLabel?: (label: string, value: string) => void;
|
||||
onStartScanning?: () => void;
|
||||
onStopScanning?: () => void;
|
||||
}
|
||||
@ -39,7 +94,7 @@ interface LogsProps {
|
||||
interface LogsState {
|
||||
dedup: LogsDedupStrategy;
|
||||
hiddenLogLevels: Set<LogLevel>;
|
||||
showLabels: boolean;
|
||||
showLabels: boolean | null; // Tristate: null means auto
|
||||
showLocalTime: boolean;
|
||||
showUtc: boolean;
|
||||
}
|
||||
@ -48,7 +103,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
||||
state = {
|
||||
dedup: LogsDedupStrategy.none,
|
||||
hiddenLogLevels: new Set(),
|
||||
showLabels: true,
|
||||
showLabels: null,
|
||||
showLocalTime: true,
|
||||
showUtc: false,
|
||||
};
|
||||
@ -99,9 +154,12 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className = '', data, loading = false, position, range, scanning, scanRange } = this.props;
|
||||
const { dedup, hiddenLogLevels, showLabels, showLocalTime, showUtc } = this.state;
|
||||
const { className = '', data, loading = false, onClickLabel, position, range, scanning, scanRange } = this.props;
|
||||
const { dedup, hiddenLogLevels, showLocalTime, showUtc } = this.state;
|
||||
let { showLabels } = this.state;
|
||||
const hasData = data && data.rows && data.rows.length > 0;
|
||||
|
||||
// Filtering
|
||||
const filteredData = filterLogLevels(data, hiddenLogLevels);
|
||||
const dedupedData = dedupLogRows(filteredData, dedup);
|
||||
const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0);
|
||||
@ -109,9 +167,17 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
||||
if (dedup !== LogsDedupStrategy.none) {
|
||||
meta.push({
|
||||
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
|
||||
if (showUtc) {
|
||||
cssColumnSizes.push('minmax(100px, max-content)');
|
||||
@ -177,7 +243,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
||||
{meta.map(item => (
|
||||
<div className="logs-meta-item" key={item.label}>
|
||||
<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>
|
||||
@ -201,8 +267,8 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
||||
{showUtc && <div title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>{row.timestamp}</div>}
|
||||
{showLocalTime && <div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>}
|
||||
{showLabels && (
|
||||
<div className="max-width" title={row.labels}>
|
||||
{row.labels}
|
||||
<div className="logs-row-labels">
|
||||
<Labels labels={row.uniqueLabels} onClickLabel={onClickLabel} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
|
@ -3,9 +3,11 @@ import _ from 'lodash';
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import { LogsStream, LogsModel, makeSeriesForLogs } from 'app/core/logs_model';
|
||||
import { PluginMeta, DataQuery } from 'app/types';
|
||||
import { addLabelToSelector } from 'app/plugins/datasource/prometheus/add_label_to_query';
|
||||
|
||||
import LanguageProvider from './language_provider';
|
||||
import { mergeStreamsToLogs } from './result_transformer';
|
||||
import { formatQuery, parseQuery } from './query_utils';
|
||||
|
||||
export const DEFAULT_LIMIT = 1000;
|
||||
|
||||
@ -16,20 +18,6 @@ const DEFAULT_QUERY_PARAMS = {
|
||||
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) {
|
||||
return Object.keys(data)
|
||||
.map(k => {
|
||||
@ -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) {
|
||||
if (_.isString(date)) {
|
||||
date = dateMath.parse(date, roundUp);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { parseQuery } from './datasource';
|
||||
import { parseQuery } from './query_utils';
|
||||
|
||||
describe('parseQuery', () => {
|
||||
it('returns empty for empty string', () => {
|
17
public/app/plugins/datasource/logging/query_utils.ts
Normal file
17
public/app/plugins/datasource/logging/query_utils.ts
Normal file
@ -0,0 +1,17 @@
|
||||
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()', () => {
|
||||
});
|
||||
|
||||
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()', () => {
|
||||
});
|
||||
|
||||
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()', () => {
|
||||
});
|
||||
|
||||
it('returns no common labels on differing sets', () => {
|
||||
expect(findCommonLabels([{ foo: '"bar"' }, {}])).toEqual({});
|
||||
expect(findCommonLabels([{}, { foo: '"bar"' }])).toEqual({});
|
||||
expect(findCommonLabels([{ baz: '42' }, { foo: '"bar"' }])).toEqual({});
|
||||
expect(findCommonLabels([{ foo: '42', baz: '"bar"' }, { foo: '"bar"' }])).toEqual({});
|
||||
expect(findCommonLabels([{ foo: 'bar' }, {}])).toEqual({});
|
||||
expect(findCommonLabels([{}, { foo: 'bar' }])).toEqual({});
|
||||
expect(findCommonLabels([{ baz: '42' }, { foo: 'bar' }])).toEqual({});
|
||||
expect(findCommonLabels([{ foo: '42', baz: 'bar' }, { foo: 'bar' }])).toEqual({});
|
||||
});
|
||||
|
||||
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()', () => {
|
||||
expect(mergeStreamsToLogs([stream1]).rows).toMatchObject([
|
||||
{
|
||||
entry: 'WARN boooo',
|
||||
labels: '{foo="bar"}',
|
||||
labels: { foo: 'bar' },
|
||||
key: 'EK1970-01-01T00:00:00Z{foo="bar"}',
|
||||
logLevel: 'warning',
|
||||
uniqueLabels: '',
|
||||
uniqueLabels: {},
|
||||
},
|
||||
]);
|
||||
});
|
||||
@ -140,21 +140,21 @@ describe('mergeStreamsToLogs()', () => {
|
||||
expect(mergeStreamsToLogs([stream1, stream2]).rows).toMatchObject([
|
||||
{
|
||||
entry: 'INFO 2',
|
||||
labels: '{foo="bar", baz="2"}',
|
||||
labels: { foo: 'bar', baz: '2' },
|
||||
logLevel: 'info',
|
||||
uniqueLabels: '{baz="2"}',
|
||||
uniqueLabels: { baz: '2' },
|
||||
},
|
||||
{
|
||||
entry: 'WARN boooo',
|
||||
labels: '{foo="bar", baz="1"}',
|
||||
labels: { foo: 'bar', baz: '1' },
|
||||
logLevel: 'warning',
|
||||
uniqueLabels: '{baz="1"}',
|
||||
uniqueLabels: { baz: '1' },
|
||||
},
|
||||
{
|
||||
entry: 'INFO 1',
|
||||
labels: '{foo="bar", baz="2"}',
|
||||
labels: { foo: 'bar', baz: '2' },
|
||||
logLevel: 'info',
|
||||
uniqueLabels: '{baz="2"}',
|
||||
uniqueLabels: { baz: '2' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
LogsStream,
|
||||
LogsStreamEntry,
|
||||
LogsStreamLabels,
|
||||
LogsMetaKind,
|
||||
} from 'app/core/logs_model';
|
||||
import { DEFAULT_LIMIT } from './datasource';
|
||||
|
||||
@ -40,7 +41,7 @@ export function getLogLevel(line: string): LogLevel {
|
||||
/**
|
||||
* 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.
|
||||
@ -104,11 +105,17 @@ export function formatLabels(labels: LogsStreamLabels, defaultValue = ''): strin
|
||||
return defaultValue;
|
||||
}
|
||||
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('');
|
||||
}
|
||||
|
||||
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;
|
||||
// Assumes unique-ness, needs nanosec precision for timestamp
|
||||
const key = `EK${timestamp}${labels}`;
|
||||
@ -120,13 +127,13 @@ export function processEntry(entry: LogsStreamEntry, labels: string, uniqueLabel
|
||||
|
||||
return {
|
||||
key,
|
||||
labels,
|
||||
logLevel,
|
||||
timeFromNow,
|
||||
timeEpochMs,
|
||||
timeLocal,
|
||||
uniqueLabels,
|
||||
entry: line,
|
||||
labels: parsedLabels,
|
||||
searchWords: search ? [search] : [],
|
||||
timestamp: timestamp,
|
||||
};
|
||||
@ -141,7 +148,7 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_LIMIT)
|
||||
const commonLabels = findCommonLabels(streams.map(model => model.parsedLabels));
|
||||
streams = streams.map(stream => ({
|
||||
...stream,
|
||||
uniqueLabels: formatLabels(findUniqueLabels(stream.parsedLabels, commonLabels)),
|
||||
uniqueLabels: findUniqueLabels(stream.parsedLabels, commonLabels),
|
||||
}));
|
||||
|
||||
// Merge stream entries into single list of log rows
|
||||
@ -149,7 +156,9 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_LIMIT)
|
||||
.reduce(
|
||||
(acc: LogRow[], stream: LogsStream) => [
|
||||
...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)
|
||||
if (_.size(commonLabels) > 0) {
|
||||
meta.push({
|
||||
label: 'Common labels',
|
||||
value: formatLabels(commonLabels),
|
||||
value: commonLabels,
|
||||
kind: LogsMetaKind.LabelsMap,
|
||||
});
|
||||
}
|
||||
if (limit) {
|
||||
meta.push({
|
||||
label: 'Limit',
|
||||
value: `${limit} (${sortedRows.length} returned)`,
|
||||
kind: LogsMetaKind.String,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@ export function addLabelToQuery(query: string, key: string, value: string, opera
|
||||
const selectorWithLabel = addLabelToSelector(selector, key, value, operator);
|
||||
lastIndex = match.index + match[1].length + 2;
|
||||
suffix = query.slice(match.index + match[0].length);
|
||||
parts.push(prefix, '{', selectorWithLabel, '}');
|
||||
parts.push(prefix, selectorWithLabel);
|
||||
match = selectorRegexp.exec(query);
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ export function addLabelToQuery(query: string, key: string, value: string, opera
|
||||
|
||||
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 = [];
|
||||
|
||||
// Split selector into labels
|
||||
@ -76,13 +76,15 @@ function addLabelToSelector(selector: string, labelKey: string, labelValue: stri
|
||||
parsedLabels.push({ key: labelKey, operator: operatorForLabelKey, value: `"${labelValue}"` });
|
||||
|
||||
// Sort labels by key and put them together
|
||||
return _.chain(parsedLabels)
|
||||
const formatted = _.chain(parsedLabels)
|
||||
.uniqWith(_.isEqual)
|
||||
.compact()
|
||||
.sortBy('key')
|
||||
.map(({ key, operator, value }) => `${key}${operator}${value}`)
|
||||
.value()
|
||||
.join(',');
|
||||
|
||||
return `{${formatted}}`;
|
||||
}
|
||||
|
||||
function isPositionInsideChars(text: string, position: number, openChar: string, closeChar: string) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import addLabelToQuery from '../add_label_to_query';
|
||||
import { addLabelToQuery, addLabelToSelector } from '../add_label_to_query';
|
||||
|
||||
describe('addLabelToQuery()', () => {
|
||||
it('should add label to simple query', () => {
|
||||
@ -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 @@
|
||||
border-radius: $border-radius;
|
||||
margin: 2*$panel-margin 0;
|
||||
border: $panel-border;
|
||||
justify-items: flex-start;
|
||||
align-items: flex-start;
|
||||
|
||||
> * {
|
||||
margin-right: 1em;
|
||||
@ -276,11 +278,11 @@
|
||||
.logs-meta {
|
||||
flex: 1;
|
||||
color: $text-color-weak;
|
||||
padding: 2px 0;
|
||||
// Align first line with controls labels
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.logs-meta-item {
|
||||
display: inline-block;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
@ -294,6 +296,12 @@
|
||||
font-family: $font-family-monospace;
|
||||
}
|
||||
|
||||
.logs-meta-item__value-labels {
|
||||
// compensate for the labels padding
|
||||
position: relative;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.logs-row-match-highlight {
|
||||
// Undoing mark styling
|
||||
background: inherit;
|
||||
@ -356,6 +364,25 @@
|
||||
background-color: #1f78c1;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user