Merge remote-tracking branch 'grafana/master' into influx-db-query2

* grafana/master: (35 commits)
  Update CHANGELOG.md
  Update CHANGELOG.md
  fixed some images (#9237)
  release: bumped version to v4.5.0 stable
  docs: minor update
  plugins: add styles for ol tags in markdown
  docs: minor fixes to images
  Docs image updates (#9225)
  fix: improve behavior when switching back and forth between x-axis modes, fixes #9229
  fixes for metrics tab when data source was not found
  provide ace editor for external datasource plugin (#9224)
  ux: increased code editor auto complete width from 320px to 550px, fixes #9203
  docs: windows - add note about ini comments
  prometheus: added completer unit test, #9208
  docs: minor update
  docs: kiosk mode options add to playlist doc
  influxdb: small css fix for order by in query editor
  style: corrected indentation in sass file
  replaced old images and gifs with new ones (#9217)
  ux: success/error alerts refactoring, #9214
  ...
This commit is contained in:
ryan 2017-09-14 13:09:59 +02:00
commit bc03640d6b
54 changed files with 364 additions and 238 deletions

View File

@ -7,11 +7,13 @@
- UX changes to nav & side menu
- New dashboard grid layout system
# 4.5.0 (unreleased)
## Enhancements
# 4.5.0 (2017-09-14)
## Fixes & Enhancements since beta1
* **Security**: Security fix for api vulnerability (in multiple org setups).
* **Shortcuts**: Adds shortcut for creating new dashboard [#8876](https://github.com/grafana/grafana/pull/8876) thx [@mtanda](https://github.com/mtanda)
* **Graph**: Right Y-Axis label position fixed [#9172](https://github.com/grafana/grafana/pull/9172)
* **General**: Improve rounding of time intervals [#9197](https://github.com/grafana/grafana/pull/9197), thx [@alin-amana](https://github.com/alin-amana)
# 4.5.0-beta1 (2017-09-05)

View File

@ -41,7 +41,9 @@ Proxy access means that the Grafana backend will proxy all requests from the bro
Click the ``Select metric`` link to start navigating the metric space. One you start you can continue using the mouse
or keyboard arrow keys. You can select a wildcard and still continue.
![](/img/docs/animated_gifs/graphite_query1.gif)
{{< docs-imagebox img="/img/docs/v45/graphite_query1_still.png"
animated-gif="/img/docs/v45/graphite_query1.gif" >}}
### Functions
@ -50,18 +52,26 @@ a function is selected it will be added and your focus will be in the text box o
a parameter just click on it and it will turn into a text box. To delete a function click the function name followed
by the x icon.
![](/img/docs/animated_gifs/graphite_query2.gif)
{{< docs-imagebox img="/img/docs/v45/graphite_query2_still.png"
animated-gif="/img/docs/v45/graphite_query2.gif" >}}
### Optional parameters
Some functions like aliasByNode support an optional second argument. To add this parameter specify for example 3,-2 as the first parameter and the function editor will adapt and move the -2 to a second parameter. To remove the second optional parameter just click on it and leave it blank and the editor will remove it.
![](/img/docs/animated_gifs/func_editor_optional_params.gif)
{{< docs-imagebox img="/img/docs/v45/graphite_query3_still.png"
animated-gif="/img/docs/v45/graphite_query3.gif" >}}
### Nested Queries
You can reference queries by the row “letter” that theyre on (similar to Microsoft Excel). If you add a second query to a graph, you can reference the first query simply by typing in #A. This provides an easy and convenient way to build compounded queries.
{{< docs-imagebox img="/img/docs/v45/graphite_nested_queries_still.png"
animated-gif="/img/docs/v45/graphite_nested_queries.gif" >}}
## Point consolidation
All Graphite metrics are consolidated so that Graphite doesn't return more data points than there are pixels in the graph. By default,

View File

@ -41,7 +41,7 @@ mode is also more secure as the username & password will never reach the browser
## Query Editor
![](/assets/img/blog/v2.6/influxdb_editor_v3.gif)
{{< docs-imagebox img="/img/docs/v45/influxdb_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v45/influxdb_query.gif" >}}
You find the InfluxDB editor in the metrics tab in Graph or Singlestat panel's edit mode. You enter edit mode by clicking the
panel title, then edit. The editor allows you to select metrics and tags.
@ -57,10 +57,8 @@ will automatically adjust the filter tag condition to use the InfluxDB regex mat
### Field & Aggregation functions
In the `SELECT` row you can specify what fields and functions you want to use. If you have a
group by time you need an aggregation function. Some functions like derivative require an aggregation function.
The editor tries simplify and unify this part of the query. For example:
![](/img/docs/influxdb/select_editor.png)
group by time you need an aggregation function. Some functions like derivative require an aggregation function. The editor tries simplify and unify this part of the query. For example:<br>
![](/img/docs/influxdb/select_editor.png)<br>
The above will generate the following InfluxDB `SELECT` clause:

View File

@ -11,8 +11,7 @@ weight = 7
# Using MySQL in Grafana
> Only available in Grafana v4.3+. This data source is not ready for
> production use, currently in development (alpha state).
> Only available in Grafana v4.3+.
Grafana ships with a built-in MySQL data source plugin that allow you to query any visualize
data from a MySQL compatible database.
@ -58,8 +57,7 @@ If the `Format as` query option is set to `Table` then you can basically do any
Query editor with example query:
![](/img/docs/v43/mysql_table_query.png)
{{< docs-imagebox img="/img/docs/v45/mysql_table_query.png" >}}
The query:

View File

@ -39,7 +39,8 @@ Name | Description
Open a graph in edit mode by click the title > Edit (or by pressing `e` key while hovering over panel).
![](/img/docs/v43/prometheus_query_editor.png)
{{< docs-imagebox img="/img/docs/v45/prometheus_query_editor_still.png"
animated-gif="/img/docs/v45/prometheus_query_editor.gif" >}}
Name | Description
------- | --------

View File

@ -50,15 +50,11 @@ populate the template variable to a desired value from the link.
The metrics tab defines what series data and sources to render. Each datasource provides different
options.
## Axes & Grid
## Axes
![](/img/docs/v43/graph_axes_grid_options.png)
The Axes & Grid tab controls the display of axes, grids and legend.
### Axes
The ``Left Y`` and ``Right Y`` can be customized using:
The Axes tab controls the display of axes, grids and legend. The ``Left Y`` and ``Right Y`` can be customized using:
- ``Unit`` - The display unit for the Y value
- ``Grid Max`` - The maximum Y value. (default auto)

View File

@ -12,7 +12,7 @@ weight = 2
# Table Panel
<img src="/assets/img/features/table-panel.png">
<img class="screenshot" src="/assets/img/features/table-panel.png">
The new table panel is very flexible, supporting both multiple modes for time series as well as for
table, annotation and raw JSON data. It also provides date formatting and value formatting and coloring options.
@ -22,55 +22,63 @@ To view table panels in action and test different configurations with sample dat
## Options overview
The table panel has many ways to manipulate your data for optimal presentation.
{{< docs-imagebox img="/img/docs/v45/table_options.png" class="docs-image--no-shadow" max-width= "500px" >}}
<img class="no-shadow" src="/img/docs/v2/table-config2.png">
1. `Data`: Control how your query is transformed into a table.
2. `Table Display`: Table display options.
3. `Column Styles`: Column value formatting and display options.
2. `Paging`: Table display options.
## Data to Table
<img class="no-shadow" src="/img/docs/v2/table-data-options.png">
{{< docs-imagebox img="/img/docs/v45/table_data_options.png" max-width="500px" class="docs-image--right">}}
The data section contains the **To Table Transform (1)**. This is the primary option for how your data/metric
query should be transformed into a table format. The **Columns (2)** option allows you to select what columns
you want in the table. Only applicable for some transforms.
<div class="clearfix"></div>
### Time series to rows
<img src="/img/docs/v2/table_ts_to_rows2.png">
{{< docs-imagebox img="/img/docs/v45/table_ts_to_rows.png" >}}
In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column. Where `Metric` is the name of the time series.
### Time series to columns
![](/img/docs/v2/table_ts_to_columns2.png)
{{< docs-imagebox img="/img/docs/v45/table_ts_to_columns.png" >}}
This transform allows you to take multiple time series and group them by time. Which will result in the primary column being `Time` and a column for each time series.
### Time series aggregations
![](/img/docs/v2/table_ts_to_aggregations2.png)
{{< docs-imagebox img="/img/docs/v45/table_ts_to_aggregations.png" >}}
This table transformation will lay out your table into rows by metric, allowing columns of `Avg`, `Min`, `Max`, `Total`, `Current` and `Count`. More than one column can be added.
### Annotations
![](/img/docs/v2/table_annotations.png)
{{< docs-imagebox img="/img/docs/v45/table_annotations.png" >}}
If you have annotations enabled in the dashboard you can have the table show them. If you configure this
mode then any queries you have in the metrics tab will be ignored.
### JSON Data
![](/img/docs/v2/table_json_data.png)
{{< docs-imagebox img="/img/docs/v45/table_json_data.png" max-width="500px" >}}
If you have an Elasticsearch **Raw Document** query or an Elasticsearch query without a `date histogram` use this
transform mode and pick the columns using the **Columns** section.
![](/img/docs/v2/elastic_raw_doc.png)
{{< docs-imagebox img="/img/docs/v45/elastic_raw_doc.png" >}}
## Table Display
<img class="no-shadow" src="/img/docs/v2/table-display.png">
{{< docs-imagebox img="/img/docs/v45/table_paging.png" class="docs-image--no-shadow docs-image--right" max-width="350px" >}}
1. `Pagination (Page Size)`: The table display fields allow you to control The `Pagination` (page size) is the threshold at which the table rows will be broken into pages. For example, if your table had 95 records with a pagination value of 10, your table would be split across 9 pages.
2. `Scroll`: The `scroll bar` checkbox toggles the ability to scroll within the panel, when unchecked, the panel height will grow to display all rows.
@ -81,13 +89,11 @@ transform mode and pick the columns using the **Columns** section.
The column styles allow you control how dates and numbers are formatted.
<img class="no-shadow" src="/img/docs/v2/Column-Options.png">
{{< docs-imagebox img="/img/docs/v45/table_column_styles.png" class="docs-image--no-shadow" >}}
1. `Name or regex`: The Name or Regex field controls what columns the rule should be applied to. The regex or name filter will be matched against the column name not against column values.
2. `Type`: The three supported types of types are `Number`, `String` and `Date`.
3. `Title`: Title for the column, when using a Regex the title can include replacement strings like `$1`.
4. `Format`: Specify date format. Only available when `Type` is set to `Date`.
5. `Coloring` and `Thresholds`: Specify color mode and thresholds limits.
6. `Unit` and `Decimals`: Specify unit and decimal precision for numbers.
7. `Add column style rule`: Add new column rule.
2. `Column Header`: Title for the column, when using a Regex the title can include replacement strings like `$1`.
3. `Add column style rule`: Add new column rule.
4. `Thresholds` and `Coloring`: Specify color mode and thresholds limits.
5. `Type`: The three supported types of types are `Number`, `String` and `Date`. `Unit` and `Decimals`: Specify unit and decimal precision for numbers.`Format`: Specify date format for dates.

View File

@ -8,7 +8,7 @@ weight = 7
# Keyboard shortcuts
{{< docs-imagebox img="/img/docs/v4/shortcuts.png" max-width="20rem" >}}
{{< docs-imagebox img="/img/docs/v4/shortcuts.png" max-width="20rem" class="docs-image--right" >}}
Grafana v4 introduces a number of really powerful keyboard shortcuts. You can now focus a panel
by hovering over it with your mouse. With a panel focused you can simple hit `e` to toggle panel

View File

@ -16,16 +16,13 @@ weight = -4
### New prometheus query editor
The new query editor has full syntax highlighting. As well as auto complete for metrics, functions, and range vectors.
The new query editor has full syntax highlighting. As well as auto complete for metrics, functions, and range vectors. There is also integrated function docs right from the query editor!
![](/img/docs/v45/new_prom_editor_1.png)
There is also integrated function docs right from the query editor!
![](/img/docs/v45/new_prom_editor_2.png)
{{< docs-imagebox img="/img/docs/v45/prometheus_query_editor_still.png" class="docs-image--block" animated-gif="/img/docs/v45/prometheus_query_editor.gif" >}}
### Elasticsearch: Add ad-hoc filters from the table panel
![](/img/docs/v45/elastic_ad_hoc_filters.png)
{{< docs-imagebox img="/img/docs/v45/elastic_ad_hoc_filters.png" class="docs-image--block" >}}
### Table cell links!
Create column styles that turn cells into links that use the value in the cell (or other other row values) to generate a url to another dashboard or system:

View File

@ -27,7 +27,7 @@ this folder to anywhere you want Grafana to run from. Go into the
The default Grafana port is `3000`, this port requires extra permissions
on windows. Edit `custom.ini` and uncomment the `http_port`
configuration option and change it to something like `8080` or similar.
configuration option (`;` is the comment character in ini files) and change it to something like `8080` or similar.
That port should not require extra Windows privileges.
Start Grafana by executing `grafana-server.exe`, preferably from the

View File

@ -16,7 +16,7 @@ Since Grafana automatically scales Dashboards to any resolution they're perfect
## Creating a Playlist
{{< docs-imagebox img="/img/docs/v3/playlist.png" max-width="25rem" >}}
{{< docs-imagebox img="/img/docs/v3/playlist.png" max-width="25rem" class="docs-image--right">}}
The Playlist feature can be accessed from Grafana's sidemenu, in the Dashboard submenu.
@ -43,3 +43,25 @@ Playlists can also be manually controlled utilizing the Playlist controls at the
Click the stop button to stop the Playlist, and exit to the current Dashboard.
Click the next button to advance to the next Dashboard in the Playlist.
Click the back button to rewind to the previous Dashboard in the Playlist.
## TV or Kiosk Mode
In TV mode the top navbar, row & panel controls will all fade to transparent.
This happens automatically after one minute of user inactivity but can also be toggled manually
with the `d v` sequence shortcut. Any mouse movement or keyboard action will
restore navbar & controls.
Another feature is the kiosk mode - in kiosk mode the navbar is completely hidden/removed from view. This can be enabled with the `d k`
shortcut.
To put a playlist into kiosk mode, use the `d k` shortcut after the playlist has started. The same shortcut will toggle the playlist out of kiosk mode.
### Linking to the Playlist in Kiosk Mode
If you want to create a link to the playlist with kiosk mode enabled:
1. Copy the Start Url (by right clicking on the Play button and choosing Copy link address).
2. Add the `?kiosk` parameter to the url.
For example, to open the first playlist on the Grafana Play site in kiosk mode: [http://play.grafana.org/playlists/play/1?kiosk](http://play.grafana.org/playlists/play/1?kiosk)

View File

@ -74,7 +74,9 @@ If you do not get an image when opening this link verify that the required font
### Grafana API Key
<img src="/img/docs/v2/orgdropdown_api_keys.png" style="width: 150px" class="right"></img>
<!--<img src="/img/docs/v2/orgdropdown_api_keys.png" style="width: 150px" class="right"></img>-->
{{< docs-imagebox img="/img/docs/v2/orgdropdown_api_keys.png" max-width="150px" class="docs-image--right">}}
You need to set the environment variable `HUBOT_GRAFANA_API_KEY` to a Grafana API Key.
You can add these from the API Keys page which you find in the Organization dropdown.

View File

@ -4,7 +4,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
"version": "4.5.0-beta1",
"version": "4.5.0",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"
@ -53,10 +53,6 @@
"systemjs": "0.19.41",
"zone.js": "^0.7.2"
},
"engines": {
"node": "4.x",
"npm": "2.14.x"
},
"scripts": {
"build": "./node_modules/grunt-cli/bin/grunt",
"test": "./node_modules/grunt-cli/bin/grunt test",

View File

@ -29,7 +29,7 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
return ApiError(400, "Query missing datasourceId", nil)
}
dsQuery := models.GetDataSourceByIdQuery{Id: dsId}
dsQuery := models.GetDataSourceByIdQuery{Id: dsId, OrgId: c.OrgId}
if err := bus.Dispatch(&dsQuery); err != nil {
return ApiError(500, "failed to fetch data source", err)
}

View File

@ -5,6 +5,8 @@ import (
"strings"
"time"
gocontext "context"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
@ -112,6 +114,10 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *
resp, err := c.HandleRequest(context.Ctx, req)
if err != nil {
if err == gocontext.DeadlineExceeded {
return nil, fmt.Errorf("Alert execution exceeded the timeout")
}
return nil, fmt.Errorf("tsdb.HandleRequest() error %v", err)
}

View File

@ -27,12 +27,13 @@ func Decrypt(payload []byte, secret string) ([]byte, error) {
}
iv := payload[saltLength : saltLength+aes.BlockSize]
payload = payload[saltLength+aes.BlockSize:]
payloadDst := make([]byte, len(payload))
stream := cipher.NewCFBDecrypter(block, iv)
// XORKeyStream can work in-place if the two arguments are the same.
stream.XORKeyStream(payload, payload)
return payload, nil
stream.XORKeyStream(payloadDst, payload)
return payloadDst, nil
}
func Encrypt(payload []byte, secret string) ([]byte, error) {

View File

@ -40,11 +40,11 @@ const DEFAULT_MAX_LINES = 10;
const DEFAULT_TAB_SIZE = 2;
const DEFAULT_BEHAVIOURS = true;
const GRAFANA_MODULES = ['mode-prometheus', 'snippets-prometheus', 'theme-grafana-dark'];
const GRAFANA_MODULES = ['theme-grafana-dark'];
const GRAFANA_MODULE_BASE = "public/app/core/components/code_editor/";
// Trick for loading additional modules
function setModuleUrl(moduleType, name) {
function setModuleUrl(moduleType, name, pluginBaseUrl = null) {
let baseUrl = ACE_SRC_BASE;
let aceModeName = `ace/${moduleType}/${name}`;
let moduleName = `${moduleType}-${name}`;
@ -54,6 +54,10 @@ function setModuleUrl(moduleType, name) {
baseUrl = GRAFANA_MODULE_BASE;
}
if (pluginBaseUrl) {
baseUrl = pluginBaseUrl + '/';
}
if (moduleType === 'snippets') {
componentName = `${moduleType}/${name}.js`;
}
@ -111,6 +115,17 @@ function link(scope, elem, attrs) {
let textarea = elem.find("textarea");
textarea.addClass('gf-form-input');
if (scope.codeEditorFocus) {
setTimeout(function () {
textarea.focus();
var domEl = textarea[0];
if (domEl.setSelectionRange) {
var pos = textarea.val().length * 2;
domEl.setSelectionRange(pos, pos);
}
}, 100);
}
// Event handlers
editorSession.on('change', (e) => {
scope.$apply(() => {
@ -148,8 +163,8 @@ function link(scope, elem, attrs) {
function setLangMode(lang) {
let aceModeName = `ace/mode/${lang}`;
setModuleUrl("mode", lang);
setModuleUrl("snippets", lang);
setModuleUrl("mode", lang, scope.datasource.meta.baseUrl || null);
setModuleUrl("snippets", lang, scope.datasource.meta.baseUrl || null);
editorSession.setMode(aceModeName);
ace.config.loadModule("ace/ext/language_tools", (language_tools) => {
@ -199,6 +214,8 @@ export function codeEditorDirective() {
template: editorTemplate,
scope: {
content: "=",
datasource: "=",
codeEditorFocus: "<",
onChange: "&",
getCompleter: "&"
},

View File

@ -16,7 +16,7 @@ export class AlertSrv {
init() {
this.$rootScope.onAppEvent('alert-error', (e, alert) => {
this.set(alert[0], alert[1], 'error', 7000);
this.set(alert[0], alert[1], 'error', 12000);
}, this.$rootScope);
this.$rootScope.onAppEvent('alert-warning', (e, alert) => {
@ -33,6 +33,14 @@ export class AlertSrv {
appEvents.on('confirm-modal', this.showConfirmModal.bind(this));
}
getIconForSeverity(severity) {
switch (severity) {
case 'success': return 'fa fa-check';
case 'error': return 'fa fa-exclamation-triangle';
default: return 'fa fa-exclamation';
}
}
set(title, text, severity, timeout) {
if (_.isObject(text)) {
console.log('alert error', text);
@ -45,6 +53,7 @@ export class AlertSrv {
title: title || '',
text: text || '',
severity: severity || 'info',
icon: this.getIconForSeverity(severity)
};
var newAlertJson = angular.toJson(newAlert);

View File

@ -64,7 +64,13 @@ export class BackendSrv {
}
if (data.message) {
this.alertSrv.set("Problem!", data.message, data.severity, 10000);
let description = "";
let message = data.message;
if (message.length > 80) {
description = message;
message = "Error";
}
this.alertSrv.set(message, description, data.severity, 10000);
}
throw data;
@ -97,7 +103,7 @@ export class BackendSrv {
return results.data;
}, err => {
// handle unauthorized
if (err.status === 401 && firstAttempt) {
if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) {
return this.loginPing().then(() => {
options.retry = 1;
return this.request(options);

View File

@ -17,90 +17,87 @@ function($, _) {
kbn.round_interval = function(interval) {
switch (true) {
// 0.015s
case (interval <= 15):
case (interval < 15):
return 10; // 0.01s
// 0.035s
case (interval <= 35):
case (interval < 35):
return 20; // 0.02s
// 0.075s
case (interval <= 75):
case (interval < 75):
return 50; // 0.05s
// 0.15s
case (interval <= 150):
case (interval < 150):
return 100; // 0.1s
// 0.35s
case (interval <= 350):
case (interval < 350):
return 200; // 0.2s
// 0.75s
case (interval <= 750):
case (interval < 750):
return 500; // 0.5s
// 1.5s
case (interval <= 1500):
case (interval < 1500):
return 1000; // 1s
// 3.5s
case (interval <= 3500):
case (interval < 3500):
return 2000; // 2s
// 7.5s
case (interval <= 7500):
case (interval < 7500):
return 5000; // 5s
// 12.5s
case (interval <= 12500):
case (interval < 12500):
return 10000; // 10s
// 17.5s
case (interval <= 17500):
case (interval < 17500):
return 15000; // 15s
// 25s
case (interval <= 25000):
case (interval < 25000):
return 20000; // 20s
// 45s
case (interval <= 45000):
case (interval < 45000):
return 30000; // 30s
// 1.5m
case (interval <= 90000):
case (interval < 90000):
return 60000; // 1m
// 3.5m
case (interval <= 210000):
case (interval < 210000):
return 120000; // 2m
// 7.5m
case (interval <= 450000):
case (interval < 450000):
return 300000; // 5m
// 12.5m
case (interval <= 750000):
case (interval < 750000):
return 600000; // 10m
// 12.5m
case (interval <= 1050000):
case (interval < 1050000):
return 900000; // 15m
// 25m
case (interval <= 1500000):
case (interval < 1500000):
return 1200000; // 20m
// 45m
case (interval <= 2700000):
case (interval < 2700000):
return 1800000; // 30m
// 1.5h
case (interval <= 5400000):
case (interval < 5400000):
return 3600000; // 1h
// 2.5h
case (interval <= 9000000):
case (interval < 9000000):
return 7200000; // 2h
// 4.5h
case (interval <= 16200000):
case (interval < 16200000):
return 10800000; // 3h
// 9h
case (interval <= 32400000):
case (interval < 32400000):
return 21600000; // 6h
// 24h
case (interval <= 86400000):
// 1d
case (interval < 86400000):
return 43200000; // 12h
// 48h
case (interval <= 172800000):
return 86400000; // 24h
// 1w
case (interval <= 604800000):
return 86400000; // 24h
case (interval < 604800000):
return 86400000; // 1d
// 3w
case (interval <= 1814400000):
case (interval < 1814400000):
return 604800000; // 1w
// 2y
// 6w
case (interval < 3628800000):
return 2592000000; // 30d
default:
@ -134,7 +131,7 @@ function($, _) {
return nummilliseconds + 'ms';
}
return 'less then a millisecond'; //'just now' //or other string you like;
return 'less than a millisecond'; //'just now' //or other string you like;
};
kbn.to_percent = function(number,outof) {

View File

@ -83,7 +83,7 @@ export class DashboardSrv {
}
this.$rootScope.appEvent('dashboard-saved', this.dash);
this.$rootScope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + clone.title]);
this.$rootScope.appEvent('alert-success', ['Dashboard saved']);
}
save(clone, options) {

View File

@ -116,16 +116,14 @@ class TimeSrv {
setAutoRefresh(interval) {
this.dashboard.refresh = interval;
this.cancelNextRefresh();
if (interval) {
var intervalMs = kbn.interval_to_ms(interval);
this.$timeout(() => {
this.refreshTimer = this.timer.register(this.$timeout(() => {
this.startNextRefreshTimer(intervalMs);
this.refreshDashboard();
}, intervalMs);
} else {
this.cancelNextRefresh();
}, intervalMs));
}
// update url

View File

@ -9,7 +9,7 @@ export class MetricsTabCtrl {
panel: any;
panelCtrl: any;
datasources: any[];
current: any;
datasourceInstance: any;
nextRefId: string;
dashboard: DashboardModel;
panelDsValue: any;
@ -29,23 +29,26 @@ export class MetricsTabCtrl {
this.panel = this.panelCtrl.panel;
this.dashboard = this.panelCtrl.dashboard;
this.datasources = datasourceSrv.getMetricSources();
this.panelDsValue = this.panelCtrl.panel.datasource || null;
this.panelDsValue = this.panelCtrl.panel.datasource;
for (let ds of this.datasources) {
if (ds.value === this.panelDsValue) {
this.current = ds;
this.datasourceInstance = ds;
}
}
this.addQueryDropdown = {text: 'Add Query', value: null, fake: true};
// update next ref id
this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
this.updateDatasourceOptions();
}
updateDatasourceOptions() {
this.hasQueryHelp = this.current.meta.hasQueryHelp;
this.queryOptions = this.current.meta.queryOptions;
if (this.datasourceInstance) {
this.hasQueryHelp = this.datasourceInstance.meta.hasQueryHelp;
this.queryOptions = this.datasourceInstance.meta.queryOptions;
}
}
getOptions(includeBuiltin) {
@ -61,7 +64,7 @@ export class MetricsTabCtrl {
return;
}
this.current = option.datasource;
this.datasourceInstance = option.datasource;
this.panelCtrl.setDatasource(option.datasource);
this.updateDatasourceOptions();
}
@ -85,7 +88,7 @@ export class MetricsTabCtrl {
this.queryTroubleshooterOpen = false;
this.helpOpen = !this.helpOpen;
this.backendSrv.get(`/api/plugins/${this.current.meta.id}/markdown/query_help`).then(res => {
this.backendSrv.get(`/api/plugins/${this.datasourceInstance.meta.id}/markdown/query_help`).then(res => {
var md = new Remarkable();
this.helpHtml = this.$sce.trustAsHtml(md.render(res));
});

View File

@ -73,7 +73,7 @@
</div>
</div>
<div class="query-editor-rows gf-form-group">
<div class="query-editor-rows gf-form-group" ng-if="ctrl.datasourceInstance">
<div ng-repeat="target in ctrl.panel.targets" ng-class="{'gf-form-disabled': target.hide}">
<rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
<plugin-component type="query-ctrl">
@ -89,11 +89,11 @@
</span>
<span class="gf-form-query-letter-cell-letter">{{ctrl.panelCtrl.nextRefId}}</span>
</label>
<button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addQuery()" ng-hide="ctrl.current.meta.mixed">
<button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addQuery()" ng-hide="ctrl.datasourceInstance.meta.mixed">
Add Query
</button>
<div class="dropdown" ng-if="ctrl.current.meta.mixed">
<div class="dropdown" ng-if="ctrl.datasourceInstance.meta.mixed">
<gf-form-dropdown model="ctrl.addQueryDropdown" get-options="ctrl.getOptions(false)" on-change="ctrl.addMixedQuery($option)">
</gf-form-dropdown>
</div>

View File

@ -10,9 +10,11 @@ export class QueryCtrl {
panel: any;
hasRawMode: boolean;
error: string;
isLastQuery: boolean;
constructor(public $scope, private $injector) {
this.panel = this.panelCtrl.panel;
this.isLastQuery = _.indexOf(this.panel.targets, this.target) === (this.panel.targets.length - 1);
}
refresh() {

View File

@ -126,21 +126,18 @@ export class DataSourceEditCtrl {
return;
}
this.testing = {done: false};
this.testing = {done: false, status: 'error'};
// make test call in no backend cache context
this.backendSrv.withNoBackendCache(() => {
return datasource.testDatasource().then(result => {
this.testing.message = result.message;
this.testing.status = result.status;
this.testing.title = result.title;
}).catch(err => {
if (err.statusText) {
this.testing.message = err.statusText;
this.testing.title = "HTTP Error";
this.testing.message = 'HTTP Error ' + err.statusText;
} else {
this.testing.message = err.message;
this.testing.title = "Unknown error";
}
});
}).finally(() => {

View File

@ -59,9 +59,14 @@
<div ng-if="ctrl.testing" class="gf-form-group">
<h5 ng-show="!ctrl.testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>
<div class="alert-{{ctrl.testing.status}} alert">
<div class="alert-title">{{ctrl.testing.title}}</div>
<div ng-bind='ctrl.testing.message'></div>
<div class="alert-{{ctrl.testing.status}} alert" ng-show="ctrl.testing.done">
<div class="alert-icon">
<i class="fa fa-exclamation-triangle" ng-show="ctrl.testing.status === 'error'"></i>
<i class="fa fa-check" ng-show="ctrl.testing.status !== 'error'"></i>
</div>
<div class="alert-body">
<div class="alert-title">{{ctrl.testing.message}}</div>
</div>
</div>
</div>

View File

@ -335,7 +335,7 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
var dimensions = {};
return this.getDimensionValues(region, namespace, metricName, 'ServiceName', dimensions).then(function () {
return { status: 'success', message: 'Data source is working', title: 'Success' };
return { status: 'success', message: 'Data source is working' };
});
};

View File

@ -175,9 +175,9 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
return this.getFields({type: 'date'}).then(function(dateFields) {
var timeField = _.find(dateFields, {text: this.timeField});
if (!timeField) {
return { status: "error", message: "No date field named " + this.timeField + ' found', title: "Error" };
return { status: "error", message: "No date field named " + this.timeField + ' found' };
}
return { status: "success", message: "Index OK. Time field name OK.", title: "Success" };
return { status: "success", message: "Index OK. Time field name OK." };
}.bind(this), function(err) {
console.log(err);
if (err.data && err.data.error) {
@ -185,9 +185,9 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
if (err.data.error.reason) {
message = err.data.error.reason;
}
return { status: "error", message: message, title: "Error" };
return { status: "error", message: message };
} else {
return { status: "error", message: err.status, title: "Error" };
return { status: "error", message: err.status };
}
});
};

View File

@ -205,7 +205,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
this.testDatasource = function() {
return this.metricFindQuery('*').then(function () {
return { status: "success", message: "Data source is working", title: "Success" };
return { status: "success", message: "Data source is working"};
});
};

View File

@ -24,6 +24,11 @@ describe('GraphiteQueryCtrl', function() {
ctx.target = {target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)'};
ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
ctx.panelCtrl = {panel: {}};
ctx.panelCtrl = {
panel: {
targets: [ctx.target]
}
};
ctx.panelCtrl.refresh = sinon.spy();
ctx.ctrl = $controller(GraphiteQueryCtrl, {$scope: ctx.scope}, {

View File

@ -196,11 +196,11 @@ export default class InfluxDatasource {
return this.metricFindQuery('SHOW DATABASES').then(res => {
let found = _.find(res, {text: this.database});
if (!found) {
return { status: "error", message: "Could not find the specified database name.", title: "DB Not found" };
return { status: "error", message: "Could not find the specified database name." };
}
return { status: "success", message: "Data source is working", title: "Success" };
return { status: "success", message: "Data source is working" };
}).catch(err => {
return { status: "error", message: err.message, title: "Test Failed" };
return { status: "error", message: err.message };
});
}

View File

@ -92,7 +92,7 @@
<div class="gf-form-inline" ng-if="ctrl.target.orderByTime === 'DESC'">
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">ORDER BY</label>
<label class="gf-form-label pointer" ng-click="ctrl.removeOrderByTime()">time <span classs="query-keyword">DESC</span> <i class="fa fa-remove"></i></label>
<label class="gf-form-label pointer" ng-click="ctrl.removeOrderByTime()">time <span class="query-keyword">DESC</span> <i class="fa fa-remove"></i></label>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>

View File

@ -19,9 +19,13 @@ describe('InfluxDBQueryCtrl', function() {
ctx.$q = $q;
ctx.scope = $rootScope.$new();
ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
ctx.panelCtrl = {panel: {}};
ctx.panelCtrl.refresh = sinon.spy();
ctx.target = {target: {}};
ctx.panelCtrl = {
panel: {
targets: [ctx.target]
}
};
ctx.panelCtrl.refresh = sinon.spy();
ctx.ctrl = $controller(InfluxQueryCtrl, {$scope: ctx.scope}, {
panelCtrl: ctx.panelCtrl,
target: ctx.target,

View File

@ -118,13 +118,13 @@ export class MysqlDatasource {
}],
}
}).then(res => {
return { status: "success", message: "Database Connection OK", title: "Success" };
return { status: "success", message: "Database Connection OK"};
}).catch(err => {
console.log(err);
if (err.data && err.data.message) {
return { status: "error", message: err.data.message, title: "Error" };
return { status: "error", message: err.data.message };
} else {
return { status: "error", message: err.status, title: "Error" };
return { status: "error", message: err.status };
}
});
}

View File

@ -296,7 +296,7 @@ function (angular, _, dateMath) {
this.testDatasource = function() {
return this._performSuggestQuery('cpu', 'metrics').then(function () {
return { status: "success", message: "Data source is working", title: "Success" };
return { status: "success", message: "Data source is working" };
});
};

View File

@ -18,7 +18,11 @@ describe('OpenTsQueryCtrl', function() {
ctx.$q = $q;
ctx.scope = $rootScope.$new();
ctx.target = {target: ''};
ctx.panelCtrl = {panel: {}};
ctx.panelCtrl = {
panel: {
targets: [ctx.target]
}
};
ctx.panelCtrl.refresh = sinon.spy();
ctx.datasource.getAggregators = sinon.stub().returns(ctx.$q.when([]));
ctx.datasource.getFilterTypes = sinon.stub().returns(ctx.$q.when([]));

View File

@ -241,7 +241,7 @@ export class PrometheusDatasource {
testDatasource() {
return this.metricFindQuery('metrics(.*)').then(function() {
return { status: 'success', message: 'Data source is working', title: 'Success' };
return { status: 'success', message: 'Data source is working'};
});
}

View File

@ -65,20 +65,20 @@ var PrometheusHighlightRules = function() {
regex : "\\s+"
} ],
"start-label-matcher" : [ {
token : "label.name",
token : "keyword",
regex : '[a-zA-Z_][a-zA-Z0-9_]*'
}, {
token : "label.matching_operator",
regex : '=|!=|=~|!~'
token : "keyword.operator",
regex : '=~|=|!~|!='
}, {
token : "label.value",
token : "string",
regex : '"[^"]*"|\'[^\']*\''
}, {
token : "label.matching_delimiter",
token : "punctuation.operator",
regex : ",",
push : 'start-label-matcher'
}, {
token : "label.matching_end",
token : "paren.rparen",
regex : "}",
next : "start"
} ]

View File

@ -1,8 +1,8 @@
<query-editor-row query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="false">
<div class="gf-form-inline">
<div class="gf-form gf-form--grow">
<code-editor content="ctrl.target.expr" on-change="ctrl.refreshMetricData()"
get-completer="ctrl.getCompleter()" data-mode="prometheus">
<code-editor content="ctrl.target.expr" datasource="ctrl.datasource" on-change="ctrl.refreshMetricData()"
get-completer="ctrl.getCompleter()" data-mode="prometheus" code-editor-focus="ctrl.isLastQuery">
</code-editor>
</div>
</div>

View File

@ -0,0 +1,27 @@
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import {PromCompleter} from '../completer';
import {PrometheusDatasource} from '../datasource';
describe('Prometheus editor completer', function() {
let editor = {};
let session = {
getTokenAt: sinon.stub().returns({}),
getLine: sinon.stub().returns(""),
};
let datasourceStub = <PrometheusDatasource>{};
let completer = new PromCompleter(datasourceStub);
describe("When inside brackets", () => {
it("Should return range vectors", () => {
completer.getCompletions(editor, session, 10, "[", (s, res) => {
expect(res[0]).to.eql({caption: '1s', value: '[1s', meta: 'range vector'});
});
});
});
});

View File

@ -48,26 +48,14 @@
<div class="gf-form">
<label class="gf-form-label width-6">Mode</label>
<div class="gf-form-select-wrapper max-width-15">
<select class="gf-form-input" ng-model="ctrl.panel.xaxis.mode" ng-options="v as k for (k, v) in ctrl.xAxisModes" ng-change="ctrl.xAxisOptionChanged()"> </select>
<select class="gf-form-input" ng-model="ctrl.panel.xaxis.mode" ng-options="v as k for (k, v) in ctrl.xAxisModes" ng-change="ctrl.xAxisModeChanged()"> </select>
</div>
</div>
<!-- Table mode -->
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'field'">
<label class="gf-form-label width-6">Name</label>
<metric-segment-model property="ctrl.panel.xaxis.name" get-options="ctrl.getDataFieldNames(false)" on-change="ctrl.xAxisOptionChanged()" custom="false" css-class="width-10" select-mode="true"></metric-segment-model>
</div>
<!-- Series mode -->
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'field'">
<label class="gf-form-label width-6">Value</label>
<metric-segment-model property="ctrl.panel.xaxis.values[0]" get-options="ctrl.getDataFieldNames(true)" on-change="ctrl.xAxisOptionChanged()" custom="false" css-class="width-10" select-mode="true"></metric-segment-model>
</div>
<!-- Series mode -->
<div class="gf-form" ng-if="ctrl.panel.xaxis.mode === 'series'">
<label class="gf-form-label width-6">Value</label>
<metric-segment-model property="ctrl.panel.xaxis.values[0]" options="ctrl.xAxisStatOptions" on-change="ctrl.xAxisOptionChanged()" custom="false" css-class="width-10" select-mode="true"></metric-segment-model>
<metric-segment-model property="ctrl.panel.xaxis.values[0]" options="ctrl.xAxisStatOptions" on-change="ctrl.xAxisValueChanged()" custom="false" css-class="width-10" select-mode="true"></metric-segment-model>
</div>
<!-- Histogram mode -->

View File

@ -59,10 +59,12 @@ export class AxesEditorCtrl {
this.panelCtrl.render();
}
xAxisOptionChanged() {
if (!this.panel.xaxis.values || !this.panel.xaxis.values[0]){
this.panelCtrl.processor.setPanelDefaultsForNewXAxisMode();
}
xAxisModeChanged() {
this.panelCtrl.processor.setPanelDefaultsForNewXAxisMode();
this.panelCtrl.onDataReceived(this.panelCtrl.dataList);
}
xAxisValueChanged() {
this.panelCtrl.onDataReceived(this.panelCtrl.dataList);
}

View File

@ -3,7 +3,7 @@
<div class="gf-form">
<span class="gf-form-label">Mode</span>
<span class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="ctrl.panel.mode" ng-options="f for f in ['html','markdown','text']"></select>
<select class="gf-form-input" ng-model="ctrl.panel.mode" ng-options="f for f in ['html','markdown']"></select>
</span>
</div>
</div>

View File

@ -103,7 +103,7 @@ $tight-form-func-bg: #333;
$tight-form-func-highlight-bg: #444;
$modal-background: $black;
$code-tag-bg: $dark-5;
$code-tag-bg: $gray-1;
$code-tag-border: lighten($code-tag-bg, 2%);
@ -238,17 +238,15 @@ $paginationActiveBackground: $blue;
// Form states and alerts
// -------------------------
$state-warning-text: $warn;
$state-warning-bg: $brand-warning;
$warning-text-color: $warn;
$error-text-color: #E84D4D;
$success-text-color: #12D95A;
$info-text-color: $blue-dark;
$errorText: #E84D4D;
$errorBackground: $btn-danger-bg;
$successText: #12D95A;
$successBackground: $btn-success-bg;
$infoText: $blue-dark;
$infoBackground: $blue-dark;
$alert-error-bg: linear-gradient(90deg, #d44939, #e0603d);
$alert-success-bg: linear-gradient(90deg, #3aa655, #47b274);
$alert-warning-bg: linear-gradient(90deg, #d44939, #e0603d);
$alert-info-bg: linear-gradient(100deg, #1a4552, #00374a);
// popover
$popover-bg: $panel-bg;
@ -258,6 +256,8 @@ $popover-border-color: $gray-1;
$popover-help-bg: $btn-secondary-bg;
$popover-help-color: $text-color;
$popover-error-bg: $btn-danger-bg;
// Tooltips and popovers
// -------------------------
$tooltipColor: $popover-help-color;
@ -276,7 +276,7 @@ $card-background-hover: linear-gradient(135deg, #343434, #262626);
$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3);
// info box
$info-box-background: linear-gradient(100deg, #1a4552, #0b2127);
$info-box-background: linear-gradient(100deg, #1a4552, #00374a);
// footer
$footer-link-color: $gray-1;

View File

@ -259,29 +259,23 @@ $paginationActiveBackground: $blue;
// Form states and alerts
// -------------------------
$state-warning-text: lighten($orange, 10%);
$state-warning-bg: $orange;
$warningBorder: transparent;
$warning-text-color: lighten($orange, 10%);
$error-text-color: lighten($red, 10%);
$success-text-color: lighten($green, 10%);
$info-text-color: $blue;
$errorText: lighten($red, 10%);
$errorBackground: $red;
$errorBorder: transparent;
$successText: lighten($green, 10%);
$successBackground: $green;
$successBorder: transparent;
$infoText: $blue;
$infoBackground: $blue-dark;
$infoBorder: transparent;
$alert-error-bg: linear-gradient(90deg, #d44939, #e0603d);
$alert-success-bg: linear-gradient(90deg, #3aa655, #47b274);
$alert-warning-bg: linear-gradient(90deg, #d44939, #e0603d);
$alert-info-bg: $blue-dark;
// popover
$popover-bg: $gray-5;
$popover-color: $text-color;
$popover-border-color: $gray-3;
$popover-help-bg: $blue-dark;
$popover-help-color: $gray-6;
$popover-error-bg: $btn-danger-bg;
// Tooltips and popovers
// -------------------------

View File

@ -31,21 +31,21 @@ cite { font-style: normal; }
a.muted:hover,
a.muted:focus { color: darken($text-muted, 10%); }
.text-warning { color: $state-warning-text; }
.text-warning { color: $warning-text-color; }
a.text-warning:hover,
a.text-warning:focus { color: darken($state-warning-text, 10%); }
a.text-warning:focus { color: darken($warning-text-color, 10%); }
.text-error { color: $errorText; }
.text-error { color: $error-text-color; }
a.text-error:hover,
a.text-error:focus { color: darken($errorText, 10%); }
a.text-error:focus { color: darken($error-text-color, 10%); }
.text-info { color: $infoText; }
.text-info { color: $info-text-color; }
a.text-info:hover,
a.text-info:focus { color: darken($infoText, 10%); }
a.text-info:focus { color: darken($info-text-color, 10%); }
.text-success { color: $successText; }
.text-success { color: $success-text-color; }
a.text-success:hover,
a.text-success:focus { color: darken($successText, 10%); }
a.text-success:focus { color: darken($success-text-color, 10%); }
a { cursor: pointer; }
a[disabled] {
@ -130,7 +130,7 @@ small,
mark,
.mark {
padding: .2em;
background-color: $state-warning-bg;
background: $alert-warning-bg;
}
@ -296,7 +296,7 @@ a.external-link {
max-width: 100%;
}
ul {
ul, ol {
padding-left: $spacer*1.5;
margin-bottom: $spacer;
}
@ -328,7 +328,7 @@ a.external-link {
margin-bottom: 0;
}
ul:last-child {
ul:last-child, ol:last-child {
margin-bottom: 0;
}
}

View File

@ -7,60 +7,57 @@
// -------------------------
.alert {
padding: 0.5rem 2rem 0.5rem 1rem;
padding: 1.25rem 2rem 1.25rem 1.5rem;
margin-bottom: $line-height-base;
text-shadow: 0 1px 0 rgba(255,255,255,.5);
background-color: $state-warning-bg;
text-shadow: 0 2px 0 rgba(255,255,255,.5);
background: $alert-error-bg;
position: relative;
color: $white;
text-shadow: 0 1px 0 rgba(0,0,0,.5);
text-shadow: 0 1px 0 rgba(0,0,0,.2);
border-radius: 2px;
display: flex;
flex-direction: row;
}
// Alternate styles
// -------------------------
.alert-success {
background-color: $successBackground;
background: $alert-success-bg;
}
.alert-danger,
.alert-error {
background-color: $errorBackground;
background: $alert-error-bg;
}
.alert-info {
background-color: $infoBackground;
background: $alert-info-bg;
}
.alert-warning {
background-color: $state-warning-bg;
background: $alert-warning-bg;
}
.page-alert-list {
z-index: 8000;
min-width: 300px;
max-width: 300px;
min-width: 400px;
max-width: 600px;
position: fixed;
right: 20px;
top: 56px;
right: 10px;
top: 60px;
}
.alert-close {
position: absolute;
top: -4px;
right: -2px;
width: 16px;
height: 16px;
padding: 0;
background: $white;
border-radius: 50%;
padding: 0 0 0 1rem;
border: none;
font-size: 1.1rem;
color: $dark-4;
background: none;
display: flex;
align-items: center;
.fa {
position: relative;
top: -2px;
align-self: flex-end;
font-size: 1.5rem;
color: rgba(255,255,255,.75)
}
}
@ -68,3 +65,18 @@
font-weight: $font-weight-semi-bold;
padding-bottom: 2px;
}
.alert-icon {
padding: 0 1rem 0 0;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
.fa {
font-size: 1.5rem;
}
}
.alert-body {
flex-grow: 1;
}

View File

@ -28,7 +28,7 @@
background-color: $dropdownBackground !important;
color: $dropdownLinkColor !important;
border: 1px solid $dropdownBorder !important;
width: 320px !important;
width: 550px !important;
.ace_scroller {
.ace_selected, .ace_active-line, .ace_line-hover {
@ -77,3 +77,7 @@ $doc-font-size: $font-size-sm;
.ace_tooltip {
border-radius: 3px;
}
.ace_hidden-cursors .ace_cursor {
opacity: 0 !important;
}

View File

@ -5,7 +5,7 @@ $useDropShadow: false;
$attachmentOffset: 0%;
$easing: cubic-bezier(0, 0, 0.265, 1.00);
@include drop-theme("error", $errorBackground, $popover-color);
@include drop-theme("error", $popover-error-bg, $popover-color);
@include drop-theme("popover", $popover-bg, $popover-color, $popover-border-color);
@include drop-theme("help", $popover-help-bg, $popover-help-color);

View File

@ -138,7 +138,7 @@ div.flot-text {
&--error {
display: block;
color: $text-color;
@include panel-corner-color($errorBackground);
@include panel-corner-color($popover-error-bg);
.fa:before {
content: "\f12a";
}

View File

@ -167,6 +167,20 @@ define([
var res = kbn.calculateInterval(range, 900, '>15ms');
expect(res.interval).to.be('15ms');
});
it('1d 1 resolution', function() {
var range = { from: dateMath.parse('now-1d'), to: dateMath.parse('now') };
var res = kbn.calculateInterval(range, 1, null);
expect(res.interval).to.be('1d');
expect(res.intervalMs).to.be(86400000);
});
it('86399s 1 resolution', function() {
var range = { from: dateMath.parse('now-86390s'), to: dateMath.parse('now') };
var res = kbn.calculateInterval(range, 1, null);
expect(res.interval).to.be('12h');
expect(res.intervalMs).to.be(43200000);
});
});
describe('hex', function() {

View File

@ -31,11 +31,14 @@
<div class="page-alert-list">
<div ng-repeat='alert in dashAlerts.list' class="alert-{{alert.severity}} alert">
<div class="alert-icon"><i class="{{alert.icon}}"></i></div>
<div class="alert-body">
<div class="alert-title">{{alert.title}}</div>
<div class="alert-text" ng-bind='alert.text'></div>
</div>
<button type="button" class="alert-close" ng-click="dashAlerts.clear(alert)">
<i class="fa fa-times-circle"></i>
<i class="fa fa fa-remove"></i>
</button>
<div class="alert-title">{{alert.title}}</div>
<div ng-bind='alert.text'></div>
</div>
</div>