mirror of
https://github.com/grafana/grafana.git
synced 2025-12-20 19:44:55 +08:00
Compare commits
41 Commits
sriram/pos
...
v7.2.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8406a5d319 | ||
|
|
8d71561898 | ||
|
|
8269ed2407 | ||
|
|
4676ee9dcf | ||
|
|
35bdf11c0c | ||
|
|
46bb0e3754 | ||
|
|
521f31a702 | ||
|
|
460ccc326f | ||
|
|
eca1c505c7 | ||
|
|
b112b13c2d | ||
|
|
a4689e646a | ||
|
|
cfc13f7e30 | ||
|
|
7054834af0 | ||
|
|
48bae0232c | ||
|
|
3a32c8f329 | ||
|
|
b9d4653737 | ||
|
|
7220fb5ab8 | ||
|
|
f00222ad03 | ||
|
|
6033499576 | ||
|
|
1a598a8a41 | ||
|
|
4ca888c798 | ||
|
|
569e2a2c74 | ||
|
|
27bfb61d2b | ||
|
|
ac7112876b | ||
|
|
711a051725 | ||
|
|
c3afe89ea2 | ||
|
|
dc4b97966f | ||
|
|
4d9f298098 | ||
|
|
95a688a469 | ||
|
|
1f6d68b38e | ||
|
|
d842db21d2 | ||
|
|
adb6d93442 | ||
|
|
80a19f014c | ||
|
|
25c7090cda | ||
|
|
89dbb0f074 | ||
|
|
a2b97958bd | ||
|
|
619d985214 | ||
|
|
ee59974edb | ||
|
|
109754eeb4 | ||
|
|
2c6020a57a | ||
|
|
dfa808ea25 |
@@ -279,11 +279,11 @@ editors_can_admin = false
|
||||
# Login cookie name
|
||||
login_cookie_name = grafana_session
|
||||
|
||||
# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days.
|
||||
login_maximum_inactive_lifetime_days = 7
|
||||
# The maximum lifetime (duration) an authenticated user can be inactive before being required to login at next visit. Default is 7 days (7d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month). The lifetime resets at each successful token rotation (token_rotation_interval_minutes).
|
||||
login_maximum_inactive_lifetime_duration =
|
||||
|
||||
# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days.
|
||||
login_maximum_lifetime_days = 30
|
||||
# The maximum lifetime (duration) an authenticated user can be logged in since login time before being required to login. Default is 30 days (30d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month).
|
||||
login_maximum_lifetime_duration =
|
||||
|
||||
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
|
||||
token_rotation_interval_minutes = 10
|
||||
@@ -823,4 +823,5 @@ interval_year = YYYY
|
||||
# Experimental feature
|
||||
use_browser_locale = false
|
||||
|
||||
|
||||
# Default timezone for user preferences. Options are 'browser' for the browser local timezone or a timezone name from IANA Time Zone database, e.g. 'UTC' or 'Europe/Amsterdam' etc.
|
||||
default_timezone = browser
|
||||
|
||||
@@ -278,11 +278,11 @@
|
||||
# Login cookie name
|
||||
;login_cookie_name = grafana_session
|
||||
|
||||
# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days,
|
||||
;login_maximum_inactive_lifetime_days = 7
|
||||
# The maximum lifetime (duration) an authenticated user can be inactive before being required to login at next visit. Default is 7 days (7d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month). The lifetime resets at each successful token rotation
|
||||
;login_maximum_inactive_lifetime_duration =
|
||||
|
||||
# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days.
|
||||
;login_maximum_lifetime_days = 30
|
||||
# The maximum lifetime (duration) an authenticated user can be logged in since login time before being required to login. Default is 30 days (30d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month).
|
||||
;login_maximum_lifetime_duration =
|
||||
|
||||
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
|
||||
;token_rotation_interval_minutes = 10
|
||||
@@ -811,3 +811,6 @@
|
||||
|
||||
# Experimental feature
|
||||
;use_browser_locale = false
|
||||
|
||||
# Default timezone for user preferences. Options are 'browser' for the browser local timezone or a timezone name from IANA Time Zone database, e.g. 'UTC' or 'Europe/Amsterdam' etc.
|
||||
;default_timezone = browser
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
"id": 11,
|
||||
"options": {
|
||||
"content": "## Global variables\n\n* `__dashboard` = `${__dashboard}`\n* `__dashboard.name` = `${__dashboard.name}`\n* `__dashboard.uid` = `${__dashboard.uid}`\n* `__org.name` = `${__org.name}`\n* `__org.id` = `${__org.id}`\n* `__user.id` = `${__user.id}`\n* `__user.login` = `${__user.login}`\n \n## Formats\n\n* `Server:raw` = `${Server:raw}`\n* `Server:regex` = `${Server:regex}`\n* `Server:lucene` = `${Server:lucene}`\n* `Server:glob` = `${Server:glob}`\n* `Server:pipe` = `${Server:pipe}`\n* `Server:distributed` = `${Server:distributed}`\n* `Server:csv` = `${Server:csv}`\n* `Server:html` = `${Server:html}`\n* `Server:json` = `${Server:json}`\n* `Server:percentencode` = `${Server:percentencode}`\n* `Server:singlequote` = `${Server:singlequote}`\n* `Server:doublequote` = `${Server:doublequote}`\n* `Server:sqlstring` = `${Server:sqlstring}`\n* `Server:date` = `${Server:date}`\n\n",
|
||||
"content": "## Global variables\n\n* `__dashboard` = `${__dashboard}`\n* `__dashboard.name` = `${__dashboard.name}`\n* `__dashboard.uid` = `${__dashboard.uid}`\n* `__org.name` = `${__org.name}`\n* `__org.id` = `${__org.id}`\n* `__user.id` = `${__user.id}`\n* `__user.login` = `${__user.login}`\n \n## Formats\n\n* `Server:raw` = `${Server:raw}`\n* `Server:regex` = `${Server:regex}`\n* `Server:lucene` = `${Server:lucene}`\n* `Server:glob` = `${Server:glob}`\n* `Server:pipe` = `${Server:pipe}`\n* `Server:distributed` = `${Server:distributed}`\n* `Server:csv` = `${Server:csv}`\n* `Server:html` = `${Server:html}`\n* `Server:json` = `${Server:json}`\n* `Server:percentencode` = `${Server:percentencode}`\n* `Server:singlequote` = `${Server:singlequote}`\n* `Server:doublequote` = `${Server:doublequote}`\n* `Server:sqlstring` = `${Server:sqlstring}`\n* `Server:date` = `${Server:date}`\n* `Server:text` = `${Server:text}`\n\n",
|
||||
"mode": "markdown"
|
||||
},
|
||||
"pluginVersion": "7.1.0",
|
||||
|
||||
@@ -1391,7 +1391,7 @@ For more information about Grafana Enterprise, refer to [Grafana Enterprise]({{<
|
||||
|
||||
### enable
|
||||
|
||||
Keys of alpha features to enable, separated by space. Available alpha features are: `transformations`, `standaloneAlerts`
|
||||
Keys of alpha features to enable, separated by space. Available alpha features are: `transformations`
|
||||
|
||||
## [date_formats]
|
||||
|
||||
@@ -1426,3 +1426,6 @@ interval_year = YYYY
|
||||
Set this to `true` to have date formats be automatically be derived from browser locale. Defaults to `false`. This
|
||||
is an experimental feature right now with a few problems that remain unsolved.
|
||||
|
||||
### default_timezone
|
||||
|
||||
Used as the default timezone for user preferences. Can be either `browser` for the browser local timezone or a timezone name from IANA Time Zone database, e.g. `UTC` or `Europe/Amsterdam` etc.
|
||||
@@ -32,7 +32,7 @@ Your Grafana preferences include whether uses the dark or light theme, your home
|
||||
1. In the Preferences section, you can edit any of the following:
|
||||
- **UI Theme -** Click to set the **Dark** or **Light** to select a theme. **Default** is either the dark theme or the theme selected by your Grafana administrator.
|
||||
- **Home Dashboard -** Refer to [Set your personal home dashboard]({{< relref "change-home-dashboard.md#set-your-personal-home-dashboard" >}}) for more information.
|
||||
- **Timezone -** Click to select an option in the **Timezone** list. Refer to [Time range controls]({{< relref "../dashboards/time-range-controls.md" >}}) for more information about Grafana time settings.
|
||||
- **Timezone -** Click to select an option in the **Timezone** list. **Default** is either the browser local timezone or the timezone selected by your Grafana administrator. Refer to [Time range controls]({{< relref "../dashboards/time-range-controls.md" >}}) for more information about Grafana time settings.
|
||||
1. Click **Save**.
|
||||
|
||||
## View your assigned organizations
|
||||
|
||||
@@ -59,11 +59,13 @@ Example:
|
||||
# Login cookie name
|
||||
login_cookie_name = grafana_session
|
||||
|
||||
# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days.
|
||||
login_maximum_inactive_lifetime_days = 7
|
||||
|
||||
# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days.
|
||||
login_maximum_lifetime_days = 30
|
||||
# The maximum lifetime (duration) an authenticated user can be inactive before being required to login at next visit. Default is 7 days (7d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month). The lifetime resets at each successful token rotation (token_rotation_interval_minutes).
|
||||
login_maximum_inactive_lifetime_duration =
|
||||
|
||||
|
||||
# The maximum lifetime (duration) an authenticated user can be logged in since login time before being required to login. Default is 30 days (30d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month).
|
||||
login_maximum_lifetime_duration =
|
||||
|
||||
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
|
||||
token_rotation_interval_minutes = 10
|
||||
|
||||
@@ -212,7 +212,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
`GET /api/orgs/name/:orgName`
|
||||
|
||||
Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api).
|
||||
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
@@ -463,7 +463,7 @@ Content-Type: application/json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"User added to organization"}
|
||||
{"message":"User added to organization", "userId": 1}
|
||||
```
|
||||
|
||||
### Update Users in Organization
|
||||
|
||||
@@ -8,23 +8,19 @@ weight = 300
|
||||
|
||||
# Field configuration options
|
||||
|
||||
This page explains what field configurations and field overrides in Grafana are and how to use them. It also includes [examples](#examples) if you need an idea of how this feature might be useful in the real world.
|
||||
This page explains what field options and field overrides in Grafana are and how to use them. It also includes
|
||||
[examples](#examples) if you need an idea of how this feature might be useful in the real world.
|
||||
|
||||
> **Note:** This documentation refers to a Grafana 7.0 beta feature. This documentation will be frequently updated to reflect updates to the feature, and it will probably be broken into smaller sections when the feature moves out of beta.
|
||||
|
||||
The data model behind Grafana, the [data frame]({{< relref "../developers/plugins/data-frames.md" >}}), is a columnar-oriented table structure.
|
||||
|
||||
Each column within this structure is called a _field_. Grafana allows to customize how a particular field is displayed in the visualization.
|
||||
The data model used in Grafana, the [data frame]({{< relref "../developers/plugins/data-frames.md" >}}),
|
||||
is a columnar-oriented table structure that unifies both time series and table query results. Each column within this structure is called a _field_. A field can represent a single time series or table column.
|
||||
|
||||
## Field configuration options and overrides
|
||||
|
||||
Field configuration options allow you to change how the data is displayed in your visualizations. Options and overrides that you apply do not change the data, they change how Grafana displays the data.
|
||||
|
||||
> **Note:** The time fields are not affected by field configuration options or overrides.
|
||||
_Field configuration options_, both standard and custom, can be found in the **Field** tab in the panel editor. Changes on this tab apply to all fields (i.e. series/columns). For example, if you change the unit to percentage, then all fields with numeric values are displayed in percentages. [Apply a field option](#configure-all-fields).
|
||||
|
||||
_Field configuration options_, both standard and custom, are applied in the **Field** tab in the panel editor. Changes on this panel apply to all fields in the visualization. For example, if you change the unit to percentage, then all fields with numeric values will be displayed in percentages. [Apply a field option](#configure-all-fields).
|
||||
|
||||
_Field overrides_ are applied in the _Overrides_ tab in the panel editor. They are exactly the same as field configuration options except that they only change fields you select. The current feature only allows you to change one field at a time, but future improvements will offer more flexibility. [Apply an override](#override-a-field).
|
||||
_Field overrides_ can be added in the **Overrides** tab in the panel editor. There you can add the same options as you find in the **Field** tab, but they are only applied to specific fields. [Apply an override](#override-a-field).
|
||||
|
||||
All [field options](#field-options) are defined below.
|
||||
|
||||
@@ -42,26 +38,16 @@ Standard field options are:
|
||||
- [Value mappings](#value-mapping)
|
||||
- [Data links](#data-links)
|
||||
|
||||
You can apply standard field options to the following panel visualizations:
|
||||
|
||||
- [Bar gauge]({{< relref "visualizations/bar-gauge-panel.md" >}})
|
||||
- [Gauge]({{< relref "visualizations/gauge-panel.md" >}})
|
||||
- [Stat]({{< relref "visualizations/stat-panel.md" >}})
|
||||
- [Table]({{< relref "visualizations/table-panel.md" >}})
|
||||
You can apply standard field options to most of the built-in Grafana panels. Some older panels and community panels that have yet to update to the new panel and data model will be missing either all or some of these field options.
|
||||
|
||||
### Custom field options
|
||||
|
||||
You can only apply custom field options to table visualizations. Plugin authors can add their own custom field options as well, and they might differ across visualizations.
|
||||
|
||||
Custom field options are:
|
||||
|
||||
- [Column width](#column-width)
|
||||
- [Column alignment](#column-alignment)
|
||||
- [Cell display mode](#cell-display-mode)
|
||||
Some visualizations have custom field options. For example the [Table]({{< relref "visualizations/table-panel.md" >}}) visualization has many custom field options. Community panels can add their own custom field options as well, and they might differ across visualizations.
|
||||
|
||||
## Configure all fields
|
||||
|
||||
To change how all fields display data, you apply a [field option](#field-options). Usually you apply changes that you want to most of or all of the fields here, rather than applying field overrides to exceptions.
|
||||
To change how all fields display data, you change an option in the **Field** tab. In the **Overrides** tab
|
||||
you then override that for specific fields.
|
||||
|
||||
1. Navigate to the panel you want to edit, click the panel title, and then click **Edit**.
|
||||
1. Click the **Field** tab.
|
||||
@@ -69,9 +55,9 @@ To change how all fields display data, you apply a [field option](#field-options
|
||||
1. Enter options by adding values in the fields. To return options to default values, delete the white text in the fields.
|
||||
1. When finished, click **Save** to save all panel edits to the dashboard.
|
||||
|
||||
## Override a field
|
||||
## Configure specific fields with overrides
|
||||
|
||||
Field overrides allow you to change the settings for one field (column in tables) to be different than the others. Field options for overrides are exactly the same as the field options available in a particular visualization. The only difference is that you choose which field to apply them to.
|
||||
Overrides allow you to change the settings for one or more fields (i.e. series or column). What fields are targeted by the override depends on the matcher. Field options for overrides are exactly the same as the field options available in a particular visualization. The only difference is that you choose which fields to apply them to.
|
||||
|
||||
1. Navigate to the panel you want to edit, click the panel title, and then click **Edit**.
|
||||
1. Click the **Overrides** tab.
|
||||
@@ -83,21 +69,21 @@ Field overrides allow you to change the settings for one field (column in tables
|
||||
1. Continue to add overrides to this field by clicking **Add override property**, or you can click **Add override** and select a different field to add overrides to.
|
||||
1. When finished, click **Save** to save all panel edits to the dashboard.
|
||||
|
||||
## Filter options
|
||||
## Select fields
|
||||
|
||||
This section explains all available filter options for field overrides. They are listed in alphabetical order.
|
||||
This section explains the different ways you can select which fields an override rule will be applied to.
|
||||
|
||||
### Filter field by name
|
||||
### Fields with name
|
||||
|
||||
Allows you to select a field from the list of all available fields that the override will be applied to.
|
||||
Allows you to select a field from the list of all available fields. Properties you add to a rule with this selector will only be applied to this single field.
|
||||
|
||||
### Filter field by name using regex
|
||||
### Fields with name matching regex
|
||||
|
||||
Allows you to type in a regular expression against which fields to be overridden will be matched.
|
||||
Allows you to specify a regular expression. Properties you add to a rule with this selector will be applied to all fields where the field name match the regex.
|
||||
|
||||
### Filter field by type
|
||||
### Fields with type
|
||||
|
||||
Allows you to select fields by their type (string, numeric, etc).
|
||||
Allows you to select fields by their type (string, numeric, etc). Properties you add to a rule with this selector will be applied to all fields of matching type.
|
||||
|
||||
## Field options
|
||||
|
||||
@@ -105,37 +91,6 @@ This section explains all available field options. They are listed in alphabetic
|
||||
|
||||
Most field options will not affect the visualization until you click outside of the field option box you are editing or press Enter.
|
||||
|
||||
### Cell display mode
|
||||
|
||||
This custom field option applies only to table visualizations.
|
||||
|
||||
By default, Grafana automatically chooses display settings. You can override the settings by choosing one of the following options to change all fields.
|
||||
|
||||
- **Color text -** If thresholds are set, then the field text is displayed in the appropriate threshold color.
|
||||
- **Color background -** If thresholds are set, then the field background is displayed in the appropriate threshold color.
|
||||
- **Gradient gauge -** The threshold levels define a gradient.
|
||||
- **LCD gauge -** The gauge is split up in small cells that are lit or unlit.
|
||||
- **JSON view -** Shows value formatted as code. If a value is an object the JSON view allowing browsing the JSON object will appear on hover
|
||||
|
||||
### Column alignment
|
||||
|
||||
This custom field option applies only to table visualizations.
|
||||
|
||||
Choose how Grafana should align cell contents:
|
||||
|
||||
- Auto (default)
|
||||
- Left
|
||||
- Center
|
||||
- Right
|
||||
|
||||
### Column width
|
||||
|
||||
This custom field option applies only to table visualizations.
|
||||
|
||||
By default, Grafana automatically calculates the column width based on the cell contents. In this field option, can override the setting and define the width for all columns in pixels.
|
||||
|
||||
For example, if you enter `100` in the field, then when you click outside the field, all the columns will be set to 100 pixels wide.
|
||||
|
||||
### Decimals
|
||||
|
||||
Number of decimals to render value with. Leave empty for Grafana to use the number of decimals provided by the data source.
|
||||
|
||||
@@ -45,8 +45,8 @@ Transformations are available from the Transform tab in the bottom pane of the p
|
||||
|
||||
1. Navigate to the panel that you want to add transformations, click the panel title and then click **Edit**.
|
||||
1. Click the **Transform** tab.
|
||||
1. Click a transformation to select it.
|
||||
|
||||
1. Click a transformation to select it.
|
||||
|
||||
A transformation row appears that allows you to configure the transformation options.
|
||||
|
||||
Click **Add transformation** to apply another transformation. Keep in mind that the next transformation acts on the result set returned by the previous transformation.
|
||||
@@ -107,25 +107,24 @@ In the example below, we have two queries returning table data. It is visualized
|
||||
Query A:
|
||||
|
||||
| Time | Job | Uptime |
|
||||
|---------------------|---------|-----------|
|
||||
| ------------------- | ------- | --------- |
|
||||
| 2020-07-07 11:34:20 | node | 25260122 |
|
||||
| 2020-07-07 11:24:20 | postgre | 123001233 |
|
||||
|
||||
Query B:
|
||||
|
||||
| Time | Job | Errors |
|
||||
|---------------------|---------|--------|
|
||||
| ------------------- | ------- | ------ |
|
||||
| 2020-07-07 11:34:20 | node | 15 |
|
||||
| 2020-07-07 11:24:20 | postgre | 5 |
|
||||
|
||||
Here is the result after applying the `Merge` transformation.
|
||||
|
||||
| Time | Job | Errors | Uptime |
|
||||
|---------------------|---------|--------|-----------|
|
||||
| ------------------- | ------- | ------ | --------- |
|
||||
| 2020-07-07 11:34:20 | node | 15 | 25260122 |
|
||||
| 2020-07-07 11:24:20 | postgre | 5 | 123001233 |
|
||||
|
||||
|
||||
### Filter by name
|
||||
|
||||
Use this transformation to remove portions of the query results.
|
||||
@@ -170,7 +169,7 @@ Use this transformation to rename, reorder, or hide fields returned by the query
|
||||
Grafana displays a list of fields returned by the query. You can:
|
||||
|
||||
- Change field order by hovering your cursor over a field. The cursor turns into a hand and then you can drag the field to its new place.
|
||||
- Hide or show a field by clicking the eye icon next to the field name.
|
||||
- Hide or show a field by clicking the eye icon next to the field name.
|
||||
- Rename fields by typing a new name in the **Rename <field>** box.
|
||||
|
||||
In the example below, I hid the value field and renamed Max and Min.
|
||||
@@ -209,12 +208,30 @@ In the example below, I added two fields together and named them Sum.
|
||||
|
||||
### Labels to fields
|
||||
|
||||
Use this transformation to group series by time and return labels or tags as fields.
|
||||
|
||||
> **Note:** In order to apply this transformation, you must have a query to a data source that returns labeled fields.
|
||||
> **Note:** In order to apply this transformation, your query needs to returns labeled fields.
|
||||
|
||||
When you select this transformation, Grafana automatically transforms all labeled data into fields.
|
||||
|
||||
Example: Given a query result of two time series
|
||||
|
||||
1: labels Server=Server A, Datacenter=EU
|
||||
2: labels Server=Server B, Datacenter=EU
|
||||
|
||||
This would result in a table like this
|
||||
|
||||
| Time | Server | Datacenter | Value |
|
||||
| ------------------- | -------- | ---------- | ----- |
|
||||
| 2020-07-07 11:34:20 | Server A | EU | 1 |
|
||||
| 2020-07-07 11:34:20 | Server B | EU | 2 |
|
||||
|
||||
**Value field name**
|
||||
If you where to select `Server` as in the **Value field name** you would get one field for every value of the `Server`
|
||||
label.
|
||||
|
||||
| Time | Datacenter | Server A | Server B |
|
||||
| ------------------- | ---------- | -------- | -------- |
|
||||
| 2020-07-07 11:34:20 | EU | 1 | 2 |
|
||||
|
||||
For this example, I manually defined labels in the Random Walk visualization of TestData DB.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/transformations/labels-to-fields-before-7-0.png" class="docs-image--no-shadow" max-width= "1100px" >}}
|
||||
@@ -223,7 +240,6 @@ After I apply the transformation, my labels appear in the table as fields.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/transformations/labels-to-fields-after-7-0.png" class="docs-image--no-shadow" max-width= "1100px" >}}
|
||||
|
||||
|
||||
### Group By
|
||||
|
||||
> **Note:** This transformation is only available in Grafana 7.2+.
|
||||
@@ -232,57 +248,58 @@ This transformation groups the data by a specified field (column) value and proc
|
||||
|
||||
Here's an example of original data.
|
||||
|
||||
| Time | Server ID | CPU Temperature | Server Status
|
||||
|---------------------|-------------|-----------------|----------
|
||||
| 2020-07-07 11:34:20 | server 1 | 80 | Shutdown
|
||||
| 2020-07-07 11:34:20 | server 3 | 62 | OK
|
||||
| 2020-07-07 10:32:20 | server 2 | 90 | Overload
|
||||
| 2020-07-07 10:31:22 | server 3 | 55 | OK
|
||||
| 2020-07-07 09:30:57 | server 3 | 62 | Rebooting
|
||||
| 2020-07-07 09:30:05 | server 2 | 88 | OK
|
||||
| 2020-07-07 09:28:06 | server 1 | 80 | OK
|
||||
| 2020-07-07 09:25:05 | server 2 | 88 | OK
|
||||
| 2020-07-07 09:23:07 | server 1 | 86 | OK
|
||||
| Time | Server ID | CPU Temperature | Server Status |
|
||||
| ------------------- | --------- | --------------- | ------------- |
|
||||
| 2020-07-07 11:34:20 | server 1 | 80 | Shutdown |
|
||||
| 2020-07-07 11:34:20 | server 3 | 62 | OK |
|
||||
| 2020-07-07 10:32:20 | server 2 | 90 | Overload |
|
||||
| 2020-07-07 10:31:22 | server 3 | 55 | OK |
|
||||
| 2020-07-07 09:30:57 | server 3 | 62 | Rebooting |
|
||||
| 2020-07-07 09:30:05 | server 2 | 88 | OK |
|
||||
| 2020-07-07 09:28:06 | server 1 | 80 | OK |
|
||||
| 2020-07-07 09:25:05 | server 2 | 88 | OK |
|
||||
| 2020-07-07 09:23:07 | server 1 | 86 | OK |
|
||||
|
||||
This transformation goes in two steps. First you specify one or multiple fields to group the data by. This will group all the same values of those fields together, as if you sorted them. For instance if we `Group By` the `Server ID` field, it would group the data this way:
|
||||
|
||||
| Time | Server ID | CPU Temperature | Server Status
|
||||
|---------------------|-------------|-----------------|----------
|
||||
| 2020-07-07 11:34:20 | **server 1** | 80 | Shutdown
|
||||
| 2020-07-07 09:28:06 | **server 1** | 80 | OK
|
||||
| 2020-07-07 09:23:07 | **server 1** | 86 | OK
|
||||
| Time | Server ID | CPU Temperature | Server Status |
|
||||
| ------------------- | ------------ | --------------- | ------------- |
|
||||
| 2020-07-07 11:34:20 | **server 1** | 80 | Shutdown |
|
||||
| 2020-07-07 09:28:06 | **server 1** | 80 | OK |
|
||||
| 2020-07-07 09:23:07 | **server 1** | 86 | OK |
|
||||
|
||||
|
|
||||
| 2020-07-07 10:32:20 | server 2 | 90 | Overload
|
||||
| 2020-07-07 09:30:05 | server 2 | 88 | OK
|
||||
| 2020-07-07 09:25:05 | server 2 | 88 | OK
|
||||
| 2020-07-07 10:32:20 | server 2 | 90 | Overload
|
||||
| 2020-07-07 09:30:05 | server 2 | 88 | OK
|
||||
| 2020-07-07 09:25:05 | server 2 | 88 | OK
|
||||
|
|
||||
| 2020-07-07 11:34:20 | ***server 3*** | 62 | OK
|
||||
| 2020-07-07 10:31:22 | ***server 3*** | 55 | OK
|
||||
| 2020-07-07 09:30:57 | ***server 3*** | 62 | Rebooting
|
||||
| 2020-07-07 11:34:20 | **_server 3_** | 62 | OK
|
||||
| 2020-07-07 10:31:22 | **_server 3_** | 55 | OK
|
||||
| 2020-07-07 09:30:57 | **_server 3_** | 62 | Rebooting
|
||||
|
||||
All rows with the same value of `Server ID` are grouped together.
|
||||
|
||||
After choosing which field you want to group your data by, you can add various calculations on the other fields, and the calculation will be applied on each group of rows. For instance, we could want to calculate the average `CPU temperature` for each of those servers. So we can add the _mean_ calculation applied on the `CPU Temperature` field to get the following:
|
||||
|
||||
| Server ID | CPU Temperature (mean)
|
||||
|-----------|--------------------------
|
||||
| server 1 | 82
|
||||
| server 2 | 88.6
|
||||
| server 3 | 59.6
|
||||
| Server ID | CPU Temperature (mean) |
|
||||
| --------- | ---------------------- |
|
||||
| server 1 | 82 |
|
||||
| server 2 | 88.6 |
|
||||
| server 3 | 59.6 |
|
||||
|
||||
And we can add more than one of those calculation. For instance :
|
||||
|
||||
- For field `Time`, we can calculate the *Last* value, to know when the last data point was received for each server
|
||||
- For field `Server Status`, we can calculate the *Last* value to know what is the last state value for each server
|
||||
- For field `Temperature`, we can also calculate the *Last* value to know what is the latest monitored temperature for each server
|
||||
- For field `Time`, we can calculate the _Last_ value, to know when the last data point was received for each server
|
||||
- For field `Server Status`, we can calculate the _Last_ value to know what is the last state value for each server
|
||||
- For field `Temperature`, we can also calculate the _Last_ value to know what is the latest monitored temperature for each server
|
||||
|
||||
We would then get :
|
||||
|
||||
| Server ID | CPU Temperature (mean) | CPU Temperature (last) | Time (last) | Server Status (last)
|
||||
|-----------|-------------------------- |------------------------|------------------|----------------------
|
||||
| server 1 | 82 | 80 | 2020-07-07 11:34:20 | Shutdown
|
||||
| server 2 | 88.6 | 90 | 2020-07-07 10:32:20 | Overload
|
||||
| server 3 | 59.6 | 62 | 2020-07-07 11:34:20 | OK
|
||||
| Server ID | CPU Temperature (mean) | CPU Temperature (last) | Time (last) | Server Status (last) |
|
||||
| --------- | ---------------------- | ---------------------- | ------------------- | -------------------- |
|
||||
| server 1 | 82 | 80 | 2020-07-07 11:34:20 | Shutdown |
|
||||
| server 2 | 88.6 | 90 | 2020-07-07 10:32:20 | Overload |
|
||||
| server 3 | 59.6 | 62 | 2020-07-07 11:34:20 | OK |
|
||||
|
||||
This transformation allows you to extract some key information out of your time series and display them in a convenient way.
|
||||
|
||||
@@ -290,7 +307,7 @@ This transformation allows you to extract some key information out of your time
|
||||
|
||||
> **Note:** This transformation is only available in Grafana 7.1+.
|
||||
|
||||
Use this transformation to combine the result from multiple time series data queries into one single result. This is helpful when using the table panel visualization.
|
||||
Use this transformation to combine the result from multiple time series data queries into one single result. This is helpful when using the table panel visualization.
|
||||
|
||||
The result from this transformation will contain three columns: `Time`, `Metric`, and `Value`. The `Metric` column is added so you easily can see from which query the metric originates from. Customize this value by defining `Label` on the source query.
|
||||
|
||||
@@ -299,7 +316,7 @@ In the example below, we have two queries returning time series data. It is visu
|
||||
Query A:
|
||||
|
||||
| Time | Temperature |
|
||||
|---------------------|-------------|
|
||||
| ------------------- | ----------- |
|
||||
| 2020-07-07 11:34:20 | 25 |
|
||||
| 2020-07-07 10:31:22 | 22 |
|
||||
| 2020-07-07 09:30:05 | 19 |
|
||||
@@ -307,7 +324,7 @@ Query A:
|
||||
Query B:
|
||||
|
||||
| Time | Humidity |
|
||||
|---------------------|----------|
|
||||
| ------------------- | -------- |
|
||||
| 2020-07-07 11:34:20 | 24 |
|
||||
| 2020-07-07 10:32:20 | 29 |
|
||||
| 2020-07-07 09:30:57 | 33 |
|
||||
@@ -315,7 +332,7 @@ Query B:
|
||||
Here is the result after applying the `Series to rows` transformation.
|
||||
|
||||
| Time | Metric | Value |
|
||||
|---------------------|-------------|-------|
|
||||
| ------------------- | ----------- | ----- |
|
||||
| 2020-07-07 11:34:20 | Temperature | 25 |
|
||||
| 2020-07-07 11:34:20 | Humidity | 22 |
|
||||
| 2020-07-07 10:32:20 | Humidity | 29 |
|
||||
|
||||
@@ -27,6 +27,43 @@ Table visualizations allow you to apply:
|
||||
|
||||
- **Show header -** Show or hide column names imported from your data source..
|
||||
|
||||
### Custom field options
|
||||
|
||||
- [Column width](#column-width)
|
||||
- [Column alignment](#column-alignment)
|
||||
- [Cell display mode](#cell-display-mode)
|
||||
|
||||
### Column alignment
|
||||
|
||||
This custom field option applies only to table visualizations.
|
||||
|
||||
Choose how Grafana should align cell contents:
|
||||
|
||||
- Auto (default)
|
||||
- Left
|
||||
- Center
|
||||
- Right
|
||||
|
||||
### Column width
|
||||
|
||||
This custom field option applies only to table visualizations.
|
||||
|
||||
By default, Grafana automatically calculates the column width based on the cell contents. In this field option, can override the setting and define the width for all columns in pixels.
|
||||
|
||||
For example, if you enter `100` in the field, then when you click outside the field, all the columns will be set to 100 pixels wide.
|
||||
|
||||
#### Cell display mode
|
||||
|
||||
This custom field option applies only to table visualizations.
|
||||
|
||||
By default, Grafana automatically chooses display settings. You can override the settings by choosing one of the following options to change all fields.
|
||||
|
||||
- **Color text -** If thresholds are set, then the field text is displayed in the appropriate threshold color.
|
||||
- **Color background -** If thresholds are set, then the field background is displayed in the appropriate threshold color.
|
||||
- **Gradient gauge -** The threshold levels define a gradient.
|
||||
- **LCD gauge -** The gauge is split up in small cells that are lit or unlit.
|
||||
- **JSON view -** Shows value formatted as code. If a value is an object the JSON view allowing browsing the JSON object will appear on hover
|
||||
|
||||
## Tips
|
||||
|
||||
### Display original string value
|
||||
|
||||
@@ -143,3 +143,13 @@ servers = ["test'1", "test2"]
|
||||
String to interpolate: '${servers:sqlstring}'
|
||||
Interpolation result: "'test''1','test2'"
|
||||
```
|
||||
|
||||
## Text
|
||||
|
||||
Formats single- and multi-valued variables into their text representation. For a single variable it will just return the text representation. For multi-valued variables it will return the text representation combined with `+`.
|
||||
|
||||
```bash
|
||||
servers = ["test1", "test2"]
|
||||
String to interpolate: '${servers:text}'
|
||||
Interpolation result: "test1 + test2"
|
||||
```
|
||||
@@ -33,11 +33,12 @@ e2e.scenario({
|
||||
`Server:doublequote = "A'A\\"A","BB\\B","CCC"`,
|
||||
`Server:sqlstring = 'A''A"A','BB\\\B','CCC'`,
|
||||
`Server:date = null`,
|
||||
`Server:text = All`,
|
||||
];
|
||||
|
||||
e2e()
|
||||
.get('.markdown-html li')
|
||||
.should('have.length', 21)
|
||||
.should('have.length', 22)
|
||||
.each(element => {
|
||||
items.push(element.text());
|
||||
})
|
||||
|
||||
@@ -75,44 +75,6 @@ e2e.scenario({
|
||||
|
||||
e2e().wait('@apiPostQuery');
|
||||
|
||||
// Change order or query rows
|
||||
// Check the order of the rows before
|
||||
e2e.components.QueryEditorRows.rows()
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
e2e.components.QueryEditorRow.title('B').should('be.visible');
|
||||
});
|
||||
|
||||
e2e.components.QueryEditorRows.rows()
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
e2e.components.QueryEditorRow.title('A').should('be.visible');
|
||||
});
|
||||
|
||||
// Change so A is first
|
||||
e2e.components.QueryEditorRow.actionButton('Move query up')
|
||||
.eq(1)
|
||||
.click();
|
||||
|
||||
e2e().wait('@apiPostQuery');
|
||||
|
||||
// Avoid flaky tests
|
||||
// Maybe the virtual dom performs optimzations such as node position swapping, meaning 1 becomes 0 and it gets that element before the change because and never finds title 'A'
|
||||
e2e().wait(250);
|
||||
|
||||
// Check the order of the rows after change
|
||||
e2e.components.QueryEditorRows.rows()
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
e2e.components.QueryEditorRow.title('A').should('be.visible');
|
||||
});
|
||||
|
||||
e2e.components.QueryEditorRows.rows()
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
e2e.components.QueryEditorRow.title('B').should('be.visible');
|
||||
});
|
||||
|
||||
// Disable / enable row
|
||||
expectInspectorResultAndClose(keys => {
|
||||
const length = keys.length;
|
||||
@@ -120,7 +82,7 @@ e2e.scenario({
|
||||
expect(keys[length - 1].innerText).equals('B:');
|
||||
});
|
||||
|
||||
// Disable row with refId B
|
||||
// Disable row with refId A
|
||||
e2e.components.QueryEditorRow.actionButton('Disable/enable query')
|
||||
.eq(1)
|
||||
.should('be.visible')
|
||||
@@ -130,7 +92,7 @@ e2e.scenario({
|
||||
|
||||
expectInspectorResultAndClose(keys => {
|
||||
const length = keys.length;
|
||||
expect(keys[length - 1].innerText).equals('A:');
|
||||
expect(keys[length - 1].innerText).equals('B:');
|
||||
});
|
||||
|
||||
// Enable row with refId B
|
||||
|
||||
17
e2e/suite1/specs/solo-route.spec.ts
Normal file
17
e2e/suite1/specs/solo-route.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
e2e.scenario({
|
||||
describeName: 'Solo Route',
|
||||
itName: 'Can view panels with shared queries in fullsceen',
|
||||
addScenarioDataSource: false,
|
||||
addScenarioDashBoard: false,
|
||||
skipScenario: false,
|
||||
scenario: () => {
|
||||
// open Panel Tests - Bar Gauge
|
||||
e2e.pages.SoloPanel.visit('ZqZnVvFZz/datasource-tests-shared-queries?orgId=1&panelId=4');
|
||||
|
||||
e2e()
|
||||
.get('canvas')
|
||||
.should('have.length', 6);
|
||||
},
|
||||
});
|
||||
@@ -2,5 +2,5 @@
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"packages": ["packages/*"],
|
||||
"version": "7.2.0-pre.0"
|
||||
"version": "7.2.0-beta.2"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"name": "grafana",
|
||||
"version": "7.2.0-beta1",
|
||||
"version": "7.2.0-beta2",
|
||||
"repository": "github:grafana/grafana",
|
||||
"scripts": {
|
||||
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/data",
|
||||
"version": "7.2.0-pre.0",
|
||||
"version": "7.2.0-beta.2",
|
||||
"description": "Grafana Data Library",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
|
||||
@@ -317,7 +317,7 @@ export class PanelPlugin<TOptions = any, TFieldConfigOptions extends object = an
|
||||
|
||||
for (const customProp of builder.getRegistry().list()) {
|
||||
customProp.isCustom = true;
|
||||
customProp.category = ['Custom options'].concat(customProp.category || []);
|
||||
customProp.category = [`${this.meta.name} options`].concat(customProp.category || []);
|
||||
// need to do something to make the custom items not conflict with standard ones
|
||||
// problem is id (registry index) is used as property path
|
||||
// so sort of need a property path on the FieldPropertyEditorItem
|
||||
|
||||
@@ -50,7 +50,16 @@ describe('Labels as Columns', () => {
|
||||
name: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [1000, 2000] },
|
||||
{ name: 'Value', type: FieldType.number, values: [1, 2], labels: { location: 'inside', name: 'Request' } },
|
||||
{
|
||||
name: 'Value',
|
||||
type: FieldType.number,
|
||||
values: [1, 2],
|
||||
labels: { location: 'inside', name: 'Request' },
|
||||
config: {
|
||||
displayName: 'Custom1',
|
||||
displayNameFromDS: 'Custom2',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -51,7 +51,9 @@ export const labelsToFieldsTransformer: DataTransformerInfo<LabelsToFieldsOption
|
||||
name,
|
||||
config: {
|
||||
...field.config,
|
||||
// we need to clear thes for this transform as these can contain label names that we no longer want
|
||||
displayName: undefined,
|
||||
displayNameFromDS: undefined,
|
||||
},
|
||||
labels: undefined,
|
||||
});
|
||||
|
||||
87
packages/grafana-data/src/types/annotations.ts
Normal file
87
packages/grafana-data/src/types/annotations.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { DataQuery, QueryEditorProps } from './datasource';
|
||||
import { DataFrame } from './dataFrame';
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
/**
|
||||
* This JSON object is stored in the dashboard json model.
|
||||
*/
|
||||
export interface AnnotationQuery<TQuery extends DataQuery = DataQuery> {
|
||||
datasource: string;
|
||||
enable: boolean;
|
||||
name: string;
|
||||
iconColor: string;
|
||||
|
||||
// Standard datasource query
|
||||
target?: TQuery;
|
||||
|
||||
// Convert a dataframe to an AnnotationEvent
|
||||
mappings?: AnnotationEventMappings;
|
||||
}
|
||||
|
||||
export interface AnnotationEvent {
|
||||
id?: string;
|
||||
annotation?: any;
|
||||
dashboardId?: number;
|
||||
panelId?: number;
|
||||
userId?: number;
|
||||
login?: string;
|
||||
email?: string;
|
||||
avatarUrl?: string;
|
||||
time?: number;
|
||||
timeEnd?: number;
|
||||
isRegion?: boolean;
|
||||
title?: string;
|
||||
text?: string;
|
||||
type?: string;
|
||||
tags?: string[];
|
||||
|
||||
// Currently used to merge annotations from alerts and dashboard
|
||||
source?: any; // source.type === 'dashboard'
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha -- any value other than `field` is experimental
|
||||
*/
|
||||
export enum AnnotationEventFieldSource {
|
||||
Field = 'field', // Default -- find the value with a matching key
|
||||
Text = 'text', // Write a constant string into the value
|
||||
Skip = 'skip', // Do not include the field
|
||||
}
|
||||
|
||||
export interface AnnotationEventFieldMapping {
|
||||
source?: AnnotationEventFieldSource; // defautls to 'field'
|
||||
value?: string;
|
||||
regex?: string;
|
||||
}
|
||||
|
||||
export type AnnotationEventMappings = Partial<Record<keyof AnnotationEvent, AnnotationEventFieldMapping>>;
|
||||
|
||||
/**
|
||||
* Since Grafana 7.2
|
||||
*
|
||||
* This offers a generic approach to annotation processing
|
||||
*/
|
||||
export interface AnnotationSupport<TQuery extends DataQuery = DataQuery, TAnno = AnnotationQuery<TQuery>> {
|
||||
/**
|
||||
* This hook lets you manipulate any existing stored values before running them though the processor.
|
||||
* This is particularly helpful when dealing with migrating old formats. ie query as a string vs object
|
||||
*/
|
||||
prepareAnnotation?(json: any): TAnno;
|
||||
|
||||
/**
|
||||
* Convert the stored JSON model to a standard datasource query object.
|
||||
* This query will be executed in the datasource and the results converted into events.
|
||||
* Returning an undefined result will quietly skip query execution
|
||||
*/
|
||||
prepareQuery?(anno: TAnno): TQuery | undefined;
|
||||
|
||||
/**
|
||||
* When the standard frame > event processing is insufficient, this allows explicit control of the mappings
|
||||
*/
|
||||
processEvents?(anno: TAnno, data: DataFrame[]): AnnotationEvent[] | undefined;
|
||||
|
||||
/**
|
||||
* Specify a custom QueryEditor for the annotation page. If not specified, the standard one will be used
|
||||
*/
|
||||
QueryEditor?: ComponentType<QueryEditorProps<any, TQuery>>;
|
||||
}
|
||||
@@ -42,7 +42,6 @@ export interface FeatureToggles {
|
||||
meta: boolean;
|
||||
datasourceInsights: boolean;
|
||||
reportGrid: boolean;
|
||||
standaloneAlerts: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -132,27 +132,6 @@ export enum NullValueMode {
|
||||
AsZero = 'null as zero',
|
||||
}
|
||||
|
||||
export interface AnnotationEvent {
|
||||
id?: string;
|
||||
annotation?: any;
|
||||
dashboardId?: number;
|
||||
panelId?: number;
|
||||
userId?: number;
|
||||
login?: string;
|
||||
email?: string;
|
||||
avatarUrl?: string;
|
||||
time?: number;
|
||||
timeEnd?: number;
|
||||
isRegion?: boolean;
|
||||
title?: string;
|
||||
text?: string;
|
||||
type?: string;
|
||||
tags?: string[];
|
||||
|
||||
// Currently used to merge annotations from alerts and dashboard
|
||||
source?: any; // source.type === 'dashboard'
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes and API for exposing panel specific data configurations.
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,8 @@ import { ComponentType } from 'react';
|
||||
import { GrafanaPlugin, PluginMeta } from './plugin';
|
||||
import { PanelData } from './panel';
|
||||
import { LogRowModel } from './logs';
|
||||
import { AnnotationEvent, KeyValue, LoadingState, TableData, TimeSeries } from './data';
|
||||
import { AnnotationEvent, AnnotationSupport } from './annotations';
|
||||
import { KeyValue, LoadingState, TableData, TimeSeries } from './data';
|
||||
import { DataFrame, DataFrameDTO } from './dataFrame';
|
||||
import { RawTimeRange, TimeRange } from './time';
|
||||
import { ScopedVars } from './ScopedVars';
|
||||
@@ -155,8 +156,7 @@ export interface DataSourceConstructor<
|
||||
*/
|
||||
export abstract class DataSourceApi<
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData,
|
||||
TAnno = TQuery // defatult to direct query
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
> {
|
||||
/**
|
||||
* Set in constructor
|
||||
@@ -267,13 +267,23 @@ export abstract class DataSourceApi<
|
||||
|
||||
showContextToggle?(row?: LogRowModel): boolean;
|
||||
|
||||
/**
|
||||
* Can be optionally implemented to allow datasource to be a source of annotations for dashboard. To be visible
|
||||
* in the annotation editor `annotations` capability also needs to be enabled in plugin.json.
|
||||
*/
|
||||
annotationQuery?(options: AnnotationQueryRequest<TAnno>): Promise<AnnotationEvent[]>;
|
||||
|
||||
interpolateVariablesInQueries?(queries: TQuery[], scopedVars: ScopedVars | {}): TQuery[];
|
||||
|
||||
/**
|
||||
* An annotation processor allows explict control for how annotations are managed.
|
||||
*
|
||||
* It is only necessary to configure an annotation processor if the default behavior is not desirable
|
||||
*/
|
||||
annotations?: AnnotationSupport<TQuery>;
|
||||
|
||||
/**
|
||||
* Can be optionally implemented to allow datasource to be a source of annotations for dashboard.
|
||||
* This function will only be called if an angular {@link AnnotationsQueryCtrl} is configured and
|
||||
* the {@link annotations} is undefined
|
||||
*
|
||||
* @deprecated -- prefer using {@link AnnotationSupport}
|
||||
*/
|
||||
annotationQuery?(options: AnnotationQueryRequest<TQuery>): Promise<AnnotationEvent[]>;
|
||||
}
|
||||
|
||||
export interface MetadataInspectorProps<
|
||||
@@ -473,12 +483,6 @@ export interface MetricFindValue {
|
||||
expandable?: boolean;
|
||||
}
|
||||
|
||||
export interface BaseAnnotationQuery {
|
||||
datasource: string;
|
||||
enable: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DataSourceJsonData {
|
||||
authType?: string;
|
||||
defaultRegion?: string;
|
||||
@@ -547,20 +551,19 @@ export interface DataSourceSelectItem {
|
||||
|
||||
/**
|
||||
* Options passed to the datasource.annotationQuery method. See docs/plugins/developing/datasource.md
|
||||
*
|
||||
* @deprecated -- use {@link AnnotationSupport}
|
||||
*/
|
||||
export interface AnnotationQueryRequest<TAnno = {}> {
|
||||
export interface AnnotationQueryRequest<MoreOptions = {}> {
|
||||
range: TimeRange;
|
||||
rangeRaw: RawTimeRange;
|
||||
interval: string;
|
||||
intervalMs: number;
|
||||
maxDataPoints?: number;
|
||||
app: CoreApp | string;
|
||||
|
||||
// Should be DataModel but cannot import that here from the main app. Needs to be moved to package first.
|
||||
dashboard: any;
|
||||
|
||||
// The annotation query and common properties
|
||||
annotation: BaseAnnotationQuery & TAnno;
|
||||
annotation: {
|
||||
datasource: string;
|
||||
enable: boolean;
|
||||
name: string;
|
||||
} & MoreOptions;
|
||||
}
|
||||
|
||||
export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './data';
|
||||
export * from './dataFrame';
|
||||
export * from './dataLink';
|
||||
export * from './annotations';
|
||||
export * from './logs';
|
||||
export * from './navModel';
|
||||
export * from './select';
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
toTimeTicks,
|
||||
} from './dateTimeFormatters';
|
||||
import { toHex, sci, toHex0x, toPercent, toPercentUnit } from './arithmeticFormatters';
|
||||
import { binarySIPrefix, currency, decimalSIPrefix } from './symbolFormatters';
|
||||
import { binaryPrefix, currency, SIPrefix } from './symbolFormatters';
|
||||
|
||||
export const getCategories = (): ValueFormatCategory[] => [
|
||||
{
|
||||
@@ -75,14 +75,14 @@ export const getCategories = (): ValueFormatCategory[] => [
|
||||
{
|
||||
name: 'Computation',
|
||||
formats: [
|
||||
{ name: 'FLOP/s', id: 'flops', fn: decimalSIPrefix('FLOP/s') },
|
||||
{ name: 'MFLOP/s', id: 'mflops', fn: decimalSIPrefix('FLOP/s', 2) },
|
||||
{ name: 'GFLOP/s', id: 'gflops', fn: decimalSIPrefix('FLOP/s', 3) },
|
||||
{ name: 'TFLOP/s', id: 'tflops', fn: decimalSIPrefix('FLOP/s', 4) },
|
||||
{ name: 'PFLOP/s', id: 'pflops', fn: decimalSIPrefix('FLOP/s', 5) },
|
||||
{ name: 'EFLOP/s', id: 'eflops', fn: decimalSIPrefix('FLOP/s', 6) },
|
||||
{ name: 'ZFLOP/s', id: 'zflops', fn: decimalSIPrefix('FLOP/s', 7) },
|
||||
{ name: 'YFLOP/s', id: 'yflops', fn: decimalSIPrefix('FLOP/s', 8) },
|
||||
{ name: 'FLOP/s', id: 'flops', fn: SIPrefix('FLOP/s') },
|
||||
{ name: 'MFLOP/s', id: 'mflops', fn: SIPrefix('FLOP/s', 2) },
|
||||
{ name: 'GFLOP/s', id: 'gflops', fn: SIPrefix('FLOP/s', 3) },
|
||||
{ name: 'TFLOP/s', id: 'tflops', fn: SIPrefix('FLOP/s', 4) },
|
||||
{ name: 'PFLOP/s', id: 'pflops', fn: SIPrefix('FLOP/s', 5) },
|
||||
{ name: 'EFLOP/s', id: 'eflops', fn: SIPrefix('FLOP/s', 6) },
|
||||
{ name: 'ZFLOP/s', id: 'zflops', fn: SIPrefix('FLOP/s', 7) },
|
||||
{ name: 'YFLOP/s', id: 'yflops', fn: SIPrefix('FLOP/s', 8) },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -128,45 +128,52 @@ export const getCategories = (): ValueFormatCategory[] => [
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Data (IEC)',
|
||||
name: 'Data',
|
||||
formats: [
|
||||
{ name: 'bits(IEC)', id: 'bits', fn: binarySIPrefix('b') },
|
||||
{ name: 'bytes(IEC)', id: 'bytes', fn: binarySIPrefix('B') },
|
||||
{ name: 'kibibytes', id: 'kbytes', fn: binarySIPrefix('B', 1) },
|
||||
{ name: 'mebibytes', id: 'mbytes', fn: binarySIPrefix('B', 2) },
|
||||
{ name: 'gibibytes', id: 'gbytes', fn: binarySIPrefix('B', 3) },
|
||||
{ name: 'tebibytes', id: 'tbytes', fn: binarySIPrefix('B', 4) },
|
||||
{ name: 'pebibytes', id: 'pbytes', fn: binarySIPrefix('B', 5) },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Data (metric)',
|
||||
formats: [
|
||||
{ name: 'bits(Metric)', id: 'decbits', fn: decimalSIPrefix('b') },
|
||||
{ name: 'bytes(Metric)', id: 'decbytes', fn: decimalSIPrefix('B') },
|
||||
{ name: 'kilobytes', id: 'deckbytes', fn: decimalSIPrefix('B', 1) },
|
||||
{ name: 'megabytes', id: 'decmbytes', fn: decimalSIPrefix('B', 2) },
|
||||
{ name: 'gigabytes', id: 'decgbytes', fn: decimalSIPrefix('B', 3) },
|
||||
{ name: 'terabytes', id: 'dectbytes', fn: decimalSIPrefix('B', 4) },
|
||||
{ name: 'petabytes', id: 'decpbytes', fn: decimalSIPrefix('B', 5) },
|
||||
{ name: 'bytes(IEC)', id: 'bytes', fn: binaryPrefix('B') },
|
||||
{ name: 'bytes(SI)', id: 'decbytes', fn: SIPrefix('B') },
|
||||
{ name: 'bits(IEC)', id: 'bits', fn: binaryPrefix('b') },
|
||||
{ name: 'bits(SI)', id: 'decbits', fn: SIPrefix('b') },
|
||||
{ name: 'kibibytes', id: 'kbytes', fn: binaryPrefix('B', 1) },
|
||||
{ name: 'kilobytes', id: 'deckbytes', fn: SIPrefix('B', 1) },
|
||||
{ name: 'mebibytes', id: 'mbytes', fn: binaryPrefix('B', 2) },
|
||||
{ name: 'megabytes', id: 'decmbytes', fn: SIPrefix('B', 2) },
|
||||
{ name: 'gibibytes', id: 'gbytes', fn: binaryPrefix('B', 3) },
|
||||
{ name: 'gigabytes', id: 'decgbytes', fn: SIPrefix('B', 3) },
|
||||
{ name: 'tebibytes', id: 'tbytes', fn: binaryPrefix('B', 4) },
|
||||
{ name: 'terabytes', id: 'dectbytes', fn: SIPrefix('B', 4) },
|
||||
{ name: 'pebibytes', id: 'pbytes', fn: binaryPrefix('B', 5) },
|
||||
{ name: 'petabytes', id: 'decpbytes', fn: SIPrefix('B', 5) },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Data rate',
|
||||
formats: [
|
||||
{ name: 'packets/sec', id: 'pps', fn: decimalSIPrefix('pps') },
|
||||
{ name: 'bits/sec', id: 'bps', fn: decimalSIPrefix('bps') },
|
||||
{ name: 'bytes/sec', id: 'Bps', fn: decimalSIPrefix('B/s') },
|
||||
{ name: 'kilobytes/sec', id: 'KBs', fn: decimalSIPrefix('B/s', 1) },
|
||||
{ name: 'kilobits/sec', id: 'Kbits', fn: decimalSIPrefix('bps', 1) },
|
||||
{ name: 'megabytes/sec', id: 'MBs', fn: decimalSIPrefix('B/s', 2) },
|
||||
{ name: 'megabits/sec', id: 'Mbits', fn: decimalSIPrefix('bps', 2) },
|
||||
{ name: 'gigabytes/sec', id: 'GBs', fn: decimalSIPrefix('B/s', 3) },
|
||||
{ name: 'gigabits/sec', id: 'Gbits', fn: decimalSIPrefix('bps', 3) },
|
||||
{ name: 'terabytes/sec', id: 'TBs', fn: decimalSIPrefix('B/s', 4) },
|
||||
{ name: 'terabits/sec', id: 'Tbits', fn: decimalSIPrefix('bps', 4) },
|
||||
{ name: 'petabytes/sec', id: 'PBs', fn: decimalSIPrefix('B/s', 5) },
|
||||
{ name: 'petabits/sec', id: 'Pbits', fn: decimalSIPrefix('bps', 5) },
|
||||
{ name: 'packets/sec', id: 'pps', fn: SIPrefix('p/s') },
|
||||
{ name: 'bytes/sec(IEC)', id: 'Bps', fn: binaryPrefix('B/s') },
|
||||
{ name: 'bytes/sec(SI)', id: 'decBps', fn: SIPrefix('B/s') },
|
||||
{ name: 'bits/sec(IEC)', id: 'bps', fn: binaryPrefix('b/s') },
|
||||
{ name: 'bits/sec(SI)', id: 'decbps', fn: SIPrefix('b/s') },
|
||||
{ name: 'kibibytes/sec', id: 'KiBs', fn: binaryPrefix('B/s', 1) },
|
||||
{ name: 'kibibits/sec', id: 'Kibits', fn: binaryPrefix('b/s', 1) },
|
||||
{ name: 'kilobytes/sec', id: 'KBs', fn: SIPrefix('B/s', 1) },
|
||||
{ name: 'kilobits/sec', id: 'Kbits', fn: SIPrefix('b/s', 1) },
|
||||
{ name: 'mibibytes/sec', id: 'MiBs', fn: binaryPrefix('B/s', 2) },
|
||||
{ name: 'mibibits/sec', id: 'Mibits', fn: binaryPrefix('b/s', 2) },
|
||||
{ name: 'megabytes/sec', id: 'MBs', fn: SIPrefix('B/s', 2) },
|
||||
{ name: 'megabits/sec', id: 'Mbits', fn: SIPrefix('b/s', 2) },
|
||||
{ name: 'gibibytes/sec', id: 'GiBs', fn: binaryPrefix('B/s', 3) },
|
||||
{ name: 'gibibits/sec', id: 'Gibits', fn: binaryPrefix('b/s', 3) },
|
||||
{ name: 'gigabytes/sec', id: 'GBs', fn: SIPrefix('B/s', 3) },
|
||||
{ name: 'gigabits/sec', id: 'Gbits', fn: SIPrefix('b/s', 3) },
|
||||
{ name: 'tebibytes/sec', id: 'TiBs', fn: binaryPrefix('B/s', 4) },
|
||||
{ name: 'tebibits/sec', id: 'Tibits', fn: binaryPrefix('b/s', 4) },
|
||||
{ name: 'terabytes/sec', id: 'TBs', fn: SIPrefix('B/s', 4) },
|
||||
{ name: 'terabits/sec', id: 'Tbits', fn: SIPrefix('b/s', 4) },
|
||||
{ name: 'petibytes/sec', id: 'PiBs', fn: binaryPrefix('B/s', 5) },
|
||||
{ name: 'petibits/sec', id: 'Pibits', fn: binaryPrefix('b/s', 5) },
|
||||
{ name: 'petabytes/sec', id: 'PBs', fn: SIPrefix('B/s', 5) },
|
||||
{ name: 'petabits/sec', id: 'Pbits', fn: SIPrefix('b/s', 5) },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -183,44 +190,44 @@ export const getCategories = (): ValueFormatCategory[] => [
|
||||
{
|
||||
name: 'Energy',
|
||||
formats: [
|
||||
{ name: 'Watt (W)', id: 'watt', fn: decimalSIPrefix('W') },
|
||||
{ name: 'Kilowatt (kW)', id: 'kwatt', fn: decimalSIPrefix('W', 1) },
|
||||
{ name: 'Megawatt (MW)', id: 'megwatt', fn: decimalSIPrefix('W', 2) },
|
||||
{ name: 'Gigawatt (GW)', id: 'gwatt', fn: decimalSIPrefix('W', 3) },
|
||||
{ name: 'Milliwatt (mW)', id: 'mwatt', fn: decimalSIPrefix('W', -1) },
|
||||
{ name: 'Watt (W)', id: 'watt', fn: SIPrefix('W') },
|
||||
{ name: 'Kilowatt (kW)', id: 'kwatt', fn: SIPrefix('W', 1) },
|
||||
{ name: 'Megawatt (MW)', id: 'megwatt', fn: SIPrefix('W', 2) },
|
||||
{ name: 'Gigawatt (GW)', id: 'gwatt', fn: SIPrefix('W', 3) },
|
||||
{ name: 'Milliwatt (mW)', id: 'mwatt', fn: SIPrefix('W', -1) },
|
||||
{ name: 'Watt per square meter (W/m²)', id: 'Wm2', fn: toFixedUnit('W/m²') },
|
||||
{ name: 'Volt-ampere (VA)', id: 'voltamp', fn: decimalSIPrefix('VA') },
|
||||
{ name: 'Kilovolt-ampere (kVA)', id: 'kvoltamp', fn: decimalSIPrefix('VA', 1) },
|
||||
{ name: 'Volt-ampere reactive (var)', id: 'voltampreact', fn: decimalSIPrefix('var') },
|
||||
{ name: 'Kilovolt-ampere reactive (kvar)', id: 'kvoltampreact', fn: decimalSIPrefix('var', 1) },
|
||||
{ name: 'Watt-hour (Wh)', id: 'watth', fn: decimalSIPrefix('Wh') },
|
||||
{ name: 'Watt-hour per Kilogram (Wh/kg)', id: 'watthperkg', fn: decimalSIPrefix('Wh/kg') },
|
||||
{ name: 'Kilowatt-hour (kWh)', id: 'kwatth', fn: decimalSIPrefix('Wh', 1) },
|
||||
{ name: 'Kilowatt-min (kWm)', id: 'kwattm', fn: decimalSIPrefix('W-Min', 1) },
|
||||
{ name: 'Ampere-hour (Ah)', id: 'amph', fn: decimalSIPrefix('Ah') },
|
||||
{ name: 'Kiloampere-hour (kAh)', id: 'kamph', fn: decimalSIPrefix('Ah', 1) },
|
||||
{ name: 'Milliampere-hour (mAh)', id: 'mamph', fn: decimalSIPrefix('Ah', -1) },
|
||||
{ name: 'Joule (J)', id: 'joule', fn: decimalSIPrefix('J') },
|
||||
{ name: 'Electron volt (eV)', id: 'ev', fn: decimalSIPrefix('eV') },
|
||||
{ name: 'Ampere (A)', id: 'amp', fn: decimalSIPrefix('A') },
|
||||
{ name: 'Kiloampere (kA)', id: 'kamp', fn: decimalSIPrefix('A', 1) },
|
||||
{ name: 'Milliampere (mA)', id: 'mamp', fn: decimalSIPrefix('A', -1) },
|
||||
{ name: 'Volt (V)', id: 'volt', fn: decimalSIPrefix('V') },
|
||||
{ name: 'Kilovolt (kV)', id: 'kvolt', fn: decimalSIPrefix('V', 1) },
|
||||
{ name: 'Millivolt (mV)', id: 'mvolt', fn: decimalSIPrefix('V', -1) },
|
||||
{ name: 'Decibel-milliwatt (dBm)', id: 'dBm', fn: decimalSIPrefix('dBm') },
|
||||
{ name: 'Ohm (Ω)', id: 'ohm', fn: decimalSIPrefix('Ω') },
|
||||
{ name: 'Kiloohm (kΩ)', id: 'kohm', fn: decimalSIPrefix('Ω', 1) },
|
||||
{ name: 'Megaohm (MΩ)', id: 'Mohm', fn: decimalSIPrefix('Ω', 2) },
|
||||
{ name: 'Farad (F)', id: 'farad', fn: decimalSIPrefix('F') },
|
||||
{ name: 'Microfarad (µF)', id: 'µfarad', fn: decimalSIPrefix('F', -2) },
|
||||
{ name: 'Nanofarad (nF)', id: 'nfarad', fn: decimalSIPrefix('F', -3) },
|
||||
{ name: 'Picofarad (pF)', id: 'pfarad', fn: decimalSIPrefix('F', -4) },
|
||||
{ name: 'Femtofarad (fF)', id: 'ffarad', fn: decimalSIPrefix('F', -5) },
|
||||
{ name: 'Henry (H)', id: 'henry', fn: decimalSIPrefix('H') },
|
||||
{ name: 'Millihenry (mH)', id: 'mhenry', fn: decimalSIPrefix('H', -1) },
|
||||
{ name: 'Microhenry (µH)', id: 'µhenry', fn: decimalSIPrefix('H', -2) },
|
||||
{ name: 'Lumens (Lm)', id: 'lumens', fn: decimalSIPrefix('Lm') },
|
||||
{ name: 'Volt-ampere (VA)', id: 'voltamp', fn: SIPrefix('VA') },
|
||||
{ name: 'Kilovolt-ampere (kVA)', id: 'kvoltamp', fn: SIPrefix('VA', 1) },
|
||||
{ name: 'Volt-ampere reactive (var)', id: 'voltampreact', fn: SIPrefix('var') },
|
||||
{ name: 'Kilovolt-ampere reactive (kvar)', id: 'kvoltampreact', fn: SIPrefix('var', 1) },
|
||||
{ name: 'Watt-hour (Wh)', id: 'watth', fn: SIPrefix('Wh') },
|
||||
{ name: 'Watt-hour per Kilogram (Wh/kg)', id: 'watthperkg', fn: SIPrefix('Wh/kg') },
|
||||
{ name: 'Kilowatt-hour (kWh)', id: 'kwatth', fn: SIPrefix('Wh', 1) },
|
||||
{ name: 'Kilowatt-min (kWm)', id: 'kwattm', fn: SIPrefix('W-Min', 1) },
|
||||
{ name: 'Ampere-hour (Ah)', id: 'amph', fn: SIPrefix('Ah') },
|
||||
{ name: 'Kiloampere-hour (kAh)', id: 'kamph', fn: SIPrefix('Ah', 1) },
|
||||
{ name: 'Milliampere-hour (mAh)', id: 'mamph', fn: SIPrefix('Ah', -1) },
|
||||
{ name: 'Joule (J)', id: 'joule', fn: SIPrefix('J') },
|
||||
{ name: 'Electron volt (eV)', id: 'ev', fn: SIPrefix('eV') },
|
||||
{ name: 'Ampere (A)', id: 'amp', fn: SIPrefix('A') },
|
||||
{ name: 'Kiloampere (kA)', id: 'kamp', fn: SIPrefix('A', 1) },
|
||||
{ name: 'Milliampere (mA)', id: 'mamp', fn: SIPrefix('A', -1) },
|
||||
{ name: 'Volt (V)', id: 'volt', fn: SIPrefix('V') },
|
||||
{ name: 'Kilovolt (kV)', id: 'kvolt', fn: SIPrefix('V', 1) },
|
||||
{ name: 'Millivolt (mV)', id: 'mvolt', fn: SIPrefix('V', -1) },
|
||||
{ name: 'Decibel-milliwatt (dBm)', id: 'dBm', fn: SIPrefix('dBm') },
|
||||
{ name: 'Ohm (Ω)', id: 'ohm', fn: SIPrefix('Ω') },
|
||||
{ name: 'Kiloohm (kΩ)', id: 'kohm', fn: SIPrefix('Ω', 1) },
|
||||
{ name: 'Megaohm (MΩ)', id: 'Mohm', fn: SIPrefix('Ω', 2) },
|
||||
{ name: 'Farad (F)', id: 'farad', fn: SIPrefix('F') },
|
||||
{ name: 'Microfarad (µF)', id: 'µfarad', fn: SIPrefix('F', -2) },
|
||||
{ name: 'Nanofarad (nF)', id: 'nfarad', fn: SIPrefix('F', -3) },
|
||||
{ name: 'Picofarad (pF)', id: 'pfarad', fn: SIPrefix('F', -4) },
|
||||
{ name: 'Femtofarad (fF)', id: 'ffarad', fn: SIPrefix('F', -5) },
|
||||
{ name: 'Henry (H)', id: 'henry', fn: SIPrefix('H') },
|
||||
{ name: 'Millihenry (mH)', id: 'mhenry', fn: SIPrefix('H', -1) },
|
||||
{ name: 'Microhenry (µH)', id: 'µhenry', fn: SIPrefix('H', -2) },
|
||||
{ name: 'Lumens (Lm)', id: 'lumens', fn: SIPrefix('Lm') },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -239,50 +246,50 @@ export const getCategories = (): ValueFormatCategory[] => [
|
||||
{
|
||||
name: 'Force',
|
||||
formats: [
|
||||
{ name: 'Newton-meters (Nm)', id: 'forceNm', fn: decimalSIPrefix('Nm') },
|
||||
{ name: 'Kilonewton-meters (kNm)', id: 'forcekNm', fn: decimalSIPrefix('Nm', 1) },
|
||||
{ name: 'Newtons (N)', id: 'forceN', fn: decimalSIPrefix('N') },
|
||||
{ name: 'Kilonewtons (kN)', id: 'forcekN', fn: decimalSIPrefix('N', 1) },
|
||||
{ name: 'Newton-meters (Nm)', id: 'forceNm', fn: SIPrefix('Nm') },
|
||||
{ name: 'Kilonewton-meters (kNm)', id: 'forcekNm', fn: SIPrefix('Nm', 1) },
|
||||
{ name: 'Newtons (N)', id: 'forceN', fn: SIPrefix('N') },
|
||||
{ name: 'Kilonewtons (kN)', id: 'forcekN', fn: SIPrefix('N', 1) },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Hash rate',
|
||||
formats: [
|
||||
{ name: 'hashes/sec', id: 'Hs', fn: decimalSIPrefix('H/s') },
|
||||
{ name: 'kilohashes/sec', id: 'KHs', fn: decimalSIPrefix('H/s', 1) },
|
||||
{ name: 'megahashes/sec', id: 'MHs', fn: decimalSIPrefix('H/s', 2) },
|
||||
{ name: 'gigahashes/sec', id: 'GHs', fn: decimalSIPrefix('H/s', 3) },
|
||||
{ name: 'terahashes/sec', id: 'THs', fn: decimalSIPrefix('H/s', 4) },
|
||||
{ name: 'petahashes/sec', id: 'PHs', fn: decimalSIPrefix('H/s', 5) },
|
||||
{ name: 'exahashes/sec', id: 'EHs', fn: decimalSIPrefix('H/s', 6) },
|
||||
{ name: 'hashes/sec', id: 'Hs', fn: SIPrefix('H/s') },
|
||||
{ name: 'kilohashes/sec', id: 'KHs', fn: SIPrefix('H/s', 1) },
|
||||
{ name: 'megahashes/sec', id: 'MHs', fn: SIPrefix('H/s', 2) },
|
||||
{ name: 'gigahashes/sec', id: 'GHs', fn: SIPrefix('H/s', 3) },
|
||||
{ name: 'terahashes/sec', id: 'THs', fn: SIPrefix('H/s', 4) },
|
||||
{ name: 'petahashes/sec', id: 'PHs', fn: SIPrefix('H/s', 5) },
|
||||
{ name: 'exahashes/sec', id: 'EHs', fn: SIPrefix('H/s', 6) },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Mass',
|
||||
formats: [
|
||||
{ name: 'milligram (mg)', id: 'massmg', fn: decimalSIPrefix('g', -1) },
|
||||
{ name: 'gram (g)', id: 'massg', fn: decimalSIPrefix('g') },
|
||||
{ name: 'kilogram (kg)', id: 'masskg', fn: decimalSIPrefix('g', 1) },
|
||||
{ name: 'milligram (mg)', id: 'massmg', fn: SIPrefix('g', -1) },
|
||||
{ name: 'gram (g)', id: 'massg', fn: SIPrefix('g') },
|
||||
{ name: 'kilogram (kg)', id: 'masskg', fn: SIPrefix('g', 1) },
|
||||
{ name: 'metric ton (t)', id: 'masst', fn: toFixedUnit('t') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Length',
|
||||
formats: [
|
||||
{ name: 'millimeter (mm)', id: 'lengthmm', fn: decimalSIPrefix('m', -1) },
|
||||
{ name: 'millimeter (mm)', id: 'lengthmm', fn: SIPrefix('m', -1) },
|
||||
{ name: 'feet (ft)', id: 'lengthft', fn: toFixedUnit('ft') },
|
||||
{ name: 'meter (m)', id: 'lengthm', fn: decimalSIPrefix('m') },
|
||||
{ name: 'kilometer (km)', id: 'lengthkm', fn: decimalSIPrefix('m', 1) },
|
||||
{ name: 'meter (m)', id: 'lengthm', fn: SIPrefix('m') },
|
||||
{ name: 'kilometer (km)', id: 'lengthkm', fn: SIPrefix('m', 1) },
|
||||
{ name: 'mile (mi)', id: 'lengthmi', fn: toFixedUnit('mi') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Pressure',
|
||||
formats: [
|
||||
{ name: 'Millibars', id: 'pressurembar', fn: decimalSIPrefix('bar', -1) },
|
||||
{ name: 'Bars', id: 'pressurebar', fn: decimalSIPrefix('bar') },
|
||||
{ name: 'Kilobars', id: 'pressurekbar', fn: decimalSIPrefix('bar', 1) },
|
||||
{ name: 'Pascals', id: 'pressurepa', fn: decimalSIPrefix('Pa') },
|
||||
{ name: 'Millibars', id: 'pressurembar', fn: SIPrefix('bar', -1) },
|
||||
{ name: 'Bars', id: 'pressurebar', fn: SIPrefix('bar') },
|
||||
{ name: 'Kilobars', id: 'pressurekbar', fn: SIPrefix('bar', 1) },
|
||||
{ name: 'Pascals', id: 'pressurepa', fn: SIPrefix('Pa') },
|
||||
{ name: 'Hectopascals', id: 'pressurehpa', fn: toFixedUnit('hPa') },
|
||||
{ name: 'Kilopascals', id: 'pressurekpa', fn: toFixedUnit('kPa') },
|
||||
{ name: 'Inches of mercury', id: 'pressurehg', fn: toFixedUnit('"Hg') },
|
||||
@@ -292,26 +299,26 @@ export const getCategories = (): ValueFormatCategory[] => [
|
||||
{
|
||||
name: 'Radiation',
|
||||
formats: [
|
||||
{ name: 'Becquerel (Bq)', id: 'radbq', fn: decimalSIPrefix('Bq') },
|
||||
{ name: 'curie (Ci)', id: 'radci', fn: decimalSIPrefix('Ci') },
|
||||
{ name: 'Gray (Gy)', id: 'radgy', fn: decimalSIPrefix('Gy') },
|
||||
{ name: 'rad', id: 'radrad', fn: decimalSIPrefix('rad') },
|
||||
{ name: 'Sievert (Sv)', id: 'radsv', fn: decimalSIPrefix('Sv') },
|
||||
{ name: 'milliSievert (mSv)', id: 'radmsv', fn: decimalSIPrefix('Sv', -1) },
|
||||
{ name: 'microSievert (µSv)', id: 'radusv', fn: decimalSIPrefix('Sv', -2) },
|
||||
{ name: 'rem', id: 'radrem', fn: decimalSIPrefix('rem') },
|
||||
{ name: 'Exposure (C/kg)', id: 'radexpckg', fn: decimalSIPrefix('C/kg') },
|
||||
{ name: 'roentgen (R)', id: 'radr', fn: decimalSIPrefix('R') },
|
||||
{ name: 'Sievert/hour (Sv/h)', id: 'radsvh', fn: decimalSIPrefix('Sv/h') },
|
||||
{ name: 'milliSievert/hour (mSv/h)', id: 'radmsvh', fn: decimalSIPrefix('Sv/h', -1) },
|
||||
{ name: 'microSievert/hour (µSv/h)', id: 'radusvh', fn: decimalSIPrefix('Sv/h', -2) },
|
||||
{ name: 'Becquerel (Bq)', id: 'radbq', fn: SIPrefix('Bq') },
|
||||
{ name: 'curie (Ci)', id: 'radci', fn: SIPrefix('Ci') },
|
||||
{ name: 'Gray (Gy)', id: 'radgy', fn: SIPrefix('Gy') },
|
||||
{ name: 'rad', id: 'radrad', fn: SIPrefix('rad') },
|
||||
{ name: 'Sievert (Sv)', id: 'radsv', fn: SIPrefix('Sv') },
|
||||
{ name: 'milliSievert (mSv)', id: 'radmsv', fn: SIPrefix('Sv', -1) },
|
||||
{ name: 'microSievert (µSv)', id: 'radusv', fn: SIPrefix('Sv', -2) },
|
||||
{ name: 'rem', id: 'radrem', fn: SIPrefix('rem') },
|
||||
{ name: 'Exposure (C/kg)', id: 'radexpckg', fn: SIPrefix('C/kg') },
|
||||
{ name: 'roentgen (R)', id: 'radr', fn: SIPrefix('R') },
|
||||
{ name: 'Sievert/hour (Sv/h)', id: 'radsvh', fn: SIPrefix('Sv/h') },
|
||||
{ name: 'milliSievert/hour (mSv/h)', id: 'radmsvh', fn: SIPrefix('Sv/h', -1) },
|
||||
{ name: 'microSievert/hour (µSv/h)', id: 'radusvh', fn: SIPrefix('Sv/h', -2) },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Rotational Speed',
|
||||
formats: [
|
||||
{ name: 'Revolutions per minute (rpm)', id: 'rotrpm', fn: toFixedUnit('rpm') },
|
||||
{ name: 'Hertz (Hz)', id: 'rothz', fn: decimalSIPrefix('Hz') },
|
||||
{ name: 'Hertz (Hz)', id: 'rothz', fn: SIPrefix('Hz') },
|
||||
{ name: 'Radians per second (rad/s)', id: 'rotrads', fn: toFixedUnit('rad/s') },
|
||||
{ name: 'Degrees per second (°/s)', id: 'rotdegs', fn: toFixedUnit('°/s') },
|
||||
],
|
||||
@@ -327,7 +334,7 @@ export const getCategories = (): ValueFormatCategory[] => [
|
||||
{
|
||||
name: 'Time',
|
||||
formats: [
|
||||
{ name: 'Hertz (1/s)', id: 'hertz', fn: decimalSIPrefix('Hz') },
|
||||
{ name: 'Hertz (1/s)', id: 'hertz', fn: SIPrefix('Hz') },
|
||||
{ name: 'nanoseconds (ns)', id: 'ns', fn: toNanoSeconds },
|
||||
{ name: 'microseconds (µs)', id: 'µs', fn: toMicroSeconds },
|
||||
{ name: 'milliseconds (ms)', id: 'ms', fn: toMilliSeconds },
|
||||
@@ -371,8 +378,8 @@ export const getCategories = (): ValueFormatCategory[] => [
|
||||
{
|
||||
name: 'Volume',
|
||||
formats: [
|
||||
{ name: 'millilitre (mL)', id: 'mlitre', fn: decimalSIPrefix('L', -1) },
|
||||
{ name: 'litre (L)', id: 'litre', fn: decimalSIPrefix('L') },
|
||||
{ name: 'millilitre (mL)', id: 'mlitre', fn: SIPrefix('L', -1) },
|
||||
{ name: 'litre (L)', id: 'litre', fn: SIPrefix('L') },
|
||||
{ name: 'cubic meter', id: 'm3', fn: toFixedUnit('m³') },
|
||||
{ name: 'Normal cubic meter', id: 'Nm3', fn: toFixedUnit('Nm³') },
|
||||
{ name: 'cubic decimeter', id: 'dm3', fn: toFixedUnit('dm³') },
|
||||
|
||||
@@ -53,7 +53,7 @@ export function getOffsetFromSIPrefix(c: string): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function binarySIPrefix(unit: string, offset = 0): ValueFormatter {
|
||||
export function binaryPrefix(unit: string, offset = 0): ValueFormatter {
|
||||
const prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset);
|
||||
const units = prefixes.map(p => {
|
||||
return ' ' + p + unit;
|
||||
@@ -61,7 +61,7 @@ export function binarySIPrefix(unit: string, offset = 0): ValueFormatter {
|
||||
return scaledUnits(1024, units);
|
||||
}
|
||||
|
||||
export function decimalSIPrefix(unit: string, offset = 0): ValueFormatter {
|
||||
export function SIPrefix(unit: string, offset = 0): ValueFormatter {
|
||||
let prefixes = ['f', 'p', 'n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
|
||||
prefixes = prefixes.slice(5 + (offset || 0));
|
||||
const units = prefixes.map(p => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getCategories } from './categories';
|
||||
import { DecimalCount } from '../types/displayValue';
|
||||
import { toDateTimeValueFormatter } from './dateTimeFormatters';
|
||||
import { getOffsetFromSIPrefix, decimalSIPrefix, currency } from './symbolFormatters';
|
||||
import { getOffsetFromSIPrefix, SIPrefix, currency } from './symbolFormatters';
|
||||
import { TimeZone } from '../types';
|
||||
|
||||
export interface FormattedValue {
|
||||
@@ -213,7 +213,7 @@ export function getValueFormat(id?: string | null): ValueFormatter {
|
||||
if (key === 'si') {
|
||||
const offset = getOffsetFromSIPrefix(sub.charAt(0));
|
||||
const unit = offset === 0 ? sub : sub.substring(1);
|
||||
return decimalSIPrefix(unit, offset);
|
||||
return SIPrefix(unit, offset);
|
||||
}
|
||||
|
||||
if (key === 'count') {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/e2e-selectors",
|
||||
"version": "7.2.0-pre.0",
|
||||
"version": "7.2.0-beta.2",
|
||||
"description": "Grafana End-to-End Test Selectors Library",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
@@ -133,4 +133,7 @@ export const Pages = {
|
||||
navBar: () => '.explore-toolbar',
|
||||
},
|
||||
},
|
||||
SoloPanel: {
|
||||
url: (page: string) => `/d-solo/${page}`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/e2e",
|
||||
"version": "7.2.0-pre.0",
|
||||
"version": "7.2.0-beta.2",
|
||||
"description": "Grafana End-to-End Test Library",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -44,7 +44,7 @@
|
||||
"types": "src/index.ts",
|
||||
"dependencies": {
|
||||
"@cypress/webpack-preprocessor": "4.1.3",
|
||||
"@grafana/e2e-selectors": "7.2.0-pre.0",
|
||||
"@grafana/e2e-selectors": "7.2.0-beta.2",
|
||||
"@grafana/tsconfig": "^1.0.0-rc1",
|
||||
"@mochajs/json-file-reporter": "^1.2.0",
|
||||
"blink-diff": "1.0.13",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/runtime",
|
||||
"version": "7.2.0-pre.0",
|
||||
"version": "7.2.0-beta.2",
|
||||
"description": "Grafana Runtime Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -22,8 +22,8 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grafana/data": "7.2.0-pre.0",
|
||||
"@grafana/ui": "7.2.0-pre.0",
|
||||
"@grafana/data": "7.2.0-beta.2",
|
||||
"@grafana/ui": "7.2.0-beta.2",
|
||||
"systemjs": "0.20.19",
|
||||
"systemjs-plugin-css": "0.1.37"
|
||||
},
|
||||
|
||||
@@ -56,7 +56,6 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
meta: false,
|
||||
datasourceInsights: false,
|
||||
reportGrid: false,
|
||||
standaloneAlerts: false,
|
||||
};
|
||||
licenseInfo: LicenseInfo = {} as LicenseInfo;
|
||||
rendererAvailable = false;
|
||||
|
||||
@@ -122,6 +122,8 @@ export class DataSourceWithBackend<
|
||||
/**
|
||||
* Override to skip executing a query
|
||||
*
|
||||
* @returns false if the query should be skipped
|
||||
*
|
||||
* @virtual
|
||||
*/
|
||||
filterQuery?(query: TQuery): boolean;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/toolkit",
|
||||
"version": "7.2.0-pre.0",
|
||||
"version": "7.2.0-beta.2",
|
||||
"description": "Grafana Toolkit",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
|
||||
@@ -37,6 +37,10 @@ const copyIfNonExistent = (srcPath: string, destPath: string) =>
|
||||
|
||||
export const prepare = useSpinner<void>('Preparing', async () => {
|
||||
await Promise.all([
|
||||
// Remove local dependencies for @grafana/data/node_modules
|
||||
// See: https://github.com/grafana/grafana/issues/26748
|
||||
rimraf(resolvePath(__dirname, 'node_modules/@grafana/data/node_modules')),
|
||||
|
||||
// Copy only if local tsconfig does not exist. Otherwise this will work, but have odd behavior
|
||||
copyIfNonExistent(
|
||||
resolvePath(__dirname, '../../config/tsconfig.plugin.local.json'),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/ui",
|
||||
"version": "7.2.0-pre.0",
|
||||
"version": "7.2.0-beta.2",
|
||||
"description": "Grafana Components Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -27,8 +27,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/core": "^10.0.27",
|
||||
"@grafana/data": "7.2.0-pre.0",
|
||||
"@grafana/e2e-selectors": "7.2.0-pre.0",
|
||||
"@grafana/data": "7.2.0-beta.2",
|
||||
"@grafana/e2e-selectors": "7.2.0-beta.2",
|
||||
"@grafana/slate-react": "0.22.9-grafana",
|
||||
"@grafana/tsconfig": "^1.0.0-rc1",
|
||||
"@iconscout/react-unicons": "1.1.4",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
|
||||
import { Meta, Props } from '@storybook/addon-docs/blocks';
|
||||
import { Cascader } from './Cascader';
|
||||
|
||||
<Meta title="MDX|Cascader" component={Cascader} />
|
||||
|
||||
# Cascader
|
||||
|
||||
The cascader component is a `Select` with a cascading flyout menu. When you have lots of options in your select, they can be hard to navigate from a regular dropdown list. In that case you can use the cascader to organize your options into groups hierarchically. Just like in the `Select` component, the cascader input doubles as a search field to quickly jump to a selection without navigating the list.
|
||||
The cascader component is a `Select` with a cascading flyout menu. When you have lots of options in your select, they can be hard to navigate from a regular dropdown list. In that case you can use the cascader to organize your options into groups hierarchically. Just like in the `Select` component, the cascader input doubles as a search field to quickly jump to a selection without navigating the list.
|
||||
|
||||
You can either use the `Simple` cascader component for an empty input as default state or use the `initialValue` or `allowCustomValue` fields to pre-fill your cascader. Initial value means that one of the options from your cascaded list is pre-selected. Custom value means that apart from existing options from the list, your users can add custom values to the list by typing them in the `Select` input.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { FC, ReactNode, useState } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { css, cx } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { useStyles } from '../../themes';
|
||||
import { Icon } from '..';
|
||||
@@ -13,14 +13,20 @@ export interface Props {
|
||||
export const CollapsableSection: FC<Props> = ({ label, isOpen, children }) => {
|
||||
const [open, toggleOpen] = useState<boolean>(isOpen);
|
||||
const styles = useStyles(collapsableSectionStyles);
|
||||
const headerClass = cx({
|
||||
[styles.header]: true,
|
||||
[styles.headerCollapsed]: !open,
|
||||
});
|
||||
|
||||
const tooltip = `Click to ${open ? 'collapse' : 'expand'}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div onClick={() => toggleOpen(!open)} className={styles.header}>
|
||||
<Icon name={open ? 'angle-down' : 'angle-right'} size="xl" />
|
||||
<div onClick={() => toggleOpen(!open)} className={headerClass} title={tooltip}>
|
||||
{label}
|
||||
<Icon name={open ? 'angle-down' : 'angle-right'} size="xl" className={styles.icon} />
|
||||
</div>
|
||||
<div className={styles.content}>{open && children}</div>
|
||||
{open && <div className={styles.content}>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -28,11 +34,19 @@ export const CollapsableSection: FC<Props> = ({ label, isOpen, children }) => {
|
||||
const collapsableSectionStyles = (theme: GrafanaTheme) => {
|
||||
return {
|
||||
header: css`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: ${theme.typography.size.lg};
|
||||
cursor: pointer;
|
||||
`,
|
||||
headerCollapsed: css`
|
||||
border-bottom: 1px solid ${theme.colors.border2};
|
||||
`,
|
||||
icon: css`
|
||||
color: ${theme.colors.textWeak};
|
||||
`,
|
||||
content: css`
|
||||
padding: ${theme.spacing.md} 0 ${theme.spacing.md} ${theme.spacing.md};
|
||||
padding: ${theme.spacing.md} 0;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
import { Meta, Props } from '@storybook/addon-docs/blocks';
|
||||
import { ConfirmButton } from './ConfirmButton';
|
||||
|
||||
<Meta title="MDX|ConfirmButton" component={ConfirmButton} />
|
||||
|
||||
# ConfirmButton
|
||||
|
||||
The ConfirmButton is an interactive component that adds a double-confirm option to a clickable action. When clicked, the action is replaced by an inline confirmation with the option to cancel. In Grafana, this is used for example for editing values in settings tables.
|
||||
The ConfirmButton is an interactive component that adds a double-confirm option to a clickable action. When clicked, the action is replaced by an inline confirmation with the option to cancel. In Grafana, this is used, for example, for editing values in settings tables.
|
||||
|
||||
## Variants
|
||||
|
||||
There are four variants of the `ConfirmButton`: primary, secondary, destructive, and link. The primary and secondary variants include a primary or secondary `Button` component. The primary and secondary variant should be used to confirm actions like saving or adding data. The destructive variant includes a destructive `Button` component. The destructive variant should be used to double-confirm a deletion or removal of an element. The link variant doesn't include any button and double-confirms as links instead.
|
||||
|
||||
Apart from the button variant, you can also modify the button size and the button text.
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
<ConfirmButton
|
||||
closeOnConfirm
|
||||
size='md'
|
||||
confirmText='Are you sure?'
|
||||
confirmVariant='secondary'
|
||||
onConfirm={() => {
|
||||
console.log('Action confirmed!')
|
||||
}}
|
||||
>
|
||||
Click me
|
||||
</ConfirmButton>
|
||||
```
|
||||
<Props of={ConfirmButton} />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { Button } from '../Button';
|
||||
import { DeleteButton } from './DeleteButton';
|
||||
import mdx from './ConfirmButton.mdx';
|
||||
|
||||
const getKnobs = () => {
|
||||
return {
|
||||
@@ -31,6 +32,11 @@ export default {
|
||||
component: ConfirmButton,
|
||||
decorators: [withCenteredStory],
|
||||
subcomponents: { DeleteButton },
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const basic = () => {
|
||||
|
||||
@@ -53,15 +53,24 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
});
|
||||
|
||||
export interface Props extends Themeable {
|
||||
/** Confirm action callback */
|
||||
onConfirm(): void;
|
||||
/** Custom button styles */
|
||||
className?: string;
|
||||
/** Button size */
|
||||
size?: ComponentSize;
|
||||
/** Text for the Confirm button */
|
||||
confirmText?: string;
|
||||
/** Disable button click action */
|
||||
disabled?: boolean;
|
||||
/** Variant of the Confirm button */
|
||||
confirmVariant?: ButtonVariant;
|
||||
/** Hide confirm actions when after of them is clicked */
|
||||
closeOnConfirm?: boolean;
|
||||
|
||||
onConfirm(): void;
|
||||
/** Optional on click handler for the original button */
|
||||
onClick?(): void;
|
||||
/** Callback for the cancel action */
|
||||
onCancel?(): void;
|
||||
}
|
||||
|
||||
@@ -70,13 +79,6 @@ interface State {
|
||||
}
|
||||
|
||||
class UnThemedConfirmButton extends PureComponent<Props, State> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
size: 'md',
|
||||
confirmText: 'Save',
|
||||
disabled: false,
|
||||
confirmVariant: 'primary',
|
||||
};
|
||||
|
||||
state: State = {
|
||||
showConfirm: false,
|
||||
};
|
||||
@@ -106,7 +108,7 @@ class UnThemedConfirmButton extends PureComponent<Props, State> {
|
||||
this.props.onCancel();
|
||||
}
|
||||
};
|
||||
onConfirm = (event: SyntheticEvent) => {
|
||||
onConfirm = () => {
|
||||
this.props.onConfirm();
|
||||
if (this.props.closeOnConfirm) {
|
||||
this.setState({
|
||||
@@ -166,4 +168,13 @@ class UnThemedConfirmButton extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
export const ConfirmButton = withTheme(UnThemedConfirmButton);
|
||||
|
||||
// Declare defaultProps directly on the themed component so they are displayed
|
||||
// in the props table
|
||||
ConfirmButton.defaultProps = {
|
||||
size: 'md',
|
||||
confirmText: 'Save',
|
||||
disabled: false,
|
||||
confirmVariant: 'primary',
|
||||
};
|
||||
ConfirmButton.displayName = 'ConfirmButton';
|
||||
|
||||
@@ -4,9 +4,12 @@ import { ComponentSize } from '../../types/size';
|
||||
import { Button } from '../Button';
|
||||
|
||||
export interface Props {
|
||||
size?: ComponentSize;
|
||||
disabled?: boolean;
|
||||
/** Confirm action callback */
|
||||
onConfirm(): void;
|
||||
/** Button size */
|
||||
size?: ComponentSize;
|
||||
/** Disable button click action */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const DeleteButton: FC<Props> = ({ size, disabled, onConfirm }) => {
|
||||
|
||||
@@ -7,4 +7,18 @@ import { ConfirmModal } from './ConfirmModal';
|
||||
|
||||
Used to request user for action confirmation, e.g. deleting items. Triggers provided 'onConfirm' callback.
|
||||
|
||||
# Usage
|
||||
```jsx
|
||||
<ConfirmModal
|
||||
isOpen={false}
|
||||
title='Delete user'
|
||||
body='Are you sure you want to delete this user?'
|
||||
confirmText='Confirm'
|
||||
icon='exclamation-triangle'
|
||||
onConfirm={() => console.log('Confirm action')}
|
||||
onDismiss={() => console.log('Dismiss action')}
|
||||
/>
|
||||
```
|
||||
|
||||
|
||||
<Props of={ConfirmModal} />
|
||||
|
||||
@@ -8,13 +8,21 @@ import { GrafanaTheme } from '@grafana/data';
|
||||
import { HorizontalGroup } from '..';
|
||||
|
||||
export interface Props {
|
||||
/** Toggle modal's open/closed state */
|
||||
isOpen: boolean;
|
||||
/** Title for the modal header */
|
||||
title: string;
|
||||
/** Modal content */
|
||||
body: React.ReactNode;
|
||||
/** Text for confirm button */
|
||||
confirmText: string;
|
||||
/** Text for dismiss button */
|
||||
dismissText?: string;
|
||||
/** Icon for the modal header */
|
||||
icon?: IconName;
|
||||
/** Confirm action callback */
|
||||
onConfirm(): void;
|
||||
/** Dismiss action callback */
|
||||
onDismiss(): void;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ To add more context to the input you can add either text or an icon before or af
|
||||
|
||||
## Usage in forms with Field
|
||||
|
||||
`Input` should be used with the `Field` component to get labels and descriptions. It should also be used for validation. See the `Field` component for more information.
|
||||
`Input` should be used with the `Field` component to get labels and descriptions. It can also be used for validation by using the `required` attribute. See the `Field` component for more information.
|
||||
|
||||
```jsx
|
||||
<Field label="Important information" description="This information is very important, so you really need to fill it in">
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
export { Controller as InputControl } from 'react-hook-form';
|
||||
/**
|
||||
* Rollup does not support renamed exports so do not change this to export { Controller as InputControl } ...
|
||||
*/
|
||||
import { Controller } from 'react-hook-form';
|
||||
export const InputControl = Controller;
|
||||
|
||||
@@ -20,7 +20,7 @@ export const fieldNameByRegexMatcherItem: FieldMatcherUIRegistryItem<string> = {
|
||||
id: FieldMatcherID.byRegexp,
|
||||
component: FieldNameByRegexMatcherEditor,
|
||||
matcher: fieldMatchers.get(FieldMatcherID.byRegexp),
|
||||
name: 'Filter by field using regex',
|
||||
description: 'Set properties for fields with names matching provided regex',
|
||||
name: 'Fields with name matching regex',
|
||||
description: 'Set properties for fields with names matching a regex',
|
||||
optionsToLabel: options => options,
|
||||
};
|
||||
|
||||
@@ -26,8 +26,8 @@ export const fieldNameMatcherItem: FieldMatcherUIRegistryItem<string> = {
|
||||
id: FieldMatcherID.byName,
|
||||
component: FieldNameMatcherEditor,
|
||||
matcher: fieldMatchers.get(FieldMatcherID.byName),
|
||||
name: 'Filter by field',
|
||||
description: 'Set properties for fields matching the name',
|
||||
name: 'Fields with name',
|
||||
description: 'Set properties for a specific field',
|
||||
optionsToLabel: options => options,
|
||||
};
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ export const fieldTypeMatcherItem: FieldMatcherUIRegistryItem<string> = {
|
||||
id: FieldMatcherID.byType,
|
||||
component: FieldTypeMatcherEditor,
|
||||
matcher: fieldMatchers.get(FieldMatcherID.byType),
|
||||
name: 'Filter by type',
|
||||
description: 'Set properties for fields matching a type',
|
||||
name: 'Fields with type',
|
||||
description: 'Set properties for fields of a specific type (number, string, boolean)',
|
||||
optionsToLabel: options => options,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@jaegertracing/jaeger-ui-components",
|
||||
"version": "7.2.0-pre.0",
|
||||
"version": "7.2.0-beta.2",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -14,8 +14,8 @@
|
||||
"typescript": "3.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grafana/data": "7.2.0-pre.0",
|
||||
"@grafana/ui": "7.2.0-pre.0",
|
||||
"@grafana/data": "7.2.0-beta.2",
|
||||
"@grafana/ui": "7.2.0-beta.2",
|
||||
"@types/classnames": "^2.2.7",
|
||||
"@types/deep-freeze": "^0.1.1",
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func ValidateOrgAlert(c *models.ReqContext) {
|
||||
@@ -287,13 +288,12 @@ func UpdateAlertNotification(c *models.ReqContext, cmd models.UpdateAlertNotific
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == models.ErrAlertNotificationNotFound {
|
||||
return Error(404, err.Error(), err)
|
||||
}
|
||||
return Error(500, "Failed to update alert notification", err)
|
||||
}
|
||||
|
||||
if cmd.Result == nil {
|
||||
return Error(404, "Alert notification not found", nil)
|
||||
}
|
||||
|
||||
query := models.GetAlertNotificationsQuery{
|
||||
OrgId: c.OrgId,
|
||||
Id: cmd.Id,
|
||||
@@ -316,13 +316,12 @@ func UpdateAlertNotificationByUID(c *models.ReqContext, cmd models.UpdateAlertNo
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == models.ErrAlertNotificationNotFound {
|
||||
return Error(404, err.Error(), nil)
|
||||
}
|
||||
return Error(500, "Failed to update alert notification", err)
|
||||
}
|
||||
|
||||
if cmd.Result == nil {
|
||||
return Error(404, "Alert notification not found", nil)
|
||||
}
|
||||
|
||||
query := models.GetAlertNotificationsWithUidQuery{
|
||||
OrgId: cmd.OrgId,
|
||||
Uid: cmd.Uid,
|
||||
@@ -390,6 +389,9 @@ func DeleteAlertNotification(c *models.ReqContext) Response {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == models.ErrAlertNotificationNotFound {
|
||||
return Error(404, err.Error(), nil)
|
||||
}
|
||||
return Error(500, "Failed to delete alert notification", err)
|
||||
}
|
||||
|
||||
@@ -403,10 +405,16 @@ func DeleteAlertNotificationByUID(c *models.ReqContext) Response {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == models.ErrAlertNotificationNotFound {
|
||||
return Error(404, err.Error(), nil)
|
||||
}
|
||||
return Error(500, "Failed to delete alert notification", err)
|
||||
}
|
||||
|
||||
return Success("Notification deleted")
|
||||
return JSON(200, util.DynMap{
|
||||
"message": "Notification deleted",
|
||||
"id": cmd.DeletedAlertNotificationId,
|
||||
})
|
||||
}
|
||||
|
||||
//POST /api/alert-notifications/test
|
||||
|
||||
@@ -327,10 +327,6 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
||||
Children: []*dtos.NavLink{},
|
||||
})
|
||||
|
||||
sort.SliceStable(navTree, func(i, j int) bool {
|
||||
return navTree[i].SortWeight < navTree[j].SortWeight
|
||||
})
|
||||
|
||||
return navTree, nil
|
||||
}
|
||||
|
||||
@@ -434,6 +430,10 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
|
||||
|
||||
hs.HooksService.RunIndexDataHooks(&data, c)
|
||||
|
||||
sort.SliceStable(data.NavTree, func(i, j int) bool {
|
||||
return data.NavTree[i].SortWeight < data.NavTree[j].SortWeight
|
||||
})
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -249,7 +249,7 @@ func (hs *HTTPServer) loginUserWithUser(user *models.User, c *models.ReqContext)
|
||||
}
|
||||
|
||||
hs.log.Info("Successful Login", "User", user.Email)
|
||||
middleware.WriteSessionCookie(c, userToken.UnhashedToken, hs.Cfg.LoginMaxLifetimeDays)
|
||||
middleware.WriteSessionCookie(c, userToken.UnhashedToken, hs.Cfg.LoginMaxLifetime)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,10 @@ func inviteExistingUserToOrg(c *models.ReqContext, user *models.User, inviteDto
|
||||
}
|
||||
}
|
||||
|
||||
return Success(fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName))
|
||||
return JSON(200, util.DynMap{
|
||||
"message": fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName),
|
||||
"userId": user.Id,
|
||||
})
|
||||
}
|
||||
|
||||
func RevokeInvite(c *models.ReqContext) Response {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
// POST /api/org/users
|
||||
@@ -35,12 +36,18 @@ func addOrgUserHelper(cmd models.AddOrgUserCommand) Response {
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == models.ErrOrgUserAlreadyAdded {
|
||||
return Error(409, "User is already member of this organization", nil)
|
||||
return JSON(409, util.DynMap{
|
||||
"message": "User is already member of this organization",
|
||||
"userId": cmd.UserId,
|
||||
})
|
||||
}
|
||||
return Error(500, "Could not add user to organization", err)
|
||||
}
|
||||
|
||||
return Success("User added to organization")
|
||||
return JSON(200, util.DynMap{
|
||||
"message": "User added to organization",
|
||||
"userId": cmd.UserId,
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/org/users
|
||||
|
||||
@@ -261,22 +261,21 @@ func rotateEndOfRequestFunc(ctx *models.ReqContext, authTokenService models.User
|
||||
}
|
||||
|
||||
if rotated {
|
||||
WriteSessionCookie(ctx, token.UnhashedToken, setting.LoginMaxLifetimeDays)
|
||||
WriteSessionCookie(ctx, token.UnhashedToken, setting.LoginMaxLifetime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WriteSessionCookie(ctx *models.ReqContext, value string, maxLifetimeDays int) {
|
||||
func WriteSessionCookie(ctx *models.ReqContext, value string, maxLifetime time.Duration) {
|
||||
if setting.Env == setting.DEV {
|
||||
ctx.Logger.Info("New token", "unhashed token", value)
|
||||
}
|
||||
|
||||
var maxAge int
|
||||
if maxLifetimeDays <= 0 {
|
||||
if maxLifetime <= 0 {
|
||||
maxAge = -1
|
||||
} else {
|
||||
maxAgeHours := (time.Duration(setting.LoginMaxLifetimeDays) * 24 * time.Hour) + time.Hour
|
||||
maxAge = int(maxAgeHours.Seconds())
|
||||
maxAge = int(maxLifetime.Seconds())
|
||||
}
|
||||
|
||||
WriteCookie(ctx.Resp, setting.LoginCookieName, url.QueryEscape(value), maxAge, newCookieOptions)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/gtime"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
authproxy "github.com/grafana/grafana/pkg/middleware/auth_proxy"
|
||||
@@ -253,8 +254,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
maxAgeHours := (time.Duration(setting.LoginMaxLifetimeDays) * 24 * time.Hour)
|
||||
maxAge := (maxAgeHours + time.Hour).Seconds()
|
||||
maxAge := int(setting.LoginMaxLifetime.Seconds())
|
||||
|
||||
sameSitePolicies := []http.SameSite{
|
||||
http.SameSiteNoneMode,
|
||||
@@ -272,7 +272,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
Value: "rotated",
|
||||
Path: expectedCookiePath,
|
||||
HttpOnly: true,
|
||||
MaxAge: int(maxAge),
|
||||
MaxAge: maxAge,
|
||||
Secure: setting.CookieSecure,
|
||||
SameSite: sameSitePolicy,
|
||||
}
|
||||
@@ -303,7 +303,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
Value: "rotated",
|
||||
Path: expectedCookiePath,
|
||||
HttpOnly: true,
|
||||
MaxAge: int(maxAge),
|
||||
MaxAge: maxAge,
|
||||
Secure: setting.CookieSecure,
|
||||
}
|
||||
|
||||
@@ -546,7 +546,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc) {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
setting.LoginCookieName = "grafana_session"
|
||||
setting.LoginMaxLifetimeDays = 30
|
||||
setting.LoginMaxLifetime, _ = gtime.ParseInterval("30d")
|
||||
|
||||
sc := &scenarioContext{}
|
||||
|
||||
@@ -637,7 +637,7 @@ func TestTokenRotationAtEndOfRequest(t *testing.T) {
|
||||
|
||||
func initTokenRotationTest(ctx context.Context) (*models.ReqContext, *httptest.ResponseRecorder, error) {
|
||||
setting.LoginCookieName = "login_token"
|
||||
setting.LoginMaxLifetimeDays = 7
|
||||
setting.LoginMaxLifetime, _ = gtime.ParseInterval("7d")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req, err := http.NewRequestWithContext(ctx, "", "", nil)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAlertNotificationNotFound = errors.New("Alert notification not found")
|
||||
ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified")
|
||||
ErrAlertNotificationStateNotFound = errors.New("alert notification state not found")
|
||||
ErrAlertNotificationStateVersionConflict = errors.New("alert notification state update version conflict")
|
||||
@@ -94,6 +95,8 @@ type DeleteAlertNotificationCommand struct {
|
||||
type DeleteAlertNotificationWithUidCommand struct {
|
||||
Uid string
|
||||
OrgId int64
|
||||
|
||||
DeletedAlertNotificationId int64
|
||||
}
|
||||
|
||||
type GetAlertNotificationUidQuery struct {
|
||||
|
||||
@@ -384,6 +384,10 @@ type ValidateDashboardBeforeSaveCommand struct {
|
||||
Result *ValidateDashboardBeforeSaveResult
|
||||
}
|
||||
|
||||
type DeleteOrphanedProvisionedDashboardsCommand struct {
|
||||
ReaderNames []string
|
||||
}
|
||||
|
||||
//
|
||||
// QUERIES
|
||||
//
|
||||
|
||||
@@ -84,9 +84,9 @@ func instrumentRoundtrip(datasourceName string, next http.RoundTripper) promhttp
|
||||
promhttp.InstrumentRoundTripperInFlight(requestInFlight, next))).
|
||||
RoundTrip(r)
|
||||
|
||||
// we avoid measing contentlength less the zero because it indicates
|
||||
// we avoid measuring contentlength less than zero because it indicates
|
||||
// that the content size is unknown. https://godoc.org/github.com/badu/http#Response
|
||||
if res.ContentLength > 0 {
|
||||
if res != nil && res.ContentLength > 0 {
|
||||
responseSizeSummary.Observe(float64(res.ContentLength))
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ func (a *queryEndpointAdapter) Query(ctx context.Context, ds *models.DataSource,
|
||||
DataSourceInstanceSettings: instanceSettings,
|
||||
},
|
||||
Queries: []backend.DataQuery{},
|
||||
Headers: query.Headers,
|
||||
}
|
||||
|
||||
for _, q := range query.Queries {
|
||||
|
||||
@@ -59,6 +59,7 @@ func (tw *DatasourcePluginWrapperV2) Query(ctx context.Context, ds *models.DataS
|
||||
DataSourceInstanceSettings: backend.ToProto().DataSourceInstanceSettings(instanceSettings),
|
||||
},
|
||||
Queries: []*pluginv2.DataQuery{},
|
||||
Headers: query.Headers,
|
||||
}
|
||||
|
||||
for _, q := range query.Queries {
|
||||
|
||||
@@ -28,6 +28,7 @@ func init() {
|
||||
Placeholder: "Pagerduty Integration Key",
|
||||
PropertyName: "integrationKey",
|
||||
Required: true,
|
||||
Secure: true,
|
||||
},
|
||||
{
|
||||
Label: "Severity",
|
||||
|
||||
@@ -397,13 +397,11 @@ func (s *UserAuthTokenService) GetUserTokens(ctx context.Context, userId int64)
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) createdAfterParam() int64 {
|
||||
tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
|
||||
return getTime().Add(-tokenMaxLifetime).Unix()
|
||||
return getTime().Add(-s.Cfg.LoginMaxLifetime).Unix()
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) rotatedAfterParam() int64 {
|
||||
tokenMaxInactiveLifetime := time.Duration(s.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour
|
||||
return getTime().Add(-tokenMaxInactiveLifetime).Unix()
|
||||
return getTime().Add(-s.Cfg.LoginMaxInactiveLifetime).Unix()
|
||||
}
|
||||
|
||||
func hashToken(token string) string {
|
||||
|
||||
@@ -494,13 +494,14 @@ func TestUserAuthToken(t *testing.T) {
|
||||
|
||||
func createTestContext(t *testing.T) *testContext {
|
||||
t.Helper()
|
||||
|
||||
maxInactiveDurationVal, _ := time.ParseDuration("168h")
|
||||
maxLifetimeDurationVal, _ := time.ParseDuration("720h")
|
||||
sqlstore := sqlstore.InitTestDB(t)
|
||||
tokenService := &UserAuthTokenService{
|
||||
SQLStore: sqlstore,
|
||||
Cfg: &setting.Cfg{
|
||||
LoginMaxInactiveLifetimeDays: 7,
|
||||
LoginMaxLifetimeDays: 30,
|
||||
LoginMaxInactiveLifetime: maxInactiveDurationVal,
|
||||
LoginMaxLifetime: maxLifetimeDurationVal,
|
||||
TokenRotationIntervalMinutes: 10,
|
||||
},
|
||||
log: log.New("test-logger"),
|
||||
|
||||
@@ -8,11 +8,12 @@ import (
|
||||
)
|
||||
|
||||
func (srv *UserAuthTokenService) Run(ctx context.Context) error {
|
||||
var err error
|
||||
ticker := time.NewTicker(time.Hour)
|
||||
maxInactiveLifetime := time.Duration(srv.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour
|
||||
maxLifetime := time.Duration(srv.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
|
||||
maxInactiveLifetime := srv.Cfg.LoginMaxInactiveLifetime
|
||||
maxLifetime := srv.Cfg.LoginMaxLifetime
|
||||
|
||||
err := srv.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func() {
|
||||
err = srv.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func() {
|
||||
if _, err := srv.deleteExpiredTokens(ctx, maxInactiveLifetime, maxLifetime); err != nil {
|
||||
srv.log.Error("An error occurred while deleting expired tokens", "err", err)
|
||||
}
|
||||
@@ -24,7 +25,7 @@ func (srv *UserAuthTokenService) Run(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
err := srv.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func() {
|
||||
err = srv.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func() {
|
||||
if _, err := srv.deleteExpiredTokens(ctx, maxInactiveLifetime, maxLifetime); err != nil {
|
||||
srv.log.Error("An error occurred while deleting expired tokens", "err", err)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ import (
|
||||
func TestUserAuthTokenCleanup(t *testing.T) {
|
||||
Convey("Test user auth token cleanup", t, func() {
|
||||
ctx := createTestContext(t)
|
||||
ctx.tokenService.Cfg.LoginMaxInactiveLifetimeDays = 7
|
||||
ctx.tokenService.Cfg.LoginMaxLifetimeDays = 30
|
||||
maxInactiveLifetime, _ := time.ParseDuration("168h")
|
||||
maxLifetime, _ := time.ParseDuration("720h")
|
||||
ctx.tokenService.Cfg.LoginMaxInactiveLifetime = maxInactiveLifetime
|
||||
ctx.tokenService.Cfg.LoginMaxLifetime = maxLifetime
|
||||
|
||||
insertToken := func(token string, prev string, createdAt, rotatedAt int64) {
|
||||
ut := userAuthToken{AuthToken: token, PrevAuthToken: prev, CreatedAt: createdAt, RotatedAt: rotatedAt, UserAgent: "", ClientIp: ""}
|
||||
@@ -27,7 +29,7 @@ func TestUserAuthTokenCleanup(t *testing.T) {
|
||||
}
|
||||
|
||||
Convey("should delete tokens where token rotation age is older than or equal 7 days", func() {
|
||||
from := t.Add(-7 * 24 * time.Hour)
|
||||
from := t.Add(-168 * time.Hour)
|
||||
|
||||
// insert three old tokens that should be deleted
|
||||
for i := 0; i < 3; i++ {
|
||||
@@ -40,7 +42,7 @@ func TestUserAuthTokenCleanup(t *testing.T) {
|
||||
insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), from.Unix(), from.Unix())
|
||||
}
|
||||
|
||||
affected, err := ctx.tokenService.deleteExpiredTokens(context.Background(), 7*24*time.Hour, 30*24*time.Hour)
|
||||
affected, err := ctx.tokenService.deleteExpiredTokens(context.Background(), 168*time.Hour, 30*24*time.Hour)
|
||||
So(err, ShouldBeNil)
|
||||
So(affected, ShouldEqual, 3)
|
||||
})
|
||||
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
@@ -16,6 +18,7 @@ type DashboardProvisioner interface {
|
||||
PollChanges(ctx context.Context)
|
||||
GetProvisionerResolvedPath(name string) string
|
||||
GetAllowUIUpdatesFromConfig(name string) bool
|
||||
CleanUpOrphanedDashboards()
|
||||
}
|
||||
|
||||
// DashboardProvisionerFactory creates DashboardProvisioners based on input
|
||||
@@ -71,6 +74,19 @@ func (provider *Provisioner) Provision() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUpOrphanedDashboards deletes provisioned dashboards missing a linked reader.
|
||||
func (provider *Provisioner) CleanUpOrphanedDashboards() {
|
||||
currentReaders := make([]string, len(provider.fileReaders))
|
||||
|
||||
for index, reader := range provider.fileReaders {
|
||||
currentReaders[index] = reader.Cfg.Name
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&models.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: currentReaders}); err != nil {
|
||||
provider.log.Warn("Failed to delete orphaned provisioned dashboards", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// PollChanges starts polling for changes in dashboard definition files. It creates goroutine for each provider
|
||||
// defined in the config.
|
||||
func (provider *Provisioner) PollChanges(ctx context.Context) {
|
||||
|
||||
@@ -60,3 +60,6 @@ func (dpm *ProvisionerMock) GetAllowUIUpdatesFromConfig(name string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CleanUpOrphanedDashboards not implemented for mocks
|
||||
func (dpm *ProvisionerMock) CleanUpOrphanedDashboards() {}
|
||||
|
||||
@@ -143,9 +143,11 @@ func (ps *provisioningServiceImpl) ProvisionDashboards() error {
|
||||
defer ps.mutex.Unlock()
|
||||
|
||||
ps.cancelPolling()
|
||||
dashProvisioner.CleanUpOrphanedDashboards()
|
||||
|
||||
if err := dashProvisioner.Provision(); err != nil {
|
||||
// If we fail to provision with the new provisioner, mutex will unlock and the polling we restart with the
|
||||
err = dashProvisioner.Provision()
|
||||
if err != nil {
|
||||
// If we fail to provision with the new provisioner, the mutex will unlock and the polling will restart with the
|
||||
// old provisioner as we did not switch them yet.
|
||||
return errutil.Wrap("Failed to provision dashboards", err)
|
||||
}
|
||||
|
||||
@@ -33,9 +33,18 @@ func init() {
|
||||
func DeleteAlertNotification(cmd *models.DeleteAlertNotificationCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
sql := "DELETE FROM alert_notification WHERE alert_notification.org_id = ? AND alert_notification.id = ?"
|
||||
if _, err := sess.Exec(sql, cmd.OrgId, cmd.Id); err != nil {
|
||||
res, err := sess.Exec(sql, cmd.OrgId, cmd.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return models.ErrAlertNotificationNotFound
|
||||
}
|
||||
|
||||
if _, err := sess.Exec("DELETE FROM alert_notification_state WHERE alert_notification_state.org_id = ? AND alert_notification_state.notifier_id = ?", cmd.OrgId, cmd.Id); err != nil {
|
||||
return err
|
||||
@@ -51,14 +60,17 @@ func DeleteAlertNotificationWithUid(cmd *models.DeleteAlertNotificationWithUidCo
|
||||
return err
|
||||
}
|
||||
|
||||
if existingNotification.Result != nil {
|
||||
deleteCommand := &models.DeleteAlertNotificationCommand{
|
||||
Id: existingNotification.Result.Id,
|
||||
OrgId: existingNotification.Result.OrgId,
|
||||
}
|
||||
if err := bus.Dispatch(deleteCommand); err != nil {
|
||||
return err
|
||||
}
|
||||
if existingNotification.Result == nil {
|
||||
return models.ErrAlertNotificationNotFound
|
||||
}
|
||||
|
||||
cmd.DeletedAlertNotificationId = existingNotification.Result.Id
|
||||
deleteCommand := &models.DeleteAlertNotificationCommand{
|
||||
Id: existingNotification.Result.Id,
|
||||
OrgId: existingNotification.Result.OrgId,
|
||||
}
|
||||
if err := bus.Dispatch(deleteCommand); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -367,6 +379,10 @@ func UpdateAlertNotification(cmd *models.UpdateAlertNotificationCommand) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if current.Id == 0 {
|
||||
return models.ErrAlertNotificationNotFound
|
||||
}
|
||||
|
||||
// check if name exists
|
||||
sameNameQuery := &models.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
|
||||
if err := getAlertNotificationInternal(sameNameQuery, sess); err != nil {
|
||||
@@ -433,7 +449,7 @@ func UpdateAlertNotificationWithUid(cmd *models.UpdateAlertNotificationWithUidCo
|
||||
current := getAlertNotificationWithUidQuery.Result
|
||||
|
||||
if current == nil {
|
||||
return fmt.Errorf("Cannot update, alert notification uid %s doesn't exist", cmd.Uid)
|
||||
return models.ErrAlertNotificationNotFound
|
||||
}
|
||||
|
||||
if cmd.NewUid == "" {
|
||||
|
||||
@@ -390,5 +390,87 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
So(result, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Cannot update non-existing Alert Notification", func() {
|
||||
updateCmd := &models.UpdateAlertNotificationCommand{
|
||||
Name: "NewName",
|
||||
Type: "webhook",
|
||||
OrgId: 1,
|
||||
SendReminder: true,
|
||||
DisableResolveMessage: true,
|
||||
Frequency: "60s",
|
||||
Settings: simplejson.New(),
|
||||
Id: 1,
|
||||
}
|
||||
err := UpdateAlertNotification(updateCmd)
|
||||
So(err, ShouldEqual, models.ErrAlertNotificationNotFound)
|
||||
|
||||
Convey("using UID", func() {
|
||||
updateWithUidCmd := &models.UpdateAlertNotificationWithUidCommand{
|
||||
Name: "NewName",
|
||||
Type: "webhook",
|
||||
OrgId: 1,
|
||||
SendReminder: true,
|
||||
DisableResolveMessage: true,
|
||||
Frequency: "60s",
|
||||
Settings: simplejson.New(),
|
||||
Uid: "uid",
|
||||
NewUid: "newUid",
|
||||
}
|
||||
err := UpdateAlertNotificationWithUid(updateWithUidCmd)
|
||||
So(err, ShouldEqual, models.ErrAlertNotificationNotFound)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can delete Alert Notification", func() {
|
||||
cmd := &models.CreateAlertNotificationCommand{
|
||||
Name: "ops update",
|
||||
Type: "email",
|
||||
OrgId: 1,
|
||||
SendReminder: false,
|
||||
Settings: simplejson.New(),
|
||||
}
|
||||
|
||||
err := CreateAlertNotificationCommand(cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
deleteCmd := &models.DeleteAlertNotificationCommand{
|
||||
Id: cmd.Result.Id,
|
||||
OrgId: 1,
|
||||
}
|
||||
err = DeleteAlertNotification(deleteCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("using UID", func() {
|
||||
err := CreateAlertNotificationCommand(cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
deleteWithUidCmd := &models.DeleteAlertNotificationWithUidCommand{
|
||||
Uid: cmd.Result.Uid,
|
||||
OrgId: 1,
|
||||
}
|
||||
err = DeleteAlertNotificationWithUid(deleteWithUidCmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(deleteWithUidCmd.DeletedAlertNotificationId, ShouldEqual, cmd.Result.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Cannot delete non-existing Alert Notification", func() {
|
||||
deleteCmd := &models.DeleteAlertNotificationCommand{
|
||||
Id: 1,
|
||||
OrgId: 1,
|
||||
}
|
||||
err := DeleteAlertNotification(deleteCmd)
|
||||
So(err, ShouldEqual, models.ErrAlertNotificationNotFound)
|
||||
|
||||
Convey("using UID", func() {
|
||||
deleteWithUidCmd := &models.DeleteAlertNotificationWithUidCommand{
|
||||
Uid: "uid",
|
||||
OrgId: 1,
|
||||
}
|
||||
err = DeleteAlertNotificationWithUid(deleteWithUidCmd)
|
||||
So(err, ShouldEqual, models.ErrAlertNotificationNotFound)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -353,57 +353,61 @@ func GetDashboardTags(query *models.GetDashboardTagsQuery) error {
|
||||
|
||||
func DeleteDashboard(cmd *models.DeleteDashboardCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
dashboard := models.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId}
|
||||
has, err := sess.Get(&dashboard)
|
||||
return deleteDashboard(cmd, sess)
|
||||
})
|
||||
}
|
||||
|
||||
func deleteDashboard(cmd *models.DeleteDashboardCommand, sess *DBSession) error {
|
||||
dashboard := models.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId}
|
||||
has, err := sess.Get(&dashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return models.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
deletes := []string{
|
||||
"DELETE FROM dashboard_tag WHERE dashboard_id = ? ",
|
||||
"DELETE FROM star WHERE dashboard_id = ? ",
|
||||
"DELETE FROM dashboard WHERE id = ?",
|
||||
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
|
||||
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
|
||||
"DELETE FROM annotation WHERE dashboard_id = ?",
|
||||
"DELETE FROM dashboard_provisioning WHERE dashboard_id = ?",
|
||||
}
|
||||
|
||||
if dashboard.IsFolder {
|
||||
deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)")
|
||||
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
|
||||
|
||||
dashIds := []struct {
|
||||
Id int64
|
||||
}{}
|
||||
err := sess.SQL("select id from dashboard where folder_id = ?", dashboard.Id).Find(&dashIds)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return models.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
deletes := []string{
|
||||
"DELETE FROM dashboard_tag WHERE dashboard_id = ? ",
|
||||
"DELETE FROM star WHERE dashboard_id = ? ",
|
||||
"DELETE FROM dashboard WHERE id = ?",
|
||||
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
|
||||
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
|
||||
"DELETE FROM annotation WHERE dashboard_id = ?",
|
||||
"DELETE FROM dashboard_provisioning WHERE dashboard_id = ?",
|
||||
}
|
||||
|
||||
if dashboard.IsFolder {
|
||||
deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)")
|
||||
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
|
||||
|
||||
dashIds := []struct {
|
||||
Id int64
|
||||
}{}
|
||||
err := sess.SQL("select id from dashboard where folder_id = ?", dashboard.Id).Find(&dashIds)
|
||||
if err != nil {
|
||||
for _, id := range dashIds {
|
||||
if err := deleteAlertDefinition(id.Id, sess); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, id := range dashIds {
|
||||
if err := deleteAlertDefinition(id.Id, sess); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
|
||||
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, sql := range deletes {
|
||||
_, err := sess.Exec(sql, dashboard.Id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, sql := range deletes {
|
||||
_, err := sess.Exec(sql, dashboard.Id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDashboards(query *models.GetDashboardsQuery) error {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
@@ -10,6 +12,7 @@ func init() {
|
||||
bus.AddHandler("sql", SaveProvisionedDashboard)
|
||||
bus.AddHandler("sql", GetProvisionedDataByDashboardId)
|
||||
bus.AddHandler("sql", UnprovisionDashboard)
|
||||
bus.AddHandler("sql", DeleteOrphanedProvisionedDashboards)
|
||||
}
|
||||
|
||||
type DashboardExtras struct {
|
||||
@@ -88,3 +91,26 @@ func UnprovisionDashboard(cmd *models.UnprovisionDashboardCommand) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteOrphanedProvisionedDashboards(cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error {
|
||||
var result []*models.DashboardProvisioning
|
||||
|
||||
convertedReaderNames := make([]interface{}, len(cmd.ReaderNames))
|
||||
for index, readerName := range cmd.ReaderNames {
|
||||
convertedReaderNames[index] = readerName
|
||||
}
|
||||
|
||||
err := x.NotIn("name", convertedReaderNames...).Find(&result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, deleteDashCommand := range result {
|
||||
err := DeleteDashboard(&models.DeleteDashboardCommand{Id: deleteDashCommand.DashboardId})
|
||||
if err != nil && !errors.Is(err, models.ErrDashboardNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -54,6 +54,43 @@ func TestDashboardProvisioningTest(t *testing.T) {
|
||||
So(cmd.Result.Id, ShouldNotEqual, 0)
|
||||
dashId := cmd.Result.Id
|
||||
|
||||
Convey("Deleting orphaned provisioned dashboards", func() {
|
||||
anotherCmd := &models.SaveProvisionedDashboardCommand{
|
||||
DashboardCmd: &models.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
IsFolder: false,
|
||||
FolderId: folderCmd.Result.Id,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "another_dashboard",
|
||||
}),
|
||||
},
|
||||
DashboardProvisioning: &models.DashboardProvisioning{
|
||||
Name: "another_reader",
|
||||
ExternalId: "/var/grafana.json",
|
||||
Updated: now.Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
err := SaveProvisionedDashboard(anotherCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := &models.GetDashboardsQuery{DashboardIds: []int64{anotherCmd.Result.Id}}
|
||||
err = GetDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldNotBeNil)
|
||||
|
||||
deleteCmd := &models.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: []string{"default"}}
|
||||
So(DeleteOrphanedProvisionedDashboards(deleteCmd), ShouldBeNil)
|
||||
|
||||
query = &models.GetDashboardsQuery{DashboardIds: []int64{cmd.Result.Id, anotherCmd.Result.Id}}
|
||||
err = GetDashboards(query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Id, ShouldEqual, dashId)
|
||||
})
|
||||
|
||||
Convey("Can query for provisioned dashboards", func() {
|
||||
query := &models.GetProvisionedDashboardDataQuery{Name: "default"}
|
||||
err := GetProvisionedDashboardDataQuery(query)
|
||||
|
||||
@@ -10,13 +10,13 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
func (ss *SqlStore) addPreferencesQueryAndCommandHandlers() {
|
||||
bus.AddHandler("sql", GetPreferences)
|
||||
bus.AddHandler("sql", GetPreferencesWithDefaults)
|
||||
bus.AddHandler("sql", ss.GetPreferencesWithDefaults)
|
||||
bus.AddHandler("sql", SavePreferences)
|
||||
}
|
||||
|
||||
func GetPreferencesWithDefaults(query *models.GetPreferencesWithDefaultsQuery) error {
|
||||
func (ss *SqlStore) GetPreferencesWithDefaults(query *models.GetPreferencesWithDefaultsQuery) error {
|
||||
params := make([]interface{}, 0)
|
||||
filter := ""
|
||||
|
||||
@@ -43,7 +43,7 @@ func GetPreferencesWithDefaults(query *models.GetPreferencesWithDefaultsQuery) e
|
||||
|
||||
res := &models.Preferences{
|
||||
Theme: setting.DefaultTheme,
|
||||
Timezone: "browser",
|
||||
Timezone: ss.Cfg.DateFormats.DefaultTimezone,
|
||||
HomeDashboardId: 0,
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,17 @@ import (
|
||||
|
||||
func TestPreferencesDataAccess(t *testing.T) {
|
||||
Convey("Testing preferences data access", t, func() {
|
||||
InitTestDB(t)
|
||||
ss := InitTestDB(t)
|
||||
|
||||
Convey("GetPreferencesWithDefaults with no saved preferences should return defaults", func() {
|
||||
setting.DefaultTheme = "light"
|
||||
ss.Cfg.DateFormats.DefaultTimezone = "UTC"
|
||||
|
||||
query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{}}
|
||||
err := GetPreferencesWithDefaults(query)
|
||||
err := ss.GetPreferencesWithDefaults(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.Theme, ShouldEqual, setting.DefaultTheme)
|
||||
So(query.Result.Timezone, ShouldEqual, "browser")
|
||||
So(query.Result.Theme, ShouldEqual, "light")
|
||||
So(query.Result.Timezone, ShouldEqual, "UTC")
|
||||
So(query.Result.HomeDashboardId, ShouldEqual, 0)
|
||||
})
|
||||
|
||||
@@ -29,7 +32,7 @@ func TestPreferencesDataAccess(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 1}}
|
||||
err = GetPreferencesWithDefaults(query)
|
||||
err = ss.GetPreferencesWithDefaults(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.HomeDashboardId, ShouldEqual, 4)
|
||||
})
|
||||
@@ -41,7 +44,7 @@ func TestPreferencesDataAccess(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 2}}
|
||||
err = GetPreferencesWithDefaults(query)
|
||||
err = ss.GetPreferencesWithDefaults(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.HomeDashboardId, ShouldEqual, 1)
|
||||
})
|
||||
@@ -57,7 +60,7 @@ func TestPreferencesDataAccess(t *testing.T) {
|
||||
query := &models.GetPreferencesWithDefaultsQuery{
|
||||
User: &models.SignedInUser{OrgId: 1, Teams: []int64{2, 3}},
|
||||
}
|
||||
err = GetPreferencesWithDefaults(query)
|
||||
err = ss.GetPreferencesWithDefaults(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.HomeDashboardId, ShouldEqual, 3)
|
||||
})
|
||||
@@ -71,7 +74,7 @@ func TestPreferencesDataAccess(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1}}
|
||||
err = GetPreferencesWithDefaults(query)
|
||||
err = ss.GetPreferencesWithDefaults(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.HomeDashboardId, ShouldEqual, 1)
|
||||
})
|
||||
@@ -89,7 +92,7 @@ func TestPreferencesDataAccess(t *testing.T) {
|
||||
query := &models.GetPreferencesWithDefaultsQuery{
|
||||
User: &models.SignedInUser{OrgId: 1, UserId: 1, Teams: []int64{2, 3}},
|
||||
}
|
||||
err = GetPreferencesWithDefaults(query)
|
||||
err = ss.GetPreferencesWithDefaults(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.HomeDashboardId, ShouldEqual, 4)
|
||||
})
|
||||
@@ -107,7 +110,7 @@ func TestPreferencesDataAccess(t *testing.T) {
|
||||
query := &models.GetPreferencesWithDefaultsQuery{
|
||||
User: &models.SignedInUser{OrgId: 1, UserId: 2},
|
||||
}
|
||||
err = GetPreferencesWithDefaults(query)
|
||||
err = ss.GetPreferencesWithDefaults(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.HomeDashboardId, ShouldEqual, 1)
|
||||
})
|
||||
|
||||
@@ -102,6 +102,7 @@ func (ss *SqlStore) Init() error {
|
||||
// Register handlers
|
||||
ss.addUserQueryAndCommandHandlers()
|
||||
ss.addAlertNotificationUidByIdHandler()
|
||||
ss.addPreferencesQueryAndCommandHandlers()
|
||||
|
||||
err = ss.logOrgsNotice()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
type DateFormats struct {
|
||||
FullDate string `json:"fullDate"`
|
||||
UseBrowserLocale bool `json:"useBrowserLocale"`
|
||||
Interval DateFormatIntervals `json:"interval"`
|
||||
DefaultTimezone string `json:"defaultTimezone"`
|
||||
}
|
||||
|
||||
type DateFormatIntervals struct {
|
||||
@@ -15,6 +22,23 @@ type DateFormatIntervals struct {
|
||||
Year string `json:"year"`
|
||||
}
|
||||
|
||||
const LocalBrowserTimezone = "browser"
|
||||
|
||||
func valueAsTimezone(section *ini.Section, keyName string, defaultValue string) (string, error) {
|
||||
timezone := section.Key(keyName).MustString(defaultValue)
|
||||
|
||||
if timezone == LocalBrowserTimezone {
|
||||
return LocalBrowserTimezone, nil
|
||||
}
|
||||
|
||||
location, err := time.LoadLocation(timezone)
|
||||
if err != nil {
|
||||
return LocalBrowserTimezone, err
|
||||
}
|
||||
|
||||
return location.String(), nil
|
||||
}
|
||||
|
||||
func (cfg *Cfg) readDateFormats() {
|
||||
dateFormats := cfg.Raw.Section("date_formats")
|
||||
cfg.DateFormats.FullDate = valueAsString(dateFormats, "full_date", "YYYY-MM-DD HH:mm:ss")
|
||||
@@ -25,4 +49,10 @@ func (cfg *Cfg) readDateFormats() {
|
||||
cfg.DateFormats.Interval.Month = valueAsString(dateFormats, "interval_month", "YYYY-MM")
|
||||
cfg.DateFormats.Interval.Year = "YYYY"
|
||||
cfg.DateFormats.UseBrowserLocale = dateFormats.Key("date_format_use_browser_locale").MustBool(false)
|
||||
|
||||
timezone, err := valueAsTimezone(dateFormats, "default_timezone", LocalBrowserTimezone)
|
||||
if err != nil {
|
||||
cfg.Logger.Warn("Unknown timezone as default_timezone", "err", err)
|
||||
}
|
||||
cfg.DateFormats.DefaultTimezone = timezone
|
||||
}
|
||||
|
||||
37
pkg/setting/date_formats_test.go
Normal file
37
pkg/setting/date_formats_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValueAsTimezone(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
output string
|
||||
hasErr bool
|
||||
}{
|
||||
"browser": {"browser", false},
|
||||
"UTC": {"UTC", false},
|
||||
"utc": {"browser", true},
|
||||
"Amsterdam": {"browser", true},
|
||||
"europe/amsterdam": {"browser", true},
|
||||
"Europe/Amsterdam": {"Europe/Amsterdam", false},
|
||||
}
|
||||
|
||||
sec, err := ini.Empty().NewSection("test")
|
||||
assert.NoError(t, err)
|
||||
key, err := sec.NewKey("test", "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
for input, expected := range tests {
|
||||
key.SetValue(input)
|
||||
|
||||
output, err := valueAsTimezone(sec, "test", "default")
|
||||
|
||||
assert.Equal(t, expected.hasErr, err != nil, "Invalid has err for input: %s err: %v", input, err)
|
||||
assert.Equal(t, expected.output, output, "Invalid output for input: %s", input)
|
||||
}
|
||||
}
|
||||
@@ -140,10 +140,10 @@ var (
|
||||
ViewersCanEdit bool
|
||||
|
||||
// Http auth
|
||||
AdminUser string
|
||||
AdminPassword string
|
||||
LoginCookieName string
|
||||
LoginMaxLifetimeDays int
|
||||
AdminUser string
|
||||
AdminPassword string
|
||||
LoginCookieName string
|
||||
LoginMaxLifetime time.Duration
|
||||
|
||||
AnonymousEnabled bool
|
||||
AnonymousOrgName string
|
||||
@@ -278,8 +278,8 @@ type Cfg struct {
|
||||
|
||||
// Auth
|
||||
LoginCookieName string
|
||||
LoginMaxInactiveLifetimeDays int
|
||||
LoginMaxLifetimeDays int
|
||||
LoginMaxInactiveLifetime time.Duration
|
||||
LoginMaxLifetime time.Duration
|
||||
TokenRotationIntervalMinutes int
|
||||
|
||||
// OAuth
|
||||
@@ -315,11 +315,6 @@ func (c Cfg) IsExpressionsEnabled() bool {
|
||||
return c.FeatureToggles["expressions"]
|
||||
}
|
||||
|
||||
// IsStandaloneAlertsEnabled returns whether the standalone alerts feature is enabled.
|
||||
func (c Cfg) IsStandaloneAlertsEnabled() bool {
|
||||
return c.FeatureToggles["standaloneAlerts"]
|
||||
}
|
||||
|
||||
// IsLiveEnabled returns if grafana live should be enabled
|
||||
func (c Cfg) IsLiveEnabled() bool {
|
||||
return c.FeatureToggles["live"]
|
||||
@@ -951,15 +946,38 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func readAuthSettings(iniFile *ini.File, cfg *Cfg) error {
|
||||
func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
|
||||
auth := iniFile.Section("auth")
|
||||
|
||||
LoginCookieName = valueAsString(auth, "login_cookie_name", "grafana_session")
|
||||
cfg.LoginCookieName = LoginCookieName
|
||||
cfg.LoginMaxInactiveLifetimeDays = auth.Key("login_maximum_inactive_lifetime_days").MustInt(7)
|
||||
maxInactiveDaysVal := auth.Key("login_maximum_inactive_lifetime_days").MustString("")
|
||||
if maxInactiveDaysVal != "" {
|
||||
maxInactiveDaysVal = fmt.Sprintf("%sd", maxInactiveDaysVal)
|
||||
cfg.Logger.Warn("[Deprecated] the configuration setting 'login_maximum_inactive_lifetime_days' is deprecated, please use 'login_maximum_inactive_lifetime_duration' instead")
|
||||
} else {
|
||||
maxInactiveDaysVal = "7d"
|
||||
}
|
||||
maxInactiveDurationVal := valueAsString(auth, "login_maximum_inactive_lifetime_duration", maxInactiveDaysVal)
|
||||
cfg.LoginMaxInactiveLifetime, err = gtime.ParseInterval(maxInactiveDurationVal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
maxLifetimeDaysVal := auth.Key("login_maximum_lifetime_days").MustString("")
|
||||
if maxLifetimeDaysVal != "" {
|
||||
maxLifetimeDaysVal = fmt.Sprintf("%sd", maxLifetimeDaysVal)
|
||||
cfg.Logger.Warn("[Deprecated] the configuration setting 'login_maximum_lifetime_days' is deprecated, please use 'login_maximum_lifetime_duration' instead")
|
||||
} else {
|
||||
maxLifetimeDaysVal = "7d"
|
||||
}
|
||||
maxLifetimeDurationVal := valueAsString(auth, "login_maximum_lifetime_duration", maxLifetimeDaysVal)
|
||||
cfg.LoginMaxLifetime, err = gtime.ParseInterval(maxLifetimeDurationVal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
LoginMaxLifetime = cfg.LoginMaxLifetime
|
||||
|
||||
LoginMaxLifetimeDays = auth.Key("login_maximum_lifetime_days").MustInt(30)
|
||||
cfg.LoginMaxLifetimeDays = LoginMaxLifetimeDays
|
||||
cfg.ApiKeyMaxSecondsToLive = auth.Key("api_key_max_seconds_to_live").MustInt64(-1)
|
||||
|
||||
cfg.TokenRotationIntervalMinutes = auth.Key("token_rotation_interval_minutes").MustInt(10)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -314,3 +315,49 @@ func TestParseAppUrlAndSubUrl(t *testing.T) {
|
||||
require.Equal(t, tc.expectedAppSubURL, appSubURL)
|
||||
}
|
||||
}
|
||||
func TestAuthDurationSettings(t *testing.T) {
|
||||
f := ini.Empty()
|
||||
cfg := NewCfg()
|
||||
sec, err := f.NewSection("auth")
|
||||
require.NoError(t, err)
|
||||
_, err = sec.NewKey("login_maximum_inactive_lifetime_days", "10")
|
||||
require.NoError(t, err)
|
||||
_, err = sec.NewKey("login_maximum_inactive_lifetime_duration", "")
|
||||
require.NoError(t, err)
|
||||
maxInactiveDaysTest, _ := time.ParseDuration("240h")
|
||||
err = readAuthSettings(f, cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, maxInactiveDaysTest, cfg.LoginMaxInactiveLifetime)
|
||||
|
||||
f = ini.Empty()
|
||||
sec, err = f.NewSection("auth")
|
||||
require.NoError(t, err)
|
||||
_, err = sec.NewKey("login_maximum_inactive_lifetime_duration", "824h")
|
||||
require.NoError(t, err)
|
||||
maxInactiveDurationTest, _ := time.ParseDuration("824h")
|
||||
err = readAuthSettings(f, cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, maxInactiveDurationTest, cfg.LoginMaxInactiveLifetime)
|
||||
|
||||
f = ini.Empty()
|
||||
sec, err = f.NewSection("auth")
|
||||
require.NoError(t, err)
|
||||
_, err = sec.NewKey("login_maximum_lifetime_days", "24")
|
||||
require.NoError(t, err)
|
||||
_, err = sec.NewKey("login_maximum_lifetime_duration", "")
|
||||
require.NoError(t, err)
|
||||
maxLifetimeDaysTest, _ := time.ParseDuration("576h")
|
||||
err = readAuthSettings(f, cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, maxLifetimeDaysTest, cfg.LoginMaxLifetime)
|
||||
|
||||
f = ini.Empty()
|
||||
sec, err = f.NewSection("auth")
|
||||
require.NoError(t, err)
|
||||
_, err = sec.NewKey("login_maximum_lifetime_duration", "824h")
|
||||
require.NoError(t, err)
|
||||
maxLifetimeDurationTest, _ := time.ParseDuration("824h")
|
||||
err = readAuthSettings(f, cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, maxLifetimeDurationTest, cfg.LoginMaxLifetime)
|
||||
}
|
||||
|
||||
@@ -211,6 +211,11 @@ func TestAppInsightsPluginRoutes(t *testing.T) {
|
||||
func TestInsightsDimensionsUnmarshalJSON(t *testing.T) {
|
||||
a := []byte(`"foo"`)
|
||||
b := []byte(`["foo"]`)
|
||||
c := []byte(`["none"]`)
|
||||
d := []byte(`["None"]`)
|
||||
e := []byte("null")
|
||||
f := []byte(`""`)
|
||||
g := []byte(`"none"`)
|
||||
|
||||
var as InsightsDimensions
|
||||
var bs InsightsDimensions
|
||||
@@ -223,4 +228,29 @@ func TestInsightsDimensionsUnmarshalJSON(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, []string{"foo"}, []string(bs))
|
||||
|
||||
var cs InsightsDimensions
|
||||
err = json.Unmarshal(c, &cs)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cs)
|
||||
|
||||
var ds InsightsDimensions
|
||||
err = json.Unmarshal(d, &ds)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, ds)
|
||||
|
||||
var es InsightsDimensions
|
||||
err = json.Unmarshal(e, &es)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, es)
|
||||
|
||||
var fs InsightsDimensions
|
||||
err = json.Unmarshal(f, &fs)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, fs)
|
||||
|
||||
var gs InsightsDimensions
|
||||
err = json.Unmarshal(g, &gs)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, gs)
|
||||
}
|
||||
|
||||
@@ -170,7 +170,14 @@ func (s *InsightsDimensions) UnmarshalJSON(data []byte) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*s = InsightsDimensions(sa)
|
||||
dimensions := []string{}
|
||||
for _, v := range sa {
|
||||
if v == "none" || v == "None" {
|
||||
continue
|
||||
}
|
||||
dimensions = append(dimensions, v)
|
||||
}
|
||||
*s = InsightsDimensions(dimensions)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@grafana-plugins/input-datasource",
|
||||
"version": "7.2.0-pre.0",
|
||||
"version": "7.2.0-beta.2",
|
||||
"description": "Input Datasource",
|
||||
"private": true,
|
||||
"repository": {
|
||||
@@ -16,9 +16,9 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@grafana/data": "7.2.0-pre.0",
|
||||
"@grafana/toolkit": "7.2.0-pre.0",
|
||||
"@grafana/ui": "7.2.0-pre.0"
|
||||
"@grafana/data": "7.2.0-beta.2",
|
||||
"@grafana/toolkit": "7.2.0-beta.2",
|
||||
"@grafana/ui": "7.2.0-beta.2"
|
||||
},
|
||||
"volta": {
|
||||
"node": "12.16.2"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { HorizontalGroup, Icon, renderOrCallToRender, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { Icon, renderOrCallToRender, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { css } from 'emotion';
|
||||
import { useUpdateEffect } from 'react-use';
|
||||
@@ -66,33 +66,36 @@ export const QueryOperationRow: React.FC<QueryOperationRowProps> = ({
|
||||
|
||||
const rowHeader = (
|
||||
<div className={styles.header}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<div className={styles.titleWrapper} onClick={onRowToggle} aria-label="Query operation row title">
|
||||
{draggable && (
|
||||
<Icon title="Drag and drop to reorder" name="draggabledots" size="lg" className={styles.dragIcon} />
|
||||
)}
|
||||
<Icon name={isContentVisible ? 'angle-down' : 'angle-right'} className={styles.collapseIcon} />
|
||||
{title && <span className={styles.title}>{titleElement}</span>}
|
||||
{headerElement}
|
||||
</div>
|
||||
{actions && actionsElement}
|
||||
</HorizontalGroup>
|
||||
<div className={styles.titleWrapper} onClick={onRowToggle} aria-label="Query operation row title">
|
||||
<Icon name={isContentVisible ? 'angle-down' : 'angle-right'} className={styles.collapseIcon} />
|
||||
{title && <span className={styles.title}>{titleElement}</span>}
|
||||
{headerElement}
|
||||
</div>
|
||||
{actions && actionsElement}
|
||||
{draggable && (
|
||||
<Icon title="Drag and drop to reorder" name="draggabledots" size="lg" className={styles.dragIcon} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return draggable ? (
|
||||
<Draggable draggableId={id} index={index}>
|
||||
{provided => {
|
||||
return (
|
||||
<>
|
||||
<div ref={provided.innerRef} className={styles.wrapper} {...provided.draggableProps}>
|
||||
<div {...provided.dragHandleProps}>{rowHeader}</div>
|
||||
{isContentVisible && <div className={styles.content}>{children}</div>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Draggable>
|
||||
) : (
|
||||
|
||||
if (draggable) {
|
||||
return (
|
||||
<Draggable draggableId={id} index={index}>
|
||||
{provided => {
|
||||
return (
|
||||
<>
|
||||
<div ref={provided.innerRef} className={styles.wrapper} {...provided.draggableProps}>
|
||||
<div {...provided.dragHandleProps}>{rowHeader}</div>
|
||||
{isContentVisible && <div className={styles.content}>{children}</div>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{rowHeader}
|
||||
{isContentVisible && <div className={styles.content}>{children}</div>}
|
||||
@@ -116,8 +119,11 @@ const getQueryOperationRowStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
justify-content: space-between;
|
||||
`,
|
||||
dragIcon: css`
|
||||
opacity: 0.4;
|
||||
cursor: drag;
|
||||
color: ${theme.colors.textWeak};
|
||||
&:hover {
|
||||
color: ${theme.colors.text};
|
||||
}
|
||||
`,
|
||||
collapseIcon: css`
|
||||
color: ${theme.colors.textWeak};
|
||||
@@ -128,7 +134,10 @@ const getQueryOperationRowStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
titleWrapper: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
margin-right: ${theme.spacing.sm};
|
||||
`,
|
||||
title: css`
|
||||
font-weight: ${theme.typography.weight.semibold};
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
import { getThemeColor } from 'app/core/utils/colors';
|
||||
|
||||
import { deduplicateLogRowsById } from 'app/core/utils/explore';
|
||||
import { decimalSIPrefix } from '@grafana/data/src/valueFormats/symbolFormatters';
|
||||
import { SIPrefix } from '@grafana/data/src/valueFormats/symbolFormatters';
|
||||
|
||||
export const LogLevelColor = {
|
||||
[LogLevel.critical]: colors[7],
|
||||
@@ -437,7 +437,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
|
||||
}
|
||||
|
||||
if (totalBytes > 0) {
|
||||
const { text, suffix } = decimalSIPrefix('B')(totalBytes);
|
||||
const { text, suffix } = SIPrefix('B')(totalBytes);
|
||||
meta.push({
|
||||
label: 'Total bytes processed',
|
||||
value: `${text} ${suffix}`,
|
||||
|
||||
@@ -342,6 +342,7 @@ export class BackendSrv implements BackendService {
|
||||
// in throwIfEmpty we'll then throw an cancelled error and then we'll return the correct result in the catchError or rethrow
|
||||
throwIfEmpty(() => ({
|
||||
type: DataQueryErrorType.Cancelled,
|
||||
cancelled: true,
|
||||
data: null,
|
||||
status: this.HTTP_REQUEST_CANCELED,
|
||||
statusText: 'Request was aborted',
|
||||
|
||||
@@ -353,6 +353,7 @@ describe('backendSrv', () => {
|
||||
|
||||
expect(slowError).toEqual({
|
||||
type: DataQueryErrorType.Cancelled,
|
||||
cancelled: true,
|
||||
data: null,
|
||||
status: -1,
|
||||
statusText: 'Request was aborted',
|
||||
|
||||
@@ -86,7 +86,7 @@ export class EditNotificationChannelPage extends PureComponent<Props> {
|
||||
<h2 className="page-sub-heading">Edit notification channel</h2>
|
||||
{notificationChannel && notificationChannel.id > 0 ? (
|
||||
<Form
|
||||
width={600}
|
||||
maxWidth={600}
|
||||
onSubmit={this.onSubmit}
|
||||
defaultValues={{
|
||||
...notificationChannel,
|
||||
|
||||
@@ -52,7 +52,7 @@ class NewNotificationChannelPage extends PureComponent<Props> {
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents>
|
||||
<h2 className="page-sub-heading">New notification channel</h2>
|
||||
<Form onSubmit={this.onSubmit} validateOn="onChange" defaultValues={defaultValues}>
|
||||
<Form onSubmit={this.onSubmit} validateOn="onChange" defaultValues={defaultValues} maxWidth={600}>
|
||||
{({ register, errors, control, getValues, watch }) => {
|
||||
const selectedChannel = notificationChannelTypes.find(c => c.value === getValues().type.value);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { FC } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { CollapsableSection, Field, Input, InputControl, Select } from '@grafana/ui';
|
||||
import { Field, Input, InputControl, Select } from '@grafana/ui';
|
||||
import { NotificationChannelOptions } from './NotificationChannelOptions';
|
||||
import { NotificationSettingsProps } from './NotificationChannelForm';
|
||||
import { NotificationChannelSecureFields, NotificationChannelType } from '../../../types';
|
||||
@@ -23,7 +23,7 @@ export const BasicSettings: FC<Props> = ({
|
||||
resetSecureField,
|
||||
}) => {
|
||||
return (
|
||||
<CollapsableSection label="Channel" isOpen>
|
||||
<>
|
||||
<Field label="Name" invalid={!!errors.name} error={errors.name && errors.name.message}>
|
||||
<Input name="name" ref={register({ required: 'Name is required' })} />
|
||||
</Field>
|
||||
@@ -39,6 +39,6 @@ export const BasicSettings: FC<Props> = ({
|
||||
errors={errors}
|
||||
control={control}
|
||||
/>
|
||||
</CollapsableSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -41,9 +41,14 @@ export const NotificationChannelForm: FC<Props> = ({
|
||||
}, []);
|
||||
|
||||
const currentFormValues = getValues();
|
||||
return selectedChannel ? (
|
||||
<>
|
||||
<div className={styles.basicSettings}>
|
||||
|
||||
if (!selectedChannel) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.formContainer}>
|
||||
<div className={styles.formItem}>
|
||||
<BasicSettings
|
||||
selectedChannel={selectedChannel}
|
||||
channels={selectableChannels}
|
||||
@@ -54,8 +59,10 @@ export const NotificationChannelForm: FC<Props> = ({
|
||||
errors={errors}
|
||||
control={control}
|
||||
/>
|
||||
{/* If there are no non-required fields, don't render this section*/}
|
||||
{selectedChannel.options.filter(o => !o.required).length > 0 && (
|
||||
</div>
|
||||
{/* If there are no non-required fields, don't render this section*/}
|
||||
{selectedChannel.options.filter(o => !o.required).length > 0 && (
|
||||
<div className={styles.formItem}>
|
||||
<ChannelSettings
|
||||
selectedChannel={selectedChannel}
|
||||
secureFields={secureFields}
|
||||
@@ -65,7 +72,9 @@ export const NotificationChannelForm: FC<Props> = ({
|
||||
errors={errors}
|
||||
control={control}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.formItem}>
|
||||
<NotificationSettings
|
||||
imageRendererAvailable={imageRendererAvailable}
|
||||
currentFormValues={currentFormValues}
|
||||
@@ -74,27 +83,32 @@ export const NotificationChannelForm: FC<Props> = ({
|
||||
control={control}
|
||||
/>
|
||||
</div>
|
||||
<HorizontalGroup>
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="button" variant="secondary" onClick={() => onTestChannel(getValues({ nest: true }))}>
|
||||
Test
|
||||
</Button>
|
||||
<a href="/alerting/notifications">
|
||||
<Button type="button" variant="secondary">
|
||||
Back
|
||||
<div className={styles.formButtons}>
|
||||
<HorizontalGroup>
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="button" variant="secondary" onClick={() => onTestChannel(getValues({ nest: true }))}>
|
||||
Test
|
||||
</Button>
|
||||
</a>
|
||||
</HorizontalGroup>
|
||||
</>
|
||||
) : (
|
||||
<Spinner />
|
||||
<a href="/alerting/notifications">
|
||||
<Button type="button" variant="secondary">
|
||||
Back
|
||||
</Button>
|
||||
</a>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
basicSettings: css`
|
||||
margin-bottom: ${theme.spacing.xl};
|
||||
formContainer: css``,
|
||||
formItem: css`
|
||||
flex-grow: 1;
|
||||
padding-top: ${theme.spacing.md};
|
||||
`,
|
||||
formButtons: css`
|
||||
padding-top: ${theme.spacing.xl};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -59,9 +59,14 @@ export const NotificationChannelOptions: FC<Props> = ({
|
||||
<Input
|
||||
readOnly={true}
|
||||
value="Configured"
|
||||
addonAfter={
|
||||
<Button onClick={() => onResetSecureField(option.propertyName)} variant="secondary" type="button">
|
||||
Reset
|
||||
suffix={
|
||||
<Button
|
||||
onClick={() => onResetSecureField(option.propertyName)}
|
||||
variant="link"
|
||||
type="button"
|
||||
size="sm"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -7,12 +7,30 @@ import coreModule from 'app/core/core_module';
|
||||
// Utils & Services
|
||||
import { dedupAnnotations } from './events_processing';
|
||||
// Types
|
||||
import { DashboardModel, PanelModel } from '../dashboard/state';
|
||||
import { AnnotationEvent, AppEvents, DataSourceApi, PanelEvents, TimeRange, CoreApp } from '@grafana/data';
|
||||
import { DashboardModel } from '../dashboard/state';
|
||||
import {
|
||||
AnnotationEvent,
|
||||
AppEvents,
|
||||
DataSourceApi,
|
||||
PanelEvents,
|
||||
rangeUtil,
|
||||
DataQueryRequest,
|
||||
CoreApp,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
import { getBackendSrv, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { appEvents } from 'app/core/core';
|
||||
import { getTimeSrv } from '../dashboard/services/TimeSrv';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { AnnotationQueryResponse, AnnotationQueryOptions } from './types';
|
||||
import { standardAnnotationSupport } from './standardAnnotationSupport';
|
||||
import { runRequest } from '../dashboard/state/runRequest';
|
||||
|
||||
let counter = 100;
|
||||
function getNextRequestId() {
|
||||
return 'AQ' + counter++;
|
||||
}
|
||||
|
||||
export class AnnotationsSrv {
|
||||
globalAnnotationsPromise: any;
|
||||
@@ -32,7 +50,7 @@ export class AnnotationsSrv {
|
||||
this.datasourcePromises = null;
|
||||
}
|
||||
|
||||
getAnnotations(options: { dashboard: DashboardModel; panel: PanelModel; range: TimeRange }) {
|
||||
getAnnotations(options: AnnotationQueryOptions) {
|
||||
return Promise.all([this.getGlobalAnnotations(options), this.getAlertStates(options)])
|
||||
.then(results => {
|
||||
// combine the annotations and flatten results
|
||||
@@ -103,7 +121,7 @@ export class AnnotationsSrv {
|
||||
return this.alertStatesPromise;
|
||||
}
|
||||
|
||||
getGlobalAnnotations(options: { dashboard: DashboardModel; panel: PanelModel; range: TimeRange }) {
|
||||
getGlobalAnnotations(options: AnnotationQueryOptions) {
|
||||
const dashboard = options.dashboard;
|
||||
|
||||
if (this.globalAnnotationsPromise) {
|
||||
@@ -114,9 +132,6 @@ export class AnnotationsSrv {
|
||||
const promises = [];
|
||||
const dsPromises = [];
|
||||
|
||||
// No more points than pixels
|
||||
const maxDataPoints = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
|
||||
for (const annotation of dashboard.annotations.list) {
|
||||
if (!annotation.enable) {
|
||||
continue;
|
||||
@@ -130,21 +145,21 @@ export class AnnotationsSrv {
|
||||
promises.push(
|
||||
datasourcePromise
|
||||
.then((datasource: DataSourceApi) => {
|
||||
if (!datasource.annotationQuery) {
|
||||
return [];
|
||||
// Use the legacy annotationQuery unless annotation support is explicitly defined
|
||||
if (datasource.annotationQuery && !datasource.annotations) {
|
||||
return datasource.annotationQuery({
|
||||
range,
|
||||
rangeRaw: range.raw,
|
||||
annotation: annotation,
|
||||
dashboard: dashboard,
|
||||
});
|
||||
}
|
||||
|
||||
// Add interval to annotation queries
|
||||
const interval = kbn.calculateInterval(range, maxDataPoints, datasource.interval);
|
||||
|
||||
return datasource.annotationQuery({
|
||||
...interval,
|
||||
app: CoreApp.Dashboard,
|
||||
range,
|
||||
rangeRaw: range.raw,
|
||||
annotation: annotation,
|
||||
dashboard: dashboard,
|
||||
});
|
||||
// Note: future annotatoin lifecycle will use observables directly
|
||||
return executeAnnotationQuery(options, datasource, annotation)
|
||||
.toPromise()
|
||||
.then(res => {
|
||||
return res.events ?? [];
|
||||
});
|
||||
})
|
||||
.then(results => {
|
||||
// store response in annotation object if this is a snapshot call
|
||||
@@ -195,4 +210,63 @@ export class AnnotationsSrv {
|
||||
}
|
||||
}
|
||||
|
||||
export function executeAnnotationQuery(
|
||||
options: AnnotationQueryOptions,
|
||||
datasource: DataSourceApi,
|
||||
savedJsonAnno: any
|
||||
): Observable<AnnotationQueryResponse> {
|
||||
const processor = {
|
||||
...standardAnnotationSupport,
|
||||
...datasource.annotations,
|
||||
};
|
||||
|
||||
const annotation = processor.prepareAnnotation!(savedJsonAnno);
|
||||
if (!annotation) {
|
||||
return of({});
|
||||
}
|
||||
|
||||
const query = processor.prepareQuery!(annotation);
|
||||
if (!query) {
|
||||
return of({});
|
||||
}
|
||||
|
||||
// No more points than pixels
|
||||
const maxDataPoints = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
|
||||
// Add interval to annotation queries
|
||||
const interval = rangeUtil.calculateInterval(options.range, maxDataPoints, datasource.interval);
|
||||
|
||||
const scopedVars: ScopedVars = {
|
||||
__interval: { text: interval.interval, value: interval.interval },
|
||||
__interval_ms: { text: interval.intervalMs.toString(), value: interval.intervalMs },
|
||||
__annotation: { text: annotation.name, value: annotation },
|
||||
};
|
||||
|
||||
const queryRequest: DataQueryRequest = {
|
||||
startTime: Date.now(),
|
||||
requestId: getNextRequestId(),
|
||||
range: options.range,
|
||||
maxDataPoints,
|
||||
scopedVars,
|
||||
...interval,
|
||||
app: CoreApp.Dashboard,
|
||||
|
||||
timezone: options.dashboard.timezone,
|
||||
|
||||
targets: [
|
||||
{
|
||||
...query,
|
||||
refId: 'Anno',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return runRequest(datasource, queryRequest).pipe(
|
||||
map(panelData => {
|
||||
const events = panelData.series ? processor.processEvents!(annotation, panelData.series) : [];
|
||||
return { panelData, events };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
coreModule.service('annotationsSrv', AnnotationsSrv);
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import {
|
||||
SelectableValue,
|
||||
getFieldDisplayName,
|
||||
AnnotationEvent,
|
||||
AnnotationEventMappings,
|
||||
AnnotationEventFieldMapping,
|
||||
formattedValueToString,
|
||||
AnnotationEventFieldSource,
|
||||
getValueFormat,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { annotationEventNames, AnnotationFieldInfo } from '../standardAnnotationSupport';
|
||||
import { Select, Tooltip, Icon } from '@grafana/ui';
|
||||
import { AnnotationQueryResponse } from '../types';
|
||||
|
||||
// const valueOptions: Array<SelectableValue<AnnotationEventFieldSource>> = [
|
||||
// { value: AnnotationEventFieldSource.Field, label: 'Field', description: 'Set the field value from a response field' },
|
||||
// { value: AnnotationEventFieldSource.Text, label: 'Text', description: 'Enter direct text for the value' },
|
||||
// { value: AnnotationEventFieldSource.Skip, label: 'Skip', description: 'Hide this field' },
|
||||
// ];
|
||||
|
||||
interface Props {
|
||||
response?: AnnotationQueryResponse;
|
||||
|
||||
mappings?: AnnotationEventMappings;
|
||||
|
||||
change: (mappings?: AnnotationEventMappings) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
fieldNames: Array<SelectableValue<string>>;
|
||||
}
|
||||
|
||||
export class AnnotationFieldMapper extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fieldNames: [],
|
||||
};
|
||||
}
|
||||
|
||||
updateFields = () => {
|
||||
const frame = this.props.response?.panelData?.series[0];
|
||||
if (frame && frame.fields) {
|
||||
const fieldNames = frame.fields.map(f => {
|
||||
const name = getFieldDisplayName(f, frame);
|
||||
|
||||
let description = '';
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
if (i > 0) {
|
||||
description += ', ';
|
||||
}
|
||||
if (i > 2) {
|
||||
description += '...';
|
||||
break;
|
||||
}
|
||||
description += f.values.get(i);
|
||||
}
|
||||
|
||||
if (description.length > 50) {
|
||||
description = description.substring(0, 50) + '...';
|
||||
}
|
||||
|
||||
return {
|
||||
label: `${name} (${f.type})`,
|
||||
value: name,
|
||||
description,
|
||||
};
|
||||
});
|
||||
this.setState({ fieldNames });
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
componentDidUpdate(oldProps: Props) {
|
||||
if (oldProps.response !== this.props.response) {
|
||||
this.updateFields();
|
||||
}
|
||||
}
|
||||
|
||||
onFieldSourceChange = (k: keyof AnnotationEvent, v: SelectableValue<AnnotationEventFieldSource>) => {
|
||||
const mappings = this.props.mappings || {};
|
||||
const mapping = mappings[k] || {};
|
||||
|
||||
this.props.change({
|
||||
...mappings,
|
||||
[k]: {
|
||||
...mapping,
|
||||
source: v.value || AnnotationEventFieldSource.Field,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onFieldNameChange = (k: keyof AnnotationEvent, v: SelectableValue<string>) => {
|
||||
const mappings = this.props.mappings || {};
|
||||
const mapping = mappings[k] || {};
|
||||
|
||||
this.props.change({
|
||||
...mappings,
|
||||
[k]: {
|
||||
...mapping,
|
||||
value: v.value,
|
||||
source: AnnotationEventFieldSource.Field,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
renderRow(row: AnnotationFieldInfo, mapping: AnnotationEventFieldMapping, first?: AnnotationEvent) {
|
||||
const { fieldNames } = this.state;
|
||||
|
||||
let picker = fieldNames;
|
||||
const current = mapping.value;
|
||||
let currentValue = fieldNames.find(f => current === f.value);
|
||||
if (current) {
|
||||
picker = [...fieldNames];
|
||||
if (!currentValue) {
|
||||
picker.push({
|
||||
label: current,
|
||||
value: current,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let value = first ? first[row.key] : '';
|
||||
if (value && row.key.startsWith('time')) {
|
||||
const fmt = getValueFormat('dateTimeAsIso');
|
||||
value = formattedValueToString(fmt(value as number));
|
||||
}
|
||||
if (value === null || value === undefined) {
|
||||
value = ''; // empty string
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={row.key}>
|
||||
<td>
|
||||
{row.key}{' '}
|
||||
{row.help && (
|
||||
<Tooltip content={row.help}>
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</td>
|
||||
{/* <td>
|
||||
<Select
|
||||
value={valueOptions.find(v => v.value === mapping.source) || valueOptions[0]}
|
||||
options={valueOptions}
|
||||
onChange={(v: SelectableValue<AnnotationEventFieldSource>) => {
|
||||
this.onFieldSourceChange(row.key, v);
|
||||
}}
|
||||
/>
|
||||
</td> */}
|
||||
<td>
|
||||
<Select
|
||||
value={currentValue}
|
||||
options={picker}
|
||||
placeholder={row.placeholder || row.key}
|
||||
onChange={(v: SelectableValue<string>) => {
|
||||
this.onFieldNameChange(row.key, v);
|
||||
}}
|
||||
noOptionsMessage="Unknown field names"
|
||||
allowCustomValue={true}
|
||||
/>
|
||||
</td>
|
||||
<td>{`${value}`}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const first = this.props.response?.events?.[0];
|
||||
const mappings = this.props.mappings || {};
|
||||
|
||||
return (
|
||||
<table className="filter-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Annotation</th>
|
||||
<th>From</th>
|
||||
<th>First Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{annotationEventNames.map(row => {
|
||||
return this.renderRow(row, mappings[row.key] || {}, first);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { AnnotationEventMappings, DataQuery, LoadingState, DataSourceApi, AnnotationQuery } from '@grafana/data';
|
||||
import { Spinner, Icon, IconName, Button } from '@grafana/ui';
|
||||
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { cx, css } from 'emotion';
|
||||
import { standardAnnotationSupport } from '../standardAnnotationSupport';
|
||||
import { executeAnnotationQuery } from '../annotations_srv';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { AnnotationQueryResponse } from '../types';
|
||||
import { AnnotationFieldMapper } from './AnnotationResultMapper';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
interface Props {
|
||||
datasource: DataSourceApi;
|
||||
annotation: AnnotationQuery<DataQuery>;
|
||||
change: (annotation: AnnotationQuery<DataQuery>) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
running?: boolean;
|
||||
response?: AnnotationQueryResponse;
|
||||
}
|
||||
|
||||
export default class StandardAnnotationQueryEditor extends PureComponent<Props, State> {
|
||||
state = {} as State;
|
||||
|
||||
componentDidMount() {
|
||||
this.verifyDataSource();
|
||||
}
|
||||
|
||||
componentDidUpdate(oldProps: Props) {
|
||||
if (this.props.annotation !== oldProps.annotation) {
|
||||
this.verifyDataSource();
|
||||
}
|
||||
}
|
||||
|
||||
verifyDataSource() {
|
||||
const { datasource, annotation } = this.props;
|
||||
|
||||
// Handle any migration issues
|
||||
const processor = {
|
||||
...standardAnnotationSupport,
|
||||
...datasource.annotations,
|
||||
};
|
||||
|
||||
const fixed = processor.prepareAnnotation!(annotation);
|
||||
if (fixed !== annotation) {
|
||||
this.props.change(fixed);
|
||||
} else {
|
||||
this.onRunQuery();
|
||||
}
|
||||
}
|
||||
|
||||
onRunQuery = async () => {
|
||||
const { datasource, annotation } = this.props;
|
||||
this.setState({
|
||||
running: true,
|
||||
});
|
||||
const response = await executeAnnotationQuery(
|
||||
{
|
||||
range: getTimeSrv().timeRange(),
|
||||
panel: {} as PanelModel,
|
||||
dashboard: getDashboardSrv().getCurrent(),
|
||||
},
|
||||
datasource,
|
||||
annotation
|
||||
).toPromise();
|
||||
this.setState({
|
||||
running: false,
|
||||
response,
|
||||
});
|
||||
};
|
||||
|
||||
onQueryChange = (target: DataQuery) => {
|
||||
this.props.change({
|
||||
...this.props.annotation,
|
||||
target,
|
||||
});
|
||||
};
|
||||
|
||||
onMappingChange = (mappings: AnnotationEventMappings) => {
|
||||
this.props.change({
|
||||
...this.props.annotation,
|
||||
mappings,
|
||||
});
|
||||
};
|
||||
|
||||
renderStatus() {
|
||||
const { response, running } = this.state;
|
||||
let rowStyle = 'alert-info';
|
||||
let text = '...';
|
||||
let icon: IconName | undefined = undefined;
|
||||
|
||||
if (running || response?.panelData?.state === LoadingState.Loading || !response) {
|
||||
text = 'loading...';
|
||||
} else {
|
||||
const { events, panelData } = response;
|
||||
|
||||
if (panelData?.error) {
|
||||
rowStyle = 'alert-error';
|
||||
icon = 'exclamation-triangle';
|
||||
text = panelData.error.message ?? 'error';
|
||||
} else if (!events?.length) {
|
||||
rowStyle = 'alert-warning';
|
||||
icon = 'exclamation-triangle';
|
||||
text = 'No events found';
|
||||
} else {
|
||||
const frame = panelData?.series[0];
|
||||
|
||||
text = `${events.length} events (from ${frame?.fields.length} fields)`;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
rowStyle,
|
||||
css`
|
||||
margin: 4px 0px;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
{icon && (
|
||||
<>
|
||||
<Icon name={icon} />
|
||||
|
||||
</>
|
||||
)}
|
||||
{text}
|
||||
</div>
|
||||
<div>
|
||||
{running ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Button variant="secondary" size="xs" onClick={this.onRunQuery}>
|
||||
TEST
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { datasource, annotation } = this.props;
|
||||
const { response } = this.state;
|
||||
|
||||
// Find the annotaiton runner
|
||||
let QueryEditor = datasource.annotations?.QueryEditor || datasource.components?.QueryEditor;
|
||||
if (!QueryEditor) {
|
||||
return <div>Annotations are not supported. This datasource needs to export a QueryEditor</div>;
|
||||
}
|
||||
|
||||
const query = annotation.target ?? { refId: 'Anno' };
|
||||
return (
|
||||
<>
|
||||
<QueryEditor
|
||||
key={datasource?.name}
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={this.onQueryChange}
|
||||
onRunQuery={this.onRunQuery}
|
||||
data={response?.panelData}
|
||||
range={getTimeSrv().timeRange()}
|
||||
/>
|
||||
{this.renderStatus()}
|
||||
|
||||
<AnnotationFieldMapper response={response} mappings={annotation.mappings} change={this.onMappingChange} />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Careful to use a unique directive name! many plugins already use "annotationEditor" and have conflicts
|
||||
coreModule.directive('standardAnnotationEditor', [
|
||||
'reactDirective',
|
||||
(reactDirective: any) => {
|
||||
return reactDirective(StandardAnnotationQueryEditor, ['annotation', 'datasource', 'change']);
|
||||
},
|
||||
]);
|
||||
@@ -7,10 +7,13 @@ import DatasourceSrv from '../plugins/datasource_srv';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { AppEvents } from '@grafana/data';
|
||||
|
||||
// Registeres the angular directive
|
||||
import './components/StandardAnnotationQueryEditor';
|
||||
|
||||
export class AnnotationsEditorCtrl {
|
||||
mode: any;
|
||||
datasources: any;
|
||||
annotations: any;
|
||||
annotations: any[];
|
||||
currentAnnotation: any;
|
||||
currentDatasource: any;
|
||||
currentIsNew: any;
|
||||
@@ -69,6 +72,19 @@ export class AnnotationsEditorCtrl {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the react editor
|
||||
*/
|
||||
onAnnotationChange = (annotation: any) => {
|
||||
const currentIndex = this.dashboard.annotations.list.indexOf(this.currentAnnotation);
|
||||
if (currentIndex >= 0) {
|
||||
this.dashboard.annotations.list[currentIndex] = annotation;
|
||||
} else {
|
||||
console.warn('updating annotatoin, but not in the dashboard', annotation);
|
||||
}
|
||||
this.currentAnnotation = annotation;
|
||||
};
|
||||
|
||||
edit(annotation: any) {
|
||||
this.currentAnnotation = annotation;
|
||||
this.currentAnnotation.showIn = this.currentAnnotation.showIn || 0;
|
||||
|
||||
@@ -117,7 +117,15 @@
|
||||
|
||||
<h5 class="section-heading">Query</h5>
|
||||
<rebuild-on-change property="ctrl.currentDatasource">
|
||||
<plugin-component type="annotations-query-ctrl"> </plugin-component>
|
||||
<!-- Legacy angular -->
|
||||
<plugin-component ng-if="!ctrl.currentDatasource.annotations" type="annotations-query-ctrl"> </plugin-component>
|
||||
|
||||
<!-- React query editor -->
|
||||
<standard-annotation-editor
|
||||
ng-if="ctrl.currentDatasource.annotations"
|
||||
annotation="ctrl.currentAnnotation"
|
||||
datasource="ctrl.currentDatasource"
|
||||
change="ctrl.onAnnotationChange" />
|
||||
</rebuild-on-change>
|
||||
|
||||
<div class="gf-form">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user