Browse Source

Merge pull request #15103 from grafana/14762-page-component-everywhere

Page component on all React pages pt2
Torkel Ödegaard 7 years ago
parent
commit
57fe7d5050
29 changed files with 1023 additions and 782 deletions
  1. 1 1
      public/app/core/components/PageHeader/PageHeader.tsx
  2. 1 1
      public/app/core/components/PageLoader/PageLoader.tsx
  3. 11 9
      public/app/features/admin/ServerStats.tsx
  4. 231 91
      public/app/features/admin/__snapshots__/ServerStats.test.tsx.snap
  5. 3 2
      public/app/features/alerting/AlertRuleList.test.tsx
  6. 13 12
      public/app/features/alerting/AlertRuleList.tsx
  7. 14 16
      public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap
  8. 14 4
      public/app/features/alerting/state/actions.ts
  9. 1 1
      public/app/features/alerting/state/reducers.test.ts
  10. 7 3
      public/app/features/alerting/state/reducers.ts
  11. 1 0
      public/app/features/datasources/DataSourceDashboards.test.tsx
  12. 15 14
      public/app/features/datasources/DataSourceDashboards.tsx
  13. 41 39
      public/app/features/datasources/NewDataSourcePage.tsx
  14. 7 8
      public/app/features/datasources/__snapshots__/DataSourceDashboards.test.tsx.snap
  15. 17 17
      public/app/features/datasources/settings/DataSourceSettingsPage.tsx
  16. 382 371
      public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap
  17. 12 1
      public/app/features/datasources/state/actions.ts
  18. 5 1
      public/app/features/datasources/state/reducers.ts
  19. 7 8
      public/app/features/folders/FolderPermissions.tsx
  20. 45 32
      public/app/features/folders/FolderSettingsPage.tsx
  21. 112 106
      public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap
  22. 16 3
      public/app/features/plugins/state/actions.ts
  23. 5 1
      public/app/features/plugins/state/reducers.ts
  24. 13 8
      public/app/features/teams/TeamPages.tsx
  25. 44 33
      public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap
  26. 1 0
      public/app/types/alerting.ts
  27. 1 0
      public/app/types/datasources.ts
  28. 1 0
      public/app/types/plugins.ts
  29. 2 0
      public/app/types/store.ts

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

@@ -80,7 +80,7 @@ const Navigation = ({ main }: { main: NavModelItem }) => {
 };
 
 export default class PageHeader extends React.Component<Props, any> {
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
   }
 

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

@@ -4,7 +4,7 @@ interface Props {
   pageName?: string;
 }
 
-const PageLoader: FC<Props> = ({ pageName }) => {
+const PageLoader: FC<Props> = ({ pageName = '' }) => {
   const loadingText = `Loading ${pageName}...`;
   return (
     <div className="page-loader-wrapper">

+ 11 - 9
public/app/features/admin/ServerStats.tsx

@@ -4,7 +4,7 @@ import { connect } from 'react-redux';
 import { NavModel, StoreState } from 'app/types';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getServerStats, ServerStat } from './state/apis';
-import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import Page from 'app/core/components/Page/Page';
 
 interface Props {
   navModel: NavModel;
@@ -13,21 +13,24 @@ interface Props {
 
 interface State {
   stats: ServerStat[];
+  isLoading: boolean;
 }
 
 export class ServerStats extends PureComponent<Props, State> {
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
 
     this.state = {
       stats: [],
+      isLoading: false
     };
   }
 
   async componentDidMount() {
     try {
+      this.setState({ isLoading: true });
       const stats = await this.props.getServerStats();
-      this.setState({ stats });
+      this.setState({ stats, isLoading: false });
     } catch (error) {
       console.error(error);
     }
@@ -35,12 +38,11 @@ export class ServerStats extends PureComponent<Props, State> {
 
   render() {
     const { navModel } = this.props;
-    const { stats } = this.state;
+    const { stats, isLoading } = this.state;
 
     return (
-      <div>
-        <PageHeader model={navModel} />
-        <div className="page-container page-body">
+      <Page navModel={navModel}>
+        <Page.Contents isLoading={isLoading}>
           <table className="filter-table form-inline">
             <thead>
               <tr>
@@ -50,8 +52,8 @@ export class ServerStats extends PureComponent<Props, State> {
             </thead>
             <tbody>{stats.map(StatItem)}</tbody>
           </table>
-        </div>
-      </div>
+        </Page.Contents>
+      </Page>
     );
   }
 }

+ 231 - 91
public/app/features/admin/__snapshots__/ServerStats.test.tsx.snap

@@ -1,118 +1,258 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`ServerStats Should render table with stats 1`] = `
-<div>
+<div
+  className="page-scrollbar-wrapper"
+>
   <div
-    className="page-header-canvas"
+    className="custom-scrollbars"
+    style={
+      Object {
+        "height": "auto",
+        "maxHeight": "100%",
+        "minHeight": "100%",
+        "overflow": "hidden",
+        "position": "relative",
+        "width": "100%",
+      }
+    }
   >
     <div
-      className="page-container"
+      className="view"
+      style={
+        Object {
+          "WebkitOverflowScrolling": "touch",
+          "bottom": undefined,
+          "left": undefined,
+          "marginBottom": 0,
+          "marginRight": 0,
+          "maxHeight": "calc(100% + 0px)",
+          "minHeight": "calc(100% + 0px)",
+          "overflow": "scroll",
+          "position": "relative",
+          "right": undefined,
+          "top": undefined,
+        }
+      }
     >
       <div
-        className="page-header"
+        className="page-scrollbar-content"
       >
         <div
-          className="page-header__inner"
+          className="page-header-canvas"
         >
-          <span
-            className="page-header__logo"
-          >
-            <i
-              className="page-header__icon fa fa-fw fa-warning"
-            />
-          </span>
           <div
-            className="page-header__info-block"
+            className="page-container"
           >
-            <h1
-              className="page-header__title"
-            >
-              Admin
-            </h1>
             <div
-              className="page-header__sub-title"
+              className="page-header"
             >
-              subTitle
+              <div
+                className="page-header__inner"
+              >
+                <span
+                  className="page-header__logo"
+                >
+                  <i
+                    className="page-header__icon fa fa-fw fa-warning"
+                  />
+                </span>
+                <div
+                  className="page-header__info-block"
+                >
+                  <h1
+                    className="page-header__title"
+                  >
+                    Admin
+                  </h1>
+                  <div
+                    className="page-header__sub-title"
+                  >
+                    subTitle
+                  </div>
+                </div>
+              </div>
+              <nav>
+                <div
+                  className="gf-form-select-wrapper width-20 page-header__select-nav"
+                >
+                  <label
+                    className="gf-form-select-icon icon"
+                    htmlFor="page-header-select-nav"
+                  />
+                  <select
+                    className="gf-select-nav gf-form-input"
+                    id="page-header-select-nav"
+                    onChange={[Function]}
+                    value="Admin"
+                  >
+                    <option
+                      value="Admin"
+                    >
+                      Admin
+                    </option>
+                  </select>
+                </div>
+                <ul
+                  className="gf-tabs page-header__tabs"
+                >
+                  <li
+                    className="gf-tabs-item"
+                  >
+                    <a
+                      className="gf-tabs-link active"
+                      href="Admin"
+                    >
+                      <i
+                        className="icon"
+                      />
+                      Admin
+                    </a>
+                  </li>
+                </ul>
+              </nav>
             </div>
           </div>
         </div>
-        <nav>
+        <div
+          className="page-container page-body"
+        >
+          <table
+            className="filter-table form-inline"
+          >
+            <thead>
+              <tr>
+                <th>
+                  Name
+                </th>
+                <th>
+                  Value
+                </th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr>
+                <td>
+                  Total dashboards
+                </td>
+                <td>
+                  10
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  Total Users
+                </td>
+                <td>
+                  1
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+        <footer
+          className="footer"
+        >
           <div
-            className="gf-form-select-wrapper width-20 page-header__select-nav"
+            className="text-center"
           >
-            <label
-              className="gf-form-select-icon icon"
-              htmlFor="page-header-select-nav"
-            />
-            <select
-              className="gf-select-nav gf-form-input"
-              id="page-header-select-nav"
-              onChange={[Function]}
-              value="Admin"
-            >
-              <option
-                value="Admin"
-              >
-                Admin
-              </option>
-            </select>
+            <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"
+                >
+                  Grafana
+                </a>
+                 
+                <span>
+                  v
+                  v1.0
+                   (commit: 
+                  1
+                  )
+                </span>
+              </li>
+            </ul>
           </div>
-          <ul
-            className="gf-tabs page-header__tabs"
-          >
-            <li
-              className="gf-tabs-item"
-            >
-              <a
-                className="gf-tabs-link active"
-                href="Admin"
-              >
-                <i
-                  className="icon"
-                />
-                Admin
-              </a>
-            </li>
-          </ul>
-        </nav>
+        </footer>
       </div>
     </div>
-  </div>
-  <div
-    className="page-container page-body"
-  >
-    <table
-      className="filter-table form-inline"
+    <div
+      className="track-horizontal"
+      style={
+        Object {
+          "display": "none",
+          "height": 6,
+          "position": "absolute",
+        }
+      }
     >
-      <thead>
-        <tr>
-          <th>
-            Name
-          </th>
-          <th>
-            Value
-          </th>
-        </tr>
-      </thead>
-      <tbody>
-        <tr>
-          <td>
-            Total dashboards
-          </td>
-          <td>
-            10
-          </td>
-        </tr>
-        <tr>
-          <td>
-            Total Users
-          </td>
-          <td>
-            1
-          </td>
-        </tr>
-      </tbody>
-    </table>
+      <div
+        className="thumb-horizontal"
+        style={
+          Object {
+            "display": "block",
+            "height": "100%",
+            "position": "relative",
+          }
+        }
+      />
+    </div>
+    <div
+      className="track-vertical"
+      style={
+        Object {
+          "display": "none",
+          "position": "absolute",
+          "width": 6,
+        }
+      }
+    >
+      <div
+        className="thumb-vertical"
+        style={
+          Object {
+            "display": "block",
+            "position": "relative",
+            "width": "100%",
+          }
+        }
+      />
+    </div>
   </div>
 </div>
 `;

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

@@ -18,6 +18,7 @@ const setup = (propOverrides?: object) => {
     togglePauseAlertRule: jest.fn(),
     stateFilter: '',
     search: '',
+    isLoading: false
   };
 
   Object.assign(props, propOverrides);
@@ -121,7 +122,7 @@ describe('Functions', () => {
   describe('State filter changed', () => {
     it('should update location', () => {
       const { instance } = setup();
-      const mockEvent = { target: { value: 'alerting' } };
+      const mockEvent = { target: { value: 'alerting' } } as React.ChangeEvent<HTMLSelectElement>;
 
       instance.onStateFilterChanged(mockEvent);
 
@@ -146,7 +147,7 @@ describe('Functions', () => {
   describe('Search query change', () => {
     it('should set search query', () => {
       const { instance } = setup();
-      const mockEvent = { target: { value: 'dashboard' } };
+      const mockEvent = { target: { value: 'dashboard' } } as React.ChangeEvent<HTMLInputElement>;
 
       instance.onSearchQueryChange(mockEvent);
 

+ 13 - 12
public/app/features/alerting/AlertRuleList.tsx

@@ -1,7 +1,7 @@
 import React, { PureComponent } from 'react';
 import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
-import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import Page from 'app/core/components/Page/Page';
 import AlertRuleItem from './AlertRuleItem';
 import appEvents from 'app/core/app_events';
 import { updateLocation } from 'app/core/actions';
@@ -19,6 +19,7 @@ export interface Props {
   togglePauseAlertRule: typeof togglePauseAlertRule;
   stateFilter: string;
   search: string;
+  isLoading: boolean;
 }
 
 export class AlertRuleList extends PureComponent<Props, any> {
@@ -54,9 +55,9 @@ export class AlertRuleList extends PureComponent<Props, any> {
     return 'all';
   }
 
-  onStateFilterChanged = event => {
+  onStateFilterChanged = (evt: React.ChangeEvent<HTMLSelectElement>) => {
     this.props.updateLocation({
-      query: { state: event.target.value },
+      query: { state: evt.target.value },
     });
   };
 
@@ -68,8 +69,8 @@ export class AlertRuleList extends PureComponent<Props, any> {
     });
   };
 
-  onSearchQueryChange = event => {
-    const { value } = event.target;
+  onSearchQueryChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
+    const { value } = evt.target;
     this.props.setSearchQuery(value);
   };
 
@@ -77,7 +78,7 @@ export class AlertRuleList extends PureComponent<Props, any> {
     this.props.togglePauseAlertRule(rule.id, { paused: rule.state !== 'paused' });
   };
 
-  alertStateFilterOption = ({ text, value }) => {
+  alertStateFilterOption = ({ text, value }: { text: string; value: string; }) => {
     return (
       <option key={value} value={value}>
         {text}
@@ -86,12 +87,11 @@ export class AlertRuleList extends PureComponent<Props, any> {
   };
 
   render() {
-    const { navModel, alertRules, search } = this.props;
+    const { navModel, alertRules, search, isLoading } = this.props;
 
     return (
-      <div>
-        <PageHeader model={navModel} />
-        <div className="page-container page-body">
+      <Page navModel={navModel}>
+        <Page.Contents isLoading={isLoading}>
           <div className="page-action-bar">
             <div className="gf-form gf-form--grow">
               <label className="gf-form--has-input-icon gf-form--grow">
@@ -131,8 +131,8 @@ export class AlertRuleList extends PureComponent<Props, any> {
               ))}
             </ol>
           </section>
-        </div>
-      </div>
+        </Page.Contents>
+      </Page>
     );
   }
 }
@@ -142,6 +142,7 @@ const mapStateToProps = (state: StoreState) => ({
   alertRules: getAlertRuleItems(state.alertRules),
   stateFilter: state.location.query.state,
   search: getSearchQuery(state.alertRules),
+  isLoading: state.alertRules.isLoading
 });
 
 const mapDispatchToProps = {

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

@@ -1,12 +1,11 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Render should render alert rules 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={Object {}}
+>
+  <PageContents
+    isLoading={false}
   >
     <div
       className="page-action-bar"
@@ -151,17 +150,16 @@ exports[`Render should render alert rules 1`] = `
         />
       </ol>
     </section>
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;
 
 exports[`Render should render component 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={Object {}}
+>
+  <PageContents
+    isLoading={false}
   >
     <div
       className="page-action-bar"
@@ -263,6 +261,6 @@ exports[`Render should render component 1`] = `
         className="alert-rule-list"
       />
     </section>
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;

+ 14 - 4
public/app/features/alerting/state/actions.ts

@@ -4,11 +4,16 @@ import { ThunkAction } from 'redux-thunk';
 
 export enum ActionTypes {
   LoadAlertRules = 'LOAD_ALERT_RULES',
+  LoadedAlertRules = 'LOADED_ALERT_RULES',
   SetSearchQuery = 'SET_ALERT_SEARCH_QUERY',
 }
 
 export interface LoadAlertRulesAction {
   type: ActionTypes.LoadAlertRules;
+}
+
+export interface LoadedAlertRulesAction {
+  type: ActionTypes.LoadedAlertRules;
   payload: AlertRuleDTO[];
 }
 
@@ -17,8 +22,12 @@ export interface SetSearchQueryAction {
   payload: string;
 }
 
-export const loadAlertRules = (rules: AlertRuleDTO[]): LoadAlertRulesAction => ({
+export const loadAlertRules = (): LoadAlertRulesAction => ({
   type: ActionTypes.LoadAlertRules,
+});
+
+export const loadedAlertRules = (rules: AlertRuleDTO[]): LoadedAlertRulesAction => ({
+  type: ActionTypes.LoadedAlertRules,
   payload: rules,
 });
 
@@ -27,14 +36,15 @@ export const setSearchQuery = (query: string): SetSearchQueryAction => ({
   payload: query,
 });
 
-export type Action = LoadAlertRulesAction | SetSearchQueryAction;
+export type Action = LoadAlertRulesAction | LoadedAlertRulesAction | SetSearchQueryAction;
 
 type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
 
 export function getAlertRulesAsync(options: { state: string }): ThunkResult<void> {
   return async dispatch => {
-    const rules = await getBackendSrv().get('/api/alerts', options);
-    dispatch(loadAlertRules(rules));
+    dispatch(loadAlertRules());
+    const rules: AlertRuleDTO[] = await getBackendSrv().get('/api/alerts', options);
+    dispatch(loadedAlertRules(rules));
   };
 }
 

+ 1 - 1
public/app/features/alerting/state/reducers.test.ts

@@ -80,7 +80,7 @@ describe('Alert rules', () => {
 
   it('should set alert rules', () => {
     const action: Action = {
-      type: ActionTypes.LoadAlertRules,
+      type: ActionTypes.LoadedAlertRules,
       payload: payload,
     };
 

+ 7 - 3
public/app/features/alerting/state/reducers.ts

@@ -3,7 +3,7 @@ import { AlertRuleDTO, AlertRule, AlertRulesState } from 'app/types';
 import { Action, ActionTypes } from './actions';
 import alertDef from './alertDef';
 
-export const initialState: AlertRulesState = { items: [], searchQuery: '' };
+export const initialState: AlertRulesState = { items: [], searchQuery: '', isLoading: false };
 
 function convertToAlertRule(rule, state): AlertRule {
   const stateModel = alertDef.getStateDisplayModel(state);
@@ -29,17 +29,21 @@ function convertToAlertRule(rule, state): AlertRule {
 export const alertRulesReducer = (state = initialState, action: Action): AlertRulesState => {
   switch (action.type) {
     case ActionTypes.LoadAlertRules: {
+      return { ...state, isLoading: true };
+    }
+
+    case ActionTypes.LoadedAlertRules: {
       const alertRules: AlertRuleDTO[] = action.payload;
 
       const alertRulesViewModel: AlertRule[] = alertRules.map(rule => {
         return convertToAlertRule(rule, rule.state);
       });
 
-      return { items: alertRulesViewModel, searchQuery: state.searchQuery };
+      return { ...state, items: alertRulesViewModel, isLoading: false };
     }
 
     case ActionTypes.SetSearchQuery:
-      return { items: state.items, searchQuery: action.payload };
+      return { ...state, searchQuery: action.payload };
   }
 
   return state;

+ 1 - 0
public/app/features/datasources/DataSourceDashboards.test.tsx

@@ -14,6 +14,7 @@ const setup = (propOverrides?: object) => {
     loadDataSource: jest.fn(),
     loadPluginDashboards: jest.fn(),
     removeDashboard: jest.fn(),
+    isLoading: false
   };
 
   Object.assign(props, propOverrides);

+ 15 - 14
public/app/features/datasources/DataSourceDashboards.tsx

@@ -4,7 +4,7 @@ import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
 
 // Components
-import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import Page from 'app/core/components/Page/Page';
 import DashboardTable from './DashboardsTable';
 
 // Actions & Selectors
@@ -16,7 +16,7 @@ import { importDashboard, removeDashboard } from '../dashboard/state/actions';
 import { getDataSource } from './state/selectors';
 
 // Types
-import { NavModel, PluginDashboard } from 'app/types';
+import { NavModel, PluginDashboard, StoreState } from 'app/types';
 import { DataSourceSettings } from '@grafana/ui/src/types';
 
 export interface Props {
@@ -28,6 +28,7 @@ export interface Props {
   loadDataSource: typeof loadDataSource;
   loadPluginDashboards: typeof loadPluginDashboards;
   removeDashboard: typeof removeDashboard;
+  isLoading: boolean;
 }
 
 export class DataSourceDashboards extends PureComponent<Props> {
@@ -64,30 +65,30 @@ export class DataSourceDashboards extends PureComponent<Props> {
   };
 
   render() {
-    const { dashboards, navModel } = this.props;
+    const { dashboards, navModel, isLoading } = this.props;
     return (
-      <div>
-        <PageHeader model={navModel} />
-        <div className="page-container page-body">
+      <Page navModel={navModel}>
+        <Page.Contents isLoading={isLoading}>
           <DashboardTable
-            dashboards={dashboards}
-            onImport={(dashboard, overwrite) => this.onImport(dashboard, overwrite)}
-            onRemove={dashboard => this.onRemove(dashboard)}
-          />
-        </div>
-      </div>
+              dashboards={dashboards}
+              onImport={(dashboard, overwrite) => this.onImport(dashboard, overwrite)}
+              onRemove={dashboard => this.onRemove(dashboard)}
+            />
+
+        </Page.Contents>
+      </Page>
     );
   }
 }
 
-function mapStateToProps(state) {
+function mapStateToProps(state: StoreState) {
   const pageId = getRouteParamsId(state.location);
-
   return {
     navModel: getNavModel(state.navIndex, `datasource-dashboards-${pageId}`),
     pageId: pageId,
     dashboards: state.plugins.dashboards,
     dataSource: getDataSource(state.dataSources, pageId),
+    isLoading: state.plugins.isLoadingPluginDashboards
   };
 }
 

+ 41 - 39
public/app/features/datasources/NewDataSourcePage.tsx

@@ -1,8 +1,8 @@
 import React, { PureComponent } from 'react';
 import { connect } from 'react-redux';
 import { hot } from 'react-hot-loader';
-import PageHeader from 'app/core/components/PageHeader/PageHeader';
-import { NavModel, Plugin } from 'app/types';
+import Page from 'app/core/components/Page/Page';
+import { NavModel, Plugin, StoreState } from 'app/types';
 import { addDataSource, loadDataSourceTypes, setDataSourceTypeSearchQuery } from './state/actions';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getDataSourceTypes } from './state/selectors';
@@ -10,6 +10,7 @@ import { getDataSourceTypes } from './state/selectors';
 export interface Props {
   navModel: NavModel;
   dataSourceTypes: Plugin[];
+  isLoading: boolean;
   addDataSource: typeof addDataSource;
   loadDataSourceTypes: typeof loadDataSourceTypes;
   dataSourceTypeSearchQuery: string;
@@ -21,58 +22,59 @@ class NewDataSourcePage extends PureComponent<Props> {
     this.props.loadDataSourceTypes();
   }
 
-  onDataSourceTypeClicked = type => {
-    this.props.addDataSource(type);
+  onDataSourceTypeClicked = (plugin: Plugin) => {
+    this.props.addDataSource(plugin);
   };
 
-  onSearchQueryChange = event => {
+  onSearchQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
     this.props.setDataSourceTypeSearchQuery(event.target.value);
   };
 
   render() {
-    const { navModel, dataSourceTypes, dataSourceTypeSearchQuery } = this.props;
-
+    const { navModel, dataSourceTypes, dataSourceTypeSearchQuery, isLoading } = this.props;
     return (
-      <div>
-        <PageHeader model={navModel} />
-        <div className="page-container page-body">
-          <h2 className="add-data-source-header">Choose data source type</h2>
-          <div className="add-data-source-search">
-            <label className="gf-form--has-input-icon">
-              <input
-                type="text"
-                className="gf-form-input width-20"
-                value={dataSourceTypeSearchQuery}
-                onChange={this.onSearchQueryChange}
-                placeholder="Filter by name or type"
-              />
-              <i className="gf-form-input-icon fa fa-search" />
-            </label>
-          </div>
-          <div className="add-data-source-grid">
-            {dataSourceTypes.map((type, index) => {
-              return (
-                <div
-                  onClick={() => this.onDataSourceTypeClicked(type)}
-                  className="add-data-source-grid-item"
-                  key={`${type.id}-${index}`}
-                >
-                  <img className="add-data-source-grid-item-logo" src={type.info.logos.small} />
-                  <span className="add-data-source-grid-item-text">{type.name}</span>
-                </div>
-              );
-            })}
+      <Page navModel={navModel}>
+        <Page.Contents isLoading={isLoading}>
+          <div className="page-container page-body">
+            <h2 className="add-data-source-header">Choose data source type</h2>
+            <div className="add-data-source-search">
+              <label className="gf-form--has-input-icon">
+                <input
+                  type="text"
+                  className="gf-form-input width-20"
+                  value={dataSourceTypeSearchQuery}
+                  onChange={this.onSearchQueryChange}
+                  placeholder="Filter by name or type"
+                />
+                <i className="gf-form-input-icon fa fa-search" />
+              </label>
+            </div>
+            <div className="add-data-source-grid">
+              {dataSourceTypes.map((plugin, index) => {
+                return (
+                  <div
+                    onClick={() => this.onDataSourceTypeClicked(plugin)}
+                    className="add-data-source-grid-item"
+                    key={`${plugin.id}-${index}`}
+                  >
+                    <img className="add-data-source-grid-item-logo" src={plugin.info.logos.small} />
+                    <span className="add-data-source-grid-item-text">{plugin.name}</span>
+                  </div>
+                );
+              })}
+            </div>
           </div>
-        </div>
-      </div>
+        </Page.Contents>
+      </Page>
     );
   }
 }
 
-function mapStateToProps(state) {
+function mapStateToProps(state: StoreState) {
   return {
     navModel: getNavModel(state.navIndex, 'datasources'),
     dataSourceTypes: getDataSourceTypes(state.dataSources),
+    isLoading: state.dataSources.isLoadingDataSources
   };
 }
 

+ 7 - 8
public/app/features/datasources/__snapshots__/DataSourceDashboards.test.tsx.snap

@@ -1,18 +1,17 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Render should render component 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={Object {}}
+>
+  <PageContents
+    isLoading={false}
   >
     <DashboardsTable
       dashboards={Array []}
       onImport={[Function]}
       onRemove={[Function]}
     />
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;

+ 17 - 17
public/app/features/datasources/settings/DataSourceSettingsPage.tsx

@@ -4,8 +4,7 @@ import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
 
 // Components
-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 PluginSettings from './PluginSettings';
 import BasicSettings from './BasicSettings';
 import ButtonRow from './ButtonRow';
@@ -22,7 +21,7 @@ import { getNavModel } from 'app/core/selectors/navModel';
 import { getRouteParamsId } from 'app/core/selectors/location';
 
 // Types
-import { NavModel, Plugin } from 'app/types/';
+import { NavModel, Plugin, StoreState } from 'app/types/';
 import { DataSourceSettings } from '@grafana/ui/src/types/';
 import { getDataSourceLoadingNav } from '../state/navModel';
 
@@ -51,7 +50,7 @@ enum DataSourceStates {
 }
 
 export class DataSourceSettingsPage extends PureComponent<Props, State> {
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
 
     this.state = {
@@ -65,8 +64,8 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
     await loadDataSource(pageId);
   }
 
-  onSubmit = async event => {
-    event.preventDefault();
+  onSubmit = async (evt: React.FormEvent<HTMLFormElement>) => {
+    evt.preventDefault();
 
     await this.props.updateDataSource({ ...this.state.dataSource, name: this.props.dataSource.name });
 
@@ -89,7 +88,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
     this.props.deleteDataSource();
   };
 
-  onModelChange = dataSource => {
+  onModelChange = (dataSource: DataSourceSettings) => {
     this.setState({
       dataSource: dataSource,
     });
@@ -170,17 +169,18 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
     });
   }
 
+  get hasDataSource() {
+    return Object.keys(this.props.dataSource).length > 0;
+  }
+
   render() {
     const { dataSource, dataSourceMeta, navModel, setDataSourceName, setIsDefault } = this.props;
     const { testingMessage, testingStatus } = this.state;
 
     return (
-      <div>
-        <PageHeader model={navModel} />
-        {Object.keys(dataSource).length === 0 ? (
-          <PageLoader pageName="Data source settings" />
-        ) : (
-          <div className="page-container page-body">
+      <Page navModel={navModel}>
+        <Page.Contents isLoading={!this.hasDataSource}>
+          {this.hasDataSource && <div className="page-container page-body">
             <div>
               <form onSubmit={this.onSubmit}>
                 {this.isReadOnly() && this.renderIsReadOnlyMessage()}
@@ -225,14 +225,14 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
                 />
               </form>
             </div>
-          </div>
-        )}
-      </div>
+          </div>}
+        </Page.Contents>
+      </Page>
     );
   }
 }
 
-function mapStateToProps(state) {
+function mapStateToProps(state: StoreState) {
   const pageId = getRouteParamsId(state.location);
   const dataSource = getDataSource(state.dataSources, pageId);
 

+ 382 - 371
public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap

@@ -1,415 +1,426 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Render should render alpha info text 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={Object {}}
+>
+  <PageContents
+    isLoading={false}
   >
-    <div>
-      <form
-        onSubmit={[Function]}
-      >
-        <div
-          className="grafana-info-box"
+    <div
+      className="page-container page-body"
+    >
+      <div>
+        <form
+          onSubmit={[Function]}
         >
-          This plugin is marked as being in alpha state, which means it is in early development phase and updates will include breaking changes.
-        </div>
-        <BasicSettings
-          dataSourceName="gdev-cloudwatch"
-          isDefault={false}
-          onDefaultChange={[Function]}
-          onNameChange={[Function]}
-        />
-        <PluginSettings
-          dataSource={
-            Object {
-              "access": "",
-              "basicAuth": false,
-              "basicAuthPassword": "",
-              "basicAuthUser": "",
-              "database": "",
-              "id": 13,
-              "isDefault": false,
-              "jsonData": Object {
-                "authType": "credentials",
-                "defaultRegion": "eu-west-2",
-              },
-              "name": "gdev-cloudwatch",
-              "orgId": 1,
-              "password": "",
-              "readOnly": false,
-              "type": "cloudwatch",
-              "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
-              "url": "",
-              "user": "",
-              "withCredentials": false,
-            }
-          }
-          dataSourceMeta={
-            Object {
-              "defaultNavUrl": "some/url",
-              "enabled": false,
-              "hasUpdate": false,
-              "id": "1",
-              "info": Object {
-                "author": Object {
-                  "name": "Grafana Labs",
-                  "url": "url/to/GrafanaLabs",
+          <div
+            className="grafana-info-box"
+          >
+            This plugin is marked as being in alpha state, which means it is in early development phase and updates will include breaking changes.
+          </div>
+          <BasicSettings
+            dataSourceName="gdev-cloudwatch"
+            isDefault={false}
+            onDefaultChange={[Function]}
+            onNameChange={[Function]}
+          />
+          <PluginSettings
+            dataSource={
+              Object {
+                "access": "",
+                "basicAuth": false,
+                "basicAuthPassword": "",
+                "basicAuthUser": "",
+                "database": "",
+                "id": 13,
+                "isDefault": false,
+                "jsonData": Object {
+                  "authType": "credentials",
+                  "defaultRegion": "eu-west-2",
                 },
-                "description": "pretty decent plugin",
-                "links": Array [
-                  Object {
-                    "name": "project",
-                    "url": "one link",
+                "name": "gdev-cloudwatch",
+                "orgId": 1,
+                "password": "",
+                "readOnly": false,
+                "type": "cloudwatch",
+                "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+                "url": "",
+                "user": "",
+                "withCredentials": false,
+              }
+            }
+            dataSourceMeta={
+              Object {
+                "defaultNavUrl": "some/url",
+                "enabled": false,
+                "hasUpdate": false,
+                "id": "1",
+                "info": Object {
+                  "author": Object {
+                    "name": "Grafana Labs",
+                    "url": "url/to/GrafanaLabs",
                   },
-                ],
-                "logos": Object {
-                  "large": "large/logo",
-                  "small": "small/logo",
-                },
-                "screenshots": Array [
-                  Object {
-                    "path": "screenshot",
+                  "description": "pretty decent plugin",
+                  "links": Array [
+                    Object {
+                      "name": "project",
+                      "url": "one link",
+                    },
+                  ],
+                  "logos": Object {
+                    "large": "large/logo",
+                    "small": "small/logo",
                   },
-                ],
-                "updated": "2018-09-26",
-                "version": "1",
-              },
-              "latestVersion": "1",
-              "module": Object {},
-              "name": "pretty cool plugin 1",
-              "pinned": false,
-              "state": "alpha",
-              "type": "",
+                  "screenshots": Array [
+                    Object {
+                      "path": "screenshot",
+                    },
+                  ],
+                  "updated": "2018-09-26",
+                  "version": "1",
+                },
+                "latestVersion": "1",
+                "module": Object {},
+                "name": "pretty cool plugin 1",
+                "pinned": false,
+                "state": "alpha",
+                "type": "",
+              }
             }
-          }
-          onModelChange={[Function]}
-        />
-        <div
-          className="gf-form-group section"
-        />
-        <ButtonRow
-          isReadOnly={false}
-          onDelete={[Function]}
-          onSubmit={[Function]}
-        />
-      </form>
+            onModelChange={[Function]}
+          />
+          <div
+            className="gf-form-group section"
+          />
+          <ButtonRow
+            isReadOnly={false}
+            onDelete={[Function]}
+            onSubmit={[Function]}
+          />
+        </form>
+      </div>
     </div>
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;
 
 exports[`Render should render beta info text 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={Object {}}
+>
+  <PageContents
+    isLoading={false}
   >
-    <div>
-      <form
-        onSubmit={[Function]}
-      >
-        <div
-          className="grafana-info-box"
+    <div
+      className="page-container page-body"
+    >
+      <div>
+        <form
+          onSubmit={[Function]}
         >
-          This plugin is marked as being in a beta development state. This means it is in currently in active development and could be missing important features.
-        </div>
-        <BasicSettings
-          dataSourceName="gdev-cloudwatch"
-          isDefault={false}
-          onDefaultChange={[Function]}
-          onNameChange={[Function]}
-        />
-        <PluginSettings
-          dataSource={
-            Object {
-              "access": "",
-              "basicAuth": false,
-              "basicAuthPassword": "",
-              "basicAuthUser": "",
-              "database": "",
-              "id": 13,
-              "isDefault": false,
-              "jsonData": Object {
-                "authType": "credentials",
-                "defaultRegion": "eu-west-2",
-              },
-              "name": "gdev-cloudwatch",
-              "orgId": 1,
-              "password": "",
-              "readOnly": false,
-              "type": "cloudwatch",
-              "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
-              "url": "",
-              "user": "",
-              "withCredentials": false,
-            }
-          }
-          dataSourceMeta={
-            Object {
-              "defaultNavUrl": "some/url",
-              "enabled": false,
-              "hasUpdate": false,
-              "id": "1",
-              "info": Object {
-                "author": Object {
-                  "name": "Grafana Labs",
-                  "url": "url/to/GrafanaLabs",
+          <div
+            className="grafana-info-box"
+          >
+            This plugin is marked as being in a beta development state. This means it is in currently in active development and could be missing important features.
+          </div>
+          <BasicSettings
+            dataSourceName="gdev-cloudwatch"
+            isDefault={false}
+            onDefaultChange={[Function]}
+            onNameChange={[Function]}
+          />
+          <PluginSettings
+            dataSource={
+              Object {
+                "access": "",
+                "basicAuth": false,
+                "basicAuthPassword": "",
+                "basicAuthUser": "",
+                "database": "",
+                "id": 13,
+                "isDefault": false,
+                "jsonData": Object {
+                  "authType": "credentials",
+                  "defaultRegion": "eu-west-2",
                 },
-                "description": "pretty decent plugin",
-                "links": Array [
-                  Object {
-                    "name": "project",
-                    "url": "one link",
+                "name": "gdev-cloudwatch",
+                "orgId": 1,
+                "password": "",
+                "readOnly": false,
+                "type": "cloudwatch",
+                "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+                "url": "",
+                "user": "",
+                "withCredentials": false,
+              }
+            }
+            dataSourceMeta={
+              Object {
+                "defaultNavUrl": "some/url",
+                "enabled": false,
+                "hasUpdate": false,
+                "id": "1",
+                "info": Object {
+                  "author": Object {
+                    "name": "Grafana Labs",
+                    "url": "url/to/GrafanaLabs",
                   },
-                ],
-                "logos": Object {
-                  "large": "large/logo",
-                  "small": "small/logo",
-                },
-                "screenshots": Array [
-                  Object {
-                    "path": "screenshot",
+                  "description": "pretty decent plugin",
+                  "links": Array [
+                    Object {
+                      "name": "project",
+                      "url": "one link",
+                    },
+                  ],
+                  "logos": Object {
+                    "large": "large/logo",
+                    "small": "small/logo",
                   },
-                ],
-                "updated": "2018-09-26",
-                "version": "1",
-              },
-              "latestVersion": "1",
-              "module": Object {},
-              "name": "pretty cool plugin 1",
-              "pinned": false,
-              "state": "beta",
-              "type": "",
+                  "screenshots": Array [
+                    Object {
+                      "path": "screenshot",
+                    },
+                  ],
+                  "updated": "2018-09-26",
+                  "version": "1",
+                },
+                "latestVersion": "1",
+                "module": Object {},
+                "name": "pretty cool plugin 1",
+                "pinned": false,
+                "state": "beta",
+                "type": "",
+              }
             }
-          }
-          onModelChange={[Function]}
-        />
-        <div
-          className="gf-form-group section"
-        />
-        <ButtonRow
-          isReadOnly={false}
-          onDelete={[Function]}
-          onSubmit={[Function]}
-        />
-      </form>
+            onModelChange={[Function]}
+          />
+          <div
+            className="gf-form-group section"
+          />
+          <ButtonRow
+            isReadOnly={false}
+            onDelete={[Function]}
+            onSubmit={[Function]}
+          />
+        </form>
+      </div>
     </div>
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;
 
 exports[`Render should render component 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={Object {}}
+>
+  <PageContents
+    isLoading={false}
   >
-    <div>
-      <form
-        onSubmit={[Function]}
-      >
-        <BasicSettings
-          dataSourceName="gdev-cloudwatch"
-          isDefault={false}
-          onDefaultChange={[Function]}
-          onNameChange={[Function]}
-        />
-        <PluginSettings
-          dataSource={
-            Object {
-              "access": "",
-              "basicAuth": false,
-              "basicAuthPassword": "",
-              "basicAuthUser": "",
-              "database": "",
-              "id": 13,
-              "isDefault": false,
-              "jsonData": Object {
-                "authType": "credentials",
-                "defaultRegion": "eu-west-2",
-              },
-              "name": "gdev-cloudwatch",
-              "orgId": 1,
-              "password": "",
-              "readOnly": false,
-              "type": "cloudwatch",
-              "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
-              "url": "",
-              "user": "",
-              "withCredentials": false,
-            }
-          }
-          dataSourceMeta={
-            Object {
-              "defaultNavUrl": "some/url",
-              "enabled": false,
-              "hasUpdate": false,
-              "id": "1",
-              "info": Object {
-                "author": Object {
-                  "name": "Grafana Labs",
-                  "url": "url/to/GrafanaLabs",
+    <div
+      className="page-container page-body"
+    >
+      <div>
+        <form
+          onSubmit={[Function]}
+        >
+          <BasicSettings
+            dataSourceName="gdev-cloudwatch"
+            isDefault={false}
+            onDefaultChange={[Function]}
+            onNameChange={[Function]}
+          />
+          <PluginSettings
+            dataSource={
+              Object {
+                "access": "",
+                "basicAuth": false,
+                "basicAuthPassword": "",
+                "basicAuthUser": "",
+                "database": "",
+                "id": 13,
+                "isDefault": false,
+                "jsonData": Object {
+                  "authType": "credentials",
+                  "defaultRegion": "eu-west-2",
                 },
-                "description": "pretty decent plugin",
-                "links": Array [
-                  Object {
-                    "name": "project",
-                    "url": "one link",
+                "name": "gdev-cloudwatch",
+                "orgId": 1,
+                "password": "",
+                "readOnly": false,
+                "type": "cloudwatch",
+                "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+                "url": "",
+                "user": "",
+                "withCredentials": false,
+              }
+            }
+            dataSourceMeta={
+              Object {
+                "defaultNavUrl": "some/url",
+                "enabled": false,
+                "hasUpdate": false,
+                "id": "1",
+                "info": Object {
+                  "author": Object {
+                    "name": "Grafana Labs",
+                    "url": "url/to/GrafanaLabs",
                   },
-                ],
-                "logos": Object {
-                  "large": "large/logo",
-                  "small": "small/logo",
-                },
-                "screenshots": Array [
-                  Object {
-                    "path": "screenshot",
+                  "description": "pretty decent plugin",
+                  "links": Array [
+                    Object {
+                      "name": "project",
+                      "url": "one link",
+                    },
+                  ],
+                  "logos": Object {
+                    "large": "large/logo",
+                    "small": "small/logo",
                   },
-                ],
-                "updated": "2018-09-26",
-                "version": "1",
-              },
-              "latestVersion": "1",
-              "module": Object {},
-              "name": "pretty cool plugin 1",
-              "pinned": false,
-              "state": "",
-              "type": "",
+                  "screenshots": Array [
+                    Object {
+                      "path": "screenshot",
+                    },
+                  ],
+                  "updated": "2018-09-26",
+                  "version": "1",
+                },
+                "latestVersion": "1",
+                "module": Object {},
+                "name": "pretty cool plugin 1",
+                "pinned": false,
+                "state": "",
+                "type": "",
+              }
             }
-          }
-          onModelChange={[Function]}
-        />
-        <div
-          className="gf-form-group section"
-        />
-        <ButtonRow
-          isReadOnly={false}
-          onDelete={[Function]}
-          onSubmit={[Function]}
-        />
-      </form>
+            onModelChange={[Function]}
+          />
+          <div
+            className="gf-form-group section"
+          />
+          <ButtonRow
+            isReadOnly={false}
+            onDelete={[Function]}
+            onSubmit={[Function]}
+          />
+        </form>
+      </div>
     </div>
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;
 
 exports[`Render should render is ready only message 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={Object {}}
+>
+  <PageContents
+    isLoading={false}
   >
-    <div>
-      <form
-        onSubmit={[Function]}
-      >
-        <div
-          className="grafana-info-box span8"
+    <div
+      className="page-container page-body"
+    >
+      <div>
+        <form
+          onSubmit={[Function]}
         >
-          This datasource was added by config and cannot be modified using the UI. Please contact your server admin to update this datasource.
-        </div>
-        <BasicSettings
-          dataSourceName="gdev-cloudwatch"
-          isDefault={false}
-          onDefaultChange={[Function]}
-          onNameChange={[Function]}
-        />
-        <PluginSettings
-          dataSource={
-            Object {
-              "access": "",
-              "basicAuth": false,
-              "basicAuthPassword": "",
-              "basicAuthUser": "",
-              "database": "",
-              "id": 13,
-              "isDefault": false,
-              "jsonData": Object {
-                "authType": "credentials",
-                "defaultRegion": "eu-west-2",
-              },
-              "name": "gdev-cloudwatch",
-              "orgId": 1,
-              "password": "",
-              "readOnly": true,
-              "type": "cloudwatch",
-              "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
-              "url": "",
-              "user": "",
-              "withCredentials": false,
-            }
-          }
-          dataSourceMeta={
-            Object {
-              "defaultNavUrl": "some/url",
-              "enabled": false,
-              "hasUpdate": false,
-              "id": "1",
-              "info": Object {
-                "author": Object {
-                  "name": "Grafana Labs",
-                  "url": "url/to/GrafanaLabs",
+          <div
+            className="grafana-info-box span8"
+          >
+            This datasource was added by config and cannot be modified using the UI. Please contact your server admin to update this datasource.
+          </div>
+          <BasicSettings
+            dataSourceName="gdev-cloudwatch"
+            isDefault={false}
+            onDefaultChange={[Function]}
+            onNameChange={[Function]}
+          />
+          <PluginSettings
+            dataSource={
+              Object {
+                "access": "",
+                "basicAuth": false,
+                "basicAuthPassword": "",
+                "basicAuthUser": "",
+                "database": "",
+                "id": 13,
+                "isDefault": false,
+                "jsonData": Object {
+                  "authType": "credentials",
+                  "defaultRegion": "eu-west-2",
                 },
-                "description": "pretty decent plugin",
-                "links": Array [
-                  Object {
-                    "name": "project",
-                    "url": "one link",
+                "name": "gdev-cloudwatch",
+                "orgId": 1,
+                "password": "",
+                "readOnly": true,
+                "type": "cloudwatch",
+                "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+                "url": "",
+                "user": "",
+                "withCredentials": false,
+              }
+            }
+            dataSourceMeta={
+              Object {
+                "defaultNavUrl": "some/url",
+                "enabled": false,
+                "hasUpdate": false,
+                "id": "1",
+                "info": Object {
+                  "author": Object {
+                    "name": "Grafana Labs",
+                    "url": "url/to/GrafanaLabs",
                   },
-                ],
-                "logos": Object {
-                  "large": "large/logo",
-                  "small": "small/logo",
-                },
-                "screenshots": Array [
-                  Object {
-                    "path": "screenshot",
+                  "description": "pretty decent plugin",
+                  "links": Array [
+                    Object {
+                      "name": "project",
+                      "url": "one link",
+                    },
+                  ],
+                  "logos": Object {
+                    "large": "large/logo",
+                    "small": "small/logo",
                   },
-                ],
-                "updated": "2018-09-26",
-                "version": "1",
-              },
-              "latestVersion": "1",
-              "module": Object {},
-              "name": "pretty cool plugin 1",
-              "pinned": false,
-              "state": "",
-              "type": "",
+                  "screenshots": Array [
+                    Object {
+                      "path": "screenshot",
+                    },
+                  ],
+                  "updated": "2018-09-26",
+                  "version": "1",
+                },
+                "latestVersion": "1",
+                "module": Object {},
+                "name": "pretty cool plugin 1",
+                "pinned": false,
+                "state": "",
+                "type": "",
+              }
             }
-          }
-          onModelChange={[Function]}
-        />
-        <div
-          className="gf-form-group section"
-        />
-        <ButtonRow
-          isReadOnly={true}
-          onDelete={[Function]}
-          onSubmit={[Function]}
-        />
-      </form>
+            onModelChange={[Function]}
+          />
+          <div
+            className="gf-form-group section"
+          />
+          <ButtonRow
+            isReadOnly={true}
+            onDelete={[Function]}
+            onSubmit={[Function]}
+          />
+        </form>
+      </div>
     </div>
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;
 
 exports[`Render should render loader 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <PageLoader
-    pageName="Data source settings"
+<Page
+  navModel={Object {}}
+>
+  <PageContents
+    isLoading={true}
   />
-</div>
+</Page>
 `;

+ 12 - 1
public/app/features/datasources/state/actions.ts

@@ -12,6 +12,7 @@ import { Plugin, StoreState } from 'app/types';
 export enum ActionTypes {
   LoadDataSources = 'LOAD_DATA_SOURCES',
   LoadDataSourceTypes = 'LOAD_DATA_SOURCE_TYPES',
+  LoadedDataSourceTypes = 'LOADED_DATA_SOURCE_TYPES',
   LoadDataSource = 'LOAD_DATA_SOURCE',
   LoadDataSourceMeta = 'LOAD_DATA_SOURCE_META',
   SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY',
@@ -38,6 +39,10 @@ interface SetDataSourcesLayoutModeAction {
 
 interface LoadDataSourceTypesAction {
   type: ActionTypes.LoadDataSourceTypes;
+}
+
+interface LoadedDataSourceTypesAction {
+  type: ActionTypes.LoadedDataSourceTypes;
   payload: Plugin[];
 }
 
@@ -81,8 +86,12 @@ const dataSourceMetaLoaded = (dataSourceMeta: Plugin): LoadDataSourceMetaAction
   payload: dataSourceMeta,
 });
 
-const dataSourceTypesLoaded = (dataSourceTypes: Plugin[]): LoadDataSourceTypesAction => ({
+const dataSourceTypesLoad = (): LoadDataSourceTypesAction => ({
   type: ActionTypes.LoadDataSourceTypes,
+});
+
+const dataSourceTypesLoaded = (dataSourceTypes: Plugin[]): LoadedDataSourceTypesAction => ({
+  type: ActionTypes.LoadedDataSourceTypes,
   payload: dataSourceTypes,
 });
 
@@ -117,6 +126,7 @@ export type Action =
   | SetDataSourcesLayoutModeAction
   | UpdateLocationAction
   | LoadDataSourceTypesAction
+  | LoadedDataSourceTypesAction
   | SetDataSourceTypeSearchQueryAction
   | LoadDataSourceAction
   | UpdateNavIndexAction
@@ -167,6 +177,7 @@ export function addDataSource(plugin: Plugin): ThunkResult<void> {
 
 export function loadDataSourceTypes(): ThunkResult<void> {
   return async dispatch => {
+    dispatch(dataSourceTypesLoad());
     const result = await getBackendSrv().get('/api/plugins', { enabled: 1, type: 'datasource' });
     dispatch(dataSourceTypesLoaded(result));
   };

+ 5 - 1
public/app/features/datasources/state/reducers.ts

@@ -12,6 +12,7 @@ const initialState: DataSourcesState = {
   dataSourceTypes: [] as Plugin[],
   dataSourceTypeSearchQuery: '',
   hasFetched: false,
+  isLoadingDataSources: false,
   dataSourceMeta: {} as Plugin,
 };
 
@@ -30,7 +31,10 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo
       return { ...state, layoutMode: action.payload };
 
     case ActionTypes.LoadDataSourceTypes:
-      return { ...state, dataSourceTypes: action.payload };
+      return { ...state, dataSourceTypes: [], isLoadingDataSources: true };
+
+    case ActionTypes.LoadedDataSourceTypes:
+      return { ...state, dataSourceTypes: action.payload, isLoadingDataSources: false };
 
     case ActionTypes.SetDataSourceTypeSearchQuery:
       return { ...state, dataSourceTypeSearchQuery: action.payload };

+ 7 - 8
public/app/features/folders/FolderPermissions.tsx

@@ -1,7 +1,7 @@
 import React, { PureComponent } from 'react';
 import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
-import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import Page from 'app/core/components/Page/Page';
 import { Tooltip } from '@grafana/ui';
 import SlideDown from 'app/core/components/Animations/SlideDown';
 import { getNavModel } from 'app/core/selectors/navModel';
@@ -35,7 +35,7 @@ export interface State {
 }
 
 export class FolderPermissions extends PureComponent<Props, State> {
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
 
     this.state = {
@@ -73,15 +73,14 @@ export class FolderPermissions extends PureComponent<Props, State> {
     const { isAdding } = this.state;
 
     if (folder.id === 0) {
-      return <PageHeader model={navModel} />;
+      return <Page navModel={navModel}><Page.Contents isLoading={true}><span></span></Page.Contents></Page>;
     }
 
     const folderInfo = { title: folder.title, url: folder.url, id: folder.id };
 
     return (
-      <div>
-        <PageHeader model={navModel} />
-        <div className="page-container page-body">
+      <Page navModel={navModel}>
+        <Page.Contents>
           <div className="page-action-bar">
             <h3 className="page-sub-heading">Folder Permissions</h3>
             <Tooltip placement="auto" content={<PermissionsInfo />}>
@@ -104,8 +103,8 @@ export class FolderPermissions extends PureComponent<Props, State> {
             isFetching={false}
             folderInfo={folderInfo}
           />
-        </div>
-      </div>
+        </Page.Contents>
+      </Page>
     );
   }
 }

+ 45 - 32
public/app/features/folders/FolderSettingsPage.tsx

@@ -1,7 +1,7 @@
 import React, { PureComponent } from 'react';
 import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
-import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import Page from 'app/core/components/Page/Page';
 import appEvents from 'app/core/app_events';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { NavModel, StoreState, FolderState } from 'app/types';
@@ -18,23 +18,35 @@ export interface Props {
   deleteFolder: typeof deleteFolder;
 }
 
-export class FolderSettingsPage extends PureComponent<Props> {
+export interface State {
+  isLoading: boolean;
+}
+
+export class FolderSettingsPage extends PureComponent<Props, State> {
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      isLoading: false
+    };
+  }
+
   componentDidMount() {
     this.props.getFolderByUid(this.props.folderUid);
   }
 
-  onTitleChange = evt => {
+  onTitleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
     this.props.setFolderTitle(evt.target.value);
   };
 
-  onSave = async evt => {
+  onSave = async (evt: React.FormEvent<HTMLFormElement>) => {
     evt.preventDefault();
     evt.stopPropagation();
-
+    this.setState({isLoading: true});
     await this.props.saveFolder(this.props.folder);
+    this.setState({isLoading: false});
   };
 
-  onDelete = evt => {
+  onDelete = (evt: React.MouseEvent<HTMLButtonElement>) => {
     evt.stopPropagation();
     evt.preventDefault();
 
@@ -53,34 +65,35 @@ export class FolderSettingsPage extends PureComponent<Props> {
     const { navModel, folder } = this.props;
 
     return (
-      <div>
-        <PageHeader model={navModel} />
-        <div className="page-container page-body">
-          <h2 className="page-sub-heading">Folder Settings</h2>
+      <Page navModel={navModel}>
+        <Page.Contents isLoading={this.state.isLoading}>
+          <div className="page-container page-body">
+            <h2 className="page-sub-heading">Folder Settings</h2>
 
-          <div className="section gf-form-group">
-            <form name="folderSettingsForm" onSubmit={this.onSave}>
-              <div className="gf-form">
-                <label className="gf-form-label width-7">Name</label>
-                <input
-                  type="text"
-                  className="gf-form-input width-30"
-                  value={folder.title}
-                  onChange={this.onTitleChange}
-                />
-              </div>
-              <div className="gf-form-button-row">
-                <button type="submit" className="btn btn-success" disabled={!folder.canSave || !folder.hasChanged}>
-                  <i className="fa fa-save" /> Save
-                </button>
-                <button className="btn btn-danger" onClick={this.onDelete} disabled={!folder.canSave}>
-                  <i className="fa fa-trash" /> Delete
-                </button>
-              </div>
-            </form>
+            <div className="section gf-form-group">
+              <form name="folderSettingsForm" onSubmit={this.onSave}>
+                <div className="gf-form">
+                  <label className="gf-form-label width-7">Name</label>
+                  <input
+                    type="text"
+                    className="gf-form-input width-30"
+                    value={folder.title}
+                    onChange={this.onTitleChange}
+                  />
+                </div>
+                <div className="gf-form-button-row">
+                  <button type="submit" className="btn btn-success" disabled={!folder.canSave || !folder.hasChanged}>
+                    <i className="fa fa-save" /> Save
+                  </button>
+                  <button className="btn btn-danger" onClick={this.onDelete} disabled={!folder.canSave}>
+                    <i className="fa fa-trash" /> Delete
+                  </button>
+                </div>
+              </form>
+            </div>
           </div>
-        </div>
-      </div>
+        </Page.Contents>
+      </Page>
     );
   }
 }

+ 112 - 106
public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap

@@ -1,131 +1,137 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Render should enable save button 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={Object {}}
+>
+  <PageContents
+    isLoading={false}
   >
-    <h2
-      className="page-sub-heading"
-    >
-      Folder Settings
-    </h2>
     <div
-      className="section gf-form-group"
+      className="page-container page-body"
     >
-      <form
-        name="folderSettingsForm"
-        onSubmit={[Function]}
+      <h2
+        className="page-sub-heading"
       >
-        <div
-          className="gf-form"
-        >
-          <label
-            className="gf-form-label width-7"
-          >
-            Name
-          </label>
-          <input
-            className="gf-form-input width-30"
-            onChange={[Function]}
-            type="text"
-            value="loading"
-          />
-        </div>
-        <div
-          className="gf-form-button-row"
+        Folder Settings
+      </h2>
+      <div
+        className="section gf-form-group"
+      >
+        <form
+          name="folderSettingsForm"
+          onSubmit={[Function]}
         >
-          <button
-            className="btn btn-success"
-            disabled={false}
-            type="submit"
+          <div
+            className="gf-form"
           >
-            <i
-              className="fa fa-save"
+            <label
+              className="gf-form-label width-7"
+            >
+              Name
+            </label>
+            <input
+              className="gf-form-input width-30"
+              onChange={[Function]}
+              type="text"
+              value="loading"
             />
-             Save
-          </button>
-          <button
-            className="btn btn-danger"
-            disabled={false}
-            onClick={[Function]}
+          </div>
+          <div
+            className="gf-form-button-row"
           >
-            <i
-              className="fa fa-trash"
-            />
-             Delete
-          </button>
-        </div>
-      </form>
+            <button
+              className="btn btn-success"
+              disabled={false}
+              type="submit"
+            >
+              <i
+                className="fa fa-save"
+              />
+               Save
+            </button>
+            <button
+              className="btn btn-danger"
+              disabled={false}
+              onClick={[Function]}
+            >
+              <i
+                className="fa fa-trash"
+              />
+               Delete
+            </button>
+          </div>
+        </form>
+      </div>
     </div>
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;
 
 exports[`Render should render component 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={Object {}}
+>
+  <PageContents
+    isLoading={false}
   >
-    <h2
-      className="page-sub-heading"
-    >
-      Folder Settings
-    </h2>
     <div
-      className="section gf-form-group"
+      className="page-container page-body"
     >
-      <form
-        name="folderSettingsForm"
-        onSubmit={[Function]}
+      <h2
+        className="page-sub-heading"
       >
-        <div
-          className="gf-form"
-        >
-          <label
-            className="gf-form-label width-7"
-          >
-            Name
-          </label>
-          <input
-            className="gf-form-input width-30"
-            onChange={[Function]}
-            type="text"
-            value="loading"
-          />
-        </div>
-        <div
-          className="gf-form-button-row"
+        Folder Settings
+      </h2>
+      <div
+        className="section gf-form-group"
+      >
+        <form
+          name="folderSettingsForm"
+          onSubmit={[Function]}
         >
-          <button
-            className="btn btn-success"
-            disabled={true}
-            type="submit"
+          <div
+            className="gf-form"
           >
-            <i
-              className="fa fa-save"
+            <label
+              className="gf-form-label width-7"
+            >
+              Name
+            </label>
+            <input
+              className="gf-form-input width-30"
+              onChange={[Function]}
+              type="text"
+              value="loading"
             />
-             Save
-          </button>
-          <button
-            className="btn btn-danger"
-            disabled={false}
-            onClick={[Function]}
+          </div>
+          <div
+            className="gf-form-button-row"
           >
-            <i
-              className="fa fa-trash"
-            />
-             Delete
-          </button>
-        </div>
-      </form>
+            <button
+              className="btn btn-success"
+              disabled={true}
+              type="submit"
+            >
+              <i
+                className="fa fa-save"
+              />
+               Save
+            </button>
+            <button
+              className="btn btn-danger"
+              disabled={false}
+              onClick={[Function]}
+            >
+              <i
+                className="fa fa-trash"
+              />
+               Delete
+            </button>
+          </div>
+        </form>
+      </div>
     </div>
-  </div>
-</div>
+  </PageContents>
+</Page>
 `;

+ 16 - 3
public/app/features/plugins/state/actions.ts

@@ -7,6 +7,7 @@ import { PluginDashboard } from '../../../types/plugins';
 export enum ActionTypes {
   LoadPlugins = 'LOAD_PLUGINS',
   LoadPluginDashboards = 'LOAD_PLUGIN_DASHBOARDS',
+  LoadedPluginDashboards = 'LOADED_PLUGIN_DASHBOARDS',
   SetPluginsSearchQuery = 'SET_PLUGIN_SEARCH_QUERY',
   SetLayoutMode = 'SET_LAYOUT_MODE',
 }
@@ -18,6 +19,10 @@ export interface LoadPluginsAction {
 
 export interface LoadPluginDashboardsAction {
   type: ActionTypes.LoadPluginDashboards;
+}
+
+export interface LoadedPluginDashboardsAction {
+  type: ActionTypes.LoadedPluginDashboards;
   payload: PluginDashboard[];
 }
 
@@ -46,12 +51,20 @@ const pluginsLoaded = (plugins: Plugin[]): LoadPluginsAction => ({
   payload: plugins,
 });
 
-const pluginDashboardsLoaded = (dashboards: PluginDashboard[]): LoadPluginDashboardsAction => ({
+const pluginDashboardsLoad = (): LoadPluginDashboardsAction => ({
   type: ActionTypes.LoadPluginDashboards,
+});
+
+const pluginDashboardsLoaded = (dashboards: PluginDashboard[]): LoadedPluginDashboardsAction => ({
+  type: ActionTypes.LoadedPluginDashboards,
   payload: dashboards,
 });
 
-export type Action = LoadPluginsAction | LoadPluginDashboardsAction | SetPluginsSearchQueryAction | SetLayoutModeAction;
+export type Action = LoadPluginsAction
+  | LoadPluginDashboardsAction
+  | LoadedPluginDashboardsAction
+  | SetPluginsSearchQueryAction
+  | SetLayoutModeAction;
 
 type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
 
@@ -64,8 +77,8 @@ export function loadPlugins(): ThunkResult<void> {
 
 export function loadPluginDashboards(): ThunkResult<void> {
   return async (dispatch, getStore) => {
+    dispatch(pluginDashboardsLoad());
     const dataSourceType = getStore().dataSources.dataSource.type;
-
     const response = await getBackendSrv().get(`api/plugins/${dataSourceType}/dashboards`);
     dispatch(pluginDashboardsLoaded(response));
   };

+ 5 - 1
public/app/features/plugins/state/reducers.ts

@@ -9,6 +9,7 @@ export const initialState: PluginsState = {
   layoutMode: LayoutModes.Grid,
   hasFetched: false,
   dashboards: [] as PluginDashboard[],
+  isLoadingPluginDashboards: false
 };
 
 export const pluginsReducer = (state = initialState, action: Action): PluginsState => {
@@ -23,7 +24,10 @@ export const pluginsReducer = (state = initialState, action: Action): PluginsSta
       return { ...state, layoutMode: action.payload };
 
     case ActionTypes.LoadPluginDashboards:
-      return { ...state, dashboards: action.payload };
+      return { ...state, dashboards: [], isLoadingPluginDashboards: true };
+
+    case ActionTypes.LoadedPluginDashboards:
+      return { ...state, dashboards: action.payload, isLoadingPluginDashboards: false };
   }
   return state;
 };

+ 13 - 8
public/app/features/teams/TeamPages.tsx

@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
 import _ from 'lodash';
 import { hot } from 'react-hot-loader';
 import config from 'app/core/config';
-import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import Page from 'app/core/components/Page/Page';
 import TeamMembers from './TeamMembers';
 import TeamSettings from './TeamSettings';
 import TeamGroupSync from './TeamGroupSync';
@@ -24,6 +24,7 @@ export interface Props {
 
 interface State {
   isSyncEnabled: boolean;
+  isLoading: boolean;
 }
 
 enum PageTypes {
@@ -33,10 +34,11 @@ enum PageTypes {
 }
 
 export class TeamPages extends PureComponent<Props, State> {
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
 
     this.state = {
+      isLoading: false,
       isSyncEnabled: config.buildInfo.isEnterprise,
     };
   }
@@ -47,8 +49,10 @@ export class TeamPages extends PureComponent<Props, State> {
 
   async fetchTeam() {
     const { loadTeam, teamId } = this.props;
-
-    return await loadTeam(teamId);
+    this.setState({isLoading: true});
+    const team = await loadTeam(teamId);
+    this.setState({isLoading: false});
+    return team;
   }
 
   getCurrentPage() {
@@ -78,10 +82,11 @@ export class TeamPages extends PureComponent<Props, State> {
     const { team, navModel } = this.props;
 
     return (
-      <div>
-        <PageHeader model={navModel} />
-        {team && Object.keys(team).length !== 0 && <div className="page-container page-body">{this.renderPage()}</div>}
-      </div>
+      <Page navModel={navModel}>
+        <Page.Contents isLoading={this.state.isLoading}>
+          {team && Object.keys(team).length !== 0 && <div className="page-container page-body">{this.renderPage()}</div>}
+        </Page.Contents>
+      </Page>
     );
   }
 }

+ 44 - 33
public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap

@@ -1,50 +1,61 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Render should render component 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
+<Page
+  navModel={Object {}}
+>
+  <PageContents
+    isLoading={true}
   />
-</div>
+</Page>
 `;
 
 exports[`Render should render group sync page 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={Object {}}
+>
+  <PageContents
+    isLoading={true}
   >
-    <Connect(TeamGroupSync) />
-  </div>
-</div>
+    <div
+      className="page-container page-body"
+    >
+      <Connect(TeamGroupSync) />
+    </div>
+  </PageContents>
+</Page>
 `;
 
 exports[`Render should render member page if team not empty 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={Object {}}
+>
+  <PageContents
+    isLoading={true}
   >
-    <Connect(TeamMembers)
-      syncEnabled={true}
-    />
-  </div>
-</div>
+    <div
+      className="page-container page-body"
+    >
+      <Connect(TeamMembers)
+        syncEnabled={true}
+      />
+    </div>
+  </PageContents>
+</Page>
 `;
 
 exports[`Render should render settings and preferences page 1`] = `
-<div>
-  <PageHeader
-    model={Object {}}
-  />
-  <div
-    className="page-container page-body"
+<Page
+  navModel={Object {}}
+>
+  <PageContents
+    isLoading={true}
   >
-    <Connect(TeamSettings) />
-  </div>
-</div>
+    <div
+      className="page-container page-body"
+    >
+      <Connect(TeamSettings) />
+    </div>
+  </PageContents>
+</Page>
 `;

+ 1 - 0
public/app/types/alerting.ts

@@ -32,4 +32,5 @@ export interface AlertRule {
 export interface AlertRulesState {
   items: AlertRule[];
   searchQuery: string;
+  isLoading: boolean;
 }

+ 1 - 0
public/app/types/datasources.ts

@@ -12,4 +12,5 @@ export interface DataSourcesState {
   dataSource: DataSourceSettings;
   dataSourceMeta: Plugin;
   hasFetched: boolean;
+  isLoadingDataSources: boolean;
 }

+ 1 - 0
public/app/types/plugins.ts

@@ -47,6 +47,7 @@ export interface PluginsState {
   layoutMode: string;
   hasFetched: boolean;
   dashboards: PluginDashboard[];
+  isLoadingPluginDashboards: boolean;
 }
 
 export interface VariableQueryProps {

+ 2 - 0
public/app/types/store.ts

@@ -9,6 +9,7 @@ import { ExploreState } from './explore';
 import { UsersState, UserState } from './user';
 import { OrganizationState } from './organization';
 import { AppNotificationsState } from './appNotifications';
+import { PluginsState } from './plugins';
 
 export interface StoreState {
   navIndex: NavIndex;
@@ -24,4 +25,5 @@ export interface StoreState {
   organization: OrganizationState;
   appNotifications: AppNotificationsState;
   user: UserState;
+  plugins: PluginsState;
 }