From 545d7b9477323b1376d68c82cbfeef8e7a1cd177 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 11 Jan 2018 15:42:45 +0100 Subject: [PATCH] dashfolders: convert folder settings to React --- pkg/api/index.go | 4 +- public/app/containers/IContainerProps.ts | 2 + .../ManageDashboards/FolderSettings.jest.tsx | 78 +++++++++ .../ManageDashboards/FolderSettings.tsx | 153 ++++++++++++++++++ .../manage_dashboards/manage_dashboards.html | 5 +- public/app/core/services/bridge_srv.ts | 6 +- public/app/core/specs/bridge_srv.jest.ts | 2 +- .../features/dashboard/folder_page_loader.ts | 28 ++-- .../dashboard/folder_settings_ctrl.ts | 2 +- public/app/routes/routes.ts | 8 +- public/app/stores/FolderStore/FolderStore.ts | 45 ++++++ public/app/stores/NavStore/NavStore.jest.ts | 47 ++++++ public/app/stores/NavStore/NavStore.ts | 39 +++++ public/app/stores/RootStore/RootStore.ts | 3 + public/app/stores/ViewStore/ViewStore.ts | 11 +- public/test/mocks/common.ts | 2 + 16 files changed, 408 insertions(+), 27 deletions(-) create mode 100644 public/app/containers/ManageDashboards/FolderSettings.jest.tsx create mode 100644 public/app/containers/ManageDashboards/FolderSettings.tsx create mode 100644 public/app/stores/FolderStore/FolderStore.ts create mode 100644 public/app/stores/NavStore/NavStore.jest.ts diff --git a/pkg/api/index.go b/pkg/api/index.go index 1b836356189..1a086f5fe0d 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -102,8 +102,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { } dashboardChildNavs := []*dtos.NavLink{ - {Text: "Home", Url: setting.AppSubUrl + "/", Icon: "gicon gicon-home", HideFromTabs: true}, - {Divider: true, HideFromTabs: true}, + {Text: "Home", Id: "home", Url: setting.AppSubUrl + "/", Icon: "gicon gicon-home", HideFromTabs: true}, + {Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true}, {Text: "Manage", Id: "manage-dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "gicon gicon-manage"}, {Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "gicon gicon-playlists"}, {Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "gicon gicon-snapshots"}, diff --git a/public/app/containers/IContainerProps.ts b/public/app/containers/IContainerProps.ts index aecba4e8ad3..a5629184864 100644 --- a/public/app/containers/IContainerProps.ts +++ b/public/app/containers/IContainerProps.ts @@ -3,6 +3,7 @@ import { ServerStatsStore } from './../stores/ServerStatsStore/ServerStatsStore' import { NavStore } from './../stores/NavStore/NavStore'; import { AlertListStore } from './../stores/AlertListStore/AlertListStore'; import { ViewStore } from './../stores/ViewStore/ViewStore'; +import { FolderStore } from './../stores/FolderStore/FolderStore'; interface IContainerProps { search: typeof SearchStore.Type; @@ -10,6 +11,7 @@ interface IContainerProps { nav: typeof NavStore.Type; alertList: typeof AlertListStore.Type; view: typeof ViewStore.Type; + folder: typeof FolderStore.Type; } export default IContainerProps; diff --git a/public/app/containers/ManageDashboards/FolderSettings.jest.tsx b/public/app/containers/ManageDashboards/FolderSettings.jest.tsx new file mode 100644 index 00000000000..5bd9e642d51 --- /dev/null +++ b/public/app/containers/ManageDashboards/FolderSettings.jest.tsx @@ -0,0 +1,78 @@ +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.getDashboard.mockReturnValue( + Promise.resolve({ + dashboard: { + id: 1, + title: 'Folder Name', + }, + meta: { + slug: 'folder-name', + canSave: true, + }, + }) + ); + + const store = RootStore.create( + {}, + { + backendSrv: backendSrv, + } + ); + + wrapper = shallow(); + return wrapper + .dive() + .instance() + .loadStore() + .then(() => { + page = wrapper.dive(); + }); + }); + + 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 new file mode 100644 index 00000000000..acf34b925ab --- /dev/null +++ b/public/app/containers/ManageDashboards/FolderSettings.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { inject, observer } from 'mobx-react'; +import { toJS } from 'mobx'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import IContainerProps from 'app/containers/IContainerProps'; +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; + dashboard: any; + + constructor(props) { + super(props); + this.loadStore(); + } + + loadStore() { + const { nav, folder, view } = this.props; + + return folder.load(view.routeParams.get('slug') as string).then(res => { + this.formSnapshot = getSnapshot(folder); + this.dashboard = res.dashboard; + + 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 + .saveDashboard(this.dashboard, { 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); + } + + 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 this.props.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; + + 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.props.folder.saveDashboard(this.dashboard, { overwrite: true }); + }, + }); + } + + if (err.data && err.data.status === 'name-exists') { + err.isHandled = true; + + appEvents.emit('alert-error', ['A folder or dashboard with this name exists already.']); + } + } + + render() { + const { nav, folder } = this.props; + + if (!folder.folder || !nav.main) { + return

Loading

; + } + + return ( +
+ +
+

Folder Settings

+ +
+
+
+ + +
+
+ + +
+ +
+
+
+ ); + } +} diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html index a5bb9204cf8..2a0b037d116 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.html +++ b/public/app/core/components/manage_dashboards/manage_dashboards.html @@ -95,11 +95,12 @@
- + on-tag-selected="ctrl.filterByTag($tag)" + />
diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index 561f651b089..f0166d89e9d 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -10,7 +10,7 @@ export class BridgeSrv { private fullPageReloadRoutes; /** @ngInject */ - constructor(private $location, private $timeout, private $window, private $rootScope) { + constructor(private $location, private $timeout, private $window, private $rootScope, private $route) { this.appSubUrl = config.appSubUrl; this.fullPageReloadRoutes = ['/logout']; } @@ -29,14 +29,14 @@ export class BridgeSrv { this.$rootScope.$on('$routeUpdate', (evt, data) => { let angularUrl = this.$location.url(); if (store.view.currentUrl !== angularUrl) { - store.view.updatePathAndQuery(this.$location.path(), this.$location.search()); + store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); } }); this.$rootScope.$on('$routeChangeSuccess', (evt, data) => { let angularUrl = this.$location.url(); if (store.view.currentUrl !== angularUrl) { - store.view.updatePathAndQuery(this.$location.path(), this.$location.search()); + store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); } }); diff --git a/public/app/core/specs/bridge_srv.jest.ts b/public/app/core/specs/bridge_srv.jest.ts index f29c88d7a05..2e32f9bd600 100644 --- a/public/app/core/specs/bridge_srv.jest.ts +++ b/public/app/core/specs/bridge_srv.jest.ts @@ -10,7 +10,7 @@ describe('BridgeSrv', () => { let searchSrv; beforeEach(() => { - searchSrv = new BridgeSrv(null, null, null, null); + searchSrv = new BridgeSrv(null, null, null, null, null); }); describe('With /subUrl as appSubUrl', () => { diff --git a/public/app/features/dashboard/folder_page_loader.ts b/public/app/features/dashboard/folder_page_loader.ts index 2f9ef382a8e..ae610cb8014 100755 --- a/public/app/features/dashboard/folder_page_loader.ts +++ b/public/app/features/dashboard/folder_page_loader.ts @@ -43,33 +43,33 @@ export class FolderPageLoader { ctrl.navModel.main.text = ''; ctrl.navModel.main.breadcrumbs = [{ title: 'Dashboards', url: 'dashboards' }, { title: folderTitle }]; - const folderUrl = this.createFolderUrl(folderId, result.meta.type, result.meta.slug); + const folderUrl = this.createFolderUrl(folderId, result.meta.slug); const dashTab = _.find(ctrl.navModel.main.children, { id: 'manage-folder-dashboards', }); dashTab.url = folderUrl; - if (result.meta.canAdmin) { - const permTab = _.find(ctrl.navModel.main.children, { - id: 'manage-folder-permissions', - }); + if (result.meta.canAdmin) { + const permTab = _.find(ctrl.navModel.main.children, { + id: 'manage-folder-permissions', + }); - permTab.url = folderUrl + '/permissions'; + permTab.url = folderUrl + '/permissions'; - const settingsTab = _.find(ctrl.navModel.main.children, { - id: 'manage-folder-settings', - }); - settingsTab.url = folderUrl + '/settings'; - } else { - ctrl.navModel.main.children = [dashTab]; - } + const settingsTab = _.find(ctrl.navModel.main.children, { + id: 'manage-folder-settings', + }); + settingsTab.url = folderUrl + '/settings'; + } else { + ctrl.navModel.main.children = [dashTab]; + } return result; }); } - createFolderUrl(folderId: number, type: string, slug: string) { + createFolderUrl(folderId: number, slug: string) { return `dashboards/folder/${folderId}/${slug}`; } } diff --git a/public/app/features/dashboard/folder_settings_ctrl.ts b/public/app/features/dashboard/folder_settings_ctrl.ts index 88abc5f9874..7bb3d469a86 100644 --- a/public/app/features/dashboard/folder_settings_ctrl.ts +++ b/public/app/features/dashboard/folder_settings_ctrl.ts @@ -38,7 +38,7 @@ export class FolderSettingsCtrl { return this.backendSrv .saveDashboard(this.dashboard, { overwrite: false }) .then(result => { - var folderUrl = this.folderPageLoader.createFolderUrl(this.folderId, this.meta.type, result.slug); + var folderUrl = this.folderPageLoader.createFolderUrl(this.folderId, result.slug); if (folderUrl !== this.$location.path()) { this.$location.url(folderUrl + '/settings'); } diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index ec65e4ec25a..f4285cbeb86 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -2,6 +2,7 @@ import './dashboard_loaders'; import './ReactContainer'; import { ServerStats } from 'app/containers/ServerStats/ServerStats'; import { AlertRuleList } from 'app/containers/AlertRuleList/AlertRuleList'; +import { FolderSettings } from 'app/containers/ManageDashboards/FolderSettings'; /** @ngInject **/ export function setupAngularRoutes($routeProvider, $locationProvider) { @@ -68,9 +69,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controllerAs: 'ctrl', }) .when('/dashboards/folder/:folderId/:slug/settings', { - templateUrl: 'public/app/features/dashboard/partials/folder_settings.html', - controller: 'FolderSettingsCtrl', - controllerAs: 'ctrl', + template: '', + resolve: { + component: () => FolderSettings, + }, }) .when('/dashboards/folder/:folderId/:slug', { templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html', diff --git a/public/app/stores/FolderStore/FolderStore.ts b/public/app/stores/FolderStore/FolderStore.ts new file mode 100644 index 00000000000..1928fbcad61 --- /dev/null +++ b/public/app/stores/FolderStore/FolderStore.ts @@ -0,0 +1,45 @@ +import { types, getEnv, flow } from 'mobx-state-tree'; + +export const Folder = types.model('Folder', { + id: types.identifier(types.number), + slug: types.string, + title: types.string, + canSave: types.boolean, + hasChanged: types.boolean, +}); + +export const FolderStore = types + .model('FolderStore', { + folder: types.maybe(Folder), + }) + .actions(self => ({ + load: flow(function* load(slug: string) { + const backendSrv = getEnv(self).backendSrv; + const res = yield backendSrv.getDashboard('db', slug); + self.folder = Folder.create({ + id: res.dashboard.id, + title: res.dashboard.title, + slug: res.meta.slug, + canSave: res.meta.canSave, + hasChanged: false, + }); + return res; + }), + setTitle: function(originalTitle: string, title: string) { + self.folder.title = title; + self.folder.hasChanged = originalTitle.toLowerCase() !== title.trim().toLowerCase() && title.trim().length > 0; + }, + saveDashboard: flow(function* saveDashboard(dashboard: any, options: any) { + const backendSrv = getEnv(self).backendSrv; + dashboard.title = self.folder.title.trim(); + + const res = yield backendSrv.saveDashboard(dashboard, options); + self.folder.slug = res.slug; + return `dashboards/folder/${self.folder.id}/${res.slug}/settings`; + }), + deleteFolder: flow(function* deleteFolder() { + const backendSrv = getEnv(self).backendSrv; + + return backendSrv.deleteDashboard(self.folder.slug); + }), + })); diff --git a/public/app/stores/NavStore/NavStore.jest.ts b/public/app/stores/NavStore/NavStore.jest.ts new file mode 100644 index 00000000000..b1ad820910d --- /dev/null +++ b/public/app/stores/NavStore/NavStore.jest.ts @@ -0,0 +1,47 @@ +import { NavStore } from './NavStore'; + +describe('NavStore', () => { + const folderId = 1; + const folderTitle = 'Folder Name'; + const folderSlug = 'folder-name'; + const canAdmin = true; + + const folder = { + id: folderId, + slug: folderSlug, + title: folderTitle, + canAdmin: canAdmin, + }; + + let store; + + beforeEach(() => { + store = NavStore.create(); + store.initFolderNav(folder, 'manage-folder-settings'); + }); + + it('Should set text', () => { + expect(store.main.text).toBe(folderTitle); + }); + + it('Should load nav with tabs', () => { + expect(store.main.children.length).toBe(3); + expect(store.main.children[0].id).toBe('manage-folder-dashboards'); + expect(store.main.children[1].id).toBe('manage-folder-permissions'); + expect(store.main.children[2].id).toBe('manage-folder-settings'); + }); + + it('Should set correct urls for each tab', () => { + expect(store.main.children.length).toBe(3); + expect(store.main.children[0].url).toBe(`dashboards/folder/${folderId}/${folderSlug}`); + expect(store.main.children[1].url).toBe(`dashboards/folder/${folderId}/${folderSlug}/permissions`); + expect(store.main.children[2].url).toBe(`dashboards/folder/${folderId}/${folderSlug}/settings`); + }); + + it('Should set active tab', () => { + expect(store.main.children.length).toBe(3); + expect(store.main.children[0].active).toBe(false); + expect(store.main.children[1].active).toBe(false); + expect(store.main.children[2].active).toBe(true); + }); +}); diff --git a/public/app/stores/NavStore/NavStore.ts b/public/app/stores/NavStore/NavStore.ts index 1cf622fc3df..e6491e72372 100644 --- a/public/app/stores/NavStore/NavStore.ts +++ b/public/app/stores/NavStore/NavStore.ts @@ -38,4 +38,43 @@ export const NavStore = types self.main = NavItem.create(main); self.node = NavItem.create(node); }, + initFolderNav(folder: any, activeChildId: string) { + const folderUrl = createFolderUrl(folder.id, folder.slug); + + self.main = { + icon: 'fa fa-folder-open', + id: 'manage-folder', + subTitle: 'Manage folder dashboards & permissions', + url: '', + text: folder.title, + breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], + children: [ + { + active: activeChildId === 'manage-folder-dashboards', + icon: 'fa fa-fw fa-th-large', + id: 'manage-folder-dashboards', + text: 'Dashboards', + url: folderUrl, + }, + { + active: activeChildId === 'manage-folder-permissions', + icon: 'fa fa-fw fa-lock', + id: 'manage-folder-permissions', + text: 'Permissions', + url: folderUrl + '/permissions', + }, + { + active: activeChildId === 'manage-folder-settings', + icon: 'fa fa-fw fa-cog', + id: 'manage-folder-settings', + text: 'Settings', + url: folderUrl + '/settings', + }, + ], + }; + }, })); + +function createFolderUrl(folderId: number, slug: string) { + return `dashboards/folder/${folderId}/${slug}`; +} diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts index 88221c0ebd5..87709464bbc 100644 --- a/public/app/stores/RootStore/RootStore.ts +++ b/public/app/stores/RootStore/RootStore.ts @@ -4,6 +4,7 @@ import { ServerStatsStore } from './../ServerStatsStore/ServerStatsStore'; import { NavStore } from './../NavStore/NavStore'; import { AlertListStore } from './../AlertListStore/AlertListStore'; import { ViewStore } from './../ViewStore/ViewStore'; +import { FolderStore } from './../FolderStore/FolderStore'; export const RootStore = types.model({ search: types.optional(SearchStore, { @@ -19,7 +20,9 @@ export const RootStore = types.model({ view: types.optional(ViewStore, { path: '', query: {}, + routeParams: {}, }), + folder: types.optional(FolderStore, {}), }); type IRootStoreType = typeof RootStore.Type; diff --git a/public/app/stores/ViewStore/ViewStore.ts b/public/app/stores/ViewStore/ViewStore.ts index fb1111cf108..54af0584773 100644 --- a/public/app/stores/ViewStore/ViewStore.ts +++ b/public/app/stores/ViewStore/ViewStore.ts @@ -15,6 +15,7 @@ export const ViewStore = types .model({ path: types.string, query: types.map(QueryValueType), + routeParams: types.map(QueryValueType), }) .views(self => ({ get currentUrl() { @@ -34,9 +35,17 @@ export const ViewStore = types } } - function updatePathAndQuery(path: string, query: any) { + function updateRouteParams(routeParams: any) { + self.routeParams.clear(); + for (let key of Object.keys(routeParams)) { + self.routeParams.set(key, routeParams[key]); + } + } + + function updatePathAndQuery(path: string, query: any, routeParams: any) { self.path = path; updateQuery(query); + updateRouteParams(routeParams); } return { diff --git a/public/test/mocks/common.ts b/public/test/mocks/common.ts index 0c0ae0aea6b..da63381ddf4 100644 --- a/public/test/mocks/common.ts +++ b/public/test/mocks/common.ts @@ -1,5 +1,6 @@ export const backendSrv = { get: jest.fn(), + getDashboard: jest.fn(), post: jest.fn(), }; @@ -11,5 +12,6 @@ export function createNavTree(...args) { node.push(child); node = child.children; } + return root; }