mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into explore/dedup-strategu-url
This commit is contained in:
commit
14bf960b64
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
13
README.md
13
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
54
devenv/docker/blocks/freeipa/docker-compose.yaml
Normal file
54
devenv/docker/blocks/freeipa/docker-compose.yaml
Normal file
@ -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
|
74
devenv/docker/blocks/freeipa/ldap_freeipa.toml
Normal file
74
devenv/docker/blocks/freeipa/ldap_freeipa.toml
Normal file
@ -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"
|
32
devenv/docker/blocks/freeipa/notes.md
Normal file
32
devenv/docker/blocks/freeipa/notes.md
Normal file
@ -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
|
||||
```
|
@ -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",
|
||||
|
@ -8,6 +8,6 @@
|
||||
"tslint": "echo \"Nothing to do\"",
|
||||
"typecheck": "echo \"Nothing to do\""
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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 (
|
||||
<UseState initialState="#00ff00">
|
||||
{(selectedColor, updateSelectedColor) => {
|
||||
return (
|
||||
<ColorPicker
|
||||
enableNamedColors={enableNamedColors}
|
||||
color={selectedColor}
|
||||
onChange={color => {
|
||||
action('Color changed')(color);
|
||||
updateSelectedColor(color);
|
||||
}}
|
||||
theme={selectedTheme || undefined}
|
||||
/>
|
||||
);
|
||||
return renderComponentWithTheme(ColorPicker, {
|
||||
enableNamedColors,
|
||||
color: selectedColor,
|
||||
onChange: (color: any) => {
|
||||
action('Color changed')(color);
|
||||
updateSelectedColor(color);
|
||||
},
|
||||
});
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
});
|
||||
|
||||
ColorPickerStories.add('Series color picker', () => {
|
||||
const { selectedTheme, enableNamedColors } = getColorPickerKnobs();
|
||||
const { enableNamedColors } = getColorPickerKnobs();
|
||||
|
||||
return (
|
||||
<UseState initialState="#00ff00">
|
||||
@ -52,7 +49,6 @@ ColorPickerStories.add('Series color picker', () => {
|
||||
onToggleAxis={() => {}}
|
||||
color={selectedColor}
|
||||
onChange={color => updateSelectedColor(color)}
|
||||
theme={selectedTheme || undefined}
|
||||
>
|
||||
<div style={{ color: selectedColor, cursor: 'pointer' }}>Open color picker</div>
|
||||
</SeriesColorPicker>
|
||||
|
@ -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 = <T extends ColorPickerProps>(
|
||||
popover: React.ComponentType<T>,
|
||||
displayName = 'ColorPicker',
|
||||
renderPopoverArrowFunction?: RenderPopperArrowFn
|
||||
) => {
|
||||
return class ColorPicker extends Component<T, any> {
|
||||
static displayName = displayName;
|
||||
@ -50,17 +48,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
...this.props,
|
||||
onChange: this.handleColorChange,
|
||||
});
|
||||
const { theme, withArrow, children } = this.props;
|
||||
|
||||
const renderArrow: RenderPopperArrowFn = ({ arrowProps, placement }) => {
|
||||
return (
|
||||
<div
|
||||
{...arrowProps}
|
||||
data-placement={placement}
|
||||
className={`ColorPicker__arrow ColorPicker__arrow--${theme === GrafanaTheme.Light ? 'light' : 'dark'}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const { theme, children } = this.props;
|
||||
|
||||
return (
|
||||
<PopperController content={popoverElement} hideAfter={300}>
|
||||
@ -72,7 +60,6 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
{...popperProps}
|
||||
referenceElement={this.pickerTriggerRef.current}
|
||||
wrapperClassName="ColorPicker"
|
||||
renderArrow={withArrow && (renderPopoverArrowFunction || renderArrow)}
|
||||
onMouseLeave={hidePopper}
|
||||
onMouseEnter={showPopper}
|
||||
/>
|
||||
@ -95,7 +82,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
<div
|
||||
className="sp-preview-inner"
|
||||
style={{
|
||||
backgroundColor: getColorFromHexRgbOrName(this.props.color || '#000000', theme),
|
||||
backgroundColor: getColorFromHexRgbOrName(this.props.color || '#000000', theme.type),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -110,5 +97,5 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
};
|
||||
};
|
||||
|
||||
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'));
|
||||
|
@ -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 (
|
||||
<ColorPickerPopover
|
||||
color="#BC67E6"
|
||||
onChange={color => {
|
||||
console.log(color);
|
||||
}}
|
||||
theme={selectedTheme || undefined}
|
||||
/>
|
||||
);
|
||||
return renderComponentWithTheme(ColorPickerPopover, {
|
||||
color: '#BC67E6',
|
||||
onChange: (color: any) => {
|
||||
console.log(color);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
ColorPickerPopoverStories.add('SeriesColorPickerPopover', () => {
|
||||
const selectedTheme = getThemeKnob();
|
||||
|
||||
return (
|
||||
<SeriesColorPickerPopover
|
||||
color="#BC67E6"
|
||||
onChange={color => {
|
||||
console.log(color);
|
||||
}}
|
||||
theme={selectedTheme || undefined}
|
||||
/>
|
||||
);
|
||||
return renderComponentWithTheme(SeriesColorPickerPopover, {
|
||||
color: '#BC67E6',
|
||||
onChange: (color: any) => {
|
||||
console.log(color);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -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(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} />);
|
||||
const wrapper = mount(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} 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(<ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} />);
|
||||
const wrapper = mount(<ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} 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(
|
||||
<ColorPickerPopover color={BasicGreen.variants.dark} onChange={onChangeSpy} theme={GrafanaTheme.Light} />
|
||||
<ColorPickerPopover color={BasicGreen.variants.dark} onChange={onChangeSpy} theme={getTheme(GrafanaThemeType.Light)} />
|
||||
);
|
||||
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);
|
||||
|
@ -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<T> extends ColorPickerProps, PopperContentProps {
|
||||
customPickers?: T;
|
||||
@ -43,7 +43,7 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> 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<T extends CustomPickersDescriptor> extends React
|
||||
case 'spectrum':
|
||||
return <SpectrumPalette color={color} onChange={this.handleChange} theme={theme} />;
|
||||
case 'palette':
|
||||
return <NamedColorsPalette color={getColorName(color, theme)} onChange={this.handleChange} theme={theme} />;
|
||||
return (
|
||||
<NamedColorsPalette color={getColorName(color, theme.type)} onChange={this.handleChange} theme={theme} />
|
||||
);
|
||||
default:
|
||||
return this.renderCustomPicker(activePicker);
|
||||
}
|
||||
@ -88,11 +90,7 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
||||
<>
|
||||
{Object.keys(customPickers).map(key => {
|
||||
return (
|
||||
<div
|
||||
className={this.getTabClassName(key)}
|
||||
onClick={this.handleTabChange(key)}
|
||||
key={key}
|
||||
>
|
||||
<div className={this.getTabClassName(key)} onClick={this.handleTabChange(key)} key={key}>
|
||||
{customPickers[key].name}
|
||||
</div>
|
||||
);
|
||||
@ -103,21 +101,14 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
||||
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
const colorPickerTheme = theme || GrafanaTheme.Dark;
|
||||
|
||||
const colorPickerTheme = theme.type || GrafanaThemeType.Dark;
|
||||
return (
|
||||
<div className={`ColorPickerPopover ColorPickerPopover--${colorPickerTheme}`}>
|
||||
<div className="ColorPickerPopover__tabs">
|
||||
<div
|
||||
className={this.getTabClassName('palette')}
|
||||
onClick={this.handleTabChange('palette')}
|
||||
>
|
||||
<div className={this.getTabClassName('palette')} onClick={this.handleTabChange('palette')}>
|
||||
Colors
|
||||
</div>
|
||||
<div
|
||||
className={this.getTabClassName('spectrum')}
|
||||
onClick={this.handleTabChange('spectrum')}
|
||||
>
|
||||
<div className={this.getTabClassName('spectrum')} onClick={this.handleTabChange('spectrum')}>
|
||||
Custom
|
||||
</div>
|
||||
{this.renderCustomPickerTabs()}
|
||||
@ -128,3 +119,4 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<ColorSwatchProps> = ({
|
||||
}) => {
|
||||
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<NamedColorsGroupProps> = ({
|
||||
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<NamedColorsGroupProps> = ({
|
||||
<ColorSwatch
|
||||
key={color.name}
|
||||
isSelected={color.name === selectedColor}
|
||||
color={getColorForTheme(color, theme)}
|
||||
color={getColorForTheme(color, theme.type)}
|
||||
onClick={() => onColorSelect(color)}
|
||||
theme={theme}
|
||||
/>
|
||||
|
@ -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 (
|
||||
<UseState initialState={selectedColor}>
|
||||
{(selectedColor, updateSelectedColor) => {
|
||||
return <NamedColorsPalette color={selectedColor} onChange={updateSelectedColor} />;
|
||||
return renderComponentWithTheme(NamedColorsPalette, {
|
||||
color: selectedColor,
|
||||
onChange: updateSelectedColor,
|
||||
});
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
@ -45,7 +49,10 @@ NamedColorsPaletteStories.add('Named colors swatch - support for named colors',
|
||||
return (
|
||||
<UseState initialState={selectedColor}>
|
||||
{(selectedColor, updateSelectedColor) => {
|
||||
return <NamedColorsPalette color={getColorName(selectedColor)} onChange={updateSelectedColor} />;
|
||||
return renderComponentWithTheme(NamedColorsPalette, {
|
||||
color: getColorName(selectedColor),
|
||||
onChange: updateSelectedColor,
|
||||
});
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
|
@ -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(<NamedColorsPalette color={BasicGreen.name} theme={GrafanaTheme.Dark} onChange={() => {}} />);
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={getTheme()} onChange={() => {}} />);
|
||||
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
|
||||
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
|
||||
|
||||
wrapper.unmount();
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={GrafanaTheme.Light} onChange={() => {}} />);
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={getTheme(GrafanaThemeType.Light)} onChange={() => {}} />);
|
||||
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(<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} />);
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} theme={getTheme()}/>);
|
||||
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
|
||||
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
|
||||
});
|
||||
|
@ -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<SeriesColorPickerPopoverProps> = props => {
|
||||
const { yaxis, onToggleAxis, color, ...colorPickerProps } = props;
|
||||
|
||||
return (
|
||||
<ColorPickerPopover
|
||||
{...colorPickerProps}
|
||||
@ -85,3 +85,6 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// This component is to enable SeriecColorPickerPopover usage via series-color-picker-popover directive
|
||||
export const SeriesColorPickerPopoverWithTheme = withTheme(SeriesColorPickerPopover);
|
||||
|
@ -1,22 +1,19 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { withKnobs } from '@storybook/addon-knobs';
|
||||
import SpectrumPalette from './SpectrumPalette';
|
||||
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 SpectrumPaletteStories = storiesOf('UI/ColorPicker/Palettes/SpectrumPalette', module);
|
||||
|
||||
SpectrumPaletteStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
|
||||
SpectrumPaletteStories.addDecorator(withCenteredStory);
|
||||
|
||||
SpectrumPaletteStories.add('default', () => {
|
||||
const selectedTheme = getThemeKnob();
|
||||
|
||||
return (
|
||||
<UseState initialState="red">
|
||||
{(selectedColor, updateSelectedColor) => {
|
||||
return <SpectrumPalette theme={selectedTheme} color={selectedColor} onChange={updateSelectedColor} />;
|
||||
return renderComponentWithTheme(SpectrumPalette, { color: selectedColor, onChange: updateSelectedColor });
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
|
@ -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) => (
|
||||
<SpectrumPalettePointer {...props} theme={theme} />
|
||||
);
|
||||
|
||||
@ -92,7 +92,7 @@ const SpectrumPalette: React.FunctionComponent<SpectrumPaletteProps> = ({ color,
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
<ColorInput color={color} onChange={onChange} style={{ marginTop: '16px' }} />
|
||||
<ColorInput theme={theme} color={color} onChange={onChange} style={{ marginTop: '16px' }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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<SpectrumPalettePointerProps> = ({
|
||||
theme,
|
||||
direction,
|
||||
}) => {
|
||||
const SpectrumPalettePointer: React.FunctionComponent<SpectrumPalettePointerProps> = ({ theme, direction }) => {
|
||||
const styles = {
|
||||
picker: {
|
||||
width: '16px',
|
||||
@ -17,7 +15,14 @@ const SpectrumPalettePointer: React.FunctionComponent<SpectrumPalettePointerProp
|
||||
},
|
||||
};
|
||||
|
||||
const pointerColor = theme === GrafanaTheme.Light ? '#3F444D' : '#8E8E8E';
|
||||
|
||||
const pointerColor = selectThemeVariant(
|
||||
{
|
||||
light: theme.colors.dark3,
|
||||
dark: theme.colors.gray2,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
let pointerStyles: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
|
@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import { Gauge, Props } from './Gauge';
|
||||
import { ValueMapping, MappingType } from '../../types';
|
||||
import { getTheme } from '../../themes';
|
||||
|
||||
jest.mock('jquery', () => ({
|
||||
plot: jest.fn(),
|
||||
@ -24,6 +25,7 @@ const setup = (propOverrides?: object) => {
|
||||
width: 300,
|
||||
value: 25,
|
||||
decimals: 0,
|
||||
theme: getTheme()
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
@ -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<Props> {
|
||||
thresholds: [],
|
||||
unit: 'none',
|
||||
stat: 'avg',
|
||||
theme: GrafanaTheme.Dark,
|
||||
theme: GrafanaThemeType.Dark,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@ -77,19 +77,19 @@ export class Gauge extends PureComponent<Props> {
|
||||
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<Props> {
|
||||
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<Props> {
|
||||
|
||||
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;
|
||||
|
@ -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;
|
||||
|
@ -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<Props, State> {
|
||||
<div className="thresholds-row-input-inner-color">
|
||||
{threshold.color && (
|
||||
<div className="thresholds-row-input-inner-color-colorpicker">
|
||||
<ColorPicker color={threshold.color} onChange={color => this.onChangeThresholdColor(threshold, color)} />
|
||||
<ColorPicker
|
||||
color={threshold.color}
|
||||
onChange={color => this.onChangeThresholdColor(threshold, color)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -188,27 +191,35 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { thresholds } = this.state;
|
||||
const { theme } = this.props;
|
||||
|
||||
return (
|
||||
<PanelOptionsGroup title="Thresholds">
|
||||
<div className="thresholds">
|
||||
{thresholds.map((threshold, index) => {
|
||||
return (
|
||||
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
|
||||
<div className="thresholds-row-add-button" onClick={() => this.onAddThreshold(threshold.index + 1)}>
|
||||
<i className="fa fa-plus" />
|
||||
</div>
|
||||
<div
|
||||
className="thresholds-row-color-indicator"
|
||||
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme) }}
|
||||
/>
|
||||
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
|
||||
<ThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<PanelOptionsGroup title="Thresholds">
|
||||
<div className="thresholds">
|
||||
{thresholds.map((threshold, index) => {
|
||||
return (
|
||||
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
|
||||
<div
|
||||
className="thresholds-row-add-button"
|
||||
onClick={() => this.onAddThreshold(threshold.index + 1)}
|
||||
>
|
||||
<i className="fa fa-plus" />
|
||||
</div>
|
||||
<div
|
||||
className="thresholds-row-color-indicator"
|
||||
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme.type) }}
|
||||
/>
|
||||
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PanelOptionsGroup>
|
||||
</PanelOptionsGroup>
|
||||
);
|
||||
}}
|
||||
</ThemeContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
|
@ -1,3 +1,5 @@
|
||||
export * from './components';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
export * from './themes';
|
||||
export * from './themes/ThemeContext';
|
||||
|
20
packages/grafana-ui/src/themes/ThemeContext.tsx
Normal file
20
packages/grafana-ui/src/themes/ThemeContext.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { GrafanaThemeType, Themeable } from '../types';
|
||||
import { getTheme } from './index';
|
||||
|
||||
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||
type Subtract<T, K> = Omit<T, keyof K>;
|
||||
|
||||
// Use Grafana Dark theme by default
|
||||
export const ThemeContext = React.createContext(getTheme(GrafanaThemeType.Dark));
|
||||
|
||||
export const withTheme = <P extends Themeable>(Component: React.ComponentType<P>) => {
|
||||
const WithTheme: React.FunctionComponent<Subtract<P, Themeable>> = props => {
|
||||
// @ts-ignore
|
||||
return <ThemeContext.Consumer>{theme => <Component {...props} theme={theme} />}</ThemeContext.Consumer>;
|
||||
};
|
||||
|
||||
WithTheme.displayName = `WithTheme(${Component.displayName})`;
|
||||
|
||||
return WithTheme;
|
||||
};
|
69
packages/grafana-ui/src/themes/dark.ts
Normal file
69
packages/grafana-ui/src/themes/dark.ts
Normal file
@ -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;
|
62
packages/grafana-ui/src/themes/default.ts
Normal file
62
packages/grafana-ui/src/themes/default.ts
Normal file
@ -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;
|
14
packages/grafana-ui/src/themes/index.ts
Normal file
14
packages/grafana-ui/src/themes/index.ts
Normal file
@ -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;
|
||||
};
|
||||
};
|
70
packages/grafana-ui/src/themes/light.ts
Normal file
70
packages/grafana-ui/src/themes/light.ts
Normal file
@ -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;
|
52
packages/grafana-ui/src/themes/selectThemeVariant.test.ts
Normal file
52
packages/grafana-ui/src/themes/selectThemeVariant.test.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
9
packages/grafana-ui/src/themes/selectThemeVariant.ts
Normal file
9
packages/grafana-ui/src/themes/selectThemeVariant.ts
Normal file
@ -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];
|
||||
};
|
@ -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';
|
||||
|
129
packages/grafana-ui/src/types/theme.ts
Normal file
129
packages/grafana-ui/src/types/theme.ts
Normal file
@ -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;
|
||||
}
|
@ -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)');
|
||||
});
|
||||
|
@ -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<Hue, ColorDefinition[]>();
|
||||
|
||||
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']);
|
||||
|
@ -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
|
||||
);
|
||||
};
|
41
packages/grafana-ui/src/utils/storybook/withTheme.tsx
Normal file
41
packages/grafana-ui/src/utils/storybook/withTheme.tsx
Normal file
@ -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 (
|
||||
<ThemeContext.Provider value={getTheme(themeKnob)}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
// 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<any>, props: any) => {
|
||||
return (
|
||||
<ThemeContext.Consumer>
|
||||
{theme => {
|
||||
return React.createElement(component, {
|
||||
...props,
|
||||
theme,
|
||||
});
|
||||
}}
|
||||
</ThemeContext.Consumer>
|
||||
);
|
||||
};
|
||||
|
||||
export const withTheme = (story: RenderFunction) => <ThemableStory>{story()}</ThemableStory>;
|
54
pkg/infra/usagestats/service.go
Normal file
54
pkg/infra/usagestats/service.go
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
177
pkg/infra/usagestats/usage_stats.go
Normal file
177
pkg/infra/usagestats/usage_stats.go
Normal file
@ -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"
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||
})
|
||||
})
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ type SystemStats struct {
|
||||
FolderPermissions int64
|
||||
Folders int64
|
||||
ProvisionedDashboards int64
|
||||
Sessions int64
|
||||
}
|
||||
|
||||
type DataSourceStats struct {
|
||||
|
@ -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)
|
||||
|
@ -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"},
|
||||
|
@ -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{
|
||||
|
@ -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',
|
||||
|
@ -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> = props => {
|
||||
@ -22,7 +23,12 @@ export const FadeIn: FC<Props> = props => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition in={props.in} timeout={props.duration} unmountOnExit={props.unmountOnExit || false}>
|
||||
<Transition
|
||||
in={props.in}
|
||||
timeout={props.duration}
|
||||
unmountOnExit={props.unmountOnExit || false}
|
||||
onExited={props.onExited}
|
||||
>
|
||||
{state => (
|
||||
<div
|
||||
style={{
|
||||
|
@ -8,6 +8,16 @@ jest.mock('../../app_events', () => ({
|
||||
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,
|
||||
|
@ -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');
|
||||
};
|
||||
|
@ -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 - ',
|
||||
|
@ -1,4 +1,3 @@
|
||||
import './directives/dash_class';
|
||||
import './directives/dropdown_typeahead';
|
||||
import './directives/autofill_event_fix';
|
||||
import './directives/metric_segment';
|
||||
|
@ -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);
|
@ -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
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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<Settings>(config);
|
||||
export const ConfigConsumer = ConfigContext.Consumer;
|
||||
@ -13,16 +13,20 @@ export const provideConfig = (component: React.ComponentType<any>) => {
|
||||
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 (
|
||||
<ConfigConsumer>
|
||||
{({ bootData }) => {
|
||||
return children(bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark);
|
||||
{config => {
|
||||
return <ThemeContext.Provider value={getCurrentTheme()}>{children}</ThemeContext.Provider>;
|
||||
}}
|
||||
</ConfigConsumer>
|
||||
);
|
||||
};
|
||||
|
||||
export const provideTheme = (component: React.ComponentType<any>) => {
|
||||
return provideConfig((props: any) => <ThemeProvider>{React.createElement(component, { ...props })}</ThemeProvider>);
|
||||
};
|
||||
|
@ -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);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
@ -12,3 +12,4 @@ import './manage-dashboards';
|
||||
import './teams/CreateTeamCtrl';
|
||||
import './profile/all';
|
||||
import './datasources/settings/HttpSettingsCtrl';
|
||||
import './datasources/settings/TlsAuthSettingsCtrl';
|
||||
|
@ -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<Props> {
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.playlistSrv = this.props.$injector.get('playlistSrv');
|
||||
}
|
||||
|
||||
@ -123,26 +123,54 @@ export class DashNav extends PureComponent<Props> {
|
||||
});
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="navbar">
|
||||
<>
|
||||
<div>
|
||||
<a className="navbar-page-btn" onClick={this.onOpenSearch}>
|
||||
<i className="gicon gicon-dashboard" />
|
||||
{!this.isInFullscreenOrSettings && <i className="gicon gicon-dashboard" />}
|
||||
{haveFolder && <span className="navbar-page-btn--folder">{folderTitle} / </span>}
|
||||
{dashboard.title}
|
||||
<i className="fa fa-caret-down" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="navbar__spacer" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
get isInFullscreenOrSettings() {
|
||||
return this.props.editview || this.props.isFullscreen;
|
||||
}
|
||||
|
||||
renderBackButton() {
|
||||
return (
|
||||
<div className="navbar-edit">
|
||||
<Tooltip content="Go back (Esc)">
|
||||
<button className="navbar-edit__back-btn" onClick={this.onClose}>
|
||||
<i className="fa fa-arrow-left" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard, onAddPanel } = this.props;
|
||||
const { canStar, canSave, canShare, showSettings, isStarred } = dashboard.meta;
|
||||
const { snapshot } = dashboard;
|
||||
|
||||
const snapshotUrl = snapshot && snapshot.originalUrl;
|
||||
|
||||
return (
|
||||
<div className="navbar">
|
||||
{this.isInFullscreenOrSettings && this.renderBackButton()}
|
||||
{this.renderDashboardTitleSearchButton()}
|
||||
|
||||
{this.playlistSrv.isPlaying && (
|
||||
<div className="navbar-buttons navbar-buttons--playlist">
|
||||
@ -228,17 +256,6 @@ export class DashNav extends PureComponent<Props> {
|
||||
</div>
|
||||
|
||||
<div className="gf-timepicker-nav" ref={element => (this.timePickerEl = element)} />
|
||||
|
||||
{(isFullscreen || editview) && (
|
||||
<div className="navbar-buttons navbar-buttons--close">
|
||||
<DashNavButton
|
||||
tooltip="Back to dashboard"
|
||||
classSuffix="primary"
|
||||
icon="fa fa-reply"
|
||||
onClick={this.onClose}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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<Props, State> {
|
||||
|
||||
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<Props, State> {
|
||||
refresh: () => panel.refresh(),
|
||||
render: () => panel.render(),
|
||||
events: panel.events,
|
||||
range: getTimeSrv().timeRange(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -97,6 +106,8 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -119,7 +119,12 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
template +=
|
||||
`
|
||||
<div class="panel-options-group" ng-cloak>` +
|
||||
(i > 0 ? `<div class="panel-options-group__header">{{ctrl.editorTabs[${i}].title}}</div>` : '') +
|
||||
(i > 0
|
||||
? `<div class="panel-options-group__header">
|
||||
<span class="panel-options-group__title">{{ctrl.editorTabs[${i}].title}}
|
||||
</span>
|
||||
</div>`
|
||||
: '') +
|
||||
`<div class="panel-options-group__body">
|
||||
<panel-editor-tab editor-tab="ctrl.editorTabs[${i}]" ctrl="ctrl"></panel-editor-tab>
|
||||
</div>
|
||||
@ -144,6 +149,10 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
clearQuery = () => {
|
||||
this.setState({ searchQuery: '' });
|
||||
};
|
||||
|
||||
onPanelOptionsChanged = (options: any) => {
|
||||
this.props.panel.updateOptions(options);
|
||||
this.forceUpdate();
|
||||
@ -228,10 +237,15 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorTabBody heading="Visualization" renderToolbar={this.renderToolbar} toolbarItems={[pluginHelp]}
|
||||
scrollTop={scrollTop} setScrollTop={this.setScrollTop}>
|
||||
<EditorTabBody
|
||||
heading="Visualization"
|
||||
renderToolbar={this.renderToolbar}
|
||||
toolbarItems={[pluginHelp]}
|
||||
scrollTop={scrollTop}
|
||||
setScrollTop={this.setScrollTop}
|
||||
>
|
||||
<>
|
||||
<FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true}>
|
||||
<FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true} onExited={this.clearQuery}>
|
||||
<VizTypePicker
|
||||
current={plugin}
|
||||
onTypeChanged={this.onTypeChanged}
|
||||
@ -247,11 +261,11 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
urlOpenVizPicker: !!state.location.query.openVizPicker
|
||||
urlOpenVizPicker: !!state.location.query.openVizPicker,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateLocation
|
||||
updateLocation,
|
||||
};
|
||||
|
||||
export default connectWithStore(VisualizationTab, mapStateToProps, mapDispatchToProps);
|
||||
|
@ -101,53 +101,5 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="(current.jsonData.tlsAuth || current.jsonData.tlsAuthWithCACert) && current.access=='proxy'">
|
||||
<div class="gf-form">
|
||||
<h6>TLS Auth Details</h6>
|
||||
<info-popover mode="header">TLS Certs are encrypted and stored in the Grafana database.</info-popover>
|
||||
</div>
|
||||
<div ng-if="current.jsonData.tlsAuthWithCACert">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<label class="gf-form-label width-7">CA Cert</label>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsCACert">
|
||||
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsCACert" placeholder="Begins with -----BEGIN CERTIFICATE-----"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsCACert">
|
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsCACert = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="current.jsonData.tlsAuth">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<label class="gf-form-label width-7">Client Cert</label>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientCert">
|
||||
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientCert" placeholder="Begins with -----BEGIN CERTIFICATE-----" required></textarea>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientCert">
|
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientCert = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<label class="gf-form-label width-7">Client Key</label>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientKey">
|
||||
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientKey" placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" required></textarea>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey">
|
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientKey = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<datasource-tls-auth-settings current="current" ng-if="(current.jsonData.tlsAuth || current.jsonData.tlsAuthWithCACert) && current.access=='proxy'">
|
||||
</datasource-tls-auth-settings>
|
@ -0,0 +1,62 @@
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<h6>TLS Auth Details</h6>
|
||||
<info-popover mode="header">TLS Certs are encrypted and stored in the Grafana database.</info-popover>
|
||||
</div>
|
||||
<div ng-if="current.jsonData.tlsAuthWithCACert">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--v-stretch"><label class="gf-form-label width-7">CA Cert</label></div>
|
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsCACert">
|
||||
<textarea
|
||||
rows="7"
|
||||
class="gf-form-input gf-form-textarea"
|
||||
ng-model="current.secureJsonData.tlsCACert"
|
||||
placeholder="Begins with -----BEGIN CERTIFICATE-----"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsCACert">
|
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured" />
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsCACert = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="current.jsonData.tlsAuth">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--v-stretch"><label class="gf-form-label width-7">Client Cert</label></div>
|
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientCert">
|
||||
<textarea
|
||||
rows="7"
|
||||
class="gf-form-input gf-form-textarea"
|
||||
ng-model="current.secureJsonData.tlsClientCert"
|
||||
placeholder="Begins with -----BEGIN CERTIFICATE-----"
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientCert">
|
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured" />
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientCert = false"
|
||||
>reset</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--v-stretch"><label class="gf-form-label width-7">Client Key</label></div>
|
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientKey">
|
||||
<textarea
|
||||
rows="7"
|
||||
class="gf-form-input gf-form-textarea"
|
||||
ng-model="current.secureJsonData.tlsClientKey"
|
||||
placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----"
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey">
|
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured" />
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientKey = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -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',
|
||||
};
|
||||
});
|
@ -205,28 +205,35 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
||||
<div className="explore-container">
|
||||
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
|
||||
<AutoSizer onResize={this.onResize} disableHeight>
|
||||
{({ width }) => (
|
||||
<main className="m-t-2" style={{ width }}>
|
||||
<ErrorBoundary>
|
||||
{showingStartPage && <StartPage onClickExample={this.onClickExample} />}
|
||||
{!showingStartPage && (
|
||||
<>
|
||||
{supportsGraph && !supportsLogs && <GraphContainer width={width} exploreId={exploreId} />}
|
||||
{supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
|
||||
{supportsLogs && (
|
||||
<LogsContainer
|
||||
exploreId={exploreId}
|
||||
onChangeTime={this.onChangeTime}
|
||||
onClickLabel={this.onClickLabel}
|
||||
onStartScanning={this.onStartScanning}
|
||||
onStopScanning={this.onStopScanning}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
)}
|
||||
{({ width }) => {
|
||||
if (width === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="m-t-2" style={{ width }}>
|
||||
<ErrorBoundary>
|
||||
{showingStartPage && <StartPage onClickExample={this.onClickExample} />}
|
||||
{!showingStartPage && (
|
||||
<>
|
||||
{supportsGraph && !supportsLogs && <GraphContainer width={width} exploreId={exploreId} />}
|
||||
{supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
|
||||
{supportsLogs && (
|
||||
<LogsContainer
|
||||
width={width}
|
||||
exploreId={exploreId}
|
||||
onChangeTime={this.onChangeTime}
|
||||
onClickLabel={this.onClickLabel}
|
||||
onStartScanning={this.onStartScanning}
|
||||
onStopScanning={this.onStopScanning}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
)}
|
||||
|
@ -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<Props, State> {
|
||||
range,
|
||||
scanning,
|
||||
scanRange,
|
||||
width,
|
||||
} = this.props;
|
||||
|
||||
if (!data) {
|
||||
@ -216,6 +218,7 @@ export default class Logs extends PureComponent<Props, State> {
|
||||
<Graph
|
||||
data={timeSeries}
|
||||
height={100}
|
||||
width={width}
|
||||
range={range}
|
||||
id={`explore-logs-graph-${exploreId}`}
|
||||
onChangeTime={this.props.onChangeTime}
|
||||
|
@ -27,6 +27,7 @@ interface LogsContainerProps {
|
||||
toggleLogs: typeof toggleLogs;
|
||||
changeDedupStrategy: typeof changeDedupStrategy;
|
||||
dedupStrategy: LogsDedupStrategy;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
@ -51,7 +52,8 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
range,
|
||||
showingLogs,
|
||||
scanning,
|
||||
scanRange
|
||||
scanRange,
|
||||
width,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@ -71,6 +73,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
range={range}
|
||||
scanning={scanning}
|
||||
scanRange={scanRange}
|
||||
width={width}
|
||||
/>
|
||||
</Panel>
|
||||
);
|
||||
|
@ -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<LokiDatasource, LokiQuery>;
|
||||
|
||||
interface State {
|
||||
query: LokiQuery;
|
||||
}
|
||||
// interface State {
|
||||
// query: LokiQuery;
|
||||
// }
|
||||
|
||||
export class LokiQueryEditor extends PureComponent<Props> {
|
||||
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 (
|
||||
<div>
|
||||
<div className="gf-form">
|
||||
<div className="gf-form-label">Loki is currently not supported as dashboard data source. We are working on it!</div>
|
||||
</div>
|
||||
{/*
|
||||
<LokiQueryField
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
@ -78,6 +82,7 @@ export class LokiQueryEditor extends PureComponent<Props> {
|
||||
<div className="gf-form-label gf-form-label--grow" />
|
||||
</div>
|
||||
</div>
|
||||
*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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({}, {});
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
<h3 class="page-heading">MySQL Connection</h3>
|
||||
|
||||
<div class="gf-form-group">
|
||||
@ -22,7 +21,23 @@
|
||||
<input type="password" class="gf-form-input" ng-model='ctrl.current.password' placeholder="password"></input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-checkbox class="gf-form" label="TLS Client Auth" label-class="width-10"
|
||||
checked="ctrl.current.jsonData.tlsAuth" switch-class="max-width-6"></gf-form-checkbox>
|
||||
<gf-form-checkbox class="gf-form" label="With CA Cert" tooltip="Needed for
|
||||
verifing self-signed TLS Certs" checked="ctrl.current.jsonData.tlsAuthWithCACert" label-class="width-11"
|
||||
switch-class="max-width-6"></gf-form-checkbox>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-checkbox class="gf-form" label="Skip TLS Verify" label-class="width-10"
|
||||
checked="ctrl.current.jsonData.tlsSkipVerify" switch-class="max-width-6"></gf-form-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<datasource-tls-auth-settings current="ctrl.current" ng-if="(ctrl.current.jsonData.tlsAuth || ctrl.current.jsonData.tlsAuthWithCACert)">
|
||||
</datasource-tls-auth-settings>
|
||||
|
||||
<b>Connection limits</b>
|
||||
|
||||
@ -84,4 +99,3 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -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<GaugeOptions> {}
|
||||
|
||||
@ -38,7 +37,7 @@ export class GaugePanel extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<ThemeContext.Consumer>
|
||||
{theme => (
|
||||
<Gauge
|
||||
value={value}
|
||||
@ -50,7 +49,7 @@ export class GaugePanel extends PureComponent<Props> {
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
</ThemeProvider>
|
||||
</ThemeContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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<PanelOptionsProps<G
|
||||
|
||||
render() {
|
||||
const { onChange, options } = this.props;
|
||||
return (
|
||||
<ThemeProvider>
|
||||
{(theme) => (
|
||||
<>
|
||||
<PanelOptionsGrid>
|
||||
<ValueOptions onChange={onChange} options={options} />
|
||||
<GaugeOptionsEditor onChange={onChange} options={options} />
|
||||
<ThresholdsEditor
|
||||
onChange={this.onThresholdsChanged}
|
||||
thresholds={options.thresholds}
|
||||
theme={theme}
|
||||
/>
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
|
||||
</>
|
||||
)}
|
||||
</ThemeProvider>
|
||||
return (
|
||||
<>
|
||||
<PanelOptionsGrid>
|
||||
<ValueOptions onChange={onChange} options={options} />
|
||||
<GaugeOptionsEditor onChange={onChange} options={options} />
|
||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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<LegendSeriesIconProps, LegendSeries
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
{theme => {
|
||||
return (
|
||||
<SeriesColorPicker
|
||||
yaxis={this.props.yaxis}
|
||||
color={this.props.color}
|
||||
onChange={this.props.onColorChange}
|
||||
onToggleAxis={this.props.onToggleAxis}
|
||||
theme={theme}
|
||||
enableNamedColors
|
||||
>
|
||||
<span className="graph-legend-icon">
|
||||
<SeriesIcon color={this.props.color} />
|
||||
</span>
|
||||
</SeriesColorPicker>
|
||||
);
|
||||
}}
|
||||
</ThemeProvider>
|
||||
<SeriesColorPicker
|
||||
yaxis={this.props.yaxis}
|
||||
color={this.props.color}
|
||||
onChange={this.props.onColorChange}
|
||||
onToggleAxis={this.props.onToggleAxis}
|
||||
enableNamedColors
|
||||
>
|
||||
<span className="graph-legend-icon">
|
||||
<SeriesIcon color={this.props.color} />
|
||||
</span>
|
||||
</SeriesColorPicker>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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', {
|
||||
|
@ -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))));
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ export interface LocationState {
|
||||
query: UrlQueryMap;
|
||||
routeParams: UrlQueryMap;
|
||||
replace: boolean;
|
||||
lastUpdated: number;
|
||||
}
|
||||
|
||||
export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[];
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user