Merge branch 'master' into develop

This commit is contained in:
Torkel Ödegaard
2018-12-11 10:00:29 +01:00
29 changed files with 558 additions and 138 deletions

View File

@@ -16,7 +16,7 @@ export function registerAngularDirectives() {
react2AngularDirective('searchResult', SearchResult, []);
react2AngularDirective('tagFilter', TagFilter, [
'tags',
['onSelect', { watchDepth: 'reference' }],
['onChange', { watchDepth: 'reference' }],
['tagOptions', { watchDepth: 'reference' }],
]);
}

View File

@@ -10,7 +10,7 @@ import ResetStyles from 'app/core/components/Picker/ResetStyles';
export interface Props {
tags: string[];
tagOptions: () => any;
onSelect: (tag: string) => void;
onChange: (tags: string[]) => void;
}
export class TagFilter extends React.Component<Props, any> {
@@ -18,12 +18,9 @@ export class TagFilter extends React.Component<Props, any> {
constructor(props) {
super(props);
this.searchTags = this.searchTags.bind(this);
this.onChange = this.onChange.bind(this);
}
searchTags(query) {
onLoadOptions = query => {
return this.props.tagOptions().then(options => {
return options.map(option => ({
value: option.term,
@@ -31,18 +28,20 @@ export class TagFilter extends React.Component<Props, any> {
count: option.count,
}));
});
}
};
onChange(newTags) {
this.props.onSelect(newTags);
}
onChange = (newTags: any[]) => {
this.props.onChange(newTags.map(tag => tag.value));
};
render() {
const tags = this.props.tags.map(tag => ({ value: tag, label: tag, count: 0 }));
const selectOptions = {
classNamePrefix: 'gf-form-select-box',
isMulti: true,
defaultOptions: true,
loadOptions: this.searchTags,
loadOptions: this.onLoadOptions,
onChange: this.onChange,
className: 'gf-form-input gf-form-input--form-dropdown',
placeholder: 'Tags',
@@ -50,7 +49,7 @@ export class TagFilter extends React.Component<Props, any> {
noOptionsMessage: () => 'No tags found',
getOptionValue: i => i.value,
getOptionLabel: i => i.label,
value: this.props.tags,
value: tags,
styles: ResetStyles,
components: {
Option: TagOption,

View File

@@ -1,4 +1,5 @@
import React, { SFC, ReactNode, PureComponent } from 'react';
import Tooltip from 'app/core/components/Tooltip/Tooltip';
interface ToggleButtonGroupProps {
label?: string;
@@ -25,9 +26,17 @@ interface ToggleButtonProps {
value: any;
className?: string;
children: ReactNode;
tooltip?: string;
}
export const ToggleButton: SFC<ToggleButtonProps> = ({ children, selected, className = '', value, onChange }) => {
export const ToggleButton: SFC<ToggleButtonProps> = ({
children,
selected,
className = '',
value = null,
tooltip,
onChange,
}) => {
const handleChange = event => {
event.stopPropagation();
if (onChange) {
@@ -36,9 +45,15 @@ export const ToggleButton: SFC<ToggleButtonProps> = ({ children, selected, class
};
const btnClassName = `btn ${className} ${selected ? 'active' : ''}`;
return (
const button = (
<button className={btnClassName} onClick={handleChange}>
<span>{children}</span>
</button>
);
if (tooltip) {
return <Tooltip content={tooltip}>{button}</Tooltip>;
} else {
return button;
}
};

View File

@@ -44,7 +44,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
const drop = new Drop({
target: this.pickerElem,
content: dropContentElem,
position: 'top center',
position: 'bottom center',
classes: 'drop-popover',
openOn: 'hover',
hoverCloseDelay: 200,

View File

@@ -41,7 +41,7 @@
</a>
</div>
<tag-filter tags="ctrl.query.tag" tagOptions="ctrl.getTags" onSelect="ctrl.onTagSelect">
<tag-filter tags="ctrl.query.tag" tagOptions="ctrl.getTags" onChange="ctrl.onTagFiltersChanged">
</tag-filter>
</div>

View File

@@ -25,8 +25,6 @@ export class SearchCtrl {
appEvents.on('hide-dash-search', this.closeSearch.bind(this), $scope);
this.initialFolderFilterTitle = 'All';
this.getTags = this.getTags.bind(this);
this.onTagSelect = this.onTagSelect.bind(this);
this.isEditor = contextSrv.isEditor;
this.hasEditPermissionInFolders = contextSrv.hasEditPermissionInFolders;
}
@@ -162,7 +160,7 @@ export class SearchCtrl {
const localSearchId = this.currentSearchId;
const query = {
...this.query,
tag: this.query.tag.map(i => i.value),
tag: this.query.tag,
};
return this.searchSrv.search(query).then(results => {
@@ -195,14 +193,14 @@ export class SearchCtrl {
evt.preventDefault();
}
getTags() {
getTags = () => {
return this.searchSrv.getDashboardTags();
}
};
onTagSelect(newTags) {
this.query.tag = newTags;
onTagFiltersChanged = (tags: string[]) => {
this.query.tag = tags;
this.search();
}
};
clearSearchFilter() {
this.query.tag = [];

View File

@@ -15,7 +15,7 @@ const TopSectionItem: SFC<Props> = props => {
{link.img && <img src={link.img} />}
</span>
</a>
{link.children && <SideMenuDropDown link={link} />}
<SideMenuDropDown link={link} />
</div>
);
};

View File

@@ -13,5 +13,8 @@ exports[`Render should render component 1`] = `
<i />
</span>
</a>
<SideMenuDropDown
link={Object {}}
/>
</div>
`;

View File

@@ -88,6 +88,13 @@ export interface LogsStreamLabels {
[key: string]: string;
}
export enum LogsDedupDescription {
none = 'No de-duplication',
exact = 'De-duplication of successive lines that are identical, ignoring ISO datetimes.',
numbers = 'De-duplication of successive lines that are identical when ignoring numbers, e.g., IP addresses, latencies.',
signature = 'De-duplication of successive lines that have identical punctuation and whitespace.',
}
export enum LogsDedupStrategy {
none = 'none',
exact = 'exact',
@@ -242,32 +249,47 @@ export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSerie
// Graph time series by log level
const seriesByLevel = {};
const bucketSize = intervalMs * 10;
const seriesList = [];
for (const row of rows) {
if (!seriesByLevel[row.logLevel]) {
seriesByLevel[row.logLevel] = { lastTs: null, datapoints: [], alias: row.logLevel };
let series = seriesByLevel[row.logLevel];
if (!series) {
seriesByLevel[row.logLevel] = series = {
lastTs: null,
datapoints: [],
alias: row.logLevel,
color: LogLevelColor[row.logLevel],
};
seriesList.push(series);
}
const levelSeries = seriesByLevel[row.logLevel];
// Bucket to nearest minute
// align time to bucket size
const time = Math.round(row.timeEpochMs / bucketSize) * bucketSize;
// Entry for time
if (time === levelSeries.lastTs) {
levelSeries.datapoints[levelSeries.datapoints.length - 1][0]++;
if (time === series.lastTs) {
series.datapoints[series.datapoints.length - 1][0]++;
} else {
levelSeries.datapoints.push([1, time]);
levelSeries.lastTs = time;
series.datapoints.push([1, time]);
series.lastTs = time;
}
// add zero to other levels to aid stacking so each level series has same number of points
for (const other of seriesList) {
if (other !== series && other.lastTs !== time) {
other.datapoints.push([0, time]);
other.lastTs = time;
}
}
}
return Object.keys(seriesByLevel).reduce((acc, level) => {
if (seriesByLevel[level]) {
const gs = new TimeSeries(seriesByLevel[level]);
gs.setColor(LogLevelColor[level]);
acc.push(gs);
}
return acc;
}, []);
return seriesList.map(series => {
series.datapoints.sort((a, b) => {
return a[1] - b[1];
});
return new TimeSeries(series);
});
}

View File

@@ -40,8 +40,8 @@ import Graph from './Graph';
import Logs from './Logs';
import Table from './Table';
import ErrorBoundary from './ErrorBoundary';
import TimePicker from './TimePicker';
import { Alert } from './Error';
import TimePicker, { parseTime } from './TimePicker';
interface ExploreProps {
datasourceSrv: DatasourceSrv;
@@ -119,7 +119,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
} else {
const { datasource, queries, range } = props.urlState as ExploreUrlState;
initialQueries = ensureQueries(queries);
const initialRange = range || { ...DEFAULT_RANGE };
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) } || { ...DEFAULT_RANGE };
// Millies step for helper bar charts
const initialGraphInterval = 15 * 1000;
this.state = {
@@ -687,7 +687,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
}
this.setState(state => {
const { history, queryTransactions, scanning } = state;
const { history, queryTransactions } = state;
let { scanning } = state;
// Transaction might have been discarded
const transaction = queryTransactions.find(qt => qt.id === transactionId);
@@ -724,15 +725,21 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
const nextHistory = updateHistory(history, datasourceId, queries);
// Keep scanning for results if this was the last scanning transaction
if (_.size(result) === 0 && scanning) {
const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
if (!other) {
this.scanTimer = setTimeout(this.scanPreviousRange, 1000);
if (scanning) {
if (_.size(result) === 0) {
const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
if (!other) {
this.scanTimer = setTimeout(this.scanPreviousRange, 1000);
}
} else {
// We can stop scanning if we have a result
scanning = false;
}
}
return {
...results,
scanning,
history: nextHistory,
queryTransactions: nextQueryTransactions,
};

View File

@@ -69,7 +69,7 @@ export class Stats extends PureComponent<{
class Label extends PureComponent<
{
allRows?: LogRow[];
getRows?: () => LogRow[];
label: string;
plain?: boolean;
value: string;
@@ -98,13 +98,14 @@ class Label extends PureComponent<
if (state.showStats) {
return { showStats: false, stats: null };
}
const stats = calculateLogsLabelStats(this.props.allRows, this.props.label);
const allRows = this.props.getRows();
const stats = calculateLogsLabelStats(allRows, this.props.label);
return { showStats: true, stats };
});
};
render() {
const { allRows, label, plain, value } = this.props;
const { getRows, label, plain, value } = this.props;
const { showStats, stats } = this.state;
const tooltip = `${label}: ${value}`;
return (
@@ -115,12 +116,12 @@ class Label extends PureComponent<
{!plain && (
<span title="Filter for label" onClick={this.onClickLabel} className="logs-label__icon fa fa-search-plus" />
)}
{!plain && allRows && <span onClick={this.onClickStats} className="logs-label__icon fa fa-signal" />}
{!plain && getRows && <span onClick={this.onClickStats} className="logs-label__icon fa fa-signal" />}
{showStats && (
<span className="logs-label__stats">
<Stats
stats={stats}
rowCount={allRows.length}
rowCount={getRows().length}
label={label}
value={value}
onClickClose={this.onClickClose}
@@ -133,15 +134,15 @@ class Label extends PureComponent<
}
export default class LogLabels extends PureComponent<{
allRows?: LogRow[];
getRows?: () => LogRow[];
labels: LogsStreamLabels;
plain?: boolean;
onClickLabel?: (label: string, value: string) => void;
}> {
render() {
const { allRows, labels, onClickLabel, plain } = this.props;
const { getRows, labels, onClickLabel, plain } = this.props;
return Object.keys(labels).map(key => (
<Label key={key} allRows={allRows} label={key} value={labels[key]} plain={plain} onClickLabel={onClickLabel} />
<Label key={key} getRows={getRows} label={key} value={labels[key]} plain={plain} onClickLabel={onClickLabel} />
));
}
}

View File

@@ -6,6 +6,7 @@ import classnames from 'classnames';
import * as rangeUtil from 'app/core/utils/rangeutil';
import { RawTimeRange } from 'app/types/series';
import {
LogsDedupDescription,
LogsDedupStrategy,
LogsModel,
dedupLogRows,
@@ -56,13 +57,13 @@ const FieldHighlight = onClick => props => {
};
interface RowProps {
allRows: LogRow[];
highlighterExpressions?: string[];
row: LogRow;
showDuplicates: boolean;
showLabels: boolean | null; // Tristate: null means auto
showLocalTime: boolean;
showUtc: boolean;
getRows: () => LogRow[];
onClickLabel?: (label: string, value: string) => void;
}
@@ -107,11 +108,12 @@ class Row extends PureComponent<RowProps, RowState> {
};
onClickHighlight = (fieldText: string) => {
const { allRows } = this.props;
const { getRows } = this.props;
const { parser } = this.state;
const fieldMatch = fieldText.match(parser.fieldRegex);
if (fieldMatch) {
const allRows = getRows();
// Build value-agnostic row matcher based on the field label
const fieldLabel = fieldMatch[1];
const fieldValue = fieldMatch[2];
@@ -151,7 +153,7 @@ class Row extends PureComponent<RowProps, RowState> {
render() {
const {
allRows,
getRows,
highlighterExpressions,
onClickLabel,
row,
@@ -193,7 +195,7 @@ class Row extends PureComponent<RowProps, RowState> {
)}
{showLabels && (
<div className="logs-row__labels">
<LogLabels allRows={allRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
<LogLabels getRows={getRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
</div>
)}
<div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}>
@@ -393,29 +395,11 @@ 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(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...';
// React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead
const getRows = () => processedRows;
return (
<div className="logs-panel">
<div className="logs-panel-graph">
@@ -436,7 +420,13 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
<Switch label="Labels" checked={showLabels} onChange={this.onChangeLabels} transparent />
<ToggleButtonGroup label="Dedup" transparent={true}>
{Object.keys(LogsDedupStrategy).map((dedupType, i) => (
<ToggleButton key={i} value={dedupType} onChange={this.onChangeDedup} selected={dedup === dedupType}>
<ToggleButton
key={i}
value={dedupType}
onChange={this.onChangeDedup}
selected={dedup === dedupType}
tooltip={LogsDedupDescription[dedupType]}
>
{dedupType}
</ToggleButton>
))}
@@ -463,7 +453,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
firstRows.map(row => (
<Row
key={row.key + row.duplicates}
allRows={processedRows}
getRows={getRows}
highlighterExpressions={highlighterExpressions}
row={row}
showDuplicates={showDuplicates}
@@ -479,7 +469,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
lastRows.map(row => (
<Row
key={row.key + row.duplicates}
allRows={processedRows}
getRows={getRows}
row={row}
showDuplicates={showDuplicates}
showLabels={showLabels}

View File

@@ -15,11 +15,14 @@ export const DEFAULT_RANGE = {
* Return a human-editable string of either relative (inludes "now") or absolute local time (in the shape of DATE_FORMAT).
* @param value Epoch or relative time
*/
export function parseTime(value: string, isUtc = false): string {
export function parseTime(value: string | moment.Moment, isUtc = false, ensureString = false): string | moment.Moment {
if (moment.isMoment(value)) {
if (ensureString) {
return value.format(DATE_FORMAT);
}
return value;
}
if (value.indexOf('now') !== -1) {
if ((value as string).indexOf('now') !== -1) {
return value;
}
let time: any = value;
@@ -50,6 +53,16 @@ interface TimePickerState {
toRaw: string;
}
/**
* TimePicker with dropdown menu for relative dates.
*
* Initialize with a range that is either based on relative time strings,
* or on Moment objects.
* Internally the component needs to keep a string representation in `fromRaw`
* and `toRaw` for the controlled inputs.
* When a time is picked, `onChangeTime` is called with the new range that
* is again based on relative time strings or Moment objects.
*/
export default class TimePicker extends PureComponent<TimePickerProps, TimePickerState> {
dropdownEl: any;
@@ -75,9 +88,9 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
const from = props.range ? props.range.from : DEFAULT_RANGE.from;
const to = props.range ? props.range.to : DEFAULT_RANGE.to;
// Ensure internal format
const fromRaw = parseTime(from, props.isUtc);
const toRaw = parseTime(to, props.isUtc);
// Ensure internal string format
const fromRaw = parseTime(from, props.isUtc, true);
const toRaw = parseTime(to, props.isUtc, true);
const range = {
from: fromRaw,
to: toRaw,

View File

@@ -95,10 +95,17 @@ export class MetricsTabCtrl {
target.datasource = config.defaultDatasource;
}
});
} else if (this.datasourceInstance && this.datasourceInstance.meta.mixed) {
_.each(this.panel.targets, target => {
delete target.datasource;
});
} else if (this.datasourceInstance) {
// if switching from mixed
if (this.datasourceInstance.meta.mixed) {
_.each(this.panel.targets, target => {
delete target.datasource;
});
} else if (this.datasourceInstance.meta.id !== datasource.meta.id) {
// we are changing data source type, clear queries
this.panel.targets = [{ refId: 'A' }];
this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
}
}
this.datasourceInstance = datasource;

View File

@@ -38,8 +38,9 @@ export class CustomVariable implements Variable {
}
updateOptions() {
// extract options in comma separated string
this.options = _.map(this.query.split(/[,]+/), text => {
// extract options in comma separated string (use backslash to escape wanted commas)
this.options = _.map(this.query.match(/(?:\\,|[^,])+/g), text => {
text = text.replace('\\,', ',');
return { text: text.trim(), value: text.trim() };
});

View File

@@ -151,7 +151,7 @@
<h5 class="section-heading">Custom Options</h5>
<div class="gf-form">
<span class="gf-form-label width-14">Values separated by comma</span>
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue"
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue, escaped\,value"
required></input>
</div>
</div>

View File

@@ -493,15 +493,17 @@ describe('VariableSrv', function(this: any) {
scenario.setup(() => {
scenario.variableModel = {
type: 'custom',
query: 'hej, hop, asd',
query: 'hej, hop, asd, escaped\\,var',
name: 'test',
};
});
it('should update options array', () => {
expect(scenario.variable.options.length).toBe(3);
expect(scenario.variable.options.length).toBe(4);
expect(scenario.variable.options[0].text).toBe('hej');
expect(scenario.variable.options[1].value).toBe('hop');
expect(scenario.variable.options[2].value).toBe('asd');
expect(scenario.variable.options[3].value).toBe('escaped,var');
});
});

View File

@@ -130,6 +130,33 @@ describe('TimeRegionManager', () => {
});
});
plotOptionsScenario('for time from/to region', ctx => {
const regions = [{ from: '00:00', to: '05:00', fill: true, colorMode: 'red' }];
const from = moment('2018-12-01T00:00+01:00');
const to = moment('2018-12-03T23:59+01:00');
ctx.setup(regions, from, to);
it('should add 3 markings', () => {
expect(ctx.options.grid.markings.length).toBe(3);
});
it('should add one fill between 00:00 and 05:00 each day', () => {
const markings = ctx.options.grid.markings;
expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-12-01T01:00:00+01:00').format());
expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-12-01T06:00:00+01:00').format());
expect(markings[0].color).toBe(colorModes.red.color.fill);
expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-12-02T01:00:00+01:00').format());
expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-12-02T06:00:00+01:00').format());
expect(markings[1].color).toBe(colorModes.red.color.fill);
expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-12-03T01:00:00+01:00').format());
expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-12-03T06:00:00+01:00').format());
expect(markings[2].color).toBe(colorModes.red.color.fill);
});
});
plotOptionsScenario('for day of week from/to region', ctx => {
const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }];
const from = moment('2018-01-01T18:45:05+01:00');
@@ -211,6 +238,42 @@ describe('TimeRegionManager', () => {
});
});
plotOptionsScenario('for day of week from/to time region', ctx => {
const regions = [{ fromDayOfWeek: 7, from: '23:00', toDayOfWeek: 1, to: '01:40', fill: true, colorMode: 'red' }];
const from = moment('2018-12-07T12:51:19+01:00');
const to = moment('2018-12-10T13:51:29+01:00');
ctx.setup(regions, from, to);
it('should add 1 marking', () => {
expect(ctx.options.grid.markings.length).toBe(1);
});
it('should add one fill between sunday 23:00 and monday 01:40', () => {
const markings = ctx.options.grid.markings;
expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-12-10T00:00:00+01:00').format());
expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-12-10T02:40:00+01:00').format());
});
});
plotOptionsScenario('for day of week from/to time region', ctx => {
const regions = [{ fromDayOfWeek: 6, from: '03:00', toDayOfWeek: 7, to: '02:00', fill: true, colorMode: 'red' }];
const from = moment('2018-12-07T12:51:19+01:00');
const to = moment('2018-12-10T13:51:29+01:00');
ctx.setup(regions, from, to);
it('should add 1 marking', () => {
expect(ctx.options.grid.markings.length).toBe(1);
});
it('should add one fill between saturday 03:00 and sunday 02:00', () => {
const markings = ctx.options.grid.markings;
expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-12-08T04:00:00+01:00').format());
expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-12-09T03:00:00+01:00').format());
});
});
plotOptionsScenario('for day of week from/to time region with daylight saving time', ctx => {
const regions = [{ fromDayOfWeek: 7, from: '20:00', toDayOfWeek: 7, to: '23:00', fill: true, colorMode: 'red' }];
const from = moment('2018-03-17T06:00:00+01:00');

View File

@@ -87,6 +87,14 @@ export class TimeRegionManager {
continue;
}
if (timeRegion.from && !timeRegion.to) {
timeRegion.to = timeRegion.from;
}
if (!timeRegion.from && timeRegion.to) {
timeRegion.from = timeRegion.to;
}
hRange = {
from: this.parseTimeRange(timeRegion.from),
to: this.parseTimeRange(timeRegion.to),
@@ -108,21 +116,13 @@ export class TimeRegionManager {
hRange.to.dayOfWeek = Number(timeRegion.toDayOfWeek);
}
if (!hRange.from.h && hRange.to.h) {
hRange.from = hRange.to;
}
if (hRange.from.h && !hRange.to.h) {
hRange.to = hRange.from;
}
if (hRange.from.dayOfWeek && !hRange.from.h && !hRange.from.m) {
if (hRange.from.dayOfWeek && hRange.from.h === null && hRange.from.m === null) {
hRange.from.h = 0;
hRange.from.m = 0;
hRange.from.s = 0;
}
if (hRange.to.dayOfWeek && !hRange.to.h && !hRange.to.m) {
if (hRange.to.dayOfWeek && hRange.to.h === null && hRange.to.m === null) {
hRange.to.h = 23;
hRange.to.m = 59;
hRange.to.s = 59;
@@ -169,8 +169,16 @@ export class TimeRegionManager {
fromEnd.add(hRange.to.h - hRange.from.h, 'hours');
} else if (hRange.from.h + hRange.to.h < 23) {
fromEnd.add(hRange.to.h, 'hours');
while (fromEnd.hour() !== hRange.to.h) {
fromEnd.add(-1, 'hours');
}
} else {
fromEnd.add(24 - hRange.from.h, 'hours');
while (fromEnd.hour() !== hRange.to.h) {
fromEnd.add(1, 'hours');
}
}
fromEnd.set('minute', hRange.to.m);

View File

@@ -107,7 +107,10 @@ class SingleStatCtrl extends MetricsPanelCtrl {
}
onDataReceived(dataList) {
const data: any = {};
const data: any = {
scopedVars: _.extend({}, this.panel.scopedVars),
};
if (dataList.length > 0 && dataList[0].type === 'table') {
this.dataType = 'table';
const tableData = dataList.map(this.tableHandler.bind(this));
@@ -117,6 +120,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
this.series = dataList.map(this.seriesHandler.bind(this));
this.setValues(data);
}
this.data = data;
this.render();
}
@@ -320,7 +324,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
}
// Add $__name variable for using in prefix or postfix
data.scopedVars = _.extend({}, this.panel.scopedVars);
data.scopedVars['__name'] = { value: this.series[0].label };
}
this.setValueMapping(data);

View File

@@ -139,7 +139,7 @@ $column-horizontal-spacing: 10px;
&--warning,
&--warn {
&::after {
background-color: $warn;
background-color: $yellow;
}
}

View File

@@ -178,7 +178,7 @@
</p>
<p>
1. This could be caused by your reverse proxy settings.<br /><br />
2. If you host grafana under subpath make sure your grafana.ini root_path setting includes subpath<br /> <br />
2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath<br /> <br />
3. If you have a local dev build make sure you build frontend using: npm run dev, npm run watch, or npm run
build<br /> <br />
4. Sometimes restarting grafana-server can help<br />