mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
dashfolders: convert folder settings to React
This commit is contained in:
parent
e1aff1d5ff
commit
545d7b9477
@ -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;
|
||||
|
@ -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(<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);
|
||||
});
|
||||
});
|
153
public/app/containers/ManageDashboards/FolderSettings.tsx
Normal file
153
public/app/containers/ManageDashboards/FolderSettings.tsx
Normal file
@ -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<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',
|
||||
|
45
public/app/stores/FolderStore/FolderStore.ts
Normal file
45
public/app/stores/FolderStore/FolderStore.ts
Normal file
@ -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);
|
||||
}),
|
||||
}));
|
47
public/app/stores/NavStore/NavStore.jest.ts
Normal file
47
public/app/stores/NavStore/NavStore.jest.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user