Dashboard: Adds Logs Panel (alpha) as visualization option for Dashboards (#18641)

* WIP: intial commit

* Switch: Adds tooltip

* Refactor: Adds props to LogsPanelEditor

* Refactor: Moves LogRowContextProvider to grafana/ui

* Refactor: Moves LogRowContext and Alert to grafana/ui

* Refactor: Moves LogLabelStats to grafana/ui

* Refactor: Moves LogLabels and LogLabel to grafana/ui

* Refactor: Moves LogMessageAnsi and ansicolor to grafana/ui

* Refactor: Moves calculateFieldStats, LogsParsers and getParser to grafana/data

* Refactor: Moves findHighlightChunksInText to grafana/data

* Refactor: Moves LogRow to grafana/ui

* Refactor: Moving ExploreGraphPanel to grafana/ui

* Refactor: Copies Logs to grafana/ui

* Refactor: Moves ToggleButtonGroup to grafana/ui

* Refactor: Adds Logs to LogsPanel

* Refactor: Moves styles to emotion

* Feature: Adds LogsRows

* Refactor: Introduces render limit

* Styles: Moves styles to emotion

* Styles: Moves styles to emotion

* Styles: Moves styles to emotion

* Styles: Moves styles to emotion

* Refactor: Adds sorting to LogsPanelEditor

* Tests: Adds tests for sorting

* Refactor: Changes according to PR comments

* Refactor: Changes according to PR comments

* Refactor: Moves Logs and ExploreGraphPanel out of grafana/ui

* Fix: Shows the Show context label again
This commit is contained in:
Hugo Häggmark
2019-08-26 08:11:07 +02:00
committed by GitHub
parent 98a512a3c7
commit e5e7bd3153
55 changed files with 1765 additions and 1293 deletions

View File

@@ -1,11 +1,10 @@
import React from 'react';
import { PanelData } from '@grafana/ui';
import { PanelData, GraphSeriesToggler } from '@grafana/ui';
import { GraphSeriesXY } from '@grafana/data';
import { getGraphSeriesModel } from './getGraphSeriesModel';
import { Options, SeriesOptions } from './types';
import { SeriesColorChangeHandler, SeriesAxisToggleHandler } from '@grafana/ui/src/components/Graph/GraphWithLegend';
import { GraphSeriesToggler } from './GraphSeriesToggler';
interface GraphPanelControllerAPI {
series: GraphSeriesXY[];

View File

@@ -1,85 +0,0 @@
import React from 'react';
import { GraphSeriesXY } from '@grafana/data';
import difference from 'lodash/difference';
import isEqual from 'lodash/isEqual';
interface GraphSeriesTogglerAPI {
onSeriesToggle: (label: string, event: React.MouseEvent<HTMLElement>) => void;
toggledSeries: GraphSeriesXY[];
}
interface GraphSeriesTogglerProps {
children: (api: GraphSeriesTogglerAPI) => JSX.Element;
series: GraphSeriesXY[];
onHiddenSeriesChanged?: (hiddenSeries: string[]) => void;
}
interface GraphSeriesTogglerState {
hiddenSeries: string[];
toggledSeries: GraphSeriesXY[];
}
export class GraphSeriesToggler extends React.Component<GraphSeriesTogglerProps, GraphSeriesTogglerState> {
constructor(props: GraphSeriesTogglerProps) {
super(props);
this.onSeriesToggle = this.onSeriesToggle.bind(this);
this.state = {
hiddenSeries: [],
toggledSeries: props.series,
};
}
componentDidUpdate(prevProps: Readonly<GraphSeriesTogglerProps>) {
const { series } = this.props;
if (!isEqual(prevProps.series, series)) {
this.setState({ hiddenSeries: [], toggledSeries: series });
}
}
onSeriesToggle(label: string, event: React.MouseEvent<HTMLElement>) {
const { series, onHiddenSeriesChanged } = this.props;
const { hiddenSeries } = this.state;
if (event.ctrlKey || event.metaKey || event.shiftKey) {
// Toggling series with key makes the series itself to toggle
const newHiddenSeries =
hiddenSeries.indexOf(label) > -1
? hiddenSeries.filter(series => series !== label)
: hiddenSeries.concat([label]);
const toggledSeries = series.map(series => ({
...series,
isVisible: newHiddenSeries.indexOf(series.label) === -1,
}));
this.setState({ hiddenSeries: newHiddenSeries, toggledSeries }, () =>
onHiddenSeriesChanged ? onHiddenSeriesChanged(newHiddenSeries) : undefined
);
return;
}
// Toggling series with out key toggles all the series but the clicked one
const allSeriesLabels = series.map(series => series.label);
const newHiddenSeries =
hiddenSeries.length + 1 === allSeriesLabels.length ? [] : difference(allSeriesLabels, [label]);
const toggledSeries = series.map(series => ({
...series,
isVisible: newHiddenSeries.indexOf(series.label) === -1,
}));
this.setState({ hiddenSeries: newHiddenSeries, toggledSeries }, () =>
onHiddenSeriesChanged ? onHiddenSeriesChanged(newHiddenSeries) : undefined
);
}
render() {
const { children } = this.props;
const { toggledSeries } = this.state;
return children({
onSeriesToggle: this.onSeriesToggle,
toggledSeries,
});
}
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { PanelProps, LogRows, CustomScrollbar } from '@grafana/ui';
import { Options } from './types';
import { LogsDedupStrategy } from '@grafana/data';
import { dataFrameToLogsModel } from 'app/core/logs_model';
import { sortLogsResult } from 'app/core/utils/explore';
interface LogsPanelProps extends PanelProps<Options> {}
export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
data,
timeZone,
options: { showTime, sortOrder },
width,
}) => {
if (!data) {
return (
<div className="panel-empty">
<p>No data found in response</p>
</div>
);
}
const newResults = data ? dataFrameToLogsModel(data.series, data.request.intervalMs) : null;
const sortedNewResults = sortLogsResult(newResults, sortOrder);
return (
<CustomScrollbar autoHide>
<LogRows
data={sortedNewResults}
dedupStrategy={LogsDedupStrategy.none}
highlighterExpressions={[]}
showTime={showTime}
showLabels={false}
timeZone={timeZone}
/>
</CustomScrollbar>
);
};

View File

@@ -0,0 +1,46 @@
// Libraries
import React, { PureComponent } from 'react';
import { PanelEditorProps, Switch, PanelOptionsGrid, PanelOptionsGroup, FormLabel, Select } from '@grafana/ui';
// Types
import { Options } from './types';
import { SortOrder } from 'app/core/utils/explore';
import { SelectableValue } from '@grafana/data';
const sortOrderOptions = [
{ value: SortOrder.Descending, label: 'Descending' },
{ value: SortOrder.Ascending, label: 'Ascending' },
];
export class LogsPanelEditor extends PureComponent<PanelEditorProps<Options>> {
onToggleTime = () => {
const { options, onOptionsChange } = this.props;
const { showTime } = options;
onOptionsChange({ ...options, showTime: !showTime });
};
onShowValuesChange = (item: SelectableValue<SortOrder>) => {
const { options, onOptionsChange } = this.props;
onOptionsChange({ ...options, sortOrder: item.value });
};
render() {
const { showTime, sortOrder } = this.props.options;
const value = sortOrderOptions.filter(option => option.value === sortOrder)[0];
return (
<>
<PanelOptionsGrid>
<PanelOptionsGroup title="Columns">
<Switch label="Time" labelClass="width-10" checked={showTime} onChange={this.onToggleTime} />
<div className="gf-form">
<FormLabel>Order</FormLabel>
<Select options={sortOrderOptions} value={value} onChange={this.onShowValuesChange} />
</div>
</PanelOptionsGroup>
</PanelOptionsGrid>
</>
);
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="144px" height="144px" viewBox="0 0 144 144" version="1.1">
<g id="surface736507">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(90.196078%,49.411765%,13.333333%);fill-opacity:1;" d="M 93 42 L 132 42 L 132 30 C 132 23.371094 126.628906 18 120 18 L 93 18 Z M 93 42 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(92.54902%,94.117647%,94.509804%);fill-opacity:1;" d="M 120 18 L 42 18 C 35.371094 18 30 23.371094 30 30 L 30 114 C 30 120.628906 35.371094 126 42 126 L 96 126 C 102.628906 126 108 120.628906 108 114 L 108 30 C 108 23.371094 113.371094 18 120 18 Z M 120 18 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(90.196078%,49.411765%,13.333333%);fill-opacity:1;" d="M 96 126 L 24 126 C 17.371094 126 12 120.628906 12 114 L 12 105 L 84 105 L 84 114 C 84 120.628906 89.371094 126 96 126 Z M 48 42 L 90 42 L 90 48 L 48 48 Z M 48 57 L 78 57 L 78 63 L 48 63 Z M 48 72 L 90 72 L 90 78 L 48 78 Z M 48 87 L 78 87 L 78 93 L 48 93 Z M 48 87 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,6 @@
import { PanelPlugin } from '@grafana/ui';
import { Options, defaults } from './types';
import { LogsPanel } from './LogsPanel';
import { LogsPanelEditor } from './LogsPanelEditor';
export const plugin = new PanelPlugin<Options>(LogsPanel).setDefaults(defaults).setEditor(LogsPanelEditor);

View File

@@ -0,0 +1,17 @@
{
"type": "panel",
"name": "Logs",
"id": "logs",
"state": "alpha",
"info": {
"author": {
"name": "Grafana Project",
"url": "https://grafana.com"
},
"logos": {
"small": "img/icn-logs-panel.svg",
"large": "img/icn-logs-panel.svg"
}
}
}

View File

@@ -0,0 +1,11 @@
import { SortOrder } from 'app/core/utils/explore';
export interface Options {
showTime: boolean;
sortOrder: SortOrder;
}
export const defaults: Options = {
showTime: true,
sortOrder: SortOrder.Descending,
};