mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into bar-gauge-poc
This commit is contained in:
commit
711e4e926b
@ -38,7 +38,7 @@ provider (listed above). There is also options for allowing self sign up.
|
||||
|
||||
### Login and short-lived tokens
|
||||
|
||||
> The followung applies when using Grafana's built in user authentication, LDAP (without Auth proxy) or OAuth integration.
|
||||
> The following applies when using Grafana's built in user authentication, LDAP (without Auth proxy) or OAuth integration.
|
||||
|
||||
Grafana are using short-lived tokens as a mechanism for verifying authenticated users.
|
||||
These short-lived tokens are rotated each `token_rotation_interval_minutes` for an active authenticated user.
|
||||
|
@ -27,6 +27,7 @@ The main highlights are:
|
||||
- [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.
|
||||
- [Removal of user session storage]({{< relref "#easier-to-deploy-improved-security" >}}) makes Grafana easier to deploy & improves security.
|
||||
|
||||
## Explore
|
||||
|
||||
@ -113,30 +114,42 @@ 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
|
||||
## 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
|
||||
## 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
|
||||
## 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
|
||||
## Easier to deploy & improved security
|
||||
|
||||
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.
|
||||
Grafana 6.0 removes the need of configuring and setup of additional storage for [user sessions](/tutorials/ha_setup/#user-sessions). This should make it easier to deploy and operate Grafana in a
|
||||
high availability setup and/or if you're using a stateless user session storage like Redis, Memcache, Postgres or MySQL.
|
||||
|
||||
This release will force all users to log in again since their previous token is not valid anymore.
|
||||
Instead of user sessions a solution based on short-lived tokens that are rotated frequently have been implemented. This also replaces the old "remember me cookie"
|
||||
solution, which allowed a user to be logged in between browser sessions, and which have been subject to several security holes throughout the years.
|
||||
Read more about the short-lived token solution and how to configure it [here](/auth/overview/#login-and-short-lived-tokens).
|
||||
|
||||
### Named Colors
|
||||
> Please note that due to these changes, all users will be required to login upon next visit after upgrade.
|
||||
|
||||
Besides these changes we have also made security improvements regarding Cross-Site Request Forgery (CSRF) and Cross-site Scripting (XSS) vulnerabilities:
|
||||
|
||||
* Cookies are per default using the [SameSite](/installation/configuration/#cookie-samesite) attribute to protect against CSRF attacks
|
||||
* Script tags in text panels are per default [disabled](/installation/configuration/#disable-sanitize-html) to protect against XSS attacks
|
||||
|
||||
> If you're using [Auth Proxy Authentication](/auth/auth-proxy/) you still need to have user sessions setup and configured
|
||||
but our goal is to remove this requirements in a near future.
|
||||
|
||||
## Named Colors
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v60/named_colors.png" max-width="400px" class="docs-image--right" caption="Named Colors" >}}
|
||||
|
||||
@ -148,12 +161,16 @@ Named colors also enables Grafana to adapt colors to the current theme.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
### Other features
|
||||
## 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`
|
||||
|
||||
## Upgrading
|
||||
|
||||
See [upgrade notes](/installation/upgrading/#upgrading-to-v6-0).
|
||||
|
||||
## 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.
|
||||
|
@ -160,6 +160,13 @@ The path to the directory where the front end files (HTML, JS, and CSS
|
||||
files). Default to `public` which is why the Grafana binary needs to be
|
||||
executed with working directory set to the installation path.
|
||||
|
||||
### enable_gzip
|
||||
|
||||
Set this option to `true` to enable HTTP compression, this can improve
|
||||
transfer speed and bandwidth utilization. It is recommended that most
|
||||
users set it to `true`. By default it is set to `false` for compatibility
|
||||
reasons.
|
||||
|
||||
### cert_file
|
||||
|
||||
Path to the certificate file (if `protocol` is set to `https`).
|
||||
@ -594,7 +601,7 @@ Default setting for new alert rules. Defaults to categorize error and timeouts a
|
||||
|
||||
Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok)
|
||||
|
||||
# concurrent_render_limit
|
||||
### concurrent_render_limit
|
||||
|
||||
> Available in 5.3 and above
|
||||
|
||||
|
@ -117,3 +117,34 @@ One of the database migrations included in this release will update all annotati
|
||||
We've got one report where using systemd, PostgreSQL and a large amount of annotations (table size 1645mb) took 8-20 minutes for the database migration to complete. However, the grafana-server process was killed after 90 seconds by systemd. Any database migration queries in progress when systemd kills the grafana-server process continues to execute in database until finished.
|
||||
|
||||
If you're using systemd and have a large amount of annotations consider temporary adjusting the systemd `TimeoutStartSec` setting to something high like `30m` before upgrading.
|
||||
|
||||
## Upgrading to v6.0
|
||||
|
||||
If you have text panels with script tags they will no longer work due to a new setting that per default disallow unsanitzied HTML.
|
||||
Read more [here](/installation/configuration/#disable-sanitize-html) about this new setting.
|
||||
|
||||
### Authentication and security
|
||||
|
||||
If your using Grafana's builtin, LDAP (without Auth Proxy) or OAuth authentication all users will be required to login upon the next visit after the upgrade.
|
||||
|
||||
If you have `cookie_secure` set to `true` in the `session` section you probably want to change the `cookie_secure` to `true` in the `security` section as well. Ending up with a configuration like this:
|
||||
|
||||
```ini
|
||||
[session]
|
||||
cookie_secure = true
|
||||
|
||||
[security]
|
||||
cookie_secure = true
|
||||
```
|
||||
|
||||
The `login_remember_days`, `cookie_username` and `cookie_remember_name` settings in the `security` section are no longer being used so they're safe to remove.
|
||||
|
||||
If you have `login_remember_days` configured to 0 (zero) you should change your configuration to this to accomplish similar behavior, i.e. a logged in user will maximum be logged in for 1 day until being forced to login again:
|
||||
|
||||
```ini
|
||||
[auth]
|
||||
login_maximum_inactive_lifetime_days = 1
|
||||
login_maximum_lifetime_days = 1
|
||||
```
|
||||
|
||||
The default cookie name for storing the auth token is `grafana_session`. you can configure this with `login_cookie_name` in `[auth]` settings.
|
@ -38,22 +38,81 @@ documentation article for details on value escaping during interpolation.
|
||||
|
||||
### Advanced Formatting Options
|
||||
|
||||
> Only available in Grafana v5.1+.
|
||||
|
||||
The formatting of the variable interpolation depends on the data source but there are some situations where you might want to change the default formatting. For example, the default for the MySql datasource is to join multiple values as comma-separated with quotes: `'server01','server02'`. In some cases you might want to have a comma-separated string without quotes: `server01,server02`. This is now possible with the advanced formatting options.
|
||||
|
||||
Syntax: `${var_name:option}`
|
||||
|
||||
Filter Option | Example | Raw | Interpolated | Description
|
||||
------------ | ------------- | ------------- | ------------- | -------------
|
||||
`glob` | ${servers:glob} | `'test1', 'test2'` | `{test1,test2}` | (Default) Formats multi-value variable into a glob (for Graphite queries)
|
||||
`regex` | ${servers:regex} | `'test.', 'test2'` | <code>(test\.|test2)</code> | Formats multi-value variable into a regex string
|
||||
`pipe` | ${servers:pipe} | `'test.', 'test2'` | <code>test.|test2</code> | Formats multi-value variable into a pipe-separated string
|
||||
`csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string
|
||||
`json`| ${servers:json} | `'test1', 'test2'` | `["test1","test2"]` | Formats multi-value variable as a JSON string
|
||||
`distributed`| ${servers:distributed} | `'test1', 'test2'` | `test1,servers=test2` | Formats multi-value variable in custom format for OpenTSDB.
|
||||
`lucene`| ${servers:lucene} | `'test', 'test2'` | `("test" OR "test2")` | Formats multi-value variable as a lucene expression.
|
||||
`percentencode` | ${servers:percentencode} | `'foo()bar BAZ', 'test2'` | `{foo%28%29bar%20BAZ%2Ctest2}` | Formats multi-value variable into a glob, percent-encoded.
|
||||
#### Glob
|
||||
Formats multi-value variable into a glob (for Graphite queries).
|
||||
|
||||
```bash
|
||||
servers = ['test1', 'test2']
|
||||
String to interpolate: '${servers:glob}'
|
||||
Interpolation result: '{test1,test2}'
|
||||
```
|
||||
|
||||
### Regex
|
||||
Formats multi-value variable into a regex string.
|
||||
|
||||
```bash
|
||||
servers = ['test1.', 'test2']
|
||||
String to interpolate: '${servers:regex}'
|
||||
Interpolation result: '(test\.|test2)'
|
||||
```
|
||||
|
||||
### Pipe
|
||||
Formats multi-value variable into a pipe-separated string.
|
||||
|
||||
```bash
|
||||
servers = ['test1.', 'test2']
|
||||
String to interpolate: '${servers:pipe}'
|
||||
Interpolation result: 'test.|test2'
|
||||
```
|
||||
|
||||
### Csv
|
||||
Formats multi-value variable as a comma-separated string.
|
||||
|
||||
```bash
|
||||
servers = ['test1', 'test2']
|
||||
String to interpolate: '${servers:csv}'
|
||||
Interpolation result: 'test,test2'
|
||||
```
|
||||
|
||||
### Json
|
||||
Formats multi-value variable as a comma-separated string.
|
||||
|
||||
```bash
|
||||
servers = ['test1', 'test2']
|
||||
String to interpolate: '${servers:json}'
|
||||
Interpolation result: '["test1", "test2"]'
|
||||
```
|
||||
|
||||
### Distributed
|
||||
Formats multi-value variable in custom format for OpenTSDB.
|
||||
|
||||
```bash
|
||||
servers = ['test1', 'test2']
|
||||
String to interpolate: '${servers:distributed}'
|
||||
Interpolation result: 'test1,servers=test2'
|
||||
```
|
||||
|
||||
### Lucene
|
||||
Formats multi-value variable in lucene format for Elasticsearch.
|
||||
|
||||
```bash
|
||||
servers = ['test1', 'test2']
|
||||
String to interpolate: '${servers:lucene}'
|
||||
Interpolation result: '("test1" OR "test2")'
|
||||
```
|
||||
|
||||
### Percentencode
|
||||
Formats single & multi valued variables for use in URL parameters.
|
||||
|
||||
```bash
|
||||
servers = ['foo()bar BAZ', 'test2']
|
||||
String to interpolate: '${servers:lucene}'
|
||||
Interpolation result: 'foo%28%29bar%20BAZ%2Ctest2'
|
||||
```
|
||||
|
||||
Test the formatting options on the [Grafana Play site](http://play.grafana.org/d/cJtIfcWiz/template-variable-formatting-options?orgId=1).
|
||||
|
||||
|
@ -15,7 +15,7 @@ Setting up Grafana for high availability is fairly simple. It comes down to two
|
||||
2. Decide how to store session data.
|
||||
|
||||
<div class="text-center">
|
||||
<img src="/img/docs/tutorials/grafana-high-availability.png" max-width= "800px" class="center"></img>
|
||||
<img src="/img/docs/tutorials/grafana-high-availability.png" max-width= "800px" class="center" />
|
||||
</div>
|
||||
|
||||
## Configure multiple servers to use the same database
|
||||
@ -24,8 +24,14 @@ First, you need to do is to setup MySQL or Postgres on another server and config
|
||||
You can find the configuration for doing that in the [[database]]({{< relref "configuration.md" >}}#database) section in the grafana config.
|
||||
Grafana will now persist all long term data in the database. How to configure the database for high availability is out of scope for this guide. We recommend finding an expert on for the database you're using.
|
||||
|
||||
## Alerting
|
||||
|
||||
Currently alerting supports a limited form of high availability. Since v4.2.0, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but alert notifications are only sent once per alert. Grafana does not support load distribution between servers.
|
||||
|
||||
## User sessions
|
||||
|
||||
> Beginning with Grafana v6.0 and above the following only applies when using [Auth Proxy Authentication](/auth/auth-proxy/).
|
||||
|
||||
The second thing to consider is how to deal with user sessions and how to configure your load balancer in front of Grafana.
|
||||
Grafana supports two ways of storing session data: locally on disk or in a database/cache-server.
|
||||
If you want to store sessions on disk you can use `sticky sessions` in your load balancer. If you prefer to store session data in a database/cache-server
|
||||
@ -41,6 +47,4 @@ If you use MySQL/Postgres for session storage, you first need a table to store t
|
||||
|
||||
For Grafana itself it doesn't really matter if you store the session data on disk or database/redis/memcache. But we recommend using a database/redis/memcache since it makes it easier manage the grafana servers.
|
||||
|
||||
## Alerting
|
||||
|
||||
Currently alerting supports a limited form of high availability. Since v4.2.0, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but alert notifications are only sent once per alert. Grafana does not support distributing the alert rule execution between servers. That might be added in the future but right now prefer to keep it simple.
|
||||
|
@ -121,7 +121,7 @@
|
||||
"jest": "jest --notify --watch",
|
||||
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
|
||||
"storybook": "cd packages/grafana-ui && yarn storybook",
|
||||
"prettier:check": "prettier -- --list-different \"**/*.{ts,tsx,scss}\""
|
||||
"prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\""
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
@ -9,7 +9,7 @@ import { Themeable } from '../../index';
|
||||
type GaugeValue = string | number | null;
|
||||
|
||||
export interface Props extends Themeable {
|
||||
decimals: number;
|
||||
decimals?: number | null;
|
||||
height: number;
|
||||
valueMappings: ValueMapping[];
|
||||
maxValue: number;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ComponentClass } from 'react';
|
||||
import { TimeSeries, LoadingState, TableData } from './data';
|
||||
import { TimeRange } from './time';
|
||||
|
||||
@ -19,11 +20,29 @@ export interface PanelData {
|
||||
tableData?: TableData;
|
||||
}
|
||||
|
||||
export interface PanelOptionsProps<T = any> {
|
||||
export interface PanelEditorProps<T = any> {
|
||||
options: T;
|
||||
onChange: (options: T) => void;
|
||||
}
|
||||
|
||||
export class ReactPanelPlugin<TOptions = any> {
|
||||
panel: ComponentClass<PanelProps<TOptions>>;
|
||||
editor?: ComponentClass<PanelEditorProps<TOptions>>;
|
||||
defaults?: TOptions;
|
||||
|
||||
constructor(panel: ComponentClass<PanelProps<TOptions>>) {
|
||||
this.panel = panel;
|
||||
}
|
||||
|
||||
setEditor(editor: ComponentClass<PanelEditorProps<TOptions>>) {
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
setDefaults(defaults: TOptions) {
|
||||
this.defaults = defaults;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PanelSize {
|
||||
width: number;
|
||||
height: number;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ComponentClass } from 'react';
|
||||
import { PanelProps, PanelOptionsProps } from './panel';
|
||||
import { ReactPanelPlugin } from './panel';
|
||||
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource';
|
||||
|
||||
export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
|
||||
@ -81,9 +81,7 @@ export interface PluginExports {
|
||||
|
||||
// Panel plugin
|
||||
PanelCtrl?: any;
|
||||
Panel?: ComponentClass<PanelProps>;
|
||||
PanelOptions?: ComponentClass<PanelOptionsProps>;
|
||||
PanelDefaults?: any;
|
||||
reactPanel: ReactPanelPlugin;
|
||||
}
|
||||
|
||||
export interface PluginMeta {
|
||||
|
@ -95,52 +95,9 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
|
||||
}
|
||||
}
|
||||
case "diff":
|
||||
var (
|
||||
points = series.Points
|
||||
first float64
|
||||
i int
|
||||
)
|
||||
// get the newest point
|
||||
for i = len(points) - 1; i >= 0; i-- {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
first = points[i][0].Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
// get the oldest point
|
||||
points = points[0:i]
|
||||
for i := 0; i < len(points); i++ {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
value = first - points[i][0].Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
allNull, value = calculateDiff(series, allNull, value, diff)
|
||||
case "percent_diff":
|
||||
var (
|
||||
points = series.Points
|
||||
first float64
|
||||
i int
|
||||
)
|
||||
// get the newest point
|
||||
for i = len(points) - 1; i >= 0; i-- {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
first = points[i][0].Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
// get the oldest point
|
||||
points = points[0:i]
|
||||
for i := 0; i < len(points); i++ {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
val := (first - points[i][0].Float64) / points[i][0].Float64 * 100
|
||||
value = math.Abs(val)
|
||||
break
|
||||
}
|
||||
}
|
||||
allNull, value = calculateDiff(series, allNull, value, percentDiff)
|
||||
case "count_non_null":
|
||||
for _, v := range series.Points {
|
||||
if v[0].Valid {
|
||||
@ -163,3 +120,40 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
|
||||
func NewSimpleReducer(typ string) *SimpleReducer {
|
||||
return &SimpleReducer{Type: typ}
|
||||
}
|
||||
|
||||
func calculateDiff(series *tsdb.TimeSeries, allNull bool, value float64, fn func(float64, float64) float64) (bool, float64) {
|
||||
var (
|
||||
points = series.Points
|
||||
first float64
|
||||
i int
|
||||
)
|
||||
// get the newest point
|
||||
for i = len(points) - 1; i >= 0; i-- {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
first = points[i][0].Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
if i >= 1 {
|
||||
// get the oldest point
|
||||
points = points[0:i]
|
||||
for i := 0; i < len(points); i++ {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
val := fn(first, points[i][0].Float64)
|
||||
value = math.Abs(val)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return allNull, value
|
||||
}
|
||||
|
||||
var diff = func(newest, oldest float64) float64 {
|
||||
return newest - oldest
|
||||
}
|
||||
|
||||
var percentDiff = func(newest, oldest float64) float64 {
|
||||
return (newest - oldest) / oldest * 100
|
||||
}
|
||||
|
@ -143,6 +143,18 @@ func TestSimpleReducer(t *testing.T) {
|
||||
So(result, ShouldEqual, float64(10))
|
||||
})
|
||||
|
||||
Convey("diff with only nulls", func() {
|
||||
reducer := NewSimpleReducer("diff")
|
||||
series := &tsdb.TimeSeries{
|
||||
Name: "test time serie",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
|
||||
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, false)
|
||||
})
|
||||
|
||||
Convey("percent_diff one point", func() {
|
||||
result := testReducer("percent_diff", 40)
|
||||
So(result, ShouldEqual, float64(0))
|
||||
@ -157,6 +169,18 @@ func TestSimpleReducer(t *testing.T) {
|
||||
result := testReducer("percent_diff", 30, 40, 40)
|
||||
So(result, ShouldEqual, float64(33.33333333333333))
|
||||
})
|
||||
|
||||
Convey("percent_diff with only nulls", func() {
|
||||
reducer := NewSimpleReducer("percent_diff")
|
||||
series := &tsdb.TimeSeries{
|
||||
Name: "test time serie",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
|
||||
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -104,7 +104,7 @@ func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) {
|
||||
return c.dashboardRef, nil
|
||||
}
|
||||
|
||||
const urlFormat = "%s?fullscreen=true&edit=true&tab=alert&panelId=%d&orgId=%d"
|
||||
const urlFormat = "%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d"
|
||||
|
||||
func (c *EvalContext) GetRuleUrl() (string, error) {
|
||||
if c.IsTestRun {
|
||||
|
@ -14,4 +14,3 @@ export const DASHBOARD_TOP_PADDING = 20;
|
||||
|
||||
export const PANEL_HEADER_HEIGHT = 27;
|
||||
export const PANEL_BORDER = 2;
|
||||
export const PANEL_OPTIONS_KEY_PREFIX = 'options-';
|
||||
|
@ -29,7 +29,7 @@ class AlertRuleItem extends PureComponent<Props> {
|
||||
'fa-pause': rule.state !== 'paused',
|
||||
});
|
||||
|
||||
const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`;
|
||||
const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen&edit&tab=alert`;
|
||||
|
||||
return (
|
||||
<li className="alert-rule-item">
|
||||
|
@ -21,7 +21,7 @@ exports[`Render should render component 1`] = `
|
||||
className="alert-rule-item__name"
|
||||
>
|
||||
<a
|
||||
href="https://something.something.darkside?panelId=1&fullscreen=true&edit=true&tab=alert"
|
||||
href="https://something.something.darkside?panelId=1&fullscreen&edit&tab=alert"
|
||||
>
|
||||
<Highlighter
|
||||
highlightClassName="highlight-search-match"
|
||||
@ -73,7 +73,7 @@ exports[`Render should render component 1`] = `
|
||||
</button>
|
||||
<a
|
||||
className="btn btn-small btn-inverse alert-list__btn width-2"
|
||||
href="https://something.something.darkside?panelId=1&fullscreen=true&edit=true&tab=alert"
|
||||
href="https://something.something.darkside?panelId=1&fullscreen&edit&tab=alert"
|
||||
title="Edit alert rule"
|
||||
>
|
||||
<i
|
||||
|
@ -248,7 +248,7 @@ export class DashNav extends PureComponent<Props> {
|
||||
|
||||
<div className="navbar-buttons navbar-buttons--tv">
|
||||
<DashNavButton
|
||||
tooltip="Cycke view mode"
|
||||
tooltip="Cycle view mode"
|
||||
classSuffix="tv"
|
||||
icon="fa fa-desktop"
|
||||
onClick={this.onToggleTVMode}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { DashboardPage, Props, State } from './DashboardPage';
|
||||
import { DashboardPage, Props, State, mapStateToProps } from './DashboardPage';
|
||||
import { DashboardModel } from '../state';
|
||||
import { cleanUpDashboard } from '../state/actions';
|
||||
import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock } from 'app/core/redux';
|
||||
@ -250,4 +250,36 @@ describe('DashboardPage', () => {
|
||||
expect(ctx.cleanUpDashboardMock.calls).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps with bool fullscreen', () => {
|
||||
const props = mapStateToProps({
|
||||
location: {
|
||||
routeParams: {},
|
||||
query: {
|
||||
fullscreen: true,
|
||||
edit: false,
|
||||
},
|
||||
},
|
||||
dashboard: {},
|
||||
} as any);
|
||||
|
||||
expect(props.urlFullscreen).toBe(true);
|
||||
expect(props.urlEdit).toBe(false);
|
||||
});
|
||||
|
||||
describe('mapStateToProps with string edit true', () => {
|
||||
const props = mapStateToProps({
|
||||
location: {
|
||||
routeParams: {},
|
||||
query: {
|
||||
fullscreen: false,
|
||||
edit: 'true',
|
||||
},
|
||||
},
|
||||
dashboard: {},
|
||||
} as any);
|
||||
|
||||
expect(props.urlFullscreen).toBe(false);
|
||||
expect(props.urlEdit).toBe(true);
|
||||
});
|
||||
});
|
||||
|
@ -284,15 +284,15 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
export const mapStateToProps = (state: StoreState) => ({
|
||||
urlUid: state.location.routeParams.uid,
|
||||
urlSlug: state.location.routeParams.slug,
|
||||
urlType: state.location.routeParams.type,
|
||||
editview: state.location.query.editview,
|
||||
urlPanelId: state.location.query.panelId,
|
||||
urlFolderId: state.location.query.folderId,
|
||||
urlFullscreen: state.location.query.fullscreen === true,
|
||||
urlEdit: state.location.query.edit === true,
|
||||
urlFullscreen: !!state.location.query.fullscreen,
|
||||
urlEdit: !!state.location.query.edit,
|
||||
initPhase: state.dashboard.initPhase,
|
||||
isInitSlow: state.dashboard.isInitSlow,
|
||||
initError: state.dashboard.initError,
|
||||
|
@ -78,7 +78,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 17,
|
||||
"schemaVersion": 18,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
@ -190,7 +190,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 17,
|
||||
"schemaVersion": 18,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
@ -313,7 +313,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 17,
|
||||
"schemaVersion": 18,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
@ -423,7 +423,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 17,
|
||||
"schemaVersion": 18,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
@ -518,7 +518,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 17,
|
||||
"schemaVersion": 18,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
|
@ -131,10 +131,10 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
renderReactPanel() {
|
||||
const { dashboard, panel } = this.props;
|
||||
const { dashboard, panel, isFullscreen } = this.props;
|
||||
const { plugin } = this.state;
|
||||
|
||||
return <PanelChrome plugin={plugin} panel={panel} dashboard={dashboard} />;
|
||||
return <PanelChrome plugin={plugin} panel={panel} dashboard={dashboard} isFullscreen={isFullscreen} />;
|
||||
}
|
||||
|
||||
renderAngularPanel() {
|
||||
@ -173,7 +173,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
style={styles}
|
||||
>
|
||||
{plugin.exports.Panel && this.renderReactPanel()}
|
||||
{plugin.exports.reactPanel && this.renderReactPanel()}
|
||||
{plugin.exports.PanelCtrl && this.renderAngularPanel()}
|
||||
</div>
|
||||
)}
|
||||
|
@ -162,7 +162,7 @@ export class DataPanel extends Component<Props, State> {
|
||||
}
|
||||
|
||||
onError(message, err);
|
||||
this.setState({ isFirstLoad: false });
|
||||
this.setState({ isFirstLoad: false, loading: LoadingState.Error });
|
||||
}
|
||||
};
|
||||
|
||||
@ -187,7 +187,8 @@ export class DataPanel extends Component<Props, State> {
|
||||
const { loading, isFirstLoad } = this.state;
|
||||
const panelData = this.getPanelData();
|
||||
|
||||
if (isFirstLoad && loading === LoadingState.Loading) {
|
||||
// do not render component until we have first data
|
||||
if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
|
||||
return this.renderLoadingState();
|
||||
}
|
||||
|
||||
@ -201,21 +202,17 @@ export class DataPanel extends Component<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.renderLoadingState()}
|
||||
{loading === LoadingState.Loading && this.renderLoadingState()}
|
||||
{this.props.children({ loading, panelData })}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderLoadingState(): JSX.Element {
|
||||
const { loading } = this.state;
|
||||
if (loading === LoadingState.Loading) {
|
||||
return (
|
||||
<div className="panel-loading">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
return (
|
||||
<div className="panel-loading">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ export interface Props {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
plugin: PanelPlugin;
|
||||
isFullscreen: boolean;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
@ -139,7 +140,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element {
|
||||
const { panel, plugin } = this.props;
|
||||
const { timeRange, renderCounter } = this.state;
|
||||
const PanelComponent = plugin.exports.Panel;
|
||||
const PanelComponent = plugin.exports.reactPanel.panel;
|
||||
|
||||
// This is only done to increase a counter that is used by backend
|
||||
// image rendering (phantomjs/headless chrome) to know when to capture image
|
||||
@ -153,7 +154,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
loading={loading}
|
||||
panelData={panelData}
|
||||
timeRange={timeRange}
|
||||
options={panel.getOptions(plugin.exports.PanelDefaults)}
|
||||
options={panel.getOptions(plugin.exports.reactPanel.defaults)}
|
||||
width={width - 2 * variables.panelhorizontalpadding}
|
||||
height={height - PANEL_HEADER_HEIGHT - variables.panelverticalpadding}
|
||||
renderCounter={renderCounter}
|
||||
@ -193,7 +194,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dashboard, panel } = this.props;
|
||||
const { dashboard, panel, isFullscreen } = this.props;
|
||||
const { errorMessage, timeInfo } = this.state;
|
||||
const { transparent } = panel;
|
||||
|
||||
@ -216,6 +217,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
scopedVars={panel.scopedVars}
|
||||
links={panel.links}
|
||||
error={errorMessage}
|
||||
isFullscreen={isFullscreen}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
{({ error, errorInfo }) => {
|
||||
|
@ -19,6 +19,7 @@ export interface Props {
|
||||
scopedVars?: string;
|
||||
links?: [];
|
||||
error?: string;
|
||||
isFullscreen: boolean;
|
||||
}
|
||||
|
||||
interface ClickCoordinates {
|
||||
@ -69,10 +70,9 @@ export class PanelHeader extends Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const isFullscreen = false;
|
||||
const isLoading = false;
|
||||
const { panel, dashboard, timeInfo, scopedVars, error, isFullscreen } = this.props;
|
||||
|
||||
const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
|
||||
const { panel, dashboard, timeInfo, scopedVars, error } = this.props;
|
||||
const title = templateSrv.replaceWithText(panel.title, scopedVars);
|
||||
|
||||
return (
|
||||
@ -86,11 +86,6 @@ export class PanelHeader extends Component<Props, State> {
|
||||
error={error}
|
||||
/>
|
||||
<div className={panelHeaderClass}>
|
||||
{isLoading && (
|
||||
<span className="panel-loading">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</span>
|
||||
)}
|
||||
<div className="panel-title-container" onClick={this.onMenuToggle} onMouseDown={this.onMouseDown}>
|
||||
<div className="panel-title">
|
||||
<span className="icon-gf panel-alert-icon" />
|
||||
|
@ -6,8 +6,8 @@ import React, { PureComponent } from 'react';
|
||||
import { AlertBox } from 'app/core/components/AlertBox/AlertBox';
|
||||
|
||||
// Types
|
||||
import { PanelProps } from '@grafana/ui';
|
||||
import { PanelPlugin, AppNotificationSeverity } from 'app/types';
|
||||
import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
pluginId: string;
|
||||
@ -64,7 +64,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin {
|
||||
},
|
||||
|
||||
exports: {
|
||||
Panel: NotFound,
|
||||
reactPanel: new ReactPanelPlugin(NotFound),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ export class GeneralTab extends PureComponent<Props> {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditorTabBody heading="Panel Options" toolbarItems={[]}>
|
||||
<EditorTabBody heading="General" toolbarItems={[]}>
|
||||
<div ref={element => (this.element = element)} />
|
||||
</EditorTabBody>
|
||||
);
|
||||
|
@ -45,7 +45,7 @@ interface PanelEditorTab {
|
||||
const panelEditorTabTexts = {
|
||||
[PanelEditorTabIds.Queries]: 'Queries',
|
||||
[PanelEditorTabIds.Visualization]: 'Visualization',
|
||||
[PanelEditorTabIds.Advanced]: 'Panel Options',
|
||||
[PanelEditorTabIds.Advanced]: 'General',
|
||||
[PanelEditorTabIds.Alert]: 'Alert',
|
||||
};
|
||||
|
||||
|
@ -135,7 +135,7 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
|
||||
<div className="flex-grow-1" />
|
||||
{!isAddingMixed && (
|
||||
<button className="btn navbar-button navbar-button--primary" onClick={this.onAddQueryClick}>
|
||||
<button className="btn navbar-button" onClick={this.onAddQueryClick}>
|
||||
Add Query
|
||||
</button>
|
||||
)}
|
||||
|
@ -50,33 +50,27 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
};
|
||||
}
|
||||
|
||||
getPanelDefaultOptions = () => {
|
||||
getReactPanelOptions = () => {
|
||||
const { panel, plugin } = this.props;
|
||||
|
||||
if (plugin.exports.PanelDefaults) {
|
||||
return panel.getOptions(plugin.exports.PanelDefaults);
|
||||
}
|
||||
|
||||
return panel.getOptions({});
|
||||
return panel.getOptions(plugin.exports.reactPanel.defaults);
|
||||
};
|
||||
|
||||
renderPanelOptions() {
|
||||
const { plugin, angularPanel } = this.props;
|
||||
const { PanelOptions } = plugin.exports;
|
||||
|
||||
if (angularPanel) {
|
||||
return <div ref={element => (this.element = element)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{PanelOptions ? (
|
||||
<PanelOptions options={this.getPanelDefaultOptions()} onChange={this.onPanelOptionsChanged} />
|
||||
) : (
|
||||
<p>Visualization has no options</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
if (plugin.exports.reactPanel) {
|
||||
const PanelEditor = plugin.exports.reactPanel.editor;
|
||||
|
||||
if (PanelEditor) {
|
||||
return <PanelEditor options={this.getReactPanelOptions()} onChange={this.onPanelOptionsChanged} />;
|
||||
}
|
||||
}
|
||||
|
||||
return <p>Visualization has no options</p>;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -127,7 +127,7 @@ describe('DashboardModel', () => {
|
||||
});
|
||||
|
||||
it('dashboard schema version should be set to latest', () => {
|
||||
expect(model.schemaVersion).toBe(17);
|
||||
expect(model.schemaVersion).toBe(18);
|
||||
});
|
||||
|
||||
it('graph thresholds should be migrated', () => {
|
||||
|
@ -22,7 +22,7 @@ export class DashboardMigrator {
|
||||
let i, j, k, n;
|
||||
const oldVersion = this.dashboard.schemaVersion;
|
||||
const panelUpgrades = [];
|
||||
this.dashboard.schemaVersion = 17;
|
||||
this.dashboard.schemaVersion = 18;
|
||||
|
||||
if (oldVersion === this.dashboard.schemaVersion) {
|
||||
return;
|
||||
@ -387,6 +387,30 @@ export class DashboardMigrator {
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 18) {
|
||||
// migrate change to gauge options
|
||||
panelUpgrades.push(panel => {
|
||||
if (panel['options-gauge']) {
|
||||
panel.options = panel['options-gauge'];
|
||||
panel.options.valueOptions = {
|
||||
unit: panel.options.unit,
|
||||
stat: panel.options.stat,
|
||||
decimals: panel.options.decimals,
|
||||
prefix: panel.options.prefix,
|
||||
suffix: panel.options.suffix,
|
||||
};
|
||||
// this options prop was due to a bug
|
||||
delete panel.options.options;
|
||||
delete panel.options.unit;
|
||||
delete panel.options.stat;
|
||||
delete panel.options.decimals;
|
||||
delete panel.options.prefix;
|
||||
delete panel.options.suffix;
|
||||
delete panel['options-gauge'];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (panelUpgrades.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -55,5 +55,19 @@ describe('PanelModel', () => {
|
||||
expect(model.alert).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get panel options', () => {
|
||||
it('should apply defaults', () => {
|
||||
model.options = { existingProp: 10 };
|
||||
const options = model.getOptions({
|
||||
defaultProp: true,
|
||||
existingProp: 0,
|
||||
});
|
||||
|
||||
expect(options.defaultProp).toBe(true);
|
||||
expect(options.existingProp).toBe(10);
|
||||
expect(model.options).toBe(options);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3,7 +3,6 @@ import _ from 'lodash';
|
||||
|
||||
// Types
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants';
|
||||
import { DataQuery, TimeSeries } from '@grafana/ui';
|
||||
import { TableData } from '@grafana/ui/src';
|
||||
|
||||
@ -92,6 +91,7 @@ export class PanelModel {
|
||||
timeFrom?: any;
|
||||
timeShift?: any;
|
||||
hideTimeOverride?: any;
|
||||
options: object;
|
||||
|
||||
maxDataPoints?: number;
|
||||
interval?: string;
|
||||
@ -105,8 +105,6 @@ export class PanelModel {
|
||||
hasRefreshed: boolean;
|
||||
events: Emitter;
|
||||
cacheTimeout?: any;
|
||||
|
||||
// cache props between plugins
|
||||
cachedPluginOptions?: any;
|
||||
|
||||
constructor(model) {
|
||||
@ -134,20 +132,14 @@ export class PanelModel {
|
||||
}
|
||||
|
||||
getOptions(panelDefaults) {
|
||||
return _.defaultsDeep(this[this.getOptionsKey()] || {}, panelDefaults);
|
||||
return _.defaultsDeep(this.options || {}, panelDefaults);
|
||||
}
|
||||
|
||||
updateOptions(options: object) {
|
||||
const update: any = {};
|
||||
update[this.getOptionsKey()] = options;
|
||||
Object.assign(this, update);
|
||||
this.options = options;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private getOptionsKey() {
|
||||
return PANEL_OPTIONS_KEY_PREFIX + this.type;
|
||||
}
|
||||
|
||||
getSaveModel() {
|
||||
const model: any = {};
|
||||
for (const property in this) {
|
||||
@ -240,14 +232,15 @@ export class PanelModel {
|
||||
// for angular panels only we need to remove all events and let angular panels do some cleanup
|
||||
if (fromAngularPanel) {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
for (const key of _.keys(this)) {
|
||||
if (mustKeepProps[key]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
delete this[key];
|
||||
// remove panel type specific options
|
||||
for (const key of _.keys(this)) {
|
||||
if (mustKeepProps[key]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
delete this[key];
|
||||
}
|
||||
|
||||
this.restorePanelOptions(pluginId);
|
||||
|
@ -16,9 +16,21 @@ describe('<LogMessageAnsi />', () => {
|
||||
const wrapper = shallow(<LogMessageAnsi value={value} />);
|
||||
|
||||
expect(wrapper.find('span')).toHaveLength(1);
|
||||
expect(wrapper.find('span').first().prop('style')).toMatchObject(expect.objectContaining({
|
||||
color: expect.any(String)
|
||||
}));
|
||||
expect(wrapper.find('span').first().text()).toBe('ipsum');
|
||||
expect(
|
||||
wrapper
|
||||
.find('span')
|
||||
.first()
|
||||
.prop('style')
|
||||
).toMatchObject(
|
||||
expect.objectContaining({
|
||||
color: expect.any(String),
|
||||
})
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find('span')
|
||||
.first()
|
||||
.text()
|
||||
).toBe('ipsum');
|
||||
});
|
||||
});
|
||||
|
@ -46,15 +46,15 @@ export class LogMessageAnsi extends PureComponent<Props, State> {
|
||||
const parsed = ansicolor.parse(props.value);
|
||||
|
||||
return {
|
||||
chunks: parsed.spans.map((span) => {
|
||||
return span.css ?
|
||||
{
|
||||
style: convertCSSToStyle(span.css),
|
||||
text: span.text
|
||||
} :
|
||||
{ text: span.text };
|
||||
chunks: parsed.spans.map(span => {
|
||||
return span.css
|
||||
? {
|
||||
style: convertCSSToStyle(span.css),
|
||||
text: span.text,
|
||||
}
|
||||
: { text: span.text };
|
||||
}),
|
||||
prevValue: props.value
|
||||
prevValue: props.value,
|
||||
};
|
||||
}
|
||||
|
||||
@ -62,9 +62,14 @@ export class LogMessageAnsi extends PureComponent<Props, State> {
|
||||
const { chunks } = this.state;
|
||||
|
||||
return chunks.map(
|
||||
(chunk, index) => chunk.style ?
|
||||
<span key={index} style={chunk.style}>{chunk.text}</span> :
|
||||
chunk.text
|
||||
(chunk, index) =>
|
||||
chunk.style ? (
|
||||
<span key={index} style={chunk.style}>
|
||||
{chunk.text}
|
||||
</span>
|
||||
) : (
|
||||
chunk.text
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,10 @@ interface Props extends PanelProps<BarGaugeOptions> {}
|
||||
export class BarGaugePanel extends PureComponent<Props> {
|
||||
render() {
|
||||
const { panelData, width, height, onInterpolate, options } = this.props;
|
||||
const { valueOptions } = options;
|
||||
|
||||
const prefix = onInterpolate(options.prefix);
|
||||
const suffix = onInterpolate(options.suffix);
|
||||
const prefix = onInterpolate(valueOptions.prefix);
|
||||
const suffix = onInterpolate(valueOptions.suffix);
|
||||
|
||||
let value: TimeSeriesValue;
|
||||
|
||||
@ -29,7 +30,7 @@ export class BarGaugePanel extends PureComponent<Props> {
|
||||
});
|
||||
|
||||
if (vmSeries[0]) {
|
||||
value = vmSeries[0].stats[options.stat];
|
||||
value = vmSeries[0].stats[valueOptions.stat];
|
||||
} else {
|
||||
value = null;
|
||||
}
|
||||
@ -42,11 +43,16 @@ export class BarGaugePanel extends PureComponent<Props> {
|
||||
{theme => (
|
||||
<BarGauge
|
||||
value={value}
|
||||
{...this.props.options}
|
||||
width={width}
|
||||
height={height}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
unit={valueOptions.unit}
|
||||
decimals={valueOptions.decimals}
|
||||
thresholds={options.thresholds}
|
||||
valueMappings={options.valueMappings}
|
||||
minValue={options.minValue}
|
||||
maxValue={options.maxValue}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
|
@ -2,14 +2,15 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Components
|
||||
import { ValueOptions } from 'app/plugins/panel/gauge/ValueOptions';
|
||||
import { SingleStatValueEditor } from '../gauge/SingleStatValueEditor';
|
||||
import { ThresholdsEditor, ValueMappingsEditor, PanelOptionsGrid, PanelOptionsGroup, FormField } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { PanelOptionsProps, Threshold, ValueMapping } from '@grafana/ui';
|
||||
import { PanelEditorProps, Threshold, ValueMapping } from '@grafana/ui';
|
||||
import { BarGaugeOptions } from './types';
|
||||
import { SingleStatValueOptions } from '../gauge/types';
|
||||
|
||||
export class BarGaugePanelOptions extends PureComponent<PanelOptionsProps<BarGaugeOptions>> {
|
||||
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
|
||||
onThresholdsChanged = (thresholds: Threshold[]) =>
|
||||
this.props.onChange({
|
||||
...this.props.options,
|
||||
@ -23,15 +24,22 @@ export class BarGaugePanelOptions extends PureComponent<PanelOptionsProps<BarGau
|
||||
});
|
||||
|
||||
onMinValueChange = ({ target }) => this.props.onChange({ ...this.props.options, minValue: target.value });
|
||||
|
||||
onMaxValueChange = ({ target }) => this.props.onChange({ ...this.props.options, maxValue: target.value });
|
||||
|
||||
onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
|
||||
this.props.onChange({
|
||||
...this.props.options,
|
||||
valueOptions,
|
||||
});
|
||||
|
||||
render() {
|
||||
const { onChange, options } = this.props;
|
||||
const { options } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelOptionsGrid>
|
||||
<ValueOptions onChange={onChange} options={options} />
|
||||
<SingleStatValueEditor onChange={this.onValueOptionsChanged} options={options.valueOptions} />
|
||||
<PanelOptionsGroup title="Gauge">
|
||||
<FormField label="Min value" labelWidth={8} onChange={this.onMinValueChange} value={options.minValue} />
|
||||
<FormField label="Max value" labelWidth={8} onChange={this.onMaxValueChange} value={options.maxValue} />
|
@ -1,5 +1,9 @@
|
||||
import { BarGaugePanel } from './BarGaugePanel';
|
||||
import { BarGaugePanelOptions } from './BarGaugePanelOptions';
|
||||
import { PanelDefaults } from './types';
|
||||
import { ReactPanelPlugin } from '@grafana/ui';
|
||||
|
||||
export { BarGaugePanel as Panel, BarGaugePanelOptions as PanelOptions, PanelDefaults };
|
||||
import { BarGaugePanel } from './BarGaugePanel';
|
||||
import { BarGaugePanelEditor } from './BarGaugePanelEditor';
|
||||
import { BarGaugeOptions, defaults } from './types';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel);
|
||||
reactPanel.setEditor(BarGaugePanelEditor);
|
||||
reactPanel.setDefaults(defaults);
|
||||
|
@ -1,23 +1,24 @@
|
||||
import { Threshold, ValueMapping } from '@grafana/ui';
|
||||
import { SingleStatValueOptions } from '../gauge/types';
|
||||
|
||||
export interface BarGaugeOptions {
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
prefix: string;
|
||||
stat: string;
|
||||
suffix: string;
|
||||
unit: string;
|
||||
valueOptions: SingleStatValueOptions;
|
||||
valueMappings: ValueMapping[];
|
||||
thresholds: Threshold[];
|
||||
}
|
||||
|
||||
export const PanelDefaults: BarGaugeOptions = {
|
||||
export const defaults: BarGaugeOptions = {
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
stat: 'avg',
|
||||
unit: 'none',
|
||||
valueOptions: {
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
decimals: null,
|
||||
stat: 'avg',
|
||||
unit: 'none',
|
||||
},
|
||||
thresholds: [
|
||||
{ index: 2, value: 80, color: 'red' },
|
||||
{ index: 1, value: 50, color: 'orange' },
|
||||
|
@ -1,8 +1,14 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
import { FormField, PanelOptionsProps, PanelOptionsGroup, Switch } from '@grafana/ui';
|
||||
|
||||
// Components
|
||||
import { Switch, PanelOptionsGroup } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { FormField, PanelEditorProps } from '@grafana/ui';
|
||||
import { GaugeOptions } from './types';
|
||||
|
||||
export class GaugeOptionsEditor extends PureComponent<PanelOptionsProps<GaugeOptions>> {
|
||||
export class GaugeOptionsBox extends PureComponent<PanelEditorProps<GaugeOptions>> {
|
||||
onToggleThresholdLabels = () =>
|
||||
this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
|
||||
|
@ -16,9 +16,10 @@ interface Props extends PanelProps<GaugeOptions> {}
|
||||
export class GaugePanel extends PureComponent<Props> {
|
||||
render() {
|
||||
const { panelData, width, height, onInterpolate, options } = this.props;
|
||||
const { valueOptions } = options;
|
||||
|
||||
const prefix = onInterpolate(options.prefix);
|
||||
const suffix = onInterpolate(options.suffix);
|
||||
const prefix = onInterpolate(valueOptions.prefix);
|
||||
const suffix = onInterpolate(valueOptions.suffix);
|
||||
let value: TimeSeriesValue;
|
||||
|
||||
if (panelData.timeSeries) {
|
||||
@ -28,7 +29,7 @@ export class GaugePanel extends PureComponent<Props> {
|
||||
});
|
||||
|
||||
if (vmSeries[0]) {
|
||||
value = vmSeries[0].stats[options.stat];
|
||||
value = vmSeries[0].stats[valueOptions.stat];
|
||||
} else {
|
||||
value = null;
|
||||
}
|
||||
@ -41,11 +42,18 @@ export class GaugePanel extends PureComponent<Props> {
|
||||
{theme => (
|
||||
<Gauge
|
||||
value={value}
|
||||
{...this.props.options}
|
||||
width={width}
|
||||
height={height}
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
unit={valueOptions.unit}
|
||||
decimals={valueOptions.decimals}
|
||||
thresholds={options.thresholds}
|
||||
valueMappings={options.valueMappings}
|
||||
showThresholdLabels={options.showThresholdLabels}
|
||||
showThresholdMarkers={options.showThresholdMarkers}
|
||||
minValue={options.minValue}
|
||||
maxValue={options.maxValue}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
|
50
public/app/plugins/panel/gauge/GaugePanelEditor.tsx
Normal file
50
public/app/plugins/panel/gauge/GaugePanelEditor.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
import {
|
||||
PanelEditorProps,
|
||||
ThresholdsEditor,
|
||||
Threshold,
|
||||
PanelOptionsGrid,
|
||||
ValueMappingsEditor,
|
||||
ValueMapping,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
|
||||
import { GaugeOptionsBox } from './GaugeOptionsBox';
|
||||
import { GaugeOptions, SingleStatValueOptions } from './types';
|
||||
|
||||
export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
|
||||
onThresholdsChanged = (thresholds: Threshold[]) =>
|
||||
this.props.onChange({
|
||||
...this.props.options,
|
||||
thresholds,
|
||||
});
|
||||
|
||||
onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
|
||||
this.props.onChange({
|
||||
...this.props.options,
|
||||
valueMappings,
|
||||
});
|
||||
|
||||
onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
|
||||
this.props.onChange({
|
||||
...this.props.options,
|
||||
valueOptions,
|
||||
});
|
||||
|
||||
render() {
|
||||
const { onChange, options } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelOptionsGrid>
|
||||
<SingleStatValueEditor onChange={this.onValueOptionsChanged} options={options.valueOptions} />
|
||||
<GaugeOptionsBox onChange={onChange} options={options} />
|
||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Components
|
||||
import { ValueOptions } from 'app/plugins/panel/gauge/ValueOptions';
|
||||
import { GaugeOptionsEditor } from './GaugeOptionsEditor';
|
||||
import { ThresholdsEditor, ValueMappingsEditor } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { PanelOptionsProps, Threshold, PanelOptionsGrid, ValueMapping } from '@grafana/ui';
|
||||
import { GaugeOptions } from './types';
|
||||
|
||||
export class GaugePanelOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> {
|
||||
onThresholdsChanged = (thresholds: Threshold[]) =>
|
||||
this.props.onChange({
|
||||
...this.props.options,
|
||||
thresholds,
|
||||
});
|
||||
|
||||
onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
|
||||
this.props.onChange({
|
||||
...this.props.options,
|
||||
valueMappings,
|
||||
});
|
||||
|
||||
render() {
|
||||
const { onChange, options } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelOptionsGrid>
|
||||
<ValueOptions onChange={onChange} options={options} />
|
||||
<GaugeOptionsEditor onChange={onChange} options={options} />
|
||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,7 +1,12 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
import { FormField, FormLabel, PanelOptionsProps, PanelOptionsGroup, Select } from '@grafana/ui';
|
||||
|
||||
// Components
|
||||
import UnitPicker from 'app/core/components/Select/UnitPicker';
|
||||
import { GaugeOptions } from './types';
|
||||
import { FormField, FormLabel, PanelOptionsGroup, Select } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { SingleStatValueOptions } from './types';
|
||||
|
||||
const statOptions = [
|
||||
{ value: 'min', label: 'Min' },
|
||||
@ -19,24 +24,40 @@ const statOptions = [
|
||||
|
||||
const labelWidth = 6;
|
||||
|
||||
export class ValueOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> {
|
||||
onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
|
||||
export interface Props {
|
||||
options: SingleStatValueOptions;
|
||||
onChange: (valueOptions: SingleStatValueOptions) => void;
|
||||
}
|
||||
|
||||
export class SingleStatValueEditor extends PureComponent<Props> {
|
||||
onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
|
||||
onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value });
|
||||
|
||||
onDecimalChange = event => {
|
||||
if (!isNaN(event.target.value)) {
|
||||
this.props.onChange({ ...this.props.options, decimals: event.target.value });
|
||||
this.props.onChange({
|
||||
...this.props.options,
|
||||
decimals: parseInt(event.target.value, 10),
|
||||
});
|
||||
} else {
|
||||
this.props.onChange({
|
||||
...this.props.options,
|
||||
decimals: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value });
|
||||
|
||||
onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value });
|
||||
|
||||
render() {
|
||||
const { stat, unit, decimals, prefix, suffix } = this.props.options;
|
||||
|
||||
let decimalsString = '';
|
||||
if (Number.isFinite(decimals)) {
|
||||
decimalsString = decimals.toString();
|
||||
}
|
||||
|
||||
return (
|
||||
<PanelOptionsGroup title="Value">
|
||||
<div className="gf-form">
|
||||
@ -57,7 +78,7 @@ export class ValueOptions extends PureComponent<PanelOptionsProps<GaugeOptions>>
|
||||
labelWidth={labelWidth}
|
||||
placeholder="auto"
|
||||
onChange={this.onDecimalChange}
|
||||
value={decimals || ''}
|
||||
value={decimalsString}
|
||||
type="number"
|
||||
/>
|
||||
<FormField label="Prefix" labelWidth={labelWidth} onChange={this.onPrefixChange} value={prefix || ''} />
|
@ -1,5 +1,9 @@
|
||||
import { GaugePanelOptions } from './GaugePanelOptions';
|
||||
import { GaugePanel } from './GaugePanel';
|
||||
import { PanelDefaults } from './types';
|
||||
import { ReactPanelPlugin } from '@grafana/ui';
|
||||
|
||||
export { GaugePanel as Panel, GaugePanelOptions as PanelOptions, PanelDefaults };
|
||||
import { GaugePanelEditor } from './GaugePanelEditor';
|
||||
import { GaugePanel } from './GaugePanel';
|
||||
import { GaugeOptions, defaults } from './types';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel);
|
||||
reactPanel.setEditor(GaugePanelEditor);
|
||||
reactPanel.setDefaults(defaults);
|
||||
|
@ -1,29 +1,35 @@
|
||||
import { Threshold, ValueMapping } from '@grafana/ui';
|
||||
|
||||
export interface GaugeOptions {
|
||||
decimals: number;
|
||||
valueMappings: ValueMapping[];
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
prefix: string;
|
||||
showThresholdLabels: boolean;
|
||||
showThresholdMarkers: boolean;
|
||||
stat: string;
|
||||
suffix: string;
|
||||
thresholds: Threshold[];
|
||||
unit: string;
|
||||
valueOptions: SingleStatValueOptions;
|
||||
}
|
||||
|
||||
export const PanelDefaults: GaugeOptions = {
|
||||
export interface SingleStatValueOptions {
|
||||
unit: string;
|
||||
suffix: string;
|
||||
stat: string;
|
||||
prefix: string;
|
||||
decimals?: number | null;
|
||||
}
|
||||
|
||||
export const defaults: GaugeOptions = {
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
suffix: '',
|
||||
decimals: 0,
|
||||
stat: 'avg',
|
||||
unit: 'none',
|
||||
valueOptions: {
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
decimals: null,
|
||||
stat: 'avg',
|
||||
unit: 'none',
|
||||
},
|
||||
valueMappings: [],
|
||||
thresholds: [],
|
||||
};
|
||||
|
@ -3,10 +3,10 @@ import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Types
|
||||
import { PanelOptionsProps, Switch } from '@grafana/ui';
|
||||
import { PanelEditorProps, Switch } from '@grafana/ui';
|
||||
import { Options } from './types';
|
||||
|
||||
export class GraphPanelOptions extends PureComponent<PanelOptionsProps<Options>> {
|
||||
export class GraphPanelEditor extends PureComponent<PanelEditorProps<Options>> {
|
||||
onToggleLines = () => {
|
||||
this.props.onChange({ ...this.props.options, showLines: !this.props.options.showLines });
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { GraphPanel } from './GraphPanel';
|
||||
import { GraphPanelOptions } from './GraphPanelOptions';
|
||||
import { GraphPanelEditor } from './GraphPanelEditor';
|
||||
|
||||
export { GraphPanel as Panel, GraphPanelOptions as PanelOptions };
|
||||
export { GraphPanel as Panel, GraphPanelEditor as PanelOptions };
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { PanelProps } from '@grafana/ui';
|
||||
import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
|
||||
|
||||
export class Text2 extends PureComponent<PanelProps> {
|
||||
constructor(props: PanelProps) {
|
||||
@ -11,4 +11,4 @@ export class Text2 extends PureComponent<PanelProps> {
|
||||
}
|
||||
}
|
||||
|
||||
export { Text2 as Panel };
|
||||
export const reactPanel = new ReactPanelPlugin(Text2);
|
||||
|
Loading…
Reference in New Issue
Block a user