Просмотр исходного кода

Merge branch 'master' into react-query-editor

Torkel Ödegaard 7 лет назад
Родитель
Сommit
a6683dd90e
87 измененных файлов с 1290 добавлено и 907 удалено
  1. 1 1
      docs/sources/features/datasources/cloudwatch.md
  2. 2 1
      packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
  3. 2 2
      packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap
  4. 24 0
      packages/grafana-ui/src/components/FormField/FormField.test.tsx
  5. 25 0
      packages/grafana-ui/src/components/FormField/FormField.tsx
  6. 12 0
      packages/grafana-ui/src/components/FormField/_FormField.scss
  7. 19 0
      packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap
  8. 42 0
      packages/grafana-ui/src/components/FormLabel/FormLabel.tsx
  9. 0 23
      packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx
  10. 1 1
      packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss
  11. 1 1
      packages/grafana-ui/src/components/Select/Select.tsx
  12. 1 0
      packages/grafana-ui/src/components/Select/_Select.scss
  13. 16 17
      packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss
  14. 48 53
      packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx
  15. 14 16
      packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx
  16. 29 23
      packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx
  17. 0 0
      packages/grafana-ui/src/components/ValueMappingsEditor/_ValueMappingsEditor.scss
  18. 6 6
      packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap
  19. 2 0
      packages/grafana-ui/src/components/index.scss
  20. 5 1
      packages/grafana-ui/src/components/index.ts
  21. 0 16
      packages/grafana-ui/src/types/gauge.ts
  22. 0 1
      packages/grafana-ui/src/types/index.ts
  23. 2 0
      packages/grafana-ui/src/types/panel.ts
  24. 2 2
      public/app/core/components/Animations/FadeIn.tsx
  25. 50 0
      public/app/core/components/Footer/Footer.tsx
  26. 0 25
      public/app/core/components/Label/Label.tsx
  27. 2 2
      public/app/core/components/LayoutSelector/LayoutSelector.tsx
  28. 75 0
      public/app/core/components/Page/Page.tsx
  29. 26 0
      public/app/core/components/Page/PageContents.tsx
  30. 3 3
      public/app/core/components/PageHeader/PageHeader.tsx
  31. 3 3
      public/app/core/components/PageLoader/PageLoader.tsx
  32. 3 4
      public/app/core/components/SharedPreferences/SharedPreferences.tsx
  33. 2 2
      public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx
  34. 2 2
      public/app/core/components/sidemenu/DropDownChild.tsx
  35. 2 2
      public/app/core/components/sidemenu/SideMenuDropDown.tsx
  36. 2 2
      public/app/core/components/sidemenu/SignIn.tsx
  37. 2 2
      public/app/core/components/sidemenu/TopSection.tsx
  38. 2 2
      public/app/core/components/sidemenu/TopSectionItem.tsx
  39. 2 0
      public/app/core/config.ts
  40. 4 0
      public/app/core/selectors/navModel.ts
  41. 8 1
      public/app/features/api-keys/ApiKeysPage.test.tsx
  42. 12 14
      public/app/features/api-keys/ApiKeysPage.tsx
  43. 130 110
      public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap
  44. 2 2
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx
  45. 2 2
      public/app/features/dashboard/panel_editor/DataSourceOption.tsx
  46. 2 2
      public/app/features/dashboard/panel_editor/QueryOptions.tsx
  47. 2 2
      public/app/features/datasources/DashboardsTable.tsx
  48. 8 1
      public/app/features/datasources/DataSourcesListPage.test.tsx
  49. 27 27
      public/app/features/datasources/DataSourcesListPage.tsx
  50. 31 19
      public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap
  51. 5 5
      public/app/features/datasources/settings/BasicSettings.tsx
  52. 2 2
      public/app/features/datasources/settings/ButtonRow.tsx
  53. 2 2
      public/app/features/explore/Error.tsx
  54. 9 3
      public/app/features/explore/state/actions.ts
  55. 8 1
      public/app/features/org/OrgDetailsPage.test.tsx
  56. 17 18
      public/app/features/org/OrgDetailsPage.tsx
  57. 2 2
      public/app/features/org/OrgProfile.tsx
  58. 47 27
      public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap
  59. 2 2
      public/app/features/plugins/PluginList.tsx
  60. 2 2
      public/app/features/plugins/PluginListItem.tsx
  61. 8 1
      public/app/features/plugins/PluginListPage.test.tsx
  62. 19 21
      public/app/features/plugins/PluginListPage.tsx
  63. 32 19
      public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap
  64. 8 1
      public/app/features/teams/TeamList.test.tsx
  65. 7 7
      public/app/features/teams/TeamList.tsx
  66. 4 4
      public/app/features/teams/TeamSettings.tsx
  67. 308 288
      public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
  68. 8 1
      public/app/features/users/UsersListPage.test.tsx
  69. 9 9
      public/app/features/users/UsersListPage.tsx
  70. 2 2
      public/app/features/users/UsersTable.tsx
  71. 32 19
      public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap
  72. 2 2
      public/app/plugins/datasource/stackdriver/components/AlignmentPeriods.tsx
  73. 2 2
      public/app/plugins/datasource/stackdriver/components/Alignments.tsx
  74. 2 2
      public/app/plugins/datasource/stackdriver/components/AnnotationsHelp.tsx
  75. 2 2
      public/app/plugins/datasource/stackdriver/components/SimpleSelect.tsx
  76. 4 10
      public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx
  77. 2 1
      public/app/plugins/panel/gauge/GaugePanel.tsx
  78. 16 5
      public/app/plugins/panel/gauge/GaugePanelOptions.tsx
  79. 14 24
      public/app/plugins/panel/gauge/ValueOptions.tsx
  80. 15 1
      public/app/plugins/panel/gauge/types.ts
  81. 1 1
      public/app/viz/Gauge.test.tsx
  82. 10 21
      public/app/viz/Gauge.tsx
  83. 1 2
      public/sass/_grafana.scss
  84. 8 0
      public/sass/components/_footer.scss
  85. 16 1
      public/sass/layout/_page.scss
  86. 3 3
      scripts/build/ci-deploy/Dockerfile
  87. 1 1
      scripts/build/ci-deploy/build-deploy.sh

+ 1 - 1
docs/sources/features/datasources/cloudwatch.md

@@ -38,7 +38,7 @@ Name | Description
 
 
 ### IAM Roles
 ### IAM Roles
 
 
-Currently all access to CloudWatch is done server side by the Grafana backend using the official AWS SDK. If you grafana
+Currently all access to CloudWatch is done server side by the Grafana backend using the official AWS SDK. If your Grafana
 server is running on AWS you can use IAM Roles and authentication will be handled automatically.
 server is running on AWS you can use IAM Roles and authentication will be handled automatically.
 
 
 Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)
 Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)

+ 2 - 1
packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx

@@ -11,6 +11,7 @@ interface Props {
   hideTracksWhenNotNeeded?: boolean;
   hideTracksWhenNotNeeded?: boolean;
   scrollTop?: number;
   scrollTop?: number;
   setScrollTop: (value: React.MouseEvent<HTMLElement>) => void;
   setScrollTop: (value: React.MouseEvent<HTMLElement>) => void;
+  autoHeightMin?: number | string;
 }
 }
 
 
 /**
 /**
@@ -26,6 +27,7 @@ export class CustomScrollbar extends PureComponent<Props> {
     hideTracksWhenNotNeeded: false,
     hideTracksWhenNotNeeded: false,
     scrollTop: 0,
     scrollTop: 0,
     setScrollTop: () => {},
     setScrollTop: () => {},
+    autoHeightMin: '0'
   };
   };
 
 
   private ref: React.RefObject<Scrollbars>;
   private ref: React.RefObject<Scrollbars>;
@@ -65,7 +67,6 @@ export class CustomScrollbar extends PureComponent<Props> {
         autoHeight={true}
         autoHeight={true}
         // These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
         // These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
         // Before these where set to inhert but that caused problems with cut of legends in firefox
         // Before these where set to inhert but that caused problems with cut of legends in firefox
-        autoHeightMin={'0'}
         autoHeightMax={autoMaxHeight}
         autoHeightMax={autoMaxHeight}
         renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
         renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
         renderTrackVertical={props => <div {...props} className="track-vertical" />}
         renderTrackVertical={props => <div {...props} className="track-vertical" />}

+ 2 - 2
packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap

@@ -7,7 +7,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
     Object {
     Object {
       "height": "auto",
       "height": "auto",
       "maxHeight": "100%",
       "maxHeight": "100%",
-      "minHeight": "0",
+      "minHeight": 0,
       "overflow": "hidden",
       "overflow": "hidden",
       "position": "relative",
       "position": "relative",
       "width": "100%",
       "width": "100%",
@@ -24,7 +24,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
         "marginBottom": 0,
         "marginBottom": 0,
         "marginRight": 0,
         "marginRight": 0,
         "maxHeight": "calc(100% + 0px)",
         "maxHeight": "calc(100% + 0px)",
-        "minHeight": "calc(0 + 0px)",
+        "minHeight": 0,
         "overflow": "scroll",
         "overflow": "scroll",
         "position": "relative",
         "position": "relative",
         "right": undefined,
         "right": undefined,

+ 24 - 0
packages/grafana-ui/src/components/FormField/FormField.test.tsx

@@ -0,0 +1,24 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { FormField, Props } from './FormField';
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    label: 'Test',
+    labelWidth: 11,
+    value: 10,
+    onChange: jest.fn(),
+  };
+
+  Object.assign(props, propOverrides);
+
+  return shallow(<FormField {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 25 - 0
packages/grafana-ui/src/components/FormField/FormField.tsx

@@ -0,0 +1,25 @@
+import React, { InputHTMLAttributes, FunctionComponent } from 'react';
+import { FormLabel } from '..';
+
+export interface Props extends InputHTMLAttributes<HTMLInputElement> {
+  label: string;
+  labelWidth?: number;
+  inputWidth?: number;
+}
+
+const defaultProps = {
+  labelWidth: 6,
+  inputWidth: 12,
+};
+
+const FormField: FunctionComponent<Props> = ({ label, labelWidth, inputWidth, ...inputProps }) => {
+  return (
+    <div className="form-field">
+      <FormLabel width={labelWidth}>{label}</FormLabel>
+      <input type="text" className={`gf-form-input width-${inputWidth}`} {...inputProps} />
+    </div>
+  );
+};
+
+FormField.defaultProps = defaultProps;
+export { FormField };

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

@@ -0,0 +1,12 @@
+.form-field {
+  margin-bottom: $gf-form-margin;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  text-align: left;
+  position: relative;
+
+  &--grow {
+    flex-grow: 1;
+  }
+}

+ 19 - 0
packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap

@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div
+  className="form-field"
+>
+  <Component
+    width={11}
+  >
+    Test
+  </Component>
+  <input
+    className="gf-form-input width-12"
+    onChange={[MockFunction]}
+    type="text"
+    value={10}
+  />
+</div>
+`;

+ 42 - 0
packages/grafana-ui/src/components/FormLabel/FormLabel.tsx

@@ -0,0 +1,42 @@
+import React, { FunctionComponent, ReactNode } from 'react';
+import classNames from 'classnames';
+import { Tooltip } from '..';
+
+interface Props {
+  children: ReactNode;
+  className?: string;
+  htmlFor?: string;
+  isFocused?: boolean;
+  isInvalid?: boolean;
+  tooltip?: string;
+  width?: number;
+}
+
+export const FormLabel: FunctionComponent<Props> = ({
+  children,
+  isFocused,
+  isInvalid,
+  className,
+  htmlFor,
+  tooltip,
+  width,
+  ...rest
+}) => {
+  const classes = classNames(`gf-form-label width-${width ? width : '10'}`, className, {
+    'gf-form-label--is-focused': isFocused,
+    'gf-form-label--is-invalid': isInvalid,
+  });
+
+  return (
+    <label className={classes} {...rest} htmlFor={htmlFor}>
+      {children}
+      {tooltip && (
+        <Tooltip placement="auto" content={tooltip}>
+          <div className="gf-form-help-icon--right-normal">
+            <i className="gicon gicon-question gicon--has-hover" />
+          </div>
+        </Tooltip>
+      )}
+    </label>
+  );
+};

+ 0 - 23
packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx

@@ -1,23 +0,0 @@
-import React, { SFC, ReactNode } from 'react';
-import classNames from 'classnames';
-
-interface Props {
-  children: ReactNode;
-  htmlFor?: string;
-  className?: string;
-  isFocused?: boolean;
-  isInvalid?: boolean;
-}
-
-export const GfFormLabel: SFC<Props> = ({ children, isFocused, isInvalid, className, htmlFor, ...rest }) => {
-  const classes = classNames('gf-form-label', className, {
-    'gf-form-label--is-focused': isFocused,
-    'gf-form-label--is-invalid': isInvalid,
-  });
-
-  return (
-    <label className={classes} {...rest} htmlFor={htmlFor}>
-      {children}
-    </label>
-  );
-};

+ 1 - 1
packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss

@@ -6,7 +6,7 @@
 }
 }
 
 
 .panel-options-group__header {
 .panel-options-group__header {
-  padding: 4px 20px;
+  padding: 4px 8px;
   font-size: 1.1rem;
   font-size: 1.1rem;
   background: $panel-options-group-header-bg;
   background: $panel-options-group-header-bg;
   position: relative;
   position: relative;

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

@@ -16,7 +16,7 @@ import SelectOptionGroup from './SelectOptionGroup';
 import IndicatorsContainer from './IndicatorsContainer';
 import IndicatorsContainer from './IndicatorsContainer';
 import NoOptionsMessage from './NoOptionsMessage';
 import NoOptionsMessage from './NoOptionsMessage';
 import resetSelectStyles from './resetSelectStyles';
 import resetSelectStyles from './resetSelectStyles';
-import { CustomScrollbar } from '@grafana/ui';
+import { CustomScrollbar } from '..';
 
 
 export interface SelectOptionItem {
 export interface SelectOptionItem {
   label?: string;
   label?: string;

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

@@ -102,6 +102,7 @@ $select-input-bg-disabled: $input-bg-disabled;
 .gf-form-select-box__value-container {
 .gf-form-select-box__value-container {
   display: table-cell;
   display: table-cell;
   padding: 6px 10px;
   padding: 6px 10px;
+  vertical-align: middle;
   > div {
   > div {
     display: inline-block;
     display: inline-block;
   }
   }

+ 16 - 17
packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss

@@ -8,6 +8,12 @@
   height: 70px;
   height: 70px;
 }
 }
 
 
+.thresholds-row:first-child > .thresholds-row-color-indicator {
+  border-top-left-radius: $border-radius;
+  border-top-right-radius: $border-radius;
+  overflow: hidden;
+}
+
 .thresholds-row:last-child > .thresholds-row-color-indicator {
 .thresholds-row:last-child > .thresholds-row-color-indicator {
   border-bottom-left-radius: $border-radius;
   border-bottom-left-radius: $border-radius;
   border-bottom-right-radius: $border-radius;
   border-bottom-right-radius: $border-radius;
@@ -33,7 +39,7 @@
 }
 }
 
 
 .thresholds-row-color-indicator {
 .thresholds-row-color-indicator {
-  width: 20px;
+  width: 10px;
 }
 }
 
 
 .thresholds-row-input {
 .thresholds-row-input {
@@ -45,18 +51,6 @@
   display: flex;
   display: flex;
   justify-content: center;
   justify-content: center;
   flex-direction: row;
   flex-direction: row;
-  height: 42px;
-}
-
-.thresholds-row-input-inner > div {
-  border-left: 1px solid $input-label-border-color;
-  border-top: 1px solid $input-label-border-color;
-  border-bottom: 1px solid $input-label-border-color;
-}
-
-.thresholds-row-input-inner > *:nth-child(2) {
-  border-top-left-radius: $border-radius;
-  border-bottom-left-radius: $border-radius;
 }
 }
 
 
 .thresholds-row-input-inner > *:last-child {
 .thresholds-row-input-inner > *:last-child {
@@ -74,9 +68,11 @@
 }
 }
 
 
 .thresholds-row-input-inner-value > input {
 .thresholds-row-input-inner-value > input {
-  height: 100%;
-  padding: 8px 10px;
+  height: $gf-form-input-height;
+  padding: $input-padding-y $input-padding-x;
   width: 150px;
   width: 150px;
+  border-top: 1px solid $input-label-border-color;
+  border-bottom: 1px solid $input-label-border-color;
 }
 }
 
 
 .thresholds-row-input-inner-color {
 .thresholds-row-input-inner-color {
@@ -85,6 +81,7 @@
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
   background-color: $input-bg;
   background-color: $input-bg;
+  border: 1px solid $input-label-border-color;
 }
 }
 
 
 .thresholds-row-input-inner-color-colorpicker {
 .thresholds-row-input-inner-color-colorpicker {
@@ -99,8 +96,10 @@
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
-  height: 42px;
+  height: $gf-form-input-height;
+  padding: $input-padding-y $input-padding-x;
   width: 42px;
   width: 42px;
-  background-color: $input-label-border-color;
+  background-color: $input-label-bg;
+  border: 1px solid $input-label-border-color;
   cursor: pointer;
   cursor: pointer;
 }
 }

+ 48 - 53
public/app/plugins/panel/gauge/MappingRow.tsx → packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx

@@ -1,22 +1,22 @@
-import React, { PureComponent } from 'react';
-import { MappingType, RangeMap, Select, ValueMap } from '@grafana/ui';
+import React, { ChangeEvent, PureComponent } from 'react';
 
 
-import { Label } from 'app/core/components/Label/Label';
+import { MappingType, ValueMapping } from '../../types';
+import { FormField, FormLabel, Select } from '..';
 
 
-interface Props {
-  mapping: ValueMap | RangeMap;
-  updateMapping: (mapping) => void;
-  removeMapping: () => void;
+export interface Props {
+  valueMapping: ValueMapping;
+  updateValueMapping: (valueMapping: ValueMapping) => void;
+  removeValueMapping: () => void;
 }
 }
 
 
 interface State {
 interface State {
-  from: string;
+  from?: string;
   id: number;
   id: number;
   operator: string;
   operator: string;
   text: string;
   text: string;
-  to: string;
+  to?: string;
   type: MappingType;
   type: MappingType;
-  value: string;
+  value?: string;
 }
 }
 
 
 const mappingOptions = [
 const mappingOptions = [
@@ -25,36 +25,34 @@ const mappingOptions = [
 ];
 ];
 
 
 export default class MappingRow extends PureComponent<Props, State> {
 export default class MappingRow extends PureComponent<Props, State> {
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
     super(props);
 
 
-    this.state = {
-      ...props.mapping,
-    };
+    this.state = { ...props.valueMapping };
   }
   }
 
 
-  onMappingValueChange = event => {
+  onMappingValueChange = (event: ChangeEvent<HTMLInputElement>) => {
     this.setState({ value: event.target.value });
     this.setState({ value: event.target.value });
   };
   };
 
 
-  onMappingFromChange = event => {
+  onMappingFromChange = (event: ChangeEvent<HTMLInputElement>) => {
     this.setState({ from: event.target.value });
     this.setState({ from: event.target.value });
   };
   };
 
 
-  onMappingToChange = event => {
+  onMappingToChange = (event: ChangeEvent<HTMLInputElement>) => {
     this.setState({ to: event.target.value });
     this.setState({ to: event.target.value });
   };
   };
 
 
-  onMappingTextChange = event => {
+  onMappingTextChange = (event: ChangeEvent<HTMLInputElement>) => {
     this.setState({ text: event.target.value });
     this.setState({ text: event.target.value });
   };
   };
 
 
-  onMappingTypeChange = mappingType => {
+  onMappingTypeChange = (mappingType: MappingType) => {
     this.setState({ type: mappingType });
     this.setState({ type: mappingType });
   };
   };
 
 
   updateMapping = () => {
   updateMapping = () => {
-    this.props.updateMapping({ ...this.state });
+    this.props.updateValueMapping({ ...this.state } as ValueMapping);
   };
   };
 
 
   renderRow() {
   renderRow() {
@@ -63,30 +61,28 @@ export default class MappingRow extends PureComponent<Props, State> {
     if (type === MappingType.RangeToText) {
     if (type === MappingType.RangeToText) {
       return (
       return (
         <>
         <>
-          <div className="gf-form">
-            <Label width={4}>From</Label>
-            <input
-              className="gf-form-input width-8"
-              value={from}
-              onBlur={this.updateMapping}
-              onChange={this.onMappingFromChange}
-            />
-          </div>
-          <div className="gf-form">
-            <Label width={4}>To</Label>
+          <FormField
+            label="From"
+            labelWidth={4}
+            inputWidth={8}
+            onBlur={this.updateMapping}
+            onChange={this.onMappingFromChange}
+            value={from}
+          />
+          <FormField
+            label="To"
+            labelWidth={4}
+            inputWidth={8}
+            onBlur={this.updateMapping}
+            onChange={this.onMappingToChange}
+            value={to}
+          />
+          <div className="gf-form gf-form--grow">
+            <FormLabel width={4}>Text</FormLabel>
             <input
             <input
-              className="gf-form-input width-8"
-              value={to}
+              className="gf-form-input"
               onBlur={this.updateMapping}
               onBlur={this.updateMapping}
-              onChange={this.onMappingToChange}
-            />
-          </div>
-          <div className="gf-form">
-            <Label width={4}>Text</Label>
-            <input
-              className="gf-form-input width-10"
               value={text}
               value={text}
-              onBlur={this.updateMapping}
               onChange={this.onMappingTextChange}
               onChange={this.onMappingTextChange}
             />
             />
           </div>
           </div>
@@ -96,17 +92,16 @@ export default class MappingRow extends PureComponent<Props, State> {
 
 
     return (
     return (
       <>
       <>
-        <div className="gf-form">
-          <Label width={4}>Value</Label>
-          <input
-            className="gf-form-input width-8"
-            onBlur={this.updateMapping}
-            onChange={this.onMappingValueChange}
-            value={value}
-          />
-        </div>
+        <FormField
+          label="Value"
+          labelWidth={4}
+          onBlur={this.updateMapping}
+          onChange={this.onMappingValueChange}
+          value={value}
+          inputWidth={8}
+        />
         <div className="gf-form gf-form--grow">
         <div className="gf-form gf-form--grow">
-          <Label width={4}>Text</Label>
+          <FormLabel width={4}>Text</FormLabel>
           <input
           <input
             className="gf-form-input"
             className="gf-form-input"
             onBlur={this.updateMapping}
             onBlur={this.updateMapping}
@@ -124,7 +119,7 @@ export default class MappingRow extends PureComponent<Props, State> {
     return (
     return (
       <div className="gf-form-inline">
       <div className="gf-form-inline">
         <div className="gf-form">
         <div className="gf-form">
-          <Label width={5}>Type</Label>
+          <FormLabel width={5}>Type</FormLabel>
           <Select
           <Select
             placeholder="Choose type"
             placeholder="Choose type"
             isSearchable={false}
             isSearchable={false}
@@ -136,7 +131,7 @@ export default class MappingRow extends PureComponent<Props, State> {
         </div>
         </div>
         {this.renderRow()}
         {this.renderRow()}
         <div className="gf-form">
         <div className="gf-form">
-          <button onClick={this.props.removeMapping} className="gf-form-label gf-form-label--btn">
+          <button onClick={this.props.removeValueMapping} className="gf-form-label gf-form-label--btn">
             <i className="fa fa-times" />
             <i className="fa fa-times" />
           </button>
           </button>
         </div>
         </div>

+ 14 - 16
public/app/plugins/panel/gauge/ValueMappings.test.tsx → packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx

@@ -1,27 +1,23 @@
 import React from 'react';
 import React from 'react';
 import { shallow } from 'enzyme';
 import { shallow } from 'enzyme';
-import { GaugeOptions, MappingType, PanelOptionsProps } from '@grafana/ui';
-import { defaultProps } from 'app/plugins/panel/gauge/GaugePanelOptions';
 
 
-import ValueMappings from './ValueMappings';
+import { ValueMappingsEditor, Props } from './ValueMappingsEditor';
+import { MappingType } from '../../types/panel';
 
 
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
-  const props: PanelOptionsProps<GaugeOptions> = {
+  const props: Props = {
     onChange: jest.fn(),
     onChange: jest.fn(),
-    options: {
-      ...defaultProps.options,
-      mappings: [
-        { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
-        { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
-      ],
-    },
+    valueMappings: [
+      { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
+      { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
+    ],
   };
   };
 
 
   Object.assign(props, propOverrides);
   Object.assign(props, propOverrides);
 
 
-  const wrapper = shallow(<ValueMappings {...props} />);
+  const wrapper = shallow(<ValueMappingsEditor {...props} />);
 
 
-  const instance = wrapper.instance() as ValueMappings;
+  const instance = wrapper.instance() as ValueMappingsEditor;
 
 
   return {
   return {
     instance,
     instance,
@@ -40,18 +36,20 @@ describe('Render', () => {
 describe('On remove mapping', () => {
 describe('On remove mapping', () => {
   it('Should remove mapping with id 0', () => {
   it('Should remove mapping with id 0', () => {
     const { instance } = setup();
     const { instance } = setup();
+
     instance.onRemoveMapping(1);
     instance.onRemoveMapping(1);
 
 
-    expect(instance.state.mappings).toEqual([
+    expect(instance.state.valueMappings).toEqual([
       { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
       { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
     ]);
     ]);
   });
   });
 
 
   it('should remove mapping with id 1', () => {
   it('should remove mapping with id 1', () => {
     const { instance } = setup();
     const { instance } = setup();
+
     instance.onRemoveMapping(2);
     instance.onRemoveMapping(2);
 
 
-    expect(instance.state.mappings).toEqual([
+    expect(instance.state.valueMappings).toEqual([
       { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
       { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
     ]);
     ]);
   });
   });
@@ -67,7 +65,7 @@ describe('Next id to add', () => {
   });
   });
 
 
   it('should default to 1', () => {
   it('should default to 1', () => {
-    const { instance } = setup({ options: { ...defaultProps.options } });
+    const { instance } = setup({ valueMappings: [] });
 
 
     expect(instance.state.nextIdToAdd).toEqual(1);
     expect(instance.state.nextIdToAdd).toEqual(1);
   });
   });

+ 29 - 23
public/app/plugins/panel/gauge/ValueMappings.tsx → packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx

@@ -1,33 +1,39 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
-import { GaugeOptions, PanelOptionsProps, MappingType, RangeMap, ValueMap, PanelOptionsGroup } from '@grafana/ui';
 
 
 import MappingRow from './MappingRow';
 import MappingRow from './MappingRow';
+import { MappingType, ValueMapping } from '../../types/panel';
+import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
+
+export interface Props {
+  valueMappings: ValueMapping[];
+  onChange: (valueMappings: ValueMapping[]) => void;
+}
 
 
 interface State {
 interface State {
-  mappings: Array<ValueMap | RangeMap>;
+  valueMappings: ValueMapping[];
   nextIdToAdd: number;
   nextIdToAdd: number;
 }
 }
 
 
-export default class ValueMappings extends PureComponent<PanelOptionsProps<GaugeOptions>, State> {
-  constructor(props) {
+export class ValueMappingsEditor extends PureComponent<Props, State> {
+  constructor(props: Props) {
     super(props);
     super(props);
 
 
-    const mappings = props.options.mappings;
+    const mappings = props.valueMappings;
 
 
     this.state = {
     this.state = {
-      mappings: mappings || [],
-      nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromMappings(mappings) : 1,
+      valueMappings: mappings,
+      nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromValueMappings(mappings) : 1,
     };
     };
   }
   }
 
 
-  getMaxIdFromMappings(mappings) {
+  getMaxIdFromValueMappings(mappings: ValueMapping[]) {
     return Math.max.apply(null, mappings.map(mapping => mapping.id).map(m => m)) + 1;
     return Math.max.apply(null, mappings.map(mapping => mapping.id).map(m => m)) + 1;
   }
   }
 
 
   addMapping = () =>
   addMapping = () =>
     this.setState(prevState => ({
     this.setState(prevState => ({
-      mappings: [
-        ...prevState.mappings,
+      valueMappings: [
+        ...prevState.valueMappings,
         {
         {
           id: prevState.nextIdToAdd,
           id: prevState.nextIdToAdd,
           operator: '',
           operator: '',
@@ -41,23 +47,23 @@ export default class ValueMappings extends PureComponent<PanelOptionsProps<Gauge
       nextIdToAdd: prevState.nextIdToAdd + 1,
       nextIdToAdd: prevState.nextIdToAdd + 1,
     }));
     }));
 
 
-  onRemoveMapping = id => {
+  onRemoveMapping = (id: number) => {
     this.setState(
     this.setState(
       prevState => ({
       prevState => ({
-        mappings: prevState.mappings.filter(m => {
+        valueMappings: prevState.valueMappings.filter(m => {
           return m.id !== id;
           return m.id !== id;
         }),
         }),
       }),
       }),
       () => {
       () => {
-        this.props.onChange({ ...this.props.options, mappings: this.state.mappings });
+        this.props.onChange(this.state.valueMappings);
       }
       }
     );
     );
   };
   };
 
 
-  updateGauge = mapping => {
+  updateGauge = (mapping: ValueMapping) => {
     this.setState(
     this.setState(
       prevState => ({
       prevState => ({
-        mappings: prevState.mappings.map(m => {
+        valueMappings: prevState.valueMappings.map(m => {
           if (m.id === mapping.id) {
           if (m.id === mapping.id) {
             return { ...mapping };
             return { ...mapping };
           }
           }
@@ -66,24 +72,24 @@ export default class ValueMappings extends PureComponent<PanelOptionsProps<Gauge
         }),
         }),
       }),
       }),
       () => {
       () => {
-        this.props.onChange({ ...this.props.options, mappings: this.state.mappings });
+        this.props.onChange(this.state.valueMappings);
       }
       }
     );
     );
   };
   };
 
 
   render() {
   render() {
-    const { mappings } = this.state;
+    const { valueMappings } = this.state;
 
 
     return (
     return (
       <PanelOptionsGroup title="Value Mappings">
       <PanelOptionsGroup title="Value Mappings">
         <div>
         <div>
-          {mappings.length > 0 &&
-            mappings.map((mapping, index) => (
+          {valueMappings.length > 0 &&
+            valueMappings.map((valueMapping, index) => (
               <MappingRow
               <MappingRow
-                key={`${mapping.text}-${index}`}
-                mapping={mapping}
-                updateMapping={this.updateGauge}
-                removeMapping={() => this.onRemoveMapping(mapping.id)}
+                key={`${valueMapping.text}-${index}`}
+                valueMapping={valueMapping}
+                updateValueMapping={this.updateGauge}
+                removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
               />
               />
             ))}
             ))}
         </div>
         </div>

+ 0 - 0
public/sass/components/_value-mappings.scss → packages/grafana-ui/src/components/ValueMappingsEditor/_ValueMappingsEditor.scss


+ 6 - 6
public/app/plugins/panel/gauge/__snapshots__/ValueMappings.test.tsx.snap → packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap

@@ -7,7 +7,9 @@ exports[`Render should render component 1`] = `
   <div>
   <div>
     <MappingRow
     <MappingRow
       key="Ok-0"
       key="Ok-0"
-      mapping={
+      removeValueMapping={[Function]}
+      updateValueMapping={[Function]}
+      valueMapping={
         Object {
         Object {
           "id": 1,
           "id": 1,
           "operator": "",
           "operator": "",
@@ -16,12 +18,12 @@ exports[`Render should render component 1`] = `
           "value": "20",
           "value": "20",
         }
         }
       }
       }
-      removeMapping={[Function]}
-      updateMapping={[Function]}
     />
     />
     <MappingRow
     <MappingRow
       key="Meh-1"
       key="Meh-1"
-      mapping={
+      removeValueMapping={[Function]}
+      updateValueMapping={[Function]}
+      valueMapping={
         Object {
         Object {
           "from": "21",
           "from": "21",
           "id": 2,
           "id": 2,
@@ -31,8 +33,6 @@ exports[`Render should render component 1`] = `
           "type": 2,
           "type": 2,
         }
         }
       }
       }
-      removeMapping={[Function]}
-      updateMapping={[Function]}
     />
     />
   </div>
   </div>
   <div
   <div

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

@@ -6,3 +6,5 @@
 @import 'PanelOptionsGroup/PanelOptionsGroup';
 @import 'PanelOptionsGroup/PanelOptionsGroup';
 @import 'PanelOptionsGrid/PanelOptionsGrid';
 @import 'PanelOptionsGrid/PanelOptionsGrid';
 @import 'ColorPicker/ColorPicker';
 @import 'ColorPicker/ColorPicker';
+@import 'ValueMappingsEditor/ValueMappingsEditor';
+@import "FormField/FormField";

+ 5 - 1
packages/grafana-ui/src/components/index.ts

@@ -9,12 +9,16 @@ export { IndicatorsContainer } from './Select/IndicatorsContainer';
 export { NoOptionsMessage } from './Select/NoOptionsMessage';
 export { NoOptionsMessage } from './Select/NoOptionsMessage';
 export { default as resetSelectStyles } from './Select/resetSelectStyles';
 export { default as resetSelectStyles } from './Select/resetSelectStyles';
 
 
+// Forms
+export { FormLabel } from './FormLabel/FormLabel';
+export { FormField } from './FormField/FormField';
+
 export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
 export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
 export { ColorPicker } from './ColorPicker/ColorPicker';
 export { ColorPicker } from './ColorPicker/ColorPicker';
 export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
 export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
 export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker';
 export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker';
 export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
 export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
-export { GfFormLabel } from './GfFormLabel/GfFormLabel';
 export { Graph } from './Graph/Graph';
 export { Graph } from './Graph/Graph';
 export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
 export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
 export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
 export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
+export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';

+ 0 - 16
packages/grafana-ui/src/types/gauge.ts

@@ -1,16 +0,0 @@
-import { RangeMap, Threshold, ValueMap } from './panel';
-
-export interface GaugeOptions {
-  baseColor: string;
-  decimals: number;
-  mappings: Array<RangeMap | ValueMap>;
-  maxValue: number;
-  minValue: number;
-  prefix: string;
-  showThresholdLabels: boolean;
-  showThresholdMarkers: boolean;
-  stat: string;
-  suffix: string;
-  thresholds: Threshold[];
-  unit: string;
-}

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

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

+ 2 - 0
packages/grafana-ui/src/types/panel.ts

@@ -56,6 +56,8 @@ interface BaseMap {
   type: MappingType;
   type: MappingType;
 }
 }
 
 
+export type ValueMapping = ValueMap | RangeMap;
+
 export interface ValueMap extends BaseMap {
 export interface ValueMap extends BaseMap {
   value: string;
   value: string;
 }
 }

+ 2 - 2
public/app/core/components/Animations/FadeIn.tsx

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import Transition from 'react-transition-group/Transition';
 import Transition from 'react-transition-group/Transition';
 
 
 interface Props {
 interface Props {
@@ -8,7 +8,7 @@ interface Props {
   unmountOnExit?: boolean;
   unmountOnExit?: boolean;
 }
 }
 
 
-export const FadeIn: SFC<Props> = props => {
+export const FadeIn: FC<Props> = props => {
   const defaultStyle = {
   const defaultStyle = {
     transition: `opacity ${props.duration}ms linear`,
     transition: `opacity ${props.duration}ms linear`,
     opacity: 0,
     opacity: 0,

+ 50 - 0
public/app/core/components/Footer/Footer.tsx

@@ -0,0 +1,50 @@
+import React, { FC } from 'react';
+import { Tooltip } from '@grafana/ui';
+
+interface Props {
+  appName: string;
+  buildVersion: string;
+  buildCommit: string;
+  newGrafanaVersionExists: boolean;
+  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 && (
+            <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;

+ 0 - 25
public/app/core/components/Label/Label.tsx

@@ -1,25 +0,0 @@
-import React, { SFC, ReactNode } from 'react';
-import { Tooltip } from '@grafana/ui';
-
-interface Props {
-  tooltip?: string;
-  for?: string;
-  children: ReactNode;
-  width?: number;
-  className?: string;
-}
-
-export const Label: SFC<Props> = props => {
-  return (
-    <span className={`gf-form-label width-${props.width ? props.width : '10'}`}>
-      <span>{props.children}</span>
-      {props.tooltip && (
-        <Tooltip placement="auto" content={props.tooltip}>
-          <div className="gf-form-help-icon--right-normal">
-            <i className="gicon gicon-question gicon--has-hover" />
-          </div>
-        </Tooltip>
-      )}
-    </span>
-  );
-};

+ 2 - 2
public/app/core/components/LayoutSelector/LayoutSelector.tsx

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 
 
 export type LayoutMode = LayoutModes.Grid | LayoutModes.List;
 export type LayoutMode = LayoutModes.Grid | LayoutModes.List;
 
 
@@ -12,7 +12,7 @@ interface Props {
   onLayoutModeChanged: (mode: LayoutMode) => {};
   onLayoutModeChanged: (mode: LayoutMode) => {};
 }
 }
 
 
-const LayoutSelector: SFC<Props> = props => {
+const LayoutSelector: FC<Props> = props => {
   const { mode, onLayoutModeChanged } = props;
   const { mode, onLayoutModeChanged } = props;
   return (
   return (
     <div className="layout-selector">
     <div className="layout-selector">

+ 75 - 0
public/app/core/components/Page/Page.tsx

@@ -0,0 +1,75 @@
+// Libraries
+import React, { Component } from 'react';
+import config from 'app/core/config';
+import { NavModel } from 'app/types';
+import { getTitleFromNavModel } from 'app/core/selectors/navModel';
+
+// Components
+import PageHeader from '../PageHeader/PageHeader';
+import Footer from '../Footer/Footer';
+import PageContents from './PageContents';
+import { CustomScrollbar } from '@grafana/ui';
+
+interface Props {
+  title?: string;
+  children: JSX.Element[] | JSX.Element;
+  navModel: NavModel;
+}
+
+class Page extends Component<Props> {
+  private bodyClass = 'is-react';
+  private body = document.body;
+  static Header = PageHeader;
+  static Contents = PageContents;
+
+  componentDidMount() {
+    this.body.classList.add(this.bodyClass);
+    this.updateTitle();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.title !== this.props.title) {
+      this.updateTitle();
+    }
+  }
+
+  componentWillUnmount() {
+    this.body.classList.remove(this.bodyClass);
+  }
+
+  updateTitle = () => {
+    const title = this.getPageTitle;
+    document.title = title ? title + ' - Grafana' : 'Grafana';
+  }
+
+  get getPageTitle () {
+    const { navModel } = this.props;
+    if (navModel) {
+      return getTitleFromNavModel(navModel) || undefined;
+    }
+    return undefined;
+  }
+
+  render() {
+    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>
+    );
+  }
+}
+
+export default Page;

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

@@ -0,0 +1,26 @@
+// Libraries
+import React, { Component } from 'react';
+
+// Components
+import PageLoader from '../PageLoader/PageLoader';
+
+interface Props {
+  isLoading?: boolean;
+  children: JSX.Element[] | JSX.Element;
+}
+
+class PageContents extends Component<Props> {
+
+  render() {
+    const { isLoading } = this.props;
+
+    return (
+      <div className="page-container page-body">
+        {isLoading && <PageLoader />}
+        {this.props.children}
+      </div>
+    );
+  }
+}
+
+export default PageContents;

+ 3 - 3
public/app/core/components/PageHeader/PageHeader.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { FormEvent } from 'react';
 import { NavModel, NavModelItem } from 'app/types';
 import { NavModel, NavModelItem } from 'app/types';
 import classNames from 'classnames';
 import classNames from 'classnames';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
@@ -12,8 +12,8 @@ const SelectNav = ({ main, customCss }: { main: NavModelItem; customCss: string
     return navItem.active === true;
     return navItem.active === true;
   });
   });
 
 
-  const gotoUrl = evt => {
-    const element = evt.target;
+  const gotoUrl = (evt: FormEvent) => {
+    const element = evt.target as HTMLSelectElement;
     const url = element.options[element.selectedIndex].value;
     const url = element.options[element.selectedIndex].value;
     appEvents.emit('location-change', { href: url });
     appEvents.emit('location-change', { href: url });
   };
   };

+ 3 - 3
public/app/core/components/PageLoader/PageLoader.tsx

@@ -1,10 +1,10 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 
 
 interface Props {
 interface Props {
-  pageName: string;
+  pageName?: string;
 }
 }
 
 
-const PageLoader: SFC<Props> = ({ pageName }) => {
+const PageLoader: FC<Props> = ({ pageName }) => {
   const loadingText = `Loading ${pageName}...`;
   const loadingText = `Loading ${pageName}...`;
   return (
   return (
     <div className="page-loader-wrapper">
     <div className="page-loader-wrapper">

+ 3 - 4
public/app/core/components/SharedPreferences/SharedPreferences.tsx

@@ -1,7 +1,6 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 
 
-import { Label } from 'app/core/components/Label/Label';
-import { Select } from '@grafana/ui';
+import { FormLabel, Select } from '@grafana/ui';
 import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
 import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
 
 
 import { DashboardSearchHit } from 'app/types';
 import { DashboardSearchHit } from 'app/types';
@@ -100,12 +99,12 @@ export class SharedPreferences extends PureComponent<Props, State> {
           />
           />
         </div>
         </div>
         <div className="gf-form">
         <div className="gf-form">
-          <Label
+          <FormLabel
             width={11}
             width={11}
             tooltip="Not finding dashboard you want? Star it first, then it should appear in this select box."
             tooltip="Not finding dashboard you want? Star it first, then it should appear in this select box."
           >
           >
             Home Dashboard
             Home Dashboard
-          </Label>
+          </FormLabel>
           <Select
           <Select
             value={dashboards.find(dashboard => dashboard.id === homeDashboardId)}
             value={dashboards.find(dashboard => dashboard.id === homeDashboardId)}
             getOptionValue={i => i.id}
             getOptionValue={i => i.id}

+ 2 - 2
public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx

@@ -1,4 +1,4 @@
-import React, { SFC, ReactNode, PureComponent } from 'react';
+import React, { FC, ReactNode, PureComponent } from 'react';
 import { Tooltip } from '@grafana/ui';
 import { Tooltip } from '@grafana/ui';
 
 
 interface ToggleButtonGroupProps {
 interface ToggleButtonGroupProps {
@@ -29,7 +29,7 @@ interface ToggleButtonProps {
   tooltip?: string;
   tooltip?: string;
 }
 }
 
 
-export const ToggleButton: SFC<ToggleButtonProps> = ({
+export const ToggleButton: FC<ToggleButtonProps> = ({
   children,
   children,
   selected,
   selected,
   className = '',
   className = '',

+ 2 - 2
public/app/core/components/sidemenu/DropDownChild.tsx

@@ -1,10 +1,10 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 
 
 export interface Props {
 export interface Props {
   child: any;
   child: any;
 }
 }
 
 
-const DropDownChild: SFC<Props> = props => {
+const DropDownChild: FC<Props> = props => {
   const { child } = props;
   const { child } = props;
   const listItemClassName = child.divider ? 'divider' : '';
   const listItemClassName = child.divider ? 'divider' : '';
 
 

+ 2 - 2
public/app/core/components/sidemenu/SideMenuDropDown.tsx

@@ -1,11 +1,11 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import DropDownChild from './DropDownChild';
 import DropDownChild from './DropDownChild';
 
 
 interface Props {
 interface Props {
   link: any;
   link: any;
 }
 }
 
 
-const SideMenuDropDown: SFC<Props> = props => {
+const SideMenuDropDown: FC<Props> = props => {
   const { link } = props;
   const { link } = props;
   return (
   return (
     <ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">
     <ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">

+ 2 - 2
public/app/core/components/sidemenu/SignIn.tsx

@@ -1,6 +1,6 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 
 
-const SignIn: SFC<any> = () => {
+const SignIn: FC<any> = () => {
   const loginUrl = `login?redirect=${encodeURIComponent(window.location.pathname)}`;
   const loginUrl = `login?redirect=${encodeURIComponent(window.location.pathname)}`;
   return (
   return (
     <div className="sidemenu-item">
     <div className="sidemenu-item">

+ 2 - 2
public/app/core/components/sidemenu/TopSection.tsx

@@ -1,9 +1,9 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import _ from 'lodash';
 import _ from 'lodash';
 import TopSectionItem from './TopSectionItem';
 import TopSectionItem from './TopSectionItem';
 import config from '../../config';
 import config from '../../config';
 
 
-const TopSection: SFC<any> = () => {
+const TopSection: FC<any> = () => {
   const navTree = _.cloneDeep(config.bootData.navTree);
   const navTree = _.cloneDeep(config.bootData.navTree);
   const mainLinks = _.filter(navTree, item => !item.hideFromMenu);
   const mainLinks = _.filter(navTree, item => !item.hideFromMenu);
 
 

+ 2 - 2
public/app/core/components/sidemenu/TopSectionItem.tsx

@@ -1,11 +1,11 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import SideMenuDropDown from './SideMenuDropDown';
 import SideMenuDropDown from './SideMenuDropDown';
 
 
 export interface Props {
 export interface Props {
   link: any;
   link: any;
 }
 }
 
 
-const TopSectionItem: SFC<Props> = props => {
+const TopSectionItem: FC<Props> = props => {
   const { link } = props;
   const { link } = props;
   return (
   return (
     <div className="sidemenu-item dropdown">
     <div className="sidemenu-item dropdown">

+ 2 - 0
public/app/core/config.ts

@@ -6,6 +6,8 @@ export interface BuildInfo {
   commit: string;
   commit: string;
   isEnterprise: boolean;
   isEnterprise: boolean;
   env: string;
   env: string;
+  latestVersion: string;
+  hasUpdate: boolean;
 }
 }
 
 
 export class Settings {
 export class Settings {

+ 4 - 0
public/app/core/selectors/navModel.ts

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

+ 8 - 1
public/app/features/api-keys/ApiKeysPage.test.tsx

@@ -6,7 +6,14 @@ import { getMultipleMockKeys, getMockKey } from './__mocks__/apiKeysMock';
 
 
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
   const props: Props = {
   const props: Props = {
-    navModel: {} as NavModel,
+    navModel: {
+      main: {
+        text: 'Configuration'
+      },
+      node: {
+        text: 'Api Keys'
+      }
+    } as NavModel,
     apiKeys: [] as ApiKey[],
     apiKeys: [] as ApiKey[],
     searchQuery: '',
     searchQuery: '',
     hasFetched: false,
     hasFetched: false,

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

@@ -6,8 +6,7 @@ import { NavModel, ApiKey, NewApiKey, OrgRole } from 'app/types';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getApiKeys, getApiKeysCount } from './state/selectors';
 import { getApiKeys, getApiKeysCount } from './state/selectors';
 import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
 import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
-import PageHeader from 'app/core/components/PageHeader/PageHeader';
-import PageLoader from 'app/core/components/PageLoader/PageLoader';
+import Page from 'app/core/components/Page/Page';
 import SlideDown from 'app/core/components/Animations/SlideDown';
 import SlideDown from 'app/core/components/Animations/SlideDown';
 import ApiKeysAddedModal from './ApiKeysAddedModal';
 import ApiKeysAddedModal from './ApiKeysAddedModal';
 import config from 'app/core/config';
 import config from 'app/core/config';
@@ -240,18 +239,17 @@ export class ApiKeysPage extends PureComponent<Props, any> {
     const { hasFetched, navModel, apiKeysCount } = this.props;
     const { hasFetched, navModel, apiKeysCount } = this.props;
 
 
     return (
     return (
-      <div>
-        <PageHeader model={navModel} />
-        {hasFetched ? (
-          apiKeysCount > 0 ? (
-            this.renderApiKeyList()
-          ) : (
-            this.renderEmptyList()
-          )
-        ) : (
-          <PageLoader pageName="Api keys" />
-        )}
-      </div>
+      <Page navModel={navModel}>
+        <Page.Contents isLoading={!hasFetched}>
+          {hasFetched && (
+            apiKeysCount > 0 ? (
+              this.renderApiKeyList()
+            ) : (
+              this.renderEmptyList()
+            )
+          )}
+        </Page.Contents>
+      </Page>
     );
     );
   }
   }
 }
 }

+ 130 - 110
public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap

@@ -1,132 +1,152 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 
 exports[`Render should render API keys table if there are any keys 1`] = `
 exports[`Render should render API keys table if there are any keys 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Api Keys",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={true}
   />
   />
-  <PageLoader
-    pageName="Api keys"
-  />
-</div>
+</Page>
 `;
 `;
 
 
 exports[`Render should render CTA if there are no API keys 1`] = `
 exports[`Render should render CTA if there are no API keys 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Api Keys",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={false}
   >
   >
-    <EmptyListCTA
-      model={
-        Object {
-          "buttonIcon": "fa fa-plus",
-          "buttonLink": "#",
-          "buttonTitle": " New API Key",
-          "onClick": [Function],
-          "proTip": "Remember you can provide view-only API access to other applications.",
-          "proTipLink": "",
-          "proTipLinkTitle": "",
-          "proTipTarget": "_blank",
-          "title": "You haven't added any API Keys yet.",
-        }
-      }
-    />
-    <Component
-      in={false}
+    <div
+      className="page-container page-body"
     >
     >
-      <div
-        className="cta-form"
+      <EmptyListCTA
+        model={
+          Object {
+            "buttonIcon": "fa fa-plus",
+            "buttonLink": "#",
+            "buttonTitle": " New API Key",
+            "onClick": [Function],
+            "proTip": "Remember you can provide view-only API access to other applications.",
+            "proTipLink": "",
+            "proTipLinkTitle": "",
+            "proTipTarget": "_blank",
+            "title": "You haven't added any API Keys yet.",
+          }
+        }
+      />
+      <Component
+        in={false}
       >
       >
-        <button
-          className="cta-form__close btn btn-transparent"
-          onClick={[Function]}
-        >
-          <i
-            className="fa fa-close"
-          />
-        </button>
-        <h5>
-          Add API Key
-        </h5>
-        <form
-          className="gf-form-group"
-          onSubmit={[Function]}
+        <div
+          className="cta-form"
         >
         >
-          <div
-            className="gf-form-inline"
+          <button
+            className="cta-form__close btn btn-transparent"
+            onClick={[Function]}
+          >
+            <i
+              className="fa fa-close"
+            />
+          </button>
+          <h5>
+            Add API Key
+          </h5>
+          <form
+            className="gf-form-group"
+            onSubmit={[Function]}
           >
           >
             <div
             <div
-              className="gf-form max-width-21"
-            >
-              <span
-                className="gf-form-label"
-              >
-                Key name
-              </span>
-              <input
-                className="gf-form-input"
-                onChange={[Function]}
-                placeholder="Name"
-                type="text"
-                value=""
-              />
-            </div>
-            <div
-              className="gf-form"
+              className="gf-form-inline"
             >
             >
-              <span
-                className="gf-form-label"
+              <div
+                className="gf-form max-width-21"
               >
               >
-                Role
-              </span>
-              <span
-                className="gf-form-select-wrapper"
-              >
-                <select
-                  className="gf-form-input gf-size-auto"
+                <span
+                  className="gf-form-label"
+                >
+                  Key name
+                </span>
+                <input
+                  className="gf-form-input"
                   onChange={[Function]}
                   onChange={[Function]}
-                  value="Viewer"
+                  placeholder="Name"
+                  type="text"
+                  value=""
+                />
+              </div>
+              <div
+                className="gf-form"
+              >
+                <span
+                  className="gf-form-label"
                 >
                 >
-                  <option
-                    key="Viewer"
-                    label="Viewer"
+                  Role
+                </span>
+                <span
+                  className="gf-form-select-wrapper"
+                >
+                  <select
+                    className="gf-form-input gf-size-auto"
+                    onChange={[Function]}
                     value="Viewer"
                     value="Viewer"
                   >
                   >
-                    Viewer
-                  </option>
-                  <option
-                    key="Editor"
-                    label="Editor"
-                    value="Editor"
-                  >
-                    Editor
-                  </option>
-                  <option
-                    key="Admin"
-                    label="Admin"
-                    value="Admin"
-                  >
-                    Admin
-                  </option>
-                </select>
-              </span>
-            </div>
-            <div
-              className="gf-form"
-            >
-              <button
-                className="btn gf-form-btn btn-success"
+                    <option
+                      key="Viewer"
+                      label="Viewer"
+                      value="Viewer"
+                    >
+                      Viewer
+                    </option>
+                    <option
+                      key="Editor"
+                      label="Editor"
+                      value="Editor"
+                    >
+                      Editor
+                    </option>
+                    <option
+                      key="Admin"
+                      label="Admin"
+                      value="Admin"
+                    >
+                      Admin
+                    </option>
+                  </select>
+                </span>
+              </div>
+              <div
+                className="gf-form"
               >
               >
-                Add
-              </button>
+                <button
+                  className="btn gf-form-btn btn-success"
+                >
+                  Add
+                </button>
+              </div>
             </div>
             </div>
-          </div>
-        </form>
-      </div>
-    </Component>
-  </div>
-</div>
+          </form>
+        </div>
+      </Component>
+    </div>
+  </PageContents>
+</Page>
 `;
 `;

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

@@ -1,11 +1,11 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import { PanelMenuItem } from '@grafana/ui';
 import { PanelMenuItem } from '@grafana/ui';
 
 
 interface Props {
 interface Props {
   children: any;
   children: any;
 }
 }
 
 
-export const PanelHeaderMenuItem: SFC<Props & PanelMenuItem> = props => {
+export const PanelHeaderMenuItem: FC<Props & PanelMenuItem> = props => {
   const isSubMenu = props.type === 'submenu';
   const isSubMenu = props.type === 'submenu';
   const isDivider = props.type === 'divider';
   const isDivider = props.type === 'divider';
   return isDivider ? (
   return isDivider ? (

+ 2 - 2
public/app/features/dashboard/panel_editor/DataSourceOption.tsx

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import { Tooltip } from '@grafana/ui';
 import { Tooltip } from '@grafana/ui';
 
 
 interface Props {
 interface Props {
@@ -10,7 +10,7 @@ interface Props {
   tooltipInfo?: any;
   tooltipInfo?: any;
 }
 }
 
 
-export const DataSourceOptions: SFC<Props> = ({ label, placeholder, name, value, onChange, tooltipInfo }) => {
+export const DataSourceOptions: FC<Props> = ({ label, placeholder, name, value, onChange, tooltipInfo }) => {
   const dsOption = (
   const dsOption = (
     <div className="gf-form gf-form--flex-end">
     <div className="gf-form gf-form--flex-end">
       <label className="gf-form-label">{label}</label>
       <label className="gf-form-label">{label}</label>

+ 2 - 2
public/app/features/dashboard/panel_editor/QueryOptions.tsx

@@ -10,7 +10,7 @@ import { Input } from 'app/core/components/Form';
 import { EventsWithValidation } from 'app/core/components/Form/Input';
 import { EventsWithValidation } from 'app/core/components/Form/Input';
 import { InputStatus } from 'app/core/components/Form/Input';
 import { InputStatus } from 'app/core/components/Form/Input';
 import DataSourceOption from './DataSourceOption';
 import DataSourceOption from './DataSourceOption';
-import { GfFormLabel } from '@grafana/ui';
+import { FormLabel } from '@grafana/ui';
 
 
 // Types
 // Types
 import { PanelModel } from '../panel_model';
 import { PanelModel } from '../panel_model';
@@ -164,7 +164,7 @@ export class QueryOptions extends PureComponent<Props, State> {
         {this.renderOptions()}
         {this.renderOptions()}
 
 
         <div className="gf-form">
         <div className="gf-form">
-          <GfFormLabel>Relative time</GfFormLabel>
+          <FormLabel>Relative time</FormLabel>
           <Input
           <Input
             type="text"
             type="text"
             className="width-6"
             className="width-6"

+ 2 - 2
public/app/features/datasources/DashboardsTable.tsx

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import { PluginDashboard } from '../../types';
 import { PluginDashboard } from '../../types';
 
 
 export interface Props {
 export interface Props {
@@ -7,7 +7,7 @@ export interface Props {
   onRemove: (dashboard) => void;
   onRemove: (dashboard) => void;
 }
 }
 
 
-const DashboardsTable: SFC<Props> = ({ dashboards, onImport, onRemove }) => {
+const DashboardsTable: FC<Props> = ({ dashboards, onImport, onRemove }) => {
   function buttonText(dashboard: PluginDashboard) {
   function buttonText(dashboard: PluginDashboard) {
     return dashboard.revision !== dashboard.importedRevision ? 'Update' : 'Re-import';
     return dashboard.revision !== dashboard.importedRevision ? 'Update' : 'Re-import';
   }
   }

+ 8 - 1
public/app/features/datasources/DataSourcesListPage.test.tsx

@@ -10,7 +10,14 @@ const setup = (propOverrides?: object) => {
     dataSources: [] as DataSource[],
     dataSources: [] as DataSource[],
     layoutMode: LayoutModes.Grid,
     layoutMode: LayoutModes.Grid,
     loadDataSources: jest.fn(),
     loadDataSources: jest.fn(),
-    navModel: {} as NavModel,
+    navModel: {
+      main: {
+        text: 'Configuration'
+      },
+      node: {
+        text: 'Data Sources'
+      }
+    } as NavModel,
     dataSourcesCount: 0,
     dataSourcesCount: 0,
     searchQuery: '',
     searchQuery: '',
     setDataSourcesSearchQuery: jest.fn(),
     setDataSourcesSearchQuery: jest.fn(),

+ 27 - 27
public/app/features/datasources/DataSourcesListPage.tsx

@@ -1,15 +1,15 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
 import { hot } from 'react-hot-loader';
 import { hot } from 'react-hot-loader';
-import PageHeader from '../../core/components/PageHeader/PageHeader';
-import PageLoader from 'app/core/components/PageLoader/PageLoader';
-import OrgActionBar from '../../core/components/OrgActionBar/OrgActionBar';
-import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA';
+import Page from 'app/core/components/Page/Page';
+import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar';
+import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
 import DataSourcesList from './DataSourcesList';
 import DataSourcesList from './DataSourcesList';
-import { DataSource, NavModel } from 'app/types';
-import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector';
+import { DataSource, NavModel, StoreState } from 'app/types';
+import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
 import { loadDataSources, setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions';
 import { loadDataSources, setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions';
-import { getNavModel } from '../../core/selectors/navModel';
+import { getNavModel } from 'app/core/selectors/navModel';
+
 import {
 import {
   getDataSources,
   getDataSources,
   getDataSourcesCount,
   getDataSourcesCount,
@@ -67,30 +67,30 @@ export class DataSourcesListPage extends PureComponent<Props> {
     };
     };
 
 
     return (
     return (
-      <div>
-        <PageHeader model={navModel} />
-        <div className="page-container page-body">
-          {!hasFetched && <PageLoader pageName="Data sources" />}
-          {hasFetched && dataSourcesCount === 0 && <EmptyListCTA model={emptyListModel} />}
-          {hasFetched &&
-            dataSourcesCount > 0 && [
-              <OrgActionBar
-                layoutMode={layoutMode}
-                searchQuery={searchQuery}
-                onSetLayoutMode={mode => setDataSourcesLayoutMode(mode)}
-                setSearchQuery={query => setDataSourcesSearchQuery(query)}
-                linkButton={linkButton}
-                key="action-bar"
-              />,
-              <DataSourcesList dataSources={dataSources} layoutMode={layoutMode} key="list" />,
-            ]}
-        </div>
-      </div>
+      <Page navModel={navModel}>
+        <Page.Contents isLoading={!hasFetched}>
+          <>
+            {hasFetched && dataSourcesCount === 0 && <EmptyListCTA model={emptyListModel} />}
+            {hasFetched &&
+              dataSourcesCount > 0 && [
+                <OrgActionBar
+                  layoutMode={layoutMode}
+                  searchQuery={searchQuery}
+                  onSetLayoutMode={mode => setDataSourcesLayoutMode(mode)}
+                  setSearchQuery={query => setDataSourcesSearchQuery(query)}
+                  linkButton={linkButton}
+                  key="action-bar"
+                />,
+                <DataSourcesList dataSources={dataSources} layoutMode={layoutMode} key="list" />,
+              ]}
+          </>
+        </Page.Contents>
+      </Page>
     );
     );
   }
   }
 }
 }
 
 
-function mapStateToProps(state) {
+function mapStateToProps(state: StoreState) {
   return {
   return {
     navModel: getNavModel(state.navIndex, 'datasources'),
     navModel: getNavModel(state.navIndex, 'datasources'),
     dataSources: getDataSources(state.dataSources),
     dataSources: getDataSources(state.dataSources),

+ 31 - 19
public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap

@@ -1,12 +1,20 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 
 exports[`Render should render action bar and datasources 1`] = `
 exports[`Render should render action bar and datasources 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Data Sources",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={false}
   >
   >
     <OrgActionBar
     <OrgActionBar
       key="action-bar"
       key="action-bar"
@@ -143,21 +151,25 @@ exports[`Render should render action bar and datasources 1`] = `
       key="list"
       key="list"
       layoutMode="grid"
       layoutMode="grid"
     />
     />
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;
 `;
 
 
 exports[`Render should render component 1`] = `
 exports[`Render should render component 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Data Sources",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={true}
   />
   />
-  <div
-    className="page-container page-body"
-  >
-    <PageLoader
-      pageName="Data sources"
-    />
-  </div>
-</div>
+</Page>
 `;
 `;

+ 5 - 5
public/app/features/datasources/settings/BasicSettings.tsx

@@ -1,5 +1,5 @@
-import React, { SFC } from 'react';
-import { Label } from 'app/core/components/Label/Label';
+import React, { FC } from 'react';
+import { FormLabel } from '@grafana/ui';
 import { Switch } from '../../../core/components/Switch/Switch';
 import { Switch } from '../../../core/components/Switch/Switch';
 
 
 export interface Props {
 export interface Props {
@@ -9,19 +9,19 @@ export interface Props {
   onDefaultChange: (value: boolean) => void;
   onDefaultChange: (value: boolean) => void;
 }
 }
 
 
-const BasicSettings: SFC<Props> = ({ dataSourceName, isDefault, onDefaultChange, onNameChange }) => {
+const BasicSettings: FC<Props> = ({ dataSourceName, isDefault, onDefaultChange, onNameChange }) => {
   return (
   return (
     <div className="gf-form-group">
     <div className="gf-form-group">
       <div className="gf-form-inline">
       <div className="gf-form-inline">
         <div className="gf-form max-width-30" style={{ marginRight: '3px' }}>
         <div className="gf-form max-width-30" style={{ marginRight: '3px' }}>
-          <Label
+          <FormLabel
             tooltip={
             tooltip={
               'The name is used when you select the data source in panels. The Default data source is ' +
               'The name is used when you select the data source in panels. The Default data source is ' +
               'preselected in new panels.'
               'preselected in new panels.'
             }
             }
           >
           >
             Name
             Name
-          </Label>
+          </FormLabel>
           <input
           <input
             className="gf-form-input max-width-23"
             className="gf-form-input max-width-23"
             type="text"
             type="text"

+ 2 - 2
public/app/features/datasources/settings/ButtonRow.tsx

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 
 
 export interface Props {
 export interface Props {
   isReadOnly: boolean;
   isReadOnly: boolean;
@@ -6,7 +6,7 @@ export interface Props {
   onSubmit: (event) => void;
   onSubmit: (event) => void;
 }
 }
 
 
-const ButtonRow: SFC<Props> = ({ isReadOnly, onDelete, onSubmit }) => {
+const ButtonRow: FC<Props> = ({ isReadOnly, onDelete, onSubmit }) => {
   return (
   return (
     <div className="gf-form-button-row">
     <div className="gf-form-button-row">
       <button type="submit" className="btn btn-success" disabled={isReadOnly} onClick={event => onSubmit(event)}>
       <button type="submit" className="btn btn-success" disabled={isReadOnly} onClick={event => onSubmit(event)}>

+ 2 - 2
public/app/features/explore/Error.tsx

@@ -1,10 +1,10 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 
 
 interface Props {
 interface Props {
   message: any;
   message: any;
 }
 }
 
 
-export const Alert: SFC<Props> = props => {
+export const Alert: FC<Props> = props => {
   const { message } = props;
   const { message } = props;
   return (
   return (
     <div className="gf-form-group section">
     <div className="gf-form-group section">

+ 9 - 3
public/app/features/explore/state/actions.ts

@@ -161,11 +161,17 @@ export function initializeExplore(
       },
       },
     });
     });
 
 
-    if (exploreDatasources.length > 1) {
+    if (exploreDatasources.length >= 1) {
       let instance;
       let instance;
       if (datasource) {
       if (datasource) {
-        instance = await getDatasourceSrv().get(datasource);
-      } else {
+        try {
+          instance = await getDatasourceSrv().get(datasource);
+        } catch (error) {
+          console.error(error);
+        }
+      }
+      // Checking on instance here because requested datasource could be deleted already
+      if (!instance) {
         instance = await getDatasourceSrv().get();
         instance = await getDatasourceSrv().get();
       }
       }
       dispatch(loadDatasource(exploreId, instance));
       dispatch(loadDatasource(exploreId, instance));

+ 8 - 1
public/app/features/org/OrgDetailsPage.test.tsx

@@ -6,7 +6,14 @@ import { NavModel, Organization } from '../../types';
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
   const props: Props = {
   const props: Props = {
     organization: {} as Organization,
     organization: {} as Organization,
-    navModel: {} as NavModel,
+    navModel: {
+      main: {
+        text: 'Configuration'
+      },
+      node: {
+        text: 'Org details'
+      }
+    } as NavModel,
     loadOrganization: jest.fn(),
     loadOrganization: jest.fn(),
     setOrganizationName: jest.fn(),
     setOrganizationName: jest.fn(),
     updateOrganization: jest.fn(),
     updateOrganization: jest.fn(),

+ 17 - 18
public/app/features/org/OrgDetailsPage.tsx

@@ -1,13 +1,12 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import { hot } from 'react-hot-loader';
 import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
-import PageHeader from '../../core/components/PageHeader/PageHeader';
-import PageLoader from '../../core/components/PageLoader/PageLoader';
+import Page from 'app/core/components/Page/Page';
 import OrgProfile from './OrgProfile';
 import OrgProfile from './OrgProfile';
 import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences';
 import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences';
 import { loadOrganization, setOrganizationName, updateOrganization } from './state/actions';
 import { loadOrganization, setOrganizationName, updateOrganization } from './state/actions';
 import { NavModel, Organization, StoreState } from 'app/types';
 import { NavModel, Organization, StoreState } from 'app/types';
-import { getNavModel } from '../../core/selectors/navModel';
+import { getNavModel } from 'app/core/selectors/navModel';
 
 
 export interface Props {
 export interface Props {
   navModel: NavModel;
   navModel: NavModel;
@@ -35,22 +34,22 @@ export class OrgDetailsPage extends PureComponent<Props> {
     const isLoading = Object.keys(organization).length === 0;
     const isLoading = Object.keys(organization).length === 0;
 
 
     return (
     return (
-      <div>
-        <PageHeader model={navModel} />
-        <div className="page-container page-body">
-          {isLoading && <PageLoader pageName="Organization" />}
-          {!isLoading && (
-            <div>
-              <OrgProfile
-                onOrgNameChange={name => this.onOrgNameChange(name)}
-                onSubmit={this.onUpdateOrganization}
-                orgName={organization.name}
-              />
-              <SharedPreferences resourceUri="org" />
+      <Page navModel={navModel}>
+        <Page.Contents isLoading={isLoading}>
+            <div className="page-container page-body">
+              {!isLoading && (
+                <div>
+                  <OrgProfile
+                    onOrgNameChange={name => this.onOrgNameChange(name)}
+                    onSubmit={this.onUpdateOrganization}
+                    orgName={organization.name}
+                  />
+                  <SharedPreferences resourceUri="org" />
+                </div>
+              )}
             </div>
             </div>
-          )}
-        </div>
-      </div>
+        </Page.Contents>
+      </Page>
     );
     );
   }
   }
 }
 }

+ 2 - 2
public/app/features/org/OrgProfile.tsx

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 
 
 export interface Props {
 export interface Props {
   orgName: string;
   orgName: string;
@@ -6,7 +6,7 @@ export interface Props {
   onOrgNameChange: (orgName: string) => void;
   onOrgNameChange: (orgName: string) => void;
 }
 }
 
 
-const OrgProfile: SFC<Props> = ({ onSubmit, onOrgNameChange, orgName }) => {
+const OrgProfile: FC<Props> = ({ onSubmit, onOrgNameChange, orgName }) => {
   return (
   return (
     <div>
     <div>
       <h3 className="page-sub-heading">Organization profile</h3>
       <h3 className="page-sub-heading">Organization profile</h3>

+ 47 - 27
public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap

@@ -1,38 +1,58 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 
 exports[`Render should render component 1`] = `
 exports[`Render should render component 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Org details",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={true}
   >
   >
-    <PageLoader
-      pageName="Organization"
+    <div
+      className="page-container page-body"
     />
     />
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;
 `;
 
 
 exports[`Render should render organization and preferences 1`] = `
 exports[`Render should render organization and preferences 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Org details",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={false}
   >
   >
-    <div>
-      <OrgProfile
-        onOrgNameChange={[Function]}
-        onSubmit={[Function]}
-        orgName="Cool org"
-      />
-      <SharedPreferences
-        resourceUri="org"
-      />
+    <div
+      className="page-container page-body"
+    >
+      <div>
+        <OrgProfile
+          onOrgNameChange={[Function]}
+          onSubmit={[Function]}
+          orgName="Cool org"
+        />
+        <SharedPreferences
+          resourceUri="org"
+        />
+      </div>
     </div>
     </div>
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;
 `;

+ 2 - 2
public/app/features/plugins/PluginList.tsx

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import classNames from 'classnames';
 import classNames from 'classnames';
 import PluginListItem from './PluginListItem';
 import PluginListItem from './PluginListItem';
 import { Plugin } from 'app/types';
 import { Plugin } from 'app/types';
@@ -9,7 +9,7 @@ interface Props {
   layoutMode: LayoutMode;
   layoutMode: LayoutMode;
 }
 }
 
 
-const PluginList: SFC<Props> = props => {
+const PluginList: FC<Props> = props => {
   const { plugins, layoutMode } = props;
   const { plugins, layoutMode } = props;
 
 
   const listStyle = classNames({
   const listStyle = classNames({

+ 2 - 2
public/app/features/plugins/PluginListItem.tsx

@@ -1,11 +1,11 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import { Plugin } from 'app/types';
 import { Plugin } from 'app/types';
 
 
 interface Props {
 interface Props {
   plugin: Plugin;
   plugin: Plugin;
 }
 }
 
 
-const PluginListItem: SFC<Props> = props => {
+const PluginListItem: FC<Props> = props => {
   const { plugin } = props;
   const { plugin } = props;
 
 
   return (
   return (

+ 8 - 1
public/app/features/plugins/PluginListPage.test.tsx

@@ -6,7 +6,14 @@ import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector
 
 
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
   const props: Props = {
   const props: Props = {
-    navModel: {} as NavModel,
+    navModel: {
+      main: {
+        text: 'Configuration'
+      },
+      node: {
+        text: 'Plugins'
+      }
+    } as NavModel,
     plugins: [] as Plugin[],
     plugins: [] as Plugin[],
     searchQuery: '',
     searchQuery: '',
     setPluginsSearchQuery: jest.fn(),
     setPluginsSearchQuery: jest.fn(),

+ 19 - 21
public/app/features/plugins/PluginListPage.tsx

@@ -1,15 +1,14 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import { hot } from 'react-hot-loader';
 import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
-import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import Page from 'app/core/components/Page/Page';
 import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar';
 import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar';
-import PageLoader from 'app/core/components/PageLoader/PageLoader';
 import PluginList from './PluginList';
 import PluginList from './PluginList';
 import { NavModel, Plugin } from 'app/types';
 import { NavModel, Plugin } from 'app/types';
 import { loadPlugins, setPluginsLayoutMode, setPluginsSearchQuery } from './state/actions';
 import { loadPlugins, setPluginsLayoutMode, setPluginsSearchQuery } from './state/actions';
-import { getNavModel } from '../../core/selectors/navModel';
+import { getNavModel } from 'app/core/selectors/navModel';
 import { getLayoutMode, getPlugins, getPluginsSearchQuery } from './state/selectors';
 import { getLayoutMode, getPlugins, getPluginsSearchQuery } from './state/selectors';
-import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector';
+import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
 
 
 export interface Props {
 export interface Props {
   navModel: NavModel;
   navModel: NavModel;
@@ -48,23 +47,22 @@ export class PluginListPage extends PureComponent<Props> {
     };
     };
 
 
     return (
     return (
-      <div>
-        <PageHeader model={navModel} />
-        <div className="page-container page-body">
-          <OrgActionBar
-            searchQuery={searchQuery}
-            layoutMode={layoutMode}
-            onSetLayoutMode={mode => setPluginsLayoutMode(mode)}
-            setSearchQuery={query => setPluginsSearchQuery(query)}
-            linkButton={linkButton}
-          />
-          {hasFetched ? (
-            plugins && <PluginList plugins={plugins} layoutMode={layoutMode} />
-          ) : (
-            <PageLoader pageName="Plugins" />
-          )}
-        </div>
-      </div>
+      <Page navModel={navModel}>
+        <Page.Contents isLoading={!hasFetched}>
+          <>
+            <OrgActionBar
+              searchQuery={searchQuery}
+              layoutMode={layoutMode}
+              onSetLayoutMode={mode => setPluginsLayoutMode(mode)}
+              setSearchQuery={query => setPluginsSearchQuery(query)}
+              linkButton={linkButton}
+            />
+            {hasFetched && plugins && (
+              plugins && <PluginList plugins={plugins} layoutMode={layoutMode} />
+            )}
+          </>
+        </Page.Contents>
+      </Page>
     );
     );
   }
   }
 }
 }

+ 32 - 19
public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap

@@ -1,12 +1,20 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 
 exports[`Render should render component 1`] = `
 exports[`Render should render component 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Plugins",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={true}
   >
   >
     <OrgActionBar
     <OrgActionBar
       layoutMode="grid"
       layoutMode="grid"
@@ -20,20 +28,25 @@ exports[`Render should render component 1`] = `
       searchQuery=""
       searchQuery=""
       setSearchQuery={[Function]}
       setSearchQuery={[Function]}
     />
     />
-    <PageLoader
-      pageName="Plugins"
-    />
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;
 `;
 
 
 exports[`Render should render list 1`] = `
 exports[`Render should render list 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Plugins",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={false}
   >
   >
     <OrgActionBar
     <OrgActionBar
       layoutMode="grid"
       layoutMode="grid"
@@ -51,6 +64,6 @@ exports[`Render should render list 1`] = `
       layoutMode="grid"
       layoutMode="grid"
       plugins={Array []}
       plugins={Array []}
     />
     />
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;
 `;

+ 8 - 1
public/app/features/teams/TeamList.test.tsx

@@ -6,7 +6,14 @@ import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks';
 
 
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
   const props: Props = {
   const props: Props = {
-    navModel: {} as NavModel,
+    navModel: {
+      main: {
+        text: 'Configuration'
+      },
+      node: {
+        text: 'Team List'
+      }
+    } as NavModel,
     teams: [] as Team[],
     teams: [] as Team[],
     loadTeams: jest.fn(),
     loadTeams: jest.fn(),
     deleteTeam: jest.fn(),
     deleteTeam: jest.fn(),

+ 7 - 7
public/app/features/teams/TeamList.tsx

@@ -1,11 +1,10 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
 import { hot } from 'react-hot-loader';
 import { hot } from 'react-hot-loader';
-import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import Page from 'app/core/components/Page/Page';
 import { DeleteButton } from '@grafana/ui';
 import { DeleteButton } from '@grafana/ui';
 import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
 import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
-import PageLoader from 'app/core/components/PageLoader/PageLoader';
-import { NavModel, Team } from '../../types';
+import { NavModel, Team } from 'app/types';
 import { loadTeams, deleteTeam, setSearchQuery } from './state/actions';
 import { loadTeams, deleteTeam, setSearchQuery } from './state/actions';
 import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors';
 import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getNavModel } from 'app/core/selectors/navModel';
@@ -141,10 +140,11 @@ export class TeamList extends PureComponent<Props, any> {
     const { hasFetched, navModel } = this.props;
     const { hasFetched, navModel } = this.props;
 
 
     return (
     return (
-      <div>
-        <PageHeader model={navModel} />
-        {hasFetched ? this.renderList() : <PageLoader pageName="Teams" />}
-      </div>
+      <Page navModel={navModel}>
+        <Page.Contents isLoading={!hasFetched}>
+          {hasFetched && this.renderList()}
+        </Page.Contents>
+      </Page>
     );
     );
   }
   }
 }
 }

+ 4 - 4
public/app/features/teams/TeamSettings.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
+import { FormLabel } from '@grafana/ui';
 
 
-import { Label } from 'app/core/components/Label/Label';
 import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences';
 import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences';
 import { updateTeam } from './state/actions';
 import { updateTeam } from './state/actions';
 import { getRouteParamsId } from 'app/core/selectors/location';
 import { getRouteParamsId } from 'app/core/selectors/location';
@@ -51,7 +51,7 @@ export class TeamSettings extends React.Component<Props, State> {
         <h3 className="page-sub-heading">Team Settings</h3>
         <h3 className="page-sub-heading">Team Settings</h3>
         <form name="teamDetailsForm" className="gf-form-group" onSubmit={this.onUpdate}>
         <form name="teamDetailsForm" className="gf-form-group" onSubmit={this.onUpdate}>
           <div className="gf-form max-width-30">
           <div className="gf-form max-width-30">
-            <Label>Name</Label>
+            <FormLabel>Name</FormLabel>
             <input
             <input
               type="text"
               type="text"
               required
               required
@@ -62,9 +62,9 @@ export class TeamSettings extends React.Component<Props, State> {
           </div>
           </div>
 
 
           <div className="gf-form max-width-30">
           <div className="gf-form max-width-30">
-            <Label tooltip="This is optional and is primarily used to set the team profile avatar (via gravatar service)">
+            <FormLabel tooltip="This is optional and is primarily used to set the team profile avatar (via gravatar service)">
               Email
               Email
-            </Label>
+            </FormLabel>
             <input
             <input
               type="email"
               type="email"
               className="gf-form-input max-width-22"
               className="gf-form-input max-width-22"

+ 308 - 288
public/app/features/teams/__snapshots__/TeamList.test.tsx.snap

@@ -1,336 +1,356 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 
 exports[`Render should render component 1`] = `
 exports[`Render should render component 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Team List",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={true}
   />
   />
-  <PageLoader
-    pageName="Teams"
-  />
-</div>
+</Page>
 `;
 `;
 
 
 exports[`Render should render teams table 1`] = `
 exports[`Render should render teams table 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Team List",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={false}
   >
   >
     <div
     <div
-      className="page-action-bar"
+      className="page-container page-body"
     >
     >
       <div
       <div
-        className="gf-form gf-form--grow"
+        className="page-action-bar"
       >
       >
-        <label
-          className="gf-form--has-input-icon gf-form--grow"
+        <div
+          className="gf-form gf-form--grow"
         >
         >
-          <input
-            className="gf-form-input"
-            onChange={[Function]}
-            placeholder="Search teams"
-            type="text"
-            value=""
-          />
-          <i
-            className="gf-form-input-icon fa fa-search"
-          />
-        </label>
+          <label
+            className="gf-form--has-input-icon gf-form--grow"
+          >
+            <input
+              className="gf-form-input"
+              onChange={[Function]}
+              placeholder="Search teams"
+              type="text"
+              value=""
+            />
+            <i
+              className="gf-form-input-icon fa fa-search"
+            />
+          </label>
+        </div>
+        <div
+          className="page-action-bar__spacer"
+        />
+        <a
+          className="btn btn-success"
+          href="org/teams/new"
+        >
+          New team
+        </a>
       </div>
       </div>
       <div
       <div
-        className="page-action-bar__spacer"
-      />
-      <a
-        className="btn btn-success"
-        href="org/teams/new"
+        className="admin-list-table"
       >
       >
-        New team
-      </a>
-    </div>
-    <div
-      className="admin-list-table"
-    >
-      <table
-        className="filter-table filter-table--hover form-inline"
-      >
-        <thead>
-          <tr>
-            <th />
-            <th>
-              Name
-            </th>
-            <th>
-              Email
-            </th>
-            <th>
-              Members
-            </th>
-            <th
-              style={
-                Object {
-                  "width": "1%",
+        <table
+          className="filter-table filter-table--hover form-inline"
+        >
+          <thead>
+            <tr>
+              <th />
+              <th>
+                Name
+              </th>
+              <th>
+                Email
+              </th>
+              <th>
+                Members
+              </th>
+              <th
+                style={
+                  Object {
+                    "width": "1%",
+                  }
                 }
                 }
-              }
-            />
-          </tr>
-        </thead>
-        <tbody>
-          <tr
-            key="1"
-          >
-            <td
-              className="width-4 text-center link-td"
+              />
+            </tr>
+          </thead>
+          <tbody>
+            <tr
+              key="1"
             >
             >
-              <a
-                href="org/teams/edit/1"
+              <td
+                className="width-4 text-center link-td"
               >
               >
-                <img
-                  className="filter-table__avatar"
-                  src="some/url/"
-                />
-              </a>
-            </td>
-            <td
-              className="link-td"
-            >
-              <a
-                href="org/teams/edit/1"
+                <a
+                  href="org/teams/edit/1"
+                >
+                  <img
+                    className="filter-table__avatar"
+                    src="some/url/"
+                  />
+                </a>
+              </td>
+              <td
+                className="link-td"
               >
               >
-                test-1
-              </a>
-            </td>
-            <td
-              className="link-td"
-            >
-              <a
-                href="org/teams/edit/1"
+                <a
+                  href="org/teams/edit/1"
+                >
+                  test-1
+                </a>
+              </td>
+              <td
+                className="link-td"
               >
               >
-                test-1@test.com
-              </a>
-            </td>
-            <td
-              className="link-td"
-            >
-              <a
-                href="org/teams/edit/1"
+                <a
+                  href="org/teams/edit/1"
+                >
+                  test-1@test.com
+                </a>
+              </td>
+              <td
+                className="link-td"
               >
               >
-                1
-              </a>
-            </td>
-            <td
-              className="text-right"
-            >
-              <DeleteButton
-                onConfirm={[Function]}
-              />
-            </td>
-          </tr>
-          <tr
-            key="2"
-          >
-            <td
-              className="width-4 text-center link-td"
-            >
-              <a
-                href="org/teams/edit/2"
+                <a
+                  href="org/teams/edit/1"
+                >
+                  1
+                </a>
+              </td>
+              <td
+                className="text-right"
               >
               >
-                <img
-                  className="filter-table__avatar"
-                  src="some/url/"
+                <DeleteButton
+                  onConfirm={[Function]}
                 />
                 />
-              </a>
-            </td>
-            <td
-              className="link-td"
+              </td>
+            </tr>
+            <tr
+              key="2"
             >
             >
-              <a
-                href="org/teams/edit/2"
+              <td
+                className="width-4 text-center link-td"
               >
               >
-                test-2
-              </a>
-            </td>
-            <td
-              className="link-td"
-            >
-              <a
-                href="org/teams/edit/2"
+                <a
+                  href="org/teams/edit/2"
+                >
+                  <img
+                    className="filter-table__avatar"
+                    src="some/url/"
+                  />
+                </a>
+              </td>
+              <td
+                className="link-td"
               >
               >
-                test-2@test.com
-              </a>
-            </td>
-            <td
-              className="link-td"
-            >
-              <a
-                href="org/teams/edit/2"
+                <a
+                  href="org/teams/edit/2"
+                >
+                  test-2
+                </a>
+              </td>
+              <td
+                className="link-td"
               >
               >
-                2
-              </a>
-            </td>
-            <td
-              className="text-right"
-            >
-              <DeleteButton
-                onConfirm={[Function]}
-              />
-            </td>
-          </tr>
-          <tr
-            key="3"
-          >
-            <td
-              className="width-4 text-center link-td"
-            >
-              <a
-                href="org/teams/edit/3"
+                <a
+                  href="org/teams/edit/2"
+                >
+                  test-2@test.com
+                </a>
+              </td>
+              <td
+                className="link-td"
               >
               >
-                <img
-                  className="filter-table__avatar"
-                  src="some/url/"
+                <a
+                  href="org/teams/edit/2"
+                >
+                  2
+                </a>
+              </td>
+              <td
+                className="text-right"
+              >
+                <DeleteButton
+                  onConfirm={[Function]}
                 />
                 />
-              </a>
-            </td>
-            <td
-              className="link-td"
+              </td>
+            </tr>
+            <tr
+              key="3"
             >
             >
-              <a
-                href="org/teams/edit/3"
+              <td
+                className="width-4 text-center link-td"
               >
               >
-                test-3
-              </a>
-            </td>
-            <td
-              className="link-td"
-            >
-              <a
-                href="org/teams/edit/3"
+                <a
+                  href="org/teams/edit/3"
+                >
+                  <img
+                    className="filter-table__avatar"
+                    src="some/url/"
+                  />
+                </a>
+              </td>
+              <td
+                className="link-td"
               >
               >
-                test-3@test.com
-              </a>
-            </td>
-            <td
-              className="link-td"
-            >
-              <a
-                href="org/teams/edit/3"
+                <a
+                  href="org/teams/edit/3"
+                >
+                  test-3
+                </a>
+              </td>
+              <td
+                className="link-td"
               >
               >
-                3
-              </a>
-            </td>
-            <td
-              className="text-right"
-            >
-              <DeleteButton
-                onConfirm={[Function]}
-              />
-            </td>
-          </tr>
-          <tr
-            key="4"
-          >
-            <td
-              className="width-4 text-center link-td"
-            >
-              <a
-                href="org/teams/edit/4"
+                <a
+                  href="org/teams/edit/3"
+                >
+                  test-3@test.com
+                </a>
+              </td>
+              <td
+                className="link-td"
+              >
+                <a
+                  href="org/teams/edit/3"
+                >
+                  3
+                </a>
+              </td>
+              <td
+                className="text-right"
               >
               >
-                <img
-                  className="filter-table__avatar"
-                  src="some/url/"
+                <DeleteButton
+                  onConfirm={[Function]}
                 />
                 />
-              </a>
-            </td>
-            <td
-              className="link-td"
+              </td>
+            </tr>
+            <tr
+              key="4"
             >
             >
-              <a
-                href="org/teams/edit/4"
+              <td
+                className="width-4 text-center link-td"
               >
               >
-                test-4
-              </a>
-            </td>
-            <td
-              className="link-td"
-            >
-              <a
-                href="org/teams/edit/4"
+                <a
+                  href="org/teams/edit/4"
+                >
+                  <img
+                    className="filter-table__avatar"
+                    src="some/url/"
+                  />
+                </a>
+              </td>
+              <td
+                className="link-td"
               >
               >
-                test-4@test.com
-              </a>
-            </td>
-            <td
-              className="link-td"
-            >
-              <a
-                href="org/teams/edit/4"
+                <a
+                  href="org/teams/edit/4"
+                >
+                  test-4
+                </a>
+              </td>
+              <td
+                className="link-td"
               >
               >
-                4
-              </a>
-            </td>
-            <td
-              className="text-right"
-            >
-              <DeleteButton
-                onConfirm={[Function]}
-              />
-            </td>
-          </tr>
-          <tr
-            key="5"
-          >
-            <td
-              className="width-4 text-center link-td"
-            >
-              <a
-                href="org/teams/edit/5"
+                <a
+                  href="org/teams/edit/4"
+                >
+                  test-4@test.com
+                </a>
+              </td>
+              <td
+                className="link-td"
               >
               >
-                <img
-                  className="filter-table__avatar"
-                  src="some/url/"
+                <a
+                  href="org/teams/edit/4"
+                >
+                  4
+                </a>
+              </td>
+              <td
+                className="text-right"
+              >
+                <DeleteButton
+                  onConfirm={[Function]}
                 />
                 />
-              </a>
-            </td>
-            <td
-              className="link-td"
+              </td>
+            </tr>
+            <tr
+              key="5"
             >
             >
-              <a
-                href="org/teams/edit/5"
+              <td
+                className="width-4 text-center link-td"
               >
               >
-                test-5
-              </a>
-            </td>
-            <td
-              className="link-td"
-            >
-              <a
-                href="org/teams/edit/5"
+                <a
+                  href="org/teams/edit/5"
+                >
+                  <img
+                    className="filter-table__avatar"
+                    src="some/url/"
+                  />
+                </a>
+              </td>
+              <td
+                className="link-td"
               >
               >
-                test-5@test.com
-              </a>
-            </td>
-            <td
-              className="link-td"
-            >
-              <a
-                href="org/teams/edit/5"
+                <a
+                  href="org/teams/edit/5"
+                >
+                  test-5
+                </a>
+              </td>
+              <td
+                className="link-td"
               >
               >
-                5
-              </a>
-            </td>
-            <td
-              className="text-right"
-            >
-              <DeleteButton
-                onConfirm={[Function]}
-              />
-            </td>
-          </tr>
-        </tbody>
-      </table>
+                <a
+                  href="org/teams/edit/5"
+                >
+                  test-5@test.com
+                </a>
+              </td>
+              <td
+                className="link-td"
+              >
+                <a
+                  href="org/teams/edit/5"
+                >
+                  5
+                </a>
+              </td>
+              <td
+                className="text-right"
+              >
+                <DeleteButton
+                  onConfirm={[Function]}
+                />
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
     </div>
     </div>
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;
 `;

+ 8 - 1
public/app/features/users/UsersListPage.test.tsx

@@ -11,7 +11,14 @@ jest.mock('../../core/app_events', () => ({
 
 
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
   const props: Props = {
   const props: Props = {
-    navModel: {} as NavModel,
+    navModel: {
+      main: {
+        text: 'Configuration'
+      },
+      node: {
+        text: 'Users'
+      }
+    } as NavModel,
     users: [] as OrgUser[],
     users: [] as OrgUser[],
     invitees: [] as Invitee[],
     invitees: [] as Invitee[],
     searchQuery: '',
     searchQuery: '',

+ 9 - 9
public/app/features/users/UsersListPage.tsx

@@ -2,15 +2,14 @@ import React, { PureComponent } from 'react';
 import { hot } from 'react-hot-loader';
 import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
 import Remarkable from 'remarkable';
 import Remarkable from 'remarkable';
-import PageHeader from 'app/core/components/PageHeader/PageHeader';
-import PageLoader from 'app/core/components/PageLoader/PageLoader';
+import Page from 'app/core/components/Page/Page';
 import UsersActionBar from './UsersActionBar';
 import UsersActionBar from './UsersActionBar';
 import UsersTable from './UsersTable';
 import UsersTable from './UsersTable';
 import InviteesTable from './InviteesTable';
 import InviteesTable from './InviteesTable';
 import { Invitee, NavModel, OrgUser } from 'app/types';
 import { Invitee, NavModel, OrgUser } from 'app/types';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
 import { loadUsers, loadInvitees, setUsersSearchQuery, updateUser, removeUser } from './state/actions';
 import { loadUsers, loadInvitees, setUsersSearchQuery, updateUser, removeUser } from './state/actions';
-import { getNavModel } from '../../core/selectors/navModel';
+import { getNavModel } from 'app/core/selectors/navModel';
 import { getInvitees, getUsers, getUsersSearchQuery } from './state/selectors';
 import { getInvitees, getUsers, getUsersSearchQuery } from './state/selectors';
 
 
 export interface Props {
 export interface Props {
@@ -105,16 +104,17 @@ export class UsersListPage extends PureComponent<Props, State> {
     const externalUserMngInfoHtml = this.externalUserMngInfoHtml;
     const externalUserMngInfoHtml = this.externalUserMngInfoHtml;
 
 
     return (
     return (
-      <div>
-        <PageHeader model={navModel} />
-        <div className="page-container page-body">
+      <Page navModel={navModel}>
+        <Page.Contents isLoading={!hasFetched}>
+          <>
           <UsersActionBar onShowInvites={this.onShowInvites} showInvites={this.state.showInvites} />
           <UsersActionBar onShowInvites={this.onShowInvites} showInvites={this.state.showInvites} />
           {externalUserMngInfoHtml && (
           {externalUserMngInfoHtml && (
             <div className="grafana-info-box" dangerouslySetInnerHTML={{ __html: externalUserMngInfoHtml }} />
             <div className="grafana-info-box" dangerouslySetInnerHTML={{ __html: externalUserMngInfoHtml }} />
           )}
           )}
-          {hasFetched ? this.renderTable() : <PageLoader pageName="Users" />}
-        </div>
-      </div>
+          {hasFetched && this.renderTable()}
+          </>
+        </Page.Contents>
+      </Page>
     );
     );
   }
   }
 }
 }

+ 2 - 2
public/app/features/users/UsersTable.tsx

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import { OrgUser } from 'app/types';
 import { OrgUser } from 'app/types';
 
 
 export interface Props {
 export interface Props {
@@ -7,7 +7,7 @@ export interface Props {
   onRemoveUser: (user: OrgUser) => void;
   onRemoveUser: (user: OrgUser) => void;
 }
 }
 
 
-const UsersTable: SFC<Props> = props => {
+const UsersTable: FC<Props> = props => {
   const { users, onRoleChange, onRemoveUser } = props;
   const { users, onRoleChange, onRemoveUser } = props;
 
 
   return (
   return (

+ 32 - 19
public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap

@@ -1,12 +1,20 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 
 exports[`Render should render List page 1`] = `
 exports[`Render should render List page 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Users",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={false}
   >
   >
     <Connect(UsersActionBar)
     <Connect(UsersActionBar)
       onShowInvites={[Function]}
       onShowInvites={[Function]}
@@ -17,25 +25,30 @@ exports[`Render should render List page 1`] = `
       onRoleChange={[Function]}
       onRoleChange={[Function]}
       users={Array []}
       users={Array []}
     />
     />
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;
 `;
 
 
 exports[`Render should render component 1`] = `
 exports[`Render should render component 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={
+    Object {
+      "main": Object {
+        "text": "Configuration",
+      },
+      "node": Object {
+        "text": "Users",
+      },
+    }
+  }
+>
+  <PageContents
+    isLoading={true}
   >
   >
     <Connect(UsersActionBar)
     <Connect(UsersActionBar)
       onShowInvites={[Function]}
       onShowInvites={[Function]}
       showInvites={false}
       showInvites={false}
     />
     />
-    <PageLoader
-      pageName="Users"
-    />
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;
 `;

+ 2 - 2
public/app/plugins/datasource/stackdriver/components/AlignmentPeriods.tsx

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import _ from 'lodash';
 import _ from 'lodash';
 
 
 import kbn from 'app/core/utils/kbn';
 import kbn from 'app/core/utils/kbn';
@@ -14,7 +14,7 @@ export interface Props {
   usedAlignmentPeriod: string;
   usedAlignmentPeriod: string;
 }
 }
 
 
-export const AlignmentPeriods: SFC<Props> = ({
+export const AlignmentPeriods: FC<Props> = ({
   alignmentPeriod,
   alignmentPeriod,
   templateSrv,
   templateSrv,
   onChange,
   onChange,

+ 2 - 2
public/app/plugins/datasource/stackdriver/components/Alignments.tsx

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import _ from 'lodash';
 import _ from 'lodash';
 
 
 import { MetricSelect } from 'app/core/components/Select/MetricSelect';
 import { MetricSelect } from 'app/core/components/Select/MetricSelect';
@@ -12,7 +12,7 @@ export interface Props {
   perSeriesAligner: string;
   perSeriesAligner: string;
 }
 }
 
 
-export const Alignments: SFC<Props> = ({ perSeriesAligner, templateSrv, onChange, alignOptions }) => {
+export const Alignments: FC<Props> = ({ perSeriesAligner, templateSrv, onChange, alignOptions }) => {
   return (
   return (
     <>
     <>
       <div className="gf-form-group">
       <div className="gf-form-group">

+ 2 - 2
public/app/plugins/datasource/stackdriver/components/AnnotationsHelp.tsx

@@ -1,6 +1,6 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 
 
-export const AnnotationsHelp: SFC = () => {
+export const AnnotationsHelp: FC = () => {
   return (
   return (
     <div className="gf-form grafana-info-box" style={{ padding: 0 }}>
     <div className="gf-form grafana-info-box" style={{ padding: 0 }}>
       <pre className="gf-form-pre alert alert-info" style={{ marginRight: 0 }}>
       <pre className="gf-form-pre alert alert-info" style={{ marginRight: 0 }}>

+ 2 - 2
public/app/plugins/datasource/stackdriver/components/SimpleSelect.tsx

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 
 
 interface Props {
 interface Props {
   onValueChange: (e) => void;
   onValueChange: (e) => void;
@@ -7,7 +7,7 @@ interface Props {
   label: string;
   label: string;
 }
 }
 
 
-const SimpleSelect: SFC<Props> = props => {
+const SimpleSelect: FC<Props> = props => {
   const { label, onValueChange, value, options } = props;
   const { label, onValueChange, value, options } = props;
   return (
   return (
     <div className="gf-form max-width-21">
     <div className="gf-form max-width-21">

+ 4 - 10
public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx

@@ -1,8 +1,8 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
-import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui';
+import { FormField, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui';
 
 
 import { Switch } from 'app/core/components/Switch/Switch';
 import { Switch } from 'app/core/components/Switch/Switch';
-import { Label } from '../../../core/components/Label/Label';
+import { GaugeOptions } from './types';
 
 
 export default class GaugeOptionsEditor extends PureComponent<PanelOptionsProps<GaugeOptions>> {
 export default class GaugeOptionsEditor extends PureComponent<PanelOptionsProps<GaugeOptions>> {
   onToggleThresholdLabels = () =>
   onToggleThresholdLabels = () =>
@@ -21,14 +21,8 @@ export default class GaugeOptionsEditor extends PureComponent<PanelOptionsProps<
 
 
     return (
     return (
       <PanelOptionsGroup title="Gauge">
       <PanelOptionsGroup title="Gauge">
-        <div className="gf-form">
-          <Label width={8}>Min value</Label>
-          <input type="text" className="gf-form-input width-12" onChange={this.onMinValueChange} value={minValue} />
-        </div>
-        <div className="gf-form">
-          <Label width={8}>Max value</Label>
-          <input type="text" className="gf-form-input width-12" onChange={this.onMaxValueChange} value={maxValue} />
-        </div>
+        <FormField label="Min value" labelWidth={8} onChange={this.onMinValueChange} value={minValue} />
+        <FormField label="Max value" labelWidth={8} onChange={this.onMaxValueChange} value={maxValue} />
         <Switch
         <Switch
           label="Show labels"
           label="Show labels"
           labelClass="width-8"
           labelClass="width-8"

+ 2 - 1
public/app/plugins/panel/gauge/GaugePanel.tsx

@@ -1,8 +1,9 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
-import { GaugeOptions, PanelProps, NullValueMode } from '@grafana/ui';
+import { PanelProps, NullValueMode } from '@grafana/ui';
 
 
 import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
 import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
 import Gauge from 'app/viz/Gauge';
 import Gauge from 'app/viz/Gauge';
+import { GaugeOptions } from './types';
 
 
 interface Props extends PanelProps<GaugeOptions> {}
 interface Props extends PanelProps<GaugeOptions> {}
 
 

+ 16 - 5
public/app/plugins/panel/gauge/GaugePanelOptions.tsx

@@ -1,16 +1,17 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import {
 import {
   BasicGaugeColor,
   BasicGaugeColor,
-  GaugeOptions,
   PanelOptionsProps,
   PanelOptionsProps,
   ThresholdsEditor,
   ThresholdsEditor,
   Threshold,
   Threshold,
   PanelOptionsGrid,
   PanelOptionsGrid,
+  ValueMappingsEditor,
+  ValueMapping,
 } from '@grafana/ui';
 } from '@grafana/ui';
 
 
 import ValueOptions from 'app/plugins/panel/gauge/ValueOptions';
 import ValueOptions from 'app/plugins/panel/gauge/ValueOptions';
-import ValueMappings from 'app/plugins/panel/gauge/ValueMappings';
 import GaugeOptionsEditor from './GaugeOptionsEditor';
 import GaugeOptionsEditor from './GaugeOptionsEditor';
+import { GaugeOptions } from './types';
 
 
 export const defaultProps = {
 export const defaultProps = {
   options: {
   options: {
@@ -24,7 +25,7 @@ export const defaultProps = {
     decimals: 0,
     decimals: 0,
     stat: 'avg',
     stat: 'avg',
     unit: 'none',
     unit: 'none',
-    mappings: [],
+    valueMappings: [],
     thresholds: [],
     thresholds: [],
   },
   },
 };
 };
@@ -32,7 +33,17 @@ export const defaultProps = {
 export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> {
 export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> {
   static defaultProps = defaultProps;
   static defaultProps = defaultProps;
 
 
-  onThresholdsChanged = (thresholds: Threshold[]) => this.props.onChange({ ...this.props.options, thresholds });
+  onThresholdsChanged = (thresholds: Threshold[]) =>
+    this.props.onChange({
+      ...this.props.options,
+      thresholds,
+    });
+
+  onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
+    this.props.onChange({
+      ...this.props.options,
+      valueMappings,
+    });
 
 
   render() {
   render() {
     const { onChange, options } = this.props;
     const { onChange, options } = this.props;
@@ -44,7 +55,7 @@ export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<G
           <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
           <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
         </PanelOptionsGrid>
         </PanelOptionsGrid>
 
 
-        <ValueMappings onChange={onChange} options={options} />
+        <ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
       </>
       </>
     );
     );
   }
   }

+ 14 - 24
public/app/plugins/panel/gauge/ValueOptions.tsx

@@ -1,9 +1,7 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
-import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui';
-
-import { Label } from 'app/core/components/Label/Label';
-import { Select} from '@grafana/ui';
+import { FormField, FormLabel, PanelOptionsProps, PanelOptionsGroup, Select } from '@grafana/ui';
 import UnitPicker from 'app/core/components/Select/UnitPicker';
 import UnitPicker from 'app/core/components/Select/UnitPicker';
+import { GaugeOptions } from './types';
 
 
 const statOptions = [
 const statOptions = [
   { value: 'min', label: 'Min' },
   { value: 'min', label: 'Min' },
@@ -42,7 +40,7 @@ export default class ValueOptions extends PureComponent<PanelOptionsProps<GaugeO
     return (
     return (
       <PanelOptionsGroup title="Value">
       <PanelOptionsGroup title="Value">
         <div className="gf-form">
         <div className="gf-form">
-          <Label width={labelWidth}>Stat</Label>
+          <FormLabel width={labelWidth}>Stat</FormLabel>
           <Select
           <Select
             width={12}
             width={12}
             options={statOptions}
             options={statOptions}
@@ -51,27 +49,19 @@ export default class ValueOptions extends PureComponent<PanelOptionsProps<GaugeO
           />
           />
         </div>
         </div>
         <div className="gf-form">
         <div className="gf-form">
-          <Label width={labelWidth}>Unit</Label>
+          <FormLabel width={labelWidth}>Unit</FormLabel>
           <UnitPicker defaultValue={unit} onChange={this.onUnitChange} />
           <UnitPicker defaultValue={unit} onChange={this.onUnitChange} />
         </div>
         </div>
-        <div className="gf-form">
-          <Label width={labelWidth}>Decimals</Label>
-          <input
-            className="gf-form-input width-12"
-            type="number"
-            placeholder="auto"
-            value={decimals || ''}
-            onChange={this.onDecimalChange}
-          />
-        </div>
-        <div className="gf-form">
-          <Label width={labelWidth}>Prefix</Label>
-          <input className="gf-form-input width-12" type="text" value={prefix || ''} onChange={this.onPrefixChange} />
-        </div>
-        <div className="gf-form">
-          <Label width={labelWidth}>Suffix</Label>
-          <input className="gf-form-input width-12" type="text" value={suffix || ''} onChange={this.onSuffixChange} />
-        </div>
+        <FormField
+          label="Decimals"
+          labelWidth={labelWidth}
+          placeholder="auto"
+          onChange={this.onDecimalChange}
+          value={decimals || ''}
+          type="number"
+        />
+        <FormField label="Prefix" labelWidth={labelWidth} onChange={this.onPrefixChange} value={prefix || ''} />
+        <FormField label="Suffix" labelWidth={labelWidth} onChange={this.onSuffixChange} value={suffix || ''} />
       </PanelOptionsGroup>
       </PanelOptionsGroup>
     );
     );
   }
   }

+ 15 - 1
public/app/plugins/panel/gauge/types.ts

@@ -1,2 +1,16 @@
+import { Threshold, ValueMapping } from '@grafana/ui';
 
 
-
+export interface GaugeOptions {
+  baseColor: string;
+  decimals: number;
+  valueMappings: ValueMapping[];
+  maxValue: number;
+  minValue: number;
+  prefix: string;
+  showThresholdLabels: boolean;
+  showThresholdMarkers: boolean;
+  stat: string;
+  suffix: string;
+  thresholds: Threshold[];
+  unit: string;
+}

+ 1 - 1
public/app/viz/Gauge.test.tsx

@@ -12,7 +12,7 @@ const setup = (propOverrides?: object) => {
   const props: Props = {
   const props: Props = {
     baseColor: BasicGaugeColor.Green,
     baseColor: BasicGaugeColor.Green,
     maxValue: 100,
     maxValue: 100,
-    mappings: [],
+    valueMappings: [],
     minValue: 0,
     minValue: 0,
     prefix: '',
     prefix: '',
     showThresholdMarkers: true,
     showThresholdMarkers: true,

+ 10 - 21
public/app/viz/Gauge.tsx

@@ -1,6 +1,6 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import $ from 'jquery';
 import $ from 'jquery';
-import { BasicGaugeColor, Threshold, TimeSeriesVMs, RangeMap, ValueMap, MappingType } from '@grafana/ui';
+import { BasicGaugeColor, Threshold, TimeSeriesVMs, MappingType, ValueMapping } from '@grafana/ui';
 
 
 import config from '../core/config';
 import config from '../core/config';
 import kbn from '../core/utils/kbn';
 import kbn from '../core/utils/kbn';
@@ -9,7 +9,7 @@ export interface Props {
   baseColor: string;
   baseColor: string;
   decimals: number;
   decimals: number;
   height: number;
   height: number;
-  mappings: Array<RangeMap | ValueMap>;
+  valueMappings: ValueMapping[];
   maxValue: number;
   maxValue: number;
   minValue: number;
   minValue: number;
   prefix: string;
   prefix: string;
@@ -29,7 +29,7 @@ export class Gauge extends PureComponent<Props> {
   static defaultProps = {
   static defaultProps = {
     baseColor: BasicGaugeColor.Green,
     baseColor: BasicGaugeColor.Green,
     maxValue: 100,
     maxValue: 100,
-    mappings: [],
+    valueMappings: [],
     minValue: 0,
     minValue: 0,
     prefix: '',
     prefix: '',
     showThresholdMarkers: true,
     showThresholdMarkers: true,
@@ -64,20 +64,17 @@ export class Gauge extends PureComponent<Props> {
       }
       }
     })[0];
     })[0];
 
 
-    return {
-      rangeMap,
-      valueMap,
-    };
+    return { rangeMap, valueMap };
   }
   }
 
 
   formatValue(value) {
   formatValue(value) {
-    const { decimals, mappings, prefix, suffix, unit } = this.props;
+    const { decimals, valueMappings, prefix, suffix, unit } = this.props;
 
 
     const formatFunc = kbn.valueFormats[unit];
     const formatFunc = kbn.valueFormats[unit];
     const formattedValue = formatFunc(value, decimals);
     const formattedValue = formatFunc(value, decimals);
 
 
-    if (mappings.length > 0) {
-      const { rangeMap, valueMap } = this.formatWithMappings(mappings, formattedValue);
+    if (valueMappings.length > 0) {
+      const { rangeMap, valueMap } = this.formatWithMappings(valueMappings, formattedValue);
 
 
       if (valueMap) {
       if (valueMap) {
         return `${prefix} ${valueMap} ${suffix}`;
         return `${prefix} ${valueMap} ${suffix}`;
@@ -148,10 +145,7 @@ export class Gauge extends PureComponent<Props> {
           color: index === 0 ? threshold.color : thresholds[index].color,
           color: index === 0 ? threshold.color : thresholds[index].color,
         };
         };
       }),
       }),
-      {
-        value: maxValue,
-        color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor,
-      },
+      { value: maxValue, color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor },
     ];
     ];
 
 
     const options = {
     const options = {
@@ -184,19 +178,14 @@ export class Gauge extends PureComponent<Props> {
             formatter: () => {
             formatter: () => {
               return this.formatValue(value);
               return this.formatValue(value);
             },
             },
-            font: {
-              size: fontSize,
-              family: '"Helvetica Neue", Helvetica, Arial, sans-serif',
-            },
+            font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
           },
           },
           show: true,
           show: true,
         },
         },
       },
       },
     };
     };
 
 
-    const plotSeries = {
-      data: [[0, value]],
-    };
+    const plotSeries = { data: [[0, value]] };
 
 
     try {
     try {
       $.plot(this.canvasElement, [plotSeries], options);
       $.plot(this.canvasElement, [plotSeries], options);

+ 1 - 2
public/sass/_grafana.scss

@@ -1,4 +1,4 @@
- // DEPENDENCIES
+// DEPENDENCIES
 @import '../../node_modules/react-table/react-table.css';
 @import '../../node_modules/react-table/react-table.css';
 
 
 // VENDOR
 // VENDOR
@@ -97,7 +97,6 @@
 @import 'components/add_data_source.scss';
 @import 'components/add_data_source.scss';
 @import 'components/page_loader';
 @import 'components/page_loader';
 @import 'components/toggle_button_group';
 @import 'components/toggle_button_group';
-@import 'components/value-mappings';
 @import 'components/popover-box';
 @import 'components/popover-box';
 
 
 // LOAD @grafana/ui components
 // LOAD @grafana/ui components

+ 8 - 0
public/sass/components/_footer.scss

@@ -38,6 +38,14 @@
   }
   }
 }
 }
 
 
+.is-react .footer {
+  display: none;
+}
+
+.is-react .custom-scrollbars .footer {
+  display: block;
+}
+
 // Keeping footer inside the graphic on Login screen
 // Keeping footer inside the graphic on Login screen
 .login-page {
 .login-page {
   .footer {
   .footer {

+ 16 - 1
public/sass/layout/_page.scss

@@ -20,7 +20,23 @@
   }
   }
 }
 }
 
 
+.page-scrollbar-wrapper {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  width: 100%;
+}
+
+.page-scrollbar-content {
+  display: flex;
+  min-height: 100%;
+  flex-direction: column;
+  width: 100%;
+}
+
 .page-container {
 .page-container {
+  flex-grow: 1;
+  width: 100%;
   margin-left: auto;
   margin-left: auto;
   margin-right: auto;
   margin-right: auto;
   padding-left: $spacer*2;
   padding-left: $spacer*2;
@@ -78,7 +94,6 @@
 
 
 .page-body {
 .page-body {
   padding-top: $spacer*2;
   padding-top: $spacer*2;
-  min-height: 500px;
 }
 }
 
 
 .page-heading {
 .page-heading {

+ 3 - 3
scripts/build/ci-deploy/Dockerfile

@@ -8,8 +8,6 @@ RUN git clone https://github.com/aptly-dev/aptly $GOPATH/src/github.com/aptly-de
 
 
 FROM circleci/python:2.7-stretch
 FROM circleci/python:2.7-stretch
 
 
-ENV PATH=$PATH:/opt/google-cloud-sdk/bin
-
 USER root
 USER root
 
 
 RUN pip install awscli && \
 RUN pip install awscli && \
@@ -18,7 +16,9 @@ RUN pip install awscli && \
     apt update && \
     apt update && \
     apt install -y createrepo expect && \
     apt install -y createrepo expect && \
     apt-get autoremove -y && \
     apt-get autoremove -y && \
-    rm -rf /var/lib/apt/lists/*
+    rm -rf /var/lib/apt/lists/* && \
+    ln -s /opt/google-cloud-sdk/bin/gsutil /usr/bin/gsutil && \
+    ln -s /opt/google-cloud-sdk/bin/gcloud /usr/bin/gcloud
 
 
 COPY --from=0 /go/bin/aptly /usr/local/bin/aptly
 COPY --from=0 /go/bin/aptly /usr/local/bin/aptly
 
 

+ 1 - 1
scripts/build/ci-deploy/build-deploy.sh

@@ -1,6 +1,6 @@
 #!/bin/bash
 #!/bin/bash
 
 
-_version="1.1.0"
+_version="1.2.0"
 _tag="grafana/grafana-ci-deploy:${_version}"
 _tag="grafana/grafana-ci-deploy:${_version}"
 
 
 docker build -t $_tag .
 docker build -t $_tag .