mirror of
https://github.com/grafana/grafana.git
synced 2025-02-03 12:11:09 -06:00
Merge branch 'master' into panel-edit-ux
This commit is contained in:
commit
5dd9247762
@ -927,6 +927,123 @@
|
||||
"title": "",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 0,
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 16,
|
||||
"x": 0,
|
||||
"y": 44
|
||||
},
|
||||
"id": 21,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [
|
||||
{
|
||||
"alias": "C-series",
|
||||
"steppedLine": true
|
||||
}
|
||||
],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"alias": "",
|
||||
"hide": false,
|
||||
"refId": "B",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,null,40,null,90,null,null,100,null,null,100,null,null,80,null",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"alias": "",
|
||||
"hide": false,
|
||||
"refId": "C",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "20,null40,null,null,50,null,70,null,100,null,10,null,30,null",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Null between points",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "Left is showing null between values for a normal line graph and staircase graph. Orphaned data points should be rendered as points",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 8,
|
||||
"x": 16,
|
||||
"y": 44
|
||||
},
|
||||
"id": 22,
|
||||
"links": [],
|
||||
"mode": "markdown",
|
||||
"title": "",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
@ -939,7 +1056,7 @@
|
||||
"h": 7,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 44
|
||||
"y": 51
|
||||
},
|
||||
"id": 20,
|
||||
"legend": {
|
||||
@ -1024,7 +1141,7 @@
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 51
|
||||
"y": 58
|
||||
},
|
||||
"id": 16,
|
||||
"legend": {
|
||||
@ -1127,7 +1244,7 @@
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 51
|
||||
"y": 58
|
||||
},
|
||||
"id": 17,
|
||||
"legend": {
|
||||
@ -1266,7 +1383,7 @@
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 58
|
||||
"y": 65
|
||||
},
|
||||
"id": 18,
|
||||
"legend": {
|
||||
@ -1370,7 +1487,7 @@
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 58
|
||||
"y": 65
|
||||
},
|
||||
"id": 19,
|
||||
"legend": {
|
||||
@ -1554,5 +1671,5 @@
|
||||
"timezone": "browser",
|
||||
"title": "Panel Tests - Graph",
|
||||
"uid": "5SdHCadmz",
|
||||
"version": 3
|
||||
"version": 1
|
||||
}
|
||||
|
@ -100,12 +100,12 @@ display name, especially if the display name contains spaces or special
|
||||
characters. Make sure you always use the group or subgroup name as it appears
|
||||
in the URL of the group or subgroup.
|
||||
|
||||
Here's a complete example with `alloed_sign_up` enabled, and access limited to
|
||||
Here's a complete example with `allow_sign_up` enabled, and access limited to
|
||||
the `example` and `foo/bar` groups:
|
||||
|
||||
```ini
|
||||
[auth.gitlab]
|
||||
enabled = false
|
||||
enabled = true
|
||||
allow_sign_up = true
|
||||
client_id = GITLAB_APPLICATION_ID
|
||||
client_secret = GITLAB_SECRET
|
||||
|
@ -19,3 +19,4 @@ export const Label: SFC<Props> = props => {
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
46
public/app/core/components/Switch/Switch.tsx
Normal file
46
public/app/core/components/Switch/Switch.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
export interface Props {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
labelClass?: string;
|
||||
switchClass?: string;
|
||||
onChange: (event) => any;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
id: any;
|
||||
}
|
||||
|
||||
export class Switch extends PureComponent<Props, State> {
|
||||
state = {
|
||||
id: _.uniqueId(),
|
||||
};
|
||||
|
||||
internalOnChange = event => {
|
||||
event.stopPropagation();
|
||||
this.props.onChange(event);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { labelClass, switchClass, label, checked } = this.props;
|
||||
const labelId = `check-${this.state.id}`;
|
||||
const labelClassName = `gf-form-label ${labelClass} pointer`;
|
||||
const switchClassName = `gf-form-switch ${switchClass}`;
|
||||
|
||||
return (
|
||||
<div className="gf-form">
|
||||
{label && (
|
||||
<label htmlFor={labelId} className={labelClassName}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className={switchClassName}>
|
||||
<input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />
|
||||
<label htmlFor={labelId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -373,9 +373,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
this.onModifyQueries({ type: 'ADD_FILTER', key: columnKey, value: rowValue });
|
||||
};
|
||||
|
||||
onModifyQueries = (action: object, index?: number) => {
|
||||
onModifyQueries = (action, index?: number) => {
|
||||
const { datasource } = this.state;
|
||||
if (datasource && datasource.modifyQuery) {
|
||||
const preventSubmit = action.preventSubmit;
|
||||
this.setState(
|
||||
state => {
|
||||
const { queries, queryTransactions } = state;
|
||||
@ -391,16 +392,26 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
nextQueryTransactions = [];
|
||||
} else {
|
||||
// Modify query only at index
|
||||
nextQueries = [
|
||||
...queries.slice(0, index),
|
||||
{
|
||||
key: generateQueryKey(index),
|
||||
query: datasource.modifyQuery(this.queryExpressions[index], action),
|
||||
},
|
||||
...queries.slice(index + 1),
|
||||
];
|
||||
// Discard transactions related to row query
|
||||
nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
|
||||
nextQueries = queries.map((q, i) => {
|
||||
// Synchronise all queries with local query cache to ensure consistency
|
||||
q.query = this.queryExpressions[i];
|
||||
return i === index
|
||||
? {
|
||||
key: generateQueryKey(index),
|
||||
query: datasource.modifyQuery(q.query, action),
|
||||
}
|
||||
: q;
|
||||
});
|
||||
nextQueryTransactions = queryTransactions
|
||||
// Consume the hint corresponding to the action
|
||||
.map(qt => {
|
||||
if (qt.hints != null && qt.rowIndex === index) {
|
||||
qt.hints = qt.hints.filter(hint => hint.fix.action !== action);
|
||||
}
|
||||
return qt;
|
||||
})
|
||||
// Preserve previous row query transaction to keep results visible if next query is incomplete
|
||||
.filter(qt => preventSubmit || qt.rowIndex !== index);
|
||||
}
|
||||
this.queryExpressions = nextQueries.map(q => q.query);
|
||||
return {
|
||||
@ -408,7 +419,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
queryTransactions: nextQueryTransactions,
|
||||
};
|
||||
},
|
||||
() => this.onSubmit()
|
||||
// Accepting certain fixes do not result in a well-formed query which should not be submitted
|
||||
!preventSubmit ? () => this.onSubmit() : null
|
||||
);
|
||||
}
|
||||
};
|
||||
|
72
public/app/features/explore/PlaceholdersBuffer.test.ts
Normal file
72
public/app/features/explore/PlaceholdersBuffer.test.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import PlaceholdersBuffer from './PlaceholdersBuffer';
|
||||
|
||||
describe('PlaceholdersBuffer', () => {
|
||||
it('does nothing if no placeholders are defined', () => {
|
||||
const text = 'metric';
|
||||
const buffer = new PlaceholdersBuffer(text);
|
||||
|
||||
expect(buffer.hasPlaceholders()).toBe(false);
|
||||
expect(buffer.toString()).toBe(text);
|
||||
expect(buffer.getNextMoveOffset()).toBe(0);
|
||||
});
|
||||
|
||||
it('respects the traversal order of placeholders', () => {
|
||||
const text = 'sum($2 offset $1) by ($3)';
|
||||
const buffer = new PlaceholdersBuffer(text);
|
||||
|
||||
expect(buffer.hasPlaceholders()).toBe(true);
|
||||
expect(buffer.toString()).toBe('sum( offset ) by ()');
|
||||
expect(buffer.getNextMoveOffset()).toBe(12);
|
||||
|
||||
buffer.setNextPlaceholderValue('1h');
|
||||
|
||||
expect(buffer.hasPlaceholders()).toBe(true);
|
||||
expect(buffer.toString()).toBe('sum( offset 1h) by ()');
|
||||
expect(buffer.getNextMoveOffset()).toBe(-10);
|
||||
|
||||
buffer.setNextPlaceholderValue('metric');
|
||||
|
||||
expect(buffer.hasPlaceholders()).toBe(true);
|
||||
expect(buffer.toString()).toBe('sum(metric offset 1h) by ()');
|
||||
expect(buffer.getNextMoveOffset()).toBe(16);
|
||||
|
||||
buffer.setNextPlaceholderValue('label');
|
||||
|
||||
expect(buffer.hasPlaceholders()).toBe(false);
|
||||
expect(buffer.toString()).toBe('sum(metric offset 1h) by (label)');
|
||||
expect(buffer.getNextMoveOffset()).toBe(0);
|
||||
});
|
||||
|
||||
it('respects the traversal order of adjacent placeholders', () => {
|
||||
const text = '$1$3$2$4';
|
||||
const buffer = new PlaceholdersBuffer(text);
|
||||
|
||||
expect(buffer.hasPlaceholders()).toBe(true);
|
||||
expect(buffer.toString()).toBe('');
|
||||
expect(buffer.getNextMoveOffset()).toBe(0);
|
||||
|
||||
buffer.setNextPlaceholderValue('1');
|
||||
|
||||
expect(buffer.hasPlaceholders()).toBe(true);
|
||||
expect(buffer.toString()).toBe('1');
|
||||
expect(buffer.getNextMoveOffset()).toBe(0);
|
||||
|
||||
buffer.setNextPlaceholderValue('2');
|
||||
|
||||
expect(buffer.hasPlaceholders()).toBe(true);
|
||||
expect(buffer.toString()).toBe('12');
|
||||
expect(buffer.getNextMoveOffset()).toBe(-1);
|
||||
|
||||
buffer.setNextPlaceholderValue('3');
|
||||
|
||||
expect(buffer.hasPlaceholders()).toBe(true);
|
||||
expect(buffer.toString()).toBe('132');
|
||||
expect(buffer.getNextMoveOffset()).toBe(1);
|
||||
|
||||
buffer.setNextPlaceholderValue('4');
|
||||
|
||||
expect(buffer.hasPlaceholders()).toBe(false);
|
||||
expect(buffer.toString()).toBe('1324');
|
||||
expect(buffer.getNextMoveOffset()).toBe(0);
|
||||
});
|
||||
});
|
112
public/app/features/explore/PlaceholdersBuffer.ts
Normal file
112
public/app/features/explore/PlaceholdersBuffer.ts
Normal file
@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Provides a stateful means of managing placeholders in text.
|
||||
*
|
||||
* Placeholders are numbers prefixed with the `$` character (e.g. `$1`).
|
||||
* Each number value represents the order in which a placeholder should
|
||||
* receive focus if multiple placeholders exist.
|
||||
*
|
||||
* Example scenario given `sum($3 offset $1) by($2)`:
|
||||
* 1. `sum( offset |) by()`
|
||||
* 2. `sum( offset 1h) by(|)`
|
||||
* 3. `sum(| offset 1h) by (label)`
|
||||
*/
|
||||
export default class PlaceholdersBuffer {
|
||||
private nextMoveOffset: number;
|
||||
private orders: number[];
|
||||
private parts: string[];
|
||||
|
||||
constructor(text: string) {
|
||||
const result = this.parse(text);
|
||||
const nextPlaceholderIndex = result.orders.length ? result.orders[0] : 0;
|
||||
this.nextMoveOffset = this.getOffsetBetween(result.parts, 0, nextPlaceholderIndex);
|
||||
this.orders = result.orders;
|
||||
this.parts = result.parts;
|
||||
}
|
||||
|
||||
clearPlaceholders() {
|
||||
this.nextMoveOffset = 0;
|
||||
this.orders = [];
|
||||
}
|
||||
|
||||
getNextMoveOffset(): number {
|
||||
return this.nextMoveOffset;
|
||||
}
|
||||
|
||||
hasPlaceholders(): boolean {
|
||||
return this.orders.length > 0;
|
||||
}
|
||||
|
||||
setNextPlaceholderValue(value: string) {
|
||||
if (this.orders.length === 0) {
|
||||
return;
|
||||
}
|
||||
const currentPlaceholderIndex = this.orders[0];
|
||||
this.parts[currentPlaceholderIndex] = value;
|
||||
this.orders = this.orders.slice(1);
|
||||
if (this.orders.length === 0) {
|
||||
this.nextMoveOffset = 0;
|
||||
return;
|
||||
}
|
||||
const nextPlaceholderIndex = this.orders[0];
|
||||
// Case should never happen but handle it gracefully in case
|
||||
if (currentPlaceholderIndex === nextPlaceholderIndex) {
|
||||
this.nextMoveOffset = 0;
|
||||
return;
|
||||
}
|
||||
const backwardMove = currentPlaceholderIndex > nextPlaceholderIndex;
|
||||
const indices = backwardMove
|
||||
? { start: nextPlaceholderIndex + 1, end: currentPlaceholderIndex + 1 }
|
||||
: { start: currentPlaceholderIndex + 1, end: nextPlaceholderIndex };
|
||||
this.nextMoveOffset = (backwardMove ? -1 : 1) * this.getOffsetBetween(this.parts, indices.start, indices.end);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.parts.join('');
|
||||
}
|
||||
|
||||
private getOffsetBetween(parts: string[], startIndex: number, endIndex: number) {
|
||||
return parts.slice(startIndex, endIndex).reduce((offset, part) => offset + part.length, 0);
|
||||
}
|
||||
|
||||
private parse(text: string): ParseResult {
|
||||
const placeholderRegExp = /\$(\d+)/g;
|
||||
const parts = [];
|
||||
const orders = [];
|
||||
let textOffset = 0;
|
||||
while (true) {
|
||||
const match = placeholderRegExp.exec(text);
|
||||
if (!match) {
|
||||
break;
|
||||
}
|
||||
const part = text.slice(textOffset, match.index);
|
||||
parts.push(part);
|
||||
// Accounts for placeholders at text boundaries
|
||||
if (part !== '') {
|
||||
parts.push('');
|
||||
}
|
||||
const order = parseInt(match[1], 10);
|
||||
orders.push({ index: parts.length - 1, order });
|
||||
textOffset += part.length + match.length;
|
||||
}
|
||||
// Ensures string serialisation still works if no placeholders were parsed
|
||||
// and also accounts for the remainder of text with placeholders
|
||||
parts.push(text.slice(textOffset));
|
||||
return {
|
||||
// Placeholder values do not necessarily appear sequentially so sort the
|
||||
// indices to traverse in priority order
|
||||
orders: orders.sort((o1, o2) => o1.order - o2.order).map(o => o.index),
|
||||
parts,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type ParseResult = {
|
||||
/**
|
||||
* Indices to placeholder items in `parts` in traversal order.
|
||||
*/
|
||||
orders: number[];
|
||||
/**
|
||||
* Parts comprising the original text with placeholders occupying distinct items.
|
||||
*/
|
||||
parts: string[];
|
||||
};
|
@ -12,6 +12,7 @@ import NewlinePlugin from './slate-plugins/newline';
|
||||
|
||||
import Typeahead from './Typeahead';
|
||||
import { makeFragment, makeValue } from './Value';
|
||||
import PlaceholdersBuffer from './PlaceholdersBuffer';
|
||||
|
||||
export const TYPEAHEAD_DEBOUNCE = 100;
|
||||
|
||||
@ -61,12 +62,15 @@ export interface TypeaheadInput {
|
||||
|
||||
class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadFieldState> {
|
||||
menuEl: HTMLElement | null;
|
||||
placeholdersBuffer: PlaceholdersBuffer;
|
||||
plugins: any[];
|
||||
resetTimer: any;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.placeholdersBuffer = new PlaceholdersBuffer(props.initialValue || '');
|
||||
|
||||
// Base plugins
|
||||
this.plugins = [ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins];
|
||||
|
||||
@ -76,7 +80,7 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
|
||||
typeaheadIndex: 0,
|
||||
typeaheadPrefix: '',
|
||||
typeaheadText: '',
|
||||
value: makeValue(props.initialValue || '', props.syntax),
|
||||
value: makeValue(this.placeholdersBuffer.toString(), props.syntax),
|
||||
};
|
||||
}
|
||||
|
||||
@ -101,12 +105,14 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
|
||||
componentWillReceiveProps(nextProps: TypeaheadFieldProps) {
|
||||
if (nextProps.syntaxLoaded && !this.props.syntaxLoaded) {
|
||||
// Need a bogus edit to re-render the editor after syntax has fully loaded
|
||||
this.onChange(
|
||||
this.state.value
|
||||
.change()
|
||||
.insertText(' ')
|
||||
.deleteBackward()
|
||||
);
|
||||
const change = this.state.value
|
||||
.change()
|
||||
.insertText(' ')
|
||||
.deleteBackward();
|
||||
if (this.placeholdersBuffer.hasPlaceholders()) {
|
||||
change.move(this.placeholdersBuffer.getNextMoveOffset()).focus();
|
||||
}
|
||||
this.onChange(change);
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,7 +295,17 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
|
||||
}
|
||||
|
||||
const suggestion = getSuggestionByIndex(suggestions, typeaheadIndex);
|
||||
this.applyTypeahead(change, suggestion);
|
||||
const nextChange = this.applyTypeahead(change, suggestion);
|
||||
|
||||
const insertTextOperation = nextChange.operations.find(operation => operation.type === 'insert_text');
|
||||
if (insertTextOperation) {
|
||||
const suggestionText = insertTextOperation.text;
|
||||
this.placeholdersBuffer.setNextPlaceholderValue(suggestionText);
|
||||
if (this.placeholdersBuffer.hasPlaceholders()) {
|
||||
nextChange.move(this.placeholdersBuffer.getNextMoveOffset()).focus();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
@ -336,6 +352,8 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
|
||||
// If we dont wait here, menu clicks wont work because the menu
|
||||
// will be gone.
|
||||
this.resetTimer = setTimeout(this.resetTypeahead, 100);
|
||||
// Disrupting placeholder entry wipes all remaining placeholders needing input
|
||||
this.placeholdersBuffer.clearPlaceholders();
|
||||
if (onBlur) {
|
||||
onBlur();
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Label } from 'app/core/components/Forms/Forms';
|
||||
import { Label } from 'app/core/components/Label/Label';
|
||||
import { Team } from '../../types';
|
||||
import { updateTeam } from './state/actions';
|
||||
import { getRouteParamsId } from '../../core/selectors/location';
|
||||
|
@ -464,6 +464,9 @@ export class PrometheusDatasource {
|
||||
case 'ADD_RATE': {
|
||||
return `rate(${query}[5m])`;
|
||||
}
|
||||
case 'ADD_SUM': {
|
||||
return `sum(${query.trim()}) by ($1)`;
|
||||
}
|
||||
case 'EXPAND_RULES': {
|
||||
const mapping = action.mapping;
|
||||
if (mapping) {
|
||||
|
@ -162,16 +162,29 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
let refresher: Promise<any> = null;
|
||||
const suggestions: CompletionItemGroup[] = [];
|
||||
|
||||
// sum(foo{bar="1"}) by (|)
|
||||
const line = value.anchorBlock.getText();
|
||||
const cursorOffset: number = value.anchorOffset;
|
||||
// sum(foo{bar="1"}) by (
|
||||
const leftSide = line.slice(0, cursorOffset);
|
||||
// Stitch all query lines together to support multi-line queries
|
||||
let queryOffset;
|
||||
const queryText = value.document.getBlocks().reduce((text, block) => {
|
||||
const blockText = block.getText();
|
||||
if (value.anchorBlock.key === block.key) {
|
||||
// Newline characters are not accounted for but this is irrelevant
|
||||
// for the purpose of extracting the selector string
|
||||
queryOffset = value.anchorOffset + text.length;
|
||||
}
|
||||
text += blockText;
|
||||
return text;
|
||||
}, '');
|
||||
|
||||
const leftSide = queryText.slice(0, queryOffset);
|
||||
const openParensAggregationIndex = leftSide.lastIndexOf('(');
|
||||
const openParensSelectorIndex = leftSide.slice(0, openParensAggregationIndex).lastIndexOf('(');
|
||||
const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex;
|
||||
// foo{bar="1"}
|
||||
const selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
|
||||
|
||||
let selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
|
||||
|
||||
// Range vector syntax not accounted for by subsequent parse so discard it if present
|
||||
selectorString = selectorString.replace(/\[[^\]]+\]$/, '');
|
||||
|
||||
const selector = parseSelector(selectorString, selectorString.length - 2).selector;
|
||||
|
||||
const labelKeys = this.labelKeys[selector];
|
||||
|
@ -2,6 +2,11 @@ import _ from 'lodash';
|
||||
|
||||
import { QueryHint } from 'app/types/explore';
|
||||
|
||||
/**
|
||||
* Number of time series results needed before starting to suggest sum aggregation hints
|
||||
*/
|
||||
export const SUM_HINT_THRESHOLD_COUNT = 20;
|
||||
|
||||
export function getQueryHints(query: string, series?: any[], datasource?: any): QueryHint[] {
|
||||
const hints = [];
|
||||
|
||||
@ -90,5 +95,24 @@ export function getQueryHints(query: string, series?: any[], datasource?: any):
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (series.length >= SUM_HINT_THRESHOLD_COUNT) {
|
||||
const simpleMetric = query.trim().match(/^\w+$/);
|
||||
if (simpleMetric) {
|
||||
hints.push({
|
||||
type: 'ADD_SUM',
|
||||
label: 'Many time series results returned.',
|
||||
fix: {
|
||||
label: 'Consider aggregating with sum().',
|
||||
action: {
|
||||
type: 'ADD_SUM',
|
||||
query: query,
|
||||
preventSubmit: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return hints.length > 0 ? hints : null;
|
||||
}
|
||||
|
@ -198,5 +198,76 @@ describe('Language completion provider', () => {
|
||||
expect(result.context).toBe('context-aggregation');
|
||||
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
|
||||
});
|
||||
|
||||
it('returns label suggestions inside a multi-line aggregation context', () => {
|
||||
const instance = new LanguageProvider(datasource, {
|
||||
labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
|
||||
});
|
||||
const value = Plain.deserialize('sum(\nmetric\n)\nby ()');
|
||||
const aggregationTextBlock = value.document.getBlocksAsArray()[3];
|
||||
const range = value.selection.moveToStartOf(aggregationTextBlock).merge({ anchorOffset: 4 });
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-aggregation'],
|
||||
value: valueWithSelection,
|
||||
});
|
||||
expect(result.context).toBe('context-aggregation');
|
||||
expect(result.suggestions).toEqual([
|
||||
{
|
||||
items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
|
||||
label: 'Labels',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns label suggestions inside an aggregation context with a range vector', () => {
|
||||
const instance = new LanguageProvider(datasource, {
|
||||
labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
|
||||
});
|
||||
const value = Plain.deserialize('sum(rate(metric[1h])) by ()');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 26,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-aggregation'],
|
||||
value: valueWithSelection,
|
||||
});
|
||||
expect(result.context).toBe('context-aggregation');
|
||||
expect(result.suggestions).toEqual([
|
||||
{
|
||||
items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
|
||||
label: 'Labels',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns label suggestions inside an aggregation context with a range vector and label', () => {
|
||||
const instance = new LanguageProvider(datasource, {
|
||||
labelKeys: { '{__name__="metric",label1="value"}': ['label1', 'label2', 'label3'] },
|
||||
});
|
||||
const value = Plain.deserialize('sum(rate(metric{label1="value"}[1h])) by ()');
|
||||
const range = value.selection.merge({
|
||||
anchorOffset: 42,
|
||||
});
|
||||
const valueWithSelection = value.change().select(range).value;
|
||||
const result = instance.provideCompletionItems({
|
||||
text: '',
|
||||
prefix: '',
|
||||
wrapperClasses: ['context-aggregation'],
|
||||
value: valueWithSelection,
|
||||
});
|
||||
expect(result.context).toBe('context-aggregation');
|
||||
expect(result.suggestions).toEqual([
|
||||
{
|
||||
items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
|
||||
label: 'Labels',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getQueryHints } from '../query_hints';
|
||||
import { getQueryHints, SUM_HINT_THRESHOLD_COUNT } from '../query_hints';
|
||||
|
||||
describe('getQueryHints()', () => {
|
||||
it('returns no hints for no series', () => {
|
||||
@ -79,4 +79,25 @@ describe('getQueryHints()', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a sum hint when many time series results are returned for a simple metric', () => {
|
||||
const seriesCount = SUM_HINT_THRESHOLD_COUNT;
|
||||
const series = Array.from({ length: seriesCount }, _ => ({
|
||||
datapoints: [[0, 0], [0, 0]],
|
||||
}));
|
||||
const hints = getQueryHints('metric', series);
|
||||
expect(hints.length).toBe(1);
|
||||
expect(hints[0]).toMatchObject({
|
||||
type: 'ADD_SUM',
|
||||
label: 'Many time series results returned.',
|
||||
fix: {
|
||||
label: 'Consider aggregating with sum().',
|
||||
action: {
|
||||
type: 'ADD_SUM',
|
||||
query: 'metric',
|
||||
preventSubmit: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -114,7 +114,6 @@ export default class StackdriverDatasource {
|
||||
if (!queryRes.series) {
|
||||
return;
|
||||
}
|
||||
this.projectName = queryRes.meta.defaultProject;
|
||||
const unit = this.resolvePanelUnitFromTargets(options.targets);
|
||||
queryRes.series.forEach(series => {
|
||||
let timeSerie: any = {
|
||||
|
@ -5,6 +5,7 @@ import React, { PureComponent } from 'react';
|
||||
// Components
|
||||
import Graph from 'app/viz/Graph';
|
||||
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
||||
import { Switch } from 'app/core/components/Switch/Switch';
|
||||
|
||||
// Types
|
||||
import { PanelProps, NullValueMode } from 'app/types';
|
||||
@ -35,10 +36,13 @@ export class Graph2 extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
export class TextOptions extends PureComponent<any> {
|
||||
onChange = () => {};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="section-heading">Draw Modes</h5>
|
||||
<Switch label="Lines" checked={true} onChange={this.onChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -119,6 +119,7 @@ export interface QueryFix {
|
||||
export interface QueryFixAction {
|
||||
type: string;
|
||||
query?: string;
|
||||
preventSubmit?: boolean;
|
||||
}
|
||||
|
||||
export interface QueryHint {
|
||||
|
42
public/vendor/flot/jquery.flot.js
vendored
42
public/vendor/flot/jquery.flot.js
vendored
@ -2271,9 +2271,51 @@ Licensed under the MIT license.
|
||||
});
|
||||
}
|
||||
|
||||
function drawOrphanedPoints(series) {
|
||||
/* Filters series data for points with no neighbors before or after
|
||||
* and plots single 0.5 radius points for them so that they are displayed.
|
||||
*/
|
||||
var abandonedPoints = [];
|
||||
var beforeX = null;
|
||||
var afterX = null;
|
||||
var datapoints = series.datapoints;
|
||||
// find any points with no neighbors before or after
|
||||
var emptyPoints = [];
|
||||
for (var j = 0; j < datapoints.pointsize - 2; j++) {
|
||||
emptyPoints.push(0);
|
||||
}
|
||||
for (var i = 0; i < datapoints.points.length; i += datapoints.pointsize) {
|
||||
var x = datapoints.points[i], y = datapoints.points[i + 1];
|
||||
if (i === datapoints.points.length - datapoints.pointsize) {
|
||||
afterX = null;
|
||||
} else {
|
||||
afterX = datapoints.points[i + datapoints.pointsize];
|
||||
}
|
||||
if (x !== null && y !== null && beforeX === null && afterX === null) {
|
||||
abandonedPoints.push(x);
|
||||
abandonedPoints.push(y);
|
||||
abandonedPoints.push.apply(abandonedPoints, emptyPoints);
|
||||
}
|
||||
beforeX = x;
|
||||
|
||||
}
|
||||
var olddatapoints = datapoints.points
|
||||
datapoints.points = abandonedPoints;
|
||||
|
||||
series.points.radius = series.lines.lineWidth/2;
|
||||
// plot the orphan points with a radius of lineWidth/2
|
||||
drawSeriesPoints(series);
|
||||
// reset old info
|
||||
datapoints.points = olddatapoints;
|
||||
}
|
||||
|
||||
function drawSeries(series) {
|
||||
if (series.lines.show)
|
||||
drawSeriesLines(series);
|
||||
if (!series.points.show && !series.bars.show) {
|
||||
// not necessary if user wants points displayed for everything
|
||||
drawOrphanedPoints(series);
|
||||
}
|
||||
if (series.bars.show)
|
||||
drawSeriesBars(series);
|
||||
if (series.points.show)
|
||||
|
Loading…
Reference in New Issue
Block a user