Merge branch 'master' into table-data-support

This commit is contained in:
Peter Holmberg 2019-01-31 14:16:06 +01:00
commit ed0f5b71c7
84 changed files with 665 additions and 279 deletions

View File

@ -333,6 +333,7 @@ jobs:
docker:
- image: grafana/grafana-ci-deploy:1.2.0
steps:
- checkout
- attach_workspace:
at: .
- run:

View File

@ -1,4 +1,9 @@
# 6.0.0-beta1 (unreleased)
# 6.0.0-beta2 (unreleased)
### Minor
* **Pushover**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae)
# 6.0.0-beta1 (2019-01-30)
### New Features
@ -7,9 +12,11 @@
* **Influxdb**: Add support for time zone (`tz`) clause [#10322](https://github.com/grafana/grafana/issues/10322), thx [@cykl](https://github.com/cykl)
* **Snapshots**: Enable deletion of public snapshot [#14109](https://github.com/grafana/grafana/issues/14109)
* **Provisioning**: Provisioning support for alert notifiers [#10487](https://github.com/grafana/grafana/issues/10487), thx [@pbakulev](https://github.com/pbakulev)
* **Explore**: A whole new way to do ad-hoc metric queries and exploration. Split view in half and compare metrics & logs and much much more. [Read more here](http://docs.grafana.org/features/explore/)
### Minor
* **Templating**: Built in time range variables `$__from` and `$__to`, [#1909](https://github.com/grafana/grafana/issues/1909)
* **Alerting**: Use separate timeouts for alert evals and notifications [#14701](https://github.com/grafana/grafana/issues/14701), thx [@sharkpc0813](https://github.com/sharkpc0813)
* **Elasticsearch**: Add support for offset in date histogram aggregation [#12653](https://github.com/grafana/grafana/issues/12653), thx [@mattiarossi](https://github.com/mattiarossi)
* **Elasticsearch**: Add support for moving average and derivative using doc count (metric count) [#8843](https://github.com/grafana/grafana/issues/8843) [#11175](https://github.com/grafana/grafana/issues/11175)

View File

@ -12,8 +12,8 @@ weight = 11
# Using Google Stackdriver in Grafana
> Only available in Grafana v5.3+.
> The datasource is currently a beta feature and is subject to change.
> Available as a beta feature in Grafana v5.3.x and v5.4.x.
> Officially released in Grafana v6.0.0
Grafana ships with built-in support for Google Stackdriver. Just add it as a datasource and you are ready to build dashboards for your Stackdriver metrics.

View File

@ -27,23 +27,6 @@ For infrastructure monitoring and incident response, you no longer need to switc
If you just want to explore your data and do not want to create a dashboard then Explore makes this much easier. Explore will show the results as both a graph and a table enabling you to see trends in the data and more detail at the same time (if the datasource supports both graph and table data).
## Turning the Explore Feature On
Explore will be officially released in Grafana 6.0. It is however already in the latest nightly builds of Grafana and can be turned using a feature flag in the config file. Restart Grafana after making the config file change.
```ini
[explore]
# Enable the Explore section
enabled = true
```
Or if using docker:
```bash
docker pull grafana/grafana:master
docker run --name grafana -p 3000:3000 -e "GF_EXPLORE_ENABLED=true" grafana/grafana:master
```
## How to Start Exploring
There is a new Explore icon on the menu bar to the left. This opens a new empty Explore tab.
@ -116,7 +99,14 @@ The Logs Explorer (the `Log labels` button) next to the query field shows a list
Once the result is returned, the log panel shows a list of log rows and a bar chart where the x-axis shows the time and the y-axis shows the frequency/count.
{{< docs-imagebox img="/img/docs/v60/explore_loki.png" class="docs-image--no-shadow" caption="Explore Loki Log Streams" >}}
<div class="medium-6 columns">
<video width="800" height="500" controls>
<source src="/assets/videos/explore_loki.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<br />
#### Log Stream Selector

View File

@ -26,7 +26,7 @@ Grafana v5.3 brings new features, many enhancements and bug fixes. This article
{{< docs-imagebox img="/img/docs/v53/stackdriver-with-heatmap.png" max-width= "600px" class="docs-image--no-shadow docs-image--right" >}}
Grafana v5.3 ships with built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) and enables you to visualize your Stackdriver metrics in Grafana.
Grafana v5.3 ships with built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) and enables you to visualize your Stackdriver metrics in Grafana.
Getting started with the plugin is easy. Simply create a GCE Service account that has access to the Stackdriver API scope, download the Service Account key file from Google and upload it on the Stackdriver datasource config page in Grafana and you should have a secure server-to-server authentication setup. Like other core plugins, Stackdriver has built-in support for alerting. It also comes with support for heatmaps and basic variables.

View File

@ -0,0 +1,159 @@
+++
title = "What's New in Grafana v6.0"
description = "Feature & improvement highlights for Grafana v6.0"
keywords = ["grafana", "new", "documentation", "6.0"]
type = "docs"
[menu.docs]
name = "Version 6.0"
identifier = "v6.0"
parent = "whatsnew"
weight = -11
+++
# What's New in Grafana v6.0
This update to Grafana introduces a new way of exploring your data, support for log data and tons of other features.
Grafana v6.0 is out in **Beta**, [Download Now!](https://grafana.com/grafana/download/beta)
The main highlights are:
- [Explore]({{< relref "#explore" >}}) - A new query focused workflow for ad-hoc data exploration and troubleshooting.
- [Grafana Loki]({{< relref "#explore-and-grafana-loki" >}}) - Integration with the new open source log aggregation system from Grafana Labs.
- [Gauge Panel]({{< relref "#gauge-panel" >}}) - A new standalone panel for gauges.
- [New Panel Editor UX]({{< relref "#new-panel-editor" >}}) improves panel editing
and enables easy switching between different visualizations.
- [Google Stackdriver Datasource]({{< relref "#google-stackdriver-datasource" >}}) is out of beta and is officially released.
- [Azure Monitor]({{< relref "#azure-monitor-datasource" >}}) plugin is ported from being an external plugin to being a core datasource
- [React Plugin]({{< relref "#react-panels-query-editors" >}}) support enables an easier way to build plugins.
- [Named Colors]({{< relref "#named-colors" >}}) in our new improved color picker.
## Explore
{{< docs-imagebox img="/img/docs/v60/explore_prometheus.png" max-width="800px" class="docs-image--right" caption="Screenshot of the new Explore option in the panel menu" >}}
Grafana's dashboard UI is all about building dashboards for visualization. **Explore** strips away all the dashboard and panel options so that you can focus on the query & metric exploration. Iterate until you have a working query and then think about building a dashboard. You can also jump from a dashboard panel into **Explore** and from there do some ad-hoc query exporation with the panel queries as a starting point.
For infrastructure monitoring and incident response, you no longer need to switch to other tools to debug what went wrong. **Explore** allows you to dig deeper into your metrics and logs to find the cause. Grafana's new logging datasource, [Loki](https://github.com/grafana/loki) is tightly integrated into Explore and allows you to correlate metrics and logs by viewing them side-by-side.
**Explore** is a new paradigm for Grafana. It creates a new interactive debugging workflow that integrates two pillars
of observability - metrics and logs. Explore works with every datasource but for Prometheus we have customized the
query editor and the experience to provide the best possible exploration UX.
### Explore and Prometheus
Explore features a new [Prometheus query editor](/features/explore/#prometheus-specific-features). This new editor has improved autocomplete, metric tree selector,
integrations with the Explore table view for easy label filtering and useful query hints that can automatically apply
functions to your query. There is also integration between Prometheus and Grafana Loki (see more about Loki below) that
enabled jumping between metrics query and logs query with preserved label filters.
### Explore splits
Explore supports splitting the view so you can compare different queries, different datasources and metrics & logs side by side!
{{< docs-imagebox img="/img/docs/v60/explore_split.png" max-width="800px" caption="Screenshot of the new Explore option in the panel menu" >}}
<br />
### Explore and Grafana Loki
The log exploration & visualization features in Explore are available to any data source but are currently only implemented by the new open source log
aggregation system from Grafana Lab called [Grafana Loki](https://github.com/grafana/loki).
Loki is a horizontally-scalable, highly-available, multi-tenant log aggregation system inspired by Prometheus. It is designed to be very cost effective, as it does not index the contents of the logs, but rather a set of labels for each log stream. The logs from Loki are queried in a similar way to querying with label selectors in Prometheus. It uses labels to group log streams which can be made to match up with your Prometheus labels.
Read more about Grafana Loki [here](https://github.com/grafana/loki) or [Grafana Labs hosted Loki](https://grafana.com/loki).
The Explore feature allows you to query logs and features a new log panel. In the near future, we will be adding support
for other log sources to Explore and the next planned integration is Elasticsearch.
<div class="medium-6 columns">
<video width="800" height="500" controls>
<source src="/assets/videos/explore_loki.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<br />
## New Panel Editor
Grafana v6.0 has a completely redesigned UX around editing panels. You can now resize the visualization area if you want
more space for queries & options and vice versa. You can now also change visualization (panel type) from within the new
panel edit mode. No need to add a new panel to try out different visualizations! Checkout the
video below to see the new Panel Editor in action.
<div class="medium-6 columns">
<video width="800" height="500" controls>
<source src="/assets/videos/panel_change_viz.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<br>
### Gauge Panel
We have created a new separate Gauge panel as we felt having this visualization be a hidden option in the Singlestat panel
was not ideal. When it supports 100% of the Singlestat Gauge features we plan to add a migration so all
singlestats that use it become Gauge panels instead. This new panel contains a new **Threshold** editor that we will
continue to refine and start using in other panels.
{{< docs-imagebox img="/img/docs/v60/gauge_panel.png" max-width="600px" caption="Gauge Panel" >}}
<br>
### React Panels & Query Editors
A major part of all the work that has gone into Grafana v6.0 has been on the migration to React. This investment
is part of the future proofing of Grafana and it's code base and ecosystem. Starting in v6.0 **Panels** and **Data
source** plugins can be written in React using our published `@grafana/ui` sdk library. More information on this
will be shared closer to or just after release.
{{< docs-imagebox img="/img/docs/v60/react_panels.png" max-width="600px" caption="React Panel" >}}
<br />
### Google Stackdriver Datasource
Built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) is officially released in Grafana 6.0. Beta support was added in Grafana 5.3 and we have added lots of improvements since then.
To get started read the guide: [Using Google Stackdriver in Grafana](/features/datasources/stackdriver/).
### Azure Monitor Datasource
One of the goals of the Grafana v6.0 release is to add support for the three major clouds. Amazon Cloudwatch has been a core datasource for years and Google Stackdriver is also now supported. We developed an external plugin for Azure Monitor last year and for this release the [plugin](https://grafana.com/plugins/grafana-azure-monitor-datasource) is being moved into Grafana to be one of the built-in datasources. For users of the external plugin, Grafana will automatically start using the built-in version. As a core datasource, the Azure Monitor datasource will get alerting support for the official 6.0 release.
The Azure Monitor datasource integrates four Azure services with Grafana - Azure Monitor, Azure Log Analytics, Azure Application Insights and Azure Application Insights Analytics.
### Provisioning support for alert notifiers
Grafana now added support for provisioning alert notifiers from configuration files. Allowing operators to provision notifiers without using the UI or the API. A new field called `uid` has been introduced which is a string identifier that the administrator can set themselves. Same kind of identifier used for dashboards since v5.0. This feature makes it possible to use the same notifier configuration in multiple environments and refer to notifiers in dashboard json by a string identifier instead of the numeric id which depends on insert order and how many notifiers that exists in the instance.
### Auth and session token improvements
The previous session storage implementation in Grafana was causing problems in larger HA setups due to too many write requests to the database. The remember me token also have several security issues which is why we decided to rewrite auth middleware in Grafana and remove the session storage since most operations using the session storage could be rewritten to use cookies or data already made available earlier in the request.
If you are using `Auth proxy` for authentication the session storage will still be used but our goal is to remove this ASAP as well.
This release will force all users to log in again since their previous token is not valid anymore.
### Named Colors
{{< docs-imagebox img="/img/docs/v60/named_colors.png" max-width="400px" class="docs-image--right" caption="Named Colors" >}}
We have updated the color picker to show named colors and primary colors. We hope this will improve accessibility and
helps making colors more consistent across dashboards. We hope to do more in this color picker in the future, like show
colors used in the dashboard.
Named colors also enables Grafana to adapt colors to the current theme.
<div class="clearfix"></div>
### Other features
- The ElasticSearch datasource now supports [bucket script pipeline aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-script-aggregation.html). This gives the ability to do per bucket computations like the difference or ratio between two metrics.
- Support for Google Hangouts Chat alert notifications
- New built in template variables for the current time range in `$__from` and `$__to`
## Changelog
Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list of new features, changes, and bug fixes.

View File

@ -248,7 +248,7 @@ Grafana has global built-in variables that can be used in expressions in the que
### Time range variables
Grafana has two built in time range variables in `$__from` and `$__to`. They are currently always interpolated
as epoch milliseconds.
as epoch milliseconds. These variables are only available in Grafana v6.0 and above.
### The $__interval Variable

View File

@ -5,7 +5,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
"version": "6.0.0-pre1",
"version": "6.0.0-prebeta2",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"
@ -25,7 +25,9 @@
"@types/node": "^8.0.31",
"@types/react": "^16.7.6",
"@types/react-dom": "^16.0.9",
"@types/react-grid-layout": "^0.16.6",
"@types/react-select": "^2.0.4",
"@types/react-virtualized": "^9.18.12",
"angular-mocks": "1.6.6",
"autoprefixer": "^6.4.0",
"axios": "^0.17.1",
@ -116,7 +118,8 @@
"typecheck": "tsc --noEmit",
"jest": "jest --notify --watch",
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
"precommit": "grunt precommit"
"precommit": "grunt precommit",
"storybook": "cd packages/grafana-ui && yarn storybook"
},
"husky": {
"hooks": {

View File

@ -1,7 +1,6 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withKnobs } from '@storybook/addon-knobs';
import SpectrumPalette from './SpectrumPalette';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { UseState } from '../../utils/storybook/UseState';
@ -11,8 +10,9 @@ const SpectrumPaletteStories = storiesOf('UI/ColorPicker/Palettes/SpectrumPalett
SpectrumPaletteStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
SpectrumPaletteStories.add('Named colors swatch - support for named colors', () => {
SpectrumPaletteStories.add('default', () => {
const selectedTheme = getThemeKnob();
return (
<UseState initialState="red">
{(selectedColor, updateSelectedColor) => {

View File

@ -1,8 +1,11 @@
package notifiers
import (
"bytes"
"fmt"
"net/url"
"io"
"mime/multipart"
"os"
"strconv"
"github.com/grafana/grafana/pkg/bus"
@ -91,6 +94,7 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error)
retry, _ := strconv.Atoi(model.Settings.Get("retry").MustString())
expire, _ := strconv.Atoi(model.Settings.Get("expire").MustString())
sound := model.Settings.Get("sound").MustString()
uploadImage := model.Settings.Get("uploadImage").MustBool(true)
if userKey == "" {
return nil, alerting.ValidationError{Reason: "User key not given"}
@ -107,6 +111,7 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error)
Expire: expire,
Device: device,
Sound: sound,
Upload: uploadImage,
log: log.New("alerting.notifier.pushover"),
}, nil
}
@ -120,6 +125,7 @@ type PushoverNotifier struct {
Expire int
Device string
Sound string
Upload bool
log log.Logger
}
@ -140,38 +146,22 @@ func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
if evalContext.Error != nil {
message += fmt.Sprintf("\n<b>Error message:</b> %s", evalContext.Error.Error())
}
if evalContext.ImagePublicUrl != "" {
message += fmt.Sprintf("\n<a href=\"%s\">Show graph image</a>", evalContext.ImagePublicUrl)
}
if message == "" {
message = "Notification message missing (Set a notification message to replace this text.)"
}
q := url.Values{}
q.Add("user", this.UserKey)
q.Add("token", this.ApiToken)
q.Add("priority", strconv.Itoa(this.Priority))
if this.Priority == 2 {
q.Add("retry", strconv.Itoa(this.Retry))
q.Add("expire", strconv.Itoa(this.Expire))
headers, uploadBody, err := this.genPushoverBody(evalContext, message, ruleUrl)
if err != nil {
this.log.Error("Failed to generate body for pushover", "error", err)
return err
}
if this.Device != "" {
q.Add("device", this.Device)
}
if this.Sound != "default" {
q.Add("sound", this.Sound)
}
q.Add("title", evalContext.GetNotificationTitle())
q.Add("url", ruleUrl)
q.Add("url_title", "Show dashboard with alert")
q.Add("message", message)
q.Add("html", "1")
cmd := &m.SendWebhookSync{
Url: PUSHOVER_ENDPOINT,
HttpMethod: "POST",
HttpHeader: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
Body: q.Encode(),
HttpHeader: headers,
Body: uploadBody.String(),
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
@ -181,3 +171,109 @@ func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
return nil
}
func (this *PushoverNotifier) genPushoverBody(evalContext *alerting.EvalContext, message string, ruleUrl string) (map[string]string, bytes.Buffer, error) {
var b bytes.Buffer
var err error
w := multipart.NewWriter(&b)
// Add image only if requested and available
if this.Upload && evalContext.ImageOnDiskPath != "" {
f, err := os.Open(evalContext.ImageOnDiskPath)
if err != nil {
return nil, b, err
}
defer f.Close()
fw, err := w.CreateFormFile("attachment", evalContext.ImageOnDiskPath)
if err != nil {
return nil, b, err
}
_, err = io.Copy(fw, f)
if err != nil {
return nil, b, err
}
}
// Add the user token
err = w.WriteField("user", this.UserKey)
if err != nil {
return nil, b, err
}
// Add the api token
err = w.WriteField("token", this.ApiToken)
if err != nil {
return nil, b, err
}
// Add priority
err = w.WriteField("priority", strconv.Itoa(this.Priority))
if err != nil {
return nil, b, err
}
if this.Priority == 2 {
err = w.WriteField("retry", strconv.Itoa(this.Retry))
if err != nil {
return nil, b, err
}
err = w.WriteField("expire", strconv.Itoa(this.Expire))
if err != nil {
return nil, b, err
}
}
// Add device
if this.Device != "" {
err = w.WriteField("device", this.Device)
if err != nil {
return nil, b, err
}
}
// Add sound
if this.Sound != "default" {
err = w.WriteField("sound", this.Sound)
if err != nil {
return nil, b, err
}
}
// Add title
err = w.WriteField("title", evalContext.GetNotificationTitle())
if err != nil {
return nil, b, err
}
// Add URL
err = w.WriteField("url", ruleUrl)
if err != nil {
return nil, b, err
}
// Add URL title
err = w.WriteField("url_title", "Show dashboard with alert")
if err != nil {
return nil, b, err
}
// Add message
err = w.WriteField("message", message)
if err != nil {
return nil, b, err
}
// Mark as html message
err = w.WriteField("html", "1")
if err != nil {
return nil, b, err
}
w.Close()
headers := map[string]string{
"Content-Type": w.FormDataContentType(),
}
return headers, b, nil
}

View File

@ -1,7 +1,7 @@
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
export class BackendSrv {
private inFlightRequests = {};

View File

@ -1,6 +1,7 @@
import kbn from 'app/core/utils/kbn';
import { getFlotTickDecimals } from 'app/core/utils/ticks';
import _ from 'lodash';
import { getValueFormat } from '@grafana/ui';
function matchSeriesOverride(aliasOrRegex, seriesAlias) {
if (!aliasOrRegex) {
@ -31,13 +32,13 @@ export function updateLegendValues(data: TimeSeries[], panel, height) {
const yaxes = panel.yaxes;
const seriesYAxis = series.yaxis || 1;
const axis = yaxes[seriesYAxis - 1];
const formater = kbn.valueFormats[axis.format];
const formatter = getValueFormat(axis.format);
// decimal override
if (_.isNumber(panel.decimals)) {
series.updateLegendValues(formater, panel.decimals, null);
series.updateLegendValues(formatter, panel.decimals, null);
} else if (_.isNumber(axis.decimals)) {
series.updateLegendValues(formater, axis.decimals + 1, null);
series.updateLegendValues(formatter, axis.decimals + 1, null);
} else {
// auto decimals
// legend and tooltip gets one more decimal precision
@ -45,7 +46,7 @@ export function updateLegendValues(data: TimeSeries[], panel, height) {
const { datamin, datamax } = getDataMinMax(data);
const { tickDecimals, scaledDecimals } = getFlotTickDecimals(datamin, datamax, axis, height);
const tickDecimalsPlusOne = (tickDecimals || -1) + 1;
series.updateLegendValues(formater, tickDecimalsPlusOne, scaledDecimals + 2);
series.updateLegendValues(formatter, tickDecimalsPlusOne, scaledDecimals + 2);
}
}
}
@ -105,7 +106,7 @@ export default class TimeSeries {
this.aliasEscaped = _.escape(opts.alias);
this.color = opts.color;
this.bars = { fillColor: opts.color };
this.valueFormater = kbn.valueFormats.none;
this.valueFormater = getValueFormat('none');
this.stats = {};
this.legend = true;
this.unit = opts.unit;

View File

@ -12,8 +12,8 @@ import StateHistory from './StateHistory';
import 'app/features/alerting/AlertTabCtrl';
// Types
import { DashboardModel } from '../dashboard/dashboard_model';
import { PanelModel } from '../dashboard/panel_model';
import { DashboardModel } from '../dashboard/state/DashboardModel';
import { PanelModel } from '../dashboard/state/PanelModel';
import { TestRuleResult } from './TestRuleResult';
interface Props {

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import alertDef from './state/alertDef';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { DashboardModel } from '../dashboard/dashboard_model';
import { DashboardModel } from '../dashboard/state/DashboardModel';
import appEvents from '../../core/app_events';
interface Props {

View File

@ -1,6 +1,6 @@
import React from 'react';
import { shallow } from 'enzyme';
import { DashboardModel } from '../dashboard/dashboard_model';
import { DashboardModel } from '../dashboard/state/DashboardModel';
import { Props, TestRuleResult } from './TestRuleResult';
jest.mock('app/core/services/backend_srv', () => ({

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { DashboardModel } from '../dashboard/dashboard_model';
import { DashboardModel } from '../dashboard/state/DashboardModel';
import { LoadingPlaceholder } from '@grafana/ui/src';
export interface Props {

View File

@ -10,7 +10,7 @@ import coreModule from 'app/core/core_module';
import { makeRegions, dedupAnnotations } from './events_processing';
// Types
import { DashboardModel } from '../dashboard/dashboard_model';
import { DashboardModel } from '../dashboard/state/DashboardModel';
export class AnnotationsSrv {
globalAnnotationsPromise: any;

View File

@ -1,5 +1,3 @@
import '../annotations_srv';
import 'app/features/dashboard/time_srv';
import { AnnotationsSrv } from '../annotations_srv';
describe('AnnotationsSrv', () => {

View File

@ -1,8 +1,8 @@
import React from 'react';
import _ from 'lodash';
import config from 'app/core/config';
import { PanelModel } from '../../panel_model';
import { DashboardModel } from '../../dashboard_model';
import { PanelModel } from '../../state/PanelModel';
import { DashboardModel } from '../../state/DashboardModel';
import store from 'app/core/store';
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
import { updateLocation } from 'app/core/actions';

View File

@ -7,7 +7,7 @@ jest.mock('app/core/store', () => {
import _ from 'lodash';
import config from 'app/core/config';
import { DashboardExporter } from './DashboardExporter';
import { DashboardModel } from '../../dashboard_model';
import { DashboardModel } from '../../state/DashboardModel';
describe('given dashboard with repeated panels', () => {
let dash, exported;

View File

@ -1,6 +1,6 @@
import config from 'app/core/config';
import _ from 'lodash';
import { DashboardModel } from '../../dashboard_model';
import { DashboardModel } from '../../state/DashboardModel';
export class DashboardExporter {
constructor(private datasourceSrv) {}

View File

@ -1,7 +1,7 @@
import moment from 'moment';
import angular from 'angular';
import { appEvents, NavModel } from 'app/core/core';
import { DashboardModel } from '../../dashboard_model';
import { DashboardModel } from '../../state/DashboardModel';
export class DashNavCtrl {
dashboard: DashboardModel;

View File

@ -1,7 +1,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { DashboardRow } from '../dashgrid/DashboardRow';
import { PanelModel } from '../panel_model';
import { DashboardRow } from './DashboardRow';
import { PanelModel } from '../../state/PanelModel';
describe('DashboardRow', () => {
let wrapper, panel, dashboardMock;

View File

@ -1,7 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { PanelModel } from '../../state/PanelModel';
import { DashboardModel } from '../../state/DashboardModel';
import templateSrv from 'app/features/templating/template_srv';
import appEvents from 'app/core/app_events';

View File

@ -0,0 +1 @@
export { DashboardRow } from './DashboardRow';

View File

@ -1,5 +1,5 @@
import { coreModule, appEvents, contextSrv } from 'app/core/core';
import { DashboardModel } from '../../dashboard_model';
import { DashboardModel } from '../../state/DashboardModel';
import $ from 'jquery';
import _ from 'lodash';
import angular from 'angular';

View File

@ -24,7 +24,7 @@ export class RowOptionsCtrl {
export function rowOptionsDirective() {
return {
restrict: 'E',
templateUrl: 'public/app/features/dashboard/partials/row_options.html',
templateUrl: 'public/app/features/dashboard/components/RowOptions/template.html',
controller: RowOptionsCtrl,
bindToController: true,
controllerAs: 'ctrl',

View File

@ -3,7 +3,7 @@ import angular from 'angular';
import moment from 'moment';
import locationUtil from 'app/core/utils/location_util';
import { DashboardModel } from '../../dashboard_model';
import { DashboardModel } from '../../state/DashboardModel';
import { HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv } from './HistorySrv';
export class HistoryListCtrl {

View File

@ -1,6 +1,6 @@
import { versions, restore } from './__mocks__/history';
import { HistorySrv } from './HistorySrv';
import { DashboardModel } from '../../dashboard_model';
import { DashboardModel } from '../../state/DashboardModel';
jest.mock('app/core/store');
describe('historySrv', () => {

View File

@ -1,6 +1,6 @@
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import { DashboardModel } from '../../dashboard_model';
import { DashboardModel } from '../../state/DashboardModel';
export interface HistoryListOpts {
limit: number;

View File

@ -5,10 +5,10 @@ import coreModule from 'app/core/core_module';
import { removePanel } from 'app/features/dashboard/utils/panel';
// Services
import { AnnotationsSrv } from '../annotations/annotations_srv';
import { AnnotationsSrv } from '../../annotations/annotations_srv';
// Types
import { DashboardModel } from './dashboard_model';
import { DashboardModel } from '../state/DashboardModel';
export class DashboardCtrl {
dashboard: DashboardModel;

View File

@ -1,16 +1,30 @@
import React from 'react';
import { hot } from 'react-hot-loader';
import ReactGridLayout from 'react-grid-layout';
import ReactGridLayout, { ItemCallback } from 'react-grid-layout';
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
import { DashboardPanel } from './DashboardPanel';
import { DashboardModel } from '../dashboard_model';
import { PanelModel } from '../panel_model';
import { DashboardModel, PanelModel } from '../state';
import classNames from 'classnames';
import sizeMe from 'react-sizeme';
let lastGridWidth = 1200;
let ignoreNextWidthChange = false;
interface GridWrapperProps {
size: { width: number; };
layout: ReactGridLayout.Layout[];
onLayoutChange: (layout: ReactGridLayout.Layout[]) => void;
children: JSX.Element | JSX.Element[];
onDragStop: ItemCallback;
onResize: ItemCallback;
onResizeStop: ItemCallback;
onWidthChange: () => void;
className: string;
isResizable?: boolean;
isDraggable?: boolean;
isFullscreen?: boolean;
}
function GridWrapper({
size,
layout,
@ -24,7 +38,7 @@ function GridWrapper({
isResizable,
isDraggable,
isFullscreen,
}) {
}: GridWrapperProps) {
const width = size.width > 0 ? size.width : lastGridWidth;
// logic to ignore width changes (optimization)
@ -43,7 +57,6 @@ function GridWrapper({
className={className}
isDraggable={isDraggable}
isResizable={isResizable}
measureBeforeMount={false}
containerPadding={[0, 0]}
useCSSTransforms={false}
margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]}
@ -71,22 +84,17 @@ export class DashboardGrid extends React.Component<DashboardGridProps> {
gridToPanelMap: any;
panelMap: { [id: string]: PanelModel };
constructor(props) {
constructor(props: DashboardGridProps) {
super(props);
this.onLayoutChange = this.onLayoutChange.bind(this);
this.onResize = this.onResize.bind(this);
this.onResizeStop = this.onResizeStop.bind(this);
this.onDragStop = this.onDragStop.bind(this);
this.onWidthChange = this.onWidthChange.bind(this);
// subscribe to dashboard events
const dashboard = this.props.dashboard;
dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
dashboard.on('panel-removed', this.triggerForceUpdate.bind(this));
dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this));
dashboard.on('view-mode-changed', this.onViewModeChanged.bind(this));
dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this));
dashboard.on('row-expanded', this.triggerForceUpdate.bind(this));
dashboard.on('panel-added', this.triggerForceUpdate);
dashboard.on('panel-removed', this.triggerForceUpdate);
dashboard.on('repeats-processed', this.triggerForceUpdate);
dashboard.on('view-mode-changed', this.onViewModeChanged);
dashboard.on('row-collapsed', this.triggerForceUpdate);
dashboard.on('row-expanded', this.triggerForceUpdate);
}
buildLayout() {
@ -123,7 +131,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps> {
return layout;
}
onLayoutChange(newLayout) {
onLayoutChange = (newLayout: ReactGridLayout.Layout[]) => {
for (const newPos of newLayout) {
this.panelMap[newPos.i].updateGridPos(newPos);
}
@ -131,22 +139,22 @@ export class DashboardGrid extends React.Component<DashboardGridProps> {
this.props.dashboard.sortPanelsByGridPos();
}
triggerForceUpdate() {
triggerForceUpdate = () => {
this.forceUpdate();
}
onWidthChange() {
onWidthChange = () => {
for (const panel of this.props.dashboard.panels) {
panel.resizeDone();
}
}
onViewModeChanged(payload) {
onViewModeChanged = () => {
ignoreNextWidthChange = true;
this.forceUpdate();
}
updateGridPos(item, layout) {
updateGridPos = (item: ReactGridLayout.Layout, layout: ReactGridLayout.Layout[]) => {
this.panelMap[item.i].updateGridPos(item);
// react-grid-layout has a bug (#670), and onLayoutChange() is only called when the component is mounted.
@ -154,16 +162,17 @@ export class DashboardGrid extends React.Component<DashboardGridProps> {
this.onLayoutChange(layout);
}
onResize(layout, oldItem, newItem) {
onResize: ItemCallback = (layout, oldItem, newItem) => {
console.log();
this.panelMap[newItem.i].updateGridPos(newItem);
}
onResizeStop(layout, oldItem, newItem) {
onResizeStop: ItemCallback = (layout, oldItem, newItem) => {
this.updateGridPos(newItem, layout);
this.panelMap[newItem.i].resizeDone();
}
onDragStop(layout, oldItem, newItem) {
onDragStop: ItemCallback = (layout, oldItem, newItem) => {
this.updateGridPos(newItem, layout);
}

View File

@ -7,12 +7,11 @@ import { importPluginModule } from 'app/features/plugins/plugin_loader';
import { AddPanelWidget } from '../components/AddPanelWidget';
import { getPanelPluginNotFound } from './PanelPluginNotFound';
import { DashboardRow } from './DashboardRow';
import { DashboardRow } from '../components/DashboardRow';
import { PanelChrome } from './PanelChrome';
import { PanelEditor } from '../panel_editor/PanelEditor';
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { PanelModel, DashboardModel } from '../state';
import { PanelPlugin } from 'app/types';
import { PanelResizer } from './PanelResizer';

View File

@ -3,23 +3,20 @@ import React, { Component } from 'react';
import { Tooltip } from '@grafana/ui';
import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary';
// Services
import { getDatasourceSrv, DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { DatasourceSrv, getDatasourceSrv } from 'app/features/plugins/datasource_srv';
// Utils
import kbn from 'app/core/utils/kbn';
// Types
import {
TimeRange,
TimeSeries,
LoadingState,
DataQueryResponse,
DataQueryOptions,
DataQueryResponse,
LoadingState,
PanelData,
TableData,
} from '@grafana/ui/src/types';
TimeRange,
TimeSeries,
} from '@grafana/ui';
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
@ -40,6 +37,7 @@ export interface Props {
minInterval?: string;
maxDataPoints?: number;
children: (r: RenderProps) => JSX.Element;
onDataResponse?: (data: DataQueryResponse) => void;
}
export interface State {
@ -93,7 +91,17 @@ export class DataPanel extends Component<Props, State> {
}
private issueQueries = async () => {
const { isVisible, queries, datasource, panelId, dashboardId, timeRange, widthPixels, maxDataPoints } = this.props;
const {
isVisible,
queries,
datasource,
panelId,
dashboardId,
timeRange,
widthPixels,
maxDataPoints,
onDataResponse,
} = this.props;
if (!isVisible) {
return;
@ -136,6 +144,10 @@ export class DataPanel extends Component<Props, State> {
return;
}
if (onDataResponse) {
onDataResponse(resp);
}
this.setState({
loading: LoadingState.Done,
response: resp,

View File

@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
import { AutoSizer } from 'react-virtualized';
// Services
import { getTimeSrv, TimeSrv } from '../time_srv';
import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
// Components
import { PanelHeader } from './PanelHeader/PanelHeader';
@ -14,13 +14,13 @@ import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
// Types
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { DashboardModel, PanelModel } from '../state';
import { PanelPlugin } from 'app/types';
import { TimeRange } from '@grafana/ui';
import variables from 'sass/_variables.scss';
import templateSrv from 'app/features/templating/template_srv';
import { DataQueryResponse } from '@grafana/ui/src';
export interface Props {
panel: PanelModel;
@ -83,16 +83,42 @@ export class PanelChrome extends PureComponent<Props, State> {
return templateSrv.replace(value, this.props.panel.scopedVars, format);
};
onDataResponse = (dataQueryResponse: DataQueryResponse) => {
if (this.props.dashboard.isSnapshot()) {
this.props.panel.snapshotData = dataQueryResponse.data;
}
};
get isVisible() {
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
}
renderPanel(loading, timeSeries, width, height): JSX.Element {
const { panel, plugin } = this.props;
const { timeRange, renderCounter } = this.state;
const PanelComponent = plugin.exports.Panel;
return (
<div className="panel-content">
<PanelComponent
loading={loading}
timeSeries={timeSeries}
timeRange={timeRange}
options={panel.getOptions(plugin.exports.PanelDefaults)}
width={width - 2 * variables.panelHorizontalPadding}
height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding}
renderCounter={renderCounter}
onInterpolate={this.onInterpolate}
/>
</div>
);
}
render() {
const { panel, dashboard, plugin } = this.props;
const { refreshCounter, timeRange, timeInfo, renderCounter } = this.state;
const { panel, dashboard } = this.props;
const { refreshCounter, timeRange, timeInfo } = this.state;
const { datasource, targets, transparent } = panel;
const PanelComponent = plugin.exports.Panel;
const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`;
return (
<AutoSizer>
@ -112,32 +138,23 @@ export class PanelChrome extends PureComponent<Props, State> {
scopedVars={panel.scopedVars}
links={panel.links}
/>
<DataPanel
datasource={datasource}
queries={targets}
timeRange={timeRange}
isVisible={this.isVisible}
widthPixels={width}
refreshCounter={refreshCounter}
>
{({ loading, panelData }) => {
return (
<div className="panel-content">
<PanelComponent
loading={loading}
timeSeries={panelData.timeSeries}
timeRange={timeRange}
options={panel.getOptions(plugin.exports.PanelDefaults)}
width={width - 2 * variables.panelHorizontalPadding}
height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding}
renderCounter={renderCounter}
onInterpolate={this.onInterpolate}
/>
</div>
);
}}
</DataPanel>
{panel.snapshotData ? (
this.renderPanel(false, panel.snapshotData, width, height)
) : (
<DataPanel
datasource={datasource}
queries={targets}
timeRange={timeRange}
isVisible={this.isVisible}
widthPixels={width}
refreshCounter={refreshCounter}
onDataResponse={this.onDataResponse}
>
{({ loading, panelData }) => {
return this.renderPanel(loading, panelData.timeSeries, width, height);
}}
</DataPanel>
)}
</div>
);
}}

View File

@ -1,12 +1,13 @@
import React, { Component } from 'react';
import classNames from 'classnames';
import { isEqual } from 'lodash';
import PanelHeaderCorner from './PanelHeaderCorner';
import { PanelHeaderMenu } from './PanelHeaderMenu';
import templateSrv from 'app/features/templating/template_srv';
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { PanelModel } from 'app/features/dashboard/panel_model';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { ClickOutsideWrapper } from 'app/core/components/ClickOutsideWrapper/ClickOutsideWrapper';
export interface Props {
@ -19,21 +20,45 @@ export interface Props {
links?: [];
}
interface ClickCoordinates {
x: number;
y: number;
}
interface State {
panelMenuOpen: boolean;
}
export class PanelHeader extends Component<Props, State> {
clickCoordinates: ClickCoordinates = {x: 0, y: 0};
state = {
panelMenuOpen: false,
clickCoordinates: {x: 0, y: 0}
};
onMenuToggle = event => {
event.stopPropagation();
eventToClickCoordinates = (event: React.MouseEvent<HTMLDivElement>) => {
return {
x: event.clientX,
y: event.clientY
};
}
this.setState(prevState => ({
panelMenuOpen: !prevState.panelMenuOpen,
}));
onMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
this.clickCoordinates = this.eventToClickCoordinates(event);
};
isClick = (clickCoordinates: ClickCoordinates) => {
return isEqual(clickCoordinates, this.clickCoordinates);
}
onMenuToggle = (event: React.MouseEvent<HTMLDivElement>) => {
if (this.isClick(this.eventToClickCoordinates(event))) {
event.stopPropagation();
this.setState(prevState => ({
panelMenuOpen: !prevState.panelMenuOpen,
}));
}
};
closeMenu = () => {
@ -64,7 +89,7 @@ export class PanelHeader extends Component<Props, State> {
<i className="fa fa-spinner fa-spin" />
</span>
)}
<div className="panel-title-container" onClick={this.onMenuToggle}>
<div className="panel-title-container" onClick={this.onMenuToggle} onMouseDown={this.onMouseDown}>
<div className="panel-title">
<span className="icon-gf panel-alert-icon" />
<span className="panel-title-text">

View File

@ -1,10 +1,10 @@
import React, { Component } from 'react';
import Remarkable from 'remarkable';
import { Tooltip } from '@grafana/ui';
import { PanelModel } from 'app/features/dashboard/panel_model';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import templateSrv from 'app/features/templating/template_srv';
import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/time_srv';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
enum InfoModes {
Error = 'Error',

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { PanelModel } from 'app/features/dashboard/panel_model';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { PanelHeaderMenuItem } from './PanelHeaderMenuItem';
import { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu';
import { PanelMenuItem } from '@grafana/ui';

View File

@ -1,8 +1,8 @@
import React, { PureComponent } from 'react';
import { throttle } from 'lodash';
import Draggable from 'react-draggable';
import Draggable, { DraggableEventHandler } from 'react-draggable';
import { PanelModel } from '../panel_model';
import { PanelModel } from '../state/PanelModel';
interface Props {
isEditing: boolean;
@ -42,7 +42,7 @@ export class PanelResizer extends PureComponent<Props, State> {
return 100;
}
changeHeight = height => {
changeHeight = (height: number) => {
const sh = this.smallestHeight;
const lh = this.largestHeight;
height = height < sh ? sh : height;
@ -54,7 +54,7 @@ export class PanelResizer extends PureComponent<Props, State> {
});
};
onDrag = (evt, data) => {
onDrag: DraggableEventHandler = (evt, data) => {
const newHeight = this.state.editorHeight + data.y;
this.throttledChangeHeight(newHeight);
this.throttledResizeDone();

View File

@ -1,5 +1,4 @@
import './dashboard_ctrl';
import './time_srv';
import './containers/DashboardCtrl';
import './dashgrid/DashboardGridDirective';
// Services

View File

@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
import { EditorTabBody } from './EditorTabBody';
import { PanelModel } from '../panel_model';
import { PanelModel } from '../state/PanelModel';
import './../../panel/GeneralTabCtrl';
interface Props {

View File

@ -11,8 +11,8 @@ import { store } from 'app/store/store';
import { updateLocation } from 'app/core/actions';
import { AngularComponent } from 'app/core/services/AngularLoader';
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { PanelModel } from '../state/PanelModel';
import { DashboardModel } from '../state/DashboardModel';
import { PanelPlugin } from 'app/types/plugins';
import { Tooltip } from '@grafana/ui';

View File

@ -16,8 +16,8 @@ import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv';
import config from 'app/core/config';
// Types
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { PanelModel } from '../state/PanelModel';
import { DashboardModel } from '../state/DashboardModel';
import { DataQuery, DataSourceSelectItem } from '@grafana/ui/src/types';
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';

View File

@ -9,7 +9,7 @@ import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoa
import { Emitter } from 'app/core/utils/emitter';
// Types
import { PanelModel } from '../panel_model';
import { PanelModel } from '../state/PanelModel';
import { DataQuery, DataSourceApi } from '@grafana/ui';
interface Props {

View File

@ -13,7 +13,7 @@ import DataSourceOption from './DataSourceOption';
import { FormLabel } from '@grafana/ui';
// Types
import { PanelModel } from '../panel_model';
import { PanelModel } from '../state/PanelModel';
import { DataSourceSelectItem } from '@grafana/ui/src/types';
import { ValidationEvents } from 'app/types';

View File

@ -11,8 +11,8 @@ import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
import { FadeIn } from 'app/core/components/Animations/FadeIn';
// Types
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { PanelModel } from '../state/PanelModel';
import { DashboardModel } from '../state/DashboardModel';
import { PanelPlugin } from 'app/types/plugins';
interface Props {

View File

@ -1,7 +1,7 @@
import { ChangeTracker } from './ChangeTracker';
import { contextSrv } from 'app/core/services/context_srv';
import { DashboardModel } from '../dashboard_model';
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../state/DashboardModel';
import { PanelModel } from '../state/PanelModel';
jest.mock('app/core/services/context_srv', () => ({
contextSrv: {

View File

@ -1,6 +1,6 @@
import angular from 'angular';
import _ from 'lodash';
import { DashboardModel } from '../dashboard_model';
import { DashboardModel } from '../state/DashboardModel';
export class ChangeTracker {
current: any;

View File

@ -1,5 +1,5 @@
import coreModule from 'app/core/core_module';
import { DashboardModel } from '../dashboard_model';
import { DashboardModel } from '../state/DashboardModel';
import locationUtil from 'app/core/utils/location_util';
export class DashboardSrv {

View File

@ -1,6 +1,6 @@
import config from 'app/core/config';
import { DashboardViewStateSrv } from './DashboardViewStateSrv';
import { DashboardModel } from '../dashboard_model';
import { DashboardModel } from '../state/DashboardModel';
describe('when updating view state', () => {
const location = {

View File

@ -2,7 +2,7 @@ import angular from 'angular';
import _ from 'lodash';
import config from 'app/core/config';
import appEvents from 'app/core/app_events';
import { DashboardModel } from '../dashboard_model';
import { DashboardModel } from '../state/DashboardModel';
// represents the transient view state
// like fullscreen panel & edit

View File

@ -1,5 +1,4 @@
import { TimeSrv } from '../time_srv';
import '../time_srv';
import { TimeSrv } from './TimeSrv';
import moment from 'moment';
describe('timeSrv', () => {

View File

@ -1,6 +1,6 @@
import _ from 'lodash';
import { DashboardModel } from '../dashboard_model';
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../state/DashboardModel';
import { PanelModel } from '../state/PanelModel';
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
import { expect } from 'test/lib/common';

View File

@ -7,8 +7,8 @@ import {
MIN_PANEL_HEIGHT,
DEFAULT_PANEL_SPAN,
} from 'app/core/constants';
import { PanelModel } from './panel_model';
import { DashboardModel } from './dashboard_model';
import { PanelModel } from './PanelModel';
import { DashboardModel } from './DashboardModel';
import getFactors from 'app/core/utils/factors';
export class DashboardMigrator {

View File

@ -1,5 +1,5 @@
import _ from 'lodash';
import { DashboardModel } from '../dashboard_model';
import { DashboardModel } from '../state/DashboardModel';
import { expect } from 'test/lib/common';
jest.mock('app/core/services/context_srv', () => ({}));

View File

@ -1,6 +1,6 @@
import _ from 'lodash';
import { DashboardModel } from '../dashboard_model';
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../state/DashboardModel';
import { PanelModel } from '../state/PanelModel';
jest.mock('app/core/services/context_srv', () => ({}));

View File

@ -7,8 +7,8 @@ import { Emitter } from 'app/core/utils/emitter';
import { contextSrv } from 'app/core/services/context_srv';
import sortByKeys from 'app/core/utils/sort_by_keys';
import { PanelModel } from './panel_model';
import { DashboardMigrator } from './dashboard_migration';
import { PanelModel } from './PanelModel';
import { DashboardMigrator } from './DashboardMigrator';
import { TimeRange } from '@grafana/ui/src';
export class DashboardModel {
@ -811,6 +811,10 @@ export class DashboardModel {
return this.getTimezone() === 'utc';
}
isSnapshot() {
return this.snapshot !== undefined;
}
getTimezone() {
return this.timezone ? this.timezone : contextSrv.user.timezone;
}

View File

@ -1,5 +1,5 @@
import _ from 'lodash';
import { PanelModel } from '../panel_model';
import { PanelModel } from '../state/PanelModel';
describe('PanelModel', () => {
describe('when creating new panel model', () => {

View File

@ -4,7 +4,8 @@ import _ from 'lodash';
// Types
import { Emitter } from 'app/core/utils/emitter';
import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants';
import { DataQuery } from '@grafana/ui/src/types';
import { DataQuery, TimeSeries } from '@grafana/ui';
import { TableData } from '@grafana/ui/src';
export interface GridPos {
x: number;
@ -87,7 +88,7 @@ export class PanelModel {
datasource: string;
thresholds?: any;
snapshotData?: any;
snapshotData?: TimeSeries[] | [TableData];
timeFrom?: any;
timeShift?: any;
hideTimeOverride?: any;

View File

@ -0,0 +1,2 @@
export { DashboardModel } from './DashboardModel';
export { PanelModel } from './PanelModel';

View File

@ -2,8 +2,8 @@ import { updateLocation } from 'app/core/actions';
import { store } from 'app/store/store';
import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel';
import { PanelModel } from 'app/features/dashboard/panel_model';
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelMenuItem } from '@grafana/ui';
export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {

View File

@ -2,8 +2,8 @@
import store from 'app/core/store';
// Models
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { PanelModel } from 'app/features/dashboard/panel_model';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { TimeRange } from '@grafana/ui';
// Utils

View File

@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
// Services
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
import { getTimeSrv } from 'app/features/dashboard/time_srv';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
// Types
import { Emitter } from 'app/core/utils/emitter';

View File

@ -43,6 +43,7 @@ export enum ActionTypes {
ToggleTable = 'explore/TOGGLE_TABLE',
UpdateDatasourceInstance = 'explore/UPDATE_DATASOURCE_INSTANCE',
ResetExplore = 'explore/RESET_EXPLORE',
QueriesImported = 'explore/QueriesImported',
}
export interface AddQueryRowAction {
@ -142,7 +143,6 @@ export interface LoadDatasourceSuccessAction {
StartPage?: any;
datasourceInstance: any;
history: HistoryItem[];
initialQueries: DataQuery[];
logsHighlighterExpressions?: any[];
showingStartPage: boolean;
supportsGraph: boolean;
@ -283,6 +283,14 @@ export interface ResetExploreAction {
payload: {};
}
export interface QueriesImported {
type: ActionTypes.QueriesImported;
payload: {
exploreId: ExploreId;
queries: DataQuery[];
};
}
export type Action =
| AddQueryRowAction
| ChangeQueryAction
@ -312,4 +320,5 @@ export type Action =
| ToggleLogsAction
| ToggleTableAction
| UpdateDatasourceInstanceAction
| ResetExploreAction;
| ResetExploreAction
| QueriesImported;

View File

@ -53,6 +53,7 @@ import {
QueryTransactionStartAction,
ScanStopAction,
UpdateDatasourceInstanceAction,
QueriesImported,
} from './actionTypes';
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, ThunkableAction>;
@ -69,10 +70,15 @@ export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAct
* Loads a new datasource identified by the given name.
*/
export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
return async dispatch => {
const instance = await getDatasourceSrv().get(datasource);
dispatch(updateDatasourceInstance(exploreId, instance));
dispatch(loadDatasource(exploreId, instance));
return async (dispatch, getState) => {
const newDataSourceInstance = await getDatasourceSrv().get(datasource);
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
const modifiedQueries = getState().explore[exploreId].modifiedQueries;
await dispatch(importQueries(exploreId, modifiedQueries, currentDataSourceInstance, newDataSourceInstance));
dispatch(updateDatasourceInstance(exploreId, newDataSourceInstance));
dispatch(loadDatasource(exploreId, newDataSourceInstance));
};
}
@ -174,6 +180,7 @@ export function initializeExplore(
if (exploreDatasources.length >= 1) {
let instance;
if (datasourceName) {
try {
instance = await getDatasourceSrv().get(datasourceName);
@ -185,6 +192,7 @@ export function initializeExplore(
if (!instance) {
instance = await getDatasourceSrv().get();
}
dispatch(updateDatasourceInstance(exploreId, instance));
dispatch(loadDatasource(exploreId, instance));
} else {
@ -224,7 +232,10 @@ export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissi
/**
* Start the async process of loading a datasource to display a loading indicator
*/
export const loadDatasourcePending = (exploreId: ExploreId, requestedDatasourceName: string): LoadDatasourcePendingAction => ({
export const loadDatasourcePending = (
exploreId: ExploreId,
requestedDatasourceName: string
): LoadDatasourcePendingAction => ({
type: ActionTypes.LoadDatasourcePending,
payload: {
exploreId,
@ -232,6 +243,16 @@ export const loadDatasourcePending = (exploreId: ExploreId, requestedDatasourceN
},
});
export const queriesImported = (exploreId: ExploreId, queries: DataQuery[]): QueriesImported => {
return {
type: ActionTypes.QueriesImported,
payload: {
exploreId,
queries,
},
};
};
/**
* Datasource loading was successfully completed. The instance is stored in the state as well in case we need to
* run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
@ -240,7 +261,6 @@ export const loadDatasourcePending = (exploreId: ExploreId, requestedDatasourceN
export const loadDatasourceSuccess = (
exploreId: ExploreId,
instance: any,
queries: DataQuery[]
): LoadDatasourceSuccessAction => {
// Capabilities
const supportsGraph = instance.meta.metrics;
@ -261,7 +281,6 @@ export const loadDatasourceSuccess = (
StartPage,
datasourceInstance: instance,
history,
initialQueries: queries,
showingStartPage: Boolean(StartPage),
supportsGraph,
supportsLogs,
@ -286,6 +305,35 @@ export function updateDatasourceInstance(
};
}
export function importQueries(
exploreId: ExploreId,
queries: DataQuery[],
sourceDataSource: DataSourceApi,
targetDataSource: DataSourceApi
) {
return async dispatch => {
let importedQueries = queries;
// Check if queries can be imported from previously selected datasource
if (sourceDataSource.meta.id === targetDataSource.meta.id) {
// Keep same queries if same type of datasource
importedQueries = [...queries];
} else if (targetDataSource.importQueries) {
// Datasource-specific importers
importedQueries = await targetDataSource.importQueries(queries, sourceDataSource.meta);
} else {
// Default is blank queries
importedQueries = ensureQueries();
}
const nextQueries = importedQueries.map((q, i) => ({
...importedQueries[i],
...generateEmptyQuery(i),
}));
dispatch(queriesImported(exploreId, nextQueries));
};
}
/**
* Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback.
*/
@ -318,35 +366,12 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
instance.init();
}
// Check if queries can be imported from previously selected datasource
const queries = getState().explore[exploreId].modifiedQueries;
let importedQueries = queries;
const origin = getState().explore[exploreId].datasourceInstance;
if (origin) {
if (origin.meta.id === instance.meta.id) {
// Keep same queries if same type of datasource
importedQueries = [...queries];
} else if (instance.importQueries) {
// Datasource-specific importers
importedQueries = await instance.importQueries(queries, origin.meta);
} else {
// Default is blank queries
importedQueries = ensureQueries();
}
}
if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
// User already changed datasource again, discard results
return;
}
// Reset edit state with new queries
const nextQueries = importedQueries.map((q, i) => ({
...importedQueries[i],
...generateEmptyQuery(i),
}));
dispatch(loadDatasourceSuccess(exploreId, instance, nextQueries));
dispatch(loadDatasourceSuccess(exploreId, instance));
dispatch(runQueries(exploreId));
};
}

View File

@ -203,7 +203,6 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
StartPage,
datasourceInstance,
history,
initialQueries,
showingStartPage,
supportsGraph,
supportsLogs,
@ -217,7 +216,6 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
StartPage,
datasourceInstance,
history,
initialQueries,
showingStartPage,
supportsGraph,
supportsLogs,
@ -226,7 +224,6 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
datasourceMissing: false,
datasourceError: null,
logsHighlighterExpressions: undefined,
modifiedQueries: initialQueries.slice(),
queryTransactions: [],
};
}
@ -295,7 +292,6 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
// Append new transaction
const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
return {
...state,
queryTransactions: nextQueryTransactions,
@ -417,6 +413,14 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable };
}
case ActionTypes.QueriesImported: {
return {
...state,
initialQueries: action.payload.queries,
modifiedQueries: action.payload.queries.slice(),
};
}
}
return state;

View File

@ -11,7 +11,7 @@ jest.mock('app/core/config', () => {
});
import q from 'q';
import { PanelModel } from 'app/features/dashboard/panel_model';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { MetricsPanelCtrl } from '../metrics_panel_ctrl';
describe('MetricsPanelCtrl', () => {

View File

@ -1,6 +1,6 @@
import '../all';
import { VariableSrv } from '../variable_srv';
import { DashboardModel } from '../../dashboard/dashboard_model';
import { DashboardModel } from '../../dashboard/state/DashboardModel';
import moment from 'moment';
import $q from 'q';

View File

@ -2,7 +2,7 @@ import '../all';
import _ from 'lodash';
import { VariableSrv } from '../variable_srv';
import { DashboardModel } from '../../dashboard/dashboard_model';
import { DashboardModel } from '../../dashboard/state/DashboardModel';
import $q from 'q';
describe('VariableSrv init', function(this: any) {

View File

@ -7,8 +7,8 @@ import coreModule from 'app/core/core_module';
import { variableTypes } from './variable';
import { Graph } from 'app/core/utils/dag';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { TimeSrv } from 'app/features/dashboard/time_srv';
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
// Types
import { TimeRange } from '@grafana/ui/src';

View File

@ -11,7 +11,6 @@ import './jquery.flot.events';
import $ from 'jquery';
import _ from 'lodash';
import moment from 'moment';
import kbn from 'app/core/utils/kbn';
import { tickStep } from 'app/core/utils/ticks';
import { appEvents, coreModule, updateLegendValues } from 'app/core/core';
import GraphTooltip from './graph_tooltip';
@ -26,7 +25,7 @@ import ReactDOM from 'react-dom';
import { Legend, GraphLegendProps } from './Legend/Legend';
import { GraphCtrl } from './module';
import { GrafanaTheme } from '@grafana/ui';
import { GrafanaTheme, getValueFormat } from '@grafana/ui';
class GraphElement {
ctrl: GraphCtrl;
@ -730,10 +729,12 @@ class GraphElement {
configureAxisMode(axis, format) {
axis.tickFormatter = (val, axis) => {
if (!kbn.valueFormats[format]) {
const formatter = getValueFormat(format);
if (!formatter) {
throw new Error(`Unit '${format}' is not supported`);
}
return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
return formatter(val, axis.tickDecimals, axis.scaledDecimals);
};
}

View File

@ -43,6 +43,25 @@ describe('TimeRegionManager', () => {
});
}
describe('When colors missing in config', () => {
plotOptionsScenario('should not throw an error when fillColor is undefined', ctx => {
const regions = [
{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, lineColor: '#ffffff', colorMode: 'custom' },
];
const from = moment('2018-01-01T00:00:00+01:00');
const to = moment('2018-01-01T23:59:00+01:00');
expect(() => ctx.setup(regions, from, to)).not.toThrow();
});
plotOptionsScenario('should not throw an error when lineColor is undefined', ctx => {
const regions = [
{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, fillColor: '#ffffff', line: true, colorMode: 'custom' },
];
const from = moment('2018-01-01T00:00:00+01:00');
const to = moment('2018-01-01T23:59:00+01:00');
expect(() => ctx.setup(regions, from, to)).not.toThrow();
});
});
describe('When creating plot markings using local time', () => {
plotOptionsScenario('for day of week region', ctx => {
const regions = [{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, colorMode: 'red' }];

View File

@ -50,8 +50,8 @@ function getColor(timeRegion, theme: GrafanaTheme): TimeRegionColorDefinition {
if (timeRegion.colorMode === 'custom') {
return {
fill: getColorFromHexRgbOrName(timeRegion.fillColor, theme),
line: getColorFromHexRgbOrName(timeRegion.lineColor, theme),
fill: timeRegion.fill && timeRegion.fillColor ? getColorFromHexRgbOrName(timeRegion.fillColor, theme) : null,
line: timeRegion.line && timeRegion.lineColor ? getColorFromHexRgbOrName(timeRegion.lineColor, theme) : null,
};
}
@ -62,8 +62,8 @@ function getColor(timeRegion, theme: GrafanaTheme): TimeRegionColorDefinition {
}
return {
fill: getColorFromHexRgbOrName(colorMode.color.fill, theme),
line: getColorFromHexRgbOrName(colorMode.color.line, theme),
fill: timeRegion.fill ? getColorFromHexRgbOrName(colorMode.color.fill, theme) : null,
line: timeRegion.fill ? getColorFromHexRgbOrName(colorMode.color.line, theme) : null,
};
}

View File

@ -1,8 +1,8 @@
import * as d3 from 'd3';
import $ from 'jquery';
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import { getValueBucketBound } from './heatmap_data_converter';
import { getValueFormat } from '@grafana/ui';
const TOOLTIP_PADDING_X = 30;
const TOOLTIP_PADDING_Y = 5;
@ -268,7 +268,7 @@ export class HeatmapTooltip {
countValueFormatter(decimals, scaledDecimals = null) {
const format = 'short';
return value => {
return kbn.valueFormats[format](value, decimals, scaledDecimals);
return getValueFormat(format)(value, decimals, scaledDecimals);
};
}
}

View File

@ -2,13 +2,12 @@ import _ from 'lodash';
import $ from 'jquery';
import moment from 'moment';
import * as d3 from 'd3';
import kbn from 'app/core/utils/kbn';
import { appEvents, contextSrv } from 'app/core/core';
import * as ticksUtils from 'app/core/utils/ticks';
import { HeatmapTooltip } from './heatmap_tooltip';
import { mergeZeroBuckets } from './heatmap_data_converter';
import { getColorScale, getOpacityScale } from './color_scale';
import { GrafanaTheme, getColorFromHexRgbOrName } from '@grafana/ui';
import { GrafanaTheme, getColorFromHexRgbOrName, getValueFormat } from '@grafana/ui';
const MIN_CARD_SIZE = 1,
CARD_PADDING = 1,
@ -436,7 +435,7 @@ export class HeatmapRenderer {
const format = this.panel.yAxis.format;
return value => {
try {
return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value;
return format !== 'none' ? getValueFormat(format)(value, decimals, scaledDecimals) : value;
} catch (err) {
console.error(err.message || err);
return value;

View File

@ -8,8 +8,7 @@ import kbn from 'app/core/utils/kbn';
import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2';
import { MetricsPanelCtrl } from 'app/plugins/sdk';
import { getColorFromHexRgbOrName } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/ui';
import { GrafanaTheme, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui';
class SingleStatCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
@ -192,7 +191,8 @@ class SingleStatCtrl extends MetricsPanelCtrl {
data.valueRounded = 0;
} else {
const decimalInfo = this.getDecimalsForValue(data.value);
const formatFunc = kbn.valueFormats[this.panel.format];
const formatFunc = getValueFormat(this.panel.format);
data.valueFormatted = formatFunc(
datapoint[this.panel.tableColumn],
decimalInfo.decimals,
@ -301,6 +301,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
if (this.series && this.series.length > 0) {
const lastPoint = _.last(this.series[0].datapoints);
const lastValue = _.isArray(lastPoint) ? lastPoint[0] : null;
const formatFunc = getValueFormat(this.panel.format);
if (this.panel.valueName === 'name') {
data.value = 0;
@ -311,7 +312,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
data.valueFormatted = _.escape(lastValue);
data.valueRounded = 0;
} else if (this.panel.valueName === 'last_time') {
const formatFunc = kbn.valueFormats[this.panel.format];
data.value = lastPoint[1];
data.valueRounded = data.value;
data.valueFormatted = formatFunc(data.value, 0, 0, this.dashboard.isTimezoneUtc());
@ -320,7 +320,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
data.flotpairs = this.series[0].flotpairs;
const decimalInfo = this.getDecimalsForValue(data.value);
const formatFunc = kbn.valueFormats[this.panel.format];
data.valueFormatted = formatFunc(
data.value,

View File

@ -1,8 +1,7 @@
import _ from 'lodash';
import moment from 'moment';
import kbn from 'app/core/utils/kbn';
import { getColorFromHexRgbOrName } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/ui';
import { GrafanaTheme, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui';
export class TableRenderer {
formatters: any[];
@ -170,7 +169,7 @@ export class TableRenderer {
}
if (column.style.type === 'number') {
const valueFormatter = kbn.valueFormats[column.unit || column.style.unit];
const valueFormatter = getValueFormat(column.unit || column.style.unit);
return v => {
if (v === null || v === void 0) {

View File

@ -8,7 +8,7 @@ import coreModule from 'app/core/core_module';
import { profiler } from 'app/core/profiler';
import appEvents from 'app/core/app_events';
import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv';
import { TimeSrv, setTimeSrv } from 'app/features/dashboard/time_srv';
import { TimeSrv, setTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { DatasourceSrv, setDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { AngularLoader, setAngularLoader } from 'app/core/services/AngularLoader';
import { configureStore } from 'app/store/configureStore';

View File

@ -4,8 +4,6 @@ import { Provider } from 'react-redux';
import coreModule from 'app/core/core_module';
import { store } from 'app/store/store';
import { BackendSrv } from 'app/core/services/backend_srv';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { ContextSrv } from 'app/core/services/context_srv';
function WrapInProvider(store, Component, props) {
@ -20,8 +18,6 @@ function WrapInProvider(store, Component, props) {
export function reactContainer(
$route,
$location,
backendSrv: BackendSrv,
datasourceSrv: DatasourceSrv,
contextSrv: ContextSrv
) {
return {
@ -42,11 +38,7 @@ export function reactContainer(
component = component.default;
}
const props = {
backendSrv: backendSrv,
datasourceSrv: datasourceSrv,
routeParams: $route.current.params,
};
const props = { };
ReactDOM.render(WrapInProvider(store, component, props), elem[0]);

View File

@ -50,3 +50,4 @@
display: none;
}
}

View File

@ -2,7 +2,7 @@ import _ from 'lodash';
import config from 'app/core/config';
import * as dateMath from 'app/core/utils/datemath';
import { angularMocks, sinon } from '../lib/common';
import { PanelModel } from 'app/features/dashboard/panel_model';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
export function ControllerTestContext(this: any) {
const self = this;

View File

@ -6,8 +6,8 @@ EXTRA_OPTS="$@"
# Right now we hack this in into the publish script.
# Eventually we might want to keep a list of all previous releases somewhere.
_releaseNoteUrl="https://community.grafana.com/t/release-notes-v5-4-x/12215"
_whatsNewUrl="http://docs.grafana.org/guides/whats-new-in-v5-4/"
_releaseNoteUrl="https://community.grafana.com/t/release-notes-v6-0-x/14010"
_whatsNewUrl="http://docs.grafana.org/guides/whats-new-in-v6-0/"
./scripts/build/release_publisher/release_publisher \
--wn ${_whatsNewUrl} \

View File

@ -1773,6 +1773,13 @@
dependencies:
"@types/react" "*"
"@types/react-grid-layout@^0.16.6":
version "0.16.6"
resolved "https://registry.yarnpkg.com/@types/react-grid-layout/-/react-grid-layout-0.16.6.tgz#9149efe128e05d59c54063c7781d18b8febe112c"
integrity sha512-Jp0VfCHJE4uxekPBPpRkADKOjoSHssF2ba1ZMMAfCEqkoSkE+K+3bhI39++fbd7MqGySaqADVHeOoxlBnA3p5g==
dependencies:
"@types/react" "*"
"@types/react-select@^2.0.4":
version "2.0.11"
resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-2.0.11.tgz#9b2b1fdb12b67a5a617c5f572e15617636cc65af"
@ -1796,6 +1803,14 @@
dependencies:
"@types/react" "*"
"@types/react-virtualized@^9.18.12":
version "9.18.12"
resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.18.12.tgz#541e65c5e0b4629d6a1c6f339171c7943e016ecb"
integrity sha512-Msdpt9zvYlb5Ul4PA339QUkJ0/z2O+gaFxed1rG+2rZjbe6XdYo7jWfJe206KBnjj84DwPPIbPFQCtoGuNwNTQ==
dependencies:
"@types/prop-types" "*"
"@types/react" "*"
"@types/react@*", "@types/react@16.7.6", "@types/react@^16.7.6":
version "16.7.6"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.7.6.tgz#80e4bab0d0731ad3ae51f320c4b08bdca5f03040"