Merge branch 'master' into table-datalinks

This commit is contained in:
Dominik Prokop
2020-04-15 15:23:00 +02:00
54 changed files with 987 additions and 662 deletions
+22 -4
View File
@@ -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
+4
View File
@@ -23,6 +23,10 @@
"extractorMessageReporting": {
"default": {
"logLevel": "warning"
},
"ae-internal-missing-underscore": {
"logLevel": "none",
"addToApiReportFile": false
}
},
"tsdocMessageReporting": {
+4 -1
View File
@@ -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
-166
View File
@@ -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.
-9
View File
@@ -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.
-3
View File
@@ -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
-139
View File
@@ -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!
-89
View File
@@ -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/`.
+5 -21
View File
@@ -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);
});
},
});
+33 -2
View File
@@ -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 };
+8 -3
View File
@@ -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);
+2 -2
View File
@@ -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>
);
}
}
);
+4 -2
View File
@@ -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';
+6
View File
@@ -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
}
+103
View File
@@ -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)
})
}
+1
View File
@@ -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 {
+1
View File
@@ -58,6 +58,7 @@ type CreateUserCommand struct {
Login string
Name string
Company string
OrgId int64
OrgName string
Password string
EmailVerified bool
+12
View File
@@ -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
+8
View File
@@ -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)
+52
View File
@@ -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{
+1
View File
@@ -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")
+2
View File
@@ -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"},