Commit f2edb82e by Torkel Ödegaard Committed by GitHub

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
parent e4496080
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;
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);
});
});
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 = { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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'; ...@@ -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);
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 });
},
});
}
}
}
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> { ...@@ -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> { ...@@ -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));
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();
});
});
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));
// 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>
`;
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` }));
};
}
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],
};
}
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');
});
});
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'; ...@@ -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> { ...@@ -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),
......
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> { ...@@ -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}`)
.then(response => {
dispatch(teamLoaded(response)); dispatch(teamLoaded(response));
dispatch(updateNavIndex(buildNavModel(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()
.get(`/api/teams/${team.id}/members`)
.then(response => {
dispatch(teamMembersLoaded(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()
.post(`/api/teams/${team.id}/members`, { userId: id })
.then(() => {
dispatch(loadTeamMembers()); 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()
.delete(`/api/teams/${team.id}/members/${id}`)
.then(() => {
dispatch(loadTeamMembers()); 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}`, {
name,
email,
})
.then(() => {
dispatch(loadTeam(team.id)); 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()
.get(`/api/teams/${team.id}/groups`)
.then(response => {
dispatch(teamGroupsLoaded(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()
.post(`/api/teams/${team.id}/groups`, { groupId: groupId })
.then(() => {
dispatch(loadTeamGroups()); 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()
.delete(`/api/teams/${team.id}/groups/${groupId}`)
.then(() => {
dispatch(loadTeamGroups()); 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}`)
.then(() => {
dispatch(loadTeams()); dispatch(loadTeams());
});
}; };
} }
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'; ...@@ -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) { ...@@ -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', {
......
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);
}),
}));
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({ ...@@ -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'; ...@@ -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;
......
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'; ...@@ -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 { ...@@ -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 { ...@@ -27,4 +30,5 @@ export interface StoreState {
alertRules: AlertRulesState; alertRules: AlertRulesState;
teams: TeamsState; teams: TeamsState;
team: TeamState; team: TeamState;
folder: FolderState;
} }
...@@ -3182,7 +3182,7 @@ debug@^3.1.0: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment