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

Merge pull request #14875 from grafana/page-layout-component

CustomScrollbar on all-React-pages
Torkel Ödegaard 7 лет назад
Родитель
Сommit
c913263b23
51 измененных файлов с 954 добавлено и 638 удалено
  1. 2 1
      packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
  2. 2 2
      packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap
  3. 2 2
      public/app/core/components/Animations/FadeIn.tsx
  4. 50 0
      public/app/core/components/Footer/Footer.tsx
  5. 2 2
      public/app/core/components/LayoutSelector/LayoutSelector.tsx
  6. 75 0
      public/app/core/components/Page/Page.tsx
  7. 26 0
      public/app/core/components/Page/PageContents.tsx
  8. 3 3
      public/app/core/components/PageHeader/PageHeader.tsx
  9. 3 3
      public/app/core/components/PageLoader/PageLoader.tsx
  10. 2 2
      public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx
  11. 2 2
      public/app/core/components/sidemenu/DropDownChild.tsx
  12. 2 2
      public/app/core/components/sidemenu/SideMenuDropDown.tsx
  13. 2 2
      public/app/core/components/sidemenu/SignIn.tsx
  14. 2 2
      public/app/core/components/sidemenu/TopSection.tsx
  15. 2 2
      public/app/core/components/sidemenu/TopSectionItem.tsx
  16. 2 0
      public/app/core/config.ts
  17. 4 0
      public/app/core/selectors/navModel.ts
  18. 8 1
      public/app/features/api-keys/ApiKeysPage.test.tsx
  19. 12 14
      public/app/features/api-keys/ApiKeysPage.tsx
  20. 130 110
      public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap
  21. 2 2
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx
  22. 2 2
      public/app/features/dashboard/panel_editor/DataSourceOption.tsx
  23. 2 2
      public/app/features/datasources/DashboardsTable.tsx
  24. 8 1
      public/app/features/datasources/DataSourcesListPage.test.tsx
  25. 27 27
      public/app/features/datasources/DataSourcesListPage.tsx
  26. 31 19
      public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap
  27. 2 2
      public/app/features/datasources/settings/BasicSettings.tsx
  28. 2 2
      public/app/features/datasources/settings/ButtonRow.tsx
  29. 2 2
      public/app/features/explore/Error.tsx
  30. 8 1
      public/app/features/org/OrgDetailsPage.test.tsx
  31. 17 18
      public/app/features/org/OrgDetailsPage.tsx
  32. 2 2
      public/app/features/org/OrgProfile.tsx
  33. 47 27
      public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap
  34. 2 2
      public/app/features/plugins/PluginList.tsx
  35. 2 2
      public/app/features/plugins/PluginListItem.tsx
  36. 8 1
      public/app/features/plugins/PluginListPage.test.tsx
  37. 19 21
      public/app/features/plugins/PluginListPage.tsx
  38. 32 19
      public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap
  39. 8 1
      public/app/features/teams/TeamList.test.tsx
  40. 7 7
      public/app/features/teams/TeamList.tsx
  41. 308 288
      public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
  42. 8 1
      public/app/features/users/UsersListPage.test.tsx
  43. 9 9
      public/app/features/users/UsersListPage.tsx
  44. 2 2
      public/app/features/users/UsersTable.tsx
  45. 32 19
      public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap
  46. 2 2
      public/app/plugins/datasource/stackdriver/components/AlignmentPeriods.tsx
  47. 2 2
      public/app/plugins/datasource/stackdriver/components/Alignments.tsx
  48. 2 2
      public/app/plugins/datasource/stackdriver/components/AnnotationsHelp.tsx
  49. 2 2
      public/app/plugins/datasource/stackdriver/components/SimpleSelect.tsx
  50. 8 0
      public/sass/components/_footer.scss
  51. 16 1
      public/sass/layout/_page.scss

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

@@ -8,6 +8,7 @@ interface Props {
   autoHideDuration?: number;
   autoHideDuration?: number;
   autoMaxHeight?: string;
   autoMaxHeight?: string;
   hideTracksWhenNotNeeded?: boolean;
   hideTracksWhenNotNeeded?: boolean;
+  autoHeightMin?: number | string;
 }
 }
 
 
 /**
 /**
@@ -21,6 +22,7 @@ export class CustomScrollbar extends PureComponent<Props> {
     autoHideDuration: 200,
     autoHideDuration: 200,
     autoMaxHeight: '100%',
     autoMaxHeight: '100%',
     hideTracksWhenNotNeeded: false,
     hideTracksWhenNotNeeded: false,
+    autoHeightMin: '0'
   };
   };
 
 
   render() {
   render() {
@@ -32,7 +34,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,

+ 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;

+ 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">

+ 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/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>
 `;
 `;

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

@@ -1,4 +1,4 @@
-import React, { SFC } from 'react';
+import React, { FC } from 'react';
 import { FormLabel } from '@grafana/ui';
 import { FormLabel } from '@grafana/ui';
 import { Switch } from '../../../core/components/Switch/Switch';
 import { Switch } from '../../../core/components/Switch/Switch';
 
 
@@ -9,7 +9,7 @@ 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">

+ 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">

+ 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>
     );
     );
   }
   }
 }
 }

+ 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">

+ 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 {