diff --git a/.circleci/config.yml b/.circleci/config.yml index d46d5354089..3dfc9e5e4f0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/api-extractor.json b/api-extractor.json index 2751721e9d5..af1049c5a08 100644 --- a/api-extractor.json +++ b/api-extractor.json @@ -23,6 +23,10 @@ "extractorMessageReporting": { "default": { "logLevel": "warning" + }, + "ae-internal-missing-underscore": { + "logLevel": "none", + "addToApiReportFile": false } }, "tsdocMessageReporting": { diff --git a/docs/sources/http_api/admin.md b/docs/sources/http_api/admin.md index 125a8e38bb9..77d56f66cf7 100644 --- a/docs/sources/http_api/admin.md +++ b/docs/sources/http_api/admin.md @@ -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 diff --git a/docs/sources/installation/behind_proxy.md b/docs/sources/installation/behind_proxy.md deleted file mode 100644 index 4f0a80bfe8d..00000000000 --- a/docs/sources/installation/behind_proxy.md +++ /dev/null @@ -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 - - - - - - - - -``` - -See the [tutorial on IIS URL Rewrites](http://docs.grafana.org/tutorials/iis/) for more in-depth instructions. diff --git a/docs/sources/menu.yaml b/docs/sources/menu.yaml index 1dd52ace031..ee83f2bd085 100644 --- a/docs/sources/menu.yaml +++ b/docs/sources/menu.yaml @@ -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: diff --git a/docs/sources/plugins/developing/code-styleguide.md b/docs/sources/plugins/developing/code-styleguide.md index fabc0e9f978..288ce7c5fde 100644 --- a/docs/sources/plugins/developing/code-styleguide.md +++ b/docs/sources/plugins/developing/code-styleguide.md @@ -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. diff --git a/docs/sources/tutorials/_index.md b/docs/sources/tutorials/_index.md index 66ff8c14531..8911fc4e15d 100755 --- a/docs/sources/tutorials/_index.md +++ b/docs/sources/tutorials/_index.md @@ -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 diff --git a/docs/sources/tutorials/hubot_howto.md b/docs/sources/tutorials/hubot_howto.md deleted file mode 100755 index 162f9748f5b..00000000000 --- a/docs/sources/tutorials/hubot_howto.md +++ /dev/null @@ -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). - -
- -
- -## 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` - -
- Using the alias:
- -
- -## 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! - - diff --git a/docs/sources/tutorials/iis.md b/docs/sources/tutorials/iis.md deleted file mode 100644 index 6a2b6f7368b..00000000000 --- a/docs/sources/tutorials/iis.md +++ /dev/null @@ -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/`. diff --git a/e2e/suite1/specs/queryVariableCrud.spec.ts b/e2e/suite1/specs/queryVariableCrud.spec.ts index 6dec80d8513..162392b274d 100644 --- a/e2e/suite1/specs/queryVariableCrud.spec.ts +++ b/e2e/suite1/specs/queryVariableCrud.spec.ts @@ -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); - }); }, }); diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 987eff39a7b..8b1903f48a9 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -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 }; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 61417149332..8fe1bd70184 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -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); diff --git a/packages/grafana-runtime/src/index.ts b/packages/grafana-runtime/src/index.ts index 347e4df40d2..2b0463417aa 100644 --- a/packages/grafana-runtime/src/index.ts +++ b/packages/grafana-runtime/src/index.ts @@ -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'; diff --git a/packages/grafana-runtime/src/services/AngularLoader.ts b/packages/grafana-runtime/src/services/AngularLoader.ts index 9565a6d41f4..8948e9e8922 100644 --- a/packages/grafana-runtime/src/services/AngularLoader.ts +++ b/packages/grafana-runtime/src/services/AngularLoader.ts @@ -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 { + * element: HTMLElement; + * angularComponent: AngularComponent; + * + * componentDidMount() { + * const template = '' // 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 ( + *
(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; } diff --git a/packages/grafana-runtime/src/services/EchoSrv.ts b/packages/grafana-runtime/src/services/EchoSrv.ts index 2fcf729b781..23e127e7e18 100644 --- a/packages/grafana-runtime/src/services/EchoSrv.ts +++ b/packages/grafana-runtime/src/services/EchoSrv.ts @@ -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 { options: O; supportedEvents: EchoEventType[]; @@ -32,33 +56,84 @@ export interface EchoBackend { addEvent: (event: T) => void; } +/** + * Describes an echo event. + * + * @public + */ export interface EchoEvent { 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(event: Omit, 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); }; diff --git a/packages/grafana-runtime/src/services/LocationSrv.ts b/packages/grafana-runtime/src/services/LocationSrv.ts index 0f041feef0a..12dec43eaac 100644 --- a/packages/grafana-runtime/src/services/LocationSrv.ts +++ b/packages/grafana-runtime/src/services/LocationSrv.ts @@ -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; +/** + * 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; } diff --git a/packages/grafana-runtime/src/services/backendSrv.ts b/packages/grafana-runtime/src/services/backendSrv.ts index 2585cdd1395..9e107709bbe 100644 --- a/packages/grafana-runtime/src/services/backendSrv.ts +++ b/packages/grafana-runtime/src/services/backendSrv.ts @@ -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; - delete(url: string): Promise; - post(url: string, data?: any): Promise; - patch(url: string, data?: any): Promise; - put(url: string, data?: any): Promise; - - // If there is an error, set: err.isHandled = true - // otherwise the backend will show a message for you request(options: BackendSrvRequest): Promise; - // 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; } 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; diff --git a/packages/grafana-runtime/src/services/dataSourceSrv.ts b/packages/grafana-runtime/src/services/dataSourceSrv.ts index f0894644c80..4e87fc56157 100644 --- a/packages/grafana-runtime/src/services/dataSourceSrv.ts +++ b/packages/grafana-runtime/src/services/dataSourceSrv.ts @@ -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; } 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; } diff --git a/packages/grafana-runtime/src/services/templateSrv.ts b/packages/grafana-runtime/src/services/templateSrv.ts index 40fb64932e9..3362b581fcc 100644 --- a/packages/grafana-runtime/src/services/templateSrv.ts +++ b/packages/grafana-runtime/src/services/templateSrv.ts @@ -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; diff --git a/packages/grafana-runtime/src/types/analytics.ts b/packages/grafana-runtime/src/types/analytics.ts index 41a792d6ac5..526fb1b3acf 100644 --- a/packages/grafana-runtime/src/types/analytics.ts +++ b/packages/grafana-runtime/src/types/analytics.ts @@ -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 { datasourceName: string; datasourceId?: number; @@ -17,19 +28,44 @@ export interface DataRequestInfo extends Partial { 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 {} diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts index 7d408a0eff7..535cb20b265 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts @@ -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; } +/** + * 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; diff --git a/packages/grafana-runtime/src/utils/analytics.ts b/packages/grafana-runtime/src/utils/analytics.ts index 72da85390de..b3f090b3314 100644 --- a/packages/grafana-runtime/src/utils/analytics.ts +++ b/packages/grafana-runtime/src/utils/analytics.ts @@ -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({ type: EchoEventType.MetaAnalytics, diff --git a/packages/grafana-runtime/src/utils/plugin.ts b/packages/grafana-runtime/src/utils/plugin.ts index 1e12cb75850..5791dd00519 100644 --- a/packages/grafana-runtime/src/utils/plugin.ts +++ b/packages/grafana-runtime/src/utils/plugin.ts @@ -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 { const theme = config.bootData.user.lightTheme ? options.light : options.dark; return SystemJS.import(`${theme}!css`); diff --git a/packages/grafana-toolkit/bin/grafana-toolkit.js b/packages/grafana-toolkit/bin/grafana-toolkit.js index 2cb78b0c5a1..8bc6003d734 100755 --- a/packages/grafana-toolkit/bin/grafana-toolkit.js +++ b/packages/grafana-toolkit/bin/grafana-toolkit.js @@ -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; } diff --git a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx index c40d7434611..bd64b5f230c 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx @@ -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 { diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx index f622f12ede3..74831fdb7d9 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx @@ -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'; diff --git a/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.tsx b/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.tsx index e6ec09478db..f2698f10882 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.tsx @@ -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> = [ diff --git a/packages/grafana-ui/src/components/DataSourceSettings/HttpProxySettings.tsx b/packages/grafana-ui/src/components/DataSourceSettings/HttpProxySettings.tsx index bb09064a78a..9b6622d2640 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/HttpProxySettings.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/HttpProxySettings.tsx @@ -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 = ({ dataSourceConfig, onChange }) => { return ( diff --git a/packages/grafana-ui/src/components/Forms/Field.story.tsx b/packages/grafana-ui/src/components/Forms/Field.story.tsx index 75cd5dabfee..41aeaed495d 100644 --- a/packages/grafana-ui/src/components/Forms/Field.story.tsx +++ b/packages/grafana-ui/src/components/Forms/Field.story.tsx @@ -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 { diff --git a/packages/grafana-ui/src/components/Forms/Form.story.tsx b/packages/grafana-ui/src/components/Forms/Form.story.tsx index 0148d5e5f08..ded4478e682 100644 --- a/packages/grafana-ui/src/components/Forms/Form.story.tsx +++ b/packages/grafana-ui/src/components/Forms/Form.story.tsx @@ -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'; diff --git a/packages/grafana-ui/src/components/Forms/Legacy/Switch/Switch.mdx b/packages/grafana-ui/src/components/Forms/Legacy/Switch/Switch.mdx new file mode 100644 index 00000000000..f347718b212 --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/Legacy/Switch/Switch.mdx @@ -0,0 +1,3 @@ +# Switch + +A basic docs for Switch component diff --git a/packages/grafana-ui/src/components/Switch/Switch.story.internal.tsx b/packages/grafana-ui/src/components/Forms/Legacy/Switch/Switch.story.internal.tsx similarity index 100% rename from packages/grafana-ui/src/components/Switch/Switch.story.internal.tsx rename to packages/grafana-ui/src/components/Forms/Legacy/Switch/Switch.story.internal.tsx diff --git a/packages/grafana-ui/src/components/Forms/Legacy/Switch/Switch.tsx b/packages/grafana-ui/src/components/Forms/Legacy/Switch/Switch.tsx new file mode 100644 index 00000000000..a0c6fc958c2 --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/Legacy/Switch/Switch.tsx @@ -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) => void; +} + +export interface State { + id: string; +} + +export class Switch extends PureComponent { + state = { + id: uniqueId(), + }; + + internalOnChange = (event: React.FormEvent) => { + 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 ( +
+ +
+ ); + } +} diff --git a/packages/grafana-ui/src/components/Forms/Switch.mdx b/packages/grafana-ui/src/components/Forms/Switch.mdx deleted file mode 100644 index 1e1936a26cc..00000000000 --- a/packages/grafana-ui/src/components/Forms/Switch.mdx +++ /dev/null @@ -1,25 +0,0 @@ -import { Meta, Story, Preview, Props } from "@storybook/addon-docs/blocks"; -import { Switch } from "./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'; - - -``` - -### Props - - diff --git a/packages/grafana-ui/src/components/Forms/Switch.tsx b/packages/grafana-ui/src/components/Forms/Switch.tsx deleted file mode 100644 index 0848257a939..00000000000 --- a/packages/grafana-ui/src/components/Forms/Switch.tsx +++ /dev/null @@ -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, '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( - ({ value, checked, disabled = false, onChange, ...inputProps }, ref) => { - const theme = useTheme(); - const styles = getSwitchStyles(theme); - const switchId = uniqueId('switch-'); - - return ( -
- { - onChange?.(event); - }} - id={switchId} - {...inputProps} - ref={ref} - /> -
- ); - } -); diff --git a/packages/grafana-ui/src/components/Forms/getFormStyles.ts b/packages/grafana-ui/src/components/Forms/getFormStyles.ts index c46c3b003fd..11c329902bd 100644 --- a/packages/grafana-ui/src/components/Forms/getFormStyles.ts +++ b/packages/grafana-ui/src/components/Forms/getFormStyles.ts @@ -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( diff --git a/packages/grafana-ui/src/components/Select/Select.mdx b/packages/grafana-ui/src/components/Select/Select.mdx new file mode 100644 index 00000000000..5ca57e225c5 --- /dev/null +++ b/packages/grafana-ui/src/components/Select/Select.mdx @@ -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>(); + + return ( + - -
- +
+ { + onChange?.(event); + }} + id={switchId} + {...inputProps} + ref={ref} + /> +
); } -} +); diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 46b334b8a63..ba1579a9d75 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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, diff --git a/packages/grafana-ui/src/utils/standardEditors.tsx b/packages/grafana-ui/src/utils/standardEditors.tsx index 470ba283ea0..3bdcea7b38e 100644 --- a/packages/grafana-ui/src/utils/standardEditors.tsx +++ b/packages/grafana-ui/src/utils/standardEditors.tsx @@ -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'; diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index dff36c19084..4fd43ef7125 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -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 } diff --git a/pkg/api/admin_users_test.go b/pkg/api/admin_users_test.go index bed74a2c688..c7b32f65976 100644 --- a/pkg/api/admin_users_test.go +++ b/pkg/api/admin_users_test.go @@ -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) + }) +} diff --git a/pkg/api/dtos/user.go b/pkg/api/dtos/user.go index d6a58a98d71..3800ace02fc 100644 --- a/pkg/api/dtos/user.go +++ b/pkg/api/dtos/user.go @@ -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 { diff --git a/pkg/models/user.go b/pkg/models/user.go index 3cf9a96334e..7ecdba21137 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -58,6 +58,7 @@ type CreateUserCommand struct { Login string Name string Company string + OrgId int64 OrgName string Password string EmailVerified bool diff --git a/pkg/services/sqlstore/org.go b/pkg/services/sqlstore/org.go index 06e4350303b..4f46d10077e 100644 --- a/pkg/services/sqlstore/org.go +++ b/pkg/services/sqlstore/org.go @@ -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 diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index b4f78a5a6f0..a1294f041ce 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -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) diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index 4651ddbbb60..514fdbb6818 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -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{ diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 93358ca2e27..235aa6dd02d 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -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") diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 31d37434ca8..35a8163ff75 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -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"},