mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
@@ -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;
|
@@ -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(<FolderSettings backendSrv={backendSrv} {...store} />);
|
||||
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);
|
||||
});
|
||||
});
|
@@ -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<ContainerProps, any> {
|
||||
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 <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-save" /> 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default hot(module)(FolderSettings);
|
@@ -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;
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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 });
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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 });
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -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<ContainerProps, any> {
|
||||
export class FolderPermissions extends Component<Props> {
|
||||
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<ContainerProps, any> {
|
||||
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 <h2>Loading</h2>;
|
||||
if (folder.id === 0) {
|
||||
return <PageHeader model={navModel} />;
|
||||
}
|
||||
|
||||
const dashboardId = folder.folder.id;
|
||||
const dashboardId = folder.id;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader model={nav as any} />
|
||||
<PageHeader model={navModel} />
|
||||
<div className="page-container page-body">
|
||||
<div className="page-action-bar">
|
||||
<h3 className="page-sub-heading">Folder Permissions</h3>
|
||||
@@ -77,4 +82,17 @@ export class FolderPermissions extends Component<ContainerProps, any> {
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
55
public/app/features/folders/FolderSettingsPage.test.tsx
Normal file
55
public/app/features/folders/FolderSettingsPage.test.tsx
Normal file
@@ -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(<FolderSettingsPage {...props} />);
|
||||
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();
|
||||
});
|
||||
});
|
105
public/app/features/folders/FolderSettingsPage.tsx
Normal file
105
public/app/features/folders/FolderSettingsPage.tsx
Normal file
@@ -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<Props> {
|
||||
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 (
|
||||
<div>
|
||||
<PageHeader model={navModel} />
|
||||
<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.onSave}>
|
||||
<div className="gf-form">
|
||||
<label className="gf-form-label width-7">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input width-30"
|
||||
value={folder.title}
|
||||
onChange={this.onTitleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form-button-row">
|
||||
<button type="submit" className="btn btn-success" disabled={!folder.canSave || !folder.hasChanged}>
|
||||
<i className="fa fa-save" /> Save
|
||||
</button>
|
||||
<button className="btn btn-danger" onClick={this.onDelete} disabled={!folder.canSave}>
|
||||
<i className="fa fa-trash" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
@@ -0,0 +1,131 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should enable save button 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<h2
|
||||
className="page-sub-heading"
|
||||
>
|
||||
Folder Settings
|
||||
</h2>
|
||||
<div
|
||||
className="section gf-form-group"
|
||||
>
|
||||
<form
|
||||
name="folderSettingsForm"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<label
|
||||
className="gf-form-label width-7"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
className="gf-form-input width-30"
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value="loading"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form-button-row"
|
||||
>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
disabled={false}
|
||||
type="submit"
|
||||
>
|
||||
<i
|
||||
className="fa fa-save"
|
||||
/>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-trash"
|
||||
/>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<h2
|
||||
className="page-sub-heading"
|
||||
>
|
||||
Folder Settings
|
||||
</h2>
|
||||
<div
|
||||
className="section gf-form-group"
|
||||
>
|
||||
<form
|
||||
name="folderSettingsForm"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="gf-form"
|
||||
>
|
||||
<label
|
||||
className="gf-form-label width-7"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
className="gf-form-input width-30"
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value="loading"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form-button-row"
|
||||
>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
disabled={true}
|
||||
type="submit"
|
||||
>
|
||||
<i
|
||||
className="fa fa-save"
|
||||
/>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-trash"
|
||||
/>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
67
public/app/features/folders/state/actions.ts
Normal file
67
public/app/features/folders/state/actions.ts
Normal file
@@ -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<R> = ThunkAction<R, StoreState, undefined, any>;
|
||||
|
||||
|
||||
export function getFolderByUid(uid: string): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
const folder = await getBackendSrv().getFolderByUid(uid);
|
||||
dispatch(loadFolder(folder));
|
||||
dispatch(updateNavIndex(buildNavModel(folder)));
|
||||
};
|
||||
}
|
||||
|
||||
export function saveFolder(folder: FolderState): ThunkResult<void> {
|
||||
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<void> {
|
||||
return async dispatch => {
|
||||
await getBackendSrv().deleteFolder(uid, true);
|
||||
dispatch(updateLocation({ path: `dashboards` }));
|
||||
};
|
||||
}
|
53
public/app/features/folders/state/navModel.ts
Normal file
53
public/app/features/folders/state/navModel.ts
Normal file
@@ -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],
|
||||
};
|
||||
}
|
42
public/app/features/folders/state/reducers.test.ts
Normal file
42
public/app/features/folders/state/reducers.test.ts
Normal file
@@ -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');
|
||||
});
|
||||
});
|
33
public/app/features/folders/state/reducers.ts
Normal file
33
public/app/features/folders/state/reducers.ts
Normal file
@@ -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,
|
||||
};
|
@@ -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<Props, State> {
|
||||
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),
|
||||
|
@@ -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<void> {
|
||||
};
|
||||
}
|
||||
|
||||
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<void> {
|
||||
return async dispatch => {
|
||||
await getBackendSrv()
|
||||
.get(`/api/teams/${id}`)
|
||||
.then(response => {
|
||||
const response = await getBackendSrv().get(`/api/teams/${id}`);
|
||||
dispatch(teamLoaded(response));
|
||||
dispatch(updateNavIndex(buildNavModel(response)));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function loadTeamMembers(): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const team = getStore().team.team;
|
||||
|
||||
await getBackendSrv()
|
||||
.get(`/api/teams/${team.id}/members`)
|
||||
.then(response => {
|
||||
const response = await getBackendSrv().get(`/api/teams/${team.id}/members`);
|
||||
dispatch(teamMembersLoaded(response));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function addTeamMember(id: number): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const team = getStore().team.team;
|
||||
|
||||
await getBackendSrv()
|
||||
.post(`/api/teams/${team.id}/members`, { userId: id })
|
||||
.then(() => {
|
||||
await getBackendSrv().post(`/api/teams/${team.id}/members`, { userId: id });
|
||||
dispatch(loadTeamMembers());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function removeTeamMember(id: number): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const team = getStore().team.team;
|
||||
|
||||
await getBackendSrv()
|
||||
.delete(`/api/teams/${team.id}/members/${id}`)
|
||||
.then(() => {
|
||||
await getBackendSrv().delete(`/api/teams/${team.id}/members/${id}`);
|
||||
dispatch(loadTeamMembers());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function updateTeam(name: string, email: string): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const team = getStore().team.team;
|
||||
await getBackendSrv()
|
||||
.put(`/api/teams/${team.id}`, {
|
||||
name,
|
||||
email,
|
||||
})
|
||||
.then(() => {
|
||||
await getBackendSrv().put(`/api/teams/${team.id}`, { name, email });
|
||||
dispatch(loadTeam(team.id));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function loadTeamGroups(): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const team = getStore().team.team;
|
||||
|
||||
await getBackendSrv()
|
||||
.get(`/api/teams/${team.id}/groups`)
|
||||
.then(response => {
|
||||
const response = await getBackendSrv().get(`/api/teams/${team.id}/groups`);
|
||||
dispatch(teamGroupsLoaded(response));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function addTeamGroup(groupId: string): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const team = getStore().team.team;
|
||||
|
||||
await getBackendSrv()
|
||||
.post(`/api/teams/${team.id}/groups`, { groupId: groupId })
|
||||
.then(() => {
|
||||
await getBackendSrv().post(`/api/teams/${team.id}/groups`, { groupId: groupId });
|
||||
dispatch(loadTeamGroups());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function removeTeamGroup(groupId: string): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const team = getStore().team.team;
|
||||
|
||||
await getBackendSrv()
|
||||
.delete(`/api/teams/${team.id}/groups/${groupId}`)
|
||||
.then(() => {
|
||||
await getBackendSrv().delete(`/api/teams/${team.id}/groups/${groupId}`);
|
||||
dispatch(loadTeamGroups());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteTeam(id: number): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
await getBackendSrv()
|
||||
.delete(`/api/teams/${id}`)
|
||||
.then(() => {
|
||||
await getBackendSrv().delete(`/api/teams/${id}`);
|
||||
dispatch(loadTeams());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
67
public/app/features/teams/state/navModel.ts
Normal file
67
public/app/features/teams/state/navModel.ts
Normal file
@@ -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,
|
||||
};
|
||||
}
|
@@ -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: '<react-container />',
|
||||
resolve: {
|
||||
component: () => FolderSettings,
|
||||
component: () => FolderSettingsPage,
|
||||
},
|
||||
})
|
||||
.when('/dashboards/f/:uid/:slug', {
|
||||
|
@@ -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);
|
||||
}),
|
||||
}));
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
18
public/app/types/folder.ts
Normal file
18
public/app/types/folder.ts
Normal file
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
30
yarn.lock
30
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:
|
||||
|
Reference in New Issue
Block a user