;
+
+// Use Grafana Dark theme by default
+export const ThemeContext = React.createContext(getTheme(GrafanaThemeType.Dark));
+
+export const withTheme = (Component: React.ComponentType
) => {
+ const WithTheme: React.FunctionComponent> = props => {
+ // @ts-ignore
+ return {theme => } ;
+ };
+
+ WithTheme.displayName = `WithTheme(${Component.displayName})`;
+
+ return WithTheme;
+};
diff --git a/packages/grafana-ui/src/themes/dark.ts b/packages/grafana-ui/src/themes/dark.ts
new file mode 100644
index 00000000000..deae022f63a
--- /dev/null
+++ b/packages/grafana-ui/src/themes/dark.ts
@@ -0,0 +1,69 @@
+import tinycolor from 'tinycolor2';
+import defaultTheme from './default';
+import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
+
+const basicColors = {
+ black: '#000000',
+ white: '#ffffff',
+ dark1: '#141414',
+ dark2: '#1f1f20',
+ dark3: '#262628',
+ dark4: '#333333',
+ dark5: '#444444',
+ gray1: '#555555',
+ gray2: '#8e8e8e',
+ gray3: '#b3b3b3',
+ gray4: '#d8d9da',
+ gray5: '#ececec',
+ gray6: '#f4f5f8',
+ gray7: '#fbfbfb',
+ grayBlue: '#212327',
+ blue: '#33b5e5',
+ blueDark: '#005f81',
+ blueLight: '#00a8e6', // not used in dark theme
+ green: '#299c46',
+ red: '#d44a3a',
+ yellow: '#ecbb13',
+ pink: '#ff4444',
+ purple: '#9933cc',
+ variable: '#32d1df',
+ orange: '#eb7b18',
+};
+
+const darkTheme: GrafanaTheme = {
+ ...defaultTheme,
+ type: GrafanaThemeType.Dark,
+ name: 'Grafana Dark',
+ colors: {
+ ...basicColors,
+ inputBlack: '#09090b',
+ queryRed: '#e24d42',
+ queryGreen: '#74e680',
+ queryPurple: '#fe85fc',
+ queryKeyword: '#66d9ef',
+ queryOrange: 'eb7b18',
+ online: '#10a345',
+ warn: '#f79520',
+ critical: '#ed2e18',
+ bodyBg: '#171819',
+ pageBg: '#161719',
+ bodyColor: basicColors.gray4,
+ textColor: basicColors.gray4,
+ textColorStrong: basicColors.white,
+ textColorWeak: basicColors.gray2,
+ textColorEmphasis: basicColors.gray5,
+ textColorFaint: basicColors.dark5,
+ linkColor: new tinycolor(basicColors.white).darken(11).toString(),
+ linkColorDisabled: new tinycolor(basicColors.white).darken(11).toString(),
+ linkColorHover: basicColors.white,
+ linkColorExternal: basicColors.blue,
+ headingColor: new tinycolor(basicColors.white).darken(11).toString(),
+ },
+ background: {
+ dropdown: basicColors.dark3,
+ scrollbar: '#aeb5df',
+ scrollbar2: '#3a3a3a',
+ },
+};
+
+export default darkTheme;
diff --git a/packages/grafana-ui/src/themes/default.ts b/packages/grafana-ui/src/themes/default.ts
new file mode 100644
index 00000000000..bf318f526e7
--- /dev/null
+++ b/packages/grafana-ui/src/themes/default.ts
@@ -0,0 +1,62 @@
+
+
+const theme = {
+ name: 'Grafana Default',
+ typography: {
+ fontFamily: {
+ sansSerif: "'Roboto', Helvetica, Arial, sans-serif;",
+ serif: "Georgia, 'Times New Roman', Times, serif;",
+ monospace: "Menlo, Monaco, Consolas, 'Courier New', monospace;"
+ },
+ size: {
+ base: '13px',
+ xs: '10px',
+ s: '12px',
+ m: '14px',
+ l: '18px',
+ },
+ heading: {
+ h1: '2rem',
+ h2: '1.75rem',
+ h3: '1.5rem',
+ h4: '1.3rem',
+ h5: '1.2rem',
+ h6: '1rem',
+ },
+ weight: {
+ light: 300,
+ normal: 400,
+ semibold: 500,
+ },
+ lineHeight: {
+ xs: 1,
+ s: 1.1,
+ m: 4/3,
+ l: 1.5
+ }
+ },
+ brakpoints: {
+ xs: '0',
+ s: '544px',
+ m: '768px',
+ l: '992px',
+ xl: '1200px'
+ },
+ spacing: {
+ xs: '0',
+ s: '0.2rem',
+ m: '1rem',
+ l: '1.5rem',
+ xl: '3rem',
+ gutter: '30px',
+ },
+ border: {
+ radius: {
+ xs: '2px',
+ s: '3px',
+ m: '5px',
+ }
+ }
+};
+
+export default theme;
diff --git a/packages/grafana-ui/src/themes/index.ts b/packages/grafana-ui/src/themes/index.ts
new file mode 100644
index 00000000000..c0d9a4f2d32
--- /dev/null
+++ b/packages/grafana-ui/src/themes/index.ts
@@ -0,0 +1,14 @@
+import darkTheme from './dark';
+import lightTheme from './light';
+import { GrafanaTheme } from '../types/theme';
+
+let themeMock: ((name?: string) => GrafanaTheme) | null;
+
+export let getTheme = (name?: string) => (themeMock && themeMock(name)) || (name === 'light' ? lightTheme : darkTheme);
+
+export const mockTheme = (mock: (name: string) => GrafanaTheme) => {
+ themeMock = mock;
+ return () => {
+ themeMock = null;
+ };
+};
diff --git a/packages/grafana-ui/src/themes/light.ts b/packages/grafana-ui/src/themes/light.ts
new file mode 100644
index 00000000000..fd1f1d05b95
--- /dev/null
+++ b/packages/grafana-ui/src/themes/light.ts
@@ -0,0 +1,70 @@
+import tinycolor from 'tinycolor2';
+import defaultTheme from './default';
+import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
+
+const basicColors = {
+ black: '#000000',
+ white: '#ffffff',
+ dark1: '#13161d',
+ dark2: '#1e2028',
+ dark3: '#303133',
+ dark4: '#35373f',
+ dark5: '#41444b',
+ gray1: '#52545c',
+ gray2: '#767980',
+ gray3: '#acb6bf',
+ gray4: '#c7d0d9',
+ gray5: '#dde4ed',
+ gray6: '#e9edf2',
+ gray7: '#f7f8fa',
+ grayBlue: '#212327', // not used in light theme
+ blue: '#0083b3',
+ blueDark: '#005f81',
+ blueLight: '#00a8e6',
+ green: '#3aa655',
+ red: '#d44939',
+ yellow: '#ff851b',
+ pink: '#e671b8',
+ purple: '#9954bb',
+ variable: '#0083b3',
+ orange: '#ff7941',
+};
+
+const lightTheme: GrafanaTheme = {
+ ...defaultTheme,
+ type: GrafanaThemeType.Light,
+ name: 'Grafana Light',
+ colors: {
+ ...basicColors,
+ variable: basicColors.blue,
+ inputBlack: '#09090b',
+ queryRed: basicColors.red,
+ queryGreen: basicColors.green,
+ queryPurple: basicColors.purple,
+ queryKeyword: basicColors.blue,
+ queryOrange: basicColors.orange,
+ online: '#01a64f',
+ warn: '#f79520',
+ critical: '#ec2128',
+ bodyBg: basicColors.gray7,
+ pageBg: basicColors.gray7,
+ bodyColor: basicColors.gray1,
+ textColor: basicColors.gray1,
+ textColorStrong: basicColors.dark2,
+ textColorWeak: basicColors.gray2,
+ textColorEmphasis: basicColors.gray5,
+ textColorFaint: basicColors.dark4,
+ linkColor: basicColors.gray1,
+ linkColorDisabled: new tinycolor(basicColors.gray1).lighten(30).toString(),
+ linkColorHover: new tinycolor(basicColors.gray1).darken(20).toString(),
+ linkColorExternal: basicColors.blueLight,
+ headingColor: basicColors.gray1,
+ },
+ background: {
+ dropdown: basicColors.white,
+ scrollbar: basicColors.gray5,
+ scrollbar2: basicColors.gray5,
+ },
+};
+
+export default lightTheme;
diff --git a/packages/grafana-ui/src/themes/selectThemeVariant.test.ts b/packages/grafana-ui/src/themes/selectThemeVariant.test.ts
new file mode 100644
index 00000000000..66cb02a2372
--- /dev/null
+++ b/packages/grafana-ui/src/themes/selectThemeVariant.test.ts
@@ -0,0 +1,52 @@
+import { GrafanaThemeType } from '../types/theme';
+import { selectThemeVariant } from './selectThemeVariant';
+import { mockTheme } from './index';
+
+const lightThemeMock = {
+ color: {
+ red: '#ff0000',
+ green: '#00ff00',
+ },
+};
+
+const darkThemeMock = {
+ color: {
+ red: '#ff0000',
+ green: '#00ff00',
+ },
+};
+
+describe('Theme variable variant selector', () => {
+ // @ts-ignore
+ const restoreTheme = mockTheme(name => (name === GrafanaThemeType.Light ? lightThemeMock : darkThemeMock));
+
+ afterAll(() => {
+ restoreTheme();
+ });
+ it('return correct variable value for given theme', () => {
+ const theme = lightThemeMock;
+
+ const selectedValue = selectThemeVariant(
+ {
+ dark: theme.color.red,
+ light: theme.color.green,
+ },
+ GrafanaThemeType.Light
+ );
+
+ expect(selectedValue).toBe(lightThemeMock.color.green);
+ });
+
+ it('return dark theme variant if no theme given', () => {
+ const theme = lightThemeMock;
+
+ const selectedValue = selectThemeVariant(
+ {
+ dark: theme.color.red,
+ light: theme.color.green,
+ }
+ );
+
+ expect(selectedValue).toBe(lightThemeMock.color.red);
+ });
+});
diff --git a/packages/grafana-ui/src/themes/selectThemeVariant.ts b/packages/grafana-ui/src/themes/selectThemeVariant.ts
new file mode 100644
index 00000000000..e7e8e780222
--- /dev/null
+++ b/packages/grafana-ui/src/themes/selectThemeVariant.ts
@@ -0,0 +1,9 @@
+import { GrafanaThemeType } from '../types/theme';
+
+type VariantDescriptor = {
+ [key in GrafanaThemeType]: string | number;
+};
+
+export const selectThemeVariant = (variants: VariantDescriptor, currentTheme?: GrafanaThemeType) => {
+ return variants[currentTheme || GrafanaThemeType.Dark];
+};
diff --git a/packages/grafana-ui/src/types/index.ts b/packages/grafana-ui/src/types/index.ts
index e23b5e63af8..81bdf741f30 100644
--- a/packages/grafana-ui/src/types/index.ts
+++ b/packages/grafana-ui/src/types/index.ts
@@ -1,14 +1,7 @@
+
export * from './data';
export * from './time';
export * from './panel';
export * from './plugin';
export * from './datasource';
-
-export enum GrafanaTheme {
- Light = 'light',
- Dark = 'dark',
-}
-
-export interface Themeable {
- theme?: GrafanaTheme;
-}
+export * from './theme';
diff --git a/packages/grafana-ui/src/types/theme.ts b/packages/grafana-ui/src/types/theme.ts
new file mode 100644
index 00000000000..8a79658b423
--- /dev/null
+++ b/packages/grafana-ui/src/types/theme.ts
@@ -0,0 +1,129 @@
+export enum GrafanaThemeType {
+ Light = 'light',
+ Dark = 'dark',
+}
+
+export interface GrafanaTheme {
+ type: GrafanaThemeType;
+ name: string;
+ // TODO: not sure if should be a part of theme
+ brakpoints: {
+ xs: string;
+ s: string;
+ m: string;
+ l: string;
+ xl: string;
+ };
+ typography: {
+ fontFamily: {
+ sansSerif: string;
+ serif: string;
+ monospace: string;
+ };
+ size: {
+ base: string;
+ xs: string;
+ s: string;
+ m: string;
+ l: string;
+ };
+ weight: {
+ light: number;
+ normal: number;
+ semibold: number;
+ };
+ lineHeight: {
+ xs: number; //1
+ s: number; //1.1
+ m: number; // 4/3
+ l: number; // 1.5
+ };
+ // TODO: Refactor to use size instead of custom defs
+ heading: {
+ h1: string;
+ h2: string;
+ h3: string;
+ h4: string;
+ h5: string;
+ h6: string;
+ };
+ };
+ spacing: {
+ xs: string;
+ s: string;
+ m: string;
+ l: string;
+ gutter: string;
+ };
+ border: {
+ radius: {
+ xs: string;
+ s: string;
+ m: string;
+ };
+ };
+ background: {
+ dropdown: string;
+ scrollbar: string;
+ scrollbar2: string;
+ };
+ colors: {
+ black: string;
+ white: string;
+ dark1: string;
+ dark2: string;
+ dark3: string;
+ dark4: string;
+ dark5: string;
+ gray1: string;
+ gray2: string;
+ gray3: string;
+ gray4: string;
+ gray5: string;
+ gray6: string;
+ gray7: string;
+ grayBlue: string;
+ inputBlack: string;
+
+ // Accent colors
+ blue: string;
+ blueLight: string;
+ blueDark: string;
+ green: string;
+ red: string;
+ yellow: string;
+ pink: string;
+ purple: string;
+ variable: string;
+ orange: string;
+ queryRed: string;
+ queryGreen: string;
+ queryPurple: string;
+ queryKeyword: string;
+ queryOrange: string;
+
+ // Status colors
+ online: string;
+ warn: string;
+ critical: string;
+
+ // TODO: move to background section
+ bodyBg: string;
+ pageBg: string;
+ bodyColor: string;
+ textColor: string;
+ textColorStrong: string;
+ textColorWeak: string;
+ textColorFaint: string;
+ textColorEmphasis: string;
+ linkColor: string;
+ linkColorDisabled: string;
+ linkColorHover: string;
+ linkColorExternal: string;
+ headingColor: string;
+ };
+}
+
+export interface Themeable {
+ theme: GrafanaTheme;
+}
diff --git a/packages/grafana-ui/src/utils/namedColorsPalette.test.ts b/packages/grafana-ui/src/utils/namedColorsPalette.test.ts
index c6a1aaf0dd0..aa57b46636c 100644
--- a/packages/grafana-ui/src/utils/namedColorsPalette.test.ts
+++ b/packages/grafana-ui/src/utils/namedColorsPalette.test.ts
@@ -5,20 +5,20 @@ import {
getColorFromHexRgbOrName,
getColorDefinitionByName,
} from './namedColorsPalette';
-import { GrafanaTheme } from '../types/index';
+import { GrafanaThemeType } from '../types/index';
describe('colors', () => {
const SemiDarkBlue = getColorDefinitionByName('semi-dark-blue');
describe('getColorDefinition', () => {
it('returns undefined for unknown hex', () => {
- expect(getColorDefinition('#ff0000', GrafanaTheme.Light)).toBeUndefined();
- expect(getColorDefinition('#ff0000', GrafanaTheme.Dark)).toBeUndefined();
+ expect(getColorDefinition('#ff0000', GrafanaThemeType.Light)).toBeUndefined();
+ expect(getColorDefinition('#ff0000', GrafanaThemeType.Dark)).toBeUndefined();
});
it('returns definition for known hex', () => {
- expect(getColorDefinition(SemiDarkBlue.variants.light, GrafanaTheme.Light)).toEqual(SemiDarkBlue);
- expect(getColorDefinition(SemiDarkBlue.variants.dark, GrafanaTheme.Dark)).toEqual(SemiDarkBlue);
+ expect(getColorDefinition(SemiDarkBlue.variants.light, GrafanaThemeType.Light)).toEqual(SemiDarkBlue);
+ expect(getColorDefinition(SemiDarkBlue.variants.dark, GrafanaThemeType.Dark)).toEqual(SemiDarkBlue);
});
});
@@ -28,8 +28,8 @@ describe('colors', () => {
});
it('returns name for known hex', () => {
- expect(getColorName(SemiDarkBlue.variants.light, GrafanaTheme.Light)).toEqual(SemiDarkBlue.name);
- expect(getColorName(SemiDarkBlue.variants.dark, GrafanaTheme.Dark)).toEqual(SemiDarkBlue.name);
+ expect(getColorName(SemiDarkBlue.variants.light, GrafanaThemeType.Light)).toEqual(SemiDarkBlue.name);
+ expect(getColorName(SemiDarkBlue.variants.dark, GrafanaThemeType.Dark)).toEqual(SemiDarkBlue.name);
});
});
@@ -53,12 +53,14 @@ describe('colors', () => {
});
it("returns correct variant's hex for known color if theme specified", () => {
- expect(getColorFromHexRgbOrName(SemiDarkBlue.name, GrafanaTheme.Light)).toBe(SemiDarkBlue.variants.light);
+ expect(getColorFromHexRgbOrName(SemiDarkBlue.name, GrafanaThemeType.Light)).toBe(SemiDarkBlue.variants.light);
});
it('returns color if specified as hex or rgb/a', () => {
expect(getColorFromHexRgbOrName('ff0000')).toBe('ff0000');
expect(getColorFromHexRgbOrName('#ff0000')).toBe('#ff0000');
+ expect(getColorFromHexRgbOrName('#FF0000')).toBe('#FF0000');
+ expect(getColorFromHexRgbOrName('#CCC')).toBe('#CCC');
expect(getColorFromHexRgbOrName('rgb(0,0,0)')).toBe('rgb(0,0,0)');
expect(getColorFromHexRgbOrName('rgba(0,0,0,1)')).toBe('rgba(0,0,0,1)');
});
diff --git a/packages/grafana-ui/src/utils/namedColorsPalette.ts b/packages/grafana-ui/src/utils/namedColorsPalette.ts
index 5312b27ad26..ee5741e794e 100644
--- a/packages/grafana-ui/src/utils/namedColorsPalette.ts
+++ b/packages/grafana-ui/src/utils/namedColorsPalette.ts
@@ -1,5 +1,5 @@
import { flatten } from 'lodash';
-import { GrafanaTheme } from '../types';
+import { GrafanaThemeType } from '../types';
type Hue = 'green' | 'yellow' | 'red' | 'blue' | 'orange' | 'purple';
@@ -68,16 +68,16 @@ export const getColorDefinitionByName = (name: Color): ColorDefinition => {
return flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.name === name)[0];
};
-export const getColorDefinition = (hex: string, theme: GrafanaTheme): ColorDefinition | undefined => {
+export const getColorDefinition = (hex: string, theme: GrafanaThemeType): ColorDefinition | undefined => {
return flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.variants[theme] === hex)[0];
};
const isHex = (color: string) => {
- const hexRegex = /^((0x){0,1}|#{0,1})([0-9A-F]{8}|[0-9A-F]{6})$/gi;
+ const hexRegex = /^((0x){0,1}|#{0,1})([0-9A-F]{8}|[0-9A-F]{6}|[0-9A-F]{3})$/gi;
return hexRegex.test(color);
};
-export const getColorName = (color?: string, theme?: GrafanaTheme): Color | undefined => {
+export const getColorName = (color?: string, theme?: GrafanaThemeType): Color | undefined => {
if (!color) {
return undefined;
}
@@ -86,7 +86,7 @@ export const getColorName = (color?: string, theme?: GrafanaTheme): Color | unde
return undefined;
}
if (isHex(color)) {
- const definition = getColorDefinition(color, theme || GrafanaTheme.Dark);
+ const definition = getColorDefinition(color, theme || GrafanaThemeType.Dark);
return definition ? definition.name : undefined;
}
@@ -98,7 +98,7 @@ export const getColorByName = (colorName: string) => {
return definition.length > 0 ? definition[0] : undefined;
};
-export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaTheme): string => {
+export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaThemeType): string => {
if (color.indexOf('rgb') > -1 || isHex(color)) {
return color;
}
@@ -112,14 +112,14 @@ export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaTheme): s
return theme ? colorDefinition.variants[theme] : colorDefinition.variants.dark;
};
-export const getColorForTheme = (color: ColorDefinition, theme?: GrafanaTheme) => {
+export const getColorForTheme = (color: ColorDefinition, theme?: GrafanaThemeType) => {
return theme ? color.variants[theme] : color.variants.dark;
};
const buildNamedColorsPalette = () => {
const palette = new Map();
- const BasicGreen = buildColorDefinition('green', 'green', ['#56A64B', '#73BF69'], true);
+ const BasicGreen = buildColorDefinition('green', 'green', ['#56A64B', '#73BF69'], true);
const DarkGreen = buildColorDefinition('green', 'dark-green', ['#19730E', '#37872D']);
const SemiDarkGreen = buildColorDefinition('green', 'semi-dark-green', ['#37872D', '#56A64B']);
const LightGreen = buildColorDefinition('green', 'light-green', ['#73BF69', '#96D98D']);
diff --git a/packages/grafana-ui/src/utils/storybook/themeKnob.ts b/packages/grafana-ui/src/utils/storybook/themeKnob.ts
deleted file mode 100644
index a3733462bea..00000000000
--- a/packages/grafana-ui/src/utils/storybook/themeKnob.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { select } from '@storybook/addon-knobs';
-import { GrafanaTheme } from '../../types';
-
-export const getThemeKnob = (defaultTheme: GrafanaTheme = GrafanaTheme.Dark) => {
- return select(
- 'Theme',
- {
- Default: defaultTheme,
- Light: GrafanaTheme.Light,
- Dark: GrafanaTheme.Dark,
- },
- defaultTheme
- );
-};
diff --git a/packages/grafana-ui/src/utils/storybook/withTheme.tsx b/packages/grafana-ui/src/utils/storybook/withTheme.tsx
new file mode 100644
index 00000000000..5417af1de05
--- /dev/null
+++ b/packages/grafana-ui/src/utils/storybook/withTheme.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { RenderFunction } from '@storybook/react';
+import { ThemeContext } from '../../themes/ThemeContext';
+import { select } from '@storybook/addon-knobs';
+import { getTheme } from '../../themes';
+import { GrafanaThemeType } from '../../types';
+
+const ThemableStory: React.FunctionComponent<{}> = ({ children }) => {
+ const themeKnob = select(
+ 'Theme',
+ {
+ Light: GrafanaThemeType.Light,
+ Dark: GrafanaThemeType.Dark,
+ },
+ GrafanaThemeType.Dark
+ );
+
+ return (
+
+ {children}
+
+
+ );
+};
+
+// Temporary solution. When we update to Storybook V5 we will be able to pass data from decorator to story
+// https://github.com/storybooks/storybook/issues/340#issuecomment-456013702
+export const renderComponentWithTheme = (component: React.ComponentType, props: any) => {
+ return (
+
+ {theme => {
+ return React.createElement(component, {
+ ...props,
+ theme,
+ });
+ }}
+
+ );
+};
+
+export const withTheme = (story: RenderFunction) => {story()} ;
diff --git a/pkg/infra/usagestats/service.go b/pkg/infra/usagestats/service.go
new file mode 100644
index 00000000000..c2bf0d06349
--- /dev/null
+++ b/pkg/infra/usagestats/service.go
@@ -0,0 +1,54 @@
+package usagestats
+
+import (
+ "context"
+ "time"
+
+ "github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/services/sqlstore"
+ "github.com/grafana/grafana/pkg/social"
+
+ "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/registry"
+ "github.com/grafana/grafana/pkg/setting"
+)
+
+var metricsLogger log.Logger = log.New("metrics")
+
+func init() {
+ registry.RegisterService(&UsageStatsService{})
+}
+
+type UsageStatsService struct {
+ Cfg *setting.Cfg `inject:""`
+ Bus bus.Bus `inject:""`
+ SQLStore *sqlstore.SqlStore `inject:""`
+
+ oauthProviders map[string]bool
+}
+
+func (uss *UsageStatsService) Init() error {
+
+ uss.oauthProviders = social.GetOAuthProviders(uss.Cfg)
+ return nil
+}
+
+func (uss *UsageStatsService) Run(ctx context.Context) error {
+ uss.updateTotalStats()
+
+ onceEveryDayTick := time.NewTicker(time.Hour * 24)
+ everyMinuteTicker := time.NewTicker(time.Minute)
+ defer onceEveryDayTick.Stop()
+ defer everyMinuteTicker.Stop()
+
+ for {
+ select {
+ case <-onceEveryDayTick.C:
+ uss.sendUsageStats(uss.oauthProviders)
+ case <-everyMinuteTicker.C:
+ uss.updateTotalStats()
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+}
diff --git a/pkg/infra/usagestats/usage_stats.go b/pkg/infra/usagestats/usage_stats.go
new file mode 100644
index 00000000000..b54de124335
--- /dev/null
+++ b/pkg/infra/usagestats/usage_stats.go
@@ -0,0 +1,177 @@
+package usagestats
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "runtime"
+ "strings"
+ "time"
+
+ "github.com/grafana/grafana/pkg/metrics"
+ "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/plugins"
+ "github.com/grafana/grafana/pkg/setting"
+)
+
+var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
+
+func (uss *UsageStatsService) sendUsageStats(oauthProviders map[string]bool) {
+ if !setting.ReportingEnabled {
+ return
+ }
+
+ metricsLogger.Debug(fmt.Sprintf("Sending anonymous usage stats to %s", usageStatsURL))
+
+ version := strings.Replace(setting.BuildVersion, ".", "_", -1)
+
+ metrics := map[string]interface{}{}
+ report := map[string]interface{}{
+ "version": version,
+ "metrics": metrics,
+ "os": runtime.GOOS,
+ "arch": runtime.GOARCH,
+ "edition": getEdition(),
+ "packaging": setting.Packaging,
+ }
+
+ statsQuery := models.GetSystemStatsQuery{}
+ if err := uss.Bus.Dispatch(&statsQuery); err != nil {
+ metricsLogger.Error("Failed to get system stats", "error", err)
+ return
+ }
+
+ metrics["stats.dashboards.count"] = statsQuery.Result.Dashboards
+ metrics["stats.users.count"] = statsQuery.Result.Users
+ metrics["stats.orgs.count"] = statsQuery.Result.Orgs
+ metrics["stats.playlist.count"] = statsQuery.Result.Playlists
+ metrics["stats.plugins.apps.count"] = len(plugins.Apps)
+ metrics["stats.plugins.panels.count"] = len(plugins.Panels)
+ metrics["stats.plugins.datasources.count"] = len(plugins.DataSources)
+ metrics["stats.alerts.count"] = statsQuery.Result.Alerts
+ metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers
+ metrics["stats.datasources.count"] = statsQuery.Result.Datasources
+ metrics["stats.stars.count"] = statsQuery.Result.Stars
+ metrics["stats.folders.count"] = statsQuery.Result.Folders
+ metrics["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions
+ metrics["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions
+ metrics["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards
+ metrics["stats.snapshots.count"] = statsQuery.Result.Snapshots
+ metrics["stats.teams.count"] = statsQuery.Result.Teams
+ metrics["stats.total_sessions.count"] = statsQuery.Result.Sessions
+
+ userCount := statsQuery.Result.Users
+ avgSessionsPerUser := statsQuery.Result.Sessions
+ if userCount != 0 {
+ avgSessionsPerUser = avgSessionsPerUser / userCount
+ }
+
+ metrics["stats.avg_sessions_per_user.count"] = avgSessionsPerUser
+
+ dsStats := models.GetDataSourceStatsQuery{}
+ if err := uss.Bus.Dispatch(&dsStats); err != nil {
+ metricsLogger.Error("Failed to get datasource stats", "error", err)
+ return
+ }
+
+ // send counters for each data source
+ // but ignore any custom data sources
+ // as sending that name could be sensitive information
+ dsOtherCount := 0
+ for _, dsStat := range dsStats.Result {
+ if models.IsKnownDataSourcePlugin(dsStat.Type) {
+ metrics["stats.ds."+dsStat.Type+".count"] = dsStat.Count
+ } else {
+ dsOtherCount += dsStat.Count
+ }
+ }
+ metrics["stats.ds.other.count"] = dsOtherCount
+
+ metrics["stats.packaging."+setting.Packaging+".count"] = 1
+
+ dsAccessStats := models.GetDataSourceAccessStatsQuery{}
+ if err := uss.Bus.Dispatch(&dsAccessStats); err != nil {
+ metricsLogger.Error("Failed to get datasource access stats", "error", err)
+ return
+ }
+
+ // send access counters for each data source
+ // but ignore any custom data sources
+ // as sending that name could be sensitive information
+ dsAccessOtherCount := make(map[string]int64)
+ for _, dsAccessStat := range dsAccessStats.Result {
+ if dsAccessStat.Access == "" {
+ continue
+ }
+
+ access := strings.ToLower(dsAccessStat.Access)
+
+ if models.IsKnownDataSourcePlugin(dsAccessStat.Type) {
+ metrics["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count
+ } else {
+ old := dsAccessOtherCount[access]
+ dsAccessOtherCount[access] = old + dsAccessStat.Count
+ }
+ }
+
+ for access, count := range dsAccessOtherCount {
+ metrics["stats.ds_access.other."+access+".count"] = count
+ }
+
+ anStats := models.GetAlertNotifierUsageStatsQuery{}
+ if err := uss.Bus.Dispatch(&anStats); err != nil {
+ metricsLogger.Error("Failed to get alert notification stats", "error", err)
+ return
+ }
+
+ for _, stats := range anStats.Result {
+ metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count
+ }
+
+ authTypes := map[string]bool{}
+ authTypes["anonymous"] = setting.AnonymousEnabled
+ authTypes["basic_auth"] = setting.BasicAuthEnabled
+ authTypes["ldap"] = setting.LdapEnabled
+ authTypes["auth_proxy"] = setting.AuthProxyEnabled
+
+ for provider, enabled := range oauthProviders {
+ authTypes["oauth_"+provider] = enabled
+ }
+
+ for authType, enabled := range authTypes {
+ enabledValue := 0
+ if enabled {
+ enabledValue = 1
+ }
+ metrics["stats.auth_enabled."+authType+".count"] = enabledValue
+ }
+
+ out, _ := json.MarshalIndent(report, "", " ")
+ data := bytes.NewBuffer(out)
+
+ client := http.Client{Timeout: 5 * time.Second}
+ go client.Post(usageStatsURL, "application/json", data)
+}
+
+func (uss *UsageStatsService) updateTotalStats() {
+ statsQuery := models.GetSystemStatsQuery{}
+ if err := uss.Bus.Dispatch(&statsQuery); err != nil {
+ metricsLogger.Error("Failed to get system stats", "error", err)
+ return
+ }
+
+ metrics.M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
+ metrics.M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
+ metrics.M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers))
+ metrics.M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
+ metrics.M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
+}
+
+func getEdition() string {
+ if setting.IsEnterprise {
+ return "enterprise"
+ } else {
+ return "oss"
+ }
+}
diff --git a/pkg/metrics/metrics_test.go b/pkg/infra/usagestats/usage_stats_test.go
similarity index 91%
rename from pkg/metrics/metrics_test.go
rename to pkg/infra/usagestats/usage_stats_test.go
index c27d6f64b8c..d343ed52b93 100644
--- a/pkg/metrics/metrics_test.go
+++ b/pkg/infra/usagestats/usage_stats_test.go
@@ -1,4 +1,4 @@
-package metrics
+package usagestats
import (
"bytes"
@@ -15,14 +15,21 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
+ "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
func TestMetrics(t *testing.T) {
Convey("Test send usage stats", t, func() {
+ uss := &UsageStatsService{
+ Bus: bus.New(),
+ SQLStore: sqlstore.InitTestDB(t),
+ }
+
var getSystemStatsQuery *models.GetSystemStatsQuery
- bus.AddHandler("test", func(query *models.GetSystemStatsQuery) error {
+ uss.Bus.AddHandler(func(query *models.GetSystemStatsQuery) error {
+
query.Result = &models.SystemStats{
Dashboards: 1,
Datasources: 2,
@@ -38,13 +45,14 @@ func TestMetrics(t *testing.T) {
ProvisionedDashboards: 12,
Snapshots: 13,
Teams: 14,
+ Sessions: 15,
}
getSystemStatsQuery = query
return nil
})
var getDataSourceStatsQuery *models.GetDataSourceStatsQuery
- bus.AddHandler("test", func(query *models.GetDataSourceStatsQuery) error {
+ uss.Bus.AddHandler(func(query *models.GetDataSourceStatsQuery) error {
query.Result = []*models.DataSourceStats{
{
Type: models.DS_ES,
@@ -68,7 +76,7 @@ func TestMetrics(t *testing.T) {
})
var getDataSourceAccessStatsQuery *models.GetDataSourceAccessStatsQuery
- bus.AddHandler("test", func(query *models.GetDataSourceAccessStatsQuery) error {
+ uss.Bus.AddHandler(func(query *models.GetDataSourceAccessStatsQuery) error {
query.Result = []*models.DataSourceAccessStats{
{
Type: models.DS_ES,
@@ -116,7 +124,7 @@ func TestMetrics(t *testing.T) {
})
var getAlertNotifierUsageStatsQuery *models.GetAlertNotifierUsageStatsQuery
- bus.AddHandler("test", func(query *models.GetAlertNotifierUsageStatsQuery) error {
+ uss.Bus.AddHandler(func(query *models.GetAlertNotifierUsageStatsQuery) error {
query.Result = []*models.NotifierUsageStats{
{
Type: "slack",
@@ -155,11 +163,11 @@ func TestMetrics(t *testing.T) {
"grafana_com": true,
}
- sendUsageStats(oauthProviders)
+ uss.sendUsageStats(oauthProviders)
Convey("Given reporting not enabled and sending usage stats", func() {
setting.ReportingEnabled = false
- sendUsageStats(oauthProviders)
+ uss.sendUsageStats(oauthProviders)
Convey("Should not gather stats or call http endpoint", func() {
So(getSystemStatsQuery, ShouldBeNil)
@@ -179,7 +187,7 @@ func TestMetrics(t *testing.T) {
setting.Packaging = "deb"
wg.Add(1)
- sendUsageStats(oauthProviders)
+ uss.sendUsageStats(oauthProviders)
Convey("Should gather stats and call http endpoint", func() {
if waitTimeout(&wg, 2*time.Second) {
@@ -221,6 +229,8 @@ func TestMetrics(t *testing.T) {
So(metrics.Get("stats.provisioned_dashboards.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.ProvisionedDashboards)
So(metrics.Get("stats.snapshots.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.Snapshots)
So(metrics.Get("stats.teams.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.Teams)
+ So(metrics.Get("stats.total_sessions.count").MustInt64(), ShouldEqual, 15)
+ So(metrics.Get("stats.avg_sessions_per_user.count").MustInt64(), ShouldEqual, 5)
So(metrics.Get("stats.ds."+models.DS_ES+".count").MustInt(), ShouldEqual, 9)
So(metrics.Get("stats.ds."+models.DS_PROMETHEUS+".count").MustInt(), ShouldEqual, 10)
@@ -246,6 +256,7 @@ func TestMetrics(t *testing.T) {
So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1)
So(metrics.Get("stats.packaging.deb.count").MustInt(), ShouldEqual, 1)
+
})
})
diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go
index 402160ef5e3..c15cb865bd3 100644
--- a/pkg/login/ldap.go
+++ b/pkg/login/ldap.go
@@ -273,23 +273,35 @@ func (a *ldapAuther) initialBind(username, userPassword string) error {
return nil
}
+func appendIfNotEmpty(slice []string, values ...string) []string {
+ for _, v := range values {
+ if v != "" {
+ slice = append(slice, v)
+ }
+ }
+ return slice
+}
+
func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
var searchResult *ldap.SearchResult
var err error
for _, searchBase := range a.server.SearchBaseDNs {
+ attributes := make([]string, 0)
+ inputs := a.server.Attr
+ attributes = appendIfNotEmpty(attributes,
+ inputs.Username,
+ inputs.Surname,
+ inputs.Email,
+ inputs.Name,
+ inputs.MemberOf)
+
searchReq := ldap.SearchRequest{
BaseDN: searchBase,
Scope: ldap.ScopeWholeSubtree,
DerefAliases: ldap.NeverDerefAliases,
- Attributes: []string{
- a.server.Attr.Username,
- a.server.Attr.Surname,
- a.server.Attr.Email,
- a.server.Attr.Name,
- a.server.Attr.MemberOf,
- },
- Filter: strings.Replace(a.server.SearchFilter, "%s", ldap.EscapeFilter(username), -1),
+ Attributes: attributes,
+ Filter: strings.Replace(a.server.SearchFilter, "%s", ldap.EscapeFilter(username), -1),
}
a.log.Debug("Ldap Search For User Request", "info", spew.Sdump(searchReq))
diff --git a/pkg/login/ldap_test.go b/pkg/login/ldap_test.go
index f1cf44dc554..c02fa02e030 100644
--- a/pkg/login/ldap_test.go
+++ b/pkg/login/ldap_test.go
@@ -6,6 +6,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/ldap.v3"
@@ -322,11 +323,51 @@ func TestLdapAuther(t *testing.T) {
So(sc.addOrgUserCmd.Role, ShouldEqual, "Admin")
})
})
+
+ Convey("When searching for a user and not all five attributes are mapped", t, func() {
+ mockLdapConnection := &mockLdapConn{}
+ entry := ldap.Entry{
+ DN: "dn", Attributes: []*ldap.EntryAttribute{
+ {Name: "username", Values: []string{"roelgerrits"}},
+ {Name: "surname", Values: []string{"Gerrits"}},
+ {Name: "email", Values: []string{"roel@test.com"}},
+ {Name: "name", Values: []string{"Roel"}},
+ {Name: "memberof", Values: []string{"admins"}},
+ }}
+ result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
+ mockLdapConnection.setSearchResult(&result)
+
+ // Set up attribute map without surname and email
+ ldapAuther := &ldapAuther{
+ server: &LdapServerConf{
+ Attr: LdapAttributeMap{
+ Username: "username",
+ Name: "name",
+ MemberOf: "memberof",
+ },
+ SearchBaseDNs: []string{"BaseDNHere"},
+ },
+ conn: mockLdapConnection,
+ log: log.New("test-logger"),
+ }
+
+ searchResult, err := ldapAuther.searchForUser("roelgerrits")
+
+ So(err, ShouldBeNil)
+ So(searchResult, ShouldNotBeNil)
+
+ // User should be searched in ldap
+ So(mockLdapConnection.searchCalled, ShouldBeTrue)
+
+ // No empty attributes should be added to the search request
+ So(len(mockLdapConnection.searchAttributes), ShouldEqual, 3)
+ })
}
type mockLdapConn struct {
- result *ldap.SearchResult
- searchCalled bool
+ result *ldap.SearchResult
+ searchCalled bool
+ searchAttributes []string
}
func (c *mockLdapConn) Bind(username, password string) error {
@@ -339,8 +380,9 @@ func (c *mockLdapConn) setSearchResult(result *ldap.SearchResult) {
c.result = result
}
-func (c *mockLdapConn) Search(*ldap.SearchRequest) (*ldap.SearchResult, error) {
+func (c *mockLdapConn) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
c.searchCalled = true
+ c.searchAttributes = sr.Attributes
return c.result, nil
}
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index 326514a9687..718a63ee768 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -1,17 +1,8 @@
package metrics
import (
- "bytes"
- "encoding/json"
- "net/http"
"runtime"
- "strings"
- "time"
- "github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/models"
- "github.com/grafana/grafana/pkg/plugins"
- "github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
)
@@ -68,23 +59,6 @@ var (
grafanaBuildVersion *prometheus.GaugeVec
)
-func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec {
- counter := prometheus.NewCounterVec(opts, labels)
-
- for _, label := range labelValues {
- counter.WithLabelValues(label).Add(0)
- }
-
- return counter
-}
-
-func newCounterStartingAtZero(opts prometheus.CounterOpts, labelValues ...string) prometheus.Counter {
- counter := prometheus.NewCounter(opts)
- counter.Add(0)
-
- return counter
-}
-
func init() {
M_Instance_Start = prometheus.NewCounter(prometheus.CounterOpts{
Name: "instance_start_total",
@@ -362,154 +336,19 @@ func initMetricVars() {
}
-func updateTotalStats() {
- statsQuery := models.GetSystemStatsQuery{}
- if err := bus.Dispatch(&statsQuery); err != nil {
- metricsLogger.Error("Failed to get system stats", "error", err)
- return
+func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec {
+ counter := prometheus.NewCounterVec(opts, labels)
+
+ for _, label := range labelValues {
+ counter.WithLabelValues(label).Add(0)
}
- M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
- M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
- M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers))
- M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
- M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
+ return counter
}
-var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
+func newCounterStartingAtZero(opts prometheus.CounterOpts, labelValues ...string) prometheus.Counter {
+ counter := prometheus.NewCounter(opts)
+ counter.Add(0)
-func getEdition() string {
- if setting.IsEnterprise {
- return "enterprise"
- } else {
- return "oss"
- }
-}
-
-func sendUsageStats(oauthProviders map[string]bool) {
- if !setting.ReportingEnabled {
- return
- }
-
- metricsLogger.Debug("Sending anonymous usage stats to stats.grafana.org")
-
- version := strings.Replace(setting.BuildVersion, ".", "_", -1)
-
- metrics := map[string]interface{}{}
- report := map[string]interface{}{
- "version": version,
- "metrics": metrics,
- "os": runtime.GOOS,
- "arch": runtime.GOARCH,
- "edition": getEdition(),
- "packaging": setting.Packaging,
- }
-
- statsQuery := models.GetSystemStatsQuery{}
- if err := bus.Dispatch(&statsQuery); err != nil {
- metricsLogger.Error("Failed to get system stats", "error", err)
- return
- }
-
- metrics["stats.dashboards.count"] = statsQuery.Result.Dashboards
- metrics["stats.users.count"] = statsQuery.Result.Users
- metrics["stats.orgs.count"] = statsQuery.Result.Orgs
- metrics["stats.playlist.count"] = statsQuery.Result.Playlists
- metrics["stats.plugins.apps.count"] = len(plugins.Apps)
- metrics["stats.plugins.panels.count"] = len(plugins.Panels)
- metrics["stats.plugins.datasources.count"] = len(plugins.DataSources)
- metrics["stats.alerts.count"] = statsQuery.Result.Alerts
- metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers
- metrics["stats.datasources.count"] = statsQuery.Result.Datasources
- metrics["stats.stars.count"] = statsQuery.Result.Stars
- metrics["stats.folders.count"] = statsQuery.Result.Folders
- metrics["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions
- metrics["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions
- metrics["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards
- metrics["stats.snapshots.count"] = statsQuery.Result.Snapshots
- metrics["stats.teams.count"] = statsQuery.Result.Teams
-
- dsStats := models.GetDataSourceStatsQuery{}
- if err := bus.Dispatch(&dsStats); err != nil {
- metricsLogger.Error("Failed to get datasource stats", "error", err)
- return
- }
-
- // send counters for each data source
- // but ignore any custom data sources
- // as sending that name could be sensitive information
- dsOtherCount := 0
- for _, dsStat := range dsStats.Result {
- if models.IsKnownDataSourcePlugin(dsStat.Type) {
- metrics["stats.ds."+dsStat.Type+".count"] = dsStat.Count
- } else {
- dsOtherCount += dsStat.Count
- }
- }
- metrics["stats.ds.other.count"] = dsOtherCount
-
- metrics["stats.packaging."+setting.Packaging+".count"] = 1
-
- dsAccessStats := models.GetDataSourceAccessStatsQuery{}
- if err := bus.Dispatch(&dsAccessStats); err != nil {
- metricsLogger.Error("Failed to get datasource access stats", "error", err)
- return
- }
-
- // send access counters for each data source
- // but ignore any custom data sources
- // as sending that name could be sensitive information
- dsAccessOtherCount := make(map[string]int64)
- for _, dsAccessStat := range dsAccessStats.Result {
- if dsAccessStat.Access == "" {
- continue
- }
-
- access := strings.ToLower(dsAccessStat.Access)
-
- if models.IsKnownDataSourcePlugin(dsAccessStat.Type) {
- metrics["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count
- } else {
- old := dsAccessOtherCount[access]
- dsAccessOtherCount[access] = old + dsAccessStat.Count
- }
- }
-
- for access, count := range dsAccessOtherCount {
- metrics["stats.ds_access.other."+access+".count"] = count
- }
-
- anStats := models.GetAlertNotifierUsageStatsQuery{}
- if err := bus.Dispatch(&anStats); err != nil {
- metricsLogger.Error("Failed to get alert notification stats", "error", err)
- return
- }
-
- for _, stats := range anStats.Result {
- metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count
- }
-
- authTypes := map[string]bool{}
- authTypes["anonymous"] = setting.AnonymousEnabled
- authTypes["basic_auth"] = setting.BasicAuthEnabled
- authTypes["ldap"] = setting.LdapEnabled
- authTypes["auth_proxy"] = setting.AuthProxyEnabled
-
- for provider, enabled := range oauthProviders {
- authTypes["oauth_"+provider] = enabled
- }
-
- for authType, enabled := range authTypes {
- enabledValue := 0
- if enabled {
- enabledValue = 1
- }
- metrics["stats.auth_enabled."+authType+".count"] = enabledValue
- }
-
- out, _ := json.MarshalIndent(report, "", " ")
- data := bytes.NewBuffer(out)
-
- client := http.Client{Timeout: 5 * time.Second}
- go client.Post(usageStatsURL, "application/json", data)
+ return counter
}
diff --git a/pkg/metrics/service.go b/pkg/metrics/service.go
index d2c0c815da9..44b83187cac 100644
--- a/pkg/metrics/service.go
+++ b/pkg/metrics/service.go
@@ -2,7 +2,6 @@ package metrics
import (
"context"
- "time"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
@@ -30,7 +29,6 @@ type InternalMetricsService struct {
intervalSeconds int64
graphiteCfg *graphitebridge.Config
- oauthProviders map[string]bool
}
func (im *InternalMetricsService) Init() error {
@@ -50,22 +48,6 @@ func (im *InternalMetricsService) Run(ctx context.Context) error {
M_Instance_Start.Inc()
- // set the total stats gauges before we publishing metrics
- updateTotalStats()
-
- onceEveryDayTick := time.NewTicker(time.Hour * 24)
- everyMinuteTicker := time.NewTicker(time.Minute)
- defer onceEveryDayTick.Stop()
- defer everyMinuteTicker.Stop()
-
- for {
- select {
- case <-onceEveryDayTick.C:
- sendUsageStats(im.oauthProviders)
- case <-everyMinuteTicker.C:
- updateTotalStats()
- case <-ctx.Done():
- return ctx.Err()
- }
- }
+ <-ctx.Done()
+ return ctx.Err()
}
diff --git a/pkg/metrics/settings.go b/pkg/metrics/settings.go
index 18b9e78d6ff..048e4134690 100644
--- a/pkg/metrics/settings.go
+++ b/pkg/metrics/settings.go
@@ -5,8 +5,6 @@ import (
"strings"
"time"
- "github.com/grafana/grafana/pkg/social"
-
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
@@ -24,8 +22,6 @@ func (im *InternalMetricsService) readSettings() error {
return fmt.Errorf("Unable to parse metrics graphite section, %v", err)
}
- im.oauthProviders = social.GetOAuthProviders(im.Cfg)
-
return nil
}
diff --git a/pkg/models/datasource_cache.go b/pkg/models/datasource_cache.go
index 66ba66e4d39..1c895514ace 100644
--- a/pkg/models/datasource_cache.go
+++ b/pkg/models/datasource_cache.go
@@ -46,19 +46,16 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
return t.Transport, nil
}
- var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool
- if ds.JsonData != nil {
- tlsClientAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
- tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
- tlsSkipVerify = ds.JsonData.Get("tlsSkipVerify").MustBool(false)
+ tlsConfig, err := ds.GetTLSConfig()
+ if err != nil {
+ return nil, err
}
+ tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient
+
transport := &http.Transport{
- TLSClientConfig: &tls.Config{
- InsecureSkipVerify: tlsSkipVerify,
- Renegotiation: tls.RenegotiateFreelyAsClient,
- },
- Proxy: http.ProxyFromEnvironment,
+ TLSClientConfig: tlsConfig,
+ Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
@@ -70,6 +67,26 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
IdleConnTimeout: 90 * time.Second,
}
+ ptc.cache[ds.Id] = cachedTransport{
+ Transport: transport,
+ updated: ds.Updated,
+ }
+
+ return transport, nil
+}
+
+func (ds *DataSource) GetTLSConfig() (*tls.Config, error) {
+ var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool
+ if ds.JsonData != nil {
+ tlsClientAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
+ tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
+ tlsSkipVerify = ds.JsonData.Get("tlsSkipVerify").MustBool(false)
+ }
+
+ tlsConfig := &tls.Config{
+ InsecureSkipVerify: tlsSkipVerify,
+ }
+
if tlsClientAuth || tlsAuthWithCACert {
decrypted := ds.SecureJsonData.Decrypt()
if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 {
@@ -78,7 +95,7 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
if !ok {
return nil, errors.New("Failed to parse TLS CA PEM certificate")
}
- transport.TLSClientConfig.RootCAs = caPool
+ tlsConfig.RootCAs = caPool
}
if tlsClientAuth {
@@ -86,14 +103,9 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
if err != nil {
return nil, err
}
- transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
+ tlsConfig.Certificates = []tls.Certificate{cert}
}
}
- ptc.cache[ds.Id] = cachedTransport{
- Transport: transport,
- updated: ds.Updated,
- }
-
- return transport, nil
+ return tlsConfig, nil
}
diff --git a/pkg/models/stats.go b/pkg/models/stats.go
index d3e145dedf4..00f881f3c59 100644
--- a/pkg/models/stats.go
+++ b/pkg/models/stats.go
@@ -15,6 +15,7 @@ type SystemStats struct {
FolderPermissions int64
Folders int64
ProvisionedDashboards int64
+ Sessions int64
}
type DataSourceStats struct {
diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go
index 2cec86e7239..4c6d6c21221 100644
--- a/pkg/services/sqlstore/stats.go
+++ b/pkg/services/sqlstore/stats.go
@@ -74,7 +74,8 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_provisioning") + `) AS provisioned_dashboards,`)
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_snapshot") + `) AS snapshots,`)
- sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("team") + `) AS teams`)
+ sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("team") + `) AS teams,`)
+ sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("user_auth_token") + `) AS sessions`)
var stats m.SystemStats
_, err := x.SQL(sb.GetSqlString(), sb.params...).Get(&stats)
diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go
index 34181d19673..ddda26dfd24 100644
--- a/pkg/tsdb/cloudwatch/metric_find_query.go
+++ b/pkg/tsdb/cloudwatch/metric_find_query.go
@@ -55,6 +55,7 @@ func init() {
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
"AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps", "BurstBalance"},
"AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "NetworkPacketsIn", "NetworkPacketsOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
+ "AWS/EC2/API": {"ClientErrors", "RequestLimitExceeded", "ServerErrors", "SuccessfulCalls"},
"AWS/EC2Spot": {"AvailableInstancePoolsCount", "BidsSubmittedForCapacity", "EligibleInstancePoolCount", "FulfilledCapacity", "MaxPercentCapacityAllocation", "PendingCapacity", "PercentCapacityAllocation", "TargetCapacity", "TerminatingCapacity"},
"AWS/ECS": {"CPUReservation", "MemoryReservation", "CPUUtilization", "MemoryUtilization"},
"AWS/EFS": {"BurstCreditBalance", "ClientConnections", "DataReadIOBytes", "DataWriteIOBytes", "MetadataIOBytes", "TotalIOBytes", "PermittedThroughput", "PercentIOLimit"},
@@ -133,6 +134,7 @@ func init() {
"AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"},
"AWS/EBS": {"VolumeId"},
"AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
+ "AWS/EC2/API": {},
"AWS/EC2Spot": {"AvailabilityZone", "FleetRequestId", "InstanceType"},
"AWS/ECS": {"ClusterName", "ServiceName"},
"AWS/EFS": {"FileSystemId"},
diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go
index 35b03e489a0..d307e12166c 100644
--- a/pkg/tsdb/mysql/mysql.go
+++ b/pkg/tsdb/mysql/mysql.go
@@ -32,6 +32,18 @@ func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin
datasource.Url,
datasource.Database,
)
+
+ tlsConfig, err := datasource.GetTLSConfig()
+ if err != nil {
+ return nil, err
+ }
+
+ if tlsConfig.RootCAs != nil || len(tlsConfig.Certificates) > 0 {
+ tlsConfigString := fmt.Sprintf("ds%d", datasource.Id)
+ mysql.RegisterTLSConfig(tlsConfigString, tlsConfig)
+ cnnstr += "&tls=" + tlsConfigString
+ }
+
logger.Debug("getEngine", "connection", cnnstr)
config := tsdb.SqlQueryEndpointConfiguration{
diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts
index 4806275e87d..6db442e7470 100644
--- a/public/app/core/angular_wrappers.ts
+++ b/public/app/core/angular_wrappers.ts
@@ -9,7 +9,7 @@ import { TagFilter } from './components/TagFilter/TagFilter';
import { SideMenu } from './components/sidemenu/SideMenu';
import { MetricSelect } from './components/Select/MetricSelect';
import AppNotificationList from './components/AppNotifications/AppNotificationList';
-import { ColorPicker, SeriesColorPickerPopover } from '@grafana/ui';
+import { ColorPicker, SeriesColorPickerPopoverWithTheme } from '@grafana/ui';
export function registerAngularDirectives() {
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
@@ -27,7 +27,7 @@ export function registerAngularDirectives() {
'color',
['onChange', { watchDepth: 'reference', wrapApply: true }],
]);
- react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [
+ react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopoverWithTheme, [
'color',
'series',
'onColorChange',
diff --git a/public/app/core/components/Animations/FadeIn.tsx b/public/app/core/components/Animations/FadeIn.tsx
index ea9a92d5f0f..d667b54261e 100644
--- a/public/app/core/components/Animations/FadeIn.tsx
+++ b/public/app/core/components/Animations/FadeIn.tsx
@@ -1,11 +1,12 @@
import React, { FC } from 'react';
-import Transition from 'react-transition-group/Transition';
+import Transition, { ExitHandler } from 'react-transition-group/Transition';
interface Props {
duration: number;
children: JSX.Element;
in: boolean;
unmountOnExit?: boolean;
+ onExited?: ExitHandler;
}
export const FadeIn: FC = props => {
@@ -22,7 +23,12 @@ export const FadeIn: FC = props => {
};
return (
-
+
{state => (
({
emit: jest.fn(),
}));
+jest.mock('app/store/store', () => ({
+ store: {
+ getState: jest.fn().mockReturnValue({
+ location: {
+ lastUpdated: 0,
+ }
+ })
+ }
+}));
+
jest.mock('app/core/services/context_srv', () => ({
contextSrv: {
sidemenu: true,
diff --git a/public/app/core/components/sidemenu/SideMenu.tsx b/public/app/core/components/sidemenu/SideMenu.tsx
index fd3e0d95564..29ef0fed069 100644
--- a/public/app/core/components/sidemenu/SideMenu.tsx
+++ b/public/app/core/components/sidemenu/SideMenu.tsx
@@ -3,9 +3,16 @@ import appEvents from '../../app_events';
import { contextSrv } from 'app/core/services/context_srv';
import TopSection from './TopSection';
import BottomSection from './BottomSection';
+import { store } from 'app/store/store';
export class SideMenu extends PureComponent {
toggleSideMenu = () => {
+ // ignore if we just made a location change, stops hiding sidemenu on double clicks of back button
+ const timeSinceLocationChanged = new Date().getTime() - store.getState().location.lastUpdated;
+ if (timeSinceLocationChanged < 1000) {
+ return;
+ }
+
contextSrv.toggleSideMenu();
appEvents.emit('toggle-sidemenu');
};
diff --git a/public/app/core/config.ts b/public/app/core/config.ts
index 368b3798117..f4254ac251a 100644
--- a/public/app/core/config.ts
+++ b/public/app/core/config.ts
@@ -1,5 +1,6 @@
import _ from 'lodash';
import { PanelPlugin } from 'app/types/plugins';
+import { GrafanaTheme, getTheme, GrafanaThemeType } from '@grafana/ui';
export interface BuildInfo {
version: string;
@@ -36,8 +37,11 @@ export class Settings {
loginError: any;
viewersCanEdit: boolean;
disableSanitizeHtml: boolean;
+ theme: GrafanaTheme;
constructor(options: Settings) {
+ this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark);
+
const defaults = {
datasources: {},
windowTitlePrefix: 'Grafana - ',
diff --git a/public/app/core/core.ts b/public/app/core/core.ts
index 1f289fc4b27..80987b8fc88 100644
--- a/public/app/core/core.ts
+++ b/public/app/core/core.ts
@@ -1,4 +1,3 @@
-import './directives/dash_class';
import './directives/dropdown_typeahead';
import './directives/autofill_event_fix';
import './directives/metric_segment';
diff --git a/public/app/core/directives/dash_class.ts b/public/app/core/directives/dash_class.ts
deleted file mode 100644
index 1fb93d29cf3..00000000000
--- a/public/app/core/directives/dash_class.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import $ from 'jquery';
-import _ from 'lodash';
-import coreModule from '../core_module';
-
-/** @ngInject */
-function dashClass($timeout) {
- return {
- link: ($scope, elem) => {
- const body = $('body');
-
- $scope.ctrl.dashboard.events.on('view-mode-changed', panel => {
- console.log('view-mode-changed', panel.fullscreen);
- if (panel.fullscreen) {
- body.addClass('panel-in-fullscreen');
- } else {
- $timeout(() => {
- body.removeClass('panel-in-fullscreen');
- });
- }
- });
-
- body.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true);
-
- $scope.$watch('ctrl.dashboardViewState.state.editview', newValue => {
- if (newValue) {
- elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue));
- setTimeout(() => {
- elem.toggleClass('dashboard-page--settings-open', _.isString(newValue));
- }, 10);
- } else {
- elem.removeClass('dashboard-page--settings-opening');
- elem.removeClass('dashboard-page--settings-open');
- }
- });
- },
- };
-}
-
-coreModule.directive('dashClass', dashClass);
diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts
index abcd5563bd0..2cde5379448 100644
--- a/public/app/core/logs_model.ts
+++ b/public/app/core/logs_model.ts
@@ -340,6 +340,11 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): Time
return a[1] - b[1];
});
- return { datapoints: series.datapoints, target: series.alias, color: series.color };
+ return {
+ datapoints: series.datapoints,
+ target: series.alias,
+ alias: series.alias,
+ color: series.color
+ };
});
}
diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts
index c038ab53c9f..dff1ac8f5c1 100644
--- a/public/app/core/reducers/location.ts
+++ b/public/app/core/reducers/location.ts
@@ -9,6 +9,7 @@ export const initialState: LocationState = {
query: {},
routeParams: {},
replace: false,
+ lastUpdated: 0,
};
export const locationReducer = (state = initialState, action: Action): LocationState => {
@@ -28,6 +29,7 @@ export const locationReducer = (state = initialState, action: Action): LocationS
query: { ...query },
routeParams: routeParams || state.routeParams,
replace: replace === true,
+ lastUpdated: new Date().getTime(),
};
}
}
diff --git a/public/app/core/utils/ConfigProvider.tsx b/public/app/core/utils/ConfigProvider.tsx
index 6883401ad27..cb3ad88b191 100644
--- a/public/app/core/utils/ConfigProvider.tsx
+++ b/public/app/core/utils/ConfigProvider.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import config, { Settings } from 'app/core/config';
-import { GrafanaTheme } from '@grafana/ui';
+import { GrafanaThemeType, ThemeContext, getTheme } from '@grafana/ui';
export const ConfigContext = React.createContext
(config);
export const ConfigConsumer = ConfigContext.Consumer;
@@ -13,16 +13,20 @@ export const provideConfig = (component: React.ComponentType) => {
return ConfigProvider;
};
-interface ThemeProviderProps {
- children: (theme: GrafanaTheme) => JSX.Element;
-}
+export const getCurrentThemeName = () =>
+ config.bootData.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark;
+export const getCurrentTheme = () => getTheme(getCurrentThemeName());
-export const ThemeProvider = ({ children }: ThemeProviderProps) => {
+export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
return (
- {({ bootData }) => {
- return children(bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark);
+ {config => {
+ return {children} ;
}}
);
};
+
+export const provideTheme = (component: React.ComponentType) => {
+ return provideConfig((props: any) => {React.createElement(component, { ...props })} );
+};
diff --git a/public/app/core/utils/react2angular.ts b/public/app/core/utils/react2angular.ts
index 1057f68fcda..eb4bccab267 100644
--- a/public/app/core/utils/react2angular.ts
+++ b/public/app/core/utils/react2angular.ts
@@ -1,11 +1,11 @@
import coreModule from 'app/core/core_module';
-import { provideConfig } from 'app/core/utils/ConfigProvider';
+import { provideTheme } from 'app/core/utils/ConfigProvider';
export function react2AngularDirective(name: string, component: any, options: any) {
coreModule.directive(name, [
'reactDirective',
reactDirective => {
- return reactDirective(provideConfig(component), options);
+ return reactDirective(provideTheme(component), options);
},
]);
}
diff --git a/public/app/features/all.ts b/public/app/features/all.ts
index 83146596ea0..d5e684e4a4e 100644
--- a/public/app/features/all.ts
+++ b/public/app/features/all.ts
@@ -12,3 +12,4 @@ import './manage-dashboards';
import './teams/CreateTeamCtrl';
import './profile/all';
import './datasources/settings/HttpSettingsCtrl';
+import './datasources/settings/TlsAuthSettingsCtrl';
diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx
index 297d7ca7ea7..6db07b5d42e 100644
--- a/public/app/features/dashboard/components/DashNav/DashNav.tsx
+++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx
@@ -9,12 +9,13 @@ import { PlaylistSrv } from 'app/features/playlist/playlist_srv';
// Components
import { DashNavButton } from './DashNavButton';
+import { Tooltip } from '@grafana/ui';
// State
import { updateLocation } from 'app/core/actions';
// Types
-import { DashboardModel } from '../../state/DashboardModel';
+import { DashboardModel } from '../../state';
export interface Props {
dashboard: DashboardModel;
@@ -33,7 +34,6 @@ export class DashNav extends PureComponent {
constructor(props: Props) {
super(props);
-
this.playlistSrv = this.props.$injector.get('playlistSrv');
}
@@ -123,26 +123,54 @@ export class DashNav extends PureComponent {
});
};
- render() {
- const { dashboard, isFullscreen, editview, onAddPanel } = this.props;
- const { canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta;
- const { snapshot } = dashboard;
+ renderDashboardTitleSearchButton() {
+ const { dashboard } = this.props;
+ const folderTitle = dashboard.meta.folderTitle;
const haveFolder = dashboard.meta.folderId > 0;
- const snapshotUrl = snapshot && snapshot.originalUrl;
return (
-
+ <>
-
+ >
+ );
+ }
+
+ get isInFullscreenOrSettings() {
+ return this.props.editview || this.props.isFullscreen;
+ }
+
+ renderBackButton() {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ render() {
+ const { dashboard, onAddPanel } = this.props;
+ const { canStar, canSave, canShare, showSettings, isStarred } = dashboard.meta;
+ const { snapshot } = dashboard;
+
+ const snapshotUrl = snapshot && snapshot.originalUrl;
+
+ return (
+
+ {this.isInFullscreenOrSettings && this.renderBackButton()}
+ {this.renderDashboardTitleSearchButton()}
{this.playlistSrv.isPlaying && (
@@ -228,17 +256,6 @@ export class DashNav extends PureComponent
{
(this.timePickerEl = element)} />
-
- {(isFullscreen || editview) && (
-
-
-
- )}
);
}
diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx
index eda10087d41..83ef70f62e7 100644
--- a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx
+++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx
@@ -7,10 +7,11 @@ import _ from 'lodash';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
import { Emitter } from 'app/core/utils/emitter';
+import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
// Types
import { PanelModel } from '../state/PanelModel';
-import { DataQuery, DataSourceApi } from '@grafana/ui';
+import { DataQuery, DataSourceApi, TimeRange } from '@grafana/ui';
interface Props {
panel: PanelModel;
@@ -43,8 +44,15 @@ export class QueryEditorRow extends PureComponent
{
componentDidMount() {
this.loadDatasource();
+ this.props.panel.events.on('refresh', this.onPanelRefresh);
}
+ onPanelRefresh = () => {
+ if (this.state.angularScope) {
+ this.state.angularScope.range = getTimeSrv().timeRange();
+ }
+ };
+
getAngularQueryComponentScope(): AngularQueryComponentScope {
const { panel, query } = this.props;
const { datasource } = this.state;
@@ -56,6 +64,7 @@ export class QueryEditorRow extends PureComponent {
refresh: () => panel.refresh(),
render: () => panel.render(),
events: panel.events,
+ range: getTimeSrv().timeRange(),
};
}
@@ -97,6 +106,8 @@ export class QueryEditorRow extends PureComponent {
}
componentWillUnmount() {
+ this.props.panel.events.off('refresh', this.onPanelRefresh);
+
if (this.angularQueryEditor) {
this.angularQueryEditor.destroy();
}
@@ -250,4 +261,5 @@ export interface AngularQueryComponentScope {
datasource: DataSourceApi;
toggleEditorMode?: () => void;
getCollapsedText?: () => string;
+ range: TimeRange;
}
diff --git a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx
index 94a403c11bf..37f109250a7 100644
--- a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx
+++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx
@@ -119,7 +119,12 @@ export class VisualizationTab extends PureComponent {
template +=
`
` +
- (i > 0 ? `` : '') +
+ (i > 0
+ ? ``
+ : '') +
`
@@ -144,6 +149,10 @@ export class VisualizationTab extends PureComponent
{
}
}
+ clearQuery = () => {
+ this.setState({ searchQuery: '' });
+ };
+
onPanelOptionsChanged = (options: any) => {
this.props.panel.updateOptions(options);
this.forceUpdate();
@@ -228,10 +237,15 @@ export class VisualizationTab extends PureComponent {
};
return (
-
+
<>
-
+
{
}
const mapStateToProps = (state: StoreState) => ({
- urlOpenVizPicker: !!state.location.query.openVizPicker
+ urlOpenVizPicker: !!state.location.query.openVizPicker,
});
const mapDispatchToProps = {
- updateLocation
+ updateLocation,
};
export default connectWithStore(VisualizationTab, mapStateToProps, mapDispatchToProps);
diff --git a/public/app/features/datasources/partials/http_settings.html b/public/app/features/datasources/partials/http_settings.html
index 521e2d3cdc6..b6f2c4fc0dd 100644
--- a/public/app/features/datasources/partials/http_settings.html
+++ b/public/app/features/datasources/partials/http_settings.html
@@ -101,53 +101,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/public/app/features/datasources/partials/tls_auth_settings.html b/public/app/features/datasources/partials/tls_auth_settings.html
new file mode 100644
index 00000000000..c852e8ec70c
--- /dev/null
+++ b/public/app/features/datasources/partials/tls_auth_settings.html
@@ -0,0 +1,62 @@
+
diff --git a/public/app/features/datasources/settings/TlsAuthSettingsCtrl.ts b/public/app/features/datasources/settings/TlsAuthSettingsCtrl.ts
new file mode 100644
index 00000000000..7c21fab404c
--- /dev/null
+++ b/public/app/features/datasources/settings/TlsAuthSettingsCtrl.ts
@@ -0,0 +1,10 @@
+import { coreModule } from 'app/core/core';
+
+coreModule.directive('datasourceTlsAuthSettings', () => {
+ return {
+ scope: {
+ current: '=',
+ },
+ templateUrl: 'public/app/features/datasources/partials/tls_auth_settings.html',
+ };
+});
diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx
index 437b50db63c..aca8f033fb3 100644
--- a/public/app/features/explore/Explore.tsx
+++ b/public/app/features/explore/Explore.tsx
@@ -205,28 +205,35 @@ export class Explore extends React.PureComponent
{
- {({ width }) => (
-
-
- {showingStartPage && }
- {!showingStartPage && (
- <>
- {supportsGraph && !supportsLogs && }
- {supportsTable && }
- {supportsLogs && (
-
- )}
- >
- )}
-
-
- )}
+ {({ width }) => {
+ if (width === 0) {
+ return null;
+ }
+
+ return (
+
+
+ {showingStartPage && }
+ {!showingStartPage && (
+ <>
+ {supportsGraph && !supportsLogs && }
+ {supportsTable && }
+ {supportsLogs && (
+
+ )}
+ >
+ )}
+
+
+ );
+ }}
)}
diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx
index af6caee6206..f41555b9121 100644
--- a/public/app/features/explore/Logs.tsx
+++ b/public/app/features/explore/Logs.tsx
@@ -51,6 +51,7 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
interface Props {
data?: LogsModel;
+ width: number;
exploreId: string;
highlighterExpressions: string[];
loading: boolean;
@@ -164,6 +165,7 @@ export default class Logs extends PureComponent {
range,
scanning,
scanRange,
+ width,
} = this.props;
if (!data) {
@@ -216,6 +218,7 @@ export default class Logs extends PureComponent {
{
@@ -51,7 +52,8 @@ export class LogsContainer extends PureComponent {
range,
showingLogs,
scanning,
- scanRange
+ scanRange,
+ width,
} = this.props;
return (
@@ -71,6 +73,7 @@ export class LogsContainer extends PureComponent {
range={range}
scanning={scanning}
scanRange={scanRange}
+ width={width}
/>
);
diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx
index a1b9e7a5df9..14fe046e098 100644
--- a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx
+++ b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx
@@ -2,61 +2,65 @@
import React, { PureComponent } from 'react';
// Components
-import { Select, SelectOptionItem } from '@grafana/ui';
+// import { Select, SelectOptionItem } from '@grafana/ui';
// Types
import { QueryEditorProps } from '@grafana/ui/src/types';
import { LokiDatasource } from '../datasource';
import { LokiQuery } from '../types';
-import { LokiQueryField } from './LokiQueryField';
+// import { LokiQueryField } from './LokiQueryField';
type Props = QueryEditorProps;
-interface State {
- query: LokiQuery;
-}
+// interface State {
+// query: LokiQuery;
+// }
export class LokiQueryEditor extends PureComponent {
- state: State = {
- query: this.props.query,
- };
-
- onRunQuery = () => {
- const { query } = this.state;
-
- this.props.onChange(query);
- this.props.onRunQuery();
- };
-
- onFieldChange = (query: LokiQuery, override?) => {
- this.setState({
- query: {
- ...this.state.query,
- expr: query.expr,
- },
- });
- };
-
- onFormatChanged = (option: SelectOptionItem) => {
- this.props.onChange({
- ...this.state.query,
- resultFormat: option.value,
- });
- };
+ // state: State = {
+ // query: this.props.query,
+ // };
+ //
+ // onRunQuery = () => {
+ // const { query } = this.state;
+ //
+ // this.props.onChange(query);
+ // this.props.onRunQuery();
+ // };
+ //
+ // onFieldChange = (query: LokiQuery, override?) => {
+ // this.setState({
+ // query: {
+ // ...this.state.query,
+ // expr: query.expr,
+ // },
+ // });
+ // };
+ //
+ // onFormatChanged = (option: SelectOptionItem) => {
+ // this.props.onChange({
+ // ...this.state.query,
+ // resultFormat: option.value,
+ // });
+ // };
render() {
- const { query } = this.state;
- const { datasource } = this.props;
- const formatOptions: SelectOptionItem[] = [
- { label: 'Time Series', value: 'time_series' },
- { label: 'Table', value: 'table' },
- ];
-
- query.resultFormat = query.resultFormat || 'time_series';
- const currentFormat = formatOptions.find(item => item.value === query.resultFormat);
+ // const { query } = this.state;
+ // const { datasource } = this.props;
+ // const formatOptions: SelectOptionItem[] = [
+ // { label: 'Time Series', value: 'time_series' },
+ // { label: 'Table', value: 'table' },
+ // ];
+ //
+ // query.resultFormat = query.resultFormat || 'time_series';
+ // const currentFormat = formatOptions.find(item => item.value === query.resultFormat);
return (
+
+
Loki is currently not supported as dashboard data source. We are working on it!
+
+ {/*
{
+ */}
);
}
diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts
index 23aa5504d3e..303cd0471d7 100644
--- a/public/app/plugins/datasource/mssql/datasource.ts
+++ b/public/app/plugins/datasource/mssql/datasource.ts
@@ -8,7 +8,7 @@ export class MssqlDatasource {
interval: string;
/** @ngInject */
- constructor(instanceSettings, private backendSrv, private $q, private templateSrv) {
+ constructor(instanceSettings, private backendSrv, private $q, private templateSrv, private timeSrv) {
this.name = instanceSettings.name;
this.id = instanceSettings.id;
this.responseParser = new ResponseParser(this.$q);
@@ -107,13 +107,18 @@ export class MssqlDatasource {
format: 'table',
};
+ const range = this.timeSrv.timeRange();
+ const data = {
+ queries: [interpolatedQuery],
+ from: range.from.valueOf().toString(),
+ to: range.to.valueOf().toString(),
+ };
+
return this.backendSrv
.datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
- data: {
- queries: [interpolatedQuery],
- },
+ data: data,
})
.then(data => this.responseParser.parseMetricFindQueryResult(refId, data));
}
diff --git a/public/app/plugins/datasource/mssql/specs/datasource.test.ts b/public/app/plugins/datasource/mssql/specs/datasource.test.ts
index 0dd496bfe59..a05848b3da8 100644
--- a/public/app/plugins/datasource/mssql/specs/datasource.test.ts
+++ b/public/app/plugins/datasource/mssql/specs/datasource.test.ts
@@ -1,6 +1,6 @@
import moment from 'moment';
import { MssqlDatasource } from '../datasource';
-import { TemplateSrvStub } from 'test/specs/helpers';
+import { TemplateSrvStub, TimeSrvStub } from 'test/specs/helpers';
import { CustomVariable } from 'app/features/templating/custom_variable';
import q from 'q';
@@ -8,13 +8,14 @@ describe('MSSQLDatasource', () => {
const ctx: any = {
backendSrv: {},
templateSrv: new TemplateSrvStub(),
+ timeSrv: new TimeSrvStub(),
};
beforeEach(() => {
ctx.$q = q;
ctx.instanceSettings = { name: 'mssql' };
- ctx.ds = new MssqlDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.$q, ctx.templateSrv);
+ ctx.ds = new MssqlDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.$q, ctx.templateSrv, ctx.timeSrv);
});
describe('When performing annotationQuery', () => {
@@ -188,6 +189,49 @@ describe('MSSQLDatasource', () => {
});
});
+ describe('When performing metricFindQuery', () => {
+ let results;
+ const query = 'select * from atable';
+ const response = {
+ results: {
+ tempvar: {
+ meta: {
+ rowCount: 1,
+ },
+ refId: 'tempvar',
+ tables: [
+ {
+ columns: [{ text: 'title' }],
+ rows: [['aTitle']],
+ },
+ ],
+ },
+ },
+ };
+ const time = {
+ from: moment(1521545610656),
+ to: moment(1521546251185)
+ };
+
+ beforeEach(() => {
+ ctx.timeSrv.setTime(time);
+
+ ctx.backendSrv.datasourceRequest = options => {
+ results = options.data;
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+
+ return ctx.ds.metricFindQuery(query);
+ });
+
+ it('should pass timerange to datasourceRequest', () => {
+ expect(results.from).toBe(time.from.valueOf().toString());
+ expect(results.to).toBe(time.to.valueOf().toString());
+ expect(results.queries.length).toBe(1);
+ expect(results.queries[0].rawSql).toBe(query);
+ });
+ });
+
describe('When interpolating variables', () => {
beforeEach(() => {
ctx.variable = new CustomVariable({}, {});
diff --git a/public/app/plugins/datasource/mysql/partials/config.html b/public/app/plugins/datasource/mysql/partials/config.html
index a35633c626a..8221a06e1ee 100644
--- a/public/app/plugins/datasource/mysql/partials/config.html
+++ b/public/app/plugins/datasource/mysql/partials/config.html
@@ -1,4 +1,3 @@
-
MySQL Connection
@@ -22,7 +21,23 @@