Merge branch 'master' into table-datalinks
This commit is contained in:
+22
-4
@@ -864,7 +864,7 @@ jobs:
|
||||
command: "./scripts/ci-job-succeeded.sh"
|
||||
when: on_success
|
||||
|
||||
scan-docker-master:
|
||||
scan-docker-images:
|
||||
docker:
|
||||
- image: circleci/buildpack-deps:stretch
|
||||
steps:
|
||||
@@ -887,11 +887,29 @@ jobs:
|
||||
name: Clear trivy cache
|
||||
command: trivy --clear-cache
|
||||
- run:
|
||||
name: Scan the latest grafana master alpine image with trivy
|
||||
name: Scan grafana/grafana:master
|
||||
command: trivy --exit-code 1 grafana/grafana:master
|
||||
- run:
|
||||
name: Scan the latest grafana master ubuntu image with trivy
|
||||
name: Scan grafana/grafana:master-ubuntu
|
||||
command: trivy --exit-code 1 grafana/grafana:master-ubuntu
|
||||
- run:
|
||||
name: Scan grafana/grafana-enterprise:master
|
||||
command: trivy --exit-code 1 grafana/grafana-enterprise:master
|
||||
- run:
|
||||
name: Scan grafana/grafana-enterprise:master-ubuntu
|
||||
command: trivy --exit-code 1 grafana/grafana-enterprise:master-ubuntu
|
||||
- run:
|
||||
name: Scan grafana/grafana:latest
|
||||
command: trivy --exit-code 1 grafana/grafana:latest
|
||||
- run:
|
||||
name: Scan grafana/grafana:latest-ubuntu
|
||||
command: trivy --exit-code 1 grafana/grafana:latest-ubuntu
|
||||
- run:
|
||||
name: Scan grafana/grafana-enterprise:latest
|
||||
command: trivy --exit-code 1 grafana/grafana-enterprise:latest
|
||||
- run:
|
||||
name: Scan grafana/grafana-enterprise:latest-ubuntu
|
||||
command: trivy --exit-code 1 grafana/grafana-enterprise:latest-ubuntu
|
||||
- save_cache:
|
||||
key: vulnerability-db
|
||||
paths:
|
||||
@@ -1227,4 +1245,4 @@ workflows:
|
||||
cron: "0 0 * * *"
|
||||
filters: *filter-only-master
|
||||
jobs:
|
||||
- scan-docker-master
|
||||
- scan-docker-images
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"extractorMessageReporting": {
|
||||
"default": {
|
||||
"logLevel": "warning"
|
||||
},
|
||||
"ae-internal-missing-underscore": {
|
||||
"logLevel": "none",
|
||||
"addToApiReportFile": false
|
||||
}
|
||||
},
|
||||
"tsdocMessageReporting": {
|
||||
|
||||
@@ -224,10 +224,13 @@ Content-Type: application/json
|
||||
"name":"User",
|
||||
"email":"user@graf.com",
|
||||
"login":"user",
|
||||
"password":"userpassword"
|
||||
"password":"userpassword",
|
||||
"OrgId": 1
|
||||
}
|
||||
```
|
||||
|
||||
Note that `OrgId` is an optional parameter that can be used to assign a new user to a different organization when [auto_assign_org](https://grafana.com/docs/grafana/latest/installation/configuration/#auto-assign-org) is set to `true`.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
+++
|
||||
title = "Running Grafana behind a reverse proxy"
|
||||
description = "Guide for running Grafana behind a reverse proxy"
|
||||
keywords = ["grafana", "nginx", "documentation", "haproxy", "reverse"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Running Grafana behind a reverse proxy"
|
||||
parent = "tutorials"
|
||||
weight = 1
|
||||
+++
|
||||
|
||||
|
||||
# Running Grafana behind a reverse proxy
|
||||
|
||||
It should be straight forward to get Grafana up and running behind a reverse proxy. But here are some things that you might run into.
|
||||
|
||||
Links and redirects will not be rendered correctly unless you set the server.domain setting.
|
||||
```bash
|
||||
[server]
|
||||
domain = foo.bar
|
||||
```
|
||||
|
||||
To use sub *path* ex `http://foo.bar/grafana` make sure to include `/grafana` in the end of root_url.
|
||||
Otherwise Grafana will not behave correctly. See example below.
|
||||
|
||||
## Examples
|
||||
Here are some example configurations for running Grafana behind a reverse proxy.
|
||||
|
||||
### Grafana configuration (ex http://foo.bar)
|
||||
|
||||
```bash
|
||||
[server]
|
||||
domain = foo.bar
|
||||
```
|
||||
|
||||
### Nginx configuration
|
||||
|
||||
Nginx is a high performance load balancer, web server and reverse proxy: https://www.nginx.com/
|
||||
|
||||
#### Nginx configuration with HTTP and Reverse Proxy enabled
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000/;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Grafana configuration with hosting HTTPS in Nginx (ex https://foo.bar)
|
||||
|
||||
```bash
|
||||
[server]
|
||||
domain = foo.bar
|
||||
root_url = https://foo.bar
|
||||
```
|
||||
|
||||
#### Nginx configuration with HTTPS, Reverse Proxy, HTTP to HTTPS redirect and URL re-writes enabled
|
||||
|
||||
Instead of http://foo.bar:3000/?orgId=1, this configuration will redirect all HTTP requests to HTTPS and re-write the URL so that port 3000 isn't visible and will result in https://foo.bar/?orgId=1
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name foo.bar;
|
||||
return 301 https://foo.bar$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name foo.bar;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
ssl_certificate /etc/nginx/certs/foo_bar.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/foo_bar_decrypted.key;
|
||||
ssl_protocols TLSv1.2;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
rewrite /(.*) /$1 break;
|
||||
proxy_pass http://localhost:3000/;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Examples with **sub path** (ex http://foo.bar/grafana)
|
||||
|
||||
#### Grafana configuration with sub path
|
||||
```bash
|
||||
[server]
|
||||
domain = foo.bar
|
||||
root_url = %(protocol)s://%(domain)s/grafana/
|
||||
```
|
||||
|
||||
#### Nginx configuration with sub path
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/www;
|
||||
index index.html index.htm;
|
||||
|
||||
location /grafana/ {
|
||||
proxy_pass http://localhost:3000/;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### HAProxy configuration with sub path
|
||||
```bash
|
||||
frontend http-in
|
||||
bind *:80
|
||||
use_backend grafana_backend if { path /grafana } or { path_beg /grafana/ }
|
||||
|
||||
backend grafana_backend
|
||||
# Requires haproxy >= 1.6
|
||||
http-request set-path %[path,regsub(^/grafana/?,/)]
|
||||
|
||||
# Works for haproxy < 1.6
|
||||
# reqrep ^([^\ ]*\ /)grafana[/]?(.*) \1\2
|
||||
|
||||
server grafana localhost:3000
|
||||
```
|
||||
|
||||
### IIS URL Rewrite Rule (Windows) with Subpath
|
||||
|
||||
IIS requires that the URL Rewrite module is installed.
|
||||
|
||||
Given:
|
||||
|
||||
- subpath `grafana`
|
||||
- Grafana installed on `http://localhost:3000`
|
||||
- server config:
|
||||
|
||||
```bash
|
||||
[server]
|
||||
domain = localhost:8080
|
||||
root_url = %(protocol)s://%(domain)s/grafana/
|
||||
```
|
||||
|
||||
Create an Inbound Rule for the parent website (localhost:8080 in this example) in IIS Manager with the following settings:
|
||||
|
||||
- pattern: `grafana(/)?(.*)`
|
||||
- check the `Ignore case` checkbox
|
||||
- rewrite URL set to `http://localhost:3000/{R:2}`
|
||||
- check the `Append query string` checkbox
|
||||
- check the `Stop processing of subsequent rules` checkbox
|
||||
|
||||
This is the rewrite rule that is generated in the `web.config`:
|
||||
|
||||
```xml
|
||||
<rewrite>
|
||||
<rules>
|
||||
<rule name="Grafana" enabled="true" stopProcessing="true">
|
||||
<match url="grafana(/)?(.*)" />
|
||||
<action type="Rewrite" url="http://localhost:3000/{R:2}" logRewrittenUrl="false" />
|
||||
</rule>
|
||||
</rules>
|
||||
</rewrite>
|
||||
```
|
||||
|
||||
See the [tutorial on IIS URL Rewrites](http://docs.grafana.org/tutorials/iis/) for more in-depth instructions.
|
||||
@@ -267,15 +267,6 @@
|
||||
link: /enterprise/license-expiration/
|
||||
- name: Export dashboard as PDF
|
||||
link: /enterprise/export-pdf/
|
||||
- name: Guides
|
||||
link: /tutorials/
|
||||
children:
|
||||
- name: Run Grafana behind a reverse proxy
|
||||
link: /installation/behind_proxy/
|
||||
- name: Run Grafana with IIS Reverse Proxy on Windows
|
||||
link: /tutorials/iis/
|
||||
- name: Integrate Hubot and Grafana
|
||||
link: /tutorials/hubot_howto/
|
||||
- name: Plugins
|
||||
link: /plugins/
|
||||
children:
|
||||
|
||||
@@ -179,4 +179,4 @@ We recommend that you use a linter for your JavaScript. For ES6, the standard li
|
||||
}
|
||||
}
|
||||
```
|
||||
5. If using Lodash, then be consequent and prefer that to the native ES6 array functions.
|
||||
5. If using Lodash, then be consistent and prefer that to the native ES6 array functions.
|
||||
|
||||
@@ -12,10 +12,7 @@ This section of the docs contains a series for tutorials and stack setup guides.
|
||||
|
||||
## Articles
|
||||
|
||||
- [Running Grafana behind a reverse proxy]({{< relref "../installation/behind_proxy.md" >}})
|
||||
- [API Tutorial: How To Create API Tokens And Dashboards For A Specific Organization]({{< relref "api_org_token_howto.md" >}})
|
||||
- [How to Use IIS with URL Rewrite as a Reverse Proxy for Grafana on Windows]({{< relref "iis.md" >}})
|
||||
- [How to integrate Hubot with Grafana]({{< relref "hubot_howto.md" >}})
|
||||
- [How to setup Grafana for high availability]({{< relref "ha_setup.md" >}})
|
||||
|
||||
## External links
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
+++
|
||||
title = "How to integrate Hubot and Grafana"
|
||||
type = "docs"
|
||||
keywords = ["grafana", "tutorials", "hubot", "slack", "hipchat", "setup", "install", "config"]
|
||||
[menu.docs]
|
||||
parent = "tutorials"
|
||||
weight = 10
|
||||
+++
|
||||
|
||||
# How to integrate Hubot with Grafana
|
||||
|
||||
Grafana 2.0 shipped with a great feature that enables it to render any graph or panel to a PNG image.
|
||||
No matter what data source you are using, the PNG image of the Graph will look the same
|
||||
as it does in your browser.
|
||||
|
||||
This guide will show you how to install and configure the [Hubot-Grafana](https://github.com/stephenyeargin/hubot-grafana)
|
||||
plugin. This plugin allows you to tell hubot to render any dashboard or graph right from a channel in
|
||||
Slack, Hipchat or Basecamp. The bot will respond with an image of the graph and a link that will
|
||||
take you to the graph.
|
||||
|
||||
> *Amazon S3 Required*: The hubot-grafana script will upload the rendered graphs to Amazon S3. This
|
||||
> is so Hipchat and Slack can show them reliably (they require the image to be publicly available).
|
||||
|
||||
<div class="text-center">
|
||||
<img src="/img/docs/tutorials/hubot_grafana.png" class="center"></a>
|
||||
</div>
|
||||
|
||||
## What is Hubot?
|
||||
|
||||
[Hubot](https://hubot.github.com/) is an universal and extensible chat bot that can be used with many chat
|
||||
services and has a huge library of third party plugins that allow you to automate anything from your
|
||||
chat rooms.
|
||||
|
||||
## Install Hubot
|
||||
|
||||
Hubot is very easy to install and host. If you do not already have a bot up and running please
|
||||
read the official [Getting Started With Hubot](https://hubot.github.com/docs/) guide.
|
||||
|
||||
## Install Hubot-Grafana script
|
||||
|
||||
In your Hubot project repo install the Grafana plugin using `npm`:
|
||||
```bash
|
||||
npm install hubot-grafana --save
|
||||
```
|
||||
Edit the file external-scripts.json, and add hubot-grafana to the list of plugins.
|
||||
|
||||
```json
|
||||
[
|
||||
"hubot-pugme",
|
||||
"hubot-shipit",
|
||||
"hubot-grafana"
|
||||
]
|
||||
```
|
||||
|
||||
## Configure
|
||||
|
||||
The `hubot-grafana` plugin requires a number of environment variables to be set in order to work properly.
|
||||
|
||||
```bash
|
||||
export HUBOT_GRAFANA_HOST=https://play.grafana.org
|
||||
export HUBOT_GRAFANA_API_KEY=abcd01234deadbeef01234
|
||||
export HUBOT_GRAFANA_S3_BUCKET=mybucket
|
||||
export HUBOT_GRAFANA_S3_ACCESS_KEY_ID=ABCDEF123456XYZ
|
||||
export HUBOT_GRAFANA_S3_SECRET_ACCESS_KEY=aBcD01234dEaDbEef01234
|
||||
export HUBOT_GRAFANA_S3_PREFIX=graphs
|
||||
export HUBOT_GRAFANA_S3_REGION=us-standard
|
||||
```
|
||||
|
||||
### Grafana server side rendering
|
||||
|
||||
The hubot plugin will take advantage of the Grafana server side rendering feature that can
|
||||
render any panel on the server using phantomjs. Grafana ships with a phantomjs binary (Linux only).
|
||||
|
||||
To verify that this feature works try the `Direct link to rendered image` link in the panel share dialog.
|
||||
If you do not get an image when opening this link verify that the required font packages are installed for phantomjs to work.
|
||||
|
||||
### Grafana API Key
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v2/orgdropdown_api_keys.png" max-width="150px" class="docs-image--right">}}
|
||||
|
||||
You need to set the environment variable `HUBOT_GRAFANA_API_KEY` to a Grafana API Key.
|
||||
You can add these from the API Keys page which you find in the Organization dropdown.
|
||||
|
||||
### Amazon S3
|
||||
|
||||
The `S3` options are optional but for the images to work properly in services like Slack and Hipchat they need
|
||||
to publicly available. By specifying the `S3` options the hubot-grafana script will publish the rendered
|
||||
panel to `S3` and it will use that URL when it posts to Slack or Hipchat.
|
||||
|
||||
## Hubot commands
|
||||
|
||||
- `hubot graf list`
|
||||
- Lists the available dashboards
|
||||
- `hubot graf db graphite-carbon-metrics`
|
||||
- Graph all panels in the dashboard
|
||||
- `hubot graf db graphite-carbon-metrics:3`
|
||||
- Graph only panel with id 3 of a particular dashboard
|
||||
- `hubot graf db graphite-carbon-metrics:cpu`
|
||||
- Graph only the panels containing "cpu" (case insensitive) in the title
|
||||
- `hubot graf db graphite-carbon-metrics now-12hr`
|
||||
- Get a dashboard with a window of 12 hours ago to now
|
||||
- `hubot graf db graphite-carbon-metrics now-24hr now-12hr`
|
||||
- Get a dashboard with a window of 24 hours ago to 12 hours ago
|
||||
- `hubot graf db graphite-carbon-metrics:3 now-8d now-1d`
|
||||
- Get only the third panel of a particular dashboard with a window of 8 days ago to yesterday
|
||||
- `hubot graf db graphite-carbon-metrics host=carbon-a`
|
||||
- Get a templated dashboard with the `$host` parameter set to `carbon-a`
|
||||
|
||||
## Aliases
|
||||
|
||||
Some of the hubot commands above can lengthy and you might have to remember the dashboard slug (url id).
|
||||
If you have a few favorite graphs you want to be able check up on often (let's say from your mobile) you
|
||||
can create hubot command aliases with the hubot script `hubot-alias`.
|
||||
|
||||
Install it:
|
||||
|
||||
```bash
|
||||
npm i --save hubot-alias
|
||||
```
|
||||
|
||||
Now add `hubot-alias` to the list of plugins in `external-scripts.json` and restart hubot.
|
||||
|
||||
Now you can add an alias like this:
|
||||
|
||||
- `hubot alias graf-lb=graf db loadbalancers:2 now-20m`
|
||||
|
||||
<div class="text-center">
|
||||
Using the alias:<br>
|
||||
<img src="/img/docs/tutorials/hubot_grafana2.png" class="center"></a>
|
||||
</div>
|
||||
|
||||
## Summary
|
||||
|
||||
Grafana is going to ship with integrated Slack and Hipchat features some day but you do
|
||||
not have to wait for that. Grafana 2 shipped with a very clever server side rendering feature
|
||||
that can render any panel to a png using phantomjs. The hubot plugin for Grafana is something
|
||||
you can install and use today!
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
+++
|
||||
title = "Grafana with IIS Reverse Proxy on Windows"
|
||||
type = "docs"
|
||||
keywords = ["grafana", "tutorials", "proxy", "IIS", "windows"]
|
||||
[menu.docs]
|
||||
parent = "tutorials"
|
||||
weight = 10
|
||||
+++
|
||||
|
||||
# How to Use IIS with URL Rewrite as a Reverse Proxy for Grafana on Windows
|
||||
|
||||
If you want Grafana to be a subpath or subfolder under a website in IIS then the URL Rewrite module for ISS can be used to support this.
|
||||
|
||||
Example:
|
||||
|
||||
- Parent site: http://localhost:8080
|
||||
- Grafana: http://localhost:3000
|
||||
|
||||
Grafana as a subpath: http://localhost:8080/grafana
|
||||
|
||||
## Setup
|
||||
|
||||
If you have not already done it, then a requirement is to install URL Rewrite module for IIS.
|
||||
|
||||
Download and install the URL Rewrite module for IIS: https://www.iis.net/downloads/microsoft/url-rewrite
|
||||
|
||||
## Grafana Config
|
||||
|
||||
The Grafana config can be set by creating a file named `custom.ini` in the `conf` subdirectory of your Grafana installation. See the [installation instructions](http://docs.grafana.org/installation/windows/#configure) for more details.
|
||||
|
||||
Given that the subpath should be `grafana` and the parent site is `localhost:8080` then add this to the `custom.ini` config file:
|
||||
|
||||
```bash
|
||||
[server]
|
||||
domain = localhost:8080
|
||||
root_url = %(protocol)s://%(domain)s/grafana/
|
||||
```
|
||||
|
||||
Restart the Grafana server after changing the config file.
|
||||
|
||||
## IIS Config
|
||||
|
||||
1. Open the IIS Manager and click on the parent website
|
||||
2. In the admin console for this website, double click on the URL Rewrite option:
|
||||
{{< docs-imagebox img="/img/docs/tutorials/IIS_admin_console.png" max-width= "800px" >}}
|
||||
|
||||
3. Click on the `Add Rule(s)...` action
|
||||
4. Choose the Blank Rule template for an Inbound Rule
|
||||
{{< docs-imagebox img="/img/docs/tutorials/IIS_add_inbound_rule.png" max-width= "800px" >}}
|
||||
|
||||
5. Create an Inbound Rule for the parent website (localhost:8080 in this example) with the following settings:
|
||||
- pattern: `grafana(/)?(.*)`
|
||||
- check the `Ignore case` checkbox
|
||||
- rewrite URL set to `http://localhost:3000/{R:2}`
|
||||
- check the `Append query string` checkbox
|
||||
- check the `Stop processing of subsequent rules` checkbox
|
||||
|
||||
{{< docs-imagebox img="/img/docs/tutorials/IIS_url_rewrite.png" max-width= "800px" >}}
|
||||
|
||||
Finally, navigate to `http://localhost:8080/grafana` (replace `http://localhost:8080` with your parent domain) and you should come to the Grafana login page.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 404 error
|
||||
|
||||
When navigating to the Grafana URL (`http://localhost:8080/grafana` in the example above) and a `HTTP Error 404.0 - Not Found` error is returned then either:
|
||||
|
||||
- the pattern for the Inbound Rule is incorrect. Edit the rule, click on the `Test pattern...` button, test the part of the URL after `http://localhost:8080/` and make sure it matches. For `grafana/login` the test should return 3 capture groups: {R:0}: `grafana` {R:1}: `/` and {R:2}: `login`.
|
||||
- The `root_url` setting in the Grafana config file does not match the parent URL with subpath.
|
||||
|
||||
### Grafana Website only shows text with no images or css
|
||||
|
||||
{{< docs-imagebox img="/img/docs/tutorials/IIS_proxy_error.png" max-width= "800px" >}}
|
||||
|
||||
1. The `root_url` setting in the Grafana config file does not match the parent URL with subpath. This could happen if the root_url is commented out by mistake (`;` is used for commenting out a line in .ini files):
|
||||
|
||||
`; root_url = %(protocol)s://%(domain)s/grafana/`
|
||||
|
||||
2. or if the subpath in the `root_url` setting does not match the subpath used in the pattern in the Inbound Rule in IIS:
|
||||
|
||||
`root_url = %(protocol)s://%(domain)s/grafana/`
|
||||
|
||||
pattern in Inbound Rule: `wrongsubpath(/)?(.*)`
|
||||
|
||||
3. or if the Rewrite URL in the Inbound Rule is incorrect.
|
||||
|
||||
The Rewrite URL should not include the subpath.
|
||||
|
||||
The Rewrite URL should contain the capture group from the pattern matching that returns the part of the URL after the subpath. The pattern used above returns 3 capture groups and the third one {R:2} returns the part of the URL after `http://localhost:8080/grafana/`.
|
||||
@@ -24,12 +24,8 @@ const assertDefaultsForNewVariable = () => {
|
||||
e2e()
|
||||
.window()
|
||||
.then((win: any) => {
|
||||
let chainer = 'not.exist';
|
||||
let value: string = undefined;
|
||||
if (win.grafanaBootData.settings.featureToggles.newVariables) {
|
||||
chainer = 'have.text';
|
||||
value = '';
|
||||
}
|
||||
const chainer = 'have.text';
|
||||
const value = '';
|
||||
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect().within(select => {
|
||||
e2e()
|
||||
@@ -85,10 +81,8 @@ const createQueryVariable = ({ name, label, dataSourceName, query }: CreateQuery
|
||||
e2e()
|
||||
.window()
|
||||
.then((win: any) => {
|
||||
let text = `string:${dataSourceName}`;
|
||||
if (win.grafanaBootData.settings.featureToggles.newVariables) {
|
||||
text = `${dataSourceName}`;
|
||||
}
|
||||
const text = `${dataSourceName}`;
|
||||
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
|
||||
.select(text)
|
||||
.blur();
|
||||
@@ -352,11 +346,7 @@ const assertUpdateItem = (data: QueryVariableData[]) => {
|
||||
e2e()
|
||||
.window()
|
||||
.then((win: any) => {
|
||||
if (win.grafanaBootData.settings.featureToggles.newVariables) {
|
||||
queryVariables[1].selectedOption = 'A constant';
|
||||
} else {
|
||||
queryVariables[1].selectedOption = 'undefined';
|
||||
}
|
||||
queryVariables[1].selectedOption = 'A constant';
|
||||
assertVariableLabelAndComponent(queryVariables[1]);
|
||||
});
|
||||
|
||||
@@ -627,11 +617,5 @@ e2e.scenario({
|
||||
|
||||
// assert that move up works
|
||||
assertMoveUpItem(queryVariables);
|
||||
|
||||
e2e()
|
||||
.window()
|
||||
.then((win: any) => {
|
||||
logSection('This scenario ran with these featureToggles', win.grafanaBootData.settings.featureToggles);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,25 +2,51 @@ import { DataSourceInstanceSettings } from './datasource';
|
||||
import { PanelPluginMeta } from './panel';
|
||||
import { GrafanaTheme } from './theme';
|
||||
|
||||
/**
|
||||
* Describes the build information that will be available via the Grafana cofiguration.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface BuildInfo {
|
||||
version: string;
|
||||
commit: string;
|
||||
isEnterprise: boolean; // deprecated: use licenseInfo.hasLicense instead
|
||||
/**
|
||||
* Is set to true when running Grafana Enterprise edition.
|
||||
*
|
||||
* @deprecated use `licenseInfo.hasLicense` instead
|
||||
*/
|
||||
isEnterprise: boolean;
|
||||
env: string;
|
||||
edition: string;
|
||||
latestVersion: string;
|
||||
hasUpdate: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes available feature toggles in Grafana. These can be configured via the
|
||||
* `conf/custom.ini` to enable features under development or not yet available in
|
||||
* stable version.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface FeatureToggles {
|
||||
transformations: boolean;
|
||||
expressions: boolean;
|
||||
newEdit: boolean;
|
||||
meta: boolean; // enterprise
|
||||
/**
|
||||
* @remarks
|
||||
* Available only in Grafana Enterprise
|
||||
*/
|
||||
meta: boolean;
|
||||
newVariables: boolean;
|
||||
tracingIntegration: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the license information about the current running instance of Grafana.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface LicenseInfo {
|
||||
hasLicense: boolean;
|
||||
expiry: number;
|
||||
@@ -28,6 +54,11 @@ export interface LicenseInfo {
|
||||
stateInfo: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes all the different Grafana configuration values available for an instance.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface GrafanaConfig {
|
||||
datasources: { [str: string]: DataSourceInstanceSettings };
|
||||
panels: { [key: string]: PanelPluginMeta };
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import extend from 'lodash/extend';
|
||||
import merge from 'lodash/merge';
|
||||
import { getTheme } from '@grafana/ui';
|
||||
import {
|
||||
DataSourceInstanceSettings,
|
||||
GrafanaTheme,
|
||||
GrafanaThemeType,
|
||||
PanelPluginMeta,
|
||||
GrafanaConfig,
|
||||
GrafanConfig,
|
||||
LicenseInfo,
|
||||
BuildInfo,
|
||||
FeatureToggles,
|
||||
@@ -80,7 +80,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
disableSanitizeHtml: false,
|
||||
};
|
||||
|
||||
extend(this, defaults, options);
|
||||
merge(this, defaults, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,4 +93,9 @@ const bootData = (window as any).grafanaBootData || {
|
||||
const options = bootData.settings;
|
||||
options.bootData = bootData;
|
||||
|
||||
/**
|
||||
* Use this to access the {@link GrafanaBootConfig} for the current running Grafana instance.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const config = new GrafanaBootConfig(options);
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
export * from './services';
|
||||
export * from './config';
|
||||
export * from './types';
|
||||
export { loadPluginCss, SystemJS } from './utils/plugin';
|
||||
export { loadPluginCss, SystemJS, PluginCssOptions } from './utils/plugin';
|
||||
export { reportMetaAnalytics } from './utils/analytics';
|
||||
export { DataSourceWithBackend } from './utils/DataSourceWithBackend';
|
||||
export { DataSourceWithBackend, HealthCheckResult, HealthStatus } from './utils/DataSourceWithBackend';
|
||||
|
||||
@@ -1,19 +1,87 @@
|
||||
/**
|
||||
* Used to enable rendering of Angular components within a
|
||||
* React component without loosing proper typings.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* class Component extends PureComponent<Props> {
|
||||
* element: HTMLElement;
|
||||
* angularComponent: AngularComponent;
|
||||
*
|
||||
* componentDidMount() {
|
||||
* const template = '<angular-component />' // angular template here;
|
||||
* const scopeProps = { ctrl: angularController }; // angular scope properties here
|
||||
* const loader = getAngularLoader();
|
||||
* this.angularComponent = loader.load(this.element, scopeProps, template);
|
||||
* }
|
||||
*
|
||||
* componentWillUnmount() {
|
||||
* if (this.angularComponent) {
|
||||
* this.angularComponent.destroy();
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* render() {
|
||||
* return (
|
||||
* <div ref={element => (this.element = element)} />
|
||||
* );
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface AngularComponent {
|
||||
/**
|
||||
* Should be called when the React component will unmount.
|
||||
*/
|
||||
destroy(): void;
|
||||
/**
|
||||
* Can be used to trigger a re-render of the Angular component.
|
||||
*/
|
||||
digest(): void;
|
||||
/**
|
||||
* Used to access the Angular scope from the React component.
|
||||
*/
|
||||
getScope(): any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to load an Angular component from the context of a React component.
|
||||
* Please see the {@link AngularComponent} for a proper example.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface AngularLoader {
|
||||
/**
|
||||
*
|
||||
* @param elem - the element that the Angular component will be loaded into.
|
||||
* @param scopeProps - values that will be accessed via the Angular scope.
|
||||
* @param template - template used by the Angular component.
|
||||
*/
|
||||
load(elem: any, scopeProps: any, template: string): AngularComponent;
|
||||
}
|
||||
|
||||
let instance: AngularLoader;
|
||||
|
||||
/**
|
||||
* Used during startup by Grafana to set the AngularLoader so it is available
|
||||
* via the the {@link getAngularLoader} to the rest of the application.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function setAngularLoader(v: AngularLoader) {
|
||||
instance = v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to retrieve the {@link AngularLoader} that enables the use of Angular
|
||||
* components within a React component.
|
||||
*
|
||||
* Please see the {@link AngularComponent} for a proper example.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function getAngularLoader(): AngularLoader {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
interface SizeMeta {
|
||||
/**
|
||||
* Describes a size with width/height
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface SizeMeta {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the meta information that are sent together with each event.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface EchoMeta {
|
||||
screenSize: SizeMeta;
|
||||
windowSize: SizeMeta;
|
||||
@@ -12,8 +22,17 @@ export interface EchoMeta {
|
||||
* A unique browser session
|
||||
*/
|
||||
sessionId: string;
|
||||
/**
|
||||
* The current users username used to login into Grafana e.g. email.
|
||||
*/
|
||||
userLogin: string;
|
||||
/**
|
||||
* The current users uniqe identifier.
|
||||
*/
|
||||
userId: number;
|
||||
/**
|
||||
* True when user is logged in into Grafana.
|
||||
*/
|
||||
userSignedIn: boolean;
|
||||
/**
|
||||
* A millisecond epoch
|
||||
@@ -25,6 +44,11 @@ export interface EchoMeta {
|
||||
timeSinceNavigationStart: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes echo backends that can be registered to receive of events.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface EchoBackend<T extends EchoEvent = any, O = any> {
|
||||
options: O;
|
||||
supportedEvents: EchoEventType[];
|
||||
@@ -32,33 +56,84 @@ export interface EchoBackend<T extends EchoEvent = any, O = any> {
|
||||
addEvent: (event: T) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes an echo event.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface EchoEvent<T extends EchoEventType = any, P = any> {
|
||||
type: EchoEventType;
|
||||
/**
|
||||
* Event payload containing event specific data.
|
||||
*/
|
||||
payload: P;
|
||||
meta: EchoMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported echo event types that can be sent via the {@link EchoSrv}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export enum EchoEventType {
|
||||
Performance = 'performance',
|
||||
MetaAnalytics = 'meta-analytics',
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to send events to all the registered backends. This should be accessed via the
|
||||
* {@link getEchoSrv} function. Will, by default, flush events to the backends every
|
||||
* 10s or when the flush function is triggered.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface EchoSrv {
|
||||
/**
|
||||
* Call this to flush current events to the echo backends.
|
||||
*/
|
||||
flush(): void;
|
||||
/**
|
||||
* Add a new echo backend to the list of backends that will receive events.
|
||||
*/
|
||||
addBackend(backend: EchoBackend): void;
|
||||
/**
|
||||
* Call this to add event that will be sent to the echo backends upon next
|
||||
* flush.
|
||||
*
|
||||
* @param event - Object containing event information.
|
||||
* @param meta - Object that will extend/override the default meta object.
|
||||
*/
|
||||
addEvent<T extends EchoEvent>(event: Omit<T, 'meta'>, meta?: {}): void;
|
||||
}
|
||||
|
||||
let singletonInstance: EchoSrv;
|
||||
|
||||
/**
|
||||
* Used during startup by Grafana to set the EchoSrv so it is available
|
||||
* via the the {@link getEchoSrv} to the rest of the application.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function setEchoSrv(instance: EchoSrv) {
|
||||
singletonInstance = instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to retrieve the {@link EchoSrv} that can be used to report events to registered
|
||||
* echo backends.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function getEchoSrv(): EchoSrv {
|
||||
return singletonInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to register echo backends that will receive Grafana echo events during application
|
||||
* runtime.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const registerEchoBackend = (backend: EchoBackend) => {
|
||||
getEchoSrv().addBackend(backend);
|
||||
};
|
||||
|
||||
@@ -1,36 +1,84 @@
|
||||
/**
|
||||
* Passed as options to the {@link LocationSrv} to describe how the automatically navigation
|
||||
* should be performed.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface LocationUpdate {
|
||||
/**
|
||||
* Target path where you automatically wants to navigate the user.
|
||||
*/
|
||||
path?: string;
|
||||
|
||||
/**
|
||||
* Specify this value if you want to add values to the query string of the URL.
|
||||
*/
|
||||
query?: UrlQueryMap;
|
||||
|
||||
/**
|
||||
* Add the query argument to the existing URL
|
||||
* If set to true, the query argument will be added to the existing URL.
|
||||
*/
|
||||
partial?: boolean;
|
||||
|
||||
/**
|
||||
* Do not change this unless you are the angular router
|
||||
* Used internally to sync the Redux state from Angular to make sure that the Redux location
|
||||
* state is in sync when navigating using the Angular router.
|
||||
*
|
||||
* @remarks
|
||||
* Do not change this unless you are the Angular router.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
routeParams?: UrlQueryMap;
|
||||
|
||||
/*
|
||||
* If true this will replace url state (ie cause no new browser history)
|
||||
* If set to true, this will replace URL state (ie. cause no new browser history).
|
||||
*/
|
||||
replace?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type to represent the value of a single query variable.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[] | undefined | null;
|
||||
|
||||
/**
|
||||
* Type to represent the values parsed from the query string.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type UrlQueryMap = Record<string, UrlQueryValue>;
|
||||
|
||||
/**
|
||||
* If you need to automatically navigate the user to a new place in the application this should
|
||||
* be done via the LocationSrv and it will make sure to update the application state accordingly.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface LocationSrv {
|
||||
update(options: LocationUpdate): void;
|
||||
}
|
||||
|
||||
let singletonInstance: LocationSrv;
|
||||
|
||||
/**
|
||||
* Used during startup by Grafana to set the LocationSrv so it is available
|
||||
* via the the {@link getLocationSrv} to the rest of the application.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function setLocationSrv(instance: LocationSrv) {
|
||||
singletonInstance = instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to retrieve the {@link LocationSrv} that can be used to automatically navigate
|
||||
* the user to a new place in Grafana.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function getLocationSrv(): LocationSrv {
|
||||
return singletonInstance;
|
||||
}
|
||||
|
||||
@@ -1,50 +1,92 @@
|
||||
/**
|
||||
* Currently implemented with:
|
||||
* https://docs.angularjs.org/api/ng/service/$http#usage
|
||||
* but that will likely change in the future
|
||||
* Used to initiate a remote call via the {@link BackendSrv}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type BackendSrvRequest = {
|
||||
url: string;
|
||||
/**
|
||||
* Number of times to retry the remote call if it fails.
|
||||
*/
|
||||
retry?: number;
|
||||
|
||||
/**
|
||||
* HTTP headers that should be passed along with the remote call.
|
||||
* Please have a look at {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API | Fetch API}
|
||||
* for supported headers.
|
||||
*/
|
||||
headers?: any;
|
||||
|
||||
/**
|
||||
* HTTP verb to perform in the remote call GET, POST, PUT etc.
|
||||
*/
|
||||
method?: string;
|
||||
|
||||
// Show a message with the result
|
||||
/**
|
||||
* If set to true an alert with the response message will be displayed
|
||||
* upon successful remote call
|
||||
*/
|
||||
showSuccessAlert?: boolean;
|
||||
|
||||
// A requestID is provided by the datasource as a unique identifier for a
|
||||
// particular query. If the requestID exists, the promise it is keyed to
|
||||
// is canceled, canceling the previous datasource request if it is still
|
||||
// in-flight.
|
||||
/**
|
||||
* Provided by the initiator to identify a particular remote call. An example
|
||||
* of this is when a datasource plugin triggers a query. If the request id already
|
||||
* exist the backendSrv will try to cancel and replace the previous call with the
|
||||
* new one.
|
||||
*/
|
||||
requestId?: string;
|
||||
|
||||
// Allow any other parameters
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to communicate via http(s) to a remote backend such as the Grafana backend,
|
||||
* a datasource etc. The BackendSrv is using the {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API | Fetch API}
|
||||
* under the hood to handle all the communication.
|
||||
*
|
||||
* The request function can be used to perform a remote call by specifing a {@link BackendSrvRequest}.
|
||||
* To make the BackendSrv a bit easier to use we have added a couple of shorthand functions that will
|
||||
* use default values executing the request.
|
||||
*
|
||||
* @remarks
|
||||
* By default Grafana will display an error message alert if the remote call fails. If you want
|
||||
* to prevent this from happending you need to catch the error thrown by the BackendSrv and
|
||||
* set the `isHandled = true` on the incoming error.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface BackendSrv {
|
||||
get(url: string, params?: any, requestId?: string): Promise<any>;
|
||||
|
||||
delete(url: string): Promise<any>;
|
||||
|
||||
post(url: string, data?: any): Promise<any>;
|
||||
|
||||
patch(url: string, data?: any): Promise<any>;
|
||||
|
||||
put(url: string, data?: any): Promise<any>;
|
||||
|
||||
// If there is an error, set: err.isHandled = true
|
||||
// otherwise the backend will show a message for you
|
||||
request(options: BackendSrvRequest): Promise<any>;
|
||||
|
||||
// DataSource requests add hooks into the query inspector
|
||||
/**
|
||||
* Special function used to communicate with datasources that will emit core
|
||||
* events that the Grafana QueryInspector and QueryEditor is listening for to be able
|
||||
* to display datasource query information. Can be skipped by adding `option.silent`
|
||||
* when initializing the request.
|
||||
*/
|
||||
datasourceRequest(options: BackendSrvRequest): Promise<any>;
|
||||
}
|
||||
|
||||
let singletonInstance: BackendSrv;
|
||||
|
||||
/**
|
||||
* Used during startup by Grafana to set the BackendSrv so it is available
|
||||
* via the the {@link getBackendSrv} to the rest of the application.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const setBackendSrv = (instance: BackendSrv) => {
|
||||
singletonInstance = instance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to retrieve the {@link BackendSrv} that can be used to communicate
|
||||
* via http(s) to a remote backend such as the Grafana backend, a datasource etc.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const getBackendSrv = (): BackendSrv => singletonInstance;
|
||||
|
||||
@@ -1,15 +1,39 @@
|
||||
import { ScopedVars, DataSourceApi } from '@grafana/data';
|
||||
|
||||
/**
|
||||
* This is the entry point for communicating with a datasource that is added as
|
||||
* a plugin (both external and internal). Via this service you will get access
|
||||
* to the {@link @grafana/data#DataSourceApi | DataSourceApi} that have a rich API for
|
||||
* communicating with the datasource.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface DataSourceSrv {
|
||||
/**
|
||||
* @param name - name of the datasource plugin you want to use.
|
||||
* @param scopedVars - variables used to interpolate a templated passed as name.
|
||||
*/
|
||||
get(name?: string, scopedVars?: ScopedVars): Promise<DataSourceApi>;
|
||||
}
|
||||
|
||||
let singletonInstance: DataSourceSrv;
|
||||
|
||||
/**
|
||||
* Used during startup by Grafana to set the DataSourceSrv so it is available
|
||||
* via the the {@link getDataSourceSrv} to the rest of the application.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function setDataSourceSrv(instance: DataSourceSrv) {
|
||||
singletonInstance = instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to retrieve the {@link DataSourceSrv} that is the entry point for communicating with
|
||||
* a datasource that is added as a plugin (both external and internal).
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function getDataSourceSrv(): DataSourceSrv {
|
||||
return singletonInstance;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
import { VariableModel } from '@grafana/data';
|
||||
|
||||
/**
|
||||
* Via the TemplateSrv consumers get access to all the available template variables
|
||||
* that can be used within the current active dashboard.
|
||||
*
|
||||
* For a mor in-depth description visit: https://grafana.com/docs/grafana/latest/reference/templating
|
||||
* @public
|
||||
*/
|
||||
export interface TemplateSrv {
|
||||
getVariables(): VariableModel[];
|
||||
}
|
||||
|
||||
let singletonInstance: TemplateSrv;
|
||||
|
||||
/**
|
||||
* Used during startup by Grafana to set the TemplateSrv so it is available
|
||||
* via the the {@link getTemplateSrv} to the rest of the application.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const setTemplateSrv = (instance: TemplateSrv) => {
|
||||
singletonInstance = instance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to retrieve the {@link TemplateSrv} that can be used to fetch available
|
||||
* template variables.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const getTemplateSrv = (): TemplateSrv => singletonInstance;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { EchoEvent, EchoEventType } from '../services/EchoSrv';
|
||||
|
||||
/**
|
||||
* Describes the basic dashboard information that can be passed as the meta
|
||||
* analytics payload.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface DashboardInfo {
|
||||
dashboardId: number;
|
||||
dashboardUid: string;
|
||||
@@ -7,6 +13,11 @@ export interface DashboardInfo {
|
||||
folderName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the data request information passed as the meta analytics payload.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface DataRequestInfo extends Partial<DashboardInfo> {
|
||||
datasourceName: string;
|
||||
datasourceId?: number;
|
||||
@@ -17,19 +28,44 @@ export interface DataRequestInfo extends Partial<DashboardInfo> {
|
||||
dataSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The meta analytics events that can be added to the echo service.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export enum MetaAnalyticsEventName {
|
||||
DashboardView = 'dashboard-view',
|
||||
DataRequest = 'data-request',
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the payload of a dashboard view event.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface DashboardViewEventPayload extends DashboardInfo {
|
||||
eventName: MetaAnalyticsEventName.DashboardView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the payload of a data request event.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface DataRequestEventPayload extends DataRequestInfo {
|
||||
eventName: MetaAnalyticsEventName.DataRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the meta analytics payload passed with the {@link MetaAnalyticsEvent}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type MetaAnalyticsEventPayload = DashboardViewEventPayload | DataRequestEventPayload;
|
||||
|
||||
/**
|
||||
* Describes meta analytics event with predefined {@link EchoEventType.MetaAnalytics} type.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface MetaAnalyticsEvent extends EchoEvent<EchoEventType.MetaAnalytics, MetaAnalyticsEventPayload> {}
|
||||
|
||||
@@ -10,21 +10,37 @@ import { Observable, from } from 'rxjs';
|
||||
import { config } from '..';
|
||||
import { getBackendSrv } from '../services';
|
||||
|
||||
// Ideally internal (exported for consistency)
|
||||
const ExpressionDatasourceID = '__expr__';
|
||||
|
||||
/**
|
||||
* Describes the current healt status of a data source plugin.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export enum HealthStatus {
|
||||
Unknown = 'UNKNOWN',
|
||||
OK = 'OK',
|
||||
Error = 'ERROR',
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the payload returned when checking the health of a data source
|
||||
* plugin.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface HealthCheckResult {
|
||||
status: HealthStatus;
|
||||
message: string;
|
||||
details?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend this class to implement a data source plugin that is depending on the Grafana
|
||||
* backend API.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class DataSourceWithBackend<
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
@@ -86,6 +102,8 @@ export class DataSourceWithBackend<
|
||||
|
||||
/**
|
||||
* Override to apply template variables
|
||||
*
|
||||
* @virtual
|
||||
*/
|
||||
applyTemplateVariables(query: DataQuery) {
|
||||
return query;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { getEchoSrv, EchoEventType } from '../services/EchoSrv';
|
||||
import { MetaAnalyticsEvent, MetaAnalyticsEventPayload } from '../types/analytics';
|
||||
|
||||
/**
|
||||
* Helper function to report meta analytics to the {@link EchoSrv}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const reportMetaAnalytics = (payload: MetaAnalyticsEventPayload) => {
|
||||
getEchoSrv().addEvent<MetaAnalyticsEvent>({
|
||||
type: EchoEventType.MetaAnalytics,
|
||||
|
||||
@@ -3,13 +3,29 @@ import { config } from '../config';
|
||||
// @ts-ignore
|
||||
import System from 'systemjs/dist/system.js';
|
||||
|
||||
/**
|
||||
* Option to specify a plugin css that should be applied for the dark
|
||||
* and the light theme.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PluginCssOptions {
|
||||
light: string;
|
||||
dark: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const SystemJS = System;
|
||||
|
||||
/**
|
||||
* Use this to load css for a Grafana plugin by specifying a {@link PluginCssOptions}
|
||||
* containing styling for the dark and the light theme.
|
||||
*
|
||||
* @param options - plugin styling for light and dark theme.
|
||||
* @public
|
||||
*/
|
||||
export function loadPluginCss(options: PluginCssOptions): Promise<any> {
|
||||
const theme = config.bootData.user.lightTheme ? options.light : options.dark;
|
||||
return SystemJS.import(`${theme}!css`);
|
||||
|
||||
@@ -15,7 +15,7 @@ const isLinkedMode = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
return fs.lstatSync(`${pwd}/node_modules/@grafana/toolkit`.replace('~', process.env.HOME)).isSymbolicLink();
|
||||
return fs.lstatSync(`${__dirname}/../../../node_modules/@grafana/toolkit`).isSymbolicLink();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { FunctionComponent } from 'react';
|
||||
|
||||
import { ColorPickerPopover, ColorPickerProps } from './ColorPickerPopover';
|
||||
import { PopoverContentProps } from '../Tooltip/Tooltip';
|
||||
import { Switch } from '../Switch/Switch';
|
||||
import { Switch } from '../Forms/Legacy/Switch/Switch';
|
||||
import { withTheme } from '../../themes/ThemeContext';
|
||||
|
||||
export interface SeriesColorPickerPopoverProps extends ColorPickerProps, PopoverContentProps {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { ChangeEvent, useContext } from 'react';
|
||||
import { DataLink, VariableSuggestion, GrafanaTheme } from '@grafana/data';
|
||||
import { FormField } from '../index';
|
||||
import { Switch } from '../Switch/Switch';
|
||||
import { Switch } from '../Forms/Legacy/Switch/Switch';
|
||||
import { css } from 'emotion';
|
||||
import { ThemeContext, stylesFactory } from '../../themes/index';
|
||||
import { DataLinkInput } from './DataLinkInput';
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Input } from '../Forms/Legacy/Input/Input';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { FormField } from '../FormField/FormField';
|
||||
import { FormLabel } from '../FormLabel/FormLabel';
|
||||
import { Switch } from '../Switch/Switch';
|
||||
import { Switch } from '../Forms/Legacy/Switch/Switch';
|
||||
import { TagsInput } from '../TagsInput/TagsInput';
|
||||
|
||||
const ACCESS_OPTIONS: Array<SelectableValue<string>> = [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { HttpSettingsBaseProps } from './types';
|
||||
import { Switch } from '../Switch/Switch';
|
||||
import { Switch } from '../Forms/Legacy/Switch/Switch';
|
||||
|
||||
export const HttpProxySettings: React.FC<HttpSettingsBaseProps> = ({ dataSourceConfig, onChange }) => {
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react';
|
||||
import { boolean, number, text } from '@storybook/addon-knobs';
|
||||
import { Field } from './Field';
|
||||
import { Input } from '../Input/Input';
|
||||
import { Switch } from './Switch';
|
||||
import { Switch } from '../Switch/Switch';
|
||||
import mdx from './Field.mdx';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Field } from './Field';
|
||||
import { Input } from '../Input/Input';
|
||||
import { Button } from '../Button';
|
||||
import { Form } from './Form';
|
||||
import { Switch } from './Switch';
|
||||
import { Switch } from '../Switch/Switch';
|
||||
import { Checkbox } from './Checkbox';
|
||||
|
||||
import { RadioButtonGroup } from './RadioButtonGroup/RadioButtonGroup';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Switch
|
||||
|
||||
A basic docs for Switch component
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import uniqueId from 'lodash/uniqueId';
|
||||
import { Tooltip } from '../../../Tooltip/Tooltip';
|
||||
import * as PopperJS from 'popper.js';
|
||||
|
||||
export interface Props {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
className?: string;
|
||||
labelClass?: string;
|
||||
switchClass?: string;
|
||||
tooltip?: string;
|
||||
tooltipPlacement?: PopperJS.Placement;
|
||||
transparent?: boolean;
|
||||
onChange: (event?: React.SyntheticEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export class Switch extends PureComponent<Props, State> {
|
||||
state = {
|
||||
id: uniqueId(),
|
||||
};
|
||||
|
||||
internalOnChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
event.stopPropagation();
|
||||
this.props.onChange(event);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
labelClass = '',
|
||||
switchClass = '',
|
||||
label,
|
||||
checked,
|
||||
transparent,
|
||||
className,
|
||||
tooltip,
|
||||
tooltipPlacement,
|
||||
} = this.props;
|
||||
|
||||
const labelId = this.state.id;
|
||||
const labelClassName = `gf-form-label ${labelClass} ${transparent ? 'gf-form-label--transparent' : ''} pointer`;
|
||||
const switchClassName = `gf-form-switch ${switchClass} ${transparent ? 'gf-form-switch--transparent' : ''}`;
|
||||
|
||||
return (
|
||||
<div className="gf-form-switch-container-react">
|
||||
<label htmlFor={labelId} className={`gf-form gf-form-switch-container ${className || ''}`}>
|
||||
{label && (
|
||||
<div className={labelClassName}>
|
||||
{label}
|
||||
{tooltip && (
|
||||
<Tooltip placement={tooltipPlacement ? tooltipPlacement : 'auto'} content={tooltip} theme={'info'}>
|
||||
<div className="gf-form-help-icon gf-form-help-icon--right-normal">
|
||||
<i className="fa fa-info-circle" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={switchClassName}>
|
||||
<input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />
|
||||
<span className="gf-form-switch__slider" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Meta, Story, Preview, Props } from "@storybook/addon-docs/blocks";
|
||||
import { Switch } from "./Switch";
|
||||
|
||||
<Meta title="MDX|Switch" component={Switch} />
|
||||
|
||||
# Switch
|
||||
|
||||
### When to use
|
||||
|
||||
`Switch` is a representation of an on-off state – like a light switch. So you can use `Switch` to toggle binary states.
|
||||
|
||||
Switches trigger changes immediately. If your component should trigger a change only after sending a form, it's better to use either `RadioButtonGroup` or `Checkbox` instead. Furthermore, switches cannot be grouped – each `Switch` triggers an independent state. If you want multiple mutually exclusive choices, the `RadioButtonGroup` is the better option. To offer multiple choices within the same group or context which are not mutually exclusive, use `Checkbox` instead.
|
||||
|
||||
|
||||
### Usage
|
||||
|
||||
```jsx
|
||||
import { Switch } from '@grafana/ui';
|
||||
|
||||
<Switch disabled={...} checked={...} onChange={...} />
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
<Props of={Switch} />
|
||||
@@ -1,101 +0,0 @@
|
||||
import React, { HTMLProps } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import uniqueId from 'lodash/uniqueId';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
import { getFocusCss } from './commonStyles';
|
||||
|
||||
export interface SwitchProps extends Omit<HTMLProps<HTMLInputElement>, 'value'> {
|
||||
value?: boolean;
|
||||
}
|
||||
|
||||
export const getSwitchStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
switch: css`
|
||||
width: 32px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
left: -100vw;
|
||||
z-index: -1000;
|
||||
position: absolute;
|
||||
|
||||
&:disabled + label {
|
||||
background: ${theme.colors.formSwitchBgDisabled};
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:checked + label {
|
||||
background: ${theme.colors.formSwitchBgActive};
|
||||
|
||||
&:hover {
|
||||
background: ${theme.colors.formSwitchBgActiveHover};
|
||||
}
|
||||
|
||||
&::after {
|
||||
transform: translate3d(18px, -50%, 0);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus + label {
|
||||
${getFocusCss(theme)};
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
background: ${theme.colors.formSwitchBg};
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${theme.colors.formSwitchBgHover};
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
background: ${theme.colors.formSwitchDot};
|
||||
top: 50%;
|
||||
transform: translate3d(2px, -50%, 0);
|
||||
transition: transform 0.2s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
export const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
|
||||
({ value, checked, disabled = false, onChange, ...inputProps }, ref) => {
|
||||
const theme = useTheme();
|
||||
const styles = getSwitchStyles(theme);
|
||||
const switchId = uniqueId('switch-');
|
||||
|
||||
return (
|
||||
<div className={cx(styles.switch)}>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
checked={value}
|
||||
onChange={event => {
|
||||
onChange?.(event);
|
||||
}}
|
||||
id={switchId}
|
||||
{...inputProps}
|
||||
ref={ref}
|
||||
/>
|
||||
<label htmlFor={switchId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -6,7 +6,7 @@ import { getFieldValidationMessageStyles } from './FieldValidationMessage';
|
||||
import { getButtonStyles, ButtonVariant } from '../Button';
|
||||
import { ComponentSize } from '../../types/size';
|
||||
import { getInputStyles } from '../Input/Input';
|
||||
import { getSwitchStyles } from './Switch';
|
||||
import { getSwitchStyles } from '../Switch/Switch';
|
||||
import { getCheckboxStyles } from './Checkbox';
|
||||
|
||||
export const getFormStyles = stylesFactory(
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Props, Preview } from "@storybook/addon-docs/blocks";
|
||||
import { Select, AsyncSelect, MultiSelect, AsyncMultiSelect } from "./Select";
|
||||
import { generateOptions } from "./mockOptions";
|
||||
|
||||
# Select variants
|
||||
|
||||
Select is an input with the ability to search and create new values. It should be used when you have a list of options. If the data has a tree structure, consider using `Cascader` instead.
|
||||
Select has some features:
|
||||
|
||||
- Search a list of values
|
||||
- Select multiple values
|
||||
- Select from async data
|
||||
- Create custom values that aren't in the list
|
||||
|
||||
## Select
|
||||
|
||||
Select is the base for every component on this page. The approaches mentioned here are also applicable to `AsyncSelect`, `MultiSelect`, `AsyncMultiSelect`.
|
||||
|
||||
### Options format
|
||||
|
||||
There are four properties for each option:
|
||||
|
||||
- `label` - Text that is visible in the menu.
|
||||
- `value` - Could be anything, but is usually a string. Used to identify what is **actually** selected.
|
||||
- `description` - Longer description that describes the choice. Use this sparingly.
|
||||
- `imgUrl` - URL to an image. Use this when an image or icon provides more context for the option.
|
||||
|
||||
```jsx
|
||||
const options = [
|
||||
{ label: "Basic option", value: 0 },
|
||||
{ label: "Option with description", value: 1, description: "this is a description" },
|
||||
{
|
||||
label: "Option with description and image",
|
||||
value: 2,
|
||||
description: "This is a very elaborate description, describing all the wonders in the world.",
|
||||
imgUrl: "https://placekitten.com/40/40",
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### Creatable option
|
||||
|
||||
Creatable option is used when you want to be able to add a custom value to the list of options. `allowCustomValue` needs to be true and you must handle the value creation with `onCreateOption`.
|
||||
|
||||
```jsx
|
||||
import { Select } from "@grafana/ui";
|
||||
|
||||
const SelectComponent = () => {
|
||||
const [value, setValue] = useState<SelectableValue<number>>();
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={option}
|
||||
value={value}
|
||||
allowCustomValue
|
||||
onCreateOption={customValue => {
|
||||
setValue(customValue);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## AsyncSelect
|
||||
|
||||
Like regular Select, but handles fetching options asynchronously. Use the `loadOptions` prop for the async function that loads the options. If `defaultOptions` is set to `true`, `loadOptions` will be called when the component is mounted.
|
||||
|
||||
```jsx
|
||||
|
||||
import { AsyncSelect } from '@grafana/ui';
|
||||
|
||||
const basicSelectAsync = () => {
|
||||
const [value, setValue] = useState<SelectableValue<string>>();
|
||||
|
||||
return (
|
||||
<AsyncSelect
|
||||
loadOptions={loadAsyncOptions}
|
||||
defaultOptions
|
||||
value={value}
|
||||
onChange={v => {
|
||||
setValue(v);
|
||||
}}
|
||||
size="md"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
Where the async function could look like this:
|
||||
|
||||
```tsx
|
||||
const loadAsyncOptions = () => {
|
||||
return new Promise()<Array<SelectableValue<string>>>(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(options);
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## MultiSelect
|
||||
|
||||
Possible to Select multiple values at the same time.
|
||||
|
||||
```tsx
|
||||
import { MultiSelect } from "@grafana/ui";
|
||||
|
||||
const multiSelect = () => {
|
||||
const [value, setValue] = useState<Array<SelectableValue<string>>>([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MultiSelect
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={v => {
|
||||
setValue(v);
|
||||
}}
|
||||
size="md"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## AsyncMultiSelect
|
||||
|
||||
Like MultiSelect but handles data asynchronously with the `loadOptions` prop.
|
||||
|
||||
## Props
|
||||
|
||||
<Props of={Select} />
|
||||
@@ -10,11 +10,18 @@ import { ButtonSelect } from './ButtonSelect';
|
||||
import { getIconKnob } from '../../utils/storybook/knobs';
|
||||
import kebabCase from 'lodash/kebabCase';
|
||||
import { generateOptions } from './mockOptions';
|
||||
import mdx from './Select.mdx';
|
||||
|
||||
export default {
|
||||
title: 'Forms/Select',
|
||||
component: Select,
|
||||
decorators: [withCenteredStory, withHorizontallyCenteredStory],
|
||||
subcomponents: { AsyncSelect, MultiSelect, AsyncMultiSelect },
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const loadAsyncOptions = () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { kebabCase } from 'lodash';
|
||||
|
||||
export const generateOptions = () => {
|
||||
export const generateOptions = (desc = false) => {
|
||||
const values = [
|
||||
'Sharilyn Markowitz',
|
||||
'Naomi Striplin',
|
||||
@@ -28,5 +28,6 @@ export const generateOptions = () => {
|
||||
return values.map<SelectableValue<string>>(name => ({
|
||||
value: kebabCase(name),
|
||||
label: name,
|
||||
description: desc ? `This is a description of ${name}` : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -6,12 +6,15 @@ export type SelectValue<T> = T | SelectableValue<T> | T[] | Array<SelectableValu
|
||||
|
||||
export interface SelectCommonProps<T> {
|
||||
allowCustomValue?: boolean;
|
||||
/** Focus is set to the Select when rendered*/
|
||||
autoFocus?: boolean;
|
||||
backspaceRemovesValue?: boolean;
|
||||
className?: string;
|
||||
/** Used for custom components. For more information, see `react-select` */
|
||||
components?: any;
|
||||
defaultValue?: any;
|
||||
disabled?: boolean;
|
||||
/** Function for formatting the text that is displayed when creating a new value*/
|
||||
formatCreateLabel?: (input: string) => string;
|
||||
getOptionLabel?: (item: SelectableValue<T>) => string;
|
||||
getOptionValue?: (item: SelectableValue<T>) => string;
|
||||
@@ -20,14 +23,17 @@ export interface SelectCommonProps<T> {
|
||||
isLoading?: boolean;
|
||||
isMulti?: boolean;
|
||||
isOpen?: boolean;
|
||||
/** Disables the possibility to type into the input*/
|
||||
isSearchable?: boolean;
|
||||
maxMenuHeight?: number;
|
||||
menuPlacement?: 'auto' | 'bottom' | 'top';
|
||||
menuPosition?: 'fixed' | 'absolute';
|
||||
/** The message to display when no options could be found */
|
||||
noOptionsMessage?: string;
|
||||
onBlur?: () => void;
|
||||
onChange: (value: SelectableValue<T>) => {} | void;
|
||||
onCloseMenu?: () => void;
|
||||
/** allowCustomValue must be enabled. Function decides what to do with that custom value. */
|
||||
onCreateOption?: (value: string) => void;
|
||||
onInputChange?: (label: string) => void;
|
||||
onKeyDown?: (event: React.KeyboardEvent) => void;
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
import { Meta, Story, Preview, Props } from "@storybook/addon-docs/blocks";
|
||||
import { Switch } from "./Switch";
|
||||
|
||||
<Meta title="MDX|Switch" component={Switch} />
|
||||
|
||||
# Switch
|
||||
|
||||
A basic docs for Switch component
|
||||
### When to use
|
||||
|
||||
`Switch` is a representation of an on-off state – like a light switch. So you can use `Switch` to toggle binary states.
|
||||
|
||||
Switches trigger changes immediately. If your component should trigger a change only after sending a form, it's better to use either `RadioButtonGroup` or `Checkbox` instead. Furthermore, switches cannot be grouped – each `Switch` triggers an independent state. If you want multiple mutually exclusive choices, the `RadioButtonGroup` is the better option. To offer multiple choices within the same group or context which are not mutually exclusive, use `Checkbox` instead.
|
||||
|
||||
|
||||
### Usage
|
||||
|
||||
```jsx
|
||||
import { Switch } from '@grafana/ui';
|
||||
|
||||
<Switch disabled={...} checked={...} onChange={...} />
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
<Props of={Switch} />
|
||||
|
||||
@@ -1,72 +1,101 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { HTMLProps } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import uniqueId from 'lodash/uniqueId';
|
||||
import { Tooltip } from '../Tooltip/Tooltip';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import * as PopperJS from 'popper.js';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
import { getFocusCss } from '../Forms/commonStyles';
|
||||
|
||||
export interface Props {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
className?: string;
|
||||
labelClass?: string;
|
||||
switchClass?: string;
|
||||
tooltip?: string;
|
||||
tooltipPlacement?: PopperJS.Placement;
|
||||
transparent?: boolean;
|
||||
onChange: (event?: React.SyntheticEvent<HTMLInputElement>) => void;
|
||||
export interface SwitchProps extends Omit<HTMLProps<HTMLInputElement>, 'value'> {
|
||||
value?: boolean;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
id: string;
|
||||
}
|
||||
export const getSwitchStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
switch: css`
|
||||
width: 32px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
|
||||
export class Switch extends PureComponent<Props, State> {
|
||||
state = {
|
||||
id: uniqueId(),
|
||||
input {
|
||||
opacity: 0;
|
||||
left: -100vw;
|
||||
z-index: -1000;
|
||||
position: absolute;
|
||||
|
||||
&:disabled + label {
|
||||
background: ${theme.colors.formSwitchBgDisabled};
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:checked + label {
|
||||
background: ${theme.colors.formSwitchBgActive};
|
||||
|
||||
&:hover {
|
||||
background: ${theme.colors.formSwitchBgActiveHover};
|
||||
}
|
||||
|
||||
&::after {
|
||||
transform: translate3d(18px, -50%, 0);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus + label {
|
||||
${getFocusCss(theme)};
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
background: ${theme.colors.formSwitchBg};
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${theme.colors.formSwitchBgHover};
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
background: ${theme.colors.formSwitchDot};
|
||||
top: 50%;
|
||||
transform: translate3d(2px, -50%, 0);
|
||||
transition: transform 0.2s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
internalOnChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
event.stopPropagation();
|
||||
this.props.onChange(event);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
labelClass = '',
|
||||
switchClass = '',
|
||||
label,
|
||||
checked,
|
||||
transparent,
|
||||
className,
|
||||
tooltip,
|
||||
tooltipPlacement,
|
||||
} = this.props;
|
||||
|
||||
const labelId = this.state.id;
|
||||
const labelClassName = `gf-form-label ${labelClass} ${transparent ? 'gf-form-label--transparent' : ''} pointer`;
|
||||
const switchClassName = `gf-form-switch ${switchClass} ${transparent ? 'gf-form-switch--transparent' : ''}`;
|
||||
export const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
|
||||
({ value, checked, disabled = false, onChange, ...inputProps }, ref) => {
|
||||
const theme = useTheme();
|
||||
const styles = getSwitchStyles(theme);
|
||||
const switchId = uniqueId('switch-');
|
||||
|
||||
return (
|
||||
<div className="gf-form-switch-container-react">
|
||||
<label htmlFor={labelId} className={`gf-form gf-form-switch-container ${className || ''}`}>
|
||||
{label && (
|
||||
<div className={labelClassName}>
|
||||
{label}
|
||||
{tooltip && (
|
||||
<Tooltip placement={tooltipPlacement ? tooltipPlacement : 'auto'} content={tooltip} theme={'info'}>
|
||||
<div className="gf-form-help-icon gf-form-help-icon--right-normal">
|
||||
<Icon name="info-circle" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={switchClassName}>
|
||||
<input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />
|
||||
<span className="gf-form-switch__slider" />
|
||||
</div>
|
||||
</label>
|
||||
<div className={cx(styles.switch)}>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
checked={value}
|
||||
onChange={event => {
|
||||
onChange?.(event);
|
||||
}}
|
||||
id={switchId}
|
||||
{...inputProps}
|
||||
ref={ref}
|
||||
/>
|
||||
<label htmlFor={switchId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -143,8 +143,10 @@ export { HorizontalGroup, VerticalGroup, Container } from './Layout/Layout';
|
||||
export { RadioButtonGroup } from './Forms/RadioButtonGroup/RadioButtonGroup';
|
||||
|
||||
export { Input } from './Input/Input';
|
||||
export { Switch } from './Forms/Switch';
|
||||
|
||||
export { Switch } from './Switch/Switch';
|
||||
export { Checkbox } from './Forms/Checkbox';
|
||||
|
||||
export { TextArea } from './TextArea/TextArea';
|
||||
|
||||
// Legacy forms
|
||||
@@ -158,7 +160,7 @@ import { ButtonSelect } from './Forms/Legacy/Select/ButtonSelect';
|
||||
//Input
|
||||
import { Input, LegacyInputStatus } from './Forms/Legacy/Input/Input';
|
||||
|
||||
import { Switch } from './Switch/Switch';
|
||||
import { Switch } from './Forms/Legacy/Switch/Switch';
|
||||
|
||||
const LegacyForms = {
|
||||
Select,
|
||||
|
||||
@@ -19,8 +19,9 @@ import {
|
||||
valueMappingsOverrideProcessor,
|
||||
ThresholdsMode,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { Switch } from '../components/Switch/Switch';
|
||||
import { NumberValueEditor, RadioButtonGroup, StringValueEditor, Select } from '../components';
|
||||
import { Switch } from '../components/Forms/Switch';
|
||||
import { ValueMappingsValueEditor } from '../components/OptionsUI/mappings';
|
||||
import { ThresholdsValueEditor } from '../components/OptionsUI/thresholds';
|
||||
import { UnitValueEditor } from '../components/OptionsUI/units';
|
||||
|
||||
@@ -14,6 +14,7 @@ func AdminCreateUser(c *models.ReqContext, form dtos.AdminCreateUserForm) {
|
||||
Email: form.Email,
|
||||
Password: form.Password,
|
||||
Name: form.Name,
|
||||
OrgId: form.OrgId,
|
||||
}
|
||||
|
||||
if len(cmd.Login) == 0 {
|
||||
@@ -30,6 +31,11 @@ func AdminCreateUser(c *models.ReqContext, form dtos.AdminCreateUserForm) {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == models.ErrOrgNotFound {
|
||||
c.JsonApiErr(400, models.ErrOrgNotFound.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
c.JsonApiErr(500, "failed to create user", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ import (
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
const (
|
||||
TestLogin = "test@example.com"
|
||||
TestPassword = "password"
|
||||
nonExistingOrgID = 1000
|
||||
)
|
||||
|
||||
func TestAdminApiEndpoint(t *testing.T) {
|
||||
role := models.ROLE_ADMIN
|
||||
Convey("Given a server admin attempts to remove themself as an admin", t, func() {
|
||||
@@ -175,6 +181,85 @@ func TestAdminApiEndpoint(t *testing.T) {
|
||||
So(userId, ShouldEqual, 42)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When a server admin attempts to create a user", t, func() {
|
||||
var userLogin string
|
||||
var orgId int64
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.CreateUserCommand) error {
|
||||
userLogin = cmd.Login
|
||||
orgId = cmd.OrgId
|
||||
|
||||
if orgId == nonExistingOrgID {
|
||||
return models.ErrOrgNotFound
|
||||
}
|
||||
|
||||
cmd.Result = models.User{Id: TestUserID}
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("Without an organization", func() {
|
||||
createCmd := dtos.AdminCreateUserForm{
|
||||
Login: TestLogin,
|
||||
Password: TestPassword,
|
||||
}
|
||||
|
||||
adminCreateUserScenario("Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
|
||||
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
|
||||
So(err, ShouldBeNil)
|
||||
So(respJSON.Get("id").MustInt64(), ShouldEqual, TestUserID)
|
||||
So(respJSON.Get("message").MustString(), ShouldEqual, "User created")
|
||||
|
||||
// test that userLogin and orgId were transmitted correctly to the handler
|
||||
So(userLogin, ShouldEqual, TestLogin)
|
||||
So(orgId, ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("With an organization", func() {
|
||||
createCmd := dtos.AdminCreateUserForm{
|
||||
Login: TestLogin,
|
||||
Password: TestPassword,
|
||||
OrgId: TestOrgID,
|
||||
}
|
||||
|
||||
adminCreateUserScenario("Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
|
||||
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
|
||||
So(err, ShouldBeNil)
|
||||
So(respJSON.Get("id").MustInt64(), ShouldEqual, TestUserID)
|
||||
So(respJSON.Get("message").MustString(), ShouldEqual, "User created")
|
||||
|
||||
So(userLogin, ShouldEqual, TestLogin)
|
||||
So(orgId, ShouldEqual, TestOrgID)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("With a nonexistent organization", func() {
|
||||
createCmd := dtos.AdminCreateUserForm{
|
||||
Login: TestLogin,
|
||||
Password: TestPassword,
|
||||
OrgId: nonExistingOrgID,
|
||||
}
|
||||
|
||||
adminCreateUserScenario("Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
So(sc.resp.Code, ShouldEqual, 400)
|
||||
|
||||
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
|
||||
So(err, ShouldBeNil)
|
||||
So(respJSON.Get("message").MustString(), ShouldEqual, "Organization not found")
|
||||
|
||||
So(userLogin, ShouldEqual, TestLogin)
|
||||
So(orgId, ShouldEqual, 1000)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func putAdminScenario(desc string, url string, routePattern string, role models.RoleType, cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) {
|
||||
@@ -324,3 +409,21 @@ func adminDeleteUserScenario(desc string, url string, routePattern string, fn sc
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
func adminCreateUserScenario(desc string, url string, routePattern string, cmd dtos.AdminCreateUserForm, fn scenarioFunc) {
|
||||
Convey(desc+" "+url, func() {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
sc := setupScenarioContext(url)
|
||||
sc.defaultHandler = Wrap(func(c *models.ReqContext) {
|
||||
sc.context = c
|
||||
sc.context.UserId = TestUserID
|
||||
|
||||
AdminCreateUser(c, cmd)
|
||||
})
|
||||
|
||||
sc.m.Post(routePattern, sc.defaultHandler)
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ type AdminCreateUserForm struct {
|
||||
Login string `json:"login"`
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password" binding:"Required"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
}
|
||||
|
||||
type AdminUpdateUserForm struct {
|
||||
|
||||
@@ -58,6 +58,7 @@ type CreateUserCommand struct {
|
||||
Login string
|
||||
Name string
|
||||
Company string
|
||||
OrgId int64
|
||||
OrgName string
|
||||
Password string
|
||||
EmailVerified bool
|
||||
|
||||
@@ -220,6 +220,18 @@ func DeleteOrg(cmd *models.DeleteOrgCommand) error {
|
||||
})
|
||||
}
|
||||
|
||||
func verifyExistingOrg(sess *DBSession, orgId int64) error {
|
||||
var org models.Org
|
||||
has, err := sess.Where("id=?", orgId).Get(&org)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return models.ErrOrgNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getOrCreateOrg(sess *DBSession, orgName string) (int64, error) {
|
||||
var org models.Org
|
||||
|
||||
|
||||
@@ -41,6 +41,14 @@ func getOrgIdForNewUser(sess *DBSession, cmd *models.CreateUserCommand) (int64,
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
if setting.AutoAssignOrg && cmd.OrgId != 0 {
|
||||
err := verifyExistingOrg(sess, cmd.OrgId)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return cmd.OrgId, nil
|
||||
}
|
||||
|
||||
orgName := cmd.OrgName
|
||||
if len(orgName) == 0 {
|
||||
orgName = util.StringsFallback2(cmd.Email, cmd.Login)
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
@@ -63,6 +65,56 @@ func TestUserDataAccess(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given an organization", func() {
|
||||
autoAssignOrg := setting.AutoAssignOrg
|
||||
setting.AutoAssignOrg = true
|
||||
defer func() {
|
||||
setting.AutoAssignOrg = autoAssignOrg
|
||||
}()
|
||||
|
||||
orgCmd := &models.CreateOrgCommand{Name: "Some Test Org"}
|
||||
err := CreateOrg(orgCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Creates user assigned to other organization", func() {
|
||||
cmd := &models.CreateUserCommand{
|
||||
Email: "usertest@test.com",
|
||||
Name: "user name",
|
||||
Login: "user_test_login",
|
||||
OrgId: orgCmd.Result.Id,
|
||||
}
|
||||
|
||||
err := CreateUser(context.Background(), cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Loading a user", func() {
|
||||
query := models.GetUserByIdQuery{Id: cmd.Result.Id}
|
||||
err := GetUserById(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(query.Result.Email, ShouldEqual, "usertest@test.com")
|
||||
So(query.Result.Password, ShouldEqual, "")
|
||||
So(query.Result.Rands, ShouldHaveLength, 10)
|
||||
So(query.Result.Salt, ShouldHaveLength, 10)
|
||||
So(query.Result.IsDisabled, ShouldBeFalse)
|
||||
So(query.Result.OrgId, ShouldEqual, orgCmd.Result.Id)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Don't create user assigned to unknown organization", func() {
|
||||
const nonExistingOrgID = 10000
|
||||
cmd := &models.CreateUserCommand{
|
||||
Email: "usertest@test.com",
|
||||
Name: "user name",
|
||||
Login: "user_test_login",
|
||||
OrgId: nonExistingOrgID,
|
||||
}
|
||||
|
||||
err := CreateUser(context.Background(), cmd)
|
||||
So(err, ShouldEqual, models.ErrOrgNotFound)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given 5 users", func() {
|
||||
users := createFiveTestUsers(func(i int) *models.CreateUserCommand {
|
||||
return &models.CreateUserCommand{
|
||||
|
||||
@@ -988,6 +988,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
pluginsSection := iniFile.Section("plugins")
|
||||
cfg.PluginsEnableAlpha = pluginsSection.Key("enable_alpha").MustBool(false)
|
||||
cfg.PluginsAppsSkipVerifyTLS = pluginsSection.Key("app_tls_skip_verify_insecure").MustBool(false)
|
||||
cfg.PluginSettings = extractPluginSettings(iniFile.Sections())
|
||||
|
||||
// Read and populate feature toggles list
|
||||
featureTogglesSection := iniFile.Section("feature_toggles")
|
||||
|
||||
@@ -49,6 +49,7 @@ func init() {
|
||||
"AWS/Athena": {"DataScannedInBytes", "EngineExecutionTime", "QueryPlanningTime", "QueryQueueTime", "QueryState", "QueryType", "ServiceProcessingTime", "TotalExecutionTime", "WorkGroup"},
|
||||
"AWS/AutoScaling": {"GroupDesiredCapacity", "GroupInServiceInstances", "GroupMaxSize", "GroupMinSize", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"},
|
||||
"AWS/Billing": {"EstimatedCharges"},
|
||||
"AWS/Chatbot": {"EventsThrottled", "EventsProcessed", "MessageDeliverySuccess", "MessageDeliveryFailure", "UnsupportedEvents"},
|
||||
"AWS/CloudFront": {"4xxErrorRate", "5xxErrorRate", "BytesDownloaded", "BytesUploaded", "Requests", "TotalErrorRate"},
|
||||
"AWS/CloudHSM": {"HsmKeysSessionOccupied", "HsmKeysTokenOccupied", "HsmSessionCount", "HsmSslCtxsOccupied", "HsmTemperature", "HsmUnhealthy", "HsmUsersAvailable", "HsmUsersMax", "InterfaceEth2OctetsInput", "InterfaceEth2OctetsOutput"},
|
||||
"AWS/CloudSearch": {"IndexUtilization", "Partitions", "SearchableDocuments", "SuccessfulRequests"},
|
||||
@@ -135,6 +136,7 @@ func init() {
|
||||
"AWS/ApplicationELB": {"AvailabilityZone", "LoadBalancer", "TargetGroup"},
|
||||
"AWS/AutoScaling": {"AutoScalingGroupName"},
|
||||
"AWS/Billing": {"Currency", "LinkedAccount", "ServiceName"},
|
||||
"AWS/Chatbot": {"ConfigurationName"},
|
||||
"AWS/CloudFront": {"DistributionId", "Region"},
|
||||
"AWS/CloudHSM": {"ClusterId", "HsmId", "Region"},
|
||||
"AWS/CloudSearch": {"ClientId", "DomainName"},
|
||||
|
||||
Reference in New Issue
Block a user