Commit 545d7b94 by Daniel Lee

dashfolders: convert folder settings to React

parent e1aff1d5
......@@ -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"},
......
......@@ -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;
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(<FolderSettings {...store} />);
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);
});
});
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<IContainerProps, any> {
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 <h2>Loading</h2>;
}
return (
<div>
<PageHeader model={nav as any} />
<div className="page-container page-body">
<h2 className="page-sub-heading">Folder Settings</h2>
<div className="section gf-form-group">
<form name="folderSettingsForm" onSubmit={this.save.bind(this)}>
<div className="gf-form">
<label className="gf-form-label width-7">Name</label>
<input
type="text"
className="gf-form-input width-30"
value={folder.folder.title}
onChange={this.onTitleChange.bind(this)}
/>
</div>
<div className="gf-form-button-row">
<button
type="submit"
className="btn btn-success"
disabled={!folder.folder.canSave || !folder.folder.hasChanged}
>
<i className="fa fa-trash" /> Save
</button>
<button className="btn btn-danger" onClick={this.delete.bind(this)} disabled={!folder.folder.canSave}>
<i className="fa fa-trash" /> Delete
</button>
</div>
</form>
</div>
</div>
</div>
);
}
}
......@@ -99,7 +99,8 @@
results="ctrl.sections"
editable="true"
on-selection-changed="ctrl.selectionChanged()"
on-tag-selected="ctrl.filterByTag($tag)" />
on-tag-selected="ctrl.filterByTag($tag)"
/>
</div>
</div>
</div>
......
......@@ -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);
}
});
......
......@@ -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', () => {
......
......@@ -43,7 +43,7 @@ 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',
......@@ -69,7 +69,7 @@ export class FolderPageLoader {
});
}
createFolderUrl(folderId: number, type: string, slug: string) {
createFolderUrl(folderId: number, slug: string) {
return `dashboards/folder/${folderId}/${slug}`;
}
}
......@@ -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');
}
......
......@@ -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: '<react-container />',
resolve: {
component: () => FolderSettings,
},
})
.when('/dashboards/folder/:folderId/:slug', {
templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html',
......
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);
}),
}));
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);
});
});
......@@ -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}`;
}
......@@ -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;
......
......@@ -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 {
......
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;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment