diff --git a/.gitignore b/.gitignore index d599f762840..2945746832a 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ devenv/docker-compose.yaml /conf/provisioning/**/custom.yaml /conf/provisioning/**/dev.yaml /conf/ldap_dev.toml +/conf/ldap_freeipa.toml profile.cov /grafana /local diff --git a/CHANGELOG.md b/CHANGELOG.md index a603fbb1a78..e6971f7952c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,17 @@ * **Stackdriver**: Template variables in filters using globbing format [#15182](https://github.com/grafana/grafana/issues/15182) * **Cloudwatch**: Add `resource_arns` template variable query function [#8207](https://github.com/grafana/grafana/issues/8207), thx [@jeroenvollenbrock](https://github.com/jeroenvollenbrock) * **Cloudwatch**: Add AWS/Neptune metrics [#14231](https://github.com/grafana/grafana/issues/14231), thx [@tcpatterson](https://github.com/tcpatterson) +* **Cloudwatch**: Add AWS/EC2/API metrics [#14233](https://github.com/grafana/grafana/issues/14233), thx [@tcpatterson](https://github.com/tcpatterson) * **Cloudwatch**: Add AWS RDS ServerlessDatabaseCapacity metric [#15265](https://github.com/grafana/grafana/pull/15265), thx [@larsjoergensen](https://github.com/larsjoergensen) +* **MySQL**: Adds datasource SSL CA/client certificates support [#8570](https://github.com/grafana/grafana/issues/8570), thx [@bugficks](https://github.com/bugficks) +* **MSSQL**: Timerange are now passed for template variable queries [#13324](https://github.com/grafana/grafana/issues/13324), thx [@thatsparesh](https://github.com/thatsparesh) * **Annotations**: Support PATCH verb in annotations http api [#12546](https://github.com/grafana/grafana/issues/12546), thx [@SamuelToh](https://github.com/SamuelToh) * **Templating**: Add json formatting to variable interpolation [#15291](https://github.com/grafana/grafana/issues/15291), thx [@mtanda](https://github.com/mtanda) +* **Login**: Anonymous usage stats for token auth [#15288](https://github.com/grafana/grafana/issues/15288) + +### 6.0.0-beta1 fixes + +* **Postgres**: Fix default port not added when port not configured [#15189](https://github.com/grafana/grafana/issues/15189) # 6.0.0-beta1 (2019-01-30) diff --git a/README.md b/README.md index 658f1e34257..2cb8bfee306 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,18 @@ Grafana is an open source, feature rich metrics dashboard and graph editor for Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB. +![](https://www.grafanacon.org/2019/images/grafanacon_la_nav-logo.png) + +Join us Feb 25-26 in Los Angeles, California for GrafanaCon - a two-day event with talks focused on Grafana and the surrounding open source monitoring ecosystem. Get deep dives into Loki, the Explore workflow and all of the new features of Grafana 6, plus participate in hands on workshops to help you get the most out of your data. + +Time is running out - grab your ticket now! http://grafanacon.org + + ## Installation -Head to [docs.grafana.org](http://docs.grafana.org/installation/) and [download](https://grafana.com/get) -the latest release. - -If you have any problems please read the [troubleshooting guide](http://docs.grafana.org/installation/troubleshooting/). +Head to [docs.grafana.org](http://docs.grafana.org/installation/) for documentation or [download](https://grafana.com/get) to get the latest release. ## Documentation & Support Be sure to read the [getting started guide](http://docs.grafana.org/guides/gettingstarted/) and the other feature guides. diff --git a/devenv/docker/blocks/freeipa/docker-compose.yaml b/devenv/docker/blocks/freeipa/docker-compose.yaml new file mode 100644 index 00000000000..8a9a5705f9d --- /dev/null +++ b/devenv/docker/blocks/freeipa/docker-compose.yaml @@ -0,0 +1,54 @@ +version: '3' + +volumes: + freeipa_data: {} + +services: + freeipa: + image: freeipa/freeipa-server:fedora-29 + container_name: freeipa + stdin_open: true + tty: true + sysctls: + - net.ipv6.conf.all.disable_ipv6=0 + hostname: ipa.example.test + environment: + # - DEBUG_TRACE=1 + - IPA_SERVER_IP=172.17.0.2 + - DEBUG_NO_EXIT=1 + - IPA_SERVER_HOSTNAME=ipa.example.test + - PASSWORD=Secret123 + - HOSTNAME=ipa.example.test + command: + - --admin-password=Secret123 + - --ds-password=Secret123 + - -U + - --realm=EXAMPLE.TEST + ports: + # FreeIPA WebUI + - "80:80" + - "443:443" + # Kerberos + - "88:88/udp" + - "88:88" + - "464:464/udp" + - "464:464" + # LDAP + - "389:389" + - "636:636" + # DNS + # - "53:53/udp" + # - "53:53" + # NTP + - "123:123/udp" + # other + - "7389:7389" + - "9443:9443" + - "9444:9444" + - "9445:9445" + tmpfs: + - /run + - /tmp + volumes: + - freeipa_data:/data:Z + - /sys/fs/cgroup:/sys/fs/cgroup:ro diff --git a/devenv/docker/blocks/freeipa/ldap_freeipa.toml b/devenv/docker/blocks/freeipa/ldap_freeipa.toml new file mode 100644 index 00000000000..358b7cdebf9 --- /dev/null +++ b/devenv/docker/blocks/freeipa/ldap_freeipa.toml @@ -0,0 +1,74 @@ +# To troubleshoot and get more log info enable ldap debug logging in grafana.ini +# [log] +# filters = ldap:debug + +[[servers]] +# Ldap server host (specify multiple hosts space separated) +host = "172.17.0.1" +# Default port is 389 or 636 if use_ssl = true +port = 389 +# Set to true if ldap server supports TLS +use_ssl = false +# Set to true if connect ldap server with STARTTLS pattern (create connection in insecure, then upgrade to secure connection with TLS) +start_tls = false +# set to true if you want to skip ssl cert validation +ssl_skip_verify = false +# set to the path to your root CA certificate or leave unset to use system defaults +# root_ca_cert = "/path/to/certificate.crt" + +# Search user bind dn +bind_dn = "uid=admin,cn=users,cn=accounts,dc=example,dc=test" +# Search user bind password +# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" +bind_password = 'Secret123' + +# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)" +search_filter = "(uid=%s)" + +# An array of base dns to search through +search_base_dns = ["cn=users,cn=accounts,dc=example,dc=test"] + +# In POSIX LDAP schemas, without memberOf attribute a secondary query must be made for groups. +# This is done by enabling group_search_filter below. You must also set member_of= "cn" +# in [servers.attributes] below. + +# Users with nested/recursive group membership and an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN +# can set group_search_filter, group_search_filter_user_attribute, group_search_base_dns and member_of +# below in such a way that the user's recursive group membership is considered. +# +# Nested Groups + Active Directory (AD) Example: +# +# AD groups store the Distinguished Names (DNs) of members, so your filter must +# recursively search your groups for the authenticating user's DN. For example: +# +# group_search_filter = "(member:1.2.840.113556.1.4.1941:=%s)" +# group_search_filter_user_attribute = "distinguishedName" +# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"] +# +# [servers.attributes] +# ... +# member_of = "distinguishedName" + +## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available) +# group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))" +## Group search filter user attribute defines what user attribute gets substituted for %s in group_search_filter. +## Defaults to the value of username in [server.attributes] +## Valid options are any of your values in [servers.attributes] +## If you are using nested groups you probably want to set this and member_of in +## [servers.attributes] to "distinguishedName" +# group_search_filter_user_attribute = "distinguishedName" +## An array of the base DNs to search through for groups. Typically uses ou=groups +# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"] + +# Specify names of the ldap attributes your ldap uses +[servers.attributes] +name = "givenName" +username = "uid" +member_of = "memberOf" +# surname = "sn" +# email = "mail" + +[[servers.group_mappings]] +# If you want to match all (or no ldap groups) then you can use wildcard +group_dn = "*" +org_role = "Viewer" diff --git a/devenv/docker/blocks/freeipa/notes.md b/devenv/docker/blocks/freeipa/notes.md new file mode 100644 index 00000000000..76afdf913c8 --- /dev/null +++ b/devenv/docker/blocks/freeipa/notes.md @@ -0,0 +1,32 @@ +# Notes on FreeIPA LDAP Docker Block + +Users have to be created manually. The docker-compose up command takes a few minutes to run. + +## Create a user + +`docker exec -it freeipa /bin/bash` + +To create a user with username: `ldap-viewer` and password: `grafana123` + +```bash +kinit admin +``` + +Log in with password `Secret123` + +```bash +ipa user-add ldap-viewer --first ldap --last viewer +ipa passwd ldap-viewer +ldappasswd -D uid=ldap-viewer,cn=users,cn=accounts,dc=example,dc=org -w test -a test -s grafana123 +``` + +## Enabling FreeIPA LDAP in Grafana + +Copy the ldap_freeipa.toml file in this folder into your `conf` folder (it is gitignored already). To enable it in the .ini file to get Grafana to use this block: + +```ini +[auth.ldap] +enabled = true +config_file = conf/ldap_freeipa.toml +; allow_sign_up = true +``` diff --git a/package.json b/package.json index 5ac751ced3f..fae51a1d856 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@types/react-dom": "^16.0.9", "@types/react-grid-layout": "^0.16.6", "@types/react-select": "^2.0.4", + "@types/react-transition-group": "^2.0.15", "@types/react-virtualized": "^9.18.12", "angular-mocks": "1.6.6", "autoprefixer": "^6.4.0", diff --git a/packages/grafana-build/package.json b/packages/grafana-build/package.json index 24fb648c8d4..056e5d2c7ea 100644 --- a/packages/grafana-build/package.json +++ b/packages/grafana-build/package.json @@ -8,6 +8,6 @@ "tslint": "echo \"Nothing to do\"", "typecheck": "echo \"Nothing to do\"" }, - "author": "", - "license": "ISC" + "author": "Grafana Labs", + "license": "Apache-2.0" } diff --git a/packages/grafana-ui/.storybook/config.ts b/packages/grafana-ui/.storybook/config.ts index 9e50c6b501a..434e717bbab 100644 --- a/packages/grafana-ui/.storybook/config.ts +++ b/packages/grafana-ui/.storybook/config.ts @@ -1,10 +1,15 @@ -import { configure } from '@storybook/react'; +import { configure, addDecorator } from '@storybook/react'; +import { withKnobs } from '@storybook/addon-knobs'; +import { withTheme } from '../src/utils/storybook/withTheme'; import '../../../public/sass/grafana.light.scss'; // automatically import all files ending in *.stories.tsx const req = require.context('../src/components', true, /.story.tsx$/); +addDecorator(withKnobs); +addDecorator(withTheme); + function loadStories() { req.keys().forEach(req); } diff --git a/packages/grafana-ui/.storybook/webpack.config.js b/packages/grafana-ui/.storybook/webpack.config.js index 44de73a1e18..307a1142a7d 100644 --- a/packages/grafana-ui/.storybook/webpack.config.js +++ b/packages/grafana-ui/.storybook/webpack.config.js @@ -1,7 +1,6 @@ const path = require('path'); module.exports = (baseConfig, env, config) => { - config.module.rules.push({ test: /\.(ts|tsx)$/, use: [ @@ -33,7 +32,12 @@ module.exports = (baseConfig, env, config) => { config: { path: __dirname + '../../../../scripts/webpack/postcss.config.js' }, }, }, - { loader: 'sass-loader', options: { sourceMap: false } }, + { + loader: 'sass-loader', + options: { + sourceMap: false + }, + }, ], }); @@ -52,5 +56,9 @@ module.exports = (baseConfig, env, config) => { }); config.resolve.extensions.push('.ts', '.tsx'); + + // Remove pure js loading rules as Storybook's Babel config is causing problems when mixing ES6 and CJS + // More about the problem we encounter: https://github.com/webpack/webpack/issues/4039 + config.module.rules = config.module.rules.filter(rule => rule.test.toString() !== /\.(mjs|jsx?)$/.toString()); return config; }; diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 0d1b14a7150..a0c76f711af 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -8,8 +8,8 @@ "typecheck": "tsc --noEmit", "storybook": "start-storybook -p 9001 -c .storybook -s ../../public" }, - "author": "", - "license": "ISC", + "author": "Grafana Labs", + "license": "Apache-2.0", "dependencies": { "@torkelo/react-select": "2.1.1", "@types/react-color": "^2.14.0", diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx index 19ae2fda978..1fb31e86d72 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx @@ -1,46 +1,43 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; -import { withKnobs, boolean } from '@storybook/addon-knobs'; +import { boolean } from '@storybook/addon-knobs'; import { SeriesColorPicker, ColorPicker } from './ColorPicker'; import { action } from '@storybook/addon-actions'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { UseState } from '../../utils/storybook/UseState'; -import { getThemeKnob } from '../../utils/storybook/themeKnob'; +import { renderComponentWithTheme } from '../../utils/storybook/withTheme'; const getColorPickerKnobs = () => { return { - selectedTheme: getThemeKnob(), enableNamedColors: boolean('Enable named colors', false), }; }; const ColorPickerStories = storiesOf('UI/ColorPicker/Pickers', module); -ColorPickerStories.addDecorator(withCenteredStory).addDecorator(withKnobs); +ColorPickerStories.addDecorator(withCenteredStory); ColorPickerStories.add('default', () => { - const { selectedTheme, enableNamedColors } = getColorPickerKnobs(); + const { enableNamedColors } = getColorPickerKnobs(); + return ( {(selectedColor, updateSelectedColor) => { - return ( - { - action('Color changed')(color); - updateSelectedColor(color); - }} - theme={selectedTheme || undefined} - /> - ); + return renderComponentWithTheme(ColorPicker, { + enableNamedColors, + color: selectedColor, + onChange: (color: any) => { + action('Color changed')(color); + updateSelectedColor(color); + }, + }); }} ); }); ColorPickerStories.add('Series color picker', () => { - const { selectedTheme, enableNamedColors } = getColorPickerKnobs(); + const { enableNamedColors } = getColorPickerKnobs(); return ( @@ -52,7 +49,6 @@ ColorPickerStories.add('Series color picker', () => { onToggleAxis={() => {}} color={selectedColor} onChange={color => updateSelectedColor(color)} - theme={selectedTheme || undefined} >
Open color picker
diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx index b6cf176a24b..a48ecc44c45 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx @@ -1,12 +1,12 @@ import React, { Component, createRef } from 'react'; import PopperController from '../Tooltip/PopperController'; -import Popper, { RenderPopperArrowFn } from '../Tooltip/Popper'; +import Popper from '../Tooltip/Popper'; import { ColorPickerPopover } from './ColorPickerPopover'; -import { Themeable, GrafanaTheme } from '../../types'; +import { Themeable } from '../../types'; import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette'; import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; import propDeprecationWarning from '../../utils/propDeprecationWarning'; - +import { withTheme } from '../../themes/ThemeContext'; type ColorPickerChangeHandler = (color: string) => void; export interface ColorPickerProps extends Themeable { @@ -18,7 +18,6 @@ export interface ColorPickerProps extends Themeable { */ onColorChange?: ColorPickerChangeHandler; enableNamedColors?: boolean; - withArrow?: boolean; children?: JSX.Element; } @@ -32,7 +31,6 @@ export const warnAboutColorPickerPropsDeprecation = (componentName: string, prop export const colorPickerFactory = ( popover: React.ComponentType, displayName = 'ColorPicker', - renderPopoverArrowFunction?: RenderPopperArrowFn ) => { return class ColorPicker extends Component { static displayName = displayName; @@ -50,17 +48,7 @@ export const colorPickerFactory = ( ...this.props, onChange: this.handleColorChange, }); - const { theme, withArrow, children } = this.props; - - const renderArrow: RenderPopperArrowFn = ({ arrowProps, placement }) => { - return ( -
- ); - }; + const { theme, children } = this.props; return ( @@ -72,7 +60,6 @@ export const colorPickerFactory = ( {...popperProps} referenceElement={this.pickerTriggerRef.current} wrapperClassName="ColorPicker" - renderArrow={withArrow && (renderPopoverArrowFunction || renderArrow)} onMouseLeave={hidePopper} onMouseEnter={showPopper} /> @@ -95,7 +82,7 @@ export const colorPickerFactory = (
@@ -110,5 +97,5 @@ export const colorPickerFactory = ( }; }; -export const ColorPicker = colorPickerFactory(ColorPickerPopover, 'ColorPicker'); -export const SeriesColorPicker = colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker'); +export const ColorPicker = withTheme(colorPickerFactory(ColorPickerPopover, 'ColorPicker')); +export const SeriesColorPicker = withTheme(colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker')); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx index dc51819a413..d749588ee31 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx @@ -1,40 +1,27 @@ -import React from 'react'; import { storiesOf } from '@storybook/react'; import { ColorPickerPopover } from './ColorPickerPopover'; -import { withKnobs } from '@storybook/addon-knobs'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; -import { getThemeKnob } from '../../utils/storybook/themeKnob'; import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; - +import { renderComponentWithTheme } from '../../utils/storybook/withTheme'; const ColorPickerPopoverStories = storiesOf('UI/ColorPicker/Popovers', module); -ColorPickerPopoverStories.addDecorator(withCenteredStory).addDecorator(withKnobs); +ColorPickerPopoverStories.addDecorator(withCenteredStory); ColorPickerPopoverStories.add('default', () => { - const selectedTheme = getThemeKnob(); - - return ( - { - console.log(color); - }} - theme={selectedTheme || undefined} - /> - ); + return renderComponentWithTheme(ColorPickerPopover, { + color: '#BC67E6', + onChange: (color: any) => { + console.log(color); + }, + }); }); ColorPickerPopoverStories.add('SeriesColorPickerPopover', () => { - const selectedTheme = getThemeKnob(); - - return ( - { - console.log(color); - }} - theme={selectedTheme || undefined} - /> - ); + return renderComponentWithTheme(SeriesColorPickerPopover, { + color: '#BC67E6', + onChange: (color: any) => { + console.log(color); + }, + }); }); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx index 28d66e7af86..444f0e658c8 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx @@ -4,7 +4,8 @@ import { ColorPickerPopover } from './ColorPickerPopover'; import { getColorDefinitionByName, getNamedColorPalette } from '../../utils/namedColorsPalette'; import { ColorSwatch } from './NamedColorsGroup'; import { flatten } from 'lodash'; -import { GrafanaTheme } from '../../types'; +import { GrafanaThemeType } from '../../types'; +import { getTheme } from '../../themes'; const allColors = flatten(Array.from(getNamedColorPalette().values())); @@ -14,7 +15,7 @@ describe('ColorPickerPopover', () => { describe('rendering', () => { it('should render provided color as selected if color provided by name', () => { - const wrapper = mount( {}} />); + const wrapper = mount( {}} theme={getTheme()}/>); const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name); const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false); @@ -24,7 +25,7 @@ describe('ColorPickerPopover', () => { }); it('should render provided color as selected if color provided by hex', () => { - const wrapper = mount( {}} />); + const wrapper = mount( {}} theme={getTheme()} />); const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name); const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false); @@ -45,7 +46,7 @@ describe('ColorPickerPopover', () => { it('should pass hex color value to onChange prop by default', () => { wrapper = mount( - + ); const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name); @@ -61,7 +62,7 @@ describe('ColorPickerPopover', () => { enableNamedColors color={BasicGreen.variants.dark} onChange={onChangeSpy} - theme={GrafanaTheme.Light} + theme={getTheme(GrafanaThemeType.Light)} /> ); const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx index d2937a1caba..b4c77a5e373 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx @@ -2,9 +2,9 @@ import React from 'react'; import { NamedColorsPalette } from './NamedColorsPalette'; import { getColorName, getColorFromHexRgbOrName } from '../../utils/namedColorsPalette'; import { ColorPickerProps, warnAboutColorPickerPropsDeprecation } from './ColorPicker'; -import { GrafanaTheme } from '../../types'; import { PopperContentProps } from '../Tooltip/PopperController'; import SpectrumPalette from './SpectrumPalette'; +import { GrafanaThemeType } from '@grafana/ui'; export interface Props extends ColorPickerProps, PopperContentProps { customPickers?: T; @@ -43,7 +43,7 @@ export class ColorPickerPopover extends React if (enableNamedColors) { return changeHandler(color); } - changeHandler(getColorFromHexRgbOrName(color, theme)); + changeHandler(getColorFromHexRgbOrName(color, theme.type)); }; handleTabChange = (tab: PickerType | keyof T) => { @@ -58,7 +58,9 @@ export class ColorPickerPopover extends React case 'spectrum': return ; case 'palette': - return ; + return ( + + ); default: return this.renderCustomPicker(activePicker); } @@ -88,11 +90,7 @@ export class ColorPickerPopover extends React <> {Object.keys(customPickers).map(key => { return ( -
+
{customPickers[key].name}
); @@ -103,21 +101,14 @@ export class ColorPickerPopover extends React render() { const { theme } = this.props; - const colorPickerTheme = theme || GrafanaTheme.Dark; - + const colorPickerTheme = theme.type || GrafanaThemeType.Dark; return (
-
+
Colors
-
+
Custom
{this.renderCustomPickerTabs()} @@ -128,3 +119,4 @@ export class ColorPickerPopover extends React ); } } + diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx index 91c4f21642a..2b5f7bb9b57 100644 --- a/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx @@ -1,8 +1,9 @@ import React, { FunctionComponent } from 'react'; -import { Themeable, GrafanaTheme } from '../../types'; +import { Themeable } from '../../types'; import { ColorDefinition, getColorForTheme } from '../../utils/namedColorsPalette'; import { Color } from 'csstype'; import { find, upperFirst } from 'lodash'; +import { selectThemeVariant } from '../../themes/selectThemeVariant'; type ColorChangeHandler = (color: ColorDefinition) => void; @@ -28,7 +29,15 @@ export const ColorSwatch: FunctionComponent = ({ }) => { const isSmall = variant === ColorSwatchVariant.Small; const swatchSize = isSmall ? '16px' : '32px'; - const selectedSwatchBorder = theme === GrafanaTheme.Light ? '#ffffff' : '#1A1B1F'; + + const selectedSwatchBorder = selectThemeVariant( + { + light: theme.colors.white, + dark: theme.colors.black, + }, + theme.type + ); + const swatchStyles = { width: swatchSize, height: swatchSize, @@ -76,7 +85,7 @@ const NamedColorsGroup: FunctionComponent = ({ key={primaryColor.name} isSelected={primaryColor.name === selectedColor} variant={ColorSwatchVariant.Large} - color={getColorForTheme(primaryColor, theme)} + color={getColorForTheme(primaryColor, theme.type)} label={upperFirst(primaryColor.hue)} onClick={() => onColorSelect(primaryColor)} theme={theme} @@ -95,7 +104,7 @@ const NamedColorsGroup: FunctionComponent = ({ onColorSelect(color)} theme={theme} /> diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx index af5de3b2a2d..f4901b28bfd 100644 --- a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx @@ -2,8 +2,9 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { NamedColorsPalette } from './NamedColorsPalette'; import { getColorName, getColorDefinitionByName } from '../../utils/namedColorsPalette'; -import { withKnobs, select } from '@storybook/addon-knobs'; +import { select } from '@storybook/addon-knobs'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import { renderComponentWithTheme } from '../../utils/storybook/withTheme'; import { UseState } from '../../utils/storybook/UseState'; const BasicGreen = getColorDefinitionByName('green'); @@ -12,7 +13,7 @@ const LightBlue = getColorDefinitionByName('light-blue'); const NamedColorsPaletteStories = storiesOf('UI/ColorPicker/Palettes/NamedColorsPalette', module); -NamedColorsPaletteStories.addDecorator(withKnobs).addDecorator(withCenteredStory); +NamedColorsPaletteStories.addDecorator(withCenteredStory); NamedColorsPaletteStories.add('Named colors swatch - support for named colors', () => { const selectedColor = select( @@ -28,7 +29,10 @@ NamedColorsPaletteStories.add('Named colors swatch - support for named colors', return ( {(selectedColor, updateSelectedColor) => { - return ; + return renderComponentWithTheme(NamedColorsPalette, { + color: selectedColor, + onChange: updateSelectedColor, + }); }} ); @@ -45,7 +49,10 @@ NamedColorsPaletteStories.add('Named colors swatch - support for named colors', return ( {(selectedColor, updateSelectedColor) => { - return ; + return renderComponentWithTheme(NamedColorsPalette, { + color: getColorName(selectedColor), + onChange: updateSelectedColor, + }); }} ); diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx index 171d26f5c56..7a1ba95e81d 100644 --- a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx @@ -3,7 +3,8 @@ import { mount, ReactWrapper } from 'enzyme'; import { NamedColorsPalette } from './NamedColorsPalette'; import { ColorSwatch } from './NamedColorsGroup'; import { getColorDefinitionByName } from '../../utils'; -import { GrafanaTheme } from '../../types'; +import { getTheme } from '../../themes'; +import { GrafanaThemeType } from '../../types'; describe('NamedColorsPalette', () => { @@ -17,18 +18,18 @@ describe('NamedColorsPalette', () => { }); it('should render provided color variant specific for theme', () => { - wrapper = mount( {}} />); + wrapper = mount( {}} />); selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name); expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark); wrapper.unmount(); - wrapper = mount( {}} />); + wrapper = mount( {}} />); selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name); expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.light); }); it('should render dar variant of provided color when theme not provided', () => { - wrapper = mount( {}} />); + wrapper = mount( {}} theme={getTheme()}/>); selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name); expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark); }); diff --git a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx index 3fa7a1f4a45..4cb8c15c002 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx @@ -4,6 +4,7 @@ import { ColorPickerPopover } from './ColorPickerPopover'; import { ColorPickerProps } from './ColorPicker'; import { PopperContentProps } from '../Tooltip/PopperController'; import { Switch } from '../Switch/Switch'; +import { withTheme } from '../../themes/ThemeContext'; export interface SeriesColorPickerPopoverProps extends ColorPickerProps, PopperContentProps { yaxis?: number; @@ -12,7 +13,6 @@ export interface SeriesColorPickerPopoverProps extends ColorPickerProps, PopperC export const SeriesColorPickerPopover: FunctionComponent = props => { const { yaxis, onToggleAxis, color, ...colorPickerProps } = props; - return ( { - const selectedTheme = getThemeKnob(); - return ( {(selectedColor, updateSelectedColor) => { - return ; + return renderComponentWithTheme(SpectrumPalette, { color: selectedColor, onChange: updateSelectedColor }); }} ); diff --git a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx index cf001cf5629..0661821f3da 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx @@ -13,7 +13,7 @@ export interface SpectrumPaletteProps extends Themeable { onChange: (color: string) => void; } -const renderPointer = (theme?: GrafanaTheme) => (props: SpectrumPalettePointerProps) => ( +const renderPointer = (theme: GrafanaTheme) => (props: SpectrumPalettePointerProps) => ( ); @@ -92,7 +92,7 @@ const SpectrumPalette: React.FunctionComponent = ({ color, }} theme={theme} /> - +
); }; diff --git a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalettePointer.tsx b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalettePointer.tsx index d0b2cbc4bff..7e3b2cf06a3 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalettePointer.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalettePointer.tsx @@ -1,14 +1,12 @@ import React from 'react'; -import { GrafanaTheme, Themeable } from '../../types'; +import { Themeable } from '../../types'; +import { selectThemeVariant } from '../../themes/selectThemeVariant'; export interface SpectrumPalettePointerProps extends Themeable { direction?: string; } -const SpectrumPalettePointer: React.FunctionComponent = ({ - theme, - direction, -}) => { +const SpectrumPalettePointer: React.FunctionComponent = ({ theme, direction }) => { const styles = { picker: { width: '16px', @@ -17,7 +15,14 @@ const SpectrumPalettePointer: React.FunctionComponent ({ plot: jest.fn(), @@ -24,6 +25,7 @@ const setup = (propOverrides?: object) => { width: 300, value: 25, decimals: 0, + theme: getTheme() }; Object.assign(props, propOverrides); diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index 04d89bf3f57..a7435a56b3c 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -1,13 +1,14 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { ValueMapping, Threshold, BasicGaugeColor, GrafanaTheme } from '../../types'; +import { ValueMapping, Threshold, BasicGaugeColor, GrafanaThemeType } from '../../types'; import { getMappedValue } from '../../utils/valueMappings'; import { getColorFromHexRgbOrName, getValueFormat } from '../../utils'; +import { Themeable } from '../../index'; type TimeSeriesValue = string | number | null; -export interface Props { +export interface Props extends Themeable { decimals: number; height: number; valueMappings: ValueMapping[]; @@ -22,7 +23,6 @@ export interface Props { unit: string; width: number; value: number; - theme?: GrafanaTheme; } const FONT_SCALE = 1; @@ -41,7 +41,7 @@ export class Gauge extends PureComponent { thresholds: [], unit: 'none', stat: 'avg', - theme: GrafanaTheme.Dark, + theme: GrafanaThemeType.Dark, }; componentDidMount() { @@ -77,19 +77,19 @@ export class Gauge extends PureComponent { const { thresholds, theme } = this.props; if (thresholds.length === 1) { - return getColorFromHexRgbOrName(thresholds[0].color, theme); + return getColorFromHexRgbOrName(thresholds[0].color, theme.type); } const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0]; if (atThreshold) { - return getColorFromHexRgbOrName(atThreshold.color, theme); + return getColorFromHexRgbOrName(atThreshold.color, theme.type); } const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value); if (belowThreshold.length > 0) { const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0]; - return getColorFromHexRgbOrName(nearestThreshold.color, theme); + return getColorFromHexRgbOrName(nearestThreshold.color, theme.type); } return BasicGaugeColor.Red; @@ -104,13 +104,13 @@ export class Gauge extends PureComponent { return [ ...thresholdsSortedByIndex.map(threshold => { if (threshold.index === 0) { - return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme) }; + return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme.type) }; } const previousThreshold = thresholdsSortedByIndex[threshold.index - 1]; - return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme) }; + return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme.type) }; }), - { value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme) }, + { value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme.type) }, ]; } @@ -126,7 +126,8 @@ export class Gauge extends PureComponent { const formattedValue = this.formatValue(value) as string; const dimension = Math.min(width, height * 1.3); - const backgroundColor = theme === GrafanaTheme.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)'; + const backgroundColor = theme.type === GrafanaThemeType.Light ? 'rgb(230,230,230)' : theme.colors.dark3; + const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1; const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio; const thresholdMarkersWidth = gaugeWidth / 5; diff --git a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss index b5b815cf57c..993bf086c95 100644 --- a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss +++ b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss @@ -30,13 +30,13 @@ &:hover { .panel-options-group__add-circle { background-color: $btn-success-bg; - color: $text-color-strong; + color: $white; } } } .panel-options-group__add-circle { - @include gradientBar($btn-success-bg, $btn-success-bg-hl, $text-color); + @include gradientBar($btn-success-bg, $btn-success-bg-hl); border-radius: 50px; width: 20px; diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index c15f66cca54..b2a2e07c58d 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -1,11 +1,11 @@ import React, { PureComponent } from 'react'; -import { Threshold, Themeable } from '../../types'; +import { Threshold } from '../../types'; import { ColorPicker } from '../ColorPicker/ColorPicker'; import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup'; import { colors } from '../../utils'; -import { getColorFromHexRgbOrName } from '@grafana/ui'; +import { getColorFromHexRgbOrName, ThemeContext } from '@grafana/ui'; -export interface Props extends Themeable { +export interface Props { thresholds: Threshold[]; onChange: (thresholds: Threshold[]) => void; } @@ -164,7 +164,10 @@ export class ThresholdsEditor extends PureComponent {
{threshold.color && (
- this.onChangeThresholdColor(threshold, color)} /> + this.onChangeThresholdColor(threshold, color)} + />
)}
@@ -188,27 +191,35 @@ export class ThresholdsEditor extends PureComponent { render() { const { thresholds } = this.state; - const { theme } = this.props; return ( - -
- {thresholds.map((threshold, index) => { - return ( -
-
this.onAddThreshold(threshold.index + 1)}> - -
-
-
{this.renderInput(threshold)}
+ + {theme => { + return ( + +
+ {thresholds.map((threshold, index) => { + return ( +
+
this.onAddThreshold(threshold.index + 1)} + > + +
+
+
{this.renderInput(threshold)}
+
+ ); + })}
- ); - })} -
-
+ + ); + }} +
); } } diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss index 200adfbfd75..490b452234f 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss +++ b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss @@ -21,7 +21,7 @@ } .thresholds-row-add-button { - @include buttonBackground($btn-success-bg, $btn-success-bg-hl, $text-color); + @include buttonBackground($btn-success-bg, $btn-success-bg-hl); align-self: center; margin-right: 5px; @@ -34,7 +34,7 @@ cursor: pointer; &:hover { - color: $text-color-strong; + color: $white; } } diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 5cd677761b0..dc435a8844d 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -14,8 +14,8 @@ export { FormLabel } from './FormLabel/FormLabel'; export { FormField } from './FormField/FormField'; export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder'; -export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker'; -export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover'; +export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker'; +export { SeriesColorPickerPopover, SeriesColorPickerPopoverWithTheme } from './ColorPicker/SeriesColorPickerPopover'; export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor'; export { Graph } from './Graph/Graph'; export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup'; diff --git a/packages/grafana-ui/src/index.ts b/packages/grafana-ui/src/index.ts index 974d976bbef..216f2f13bad 100644 --- a/packages/grafana-ui/src/index.ts +++ b/packages/grafana-ui/src/index.ts @@ -1,3 +1,5 @@ export * from './components'; export * from './types'; export * from './utils'; +export * from './themes'; +export * from './themes/ThemeContext'; diff --git a/packages/grafana-ui/src/themes/ThemeContext.tsx b/packages/grafana-ui/src/themes/ThemeContext.tsx new file mode 100644 index 00000000000..a61a71d8af6 --- /dev/null +++ b/packages/grafana-ui/src/themes/ThemeContext.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { GrafanaThemeType, Themeable } from '../types'; +import { getTheme } from './index'; + +type Omit = Pick>; +type Subtract = Omit; + +// Use Grafana Dark theme by default +export const ThemeContext = React.createContext(getTheme(GrafanaThemeType.Dark)); + +export const withTheme =

(Component: React.ComponentType

) => { + const WithTheme: React.FunctionComponent> = props => { + // @ts-ignore + return {theme => }; + }; + + WithTheme.displayName = `WithTheme(${Component.displayName})`; + + return WithTheme; +}; diff --git a/packages/grafana-ui/src/themes/dark.ts b/packages/grafana-ui/src/themes/dark.ts new file mode 100644 index 00000000000..deae022f63a --- /dev/null +++ b/packages/grafana-ui/src/themes/dark.ts @@ -0,0 +1,69 @@ +import tinycolor from 'tinycolor2'; +import defaultTheme from './default'; +import { GrafanaTheme, GrafanaThemeType } from '../types/theme'; + +const basicColors = { + black: '#000000', + white: '#ffffff', + dark1: '#141414', + dark2: '#1f1f20', + dark3: '#262628', + dark4: '#333333', + dark5: '#444444', + gray1: '#555555', + gray2: '#8e8e8e', + gray3: '#b3b3b3', + gray4: '#d8d9da', + gray5: '#ececec', + gray6: '#f4f5f8', + gray7: '#fbfbfb', + grayBlue: '#212327', + blue: '#33b5e5', + blueDark: '#005f81', + blueLight: '#00a8e6', // not used in dark theme + green: '#299c46', + red: '#d44a3a', + yellow: '#ecbb13', + pink: '#ff4444', + purple: '#9933cc', + variable: '#32d1df', + orange: '#eb7b18', +}; + +const darkTheme: GrafanaTheme = { + ...defaultTheme, + type: GrafanaThemeType.Dark, + name: 'Grafana Dark', + colors: { + ...basicColors, + inputBlack: '#09090b', + queryRed: '#e24d42', + queryGreen: '#74e680', + queryPurple: '#fe85fc', + queryKeyword: '#66d9ef', + queryOrange: 'eb7b18', + online: '#10a345', + warn: '#f79520', + critical: '#ed2e18', + bodyBg: '#171819', + pageBg: '#161719', + bodyColor: basicColors.gray4, + textColor: basicColors.gray4, + textColorStrong: basicColors.white, + textColorWeak: basicColors.gray2, + textColorEmphasis: basicColors.gray5, + textColorFaint: basicColors.dark5, + linkColor: new tinycolor(basicColors.white).darken(11).toString(), + linkColorDisabled: new tinycolor(basicColors.white).darken(11).toString(), + linkColorHover: basicColors.white, + linkColorExternal: basicColors.blue, + headingColor: new tinycolor(basicColors.white).darken(11).toString(), + }, + background: { + dropdown: basicColors.dark3, + scrollbar: '#aeb5df', + scrollbar2: '#3a3a3a', + }, +}; + +export default darkTheme; diff --git a/packages/grafana-ui/src/themes/default.ts b/packages/grafana-ui/src/themes/default.ts new file mode 100644 index 00000000000..bf318f526e7 --- /dev/null +++ b/packages/grafana-ui/src/themes/default.ts @@ -0,0 +1,62 @@ + + +const theme = { + name: 'Grafana Default', + typography: { + fontFamily: { + sansSerif: "'Roboto', Helvetica, Arial, sans-serif;", + serif: "Georgia, 'Times New Roman', Times, serif;", + monospace: "Menlo, Monaco, Consolas, 'Courier New', monospace;" + }, + size: { + base: '13px', + xs: '10px', + s: '12px', + m: '14px', + l: '18px', + }, + heading: { + h1: '2rem', + h2: '1.75rem', + h3: '1.5rem', + h4: '1.3rem', + h5: '1.2rem', + h6: '1rem', + }, + weight: { + light: 300, + normal: 400, + semibold: 500, + }, + lineHeight: { + xs: 1, + s: 1.1, + m: 4/3, + l: 1.5 + } + }, + brakpoints: { + xs: '0', + s: '544px', + m: '768px', + l: '992px', + xl: '1200px' + }, + spacing: { + xs: '0', + s: '0.2rem', + m: '1rem', + l: '1.5rem', + xl: '3rem', + gutter: '30px', + }, + border: { + radius: { + xs: '2px', + s: '3px', + m: '5px', + } + } +}; + +export default theme; diff --git a/packages/grafana-ui/src/themes/index.ts b/packages/grafana-ui/src/themes/index.ts new file mode 100644 index 00000000000..c0d9a4f2d32 --- /dev/null +++ b/packages/grafana-ui/src/themes/index.ts @@ -0,0 +1,14 @@ +import darkTheme from './dark'; +import lightTheme from './light'; +import { GrafanaTheme } from '../types/theme'; + +let themeMock: ((name?: string) => GrafanaTheme) | null; + +export let getTheme = (name?: string) => (themeMock && themeMock(name)) || (name === 'light' ? lightTheme : darkTheme); + +export const mockTheme = (mock: (name: string) => GrafanaTheme) => { + themeMock = mock; + return () => { + themeMock = null; + }; +}; diff --git a/packages/grafana-ui/src/themes/light.ts b/packages/grafana-ui/src/themes/light.ts new file mode 100644 index 00000000000..fd1f1d05b95 --- /dev/null +++ b/packages/grafana-ui/src/themes/light.ts @@ -0,0 +1,70 @@ +import tinycolor from 'tinycolor2'; +import defaultTheme from './default'; +import { GrafanaTheme, GrafanaThemeType } from '../types/theme'; + +const basicColors = { + black: '#000000', + white: '#ffffff', + dark1: '#13161d', + dark2: '#1e2028', + dark3: '#303133', + dark4: '#35373f', + dark5: '#41444b', + gray1: '#52545c', + gray2: '#767980', + gray3: '#acb6bf', + gray4: '#c7d0d9', + gray5: '#dde4ed', + gray6: '#e9edf2', + gray7: '#f7f8fa', + grayBlue: '#212327', // not used in light theme + blue: '#0083b3', + blueDark: '#005f81', + blueLight: '#00a8e6', + green: '#3aa655', + red: '#d44939', + yellow: '#ff851b', + pink: '#e671b8', + purple: '#9954bb', + variable: '#0083b3', + orange: '#ff7941', +}; + +const lightTheme: GrafanaTheme = { + ...defaultTheme, + type: GrafanaThemeType.Light, + name: 'Grafana Light', + colors: { + ...basicColors, + variable: basicColors.blue, + inputBlack: '#09090b', + queryRed: basicColors.red, + queryGreen: basicColors.green, + queryPurple: basicColors.purple, + queryKeyword: basicColors.blue, + queryOrange: basicColors.orange, + online: '#01a64f', + warn: '#f79520', + critical: '#ec2128', + bodyBg: basicColors.gray7, + pageBg: basicColors.gray7, + bodyColor: basicColors.gray1, + textColor: basicColors.gray1, + textColorStrong: basicColors.dark2, + textColorWeak: basicColors.gray2, + textColorEmphasis: basicColors.gray5, + textColorFaint: basicColors.dark4, + linkColor: basicColors.gray1, + linkColorDisabled: new tinycolor(basicColors.gray1).lighten(30).toString(), + linkColorHover: new tinycolor(basicColors.gray1).darken(20).toString(), + linkColorExternal: basicColors.blueLight, + headingColor: basicColors.gray1, + }, + background: { + dropdown: basicColors.white, + scrollbar: basicColors.gray5, + scrollbar2: basicColors.gray5, + }, +}; + +export default lightTheme; diff --git a/packages/grafana-ui/src/themes/selectThemeVariant.test.ts b/packages/grafana-ui/src/themes/selectThemeVariant.test.ts new file mode 100644 index 00000000000..66cb02a2372 --- /dev/null +++ b/packages/grafana-ui/src/themes/selectThemeVariant.test.ts @@ -0,0 +1,52 @@ +import { GrafanaThemeType } from '../types/theme'; +import { selectThemeVariant } from './selectThemeVariant'; +import { mockTheme } from './index'; + +const lightThemeMock = { + color: { + red: '#ff0000', + green: '#00ff00', + }, +}; + +const darkThemeMock = { + color: { + red: '#ff0000', + green: '#00ff00', + }, +}; + +describe('Theme variable variant selector', () => { + // @ts-ignore + const restoreTheme = mockTheme(name => (name === GrafanaThemeType.Light ? lightThemeMock : darkThemeMock)); + + afterAll(() => { + restoreTheme(); + }); + it('return correct variable value for given theme', () => { + const theme = lightThemeMock; + + const selectedValue = selectThemeVariant( + { + dark: theme.color.red, + light: theme.color.green, + }, + GrafanaThemeType.Light + ); + + expect(selectedValue).toBe(lightThemeMock.color.green); + }); + + it('return dark theme variant if no theme given', () => { + const theme = lightThemeMock; + + const selectedValue = selectThemeVariant( + { + dark: theme.color.red, + light: theme.color.green, + } + ); + + expect(selectedValue).toBe(lightThemeMock.color.red); + }); +}); diff --git a/packages/grafana-ui/src/themes/selectThemeVariant.ts b/packages/grafana-ui/src/themes/selectThemeVariant.ts new file mode 100644 index 00000000000..e7e8e780222 --- /dev/null +++ b/packages/grafana-ui/src/themes/selectThemeVariant.ts @@ -0,0 +1,9 @@ +import { GrafanaThemeType } from '../types/theme'; + +type VariantDescriptor = { + [key in GrafanaThemeType]: string | number; +}; + +export const selectThemeVariant = (variants: VariantDescriptor, currentTheme?: GrafanaThemeType) => { + return variants[currentTheme || GrafanaThemeType.Dark]; +}; diff --git a/packages/grafana-ui/src/types/index.ts b/packages/grafana-ui/src/types/index.ts index e23b5e63af8..81bdf741f30 100644 --- a/packages/grafana-ui/src/types/index.ts +++ b/packages/grafana-ui/src/types/index.ts @@ -1,14 +1,7 @@ + export * from './data'; export * from './time'; export * from './panel'; export * from './plugin'; export * from './datasource'; - -export enum GrafanaTheme { - Light = 'light', - Dark = 'dark', -} - -export interface Themeable { - theme?: GrafanaTheme; -} +export * from './theme'; diff --git a/packages/grafana-ui/src/types/theme.ts b/packages/grafana-ui/src/types/theme.ts new file mode 100644 index 00000000000..8a79658b423 --- /dev/null +++ b/packages/grafana-ui/src/types/theme.ts @@ -0,0 +1,129 @@ +export enum GrafanaThemeType { + Light = 'light', + Dark = 'dark', +} + +export interface GrafanaTheme { + type: GrafanaThemeType; + name: string; + // TODO: not sure if should be a part of theme + brakpoints: { + xs: string; + s: string; + m: string; + l: string; + xl: string; + }; + typography: { + fontFamily: { + sansSerif: string; + serif: string; + monospace: string; + }; + size: { + base: string; + xs: string; + s: string; + m: string; + l: string; + }; + weight: { + light: number; + normal: number; + semibold: number; + }; + lineHeight: { + xs: number; //1 + s: number; //1.1 + m: number; // 4/3 + l: number; // 1.5 + }; + // TODO: Refactor to use size instead of custom defs + heading: { + h1: string; + h2: string; + h3: string; + h4: string; + h5: string; + h6: string; + }; + }; + spacing: { + xs: string; + s: string; + m: string; + l: string; + gutter: string; + }; + border: { + radius: { + xs: string; + s: string; + m: string; + }; + }; + background: { + dropdown: string; + scrollbar: string; + scrollbar2: string; + }; + colors: { + black: string; + white: string; + dark1: string; + dark2: string; + dark3: string; + dark4: string; + dark5: string; + gray1: string; + gray2: string; + gray3: string; + gray4: string; + gray5: string; + gray6: string; + gray7: string; + grayBlue: string; + inputBlack: string; + + // Accent colors + blue: string; + blueLight: string; + blueDark: string; + green: string; + red: string; + yellow: string; + pink: string; + purple: string; + variable: string; + orange: string; + queryRed: string; + queryGreen: string; + queryPurple: string; + queryKeyword: string; + queryOrange: string; + + // Status colors + online: string; + warn: string; + critical: string; + + // TODO: move to background section + bodyBg: string; + pageBg: string; + bodyColor: string; + textColor: string; + textColorStrong: string; + textColorWeak: string; + textColorFaint: string; + textColorEmphasis: string; + linkColor: string; + linkColorDisabled: string; + linkColorHover: string; + linkColorExternal: string; + headingColor: string; + }; +} + +export interface Themeable { + theme: GrafanaTheme; +} diff --git a/packages/grafana-ui/src/utils/namedColorsPalette.test.ts b/packages/grafana-ui/src/utils/namedColorsPalette.test.ts index c6a1aaf0dd0..aa57b46636c 100644 --- a/packages/grafana-ui/src/utils/namedColorsPalette.test.ts +++ b/packages/grafana-ui/src/utils/namedColorsPalette.test.ts @@ -5,20 +5,20 @@ import { getColorFromHexRgbOrName, getColorDefinitionByName, } from './namedColorsPalette'; -import { GrafanaTheme } from '../types/index'; +import { GrafanaThemeType } from '../types/index'; describe('colors', () => { const SemiDarkBlue = getColorDefinitionByName('semi-dark-blue'); describe('getColorDefinition', () => { it('returns undefined for unknown hex', () => { - expect(getColorDefinition('#ff0000', GrafanaTheme.Light)).toBeUndefined(); - expect(getColorDefinition('#ff0000', GrafanaTheme.Dark)).toBeUndefined(); + expect(getColorDefinition('#ff0000', GrafanaThemeType.Light)).toBeUndefined(); + expect(getColorDefinition('#ff0000', GrafanaThemeType.Dark)).toBeUndefined(); }); it('returns definition for known hex', () => { - expect(getColorDefinition(SemiDarkBlue.variants.light, GrafanaTheme.Light)).toEqual(SemiDarkBlue); - expect(getColorDefinition(SemiDarkBlue.variants.dark, GrafanaTheme.Dark)).toEqual(SemiDarkBlue); + expect(getColorDefinition(SemiDarkBlue.variants.light, GrafanaThemeType.Light)).toEqual(SemiDarkBlue); + expect(getColorDefinition(SemiDarkBlue.variants.dark, GrafanaThemeType.Dark)).toEqual(SemiDarkBlue); }); }); @@ -28,8 +28,8 @@ describe('colors', () => { }); it('returns name for known hex', () => { - expect(getColorName(SemiDarkBlue.variants.light, GrafanaTheme.Light)).toEqual(SemiDarkBlue.name); - expect(getColorName(SemiDarkBlue.variants.dark, GrafanaTheme.Dark)).toEqual(SemiDarkBlue.name); + expect(getColorName(SemiDarkBlue.variants.light, GrafanaThemeType.Light)).toEqual(SemiDarkBlue.name); + expect(getColorName(SemiDarkBlue.variants.dark, GrafanaThemeType.Dark)).toEqual(SemiDarkBlue.name); }); }); @@ -53,12 +53,14 @@ describe('colors', () => { }); it("returns correct variant's hex for known color if theme specified", () => { - expect(getColorFromHexRgbOrName(SemiDarkBlue.name, GrafanaTheme.Light)).toBe(SemiDarkBlue.variants.light); + expect(getColorFromHexRgbOrName(SemiDarkBlue.name, GrafanaThemeType.Light)).toBe(SemiDarkBlue.variants.light); }); it('returns color if specified as hex or rgb/a', () => { expect(getColorFromHexRgbOrName('ff0000')).toBe('ff0000'); expect(getColorFromHexRgbOrName('#ff0000')).toBe('#ff0000'); + expect(getColorFromHexRgbOrName('#FF0000')).toBe('#FF0000'); + expect(getColorFromHexRgbOrName('#CCC')).toBe('#CCC'); expect(getColorFromHexRgbOrName('rgb(0,0,0)')).toBe('rgb(0,0,0)'); expect(getColorFromHexRgbOrName('rgba(0,0,0,1)')).toBe('rgba(0,0,0,1)'); }); diff --git a/packages/grafana-ui/src/utils/namedColorsPalette.ts b/packages/grafana-ui/src/utils/namedColorsPalette.ts index 5312b27ad26..ee5741e794e 100644 --- a/packages/grafana-ui/src/utils/namedColorsPalette.ts +++ b/packages/grafana-ui/src/utils/namedColorsPalette.ts @@ -1,5 +1,5 @@ import { flatten } from 'lodash'; -import { GrafanaTheme } from '../types'; +import { GrafanaThemeType } from '../types'; type Hue = 'green' | 'yellow' | 'red' | 'blue' | 'orange' | 'purple'; @@ -68,16 +68,16 @@ export const getColorDefinitionByName = (name: Color): ColorDefinition => { return flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.name === name)[0]; }; -export const getColorDefinition = (hex: string, theme: GrafanaTheme): ColorDefinition | undefined => { +export const getColorDefinition = (hex: string, theme: GrafanaThemeType): ColorDefinition | undefined => { return flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.variants[theme] === hex)[0]; }; const isHex = (color: string) => { - const hexRegex = /^((0x){0,1}|#{0,1})([0-9A-F]{8}|[0-9A-F]{6})$/gi; + const hexRegex = /^((0x){0,1}|#{0,1})([0-9A-F]{8}|[0-9A-F]{6}|[0-9A-F]{3})$/gi; return hexRegex.test(color); }; -export const getColorName = (color?: string, theme?: GrafanaTheme): Color | undefined => { +export const getColorName = (color?: string, theme?: GrafanaThemeType): Color | undefined => { if (!color) { return undefined; } @@ -86,7 +86,7 @@ export const getColorName = (color?: string, theme?: GrafanaTheme): Color | unde return undefined; } if (isHex(color)) { - const definition = getColorDefinition(color, theme || GrafanaTheme.Dark); + const definition = getColorDefinition(color, theme || GrafanaThemeType.Dark); return definition ? definition.name : undefined; } @@ -98,7 +98,7 @@ export const getColorByName = (colorName: string) => { return definition.length > 0 ? definition[0] : undefined; }; -export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaTheme): string => { +export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaThemeType): string => { if (color.indexOf('rgb') > -1 || isHex(color)) { return color; } @@ -112,14 +112,14 @@ export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaTheme): s return theme ? colorDefinition.variants[theme] : colorDefinition.variants.dark; }; -export const getColorForTheme = (color: ColorDefinition, theme?: GrafanaTheme) => { +export const getColorForTheme = (color: ColorDefinition, theme?: GrafanaThemeType) => { return theme ? color.variants[theme] : color.variants.dark; }; const buildNamedColorsPalette = () => { const palette = new Map(); - const BasicGreen = buildColorDefinition('green', 'green', ['#56A64B', '#73BF69'], true); + const BasicGreen = buildColorDefinition('green', 'green', ['#56A64B', '#73BF69'], true); const DarkGreen = buildColorDefinition('green', 'dark-green', ['#19730E', '#37872D']); const SemiDarkGreen = buildColorDefinition('green', 'semi-dark-green', ['#37872D', '#56A64B']); const LightGreen = buildColorDefinition('green', 'light-green', ['#73BF69', '#96D98D']); diff --git a/packages/grafana-ui/src/utils/storybook/themeKnob.ts b/packages/grafana-ui/src/utils/storybook/themeKnob.ts deleted file mode 100644 index a3733462bea..00000000000 --- a/packages/grafana-ui/src/utils/storybook/themeKnob.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { select } from '@storybook/addon-knobs'; -import { GrafanaTheme } from '../../types'; - -export const getThemeKnob = (defaultTheme: GrafanaTheme = GrafanaTheme.Dark) => { - return select( - 'Theme', - { - Default: defaultTheme, - Light: GrafanaTheme.Light, - Dark: GrafanaTheme.Dark, - }, - defaultTheme - ); -}; diff --git a/packages/grafana-ui/src/utils/storybook/withTheme.tsx b/packages/grafana-ui/src/utils/storybook/withTheme.tsx new file mode 100644 index 00000000000..5417af1de05 --- /dev/null +++ b/packages/grafana-ui/src/utils/storybook/withTheme.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { RenderFunction } from '@storybook/react'; +import { ThemeContext } from '../../themes/ThemeContext'; +import { select } from '@storybook/addon-knobs'; +import { getTheme } from '../../themes'; +import { GrafanaThemeType } from '../../types'; + +const ThemableStory: React.FunctionComponent<{}> = ({ children }) => { + const themeKnob = select( + 'Theme', + { + Light: GrafanaThemeType.Light, + Dark: GrafanaThemeType.Dark, + }, + GrafanaThemeType.Dark + ); + + return ( + + {children} + + + ); +}; + +// Temporary solution. When we update to Storybook V5 we will be able to pass data from decorator to story +// https://github.com/storybooks/storybook/issues/340#issuecomment-456013702 +export const renderComponentWithTheme = (component: React.ComponentType, props: any) => { + return ( + + {theme => { + return React.createElement(component, { + ...props, + theme, + }); + }} + + ); +}; + +export const withTheme = (story: RenderFunction) => {story()}; diff --git a/pkg/infra/usagestats/service.go b/pkg/infra/usagestats/service.go new file mode 100644 index 00000000000..c2bf0d06349 --- /dev/null +++ b/pkg/infra/usagestats/service.go @@ -0,0 +1,54 @@ +package usagestats + +import ( + "context" + "time" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/social" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/setting" +) + +var metricsLogger log.Logger = log.New("metrics") + +func init() { + registry.RegisterService(&UsageStatsService{}) +} + +type UsageStatsService struct { + Cfg *setting.Cfg `inject:""` + Bus bus.Bus `inject:""` + SQLStore *sqlstore.SqlStore `inject:""` + + oauthProviders map[string]bool +} + +func (uss *UsageStatsService) Init() error { + + uss.oauthProviders = social.GetOAuthProviders(uss.Cfg) + return nil +} + +func (uss *UsageStatsService) Run(ctx context.Context) error { + uss.updateTotalStats() + + onceEveryDayTick := time.NewTicker(time.Hour * 24) + everyMinuteTicker := time.NewTicker(time.Minute) + defer onceEveryDayTick.Stop() + defer everyMinuteTicker.Stop() + + for { + select { + case <-onceEveryDayTick.C: + uss.sendUsageStats(uss.oauthProviders) + case <-everyMinuteTicker.C: + uss.updateTotalStats() + case <-ctx.Done(): + return ctx.Err() + } + } +} diff --git a/pkg/infra/usagestats/usage_stats.go b/pkg/infra/usagestats/usage_stats.go new file mode 100644 index 00000000000..b54de124335 --- /dev/null +++ b/pkg/infra/usagestats/usage_stats.go @@ -0,0 +1,177 @@ +package usagestats + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "runtime" + "strings" + "time" + + "github.com/grafana/grafana/pkg/metrics" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/setting" +) + +var usageStatsURL = "https://stats.grafana.org/grafana-usage-report" + +func (uss *UsageStatsService) sendUsageStats(oauthProviders map[string]bool) { + if !setting.ReportingEnabled { + return + } + + metricsLogger.Debug(fmt.Sprintf("Sending anonymous usage stats to %s", usageStatsURL)) + + version := strings.Replace(setting.BuildVersion, ".", "_", -1) + + metrics := map[string]interface{}{} + report := map[string]interface{}{ + "version": version, + "metrics": metrics, + "os": runtime.GOOS, + "arch": runtime.GOARCH, + "edition": getEdition(), + "packaging": setting.Packaging, + } + + statsQuery := models.GetSystemStatsQuery{} + if err := uss.Bus.Dispatch(&statsQuery); err != nil { + metricsLogger.Error("Failed to get system stats", "error", err) + return + } + + metrics["stats.dashboards.count"] = statsQuery.Result.Dashboards + metrics["stats.users.count"] = statsQuery.Result.Users + metrics["stats.orgs.count"] = statsQuery.Result.Orgs + metrics["stats.playlist.count"] = statsQuery.Result.Playlists + metrics["stats.plugins.apps.count"] = len(plugins.Apps) + metrics["stats.plugins.panels.count"] = len(plugins.Panels) + metrics["stats.plugins.datasources.count"] = len(plugins.DataSources) + metrics["stats.alerts.count"] = statsQuery.Result.Alerts + metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers + metrics["stats.datasources.count"] = statsQuery.Result.Datasources + metrics["stats.stars.count"] = statsQuery.Result.Stars + metrics["stats.folders.count"] = statsQuery.Result.Folders + metrics["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions + metrics["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions + metrics["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards + metrics["stats.snapshots.count"] = statsQuery.Result.Snapshots + metrics["stats.teams.count"] = statsQuery.Result.Teams + metrics["stats.total_sessions.count"] = statsQuery.Result.Sessions + + userCount := statsQuery.Result.Users + avgSessionsPerUser := statsQuery.Result.Sessions + if userCount != 0 { + avgSessionsPerUser = avgSessionsPerUser / userCount + } + + metrics["stats.avg_sessions_per_user.count"] = avgSessionsPerUser + + dsStats := models.GetDataSourceStatsQuery{} + if err := uss.Bus.Dispatch(&dsStats); err != nil { + metricsLogger.Error("Failed to get datasource stats", "error", err) + return + } + + // send counters for each data source + // but ignore any custom data sources + // as sending that name could be sensitive information + dsOtherCount := 0 + for _, dsStat := range dsStats.Result { + if models.IsKnownDataSourcePlugin(dsStat.Type) { + metrics["stats.ds."+dsStat.Type+".count"] = dsStat.Count + } else { + dsOtherCount += dsStat.Count + } + } + metrics["stats.ds.other.count"] = dsOtherCount + + metrics["stats.packaging."+setting.Packaging+".count"] = 1 + + dsAccessStats := models.GetDataSourceAccessStatsQuery{} + if err := uss.Bus.Dispatch(&dsAccessStats); err != nil { + metricsLogger.Error("Failed to get datasource access stats", "error", err) + return + } + + // send access counters for each data source + // but ignore any custom data sources + // as sending that name could be sensitive information + dsAccessOtherCount := make(map[string]int64) + for _, dsAccessStat := range dsAccessStats.Result { + if dsAccessStat.Access == "" { + continue + } + + access := strings.ToLower(dsAccessStat.Access) + + if models.IsKnownDataSourcePlugin(dsAccessStat.Type) { + metrics["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count + } else { + old := dsAccessOtherCount[access] + dsAccessOtherCount[access] = old + dsAccessStat.Count + } + } + + for access, count := range dsAccessOtherCount { + metrics["stats.ds_access.other."+access+".count"] = count + } + + anStats := models.GetAlertNotifierUsageStatsQuery{} + if err := uss.Bus.Dispatch(&anStats); err != nil { + metricsLogger.Error("Failed to get alert notification stats", "error", err) + return + } + + for _, stats := range anStats.Result { + metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count + } + + authTypes := map[string]bool{} + authTypes["anonymous"] = setting.AnonymousEnabled + authTypes["basic_auth"] = setting.BasicAuthEnabled + authTypes["ldap"] = setting.LdapEnabled + authTypes["auth_proxy"] = setting.AuthProxyEnabled + + for provider, enabled := range oauthProviders { + authTypes["oauth_"+provider] = enabled + } + + for authType, enabled := range authTypes { + enabledValue := 0 + if enabled { + enabledValue = 1 + } + metrics["stats.auth_enabled."+authType+".count"] = enabledValue + } + + out, _ := json.MarshalIndent(report, "", " ") + data := bytes.NewBuffer(out) + + client := http.Client{Timeout: 5 * time.Second} + go client.Post(usageStatsURL, "application/json", data) +} + +func (uss *UsageStatsService) updateTotalStats() { + statsQuery := models.GetSystemStatsQuery{} + if err := uss.Bus.Dispatch(&statsQuery); err != nil { + metricsLogger.Error("Failed to get system stats", "error", err) + return + } + + metrics.M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards)) + metrics.M_StatTotal_Users.Set(float64(statsQuery.Result.Users)) + metrics.M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers)) + metrics.M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists)) + metrics.M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs)) +} + +func getEdition() string { + if setting.IsEnterprise { + return "enterprise" + } else { + return "oss" + } +} diff --git a/pkg/metrics/metrics_test.go b/pkg/infra/usagestats/usage_stats_test.go similarity index 91% rename from pkg/metrics/metrics_test.go rename to pkg/infra/usagestats/usage_stats_test.go index c27d6f64b8c..d343ed52b93 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/infra/usagestats/usage_stats_test.go @@ -1,4 +1,4 @@ -package metrics +package usagestats import ( "bytes" @@ -15,14 +15,21 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" ) func TestMetrics(t *testing.T) { Convey("Test send usage stats", t, func() { + uss := &UsageStatsService{ + Bus: bus.New(), + SQLStore: sqlstore.InitTestDB(t), + } + var getSystemStatsQuery *models.GetSystemStatsQuery - bus.AddHandler("test", func(query *models.GetSystemStatsQuery) error { + uss.Bus.AddHandler(func(query *models.GetSystemStatsQuery) error { + query.Result = &models.SystemStats{ Dashboards: 1, Datasources: 2, @@ -38,13 +45,14 @@ func TestMetrics(t *testing.T) { ProvisionedDashboards: 12, Snapshots: 13, Teams: 14, + Sessions: 15, } getSystemStatsQuery = query return nil }) var getDataSourceStatsQuery *models.GetDataSourceStatsQuery - bus.AddHandler("test", func(query *models.GetDataSourceStatsQuery) error { + uss.Bus.AddHandler(func(query *models.GetDataSourceStatsQuery) error { query.Result = []*models.DataSourceStats{ { Type: models.DS_ES, @@ -68,7 +76,7 @@ func TestMetrics(t *testing.T) { }) var getDataSourceAccessStatsQuery *models.GetDataSourceAccessStatsQuery - bus.AddHandler("test", func(query *models.GetDataSourceAccessStatsQuery) error { + uss.Bus.AddHandler(func(query *models.GetDataSourceAccessStatsQuery) error { query.Result = []*models.DataSourceAccessStats{ { Type: models.DS_ES, @@ -116,7 +124,7 @@ func TestMetrics(t *testing.T) { }) var getAlertNotifierUsageStatsQuery *models.GetAlertNotifierUsageStatsQuery - bus.AddHandler("test", func(query *models.GetAlertNotifierUsageStatsQuery) error { + uss.Bus.AddHandler(func(query *models.GetAlertNotifierUsageStatsQuery) error { query.Result = []*models.NotifierUsageStats{ { Type: "slack", @@ -155,11 +163,11 @@ func TestMetrics(t *testing.T) { "grafana_com": true, } - sendUsageStats(oauthProviders) + uss.sendUsageStats(oauthProviders) Convey("Given reporting not enabled and sending usage stats", func() { setting.ReportingEnabled = false - sendUsageStats(oauthProviders) + uss.sendUsageStats(oauthProviders) Convey("Should not gather stats or call http endpoint", func() { So(getSystemStatsQuery, ShouldBeNil) @@ -179,7 +187,7 @@ func TestMetrics(t *testing.T) { setting.Packaging = "deb" wg.Add(1) - sendUsageStats(oauthProviders) + uss.sendUsageStats(oauthProviders) Convey("Should gather stats and call http endpoint", func() { if waitTimeout(&wg, 2*time.Second) { @@ -221,6 +229,8 @@ func TestMetrics(t *testing.T) { So(metrics.Get("stats.provisioned_dashboards.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.ProvisionedDashboards) So(metrics.Get("stats.snapshots.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.Snapshots) So(metrics.Get("stats.teams.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.Teams) + So(metrics.Get("stats.total_sessions.count").MustInt64(), ShouldEqual, 15) + So(metrics.Get("stats.avg_sessions_per_user.count").MustInt64(), ShouldEqual, 5) So(metrics.Get("stats.ds."+models.DS_ES+".count").MustInt(), ShouldEqual, 9) So(metrics.Get("stats.ds."+models.DS_PROMETHEUS+".count").MustInt(), ShouldEqual, 10) @@ -246,6 +256,7 @@ func TestMetrics(t *testing.T) { So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1) So(metrics.Get("stats.packaging.deb.count").MustInt(), ShouldEqual, 1) + }) }) diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index 402160ef5e3..c15cb865bd3 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -273,23 +273,35 @@ func (a *ldapAuther) initialBind(username, userPassword string) error { return nil } +func appendIfNotEmpty(slice []string, values ...string) []string { + for _, v := range values { + if v != "" { + slice = append(slice, v) + } + } + return slice +} + func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) { var searchResult *ldap.SearchResult var err error for _, searchBase := range a.server.SearchBaseDNs { + attributes := make([]string, 0) + inputs := a.server.Attr + attributes = appendIfNotEmpty(attributes, + inputs.Username, + inputs.Surname, + inputs.Email, + inputs.Name, + inputs.MemberOf) + searchReq := ldap.SearchRequest{ BaseDN: searchBase, Scope: ldap.ScopeWholeSubtree, DerefAliases: ldap.NeverDerefAliases, - Attributes: []string{ - a.server.Attr.Username, - a.server.Attr.Surname, - a.server.Attr.Email, - a.server.Attr.Name, - a.server.Attr.MemberOf, - }, - Filter: strings.Replace(a.server.SearchFilter, "%s", ldap.EscapeFilter(username), -1), + Attributes: attributes, + Filter: strings.Replace(a.server.SearchFilter, "%s", ldap.EscapeFilter(username), -1), } a.log.Debug("Ldap Search For User Request", "info", spew.Sdump(searchReq)) diff --git a/pkg/login/ldap_test.go b/pkg/login/ldap_test.go index f1cf44dc554..c02fa02e030 100644 --- a/pkg/login/ldap_test.go +++ b/pkg/login/ldap_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" "gopkg.in/ldap.v3" @@ -322,11 +323,51 @@ func TestLdapAuther(t *testing.T) { So(sc.addOrgUserCmd.Role, ShouldEqual, "Admin") }) }) + + Convey("When searching for a user and not all five attributes are mapped", t, func() { + mockLdapConnection := &mockLdapConn{} + entry := ldap.Entry{ + DN: "dn", Attributes: []*ldap.EntryAttribute{ + {Name: "username", Values: []string{"roelgerrits"}}, + {Name: "surname", Values: []string{"Gerrits"}}, + {Name: "email", Values: []string{"roel@test.com"}}, + {Name: "name", Values: []string{"Roel"}}, + {Name: "memberof", Values: []string{"admins"}}, + }} + result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}} + mockLdapConnection.setSearchResult(&result) + + // Set up attribute map without surname and email + ldapAuther := &ldapAuther{ + server: &LdapServerConf{ + Attr: LdapAttributeMap{ + Username: "username", + Name: "name", + MemberOf: "memberof", + }, + SearchBaseDNs: []string{"BaseDNHere"}, + }, + conn: mockLdapConnection, + log: log.New("test-logger"), + } + + searchResult, err := ldapAuther.searchForUser("roelgerrits") + + So(err, ShouldBeNil) + So(searchResult, ShouldNotBeNil) + + // User should be searched in ldap + So(mockLdapConnection.searchCalled, ShouldBeTrue) + + // No empty attributes should be added to the search request + So(len(mockLdapConnection.searchAttributes), ShouldEqual, 3) + }) } type mockLdapConn struct { - result *ldap.SearchResult - searchCalled bool + result *ldap.SearchResult + searchCalled bool + searchAttributes []string } func (c *mockLdapConn) Bind(username, password string) error { @@ -339,8 +380,9 @@ func (c *mockLdapConn) setSearchResult(result *ldap.SearchResult) { c.result = result } -func (c *mockLdapConn) Search(*ldap.SearchRequest) (*ldap.SearchResult, error) { +func (c *mockLdapConn) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) { c.searchCalled = true + c.searchAttributes = sr.Attributes return c.result, nil } diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 326514a9687..718a63ee768 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -1,17 +1,8 @@ package metrics import ( - "bytes" - "encoding/json" - "net/http" "runtime" - "strings" - "time" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" ) @@ -68,23 +59,6 @@ var ( grafanaBuildVersion *prometheus.GaugeVec ) -func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec { - counter := prometheus.NewCounterVec(opts, labels) - - for _, label := range labelValues { - counter.WithLabelValues(label).Add(0) - } - - return counter -} - -func newCounterStartingAtZero(opts prometheus.CounterOpts, labelValues ...string) prometheus.Counter { - counter := prometheus.NewCounter(opts) - counter.Add(0) - - return counter -} - func init() { M_Instance_Start = prometheus.NewCounter(prometheus.CounterOpts{ Name: "instance_start_total", @@ -362,154 +336,19 @@ func initMetricVars() { } -func updateTotalStats() { - statsQuery := models.GetSystemStatsQuery{} - if err := bus.Dispatch(&statsQuery); err != nil { - metricsLogger.Error("Failed to get system stats", "error", err) - return +func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec { + counter := prometheus.NewCounterVec(opts, labels) + + for _, label := range labelValues { + counter.WithLabelValues(label).Add(0) } - M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards)) - M_StatTotal_Users.Set(float64(statsQuery.Result.Users)) - M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers)) - M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists)) - M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs)) + return counter } -var usageStatsURL = "https://stats.grafana.org/grafana-usage-report" +func newCounterStartingAtZero(opts prometheus.CounterOpts, labelValues ...string) prometheus.Counter { + counter := prometheus.NewCounter(opts) + counter.Add(0) -func getEdition() string { - if setting.IsEnterprise { - return "enterprise" - } else { - return "oss" - } -} - -func sendUsageStats(oauthProviders map[string]bool) { - if !setting.ReportingEnabled { - return - } - - metricsLogger.Debug("Sending anonymous usage stats to stats.grafana.org") - - version := strings.Replace(setting.BuildVersion, ".", "_", -1) - - metrics := map[string]interface{}{} - report := map[string]interface{}{ - "version": version, - "metrics": metrics, - "os": runtime.GOOS, - "arch": runtime.GOARCH, - "edition": getEdition(), - "packaging": setting.Packaging, - } - - statsQuery := models.GetSystemStatsQuery{} - if err := bus.Dispatch(&statsQuery); err != nil { - metricsLogger.Error("Failed to get system stats", "error", err) - return - } - - metrics["stats.dashboards.count"] = statsQuery.Result.Dashboards - metrics["stats.users.count"] = statsQuery.Result.Users - metrics["stats.orgs.count"] = statsQuery.Result.Orgs - metrics["stats.playlist.count"] = statsQuery.Result.Playlists - metrics["stats.plugins.apps.count"] = len(plugins.Apps) - metrics["stats.plugins.panels.count"] = len(plugins.Panels) - metrics["stats.plugins.datasources.count"] = len(plugins.DataSources) - metrics["stats.alerts.count"] = statsQuery.Result.Alerts - metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers - metrics["stats.datasources.count"] = statsQuery.Result.Datasources - metrics["stats.stars.count"] = statsQuery.Result.Stars - metrics["stats.folders.count"] = statsQuery.Result.Folders - metrics["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions - metrics["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions - metrics["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards - metrics["stats.snapshots.count"] = statsQuery.Result.Snapshots - metrics["stats.teams.count"] = statsQuery.Result.Teams - - dsStats := models.GetDataSourceStatsQuery{} - if err := bus.Dispatch(&dsStats); err != nil { - metricsLogger.Error("Failed to get datasource stats", "error", err) - return - } - - // send counters for each data source - // but ignore any custom data sources - // as sending that name could be sensitive information - dsOtherCount := 0 - for _, dsStat := range dsStats.Result { - if models.IsKnownDataSourcePlugin(dsStat.Type) { - metrics["stats.ds."+dsStat.Type+".count"] = dsStat.Count - } else { - dsOtherCount += dsStat.Count - } - } - metrics["stats.ds.other.count"] = dsOtherCount - - metrics["stats.packaging."+setting.Packaging+".count"] = 1 - - dsAccessStats := models.GetDataSourceAccessStatsQuery{} - if err := bus.Dispatch(&dsAccessStats); err != nil { - metricsLogger.Error("Failed to get datasource access stats", "error", err) - return - } - - // send access counters for each data source - // but ignore any custom data sources - // as sending that name could be sensitive information - dsAccessOtherCount := make(map[string]int64) - for _, dsAccessStat := range dsAccessStats.Result { - if dsAccessStat.Access == "" { - continue - } - - access := strings.ToLower(dsAccessStat.Access) - - if models.IsKnownDataSourcePlugin(dsAccessStat.Type) { - metrics["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count - } else { - old := dsAccessOtherCount[access] - dsAccessOtherCount[access] = old + dsAccessStat.Count - } - } - - for access, count := range dsAccessOtherCount { - metrics["stats.ds_access.other."+access+".count"] = count - } - - anStats := models.GetAlertNotifierUsageStatsQuery{} - if err := bus.Dispatch(&anStats); err != nil { - metricsLogger.Error("Failed to get alert notification stats", "error", err) - return - } - - for _, stats := range anStats.Result { - metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count - } - - authTypes := map[string]bool{} - authTypes["anonymous"] = setting.AnonymousEnabled - authTypes["basic_auth"] = setting.BasicAuthEnabled - authTypes["ldap"] = setting.LdapEnabled - authTypes["auth_proxy"] = setting.AuthProxyEnabled - - for provider, enabled := range oauthProviders { - authTypes["oauth_"+provider] = enabled - } - - for authType, enabled := range authTypes { - enabledValue := 0 - if enabled { - enabledValue = 1 - } - metrics["stats.auth_enabled."+authType+".count"] = enabledValue - } - - out, _ := json.MarshalIndent(report, "", " ") - data := bytes.NewBuffer(out) - - client := http.Client{Timeout: 5 * time.Second} - go client.Post(usageStatsURL, "application/json", data) + return counter } diff --git a/pkg/metrics/service.go b/pkg/metrics/service.go index d2c0c815da9..44b83187cac 100644 --- a/pkg/metrics/service.go +++ b/pkg/metrics/service.go @@ -2,7 +2,6 @@ package metrics import ( "context" - "time" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics/graphitebridge" @@ -30,7 +29,6 @@ type InternalMetricsService struct { intervalSeconds int64 graphiteCfg *graphitebridge.Config - oauthProviders map[string]bool } func (im *InternalMetricsService) Init() error { @@ -50,22 +48,6 @@ func (im *InternalMetricsService) Run(ctx context.Context) error { M_Instance_Start.Inc() - // set the total stats gauges before we publishing metrics - updateTotalStats() - - onceEveryDayTick := time.NewTicker(time.Hour * 24) - everyMinuteTicker := time.NewTicker(time.Minute) - defer onceEveryDayTick.Stop() - defer everyMinuteTicker.Stop() - - for { - select { - case <-onceEveryDayTick.C: - sendUsageStats(im.oauthProviders) - case <-everyMinuteTicker.C: - updateTotalStats() - case <-ctx.Done(): - return ctx.Err() - } - } + <-ctx.Done() + return ctx.Err() } diff --git a/pkg/metrics/settings.go b/pkg/metrics/settings.go index 18b9e78d6ff..048e4134690 100644 --- a/pkg/metrics/settings.go +++ b/pkg/metrics/settings.go @@ -5,8 +5,6 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/social" - "github.com/grafana/grafana/pkg/metrics/graphitebridge" "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" @@ -24,8 +22,6 @@ func (im *InternalMetricsService) readSettings() error { return fmt.Errorf("Unable to parse metrics graphite section, %v", err) } - im.oauthProviders = social.GetOAuthProviders(im.Cfg) - return nil } diff --git a/pkg/models/datasource_cache.go b/pkg/models/datasource_cache.go index 66ba66e4d39..1c895514ace 100644 --- a/pkg/models/datasource_cache.go +++ b/pkg/models/datasource_cache.go @@ -46,19 +46,16 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { return t.Transport, nil } - var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool - if ds.JsonData != nil { - tlsClientAuth = ds.JsonData.Get("tlsAuth").MustBool(false) - tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false) - tlsSkipVerify = ds.JsonData.Get("tlsSkipVerify").MustBool(false) + tlsConfig, err := ds.GetTLSConfig() + if err != nil { + return nil, err } + tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient + transport := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: tlsSkipVerify, - Renegotiation: tls.RenegotiateFreelyAsClient, - }, - Proxy: http.ProxyFromEnvironment, + TLSClientConfig: tlsConfig, + Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, @@ -70,6 +67,26 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { IdleConnTimeout: 90 * time.Second, } + ptc.cache[ds.Id] = cachedTransport{ + Transport: transport, + updated: ds.Updated, + } + + return transport, nil +} + +func (ds *DataSource) GetTLSConfig() (*tls.Config, error) { + var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool + if ds.JsonData != nil { + tlsClientAuth = ds.JsonData.Get("tlsAuth").MustBool(false) + tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false) + tlsSkipVerify = ds.JsonData.Get("tlsSkipVerify").MustBool(false) + } + + tlsConfig := &tls.Config{ + InsecureSkipVerify: tlsSkipVerify, + } + if tlsClientAuth || tlsAuthWithCACert { decrypted := ds.SecureJsonData.Decrypt() if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 { @@ -78,7 +95,7 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { if !ok { return nil, errors.New("Failed to parse TLS CA PEM certificate") } - transport.TLSClientConfig.RootCAs = caPool + tlsConfig.RootCAs = caPool } if tlsClientAuth { @@ -86,14 +103,9 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { if err != nil { return nil, err } - transport.TLSClientConfig.Certificates = []tls.Certificate{cert} + tlsConfig.Certificates = []tls.Certificate{cert} } } - ptc.cache[ds.Id] = cachedTransport{ - Transport: transport, - updated: ds.Updated, - } - - return transport, nil + return tlsConfig, nil } diff --git a/pkg/models/stats.go b/pkg/models/stats.go index d3e145dedf4..00f881f3c59 100644 --- a/pkg/models/stats.go +++ b/pkg/models/stats.go @@ -15,6 +15,7 @@ type SystemStats struct { FolderPermissions int64 Folders int64 ProvisionedDashboards int64 + Sessions int64 } type DataSourceStats struct { diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go index 2cec86e7239..4c6d6c21221 100644 --- a/pkg/services/sqlstore/stats.go +++ b/pkg/services/sqlstore/stats.go @@ -74,7 +74,8 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error { sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_provisioning") + `) AS provisioned_dashboards,`) sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_snapshot") + `) AS snapshots,`) - sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("team") + `) AS teams`) + sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("team") + `) AS teams,`) + sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("user_auth_token") + `) AS sessions`) var stats m.SystemStats _, err := x.SQL(sb.GetSqlString(), sb.params...).Get(&stats) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 34181d19673..ddda26dfd24 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -55,6 +55,7 @@ func init() { "AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"}, "AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps", "BurstBalance"}, "AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "NetworkPacketsIn", "NetworkPacketsOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"}, + "AWS/EC2/API": {"ClientErrors", "RequestLimitExceeded", "ServerErrors", "SuccessfulCalls"}, "AWS/EC2Spot": {"AvailableInstancePoolsCount", "BidsSubmittedForCapacity", "EligibleInstancePoolCount", "FulfilledCapacity", "MaxPercentCapacityAllocation", "PendingCapacity", "PercentCapacityAllocation", "TargetCapacity", "TerminatingCapacity"}, "AWS/ECS": {"CPUReservation", "MemoryReservation", "CPUUtilization", "MemoryUtilization"}, "AWS/EFS": {"BurstCreditBalance", "ClientConnections", "DataReadIOBytes", "DataWriteIOBytes", "MetadataIOBytes", "TotalIOBytes", "PermittedThroughput", "PercentIOLimit"}, @@ -133,6 +134,7 @@ func init() { "AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"}, "AWS/EBS": {"VolumeId"}, "AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"}, + "AWS/EC2/API": {}, "AWS/EC2Spot": {"AvailabilityZone", "FleetRequestId", "InstanceType"}, "AWS/ECS": {"ClusterName", "ServiceName"}, "AWS/EFS": {"FileSystemId"}, diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 35b03e489a0..d307e12166c 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -32,6 +32,18 @@ func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin datasource.Url, datasource.Database, ) + + tlsConfig, err := datasource.GetTLSConfig() + if err != nil { + return nil, err + } + + if tlsConfig.RootCAs != nil || len(tlsConfig.Certificates) > 0 { + tlsConfigString := fmt.Sprintf("ds%d", datasource.Id) + mysql.RegisterTLSConfig(tlsConfigString, tlsConfig) + cnnstr += "&tls=" + tlsConfigString + } + logger.Debug("getEngine", "connection", cnnstr) config := tsdb.SqlQueryEndpointConfiguration{ diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 4806275e87d..6db442e7470 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -9,7 +9,7 @@ import { TagFilter } from './components/TagFilter/TagFilter'; import { SideMenu } from './components/sidemenu/SideMenu'; import { MetricSelect } from './components/Select/MetricSelect'; import AppNotificationList from './components/AppNotifications/AppNotificationList'; -import { ColorPicker, SeriesColorPickerPopover } from '@grafana/ui'; +import { ColorPicker, SeriesColorPickerPopoverWithTheme } from '@grafana/ui'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); @@ -27,7 +27,7 @@ export function registerAngularDirectives() { 'color', ['onChange', { watchDepth: 'reference', wrapApply: true }], ]); - react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [ + react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopoverWithTheme, [ 'color', 'series', 'onColorChange', diff --git a/public/app/core/components/Animations/FadeIn.tsx b/public/app/core/components/Animations/FadeIn.tsx index ea9a92d5f0f..d667b54261e 100644 --- a/public/app/core/components/Animations/FadeIn.tsx +++ b/public/app/core/components/Animations/FadeIn.tsx @@ -1,11 +1,12 @@ import React, { FC } from 'react'; -import Transition from 'react-transition-group/Transition'; +import Transition, { ExitHandler } from 'react-transition-group/Transition'; interface Props { duration: number; children: JSX.Element; in: boolean; unmountOnExit?: boolean; + onExited?: ExitHandler; } export const FadeIn: FC = props => { @@ -22,7 +23,12 @@ export const FadeIn: FC = props => { }; return ( - + {state => (

({ emit: jest.fn(), })); +jest.mock('app/store/store', () => ({ + store: { + getState: jest.fn().mockReturnValue({ + location: { + lastUpdated: 0, + } + }) + } +})); + jest.mock('app/core/services/context_srv', () => ({ contextSrv: { sidemenu: true, diff --git a/public/app/core/components/sidemenu/SideMenu.tsx b/public/app/core/components/sidemenu/SideMenu.tsx index fd3e0d95564..29ef0fed069 100644 --- a/public/app/core/components/sidemenu/SideMenu.tsx +++ b/public/app/core/components/sidemenu/SideMenu.tsx @@ -3,9 +3,16 @@ import appEvents from '../../app_events'; import { contextSrv } from 'app/core/services/context_srv'; import TopSection from './TopSection'; import BottomSection from './BottomSection'; +import { store } from 'app/store/store'; export class SideMenu extends PureComponent { toggleSideMenu = () => { + // ignore if we just made a location change, stops hiding sidemenu on double clicks of back button + const timeSinceLocationChanged = new Date().getTime() - store.getState().location.lastUpdated; + if (timeSinceLocationChanged < 1000) { + return; + } + contextSrv.toggleSideMenu(); appEvents.emit('toggle-sidemenu'); }; diff --git a/public/app/core/config.ts b/public/app/core/config.ts index 368b3798117..f4254ac251a 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; import { PanelPlugin } from 'app/types/plugins'; +import { GrafanaTheme, getTheme, GrafanaThemeType } from '@grafana/ui'; export interface BuildInfo { version: string; @@ -36,8 +37,11 @@ export class Settings { loginError: any; viewersCanEdit: boolean; disableSanitizeHtml: boolean; + theme: GrafanaTheme; constructor(options: Settings) { + this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark); + const defaults = { datasources: {}, windowTitlePrefix: 'Grafana - ', diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 1f289fc4b27..80987b8fc88 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -1,4 +1,3 @@ -import './directives/dash_class'; import './directives/dropdown_typeahead'; import './directives/autofill_event_fix'; import './directives/metric_segment'; diff --git a/public/app/core/directives/dash_class.ts b/public/app/core/directives/dash_class.ts deleted file mode 100644 index 1fb93d29cf3..00000000000 --- a/public/app/core/directives/dash_class.ts +++ /dev/null @@ -1,39 +0,0 @@ -import $ from 'jquery'; -import _ from 'lodash'; -import coreModule from '../core_module'; - -/** @ngInject */ -function dashClass($timeout) { - return { - link: ($scope, elem) => { - const body = $('body'); - - $scope.ctrl.dashboard.events.on('view-mode-changed', panel => { - console.log('view-mode-changed', panel.fullscreen); - if (panel.fullscreen) { - body.addClass('panel-in-fullscreen'); - } else { - $timeout(() => { - body.removeClass('panel-in-fullscreen'); - }); - } - }); - - body.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true); - - $scope.$watch('ctrl.dashboardViewState.state.editview', newValue => { - if (newValue) { - elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue)); - setTimeout(() => { - elem.toggleClass('dashboard-page--settings-open', _.isString(newValue)); - }, 10); - } else { - elem.removeClass('dashboard-page--settings-opening'); - elem.removeClass('dashboard-page--settings-open'); - } - }); - }, - }; -} - -coreModule.directive('dashClass', dashClass); diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index abcd5563bd0..2cde5379448 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -340,6 +340,11 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): Time return a[1] - b[1]; }); - return { datapoints: series.datapoints, target: series.alias, color: series.color }; + return { + datapoints: series.datapoints, + target: series.alias, + alias: series.alias, + color: series.color + }; }); } diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index c038ab53c9f..dff1ac8f5c1 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -9,6 +9,7 @@ export const initialState: LocationState = { query: {}, routeParams: {}, replace: false, + lastUpdated: 0, }; export const locationReducer = (state = initialState, action: Action): LocationState => { @@ -28,6 +29,7 @@ export const locationReducer = (state = initialState, action: Action): LocationS query: { ...query }, routeParams: routeParams || state.routeParams, replace: replace === true, + lastUpdated: new Date().getTime(), }; } } diff --git a/public/app/core/utils/ConfigProvider.tsx b/public/app/core/utils/ConfigProvider.tsx index 6883401ad27..cb3ad88b191 100644 --- a/public/app/core/utils/ConfigProvider.tsx +++ b/public/app/core/utils/ConfigProvider.tsx @@ -1,6 +1,6 @@ import React from 'react'; import config, { Settings } from 'app/core/config'; -import { GrafanaTheme } from '@grafana/ui'; +import { GrafanaThemeType, ThemeContext, getTheme } from '@grafana/ui'; export const ConfigContext = React.createContext(config); export const ConfigConsumer = ConfigContext.Consumer; @@ -13,16 +13,20 @@ export const provideConfig = (component: React.ComponentType) => { return ConfigProvider; }; -interface ThemeProviderProps { - children: (theme: GrafanaTheme) => JSX.Element; -} +export const getCurrentThemeName = () => + config.bootData.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark; +export const getCurrentTheme = () => getTheme(getCurrentThemeName()); -export const ThemeProvider = ({ children }: ThemeProviderProps) => { +export const ThemeProvider = ({ children }: { children: React.ReactNode }) => { return ( - {({ bootData }) => { - return children(bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark); + {config => { + return {children}; }} ); }; + +export const provideTheme = (component: React.ComponentType) => { + return provideConfig((props: any) => {React.createElement(component, { ...props })}); +}; diff --git a/public/app/core/utils/react2angular.ts b/public/app/core/utils/react2angular.ts index 1057f68fcda..eb4bccab267 100644 --- a/public/app/core/utils/react2angular.ts +++ b/public/app/core/utils/react2angular.ts @@ -1,11 +1,11 @@ import coreModule from 'app/core/core_module'; -import { provideConfig } from 'app/core/utils/ConfigProvider'; +import { provideTheme } from 'app/core/utils/ConfigProvider'; export function react2AngularDirective(name: string, component: any, options: any) { coreModule.directive(name, [ 'reactDirective', reactDirective => { - return reactDirective(provideConfig(component), options); + return reactDirective(provideTheme(component), options); }, ]); } diff --git a/public/app/features/all.ts b/public/app/features/all.ts index 83146596ea0..d5e684e4a4e 100644 --- a/public/app/features/all.ts +++ b/public/app/features/all.ts @@ -12,3 +12,4 @@ import './manage-dashboards'; import './teams/CreateTeamCtrl'; import './profile/all'; import './datasources/settings/HttpSettingsCtrl'; +import './datasources/settings/TlsAuthSettingsCtrl'; diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 297d7ca7ea7..6db07b5d42e 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -9,12 +9,13 @@ import { PlaylistSrv } from 'app/features/playlist/playlist_srv'; // Components import { DashNavButton } from './DashNavButton'; +import { Tooltip } from '@grafana/ui'; // State import { updateLocation } from 'app/core/actions'; // Types -import { DashboardModel } from '../../state/DashboardModel'; +import { DashboardModel } from '../../state'; export interface Props { dashboard: DashboardModel; @@ -33,7 +34,6 @@ export class DashNav extends PureComponent { constructor(props: Props) { super(props); - this.playlistSrv = this.props.$injector.get('playlistSrv'); } @@ -123,26 +123,54 @@ export class DashNav extends PureComponent { }); }; - render() { - const { dashboard, isFullscreen, editview, onAddPanel } = this.props; - const { canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta; - const { snapshot } = dashboard; + renderDashboardTitleSearchButton() { + const { dashboard } = this.props; + const folderTitle = dashboard.meta.folderTitle; const haveFolder = dashboard.meta.folderId > 0; - const snapshotUrl = snapshot && snapshot.originalUrl; return ( -
+ <> -
+ + ); + } + + get isInFullscreenOrSettings() { + return this.props.editview || this.props.isFullscreen; + } + + renderBackButton() { + return ( +
+ + + +
+ ); + } + + render() { + const { dashboard, onAddPanel } = this.props; + const { canStar, canSave, canShare, showSettings, isStarred } = dashboard.meta; + const { snapshot } = dashboard; + + const snapshotUrl = snapshot && snapshot.originalUrl; + + return ( +
+ {this.isInFullscreenOrSettings && this.renderBackButton()} + {this.renderDashboardTitleSearchButton()} {this.playlistSrv.isPlaying && (
@@ -228,17 +256,6 @@ export class DashNav extends PureComponent {
(this.timePickerEl = element)} /> - - {(isFullscreen || editview) && ( -
- -
- )}
); } diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx index eda10087d41..83ef70f62e7 100644 --- a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx +++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx @@ -7,10 +7,11 @@ import _ from 'lodash'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; import { Emitter } from 'app/core/utils/emitter'; +import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; // Types import { PanelModel } from '../state/PanelModel'; -import { DataQuery, DataSourceApi } from '@grafana/ui'; +import { DataQuery, DataSourceApi, TimeRange } from '@grafana/ui'; interface Props { panel: PanelModel; @@ -43,8 +44,15 @@ export class QueryEditorRow extends PureComponent { componentDidMount() { this.loadDatasource(); + this.props.panel.events.on('refresh', this.onPanelRefresh); } + onPanelRefresh = () => { + if (this.state.angularScope) { + this.state.angularScope.range = getTimeSrv().timeRange(); + } + }; + getAngularQueryComponentScope(): AngularQueryComponentScope { const { panel, query } = this.props; const { datasource } = this.state; @@ -56,6 +64,7 @@ export class QueryEditorRow extends PureComponent { refresh: () => panel.refresh(), render: () => panel.render(), events: panel.events, + range: getTimeSrv().timeRange(), }; } @@ -97,6 +106,8 @@ export class QueryEditorRow extends PureComponent { } componentWillUnmount() { + this.props.panel.events.off('refresh', this.onPanelRefresh); + if (this.angularQueryEditor) { this.angularQueryEditor.destroy(); } @@ -250,4 +261,5 @@ export interface AngularQueryComponentScope { datasource: DataSourceApi; toggleEditorMode?: () => void; getCollapsedText?: () => string; + range: TimeRange; } diff --git a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx index 94a403c11bf..37f109250a7 100644 --- a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx +++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx @@ -119,7 +119,12 @@ export class VisualizationTab extends PureComponent { template += `
` + - (i > 0 ? `
{{ctrl.editorTabs[${i}].title}}
` : '') + + (i > 0 + ? `
+ {{ctrl.editorTabs[${i}].title}} + +
` + : '') + `
@@ -144,6 +149,10 @@ export class VisualizationTab extends PureComponent { } } + clearQuery = () => { + this.setState({ searchQuery: '' }); + }; + onPanelOptionsChanged = (options: any) => { this.props.panel.updateOptions(options); this.forceUpdate(); @@ -228,10 +237,15 @@ export class VisualizationTab extends PureComponent { }; return ( - + <> - + { } const mapStateToProps = (state: StoreState) => ({ - urlOpenVizPicker: !!state.location.query.openVizPicker + urlOpenVizPicker: !!state.location.query.openVizPicker, }); const mapDispatchToProps = { - updateLocation + updateLocation, }; export default connectWithStore(VisualizationTab, mapStateToProps, mapDispatchToProps); diff --git a/public/app/features/datasources/partials/http_settings.html b/public/app/features/datasources/partials/http_settings.html index 521e2d3cdc6..b6f2c4fc0dd 100644 --- a/public/app/features/datasources/partials/http_settings.html +++ b/public/app/features/datasources/partials/http_settings.html @@ -101,53 +101,5 @@
-
-
-
TLS Auth Details
- TLS Certs are encrypted and stored in the Grafana database. -
-
-
-
- -
-
- -
- -
- - reset -
-
-
- -
-
-
- -
-
- -
-
- - reset -
-
- -
-
- -
-
- -
-
- - reset -
-
-
-
- + + \ No newline at end of file diff --git a/public/app/features/datasources/partials/tls_auth_settings.html b/public/app/features/datasources/partials/tls_auth_settings.html new file mode 100644 index 00000000000..c852e8ec70c --- /dev/null +++ b/public/app/features/datasources/partials/tls_auth_settings.html @@ -0,0 +1,62 @@ +
+
+
TLS Auth Details
+ TLS Certs are encrypted and stored in the Grafana database. +
+
+
+
+
+ +
+ +
+ + reset +
+
+
+ +
+
+
+
+ +
+
+ + reset +
+
+ +
+
+
+ +
+
+ + reset +
+
+
+
diff --git a/public/app/features/datasources/settings/TlsAuthSettingsCtrl.ts b/public/app/features/datasources/settings/TlsAuthSettingsCtrl.ts new file mode 100644 index 00000000000..7c21fab404c --- /dev/null +++ b/public/app/features/datasources/settings/TlsAuthSettingsCtrl.ts @@ -0,0 +1,10 @@ +import { coreModule } from 'app/core/core'; + +coreModule.directive('datasourceTlsAuthSettings', () => { + return { + scope: { + current: '=', + }, + templateUrl: 'public/app/features/datasources/partials/tls_auth_settings.html', + }; +}); diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 437b50db63c..aca8f033fb3 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -205,28 +205,35 @@ export class Explore extends React.PureComponent {
- {({ width }) => ( -
- - {showingStartPage && } - {!showingStartPage && ( - <> - {supportsGraph && !supportsLogs && } - {supportsTable && } - {supportsLogs && ( - - )} - - )} - -
- )} + {({ width }) => { + if (width === 0) { + return null; + } + + return ( +
+ + {showingStartPage && } + {!showingStartPage && ( + <> + {supportsGraph && !supportsLogs && } + {supportsTable && } + {supportsLogs && ( + + )} + + )} + +
+ ); + }}
)} diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index af6caee6206..f41555b9121 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -51,6 +51,7 @@ function renderMetaItem(value: any, kind: LogsMetaKind) { interface Props { data?: LogsModel; + width: number; exploreId: string; highlighterExpressions: string[]; loading: boolean; @@ -164,6 +165,7 @@ export default class Logs extends PureComponent { range, scanning, scanRange, + width, } = this.props; if (!data) { @@ -216,6 +218,7 @@ export default class Logs extends PureComponent { { @@ -51,7 +52,8 @@ export class LogsContainer extends PureComponent { range, showingLogs, scanning, - scanRange + scanRange, + width, } = this.props; return ( @@ -71,6 +73,7 @@ export class LogsContainer extends PureComponent { range={range} scanning={scanning} scanRange={scanRange} + width={width} /> ); diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx index a1b9e7a5df9..14fe046e098 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx @@ -2,61 +2,65 @@ import React, { PureComponent } from 'react'; // Components -import { Select, SelectOptionItem } from '@grafana/ui'; +// import { Select, SelectOptionItem } from '@grafana/ui'; // Types import { QueryEditorProps } from '@grafana/ui/src/types'; import { LokiDatasource } from '../datasource'; import { LokiQuery } from '../types'; -import { LokiQueryField } from './LokiQueryField'; +// import { LokiQueryField } from './LokiQueryField'; type Props = QueryEditorProps; -interface State { - query: LokiQuery; -} +// interface State { +// query: LokiQuery; +// } export class LokiQueryEditor extends PureComponent { - state: State = { - query: this.props.query, - }; - - onRunQuery = () => { - const { query } = this.state; - - this.props.onChange(query); - this.props.onRunQuery(); - }; - - onFieldChange = (query: LokiQuery, override?) => { - this.setState({ - query: { - ...this.state.query, - expr: query.expr, - }, - }); - }; - - onFormatChanged = (option: SelectOptionItem) => { - this.props.onChange({ - ...this.state.query, - resultFormat: option.value, - }); - }; + // state: State = { + // query: this.props.query, + // }; + // + // onRunQuery = () => { + // const { query } = this.state; + // + // this.props.onChange(query); + // this.props.onRunQuery(); + // }; + // + // onFieldChange = (query: LokiQuery, override?) => { + // this.setState({ + // query: { + // ...this.state.query, + // expr: query.expr, + // }, + // }); + // }; + // + // onFormatChanged = (option: SelectOptionItem) => { + // this.props.onChange({ + // ...this.state.query, + // resultFormat: option.value, + // }); + // }; render() { - const { query } = this.state; - const { datasource } = this.props; - const formatOptions: SelectOptionItem[] = [ - { label: 'Time Series', value: 'time_series' }, - { label: 'Table', value: 'table' }, - ]; - - query.resultFormat = query.resultFormat || 'time_series'; - const currentFormat = formatOptions.find(item => item.value === query.resultFormat); + // const { query } = this.state; + // const { datasource } = this.props; + // const formatOptions: SelectOptionItem[] = [ + // { label: 'Time Series', value: 'time_series' }, + // { label: 'Table', value: 'table' }, + // ]; + // + // query.resultFormat = query.resultFormat || 'time_series'; + // const currentFormat = formatOptions.find(item => item.value === query.resultFormat); return (
+
+
Loki is currently not supported as dashboard data source. We are working on it!
+
+ {/* {
+ */}
); } diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index 23aa5504d3e..303cd0471d7 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -8,7 +8,7 @@ export class MssqlDatasource { interval: string; /** @ngInject */ - constructor(instanceSettings, private backendSrv, private $q, private templateSrv) { + constructor(instanceSettings, private backendSrv, private $q, private templateSrv, private timeSrv) { this.name = instanceSettings.name; this.id = instanceSettings.id; this.responseParser = new ResponseParser(this.$q); @@ -107,13 +107,18 @@ export class MssqlDatasource { format: 'table', }; + const range = this.timeSrv.timeRange(); + const data = { + queries: [interpolatedQuery], + from: range.from.valueOf().toString(), + to: range.to.valueOf().toString(), + }; + return this.backendSrv .datasourceRequest({ url: '/api/tsdb/query', method: 'POST', - data: { - queries: [interpolatedQuery], - }, + data: data, }) .then(data => this.responseParser.parseMetricFindQueryResult(refId, data)); } diff --git a/public/app/plugins/datasource/mssql/specs/datasource.test.ts b/public/app/plugins/datasource/mssql/specs/datasource.test.ts index 0dd496bfe59..a05848b3da8 100644 --- a/public/app/plugins/datasource/mssql/specs/datasource.test.ts +++ b/public/app/plugins/datasource/mssql/specs/datasource.test.ts @@ -1,6 +1,6 @@ import moment from 'moment'; import { MssqlDatasource } from '../datasource'; -import { TemplateSrvStub } from 'test/specs/helpers'; +import { TemplateSrvStub, TimeSrvStub } from 'test/specs/helpers'; import { CustomVariable } from 'app/features/templating/custom_variable'; import q from 'q'; @@ -8,13 +8,14 @@ describe('MSSQLDatasource', () => { const ctx: any = { backendSrv: {}, templateSrv: new TemplateSrvStub(), + timeSrv: new TimeSrvStub(), }; beforeEach(() => { ctx.$q = q; ctx.instanceSettings = { name: 'mssql' }; - ctx.ds = new MssqlDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.$q, ctx.templateSrv); + ctx.ds = new MssqlDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.$q, ctx.templateSrv, ctx.timeSrv); }); describe('When performing annotationQuery', () => { @@ -188,6 +189,49 @@ describe('MSSQLDatasource', () => { }); }); + describe('When performing metricFindQuery', () => { + let results; + const query = 'select * from atable'; + const response = { + results: { + tempvar: { + meta: { + rowCount: 1, + }, + refId: 'tempvar', + tables: [ + { + columns: [{ text: 'title' }], + rows: [['aTitle']], + }, + ], + }, + }, + }; + const time = { + from: moment(1521545610656), + to: moment(1521546251185) + }; + + beforeEach(() => { + ctx.timeSrv.setTime(time); + + ctx.backendSrv.datasourceRequest = options => { + results = options.data; + return ctx.$q.when({ data: response, status: 200 }); + }; + + return ctx.ds.metricFindQuery(query); + }); + + it('should pass timerange to datasourceRequest', () => { + expect(results.from).toBe(time.from.valueOf().toString()); + expect(results.to).toBe(time.to.valueOf().toString()); + expect(results.queries.length).toBe(1); + expect(results.queries[0].rawSql).toBe(query); + }); + }); + describe('When interpolating variables', () => { beforeEach(() => { ctx.variable = new CustomVariable({}, {}); diff --git a/public/app/plugins/datasource/mysql/partials/config.html b/public/app/plugins/datasource/mysql/partials/config.html index a35633c626a..8221a06e1ee 100644 --- a/public/app/plugins/datasource/mysql/partials/config.html +++ b/public/app/plugins/datasource/mysql/partials/config.html @@ -1,4 +1,3 @@ -

MySQL Connection

@@ -22,7 +21,23 @@
-
+ +
+
+ + +
+
+ +
+
+ + + Connection limits @@ -84,4 +99,3 @@

- diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index b6f37dde94f..5cb256ee1aa 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; // Services & Utils -import { processTimeSeries } from '@grafana/ui'; +import { processTimeSeries, ThemeContext } from '@grafana/ui'; // Components import { Gauge } from '@grafana/ui'; @@ -10,7 +10,6 @@ import { Gauge } from '@grafana/ui'; // Types import { GaugeOptions } from './types'; import { PanelProps, NullValueMode, TimeSeriesValue } from '@grafana/ui/src/types'; -import { ThemeProvider } from 'app/core/utils/ConfigProvider'; interface Props extends PanelProps {} @@ -38,7 +37,7 @@ export class GaugePanel extends PureComponent { } return ( - + {theme => ( { theme={theme} /> )} - + ); } } diff --git a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx b/public/app/plugins/panel/gauge/GaugePanelOptions.tsx index 655c596ce84..84726ac88bf 100644 --- a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx +++ b/public/app/plugins/panel/gauge/GaugePanelOptions.tsx @@ -11,7 +11,6 @@ import { import ValueOptions from 'app/plugins/panel/gauge/ValueOptions'; import GaugeOptionsEditor from './GaugeOptionsEditor'; import { GaugeOptions } from './types'; -import { ThemeProvider } from 'app/core/utils/ConfigProvider'; export const defaultProps = { options: { @@ -46,24 +45,17 @@ export default class GaugePanelOptions extends PureComponent - {(theme) => ( - <> - - - - - - - - )} - + return ( + <> + + + + + + + + ); } } diff --git a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx index d62613319b2..2cf45727c4a 100644 --- a/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx +++ b/public/app/plugins/panel/graph/Legend/LegendSeriesItem.tsx @@ -2,7 +2,6 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; import { TimeSeries } from 'app/core/core'; import { SeriesColorPicker } from '@grafana/ui'; -import { ThemeProvider } from 'app/core/utils/ConfigProvider'; export const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total']; @@ -168,24 +167,17 @@ class LegendSeriesIcon extends PureComponent - {theme => { - return ( - - - - - - ); - }} - + + + + + ); } } diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index 4141d36e273..2966bb33eb4 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { colors, GrafanaTheme, getColorFromHexRgbOrName } from '@grafana/ui'; +import { colors, getColorFromHexRgbOrName } from '@grafana/ui'; import TimeSeries from 'app/core/time_series2'; import config from 'app/core/config'; @@ -113,7 +113,7 @@ export class DataProcessor { const series = new TimeSeries({ datapoints: datapoints, alias: alias, - color: getColorFromHexRgbOrName(color, config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark), + color: getColorFromHexRgbOrName(color, config.theme.type), unit: seriesData.unit, }); diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index aeb540551b8..54ba4ed1e6f 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -25,7 +25,10 @@ import ReactDOM from 'react-dom'; import { Legend, GraphLegendProps } from './Legend/Legend'; import { GraphCtrl } from './module'; -import { GrafanaTheme, getValueFormat } from '@grafana/ui'; +import { getValueFormat } from '@grafana/ui'; +import { provideTheme } from 'app/core/utils/ConfigProvider'; + +const LegendWithThemeProvider = provideTheme(Legend); class GraphElement { ctrl: GraphCtrl; @@ -43,6 +46,7 @@ class GraphElement { legendElem: HTMLElement; constructor(private scope, private elem, private timeSrv) { + this.ctrl = scope.ctrl; this.dashboard = this.ctrl.dashboard; this.panel = this.ctrl.panel; @@ -51,10 +55,7 @@ class GraphElement { this.panelWidth = 0; this.eventManager = new EventManager(this.ctrl); this.thresholdManager = new ThresholdManager(this.ctrl); - this.timeRegionManager = new TimeRegionManager( - this.ctrl, - config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark - ); + this.timeRegionManager = new TimeRegionManager(this.ctrl, config.theme.type); this.tooltip = new GraphTooltip(this.elem, this.ctrl.dashboard, this.scope, () => { return this.sortedSeries; }); @@ -109,7 +110,7 @@ class GraphElement { onToggleAxis: this.ctrl.onToggleAxis, }; - const legendReactElem = React.createElement(Legend, legendProps); + const legendReactElem = React.createElement(LegendWithThemeProvider, legendProps); ReactDOM.render(legendReactElem, this.legendElem, () => this.renderPanel()); } diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 68d982eab13..3919c4f69a9 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -10,7 +10,7 @@ import { MetricsPanelCtrl } from 'app/plugins/sdk'; import { DataProcessor } from './data_processor'; import { axesEditorComponent } from './axes_editor'; import config from 'app/core/config'; -import { GrafanaTheme, getColorFromHexRgbOrName } from '@grafana/ui'; +import { getColorFromHexRgbOrName } from '@grafana/ui'; class GraphCtrl extends MetricsPanelCtrl { static template = template; @@ -244,7 +244,7 @@ class GraphCtrl extends MetricsPanelCtrl { } onColorChange = (series, color) => { - series.setColor(getColorFromHexRgbOrName(color, config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark)); + series.setColor(getColorFromHexRgbOrName(color, config.theme.type)); this.panel.aliasColors[series.alias] = color; this.render(); }; diff --git a/public/app/plugins/panel/graph/time_region_manager.ts b/public/app/plugins/panel/graph/time_region_manager.ts index 2917583ff36..ea39927bf57 100644 --- a/public/app/plugins/panel/graph/time_region_manager.ts +++ b/public/app/plugins/panel/graph/time_region_manager.ts @@ -1,7 +1,7 @@ import 'vendor/flot/jquery.flot'; import _ from 'lodash'; import moment from 'moment'; -import { GrafanaTheme, getColorFromHexRgbOrName } from '@grafana/ui'; +import { GrafanaThemeType, getColorFromHexRgbOrName } from '@grafana/ui'; type TimeRegionColorDefinition = { fill: string; @@ -43,7 +43,7 @@ export function getColorModes() { }); } -function getColor(timeRegion, theme: GrafanaTheme): TimeRegionColorDefinition { +function getColor(timeRegion, theme: GrafanaThemeType): TimeRegionColorDefinition { if (Object.keys(colorModes).indexOf(timeRegion.colorMode) === -1) { timeRegion.colorMode = 'red'; } @@ -58,7 +58,7 @@ function getColor(timeRegion, theme: GrafanaTheme): TimeRegionColorDefinition { const colorMode = colorModes[timeRegion.colorMode]; if (colorMode.themeDependent === true) { - return theme === GrafanaTheme.Light ? colorMode.lightColor : colorMode.darkColor; + return theme === GrafanaThemeType.Light ? colorMode.lightColor : colorMode.darkColor; } return { @@ -71,7 +71,7 @@ export class TimeRegionManager { plot: any; timeRegions: any; - constructor(private panelCtrl, private theme: GrafanaTheme = GrafanaTheme.Dark) {} + constructor(private panelCtrl, private theme: GrafanaThemeType = GrafanaThemeType.Dark) {} draw(plot) { this.timeRegions = this.panelCtrl.panel.timeRegions; diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts index 81329fe297b..dea250abf74 100644 --- a/public/app/plugins/panel/heatmap/color_legend.ts +++ b/public/app/plugins/panel/heatmap/color_legend.ts @@ -5,7 +5,7 @@ import { contextSrv } from 'app/core/core'; import { tickStep } from 'app/core/utils/ticks'; import { getColorScale, getOpacityScale } from './color_scale'; import coreModule from 'app/core/core_module'; -import { GrafanaTheme, getColorFromHexRgbOrName } from '@grafana/ui'; +import { GrafanaThemeType, getColorFromHexRgbOrName } from '@grafana/ui'; const LEGEND_HEIGHT_PX = 6; const LEGEND_WIDTH_PX = 100; @@ -250,7 +250,7 @@ function drawSimpleOpacityLegend(elem, options) { .attr('stroke-width', 0) .attr( 'fill', - getColorFromHexRgbOrName(options.cardColor, contextSrv.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark) + getColorFromHexRgbOrName(options.cardColor, contextSrv.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark) ) .style('opacity', d => legendOpacityScale(d)); } diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts index 6489c9e9895..63604382432 100644 --- a/public/app/plugins/panel/heatmap/rendering.ts +++ b/public/app/plugins/panel/heatmap/rendering.ts @@ -7,7 +7,7 @@ import * as ticksUtils from 'app/core/utils/ticks'; import { HeatmapTooltip } from './heatmap_tooltip'; import { mergeZeroBuckets } from './heatmap_data_converter'; import { getColorScale, getOpacityScale } from './color_scale'; -import { GrafanaTheme, getColorFromHexRgbOrName, getValueFormat } from '@grafana/ui'; +import { GrafanaThemeType, getColorFromHexRgbOrName, getValueFormat } from '@grafana/ui'; const MIN_CARD_SIZE = 1, CARD_PADDING = 1, @@ -663,7 +663,7 @@ export class HeatmapRenderer { if (this.panel.color.mode === 'opacity') { return getColorFromHexRgbOrName( this.panel.color.cardColor, - contextSrv.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark + contextSrv.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark ); } else { return this.colorScale(d.count); diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 2768951d2ba..21ab32278f8 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -8,7 +8,7 @@ import kbn from 'app/core/utils/kbn'; import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; import { MetricsPanelCtrl } from 'app/plugins/sdk'; -import { GrafanaTheme, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui'; +import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui'; class SingleStatCtrl extends MetricsPanelCtrl { static templateUrl = 'module.html'; @@ -588,10 +588,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { fill: 1, zero: false, lineWidth: 1, - fillColor: getColorFromHexRgbOrName( - panel.sparkline.fillColor, - config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark - ), + fillColor: getColorFromHexRgbOrName(panel.sparkline.fillColor, config.theme.type), }, }, yaxes: { show: false }, @@ -608,10 +605,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { const plotSeries = { data: data.flotpairs, - color: getColorFromHexRgbOrName( - panel.sparkline.lineColor, - config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark - ), + color: getColorFromHexRgbOrName(panel.sparkline.lineColor, config.theme.type), }; $.plot(plotCanvas, [plotSeries], options); @@ -630,7 +624,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { // Map panel colors to hex or rgb/a values data.colorMap = panel.colors.map(color => - getColorFromHexRgbOrName(color, config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark) + getColorFromHexRgbOrName(color, config.bootData.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark) ); const body = panel.gauge.show ? '' : getBigValueHtml(); diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 82763e1839a..268f5aa7ac4 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -6,7 +6,6 @@ import { transformDataToTable } from './transformers'; import { tablePanelEditor } from './editor'; import { columnOptionsTab } from './column_options'; import { TableRenderer } from './renderer'; -import { GrafanaTheme } from '@grafana/ui'; class TablePanelCtrl extends MetricsPanelCtrl { static templateUrl = 'module.html'; @@ -131,7 +130,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { this.dashboard.isTimezoneUtc(), this.$sanitize, this.templateSrv, - config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark, + config.theme.type ); return super.render(this.table); diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index 90479a67602..e9bf89f45fe 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import moment from 'moment'; import kbn from 'app/core/utils/kbn'; -import { GrafanaTheme, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui'; +import { getValueFormat, getColorFromHexRgbOrName, GrafanaThemeType } from '@grafana/ui'; export class TableRenderer { formatters: any[]; @@ -13,7 +13,7 @@ export class TableRenderer { private isUtc, private sanitize, private templateSrv, - private theme?: GrafanaTheme + private theme?: GrafanaThemeType ) { this.initColumns(); } diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index d64e74e3949..35c23239561 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -5,6 +5,7 @@ import { Provider } from 'react-redux'; import coreModule from 'app/core/core_module'; import { store } from 'app/store/store'; import { ContextSrv } from 'app/core/services/context_srv'; +import { provideTheme } from 'app/core/utils/ConfigProvider'; function WrapInProvider(store, Component, props) { return ( @@ -49,7 +50,7 @@ export function reactContainer( document.body.classList.add('is-react'); - ReactDOM.render(WrapInProvider(store, component, props), elem[0]); + ReactDOM.render(WrapInProvider(store, provideTheme(component), props), elem[0]); scope.$on('$destroy', () => { document.body.classList.remove('is-react'); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index e0029cf2464..4c9c5fd5304 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -150,8 +150,8 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controllerAs: 'ctrl', }) .when('/dashboards/f/:uid', { - templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html', - controller: 'FolderDashboardsCtrl', + templateUrl: 'public/app/features/folders/partials/folder_dashboards.html', + controller: FolderDashboardsCtrl, controllerAs: 'ctrl', }) .when('/explore', { diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index e2c33523271..2638587e96d 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -1,6 +1,6 @@ import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; import thunk from 'redux-thunk'; -import { createLogger } from 'redux-logger'; +// import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; @@ -41,7 +41,7 @@ export function configureStore() { if (process.env.NODE_ENV !== 'production') { // DEV builds we had the logger middleware - setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger())))); + setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk)))); } else { setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk)))); } diff --git a/public/app/types/location.ts b/public/app/types/location.ts index a47ef05d2be..4730f9d6ed7 100644 --- a/public/app/types/location.ts +++ b/public/app/types/location.ts @@ -15,6 +15,7 @@ export interface LocationState { query: UrlQueryMap; routeParams: UrlQueryMap; replace: boolean; + lastUpdated: number; } export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 97d7a374765..0f4e15c91ec 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -56,7 +56,7 @@ $page-bg: $gray-7; $body-color: $gray-1; $text-color: $gray-1; $text-color-strong: $dark-2; -$text-color-weak: $gray-2; +$text-color-weak: $gray-3; $text-color-faint: $gray-4; $text-color-emphasis: $dark-5; diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index 0cfa314a985..ce0fb45051e 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -1,6 +1,6 @@ .navbar { position: relative; - padding-left: 40px; + padding-left: 20px; z-index: $zindex-navbar-fixed; height: $navbarHeight; padding-right: 20px; @@ -41,15 +41,12 @@ .panel-in-fullscreen { .navbar { - padding-left: 15px; + padding-left: 20px; } .navbar-button--add-panel, .navbar-button--star, .navbar-button--tv, - .navbar-page-btn .fa-caret-down { - display: none; - } .navbar-buttons--close { display: flex; @@ -179,3 +176,33 @@ } } } + +.navbar-edit { + display: flex; + height: $navbarHeight; + align-items: center; + padding-left: 7px; +} + +.navbar-edit__back-btn { + background: transparent; + border: 2px solid $text-color; + border-radius: 50%; + width: 34px; + height: 34px; + transition: transform 0.1s ease 0.1s; + color: $text-color; + + i { + font-size: $font-size-lg; + position: relative; + top: 2px; + } + + &:hover { + color: $text-color-strong; + border-color: $text-color-strong; + } +} + + diff --git a/public/sass/components/_panel_editor.scss b/public/sass/components/_panel_editor.scss index b791231a242..475d7444b03 100644 --- a/public/sass/components/_panel_editor.scss +++ b/public/sass/components/_panel_editor.scss @@ -86,6 +86,10 @@ .panel-editor-container__panel { margin: 0 $dashboard-padding; } + + .search-container { + left: 0 !important; + } } .panel-editor-container__resizer { @@ -146,15 +150,17 @@ padding-bottom: 6px; transition: transform 1 ease; - &--current { - box-shadow: 0 0 6px $orange; - border: 1px solid $orange; - } - &:hover { box-shadow: $panel-editor-viz-item-shadow-hover; background: $panel-editor-viz-item-bg-hover; border: $panel-editor-viz-item-border-hover; + + } + + &--current { + box-shadow: 0 0 6px $orange !important; + border: 1px solid $orange !important; + background: $panel-editor-viz-item-bg !important; } } diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index daad8fd10da..eba03283510 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -21,9 +21,9 @@ // Search .search-field-wrapper { width: 100%; + height: $navbarHeight; display: flex; background-color: $navbarBackground; - box-shadow: $navbarShadow; position: relative; & > input { diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index 1570c7dd9b7..b8307186540 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -143,7 +143,7 @@ export function DashboardViewStateStub(this: any) { } export function TimeSrvStub(this: any) { - this.init = sinon.spy(); + this.init = () => {}; this.time = { from: 'now-1h', to: 'now' }; this.timeRange = function(parse) { if (parse === false) { diff --git a/scripts/webpack/sass.rule.js b/scripts/webpack/sass.rule.js index 75455fc4184..66a48a12b32 100644 --- a/scripts/webpack/sass.rule.js +++ b/scripts/webpack/sass.rule.js @@ -1,6 +1,6 @@ 'use strict'; -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = function(options) { return { @@ -23,7 +23,12 @@ module.exports = function(options) { config: { path: __dirname + '/postcss.config.js' }, }, }, - { loader: 'sass-loader', options: { sourceMap: options.sourceMap } }, + { + loader: 'sass-loader', + options: { + sourceMap: options.sourceMap + }, + }, ], }; }; diff --git a/scripts/webpack/webpack.hot.js b/scripts/webpack/webpack.hot.js index b37e4c08592..c1053f1f7da 100644 --- a/scripts/webpack/webpack.hot.js +++ b/scripts/webpack/webpack.hot.js @@ -85,7 +85,9 @@ module.exports = merge(common, { config: { path: __dirname + '/postcss.config.js' }, }, }, - 'sass-loader', // compiles Sass to CSS + { + loader: 'sass-loader' + } ], }, { diff --git a/scripts/webpack/webpack.test.js b/scripts/webpack/webpack.test.js deleted file mode 100644 index ec9ee5e26df..00000000000 --- a/scripts/webpack/webpack.test.js +++ /dev/null @@ -1,38 +0,0 @@ -const webpack = require('webpack'); -const merge = require('webpack-merge'); -const common = require('./webpack.common.js'); - -config = merge(common, { - mode: 'development', - devtool: 'cheap-module-source-map', - - externals: { - 'react/addons': true, - 'react/lib/ExecutionEnvironment': true, - 'react/lib/ReactContext': true, - }, - - module: { - rules: [ - { - test: /\.tsx?$/, - exclude: /node_modules/, - use: { - loader: 'ts-loader', - options: { - transpileOnly: true, - }, - }, - }, - ], - }, - - plugins: [ - new webpack.SourceMapDevToolPlugin({ - filename: null, // if no value is provided the sourcemap is inlined - test: /\.(ts|js)($|\?)/i, // process .js and .ts files only - }), - ], -}); - -module.exports = config;