浏览代码

Merge branch 'master' into 15330-vizpicker-red-when-0-hits

Torkel Ödegaard 6 年之前
父节点
当前提交
124b3486eb
共有 100 个文件被更改,包括 604 次插入470 次删除
  1. 8 0
      .prettierignore
  2. 9 22
      package.json
  3. 1 1
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx
  4. 1 1
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx
  5. 9 3
      packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx
  6. 0 1
      packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx
  7. 4 3
      packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx
  8. 0 1
      packages/grafana-ui/src/components/ColorPicker/SpectrumPalettePointer.tsx
  9. 1 1
      packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss
  10. 8 6
      packages/grafana-ui/src/components/EmptySearchResult/_EmptySearchResult.scss
  11. 1 1
      packages/grafana-ui/src/components/FormLabel/FormLabel.tsx
  12. 1 1
      packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
  13. 1 6
      packages/grafana-ui/src/components/PanelOptionsGrid/PanelOptionsGrid.tsx
  14. 3 1
      packages/grafana-ui/src/components/Select/Select.tsx
  15. 1 1
      packages/grafana-ui/src/components/Tooltip/Tooltip.tsx
  16. 1 1
      packages/grafana-ui/src/components/Tooltip/_Tooltip.scss
  17. 9 9
      packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx
  18. 2 2
      packages/grafana-ui/src/components/index.scss
  19. 1 1
      packages/grafana-ui/src/themes/dark.ts
  20. 7 9
      packages/grafana-ui/src/themes/default.ts
  21. 4 6
      packages/grafana-ui/src/themes/selectThemeVariant.test.ts
  22. 2 4
      packages/grafana-ui/src/themes/selectThemeVariant.ts
  23. 0 1
      packages/grafana-ui/src/types/index.ts
  24. 1 1
      packages/grafana-ui/src/utils/namedColorsPalette.test.ts
  25. 6 2
      packages/grafana-ui/src/utils/namedColorsPalette.ts
  26. 1 1
      packages/grafana-ui/src/utils/processTimeSeries.ts
  27. 1 6
      packages/grafana-ui/src/utils/storybook/withTheme.tsx
  28. 2 2
      packages/grafana-ui/src/utils/valueFormats/categories.ts
  29. 3 3
      pkg/api/api.go
  30. 4 5
      pkg/api/dashboard.go
  31. 5 1
      pkg/api/dashboard_test.go
  32. 1 0
      pkg/api/frontendsettings.go
  33. 2 0
      pkg/api/http_server.go
  34. 18 6
      pkg/login/ext_user.go
  35. 4 1
      pkg/login/ldap_test.go
  36. 8 0
      pkg/middleware/middleware_test.go
  37. 14 10
      pkg/middleware/quota.go
  38. 30 17
      pkg/middleware/quota_test.go
  39. 0 2
      pkg/middleware/recovery_test.go
  40. 1 0
      pkg/models/user_token.go
  41. 5 0
      pkg/plugins/panel_plugin.go
  42. 18 6
      pkg/services/auth/auth_token.go
  43. 12 0
      pkg/services/auth/auth_token_test.go
  44. 20 3
      pkg/services/quota/quota.go
  45. 1 1
      pkg/services/session/session.go
  46. 1 1
      public/app/core/components/ErrorBoundary/ErrorBoundary.tsx
  47. 51 0
      public/app/core/components/FilterInput/FilterInput.tsx
  48. 42 35
      public/app/core/components/Footer/Footer.tsx
  49. 8 10
      public/app/core/components/OrgActionBar/OrgActionBar.tsx
  50. 7 14
      public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap
  51. 17 16
      public/app/core/components/Page/Page.tsx
  52. 0 1
      public/app/core/components/Page/PageContents.tsx
  53. 2 1
      public/app/core/components/TagFilter/TagFilter.tsx
  54. 3 3
      public/app/core/components/sidemenu/SideMenu.test.tsx
  55. 1 1
      public/app/core/config.ts
  56. 1 1
      public/app/core/directives/autofill_event_fix.ts
  57. 1 1
      public/app/core/directives/dropdown_typeahead.ts
  58. 1 1
      public/app/core/logs_model.ts
  59. 0 1
      public/app/core/profiler.ts
  60. 1 1
      public/app/core/selectors/navModel.ts
  61. 0 3
      public/app/core/services/__mocks__/backend_srv.ts
  62. 5 8
      public/app/core/specs/PasswordStrength.test.tsx
  63. 1 1
      public/app/core/utils/explore.test.ts
  64. 8 1
      public/app/core/utils/explore.ts
  65. 4 4
      public/app/core/utils/kbn.ts
  66. 5 0
      public/app/core/utils/reselect.ts
  67. 2 2
      public/app/core/utils/scrollbar.ts
  68. 2 2
      public/app/core/utils/text.ts
  69. 7 7
      public/app/core/utils/url.ts
  70. 0 2
      public/app/features/admin/AdminEditOrgCtrl.ts
  71. 0 2
      public/app/features/admin/AdminListOrgsCtrl.ts
  72. 1 1
      public/app/features/admin/ServerStats.tsx
  73. 0 1
      public/app/features/admin/StyleGuideCtrl.ts
  74. 1 2
      public/app/features/admin/partials/orgs.html
  75. 1 2
      public/app/features/admin/partials/users.html
  76. 2 3
      public/app/features/alerting/AlertRuleList.test.tsx
  77. 11 14
      public/app/features/alerting/AlertRuleList.tsx
  78. 3 3
      public/app/features/alerting/AlertTabCtrl.ts
  79. 14 28
      public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap
  80. 1 1
      public/app/features/alerting/partials/notifications_list.html
  81. 4 5
      public/app/features/api-keys/ApiKeysPage.test.tsx
  82. 12 20
      public/app/features/api-keys/ApiKeysPage.tsx
  83. 1 7
      public/app/features/dashboard/components/AdHocFilters/AdHocFiltersCtrl.ts
  84. 4 2
      public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx
  85. 3 1
      public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx
  86. 5 5
      public/app/features/dashboard/components/DashboardRow/DashboardRow.tsx
  87. 1 1
      public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx
  88. 1 1
      public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts
  89. 2 4
      public/app/features/dashboard/components/DashboardSettings/template.html
  90. 1 1
      public/app/features/dashboard/components/SubMenu/SubMenu.tsx
  91. 30 28
      public/app/features/dashboard/containers/DashboardPage.test.tsx
  92. 10 10
      public/app/features/dashboard/dashgrid/DashboardGrid.tsx
  93. 48 24
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  94. 5 5
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx
  95. 2 7
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx
  96. 1 0
      public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx
  97. 1 1
      public/app/features/dashboard/dashgrid/PanelResizer.tsx
  98. 0 1
      public/app/features/dashboard/index.ts
  99. 37 16
      public/app/features/dashboard/panel_editor/PanelEditor.tsx
  100. 1 8
      public/app/features/dashboard/panel_editor/QueryEditorRow.tsx

+ 8 - 0
.prettierignore

@@ -0,0 +1,8 @@
+.git
+.github
+dist/
+pkg/
+node_modules
+public/vendor/
+vendor/
+

+ 9 - 22
package.json

@@ -65,7 +65,7 @@
     "html-loader": "^0.5.1",
     "html-webpack-harddisk-plugin": "^0.2.0",
     "html-webpack-plugin": "^3.2.0",
-    "husky": "^0.14.3",
+    "husky": "^1.3.1",
     "jest": "^23.6.0",
     "jest-date-mock": "^1.0.6",
     "lint-staged": "^8.1.3",
@@ -120,8 +120,8 @@
     "typecheck": "tsc --noEmit",
     "jest": "jest --notify --watch",
     "api-tests": "jest --notify --watch --config=tests/api/jest.js",
-    "precommit": "grunt precommit",
-    "storybook": "cd packages/grafana-ui && yarn storybook"
+    "storybook": "cd packages/grafana-ui && yarn storybook",
+    "prettier:check": "prettier -- --list-different \"**/*.{ts,tsx,scss}\""
   },
   "husky": {
     "hooks": {
@@ -129,18 +129,8 @@
     }
   },
   "lint-staged": {
-    "*.{ts,tsx}": [
-      "prettier --write",
-      "git add"
-    ],
-    "*.scss": [
-      "prettier --write",
-      "git add"
-    ],
-    "*pkg/**/*.go": [
-      "gofmt -w -s",
-      "git add"
-    ]
+    "*.{ts,tsx,json,scss}": ["prettier --write", "git add"],
+    "*pkg/**/*.go": ["gofmt -w -s", "git add"]
   },
   "prettier": {
     "trailingComma": "es5",
@@ -151,6 +141,7 @@
   "dependencies": {
     "@babel/polyfill": "^7.0.0",
     "@torkelo/react-select": "2.1.1",
+    "@types/reselect": "^2.2.0",
     "angular": "1.6.6",
     "angular-bindonce": "0.3.1",
     "angular-native-dragdrop": "1.2.2",
@@ -187,6 +178,7 @@
     "redux-logger": "^3.0.6",
     "redux-thunk": "^2.3.0",
     "remarkable": "^1.7.1",
+    "reselect": "^4.0.0",
     "rst2html": "github:thoward/rst2html#990cb89",
     "rxjs": "^6.3.3",
     "slate": "^0.33.4",
@@ -203,12 +195,7 @@
     "**/@types/react": "16.7.6"
   },
   "workspaces": {
-    "packages": [
-      "packages/*"
-    ],
-    "nohoist": [
-      "**/@types/*",
-      "**/@types/*/**"
-    ]
+    "packages": ["packages/*"],
+    "nohoist": ["**/@types/*", "**/@types/*/**"]
   }
 }

+ 1 - 1
packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 import { storiesOf } from '@storybook/react';
-import {  boolean } from '@storybook/addon-knobs';
+import { boolean } from '@storybook/addon-knobs';
 import { SeriesColorPicker, ColorPicker } from './ColorPicker';
 import { action } from '@storybook/addon-actions';
 import { withCenteredStory } from '../../utils/storybook/withCenteredStory';

+ 1 - 1
packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx

@@ -30,7 +30,7 @@ export const warnAboutColorPickerPropsDeprecation = (componentName: string, prop
 
 export const colorPickerFactory = <T extends ColorPickerProps>(
   popover: React.ComponentType<T>,
-  displayName = 'ColorPicker',
+  displayName = 'ColorPicker'
 ) => {
   return class ColorPicker extends Component<T, any> {
     static displayName = displayName;

+ 9 - 3
packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx

@@ -15,7 +15,7 @@ describe('ColorPickerPopover', () => {
 
   describe('rendering', () => {
     it('should render provided color as selected if color provided by name', () => {
-      const wrapper = mount(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} theme={getTheme()}/>);
+      const wrapper = mount(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} theme={getTheme()} />);
       const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
       const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
 
@@ -25,7 +25,9 @@ describe('ColorPickerPopover', () => {
     });
 
     it('should render provided color as selected if color provided by hex', () => {
-      const wrapper = mount(<ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} theme={getTheme()} />);
+      const wrapper = mount(
+        <ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} theme={getTheme()} />
+      );
       const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
       const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
 
@@ -46,7 +48,11 @@ describe('ColorPickerPopover', () => {
 
     it('should pass hex color value to onChange prop by default', () => {
       wrapper = mount(
-        <ColorPickerPopover color={BasicGreen.variants.dark} onChange={onChangeSpy} theme={getTheme(GrafanaThemeType.Light)} />
+        <ColorPickerPopover
+          color={BasicGreen.variants.dark}
+          onChange={onChangeSpy}
+          theme={getTheme(GrafanaThemeType.Light)}
+        />
       );
       const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name);
 

+ 0 - 1
packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx

@@ -119,4 +119,3 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
     );
   }
 }
-

+ 4 - 3
packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx

@@ -7,7 +7,6 @@ import { getTheme } from '../../themes';
 import { GrafanaThemeType } from '../../types';
 
 describe('NamedColorsPalette', () => {
-
   const BasicGreen = getColorDefinitionByName('green');
 
   describe('theme support for named colors', () => {
@@ -23,13 +22,15 @@ describe('NamedColorsPalette', () => {
       expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
 
       wrapper.unmount();
-      wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={getTheme(GrafanaThemeType.Light)} onChange={() => {}} />);
+      wrapper = mount(
+        <NamedColorsPalette color={BasicGreen.name} theme={getTheme(GrafanaThemeType.Light)} onChange={() => {}} />
+      );
       selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
       expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.light);
     });
 
     it('should render dar variant of provided color when theme not provided', () => {
-      wrapper = mount(<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} theme={getTheme()}/>);
+      wrapper = mount(<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} theme={getTheme()} />);
       selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
       expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
     });

+ 0 - 1
packages/grafana-ui/src/components/ColorPicker/SpectrumPalettePointer.tsx

@@ -15,7 +15,6 @@ const SpectrumPalettePointer: React.FunctionComponent<SpectrumPalettePointerProp
     },
   };
 
-
   const pointerColor = selectThemeVariant(
     {
       light: theme.colors.dark3,

+ 1 - 1
packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss

@@ -37,4 +37,4 @@
     @include gradient-horizontal($scrollbarBackground, $scrollbarBackground2);
     border-radius: 6px;
   }
-}
+}

+ 8 - 6
packages/grafana-ui/src/components/EmptySearchResult/_EmptySearchResult.scss

@@ -1,6 +1,8 @@
-.empty-search-result {
-    background-color: $empty-list-cta-bg;
-    padding: $spacer;
-    border-radius: $border-radius;
-    margin-bottom: $spacer*2;
-}
+.empty-search-result {
+  border-left: 3px solid $info-box-border-color;
+  background-color: $empty-list-cta-bg;
+  padding: $spacer;
+  min-width: 350px;
+  border-radius: $border-radius;
+  margin-bottom: $spacer*2;
+}

+ 1 - 1
packages/grafana-ui/src/components/FormLabel/FormLabel.tsx

@@ -31,7 +31,7 @@ export const FormLabel: FunctionComponent<Props> = ({
     <label className={classes} {...rest} htmlFor={htmlFor}>
       {children}
       {tooltip && (
-        <Tooltip placement="top" content={tooltip} theme={"info"}>
+        <Tooltip placement="top" content={tooltip} theme={'info'}>
           <div className="gf-form-help-icon gf-form-help-icon--right-normal">
             <i className="fa fa-info-circle" />
           </div>

+ 1 - 1
packages/grafana-ui/src/components/Gauge/Gauge.test.tsx

@@ -25,7 +25,7 @@ const setup = (propOverrides?: object) => {
     width: 300,
     value: 25,
     decimals: 0,
-    theme: getTheme()
+    theme: getTheme(),
   };
 
   Object.assign(props, propOverrides);

+ 1 - 6
packages/grafana-ui/src/components/PanelOptionsGrid/PanelOptionsGrid.tsx

@@ -6,10 +6,5 @@ interface Props {
 }
 
 export const PanelOptionsGrid: SFC<Props> = ({ children }) => {
-
-  return (
-    <div className="panel-options-grid">
-      {children}
-    </div>
-  );
+  return <div className="panel-options-grid">{children}</div>;
 };

+ 3 - 1
packages/grafana-ui/src/components/Select/Select.tsx

@@ -61,7 +61,9 @@ interface AsyncProps {
 export const MenuList = (props: any) => {
   return (
     <components.MenuList {...props}>
-      <CustomScrollbar autoHide={false} autoHeightMax="inherit">{props.children}</CustomScrollbar>
+      <CustomScrollbar autoHide={false} autoHeightMax="inherit">
+        {props.children}
+      </CustomScrollbar>
     </components.MenuList>
   );
 };

+ 1 - 1
packages/grafana-ui/src/components/Tooltip/Tooltip.tsx

@@ -21,7 +21,7 @@ export const Tooltip = ({ children, theme, ...controllerProps }: TooltipProps) =
                 onMouseEnter={showPopper}
                 onMouseLeave={hidePopper}
                 referenceElement={tooltipTriggerRef.current}
-                wrapperClassName='popper'
+                wrapperClassName="popper"
                 className={popperBackgroundClassName}
                 renderArrow={({ arrowProps, placement }) => (
                   <div className="popper__arrow" data-placement={placement} {...arrowProps} />

+ 1 - 1
packages/grafana-ui/src/components/Tooltip/_Tooltip.scss

@@ -31,7 +31,7 @@ $popper-margin-from-ref: 5px;
 
   // Themes
   &.popper__background--error {
-    @include popper-theme($tooltipBackgroundError, $tooltipBackgroundError);
+    @include popper-theme($tooltipBackgroundError, $white);
   }
 
   &.popper__background--info {

+ 9 - 9
packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx

@@ -82,15 +82,15 @@ export class ValueMappingsEditor extends PureComponent<Props, State> {
 
     return (
       <PanelOptionsGroup title="Add value mapping" onAdd={this.addMapping}>
-          {valueMappings.length > 0 &&
-            valueMappings.map((valueMapping, index) => (
-              <MappingRow
-                key={`${valueMapping.text}-${index}`}
-                valueMapping={valueMapping}
-                updateValueMapping={this.updateGauge}
-                removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
-              />
-            ))}
+        {valueMappings.length > 0 &&
+          valueMappings.map((valueMapping, index) => (
+            <MappingRow
+              key={`${valueMapping.text}-${index}`}
+              valueMapping={valueMapping}
+              updateValueMapping={this.updateGauge}
+              removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
+            />
+          ))}
       </PanelOptionsGroup>
     );
   }

+ 2 - 2
packages/grafana-ui/src/components/index.scss

@@ -7,5 +7,5 @@
 @import 'PanelOptionsGrid/PanelOptionsGrid';
 @import 'ColorPicker/ColorPicker';
 @import 'ValueMappingsEditor/ValueMappingsEditor';
-@import "FormField/FormField";
-@import "EmptySearchResult/EmptySearchResult";
+@import 'EmptySearchResult/EmptySearchResult';
+@import 'FormField/FormField';

+ 1 - 1
packages/grafana-ui/src/themes/dark.ts

@@ -1,4 +1,4 @@
-import tinycolor  from 'tinycolor2';
+import tinycolor from 'tinycolor2';
 import defaultTheme from './default';
 import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
 

+ 7 - 9
packages/grafana-ui/src/themes/default.ts

@@ -1,12 +1,10 @@
-
-
 const theme = {
   name: 'Grafana Default',
   typography: {
     fontFamily: {
       sansSerif: "'Roboto', Helvetica, Arial, sans-serif;",
       serif: "Georgia, 'Times New Roman', Times, serif;",
-      monospace: "Menlo, Monaco, Consolas, 'Courier New', monospace;"
+      monospace: "Menlo, Monaco, Consolas, 'Courier New', monospace;",
     },
     size: {
       base: '13px',
@@ -31,16 +29,16 @@ const theme = {
     lineHeight: {
       xs: 1,
       s: 1.1,
-      m: 4/3,
-      l: 1.5
-    }
+      m: 4 / 3,
+      l: 1.5,
+    },
   },
   brakpoints: {
     xs: '0',
     s: '544px',
     m: '768px',
     l: '992px',
-    xl: '1200px'
+    xl: '1200px',
   },
   spacing: {
     xs: '0',
@@ -55,8 +53,8 @@ const theme = {
       xs: '2px',
       s: '3px',
       m: '5px',
-    }
-  }
+    },
+  },
 };
 
 export default theme;

+ 4 - 6
packages/grafana-ui/src/themes/selectThemeVariant.test.ts

@@ -40,12 +40,10 @@ describe('Theme variable variant selector', () => {
   it('return dark theme variant if no theme given', () => {
     const theme = lightThemeMock;
 
-    const selectedValue = selectThemeVariant(
-      {
-        dark: theme.color.red,
-        light: theme.color.green,
-      }
-    );
+    const selectedValue = selectThemeVariant({
+      dark: theme.color.red,
+      light: theme.color.green,
+    });
 
     expect(selectedValue).toBe(lightThemeMock.color.red);
   });

+ 2 - 4
packages/grafana-ui/src/themes/selectThemeVariant.ts

@@ -1,8 +1,6 @@
-import {  GrafanaThemeType } from '../types/theme';
+import { GrafanaThemeType } from '../types/theme';
 
-type VariantDescriptor = {
-  [key in GrafanaThemeType]: string | number;
-};
+type VariantDescriptor = { [key in GrafanaThemeType]: string | number };
 
 export const selectThemeVariant = (variants: VariantDescriptor, currentTheme?: GrafanaThemeType) => {
   return variants[currentTheme || GrafanaThemeType.Dark];

+ 0 - 1
packages/grafana-ui/src/types/index.ts

@@ -1,4 +1,3 @@
-
 export * from './data';
 export * from './time';
 export * from './panel';

+ 1 - 1
packages/grafana-ui/src/utils/namedColorsPalette.test.ts

@@ -45,7 +45,7 @@ describe('colors', () => {
 
   describe('getColorFromHexRgbOrName', () => {
     it('returns black for unknown color', () => {
-      expect(getColorFromHexRgbOrName('aruba-sunshine')).toBe("#000000");
+      expect(getColorFromHexRgbOrName('aruba-sunshine')).toBe('#000000');
     });
 
     it('returns dark hex variant for known color if theme not specified', () => {

+ 6 - 2
packages/grafana-ui/src/utils/namedColorsPalette.ts

@@ -70,7 +70,9 @@ export const getColorDefinitionByName = (name: Color): ColorDefinition => {
 };
 
 export const getColorDefinition = (hex: string, theme: GrafanaThemeType): ColorDefinition | undefined => {
-  return flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.variants[theme] === hex)[0];
+  return flatten(Array.from(getNamedColorPalette().values())).filter(
+    definition => definition.variants[theme] === hex
+  )[0];
 };
 
 const isHex = (color: string) => {
@@ -95,7 +97,9 @@ export const getColorName = (color?: string, theme?: GrafanaThemeType): Color |
 };
 
 export const getColorByName = (colorName: string) => {
-  const definition = flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.name === colorName);
+  const definition = flatten(Array.from(getNamedColorPalette().values())).filter(
+    definition => definition.name === colorName
+  );
   return definition.length > 0 ? definition[0] : undefined;
 };
 

+ 1 - 1
packages/grafana-ui/src/utils/processTimeSeries.ts

@@ -51,7 +51,7 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
       }
 
       if (currentValue !== null && typeof currentValue !== 'number') {
-        throw {message: 'Time series contains non number values'};
+        throw { message: 'Time series contains non number values' };
       }
 
       // Due to missing values we could have different timeStep all along the series

+ 1 - 6
packages/grafana-ui/src/utils/storybook/withTheme.tsx

@@ -15,12 +15,7 @@ const ThemableStory: React.FunctionComponent<{}> = ({ children }) => {
     GrafanaThemeType.Dark
   );
 
-  return (
-    <ThemeContext.Provider value={getTheme(themeKnob)}>
-      {children}
-    </ThemeContext.Provider>
-
-  );
+  return <ThemeContext.Provider value={getTheme(themeKnob)}>{children}</ThemeContext.Provider>;
 };
 
 // Temporary solution. When we update to Storybook V5 we will be able to pass data from decorator to story

+ 2 - 2
packages/grafana-ui/src/utils/valueFormats/categories.ts

@@ -306,7 +306,7 @@ export const getCategories = (): ValueFormatCategory[] => [
       { name: 'kilometers/hour (km/h)', id: 'velocitykmh', fn: toFixedUnit('km/h') },
       { name: 'miles/hour (mph)', id: 'velocitymph', fn: toFixedUnit('mph') },
       { name: 'knot (kn)', id: 'velocityknot', fn: toFixedUnit('kn') },
-    ]
+    ],
   },
   {
     name: 'Volume',
@@ -318,5 +318,5 @@ export const getCategories = (): ValueFormatCategory[] => [
       { name: 'cubic decimetre', id: 'dm3', fn: toFixedUnit('dm³') },
       { name: 'gallons', id: 'gallons', fn: toFixedUnit('gal') },
     ],
-  }
+  },
 ];

+ 3 - 3
pkg/api/api.go

@@ -16,7 +16,7 @@ func (hs *HTTPServer) registerRoutes() {
 	reqOrgAdmin := middleware.ReqOrgAdmin
 	redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL()
 	redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL()
-	quota := middleware.Quota
+	quota := middleware.Quota(hs.QuotaService)
 	bind := binding.Bind
 
 	r := hs.RouteRegister
@@ -286,7 +286,7 @@ func (hs *HTTPServer) registerRoutes() {
 
 			dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff))
 
-			dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), Wrap(PostDashboard))
+			dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), Wrap(hs.PostDashboard))
 			dashboardRoute.Get("/home", Wrap(GetHomeDashboard))
 			dashboardRoute.Get("/tags", GetDashboardTags)
 			dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), Wrap(ImportDashboard))
@@ -294,7 +294,7 @@ func (hs *HTTPServer) registerRoutes() {
 			dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) {
 				dashIdRoute.Get("/versions", Wrap(GetDashboardVersions))
 				dashIdRoute.Get("/versions/:id", Wrap(GetDashboardVersion))
-				dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), Wrap(RestoreDashboardVersion))
+				dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), Wrap(hs.RestoreDashboardVersion))
 
 				dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) {
 					dashboardPermissionRoute.Get("/", Wrap(GetDashboardPermissionList))

+ 4 - 5
pkg/api/dashboard.go

@@ -18,7 +18,6 @@ import (
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/services/guardian"
-	"github.com/grafana/grafana/pkg/services/quota"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
 )
@@ -208,14 +207,14 @@ func DeleteDashboardByUID(c *m.ReqContext) Response {
 	})
 }
 
-func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
+func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.UserId = c.UserId
 
 	dash := cmd.GetDashboardModel()
 
 	if dash.Id == 0 && dash.Uid == "" {
-		limitReached, err := quota.QuotaReached(c, "dashboard")
+		limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard")
 		if err != nil {
 			return Error(500, "failed to get quota", err)
 		}
@@ -463,7 +462,7 @@ func CalculateDashboardDiff(c *m.ReqContext, apiOptions dtos.CalculateDiffOption
 }
 
 // RestoreDashboardVersion restores a dashboard to the given version.
-func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersionCommand) Response {
+func (hs *HTTPServer) RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersionCommand) Response {
 	dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"), "")
 	if rsp != nil {
 		return rsp
@@ -490,7 +489,7 @@ func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersio
 	saveCmd.Dashboard.Set("uid", dash.Uid)
 	saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
 
-	return PostDashboard(c, saveCmd)
+	return hs.PostDashboard(c, saveCmd)
 }
 
 func GetDashboardTags(c *m.ReqContext) {

+ 5 - 1
pkg/api/dashboard_test.go

@@ -881,12 +881,16 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d
 	Convey(desc+" "+url, func() {
 		defer bus.ClearBusHandlers()
 
+		hs := HTTPServer{
+			Bus: bus.GetBus(),
+		}
+
 		sc := setupScenarioContext(url)
 		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
 			sc.context = c
 			sc.context.SignedInUser = &m.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId}
 
-			return PostDashboard(c, cmd)
+			return hs.PostDashboard(c, cmd)
 		})
 
 		origNewDashboardService := dashboards.NewService

+ 1 - 0
pkg/api/frontendsettings.go

@@ -145,6 +145,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
 			"info":         panel.Info,
 			"hideFromList": panel.HideFromList,
 			"sort":         getPanelSort(panel.Id),
+			"dataFormats":  panel.DataFormats,
 		}
 	}
 

+ 2 - 0
pkg/api/http_server.go

@@ -24,6 +24,7 @@ import (
 	"github.com/grafana/grafana/pkg/services/cache"
 	"github.com/grafana/grafana/pkg/services/datasources"
 	"github.com/grafana/grafana/pkg/services/hooks"
+	"github.com/grafana/grafana/pkg/services/quota"
 	"github.com/grafana/grafana/pkg/services/rendering"
 	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
@@ -55,6 +56,7 @@ type HTTPServer struct {
 	CacheService     *cache.CacheService      `inject:""`
 	DatasourceCache  datasources.CacheService `inject:""`
 	AuthTokenService models.UserTokenService  `inject:""`
+	QuotaService     *quota.QuotaService      `inject:""`
 }
 
 func (hs *HTTPServer) Init() error {

+ 18 - 6
pkg/login/ext_user.go

@@ -4,18 +4,30 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/registry"
 	"github.com/grafana/grafana/pkg/services/quota"
 )
 
 func init() {
-	bus.AddHandler("auth", UpsertUser)
+	registry.RegisterService(&LoginService{})
 }
 
 var (
 	logger = log.New("login.ext_user")
 )
 
-func UpsertUser(cmd *m.UpsertUserCommand) error {
+type LoginService struct {
+	Bus          bus.Bus             `inject:""`
+	QuotaService *quota.QuotaService `inject:""`
+}
+
+func (ls *LoginService) Init() error {
+	ls.Bus.AddHandler(ls.UpsertUser)
+
+	return nil
+}
+
+func (ls *LoginService) UpsertUser(cmd *m.UpsertUserCommand) error {
 	extUser := cmd.ExternalUser
 
 	userQuery := &m.GetUserByAuthInfoQuery{
@@ -37,7 +49,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
 			return ErrInvalidCredentials
 		}
 
-		limitReached, err := quota.QuotaReached(cmd.ReqContext, "user")
+		limitReached, err := ls.QuotaService.QuotaReached(cmd.ReqContext, "user")
 		if err != nil {
 			log.Warn("Error getting user quota. error: %v", err)
 			return ErrGettingUserQuota
@@ -57,7 +69,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
 				AuthModule: extUser.AuthModule,
 				AuthId:     extUser.AuthId,
 			}
-			if err := bus.Dispatch(cmd2); err != nil {
+			if err := ls.Bus.Dispatch(cmd2); err != nil {
 				return err
 			}
 		}
@@ -78,12 +90,12 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
 
 	// Sync isGrafanaAdmin permission
 	if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != cmd.Result.IsAdmin {
-		if err := bus.Dispatch(&m.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil {
+		if err := ls.Bus.Dispatch(&m.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil {
 			return err
 		}
 	}
 
-	err = bus.Dispatch(&m.SyncTeamsCommand{
+	err = ls.Bus.Dispatch(&m.SyncTeamsCommand{
 		User:         cmd.Result,
 		ExternalUser: extUser,
 	})

+ 4 - 1
pkg/login/ldap_test.go

@@ -395,8 +395,11 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
 		defer bus.ClearBusHandlers()
 
 		sc := &scenarioContext{}
+		loginService := &LoginService{
+			Bus: bus.GetBus(),
+		}
 
-		bus.AddHandler("test", UpsertUser)
+		bus.AddHandler("test", loginService.UpsertUser)
 
 		bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.SyncTeamsCommand) error {
 			return nil

+ 8 - 0
pkg/middleware/middleware_test.go

@@ -682,6 +682,7 @@ type fakeUserAuthTokenService struct {
 	tryRotateTokenProvider func(token *m.UserToken, clientIP, userAgent string) (bool, error)
 	lookupTokenProvider    func(unhashedToken string) (*m.UserToken, error)
 	revokeTokenProvider    func(token *m.UserToken) error
+	activeAuthTokenCount   func() (int64, error)
 }
 
 func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
@@ -704,6 +705,9 @@ func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
 		revokeTokenProvider: func(token *m.UserToken) error {
 			return nil
 		},
+		activeAuthTokenCount: func() (int64, error) {
+			return 10, nil
+		},
 	}
 }
 
@@ -722,3 +726,7 @@ func (s *fakeUserAuthTokenService) TryRotateToken(token *m.UserToken, clientIP,
 func (s *fakeUserAuthTokenService) RevokeToken(token *m.UserToken) error {
 	return s.revokeTokenProvider(token)
 }
+
+func (s *fakeUserAuthTokenService) ActiveTokenCount() (int64, error) {
+	return s.activeAuthTokenCount()
+}

+ 14 - 10
pkg/middleware/quota.go

@@ -9,16 +9,20 @@ import (
 	"github.com/grafana/grafana/pkg/services/quota"
 )
 
-func Quota(target string) macaron.Handler {
-	return func(c *m.ReqContext) {
-		limitReached, err := quota.QuotaReached(c, target)
-		if err != nil {
-			c.JsonApiErr(500, "failed to get quota", err)
-			return
-		}
-		if limitReached {
-			c.JsonApiErr(403, fmt.Sprintf("%s Quota reached", target), nil)
-			return
+// Quota returns a function that returns a function used to call quotaservice based on target name
+func Quota(quotaService *quota.QuotaService) func(target string) macaron.Handler {
+	//https://open.spotify.com/track/7bZSoBEAEEUsGEuLOf94Jm?si=T1Tdju5qRSmmR0zph_6RBw fuuuuunky
+	return func(target string) macaron.Handler {
+		return func(c *m.ReqContext) {
+			limitReached, err := quotaService.QuotaReached(c, target)
+			if err != nil {
+				c.JsonApiErr(500, "failed to get quota", err)
+				return
+			}
+			if limitReached {
+				c.JsonApiErr(403, fmt.Sprintf("%s Quota reached", target), nil)
+				return
+			}
 		}
 	}
 }

+ 30 - 17
pkg/middleware/quota_test.go

@@ -3,9 +3,10 @@ package middleware
 import (
 	"testing"
 
+	"github.com/grafana/grafana/pkg/services/quota"
+
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
 	. "github.com/smartystreets/goconvey/convey"
 )
@@ -13,10 +14,6 @@ import (
 func TestMiddlewareQuota(t *testing.T) {
 
 	Convey("Given the grafana quota middleware", t, func() {
-		session.GetSessionCount = func() int {
-			return 4
-		}
-
 		setting.AnonymousEnabled = false
 		setting.Quota = setting.QuotaSettings{
 			Enabled: true,
@@ -39,6 +36,12 @@ func TestMiddlewareQuota(t *testing.T) {
 			},
 		}
 
+		fakeAuthTokenService := newFakeUserAuthTokenService()
+		qs := &quota.QuotaService{
+			AuthTokenService: fakeAuthTokenService,
+		}
+		QuotaFn := Quota(qs)
+
 		middlewareScenario("with user not logged in", func(sc *scenarioContext) {
 			bus.AddHandler("globalQuota", func(query *m.GetGlobalQuotaByTargetQuery) error {
 				query.Result = &m.GlobalQuotaDTO{
@@ -48,26 +51,30 @@ func TestMiddlewareQuota(t *testing.T) {
 				}
 				return nil
 			})
+
 			Convey("global quota not reached", func() {
-				sc.m.Get("/user", Quota("user"), sc.defaultHandler)
+				sc.m.Get("/user", QuotaFn("user"), sc.defaultHandler)
 				sc.fakeReq("GET", "/user").exec()
 				So(sc.resp.Code, ShouldEqual, 200)
 			})
+
 			Convey("global quota reached", func() {
 				setting.Quota.Global.User = 4
-				sc.m.Get("/user", Quota("user"), sc.defaultHandler)
+				sc.m.Get("/user", QuotaFn("user"), sc.defaultHandler)
 				sc.fakeReq("GET", "/user").exec()
 				So(sc.resp.Code, ShouldEqual, 403)
 			})
+
 			Convey("global session quota not reached", func() {
 				setting.Quota.Global.Session = 10
-				sc.m.Get("/user", Quota("session"), sc.defaultHandler)
+				sc.m.Get("/user", QuotaFn("session"), sc.defaultHandler)
 				sc.fakeReq("GET", "/user").exec()
 				So(sc.resp.Code, ShouldEqual, 200)
 			})
+
 			Convey("global session quota reached", func() {
 				setting.Quota.Global.Session = 1
-				sc.m.Get("/user", Quota("session"), sc.defaultHandler)
+				sc.m.Get("/user", QuotaFn("session"), sc.defaultHandler)
 				sc.fakeReq("GET", "/user").exec()
 				So(sc.resp.Code, ShouldEqual, 403)
 			})
@@ -95,6 +102,7 @@ func TestMiddlewareQuota(t *testing.T) {
 				}
 				return nil
 			})
+
 			bus.AddHandler("userQuota", func(query *m.GetUserQuotaByTargetQuery) error {
 				query.Result = &m.UserQuotaDTO{
 					Target: query.Target,
@@ -103,6 +111,7 @@ func TestMiddlewareQuota(t *testing.T) {
 				}
 				return nil
 			})
+
 			bus.AddHandler("orgQuota", func(query *m.GetOrgQuotaByTargetQuery) error {
 				query.Result = &m.OrgQuotaDTO{
 					Target: query.Target,
@@ -111,45 +120,49 @@ func TestMiddlewareQuota(t *testing.T) {
 				}
 				return nil
 			})
+
 			Convey("global datasource quota reached", func() {
 				setting.Quota.Global.DataSource = 4
-				sc.m.Get("/ds", Quota("data_source"), sc.defaultHandler)
+				sc.m.Get("/ds", QuotaFn("data_source"), sc.defaultHandler)
 				sc.fakeReq("GET", "/ds").exec()
 				So(sc.resp.Code, ShouldEqual, 403)
 			})
+
 			Convey("user Org quota not reached", func() {
 				setting.Quota.User.Org = 5
-				sc.m.Get("/org", Quota("org"), sc.defaultHandler)
+				sc.m.Get("/org", QuotaFn("org"), sc.defaultHandler)
 				sc.fakeReq("GET", "/org").exec()
 				So(sc.resp.Code, ShouldEqual, 200)
 			})
+
 			Convey("user Org quota reached", func() {
 				setting.Quota.User.Org = 4
-				sc.m.Get("/org", Quota("org"), sc.defaultHandler)
+				sc.m.Get("/org", QuotaFn("org"), sc.defaultHandler)
 				sc.fakeReq("GET", "/org").exec()
 				So(sc.resp.Code, ShouldEqual, 403)
 			})
+
 			Convey("org dashboard quota not reached", func() {
 				setting.Quota.Org.Dashboard = 10
-				sc.m.Get("/dashboard", Quota("dashboard"), sc.defaultHandler)
+				sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler)
 				sc.fakeReq("GET", "/dashboard").exec()
 				So(sc.resp.Code, ShouldEqual, 200)
 			})
+
 			Convey("org dashboard quota reached", func() {
 				setting.Quota.Org.Dashboard = 4
-				sc.m.Get("/dashboard", Quota("dashboard"), sc.defaultHandler)
+				sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler)
 				sc.fakeReq("GET", "/dashboard").exec()
 				So(sc.resp.Code, ShouldEqual, 403)
 			})
+
 			Convey("org dashboard quota reached but quotas disabled", func() {
 				setting.Quota.Org.Dashboard = 4
 				setting.Quota.Enabled = false
-				sc.m.Get("/dashboard", Quota("dashboard"), sc.defaultHandler)
+				sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler)
 				sc.fakeReq("GET", "/dashboard").exec()
 				So(sc.resp.Code, ShouldEqual, 200)
 			})
-
 		})
-
 	})
 }

+ 0 - 2
pkg/middleware/recovery_test.go

@@ -6,7 +6,6 @@ import (
 
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
 	. "github.com/smartystreets/goconvey/convey"
 	macaron "gopkg.in/macaron.v1"
@@ -66,7 +65,6 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
 		sc.userAuthTokenService = newFakeUserAuthTokenService()
 		sc.m.Use(GetContextHandler(sc.userAuthTokenService))
 		// mock out gc goroutine
-		session.StartSessionGC = func() {}
 		sc.m.Use(OrgRedirect())
 		sc.m.Use(AddDefaultResponseHeaders())
 

+ 1 - 0
pkg/models/user_token.go

@@ -29,4 +29,5 @@ type UserTokenService interface {
 	LookupToken(unhashedToken string) (*UserToken, error)
 	TryRotateToken(token *UserToken, clientIP, userAgent string) (bool, error)
 	RevokeToken(token *UserToken) error
+	ActiveTokenCount() (int64, error)
 }

+ 5 - 0
pkg/plugins/panel_plugin.go

@@ -4,6 +4,7 @@ import "encoding/json"
 
 type PanelPlugin struct {
 	FrontendPluginBase
+	DataFormats []string `json:"dataFormats"`
 }
 
 func (p *PanelPlugin) Load(decoder *json.Decoder, pluginDir string) error {
@@ -15,6 +16,10 @@ func (p *PanelPlugin) Load(decoder *json.Decoder, pluginDir string) error {
 		return err
 	}
 
+	if p.DataFormats == nil {
+		p.DataFormats = []string{"time_series", "table"}
+	}
+
 	Panels[p.Id] = p
 	return nil
 }

+ 18 - 6
pkg/services/auth/auth_token.go

@@ -35,6 +35,13 @@ func (s *UserAuthTokenService) Init() error {
 	return nil
 }
 
+func (s *UserAuthTokenService) ActiveTokenCount() (int64, error) {
+	var model userAuthToken
+	count, err := s.SQLStore.NewSession().Where(`created_at > ? AND rotated_at > ?`, s.createdAfterParam(), s.rotatedAfterParam()).Count(&model)
+
+	return count, err
+}
+
 func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserToken, error) {
 	clientIP = util.ParseIPAddress(clientIP)
 	token, err := util.RandomHex(16)
@@ -79,13 +86,8 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserTo
 		s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken)
 	}
 
-	tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
-	tokenMaxInactiveLifetime := time.Duration(s.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour
-	createdAfter := getTime().Add(-tokenMaxLifetime).Unix()
-	rotatedAfter := getTime().Add(-tokenMaxInactiveLifetime).Unix()
-
 	var model userAuthToken
-	exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ? AND rotated_at > ?", hashedToken, hashedToken, createdAfter, rotatedAfter).Get(&model)
+	exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ? AND rotated_at > ?", hashedToken, hashedToken, s.createdAfterParam(), s.rotatedAfterParam()).Get(&model)
 	if err != nil {
 		return nil, err
 	}
@@ -219,6 +221,16 @@ func (s *UserAuthTokenService) RevokeToken(token *models.UserToken) error {
 	return nil
 }
 
+func (s *UserAuthTokenService) createdAfterParam() int64 {
+	tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
+	return getTime().Add(-tokenMaxLifetime).Unix()
+}
+
+func (s *UserAuthTokenService) rotatedAfterParam() int64 {
+	tokenMaxInactiveLifetime := time.Duration(s.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour
+	return getTime().Add(-tokenMaxInactiveLifetime).Unix()
+}
+
 func hashToken(token string) string {
 	hashBytes := sha256.Sum256([]byte(token + setting.SecretKey))
 	return hex.EncodeToString(hashBytes[:])

+ 12 - 0
pkg/services/auth/auth_token_test.go

@@ -31,6 +31,12 @@ func TestUserAuthToken(t *testing.T) {
 			So(userToken, ShouldNotBeNil)
 			So(userToken.AuthTokenSeen, ShouldBeFalse)
 
+			Convey("Can count active tokens", func() {
+				count, err := userAuthTokenService.ActiveTokenCount()
+				So(err, ShouldBeNil)
+				So(count, ShouldEqual, 1)
+			})
+
 			Convey("When lookup unhashed token should return user auth token", func() {
 				userToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
 				So(err, ShouldBeNil)
@@ -114,6 +120,12 @@ func TestUserAuthToken(t *testing.T) {
 				notGood, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
 				So(err, ShouldEqual, models.ErrUserTokenNotFound)
 				So(notGood, ShouldBeNil)
+
+				Convey("should not find active token when expired", func() {
+					count, err := userAuthTokenService.ActiveTokenCount()
+					So(err, ShouldBeNil)
+					So(count, ShouldEqual, 0)
+				})
 			})
 
 			Convey("when rotated_at is 5 days ago and created_at is 29 days and 23:59:59 ago should not find token", func() {

+ 20 - 3
pkg/services/quota/quota.go

@@ -3,11 +3,23 @@ package quota
 import (
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/session"
+	"github.com/grafana/grafana/pkg/registry"
 	"github.com/grafana/grafana/pkg/setting"
 )
 
-func QuotaReached(c *m.ReqContext, target string) (bool, error) {
+func init() {
+	registry.RegisterService(&QuotaService{})
+}
+
+type QuotaService struct {
+	AuthTokenService m.UserTokenService `inject:""`
+}
+
+func (qs *QuotaService) Init() error {
+	return nil
+}
+
+func (qs *QuotaService) QuotaReached(c *m.ReqContext, target string) (bool, error) {
 	if !setting.Quota.Enabled {
 		return false, nil
 	}
@@ -30,7 +42,12 @@ func QuotaReached(c *m.ReqContext, target string) (bool, error) {
 				return true, nil
 			}
 			if target == "session" {
-				usedSessions := session.GetSessionCount()
+
+				usedSessions, err := qs.AuthTokenService.ActiveTokenCount()
+				if err != nil {
+					return false, err
+				}
+
 				if int64(usedSessions) > scope.DefaultLimit {
 					c.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit)
 					return true, nil

+ 1 - 1
pkg/services/session/session.go

@@ -19,7 +19,7 @@ const (
 
 var sessionManager *ms.Manager
 var sessionOptions *ms.Options
-var StartSessionGC func()
+var StartSessionGC func() = func() {}
 var GetSessionCount func() int
 var sessionLogger = log.New("session")
 var sessionConnMaxLifetime int64

+ 1 - 1
public/app/core/components/ErrorBoundary/ErrorBoundary.tsx

@@ -27,7 +27,7 @@ class ErrorBoundary extends Component<Props, State> {
   componentDidCatch(error: Error, errorInfo: ErrorInfo) {
     this.setState({
       error: error,
-      errorInfo: errorInfo
+      errorInfo: errorInfo,
     });
   }
 

+ 51 - 0
public/app/core/components/FilterInput/FilterInput.tsx

@@ -0,0 +1,51 @@
+import React, { forwardRef } from 'react';
+
+const specialChars = ['(', '[', '{', '}', ']', ')', '|', '*', '+', '-', '.', '?', '<', '>', '#', '&', '^', '$'];
+
+export const escapeStringForRegex = (value: string) => {
+  if (!value) {
+    return value;
+  }
+
+  const newValue = specialChars.reduce(
+    (escaped, currentChar) => escaped.replace(currentChar, '\\' + currentChar),
+    value
+  );
+
+  return newValue;
+};
+
+export const unEscapeStringFromRegex = (value: string) => {
+  if (!value) {
+    return value;
+  }
+
+  const newValue = specialChars.reduce(
+    (escaped, currentChar) => escaped.replace('\\' + currentChar, currentChar),
+    value
+  );
+
+  return newValue;
+};
+
+export interface Props {
+  value: string | undefined;
+  placeholder?: string;
+  labelClassName?: string;
+  inputClassName?: string;
+  onChange: (value: string) => void;
+}
+
+export const FilterInput = forwardRef<HTMLInputElement, Props>((props, ref) => (
+  <label className={props.labelClassName}>
+    <input
+      ref={ref}
+      type="text"
+      className={props.inputClassName}
+      value={unEscapeStringFromRegex(props.value)}
+      onChange={event => props.onChange(escapeStringForRegex(event.target.value))}
+      placeholder={props.placeholder ? props.placeholder : null}
+    />
+    <i className="gf-form-input-icon fa fa-search" />
+  </label>
+));

+ 42 - 35
public/app/core/components/Footer/Footer.tsx

@@ -9,42 +9,49 @@ interface Props {
   newGrafanaVersion: string;
 }
 
-export const Footer: FC<Props> = React.memo(({appName, buildVersion, buildCommit, newGrafanaVersionExists, newGrafanaVersion}) => {
-  return (
-    <footer className="footer">
-      <div className="text-center">
-        <ul>
-          <li>
-            <a href="http://docs.grafana.org" target="_blank">
-              <i className="fa fa-file-code-o" /> Docs
-            </a>
-          </li>
-          <li>
-            <a href="https://grafana.com/services/support" target="_blank">
-              <i className="fa fa-support" /> Support Plans
-            </a>
-          </li>
-          <li>
-            <a href="https://community.grafana.com/" target="_blank">
-              <i className="fa fa-comments-o" /> Community
-            </a>
-          </li>
-          <li>
-            <a href="https://grafana.com" target="_blank">{appName}</a> <span>v{buildVersion} (commit: {buildCommit})</span>
-          </li>
-          {newGrafanaVersionExists && (
+export const Footer: FC<Props> = React.memo(
+  ({ appName, buildVersion, buildCommit, newGrafanaVersionExists, newGrafanaVersion }) => {
+    return (
+      <footer className="footer">
+        <div className="text-center">
+          <ul>
             <li>
-              <Tooltip placement="auto" content={newGrafanaVersion}>
-                <a href="https://grafana.com/get" target="_blank">
-                  New version available!
-                </a>
-              </Tooltip>
+              <a href="http://docs.grafana.org" target="_blank">
+                <i className="fa fa-file-code-o" /> Docs
+              </a>
             </li>
-          )}
-        </ul>
-      </div>
-    </footer>
-  );
-});
+            <li>
+              <a href="https://grafana.com/services/support" target="_blank">
+                <i className="fa fa-support" /> Support Plans
+              </a>
+            </li>
+            <li>
+              <a href="https://community.grafana.com/" target="_blank">
+                <i className="fa fa-comments-o" /> Community
+              </a>
+            </li>
+            <li>
+              <a href="https://grafana.com" target="_blank">
+                {appName}
+              </a>{' '}
+              <span>
+                v{buildVersion} (commit: {buildCommit})
+              </span>
+            </li>
+            {newGrafanaVersionExists && (
+              <li>
+                <Tooltip placement="auto" content={newGrafanaVersion}>
+                  <a href="https://grafana.com/get" target="_blank">
+                    New version available!
+                  </a>
+                </Tooltip>
+              </li>
+            )}
+          </ul>
+        </div>
+      </footer>
+    );
+  }
+);
 
 export default Footer;

+ 8 - 10
public/app/core/components/OrgActionBar/OrgActionBar.tsx

@@ -1,5 +1,6 @@
 import React, { PureComponent } from 'react';
 import LayoutSelector, { LayoutMode } from '../LayoutSelector/LayoutSelector';
+import { FilterInput } from '../FilterInput/FilterInput';
 
 export interface Props {
   searchQuery: string;
@@ -22,16 +23,13 @@ export default class OrgActionBar extends PureComponent<Props> {
     return (
       <div className="page-action-bar">
         <div className="gf-form gf-form--grow">
-          <label className="gf-form--has-input-icon">
-            <input
-              type="text"
-              className="gf-form-input width-20"
-              value={searchQuery}
-              onChange={event => setSearchQuery(event.target.value)}
-              placeholder="Filter by name or type"
-            />
-            <i className="gf-form-input-icon fa fa-search" />
-          </label>
+          <FilterInput
+            labelClassName="gf-form--has-input-icon"
+            inputClassName="gf-form-input width-20"
+            value={searchQuery}
+            onChange={setSearchQuery}
+            placeholder={'Filter by name or type'}
+          />
           <LayoutSelector mode={layoutMode} onLayoutModeChanged={(mode: LayoutMode) => onSetLayoutMode(mode)} />
         </div>
         <div className="page-action-bar__spacer" />

+ 7 - 14
public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap

@@ -7,20 +7,13 @@ exports[`Render should render component 1`] = `
   <div
     className="gf-form gf-form--grow"
   >
-    <label
-      className="gf-form--has-input-icon"
-    >
-      <input
-        className="gf-form-input width-20"
-        onChange={[Function]}
-        placeholder="Filter by name or type"
-        type="text"
-        value=""
-      />
-      <i
-        className="gf-form-input-icon fa fa-search"
-      />
-    </label>
+    <ForwardRef
+      inputClassName="gf-form-input width-20"
+      labelClassName="gf-form--has-input-icon"
+      onChange={[MockFunction]}
+      placeholder="Filter by name or type"
+      value=""
+    />
     <LayoutSelector
       onLayoutModeChanged={[Function]}
     />

+ 17 - 16
public/app/core/components/Page/Page.tsx

@@ -33,9 +33,9 @@ class Page extends Component<Props> {
   updateTitle = () => {
     const title = this.getPageTitle;
     document.title = title ? title + ' - Grafana' : 'Grafana';
-  }
+  };
 
-  get getPageTitle () {
+  get getPageTitle() {
     const { navModel } = this.props;
     if (navModel) {
       return getTitleFromNavModel(navModel) || undefined;
@@ -47,20 +47,21 @@ class Page extends Component<Props> {
     const { navModel } = this.props;
     const { buildInfo } = config;
     return (
-        <div className="page-scrollbar-wrapper">
-          <CustomScrollbar autoHeightMin={'100%'}>
-            <div className="page-scrollbar-content">
-              <PageHeader model={navModel} />
-              {this.props.children}
-              <Footer
-                appName="Grafana"
-                buildCommit={buildInfo.commit}
-                buildVersion={buildInfo.version}
-                newGrafanaVersion={buildInfo.latestVersion}
-                newGrafanaVersionExists={buildInfo.hasUpdate} />
-            </div>
-          </CustomScrollbar>
-        </div>
+      <div className="page-scrollbar-wrapper">
+        <CustomScrollbar autoHeightMin={'100%'}>
+          <div className="page-scrollbar-content">
+            <PageHeader model={navModel} />
+            {this.props.children}
+            <Footer
+              appName="Grafana"
+              buildCommit={buildInfo.commit}
+              buildVersion={buildInfo.version}
+              newGrafanaVersion={buildInfo.latestVersion}
+              newGrafanaVersionExists={buildInfo.hasUpdate}
+            />
+          </div>
+        </CustomScrollbar>
+      </div>
     );
   }
 }

+ 0 - 1
public/app/core/components/Page/PageContents.tsx

@@ -10,7 +10,6 @@ interface Props {
 }
 
 class PageContents extends Component<Props> {
-
   render() {
     const { isLoading } = this.props;
 

+ 2 - 1
public/app/core/components/TagFilter/TagFilter.tsx

@@ -5,6 +5,7 @@ import AsyncSelect from '@torkelo/react-select/lib/Async';
 import { TagOption } from './TagOption';
 import { TagBadge } from './TagBadge';
 import { components } from '@torkelo/react-select';
+import { escapeStringForRegex } from '../FilterInput/FilterInput';
 
 export interface Props {
   tags: string[];
@@ -51,7 +52,7 @@ export class TagFilter extends React.Component<Props, any> {
       value: tags,
       styles: resetSelectStyles(),
       filterOption: (option, searchQuery) => {
-        const regex = RegExp(searchQuery, 'i');
+        const regex = RegExp(escapeStringForRegex(searchQuery), 'i');
         return regex.test(option.value);
       },
       components: {

+ 3 - 3
public/app/core/components/sidemenu/SideMenu.test.tsx

@@ -13,9 +13,9 @@ jest.mock('app/store/store', () => ({
     getState: jest.fn().mockReturnValue({
       location: {
         lastUpdated: 0,
-      }
-    })
-  }
+      },
+    }),
+  },
 }));
 
 jest.mock('app/core/services/context_srv', () => ({

+ 1 - 1
public/app/core/config.ts

@@ -57,7 +57,7 @@ export class Settings {
         isEnterprise: false,
       },
       viewersCanEdit: false,
-      disableSanitizeHtml: false
+      disableSanitizeHtml: false,
     };
 
     _.extend(this, defaults, options);

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

@@ -28,7 +28,7 @@ export function autofillEventFix($compile) {
         input.removeEventListener('animationstart', onAnimationStart);
         // input.removeEventListener('change', onChange);
       });
-    }
+    },
   };
 }
 

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

@@ -142,7 +142,7 @@ export function dropdownTypeahead2($compile) {
       const $input = $(inputTemplate);
       const $button = $(buttonTemplate);
       const timeoutId = {
-        blur: null
+        blur: null,
       };
       $input.appendTo(elem);
       $button.appendTo(elem);

+ 1 - 1
public/app/core/logs_model.ts

@@ -344,7 +344,7 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): Time
       datapoints: series.datapoints,
       target: series.alias,
       alias: series.alias,
-      color: series.color
+      color: series.color,
     };
   });
 }

+ 0 - 1
public/app/core/profiler.ts

@@ -1,4 +1,3 @@
-
 export class Profiler {
   panelsRendered: number;
   enabled: boolean;

+ 1 - 1
public/app/core/selectors/navModel.ts

@@ -43,5 +43,5 @@ export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel)
 }
 
 export const getTitleFromNavModel = (navModel: NavModel) => {
-  return `${navModel.main.text}${navModel.node.text ? ': ' + navModel.node.text : '' }`;
+  return `${navModel.main.text}${navModel.node.text ? ': ' + navModel.node.text : ''}`;
 };

+ 0 - 3
public/app/core/services/__mocks__/backend_srv.ts

@@ -1,4 +1,3 @@
-
 const backendSrv = {
   get: jest.fn(),
   getDashboard: jest.fn(),
@@ -10,5 +9,3 @@ const backendSrv = {
 export function getBackendSrv() {
   return backendSrv;
 }
-
-

+ 5 - 8
public/app/core/specs/PasswordStrength.test.tsx

@@ -1,24 +1,21 @@
 import React from 'react';
-import {shallow} from 'enzyme';
+import { shallow } from 'enzyme';
 
-import {PasswordStrength} from '../components/PasswordStrength';
+import { PasswordStrength } from '../components/PasswordStrength';
 
 describe('PasswordStrength', () => {
-
   it('should have class bad if length below 4', () => {
     const wrapper = shallow(<PasswordStrength password="asd" />);
-    expect(wrapper.find(".password-strength-bad")).toHaveLength(1);
+    expect(wrapper.find('.password-strength-bad')).toHaveLength(1);
   });
 
   it('should have class ok if length below 8', () => {
     const wrapper = shallow(<PasswordStrength password="asdasd" />);
-    expect(wrapper.find(".password-strength-ok")).toHaveLength(1);
+    expect(wrapper.find('.password-strength-ok')).toHaveLength(1);
   });
 
   it('should have class good if length above 8', () => {
     const wrapper = shallow(<PasswordStrength password="asdaasdda" />);
-    expect(wrapper.find(".password-strength-good")).toHaveLength(1);
+    expect(wrapper.find('.password-strength-good')).toHaveLength(1);
   });
-
 });
-

+ 1 - 1
public/app/core/utils/explore.test.ts

@@ -19,7 +19,7 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
     showingTable: true,
     showingLogs: true,
     dedupStrategy: LogsDedupStrategy.none,
-  }
+  },
 };
 
 describe('state functions', () => {

+ 8 - 1
public/app/core/utils/explore.ts

@@ -207,7 +207,14 @@ export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: bo
       urlState.range.to,
       urlState.datasource,
       ...urlState.queries,
-      { ui: [!!urlState.ui.showingGraph, !!urlState.ui.showingLogs, !!urlState.ui.showingTable, urlState.ui.dedupStrategy] },
+      {
+        ui: [
+          !!urlState.ui.showingGraph,
+          !!urlState.ui.showingLogs,
+          !!urlState.ui.showingTable,
+          urlState.ui.dedupStrategy,
+        ],
+      },
     ]);
   }
   return JSON.stringify(urlState);

+ 4 - 4
public/app/core/utils/kbn.ts

@@ -289,21 +289,21 @@ kbn.getUnitFormats = () => {
 //
 // Backward compatible layer for value formats to support old plugins
 //
-if (typeof Proxy !== "undefined") {
+if (typeof Proxy !== 'undefined') {
   kbn.valueFormats = new Proxy(kbn.valueFormats, {
     get(target, name, receiver) {
       if (typeof name !== 'string') {
-        throw {message: `Value format ${String(name)} is not a string` };
+        throw { message: `Value format ${String(name)} is not a string` };
       }
 
       const formatter = getValueFormat(name);
-      if  (formatter) {
+      if (formatter) {
         return formatter;
       }
 
       // default to look here
       return Reflect.get(target, name, receiver);
-    }
+    },
   });
 } else {
   kbn.valueFormats = getValueFormatterIndex();

+ 5 - 0
public/app/core/utils/reselect.ts

@@ -0,0 +1,5 @@
+import { memoize } from 'lodash';
+import { createSelectorCreator } from 'reselect';
+
+const hashFn = (...args) => args.reduce((acc, val) => acc + '-' + JSON.stringify(val), '');
+export const createLodashMemoizedSelector = createSelectorCreator(memoize, hashFn);

+ 2 - 2
public/app/core/utils/scrollbar.ts

@@ -15,7 +15,7 @@ export default function getScrollbarWidth() {
       position: 'absolute',
       top: '-9999px',
       overflow: 'scroll',
-      MsOverflowStyle: 'scrollbar'
+      MsOverflowStyle: 'scrollbar',
     };
 
     Object.keys(newStyles).map(style => {
@@ -23,7 +23,7 @@ export default function getScrollbarWidth() {
     });
 
     document.body.appendChild(div);
-    scrollbarWidth = (div.offsetWidth - div.clientWidth);
+    scrollbarWidth = div.offsetWidth - div.clientWidth;
     document.body.removeChild(div);
   } else {
     scrollbarWidth = 0;

+ 2 - 2
public/app/core/utils/text.ts

@@ -50,7 +50,7 @@ const XSSWL = Object.keys(xss.whiteList).reduce((acc, element) => {
 }, {});
 
 const sanitizeXSS = new xss.FilterXSS({
-  whiteList: XSSWL
+  whiteList: XSSWL,
 });
 
 /**
@@ -60,7 +60,7 @@ const sanitizeXSS = new xss.FilterXSS({
  * Info: https://github.com/leizongmin/js-xss#customize-css-filter
  * Whitelist: https://github.com/leizongmin/js-css-filter/blob/master/lib/default.js
  */
-export function sanitize (unsanitizedString: string): string {
+export function sanitize(unsanitizedString: string): string {
   try {
     return sanitizeXSS.process(unsanitizedString);
   } catch (error) {

+ 7 - 7
public/app/core/utils/url.ts

@@ -12,13 +12,13 @@ export function renderUrl(path: string, query: UrlQueryMap | undefined): string
 }
 
 export function encodeURIComponentAsAngularJS(val, pctEncodeSpaces) {
-  return encodeURIComponent(val).
-             replace(/%40/gi, '@').
-             replace(/%3A/gi, ':').
-             replace(/%24/g, '$').
-             replace(/%2C/gi, ',').
-             replace(/%3B/gi, ';').
-             replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
+  return encodeURIComponent(val)
+    .replace(/%40/gi, '@')
+    .replace(/%3A/gi, ':')
+    .replace(/%24/g, '$')
+    .replace(/%2C/gi, ',')
+    .replace(/%3B/gi, ';')
+    .replace(/%20/g, pctEncodeSpaces ? '%20' : '+');
 }
 
 export function toUrlParams(a) {

+ 0 - 2
public/app/features/admin/AdminEditOrgCtrl.ts

@@ -1,4 +1,3 @@
-
 export default class AdminEditOrgCtrl {
   /** @ngInject */
   constructor($scope, $routeParams, backendSrv, $location, navModelSrv) {
@@ -46,4 +45,3 @@ export default class AdminEditOrgCtrl {
     $scope.init();
   }
 }
-

+ 0 - 2
public/app/features/admin/AdminListOrgsCtrl.ts

@@ -1,4 +1,3 @@
-
 export default class AdminListOrgsCtrl {
   /** @ngInject */
   constructor($scope, backendSrv, navModelSrv) {
@@ -31,4 +30,3 @@ export default class AdminListOrgsCtrl {
     $scope.init();
   }
 }
-

+ 1 - 1
public/app/features/admin/ServerStats.tsx

@@ -22,7 +22,7 @@ export class ServerStats extends PureComponent<Props, State> {
 
     this.state = {
       stats: [],
-      isLoading: false
+      isLoading: false,
     };
   }
 

+ 0 - 1
public/app/features/admin/StyleGuideCtrl.ts

@@ -25,4 +25,3 @@ export default class StyleGuideCtrl {
     });
   }
 }
-

+ 1 - 2
public/app/features/admin/partials/orgs.html

@@ -4,8 +4,7 @@
   <div class="page-action-bar">
     <div class="page-action-bar__spacer"></div>
     <a class="page-header__cta btn btn-primary" href="org/new">
-      <i class="fa fa-plus"></i>
-      New Org
+      New org
     </a>
   </div>
 

+ 1 - 2
public/app/features/admin/partials/users.html

@@ -8,8 +8,7 @@
     </label>
     <div class="page-action-bar__spacer"></div>
     <a class="btn btn-primary" href="admin/users/create">
-      <i class="fa fa-plus"></i>
-      Add new user
+      New user
     </a>
   </div>
 

+ 2 - 3
public/app/features/alerting/AlertRuleList.test.tsx

@@ -18,7 +18,7 @@ const setup = (propOverrides?: object) => {
     togglePauseAlertRule: jest.fn(),
     stateFilter: '',
     search: '',
-    isLoading: false
+    isLoading: false,
   };
 
   Object.assign(props, propOverrides);
@@ -147,9 +147,8 @@ describe('Functions', () => {
   describe('Search query change', () => {
     it('should set search query', () => {
       const { instance } = setup();
-      const mockEvent = { target: { value: 'dashboard' } } as React.ChangeEvent<HTMLInputElement>;
 
-      instance.onSearchQueryChange(mockEvent);
+      instance.onSearchQueryChange('dashboard');
 
       expect(instance.props.setSearchQuery).toHaveBeenCalledWith('dashboard');
     });

+ 11 - 14
public/app/features/alerting/AlertRuleList.tsx

@@ -9,6 +9,7 @@ import { getNavModel } from 'app/core/selectors/navModel';
 import { NavModel, StoreState, AlertRule } from 'app/types';
 import { getAlertRulesAsync, setSearchQuery, togglePauseAlertRule } from './state/actions';
 import { getAlertRuleItems, getSearchQuery } from './state/selectors';
+import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
 
 export interface Props {
   navModel: NavModel;
@@ -69,8 +70,7 @@ export class AlertRuleList extends PureComponent<Props, any> {
     });
   };
 
-  onSearchQueryChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
-    const { value } = evt.target;
+  onSearchQueryChange = (value: string) => {
     this.props.setSearchQuery(value);
   };
 
@@ -78,7 +78,7 @@ export class AlertRuleList extends PureComponent<Props, any> {
     this.props.togglePauseAlertRule(rule.id, { paused: rule.state !== 'paused' });
   };
 
-  alertStateFilterOption = ({ text, value }: { text: string; value: string; }) => {
+  alertStateFilterOption = ({ text, value }: { text: string; value: string }) => {
     return (
       <option key={value} value={value}>
         {text}
@@ -94,16 +94,13 @@ export class AlertRuleList extends PureComponent<Props, any> {
         <Page.Contents isLoading={isLoading}>
           <div className="page-action-bar">
             <div className="gf-form gf-form--grow">
-              <label className="gf-form--has-input-icon gf-form--grow">
-                <input
-                  type="text"
-                  className="gf-form-input"
-                  placeholder="Search alerts"
-                  value={search}
-                  onChange={this.onSearchQueryChange}
-                />
-                <i className="gf-form-input-icon fa fa-search" />
-              </label>
+              <FilterInput
+                labelClassName="gf-form--has-input-icon gf-form--grow"
+                inputClassName="gf-form-input"
+                placeholder="Search alerts"
+                value={search}
+                onChange={this.onSearchQueryChange}
+              />
             </div>
             <div className="gf-form">
               <label className="gf-form-label">States</label>
@@ -142,7 +139,7 @@ const mapStateToProps = (state: StoreState) => ({
   alertRules: getAlertRuleItems(state.alertRules),
   stateFilter: state.location.query.state,
   search: getSearchQuery(state.alertRules),
-  isLoading: state.alertRules.isLoading
+  isLoading: state.alertRules.isLoading,
 });
 
 const mapDispatchToProps = {

+ 3 - 3
public/app/features/alerting/AlertTabCtrl.ts

@@ -140,7 +140,7 @@ export class AlertTabCtrl {
       name: model.name,
       iconClass: this.getNotificationIcon(model.type),
       isDefault: false,
-      uid: model.uid
+      uid: model.uid,
     });
 
     // avoid duplicates using both id and uid to be backwards compatible.
@@ -157,8 +157,8 @@ export class AlertTabCtrl {
   removeNotification(an) {
     // remove notifiers refeered to by id and uid to support notifiers added
     // before and after we added support for uid
-    _.remove(this.alert.notifications, n =>  n.uid === an.uid || n.id === an.id);
-    _.remove(this.alertNotifications, n =>  n.uid === an.uid || n.id === an.id);
+    _.remove(this.alert.notifications, n => n.uid === an.uid || n.id === an.id);
+    _.remove(this.alertNotifications, n => n.uid === an.uid || n.id === an.id);
   }
 
   initModel() {

+ 14 - 28
public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap

@@ -13,20 +13,13 @@ exports[`Render should render alert rules 1`] = `
       <div
         className="gf-form gf-form--grow"
       >
-        <label
-          className="gf-form--has-input-icon gf-form--grow"
-        >
-          <input
-            className="gf-form-input"
-            onChange={[Function]}
-            placeholder="Search alerts"
-            type="text"
-            value=""
-          />
-          <i
-            className="gf-form-input-icon fa fa-search"
-          />
-        </label>
+        <ForwardRef
+          inputClassName="gf-form-input"
+          labelClassName="gf-form--has-input-icon gf-form--grow"
+          onChange={[Function]}
+          placeholder="Search alerts"
+          value=""
+        />
       </div>
       <div
         className="gf-form"
@@ -167,20 +160,13 @@ exports[`Render should render component 1`] = `
       <div
         className="gf-form gf-form--grow"
       >
-        <label
-          className="gf-form--has-input-icon gf-form--grow"
-        >
-          <input
-            className="gf-form-input"
-            onChange={[Function]}
-            placeholder="Search alerts"
-            type="text"
-            value=""
-          />
-          <i
-            className="gf-form-input-icon fa fa-search"
-          />
-        </label>
+        <ForwardRef
+          inputClassName="gf-form-input"
+          labelClassName="gf-form--has-input-icon gf-form--grow"
+          onChange={[Function]}
+          placeholder="Search alerts"
+          value=""
+        />
       </div>
       <div
         className="gf-form"

+ 1 - 1
public/app/features/alerting/partials/notifications_list.html

@@ -8,7 +8,7 @@
       </div>
 
       <a href="alerting/notification/new" class="btn btn-primary">
-        New Channel
+        New channel
       </a>
     </div>
 

+ 4 - 5
public/app/features/api-keys/ApiKeysPage.test.tsx

@@ -8,11 +8,11 @@ const setup = (propOverrides?: object) => {
   const props: Props = {
     navModel: {
       main: {
-        text: 'Configuration'
+        text: 'Configuration',
       },
       node: {
-        text: 'Api Keys'
-      }
+        text: 'Api Keys',
+      },
     } as NavModel,
     apiKeys: [] as ApiKey[],
     searchQuery: '',
@@ -78,9 +78,8 @@ describe('Functions', () => {
   describe('on search query change', () => {
     it('should call setSearchQuery', () => {
       const { instance } = setup();
-      const mockEvent = { target: { value: 'test' } };
 
-      instance.onSearchQueryChange(mockEvent);
+      instance.onSearchQueryChange('test');
 
       expect(instance.props.setSearchQuery).toHaveBeenCalledWith('test');
     });

+ 12 - 20
public/app/features/api-keys/ApiKeysPage.tsx

@@ -13,6 +13,7 @@ import config from 'app/core/config';
 import appEvents from 'app/core/app_events';
 import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
 import { DeleteButton } from '@grafana/ui';
+import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
 
 export interface Props {
   navModel: NavModel;
@@ -59,8 +60,8 @@ export class ApiKeysPage extends PureComponent<Props, any> {
     this.props.deleteApiKey(key.id);
   }
 
-  onSearchQueryChange = evt => {
-    this.props.setSearchQuery(evt.target.value);
+  onSearchQueryChange = (value: string) => {
+    this.props.setSearchQuery(value);
   };
 
   onToggleAdding = () => {
@@ -186,21 +187,18 @@ export class ApiKeysPage extends PureComponent<Props, any> {
       <>
         <div className="page-action-bar">
           <div className="gf-form gf-form--grow">
-            <label className="gf-form--has-input-icon gf-form--grow">
-              <input
-                type="text"
-                className="gf-form-input"
-                placeholder="Search keys"
-                value={searchQuery}
-                onChange={this.onSearchQueryChange}
-              />
-              <i className="gf-form-input-icon fa fa-search" />
-            </label>
+            <FilterInput
+              labelClassName="gf-form--has-input-icon gf-form--grow"
+              inputClassName="gf-form-input"
+              placeholder="Search keys"
+              value={searchQuery}
+              onChange={this.onSearchQueryChange}
+            />
           </div>
 
           <div className="page-action-bar__spacer" />
           <button className="btn btn-primary pull-right" onClick={this.onToggleAdding} disabled={isAdding}>
-            Add API Key
+            Add API key
           </button>
         </div>
 
@@ -241,13 +239,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
     return (
       <Page navModel={navModel}>
         <Page.Contents isLoading={!hasFetched}>
-          {hasFetched && (
-            apiKeysCount > 0 ? (
-              this.renderApiKeyList()
-            ) : (
-              this.renderEmptyList()
-            )
-          )}
+          {hasFetched && (apiKeysCount > 0 ? this.renderApiKeyList() : this.renderEmptyList())}
         </Page.Contents>
       </Page>
     );

+ 1 - 7
public/app/features/dashboard/components/AdHocFilters/AdHocFiltersCtrl.ts

@@ -10,13 +10,7 @@ export class AdHocFiltersCtrl {
   removeTagFilterSegment: any;
 
   /** @ngInject */
-  constructor(
-    private uiSegmentSrv,
-    private datasourceSrv,
-    private $q,
-    private variableSrv,
-    $scope,
-  ) {
+  constructor(private uiSegmentSrv, private datasourceSrv, private $q, private variableSrv, $scope) {
     this.removeTagFilterSegment = uiSegmentSrv.newSegment({
       fake: true,
       value: '-- remove filter --',

+ 4 - 2
public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx

@@ -87,7 +87,7 @@ export class AddPanelWidget extends React.Component<Props, State> {
 
     if (tab === 'visualization') {
       location.query.tab = 'visualization';
-      location.query.openVizPicker  = true;
+      location.query.openVizPicker = true;
     }
 
     reduxStore.dispatch(updateLocation(location));
@@ -161,7 +161,9 @@ export class AddPanelWidget extends React.Component<Props, State> {
               )}
             </div>
             <div className="add-panel-widget__actions">
-              <button className="btn btn-inverse add-panel-widget__action" onClick={this.onCreateNewRow}>Convert to row</button>
+              <button className="btn btn-inverse add-panel-widget__action" onClick={this.onCreateNewRow}>
+                Convert to row
+              </button>
               {copiedPanelPlugins.length === 1 && (
                 <button
                   className="btn btn-inverse add-panel-widget__action"

+ 3 - 1
public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx

@@ -76,7 +76,9 @@ export class DashboardPermissions extends PureComponent<Props, State> {
               </div>
             </Tooltip>
             <div className="page-action-bar__spacer" />
-            <button className="btn btn-primary pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>Add Permission</button>
+            <button className="btn btn-primary pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
+              Add Permission
+            </button>
           </div>
         </div>
         <SlideDown in={isAdding}>

+ 5 - 5
public/app/features/dashboard/components/DashboardRow/DashboardRow.tsx

@@ -27,7 +27,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
 
   onVariableUpdated = () => {
     this.forceUpdate();
-  }
+  };
 
   onToggle = () => {
     this.props.dashboard.toggleRow(this.props.panel);
@@ -35,12 +35,12 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
     this.setState(prevState => {
       return { collapsed: !prevState.collapsed };
     });
-  }
+  };
 
   onUpdate = () => {
     this.props.dashboard.processRepeats();
     this.forceUpdate();
-  }
+  };
 
   onOpenSettings = () => {
     appEvents.emit('show-modal', {
@@ -51,7 +51,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
         onUpdated: this.onUpdate,
       },
     });
-  }
+  };
 
   onDelete = () => {
     appEvents.emit('confirm-modal', {
@@ -66,7 +66,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
         this.props.dashboard.removeRow(this.props.panel, false);
       },
     });
-  }
+  };
 
   render() {
     const classes = classNames({

+ 1 - 1
public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx

@@ -31,6 +31,6 @@ export class DashboardSettings extends PureComponent<Props> {
   }
 
   render() {
-    return <div className="panel-height-helper" ref={element => this.element = element} />;
+    return <div className="panel-height-helper" ref={element => (this.element = element)} />;
   }
 }

+ 1 - 1
public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts

@@ -38,7 +38,7 @@ export class SettingsCtrl {
       });
     });
 
-    this.canSaveAs = this.dashboard.meta.canEdit && contextSrv.hasEditPermissionInFolders;
+    this.canSaveAs = contextSrv.hasEditPermissionInFolders;
     this.canSave = this.dashboard.meta.canSave;
     this.canDelete = this.dashboard.meta.canSave;
 

+ 2 - 4
public/app/features/dashboard/components/DashboardSettings/template.html

@@ -11,14 +11,12 @@
 
 	<div class="dashboard-settings__aside-actions">
     <button class="btn btn-primary" ng-click="ctrl.saveDashboard()" ng-show="ctrl.canSave">
-			<i class="fa fa-save"></i> Save
+			Save
 		</button>
 		<button class="btn btn-inverse" ng-click="ctrl.openSaveAsModal()" ng-show="ctrl.canSaveAs">
-			<i class="fa fa-copy"></i>
 			Save As...
 		</button>
 		<button class="btn btn-danger" ng-click="ctrl.deleteDashboard()" ng-show="ctrl.canDelete">
-			<i class="fa fa-trash"></i>
 			Delete
 		</button>
 	</div>
@@ -101,7 +99,7 @@
 
   <div class="gf-form-button-row">
     <button class="btn btn-primary" ng-click="ctrl.saveDashboardJson()" ng-show="ctrl.canSave">
-      <i class="fa fa-save"></i> Save Changes
+      Save Changes
     </button>
   </div>
 </div>

+ 1 - 1
public/app/features/dashboard/components/SubMenu/SubMenu.tsx

@@ -31,6 +31,6 @@ export class SubMenu extends PureComponent<Props> {
   }
 
   render() {
-    return <div ref={element => this.element = element} />;
+    return <div ref={element => (this.element = element)} />;
   }
 }

+ 30 - 28
public/app/features/dashboard/containers/DashboardPage.test.tsx

@@ -3,7 +3,7 @@ import { shallow, ShallowWrapper } from 'enzyme';
 import { DashboardPage, Props, State } from './DashboardPage';
 import { DashboardModel } from '../state';
 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';
 
 jest.mock('sass/_variables.scss', () => ({
@@ -23,17 +23,20 @@ interface ScenarioContext {
 }
 
 function getTestDashboard(overrides?: any, metaOverrides?: any): DashboardModel {
-  const data = Object.assign({
-    title: 'My dashboard',
-    panels: [
-      {
-        id: 1,
-        type: 'graph',
-        title: 'My graph',
-        gridPos: { x: 0, y: 0, w: 1, h: 1 },
-      },
-    ],
-  }, overrides);
+  const data = Object.assign(
+    {
+      title: 'My dashboard',
+      panels: [
+        {
+          id: 1,
+          type: 'graph',
+          title: 'My graph',
+          gridPos: { x: 0, y: 0, w: 1, h: 1 },
+        },
+      ],
+    },
+    overrides
+  );
 
   const meta = Object.assign({ canSave: true, canEdit: true }, metaOverrides);
   return new DashboardModel(data, meta);
@@ -74,7 +77,7 @@ function dashboardPageScenario(description, scenarioFn: (ctx: ScenarioContext) =
 
         ctx.dashboard = props.dashboard;
         ctx.wrapper = shallow(<DashboardPage {...props} />);
-      }
+      },
     };
 
     beforeEach(() => {
@@ -86,8 +89,7 @@ function dashboardPageScenario(description, scenarioFn: (ctx: ScenarioContext) =
 }
 
 describe('DashboardPage', () => {
-
-  dashboardPageScenario("Given initial state", (ctx) => {
+  dashboardPageScenario('Given initial state', ctx => {
     ctx.setup(() => {
       ctx.mount();
     });
@@ -97,7 +99,7 @@ describe('DashboardPage', () => {
     });
   });
 
-  dashboardPageScenario("Dashboard is fetching slowly", (ctx) => {
+  dashboardPageScenario('Dashboard is fetching slowly', ctx => {
     ctx.setup(() => {
       ctx.mount();
       ctx.wrapper.setProps({
@@ -111,7 +113,7 @@ describe('DashboardPage', () => {
     });
   });
 
-  dashboardPageScenario("Dashboard init completed ", (ctx) => {
+  dashboardPageScenario('Dashboard init completed ', ctx => {
     ctx.setup(() => {
       ctx.mount();
       ctx.setDashboardProp();
@@ -126,7 +128,7 @@ describe('DashboardPage', () => {
     });
   });
 
-  dashboardPageScenario("When user goes into panel edit", (ctx) => {
+  dashboardPageScenario('When user goes into panel edit', ctx => {
     ctx.setup(() => {
       ctx.mount();
       ctx.setDashboardProp();
@@ -149,7 +151,7 @@ describe('DashboardPage', () => {
     });
   });
 
-  dashboardPageScenario("When user goes back to dashboard from panel edit", (ctx) => {
+  dashboardPageScenario('When user goes back to dashboard from panel edit', ctx => {
     ctx.setup(() => {
       ctx.mount();
       ctx.setDashboardProp();
@@ -179,7 +181,7 @@ describe('DashboardPage', () => {
     });
   });
 
-  dashboardPageScenario("When dashboard has editview url state", (ctx) => {
+  dashboardPageScenario('When dashboard has editview url state', ctx => {
     ctx.setup(() => {
       ctx.mount();
       ctx.setDashboardProp();
@@ -197,7 +199,7 @@ describe('DashboardPage', () => {
     });
   });
 
-  dashboardPageScenario("When adding panel", (ctx) => {
+  dashboardPageScenario('When adding panel', ctx => {
     ctx.setup(() => {
       ctx.mount();
       ctx.setDashboardProp();
@@ -214,37 +216,37 @@ describe('DashboardPage', () => {
     });
   });
 
-  dashboardPageScenario("Given panel with id 0", (ctx) => {
+  dashboardPageScenario('Given panel with id 0', ctx => {
     ctx.setup(() => {
       ctx.mount();
       ctx.setDashboardProp({
-        panels: [{ id: 0, type: 'graph'}],
+        panels: [{ id: 0, type: 'graph' }],
         schemaVersion: 17,
       });
       ctx.wrapper.setProps({
         urlEdit: true,
         urlFullscreen: true,
-        urlPanelId: '0'
+        urlPanelId: '0',
       });
     });
 
-    it('Should go into edit mode' , () => {
+    it('Should go into edit mode', () => {
       expect(ctx.wrapper.state().isEditing).toBe(true);
       expect(ctx.wrapper.state().fullscreenPanel.id).toBe(0);
     });
   });
 
-  dashboardPageScenario("When dashboard unmounts", (ctx) => {
+  dashboardPageScenario('When dashboard unmounts', ctx => {
     ctx.setup(() => {
       ctx.mount();
       ctx.setDashboardProp({
-        panels: [{ id: 0, type: 'graph'}],
+        panels: [{ id: 0, type: 'graph' }],
         schemaVersion: 17,
       });
       ctx.wrapper.unmount();
     });
 
-    it('Should call clean up action' , () => {
+    it('Should call clean up action', () => {
       expect(ctx.cleanUpDashboardMock.calls).toBe(1);
     });
   });

+ 10 - 10
public/app/features/dashboard/dashgrid/DashboardGrid.tsx

@@ -14,7 +14,7 @@ let lastGridWidth = 1200;
 let ignoreNextWidthChange = false;
 
 interface GridWrapperProps {
-  size: { width: number; };
+  size: { width: number };
   layout: ReactGridLayout.Layout[];
   onLayoutChange: (layout: ReactGridLayout.Layout[]) => void;
   children: JSX.Element | JSX.Element[];
@@ -41,7 +41,7 @@ function GridWrapper({
   isResizable,
   isDraggable,
   isFullscreen,
-}: GridWrapperProps)  {
+}: GridWrapperProps) {
   const width = size.width > 0 ? size.width : lastGridWidth;
 
   // logic to ignore width changes (optimization)
@@ -149,21 +149,21 @@ export class DashboardGrid extends PureComponent<Props> {
     }
 
     this.props.dashboard.sortPanelsByGridPos();
-  }
+  };
 
   triggerForceUpdate = () => {
     this.forceUpdate();
-  }
+  };
 
   onWidthChange = () => {
     for (const panel of this.props.dashboard.panels) {
       panel.resizeDone();
     }
-  }
+  };
 
   onViewModeChanged = () => {
     ignoreNextWidthChange = true;
-  }
+  };
 
   updateGridPos = (item: ReactGridLayout.Layout, layout: ReactGridLayout.Layout[]) => {
     this.panelMap[item.i].updateGridPos(item);
@@ -171,21 +171,21 @@ export class DashboardGrid extends PureComponent<Props> {
     // react-grid-layout has a bug (#670), and onLayoutChange() is only called when the component is mounted.
     // So it's required to call it explicitly when panel resized or moved to save layout changes.
     this.onLayoutChange(layout);
-  }
+  };
 
   onResize: ItemCallback = (layout, oldItem, newItem) => {
     console.log();
     this.panelMap[newItem.i].updateGridPos(newItem);
-  }
+  };
 
   onResizeStop: ItemCallback = (layout, oldItem, newItem) => {
     this.updateGridPos(newItem, layout);
     this.panelMap[newItem.i].resizeDone();
-  }
+  };
 
   onDragStop: ItemCallback = (layout, oldItem, newItem) => {
     this.updateGridPos(newItem, layout);
-  }
+  };
 
   renderPanels() {
     const panelElements = [];

+ 48 - 24
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -10,14 +10,14 @@ import { PanelHeader } from './PanelHeader/PanelHeader';
 import { DataPanel } from './DataPanel';
 
 // Utils
-import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
+import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel';
 import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
 import { profiler } from 'app/core/profiler';
 
 // Types
 import { DashboardModel, PanelModel } from '../state';
 import { PanelPlugin } from 'app/types';
-import { TimeRange, LoadingState } from '@grafana/ui';
+import { TimeRange, LoadingState, PanelData } from '@grafana/ui';
 
 import variables from 'sass/_variables.scss';
 import templateSrv from 'app/features/templating/template_srv';
@@ -94,7 +94,20 @@ export class PanelChrome extends PureComponent<Props, State> {
     return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
   }
 
-  renderPanel(loading, panelData, width, height): JSX.Element {
+  get hasPanelSnapshot() {
+    const { panel } = this.props;
+    return panel.snapshotData && panel.snapshotData.length;
+  }
+
+  get needsQueryExecution() {
+    return this.hasPanelSnapshot || this.props.plugin.dataFormats.length > 0;
+  }
+
+  get getDataForPanel() {
+    return this.hasPanelSnapshot ? snapshotDataToPanelData(this.props.panel) : null;
+  }
+
+  renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element {
     const { panel, plugin } = this.props;
     const { timeRange, renderCounter } = this.state;
     const PanelComponent = plugin.exports.Panel;
@@ -121,11 +134,39 @@ export class PanelChrome extends PureComponent<Props, State> {
     );
   }
 
+  renderPanelBody = (width: number, height: number): JSX.Element => {
+    const { panel } = this.props;
+    const { refreshCounter, timeRange } = this.state;
+    const { datasource, targets } = panel;
+    return (
+      <>
+        {this.needsQueryExecution ? (
+          <DataPanel
+            panelId={panel.id}
+            datasource={datasource}
+            queries={targets}
+            timeRange={timeRange}
+            isVisible={this.isVisible}
+            widthPixels={width}
+            refreshCounter={refreshCounter}
+            onDataResponse={this.onDataResponse}
+          >
+            {({ loading, panelData }) => {
+              return this.renderPanelPlugin(loading, panelData, width, height);
+            }}
+          </DataPanel>
+        ) : (
+          this.renderPanelPlugin(LoadingState.Done, this.getDataForPanel, width, height)
+        )}
+      </>
+    );
+  };
+
   render() {
-    const { panel, dashboard } = this.props;
-    const { refreshCounter, timeRange, timeInfo } = this.state;
+    const { dashboard, panel } = this.props;
+    const { timeInfo } = this.state;
+    const { transparent } = panel;
 
-    const { datasource, targets, transparent } = panel;
     const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`;
     return (
       <AutoSizer>
@@ -145,24 +186,7 @@ export class PanelChrome extends PureComponent<Props, State> {
                 scopedVars={panel.scopedVars}
                 links={panel.links}
               />
-              {panel.snapshotData ? (
-                this.renderPanel(false, panel.snapshotData, width, height)
-              ) : (
-                <DataPanel
-                  panelId={panel.id}
-                  datasource={datasource}
-                  queries={targets}
-                  timeRange={timeRange}
-                  isVisible={this.isVisible}
-                  widthPixels={width}
-                  refreshCounter={refreshCounter}
-                  onDataResponse={this.onDataResponse}
-                >
-                  {({ loading, panelData }) => {
-                    return this.renderPanel(loading, panelData, width, height);
-                  }}
-                </DataPanel>
-              )}
+              {this.renderPanelBody(width, height)}
             </div>
           );
         }}

+ 5 - 5
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx

@@ -30,18 +30,18 @@ interface State {
 }
 
 export class PanelHeader extends Component<Props, State> {
-  clickCoordinates: ClickCoordinates = {x: 0, y: 0};
+  clickCoordinates: ClickCoordinates = { x: 0, y: 0 };
   state = {
     panelMenuOpen: false,
-    clickCoordinates: {x: 0, y: 0}
+    clickCoordinates: { x: 0, y: 0 },
   };
 
   eventToClickCoordinates = (event: React.MouseEvent<HTMLDivElement>) => {
     return {
       x: event.clientX,
-      y: event.clientY
+      y: event.clientY,
     };
-  }
+  };
 
   onMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
     this.clickCoordinates = this.eventToClickCoordinates(event);
@@ -49,7 +49,7 @@ export class PanelHeader extends Component<Props, State> {
 
   isClick = (clickCoordinates: ClickCoordinates) => {
     return isEqual(clickCoordinates, this.clickCoordinates);
-  }
+  };
 
   onMenuToggle = (event: React.MouseEvent<HTMLDivElement>) => {
     if (this.isClick(this.eventToClickCoordinates(event))) {

+ 2 - 7
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx

@@ -76,13 +76,8 @@ export class PanelHeaderCorner extends Component<Props> {
     return (
       <>
         {infoMode === InfoModes.Info || infoMode === InfoModes.Links ? (
-          <Tooltip
-            content={this.getInfoContent()}
-            placement="bottom-start"
-          >
-            <div
-              className={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}
-            >
+          <Tooltip content={this.getInfoContent()} placement="bottom-start">
+            <div className={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}>
               <i className="fa" />
               <span className="panel-info-corner-inner" />
             </div>

+ 1 - 0
public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx

@@ -46,6 +46,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin {
     sort: 100,
     module: '',
     baseUrl: '',
+    dataFormats: [],
     info: {
       author: {
         name: '',

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

@@ -66,7 +66,7 @@ export class PanelResizer extends PureComponent<Props, State> {
 
     return (
       <>
-        {render(isEditing ? {height: editorHeight} : this.noStyles)}
+        {render(isEditing ? { height: editorHeight } : this.noStyles)}
         {isEditing && (
           <div className="panel-editor-container__resizer">
             <Draggable axis="y" grid={[100, 1]} onDrag={this.onDrag} position={{ x: 0, y: 0 }}>

+ 0 - 1
public/app/features/dashboard/index.ts

@@ -27,4 +27,3 @@ import DashboardPermissions from './components/DashboardPermissions/DashboardPer
 import { react2AngularDirective } from 'app/core/utils/react2angular';
 
 react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']);
-

+ 37 - 16
public/app/features/dashboard/panel_editor/PanelEditor.tsx

@@ -30,6 +30,32 @@ interface PanelEditorTab {
   text: string;
 }
 
+enum PanelEditorTabIds {
+  Queries = 'queries',
+  Visualization = 'visualization',
+  Advanced = 'advanced',
+  Alert = 'alert',
+}
+
+interface PanelEditorTab {
+  id: string;
+  text: string;
+}
+
+const panelEditorTabTexts = {
+  [PanelEditorTabIds.Queries]: 'Queries',
+  [PanelEditorTabIds.Visualization]: 'Visualization',
+  [PanelEditorTabIds.Advanced]: 'Panel Options',
+  [PanelEditorTabIds.Alert]: 'Alert',
+};
+
+const getPanelEditorTab = (tabId: PanelEditorTabIds): PanelEditorTab => {
+  return {
+    id: tabId,
+    text: panelEditorTabTexts[tabId],
+  };
+};
+
 export class PanelEditor extends PureComponent<PanelEditorProps> {
   constructor(props) {
     super(props);
@@ -72,31 +98,26 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
 
   render() {
     const { plugin } = this.props;
-    let activeTab = store.getState().location.query.tab || 'queries';
+    let activeTab: PanelEditorTabIds = store.getState().location.query.tab || PanelEditorTabIds.Queries;
 
     const tabs: PanelEditorTab[] = [
-      { id: 'queries', text: 'Queries' },
-      { id: 'visualization', text: 'Visualization' },
-      { id: 'advanced', text: 'Panel Options' },
+      getPanelEditorTab(PanelEditorTabIds.Queries),
+      getPanelEditorTab(PanelEditorTabIds.Visualization),
+      getPanelEditorTab(PanelEditorTabIds.Advanced),
     ];
 
     // handle panels that do not have queries tab
-    if (plugin.exports.PanelCtrl) {
-      if (!plugin.exports.PanelCtrl.prototype.onDataReceived) {
-        // remove queries tab
-        tabs.shift();
-        // switch tab
-        if (activeTab === 'queries') {
-          activeTab = 'visualization';
-        }
+    if (plugin.dataFormats.length === 0) {
+      // remove queries tab
+      tabs.shift();
+      // switch tab
+      if (activeTab === PanelEditorTabIds.Queries) {
+        activeTab = PanelEditorTabIds.Visualization;
       }
     }
 
     if (config.alertingEnabled && plugin.id === 'graph') {
-      tabs.push({
-        id: 'alert',
-        text: 'Alert',
-      });
+      tabs.push(getPanelEditorTab(PanelEditorTabIds.Alert));
     }
 
     return (

+ 1 - 8
public/app/features/dashboard/panel_editor/QueryEditorRow.tsx

@@ -131,14 +131,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
 
     if (datasource.pluginExports.QueryEditor) {
       const QueryEditor = datasource.pluginExports.QueryEditor;
-      return (
-        <QueryEditor
-          query={query}
-          datasource={datasource}
-          onChange={onChange}
-          onRunQuery={this.onRunQuery}
-        />
-      );
+      return <QueryEditor query={query} datasource={datasource} onChange={onChange} onRunQuery={this.onRunQuery} />;
     }
 
     return <div>Data source plugin does not export any Query Editor component</div>;

部分文件因为文件数量过多而无法显示