Jelajahi Sumber

Merge pull request #13878 from grafana/org-page-to-react

Org page to react
Torkel Ödegaard 7 tahun lalu
induk
melakukan
ee5b37eb2c
40 mengubah file dengan 884 tambahan dan 198 penghapusan
  1. 28 0
      public/app/core/actions/user.ts
  2. 2 2
      public/app/core/components/Label/Label.tsx
  3. 46 0
      public/app/core/components/Picker/SimplePicker.tsx
  4. 16 25
      public/app/core/components/Tooltip/Tooltip.tsx
  5. 2 0
      public/app/core/reducers/index.ts
  6. 15 0
      public/app/core/reducers/user.ts
  7. 3 0
      public/app/features/all.ts
  8. 7 1
      public/app/features/dashboard/state/actions.ts
  9. 2 2
      public/app/features/dashboard/state/reducers.test.ts
  10. 2 2
      public/app/features/dashboard/state/reducers.ts
  11. 0 0
      public/app/features/org/NewOrgCtrl.ts
  12. 45 0
      public/app/features/org/OrgDetailsPage.test.tsx
  13. 85 0
      public/app/features/org/OrgDetailsPage.tsx
  14. 28 0
      public/app/features/org/OrgPreferences.test.tsx
  15. 113 0
      public/app/features/org/OrgPreferences.tsx
  16. 21 0
      public/app/features/org/OrgProfile.test.tsx
  17. 44 0
      public/app/features/org/OrgProfile.tsx
  18. 0 0
      public/app/features/org/SelectOrgCtrl.ts
  19. 0 0
      public/app/features/org/UserInviteCtrl.ts
  20. 36 0
      public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap
  21. 136 0
      public/app/features/org/__snapshots__/OrgPreferences.test.tsx.snap
  22. 46 0
      public/app/features/org/__snapshots__/OrgProfile.test.tsx.snap
  23. 3 8
      public/app/features/org/all.ts
  24. 0 38
      public/app/features/org/org_details_ctrl.ts
  25. 0 21
      public/app/features/org/partials/orgDetails.html
  26. 0 92
      public/app/features/org/prefs_control.ts
  27. 118 0
      public/app/features/org/state/actions.ts
  28. 35 0
      public/app/features/org/state/reducers.ts
  29. 0 0
      public/app/features/profile/ChangePasswordCtrl.ts
  30. 0 0
      public/app/features/profile/ProfileCtrl.ts
  31. 0 0
      public/app/features/profile/partials/change_password.html
  32. 0 0
      public/app/features/profile/partials/profile.html
  33. 0 0
      public/app/features/teams/CreateTeamCtrl.ts
  34. 0 0
      public/app/features/teams/partials/create_team.html
  35. 8 5
      public/app/routes/routes.ts
  36. 2 0
      public/app/store/configureStore.ts
  37. 10 1
      public/app/types/index.ts
  38. 15 0
      public/app/types/organization.ts
  39. 9 0
      public/app/types/search.ts
  40. 7 1
      public/app/types/user.ts

+ 28 - 0
public/app/core/actions/user.ts

@@ -0,0 +1,28 @@
+import { ThunkAction } from 'redux-thunk';
+import { getBackendSrv } from '../services/backend_srv';
+import { DashboardAcl, DashboardSearchHit, StoreState } from '../../types';
+
+type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
+
+export type Action = LoadStarredDashboardsAction;
+
+export enum ActionTypes {
+  LoadStarredDashboards = 'LOAD_STARRED_DASHBOARDS',
+}
+
+interface LoadStarredDashboardsAction {
+  type: ActionTypes.LoadStarredDashboards;
+  payload: DashboardSearchHit[];
+}
+
+const starredDashboardsLoaded = (dashboards: DashboardAcl[]) => ({
+  type: ActionTypes.LoadStarredDashboards,
+  payload: dashboards,
+});
+
+export function loadStarredDashboards(): ThunkResult<void> {
+  return async dispatch => {
+    const starredDashboards = await getBackendSrv().search({ starred: true });
+    dispatch(starredDashboardsLoaded(starredDashboards));
+  };
+}

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

@@ -5,11 +5,12 @@ interface Props {
   tooltip?: string;
   tooltip?: string;
   for?: string;
   for?: string;
   children: ReactNode;
   children: ReactNode;
+  width?: number;
 }
 }
 
 
 export const Label: SFC<Props> = props => {
 export const Label: SFC<Props> = props => {
   return (
   return (
-    <span className="gf-form-label width-10">
+    <span className={`gf-form-label width-${props.width ? props.width : '10'}`}>
       <span>{props.children}</span>
       <span>{props.children}</span>
       {props.tooltip && (
       {props.tooltip && (
         <Tooltip className="gf-form-help-icon--right-normal" placement="auto" content={props.tooltip}>
         <Tooltip className="gf-form-help-icon--right-normal" placement="auto" content={props.tooltip}>
@@ -19,4 +20,3 @@ export const Label: SFC<Props> = props => {
     </span>
     </span>
   );
   );
 };
 };
-

+ 46 - 0
public/app/core/components/Picker/SimplePicker.tsx

@@ -0,0 +1,46 @@
+import React, { SFC } from 'react';
+import Select from 'react-select';
+import DescriptionOption from './DescriptionOption';
+import ResetStyles from './ResetStyles';
+
+interface Props {
+  className?: string;
+  defaultValue: any;
+  getOptionLabel: (item: any) => string;
+  getOptionValue: (item: any) => string;
+  onSelected: (item: any) => {} | void;
+  options: any[];
+  placeholder?: string;
+  width: number;
+}
+
+const SimplePicker: SFC<Props> = ({
+  className,
+  defaultValue,
+  getOptionLabel,
+  getOptionValue,
+  onSelected,
+  options,
+  placeholder,
+  width,
+}) => {
+  return (
+    <Select
+      classNamePrefix={`gf-form-select-box`}
+      className={`width-${width} gf-form-input gf-form-input--form-dropdown ${className || ''}`}
+      components={{
+        Option: DescriptionOption,
+      }}
+      defaultValue={defaultValue}
+      getOptionLabel={getOptionLabel}
+      getOptionValue={getOptionValue}
+      isSearchable={false}
+      onChange={onSelected}
+      options={options}
+      placeholder={placeholder || 'Choose'}
+      styles={ResetStyles}
+    />
+  );
+};
+
+export default SimplePicker;

+ 16 - 25
public/app/core/components/Tooltip/Tooltip.tsx

@@ -1,37 +1,28 @@
-import React from 'react';
+import React, { PureComponent } from 'react';
 import withTooltip from './withTooltip';
 import withTooltip from './withTooltip';
 import { Target } from 'react-popper';
 import { Target } from 'react-popper';
 
 
-interface TooltipProps {
+interface Props {
   tooltipSetState: (prevState: object) => void;
   tooltipSetState: (prevState: object) => void;
 }
 }
 
 
-class Tooltip extends React.Component<TooltipProps, any> {
-  constructor(props) {
-    super(props);
-    this.showTooltip = this.showTooltip.bind(this);
-    this.hideTooltip = this.hideTooltip.bind(this);
-  }
-
-  showTooltip() {
+class Tooltip extends PureComponent<Props> {
+  showTooltip = () => {
     const { tooltipSetState } = this.props;
     const { tooltipSetState } = this.props;
-    tooltipSetState(prevState => {
-      return {
-        ...prevState,
-        show: true,
-      };
-    });
-  }
 
 
-  hideTooltip() {
+    tooltipSetState(prevState => ({
+      ...prevState,
+      show: true,
+    }));
+  };
+
+  hideTooltip = () => {
     const { tooltipSetState } = this.props;
     const { tooltipSetState } = this.props;
-    tooltipSetState(prevState => {
-      return {
-        ...prevState,
-        show: false,
-      };
-    });
-  }
+    tooltipSetState(prevState => ({
+      ...prevState,
+      show: false,
+    }));
+  };
 
 
   render() {
   render() {
     return (
     return (

+ 2 - 0
public/app/core/reducers/index.ts

@@ -1,9 +1,11 @@
 import { navIndexReducer as navIndex } from './navModel';
 import { navIndexReducer as navIndex } from './navModel';
 import { locationReducer as location } from './location';
 import { locationReducer as location } from './location';
 import { appNotificationsReducer as appNotifications } from './appNotification';
 import { appNotificationsReducer as appNotifications } from './appNotification';
+import { userReducer as user } from './user';
 
 
 export default {
 export default {
   navIndex,
   navIndex,
   location,
   location,
   appNotifications,
   appNotifications,
+  user,
 };
 };

+ 15 - 0
public/app/core/reducers/user.ts

@@ -0,0 +1,15 @@
+import { DashboardSearchHit, UserState } from '../../types';
+import { Action, ActionTypes } from '../actions/user';
+
+const initialState: UserState = {
+  starredDashboards: [] as DashboardSearchHit[],
+};
+
+export const userReducer = (state: UserState = initialState, action: Action): UserState => {
+  switch (action.type) {
+    case ActionTypes.LoadStarredDashboards:
+      return { ...state, starredDashboards: action.payload };
+  }
+
+  return state;
+};

+ 3 - 0
public/app/features/all.ts

@@ -9,3 +9,6 @@ import './admin';
 import './alerting/NotificationsEditCtrl';
 import './alerting/NotificationsEditCtrl';
 import './alerting/NotificationsListCtrl';
 import './alerting/NotificationsListCtrl';
 import './manage-dashboards';
 import './manage-dashboards';
+import './teams/CreateTeamCtrl';
+import './profile/ProfileCtrl';
+import './profile/ChangePasswordCtrl';

+ 7 - 1
public/app/features/dashboard/state/actions.ts

@@ -13,6 +13,7 @@ import {
 
 
 export enum ActionTypes {
 export enum ActionTypes {
   LoadDashboardPermissions = 'LOAD_DASHBOARD_PERMISSIONS',
   LoadDashboardPermissions = 'LOAD_DASHBOARD_PERMISSIONS',
+  LoadStarredDashboards = 'LOAD_STARRED_DASHBOARDS',
 }
 }
 
 
 export interface LoadDashboardPermissionsAction {
 export interface LoadDashboardPermissionsAction {
@@ -20,7 +21,12 @@ export interface LoadDashboardPermissionsAction {
   payload: DashboardAcl[];
   payload: DashboardAcl[];
 }
 }
 
 
-export type Action = LoadDashboardPermissionsAction;
+export interface LoadStarredDashboardsAction {
+  type: ActionTypes.LoadStarredDashboards;
+  payload: DashboardAcl[];
+}
+
+export type Action = LoadDashboardPermissionsAction | LoadStarredDashboardsAction;
 
 
 type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
 type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
 
 

+ 2 - 2
public/app/features/dashboard/state/reducers.test.ts

@@ -1,6 +1,6 @@
 import { Action, ActionTypes } from './actions';
 import { Action, ActionTypes } from './actions';
 import { OrgRole, PermissionLevel, DashboardState } from 'app/types';
 import { OrgRole, PermissionLevel, DashboardState } from 'app/types';
-import { inititalState, dashboardReducer } from './reducers';
+import { initialState, dashboardReducer } from './reducers';
 
 
 describe('dashboard reducer', () => {
 describe('dashboard reducer', () => {
   describe('loadDashboardPermissions', () => {
   describe('loadDashboardPermissions', () => {
@@ -14,7 +14,7 @@ describe('dashboard reducer', () => {
           { id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit },
           { id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit },
         ],
         ],
       };
       };
-      state = dashboardReducer(inititalState, action);
+      state = dashboardReducer(initialState, action);
     });
     });
 
 
     it('should add permissions to state', async () => {
     it('should add permissions to state', async () => {

+ 2 - 2
public/app/features/dashboard/state/reducers.ts

@@ -2,11 +2,11 @@ import { DashboardState } from 'app/types';
 import { Action, ActionTypes } from './actions';
 import { Action, ActionTypes } from './actions';
 import { processAclItems } from 'app/core/utils/acl';
 import { processAclItems } from 'app/core/utils/acl';
 
 
-export const inititalState: DashboardState = {
+export const initialState: DashboardState = {
   permissions: [],
   permissions: [],
 };
 };
 
 
-export const dashboardReducer = (state = inititalState, action: Action): DashboardState => {
+export const dashboardReducer = (state = initialState, action: Action): DashboardState => {
   switch (action.type) {
   switch (action.type) {
     case ActionTypes.LoadDashboardPermissions:
     case ActionTypes.LoadDashboardPermissions:
       return {
       return {

+ 0 - 0
public/app/features/org/new_org_ctrl.ts → public/app/features/org/NewOrgCtrl.ts


+ 45 - 0
public/app/features/org/OrgDetailsPage.test.tsx

@@ -0,0 +1,45 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { OrgDetailsPage, Props } from './OrgDetailsPage';
+import { NavModel, Organization, OrganizationPreferences } from '../../types';
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    preferences: {} as OrganizationPreferences,
+    organization: {} as Organization,
+    navModel: {} as NavModel,
+    loadOrganization: jest.fn(),
+    loadOrganizationPreferences: jest.fn(),
+    loadStarredDashboards: jest.fn(),
+    setOrganizationName: jest.fn(),
+    updateOrganization: jest.fn(),
+  };
+
+  Object.assign(props, propOverrides);
+
+  return shallow(<OrgDetailsPage {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render organization and preferences', () => {
+    const wrapper = setup({
+      organization: {
+        name: 'Cool org',
+        id: 1,
+      },
+      preferences: {
+        homeDashboardId: 1,
+        theme: 'Default',
+        timezone: 'Default',
+      },
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 85 - 0
public/app/features/org/OrgDetailsPage.tsx

@@ -0,0 +1,85 @@
+import React, { PureComponent } from 'react';
+import { hot } from 'react-hot-loader';
+import { connect } from 'react-redux';
+import PageHeader from '../../core/components/PageHeader/PageHeader';
+import PageLoader from '../../core/components/PageLoader/PageLoader';
+import OrgProfile from './OrgProfile';
+import OrgPreferences from './OrgPreferences';
+import {
+  loadOrganization,
+  loadOrganizationPreferences,
+  setOrganizationName,
+  updateOrganization,
+} from './state/actions';
+import { loadStarredDashboards } from '../../core/actions/user';
+import { NavModel, Organization, OrganizationPreferences, StoreState } from 'app/types';
+import { getNavModel } from '../../core/selectors/navModel';
+
+export interface Props {
+  navModel: NavModel;
+  organization: Organization;
+  preferences: OrganizationPreferences;
+  loadOrganization: typeof loadOrganization;
+  loadOrganizationPreferences: typeof loadOrganizationPreferences;
+  loadStarredDashboards: typeof loadStarredDashboards;
+  setOrganizationName: typeof setOrganizationName;
+  updateOrganization: typeof updateOrganization;
+}
+
+export class OrgDetailsPage extends PureComponent<Props> {
+  async componentDidMount() {
+    await this.props.loadStarredDashboards();
+    await this.props.loadOrganization();
+    await this.props.loadOrganizationPreferences();
+  }
+
+  onOrgNameChange = name => {
+    this.props.setOrganizationName(name);
+  };
+
+  onUpdateOrganization = () => {
+    this.props.updateOrganization();
+  };
+
+  render() {
+    const { navModel, organization, preferences } = this.props;
+
+    return (
+      <div>
+        <PageHeader model={navModel} />
+        <div className="page-container page-body">
+          {Object.keys(organization).length === 0 || Object.keys(preferences).length === 0 ? (
+            <PageLoader pageName="Organization" />
+          ) : (
+            <div>
+              <OrgProfile
+                onOrgNameChange={name => this.onOrgNameChange(name)}
+                onSubmit={this.onUpdateOrganization}
+                orgName={organization.name}
+              />
+              <OrgPreferences />
+            </div>
+          )}
+        </div>
+      </div>
+    );
+  }
+}
+
+function mapStateToProps(state: StoreState) {
+  return {
+    navModel: getNavModel(state.navIndex, 'org-settings'),
+    organization: state.organization.organization,
+    preferences: state.organization.preferences,
+  };
+}
+
+const mapDispatchToProps = {
+  loadOrganization,
+  loadOrganizationPreferences,
+  loadStarredDashboards,
+  setOrganizationName,
+  updateOrganization,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(OrgDetailsPage));

+ 28 - 0
public/app/features/org/OrgPreferences.test.tsx

@@ -0,0 +1,28 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { OrgPreferences, Props } from './OrgPreferences';
+
+const setup = () => {
+  const props: Props = {
+    preferences: {
+      homeDashboardId: 1,
+      timezone: 'UTC',
+      theme: 'Default',
+    },
+    starredDashboards: [{ id: 1, title: 'Standard dashboard', url: '', uri: '', uid: '', type: '', tags: [] }],
+    setOrganizationTimezone: jest.fn(),
+    setOrganizationTheme: jest.fn(),
+    setOrganizationHomeDashboard: jest.fn(),
+    updateOrganizationPreferences: jest.fn(),
+  };
+
+  return shallow(<OrgPreferences {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 113 - 0
public/app/features/org/OrgPreferences.tsx

@@ -0,0 +1,113 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'react-redux';
+import { Label } from '../../core/components/Label/Label';
+import SimplePicker from '../../core/components/Picker/SimplePicker';
+import { DashboardSearchHit, OrganizationPreferences } from 'app/types';
+import {
+  setOrganizationHomeDashboard,
+  setOrganizationTheme,
+  setOrganizationTimezone,
+  updateOrganizationPreferences,
+} from './state/actions';
+
+export interface Props {
+  preferences: OrganizationPreferences;
+  starredDashboards: DashboardSearchHit[];
+  setOrganizationHomeDashboard: typeof setOrganizationHomeDashboard;
+  setOrganizationTheme: typeof setOrganizationTheme;
+  setOrganizationTimezone: typeof setOrganizationTimezone;
+  updateOrganizationPreferences: typeof updateOrganizationPreferences;
+}
+
+const themes = [{ value: '', text: 'Default' }, { value: 'dark', text: 'Dark' }, { value: 'light', text: 'Light' }];
+
+const timezones = [
+  { value: '', text: 'Default' },
+  { value: 'browser', text: 'Local browser time' },
+  { value: 'utc', text: 'UTC' },
+];
+
+export class OrgPreferences extends PureComponent<Props> {
+  onSubmitForm = event => {
+    event.preventDefault();
+    this.props.updateOrganizationPreferences();
+  };
+
+  render() {
+    const {
+      preferences,
+      starredDashboards,
+      setOrganizationHomeDashboard,
+      setOrganizationTimezone,
+      setOrganizationTheme,
+    } = this.props;
+
+    starredDashboards.unshift({ id: 0, title: 'Default', tags: [], type: '', uid: '', uri: '', url: '' });
+
+    return (
+      <form className="section gf-form-group" onSubmit={this.onSubmitForm}>
+        <h3 className="page-heading">Preferences</h3>
+        <div className="gf-form">
+          <span className="gf-form-label width-11">UI Theme</span>
+          <SimplePicker
+            defaultValue={themes.find(theme => theme.value === preferences.theme)}
+            options={themes}
+            getOptionValue={i => i.value}
+            getOptionLabel={i => i.text}
+            onSelected={theme => setOrganizationTheme(theme.value)}
+            width={20}
+          />
+        </div>
+        <div className="gf-form">
+          <Label
+            width={11}
+            tooltip="Not finding dashboard you want? Star it first, then it should appear in this select box."
+          >
+            Home Dashboard
+          </Label>
+          <SimplePicker
+            defaultValue={starredDashboards.find(dashboard => dashboard.id === preferences.homeDashboardId)}
+            getOptionValue={i => i.id}
+            getOptionLabel={i => i.title}
+            onSelected={(dashboard: DashboardSearchHit) => setOrganizationHomeDashboard(dashboard.id)}
+            options={starredDashboards}
+            placeholder="Chose default dashboard"
+            width={20}
+          />
+        </div>
+        <div className="gf-form">
+          <label className="gf-form-label width-11">Timezone</label>
+          <SimplePicker
+            defaultValue={timezones.find(timezone => timezone.value === preferences.timezone)}
+            getOptionValue={i => i.value}
+            getOptionLabel={i => i.text}
+            onSelected={timezone => setOrganizationTimezone(timezone.value)}
+            options={timezones}
+            width={20}
+          />
+        </div>
+        <div className="gf-form-button-row">
+          <button type="submit" className="btn btn-success">
+            Save
+          </button>
+        </div>
+      </form>
+    );
+  }
+}
+
+function mapStateToProps(state) {
+  return {
+    preferences: state.organization.preferences,
+    starredDashboards: state.user.starredDashboards,
+  };
+}
+
+const mapDispatchToProps = {
+  setOrganizationHomeDashboard,
+  setOrganizationTimezone,
+  setOrganizationTheme,
+  updateOrganizationPreferences,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(OrgPreferences);

+ 21 - 0
public/app/features/org/OrgProfile.test.tsx

@@ -0,0 +1,21 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import OrgProfile, { Props } from './OrgProfile';
+
+const setup = () => {
+  const props: Props = {
+    orgName: 'Main org',
+    onSubmit: jest.fn(),
+    onOrgNameChange: jest.fn(),
+  };
+
+  return shallow(<OrgProfile {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 44 - 0
public/app/features/org/OrgProfile.tsx

@@ -0,0 +1,44 @@
+import React, { SFC } from 'react';
+
+export interface Props {
+  orgName: string;
+  onSubmit: () => void;
+  onOrgNameChange: (orgName: string) => void;
+}
+
+const OrgProfile: SFC<Props> = ({ onSubmit, onOrgNameChange, orgName }) => {
+  return (
+    <div>
+      <h3 className="page-sub-heading">Organization profile</h3>
+      <form
+        name="orgForm"
+        className="gf-form-group"
+        onSubmit={event => {
+          event.preventDefault();
+          onSubmit();
+        }}
+      >
+        <div className="gf-form-inline">
+          <div className="gf-form max-width-28">
+            <span className="gf-form-label">Organization name</span>
+            <input
+              className="gf-form-input"
+              type="text"
+              onChange={event => {
+                onOrgNameChange(event.target.value);
+              }}
+              value={orgName}
+            />
+          </div>
+        </div>
+        <div className="gf-form-button-row">
+          <button type="submit" className="btn btn-success">
+            Save
+          </button>
+        </div>
+      </form>
+    </div>
+  );
+};
+
+export default OrgProfile;

+ 0 - 0
public/app/features/org/select_org_ctrl.ts → public/app/features/org/SelectOrgCtrl.ts


+ 0 - 0
public/app/features/org/user_invite_ctrl.ts → public/app/features/org/UserInviteCtrl.ts


+ 36 - 0
public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap

@@ -0,0 +1,36 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+  <div
+    className="page-container page-body"
+  >
+    <PageLoader
+      pageName="Organization"
+    />
+  </div>
+</div>
+`;
+
+exports[`Render should render organization and preferences 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+  <div
+    className="page-container page-body"
+  >
+    <div>
+      <OrgProfile
+        onOrgNameChange={[Function]}
+        onSubmit={[Function]}
+        orgName="Cool org"
+      />
+      <Connect(OrgPreferences) />
+    </div>
+  </div>
+</div>
+`;

+ 136 - 0
public/app/features/org/__snapshots__/OrgPreferences.test.tsx.snap

@@ -0,0 +1,136 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<form
+  className="section gf-form-group"
+  onSubmit={[Function]}
+>
+  <h3
+    className="page-heading"
+  >
+    Preferences
+  </h3>
+  <div
+    className="gf-form"
+  >
+    <span
+      className="gf-form-label width-11"
+    >
+      UI Theme
+    </span>
+    <SimplePicker
+      getOptionLabel={[Function]}
+      getOptionValue={[Function]}
+      onSelected={[Function]}
+      options={
+        Array [
+          Object {
+            "text": "Default",
+            "value": "",
+          },
+          Object {
+            "text": "Dark",
+            "value": "dark",
+          },
+          Object {
+            "text": "Light",
+            "value": "light",
+          },
+        ]
+      }
+      width={20}
+    />
+  </div>
+  <div
+    className="gf-form"
+  >
+    <Component
+      tooltip="Not finding dashboard you want? Star it first, then it should appear in this select box."
+      width={11}
+    >
+      Home Dashboard
+    </Component>
+    <SimplePicker
+      defaultValue={
+        Object {
+          "id": 1,
+          "tags": Array [],
+          "title": "Standard dashboard",
+          "type": "",
+          "uid": "",
+          "uri": "",
+          "url": "",
+        }
+      }
+      getOptionLabel={[Function]}
+      getOptionValue={[Function]}
+      onSelected={[Function]}
+      options={
+        Array [
+          Object {
+            "id": 0,
+            "tags": Array [],
+            "title": "Default",
+            "type": "",
+            "uid": "",
+            "uri": "",
+            "url": "",
+          },
+          Object {
+            "id": 1,
+            "tags": Array [],
+            "title": "Standard dashboard",
+            "type": "",
+            "uid": "",
+            "uri": "",
+            "url": "",
+          },
+        ]
+      }
+      placeholder="Chose default dashboard"
+      width={20}
+    />
+  </div>
+  <div
+    className="gf-form"
+  >
+    <label
+      className="gf-form-label width-11"
+    >
+      Timezone
+    </label>
+    <SimplePicker
+      getOptionLabel={[Function]}
+      getOptionValue={[Function]}
+      onSelected={[Function]}
+      options={
+        Array [
+          Object {
+            "text": "Default",
+            "value": "",
+          },
+          Object {
+            "text": "Local browser time",
+            "value": "browser",
+          },
+          Object {
+            "text": "UTC",
+            "value": "utc",
+          },
+        ]
+      }
+      width={20}
+    />
+  </div>
+  <div
+    className="gf-form-button-row"
+  >
+    <button
+      className="btn btn-success"
+      type="submit"
+    >
+      Save
+    </button>
+  </div>
+</form>
+`;

+ 46 - 0
public/app/features/org/__snapshots__/OrgProfile.test.tsx.snap

@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div>
+  <h3
+    className="page-sub-heading"
+  >
+    Organization profile
+  </h3>
+  <form
+    className="gf-form-group"
+    name="orgForm"
+    onSubmit={[Function]}
+  >
+    <div
+      className="gf-form-inline"
+    >
+      <div
+        className="gf-form max-width-28"
+      >
+        <span
+          className="gf-form-label"
+        >
+          Organization name
+        </span>
+        <input
+          className="gf-form-input"
+          onChange={[Function]}
+          type="text"
+          value="Main org"
+        />
+      </div>
+    </div>
+    <div
+      className="gf-form-button-row"
+    >
+      <button
+        className="btn btn-success"
+        type="submit"
+      >
+        Save
+      </button>
+    </div>
+  </form>
+</div>
+`;

+ 3 - 8
public/app/features/org/all.ts

@@ -1,8 +1,3 @@
-import './profile_ctrl';
-import './select_org_ctrl';
-import './change_password_ctrl';
-import './new_org_ctrl';
-import './user_invite_ctrl';
-import './create_team_ctrl';
-import './org_details_ctrl';
-import './prefs_control';
+import './SelectOrgCtrl';
+import './NewOrgCtrl';
+import './UserInviteCtrl';

+ 0 - 38
public/app/features/org/org_details_ctrl.ts

@@ -1,38 +0,0 @@
-import angular from 'angular';
-
-export class OrgDetailsCtrl {
-  /** @ngInject */
-  constructor($scope, $http, backendSrv, contextSrv, navModelSrv) {
-    $scope.init = () => {
-      $scope.getOrgInfo();
-      $scope.navModel = navModelSrv.getNav('cfg', 'org-settings', 0);
-    };
-
-    $scope.getOrgInfo = () => {
-      backendSrv.get('/api/org').then(org => {
-        $scope.org = org;
-        $scope.address = org.address;
-        contextSrv.user.orgName = org.name;
-      });
-    };
-
-    $scope.update = () => {
-      if (!$scope.orgForm.$valid) {
-        return;
-      }
-      const data = { name: $scope.org.name };
-      backendSrv.put('/api/org', data).then($scope.getOrgInfo);
-    };
-
-    $scope.updateAddress = () => {
-      if (!$scope.addressForm.$valid) {
-        return;
-      }
-      backendSrv.put('/api/org/address', $scope.address).then($scope.getOrgInfo);
-    };
-
-    $scope.init();
-  }
-}
-
-angular.module('grafana.controllers').controller('OrgDetailsCtrl', OrgDetailsCtrl);

+ 0 - 21
public/app/features/org/partials/orgDetails.html

@@ -1,21 +0,0 @@
-<page-header model="navModel"></page-header>
-
-<div class="page-container page-body">
-  <h3 class="page-sub-heading">Organization profile</h3>
-
-  <form name="orgForm" class="gf-form-group">
-    <div class="gf-form-inline">
-      <div class="gf-form max-width-28">
-        <span class="gf-form-label">Organization name</span>
-        <input class="gf-form-input" type="text" required ng-model="org.name">
-      </div>
-    </div>
-
-    <div class="gf-form-button-row">
-      <button type="submit" class="btn btn-success" ng-click="update()">Save</button>
-    </div>
-  </form>
-  <prefs-control mode="org"></prefs-control>
-</div>
-
-

+ 0 - 92
public/app/features/org/prefs_control.ts

@@ -1,92 +0,0 @@
-import config from 'app/core/config';
-import coreModule from 'app/core/core_module';
-
-export class PrefsControlCtrl {
-  prefs: any;
-  oldTheme: any;
-  prefsForm: any;
-  mode: string;
-
-  timezones: any = [
-    { value: '', text: 'Default' },
-    { value: 'browser', text: 'Local browser time' },
-    { value: 'utc', text: 'UTC' },
-  ];
-  themes: any = [{ value: '', text: 'Default' }, { value: 'dark', text: 'Dark' }, { value: 'light', text: 'Light' }];
-
-  /** @ngInject */
-  constructor(private backendSrv, private $location) {}
-
-  $onInit() {
-    return this.backendSrv.get(`/api/${this.mode}/preferences`).then(prefs => {
-      this.prefs = prefs;
-      this.oldTheme = prefs.theme;
-    });
-  }
-
-  updatePrefs() {
-    if (!this.prefsForm.$valid) {
-      return;
-    }
-
-    const cmd = {
-      theme: this.prefs.theme,
-      timezone: this.prefs.timezone,
-      homeDashboardId: this.prefs.homeDashboardId,
-    };
-
-    this.backendSrv.put(`/api/${this.mode}/preferences`, cmd).then(() => {
-      window.location.href = config.appSubUrl + this.$location.path();
-    });
-  }
-}
-
-const template = `
-<form name="ctrl.prefsForm" class="section gf-form-group">
-  <h3 class="page-heading">Preferences</h3>
-
-  <div class="gf-form">
-    <span class="gf-form-label width-11">UI Theme</span>
-    <div class="gf-form-select-wrapper max-width-20">
-      <select class="gf-form-input" ng-model="ctrl.prefs.theme" ng-options="f.value as f.text for f in ctrl.themes"></select>
-    </div>
-  </div>
-
-  <div class="gf-form">
-    <span class="gf-form-label width-11">
-      Home Dashboard
-      <info-popover mode="right-normal">
-        Not finding dashboard you want? Star it first, then it should appear in this select box.
-      </info-popover>
-    </span>
-    <dashboard-selector class="gf-form-select-wrapper max-width-20" model="ctrl.prefs.homeDashboardId">
-    </dashboard-selector>
-  </div>
-
-  <div class="gf-form">
-    <label class="gf-form-label width-11">Timezone</label>
-    <div class="gf-form-select-wrapper max-width-20">
-      <select class="gf-form-input" ng-model="ctrl.prefs.timezone" ng-options="f.value as f.text for f in ctrl.timezones"></select>
-    </div>
-  </div>
-
-  <div class="gf-form-button-row">
-    <button type="submit" class="btn btn-success" ng-click="ctrl.updatePrefs()">Save</button>
-  </div>
-</form>
-`;
-
-export function prefsControlDirective() {
-  return {
-    restrict: 'E',
-    controller: PrefsControlCtrl,
-    bindToController: true,
-    controllerAs: 'ctrl',
-    template: template,
-    scope: {
-      mode: '@',
-    },
-  };
-}
-
-coreModule.directive('prefsControl', prefsControlDirective);

+ 118 - 0
public/app/features/org/state/actions.ts

@@ -0,0 +1,118 @@
+import { ThunkAction } from 'redux-thunk';
+import { Organization, OrganizationPreferences, StoreState } from 'app/types';
+import { getBackendSrv } from '../../../core/services/backend_srv';
+
+type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
+
+export enum ActionTypes {
+  LoadOrganization = 'LOAD_ORGANISATION',
+  LoadPreferences = 'LOAD_PREFERENCES',
+  SetOrganizationName = 'SET_ORGANIZATION_NAME',
+  SetOrganizationTheme = 'SET_ORGANIZATION_THEME',
+  SetOrganizationHomeDashboard = 'SET_ORGANIZATION_HOME_DASHBOARD',
+  SetOrganizationTimezone = 'SET_ORGANIZATION_TIMEZONE',
+}
+
+interface LoadOrganizationAction {
+  type: ActionTypes.LoadOrganization;
+  payload: Organization;
+}
+
+interface LoadPreferencesAction {
+  type: ActionTypes.LoadPreferences;
+  payload: OrganizationPreferences;
+}
+
+interface SetOrganizationNameAction {
+  type: ActionTypes.SetOrganizationName;
+  payload: string;
+}
+
+interface SetOrganizationThemeAction {
+  type: ActionTypes.SetOrganizationTheme;
+  payload: string;
+}
+
+interface SetOrganizationHomeDashboardAction {
+  type: ActionTypes.SetOrganizationHomeDashboard;
+  payload: number;
+}
+
+interface SetOrganizationTimezoneAction {
+  type: ActionTypes.SetOrganizationTimezone;
+  payload: string;
+}
+
+const organisationLoaded = (organisation: Organization) => ({
+  type: ActionTypes.LoadOrganization,
+  payload: organisation,
+});
+
+const preferencesLoaded = (preferences: OrganizationPreferences) => ({
+  type: ActionTypes.LoadPreferences,
+  payload: preferences,
+});
+
+export const setOrganizationName = (orgName: string) => ({
+  type: ActionTypes.SetOrganizationName,
+  payload: orgName,
+});
+
+export const setOrganizationTheme = (theme: string) => ({
+  type: ActionTypes.SetOrganizationTheme,
+  payload: theme,
+});
+
+export const setOrganizationHomeDashboard = (id: number) => ({
+  type: ActionTypes.SetOrganizationHomeDashboard,
+  payload: id,
+});
+
+export const setOrganizationTimezone = (timezone: string) => ({
+  type: ActionTypes.SetOrganizationTimezone,
+  payload: timezone,
+});
+
+export type Action =
+  | LoadOrganizationAction
+  | LoadPreferencesAction
+  | SetOrganizationNameAction
+  | SetOrganizationThemeAction
+  | SetOrganizationHomeDashboardAction
+  | SetOrganizationTimezoneAction;
+
+export function loadOrganization(): ThunkResult<void> {
+  return async dispatch => {
+    const organisationResponse = await getBackendSrv().get('/api/org');
+    dispatch(organisationLoaded(organisationResponse));
+
+    return organisationResponse;
+  };
+}
+
+export function loadOrganizationPreferences(): ThunkResult<void> {
+  return async dispatch => {
+    const preferencesResponse = await getBackendSrv().get('/api/org/preferences');
+    dispatch(preferencesLoaded(preferencesResponse));
+  };
+}
+
+export function updateOrganization() {
+  return async (dispatch, getStore) => {
+    const organization = getStore().organization.organization;
+
+    await getBackendSrv().put('/api/org', { name: organization.name });
+
+    dispatch(loadOrganization());
+  };
+}
+
+export function updateOrganizationPreferences() {
+  return async (dispatch, getStore) => {
+    const preferences = getStore().organization.preferences;
+
+    await getBackendSrv().put('/api/org/preferences', preferences);
+
+    window.location.reload();
+  };
+}

+ 35 - 0
public/app/features/org/state/reducers.ts

@@ -0,0 +1,35 @@
+import { Organization, OrganizationPreferences, OrganizationState } from 'app/types';
+import { Action, ActionTypes } from './actions';
+
+const initialState: OrganizationState = {
+  organization: {} as Organization,
+  preferences: {} as OrganizationPreferences,
+};
+
+const organizationReducer = (state = initialState, action: Action): OrganizationState => {
+  switch (action.type) {
+    case ActionTypes.LoadOrganization:
+      return { ...state, organization: action.payload };
+
+    case ActionTypes.LoadPreferences:
+      return { ...state, preferences: action.payload };
+
+    case ActionTypes.SetOrganizationName:
+      return { ...state, organization: { ...state.organization, name: action.payload } };
+
+    case ActionTypes.SetOrganizationTheme:
+      return { ...state, preferences: { ...state.preferences, theme: action.payload } };
+
+    case ActionTypes.SetOrganizationHomeDashboard:
+      return { ...state, preferences: { ...state.preferences, homeDashboardId: action.payload } };
+
+    case ActionTypes.SetOrganizationTimezone:
+      return { ...state, preferences: { ...state.preferences, timezone: action.payload } };
+  }
+
+  return state;
+};
+
+export default {
+  organization: organizationReducer,
+};

+ 0 - 0
public/app/features/org/change_password_ctrl.ts → public/app/features/profile/ChangePasswordCtrl.ts


+ 0 - 0
public/app/features/org/profile_ctrl.ts → public/app/features/profile/ProfileCtrl.ts


+ 0 - 0
public/app/features/org/partials/change_password.html → public/app/features/profile/partials/change_password.html


+ 0 - 0
public/app/features/org/partials/profile.html → public/app/features/profile/partials/profile.html


+ 0 - 0
public/app/features/org/create_team_ctrl.ts → public/app/features/teams/CreateTeamCtrl.ts


+ 0 - 0
public/app/features/org/partials/create_team.html → public/app/features/teams/partials/create_team.html


+ 8 - 5
public/app/routes/routes.ts

@@ -14,6 +14,7 @@ import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage';
 import NewDataSourcePage from '../features/datasources/NewDataSourcePage';
 import NewDataSourcePage from '../features/datasources/NewDataSourcePage';
 import UsersListPage from 'app/features/users/UsersListPage';
 import UsersListPage from 'app/features/users/UsersListPage';
 import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards';
 import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards';
+import OrgDetailsPage from '../features/org/OrgDetailsPage';
 
 
 /** @ngInject */
 /** @ngInject */
 export function setupAngularRoutes($routeProvider, $locationProvider) {
 export function setupAngularRoutes($routeProvider, $locationProvider) {
@@ -131,8 +132,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
       },
       },
     })
     })
     .when('/org', {
     .when('/org', {
-      templateUrl: 'public/app/features/org/partials/orgDetails.html',
-      controller: 'OrgDetailsCtrl',
+      template: '<react-container />',
+      resolve: {
+        component: () => OrgDetailsPage,
+      },
     })
     })
     .when('/org/new', {
     .when('/org/new', {
       templateUrl: 'public/app/features/org/partials/newOrg.html',
       templateUrl: 'public/app/features/org/partials/newOrg.html',
@@ -164,7 +167,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
       },
       },
     })
     })
     .when('/org/teams/new', {
     .when('/org/teams/new', {
-      templateUrl: 'public/app/features/org/partials/create_team.html',
+      templateUrl: 'public/app/features/teams/partials/create_team.html',
       controller: 'CreateTeamCtrl',
       controller: 'CreateTeamCtrl',
       controllerAs: 'ctrl',
       controllerAs: 'ctrl',
     })
     })
@@ -176,12 +179,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
       },
       },
     })
     })
     .when('/profile', {
     .when('/profile', {
-      templateUrl: 'public/app/features/org/partials/profile.html',
+      templateUrl: 'public/app/features/profile/partials/profile.html',
       controller: 'ProfileCtrl',
       controller: 'ProfileCtrl',
       controllerAs: 'ctrl',
       controllerAs: 'ctrl',
     })
     })
     .when('/profile/password', {
     .when('/profile/password', {
-      templateUrl: 'public/app/features/org/partials/change_password.html',
+      templateUrl: 'public/app/features/profile/partials/change_password.html',
       controller: 'ChangePasswordCtrl',
       controller: 'ChangePasswordCtrl',
     })
     })
     .when('/profile/select-org', {
     .when('/profile/select-org', {

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

@@ -10,6 +10,7 @@ import dashboardReducers from 'app/features/dashboard/state/reducers';
 import pluginReducers from 'app/features/plugins/state/reducers';
 import pluginReducers from 'app/features/plugins/state/reducers';
 import dataSourcesReducers from 'app/features/datasources/state/reducers';
 import dataSourcesReducers from 'app/features/datasources/state/reducers';
 import usersReducers from 'app/features/users/state/reducers';
 import usersReducers from 'app/features/users/state/reducers';
+import organizationReducers from 'app/features/org/state/reducers';
 
 
 const rootReducers = {
 const rootReducers = {
   ...sharedReducers,
   ...sharedReducers,
@@ -21,6 +22,7 @@ const rootReducers = {
   ...pluginReducers,
   ...pluginReducers,
   ...dataSourcesReducers,
   ...dataSourcesReducers,
   ...usersReducers,
   ...usersReducers,
+  ...organizationReducers,
 };
 };
 
 
 export let store;
 export let store;

+ 10 - 1
public/app/types/index.ts

@@ -6,7 +6,7 @@ import { FolderDTO, FolderState, FolderInfo } from './folders';
 import { DashboardState } from './dashboard';
 import { DashboardState } from './dashboard';
 import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
 import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
 import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
 import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
-import { Invitee, OrgUser, User, UsersState } from './user';
+import { Invitee, OrgUser, User, UsersState, UserState } from './user';
 import { DataSource, DataSourcesState } from './datasources';
 import { DataSource, DataSourcesState } from './datasources';
 import {
 import {
   TimeRange,
   TimeRange,
@@ -22,12 +22,14 @@ import {
 } from './series';
 } from './series';
 import { PanelProps } from './panel';
 import { PanelProps } from './panel';
 import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins';
 import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins';
+import { Organization, OrganizationPreferences, OrganizationState } from './organization';
 import {
 import {
   AppNotification,
   AppNotification,
   AppNotificationSeverity,
   AppNotificationSeverity,
   AppNotificationsState,
   AppNotificationsState,
   AppNotificationTimeout,
   AppNotificationTimeout,
 } from './appNotifications';
 } from './appNotifications';
+import { DashboardSearchHit } from './search';
 
 
 export {
 export {
   Team,
   Team,
@@ -76,10 +78,15 @@ export {
   DataQueryResponse,
   DataQueryResponse,
   DataQueryOptions,
   DataQueryOptions,
   PluginDashboard,
   PluginDashboard,
+  Organization,
+  OrganizationState,
+  OrganizationPreferences,
   AppNotification,
   AppNotification,
   AppNotificationsState,
   AppNotificationsState,
   AppNotificationSeverity,
   AppNotificationSeverity,
   AppNotificationTimeout,
   AppNotificationTimeout,
+  DashboardSearchHit,
+  UserState,
 };
 };
 
 
 export interface StoreState {
 export interface StoreState {
@@ -92,5 +99,7 @@ export interface StoreState {
   dashboard: DashboardState;
   dashboard: DashboardState;
   dataSources: DataSourcesState;
   dataSources: DataSourcesState;
   users: UsersState;
   users: UsersState;
+  organization: OrganizationState;
   appNotifications: AppNotificationsState;
   appNotifications: AppNotificationsState;
+  user: UserState;
 }
 }

+ 15 - 0
public/app/types/organization.ts

@@ -0,0 +1,15 @@
+export interface Organization {
+  name: string;
+  id: number;
+}
+
+export interface OrganizationPreferences {
+  homeDashboardId: number;
+  theme: string;
+  timezone: string;
+}
+
+export interface OrganizationState {
+  organization: Organization;
+  preferences: OrganizationPreferences;
+}

+ 9 - 0
public/app/types/search.ts

@@ -0,0 +1,9 @@
+export interface DashboardSearchHit {
+  id: number;
+  tags: string[];
+  title: string;
+  type: string;
+  uid: string;
+  uri: string;
+  url: string;
+}

+ 7 - 1
public/app/types/user.ts

@@ -1,4 +1,6 @@
-export interface OrgUser {
+import { DashboardSearchHit } from './search';
+
+export interface OrgUser {
   avatarUrl: string;
   avatarUrl: string;
   email: string;
   email: string;
   lastSeenAt: string;
   lastSeenAt: string;
@@ -43,3 +45,7 @@ export interface UsersState {
   externalUserMngInfo: string;
   externalUserMngInfo: string;
   hasFetched: boolean;
   hasFetched: boolean;
 }
 }
+
+export interface UserState {
+  starredDashboards: DashboardSearchHit[];
+}