diff --git a/public/app/core/actions/user.ts b/public/app/core/actions/user.ts new file mode 100644 index 00000000000..dba0588c058 --- /dev/null +++ b/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 = ThunkAction; + +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 { + return async dispatch => { + const starredDashboards = await getBackendSrv().search({ starred: true }); + dispatch(starredDashboardsLoaded(starredDashboards)); + }; +} diff --git a/public/app/core/components/Label/Label.tsx b/public/app/core/components/Label/Label.tsx index 6d4fd20dbfe..9b8fb6c6e19 100644 --- a/public/app/core/components/Label/Label.tsx +++ b/public/app/core/components/Label/Label.tsx @@ -5,11 +5,12 @@ interface Props { tooltip?: string; for?: string; children: ReactNode; + width?: number; } export const Label: SFC = props => { return ( - + {props.children} {props.tooltip && ( @@ -19,4 +20,3 @@ export const Label: SFC = props => { ); }; - diff --git a/public/app/core/components/Picker/SimplePicker.tsx b/public/app/core/components/Picker/SimplePicker.tsx new file mode 100644 index 00000000000..6c9e8aca199 --- /dev/null +++ b/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 = ({ + className, + defaultValue, + getOptionLabel, + getOptionValue, + onSelected, + options, + placeholder, + width, +}) => { + return ( + { + onOrgNameChange(event.target.value); + }} + value={orgName} + /> + + +
+ +
+ + + ); +}; + +export default OrgProfile; diff --git a/public/app/features/org/select_org_ctrl.ts b/public/app/features/org/SelectOrgCtrl.ts similarity index 100% rename from public/app/features/org/select_org_ctrl.ts rename to public/app/features/org/SelectOrgCtrl.ts diff --git a/public/app/features/org/user_invite_ctrl.ts b/public/app/features/org/UserInviteCtrl.ts similarity index 100% rename from public/app/features/org/user_invite_ctrl.ts rename to public/app/features/org/UserInviteCtrl.ts diff --git a/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap b/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap new file mode 100644 index 00000000000..28806d2bf1d --- /dev/null +++ b/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`] = ` +
+ +
+ +
+
+`; + +exports[`Render should render organization and preferences 1`] = ` +
+ +
+
+ + +
+
+
+`; diff --git a/public/app/features/org/__snapshots__/OrgPreferences.test.tsx.snap b/public/app/features/org/__snapshots__/OrgPreferences.test.tsx.snap new file mode 100644 index 00000000000..06bf464a4a0 --- /dev/null +++ b/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`] = ` +
+

+ Preferences +

+
+ + UI Theme + + +
+
+ + Home Dashboard + + +
+
+ + +
+
+ +
+
+`; diff --git a/public/app/features/org/__snapshots__/OrgProfile.test.tsx.snap b/public/app/features/org/__snapshots__/OrgProfile.test.tsx.snap new file mode 100644 index 00000000000..b49b63e4532 --- /dev/null +++ b/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`] = ` +
+

+ Organization profile +

+
+
+
+ + Organization name + + +
+
+
+ +
+
+
+`; diff --git a/public/app/features/org/all.ts b/public/app/features/org/all.ts index 9cbcec8de0d..c905853cf49 100644 --- a/public/app/features/org/all.ts +++ b/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'; diff --git a/public/app/features/org/org_details_ctrl.ts b/public/app/features/org/org_details_ctrl.ts deleted file mode 100644 index 1d4a92c6e8b..00000000000 --- a/public/app/features/org/org_details_ctrl.ts +++ /dev/null @@ -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); diff --git a/public/app/features/org/partials/orgDetails.html b/public/app/features/org/partials/orgDetails.html deleted file mode 100644 index f5a49e3d3ee..00000000000 --- a/public/app/features/org/partials/orgDetails.html +++ /dev/null @@ -1,21 +0,0 @@ - - -
-

Organization profile

- -
-
-
- Organization name - -
-
- -
- -
-
- -
- - diff --git a/public/app/features/org/prefs_control.ts b/public/app/features/org/prefs_control.ts deleted file mode 100644 index 74dde250eec..00000000000 --- a/public/app/features/org/prefs_control.ts +++ /dev/null @@ -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 = ` -
-

Preferences

- -
- UI Theme -
- -
-
- -
- - Home Dashboard - - Not finding dashboard you want? Star it first, then it should appear in this select box. - - - - -
- -
- -
- -
-
- -
- -
-
-`; - -export function prefsControlDirective() { - return { - restrict: 'E', - controller: PrefsControlCtrl, - bindToController: true, - controllerAs: 'ctrl', - template: template, - scope: { - mode: '@', - }, - }; -} - -coreModule.directive('prefsControl', prefsControlDirective); diff --git a/public/app/features/org/state/actions.ts b/public/app/features/org/state/actions.ts new file mode 100644 index 00000000000..4df9083c323 --- /dev/null +++ b/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 = ThunkAction; + +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 { + return async dispatch => { + const organisationResponse = await getBackendSrv().get('/api/org'); + dispatch(organisationLoaded(organisationResponse)); + + return organisationResponse; + }; +} + +export function loadOrganizationPreferences(): ThunkResult { + 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(); + }; +} diff --git a/public/app/features/org/state/reducers.ts b/public/app/features/org/state/reducers.ts new file mode 100644 index 00000000000..b79f915a731 --- /dev/null +++ b/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, +}; diff --git a/public/app/features/org/change_password_ctrl.ts b/public/app/features/profile/ChangePasswordCtrl.ts similarity index 100% rename from public/app/features/org/change_password_ctrl.ts rename to public/app/features/profile/ChangePasswordCtrl.ts diff --git a/public/app/features/org/profile_ctrl.ts b/public/app/features/profile/ProfileCtrl.ts similarity index 100% rename from public/app/features/org/profile_ctrl.ts rename to public/app/features/profile/ProfileCtrl.ts diff --git a/public/app/features/org/partials/change_password.html b/public/app/features/profile/partials/change_password.html similarity index 100% rename from public/app/features/org/partials/change_password.html rename to public/app/features/profile/partials/change_password.html diff --git a/public/app/features/org/partials/profile.html b/public/app/features/profile/partials/profile.html similarity index 100% rename from public/app/features/org/partials/profile.html rename to public/app/features/profile/partials/profile.html diff --git a/public/app/features/org/create_team_ctrl.ts b/public/app/features/teams/CreateTeamCtrl.ts similarity index 100% rename from public/app/features/org/create_team_ctrl.ts rename to public/app/features/teams/CreateTeamCtrl.ts diff --git a/public/app/features/org/partials/create_team.html b/public/app/features/teams/partials/create_team.html similarity index 100% rename from public/app/features/org/partials/create_team.html rename to public/app/features/teams/partials/create_team.html diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 94b6b2706f2..6770011278f 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -14,6 +14,7 @@ import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage'; import NewDataSourcePage from '../features/datasources/NewDataSourcePage'; import UsersListPage from 'app/features/users/UsersListPage'; import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards'; +import OrgDetailsPage from '../features/org/OrgDetailsPage'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { @@ -131,8 +132,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { }, }) .when('/org', { - templateUrl: 'public/app/features/org/partials/orgDetails.html', - controller: 'OrgDetailsCtrl', + template: '', + resolve: { + component: () => OrgDetailsPage, + }, }) .when('/org/new', { templateUrl: 'public/app/features/org/partials/newOrg.html', @@ -164,7 +167,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { }, }) .when('/org/teams/new', { - templateUrl: 'public/app/features/org/partials/create_team.html', + templateUrl: 'public/app/features/teams/partials/create_team.html', controller: 'CreateTeamCtrl', controllerAs: 'ctrl', }) @@ -176,12 +179,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { }, }) .when('/profile', { - templateUrl: 'public/app/features/org/partials/profile.html', + templateUrl: 'public/app/features/profile/partials/profile.html', controller: 'ProfileCtrl', controllerAs: 'ctrl', }) .when('/profile/password', { - templateUrl: 'public/app/features/org/partials/change_password.html', + templateUrl: 'public/app/features/profile/partials/change_password.html', controller: 'ChangePasswordCtrl', }) .when('/profile/select-org', { diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index ccd027a0b6d..e6d86eaa9c6 100644 --- a/public/app/store/configureStore.ts +++ b/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 dataSourcesReducers from 'app/features/datasources/state/reducers'; import usersReducers from 'app/features/users/state/reducers'; +import organizationReducers from 'app/features/org/state/reducers'; const rootReducers = { ...sharedReducers, @@ -21,6 +22,7 @@ const rootReducers = { ...pluginReducers, ...dataSourcesReducers, ...usersReducers, + ...organizationReducers, }; export let store; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 4b03ef70714..c51622682d4 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -6,7 +6,7 @@ import { FolderDTO, FolderState, FolderInfo } from './folders'; import { DashboardState } from './dashboard'; import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; 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 { TimeRange, @@ -22,12 +22,14 @@ import { } from './series'; import { PanelProps } from './panel'; import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins'; +import { Organization, OrganizationPreferences, OrganizationState } from './organization'; import { AppNotification, AppNotificationSeverity, AppNotificationsState, AppNotificationTimeout, } from './appNotifications'; +import { DashboardSearchHit } from './search'; export { Team, @@ -76,10 +78,15 @@ export { DataQueryResponse, DataQueryOptions, PluginDashboard, + Organization, + OrganizationState, + OrganizationPreferences, AppNotification, AppNotificationsState, AppNotificationSeverity, AppNotificationTimeout, + DashboardSearchHit, + UserState, }; export interface StoreState { @@ -92,5 +99,7 @@ export interface StoreState { dashboard: DashboardState; dataSources: DataSourcesState; users: UsersState; + organization: OrganizationState; appNotifications: AppNotificationsState; + user: UserState; } diff --git a/public/app/types/organization.ts b/public/app/types/organization.ts new file mode 100644 index 00000000000..52cb130e082 --- /dev/null +++ b/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; +} diff --git a/public/app/types/search.ts b/public/app/types/search.ts new file mode 100644 index 00000000000..e5e17288de1 --- /dev/null +++ b/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; +} diff --git a/public/app/types/user.ts b/public/app/types/user.ts index c0b7b135ff8..37c80074dca 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -1,4 +1,6 @@ -export interface OrgUser { +import { DashboardSearchHit } from './search'; + +export interface OrgUser { avatarUrl: string; email: string; lastSeenAt: string; @@ -43,3 +45,7 @@ export interface UsersState { externalUserMngInfo: string; hasFetched: boolean; } + +export interface UserState { + starredDashboards: DashboardSearchHit[]; +}