Merge branch 'master' into develop

This commit is contained in:
Torkel Ödegaard
2018-11-20 09:47:23 +01:00
104 changed files with 3398 additions and 841 deletions

View File

@@ -29,6 +29,7 @@ export class AlertRuleList extends PureComponent<Props, any> {
{ text: 'Alerting', value: 'alerting' },
{ text: 'No Data', value: 'no_data' },
{ text: 'Paused', value: 'paused' },
{ text: 'Pending', value: 'pending' },
];
componentDidMount() {

View File

@@ -169,6 +169,7 @@ export class AlertTabCtrl {
alert.frequency = alert.frequency || '1m';
alert.handler = alert.handler || 1;
alert.notifications = alert.notifications || [];
alert.for = alert.for || '0m';
const defaultName = this.panel.title + ' alert';
alert.name = alert.name || defaultName;
@@ -217,7 +218,7 @@ export class AlertTabCtrl {
buildDefaultCondition() {
return {
type: 'query',
query: { params: ['A', '15m', 'now'] },
query: { params: ['A', '5m', 'now'] },
reducer: { type: 'avg', params: [] },
evaluator: { type: 'gt', params: [null] },
operator: { type: 'and' },
@@ -354,6 +355,7 @@ export class AlertTabCtrl {
enable() {
this.panel.alert = {};
this.initModel();
this.panel.alert.for = '5m'; //default value for new alerts. for existing alerts we use 0m to avoid breaking changes
}
evaluatorParamsChanged() {

View File

@@ -81,6 +81,12 @@ exports[`Render should render alert rules 1`] = `
>
Paused
</option>
<option
key="pending"
value="pending"
>
Pending
</option>
</select>
</div>
</div>
@@ -230,6 +236,12 @@ exports[`Render should render component 1`] = `
>
Paused
</option>
<option
key="pending"
value="pending"
>
Pending
</option>
</select>
</div>
</div>

View File

@@ -1,147 +1,159 @@
<div class="edit-tab-with-sidemenu" ng-if="ctrl.alert">
<aside class="edit-sidemenu-aside">
<ul class="edit-sidemenu">
<li ng-class="{active: ctrl.subTabIndex === 0}">
<a ng-click="ctrl.changeTabIndex(0)">Alert Config</a>
</li>
<li ng-class="{active: ctrl.subTabIndex === 1}">
<a ng-click="ctrl.changeTabIndex(1)">
Notifications <span class="muted">({{ctrl.alertNotifications.length}})</span>
</a>
</li>
<li ng-class="{active: ctrl.subTabIndex === 2}">
<a ng-click="ctrl.changeTabIndex(2)">State history</a>
</li>
<aside class="edit-sidemenu-aside">
<ul class="edit-sidemenu">
<li ng-class="{active: ctrl.subTabIndex === 0}">
<a ng-click="ctrl.changeTabIndex(0)">Alert Config</a>
</li>
<li ng-class="{active: ctrl.subTabIndex === 1}">
<a ng-click="ctrl.changeTabIndex(1)">
Notifications <span class="muted">({{ctrl.alertNotifications.length}})</span>
</a>
</li>
<li ng-class="{active: ctrl.subTabIndex === 2}">
<a ng-click="ctrl.changeTabIndex(2)">State history</a>
</li>
<li>
<a ng-click="ctrl.delete()">Delete</a>
</li>
</ul>
</aside>
<a ng-click="ctrl.delete()">Delete</a>
</li>
</ul>
</aside>
<div class="edit-tab-content">
<div ng-if="ctrl.subTabIndex === 0">
<div class="alert alert-error m-b-2" ng-show="ctrl.error">
<i class="fa fa-warning"></i> {{ctrl.error}}
</div>
<div class="edit-tab-content">
<div ng-if="ctrl.subTabIndex === 0">
<div class="alert alert-error m-b-2" ng-show="ctrl.error">
<i class="fa fa-warning"></i> {{ctrl.error}}
</div>
<div class="gf-form-group">
<h5 class="section-heading">Alert Config</h5>
<div class="gf-form">
<span class="gf-form-label width-6">Name</span>
<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name">
<span class="gf-form-label">Evaluate every</span>
<input class="gf-form-input max-width-5" type="text" ng-model="ctrl.alert.frequency"></input>
</div>
</div>
<div class="gf-form-group">
<h5 class="section-heading">Alert Config</h5>
<div class="gf-form">
<span class="gf-form-label width-6">Name</span>
<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name">
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Evaluate every</span>
<input class="gf-form-input max-width-6" type="text" ng-model="ctrl.alert.frequency">
</div>
<div class="gf-form max-width-11">
<label class="gf-form-label width-5">For</label>
<input type="text" class="gf-form-input max-width-6" ng-model="ctrl.alert.for" spellcheck='false' placeholder="5m">
<info-popover mode="right-absolute">
If an alert rule has a configured For and the query violates the configured threshold it will first go from OK to Pending.
Going from OK to Pending Grafana will not send any notifications. Once the alert rule has been firing for more than For duration, it will change to Alerting and send alert notifications.
</info-popover>
</div>
</div>
</div>
<div class="gf-form-group">
<h5 class="section-heading">Conditions</h5>
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
<div class="gf-form">
<metric-segment-model css-class="query-keyword width-5" ng-if="$index" property="conditionModel.operator.type" options="ctrl.evalOperators" custom="false"></metric-segment-model>
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
</div>
<div class="gf-form">
<query-part-editor class="gf-form-label query-part width-9" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
</query-part-editor>
<span class="gf-form-label query-keyword">OF</span>
</div>
<div class="gf-form">
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
</query-part-editor>
</div>
<div class="gf-form">
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
<input class="gf-form-input max-width-9" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
<input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
</div>
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
<i class="fa fa-trash"></i>
</a>
</label>
</div>
</div>
<div class="gf-form-group">
<h5 class="section-heading">Conditions</h5>
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
<div class="gf-form">
<metric-segment-model css-class="query-keyword width-5" ng-if="$index" property="conditionModel.operator.type" options="ctrl.evalOperators" custom="false"></metric-segment-model>
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
</div>
<div class="gf-form">
<query-part-editor class="gf-form-label query-part width-9" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
</query-part-editor>
<span class="gf-form-label query-keyword">OF</span>
</div>
<div class="gf-form">
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
</query-part-editor>
</div>
<div class="gf-form">
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
<input class="gf-form-input max-width-9" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
<input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
</div>
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
<i class="fa fa-trash"></i>
</a>
</label>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-plus"></i>
</a>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
</li>
</ul>
</label>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-plus"></i>
</a>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
</li>
</ul>
</label>
</div>
</div>
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label width-18">If no data or all values are null</span>
<span class="gf-form-label query-keyword">SET STATE TO</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="ctrl.alert.noDataState" ng-options="f.value as f.text for f in ctrl.noDataModes">
</select>
</div>
</div>
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label width-18">If no data or all values are null</span>
<span class="gf-form-label query-keyword">SET STATE TO</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="ctrl.alert.noDataState" ng-options="f.value as f.text for f in ctrl.noDataModes">
</select>
</div>
</div>
<div class="gf-form">
<span class="gf-form-label width-18">If execution error or timeout</span>
<span class="gf-form-label query-keyword">SET STATE TO</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="ctrl.alert.executionErrorState" ng-options="f.value as f.text for f in ctrl.executionErrorModes">
</select>
</div>
</div>
<div class="gf-form">
<span class="gf-form-label width-18">If execution error or timeout</span>
<span class="gf-form-label query-keyword">SET STATE TO</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="ctrl.alert.executionErrorState" ng-options="f.value as f.text for f in ctrl.executionErrorModes">
</select>
</div>
</div>
<div class="gf-form-button-row">
<button class="btn btn-inverse" ng-click="ctrl.test()">
Test Rule
</button>
</div>
</div>
<div class="gf-form-button-row">
<button class="btn btn-inverse" ng-click="ctrl.test()">
Test Rule
</button>
</div>
</div>
<div class="gf-form-group" ng-if="ctrl.testing">
Evaluating rule <i class="fa fa-spinner fa-spin"></i>
</div>
<div class="gf-form-group" ng-if="ctrl.testing">
Evaluating rule <i class="fa fa-spinner fa-spin"></i>
</div>
<div class="gf-form-group" ng-if="ctrl.testResult">
<json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
</div>
</div>
<div class="gf-form-group" ng-if="ctrl.testResult">
<json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
</div>
</div>
<div class="gf-form-group" ng-if="ctrl.subTabIndex === 1">
<h5 class="section-heading">Notifications</h5>
<div class="gf-form-inline">
<div class="gf-form max-width-30">
<span class="gf-form-label width-8">Send to</span>
<span class="gf-form-label" ng-repeat="nc in ctrl.alertNotifications" ng-style="{'background-color': nc.bgColor }">
<i class="{{nc.iconClass}}"></i>&nbsp;{{nc.name}}&nbsp;
<i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
</span>
<metric-segment segment="ctrl.addNotificationSegment" get-options="ctrl.getNotifications()" on-change="ctrl.notificationAdded()"></metric-segment>
</div>
</div>
<div class="gf-form gf-form--v-stretch">
<span class="gf-form-label width-8">Message</span>
<textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message" placeholder="Notification message details..."></textarea>
</div>
</div>
<div class="gf-form-group" ng-if="ctrl.subTabIndex === 1">
<h5 class="section-heading">Notifications</h5>
<div class="gf-form-inline">
<div class="gf-form max-width-30">
<span class="gf-form-label width-8">Send to</span>
<span class="gf-form-label" ng-repeat="nc in ctrl.alertNotifications" ng-style="{'background-color': nc.bgColor }">
<i class="{{nc.iconClass}}"></i>&nbsp;{{nc.name}}&nbsp;
<i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
</span>
<metric-segment segment="ctrl.addNotificationSegment" get-options="ctrl.getNotifications()" on-change="ctrl.notificationAdded()"></metric-segment>
</div>
</div>
<div class="gf-form gf-form--v-stretch">
<span class="gf-form-label width-8">Message</span>
<textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message" placeholder="Notification message details..."></textarea>
</div>
</div>
<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
<button class="btn btn-mini btn-danger pull-right" ng-click="ctrl.clearHistory()"><i class="fa fa-trash"></i>&nbsp;Clear history</button>
<h5 class="section-heading" style="whitespace: nowrap">
State history <span class="muted small">(last 50 state changes)</span>
</h5>
<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
<button class="btn btn-mini btn-danger pull-right" ng-click="ctrl.clearHistory()"><i class="fa fa-trash"></i>&nbsp;Clear history</button>
<h5 class="section-heading" style="whitespace: nowrap">
State history <span class="muted small">(last 50 state changes)</span>
</h5>
<div ng-show="ctrl.alertHistory.length === 0">
<br>
<i>No state changes recorded</i>
</div>
<div ng-show="ctrl.alertHistory.length === 0">
<br>
<i>No state changes recorded</i>
</div>
<ol class="alert-rule-list" >
<li class="alert-rule-item" ng-repeat="al in ctrl.alertHistory">

View File

@@ -99,6 +99,13 @@ function getStateDisplayModel(state) {
stateClass: 'alert-state-warning',
};
}
case 'unknown': {
return {
text: 'UNKNOWN',
iconClass: 'fa fa-question',
stateClass: 'alert-state-paused',
};
}
}
throw { message: 'Unknown alert state' };

View File

@@ -32,7 +32,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv,
if (event.alertId) {
const stateModel = alertDef.getStateDisplayModel(event.newState);
titleStateClass = stateModel.stateClass;
title = `<i class="icon-gf ${stateModel.iconClass}"></i> ${stateModel.text}`;
title = `<i class="${stateModel.iconClass}"></i> ${stateModel.text}`;
text = alertDef.getAlertAnnotationInfo(event);
if (event.text) {
text = text + '<br />' + event.text;

View File

@@ -7,6 +7,7 @@ import {
OK_COLOR,
ALERTING_COLOR,
NO_DATA_COLOR,
PENDING_COLOR,
DEFAULT_ANNOTATION_COLOR,
REGION_FILL_ALPHA,
} from 'app/core/utils/colors';
@@ -71,6 +72,11 @@ export class EventManager {
position: 'BOTTOM',
markerSize: 5,
},
$__pending: {
color: PENDING_COLOR,
position: 'BOTTOM',
markerSize: 5,
},
$__editing: {
color: DEFAULT_ANNOTATION_COLOR,
position: 'BOTTOM',

View File

@@ -77,6 +77,10 @@ export class DashboardSrv {
postSave(clone, data) {
this.dash.version = data.version;
// important that these happens before location redirect below
this.$rootScope.appEvent('dashboard-saved', this.dash);
this.$rootScope.appEvent('alert-success', ['Dashboard saved']);
const newUrl = locationUtil.stripBaseFromUrl(data.url);
const currentPath = this.$location.path();
@@ -84,9 +88,6 @@ export class DashboardSrv {
this.$location.url(newUrl).replace();
}
this.$rootScope.appEvent('dashboard-saved', this.dash);
this.$rootScope.appEvent('alert-success', ['Dashboard saved']);
return this.dash;
}

View File

@@ -105,7 +105,7 @@ export class PanelModel {
setViewMode(fullscreen: boolean, isEditing: boolean) {
this.fullscreen = fullscreen;
this.isEditing = isEditing;
this.events.emit('panel-size-changed');
this.events.emit('view-mode-changed');
}
updateGridPos(newPos: GridPos) {

View File

@@ -126,8 +126,7 @@ export class DashboardViewState {
if (!panel.fullscreen) {
this.enterFullscreen(panel);
} else {
// already in fullscreen view just update the view mode
} else if (this.dashboard.meta.isEditing !== this.state.edit) {
this.dashboard.setViewMode(panel, this.state.fullscreen, this.state.edit);
}
} else if (this.fullscreenPanel) {

View File

@@ -3,8 +3,9 @@ import { hot } from 'react-hot-loader';
import Select from 'react-select';
import _ from 'lodash';
import { DataSource } from 'app/types/datasources';
import { ExploreState, ExploreUrlState, HistoryItem, Query, QueryTransaction, ResultType } from 'app/types/explore';
import { RawTimeRange } from 'app/types/series';
import { RawTimeRange, DataQuery } from 'app/types/series';
import kbn from 'app/core/utils/kbn';
import colors from 'app/core/utils/colors';
import store from 'app/core/store';
@@ -16,7 +17,9 @@ import PickerOption from 'app/core/components/Picker/PickerOption';
import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer';
import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage';
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import Panel from './Panel';
import QueryRows from './QueryRows';
import Graph from './Graph';
import Logs from './Logs';
@@ -24,7 +27,6 @@ import Table from './Table';
import ErrorBoundary from './ErrorBoundary';
import TimePicker from './TimePicker';
import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
import { DataSource } from 'app/types/datasources';
const MAX_HISTORY_ITEMS = 100;
@@ -77,7 +79,7 @@ function updateHistory(history: HistoryItem[], datasourceId: string, queries: st
}
interface ExploreProps {
datasourceSrv: any;
datasourceSrv: DatasourceSrv;
onChangeSplit: (split: boolean, state?: ExploreState) => void;
onSaveState: (key: string, state: ExploreState) => void;
position: string;
@@ -92,6 +94,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
/**
* Current query expressions of the rows including their modifications, used for running queries.
* Not kept in component state to prevent edit-render roundtrips.
* TODO: make this generic (other datasources might not have string representations of current query state)
*/
queryExpressions: string[];
/**
@@ -125,6 +128,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
range: initialRange,
showingGraph: true,
showingLogs: true,
showingStartPage: false,
showingTable: true,
supportsGraph: null,
supportsLogs: null,
@@ -164,7 +168,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
}
}
async setDatasource(datasource: DataSource) {
async setDatasource(datasource: any, origin?: DataSource) {
const supportsGraph = datasource.meta.metrics;
const supportsLogs = datasource.meta.logs;
const supportsTable = datasource.meta.metrics;
@@ -193,12 +197,33 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
datasource.init();
}
// Keep queries but reset edit state
// Check if queries can be imported from previously selected datasource
let queryExpressions = this.queryExpressions;
if (origin) {
if (origin.meta.id === datasource.meta.id) {
// Keep same queries if same type of datasource
queryExpressions = [...this.queryExpressions];
} else if (datasource.importQueries) {
// Datasource-specific importers, wrapping to satisfy interface
const wrappedQueries: DataQuery[] = this.queryExpressions.map((query, index) => ({
refId: String(index),
expr: query,
}));
const modifiedQueries: DataQuery[] = await datasource.importQueries(wrappedQueries, origin.meta);
queryExpressions = modifiedQueries.map(({ expr }) => expr);
} else {
// Default is blank queries
queryExpressions = this.queryExpressions.map(() => '');
}
}
// Reset edit state with new queries
const nextQueries = this.state.queries.map((q, i) => ({
...q,
key: generateQueryKey(i),
query: this.queryExpressions[i],
query: queryExpressions[i],
}));
this.queryExpressions = queryExpressions;
// Custom components
const StartPage = datasource.pluginExports.ExploreStartPage;
@@ -215,6 +240,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
datasourceLoading: false,
datasourceName: datasource.name,
queries: nextQueries,
showingStartPage: Boolean(StartPage),
},
() => {
if (datasourceError === null) {
@@ -258,6 +284,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
};
onChangeDatasource = async option => {
const origin = this.state.datasource;
this.setState({
datasource: null,
datasourceError: null,
@@ -266,7 +293,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
});
const datasourceName = option.value;
const datasource = await this.props.datasourceSrv.get(datasourceName);
this.setDatasource(datasource);
this.setDatasource(datasource as any, origin);
};
onChangeQuery = (value: string, index: number, override?: boolean) => {
@@ -305,10 +332,11 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
onClickClear = () => {
this.queryExpressions = [''];
this.setState(
{
prevState => ({
queries: ensureQueries(),
queryTransactions: [],
},
showingStartPage: Boolean(prevState.StartPage),
}),
this.saveState
);
};
@@ -539,6 +567,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
return {
queryTransactions: nextQueryTransactions,
showingStartPage: false,
};
});
@@ -765,16 +794,13 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
range,
showingGraph,
showingLogs,
showingStartPage,
showingTable,
supportsGraph,
supportsLogs,
supportsTable,
} = this.state;
const showingBoth = showingGraph && showingTable;
const graphHeight = showingBoth ? '200px' : '400px';
const graphButtonActive = showingBoth || showingGraph ? 'active' : '';
const logsButtonActive = showingLogs ? 'active' : '';
const tableButtonActive = showingBoth || showingTable ? 'active' : '';
const graphHeight = showingGraph && showingTable ? '200px' : '400px';
const exploreClass = split ? 'explore explore-split' : 'explore';
const selectedDatasource = datasource ? exploreDatasources.find(d => d.label === datasource.name) : undefined;
const graphRangeIntervals = getIntervals(graphRange, datasource, this.el ? this.el.offsetWidth : 0);
@@ -799,8 +825,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
)
: undefined;
const loading = queryTransactions.some(qt => !qt.done);
const showStartPages = StartPage && queryTransactions.length === 0;
const viewModeCount = [supportsGraph, supportsLogs, supportsTable].filter(m => m).length;
return (
<div className={exploreClass} ref={this.getRef}>
@@ -889,55 +913,47 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
/>
<main className="m-t-2">
<ErrorBoundary>
{showStartPages && <StartPage onClickQuery={this.onClickQuery} />}
{!showStartPages && (
{showingStartPage && <StartPage onClickQuery={this.onClickQuery} />}
{!showingStartPage && (
<>
{viewModeCount > 1 && (
<div className="result-options">
{supportsGraph ? (
<button className={`btn toggle-btn ${graphButtonActive}`} onClick={this.onClickGraphButton}>
Graph
</button>
) : null}
{supportsTable ? (
<button className={`btn toggle-btn ${tableButtonActive}`} onClick={this.onClickTableButton}>
Table
</button>
) : null}
{supportsLogs ? (
<button className={`btn toggle-btn ${logsButtonActive}`} onClick={this.onClickLogsButton}>
Logs
</button>
) : null}
</div>
)}
{supportsGraph &&
showingGraph && (
{supportsGraph && (
<Panel
label="Graph"
isOpen={showingGraph}
loading={graphLoading}
onToggle={this.onClickGraphButton}
>
<Graph
data={graphResult}
height={graphHeight}
loading={graphLoading}
id={`explore-graph-${position}`}
onChangeTime={this.onChangeTime}
range={graphRange}
split={split}
/>
)}
{supportsTable && showingTable ? (
<div className="panel-container m-t-2">
</Panel>
)}
{supportsTable && (
<Panel
label="Table"
loading={tableLoading}
isOpen={showingTable}
onToggle={this.onClickTableButton}
>
<Table data={tableResult} loading={tableLoading} onClickCell={this.onClickTableCell} />
</div>
) : null}
{supportsLogs && showingLogs ? (
<Logs
data={logsResult}
loading={logsLoading}
position={position}
onChangeTime={this.onChangeTime}
range={range}
/>
) : null}
</Panel>
)}
{supportsLogs && (
<Panel label="Logs" loading={logsLoading} isOpen={showingLogs} onToggle={this.onClickLogsButton}>
<Logs
data={logsResult}
loading={logsLoading}
position={position}
onChangeTime={this.onChangeTime}
range={range}
/>
</Panel>
)}
</>
)}
</ErrorBoundary>

View File

@@ -77,7 +77,6 @@ interface GraphProps {
data: any[];
height?: string; // e.g., '200px'
id?: string;
loading?: boolean;
range: RawTimeRange;
split?: boolean;
size?: { width: number; height: number };
@@ -188,12 +187,11 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
}
render() {
const { height = '100px', id = 'graph', loading = false } = this.props;
const { height = '100px', id = 'graph' } = this.props;
const data = this.getGraphData();
return (
<div className="panel-container">
{loading && <div className="explore-panel__loader" />}
<>
{this.props.data &&
this.props.data.length > MAX_NUMBER_OF_TIME_SERIES &&
!this.state.showAllTimeSeries && (
@@ -207,7 +205,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
)}
<div id={id} className="explore-graph" style={{ height }} />
<Legend data={data} />
</div>
</>
);
}
}

View File

@@ -2,7 +2,7 @@ import React, { Fragment, PureComponent } from 'react';
import Highlighter from 'react-highlight-words';
import { RawTimeRange } from 'app/types/series';
import { LogsModel } from 'app/core/logs_model';
import { LogsDedupStrategy, LogsModel, dedupLogRows } from 'app/core/logs_model';
import { findHighlightChunksInText } from 'app/core/utils/text';
import { Switch } from 'app/core/components/Switch/Switch';
@@ -32,6 +32,7 @@ interface LogsProps {
}
interface LogsState {
dedup: LogsDedupStrategy;
showLabels: boolean;
showLocalTime: boolean;
showUtc: boolean;
@@ -39,11 +40,21 @@ interface LogsState {
export default class Logs extends PureComponent<LogsProps, LogsState> {
state = {
dedup: LogsDedupStrategy.none,
showLabels: true,
showLocalTime: true,
showUtc: false,
};
onChangeDedup = (dedup: LogsDedupStrategy) => {
this.setState(prevState => {
if (prevState.dedup === dedup) {
return { dedup: LogsDedupStrategy.none };
}
return { dedup };
});
};
onChangeLabels = (event: React.SyntheticEvent) => {
const target = event.target as HTMLInputElement;
this.setState({
@@ -67,9 +78,18 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
render() {
const { className = '', data, loading = false, position, range } = this.props;
const { showLabels, showLocalTime, showUtc } = this.state;
const { dedup, showLabels, showLocalTime, showUtc } = this.state;
const hasData = data && data.rows && data.rows.length > 0;
const cssColumnSizes = ['4px'];
const dedupedData = dedupLogRows(data, dedup);
const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0);
const meta = [...data.meta];
if (dedup !== LogsDedupStrategy.none) {
meta.push({
label: 'Dedup count',
value: String(dedupCount),
});
}
const cssColumnSizes = ['3px']; // Log-level indicator line
if (showUtc) {
cssColumnSizes.push('minmax(100px, max-content)');
}
@@ -97,15 +117,39 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
/>
</div>
<div className="panel-container logs-options">
<div className="logs-options">
<div className="logs-controls">
<Switch label="Timestamp" checked={showUtc} onChange={this.onChangeUtc} small />
<Switch label="Local time" checked={showLocalTime} onChange={this.onChangeLocalTime} small />
<Switch label="Labels" checked={showLabels} onChange={this.onChangeLabels} small />
<Switch
label="Dedup: off"
checked={dedup === LogsDedupStrategy.none}
onChange={() => this.onChangeDedup(LogsDedupStrategy.none)}
small
/>
<Switch
label="Dedup: exact"
checked={dedup === LogsDedupStrategy.exact}
onChange={() => this.onChangeDedup(LogsDedupStrategy.exact)}
small
/>
<Switch
label="Dedup: numbers"
checked={dedup === LogsDedupStrategy.numbers}
onChange={() => this.onChangeDedup(LogsDedupStrategy.numbers)}
small
/>
<Switch
label="Dedup: signature"
checked={dedup === LogsDedupStrategy.signature}
onChange={() => this.onChangeDedup(LogsDedupStrategy.signature)}
small
/>
{hasData &&
data.meta && (
meta && (
<div className="logs-meta">
{data.meta.map(item => (
{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>
@@ -116,33 +160,38 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
</div>
</div>
<div className="panel-container">
{loading && <div className="explore-panel__loader" />}
<div className="logs-entries" style={logEntriesStyle}>
{hasData &&
data.rows.map(row => (
<Fragment key={row.key}>
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''} />
{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-entries" style={logEntriesStyle}>
{hasData &&
dedupedData.rows.map(row => (
<Fragment key={row.key}>
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''}>
{row.duplicates > 0 && (
<div className="logs-row-level__duplicates" title={`${row.duplicates} duplicates`}>
{Array.apply(null, { length: row.duplicates }).map(index => (
<div className="logs-row-level__duplicate" key={`${index}`} />
))}
</div>
)}
<div>
<Highlighter
textToHighlight={row.entry}
searchWords={row.searchWords}
findChunks={findHighlightChunksInText}
highlightClassName="logs-row-match-highlight"
/>
</div>
{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>
</Fragment>
))}
</div>
{!loading && !hasData && 'No data was returned.'}
)}
<div>
<Highlighter
textToHighlight={row.entry}
searchWords={row.searchWords}
findChunks={findHighlightChunksInText}
highlightClassName="logs-row-match-highlight"
/>
</div>
</Fragment>
))}
</div>
{!loading && !hasData && 'No data was returned.'}
</div>
);
}

View File

@@ -0,0 +1,34 @@
import React, { PureComponent } from 'react';
interface Props {
isOpen: boolean;
label: string;
loading?: boolean;
onToggle: (isOpen: boolean) => void;
}
export default class Panel extends PureComponent<Props> {
onClickToggle = () => this.props.onToggle(!this.props.isOpen);
render() {
const { isOpen, loading } = this.props;
const iconClass = isOpen ? 'fa fa-caret-up' : 'fa fa-caret-down';
const loaderClass = loading ? 'explore-panel__loader explore-panel__loader--active' : 'explore-panel__loader';
return (
<div className="explore-panel panel-container">
<div className="explore-panel__header" onClick={this.onClickToggle}>
<div className="explore-panel__header-buttons">
<span className={iconClass} />
</div>
<div className="explore-panel__header-label">{this.props.label}</div>
</div>
{isOpen && (
<div className="explore-panel__body">
<div className={loaderClass} />
{this.props.children}
</div>
)}
</div>
);
}
}

View File

@@ -1,9 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div
className="panel-container"
>
<Fragment>
<div
className="explore-graph"
id="graph"
@@ -456,13 +454,11 @@ exports[`Render should render component 1`] = `
]
}
/>
</div>
</Fragment>
`;
exports[`Render should render component with disclaimer 1`] = `
<div
className="panel-container"
>
<Fragment>
<div
className="time-series-disclaimer"
>
@@ -952,13 +948,11 @@ exports[`Render should render component with disclaimer 1`] = `
]
}
/>
</div>
</Fragment>
`;
exports[`Render should show query return no time series 1`] = `
<div
className="panel-container"
>
<Fragment>
<div
className="explore-graph"
id="graph"
@@ -971,5 +965,5 @@ exports[`Render should show query return no time series 1`] = `
<Legend
data={Array []}
/>
</div>
</Fragment>
`;

View File

@@ -82,7 +82,6 @@ export class MetricsTabCtrl {
return;
}
this.datasourceInstance = option.datasource;
this.setDatasource(option.datasource);
this.updateDatasourceOptions();
}
@@ -102,6 +101,7 @@ export class MetricsTabCtrl {
});
}
this.datasourceInstance = datasource;
this.panel.datasource = datasource.value;
this.panel.refresh();
}

View File

@@ -19,7 +19,7 @@ export class DatasourceSrv {
this.datasources = {};
}
get(name?): Promise<DataSourceApi> {
get(name?: string): Promise<DataSourceApi> {
if (!name) {
return this.get(config.defaultDatasource);
}
@@ -37,7 +37,7 @@ export class DatasourceSrv {
return this.loadDatasource(name);
}
loadDatasource(name) {
loadDatasource(name: string): Promise<DataSourceApi> {
const dsConfig = config.datasources[name];
if (!dsConfig) {
return this.$q.reject({ message: 'Datasource named ' + name + ' was not found' });

View File

@@ -118,7 +118,7 @@ export class DataSourceEditCtrl {
}
testDatasource() {
this.datasourceSrv.get(this.current.name).then(datasource => {
return this.datasourceSrv.get(this.current.name).then(datasource => {
if (!datasource.testDatasource) {
return;
}
@@ -126,7 +126,7 @@ export class DataSourceEditCtrl {
this.testing = { done: false, status: 'error' };
// make test call in no backend cache context
this.backendSrv
return this.backendSrv
.withNoBackendCache(() => {
return datasource
.testDatasource()
@@ -161,8 +161,8 @@ export class DataSourceEditCtrl {
return this.backendSrv.put('/api/datasources/' + this.current.id, this.current).then(result => {
this.current = result.datasource;
this.updateNav();
this.updateFrontendSettings().then(() => {
this.testDatasource();
return this.updateFrontendSettings().then(() => {
return this.testDatasource();
});
});
} else {

View File

@@ -124,7 +124,11 @@ export class TeamGroupSync extends PureComponent<Props, State> {
</button>
<div className="empty-list-cta__pro-tip">
<i className="fa fa-rocket" /> {headerTooltip}
<a className="text-link empty-list-cta__pro-tip-link" href="asd" target="_blank">
<a
className="text-link empty-list-cta__pro-tip-link"
href="http://docs.grafana.org/auth/enhanced_ldap/"
target="_blank"
>
Learn more
</a>
</div>

View File

@@ -74,7 +74,7 @@ export class TeamMembers extends PureComponent<Props, State> {
</td>
<td>{member.login}</td>
<td>{member.email}</td>
{syncEnabled ? this.renderLabels(member.labels) : null}
{syncEnabled && this.renderLabels(member.labels)}
<td className="text-right">
<DeleteButton onConfirmDelete={() => this.onRemoveMember(member)} />
</td>
@@ -132,7 +132,7 @@ export class TeamMembers extends PureComponent<Props, State> {
<th />
<th>Name</th>
<th>Email</th>
{syncEnabled ? <th /> : ''}
{syncEnabled && <th />}
<th style={{ width: '1%' }} />
</tr>
</thead>

View File

@@ -96,7 +96,7 @@ exports[`Render should render component 1`] = `
Sync LDAP or OAuth groups with your Grafana teams.
<a
className="text-link empty-list-cta__pro-tip-link"
href="asd"
href="http://docs.grafana.org/auth/enhanced_ldap/"
target="_blank"
>
Learn more