ryan 6 年 前
コミット
4e5e548902
54 ファイル変更366 行追加276 行削除
  1. 3 0
      CHANGELOG.md
  2. 1 0
      package.json
  3. 1 1
      packages/grafana-ui/src/components/FormField/_FormField.scss
  4. 2 3
      packages/grafana-ui/src/components/Gauge/Gauge.tsx
  5. 1 1
      packages/grafana-ui/src/components/Select/_Select.scss
  6. 17 19
      packages/grafana-ui/src/themes/_variables.scss.tmpl.ts
  7. 23 16
      packages/grafana-ui/src/themes/default.ts
  8. 22 15
      packages/grafana-ui/src/types/theme.ts
  9. 2 2
      public/app/core/components/jsontree/jsontree.ts
  10. 1 1
      public/app/core/directives/give_focus.ts
  11. 0 15
      public/app/core/specs/kbn.test.ts
  12. 6 6
      public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss
  13. 0 5
      public/app/features/dashboard/containers/DashboardPage.test.tsx
  14. 1 1
      public/app/features/dashboard/dashgrid/DataPanel.tsx
  15. 0 5
      public/app/features/dashboard/dashgrid/PanelChrome.test.tsx
  16. 3 3
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  17. 1 1
      public/app/features/datasources/settings/HttpSettingsCtrl.ts
  18. 1 1
      public/app/features/panel/metrics_panel_ctrl.ts
  19. 2 2
      public/app/features/panel/panel_directive.ts
  20. 9 3
      public/app/features/plugins/datasource_srv.ts
  21. 15 0
      public/app/features/templating/datasource_variable.ts
  22. 5 4
      public/app/plugins/datasource/prometheus/result_transformer.ts
  23. 21 24
      public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts
  24. 1 0
      public/app/plugins/panel/heatmap/axes_editor.ts
  25. 20 18
      public/app/plugins/panel/heatmap/color_legend.ts
  26. 16 7
      public/app/plugins/panel/heatmap/heatmap_ctrl.ts
  27. 23 5
      public/app/plugins/panel/heatmap/heatmap_data_converter.ts
  28. 5 2
      public/app/plugins/panel/heatmap/heatmap_tooltip.ts
  29. 5 0
      public/app/plugins/panel/heatmap/partials/axes_editor.html
  30. 4 0
      public/app/plugins/panel/heatmap/partials/display_editor.html
  31. 8 2
      public/app/plugins/panel/heatmap/rendering.ts
  32. 7 9
      public/sass/_variables.generated.scss
  33. 0 8
      public/sass/_variables.generated.scss.d.ts
  34. 1 1
      public/sass/base/_type.scss
  35. 3 3
      public/sass/components/_add_data_source.scss
  36. 1 1
      public/sass/components/_alerts.scss
  37. 14 21
      public/sass/components/_cards.scss
  38. 5 5
      public/sass/components/_dashboard_settings.scss
  39. 8 9
      public/sass/components/_gf-form.scss
  40. 1 1
      public/sass/components/_infobox.scss
  41. 1 1
      public/sass/components/_page_loader.scss
  42. 2 2
      public/sass/components/_panel_editor.scss
  43. 0 1
      public/sass/components/_panel_heatmap.scss
  44. 1 1
      public/sass/components/_panel_logs.scss
  45. 4 4
      public/sass/components/_sidemenu.scss
  46. 1 1
      public/sass/components/_submenu.scss
  47. 14 13
      public/sass/pages/_explore.scss
  48. 3 3
      public/sass/utils/_widths.scss
  49. 7 10
      public/test/specs/helpers.ts
  50. 32 20
      public/vendor/ansicolor/ansicolor.ts
  51. 30 0
      scripts/circle-metrics.sh
  52. 6 0
      scripts/circle-test-frontend.sh
  53. 1 0
      tsconfig.json
  54. 5 0
      yarn.lock

+ 3 - 0
CHANGELOG.md

@@ -5,12 +5,15 @@
 
 
 ### Minor
 ### Minor
 * **Cloudwatch**: Add AWS RDS MaximumUsedTransactionIDs metric [#15077](https://github.com/grafana/grafana/pull/15077), thx [@activeshadow](https://github.com/activeshadow)
 * **Cloudwatch**: Add AWS RDS MaximumUsedTransactionIDs metric [#15077](https://github.com/grafana/grafana/pull/15077), thx [@activeshadow](https://github.com/activeshadow)
+* **Heatmap**: `Middle` bucket bound option [#15683](https://github.com/grafana/grafana/issues/15683)
+* **Heatmap**: `Reverse order` option for changing order of buckets [#15683](https://github.com/grafana/grafana/issues/15683)
 
 
 ### Bug Fixes
 ### Bug Fixes
 * **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506)
 * **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506)
 * **Datasource**: Handles nil jsondata field gracefully [#14239](https://github.com/grafana/grafana/issues/14239)
 * **Datasource**: Handles nil jsondata field gracefully [#14239](https://github.com/grafana/grafana/issues/14239)
 * **Gauge**: Interpolate scoped variables in repeated gauges [#15739](https://github.com/grafana/grafana/issues/15739)
 * **Gauge**: Interpolate scoped variables in repeated gauges [#15739](https://github.com/grafana/grafana/issues/15739)
 * **Datasource**: Empty user/password was not updated when updating datasources [#15608](https://github.com/grafana/grafana/pull/15608), thx [@Maddin-619](https://github.com/Maddin-619)
 * **Datasource**: Empty user/password was not updated when updating datasources [#15608](https://github.com/grafana/grafana/pull/15608), thx [@Maddin-619](https://github.com/Maddin-619)
+* **Heatmap**: legend shows wrong colors for small values [#14019](https://github.com/grafana/grafana/issues/14019)
 
 
 # 6.0.1 (2019-03-06)
 # 6.0.1 (2019-03-06)
 
 

+ 1 - 0
package.json

@@ -17,6 +17,7 @@
     "@babel/preset-react": "^7.0.0",
     "@babel/preset-react": "^7.0.0",
     "@babel/preset-typescript": "^7.1.0",
     "@babel/preset-typescript": "^7.1.0",
     "@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1",
     "@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1",
+    "@types/angular": "^1.6.6",
     "@types/chalk": "^2.2.0",
     "@types/chalk": "^2.2.0",
     "@types/classnames": "^2.2.6",
     "@types/classnames": "^2.2.6",
     "@types/commander": "^2.12.2",
     "@types/commander": "^2.12.2",

+ 1 - 1
packages/grafana-ui/src/components/FormField/_FormField.scss

@@ -1,5 +1,5 @@
 .form-field {
 .form-field {
-  margin-bottom: $gf-form-margin;
+  margin-bottom: $space-xxs;
   display: flex;
   display: flex;
   flex-direction: row;
   flex-direction: row;
   align-items: center;
   align-items: center;

+ 2 - 3
packages/grafana-ui/src/components/Gauge/Gauge.tsx

@@ -22,13 +22,12 @@ const FONT_SCALE = 1;
 export class Gauge extends PureComponent<Props> {
 export class Gauge extends PureComponent<Props> {
   canvasElement: any;
   canvasElement: any;
 
 
-  static defaultProps = {
+  static defaultProps: Partial<Props> = {
     maxValue: 100,
     maxValue: 100,
     minValue: 0,
     minValue: 0,
     showThresholdMarkers: true,
     showThresholdMarkers: true,
     showThresholdLabels: false,
     showThresholdLabels: false,
     thresholds: [],
     thresholds: [],
-    theme: GrafanaThemeType.Dark,
   };
   };
 
 
   componentDidMount() {
   componentDidMount() {
@@ -76,7 +75,7 @@ export class Gauge extends PureComponent<Props> {
     const fontSize = Math.min(dimension / 5, 100) * this.getFontScale(value.text.length);
     const fontSize = Math.min(dimension / 5, 100) * this.getFontScale(value.text.length);
     const thresholdLabelFontSize = fontSize / 2.5;
     const thresholdLabelFontSize = fontSize / 2.5;
 
 
-    const options = {
+    const options: any = {
       series: {
       series: {
         gauges: {
         gauges: {
           gauge: {
           gauge: {

+ 1 - 1
packages/grafana-ui/src/components/Select/_Select.scss

@@ -3,7 +3,7 @@ $select-input-bg-disabled: $input-bg-disabled;
 
 
 @mixin select-control() {
 @mixin select-control() {
   width: 100%;
   width: 100%;
-  margin-right: $gf-form-margin;
+  margin-right: $space-xs;
   @include border-radius($input-border-radius-sm);
   @include border-radius($input-border-radius-sm);
   background-color: $input-bg;
   background-color: $input-bg;
 }
 }

+ 17 - 19
packages/grafana-ui/src/themes/_variables.scss.tmpl.ts

@@ -17,7 +17,13 @@ $enable-hover-media-query: false !default;
 // Control the default styling of most Bootstrap elements by modifying these
 // Control the default styling of most Bootstrap elements by modifying these
 // variables. Mostly focused on spacing.
 // variables. Mostly focused on spacing.
 
 
-$spacer: ${theme.spacing.m} !default;
+$space-xxs: ${theme.spacing.xxs} !default;
+$space-xs: ${theme.spacing.xs} !default;
+$space-sm: ${theme.spacing.sm} !default;
+$space-md: ${theme.spacing.md} !default;
+$space-lg: ${theme.spacing.lg} !default;
+$space-xl: ${theme.spacing.xl} !default;
+$spacer: ${theme.spacing.d} !default;
 $spacer-x: $spacer !default;
 $spacer-x: $spacer !default;
 $spacer-y: $spacer !default;
 $spacer-y: $spacer !default;
 $spacers: (
 $spacers: (
@@ -46,7 +52,7 @@ $spacers: (
     ),
     ),
   ),
   ),
 ) !default;
 ) !default;
-$border-width: ${theme.border.width.s} !default;
+$border-width: ${theme.border.width.sm} !default;
 
 
 // Grid breakpoints
 // Grid breakpoints
 //
 //
@@ -55,9 +61,9 @@ $border-width: ${theme.border.width.s} !default;
 
 
 $grid-breakpoints: (
 $grid-breakpoints: (
   xs: ${theme.breakpoints.xs},
   xs: ${theme.breakpoints.xs},
-  sm: ${theme.breakpoints.s},
-  md: ${theme.breakpoints.m},
-  lg: ${theme.breakpoints.l},
+  sm: ${theme.breakpoints.sm},
+  md: ${theme.breakpoints.md},
+  lg: ${theme.breakpoints.lg},
   xl: ${theme.breakpoints.xl},
   xl: ${theme.breakpoints.xl},
 ) !default;
 ) !default;
 
 
@@ -91,12 +97,12 @@ $font-family-base: $font-family-sans-serif !default;
 $font-size-root: ${theme.typography.size.root} !default;
 $font-size-root: ${theme.typography.size.root} !default;
 $font-size-base: ${theme.typography.size.base} !default;
 $font-size-base: ${theme.typography.size.base} !default;
 
 
-$font-size-lg: ${theme.typography.size.l} !default;
-$font-size-md: ${theme.typography.size.m} !default;
-$font-size-sm: ${theme.typography.size.s} !default;
+$font-size-lg: ${theme.typography.size.lg} !default;
+$font-size-md: ${theme.typography.size.md} !default;
+$font-size-sm: ${theme.typography.size.sm} !default;
 $font-size-xs: ${theme.typography.size.xs} !default;
 $font-size-xs: ${theme.typography.size.xs} !default;
 
 
-$line-height-base: ${theme.typography.lineHeight.l} !default;
+$line-height-base: ${theme.typography.lineHeight.lg} !default;
 $font-weight-semi-bold: ${theme.typography.weight.semibold};
 $font-weight-semi-bold: ${theme.typography.weight.semibold};
 
 
 $font-size-h1: ${theme.typography.heading.h1} !default;
 $font-size-h1: ${theme.typography.heading.h1} !default;
@@ -106,10 +112,9 @@ $font-size-h4: ${theme.typography.heading.h4} !default;
 $font-size-h5: ${theme.typography.heading.h5} !default;
 $font-size-h5: ${theme.typography.heading.h5} !default;
 $font-size-h6: ${theme.typography.heading.h6} !default;
 $font-size-h6: ${theme.typography.heading.h6} !default;
 
 
-$headings-margin-bottom: ($spacer / 2) !default;
 $headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
 $headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
 $headings-font-weight: ${theme.typography.weight.normal} !default;
 $headings-font-weight: ${theme.typography.weight.normal} !default;
-$headings-line-height: ${theme.typography.lineHeight.s} !default;
+$headings-line-height: ${theme.typography.lineHeight.sm} !default;
 
 
 $hr-border-width: $border-width !default;
 $hr-border-width: $border-width !default;
 $dt-font-weight: bold !default;
 $dt-font-weight: bold !default;
@@ -160,7 +165,6 @@ $input-padding-y-lg: 10px !default;
 
 
 $input-height: 35px !default;
 $input-height: 35px !default;
 
 
-$gf-form-margin: 3px;
 $gf-form-input-height: 35px;
 $gf-form-input-height: 35px;
 
 
 $cursor-disabled: not-allowed !default;
 $cursor-disabled: not-allowed !default;
@@ -207,8 +211,7 @@ $btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
 $side-menu-width: 60px;
 $side-menu-width: 60px;
 
 
 // dashboard
 // dashboard
-$panel-margin: 10px;
-$dashboard-padding: $panel-margin * 2;
+$dashboard-padding: 10px * 2;
 $panel-horizontal-padding: 10;
 $panel-horizontal-padding: 10;
 $panel-vertical-padding: 5;
 $panel-vertical-padding: 5;
 $panel-padding: 0px $panel-horizontal-padding + 0px $panel-vertical-padding + 0px $panel-horizontal-padding + 0px;
 $panel-padding: 0px $panel-horizontal-padding + 0px $panel-vertical-padding + 0px $panel-horizontal-padding + 0px;
@@ -243,9 +246,4 @@ $external-services: (
     icon: '',
     icon: '',
   ),
   ),
 ) !default;
 ) !default;
-
-:export {
-  panelhorizontalpadding: $panel-horizontal-padding;
-  panelverticalpadding: $panel-vertical-padding;
-}
 `;
 `;

+ 23 - 16
packages/grafana-ui/src/themes/default.ts

@@ -11,9 +11,9 @@ const theme: GrafanaThemeCommons = {
       root: '14px',
       root: '14px',
       base: '13px',
       base: '13px',
       xs: '10px',
       xs: '10px',
-      s: '12px',
-      m: '14px',
-      l: '18px',
+      sm: '12px',
+      md: '14px',
+      lg: '18px',
     },
     },
     heading: {
     heading: {
       h1: '28px',
       h1: '28px',
@@ -30,35 +30,42 @@ const theme: GrafanaThemeCommons = {
     },
     },
     lineHeight: {
     lineHeight: {
       xs: 1,
       xs: 1,
-      s: 1.1,
-      m: 4 / 3,
-      l: 1.5,
+      sm: 1.1,
+      md: 4 / 3,
+      lg: 1.5,
     },
     },
   },
   },
   breakpoints: {
   breakpoints: {
     xs: '0',
     xs: '0',
-    s: '544px',
-    m: '768px',
-    l: '992px',
+    sm: '544px',
+    md: '768px',
+    lg: '992px',
     xl: '1200px',
     xl: '1200px',
   },
   },
   spacing: {
   spacing: {
-    xs: '0',
-    s: '3px',
-    m: '14px',
-    l: '21px',
+    d: '14px',
+    xxs: '2px',
+    xs: '4px',
+    sm: '8px',
+    md: '16px',
+    lg: '24px',
+    xl: '32px',
     gutter: '30px',
     gutter: '30px',
   },
   },
   border: {
   border: {
     radius: {
     radius: {
       xs: '2px',
       xs: '2px',
-      s: '3px',
-      m: '5px',
+      sm: '3px',
+      md: '5px',
     },
     },
     width: {
     width: {
-      s: '1px',
+      sm: '1px',
     },
     },
   },
   },
+  panelPadding: {
+    horizontal: 10,
+    vertical: 5,
+  },
 };
 };
 
 
 export default theme;
 export default theme;

+ 22 - 15
packages/grafana-ui/src/types/theme.ts

@@ -8,9 +8,9 @@ export interface GrafanaThemeCommons {
   // TODO: not sure if should be a part of theme
   // TODO: not sure if should be a part of theme
   breakpoints: {
   breakpoints: {
     xs: string;
     xs: string;
-    s: string;
-    m: string;
-    l: string;
+    sm: string;
+    md: string;
+    lg: string;
     xl: string;
     xl: string;
   };
   };
   typography: {
   typography: {
@@ -22,9 +22,9 @@ export interface GrafanaThemeCommons {
       root: string;
       root: string;
       base: string;
       base: string;
       xs: string;
       xs: string;
-      s: string;
-      m: string;
-      l: string;
+      sm: string;
+      md: string;
+      lg: string;
     };
     };
     weight: {
     weight: {
       light: number;
       light: number;
@@ -33,9 +33,9 @@ export interface GrafanaThemeCommons {
     };
     };
     lineHeight: {
     lineHeight: {
       xs: number; //1
       xs: number; //1
-      s: number; //1.1
-      m: number; // 4/3
-      l: number; // 1.5
+      sm: number; //1.1
+      md: number; // 4/3
+      lg: number; // 1.5
     };
     };
     // TODO: Refactor to use size instead of custom defs
     // TODO: Refactor to use size instead of custom defs
     heading: {
     heading: {
@@ -48,22 +48,29 @@ export interface GrafanaThemeCommons {
     };
     };
   };
   };
   spacing: {
   spacing: {
+    d: string;
+    xxs: string;
     xs: string;
     xs: string;
-    s: string;
-    m: string;
-    l: string;
+    sm: string;
+    md: string;
+    lg: string;
+    xl: string;
     gutter: string;
     gutter: string;
   };
   };
   border: {
   border: {
     radius: {
     radius: {
       xs: string;
       xs: string;
-      s: string;
-      m: string;
+      sm: string;
+      md: string;
     };
     };
     width: {
     width: {
-      s: string;
+      sm: string;
     };
     };
   };
   };
+  panelPadding: {
+    horizontal: number;
+    vertical: number;
+  };
 }
 }
 
 
 export interface GrafanaTheme extends GrafanaThemeCommons {
 export interface GrafanaTheme extends GrafanaThemeCommons {

+ 2 - 2
public/app/core/components/jsontree/jsontree.ts

@@ -10,13 +10,13 @@ coreModule.directive('jsonTree', [
         startExpanded: '@',
         startExpanded: '@',
         rootName: '@',
         rootName: '@',
       },
       },
-      link: (scope, elem) => {
+      link: (scope: any, elem) => {
         const jsonExp = new JsonExplorer(scope.object, 3, {
         const jsonExp = new JsonExplorer(scope.object, 3, {
           animateOpen: true,
           animateOpen: true,
         });
         });
 
 
         const html = jsonExp.render(true);
         const html = jsonExp.render(true);
-        elem.html(html);
+        elem.replaceAll(html);
       },
       },
     };
     };
   },
   },

+ 1 - 1
public/app/core/directives/give_focus.ts

@@ -14,7 +14,7 @@ coreModule.directive('giveFocus', () => {
         }
         }
         setTimeout(() => {
         setTimeout(() => {
           element.focus();
           element.focus();
-          const domEl = element[0];
+          const domEl: any = element[0];
           if (domEl.setSelectionRange) {
           if (domEl.setSelectionRange) {
             const pos = element.val().length * 2;
             const pos = element.val().length * 2;
             domEl.setSelectionRange(pos, pos);
             domEl.setSelectionRange(pos, pos);

+ 0 - 15
public/app/core/specs/kbn.test.ts

@@ -1,15 +0,0 @@
-import kbn from '../utils/kbn';
-
-describe('stringToJsRegex', () => {
-  it('should parse the valid regex value', () => {
-    const output = kbn.stringToJsRegex('/validRegexp/');
-    expect(output).toBeInstanceOf(RegExp);
-  });
-
-  it('should throw error on invalid regex value', () => {
-    const input = '/etc/hostname';
-    expect(() => {
-      kbn.stringToJsRegex(input);
-    }).toThrow();
-  });
-});

+ 6 - 6
public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss

@@ -20,7 +20,7 @@
 
 
   .gicon {
   .gicon {
     font-size: 30px;
     font-size: 30px;
-    margin-right: $spacer;
+    margin-right: $space-md;
   }
   }
 
 
   &:hover {
   &:hover {
@@ -32,16 +32,16 @@
 .add-panel-widget__title {
 .add-panel-widget__title {
   font-size: $font-size-md;
   font-size: $font-size-md;
   font-weight: $font-weight-semi-bold;
   font-weight: $font-weight-semi-bold;
-  margin-right: $spacer * 2;
+  margin-right: $space-xl;
 }
 }
 
 
 .add-panel-widget__link {
 .add-panel-widget__link {
-  margin: 0 8px;
+  margin: 0 $space-sm;
   width: 154px;
   width: 154px;
 }
 }
 
 
 .add-panel-widget__icon {
 .add-panel-widget__icon {
-  margin-bottom: 8px;
+  margin-bottom: $space-sm;
 
 
   .gicon {
   .gicon {
     color: white;
     color: white;
@@ -62,7 +62,7 @@
 
 
 .add-panel-widget__create {
 .add-panel-widget__create {
   display: inherit;
   display: inherit;
-  margin-bottom: 24px;
+  margin-bottom: $space-lg;
   // this is to have the big button appear centered
   // this is to have the big button appear centered
   margin-top: 55px;
   margin-top: 55px;
 }
 }
@@ -72,7 +72,7 @@
 }
 }
 
 
 .add-panel-widget__action {
 .add-panel-widget__action {
-  margin: 0 4px;
+  margin: 0 $space-xs;
 }
 }
 
 
 .add-panel-widget__btn-container {
 .add-panel-widget__btn-container {

+ 0 - 5
public/app/features/dashboard/containers/DashboardPage.test.tsx

@@ -6,11 +6,6 @@ import { cleanUpDashboard } from '../state/actions';
 import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock } from 'app/core/redux';
 import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock } from 'app/core/redux';
 import { DashboardRouteInfo, DashboardInitPhase } from 'app/types';
 import { DashboardRouteInfo, DashboardInitPhase } from 'app/types';
 
 
-jest.mock('sass/_variables.generated.scss', () => ({
-  panelhorizontalpadding: 10,
-  panelVerticalPadding: 10,
-}));
-
 jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({}));
 jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({}));
 
 
 interface ScenarioContext {
 interface ScenarioContext {

+ 1 - 1
public/app/features/dashboard/dashgrid/DataPanel.tsx

@@ -116,7 +116,7 @@ export class DataPanel extends Component<Props, State> {
     this.setState({ loading: LoadingState.Loading });
     this.setState({ loading: LoadingState.Loading });
 
 
     try {
     try {
-      const ds = await this.dataSourceSrv.get(datasource);
+      const ds = await this.dataSourceSrv.get(datasource, scopedVars);
 
 
       // TODO interpolate variables
       // TODO interpolate variables
       const minInterval = this.props.minInterval || ds.interval;
       const minInterval = this.props.minInterval || ds.interval;

+ 0 - 5
public/app/features/dashboard/dashgrid/PanelChrome.test.tsx

@@ -1,10 +1,5 @@
 import { PanelChrome } from './PanelChrome';
 import { PanelChrome } from './PanelChrome';
 
 
-jest.mock('sass/_variables.generated.scss', () => ({
-  panelhorizontalpadding: 10,
-  panelVerticalPadding: 10,
-}));
-
 describe('PanelChrome', () => {
 describe('PanelChrome', () => {
   let chrome: PanelChrome;
   let chrome: PanelChrome;
 
 

+ 3 - 3
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -14,6 +14,7 @@ import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary'
 import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel';
 import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel';
 import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
 import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
 import { profiler } from 'app/core/profiler';
 import { profiler } from 'app/core/profiler';
+import config from 'app/core/config';
 
 
 // Types
 // Types
 import { DashboardModel, PanelModel } from '../state';
 import { DashboardModel, PanelModel } from '../state';
@@ -21,7 +22,6 @@ import { PanelPlugin } from 'app/types';
 import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui';
 import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui';
 import { ScopedVars } from '@grafana/ui';
 import { ScopedVars } from '@grafana/ui';
 
 
-import variables from 'sass/_variables.generated.scss';
 import templateSrv from 'app/features/templating/template_srv';
 import templateSrv from 'app/features/templating/template_srv';
 
 
 const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
 const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
@@ -160,8 +160,8 @@ export class PanelChrome extends PureComponent<Props, State> {
           panelData={panelData}
           panelData={panelData}
           timeRange={timeRange}
           timeRange={timeRange}
           options={panel.getOptions(plugin.exports.reactPanel.defaults)}
           options={panel.getOptions(plugin.exports.reactPanel.defaults)}
-          width={width - 2 * variables.panelhorizontalpadding}
-          height={height - PANEL_HEADER_HEIGHT - variables.panelverticalpadding}
+          width={width - 2 * config.theme.panelPadding.horizontal}
+          height={height - PANEL_HEADER_HEIGHT - config.theme.panelPadding.vertical}
           renderCounter={renderCounter}
           renderCounter={renderCounter}
           replaceVariables={this.replaceVariables}
           replaceVariables={this.replaceVariables}
         />
         />

+ 1 - 1
public/app/features/datasources/settings/HttpSettingsCtrl.ts

@@ -9,7 +9,7 @@ coreModule.directive('datasourceHttpSettings', () => {
     },
     },
     templateUrl: 'public/app/features/datasources/partials/http_settings.html',
     templateUrl: 'public/app/features/datasources/partials/http_settings.html',
     link: {
     link: {
-      pre: ($scope, elem, attrs) => {
+      pre: ($scope: any, elem, attrs) => {
         // do not show access option if direct access is disabled
         // do not show access option if direct access is disabled
         $scope.showAccessOption = $scope.noDirectAccess !== 'true';
         $scope.showAccessOption = $scope.noDirectAccess !== 'true';
         $scope.showAccessHelp = false;
         $scope.showAccessHelp = false;

+ 1 - 1
public/app/features/panel/metrics_panel_ctrl.ts

@@ -81,7 +81,7 @@ class MetricsPanelCtrl extends PanelCtrl {
 
 
     // load datasource service
     // load datasource service
     this.datasourceSrv
     this.datasourceSrv
-      .get(this.panel.datasource)
+      .get(this.panel.datasource, this.panel.scopedVars)
       .then(this.updateTimeRange.bind(this))
       .then(this.updateTimeRange.bind(this))
       .then(this.issueQueries.bind(this))
       .then(this.issueQueries.bind(this))
       .then(this.handleQueryResult.bind(this))
       .then(this.handleQueryResult.bind(this))

+ 2 - 2
public/app/features/panel/panel_directive.ts

@@ -33,7 +33,7 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
     template: panelTemplate,
     template: panelTemplate,
     transclude: true,
     transclude: true,
     scope: { ctrl: '=' },
     scope: { ctrl: '=' },
-    link: (scope, elem) => {
+    link: (scope: any, elem) => {
       const panelContainer = elem.find('.panel-container');
       const panelContainer = elem.find('.panel-container');
       const panelContent = elem.find('.panel-content');
       const panelContent = elem.find('.panel-content');
       const cornerInfoElem = elem.find('.panel-info-corner');
       const cornerInfoElem = elem.find('.panel-info-corner');
@@ -67,7 +67,7 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
       // set initial transparency
       // set initial transparency
       if (ctrl.panel.transparent) {
       if (ctrl.panel.transparent) {
         transparentLastState = true;
         transparentLastState = true;
-        panelContainer.addClass('panel-transparent', true);
+        panelContainer.addClass('panel-transparent');
       }
       }
 
 
       // update scrollbar after mounting
       // update scrollbar after mounting

+ 9 - 3
public/app/features/plugins/datasource_srv.ts

@@ -7,7 +7,7 @@ import config from 'app/core/config';
 import { importPluginModule } from './plugin_loader';
 import { importPluginModule } from './plugin_loader';
 
 
 // Types
 // Types
-import { DataSourceApi, DataSourceSelectItem } from '@grafana/ui/src/types';
+import { DataSourceApi, DataSourceSelectItem, ScopedVars } from '@grafana/ui/src/types';
 
 
 export class DatasourceSrv {
 export class DatasourceSrv {
   datasources: { [name: string]: DataSourceApi };
   datasources: { [name: string]: DataSourceApi };
@@ -21,12 +21,18 @@ export class DatasourceSrv {
     this.datasources = {};
     this.datasources = {};
   }
   }
 
 
-  get(name?: string): Promise<DataSourceApi> {
+  get(name?: string, scopedVars?: ScopedVars): Promise<DataSourceApi> {
     if (!name) {
     if (!name) {
       return this.get(config.defaultDatasource);
       return this.get(config.defaultDatasource);
     }
     }
 
 
-    name = this.templateSrv.replace(name);
+    // Interpolation here is to support template variable in data source selection
+    name = this.templateSrv.replace(name, scopedVars, (value, variable) => {
+      if (Array.isArray(value)) {
+        return value[0];
+      }
+      return value;
+    });
 
 
     if (name === 'default') {
     if (name === 'default') {
       return this.get(config.defaultDatasource);
       return this.get(config.defaultDatasource);

+ 15 - 0
public/app/features/templating/datasource_variable.ts

@@ -6,6 +6,8 @@ export class DatasourceVariable implements Variable {
   query: string;
   query: string;
   options: any;
   options: any;
   current: any;
   current: any;
+  multi: boolean;
+  includeAll: boolean;
   refresh: any;
   refresh: any;
   skipUrlSync: boolean;
   skipUrlSync: boolean;
 
 
@@ -18,6 +20,8 @@ export class DatasourceVariable implements Variable {
     regex: '',
     regex: '',
     options: [],
     options: [],
     query: '',
     query: '',
+    multi: false,
+    includeAll: false,
     refresh: 1,
     refresh: 1,
     skipUrlSync: false,
     skipUrlSync: false,
   };
   };
@@ -69,9 +73,16 @@ export class DatasourceVariable implements Variable {
     }
     }
 
 
     this.options = options;
     this.options = options;
+    if (this.includeAll) {
+      this.addAllOption();
+    }
     return this.variableSrv.validateVariableSelectionState(this);
     return this.variableSrv.validateVariableSelectionState(this);
   }
   }
 
 
+  addAllOption() {
+    this.options.unshift({ text: 'All', value: '$__all' });
+  }
+
   dependsOn(variable) {
   dependsOn(variable) {
     if (this.regex) {
     if (this.regex) {
       return containsVariable(this.regex, variable.name);
       return containsVariable(this.regex, variable.name);
@@ -84,6 +95,9 @@ export class DatasourceVariable implements Variable {
   }
   }
 
 
   getValueForUrl() {
   getValueForUrl() {
+    if (this.current.text === 'All') {
+      return 'All';
+    }
     return this.current.value;
     return this.current.value;
   }
   }
 }
 }
@@ -91,5 +105,6 @@ export class DatasourceVariable implements Variable {
 variableTypes['datasource'] = {
 variableTypes['datasource'] = {
   name: 'Datasource',
   name: 'Datasource',
   ctor: DatasourceVariable,
   ctor: DatasourceVariable,
+  supportsMulti: true,
   description: 'Enabled you to dynamically switch the datasource for multiple panels',
   description: 'Enabled you to dynamically switch the datasource for multiple panels',
 };
 };

+ 5 - 4
public/app/plugins/datasource/prometheus/result_transformer.ts

@@ -1,5 +1,6 @@
 import _ from 'lodash';
 import _ from 'lodash';
 import TableModel from 'app/core/table_model';
 import TableModel from 'app/core/table_model';
+import { TimeSeries } from '@grafana/ui';
 
 
 export class ResultTransformer {
 export class ResultTransformer {
   constructor(private templateSrv) {}
   constructor(private templateSrv) {}
@@ -18,10 +19,10 @@ export class ResultTransformer {
       ];
       ];
     } else if (prometheusResult && options.format === 'heatmap') {
     } else if (prometheusResult && options.format === 'heatmap') {
       let seriesList = [];
       let seriesList = [];
-      prometheusResult.sort(sortSeriesByLabel);
       for (const metricData of prometheusResult) {
       for (const metricData of prometheusResult) {
         seriesList.push(this.transformMetricData(metricData, options, options.start, options.end));
         seriesList.push(this.transformMetricData(metricData, options, options.start, options.end));
       }
       }
+      seriesList.sort(sortSeriesByLabel);
       seriesList = this.transformToHistogramOverTime(seriesList);
       seriesList = this.transformToHistogramOverTime(seriesList);
       return seriesList;
       return seriesList;
     } else if (prometheusResult) {
     } else if (prometheusResult) {
@@ -197,13 +198,13 @@ export class ResultTransformer {
   }
   }
 }
 }
 
 
-function sortSeriesByLabel(s1, s2): number {
+function sortSeriesByLabel(s1: TimeSeries, s2: TimeSeries): number {
   let le1, le2;
   let le1, le2;
 
 
   try {
   try {
     // fail if not integer. might happen with bad queries
     // fail if not integer. might happen with bad queries
-    le1 = parseHistogramLabel(s1.metric.le);
-    le2 = parseHistogramLabel(s2.metric.le);
+    le1 = parseHistogramLabel(s1.target);
+    le2 = parseHistogramLabel(s2.target);
   } catch (err) {
   } catch (err) {
     console.log(err);
     console.log(err);
     return 0;
     return 0;

+ 21 - 24
public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts

@@ -2,28 +2,6 @@ import coreModule from 'app/core/core_module';
 import _ from 'lodash';
 import _ from 'lodash';
 import { FilterSegments, DefaultFilterValue } from './filter_segments';
 import { FilterSegments, DefaultFilterValue } from './filter_segments';
 
 
-export class StackdriverFilter {
-  /** @ngInject */
-  constructor() {
-    return {
-      templateUrl: 'public/app/plugins/datasource/stackdriver/partials/query.filter.html',
-      controller: 'StackdriverFilterCtrl',
-      controllerAs: 'ctrl',
-      bindToController: true,
-      restrict: 'E',
-      scope: {
-        labelData: '<',
-        loading: '<',
-        groupBys: '<',
-        filters: '<',
-        filtersChanged: '&',
-        groupBysChanged: '&',
-        hideGroupBys: '<',
-      },
-    };
-  }
-}
-
 export class StackdriverFilterCtrl {
 export class StackdriverFilterCtrl {
   defaultRemoveGroupByValue = '-- remove group by --';
   defaultRemoveGroupByValue = '-- remove group by --';
   resourceTypeValue = 'resource.type';
   resourceTypeValue = 'resource.type';
@@ -193,5 +171,24 @@ export class StackdriverFilterCtrl {
   }
   }
 }
 }
 
 
-coreModule.directive('stackdriverFilter', StackdriverFilter);
-coreModule.controller('StackdriverFilterCtrl', StackdriverFilterCtrl);
+/** @ngInject */
+function stackdriverFilter() {
+  return {
+    templateUrl: 'public/app/plugins/datasource/stackdriver/partials/query.filter.html',
+    controller: StackdriverFilterCtrl,
+    controllerAs: 'ctrl',
+    bindToController: true,
+    restrict: 'E',
+    scope: {
+      labelData: '<',
+      loading: '<',
+      groupBys: '<',
+      filters: '<',
+      filtersChanged: '&',
+      groupBysChanged: '&',
+      hideGroupBys: '<',
+    },
+  };
+}
+
+coreModule.directive('stackdriverFilter', stackdriverFilter);

+ 1 - 0
public/app/plugins/panel/heatmap/axes_editor.ts

@@ -32,6 +32,7 @@ export class AxesEditorCtrl {
       Auto: 'auto',
       Auto: 'auto',
       Upper: 'upper',
       Upper: 'upper',
       Lower: 'lower',
       Lower: 'lower',
+      Middle: 'middle',
     };
     };
   }
   }
 
 

+ 20 - 18
public/app/plugins/panel/heatmap/color_legend.ts

@@ -11,6 +11,8 @@ const LEGEND_HEIGHT_PX = 6;
 const LEGEND_WIDTH_PX = 100;
 const LEGEND_WIDTH_PX = 100;
 const LEGEND_TICK_SIZE = 0;
 const LEGEND_TICK_SIZE = 0;
 const LEGEND_VALUE_MARGIN = 0;
 const LEGEND_VALUE_MARGIN = 0;
+const LEGEND_PADDING_LEFT = 10;
+const LEGEND_SEGMENT_WIDTH = 10;
 
 
 /**
 /**
  * Color legend for heatmap editor.
  * Color legend for heatmap editor.
@@ -19,7 +21,7 @@ coreModule.directive('colorLegend', () => {
   return {
   return {
     restrict: 'E',
     restrict: 'E',
     template: '<div class="heatmap-color-legend"><svg width="16.5rem" height="24px"></svg></div>',
     template: '<div class="heatmap-color-legend"><svg width="16.5rem" height="24px"></svg></div>',
-    link: (scope, elem, attrs) => {
+    link: (scope: any, elem, attrs) => {
       const ctrl = scope.ctrl;
       const ctrl = scope.ctrl;
       const panel = scope.ctrl.panel;
       const panel = scope.ctrl.panel;
 
 
@@ -55,7 +57,7 @@ coreModule.directive('heatmapLegend', () => {
   return {
   return {
     restrict: 'E',
     restrict: 'E',
     template: `<div class="heatmap-color-legend"><svg width="${LEGEND_WIDTH_PX}px" height="${LEGEND_HEIGHT_PX}px"></svg></div>`,
     template: `<div class="heatmap-color-legend"><svg width="${LEGEND_WIDTH_PX}px" height="${LEGEND_HEIGHT_PX}px"></svg></div>`,
-    link: (scope, elem, attrs) => {
+    link: (scope: any, elem, attrs) => {
       const ctrl = scope.ctrl;
       const ctrl = scope.ctrl;
       const panel = scope.ctrl.panel;
       const panel = scope.ctrl.panel;
 
 
@@ -95,27 +97,27 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal
   const legendWidth = Math.floor(legendElem.outerWidth()) - 30;
   const legendWidth = Math.floor(legendElem.outerWidth()) - 30;
   const legendHeight = legendElem.attr('height');
   const legendHeight = legendElem.attr('height');
 
 
-  let rangeStep = 1;
-  if (rangeTo - rangeFrom > legendWidth) {
-    rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
-  }
+  const rangeStep = ((rangeTo - rangeFrom) / legendWidth) * LEGEND_SEGMENT_WIDTH;
   const widthFactor = legendWidth / (rangeTo - rangeFrom);
   const widthFactor = legendWidth / (rangeTo - rangeFrom);
   const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
   const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
 
 
   const colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
   const colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
   legend
   legend
+    .append('g')
+    .attr('class', 'legend-color-bar')
+    .attr('transform', 'translate(' + LEGEND_PADDING_LEFT + ',0)')
     .selectAll('.heatmap-color-legend-rect')
     .selectAll('.heatmap-color-legend-rect')
     .data(valuesRange)
     .data(valuesRange)
     .enter()
     .enter()
     .append('rect')
     .append('rect')
-    .attr('x', d => d * widthFactor)
+    .attr('x', d => Math.round(d * widthFactor))
     .attr('y', 0)
     .attr('y', 0)
-    .attr('width', rangeStep * widthFactor + 1) // Overlap rectangles to prevent gaps
+    .attr('width', Math.round(rangeStep * widthFactor + 1)) // Overlap rectangles to prevent gaps
     .attr('height', legendHeight)
     .attr('height', legendHeight)
     .attr('stroke-width', 0)
     .attr('stroke-width', 0)
     .attr('fill', d => colorScale(d));
     .attr('fill', d => colorScale(d));
 
 
-  drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth);
+  drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWidth, valuesRange);
 }
 }
 
 
 function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue) {
 function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue) {
@@ -126,31 +128,31 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue
   const legendWidth = Math.floor(legendElem.outerWidth()) - 30;
   const legendWidth = Math.floor(legendElem.outerWidth()) - 30;
   const legendHeight = legendElem.attr('height');
   const legendHeight = legendElem.attr('height');
 
 
-  let rangeStep = 1;
-  if (rangeTo - rangeFrom > legendWidth) {
-    rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
-  }
+  const rangeStep = ((rangeTo - rangeFrom) / legendWidth) * LEGEND_SEGMENT_WIDTH;
   const widthFactor = legendWidth / (rangeTo - rangeFrom);
   const widthFactor = legendWidth / (rangeTo - rangeFrom);
   const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
   const valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
 
 
   const opacityScale = getOpacityScale(options, maxValue, minValue);
   const opacityScale = getOpacityScale(options, maxValue, minValue);
   legend
   legend
+    .append('g')
+    .attr('class', 'legend-color-bar')
+    .attr('transform', 'translate(' + LEGEND_PADDING_LEFT + ',0)')
     .selectAll('.heatmap-opacity-legend-rect')
     .selectAll('.heatmap-opacity-legend-rect')
     .data(valuesRange)
     .data(valuesRange)
     .enter()
     .enter()
     .append('rect')
     .append('rect')
-    .attr('x', d => d * widthFactor)
+    .attr('x', d => Math.round(d * widthFactor))
     .attr('y', 0)
     .attr('y', 0)
-    .attr('width', rangeStep * widthFactor)
+    .attr('width', Math.round(rangeStep * widthFactor))
     .attr('height', legendHeight)
     .attr('height', legendHeight)
     .attr('stroke-width', 0)
     .attr('stroke-width', 0)
     .attr('fill', options.cardColor)
     .attr('fill', options.cardColor)
     .style('opacity', d => opacityScale(d));
     .style('opacity', d => opacityScale(d));
 
 
-  drawLegendValues(elem, opacityScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth);
+  drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWidth, valuesRange);
 }
 }
 
 
-function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth) {
+function drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWidth, valuesRange) {
   const legendElem = $(elem).find('svg');
   const legendElem = $(elem).find('svg');
   const legend = d3.select(legendElem.get(0));
   const legend = d3.select(legendElem.get(0));
 
 
@@ -171,7 +173,7 @@ function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minVal
 
 
   const colorRect = legendElem.find(':first-child');
   const colorRect = legendElem.find(':first-child');
   const posY = getSvgElemHeight(legendElem) + LEGEND_VALUE_MARGIN;
   const posY = getSvgElemHeight(legendElem) + LEGEND_VALUE_MARGIN;
-  const posX = getSvgElemX(colorRect);
+  const posX = getSvgElemX(colorRect) + LEGEND_PADDING_LEFT;
 
 
   d3.select(legendElem.get(0))
   d3.select(legendElem.get(0))
     .append('g')
     .append('g')

+ 16 - 7
public/app/plugins/panel/heatmap/heatmap_ctrl.ts

@@ -34,6 +34,7 @@ const panelDefaults = {
   },
   },
   dataFormat: 'timeseries',
   dataFormat: 'timeseries',
   yBucketBound: 'auto',
   yBucketBound: 'auto',
+  reverseYBuckets: false,
   xAxis: {
   xAxis: {
     show: true,
     show: true,
   },
   },
@@ -55,6 +56,7 @@ const panelDefaults = {
     showHistogram: false,
     showHistogram: false,
   },
   },
   highlightCards: true,
   highlightCards: true,
+  hideZeroBuckets: false,
 };
 };
 
 
 const colorModes = ['opacity', 'spectrum'];
 const colorModes = ['opacity', 'spectrum'];
@@ -97,7 +99,7 @@ const colorSchemes = [
   { name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'dark' },
   { name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'dark' },
 ];
 ];
 
 
-const dsSupportHistogramSort = ['prometheus', 'elasticsearch'];
+const dsSupportHistogramSort = ['elasticsearch'];
 
 
 export class HeatmapCtrl extends MetricsPanelCtrl {
 export class HeatmapCtrl extends MetricsPanelCtrl {
   static templateUrl = 'module.html';
   static templateUrl = 'module.html';
@@ -108,7 +110,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
   selectionActivated: boolean;
   selectionActivated: boolean;
   unitFormats: any;
   unitFormats: any;
   data: any;
   data: any;
-  series: any;
+  series: any[];
   timeSrv: any;
   timeSrv: any;
   dataWarning: any;
   dataWarning: any;
   decimals: number;
   decimals: number;
@@ -146,7 +148,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
   }
   }
 
 
   onRender() {
   onRender() {
-    if (!this.range) {
+    if (!this.range || !this.series) {
       return;
       return;
     }
     }
 
 
@@ -204,7 +206,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
       yBucketSize = 1;
       yBucketSize = 1;
     }
     }
 
 
-    const { cards, cardStats } = convertToCards(bucketsData);
+    const { cards, cardStats } = convertToCards(bucketsData, this.panel.hideZeroBuckets);
 
 
     this.data = {
     this.data = {
       buckets: bucketsData,
       buckets: bucketsData,
@@ -225,13 +227,20 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
       this.series.sort(sortSeriesByLabel);
       this.series.sort(sortSeriesByLabel);
     }
     }
 
 
+    if (this.panel.reverseYBuckets) {
+      this.series.reverse();
+    }
+
     // Convert histogram to heatmap. Each histogram bucket represented by the series which name is
     // Convert histogram to heatmap. Each histogram bucket represented by the series which name is
-    // a top (or bottom, depends of datasource) bucket bound. Further, these values will be used as X axis labels.
+    // a top (or bottom, depends of datasource) bucket bound. Further, these values will be used as Y axis labels.
     bucketsData = histogramToHeatmap(this.series);
     bucketsData = histogramToHeatmap(this.series);
 
 
     tsBuckets = _.map(this.series, 'label');
     tsBuckets = _.map(this.series, 'label');
     const yBucketBound = this.panel.yBucketBound;
     const yBucketBound = this.panel.yBucketBound;
-    if ((panelDatasource === 'prometheus' && yBucketBound !== 'lower') || yBucketBound === 'upper') {
+    if (
+      (panelDatasource === 'prometheus' && yBucketBound !== 'lower' && yBucketBound !== 'middle') ||
+      yBucketBound === 'upper'
+    ) {
       // Prometheus labels are upper inclusive bounds, so add empty bottom bucket label.
       // Prometheus labels are upper inclusive bounds, so add empty bottom bucket label.
       tsBuckets = [''].concat(tsBuckets);
       tsBuckets = [''].concat(tsBuckets);
     } else {
     } else {
@@ -246,7 +255,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
     // Always let yBucketSize=1 in 'tsbuckets' mode
     // Always let yBucketSize=1 in 'tsbuckets' mode
     yBucketSize = 1;
     yBucketSize = 1;
 
 
-    const { cards, cardStats } = convertToCards(bucketsData);
+    const { cards, cardStats } = convertToCards(bucketsData, this.panel.hideZeroBuckets);
 
 
     this.data = {
     this.data = {
       buckets: bucketsData,
       buckets: bucketsData,

+ 23 - 5
public/app/plugins/panel/heatmap/heatmap_data_converter.ts

@@ -93,25 +93,43 @@ function parseHistogramLabel(label: string): number {
   return value;
   return value;
 }
 }
 
 
+interface HeatmapCard {
+  x: number;
+  y: number;
+  yBounds: {
+    top: number | null;
+    bottom: number | null;
+  };
+  values: number[];
+  count: number;
+}
+
+interface HeatmapCardStats {
+  min: number;
+  max: number;
+}
+
 /**
 /**
  * Convert buckets into linear array of "cards" - objects, represented heatmap elements.
  * Convert buckets into linear array of "cards" - objects, represented heatmap elements.
  * @param  {Object} buckets
  * @param  {Object} buckets
- * @return {Array}          Array of "card" objects
+ * @return {Object}          Array of "card" objects and stats
  */
  */
-function convertToCards(buckets) {
+function convertToCards(buckets: any, hideZero = false): { cards: HeatmapCard[]; cardStats: HeatmapCardStats } {
   let min = 0,
   let min = 0,
     max = 0;
     max = 0;
-  const cards = [];
+  const cards: HeatmapCard[] = [];
   _.forEach(buckets, xBucket => {
   _.forEach(buckets, xBucket => {
     _.forEach(xBucket.buckets, yBucket => {
     _.forEach(xBucket.buckets, yBucket => {
-      const card = {
+      const card: HeatmapCard = {
         x: xBucket.x,
         x: xBucket.x,
         y: yBucket.y,
         y: yBucket.y,
         yBounds: yBucket.bounds,
         yBounds: yBucket.bounds,
         values: yBucket.values,
         values: yBucket.values,
         count: yBucket.count,
         count: yBucket.count,
       };
       };
-      cards.push(card);
+      if (!hideZero || card.count !== 0) {
+        cards.push(card);
+      }
 
 
       if (cards.length === 1) {
       if (cards.length === 1) {
         min = yBucket.count;
         min = yBucket.count;

+ 5 - 2
public/app/plugins/panel/heatmap/heatmap_tooltip.ts

@@ -114,7 +114,9 @@ export class HeatmapTooltip {
           };
           };
 
 
           boundBottom = tickFormatter(yBucketIndex);
           boundBottom = tickFormatter(yBucketIndex);
-          boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tickFormatter(yBucketIndex + 1) : '';
+          if (this.panel.yBucketBound !== 'middle') {
+            boundTop = yBucketIndex < data.tsBuckets.length - 1 ? tickFormatter(yBucketIndex + 1) : '';
+          }
         } else {
         } else {
           // Display 0 if bucket is a special 'zero' bucket
           // Display 0 if bucket is a special 'zero' bucket
           const bottom = yData.y ? yData.bounds.bottom : 0;
           const bottom = yData.y ? yData.bounds.bottom : 0;
@@ -122,8 +124,9 @@ export class HeatmapTooltip {
           boundTop = bucketBoundFormatter(yData.bounds.top);
           boundTop = bucketBoundFormatter(yData.bounds.top);
         }
         }
         valuesNumber = countValueFormatter(yData.count);
         valuesNumber = countValueFormatter(yData.count);
+        const boundStr = boundTop && boundBottom ? `${boundBottom} - ${boundTop}` : boundBottom || boundTop;
         tooltipHtml += `<div>
         tooltipHtml += `<div>
-          bucket: <b>${boundBottom} - ${boundTop}</b> <br>
+          bucket: <b>${boundStr}</b> <br>
           count: <b>${valuesNumber}</b> <br>
           count: <b>${valuesNumber}</b> <br>
         </div>`;
         </div>`;
       } else {
       } else {

+ 5 - 0
public/app/plugins/panel/heatmap/partials/axes_editor.html

@@ -40,6 +40,11 @@
         </select>
         </select>
       </div>
       </div>
     </div>
     </div>
+    <gf-form-switch ng-if="ctrl.panel.dataFormat == 'tsbuckets'"
+      class="gf-form" label-class="width-8"
+      label="Reverse order"
+      checked="ctrl.panel.reverseYBuckets" on-change="ctrl.refresh()">
+    </gf-form-switch>
   </div>
   </div>
 
 
   <div class="section gf-form-group" ng-if="ctrl.panel.dataFormat == 'timeseries'">
   <div class="section gf-form-group" ng-if="ctrl.panel.dataFormat == 'timeseries'">

+ 4 - 0
public/app/plugins/panel/heatmap/partials/display_editor.html

@@ -63,6 +63,10 @@
 
 
   <div class="section gf-form-group">
   <div class="section gf-form-group">
     <h5 class="section-heading">Buckets</h5>
     <h5 class="section-heading">Buckets</h5>
+    <gf-form-switch class="gf-form" label-class="width-8"
+      label="Hide zero"
+      checked="ctrl.panel.hideZeroBuckets" on-change="ctrl.render()">
+    </gf-form-switch>
     <div class="gf-form">
     <div class="gf-form">
       <label class="gf-form-label width-8">Space</label>
       <label class="gf-form-label width-8">Space</label>
       <input type="number" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-model="ctrl.panel.cards.cardPadding" ng-change="ctrl.refresh()" ng-model-onblur>
       <input type="number" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-model="ctrl.panel.cards.cardPadding" ng-change="ctrl.refresh()" ng-model-onblur>

+ 8 - 2
public/app/plugins/panel/heatmap/rendering.ts

@@ -379,6 +379,12 @@ export class HeatmapRenderer {
     const posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
     const posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
     this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
     this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
 
 
+    if (this.panel.yBucketBound === 'middle' && tickValues && tickValues.length) {
+      // Shift Y axis labels to the middle of bucket
+      const tickShift = 0 - this.chartHeight / (tickValues.length - 1) / 2;
+      this.heatmap.selectAll('.axis-y text').attr('transform', 'translate(' + 0 + ',' + tickShift + ')');
+    }
+
     // Remove vertical line in the right of axis labels (called domain in d3)
     // Remove vertical line in the right of axis labels (called domain in d3)
     this.heatmap
     this.heatmap
       .select('.axis-y')
       .select('.axis-y')
@@ -615,8 +621,8 @@ export class HeatmapRenderer {
       w = this.cardWidth;
       w = this.cardWidth;
     }
     }
 
 
-    // Card width should be MIN_CARD_SIZE at least
-    w = Math.max(w, MIN_CARD_SIZE);
+    // Card width should be MIN_CARD_SIZE at least, but cut cards shouldn't be displayed
+    w = w > 0 ? Math.max(w, MIN_CARD_SIZE) : 0;
     return w;
     return w;
   }
   }
 
 

+ 7 - 9
public/sass/_variables.generated.scss

@@ -20,6 +20,12 @@ $enable-hover-media-query: false !default;
 // Control the default styling of most Bootstrap elements by modifying these
 // Control the default styling of most Bootstrap elements by modifying these
 // variables. Mostly focused on spacing.
 // variables. Mostly focused on spacing.
 
 
+$space-xxs: 2px !default;
+$space-xs: 4px !default;
+$space-sm: 8px !default;
+$space-md: 16px !default;
+$space-lg: 24px !default;
+$space-xl: 32px !default;
 $spacer: 14px !default;
 $spacer: 14px !default;
 $spacer-x: $spacer !default;
 $spacer-x: $spacer !default;
 $spacer-y: $spacer !default;
 $spacer-y: $spacer !default;
@@ -109,7 +115,6 @@ $font-size-h4: 18px !default;
 $font-size-h5: 16px !default;
 $font-size-h5: 16px !default;
 $font-size-h6: 14px !default;
 $font-size-h6: 14px !default;
 
 
-$headings-margin-bottom: ($spacer / 2) !default;
 $headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
 $headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
 $headings-font-weight: 400 !default;
 $headings-font-weight: 400 !default;
 $headings-line-height: 1.1 !default;
 $headings-line-height: 1.1 !default;
@@ -163,7 +168,6 @@ $input-padding-y-lg: 10px !default;
 
 
 $input-height: 35px !default;
 $input-height: 35px !default;
 
 
-$gf-form-margin: 3px;
 $gf-form-input-height: 35px;
 $gf-form-input-height: 35px;
 
 
 $cursor-disabled: not-allowed !default;
 $cursor-disabled: not-allowed !default;
@@ -210,8 +214,7 @@ $btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
 $side-menu-width: 60px;
 $side-menu-width: 60px;
 
 
 // dashboard
 // dashboard
-$panel-margin: 10px;
-$dashboard-padding: $panel-margin * 2;
+$dashboard-padding: 10px * 2;
 $panel-horizontal-padding: 10;
 $panel-horizontal-padding: 10;
 $panel-vertical-padding: 5;
 $panel-vertical-padding: 5;
 $panel-padding: 0px $panel-horizontal-padding + 0px $panel-vertical-padding + 0px $panel-horizontal-padding + 0px;
 $panel-padding: 0px $panel-horizontal-padding + 0px $panel-vertical-padding + 0px $panel-horizontal-padding + 0px;
@@ -246,8 +249,3 @@ $external-services: (
     icon: '',
     icon: '',
   ),
   ),
 ) !default;
 ) !default;
-
-:export {
-  panelhorizontalpadding: $panel-horizontal-padding;
-  panelverticalpadding: $panel-vertical-padding;
-}

+ 0 - 8
public/sass/_variables.generated.scss.d.ts

@@ -1,8 +0,0 @@
-export interface GrafanaVariables {
-  panelhorizontalpadding: number;
-  panelverticalpadding: number;
-}
-
-declare const variables: GrafanaVariables;
-
-export default variables;

+ 1 - 1
public/sass/base/_type.scss

@@ -109,7 +109,7 @@ h6,
 .h4,
 .h4,
 .h5,
 .h5,
 .h6 {
 .h6 {
-  margin-bottom: $headings-margin-bottom;
+  margin-bottom: $space-sm;
   font-family: $headings-font-family;
   font-family: $headings-font-family;
   font-weight: $headings-font-weight;
   font-weight: $headings-font-weight;
   line-height: $headings-line-height;
   line-height: $headings-line-height;

+ 3 - 3
public/sass/components/_add_data_source.scss

@@ -1,5 +1,5 @@
 .add-data-source-header {
 .add-data-source-header {
-  margin-bottom: $spacer * 2;
+  margin-bottom: $space-xl;
   padding-top: $spacer;
   padding-top: $spacer;
   text-align: center;
   text-align: center;
 }
 }
@@ -7,7 +7,7 @@
 .add-data-source-search {
 .add-data-source-search {
   display: flex;
   display: flex;
   justify-content: center;
   justify-content: center;
-  margin-bottom: $panel-margin * 2;
+  margin-bottom: $space-lg;
 }
 }
 
 
 .add-data-source-grid {
 .add-data-source-grid {
@@ -41,6 +41,6 @@
 }
 }
 
 
 .add-data-source-grid-item-logo {
 .add-data-source-grid-item-logo {
-  margin: 0 15px;
+  margin: 0 $space-md;
   width: 55px;
   width: 55px;
 }
 }

+ 1 - 1
public/sass/components/_alerts.scss

@@ -7,7 +7,7 @@
 
 
 .alert {
 .alert {
   padding: 15px 20px;
   padding: 15px 20px;
-  margin-bottom: $panel-margin / 2;
+  margin-bottom: $space-xs;
   text-shadow: 0 2px 0 rgba(255, 255, 255, 0.5);
   text-shadow: 0 2px 0 rgba(255, 255, 255, 0.5);
   background: $alert-error-bg;
   background: $alert-error-bg;
   position: relative;
   position: relative;

+ 14 - 21
public/sass/components/_cards.scss

@@ -1,7 +1,7 @@
 .layout-selector {
 .layout-selector {
   @include clearfix();
   @include clearfix();
 
 
-  margin-left: $spacer;
+  margin-left: $space-md;
   text-align: right;
   text-align: right;
 
 
   button {
   button {
@@ -9,7 +9,7 @@
     color: $text-color-weak;
     color: $text-color-weak;
     box-shadow: $card-shadow;
     box-shadow: $card-shadow;
     border: none;
     border: none;
-    padding: 0.5rem;
+    padding: $space-sm;
     line-height: 1;
     line-height: 1;
     font-size: 130%;
     font-size: 130%;
     float: right;
     float: right;
@@ -35,7 +35,7 @@
 }
 }
 
 
 .card-section {
 .card-section {
-  margin-bottom: $spacer * 2;
+  margin-bottom: $space-xl;
 }
 }
 
 
 .card-list {
 .card-list {
@@ -50,7 +50,7 @@
   height: 100%;
   height: 100%;
   background: $card-background;
   background: $card-background;
   box-shadow: $card-shadow;
   box-shadow: $card-shadow;
-  padding: 1rem;
+  padding: $space-md;
   border-radius: 4px;
   border-radius: 4px;
 
 
   &:hover {
   &:hover {
@@ -58,7 +58,7 @@
   }
   }
 
 
   .label-tag {
   .label-tag {
-    margin-left: 6px;
+    margin-left: $space-sm;
     font-size: 11px;
     font-size: 11px;
     padding: 2px 6px;
     padding: 2px 6px;
   }
   }
@@ -80,15 +80,8 @@
   overflow: hidden;
   overflow: hidden;
 }
 }
 
 
-.card-item-cog {
-  font-size: 130%;
-  position: relative;
-  top: 1rem;
-  color: $text-muted;
-}
-
 .card-item-header {
 .card-item-header {
-  margin-bottom: $spacer;
+  margin-bottom: $space-md;
 }
 }
 
 
 .card-item-type {
 .card-item-type {
@@ -110,7 +103,7 @@
 }
 }
 
 
 .card-item-label {
 .card-item-label {
-  margin-left: 8px;
+  margin-left: $space-sm;
 }
 }
 
 
 .card-item-sub-name {
 .card-item-sub-name {
@@ -123,7 +116,7 @@
 .card-item-sub-name--header {
 .card-item-sub-name--header {
   color: $text-color-weak;
   color: $text-color-weak;
   text-transform: uppercase;
   text-transform: uppercase;
-  margin-bottom: $spacer;
+  margin-bottom: $space-md;
   font-size: $font-size-sm;
   font-size: $font-size-sm;
   font-weight: bold;
   font-weight: bold;
 }
 }
@@ -136,7 +129,7 @@
   .card-item-notice {
   .card-item-notice {
     font-size: $font-size-sm;
     font-size: $font-size-sm;
     display: inline-block;
     display: inline-block;
-    margin-left: $spacer;
+    margin-left: $space-md;
   }
   }
 
 
   .card-item-header-action {
   .card-item-header-action {
@@ -145,7 +138,7 @@
 
 
   .card-item-wrapper {
   .card-item-wrapper {
     width: 100%;
     width: 100%;
-    padding: 0 1rem 1rem 0rem;
+    padding: 0 $space-md $space-md 0;
   }
   }
 
 
   .card-item-wrapper--clickable {
   .card-item-wrapper--clickable {
@@ -153,7 +146,7 @@
   }
   }
 
 
   .card-item-figure {
   .card-item-figure {
-    margin: 0 $spacer $spacer 0;
+    margin: 0 $space-md $space-md 0;
     height: 6rem;
     height: 6rem;
 
 
     img {
     img {
@@ -195,7 +188,7 @@
   .card-item-wrapper {
   .card-item-wrapper {
     padding: 0;
     padding: 0;
     width: 100%;
     width: 100%;
-    margin-bottom: 3px;
+    margin-bottom: $space-xs;
   }
   }
 
 
   .card-item-wrapper--clickable {
   .card-item-wrapper--clickable {
@@ -212,9 +205,9 @@
   }
   }
 
 
   .card-item-figure {
   .card-item-figure {
-    margin: 0 $spacer 0 0;
+    margin: 0 $space-md 0 0;
     img {
     img {
-      width: 3.5rem;
+      width: 48px;
     }
     }
   }
   }
 
 

+ 5 - 5
public/sass/components/_dashboard_settings.scss

@@ -41,7 +41,7 @@
   font-size: $font-size-h3;
   font-size: $font-size-h3;
   padding-right: 60px;
   padding-right: 60px;
   white-space: nowrap;
   white-space: nowrap;
-  margin-bottom: $spacer;
+  margin-bottom: $space-md;
 
 
   i {
   i {
     font-size: 25px;
     font-size: 25px;
@@ -53,7 +53,7 @@
 
 
 .dashboard-settings__header {
 .dashboard-settings__header {
   font-size: $font-size-h3;
   font-size: $font-size-h3;
-  margin-bottom: $spacer * 2;
+  margin-bottom: $space-xl;
 }
 }
 
 
 .dashboard-settings__subheader {
 .dashboard-settings__subheader {
@@ -89,13 +89,13 @@
   flex-direction: column;
   flex-direction: column;
   height: 100%;
   height: 100%;
   flex-grow: 1;
   flex-grow: 1;
-  margin: $spacer * 3 $spacer * 2 0 0;
+  margin: 40px $space-xl 0 0;
 
 
   button {
   button {
-    margin-bottom: 10px;
+    margin-bottom: $space-sm;
   }
   }
 }
 }
 
 
 .dashboard-settings__json-save-button {
 .dashboard-settings__json-save-button {
-  margin-top: $spacer;
+  margin-top: $space-md;
 }
 }

+ 8 - 9
public/sass/components/_gf-form.scss

@@ -1,8 +1,7 @@
-$gf-form-margin: 3px;
 $input-border: 1px solid $input-border-color;
 $input-border: 1px solid $input-border-color;
 
 
 .gf-form {
 .gf-form {
-  margin-bottom: $gf-form-margin;
+  margin-bottom: $space-xxs;
   display: flex;
   display: flex;
   flex-direction: row;
   flex-direction: row;
   align-items: flex-start;
   align-items: flex-start;
@@ -33,7 +32,7 @@ $input-border: 1px solid $input-border-color;
 
 
 .gf-form--has-input-icon {
 .gf-form--has-input-icon {
   position: relative;
   position: relative;
-  margin-right: $gf-form-margin;
+  margin-right: $space-xs;
 
 
   .gf-form-input-icon {
   .gf-form-input-icon {
     position: absolute;
     position: absolute;
@@ -82,7 +81,7 @@ $input-border: 1px solid $input-border-color;
   align-content: flex-start;
   align-content: flex-start;
 
 
   .gf-form + .gf-form {
   .gf-form + .gf-form {
-    margin-left: $gf-form-margin;
+    margin-left: $space-xs;
   }
   }
 
 
   &--nowrap {
   &--nowrap {
@@ -147,14 +146,14 @@ $input-border: 1px solid $input-border-color;
 }
 }
 
 
 .gf-form-label + .gf-form-label {
 .gf-form-label + .gf-form-label {
-  margin-right: $gf-form-margin;
+  margin-right: $space-xs;
 }
 }
 
 
 .gf-form-pre {
 .gf-form-pre {
   display: block;
   display: block;
   flex-grow: 1;
   flex-grow: 1;
   margin: 0;
   margin: 0;
-  margin-right: $gf-form-margin;
+  margin-right: $space-xs;
   border: $input-btn-border-width solid transparent;
   border: $input-btn-border-width solid transparent;
   border-left: none;
   border-left: none;
   @include border-radius($label-border-radius-sm);
   @include border-radius($label-border-radius-sm);
@@ -336,7 +335,7 @@ $input-border: 1px solid $input-border-color;
 
 
 .gf-form-btn {
 .gf-form-btn {
   padding: $input-padding-y $input-padding-x;
   padding: $input-padding-y $input-padding-x;
-  margin-right: $gf-form-margin;
+  margin-right: $space-xs;
   line-height: $input-line-height;
   line-height: $input-line-height;
   font-size: $font-size-sm;
   font-size: $font-size-sm;
 
 
@@ -354,7 +353,7 @@ $input-border: 1px solid $input-border-color;
 }
 }
 
 
 .gf-form-dropdown-typeahead {
 .gf-form-dropdown-typeahead {
-  margin-right: $gf-form-margin;
+  //margin-right: $space-xs; ?
   position: relative;
   position: relative;
 
 
   &::after {
   &::after {
@@ -391,7 +390,7 @@ $input-border: 1px solid $input-border-color;
   }
   }
 
 
   &--header {
   &--header {
-    margin-bottom: $gf-form-margin;
+    margin-bottom: $space-xxs;
   }
   }
 
 
   &--no-padding {
   &--no-padding {

+ 1 - 1
public/sass/components/_infobox.scss

@@ -5,7 +5,7 @@
   margin-bottom: 2rem;
   margin-bottom: 2rem;
   border-top: 3px solid $info-box-border-color;
   border-top: 3px solid $info-box-border-color;
   margin-bottom: $spacer;
   margin-bottom: $spacer;
-  margin-right: $gf-form-margin;
+  margin-right: $space-xs;
   box-shadow: $card-shadow;
   box-shadow: $card-shadow;
   flex-grow: 1;
   flex-grow: 1;
 
 

+ 1 - 1
public/sass/components/_page_loader.scss

@@ -7,7 +7,7 @@
 
 
   &__spinner {
   &__spinner {
     font-size: 32px;
     font-size: 32px;
-    margin-bottom: $panel-margin;
+    margin-bottom: $space-sm;
   }
   }
 
 
   &__text {
   &__text {

+ 2 - 2
public/sass/components/_panel_editor.scss

@@ -21,7 +21,7 @@
 }
 }
 
 
 .panel-editor-container__editor {
 .panel-editor-container__editor {
-  margin-top: $panel-margin * 2;
+  margin-top: $space-lg;
   display: flex;
   display: flex;
   flex-direction: row;
   flex-direction: row;
   flex: 1 1 0;
   flex: 1 1 0;
@@ -80,7 +80,7 @@
   }
   }
 
 
   .submenu-controls {
   .submenu-controls {
-    padding: 0 $dashboard-padding $panel-margin $dashboard-padding;
+    padding: 0 $dashboard-padding $space-sm $dashboard-padding;
   }
   }
 
 
   .panel-editor-container__panel {
   .panel-editor-container__panel {

+ 0 - 1
public/sass/components/_panel_heatmap.scss

@@ -66,7 +66,6 @@ $font-size-heatmap-tick: 11px;
     height: 18px;
     height: 18px;
     float: left;
     float: left;
     white-space: nowrap;
     white-space: nowrap;
-    padding-left: 10px;
   }
   }
 
 
   .heatmap-legend-values {
   .heatmap-legend-values {

+ 1 - 1
public/sass/components/_panel_logs.scss

@@ -6,7 +6,7 @@ $column-horizontal-spacing: 10px;
   padding: $panel-padding;
   padding: $panel-padding;
   padding-top: 10px;
   padding-top: 10px;
   border-radius: $border-radius;
   border-radius: $border-radius;
-  margin: 2 * $panel-margin 0 $panel-margin;
+  margin: $space-md 0 $space-sm;
   border: $panel-border;
   border: $panel-border;
   flex-direction: column;
   flex-direction: column;
 }
 }

+ 4 - 4
public/sass/components/_sidemenu.scss

@@ -165,7 +165,7 @@
   font-size: $font-size-sm;
   font-size: $font-size-sm;
   color: $text-color-weak;
   color: $text-color-weak;
   border-bottom: 1px solid $dropdownDividerBottom;
   border-bottom: 1px solid $dropdownDividerBottom;
-  margin-bottom: 0.25rem;
+  margin-bottom: $space-xs;
   white-space: nowrap;
   white-space: nowrap;
 }
 }
 
 
@@ -190,7 +190,7 @@ li.sidemenu-org-switcher {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   > i.fa.fa-random {
   > i.fa.fa-random {
-    margin-right: 4px;
+    margin-right: $space-xs;
     top: 1px;
     top: 1px;
   }
   }
 }
 }
@@ -283,8 +283,8 @@ li.sidemenu-org-switcher {
       position: unset;
       position: unset;
       width: 100%;
       width: 100%;
       float: none;
       float: none;
-      margin-top: 0.5rem;
-      margin-bottom: 0.5rem;
+      margin-top: $space-sm;
+      margin-bottom: $space-sm;
 
 
       > li > a {
       > li > a {
         padding-left: 15px;
         padding-left: 15px;

+ 1 - 1
public/sass/components/_submenu.scss

@@ -4,7 +4,7 @@
   flex-wrap: wrap;
   flex-wrap: wrap;
   align-content: flex-start;
   align-content: flex-start;
   align-items: flex-start;
   align-items: flex-start;
-  padding: 0 0 $panel-margin 0;
+  padding: 0 0 $space-sm 0;
 }
 }
 
 
 .annotation-disabled,
 .annotation-disabled,

+ 14 - 13
public/sass/pages/_explore.scss

@@ -65,6 +65,11 @@
   font-size: 18px;
   font-size: 18px;
   min-height: 55px;
   min-height: 55px;
   line-height: 55px;
   line-height: 55px;
+  justify-content: space-between;
+  margin-left: $space-xl;
+}
+
+.explore-toolbar-header {
   justify-content: space-between;
   justify-content: space-between;
   align-items: center;
   align-items: center;
 }
 }
@@ -163,7 +168,7 @@
 }
 }
 
 
 .explore-panel {
 .explore-panel {
-  margin-top: $panel-margin;
+  margin-top: $space-sm;
 }
 }
 
 
 .explore-panel__body {
 .explore-panel__body {
@@ -182,24 +187,20 @@
 
 
 .explore-panel__header-label {
 .explore-panel__header-label {
   font-weight: 500;
   font-weight: 500;
-  margin-right: $panel-margin;
+  margin-right: $space-sm;
   font-size: $font-size-h6;
   font-size: $font-size-h6;
   box-shadow: $text-shadow-faint;
   box-shadow: $text-shadow-faint;
 }
 }
 
 
 .explore-panel__header-buttons {
 .explore-panel__header-buttons {
-  margin-right: $panel-margin;
+  margin-right: $space-sm;
   font-size: $font-size-lg;
   font-size: $font-size-lg;
   line-height: $font-size-h6;
   line-height: $font-size-h6;
 }
 }
 
 
-.result-options {
-  margin: 2 * $panel-margin 0;
-}
-
 .time-series-disclaimer {
 .time-series-disclaimer {
   width: 300px;
   width: 300px;
-  margin: $panel-margin auto;
+  margin: $space-sm auto;
   padding: 10px 0;
   padding: 10px 0;
   border-radius: $border-radius;
   border-radius: $border-radius;
   text-align: center;
   text-align: center;
@@ -207,7 +208,7 @@
 
 
   .disclaimer-icon {
   .disclaimer-icon {
     color: $yellow;
     color: $yellow;
-    margin-right: $panel-margin/2;
+    margin-right: $space-xs;
   }
   }
 
 
   .show-all-time-series {
   .show-all-time-series {
@@ -234,7 +235,7 @@
   position: relative;
   position: relative;
   overflow: hidden;
   overflow: hidden;
   background: none;
   background: none;
-  margin: $panel-margin / 2;
+  margin: $space-xs;
 }
 }
 
 
 .explore-panel__loader--active:after {
 .explore-panel__loader--active:after {
@@ -377,7 +378,7 @@
 .ReactTable .-pagination {
 .ReactTable .-pagination {
   border-top: none;
   border-top: none;
   box-shadow: none;
   box-shadow: none;
-  margin-top: $panel-margin;
+  margin-top: $space-sm;
 }
 }
 .ReactTable .-pagination .-btn {
 .ReactTable .-pagination .-btn {
   color: $blue;
   color: $blue;
@@ -418,7 +419,7 @@
 // TODO Experimental
 // TODO Experimental
 
 
 .cheat-sheet-item {
 .cheat-sheet-item {
-  margin: 2 * $panel-margin 0;
+  margin: $space-lg 0;
   width: 50%;
   width: 50%;
 }
 }
 
 
@@ -427,6 +428,6 @@
 }
 }
 
 
 .cheat-sheet-item__expression {
 .cheat-sheet-item__expression {
-  margin: $panel-margin/2 0;
+  margin: $space-xs 0;
   cursor: pointer;
   cursor: pointer;
 }
 }

+ 3 - 3
public/sass/utils/_widths.scss

@@ -8,20 +8,20 @@
 // widths
 // widths
 @for $i from 1 through 30 {
 @for $i from 1 through 30 {
   .width-#{$i} {
   .width-#{$i} {
-    width: ($spacer * $i) - $gf-form-margin !important;
+    width: ($spacer * $i) - $space-xs !important;
   }
   }
 }
 }
 
 
 @for $i from 1 through 30 {
 @for $i from 1 through 30 {
   .max-width-#{$i} {
   .max-width-#{$i} {
-    max-width: ($spacer * $i) - $gf-form-margin !important;
+    max-width: ($spacer * $i) - $space-xs !important;
     flex-grow: 1;
     flex-grow: 1;
   }
   }
 }
 }
 
 
 @for $i from 1 through 30 {
 @for $i from 1 through 30 {
   .min-width-#{$i} {
   .min-width-#{$i} {
-    min-width: ($spacer * $i) - $gf-form-margin !important;
+    min-width: ($spacer * $i) - $space-xs !important;
   }
   }
 }
 }
 
 

+ 7 - 10
public/test/specs/helpers.ts

@@ -123,6 +123,7 @@ export function ServiceTestContext(this: any) {
   };
   };
 
 
   this.createService = name => {
   this.createService = name => {
+    // @ts-ignore
     return angularMocks.inject(($q, $rootScope, $httpBackend, $injector, $location, $timeout) => {
     return angularMocks.inject(($q, $rootScope, $httpBackend, $injector, $location, $timeout) => {
       self.$q = $q;
       self.$q = $q;
       self.$rootScope = $rootScope;
       self.$rootScope = $rootScope;
@@ -145,7 +146,7 @@ export function DashboardViewStateStub(this: any) {
 export function TimeSrvStub(this: any) {
 export function TimeSrvStub(this: any) {
   this.init = () => {};
   this.init = () => {};
   this.time = { from: 'now-1h', to: 'now' };
   this.time = { from: 'now-1h', to: 'now' };
-  this.timeRange = function(parse) {
+  this.timeRange = function(parse: boolean) {
     if (parse === false) {
     if (parse === false) {
       return this.time;
       return this.time;
     }
     }
@@ -155,11 +156,7 @@ export function TimeSrvStub(this: any) {
     };
     };
   };
   };
 
 
-  this.replace = target => {
-    return target;
-  };
-
-  this.setTime = function(time) {
+  this.setTime = function(time: any) {
     this.time = time;
     this.time = time;
   };
   };
 }
 }
@@ -174,11 +171,11 @@ export function TemplateSrvStub(this: any) {
   this.variables = [];
   this.variables = [];
   this.templateSettings = { interpolate: /\[\[([\s\S]+?)\]\]/g };
   this.templateSettings = { interpolate: /\[\[([\s\S]+?)\]\]/g };
   this.data = {};
   this.data = {};
-  this.replace = function(text) {
+  this.replace = function(text: string) {
     return _.template(text, this.templateSettings)(this.data);
     return _.template(text, this.templateSettings)(this.data);
   };
   };
   this.init = () => {};
   this.init = () => {};
-  this.getAdhocFilters = () => {
+  this.getAdhocFilters = (): any => {
     return [];
     return [];
   };
   };
   this.fillVariableValuesForUrl = () => {};
   this.fillVariableValuesForUrl = () => {};
@@ -187,10 +184,10 @@ export function TemplateSrvStub(this: any) {
     return false;
     return false;
   };
   };
   this.variableInitialized = () => {};
   this.variableInitialized = () => {};
-  this.highlightVariablesAsHtml = str => {
+  this.highlightVariablesAsHtml = (str: string) => {
     return str;
     return str;
   };
   };
-  this.setGrafanaVariable = function(name, value) {
+  this.setGrafanaVariable = function(name: string, value: string) {
     this.data[name] = value;
     this.data[name] = value;
   };
   };
 }
 }

+ 32 - 20
public/vendor/ansicolor/ansicolor.ts

@@ -48,7 +48,7 @@ const colorCodes = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan'
 
 
 /*  ------------------------------------------------------------------------ */
 /*  ------------------------------------------------------------------------ */
 
 
-const clean = obj => {
+const clean = (obj: any) => {
   for (const k in obj) {
   for (const k in obj) {
     if (!obj[k]) {
     if (!obj[k]) {
       delete obj[k];
       delete obj[k];
@@ -60,11 +60,11 @@ const clean = obj => {
 /*  ------------------------------------------------------------------------ */
 /*  ------------------------------------------------------------------------ */
 
 
 class Color {
 class Color {
-  background: string;
+  background: boolean;
   name: string;
   name: string;
   brightness: number;
   brightness: number;
 
 
-  constructor(background?, name?, brightness?) {
+  constructor(background?: boolean, name?: string, brightness?: number) {
     this.background = background;
     this.background = background;
     this.name = name;
     this.name = name;
     this.brightness = brightness;
     this.brightness = brightness;
@@ -82,18 +82,21 @@ class Color {
     });
     });
   }
   }
 
 
-  defaultBrightness(value) {
+  defaultBrightness(value: number) {
     return new Color(this.background, this.name, this.brightness || value);
     return new Color(this.background, this.name, this.brightness || value);
   }
   }
 
 
-  css(inverted) {
+  css(inverted: boolean) {
     const color = inverted ? this.inverse : this;
     const color = inverted ? this.inverse : this;
 
 
+    // @ts-ignore
     const rgbName = (color.brightness === Code.bright && asBright[color.name]) || color.name;
     const rgbName = (color.brightness === Code.bright && asBright[color.name]) || color.name;
 
 
-    const prop = color.background ? 'background:' : 'color:',
-      rgb = Colors.rgb[rgbName],
-      alpha = this.brightness === Code.dim ? 0.5 : 1;
+    const prop = color.background ? 'background:' : 'color:';
+
+    // @ts-ignore
+    const rgb = Colors.rgb[rgbName];
+    const alpha = this.brightness === Code.dim ? 0.5 : 1;
 
 
     return rgb
     return rgb
       ? prop + 'rgba(' + [...rgb, alpha].join(',') + ');'
       ? prop + 'rgba(' + [...rgb, alpha].join(',') + ');'
@@ -117,17 +120,19 @@ class Code {
 
 
   value: number;
   value: number;
 
 
-  constructor(n?) {
+  constructor(n?: string | number) {
     if (n !== undefined) {
     if (n !== undefined) {
       this.value = Number(n);
       this.value = Number(n);
     }
     }
   }
   }
 
 
   get type() {
   get type() {
+    // @ts-ignore
     return types[Math.floor(this.value / 10)];
     return types[Math.floor(this.value / 10)];
   }
   }
 
 
   get subtype() {
   get subtype() {
+    // @ts-ignore
     return subtypes[this.type][this.value % 10];
     return subtypes[this.type][this.value % 10];
   }
   }
 
 
@@ -135,7 +140,7 @@ class Code {
     return this.value ? '\u001b[' + this.value + 'm' : '';
     return this.value ? '\u001b[' + this.value + 'm' : '';
   }
   }
 
 
-  static str(x) {
+  static str(x: string | number) {
     return new Code(x).str;
     return new Code(x).str;
   }
   }
 
 
@@ -146,16 +151,17 @@ class Code {
 
 
 /*  ------------------------------------------------------------------------ */
 /*  ------------------------------------------------------------------------ */
 
 
-const replaceAll = (str, a, b) => str.split(a).join(b);
+const replaceAll = (str: string, a: string, b: string) => str.split(a).join(b);
 
 
 /*  ANSI brightness codes do not overlap, e.g. "{bright}{dim}foo" will be rendered bright (not dim).
 /*  ANSI brightness codes do not overlap, e.g. "{bright}{dim}foo" will be rendered bright (not dim).
     So we fix it by adding brightness canceling before each brightness code, so the former example gets
     So we fix it by adding brightness canceling before each brightness code, so the former example gets
     converted to "{noBrightness}{bright}{noBrightness}{dim}foo" – this way it gets rendered as expected.
     converted to "{noBrightness}{bright}{noBrightness}{dim}foo" – this way it gets rendered as expected.
  */
  */
 
 
-const denormalizeBrightness = s => s.replace(/(\u001b\[(1|2)m)/g, '\u001b[22m$1');
-const normalizeBrightness = s => s.replace(/\u001b\[22m(\u001b\[(1|2)m)/g, '$1');
+const denormalizeBrightness = (s: string) => s.replace(/(\u001b\[(1|2)m)/g, '\u001b[22m$1');
+const normalizeBrightness = (s: string) => s.replace(/\u001b\[22m(\u001b\[(1|2)m)/g, '$1');
 
 
+// @ts-ignore
 const wrap = (x, openCode, closeCode) => {
 const wrap = (x, openCode, closeCode) => {
   const open = Code.str(openCode),
   const open = Code.str(openCode),
     close = Code.str(closeCode);
     close = Code.str(closeCode);
@@ -168,7 +174,7 @@ const wrap = (x, openCode, closeCode) => {
 
 
 /*  ------------------------------------------------------------------------ */
 /*  ------------------------------------------------------------------------ */
 
 
-const camel = (a, b) => a + b.charAt(0).toUpperCase() + b.slice(1);
+const camel = (a: string, b: string) => a + b.charAt(0).toUpperCase() + b.slice(1);
 
 
 const stringWrappingMethods = (() =>
 const stringWrappingMethods = (() =>
   [
   [
@@ -216,10 +222,12 @@ const stringWrappingMethods = (() =>
 
 
 /*  ------------------------------------------------------------------------ */
 /*  ------------------------------------------------------------------------ */
 
 
+// @ts-ignore
 const assignStringWrappingAPI = (target, wrapBefore = target) =>
 const assignStringWrappingAPI = (target, wrapBefore = target) =>
   stringWrappingMethods.reduce(
   stringWrappingMethods.reduce(
     (memo, [k, open, close]) =>
     (memo, [k, open, close]) =>
       O.defineProperty(memo, k, {
       O.defineProperty(memo, k, {
+        // @ts-ignore
         get: () => assignStringWrappingAPI(str => wrapBefore(wrap(str, open, close))),
         get: () => assignStringWrappingAPI(str => wrapBefore(wrap(str, open, close))),
       }),
       }),
 
 
@@ -232,7 +240,7 @@ const TEXT = 0,
   BRACKET = 1,
   BRACKET = 1,
   CODE = 2;
   CODE = 2;
 
 
-function rawParse(s) {
+function rawParse(s: string) {
   let state = TEXT,
   let state = TEXT,
     buffer = '',
     buffer = '',
     text = '',
     text = '',
@@ -333,7 +341,7 @@ export default class Colors {
   /**
   /**
    * @param {string} s a string containing ANSI escape codes.
    * @param {string} s a string containing ANSI escape codes.
    */
    */
-  constructor(s?) {
+  constructor(s?: string) {
     this.spans = s ? rawParse(s) : [];
     this.spans = s ? rawParse(s) : [];
   }
   }
 
 
@@ -342,7 +350,10 @@ export default class Colors {
   }
   }
 
 
   get parsed() {
   get parsed() {
-    let color, bgColor, brightness, styles;
+    let styles: Set<string>;
+    let brightness: number;
+    let color: Color;
+    let bgColor: Color;
 
 
     function reset() {
     function reset() {
       (color = new Color()),
       (color = new Color()),
@@ -431,6 +442,7 @@ export default class Colors {
       if (!(k in String.prototype)) {
       if (!(k in String.prototype)) {
         O.defineProperty(String.prototype, k, {
         O.defineProperty(String.prototype, k, {
           get: function() {
           get: function() {
+            // @ts-ignore
             return Colors[k](this);
             return Colors[k](this);
           },
           },
         });
         });
@@ -444,7 +456,7 @@ export default class Colors {
    * @desc parses a string containing ANSI escape codes
    * @desc parses a string containing ANSI escape codes
    * @return {Colors} parsed representation.
    * @return {Colors} parsed representation.
    */
    */
-  static parse(s) {
+  static parse(s: string) {
     return new Colors(s).parsed;
     return new Colors(s).parsed;
   }
   }
 
 
@@ -453,7 +465,7 @@ export default class Colors {
    * @param {string} s a string containing ANSI escape codes.
    * @param {string} s a string containing ANSI escape codes.
    * @return {string} clean string.
    * @return {string} clean string.
    */
    */
-  static strip(s) {
+  static strip(s: string) {
     return s.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]/g, ''); // hope V8 caches the regexp
     return s.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]/g, ''); // hope V8 caches the regexp
   }
   }
 
 
@@ -468,4 +480,4 @@ export default class Colors {
 
 
 /*  ------------------------------------------------------------------------ */
 /*  ------------------------------------------------------------------------ */
 
 
-assignStringWrappingAPI(Colors, str => str);
+assignStringWrappingAPI(Colors, (str: string) => str);

+ 30 - 0
scripts/circle-metrics.sh

@@ -0,0 +1,30 @@
+#!/bin/bash
+
+echo "Collecting code stats (typescript errors & more)"
+
+ERROR_COUNT="$(./node_modules/.bin/tsc --project tsconfig.json --noEmit --noImplicitAny true | grep -oP 'Found \K(\d+)')"
+DIRECTIVES="$(grep -r -o  directive public/app/**/*  | wc -l)"
+CONTROLLERS="$(grep -r -oP 'class .*Ctrl' public/app/**/*  | wc -l)"
+
+echo "Typescript errors: $ERROR_COUNT"
+echo "Directives: $DIRECTIVES"
+echo "Controllers: $CONTROLLERS"
+
+curl \
+   -d "{\"metrics\": {
+        \"ci.code.noImplicitAny\": $ERROR_COUNT,
+        \"ci.code.directives\": $DIRECTIVES,
+        \"ci.code.controllers\": $CONTROLLERS
+      }
+    }" \
+   -H "Content-Type: application/json" \
+   -u ci:$CIRCLE_STATS_PWD \
+   -X POST https://stats.grafana.org/metric-receiver
+
+curl https://6371:$GRAFANA_MISC_STATS_API_KEY@graphite-us-central1.grafana.net/metrics \
+  -H 'Content-type: application/json' \
+  -d '[
+      {"name":"grafana.ci-code.noImplicitAny", "interval":60, "value": '$ERROR_COUNT', "mtype": "gauge", "time": '$(date +%s)'},
+      {"name":"grafana.ci-code.directives", "interval":60, "value": '$DIRECTIVES', "mtype": "gauge", "time": '$(date +%s)'},
+      {"name":"grafana.ci-code.controllers", "interval":60, "value": '$CONTROLLERS', "mtype": "gauge", "time": '$(date +%s)'}
+   ]'

+ 6 - 0
scripts/circle-test-frontend.sh

@@ -12,3 +12,9 @@ function exit_if_fail {
 
 
 exit_if_fail npm run prettier:check
 exit_if_fail npm run prettier:check
 exit_if_fail npm run test
 exit_if_fail npm run test
+
+# On master also collect some and send some metrics
+branch="$(git rev-parse --abbrev-ref HEAD)"
+if [ "${branch}" == "master" ]; then
+  exit_if_fail ./scripts/circle-metrics.sh
+fi

+ 1 - 0
tsconfig.json

@@ -23,6 +23,7 @@
     "noImplicitThis": true,
     "noImplicitThis": true,
     "noImplicitUseStrict": false,
     "noImplicitUseStrict": false,
     "noImplicitAny": false,
     "noImplicitAny": false,
+    "downlevelIteration": true,
     "noUnusedLocals": true,
     "noUnusedLocals": true,
     "baseUrl": "public",
     "baseUrl": "public",
     "pretty": true,
     "pretty": true,

+ 5 - 0
yarn.lock

@@ -1514,6 +1514,11 @@
     react-input-autosize "^2.2.1"
     react-input-autosize "^2.2.1"
     react-transition-group "^2.2.1"
     react-transition-group "^2.2.1"
 
 
+"@types/angular@^1.6.6":
+  version "1.6.54"
+  resolved "https://registry.yarnpkg.com/@types/angular/-/angular-1.6.54.tgz#f9d5a03e4da7b021a6dabe5d63e899ed4567a5bd"
+  integrity sha512-xA1FuozWXeRQ7FClUbvk8ePL+dydBeDoCWRPFTHU5+8uvVtIIfLGiHA8CMkwsbddFCYnTDVbLxG85a/HBx7LtA==
+
 "@types/chalk@^2.2.0":
 "@types/chalk@^2.2.0":
   version "2.2.0"
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-2.2.0.tgz#b7f6e446f4511029ee8e3f43075fb5b73fbaa0ba"
   resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-2.2.0.tgz#b7f6e446f4511029ee8e3f43075fb5b73fbaa0ba"