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: {},
|
routeParams: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderUrl(path: string, query: UrlQueryMap): string {
|
function renderUrl(path: string, query: UrlQueryMap | undefined): string {
|
||||||
if (Object.keys(query).length > 0) {
|
if (query && Object.keys(query).length > 0) {
|
||||||
path += '?' + toUrlParams(query);
|
path += '?' + toUrlParams(query);
|
||||||
}
|
}
|
||||||
return path;
|
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]) {
|
if (navIndex[id]) {
|
||||||
const node = navIndex[id];
|
const node = navIndex[id];
|
||||||
const main = {
|
const main = {
|
||||||
@@ -33,7 +33,11 @@ export function getNavModel(navIndex: NavIndex, id: string): NavModel {
|
|||||||
node: node,
|
node: node,
|
||||||
main: main,
|
main: main,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
return getNotFoundModel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fallback) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getNotFoundModel();
|
||||||
}
|
}
|
||||||
|
@@ -252,16 +252,6 @@ export class BackendSrv {
|
|||||||
return this.post('/api/folders', payload);
|
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) {
|
deleteFolder(uid: string, showSuccessAlert) {
|
||||||
return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true });
|
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 coreModule from 'app/core/core_module';
|
||||||
import { FolderDashboardsCtrl } from './folder_dashboards_ctrl';
|
import { FolderDashboardsCtrl } from './folder_dashboards_ctrl';
|
||||||
import { FolderSettingsCtrl } from './folder_settings_ctrl';
|
|
||||||
import { DashboardImportCtrl } from './dashboard_import_ctrl';
|
import { DashboardImportCtrl } from './dashboard_import_ctrl';
|
||||||
import { CreateFolderCtrl } from './create_folder_ctrl';
|
import { CreateFolderCtrl } from './create_folder_ctrl';
|
||||||
|
|
||||||
coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl);
|
coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl);
|
||||||
coreModule.controller('FolderSettingsCtrl', FolderSettingsCtrl);
|
|
||||||
coreModule.controller('DashboardImportCtrl', DashboardImportCtrl);
|
coreModule.controller('DashboardImportCtrl', DashboardImportCtrl);
|
||||||
coreModule.controller('CreateFolderCtrl', CreateFolderCtrl);
|
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 React, { Component } from 'react';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import { inject, observer } from 'mobx-react';
|
import { inject, observer } from 'mobx-react';
|
||||||
import { toJS } from 'mobx';
|
import { connect } from 'react-redux';
|
||||||
import ContainerProps from 'app/containers/ContainerProps';
|
|
||||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||||
import Permissions from 'app/core/components/Permissions/Permissions';
|
import Permissions from 'app/core/components/Permissions/Permissions';
|
||||||
import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
||||||
import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo';
|
import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo';
|
||||||
import AddPermissions from 'app/core/components/Permissions/AddPermissions';
|
import AddPermissions from 'app/core/components/Permissions/AddPermissions';
|
||||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
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
|
@observer
|
||||||
export class FolderPermissions extends Component<ContainerProps, any> {
|
export class FolderPermissions extends Component<Props> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.handleAddPermission = this.handleAddPermission.bind(this);
|
this.handleAddPermission = this.handleAddPermission.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.loadStore();
|
this.props.getFolderByUid(this.props.folderUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -27,31 +40,23 @@ export class FolderPermissions extends Component<ContainerProps, any> {
|
|||||||
permissions.hideAddPermissions();
|
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() {
|
handleAddPermission() {
|
||||||
const { permissions } = this.props;
|
const { permissions } = this.props;
|
||||||
permissions.toggleAddPermissions();
|
permissions.toggleAddPermissions();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { nav, folder, permissions, backendSrv } = this.props;
|
const { navModel, permissions, backendSrv, folder } = this.props;
|
||||||
|
|
||||||
if (!folder.folder || !nav.main) {
|
if (folder.id === 0) {
|
||||||
return <h2>Loading</h2>;
|
return <PageHeader model={navModel} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dashboardId = folder.folder.id;
|
const dashboardId = folder.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PageHeader model={nav as any} />
|
<PageHeader model={navModel} />
|
||||||
<div className="page-container page-body">
|
<div className="page-container page-body">
|
||||||
<div className="page-action-bar">
|
<div className="page-action-bar">
|
||||||
<h3 className="page-sub-heading">Folder Permissions</h3>
|
<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 TeamMembers from './TeamMembers';
|
||||||
import TeamSettings from './TeamSettings';
|
import TeamSettings from './TeamSettings';
|
||||||
import TeamGroupSync from './TeamGroupSync';
|
import TeamGroupSync from './TeamGroupSync';
|
||||||
import { NavModel, Team } from '../../types';
|
import { NavModel, Team } from 'app/types';
|
||||||
import { loadTeam } from './state/actions';
|
import { loadTeam } from './state/actions';
|
||||||
import { getTeam } from './state/selectors';
|
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';
|
import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@@ -89,9 +90,10 @@ export class TeamPages extends PureComponent<Props, State> {
|
|||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
const teamId = getRouteParamsId(state.location);
|
const teamId = getRouteParamsId(state.location);
|
||||||
const pageName = getRouteParamsPage(state.location) || 'members';
|
const pageName = getRouteParamsPage(state.location) || 'members';
|
||||||
|
const teamLoadingNav = getTeamLoadingNav(pageName);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`),
|
navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav),
|
||||||
teamId: teamId,
|
teamId: teamId,
|
||||||
pageName: pageName,
|
pageName: pageName,
|
||||||
team: getTeam(state.team, teamId),
|
team: getTeam(state.team, teamId),
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { ThunkAction } from 'redux-thunk';
|
import { ThunkAction } from 'redux-thunk';
|
||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
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 { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
|
||||||
import config from 'app/core/config';
|
import { buildNavModel } from './navModel';
|
||||||
|
|
||||||
export enum ActionTypes {
|
export enum ActionTypes {
|
||||||
LoadTeams = 'LOAD_TEAMS',
|
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> {
|
export function loadTeam(id: number): ThunkResult<void> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
await getBackendSrv()
|
const response = await getBackendSrv().get(`/api/teams/${id}`);
|
||||||
.get(`/api/teams/${id}`)
|
dispatch(teamLoaded(response));
|
||||||
.then(response => {
|
dispatch(updateNavIndex(buildNavModel(response)));
|
||||||
dispatch(teamLoaded(response));
|
|
||||||
dispatch(updateNavIndex(buildNavModel(response)));
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadTeamMembers(): ThunkResult<void> {
|
export function loadTeamMembers(): ThunkResult<void> {
|
||||||
return async (dispatch, getStore) => {
|
return async (dispatch, getStore) => {
|
||||||
const team = getStore().team.team;
|
const team = getStore().team.team;
|
||||||
|
const response = await getBackendSrv().get(`/api/teams/${team.id}/members`);
|
||||||
await getBackendSrv()
|
dispatch(teamMembersLoaded(response));
|
||||||
.get(`/api/teams/${team.id}/members`)
|
|
||||||
.then(response => {
|
|
||||||
dispatch(teamMembersLoaded(response));
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addTeamMember(id: number): ThunkResult<void> {
|
export function addTeamMember(id: number): ThunkResult<void> {
|
||||||
return async (dispatch, getStore) => {
|
return async (dispatch, getStore) => {
|
||||||
const team = getStore().team.team;
|
const team = getStore().team.team;
|
||||||
|
await getBackendSrv().post(`/api/teams/${team.id}/members`, { userId: id });
|
||||||
await getBackendSrv()
|
dispatch(loadTeamMembers());
|
||||||
.post(`/api/teams/${team.id}/members`, { userId: id })
|
|
||||||
.then(() => {
|
|
||||||
dispatch(loadTeamMembers());
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeTeamMember(id: number): ThunkResult<void> {
|
export function removeTeamMember(id: number): ThunkResult<void> {
|
||||||
return async (dispatch, getStore) => {
|
return async (dispatch, getStore) => {
|
||||||
const team = getStore().team.team;
|
const team = getStore().team.team;
|
||||||
|
await getBackendSrv().delete(`/api/teams/${team.id}/members/${id}`);
|
||||||
await getBackendSrv()
|
dispatch(loadTeamMembers());
|
||||||
.delete(`/api/teams/${team.id}/members/${id}`)
|
|
||||||
.then(() => {
|
|
||||||
dispatch(loadTeamMembers());
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateTeam(name: string, email: string): ThunkResult<void> {
|
export function updateTeam(name: string, email: string): ThunkResult<void> {
|
||||||
return async (dispatch, getStore) => {
|
return async (dispatch, getStore) => {
|
||||||
const team = getStore().team.team;
|
const team = getStore().team.team;
|
||||||
await getBackendSrv()
|
await getBackendSrv().put(`/api/teams/${team.id}`, { name, email });
|
||||||
.put(`/api/teams/${team.id}`, {
|
dispatch(loadTeam(team.id));
|
||||||
name,
|
|
||||||
email,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
dispatch(loadTeam(team.id));
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadTeamGroups(): ThunkResult<void> {
|
export function loadTeamGroups(): ThunkResult<void> {
|
||||||
return async (dispatch, getStore) => {
|
return async (dispatch, getStore) => {
|
||||||
const team = getStore().team.team;
|
const team = getStore().team.team;
|
||||||
|
const response = await getBackendSrv().get(`/api/teams/${team.id}/groups`);
|
||||||
await getBackendSrv()
|
dispatch(teamGroupsLoaded(response));
|
||||||
.get(`/api/teams/${team.id}/groups`)
|
|
||||||
.then(response => {
|
|
||||||
dispatch(teamGroupsLoaded(response));
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addTeamGroup(groupId: string): ThunkResult<void> {
|
export function addTeamGroup(groupId: string): ThunkResult<void> {
|
||||||
return async (dispatch, getStore) => {
|
return async (dispatch, getStore) => {
|
||||||
const team = getStore().team.team;
|
const team = getStore().team.team;
|
||||||
|
await getBackendSrv().post(`/api/teams/${team.id}/groups`, { groupId: groupId });
|
||||||
await getBackendSrv()
|
dispatch(loadTeamGroups());
|
||||||
.post(`/api/teams/${team.id}/groups`, { groupId: groupId })
|
|
||||||
.then(() => {
|
|
||||||
dispatch(loadTeamGroups());
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeTeamGroup(groupId: string): ThunkResult<void> {
|
export function removeTeamGroup(groupId: string): ThunkResult<void> {
|
||||||
return async (dispatch, getStore) => {
|
return async (dispatch, getStore) => {
|
||||||
const team = getStore().team.team;
|
const team = getStore().team.team;
|
||||||
|
await getBackendSrv().delete(`/api/teams/${team.id}/groups/${groupId}`);
|
||||||
await getBackendSrv()
|
dispatch(loadTeamGroups());
|
||||||
.delete(`/api/teams/${team.id}/groups/${groupId}`)
|
|
||||||
.then(() => {
|
|
||||||
dispatch(loadTeamGroups());
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteTeam(id: number): ThunkResult<void> {
|
export function deleteTeam(id: number): ThunkResult<void> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
await getBackendSrv()
|
await getBackendSrv().delete(`/api/teams/${id}`);
|
||||||
.delete(`/api/teams/${id}`)
|
dispatch(loadTeams());
|
||||||
.then(() => {
|
|
||||||
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 ServerStats from 'app/features/admin/ServerStats';
|
||||||
import AlertRuleList from 'app/features/alerting/AlertRuleList';
|
import AlertRuleList from 'app/features/alerting/AlertRuleList';
|
||||||
import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions';
|
|
||||||
import TeamPages from 'app/features/teams/TeamPages';
|
import TeamPages from 'app/features/teams/TeamPages';
|
||||||
import TeamList from 'app/features/teams/TeamList';
|
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 */
|
/** @ngInject */
|
||||||
export function setupAngularRoutes($routeProvider, $locationProvider) {
|
export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||||
@@ -99,7 +99,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
.when('/dashboards/f/:uid/:slug/settings', {
|
.when('/dashboards/f/:uid/:slug/settings', {
|
||||||
template: '<react-container />',
|
template: '<react-container />',
|
||||||
resolve: {
|
resolve: {
|
||||||
component: () => FolderSettings,
|
component: () => FolderSettingsPage,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.when('/dashboards/f/:uid/:slug', {
|
.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 { types } from 'mobx-state-tree';
|
||||||
import { NavStore } from './../NavStore/NavStore';
|
import { NavStore } from './../NavStore/NavStore';
|
||||||
import { ViewStore } from './../ViewStore/ViewStore';
|
import { ViewStore } from './../ViewStore/ViewStore';
|
||||||
import { FolderStore } from './../FolderStore/FolderStore';
|
|
||||||
import { PermissionsStore } from './../PermissionsStore/PermissionsStore';
|
import { PermissionsStore } from './../PermissionsStore/PermissionsStore';
|
||||||
|
|
||||||
export const RootStore = types.model({
|
export const RootStore = types.model({
|
||||||
@@ -15,7 +14,6 @@ export const RootStore = types.model({
|
|||||||
query: {},
|
query: {},
|
||||||
routeParams: {},
|
routeParams: {},
|
||||||
}),
|
}),
|
||||||
folder: types.optional(FolderStore, {}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type RootStoreType = typeof RootStore.Type;
|
type RootStoreType = typeof RootStore.Type;
|
||||||
|
@@ -4,11 +4,13 @@ import { createLogger } from 'redux-logger';
|
|||||||
import sharedReducers from 'app/core/reducers';
|
import sharedReducers from 'app/core/reducers';
|
||||||
import alertingReducers from 'app/features/alerting/state/reducers';
|
import alertingReducers from 'app/features/alerting/state/reducers';
|
||||||
import teamsReducers from 'app/features/teams/state/reducers';
|
import teamsReducers from 'app/features/teams/state/reducers';
|
||||||
|
import foldersReducers from 'app/features/folders/state/reducers';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
...sharedReducers,
|
...sharedReducers,
|
||||||
...alertingReducers,
|
...alertingReducers,
|
||||||
...teamsReducers,
|
...teamsReducers,
|
||||||
|
...foldersReducers,
|
||||||
});
|
});
|
||||||
|
|
||||||
export let store;
|
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 { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting';
|
||||||
import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location';
|
import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location';
|
||||||
import { NavModel, NavModelItem, NavIndex } from './navModel';
|
import { NavModel, NavModelItem, NavIndex } from './navModel';
|
||||||
|
import { FolderDTO, FolderState } from './folder';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Team,
|
Team,
|
||||||
@@ -19,6 +20,8 @@ export {
|
|||||||
NavIndex,
|
NavIndex,
|
||||||
UrlQueryMap,
|
UrlQueryMap,
|
||||||
UrlQueryValue,
|
UrlQueryValue,
|
||||||
|
FolderDTO,
|
||||||
|
FolderState,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
@@ -27,4 +30,5 @@ export interface StoreState {
|
|||||||
alertRules: AlertRulesState;
|
alertRules: AlertRulesState;
|
||||||
teams: TeamsState;
|
teams: TeamsState;
|
||||||
team: TeamState;
|
team: TeamState;
|
||||||
|
folder: FolderState;
|
||||||
}
|
}
|
||||||
|
30
yarn.lock
30
yarn.lock
@@ -3182,7 +3182,7 @@ debug@^3.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.1"
|
ms "^2.1.1"
|
||||||
|
|
||||||
debuglog@*, debuglog@^1.0.1:
|
debuglog@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
|
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"
|
pkg-dir "^3.0.0"
|
||||||
resolve-cwd "^2.0.0"
|
resolve-cwd "^2.0.0"
|
||||||
|
|
||||||
imurmurhash@*, imurmurhash@^0.1.4:
|
imurmurhash@^0.1.4:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
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"
|
version "4.17.10"
|
||||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05"
|
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:
|
lodash._baseuniq@~4.6.0:
|
||||||
version "4.6.0"
|
version "4.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
|
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._createset "~4.0.0"
|
||||||
lodash._root "~3.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:
|
lodash._createset@~4.0.0:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
|
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"
|
version "3.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
|
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"
|
version "4.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
|
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:
|
lodash.sortby@^4.7.0:
|
||||||
version "4.7.0"
|
version "4.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
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"
|
isarray "0.0.1"
|
||||||
string_decoder "~0.10.x"
|
string_decoder "~0.10.x"
|
||||||
|
|
||||||
readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0:
|
readdir-scoped-modules@^1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747"
|
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
Reference in New Issue
Block a user