Merge remote-tracking branch 'origin/develop' into gauge-value-mappings

This commit is contained in:
Peter Holmberg 2018-12-07 15:10:32 +01:00
commit 4f39df900c
64 changed files with 1411 additions and 420 deletions

View File

@ -1,5 +1,5 @@
// uses code from https://github.com/antonholmquist/jason/blob/master/jason.go
// MIT Licence
// MIT License
package dynmap

View File

@ -1,5 +1,5 @@
// uses code from https://github.com/antonholmquist/jason/blob/master/jason.go
// MIT Licence
// MIT License
package dynmap

View File

@ -187,7 +187,7 @@ func TestAccountDataAccess(t *testing.T) {
err := DeleteOrg(&m.DeleteOrgCommand{Id: ac2.OrgId})
So(err, ShouldBeNil)
// remove frome ac2 from ac1 org
// remove ac2 user from ac1 org
remCmd := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac2.Id, ShouldDeleteOrphanedUser: true}
err = RemoveOrgUser(&remCmd)
So(err, ShouldBeNil)

View File

@ -99,14 +99,14 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
return inTransaction(func(sess *DBSession) error {
//Check if quota is already defined in the DB
quota := m.Quota{
Target: cmd.Target,
OrgId: cmd.OrgId,
Updated: time.Now(),
Target: cmd.Target,
OrgId: cmd.OrgId,
}
has, err := sess.Get(&quota)
if err != nil {
return err
}
quota.Updated = time.Now()
quota.Limit = cmd.Limit
if !has {
quota.Created = time.Now()
@ -201,14 +201,14 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
return inTransaction(func(sess *DBSession) error {
//Check if quota is already defined in the DB
quota := m.Quota{
Target: cmd.Target,
UserId: cmd.UserId,
Updated: time.Now(),
Target: cmd.Target,
UserId: cmd.UserId,
}
has, err := sess.Get(&quota)
if err != nil {
return err
}
quota.Updated = time.Now()
quota.Limit = cmd.Limit
if !has {
quota.Created = time.Now()

View File

@ -2,6 +2,7 @@ package sqlstore
import (
"testing"
"time"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
@ -168,5 +169,69 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
So(query.Result.Limit, ShouldEqual, 5)
So(query.Result.Used, ShouldEqual, 1)
})
// related: https://github.com/grafana/grafana/issues/14342
Convey("Should org quota updating is successful even if it called multiple time", func() {
orgCmd := m.UpdateOrgQuotaCmd{
OrgId: orgId,
Target: "org_user",
Limit: 5,
}
err := UpdateOrgQuota(&orgCmd)
So(err, ShouldBeNil)
query := m.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1}
err = GetOrgQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Limit, ShouldEqual, 5)
// XXX: resolution of `Updated` column is 1sec, so this makes delay
time.Sleep(1 * time.Second)
orgCmd = m.UpdateOrgQuotaCmd{
OrgId: orgId,
Target: "org_user",
Limit: 10,
}
err = UpdateOrgQuota(&orgCmd)
So(err, ShouldBeNil)
query = m.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1}
err = GetOrgQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Limit, ShouldEqual, 10)
})
// related: https://github.com/grafana/grafana/issues/14342
Convey("Should user quota updating is successful even if it called multiple time", func() {
userQuotaCmd := m.UpdateUserQuotaCmd{
UserId: userId,
Target: "org_user",
Limit: 5,
}
err := UpdateUserQuota(&userQuotaCmd)
So(err, ShouldBeNil)
query := m.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1}
err = GetUserQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Limit, ShouldEqual, 5)
// XXX: resolution of `Updated` column is 1sec, so this makes delay
time.Sleep(1 * time.Second)
userQuotaCmd = m.UpdateUserQuotaCmd{
UserId: userId,
Target: "org_user",
Limit: 10,
}
err = UpdateUserQuota(&userQuotaCmd)
So(err, ShouldBeNil)
query = m.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1}
err = GetUserQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Limit, ShouldEqual, 10)
})
})
}

View File

@ -541,7 +541,7 @@ func getErrorFromElasticResponse(response *es.SearchResponse) *tsdb.QueryResult
} else if reason != "" {
result.ErrorString = reason
} else {
result.ErrorString = "Unkown elasticsearch error response"
result.ErrorString = "Unknown elasticsearch error response"
}
return result

View File

@ -32,6 +32,7 @@ func init() {
renders["median"] = QueryDefinition{Renderer: functionRenderer}
renders["sum"] = QueryDefinition{Renderer: functionRenderer}
renders["mode"] = QueryDefinition{Renderer: functionRenderer}
renders["cumulative_sum"] = QueryDefinition{Renderer: functionRenderer}
renders["holt_winters"] = QueryDefinition{
Renderer: functionRenderer,

View File

@ -23,6 +23,7 @@ func TestInfluxdbQueryPart(t *testing.T) {
{mode: "alias", params: []string{"test"}, input: "mean(value)", expected: `mean(value) AS "test"`},
{mode: "count", params: []string{}, input: "distinct(value)", expected: `count(distinct(value))`},
{mode: "mode", params: []string{}, input: "value", expected: `mode(value)`},
{mode: "cumulative_sum", params: []string{}, input: "mean(value)", expected: `cumulative_sum(mean(value))`},
}
queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")}

View File

@ -84,7 +84,7 @@ func (e *OpenTsdbExecutor) createRequest(dsInfo *models.DataSource, data OpenTsd
postData, err := json.Marshal(data)
if err != nil {
plog.Info("Failed marshalling data", "error", err)
plog.Info("Failed marshaling data", "error", err)
return nil, fmt.Errorf("Failed to create request. error: %v", err)
}

View File

@ -84,7 +84,7 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> {
render() {
const { onCancel } = this.props;
const newItem = this.state;
const pickerClassName = 'width-20';
const pickerClassName = 'min-width-20';
const isValid = this.isValid();
return (
<div className="gf-form-inline cta-form">

View File

@ -40,7 +40,7 @@ export class UserPicker extends Component<Props, State> {
.then(result => {
return result.map(user => ({
id: user.userId,
label: `${user.login} - ${user.email}`,
label: user.login === user.email ? user.login : `${user.login} - ${user.email}`,
avatarUrl: user.avatarUrl,
login: user.login,
}));

View File

@ -0,0 +1,68 @@
import React, { SFC, ReactNode, PureComponent, ReactElement } from 'react';
interface ToggleButtonGroupProps {
onChange: (value) => void;
value?: any;
label?: string;
render: (props) => void;
}
export default class ToggleButtonGroup extends PureComponent<ToggleButtonGroupProps> {
getValues() {
const { children } = this.props;
return React.Children.toArray(children).map((c: ReactElement<any>) => c.props.value);
}
smallChildren() {
const { children } = this.props;
return React.Children.toArray(children).every((c: ReactElement<any>) => c.props.className.includes('small'));
}
handleToggle(toggleValue) {
const { value, onChange } = this.props;
if (value && value === toggleValue) {
return;
}
onChange(toggleValue);
}
render() {
const { value, label } = this.props;
const values = this.getValues();
const selectedValue = value || values[0];
const labelClassName = `gf-form-label ${this.smallChildren() ? 'small' : ''}`;
return (
<div className="gf-form">
<div className="toggle-button-group">
{label && <label className={labelClassName}>{label}</label>}
{this.props.render({ selectedValue, onChange: this.handleToggle.bind(this) })}
</div>
</div>
);
}
}
interface ToggleButtonProps {
onChange?: (value) => void;
selected?: boolean;
value: any;
className?: string;
children: ReactNode;
}
export const ToggleButton: SFC<ToggleButtonProps> = ({ children, selected, className = '', value, onChange }) => {
const handleChange = event => {
event.stopPropagation();
if (onChange) {
onChange(value);
}
};
const btnClassName = `btn ${className} ${selected ? 'active' : ''}`;
return (
<button className={btnClassName} onClick={handleChange}>
<span>{children}</span>
</button>
);
};

View File

@ -54,7 +54,11 @@ export class Settings {
}
}
const bootData = (window as any).grafanaBootData || { settings: {} };
const bootData = (window as any).grafanaBootData || {
settings: {},
user: {},
};
const options = bootData.settings;
options.bootData = bootData;

View File

@ -1,6 +1,6 @@
import _ from 'lodash';
import { TimeSeries } from 'app/core/core';
import colors from 'app/core/utils/colors';
import colors, { getThemeColor } from 'app/core/utils/colors';
export enum LogLevel {
crit = 'critical',
@ -22,7 +22,7 @@ export const LogLevelColor = {
[LogLevel.info]: colors[0],
[LogLevel.debug]: colors[5],
[LogLevel.trace]: colors[2],
[LogLevel.unkown]: '#ddd',
[LogLevel.unkown]: getThemeColor('#8e8e8e', '#dde4ed'),
};
export interface LogSearchMatch {
@ -95,6 +95,57 @@ export enum LogsDedupStrategy {
signature = 'signature',
}
export interface LogsParser {
/**
* Value-agnostic matcher for a field label.
* Used to filter rows, and first capture group contains the value.
*/
buildMatcher: (label: string) => RegExp;
/**
* Regex to find a field in the log line.
* First capture group contains the label value, second capture group the value.
*/
fieldRegex: RegExp;
/**
* Function to verify if this is a valid parser for the given line.
* The parser accepts the line unless it returns undefined.
*/
test: (line: string) => any;
}
export const LogsParsers: { [name: string]: LogsParser } = {
JSON: {
buildMatcher: label => new RegExp(`(?:{|,)\\s*"${label}"\\s*:\\s*"([^"]*)"`),
fieldRegex: /"(\w+)"\s*:\s*"([^"]*)"/,
test: line => {
try {
return JSON.parse(line);
} catch (error) {}
},
},
logfmt: {
buildMatcher: label => new RegExp(`(?:^|\\s)${label}=("[^"]*"|\\S+)`),
fieldRegex: /(?:^|\s)(\w+)=("[^"]*"|\S+)/,
test: line => LogsParsers.logfmt.fieldRegex.test(line),
},
};
export function calculateFieldStats(rows: LogRow[], extractor: RegExp): LogsLabelStat[] {
// Consider only rows that satisfy the matcher
const rowsWithField = rows.filter(row => extractor.test(row.entry));
const rowCount = rowsWithField.length;
// Get field value counts for eligible rows
const countsByValue = _.countBy(rowsWithField, row => (row as LogRow).entry.match(extractor)[1]);
const sortedCounts = _.chain(countsByValue)
.map((count, value) => ({ count, value, proportion: count / rowCount }))
.sortBy('count')
.reverse()
.value();
return sortedCounts;
}
export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabelStat[] {
// Consider only rows that have the given label
const rowsWithLabel = rows.filter(row => row.labels[label] !== undefined);
@ -151,6 +202,19 @@ export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): Logs
};
}
export function getParser(line: string): LogsParser {
let parser;
try {
if (LogsParsers.JSON.test(line)) {
parser = LogsParsers.JSON;
}
} catch (error) {}
if (!parser && LogsParsers.logfmt.test(line)) {
parser = LogsParsers.logfmt;
}
return parser;
}
export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>): LogsModel {
if (hiddenLogLevels.size === 0) {
return logs;
@ -170,16 +234,25 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>)
}
export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSeries[] {
// currently interval is rangeMs / resolution, which is too low for showing series as bars.
// need at least 10px per bucket, so we multiply interval by 10. Should be solved higher up the chain
// when executing queries & interval calculated and not here but this is a temporary fix.
// intervalMs = intervalMs * 10;
// Graph time series by log level
const seriesByLevel = {};
rows.forEach(row => {
const bucketSize = intervalMs * 10;
for (const row of rows) {
if (!seriesByLevel[row.logLevel]) {
seriesByLevel[row.logLevel] = { lastTs: null, datapoints: [], alias: row.logLevel };
}
const levelSeries = seriesByLevel[row.logLevel];
// Bucket to nearest minute
const time = Math.round(row.timeEpochMs / intervalMs / 10) * intervalMs * 10;
const time = Math.round(row.timeEpochMs / bucketSize) * bucketSize;
// Entry for time
if (time === levelSeries.lastTs) {
levelSeries.datapoints[levelSeries.datapoints.length - 1][0]++;
@ -187,7 +260,7 @@ export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSerie
levelSeries.datapoints.push([1, time]);
levelSeries.lastTs = time;
}
});
}
return Object.keys(seriesByLevel).reduce((acc, level) => {
if (seriesByLevel[level]) {

View File

@ -1,4 +1,12 @@
import { calculateLogsLabelStats, dedupLogRows, LogsDedupStrategy, LogsModel } from '../logs_model';
import {
calculateFieldStats,
calculateLogsLabelStats,
dedupLogRows,
getParser,
LogsDedupStrategy,
LogsModel,
LogsParsers,
} from '../logs_model';
describe('dedupLogRows()', () => {
test('should return rows as is when dedup is set to none', () => {
@ -107,6 +115,50 @@ describe('dedupLogRows()', () => {
});
});
describe('calculateFieldStats()', () => {
test('should return no stats for empty rows', () => {
expect(calculateFieldStats([], /foo=(.*)/)).toEqual([]);
});
test('should return no stats if extractor does not match', () => {
const rows = [
{
entry: 'foo=bar',
},
];
expect(calculateFieldStats(rows as any, /baz=(.*)/)).toEqual([]);
});
test('should return stats for found field', () => {
const rows = [
{
entry: 'foo="42 + 1"',
},
{
entry: 'foo=503 baz=foo',
},
{
entry: 'foo="42 + 1"',
},
{
entry: 't=2018-12-05T07:44:59+0000 foo=503',
},
];
expect(calculateFieldStats(rows as any, /foo=("[^"]*"|\S+)/)).toMatchObject([
{
value: '"42 + 1"',
count: 2,
},
{
value: '503',
count: 2,
},
]);
});
});
describe('calculateLogsLabelStats()', () => {
test('should return no stats for empty rows', () => {
expect(calculateLogsLabelStats([], '')).toEqual([]);
@ -159,3 +211,70 @@ describe('calculateLogsLabelStats()', () => {
]);
});
});
describe('getParser()', () => {
test('should return no parser on empty line', () => {
expect(getParser('')).toBeUndefined();
});
test('should return no parser on unknown line pattern', () => {
expect(getParser('To Be or not to be')).toBeUndefined();
});
test('should return logfmt parser on key value patterns', () => {
expect(getParser('foo=bar baz="41 + 1')).toEqual(LogsParsers.logfmt);
});
test('should return JSON parser on JSON log lines', () => {
// TODO implement other JSON value types than string
expect(getParser('{"foo": "bar", "baz": "41 + 1"}')).toEqual(LogsParsers.JSON);
});
});
describe('LogsParsers', () => {
describe('logfmt', () => {
const parser = LogsParsers.logfmt;
test('should detect format', () => {
expect(parser.test('foo')).toBeFalsy();
expect(parser.test('foo=bar')).toBeTruthy();
});
test('should have a valid fieldRegex', () => {
const match = 'foo=bar'.match(parser.fieldRegex);
expect(match).toBeDefined();
expect(match[1]).toBe('foo');
expect(match[2]).toBe('bar');
});
test('should build a valid value matcher', () => {
const matcher = parser.buildMatcher('foo');
const match = 'foo=bar'.match(matcher);
expect(match).toBeDefined();
expect(match[1]).toBe('bar');
});
});
describe('JSON', () => {
const parser = LogsParsers.JSON;
test('should detect format', () => {
expect(parser.test('foo')).toBeFalsy();
expect(parser.test('{"foo":"bar"}')).toBeTruthy();
});
test('should have a valid fieldRegex', () => {
const match = '{"foo":"bar"}'.match(parser.fieldRegex);
expect(match).toBeDefined();
expect(match[1]).toBe('foo');
expect(match[2]).toBe('bar');
});
test('should build a valid value matcher', () => {
const matcher = parser.buildMatcher('foo');
const match = '{"foo":"bar"}'.match(matcher);
expect(match).toBeDefined();
expect(match[1]).toBe('bar');
});
});
});

View File

@ -1,5 +1,6 @@
import _ from 'lodash';
import tinycolor from 'tinycolor2';
import config from 'app/core/config';
export const PALETTE_ROWS = 4;
export const PALETTE_COLUMNS = 14;
@ -90,5 +91,9 @@ export function hslToHex(color) {
return tinycolor(color).toHexString();
}
export function getThemeColor(dark: string, light: string): string {
return config.bootData.user.lightTheme ? light : dark;
}
export let sortedColors = sortColorsByHue(colors);
export default colors;

View File

@ -1,15 +1,15 @@
import _ from 'lodash';
import { renderUrl } from 'app/core/utils/url';
import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore';
import { DataQuery, RawTimeRange } from 'app/types/series';
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
import kbn from 'app/core/utils/kbn';
import colors from 'app/core/utils/colors';
import TimeSeries from 'app/core/time_series2';
import { parse as parseDate } from 'app/core/utils/datemath';
import store from 'app/core/store';
import colors from 'app/core/utils/colors';
import { parse as parseDate } from 'app/core/utils/datemath';
import TimeSeries from 'app/core/time_series2';
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore';
import { DataQuery, RawTimeRange, IntervalValues, DataSourceApi } from 'app/types/series';
export const DEFAULT_RANGE = {
from: 'now-6h',
@ -170,18 +170,16 @@ export function calculateResultsFromQueryTransactions(
};
}
export function getIntervals(
range: RawTimeRange,
datasource,
resolution: number
): { interval: string; intervalMs: number } {
export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, resolution: number): IntervalValues {
if (!datasource || !resolution) {
return { interval: '1s', intervalMs: 1000 };
}
const absoluteRange: RawTimeRange = {
from: parseDate(range.from, false),
to: parseDate(range.to, true),
};
return kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
}

View File

@ -223,6 +223,8 @@ export class DashboardModel {
}
panelInitialized(panel: PanelModel) {
panel.initialized();
if (!this.otherPanelInFullscreen(panel)) {
panel.refresh();
}

View File

@ -44,7 +44,7 @@ export class GeneralTab extends PureComponent<Props> {
render() {
return (
<EditorTabBody heading="Basic Panel Options" toolbarItems={[]}>
<EditorTabBody heading="Panel Options" toolbarItems={[]}>
<div ref={element => (this.element = element)} />
</EditorTabBody>
);

View File

@ -77,7 +77,7 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
const tabs = [
{ id: 'queries', text: 'Queries' },
{ id: 'visualization', text: 'Visualization' },
{ id: 'advanced', text: 'Advanced' },
{ id: 'advanced', text: 'Panel Options' },
];
if (config.alertingEnabled && plugin.id === 'graph') {

View File

@ -258,8 +258,8 @@ export class QueriesTab extends PureComponent<Props, State> {
};
const options = {
title: '',
icon: 'fa fa-cog',
title: 'Time Range',
icon: '',
disabled: false,
render: this.renderOptions,
};

View File

@ -74,6 +74,11 @@ export class DashNavCtrl {
}
showSearch() {
if (this.dashboard.meta.fullscreen) {
this.close();
return;
}
appEvents.emit('show-dash-search');
}

View File

@ -189,7 +189,7 @@ export class PanelModel {
}
}
panelInitialized() {
initialized() {
this.events.emit('panel-initialized');
}

View File

@ -666,6 +666,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
...results,
queryTransactions: nextQueryTransactions,
showingStartPage: false,
graphInterval: queryOptions.intervalMs,
};
});
@ -747,7 +748,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
console.error(response);
let error: string | JSX.Element = response;
let error: string | JSX.Element;
if (response.data) {
if (typeof response.data === 'string') {
error = response.data;
@ -764,6 +765,12 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
} else {
throw new Error('Could not handle error response');
}
} else if (response.message) {
error = response.message;
} else if (typeof response === 'string') {
error = response;
} else {
error = 'Unknown error during query transaction. Please check JS console logs.';
}
this.setState(state => {

View File

@ -24,7 +24,7 @@ function StatsRow({ active, count, proportion, value }: LogsLabelStat) {
}
const STATS_ROW_LIMIT = 5;
class Stats extends PureComponent<{
export class Stats extends PureComponent<{
stats: LogsLabelStat[];
label: string;
value: string;
@ -48,15 +48,21 @@ class Stats extends PureComponent<{
const otherProportion = otherCount / total;
return (
<>
<div className="logs-stats__info">
{label}: {total} of {rowCount} rows have that label
<span className="logs-stats__icon fa fa-window-close" onClick={onClickClose} />
<div className="logs-stats">
<div className="logs-stats__header">
<span className="logs-stats__title">
{label}: {total} of {rowCount} rows have that label
</span>
<span className="logs-stats__close fa fa-remove" onClick={onClickClose} />
</div>
{topRows.map(stat => <StatsRow key={stat.value} {...stat} active={stat.value === value} />)}
{insertActiveRow && <StatsRow key={activeRow.value} {...activeRow} active />}
{otherCount > 0 && <StatsRow key="__OTHERS__" count={otherCount} value="Other" proportion={otherProportion} />}
</>
<div className="logs-stats__body">
{topRows.map(stat => <StatsRow key={stat.value} {...stat} active={stat.value === value} />)}
{insertActiveRow && activeRow && <StatsRow key={activeRow.value} {...activeRow} active />}
{otherCount > 0 && (
<StatsRow key="__OTHERS__" count={otherCount} value="Other" proportion={otherProportion} />
)}
</div>
</div>
);
}
}

View File

@ -10,20 +10,26 @@ import {
LogsModel,
dedupLogRows,
filterLogLevels,
getParser,
LogLevel,
LogsMetaKind,
LogsLabelStat,
LogsParser,
LogRow,
calculateFieldStats,
} from 'app/core/logs_model';
import { findHighlightChunksInText } from 'app/core/utils/text';
import { Switch } from 'app/core/components/Switch/Switch';
import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
import Graph from './Graph';
import LogLabels from './LogLabels';
import LogLabels, { Stats } from './LogLabels';
const PREVIEW_LIMIT = 100;
const graphOptions = {
series: {
stack: true,
bars: {
show: true,
lineWidth: 5,
@ -36,6 +42,19 @@ const graphOptions = {
},
};
/**
* Renders a highlighted field.
* When hovering, a stats icon is shown.
*/
const FieldHighlight = onClick => props => {
return (
<span className={props.className} style={props.style}>
{props.children}
<span className="logs-row__field-highlight--icon fa fa-signal" onClick={() => onClick(props.children)} />
</span>
);
};
interface RowProps {
allRows: LogRow[];
highlighterExpressions?: string[];
@ -47,63 +66,177 @@ interface RowProps {
onClickLabel?: (label: string, value: string) => void;
}
function Row({
allRows,
highlighterExpressions,
onClickLabel,
row,
showDuplicates,
showLabels,
showLocalTime,
showUtc,
}: RowProps) {
const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
const highlights = previewHighlights ? highlighterExpressions : row.searchWords;
const needsHighlighter = highlights && highlights.length > 0;
const highlightClassName = classnames('logs-row-match-highlight', {
'logs-row-match-highlight--preview': previewHighlights,
});
return (
<>
{showDuplicates && (
<div className="logs-row-duplicates">{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}</div>
)}
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''} />
{showUtc && (
<div className="logs-row-time" title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
{row.timestamp}
</div>
)}
{showLocalTime && (
<div className="logs-row-time" title={`${row.timestamp} (${row.timeFromNow})`}>
{row.timeLocal}
</div>
)}
{showLabels && (
<div className="logs-row-labels">
<LogLabels allRows={allRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
</div>
)}
<div className="logs-row-message">
{needsHighlighter ? (
<Highlighter
textToHighlight={row.entry}
searchWords={highlights}
findChunks={findHighlightChunksInText}
highlightClassName={highlightClassName}
/>
) : (
row.entry
interface RowState {
fieldCount: number;
fieldLabel: string;
fieldStats: LogsLabelStat[];
fieldValue: string;
parsed: boolean;
parser: LogsParser;
parsedFieldHighlights: string[];
showFieldStats: boolean;
}
/**
* Renders a log line.
*
* When user hovers over it for a certain time, it lazily parses the log line.
* Once a parser is found, it will determine fields, that will be highlighted.
* When the user requests stats for a field, they will be calculated and rendered below the row.
*/
class Row extends PureComponent<RowProps, RowState> {
mouseMessageTimer: NodeJS.Timer;
state = {
fieldCount: 0,
fieldLabel: null,
fieldStats: null,
fieldValue: null,
parsed: false,
parser: null,
parsedFieldHighlights: [],
showFieldStats: false,
};
componentWillUnmount() {
clearTimeout(this.mouseMessageTimer);
}
onClickClose = () => {
this.setState({ showFieldStats: false });
};
onClickHighlight = (fieldText: string) => {
const { allRows } = this.props;
const { parser } = this.state;
const fieldMatch = fieldText.match(parser.fieldRegex);
if (fieldMatch) {
// Build value-agnostic row matcher based on the field label
const fieldLabel = fieldMatch[1];
const fieldValue = fieldMatch[2];
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 = () => {
// Don't parse right away, user might move along
this.mouseMessageTimer = setTimeout(this.parseMessage, 500);
};
onMouseOutMessage = () => {
clearTimeout(this.mouseMessageTimer);
this.setState({ parsed: false });
};
parseMessage = () => {
if (!this.state.parsed) {
const { row } = this.props;
const parser = getParser(row.entry);
if (parser) {
// Use parser to highlight detected fields
const parsedFieldHighlights = [];
this.props.row.entry.replace(new RegExp(parser.fieldRegex, 'g'), substring => {
parsedFieldHighlights.push(substring.trim());
return '';
});
this.setState({ parsedFieldHighlights, parsed: true, parser });
}
}
};
render() {
const {
allRows,
highlighterExpressions,
onClickLabel,
row,
showDuplicates,
showLabels,
showLocalTime,
showUtc,
} = this.props;
const {
fieldCount,
fieldLabel,
fieldStats,
fieldValue,
parsed,
parsedFieldHighlights,
showFieldStats,
} = this.state;
const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
const highlights = previewHighlights ? highlighterExpressions : row.searchWords;
const needsHighlighter = highlights && highlights.length > 0;
const highlightClassName = classnames('logs-row__match-highlight', {
'logs-row__match-highlight--preview': previewHighlights,
});
return (
<div className="logs-row">
{showDuplicates && (
<div className="logs-row__duplicates">{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}</div>
)}
<div className={row.logLevel ? `logs-row__level logs-row__level--${row.logLevel}` : ''} />
{showUtc && (
<div className="logs-row__time" title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
{row.timestamp}
</div>
)}
{showLocalTime && (
<div className="logs-row__time" title={`${row.timestamp} (${row.timeFromNow})`}>
{row.timeLocal}
</div>
)}
{showLabels && (
<div className="logs-row__labels">
<LogLabels allRows={allRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
</div>
)}
<div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}>
{parsed && (
<Highlighter
autoEscape
highlightTag={FieldHighlight(this.onClickHighlight)}
textToHighlight={row.entry}
searchWords={parsedFieldHighlights}
highlightClassName="logs-row__field-highlight"
/>
)}
{!parsed &&
needsHighlighter && (
<Highlighter
textToHighlight={row.entry}
searchWords={highlights}
findChunks={findHighlightChunksInText}
highlightClassName={highlightClassName}
/>
)}
{!parsed && !needsHighlighter && row.entry}
{showFieldStats && (
<div className="logs-row__stats">
<Stats
stats={fieldStats}
label={fieldLabel}
value={fieldValue}
onClickClose={this.onClickClose}
rowCount={fieldCount}
/>
</div>
)}
</div>
</div>
</>
);
);
}
}
function renderMetaItem(value: any, kind: LogsMetaKind) {
if (kind === LogsMetaKind.LabelsMap) {
return (
<span className="logs-meta-item__value-labels">
<span className="logs-meta-item__labels">
<LogLabels labels={value} plain />
</span>
);
@ -112,7 +245,6 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
}
interface LogsProps {
className?: string;
data: LogsModel;
highlighterExpressions: string[];
loading: boolean;
@ -220,7 +352,6 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
render() {
const {
className = '',
data,
highlighterExpressions,
loading = false,
@ -263,31 +394,31 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
}
// Grid options
const cssColumnSizes = [];
if (showDuplicates) {
cssColumnSizes.push('max-content');
}
// Log-level indicator line
cssColumnSizes.push('3px');
if (showUtc) {
cssColumnSizes.push('minmax(100px, max-content)');
}
if (showLocalTime) {
cssColumnSizes.push('minmax(100px, max-content)');
}
if (showLabels) {
cssColumnSizes.push('fit-content(20%)');
}
cssColumnSizes.push('1fr');
const logEntriesStyle = {
gridTemplateColumns: cssColumnSizes.join(' '),
};
// const cssColumnSizes = [];
// if (showDuplicates) {
// cssColumnSizes.push('max-content');
// }
// // Log-level indicator line
// cssColumnSizes.push('3px');
// if (showUtc) {
// cssColumnSizes.push('minmax(220px, max-content)');
// }
// if (showLocalTime) {
// cssColumnSizes.push('minmax(140px, max-content)');
// }
// if (showLabels) {
// cssColumnSizes.push('fit-content(20%)');
// }
// cssColumnSizes.push('1fr');
// const logEntriesStyle = {
// gridTemplateColumns: cssColumnSizes.join(' '),
// };
const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
return (
<div className={`${className} logs`}>
<div className="logs-graph">
<div className="logs-panel">
<div className="logs-panel-graph">
<Graph
data={data.series}
height="100px"
@ -298,39 +429,36 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
userOptions={graphOptions}
/>
</div>
<div className="logs-options">
<div className="logs-controls">
<div className="logs-panel-options">
<div className="logs-panel-controls">
<Switch label="Timestamp" checked={showUtc} onChange={this.onChangeUtc} />
<Switch label="Local time" checked={showLocalTime} onChange={this.onChangeLocalTime} />
<Switch label="Labels" checked={showLabels} onChange={this.onChangeLabels} />
<Switch
label="Dedup: off"
checked={dedup === LogsDedupStrategy.none}
onChange={() => this.onChangeDedup(LogsDedupStrategy.none)}
/>
<Switch
label="Dedup: exact"
checked={dedup === LogsDedupStrategy.exact}
onChange={() => this.onChangeDedup(LogsDedupStrategy.exact)}
/>
<Switch
label="Dedup: numbers"
checked={dedup === LogsDedupStrategy.numbers}
onChange={() => this.onChangeDedup(LogsDedupStrategy.numbers)}
/>
<Switch
label="Dedup: signature"
checked={dedup === LogsDedupStrategy.signature}
onChange={() => this.onChangeDedup(LogsDedupStrategy.signature)}
<ToggleButtonGroup
label="Dedup"
onChange={this.onChangeDedup}
value={dedup}
render={({ selectedValue, onChange }) =>
Object.keys(LogsDedupStrategy).map((dedupType, i) => (
<ToggleButton
className="btn-small"
key={i}
value={dedupType}
onChange={onChange}
selected={selectedValue === dedupType}
>
{dedupType}
</ToggleButton>
))
}
/>
{hasData &&
meta && (
<div className="logs-meta">
<div className="logs-panel-meta">
{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">{renderMetaItem(item.value, item.kind)}</span>
<div className="logs-panel-meta__item" key={item.label}>
<span className="logs-panel-meta__label">{item.label}:</span>
<span className="logs-panel-meta__value">{renderMetaItem(item.value, item.kind)}</span>
</div>
))}
</div>
@ -338,7 +466,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
</div>
</div>
<div className="logs-entries" style={logEntriesStyle}>
<div className="logs-rows">
{hasData &&
!deferLogs &&
// Only inject highlighterExpression in the first set for performance reasons
@ -375,7 +503,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
{!loading &&
!hasData &&
!scanning && (
<div className="logs-nodata">
<div className="logs-panel-nodata">
No logs found.
<a className="link" onClick={this.onClickScan}>
Scan for older logs
@ -384,7 +512,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
)}
{scanning && (
<div className="logs-nodata">
<div className="logs-panel-nodata">
<span>{scanText}</span>
<a className="link" onClick={this.onClickStopScan}>
Stop scan

View File

@ -1,5 +1,4 @@
<div class="panel panel--solo" ng-if="panel" style="width: 100%">
<div class="panel-solo" ng-if="panel">
<plugin-component type="panel">
</plugin-component>
</div>
<div class="clearfix"></div>

View File

@ -4,7 +4,7 @@ import * as elasticsearchPlugin from 'app/plugins/datasource/elasticsearch/modul
import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module';
import * as grafanaPlugin from 'app/plugins/datasource/grafana/module';
import * as influxdbPlugin from 'app/plugins/datasource/influxdb/module';
import * as loggingPlugin from 'app/plugins/datasource/logging/module';
import * as lokiPlugin from 'app/plugins/datasource/loki/module';
import * as mixedPlugin from 'app/plugins/datasource/mixed/module';
import * as mysqlPlugin from 'app/plugins/datasource/mysql/module';
import * as postgresPlugin from 'app/plugins/datasource/postgres/module';
@ -33,7 +33,7 @@ const builtInPlugins = {
'app/plugins/datasource/opentsdb/module': opentsdbPlugin,
'app/plugins/datasource/grafana/module': grafanaPlugin,
'app/plugins/datasource/influxdb/module': influxdbPlugin,
'app/plugins/datasource/logging/module': loggingPlugin,
'app/plugins/datasource/loki/module': lokiPlugin,
'app/plugins/datasource/mixed/module': mixedPlugin,
'app/plugins/datasource/mysql/module': mysqlPlugin,
'app/plugins/datasource/postgres/module': postgresPlugin,

View File

@ -115,7 +115,7 @@ export class TeamMembers extends PureComponent<Props, State> {
</button>
<h5>Add Team Member</h5>
<div className="gf-form-inline">
<UserPicker onSelected={this.onUserSelected} className="width-30" />
<UserPicker onSelected={this.onUserSelected} className="min-width-30" />
{this.state.newTeamMember && (
<button className="btn btn-success gf-form-btn" type="submit" onClick={this.onAddUserToTeam}>
Add to team

View File

@ -58,7 +58,7 @@ exports[`Render should render component 1`] = `
className="gf-form-inline"
>
<UserPicker
className="width-30"
className="min-width-30"
onSelected={[Function]}
/>
</div>
@ -152,7 +152,7 @@ exports[`Render should render team members 1`] = `
className="gf-form-inline"
>
<UserPicker
className="width-30"
className="min-width-30"
onSelected={[Function]}
/>
</div>
@ -372,7 +372,7 @@ exports[`Render should render team members when sync enabled 1`] = `
className="gf-form-inline"
>
<UserPicker
className="width-30"
className="min-width-30"
onSelected={[Function]}
/>
</div>

View File

@ -1,3 +0,0 @@
# Grafana Logging Datasource - Native Plugin
This is a **built in** datasource that allows you to connect to Grafana's logging service.

View File

@ -1,15 +0,0 @@
import Datasource from './datasource';
import LoggingStartPage from './components/LoggingStartPage';
import LoggingQueryField from './components/LoggingQueryField';
export class LoggingConfigCtrl {
static templateUrl = 'partials/config.html';
}
export {
Datasource,
LoggingConfigCtrl as ConfigCtrl,
LoggingQueryField as ExploreQueryField,
LoggingStartPage as ExploreStartPage,
};

View File

@ -0,0 +1,3 @@
# Loki Datasource - Native Plugin
This is a **built in** datasource that allows you to connect to the Loki logging service.

View File

@ -15,7 +15,7 @@ const CHEAT_SHEET_ITEMS = [
export default (props: any) => (
<div>
<h2>Logging Cheat Sheet</h2>
<h2>Loki Cheat Sheet</h2>
{CHEAT_SHEET_ITEMS.map(item => (
<div className="cheat-sheet-item" key={item.expression}>
<div className="cheat-sheet-item__title">{item.title}</div>

View File

@ -49,7 +49,7 @@ interface CascaderOption {
disabled?: boolean;
}
interface LoggingQueryFieldProps {
interface LokiQueryFieldProps {
datasource: any;
error?: string | JSX.Element;
hint?: any;
@ -60,16 +60,16 @@ interface LoggingQueryFieldProps {
onQueryChange?: (value: DataQuery, override?: boolean) => void;
}
interface LoggingQueryFieldState {
interface LokiQueryFieldState {
logLabelOptions: any[];
syntaxLoaded: boolean;
}
class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, LoggingQueryFieldState> {
class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
plugins: any[];
languageProvider: any;
constructor(props: LoggingQueryFieldProps, context) {
constructor(props: LokiQueryFieldProps, context) {
super(props, context);
if (props.datasource.languageProvider) {
@ -208,8 +208,8 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
onTypeahead={this.onTypeahead}
onWillApplySuggestion={willApplySuggestion}
onValueChanged={this.onChangeQuery}
placeholder="Enter a Logging query"
portalOrigin="logging"
placeholder="Enter a Loki Log query"
portalOrigin="loki"
syntaxLoaded={syntaxLoaded}
/>
{error ? <div className="prom-query-field-info text-error">{error}</div> : null}
@ -229,4 +229,4 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
}
}
export default LoggingQueryField;
export default LokiQueryField;

View File

@ -1,15 +1,15 @@
import React, { PureComponent } from 'react';
import LoggingCheatSheet from './LoggingCheatSheet';
import LokiCheatSheet from './LokiCheatSheet';
interface Props {
onClickExample: () => void;
}
export default class LoggingStartPage extends PureComponent<Props> {
export default class LokiStartPage extends PureComponent<Props> {
render() {
return (
<div className="grafana-info-box grafana-info-box--max-lg">
<LoggingCheatSheet onClickExample={this.props.onClickExample} />
<LokiCheatSheet onClickExample={this.props.onClickExample} />
</div>
);
}

View File

@ -0,0 +1,98 @@
import LokiDatasource from './datasource';
describe('LokiDatasource', () => {
const instanceSettings = {
url: 'myloggingurl',
};
describe('when performing testDataSource', () => {
let ds;
let result;
describe('and call succeeds', () => {
beforeEach(async () => {
const backendSrv = {
async datasourceRequest() {
return Promise.resolve({
status: 200,
data: {
values: ['avalue'],
},
});
},
};
ds = new LokiDatasource(instanceSettings, backendSrv, {});
result = await ds.testDatasource();
});
it('should return successfully', () => {
expect(result.status).toBe('success');
});
});
describe('and call fails with 401 error', () => {
beforeEach(async () => {
const backendSrv = {
async datasourceRequest() {
return Promise.reject({
statusText: 'Unauthorized',
status: 401,
data: {
message: 'Unauthorized',
},
});
},
};
ds = new LokiDatasource(instanceSettings, backendSrv, {});
result = await ds.testDatasource();
});
it('should return error status and a detailed error message', () => {
expect(result.status).toEqual('error');
expect(result.message).toBe('Loki: Unauthorized. 401. Unauthorized');
});
});
describe('and call fails with 404 error', () => {
beforeEach(async () => {
const backendSrv = {
async datasourceRequest() {
return Promise.reject({
statusText: 'Not found',
status: 404,
data: '404 page not found',
});
},
};
ds = new LokiDatasource(instanceSettings, backendSrv, {});
result = await ds.testDatasource();
});
it('should return error status and a detailed error message', () => {
expect(result.status).toEqual('error');
expect(result.message).toBe('Loki: Not found. 404. 404 page not found');
});
});
describe('and call fails with 502 error', () => {
beforeEach(async () => {
const backendSrv = {
async datasourceRequest() {
return Promise.reject({
statusText: 'Bad Gateway',
status: 502,
data: '',
});
},
};
ds = new LokiDatasource(instanceSettings, backendSrv, {});
result = await ds.testDatasource();
});
it('should return error status and a detailed error message', () => {
expect(result.status).toEqual('error');
expect(result.message).toBe('Loki: Bad Gateway. 502');
});
});
});
});

View File

@ -27,7 +27,7 @@ function serializeParams(data: any) {
.join('&');
}
export default class LoggingDatasource {
export default class LokiDatasource {
languageProvider: LanguageProvider;
/** @ngInject */
@ -94,7 +94,7 @@ export default class LoggingDatasource {
}
metadataRequest(url) {
// HACK to get label values for {job=|}, will be replaced when implementing LoggingQueryField
// HACK to get label values for {job=|}, will be replaced when implementing LokiQueryField
const apiUrl = url.replace('v1', 'prom');
return this._request(apiUrl, { silent: true }).then(res => {
const data = { data: { data: res.data.values || [] } };
@ -136,11 +136,28 @@ export default class LoggingDatasource {
}
return {
status: 'error',
message: 'Data source connected, but no labels received. Verify that logging is configured properly.',
message:
'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.',
};
})
.catch(err => {
return { status: 'error', message: err.message };
let message = 'Loki: ';
if (err.statusText) {
message += err.statusText;
} else {
message += 'Cannot connect to Loki';
}
if (err.status) {
message += `. ${err.status}`;
}
if (err.data && err.data.message) {
message += `. ${err.data.message}`;
} else if (err.data) {
message += `. ${err.data}`;
}
return { status: 'error', message: message };
});
}
}

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -0,0 +1,216 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="200px" height="200px" viewBox="0 0 200 200" style="enable-background:new 0 0 200 200;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_2_);}
.st2{fill:url(#SVGID_3_);}
.st3{fill:url(#SVGID_4_);}
.st4{fill:url(#SVGID_5_);}
.st5{fill:url(#SVGID_6_);}
.st6{fill:url(#SVGID_7_);}
.st7{fill:url(#SVGID_8_);}
.st8{fill:url(#SVGID_9_);}
.st9{fill:url(#SVGID_10_);}
.st10{fill:url(#SVGID_11_);}
.st11{fill:url(#SVGID_12_);}
.st12{fill:url(#SVGID_13_);}
.st13{fill:url(#SVGID_14_);}
.st14{fill:url(#SVGID_15_);}
.st15{fill:url(#SVGID_16_);}
.st16{fill:url(#SVGID_17_);}
.st17{fill:url(#SVGID_18_);}
.st18{fill:url(#SVGID_19_);}
.st19{fill:url(#SVGID_20_);}
.st20{fill:url(#SVGID_21_);}
.st21{fill:url(#SVGID_22_);}
.st22{fill:url(#SVGID_23_);}
.st23{fill:url(#SVGID_24_);}
.st24{fill:url(#SVGID_25_);}
.st25{fill:url(#SVGID_26_);}
.st26{fill:url(#SVGID_27_);}
.st27{fill:url(#SVGID_28_);}
.st28{fill:url(#SVGID_29_);}
.st29{fill:url(#SVGID_30_);}
.st30{fill:url(#SVGID_31_);}
.st31{fill:url(#SVGID_32_);}
</style>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="135.0285" y1="238.7858" x2="135.0285" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st0" d="M179.5,130c-6.9-9.5-18.6-16.1-30.2-17.9l-38.1-4.6l0,22.4l34.7,4c5.8,0.9,12.3,4.3,15.8,9.1
c3.5,4.7,4.9,10.5,4,16.3c-1.7,10.8-11,18.5-21.6,18.5c-1.1,0-2.3-0.1-3.4-0.3l-37.9-4.7c-5.1,8-12.2,14.7-20.6,19.2l55.2,7.4
c2.3,0.4,4.6,0.5,6.8,0.5c21.3,0,40-15.6,43.4-37.3C189.3,151.1,186.4,139.5,179.5,130z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="56.0866" y1="238.7858" x2="56.0866" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st1" d="M90.5,171c1.3-1.6,2.4-3.3,3.5-5.1c1-1.7,1.9-3.4,2.7-5.2c2.3-5.3,3.5-11.2,3.5-17.3l0-4l0-5.6l0-5.6l0-22.4
l0-5.6l0-5.6L100,43.9C100,19.7,80.2,0,55.9,0S12,19.8,12,44.1l0.1,66.7c5.7-7.6,13.3-13.7,22.1-17.6L34.1,44
c0-12.1,9.8-21.9,21.8-21.9c12.1,0,21.9,9.8,21.9,21.8L78,91.2l0,5.6l0,5.6l0,22.4l0,5.6l0,5.6l0,7.5c0,5.2-1.8,9.9-4.8,13.7
c-1.5,1.9-3.3,3.5-5.3,4.8c-3.4,2.2-7.4,3.5-11.7,3.5c-0.7,0-1.4,0-2,0l-1.4-0.2c-10.8-1.7-18.6-11.1-18.5-21.8
c0-1.1,0.1-2.1,0.2-3.2c0.7-4.3,2.6-8.1,5.3-11.1c1.6-1.8,3.5-3.3,5.5-4.5c3.2-1.8,6.9-2.9,10.8-2.9c1.1,0,2.3,0.1,3.4,0.3l7.5,1.2
l0-22.4l-4-0.6c-2.3-0.4-4.6-0.5-6.8-0.5c-3.7,0-7.3,0.5-10.8,1.4c-1.9,0.5-3.7,1.1-5.5,1.8c-1.9,0.8-3.8,1.7-5.5,2.7
c-11.2,6.4-19.4,17.7-21.6,31.4c-0.4,2.4-0.5,4.9-0.5,7.3c0,1.2,0.1,2.3,0.2,3.5c0,0.3,0.1,0.6,0.1,0.9c0.1,1.1,0.3,2.3,0.5,3.4
c0.1,0.3,0.1,0.7,0.2,1c0.2,1.1,0.5,2.1,0.8,3.2c0.1,0.4,0.2,0.7,0.3,1.1c0.3,1,0.7,2,1.1,3c0.1,0.3,0.3,0.7,0.4,1
c0.4,1,0.9,1.9,1.4,2.9c0.2,0.3,0.3,0.6,0.5,0.9c0.5,1,1.1,1.9,1.7,2.8c0.2,0.2,0.3,0.5,0.5,0.7c0.7,1,1.4,1.9,2.1,2.9
c0.1,0.1,0.2,0.3,0.4,0.4c0.8,1,1.7,1.9,2.6,2.8c0.1,0.1,0.1,0.1,0.2,0.2c1,1,2,1.9,3,2.7c0,0,0.1,0,0.1,0.1
c1.1,0.9,2.2,1.7,3.3,2.5c0,0,0,0,0.1,0.1c1.1,0.8,2.3,1.5,3.5,2.1c0.1,0,0.1,0.1,0.2,0.1c1.1,0.6,2.3,1.2,3.5,1.7
c0.2,0.1,0.3,0.1,0.5,0.2c1.1,0.5,2.2,0.9,3.3,1.2c0.3,0.1,0.6,0.2,0.9,0.3c1,0.3,2.1,0.6,3.2,0.8c0.4,0.1,0.8,0.2,1.2,0.2
c2.7,0.5,5.5,0.8,8.3,0.8C70.1,187.5,82.4,181.1,90.5,171z"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="26.7057" y1="238.7858" x2="26.7057" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st2" d="M28.2,177.5c-1-0.9-2-1.8-3-2.7"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="12.7354" y1="238.7858" x2="12.7354" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st3" d="M13,151.9c-0.2-1.1-0.4-2.2-0.5-3.4"/>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="14.8849" y1="238.7858" x2="14.8849" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st4" d="M15.4,160.1c-0.4-1-0.8-2-1.1-3"/>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="18.6023" y1="238.7858" x2="18.6023" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st5" d="M19.5,167.8c-0.6-0.9-1.2-1.9-1.7-2.8"/>
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="16.5561" y1="238.7858" x2="16.5561" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st6" d="M17.2,164c-0.5-0.9-1-1.9-1.4-2.9"/>
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="12.2907" y1="238.7858" x2="12.2907" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st7" d="M12.2,144.1c0,1.2,0.1,2.3,0.2,3.5"/>
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="23.7117" y1="238.7858" x2="23.7117" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st8" d="M25,174.6c-0.9-0.9-1.8-1.9-2.6-2.8"/>
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="20.9974" y1="238.7858" x2="20.9974" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st9" d="M22.1,171.3c-0.8-0.9-1.5-1.9-2.1-2.9"/>
<linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="13.6059" y1="238.7858" x2="13.6059" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st10" d="M14,156.1c-0.3-1-0.6-2.1-0.8-3.2"/>
<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="37.1094" y1="238.7858" x2="37.1094" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st11" d="M38.8,184c-1.2-0.5-2.3-1.1-3.5-1.7"/>
<linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="45.1619" y1="238.7858" x2="45.1619" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st12" d="M46.8,186.5c-1.1-0.2-2.1-0.5-3.2-0.8"/>
<linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="29.9487" y1="238.7858" x2="29.9487" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st13" d="M31.6,180c-1.1-0.8-2.2-1.6-3.3-2.5"/>
<linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="41.0239" y1="238.7858" x2="41.0239" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st14" d="M42.7,185.4c-1.1-0.4-2.3-0.8-3.3-1.2"/>
<linearGradient id="SVGID_16_" gradientUnits="userSpaceOnUse" x1="33.4189" y1="238.7858" x2="33.4189" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st15" d="M35.2,182.2c-1.2-0.7-2.4-1.4-3.5-2.1"/>
<linearGradient id="SVGID_17_" gradientUnits="userSpaceOnUse" x1="53.9595" y1="238.7858" x2="53.9595" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st16" d="M54.1,154.2c-0.1,0-0.1,0-0.2-0.1"/>
<linearGradient id="SVGID_18_" gradientUnits="userSpaceOnUse" x1="21.1405" y1="238.7858" x2="21.1405" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st17" d="M21.4,178.8c-0.2-0.2-0.3-0.3-0.5-0.5"/>
<linearGradient id="SVGID_19_" gradientUnits="userSpaceOnUse" x1="35.2646" y1="238.7858" x2="35.2646" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st18" d="M35.2,182.2c0.1,0,0.1,0.1,0.2,0.1"/>
<linearGradient id="SVGID_20_" gradientUnits="userSpaceOnUse" x1="39.0979" y1="238.7858" x2="39.0979" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st19" d="M39.3,184.2c-0.2-0.1-0.3-0.1-0.5-0.2"/>
<linearGradient id="SVGID_21_" gradientUnits="userSpaceOnUse" x1="31.6434" y1="238.7858" x2="31.6434" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st20" d="M31.7,180.1C31.7,180.1,31.6,180.1,31.7,180.1"/>
<linearGradient id="SVGID_22_" gradientUnits="userSpaceOnUse" x1="28.2485" y1="238.7858" x2="28.2485" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st21" d="M28.3,177.6C28.3,177.5,28.2,177.5,28.3,177.6"/>
<linearGradient id="SVGID_23_" gradientUnits="userSpaceOnUse" x1="43.1314" y1="238.7858" x2="43.1314" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st22" d="M43.6,185.7c-0.3-0.1-0.6-0.2-0.9-0.3"/>
<linearGradient id="SVGID_24_" gradientUnits="userSpaceOnUse" x1="47.3468" y1="238.7858" x2="47.3468" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st23" d="M46.8,186.5c0.4,0.1,0.8,0.2,1.2,0.2"/>
<linearGradient id="SVGID_25_" gradientUnits="userSpaceOnUse" x1="19.6975" y1="238.7858" x2="19.6975" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st24" d="M19.9,168.4c-0.2-0.2-0.3-0.5-0.5-0.7"/>
<linearGradient id="SVGID_26_" gradientUnits="userSpaceOnUse" x1="17.4923" y1="238.7858" x2="17.4923" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st25" d="M17.7,164.9c-0.2-0.3-0.3-0.6-0.5-0.9"/>
<linearGradient id="SVGID_27_" gradientUnits="userSpaceOnUse" x1="12.4278" y1="238.7858" x2="12.4278" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st26" d="M12.5,148.5c0-0.3-0.1-0.6-0.1-0.9"/>
<linearGradient id="SVGID_28_" gradientUnits="userSpaceOnUse" x1="13.0965" y1="238.7858" x2="13.0965" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st27" d="M13.2,152.9c-0.1-0.3-0.1-0.7-0.2-1"/>
<linearGradient id="SVGID_29_" gradientUnits="userSpaceOnUse" x1="14.1769" y1="238.7858" x2="14.1769" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st28" d="M14.3,157.1c-0.1-0.3-0.2-0.7-0.3-1.1"/>
<linearGradient id="SVGID_30_" gradientUnits="userSpaceOnUse" x1="15.6479" y1="238.7858" x2="15.6479" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st29" d="M15.9,161.1c-0.2-0.3-0.3-0.7-0.4-1"/>
<linearGradient id="SVGID_31_" gradientUnits="userSpaceOnUse" x1="22.2436" y1="238.7858" x2="22.2436" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st30" d="M22.4,171.7c-0.1-0.1-0.2-0.3-0.4-0.4"/>
<linearGradient id="SVGID_32_" gradientUnits="userSpaceOnUse" x1="25.1051" y1="238.7858" x2="25.1051" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st31" d="M25.2,174.8c-0.1-0.1-0.1-0.1-0.2-0.2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -36,7 +36,7 @@ export function addHistoryMetadata(item: CompletionItem, history: HistoryItem[])
};
}
export default class LoggingLanguageProvider extends LanguageProvider {
export default class LokiLanguageProvider extends LanguageProvider {
labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
logLabelOptions: any[];

View File

@ -0,0 +1,15 @@
import Datasource from './datasource';
import LokiStartPage from './components/LokiStartPage';
import LokiQueryField from './components/LokiQueryField';
export class LokiConfigCtrl {
static templateUrl = 'partials/config.html';
}
export {
Datasource,
LokiConfigCtrl as ConfigCtrl,
LokiQueryField as ExploreQueryField,
LokiStartPage as ExploreStartPage,
};

View File

@ -1,7 +1,7 @@
{
"type": "datasource",
"name": "Grafana Logging",
"id": "logging",
"name": "Loki",
"id": "loki",
"metrics": false,
"alerting": false,
"annotations": false,
@ -9,19 +9,19 @@
"explore": true,
"tables": true,
"info": {
"description": "Grafana Logging Data Source for Grafana",
"description": "Loki Logging Data Source for Grafana",
"author": {
"name": "Grafana Project",
"url": "https://grafana.com"
},
"logos": {
"small": "img/grafana_icon.svg",
"large": "img/grafana_icon.svg"
"small": "img/loki_icon.svg",
"large": "img/loki_icon.svg"
},
"links": [
{
"name": "Grafana Logging",
"url": "https://grafana.com/"
"name": "Loki",
"url": "https://github.com/grafana/loki"
}
],
"version": "5.3.0"

View File

@ -151,8 +151,7 @@ table_schema IN (
buildDatatypeQuery(column: string) {
let query = 'SELECT udt_name FROM information_schema.columns WHERE ';
query += this.buildSchemaConstraint();
query += ' AND table_name = ' + this.quoteIdentAsLiteral(this.target.table);
query += this.buildTableConstraint(this.target.table);
query += ' AND column_name = ' + this.quoteIdentAsLiteral(column);
return query;
}

View File

@ -16,7 +16,7 @@ export class ResultTransformer {
options.valueWithRefId
),
];
} else if (options.format === 'heatmap') {
} else if (prometheusResult && options.format === 'heatmap') {
let seriesList = [];
prometheusResult.sort(sortSeriesByLabel);
for (const metricData of prometheusResult) {
@ -24,7 +24,7 @@ export class ResultTransformer {
}
seriesList = this.transformToHistogramOverTime(seriesList);
return seriesList;
} else {
} else if (prometheusResult) {
const seriesList = [];
for (const metricData of prometheusResult) {
if (response.data.data.resultType === 'matrix') {
@ -82,7 +82,7 @@ export class ResultTransformer {
let i, j;
const metricLabels = {};
if (md.length === 0) {
if (!md || md.length === 0) {
return table;
}

View File

@ -10,6 +10,31 @@ describe('Prometheus Result Transformer', () => {
ctx.resultTransformer = new ResultTransformer(ctx.templateSrv);
});
describe('When nothing is returned', () => {
test('should return empty series', () => {
const response = {
status: 'success',
data: {
resultType: '',
result: null,
},
};
const series = ctx.resultTransformer.transform({ data: response }, {});
expect(series).toEqual([]);
});
test('should return empty table', () => {
const response = {
status: 'success',
data: {
resultType: '',
result: null,
},
};
const table = ctx.resultTransformer.transform({ data: response }, { format: 'table' });
expect(table).toMatchObject([{ type: 'table', rows: [] }]);
});
});
describe('When resultFormat is table', () => {
const response = {
status: 'success',

View File

@ -19,6 +19,7 @@ import {
DataQuery,
DataQueryResponse,
DataQueryOptions,
IntervalValues,
} from './series';
import { PanelProps, PanelOptionsProps, RangeMap, Threshold, ValueMap } from './panel';
import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins';
@ -94,6 +95,7 @@ export {
ValidationRule,
ValueMap,
RangeMap,
IntervalValues,
};
export interface StoreState {

View File

@ -19,6 +19,11 @@ export interface TimeRange {
raw: RawTimeRange;
}
export interface IntervalValues {
interval: string; // 10s,5m
intervalMs: number;
}
export type TimeSeriesValue = string | number | null;
export type TimeSeriesPoints = TimeSeriesValue[][];
@ -90,6 +95,9 @@ export interface DataQueryOptions {
}
export interface DataSourceApi {
/**
* min interval range
*/
interval?: string;
/**

View File

@ -59,6 +59,7 @@
@import 'components/panel_text';
@import 'components/panel_heatmap';
@import 'components/panel_add_panel';
@import 'components/panel_logs';
@import 'components/settings_permissions';
@import 'components/tagsinput';
@import 'components/tables_lists';
@ -104,6 +105,7 @@
@import 'components/page_loader';
@import 'components/unit-picker';
@import 'components/thresholds';
@import 'components/toggle_button_group';
// PAGES
@import 'pages/login';

View File

@ -237,6 +237,7 @@ $horizontalComponentOffset: 180px;
// -------------------------
$navbarHeight: 55px;
$navbarBackground: $panel-bg;
$navbarBorder: 1px solid $dark-3;
$navbarShadow: 0 0 20px black;
@ -387,3 +388,6 @@ $panel-editor-viz-item-bg-hover-active: darken($orange, 45%);
$panel-grid-placeholder-bg: darken($blue, 47%);
$panel-grid-placeholder-shadow: 0 0 4px $blue;
// logs
$logs-color-unkown: $gray-2;

View File

@ -397,3 +397,6 @@ $panel-editor-viz-item-bg-hover-active: lighten($orange, 34%);
$panel-grid-placeholder-bg: lighten($blue, 62%);
$panel-grid-placeholder-shadow: 0 0 4px $blue-light;
// logs
$logs-color-unkown: $gray-5;

View File

@ -199,7 +199,6 @@ small,
mark,
.mark {
padding: 0.2em;
background: $alert-warning-bg;
}

View File

@ -0,0 +1,294 @@
$column-horizontal-spacing: 10px;
.logs-panel-controls {
display: flex;
background-color: $page-bg;
padding: $panel-padding;
padding-top: 10px;
border-radius: $border-radius;
margin: 2*$panel-margin 0;
border: $panel-border;
justify-items: flex-start;
align-items: flex-start;
> * {
margin-right: 1em;
}
}
.logs-panel-nodata {
> * {
margin-left: 0.5em;
}
}
.logs-panel-meta {
flex: 1;
color: $text-color-weak;
// Align first line with controls labels
margin-top: -2px;
}
.logs-panel-meta__item {
margin-right: 1em;
}
.logs-panel-meta__label {
margin-right: 0.5em;
font-size: 0.9em;
font-weight: 500;
}
.logs-panel-meta__value {
font-family: $font-family-monospace;
}
.logs-panel-meta-item__labels {
// compensate for the labels padding
position: relative;
top: 4px;
}
.logs-rows {
font-family: $font-family-monospace;
font-size: 12px;
display: table;
table-layout: fixed;
}
.logs-row {
display: table-row;
> div {
display: table-cell;
padding-right: $column-horizontal-spacing;
vertical-align: middle;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
}
&:hover {
background: $page-bg;
}
}
.logs-row__time {
white-space: nowrap;
}
.logs-row__labels {
max-width: 20%;
line-height: 1.2;
}
.logs-row__message {
word-break: break-all;
min-width: 80%;
}
.logs-row__match-highlight {
// Undoing mark styling
background: inherit;
padding: inherit;
color: $typeahead-selected-color;
border-bottom: 1px solid $typeahead-selected-color;
background-color: rgba($typeahead-selected-color, 0.1);
&--preview {
background-color: rgba($typeahead-selected-color, 0.2);
border-bottom-style: dotted;
}
}
.logs-row__level {
position: relative;
&::after {
content: '';
display: block;
position: absolute;
top: 1px;
bottom: 1px;
width: 3px;
background-color: $logs-color-unkown;
}
&--critical,
&--crit {
&::after {
background-color: #705da0;
}
}
&--error,
&--err {
&::after {
background-color: #e24d42;
}
}
&--warning,
&--warn {
&::after {
background-color: $warn;
}
}
&--info {
&::after {
background-color: #7eb26d;
}
}
&--debug {
&::after {
background-color: #1f78c1;
}
}
&--trace {
&::after {
background-color: #6ed0e0;
}
}
}
.logs-row__duplicates {
text-align: right;
}
.logs-row__field-highlight {
// Undoing mark styling
background: inherit;
padding: inherit;
border-bottom: 1px dotted $typeahead-selected-color;
.logs-row__field-highlight--icon {
margin-left: 0.5em;
cursor: pointer;
display: none;
}
}
.logs-row__stats {
margin: 5px 0;
}
.logs-row__field-highlight:hover {
color: $typeahead-selected-color;
border-bottom-style: solid;
.logs-row__field-highlight--icon {
display: inline;
}
}
.logs-label {
display: inline-block;
padding: 0 2px;
background-color: $btn-inverse-bg;
border-radius: $border-radius;
margin: 0 4px 2px 0;
text-overflow: ellipsis;
white-space: nowrap;
position: relative;
}
.logs-label__icon {
border-left: $panel-border;
padding: 0 2px;
cursor: pointer;
margin-left: 2px;
}
.logs-label__stats {
position: absolute;
top: 1.25em;
left: -10px;
z-index: 100;
justify-content: space-between;
box-shadow: $popover-shadow;
}
/*
* Stats popover & message stats box
*/
.logs-stats {
background-color: $popover-bg;
color: $popover-color;
border: 1px solid $popover-border-color;
border-radius: $border-radius;
max-width: 500px;
}
.logs-stats__header {
background-color: $popover-border-color;
padding: 6px 10px;
display: flex;
}
.logs-stats__title {
font-weight: $font-weight-semi-bold;
padding-right: $spacer;
overflow: hidden;
display: inline-block;
white-space: nowrap;
text-overflow: ellipsis;
flex-grow: 1;
}
.logs-stats__body {
padding: 20px 10px 10px 10px;
}
.logs-stats__close {
cursor: pointer;
}
.logs-stats-row {
margin: $spacer/1.75 0;
&--active {
color: $blue;
position: relative;
}
&--active::after {
display: inline;
content: '*';
position: absolute;
top: 0;
left: -8px;
}
&__label {
display: flex;
margin-bottom: 1px;
}
&__value {
flex: 1;
}
&__count,
&__percent {
text-align: right;
margin-left: 0.5em;
}
&__percent {
width: 3em;
}
&__bar,
&__innerbar {
height: 4px;
overflow: hidden;
background: $text-color-faint;
}
&__innerbar {
background: $blue;
}
}

View File

@ -0,0 +1,37 @@
.toggle-button-group {
display: flex;
.gf-form-label {
background-color: $input-label-bg;
&:first-child {
border-radius: $border-radius 0 0 $border-radius;
margin: 0;
}
&.small {
padding: ($input-padding-y / 2) ($input-padding-x / 2);
font-size: $font-size-xs;
}
}
.btn {
background-color: $typeahead-selected-bg;
border-radius: 0;
color: $text-color;
&.active {
background-color: $input-bg;
&:hover {
cursor: default;
}
}
&:first-child {
border-radius: $border-radius 0 0 $border-radius;
margin: 0;
}
&:last-child {
border-radius: 0 $border-radius $border-radius 0;
margin-left: 0;
}
}
}

View File

@ -15,17 +15,23 @@ div.flot-text {
.panel {
height: 100%;
}
&--solo {
position: fixed;
bottom: 0;
right: 0;
margin: 0;
.panel-solo {
position: fixed;
bottom: 0;
right: 0;
margin: 0;
left: 0;
top: 0;
.panel-container {
border: none;
z-index: $zindex-sidemenu + 1;
}
.panel-container {
border: none;
}
.panel-menu-toggle,
.panel-menu {
display: none;
}
}

View File

@ -238,212 +238,6 @@
padding-right: 0.25em;
}
.explore {
.logs {
.logs-controls {
display: flex;
background-color: $page-bg;
padding: $panel-padding;
padding-top: 10px;
border-radius: $border-radius;
margin: 2*$panel-margin 0;
border: $panel-border;
justify-items: flex-start;
align-items: flex-start;
> * {
margin-right: 1em;
}
}
.logs-nodata {
> * {
margin-left: 0.5em;
}
}
.logs-meta {
flex: 1;
color: $text-color-weak;
// Align first line with controls labels
margin-top: -2px;
}
.logs-meta-item {
margin-right: 1em;
}
.logs-meta-item__label {
margin-right: 0.5em;
font-size: 0.9em;
font-weight: 500;
}
.logs-meta-item__value {
font-family: $font-family-monospace;
}
.logs-meta-item__value-labels {
// compensate for the labels padding
position: relative;
top: 4px;
}
.logs-entries {
display: grid;
grid-column-gap: 1rem;
grid-row-gap: 0.1rem;
font-family: $font-family-monospace;
font-size: 12px;
}
.logs-row-match-highlight {
// Undoing mark styling
background: inherit;
padding: inherit;
color: $typeahead-selected-color;
border-bottom: 1px solid $typeahead-selected-color;
background-color: rgba($typeahead-selected-color, 0.1);
}
.logs-row-match-highlight--preview {
background-color: rgba($typeahead-selected-color, 0.2);
border-bottom-style: dotted;
}
.logs-row-level {
background-color: transparent;
margin: 2px 0;
position: relative;
opacity: 0.8;
}
.logs-row-level-critical,
.logs-row-level-crit {
background-color: #705da0;
}
.logs-row-level-error,
.logs-row-level-err {
background-color: #e24d42;
}
.logs-row-level-warning,
.logs-row-level-warn {
background-color: #eab839;
}
.logs-row-level-info {
background-color: #7eb26d;
}
.logs-row-level-debug {
background-color: #1f78c1;
}
.logs-row-level-trace {
background-color: #6ed0e0;
}
.logs-row-duplicates {
text-align: right;
}
.logs-label {
display: inline-block;
padding: 0 2px;
background-color: $btn-inverse-bg;
border-radius: $border-radius;
margin: 0 4px 2px 0;
text-overflow: ellipsis;
white-space: nowrap;
position: relative;
}
.logs-label__icon {
border-left: $panel-border;
padding: 0 2px;
cursor: pointer;
margin-left: 2px;
}
.logs-label__stats {
position: absolute;
top: 1.25em;
left: -10px;
z-index: 100;
background-color: $popover-bg;
color: $popover-color;
border: 1px solid $popover-border-color;
padding: 10px;
border-radius: $border-radius;
justify-content: space-between;
box-shadow: $popover-shadow;
}
.logs-row-labels {
line-height: 1.2;
}
.logs-stats__info {
margin-bottom: $spacer / 2;
}
.logs-stats__icon {
margin-left: 0.5em;
cursor: pointer;
}
.logs-stats-row {
margin: $spacer/1.75 0;
&--active {
color: $blue;
position: relative;
}
&--active:after {
display: inline;
content: '*';
position: absolute;
top: 0;
left: -8px;
}
&__label {
display: flex;
margin-bottom: 1px;
}
&__value {
flex: 1;
}
&__count,
&__percent {
text-align: right;
margin-left: 0.5em;
}
&__percent {
width: 3em;
}
&__bar,
&__innerbar {
height: 4px;
overflow: hidden;
background: $text-color-faint;
}
&__innerbar {
background-color: $blue;
}
}
}
}
// Prometheus-specifics, to be extracted to datasource soon
.explore {

View File

@ -19,6 +19,12 @@
}
}
@for $i from 1 through 30 {
.min-width-#{$i} {
min-width: ($spacer * $i) - $gf-form-margin !important;
}
}
@for $i from 1 through 30 {
.offset-width-#{$i} {
margin-left: ($spacer * $i) !important;