浏览代码

team preferences ui

Marcus Efraimsson 7 年之前
父节点
当前提交
1194ff282e

+ 10 - 2
public/app/features/teams/TeamPages.test.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import { shallow } from 'enzyme';
 import { TeamPages, Props } from './TeamPages';
-import { NavModel, Team } from '../../types';
+import { NavModel, Team, OrganizationPreferences } from '../../types';
 import { getMockTeam } from './__mocks__/teamMocks';
 
 jest.mock('app/core/config', () => ({
@@ -15,6 +15,9 @@ const setup = (propOverrides?: object) => {
     loadTeam: jest.fn(),
     pageName: 'members',
     team: {} as Team,
+    loadStarredDashboards: jest.fn(),
+    loadTeamPreferences: jest.fn(),
+    preferences: {} as OrganizationPreferences,
   };
 
   Object.assign(props, propOverrides);
@@ -43,10 +46,15 @@ describe('Render', () => {
     expect(wrapper).toMatchSnapshot();
   });
 
-  it('should render settings page', () => {
+  it('should render settings and preferences page', () => {
     const { wrapper } = setup({
       team: getMockTeam(),
       pageName: 'settings',
+      preferences: {
+        homeDashboardId: 1,
+        theme: 'Default',
+        timezone: 'Default',
+      },
     });
 
     expect(wrapper).toMatchSnapshot();

+ 21 - 6
public/app/features/teams/TeamPages.tsx

@@ -7,12 +7,14 @@ import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import TeamMembers from './TeamMembers';
 import TeamSettings from './TeamSettings';
 import TeamGroupSync from './TeamGroupSync';
-import { NavModel, Team } from 'app/types';
-import { loadTeam } from './state/actions';
+import TeamPreferences from './TeamPreferences';
+import { NavModel, Team, OrganizationPreferences } from 'app/types';
+import { loadTeam, loadTeamPreferences } from './state/actions';
 import { getTeam } from './state/selectors';
 import { getTeamLoadingNav } from './state/navModel';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location';
+import { loadStarredDashboards } from '../../core/actions/user';
 
 export interface Props {
   team: Team;
@@ -20,6 +22,9 @@ export interface Props {
   teamId: number;
   pageName: string;
   navModel: NavModel;
+  preferences: OrganizationPreferences;
+  loadStarredDashboards: typeof loadStarredDashboards;
+  loadTeamPreferences: typeof loadTeamPreferences;
 }
 
 interface State {
@@ -41,14 +46,16 @@ export class TeamPages extends PureComponent<Props, State> {
     };
   }
 
-  componentDidMount() {
-    this.fetchTeam();
+  async componentDidMount() {
+    await this.props.loadStarredDashboards();
+    await this.fetchTeam();
+    await this.props.loadTeamPreferences();
   }
 
   async fetchTeam() {
     const { loadTeam, teamId } = this.props;
 
-    await loadTeam(teamId);
+    return await loadTeam(teamId);
   }
 
   getCurrentPage() {
@@ -66,7 +73,12 @@ export class TeamPages extends PureComponent<Props, State> {
         return <TeamMembers syncEnabled={isSyncEnabled} />;
 
       case PageTypes.Settings:
-        return <TeamSettings />;
+        return (
+          <div>
+            <TeamSettings />
+            <TeamPreferences />
+          </div>
+        );
 
       case PageTypes.GroupSync:
         return isSyncEnabled && <TeamGroupSync />;
@@ -97,11 +109,14 @@ function mapStateToProps(state) {
     teamId: teamId,
     pageName: pageName,
     team: getTeam(state.team, teamId),
+    preferences: state.preferences,
   };
 }
 
 const mapDispatchToProps = {
   loadTeam,
+  loadStarredDashboards,
+  loadTeamPreferences,
 };
 
 export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamPages));

+ 28 - 0
public/app/features/teams/TeamPreferences.test.tsx

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

+ 102 - 0
public/app/features/teams/TeamPreferences.tsx

@@ -0,0 +1,102 @@
+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 { setTeamHomeDashboard, setTeamTheme, setTeamTimezone, updateTeamPreferences } from './state/actions';
+
+export interface Props {
+  preferences: OrganizationPreferences;
+  starredDashboards: DashboardSearchHit[];
+  setTeamHomeDashboard: typeof setTeamHomeDashboard;
+  setTeamTheme: typeof setTeamTheme;
+  setTeamTimezone: typeof setTeamTimezone;
+  updateTeamPreferences: typeof updateTeamPreferences;
+}
+
+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 TeamPreferences extends PureComponent<Props> {
+  onSubmitForm = event => {
+    event.preventDefault();
+    this.props.updateTeamPreferences();
+  };
+
+  render() {
+    const { preferences, starredDashboards, setTeamHomeDashboard, setTeamTimezone, setTeamTheme } = 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 => setTeamTheme(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) => setTeamHomeDashboard(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 => setTeamTimezone(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.team.preferences,
+    starredDashboards: state.user.starredDashboards,
+  };
+}
+
+const mapDispatchToProps = {
+  setTeamHomeDashboard,
+  setTeamTimezone,
+  setTeamTheme,
+  updateTeamPreferences,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(TeamPreferences);

+ 9 - 1
public/app/features/teams/__mocks__/teamMocks.ts

@@ -1,4 +1,4 @@
-import { Team, TeamGroup, TeamMember } from 'app/types';
+import { Team, TeamGroup, TeamMember, OrganizationPreferences } from 'app/types';
 
 export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
   const teams: Team[] = [];
@@ -65,3 +65,11 @@ export const getMockTeamGroups = (amount: number): TeamGroup[] => {
 
   return groups;
 };
+
+export const getMockTeamPreferences = (): OrganizationPreferences => {
+  return {
+    theme: 'dark',
+    timezone: 'browser',
+    homeDashboardId: 1,
+  };
+};

+ 5 - 2
public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap

@@ -36,7 +36,7 @@ exports[`Render should render member page if team not empty 1`] = `
 </div>
 `;
 
-exports[`Render should render settings page 1`] = `
+exports[`Render should render settings and preferences page 1`] = `
 <div>
   <PageHeader
     model={Object {}}
@@ -44,7 +44,10 @@ exports[`Render should render settings page 1`] = `
   <div
     className="page-container page-body"
   >
-    <Connect(TeamSettings) />
+    <div>
+      <Connect(TeamSettings) />
+      <Connect(TeamPreferences) />
+    </div>
   </div>
 </div>
 `;

+ 136 - 0
public/app/features/teams/__snapshots__/TeamPreferences.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>
+`;

+ 68 - 2
public/app/features/teams/state/actions.ts

@@ -1,16 +1,20 @@
 import { ThunkAction } from 'redux-thunk';
 import { getBackendSrv } from 'app/core/services/backend_srv';
-import { StoreState, Team, TeamGroup, TeamMember } from 'app/types';
+import { StoreState, Team, TeamGroup, TeamMember, OrganizationPreferences } from 'app/types';
 import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
 import { buildNavModel } from './navModel';
 
 export enum ActionTypes {
   LoadTeams = 'LOAD_TEAMS',
   LoadTeam = 'LOAD_TEAM',
+  LoadTeamPreferences = 'LOAD_TEAM_PREFERENCES',
   SetSearchQuery = 'SET_TEAM_SEARCH_QUERY',
   SetSearchMemberQuery = 'SET_TEAM_MEMBER_SEARCH_QUERY',
   LoadTeamMembers = 'TEAM_MEMBERS_LOADED',
   LoadTeamGroups = 'TEAM_GROUPS_LOADED',
+  SetTeamTheme = 'SET_TEAM_THEME',
+  SetTeamHomeDashboard = 'SET_TEAM_HOME_DASHBOARD',
+  SetTeamTimezone = 'SET_TEAM_TIMEZONE',
 }
 
 export interface LoadTeamsAction {
@@ -23,6 +27,11 @@ export interface LoadTeamAction {
   payload: Team;
 }
 
+export interface LoadTeamPreferencesAction {
+  type: ActionTypes.LoadTeamPreferences;
+  payload: OrganizationPreferences;
+}
+
 export interface LoadTeamMembersAction {
   type: ActionTypes.LoadTeamMembers;
   payload: TeamMember[];
@@ -43,13 +52,32 @@ export interface SetSearchMemberQueryAction {
   payload: string;
 }
 
+export interface SetTeamThemeAction {
+  type: ActionTypes.SetTeamTheme;
+  payload: string;
+}
+
+export interface SetTeamHomeDashboardAction {
+  type: ActionTypes.SetTeamHomeDashboard;
+  payload: number;
+}
+
+export interface SetTeamTimezoneAction {
+  type: ActionTypes.SetTeamTimezone;
+  payload: string;
+}
+
 export type Action =
   | LoadTeamsAction
   | SetSearchQueryAction
   | LoadTeamAction
+  | LoadTeamPreferencesAction
   | LoadTeamMembersAction
   | SetSearchMemberQueryAction
-  | LoadTeamGroupsAction;
+  | LoadTeamGroupsAction
+  | SetTeamThemeAction
+  | SetTeamHomeDashboardAction
+  | SetTeamTimezoneAction;
 
 type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action | UpdateNavIndexAction>;
 
@@ -73,6 +101,11 @@ const teamGroupsLoaded = (teamGroups: TeamGroup[]): LoadTeamGroupsAction => ({
   payload: teamGroups,
 });
 
+const teamPreferencesLoaded = (preferences: OrganizationPreferences): LoadTeamPreferencesAction => ({
+  type: ActionTypes.LoadTeamPreferences,
+  payload: preferences,
+});
+
 export const setSearchMemberQuery = (searchQuery: string): SetSearchMemberQueryAction => ({
   type: ActionTypes.SetSearchMemberQuery,
   payload: searchQuery,
@@ -83,6 +116,21 @@ export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({
   payload: searchQuery,
 });
 
+export const setTeamTheme = (theme: string) => ({
+  type: ActionTypes.SetTeamTheme,
+  payload: theme,
+});
+
+export const setTeamHomeDashboard = (id: number) => ({
+  type: ActionTypes.SetTeamHomeDashboard,
+  payload: id,
+});
+
+export const setTeamTimezone = (timezone: string) => ({
+  type: ActionTypes.SetTeamTimezone,
+  payload: timezone,
+});
+
 export function loadTeams(): ThunkResult<void> {
   return async dispatch => {
     const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 });
@@ -160,3 +208,21 @@ export function deleteTeam(id: number): ThunkResult<void> {
     dispatch(loadTeams());
   };
 }
+
+export function loadTeamPreferences(): ThunkResult<void> {
+  return async (dispatch, getStore) => {
+    const team = getStore().team.team;
+    const response = await getBackendSrv().get(`/api/teams/${team.id}/preferences`);
+    dispatch(teamPreferencesLoaded(response));
+  };
+}
+
+export function updateTeamPreferences() {
+  return async (dispatch, getStore) => {
+    const team = getStore().team.team;
+    const preferences = getStore().team.preferences;
+
+    await getBackendSrv().put(`/api/teams/${team.id}/preferences`, preferences);
+    window.location.reload();
+  };
+}

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

@@ -1,6 +1,6 @@
 import { Action, ActionTypes } from './actions';
 import { initialTeamsState, initialTeamState, teamReducer, teamsReducer } from './reducers';
-import { getMockTeam, getMockTeamMember } from '../__mocks__/teamMocks';
+import { getMockTeam, getMockTeamMember, getMockTeamPreferences } from '../__mocks__/teamMocks';
 
 describe('teams reducer', () => {
   it('should set teams', () => {
@@ -69,4 +69,17 @@ describe('team reducer', () => {
 
     expect(result.searchMemberQuery).toEqual('member');
   });
+
+  it('should set team preferences', () => {
+    const mockTeamPrefs = getMockTeamPreferences();
+
+    const action: Action = {
+      type: ActionTypes.LoadTeamPreferences,
+      payload: mockTeamPrefs,
+    };
+
+    const result = teamReducer(initialTeamState, action);
+
+    expect(result.preferences).toEqual(mockTeamPrefs);
+  });
 });

+ 14 - 1
public/app/features/teams/state/reducers.ts

@@ -1,4 +1,4 @@
-import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from 'app/types';
+import { Team, TeamGroup, TeamMember, TeamsState, TeamState, OrganizationPreferences } from 'app/types';
 import { Action, ActionTypes } from './actions';
 
 export const initialTeamsState: TeamsState = { teams: [], searchQuery: '', hasFetched: false };
@@ -7,6 +7,7 @@ export const initialTeamState: TeamState = {
   members: [] as TeamMember[],
   groups: [] as TeamGroup[],
   searchMemberQuery: '',
+  preferences: {} as OrganizationPreferences,
 };
 
 export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => {
@@ -33,6 +34,18 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState
 
     case ActionTypes.LoadTeamGroups:
       return { ...state, groups: action.payload };
+
+    case ActionTypes.LoadTeamPreferences:
+      return { ...state, preferences: action.payload };
+
+    case ActionTypes.SetTeamTheme:
+      return { ...state, preferences: { ...state.preferences, theme: action.payload } };
+
+    case ActionTypes.SetTeamHomeDashboard:
+      return { ...state, preferences: { ...state.preferences, homeDashboardId: action.payload } };
+
+    case ActionTypes.SetTeamTimezone:
+      return { ...state, preferences: { ...state.preferences, timezone: action.payload } };
   }
 
   return state;

+ 9 - 2
public/app/features/teams/state/selectors.test.ts

@@ -1,6 +1,6 @@
 import { getTeam, getTeamMembers, getTeams } from './selectors';
 import { getMockTeam, getMockTeamMembers, getMultipleMockTeams } from '../__mocks__/teamMocks';
-import { Team, TeamGroup, TeamsState, TeamState } from '../../../types';
+import { Team, TeamGroup, TeamsState, TeamState, OrganizationPreferences } from '../../../types';
 
 describe('Teams selectors', () => {
   describe('Get teams', () => {
@@ -29,7 +29,13 @@ describe('Team selectors', () => {
     const mockTeam = getMockTeam();
 
     it('should return team if matching with location team', () => {
-      const mockState: TeamState = { team: mockTeam, searchMemberQuery: '', members: [], groups: [] };
+      const mockState: TeamState = {
+        team: mockTeam,
+        searchMemberQuery: '',
+        members: [],
+        groups: [],
+        preferences: {} as OrganizationPreferences,
+      };
 
       const team = getTeam(mockState, '1');
 
@@ -46,6 +52,7 @@ describe('Team selectors', () => {
         searchMemberQuery: '',
         members: mockTeamMembers,
         groups: [] as TeamGroup[],
+        preferences: {} as OrganizationPreferences,
       };
 
       const members = getTeamMembers(mockState);

+ 3 - 0
public/app/types/teams.ts

@@ -1,3 +1,5 @@
+import { OrganizationPreferences } from './organization';
+
 export interface Team {
   id: number;
   name: string;
@@ -31,4 +33,5 @@ export interface TeamState {
   members: TeamMember[];
   groups: TeamGroup[];
   searchMemberQuery: string;
+  preferences: OrganizationPreferences;
 }