Browse Source

merge master

ryan 6 năm trước cách đây
mục cha
commit
4e5e548902
54 tập tin đã thay đổi với 366 bổ sung276 xóa
  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
 * **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
 * **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)
 * **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)
+* **Heatmap**: legend shows wrong colors for small values [#14019](https://github.com/grafana/grafana/issues/14019)
 
 # 6.0.1 (2019-03-06)
 

+ 1 - 0
package.json

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

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

@@ -1,5 +1,5 @@
 .form-field {
-  margin-bottom: $gf-form-margin;
+  margin-bottom: $space-xxs;
   display: flex;
   flex-direction: row;
   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> {
   canvasElement: any;
 
-  static defaultProps = {
+  static defaultProps: Partial<Props> = {
     maxValue: 100,
     minValue: 0,
     showThresholdMarkers: true,
     showThresholdLabels: false,
     thresholds: [],
-    theme: GrafanaThemeType.Dark,
   };
 
   componentDidMount() {
@@ -76,7 +75,7 @@ export class Gauge extends PureComponent<Props> {
     const fontSize = Math.min(dimension / 5, 100) * this.getFontScale(value.text.length);
     const thresholdLabelFontSize = fontSize / 2.5;
 
-    const options = {
+    const options: any = {
       series: {
         gauges: {
           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() {
   width: 100%;
-  margin-right: $gf-form-margin;
+  margin-right: $space-xs;
   @include border-radius($input-border-radius-sm);
   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
 // 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-y: $spacer !default;
 $spacers: (
@@ -46,7 +52,7 @@ $spacers: (
     ),
   ),
 ) !default;
-$border-width: ${theme.border.width.s} !default;
+$border-width: ${theme.border.width.sm} !default;
 
 // Grid breakpoints
 //
@@ -55,9 +61,9 @@ $border-width: ${theme.border.width.s} !default;
 
 $grid-breakpoints: (
   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},
 ) !default;
 
@@ -91,12 +97,12 @@ $font-family-base: $font-family-sans-serif !default;
 $font-size-root: ${theme.typography.size.root} !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;
 
-$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-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-h6: ${theme.typography.heading.h6} !default;
 
-$headings-margin-bottom: ($spacer / 2) !default;
 $headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
 $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;
 $dt-font-weight: bold !default;
@@ -160,7 +165,6 @@ $input-padding-y-lg: 10px !default;
 
 $input-height: 35px !default;
 
-$gf-form-margin: 3px;
 $gf-form-input-height: 35px;
 
 $cursor-disabled: not-allowed !default;
@@ -207,8 +211,7 @@ $btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
 $side-menu-width: 60px;
 
 // dashboard
-$panel-margin: 10px;
-$dashboard-padding: $panel-margin * 2;
+$dashboard-padding: 10px * 2;
 $panel-horizontal-padding: 10;
 $panel-vertical-padding: 5;
 $panel-padding: 0px $panel-horizontal-padding + 0px $panel-vertical-padding + 0px $panel-horizontal-padding + 0px;
@@ -243,9 +246,4 @@ $external-services: (
     icon: '',
   ),
 ) !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',
       base: '13px',
       xs: '10px',
-      s: '12px',
-      m: '14px',
-      l: '18px',
+      sm: '12px',
+      md: '14px',
+      lg: '18px',
     },
     heading: {
       h1: '28px',
@@ -30,35 +30,42 @@ const theme: GrafanaThemeCommons = {
     },
     lineHeight: {
       xs: 1,
-      s: 1.1,
-      m: 4 / 3,
-      l: 1.5,
+      sm: 1.1,
+      md: 4 / 3,
+      lg: 1.5,
     },
   },
   breakpoints: {
     xs: '0',
-    s: '544px',
-    m: '768px',
-    l: '992px',
+    sm: '544px',
+    md: '768px',
+    lg: '992px',
     xl: '1200px',
   },
   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',
   },
   border: {
     radius: {
       xs: '2px',
-      s: '3px',
-      m: '5px',
+      sm: '3px',
+      md: '5px',
     },
     width: {
-      s: '1px',
+      sm: '1px',
     },
   },
+  panelPadding: {
+    horizontal: 10,
+    vertical: 5,
+  },
 };
 
 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
   breakpoints: {
     xs: string;
-    s: string;
-    m: string;
-    l: string;
+    sm: string;
+    md: string;
+    lg: string;
     xl: string;
   };
   typography: {
@@ -22,9 +22,9 @@ export interface GrafanaThemeCommons {
       root: string;
       base: string;
       xs: string;
-      s: string;
-      m: string;
-      l: string;
+      sm: string;
+      md: string;
+      lg: string;
     };
     weight: {
       light: number;
@@ -33,9 +33,9 @@ export interface GrafanaThemeCommons {
     };
     lineHeight: {
       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
     heading: {
@@ -48,22 +48,29 @@ export interface GrafanaThemeCommons {
     };
   };
   spacing: {
+    d: string;
+    xxs: string;
     xs: string;
-    s: string;
-    m: string;
-    l: string;
+    sm: string;
+    md: string;
+    lg: string;
+    xl: string;
     gutter: string;
   };
   border: {
     radius: {
       xs: string;
-      s: string;
-      m: string;
+      sm: string;
+      md: string;
     };
     width: {
-      s: string;
+      sm: string;
     };
   };
+  panelPadding: {
+    horizontal: number;
+    vertical: number;
+  };
 }
 
 export interface GrafanaTheme extends GrafanaThemeCommons {

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

@@ -10,13 +10,13 @@ coreModule.directive('jsonTree', [
         startExpanded: '@',
         rootName: '@',
       },
-      link: (scope, elem) => {
+      link: (scope: any, elem) => {
         const jsonExp = new JsonExplorer(scope.object, 3, {
           animateOpen: 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(() => {
           element.focus();
-          const domEl = element[0];
+          const domEl: any = element[0];
           if (domEl.setSelectionRange) {
             const pos = element.val().length * 2;
             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 {
     font-size: 30px;
-    margin-right: $spacer;
+    margin-right: $space-md;
   }
 
   &:hover {
@@ -32,16 +32,16 @@
 .add-panel-widget__title {
   font-size: $font-size-md;
   font-weight: $font-weight-semi-bold;
-  margin-right: $spacer * 2;
+  margin-right: $space-xl;
 }
 
 .add-panel-widget__link {
-  margin: 0 8px;
+  margin: 0 $space-sm;
   width: 154px;
 }
 
 .add-panel-widget__icon {
-  margin-bottom: 8px;
+  margin-bottom: $space-sm;
 
   .gicon {
     color: white;
@@ -62,7 +62,7 @@
 
 .add-panel-widget__create {
   display: inherit;
-  margin-bottom: 24px;
+  margin-bottom: $space-lg;
   // this is to have the big button appear centered
   margin-top: 55px;
 }
@@ -72,7 +72,7 @@
 }
 
 .add-panel-widget__action {
-  margin: 0 4px;
+  margin: 0 $space-xs;
 }
 
 .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 { DashboardRouteInfo, DashboardInitPhase } from 'app/types';
 
-jest.mock('sass/_variables.generated.scss', () => ({
-  panelhorizontalpadding: 10,
-  panelVerticalPadding: 10,
-}));
-
 jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({}));
 
 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 });
 
     try {
-      const ds = await this.dataSourceSrv.get(datasource);
+      const ds = await this.dataSourceSrv.get(datasource, scopedVars);
 
       // TODO interpolate variables
       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';
 
-jest.mock('sass/_variables.generated.scss', () => ({
-  panelhorizontalpadding: 10,
-  panelVerticalPadding: 10,
-}));
-
 describe('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 { PANEL_HEADER_HEIGHT } from 'app/core/constants';
 import { profiler } from 'app/core/profiler';
+import config from 'app/core/config';
 
 // Types
 import { DashboardModel, PanelModel } from '../state';
@@ -21,7 +22,6 @@ import { PanelPlugin } from 'app/types';
 import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui';
 import { ScopedVars } from '@grafana/ui';
 
-import variables from 'sass/_variables.generated.scss';
 import templateSrv from 'app/features/templating/template_srv';
 
 const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
@@ -160,8 +160,8 @@ export class PanelChrome extends PureComponent<Props, State> {
           panelData={panelData}
           timeRange={timeRange}
           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}
           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',
     link: {
-      pre: ($scope, elem, attrs) => {
+      pre: ($scope: any, elem, attrs) => {
         // do not show access option if direct access is disabled
         $scope.showAccessOption = $scope.noDirectAccess !== 'true';
         $scope.showAccessHelp = false;

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

@@ -81,7 +81,7 @@ class MetricsPanelCtrl extends PanelCtrl {
 
     // load datasource service
     this.datasourceSrv
-      .get(this.panel.datasource)
+      .get(this.panel.datasource, this.panel.scopedVars)
       .then(this.updateTimeRange.bind(this))
       .then(this.issueQueries.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,
     transclude: true,
     scope: { ctrl: '=' },
-    link: (scope, elem) => {
+    link: (scope: any, elem) => {
       const panelContainer = elem.find('.panel-container');
       const panelContent = elem.find('.panel-content');
       const cornerInfoElem = elem.find('.panel-info-corner');
@@ -67,7 +67,7 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
       // set initial transparency
       if (ctrl.panel.transparent) {
         transparentLastState = true;
-        panelContainer.addClass('panel-transparent', true);
+        panelContainer.addClass('panel-transparent');
       }
 
       // 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';
 
 // Types
-import { DataSourceApi, DataSourceSelectItem } from '@grafana/ui/src/types';
+import { DataSourceApi, DataSourceSelectItem, ScopedVars } from '@grafana/ui/src/types';
 
 export class DatasourceSrv {
   datasources: { [name: string]: DataSourceApi };
@@ -21,12 +21,18 @@ export class DatasourceSrv {
     this.datasources = {};
   }
 
-  get(name?: string): Promise<DataSourceApi> {
+  get(name?: string, scopedVars?: ScopedVars): Promise<DataSourceApi> {
     if (!name) {
       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') {
       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;
   options: any;
   current: any;
+  multi: boolean;
+  includeAll: boolean;
   refresh: any;
   skipUrlSync: boolean;
 
@@ -18,6 +20,8 @@ export class DatasourceVariable implements Variable {
     regex: '',
     options: [],
     query: '',
+    multi: false,
+    includeAll: false,
     refresh: 1,
     skipUrlSync: false,
   };
@@ -69,9 +73,16 @@ export class DatasourceVariable implements Variable {
     }
 
     this.options = options;
+    if (this.includeAll) {
+      this.addAllOption();
+    }
     return this.variableSrv.validateVariableSelectionState(this);
   }
 
+  addAllOption() {
+    this.options.unshift({ text: 'All', value: '$__all' });
+  }
+
   dependsOn(variable) {
     if (this.regex) {
       return containsVariable(this.regex, variable.name);
@@ -84,6 +95,9 @@ export class DatasourceVariable implements Variable {
   }
 
   getValueForUrl() {
+    if (this.current.text === 'All') {
+      return 'All';
+    }
     return this.current.value;
   }
 }
@@ -91,5 +105,6 @@ export class DatasourceVariable implements Variable {
 variableTypes['datasource'] = {
   name: 'Datasource',
   ctor: DatasourceVariable,
+  supportsMulti: true,
   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 TableModel from 'app/core/table_model';
+import { TimeSeries } from '@grafana/ui';
 
 export class ResultTransformer {
   constructor(private templateSrv) {}
@@ -18,10 +19,10 @@ export class ResultTransformer {
       ];
     } else if (prometheusResult && options.format === 'heatmap') {
       let seriesList = [];
-      prometheusResult.sort(sortSeriesByLabel);
       for (const metricData of prometheusResult) {
         seriesList.push(this.transformMetricData(metricData, options, options.start, options.end));
       }
+      seriesList.sort(sortSeriesByLabel);
       seriesList = this.transformToHistogramOverTime(seriesList);
       return seriesList;
     } 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;
 
   try {
     // 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) {
     console.log(err);
     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 { 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 {
   defaultRemoveGroupByValue = '-- remove group by --';
   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',
       Upper: 'upper',
       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_TICK_SIZE = 0;
 const LEGEND_VALUE_MARGIN = 0;
+const LEGEND_PADDING_LEFT = 10;
+const LEGEND_SEGMENT_WIDTH = 10;
 
 /**
  * Color legend for heatmap editor.
@@ -19,7 +21,7 @@ coreModule.directive('colorLegend', () => {
   return {
     restrict: 'E',
     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 panel = scope.ctrl.panel;
 
@@ -55,7 +57,7 @@ coreModule.directive('heatmapLegend', () => {
   return {
     restrict: 'E',
     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 panel = scope.ctrl.panel;
 
@@ -95,27 +97,27 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal
   const legendWidth = Math.floor(legendElem.outerWidth()) - 30;
   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 valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
 
   const colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
   legend
+    .append('g')
+    .attr('class', 'legend-color-bar')
+    .attr('transform', 'translate(' + LEGEND_PADDING_LEFT + ',0)')
     .selectAll('.heatmap-color-legend-rect')
     .data(valuesRange)
     .enter()
     .append('rect')
-    .attr('x', d => d * widthFactor)
+    .attr('x', d => Math.round(d * widthFactor))
     .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('stroke-width', 0)
     .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) {
@@ -126,31 +128,31 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue
   const legendWidth = Math.floor(legendElem.outerWidth()) - 30;
   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 valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
 
   const opacityScale = getOpacityScale(options, maxValue, minValue);
   legend
+    .append('g')
+    .attr('class', 'legend-color-bar')
+    .attr('transform', 'translate(' + LEGEND_PADDING_LEFT + ',0)')
     .selectAll('.heatmap-opacity-legend-rect')
     .data(valuesRange)
     .enter()
     .append('rect')
-    .attr('x', d => d * widthFactor)
+    .attr('x', d => Math.round(d * widthFactor))
     .attr('y', 0)
-    .attr('width', rangeStep * widthFactor)
+    .attr('width', Math.round(rangeStep * widthFactor))
     .attr('height', legendHeight)
     .attr('stroke-width', 0)
     .attr('fill', options.cardColor)
     .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 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 posY = getSvgElemHeight(legendElem) + LEGEND_VALUE_MARGIN;
-  const posX = getSvgElemX(colorRect);
+  const posX = getSvgElemX(colorRect) + LEGEND_PADDING_LEFT;
 
   d3.select(legendElem.get(0))
     .append('g')

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

@@ -34,6 +34,7 @@ const panelDefaults = {
   },
   dataFormat: 'timeseries',
   yBucketBound: 'auto',
+  reverseYBuckets: false,
   xAxis: {
     show: true,
   },
@@ -55,6 +56,7 @@ const panelDefaults = {
     showHistogram: false,
   },
   highlightCards: true,
+  hideZeroBuckets: false,
 };
 
 const colorModes = ['opacity', 'spectrum'];
@@ -97,7 +99,7 @@ const colorSchemes = [
   { name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'dark' },
 ];
 
-const dsSupportHistogramSort = ['prometheus', 'elasticsearch'];
+const dsSupportHistogramSort = ['elasticsearch'];
 
 export class HeatmapCtrl extends MetricsPanelCtrl {
   static templateUrl = 'module.html';
@@ -108,7 +110,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
   selectionActivated: boolean;
   unitFormats: any;
   data: any;
-  series: any;
+  series: any[];
   timeSrv: any;
   dataWarning: any;
   decimals: number;
@@ -146,7 +148,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
   }
 
   onRender() {
-    if (!this.range) {
+    if (!this.range || !this.series) {
       return;
     }
 
@@ -204,7 +206,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
       yBucketSize = 1;
     }
 
-    const { cards, cardStats } = convertToCards(bucketsData);
+    const { cards, cardStats } = convertToCards(bucketsData, this.panel.hideZeroBuckets);
 
     this.data = {
       buckets: bucketsData,
@@ -225,13 +227,20 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
       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
-    // 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);
 
     tsBuckets = _.map(this.series, 'label');
     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.
       tsBuckets = [''].concat(tsBuckets);
     } else {
@@ -246,7 +255,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
     // Always let yBucketSize=1 in 'tsbuckets' mode
     yBucketSize = 1;
 
-    const { cards, cardStats } = convertToCards(bucketsData);
+    const { cards, cardStats } = convertToCards(bucketsData, this.panel.hideZeroBuckets);
 
     this.data = {
       buckets: bucketsData,

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

@@ -93,25 +93,43 @@ function parseHistogramLabel(label: string): number {
   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.
  * @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,
     max = 0;
-  const cards = [];
+  const cards: HeatmapCard[] = [];
   _.forEach(buckets, xBucket => {
     _.forEach(xBucket.buckets, yBucket => {
-      const card = {
+      const card: HeatmapCard = {
         x: xBucket.x,
         y: yBucket.y,
         yBounds: yBucket.bounds,
         values: yBucket.values,
         count: yBucket.count,
       };
-      cards.push(card);
+      if (!hideZero || card.count !== 0) {
+        cards.push(card);
+      }
 
       if (cards.length === 1) {
         min = yBucket.count;

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

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

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

@@ -40,6 +40,11 @@
         </select>
       </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 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">
     <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">
       <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>

+ 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;
     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)
     this.heatmap
       .select('.axis-y')
@@ -615,8 +621,8 @@ export class HeatmapRenderer {
       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;
   }
 

+ 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
 // 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-x: $spacer !default;
 $spacer-y: $spacer !default;
@@ -109,7 +115,6 @@ $font-size-h4: 18px !default;
 $font-size-h5: 16px !default;
 $font-size-h6: 14px !default;
 
-$headings-margin-bottom: ($spacer / 2) !default;
 $headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
 $headings-font-weight: 400 !default;
 $headings-line-height: 1.1 !default;
@@ -163,7 +168,6 @@ $input-padding-y-lg: 10px !default;
 
 $input-height: 35px !default;
 
-$gf-form-margin: 3px;
 $gf-form-input-height: 35px;
 
 $cursor-disabled: not-allowed !default;
@@ -210,8 +214,7 @@ $btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
 $side-menu-width: 60px;
 
 // dashboard
-$panel-margin: 10px;
-$dashboard-padding: $panel-margin * 2;
+$dashboard-padding: 10px * 2;
 $panel-horizontal-padding: 10;
 $panel-vertical-padding: 5;
 $panel-padding: 0px $panel-horizontal-padding + 0px $panel-vertical-padding + 0px $panel-horizontal-padding + 0px;
@@ -246,8 +249,3 @@ $external-services: (
     icon: '',
   ),
 ) !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,
 .h5,
 .h6 {
-  margin-bottom: $headings-margin-bottom;
+  margin-bottom: $space-sm;
   font-family: $headings-font-family;
   font-weight: $headings-font-weight;
   line-height: $headings-line-height;

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

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

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

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

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

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

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

@@ -41,7 +41,7 @@
   font-size: $font-size-h3;
   padding-right: 60px;
   white-space: nowrap;
-  margin-bottom: $spacer;
+  margin-bottom: $space-md;
 
   i {
     font-size: 25px;
@@ -53,7 +53,7 @@
 
 .dashboard-settings__header {
   font-size: $font-size-h3;
-  margin-bottom: $spacer * 2;
+  margin-bottom: $space-xl;
 }
 
 .dashboard-settings__subheader {
@@ -89,13 +89,13 @@
   flex-direction: column;
   height: 100%;
   flex-grow: 1;
-  margin: $spacer * 3 $spacer * 2 0 0;
+  margin: 40px $space-xl 0 0;
 
   button {
-    margin-bottom: 10px;
+    margin-bottom: $space-sm;
   }
 }
 
 .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;
 
 .gf-form {
-  margin-bottom: $gf-form-margin;
+  margin-bottom: $space-xxs;
   display: flex;
   flex-direction: row;
   align-items: flex-start;
@@ -33,7 +32,7 @@ $input-border: 1px solid $input-border-color;
 
 .gf-form--has-input-icon {
   position: relative;
-  margin-right: $gf-form-margin;
+  margin-right: $space-xs;
 
   .gf-form-input-icon {
     position: absolute;
@@ -82,7 +81,7 @@ $input-border: 1px solid $input-border-color;
   align-content: flex-start;
 
   .gf-form + .gf-form {
-    margin-left: $gf-form-margin;
+    margin-left: $space-xs;
   }
 
   &--nowrap {
@@ -147,14 +146,14 @@ $input-border: 1px solid $input-border-color;
 }
 
 .gf-form-label + .gf-form-label {
-  margin-right: $gf-form-margin;
+  margin-right: $space-xs;
 }
 
 .gf-form-pre {
   display: block;
   flex-grow: 1;
   margin: 0;
-  margin-right: $gf-form-margin;
+  margin-right: $space-xs;
   border: $input-btn-border-width solid transparent;
   border-left: none;
   @include border-radius($label-border-radius-sm);
@@ -336,7 +335,7 @@ $input-border: 1px solid $input-border-color;
 
 .gf-form-btn {
   padding: $input-padding-y $input-padding-x;
-  margin-right: $gf-form-margin;
+  margin-right: $space-xs;
   line-height: $input-line-height;
   font-size: $font-size-sm;
 
@@ -354,7 +353,7 @@ $input-border: 1px solid $input-border-color;
 }
 
 .gf-form-dropdown-typeahead {
-  margin-right: $gf-form-margin;
+  //margin-right: $space-xs; ?
   position: relative;
 
   &::after {
@@ -391,7 +390,7 @@ $input-border: 1px solid $input-border-color;
   }
 
   &--header {
-    margin-bottom: $gf-form-margin;
+    margin-bottom: $space-xxs;
   }
 
   &--no-padding {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -8,20 +8,20 @@
 // widths
 @for $i from 1 through 30 {
   .width-#{$i} {
-    width: ($spacer * $i) - $gf-form-margin !important;
+    width: ($spacer * $i) - $space-xs !important;
   }
 }
 
 @for $i from 1 through 30 {
   .max-width-#{$i} {
-    max-width: ($spacer * $i) - $gf-form-margin !important;
+    max-width: ($spacer * $i) - $space-xs !important;
     flex-grow: 1;
   }
 }
 
 @for $i from 1 through 30 {
   .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 => {
+    // @ts-ignore
     return angularMocks.inject(($q, $rootScope, $httpBackend, $injector, $location, $timeout) => {
       self.$q = $q;
       self.$rootScope = $rootScope;
@@ -145,7 +146,7 @@ export function DashboardViewStateStub(this: any) {
 export function TimeSrvStub(this: any) {
   this.init = () => {};
   this.time = { from: 'now-1h', to: 'now' };
-  this.timeRange = function(parse) {
+  this.timeRange = function(parse: boolean) {
     if (parse === false) {
       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;
   };
 }
@@ -174,11 +171,11 @@ export function TemplateSrvStub(this: any) {
   this.variables = [];
   this.templateSettings = { interpolate: /\[\[([\s\S]+?)\]\]/g };
   this.data = {};
-  this.replace = function(text) {
+  this.replace = function(text: string) {
     return _.template(text, this.templateSettings)(this.data);
   };
   this.init = () => {};
-  this.getAdhocFilters = () => {
+  this.getAdhocFilters = (): any => {
     return [];
   };
   this.fillVariableValuesForUrl = () => {};
@@ -187,10 +184,10 @@ export function TemplateSrvStub(this: any) {
     return false;
   };
   this.variableInitialized = () => {};
-  this.highlightVariablesAsHtml = str => {
+  this.highlightVariablesAsHtml = (str: string) => {
     return str;
   };
-  this.setGrafanaVariable = function(name, value) {
+  this.setGrafanaVariable = function(name: string, value: string) {
     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) {
     if (!obj[k]) {
       delete obj[k];
@@ -60,11 +60,11 @@ const clean = obj => {
 /*  ------------------------------------------------------------------------ */
 
 class Color {
-  background: string;
+  background: boolean;
   name: string;
   brightness: number;
 
-  constructor(background?, name?, brightness?) {
+  constructor(background?: boolean, name?: string, brightness?: number) {
     this.background = background;
     this.name = name;
     this.brightness = brightness;
@@ -82,18 +82,21 @@ class Color {
     });
   }
 
-  defaultBrightness(value) {
+  defaultBrightness(value: number) {
     return new Color(this.background, this.name, this.brightness || value);
   }
 
-  css(inverted) {
+  css(inverted: boolean) {
     const color = inverted ? this.inverse : this;
 
+    // @ts-ignore
     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
       ? prop + 'rgba(' + [...rgb, alpha].join(',') + ');'
@@ -117,17 +120,19 @@ class Code {
 
   value: number;
 
-  constructor(n?) {
+  constructor(n?: string | number) {
     if (n !== undefined) {
       this.value = Number(n);
     }
   }
 
   get type() {
+    // @ts-ignore
     return types[Math.floor(this.value / 10)];
   }
 
   get subtype() {
+    // @ts-ignore
     return subtypes[this.type][this.value % 10];
   }
 
@@ -135,7 +140,7 @@ class Code {
     return this.value ? '\u001b[' + this.value + 'm' : '';
   }
 
-  static str(x) {
+  static str(x: string | number) {
     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).
     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.
  */
 
-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 open = Code.str(openCode),
     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 = (() =>
   [
@@ -216,10 +222,12 @@ const stringWrappingMethods = (() =>
 
 /*  ------------------------------------------------------------------------ */
 
+// @ts-ignore
 const assignStringWrappingAPI = (target, wrapBefore = target) =>
   stringWrappingMethods.reduce(
     (memo, [k, open, close]) =>
       O.defineProperty(memo, k, {
+        // @ts-ignore
         get: () => assignStringWrappingAPI(str => wrapBefore(wrap(str, open, close))),
       }),
 
@@ -232,7 +240,7 @@ const TEXT = 0,
   BRACKET = 1,
   CODE = 2;
 
-function rawParse(s) {
+function rawParse(s: string) {
   let state = TEXT,
     buffer = '',
     text = '',
@@ -333,7 +341,7 @@ export default class Colors {
   /**
    * @param {string} s a string containing ANSI escape codes.
    */
-  constructor(s?) {
+  constructor(s?: string) {
     this.spans = s ? rawParse(s) : [];
   }
 
@@ -342,7 +350,10 @@ export default class Colors {
   }
 
   get parsed() {
-    let color, bgColor, brightness, styles;
+    let styles: Set<string>;
+    let brightness: number;
+    let color: Color;
+    let bgColor: Color;
 
     function reset() {
       (color = new Color()),
@@ -431,6 +442,7 @@ export default class Colors {
       if (!(k in String.prototype)) {
         O.defineProperty(String.prototype, k, {
           get: function() {
+            // @ts-ignore
             return Colors[k](this);
           },
         });
@@ -444,7 +456,7 @@ export default class Colors {
    * @desc parses a string containing ANSI escape codes
    * @return {Colors} parsed representation.
    */
-  static parse(s) {
+  static parse(s: string) {
     return new Colors(s).parsed;
   }
 
@@ -453,7 +465,7 @@ export default class Colors {
    * @param {string} s a string containing ANSI escape codes.
    * @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
   }
 
@@ -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 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,
     "noImplicitUseStrict": false,
     "noImplicitAny": false,
+    "downlevelIteration": true,
     "noUnusedLocals": true,
     "baseUrl": "public",
     "pretty": true,

+ 5 - 0
yarn.lock

@@ -1514,6 +1514,11 @@
     react-input-autosize "^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":
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-2.2.0.tgz#b7f6e446f4511029ee8e3f43075fb5b73fbaa0ba"