From f2edb82e797d38ec8a53ee2bffd0c044a6571f6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 14:10:51 +0200 Subject: [PATCH] Folder pages to redux (#13235) * creating types, actions, reducer * load teams and store in redux * delete team * set search query action and tests * Teampages page * team members, bug in fetching team * flattened team state, tests for TeamMembers * test for team member selector * wip: began folder to redux migration * team settings * actions for group sync * wip: progress on redux folder store * wip: folder to redux * wip: folder settings page to redux progress * mobx -> redux: major progress on folder migration * redux: moved folders to it's own features folder * fix: added loading nav states * fix: gofmt issues * wip: working on reducer test * fix: added reducer test --- public/app/containers/ContainerProps.ts | 14 -- .../ManageDashboards/FolderSettings.test.tsx | 84 --------- .../ManageDashboards/FolderSettings.tsx | 160 ------------------ public/app/core/reducers/location.ts | 4 +- public/app/core/selectors/navModel.ts | 10 +- public/app/core/services/backend_srv.ts | 10 -- public/app/features/dashboard/all.ts | 2 - .../dashboard/folder_settings_ctrl.ts | 94 ---------- .../folders}/FolderPermissions.tsx | 56 +++--- .../folders/FolderSettingsPage.test.tsx | 55 ++++++ .../features/folders/FolderSettingsPage.tsx | 105 ++++++++++++ .../FolderSettingsPage.test.tsx.snap | 131 ++++++++++++++ public/app/features/folders/state/actions.ts | 67 ++++++++ public/app/features/folders/state/navModel.ts | 53 ++++++ .../features/folders/state/reducers.test.ts | 42 +++++ public/app/features/folders/state/reducers.ts | 33 ++++ public/app/features/teams/TeamPages.tsx | 8 +- public/app/features/teams/state/actions.ts | 117 +++---------- public/app/features/teams/state/navModel.ts | 67 ++++++++ public/app/routes/routes.ts | 6 +- public/app/stores/FolderStore/FolderStore.ts | 60 ------- public/app/stores/RootStore/RootStore.ts | 2 - public/app/stores/configureStore.ts | 2 + public/app/types/folder.ts | 18 ++ public/app/types/index.ts | 4 + yarn.lock | 30 +--- 26 files changed, 656 insertions(+), 578 deletions(-) delete mode 100644 public/app/containers/ContainerProps.ts delete mode 100644 public/app/containers/ManageDashboards/FolderSettings.test.tsx delete mode 100644 public/app/containers/ManageDashboards/FolderSettings.tsx delete mode 100644 public/app/features/dashboard/folder_settings_ctrl.ts rename public/app/{containers/ManageDashboards => features/folders}/FolderPermissions.tsx (60%) create mode 100644 public/app/features/folders/FolderSettingsPage.test.tsx create mode 100644 public/app/features/folders/FolderSettingsPage.tsx create mode 100644 public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap create mode 100644 public/app/features/folders/state/actions.ts create mode 100644 public/app/features/folders/state/navModel.ts create mode 100644 public/app/features/folders/state/reducers.test.ts create mode 100644 public/app/features/folders/state/reducers.ts create mode 100644 public/app/features/teams/state/navModel.ts delete mode 100644 public/app/stores/FolderStore/FolderStore.ts create mode 100644 public/app/types/folder.ts diff --git a/public/app/containers/ContainerProps.ts b/public/app/containers/ContainerProps.ts deleted file mode 100644 index ce09b992f80..00000000000 --- a/public/app/containers/ContainerProps.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NavStore } from './../stores/NavStore/NavStore'; -import { PermissionsStore } from './../stores/PermissionsStore/PermissionsStore'; -import { ViewStore } from './../stores/ViewStore/ViewStore'; -import { FolderStore } from './../stores/FolderStore/FolderStore'; - -interface ContainerProps { - nav: typeof NavStore.Type; - permissions: typeof PermissionsStore.Type; - view: typeof ViewStore.Type; - folder: typeof FolderStore.Type; - backendSrv: any; -} - -export default ContainerProps; diff --git a/public/app/containers/ManageDashboards/FolderSettings.test.tsx b/public/app/containers/ManageDashboards/FolderSettings.test.tsx deleted file mode 100644 index bed3d569bcc..00000000000 --- a/public/app/containers/ManageDashboards/FolderSettings.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import { FolderSettings } from './FolderSettings'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv } from 'test/mocks/common'; -import { shallow } from 'enzyme'; - -describe('FolderSettings', () => { - let wrapper; - let page; - - beforeAll(() => { - backendSrv.getFolderByUid.mockReturnValue( - Promise.resolve({ - id: 1, - uid: 'uid', - title: 'Folder Name', - url: '/dashboards/f/uid/folder-name', - canSave: true, - version: 1, - }) - ); - - const store = RootStore.create( - { - view: { - path: 'asd', - query: {}, - routeParams: { - uid: 'uid-str', - }, - }, - }, - { - backendSrv: backendSrv, - } - ); - - wrapper = shallow(); - page = wrapper.dive(); - return page - .instance() - .loadStore() - .then(() => { - page.update(); - }); - }); - - it('should set the title input field', () => { - const titleInput = page.find('.gf-form-input'); - expect(titleInput).toHaveLength(1); - expect(titleInput.prop('value')).toBe('Folder Name'); - }); - - it('should update title and enable save button when changed', () => { - const titleInput = page.find('.gf-form-input'); - const disabledSubmitButton = page.find('button[type="submit"]'); - expect(disabledSubmitButton.prop('disabled')).toBe(true); - - titleInput.simulate('change', { target: { value: 'New Title' } }); - - const updatedTitleInput = page.find('.gf-form-input'); - expect(updatedTitleInput.prop('value')).toBe('New Title'); - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(false); - }); - - it('should disable save button if title is changed back to old title', () => { - const titleInput = page.find('.gf-form-input'); - - titleInput.simulate('change', { target: { value: 'Folder Name' } }); - - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(true); - }); - - it('should disable save button if title is changed to empty string', () => { - const titleInput = page.find('.gf-form-input'); - - titleInput.simulate('change', { target: { value: '' } }); - - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(true); - }); -}); diff --git a/public/app/containers/ManageDashboards/FolderSettings.tsx b/public/app/containers/ManageDashboards/FolderSettings.tsx deleted file mode 100644 index 88830356563..00000000000 --- a/public/app/containers/ManageDashboards/FolderSettings.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; -import { toJS } from 'mobx'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import ContainerProps from 'app/containers/ContainerProps'; -import { getSnapshot } from 'mobx-state-tree'; -import appEvents from 'app/core/app_events'; - -@inject('nav', 'folder', 'view') -@observer -export class FolderSettings extends React.Component { - formSnapshot: any; - - componentDidMount() { - this.loadStore(); - } - - loadStore() { - const { nav, folder, view } = this.props; - - return folder.load(view.routeParams.get('uid') as string).then(res => { - this.formSnapshot = getSnapshot(folder); - view.updatePathAndQuery(`${res.url}/settings`, {}, {}); - - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }); - } - - onTitleChange(evt) { - this.props.folder.setTitle(this.getFormSnapshot().folder.title, evt.target.value); - } - - getFormSnapshot() { - if (!this.formSnapshot) { - this.formSnapshot = getSnapshot(this.props.folder); - } - - return this.formSnapshot; - } - - save(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - const { nav, folder, view } = this.props; - - folder - .saveFolder({ overwrite: false }) - .then(newUrl => { - view.updatePathAndQuery(newUrl, {}, {}); - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .then(() => { - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }) - .catch(this.handleSaveFolderError.bind(this)); - } - - delete(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - const { folder, view } = this.props; - const title = folder.folder.title; - - appEvents.emit('confirm-modal', { - title: 'Delete', - text: `Do you want to delete this folder and all its dashboards?`, - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: () => { - return folder.deleteFolder().then(() => { - appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]); - view.updatePathAndQuery('dashboards', '', ''); - }); - }, - }); - } - - handleSaveFolderError(err) { - if (err.data && err.data.status === 'version-mismatch') { - err.isHandled = true; - - const { nav, folder, view } = this.props; - - appEvents.emit('confirm-modal', { - title: 'Conflict', - text: 'Someone else has updated this folder.', - text2: 'Would you still like to save this folder?', - yesText: 'Save & Overwrite', - icon: 'fa-warning', - onConfirm: () => { - folder - .saveFolder({ overwrite: true }) - .then(newUrl => { - view.updatePathAndQuery(newUrl, {}, {}); - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .then(() => { - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }); - }, - }); - } - } - - render() { - const { nav, folder } = this.props; - - if (!folder.folder || !nav.main) { - return

Loading

; - } - - return ( -
- -
-

Folder Settings

- -
-
-
- - -
-
- - -
- -
-
-
- ); - } -} - -export default hot(module)(FolderSettings); diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index 4591448d082..6a356c4ea5a 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -9,8 +9,8 @@ export const initialState: LocationState = { routeParams: {}, }; -function renderUrl(path: string, query: UrlQueryMap): string { - if (Object.keys(query).length > 0) { +function renderUrl(path: string, query: UrlQueryMap | undefined): string { + if (query && Object.keys(query).length > 0) { path += '?' + toUrlParams(query); } return path; diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index 8b3a3edd84e..aa508616962 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -15,7 +15,7 @@ function getNotFoundModel(): NavModel { }; } -export function getNavModel(navIndex: NavIndex, id: string): NavModel { +export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel): NavModel { if (navIndex[id]) { const node = navIndex[id]; const main = { @@ -33,7 +33,11 @@ export function getNavModel(navIndex: NavIndex, id: string): NavModel { node: node, main: main, }; - } else { - return getNotFoundModel(); } + + if (fallback) { + return fallback; + } + + return getNotFoundModel(); } diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 2a50a1b1f12..3e8132a695b 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -252,16 +252,6 @@ export class BackendSrv { return this.post('/api/folders', payload); } - updateFolder(folder, options) { - options = options || {}; - - return this.put(`/api/folders/${folder.uid}`, { - title: folder.title, - version: folder.version, - overwrite: options.overwrite === true, - }); - } - deleteFolder(uid: string, showSuccessAlert) { return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true }); } diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index 1e28a3c9a80..adb665c47b5 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -32,11 +32,9 @@ import './dashlinks/module'; import coreModule from 'app/core/core_module'; import { FolderDashboardsCtrl } from './folder_dashboards_ctrl'; -import { FolderSettingsCtrl } from './folder_settings_ctrl'; import { DashboardImportCtrl } from './dashboard_import_ctrl'; import { CreateFolderCtrl } from './create_folder_ctrl'; coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl); -coreModule.controller('FolderSettingsCtrl', FolderSettingsCtrl); coreModule.controller('DashboardImportCtrl', DashboardImportCtrl); coreModule.controller('CreateFolderCtrl', CreateFolderCtrl); diff --git a/public/app/features/dashboard/folder_settings_ctrl.ts b/public/app/features/dashboard/folder_settings_ctrl.ts deleted file mode 100644 index a847c29ac56..00000000000 --- a/public/app/features/dashboard/folder_settings_ctrl.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { FolderPageLoader } from './folder_page_loader'; -import appEvents from 'app/core/app_events'; - -export class FolderSettingsCtrl { - folderPageLoader: FolderPageLoader; - navModel: any; - folderId: number; - uid: string; - canSave = false; - folder: any; - title: string; - hasChanged: boolean; - - /** @ngInject */ - constructor(private backendSrv, navModelSrv, private $routeParams, private $location) { - if (this.$routeParams.uid) { - this.uid = $routeParams.uid; - - this.folderPageLoader = new FolderPageLoader(this.backendSrv); - this.folderPageLoader.load(this, this.uid, 'manage-folder-settings').then(folder => { - if ($location.path() !== folder.meta.url) { - $location.path(`${folder.meta.url}/settings`).replace(); - } - - this.folder = folder; - this.canSave = this.folder.canSave; - this.title = this.folder.title; - }); - } - } - - save() { - this.titleChanged(); - - if (!this.hasChanged) { - return; - } - - this.folder.title = this.title.trim(); - - return this.backendSrv - .updateFolder(this.folder) - .then(result => { - if (result.url !== this.$location.path()) { - this.$location.url(result.url + '/settings'); - } - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .catch(this.handleSaveFolderError); - } - - titleChanged() { - this.hasChanged = this.folder.title.toLowerCase() !== this.title.trim().toLowerCase(); - } - - delete(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - appEvents.emit('confirm-modal', { - title: 'Delete', - text: `Do you want to delete this folder and all its dashboards?`, - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: () => { - return this.backendSrv.deleteFolder(this.uid).then(() => { - appEvents.emit('alert-success', ['Folder Deleted', `${this.folder.title} has been deleted`]); - this.$location.url('dashboards'); - }); - }, - }); - } - - handleSaveFolderError(err) { - if (err.data && err.data.status === 'version-mismatch') { - err.isHandled = true; - - appEvents.emit('confirm-modal', { - title: 'Conflict', - text: 'Someone else has updated this folder.', - text2: 'Would you still like to save this folder?', - yesText: 'Save & Overwrite', - icon: 'fa-warning', - onConfirm: () => { - this.backendSrv.updateFolder(this.folder, { overwrite: true }); - }, - }); - } - } -} diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx similarity index 60% rename from public/app/containers/ManageDashboards/FolderPermissions.tsx rename to public/app/features/folders/FolderPermissions.tsx index 072908d2b8e..512927c24e6 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -1,25 +1,38 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; -import { toJS } from 'mobx'; -import ContainerProps from 'app/containers/ContainerProps'; +import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { getFolderByUid } from './state/actions'; +import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore'; +import { getLoadingNav } from './state/navModel'; -@inject('nav', 'folder', 'view', 'permissions') +export interface Props { + navModel: NavModel; + getFolderByUid: typeof getFolderByUid; + folderUid: string; + folder: FolderState; + permissions: typeof PermissionsStore.Type; + backendSrv: any; +} + +@inject('permissions') @observer -export class FolderPermissions extends Component { +export class FolderPermissions extends Component { constructor(props) { super(props); this.handleAddPermission = this.handleAddPermission.bind(this); } componentDidMount() { - this.loadStore(); + this.props.getFolderByUid(this.props.folderUid); } componentWillUnmount() { @@ -27,31 +40,23 @@ export class FolderPermissions extends Component { permissions.hideAddPermissions(); } - loadStore() { - const { nav, folder, view } = this.props; - return folder.load(view.routeParams.get('uid') as string).then(res => { - view.updatePathAndQuery(`${res.url}/permissions`, {}, {}); - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions'); - }); - } - handleAddPermission() { const { permissions } = this.props; permissions.toggleAddPermissions(); } render() { - const { nav, folder, permissions, backendSrv } = this.props; + const { navModel, permissions, backendSrv, folder } = this.props; - if (!folder.folder || !nav.main) { - return

Loading

; + if (folder.id === 0) { + return ; } - const dashboardId = folder.folder.id; + const dashboardId = folder.id; return (
- +

Folder Permissions

@@ -77,4 +82,17 @@ export class FolderPermissions extends Component { } } -export default hot(module)(FolderPermissions); +const mapStateToProps = (state: StoreState) => { + const uid = state.location.routeParams.uid; + return { + navModel: getNavModel(state.navIndex, `folder-permissions-${uid}`, getLoadingNav(1)), + folderUid: uid, + folder: state.folder, + }; +}; + +const mapDispatchToProps = { + getFolderByUid, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions)); diff --git a/public/app/features/folders/FolderSettingsPage.test.tsx b/public/app/features/folders/FolderSettingsPage.test.tsx new file mode 100644 index 00000000000..3680fa9a197 --- /dev/null +++ b/public/app/features/folders/FolderSettingsPage.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { FolderSettingsPage, Props } from './FolderSettingsPage'; +import { NavModel } from 'app/types'; +import { shallow } from 'enzyme'; + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + folderUid: '1234', + folder: { + id: 0, + uid: '1234', + title: 'loading', + canSave: true, + url: 'url', + hasChanged: false, + version: 1, + }, + getFolderByUid: jest.fn(), + setFolderTitle: jest.fn(), + saveFolder: jest.fn(), + deleteFolder: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as FolderSettingsPage; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should enable save button', () => { + const { wrapper } = setup({ + folder: { + id: 1, + uid: '1234', + title: 'loading', + canSave: true, + hasChanged: true, + version: 1, + }, + }); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/folders/FolderSettingsPage.tsx b/public/app/features/folders/FolderSettingsPage.tsx new file mode 100644 index 00000000000..1eb7ccafc65 --- /dev/null +++ b/public/app/features/folders/FolderSettingsPage.tsx @@ -0,0 +1,105 @@ +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 appEvents from 'app/core/app_events'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; +import { getLoadingNav } from './state/navModel'; + +export interface Props { + navModel: NavModel; + folderUid: string; + folder: FolderState; + getFolderByUid: typeof getFolderByUid; + setFolderTitle: typeof setFolderTitle; + saveFolder: typeof saveFolder; + deleteFolder: typeof deleteFolder; +} + +export class FolderSettingsPage extends PureComponent { + componentDidMount() { + this.props.getFolderByUid(this.props.folderUid); + } + + onTitleChange = evt => { + this.props.setFolderTitle(evt.target.value); + }; + + onSave = async evt => { + evt.preventDefault(); + evt.stopPropagation(); + + await this.props.saveFolder(this.props.folder); + }; + + onDelete = evt => { + evt.stopPropagation(); + evt.preventDefault(); + + appEvents.emit('confirm-modal', { + title: 'Delete', + text: `Do you want to delete this folder and all its dashboards?`, + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + this.props.deleteFolder(this.props.folder.uid); + }, + }); + }; + + render() { + const { navModel, folder } = this.props; + + return ( +
+ +
+

Folder Settings

+ +
+
+
+ + +
+
+ + +
+ +
+
+
+ ); + } +} + +const mapStateToProps = (state: StoreState) => { + const uid = state.location.routeParams.uid; + + return { + navModel: getNavModel(state.navIndex, `folder-settings-${uid}`, getLoadingNav(2)), + folderUid: uid, + folder: state.folder, + }; +}; + +const mapDispatchToProps = { + getFolderByUid, + saveFolder, + setFolderTitle, + deleteFolder, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderSettingsPage)); diff --git a/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap new file mode 100644 index 00000000000..2de0c193d27 --- /dev/null +++ b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap @@ -0,0 +1,131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should enable save button 1`] = ` +
+ +
+

+ Folder Settings +

+
+
+
+ + +
+
+ + +
+ +
+
+
+`; + +exports[`Render should render component 1`] = ` +
+ +
+

+ Folder Settings +

+
+
+
+ + +
+
+ + +
+ +
+
+
+`; diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts new file mode 100644 index 00000000000..5d153b2fb8a --- /dev/null +++ b/public/app/features/folders/state/actions.ts @@ -0,0 +1,67 @@ +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { StoreState } from 'app/types'; +import { ThunkAction } from 'redux-thunk'; +import { FolderDTO, FolderState } from 'app/types'; +import { updateNavIndex, updateLocation } from 'app/core/actions'; +import { buildNavModel } from './navModel'; +import appEvents from 'app/core/app_events'; + +export enum ActionTypes { + LoadFolder = 'LOAD_FOLDER', + SetFolderTitle = 'SET_FOLDER_TITLE', + SaveFolder = 'SAVE_FOLDER', +} + +export interface LoadFolderAction { + type: ActionTypes.LoadFolder; + payload: FolderDTO; +} + +export interface SetFolderTitleAction { + type: ActionTypes.SetFolderTitle; + payload: string; +} + +export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ + type: ActionTypes.LoadFolder, + payload: folder, +}); + +export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({ + type: ActionTypes.SetFolderTitle, + payload: newTitle, +}); + +export type Action = LoadFolderAction | SetFolderTitleAction; + +type ThunkResult = ThunkAction; + + +export function getFolderByUid(uid: string): ThunkResult { + return async dispatch => { + const folder = await getBackendSrv().getFolderByUid(uid); + dispatch(loadFolder(folder)); + dispatch(updateNavIndex(buildNavModel(folder))); + }; +} + +export function saveFolder(folder: FolderState): ThunkResult { + return async dispatch => { + const res = await getBackendSrv().put(`/api/folders/${folder.uid}`, { + title: folder.title, + version: folder.version, + }); + + // this should be redux action at some point + appEvents.emit('alert-success', ['Folder saved']); + + dispatch(updateLocation({ path: `${res.url}/settings` })); + }; +} + +export function deleteFolder(uid: string): ThunkResult { + return async dispatch => { + await getBackendSrv().deleteFolder(uid, true); + dispatch(updateLocation({ path: `dashboards` })); + }; +} diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts new file mode 100644 index 00000000000..e6ef763d019 --- /dev/null +++ b/public/app/features/folders/state/navModel.ts @@ -0,0 +1,53 @@ +import { FolderDTO, NavModelItem, NavModel } from 'app/types'; + +export function buildNavModel(folder: FolderDTO): NavModelItem { + return { + icon: 'fa fa-folder-open', + id: 'manage-folder', + subTitle: 'Manage folder dashboards & permissions', + url: '', + text: folder.title, + breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], + children: [ + { + active: false, + icon: 'fa fa-fw fa-th-large', + id: `folder-dashboards-${folder.uid}`, + text: 'Dashboards', + url: folder.url, + }, + { + active: false, + icon: 'fa fa-fw fa-lock', + id: `folder-permissions-${folder.uid}`, + text: 'Permissions', + url: `${folder.url}/permissions`, + }, + { + active: false, + icon: 'fa fa-fw fa-cog', + id: `folder-settings-${folder.uid}`, + text: 'Settings', + url: `${folder.url}/settings`, + }, + ], + }; +} + +export function getLoadingNav(tabIndex: number): NavModel { + const main = buildNavModel({ + id: 1, + uid: 'loading', + title: 'Loading', + url: 'url', + canSave: false, + version: 0, + }); + + main.children[tabIndex].active = true; + + return { + main: main, + node: main.children[tabIndex], + }; +} diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts new file mode 100644 index 00000000000..ff37f13f97f --- /dev/null +++ b/public/app/features/folders/state/reducers.test.ts @@ -0,0 +1,42 @@ +import { Action, ActionTypes } from './actions'; +import { FolderDTO } from 'app/types'; +import { inititalState, folderReducer } from './reducers'; + +function getTestFolder(): FolderDTO { + return { + id: 1, + title: 'test folder', + uid: 'asd', + url: 'url', + canSave: true, + version: 0, + }; +} + +describe('folder reducer', () => { + it('should load folder and set hasChanged to false', () => { + const folder = getTestFolder(); + + const action: Action = { + type: ActionTypes.LoadFolder, + payload: folder, + }; + + const state = folderReducer(inititalState, action); + + expect(state.hasChanged).toEqual(false); + expect(state.title).toEqual('test folder'); + }); + + it('should set title', () => { + const action: Action = { + type: ActionTypes.SetFolderTitle, + payload: 'new title', + }; + + const state = folderReducer(inititalState, action); + + expect(state.hasChanged).toEqual(true); + expect(state.title).toEqual('new title'); + }); +}); diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts new file mode 100644 index 00000000000..41ae10d19e5 --- /dev/null +++ b/public/app/features/folders/state/reducers.ts @@ -0,0 +1,33 @@ +import { FolderState } from 'app/types'; +import { Action, ActionTypes } from './actions'; + +export const inititalState: FolderState = { + id: 0, + uid: 'loading', + title: 'loading', + url: '', + canSave: false, + hasChanged: false, + version: 0, +}; + +export const folderReducer = (state = inititalState, action: Action): FolderState => { + switch (action.type) { + case ActionTypes.LoadFolder: + return { + ...action.payload, + hasChanged: false, + }; + case ActionTypes.SetFolderTitle: + return { + ...state, + title: action.payload, + hasChanged: action.payload.trim().length > 0, + }; + } + return state; +}; + +export default { + folder: folderReducer, +}; diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index f28bde518d2..bbc8b7013ca 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -7,10 +7,11 @@ import PageHeader from 'app/core/components/PageHeader/PageHeader'; import TeamMembers from './TeamMembers'; import TeamSettings from './TeamSettings'; import TeamGroupSync from './TeamGroupSync'; -import { NavModel, Team } from '../../types'; +import { NavModel, Team } from 'app/types'; import { loadTeam } from './state/actions'; import { getTeam } from './state/selectors'; -import { getNavModel } from '../../core/selectors/navModel'; +import { getTeamLoadingNav } from './state/navModel'; +import { getNavModel } from 'app/core/selectors/navModel'; import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location'; export interface Props { @@ -89,9 +90,10 @@ export class TeamPages extends PureComponent { function mapStateToProps(state) { const teamId = getRouteParamsId(state.location); const pageName = getRouteParamsPage(state.location) || 'members'; + const teamLoadingNav = getTeamLoadingNav(pageName); return { - navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), + navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav), teamId: teamId, pageName: pageName, team: getTeam(state.team, teamId), diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 91aa899e171..d948dc1c5a3 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,8 +1,8 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from 'app/types'; +import { StoreState, Team, TeamGroup, TeamMember } from 'app/types'; import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; -import config from 'app/core/config'; +import { buildNavModel } from './navModel'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', @@ -90,148 +90,73 @@ export function loadTeams(): ThunkResult { }; } -function buildNavModel(team: Team): NavModelItem { - const navModel = { - img: team.avatarUrl, - id: 'team-' + team.id, - subTitle: 'Manage members & settings', - url: '', - text: team.name, - breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], - children: [ - { - active: false, - icon: 'gicon gicon-team', - id: `team-members-${team.id}`, - text: 'Members', - url: `org/teams/edit/${team.id}/members`, - }, - { - active: false, - icon: 'fa fa-fw fa-sliders', - id: `team-settings-${team.id}`, - text: 'Settings', - url: `org/teams/edit/${team.id}/settings`, - }, - ], - }; - - if (config.buildInfo.isEnterprise) { - navModel.children.push({ - active: false, - icon: 'fa fa-fw fa-refresh', - id: `team-groupsync-${team.id}`, - text: 'External group sync', - url: `org/teams/edit/${team.id}/groupsync`, - }); - } - - return navModel; -} - export function loadTeam(id: number): ThunkResult { return async dispatch => { - await getBackendSrv() - .get(`/api/teams/${id}`) - .then(response => { - dispatch(teamLoaded(response)); - dispatch(updateNavIndex(buildNavModel(response))); - }); + const response = await getBackendSrv().get(`/api/teams/${id}`); + dispatch(teamLoaded(response)); + dispatch(updateNavIndex(buildNavModel(response))); }; } export function loadTeamMembers(): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .get(`/api/teams/${team.id}/members`) - .then(response => { - dispatch(teamMembersLoaded(response)); - }); + const response = await getBackendSrv().get(`/api/teams/${team.id}/members`); + dispatch(teamMembersLoaded(response)); }; } export function addTeamMember(id: number): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .post(`/api/teams/${team.id}/members`, { userId: id }) - .then(() => { - dispatch(loadTeamMembers()); - }); + await getBackendSrv().post(`/api/teams/${team.id}/members`, { userId: id }); + dispatch(loadTeamMembers()); }; } export function removeTeamMember(id: number): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .delete(`/api/teams/${team.id}/members/${id}`) - .then(() => { - dispatch(loadTeamMembers()); - }); + await getBackendSrv().delete(`/api/teams/${team.id}/members/${id}`); + dispatch(loadTeamMembers()); }; } export function updateTeam(name: string, email: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - await getBackendSrv() - .put(`/api/teams/${team.id}`, { - name, - email, - }) - .then(() => { - dispatch(loadTeam(team.id)); - }); + await getBackendSrv().put(`/api/teams/${team.id}`, { name, email }); + dispatch(loadTeam(team.id)); }; } export function loadTeamGroups(): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .get(`/api/teams/${team.id}/groups`) - .then(response => { - dispatch(teamGroupsLoaded(response)); - }); + const response = await getBackendSrv().get(`/api/teams/${team.id}/groups`); + dispatch(teamGroupsLoaded(response)); }; } export function addTeamGroup(groupId: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .post(`/api/teams/${team.id}/groups`, { groupId: groupId }) - .then(() => { - dispatch(loadTeamGroups()); - }); + await getBackendSrv().post(`/api/teams/${team.id}/groups`, { groupId: groupId }); + dispatch(loadTeamGroups()); }; } export function removeTeamGroup(groupId: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .delete(`/api/teams/${team.id}/groups/${groupId}`) - .then(() => { - dispatch(loadTeamGroups()); - }); + await getBackendSrv().delete(`/api/teams/${team.id}/groups/${groupId}`); + dispatch(loadTeamGroups()); }; } export function deleteTeam(id: number): ThunkResult { return async dispatch => { - await getBackendSrv() - .delete(`/api/teams/${id}`) - .then(() => { - dispatch(loadTeams()); - }); + await getBackendSrv().delete(`/api/teams/${id}`); + dispatch(loadTeams()); }; } diff --git a/public/app/features/teams/state/navModel.ts b/public/app/features/teams/state/navModel.ts new file mode 100644 index 00000000000..2fd5a68e680 --- /dev/null +++ b/public/app/features/teams/state/navModel.ts @@ -0,0 +1,67 @@ +import { Team, NavModelItem, NavModel } from 'app/types'; +import config from 'app/core/config'; + +export function buildNavModel(team: Team): NavModelItem { + const navModel = { + img: team.avatarUrl, + id: 'team-' + team.id, + subTitle: 'Manage members & settings', + url: '', + text: team.name, + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: false, + icon: 'gicon gicon-team', + id: `team-members-${team.id}`, + text: 'Members', + url: `org/teams/edit/${team.id}/members`, + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: `team-settings-${team.id}`, + text: 'Settings', + url: `org/teams/edit/${team.id}/settings`, + }, + ], + }; + + if (config.buildInfo.isEnterprise) { + navModel.children.push({ + active: false, + icon: 'fa fa-fw fa-refresh', + id: `team-groupsync-${team.id}`, + text: 'External group sync', + url: `org/teams/edit/${team.id}/groupsync`, + }); + } + + return navModel; +} + +export function getTeamLoadingNav(pageName: string): NavModel { + const main = buildNavModel({ + avatarUrl: 'public/img/user_profile.png', + id: 1, + name: 'Loading', + email: 'loading', + memberCount: 0, + }); + + let node: NavModelItem; + + // find active page + for (const child of main.children) { + if (child.id.indexOf(pageName) > 0) { + child.active = true; + node = child; + break; + } + } + + return { + main: main, + node: node, + }; +} diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 519008d70f5..160250dce96 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -3,10 +3,10 @@ import './ReactContainer'; import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; -import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; import TeamPages from 'app/features/teams/TeamPages'; import TeamList from 'app/features/teams/TeamList'; -import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; +import FolderSettingsPage from 'app/features/folders/FolderSettingsPage'; +import FolderPermissions from 'app/features/folders/FolderPermissions'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { @@ -99,7 +99,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/dashboards/f/:uid/:slug/settings', { template: '', resolve: { - component: () => FolderSettings, + component: () => FolderSettingsPage, }, }) .when('/dashboards/f/:uid/:slug', { diff --git a/public/app/stores/FolderStore/FolderStore.ts b/public/app/stores/FolderStore/FolderStore.ts deleted file mode 100644 index 90932cbe46f..00000000000 --- a/public/app/stores/FolderStore/FolderStore.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { types, getEnv, flow } from 'mobx-state-tree'; - -export const Folder = types.model('Folder', { - id: types.identifier(types.number), - uid: types.string, - title: types.string, - url: types.string, - canSave: types.boolean, - hasChanged: types.boolean, - version: types.number, -}); - -export const FolderStore = types - .model('FolderStore', { - folder: types.maybe(Folder), - }) - .actions(self => ({ - load: flow(function* load(uid: string) { - // clear folder state - if (self.folder && self.folder.uid !== uid) { - self.folder = null; - } - - const backendSrv = getEnv(self).backendSrv; - const res = yield backendSrv.getFolderByUid(uid); - self.folder = Folder.create({ - id: res.id, - uid: res.uid, - title: res.title, - url: res.url, - canSave: res.canSave, - hasChanged: false, - version: res.version, - }); - - return res; - }), - - setTitle: (originalTitle: string, title: string) => { - self.folder.title = title; - self.folder.hasChanged = originalTitle.toLowerCase() !== title.trim().toLowerCase() && title.trim().length > 0; - }, - - saveFolder: flow(function* saveFolder(options: any) { - const backendSrv = getEnv(self).backendSrv; - self.folder.title = self.folder.title.trim(); - - const res = yield backendSrv.updateFolder(self.folder, options); - self.folder.url = res.url; - self.folder.version = res.version; - - return `${self.folder.url}/settings`; - }), - - deleteFolder: flow(function* deleteFolder() { - const backendSrv = getEnv(self).backendSrv; - - return backendSrv.deleteFolder(self.folder.uid); - }), - })); diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts index 37c13f48c61..68125fd1f4c 100644 --- a/public/app/stores/RootStore/RootStore.ts +++ b/public/app/stores/RootStore/RootStore.ts @@ -1,7 +1,6 @@ import { types } from 'mobx-state-tree'; import { NavStore } from './../NavStore/NavStore'; import { ViewStore } from './../ViewStore/ViewStore'; -import { FolderStore } from './../FolderStore/FolderStore'; import { PermissionsStore } from './../PermissionsStore/PermissionsStore'; export const RootStore = types.model({ @@ -15,7 +14,6 @@ export const RootStore = types.model({ query: {}, routeParams: {}, }), - folder: types.optional(FolderStore, {}), }); type RootStoreType = typeof RootStore.Type; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 0cdc07fd31a..e06317853f8 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -4,11 +4,13 @@ import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; +import foldersReducers from 'app/features/folders/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, ...teamsReducers, + ...foldersReducers, }); export let store; diff --git a/public/app/types/folder.ts b/public/app/types/folder.ts new file mode 100644 index 00000000000..6fbe79cce8c --- /dev/null +++ b/public/app/types/folder.ts @@ -0,0 +1,18 @@ +export interface FolderDTO { + id: number; + uid: string; + title: string; + url: string; + version: number; + canSave: boolean; +} + +export interface FolderState { + id: number; + uid: string; + title: string; + url: string; + version: number; + canSave: boolean; + hasChanged: boolean; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 92bcdb32836..52d1ba592c5 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,6 +2,7 @@ import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams'; import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; import { NavModel, NavModelItem, NavIndex } from './navModel'; +import { FolderDTO, FolderState } from './folder'; export { Team, @@ -19,6 +20,8 @@ export { NavIndex, UrlQueryMap, UrlQueryValue, + FolderDTO, + FolderState, }; export interface StoreState { @@ -27,4 +30,5 @@ export interface StoreState { alertRules: AlertRulesState; teams: TeamsState; team: TeamState; + folder: FolderState; } diff --git a/yarn.lock b/yarn.lock index fa079d15b72..2b98ff32766 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3182,7 +3182,7 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -5553,7 +5553,7 @@ import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -6990,10 +6990,6 @@ lodash-es@^4.17.5: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05" -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -7001,25 +6997,11 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" @@ -7103,10 +7085,6 @@ lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -9902,7 +9880,7 @@ readable-stream@~1.1.10: isarray "0.0.1" string_decoder "~0.10.x" -readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0: +readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" dependencies: