Commit e58c2ebc by Torkel Ödegaard

tech: remove all mobx stuff

parent 331be7d4
...@@ -60,7 +60,6 @@ ...@@ -60,7 +60,6 @@
"lint-staged": "^6.0.0", "lint-staged": "^6.0.0",
"load-grunt-tasks": "3.5.2", "load-grunt-tasks": "3.5.2",
"mini-css-extract-plugin": "^0.4.0", "mini-css-extract-plugin": "^0.4.0",
"mobx-react-devtools": "^4.2.15",
"mocha": "^4.0.1", "mocha": "^4.0.1",
"ng-annotate-loader": "^0.6.1", "ng-annotate-loader": "^0.6.1",
"ng-annotate-webpack-plugin": "^0.3.0", "ng-annotate-webpack-plugin": "^0.3.0",
...@@ -146,9 +145,6 @@ ...@@ -146,9 +145,6 @@
"immutable": "^3.8.2", "immutable": "^3.8.2",
"jquery": "^3.2.1", "jquery": "^3.2.1",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"mobx": "^3.4.1",
"mobx-react": "^4.3.5",
"mobx-state-tree": "^1.3.1",
"moment": "^2.22.2", "moment": "^2.22.2",
"mousetrap": "^1.6.0", "mousetrap": "^1.6.0",
"mousetrap-global-bind": "^1.1.0", "mousetrap-global-bind": "^1.1.0",
......
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react';
import { NavModel, NavModelItem } from 'app/types'; import { NavModel, NavModelItem } from 'app/types';
import classNames from 'classnames'; import classNames from 'classnames';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { toJS } from 'mobx';
export interface Props { export interface Props {
model: NavModel; model: NavModel;
...@@ -81,7 +79,6 @@ const Navigation = ({ main }: { main: NavModelItem }) => { ...@@ -81,7 +79,6 @@ const Navigation = ({ main }: { main: NavModelItem }) => {
); );
}; };
@observer
export default class PageHeader extends React.Component<Props, any> { export default class PageHeader extends React.Component<Props, any> {
constructor(props) { constructor(props) {
super(props); super(props);
...@@ -148,7 +145,7 @@ export default class PageHeader extends React.Component<Props, any> { ...@@ -148,7 +145,7 @@ export default class PageHeader extends React.Component<Props, any> {
return null; return null;
} }
const main = toJS(model.main); // Convert to JS if its a mobx observable const main = model.main;
return ( return (
<div className="page-header-canvas"> <div className="page-header-canvas">
......
import React, { Component } from 'react'; import React, { Component } from 'react';
import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; import { dashboardPermissionLevels } from 'app/types/acl';
export interface Props { export interface Props {
item: any; item: any;
...@@ -24,7 +24,7 @@ export default class DisabledPermissionListItem extends Component<Props, any> { ...@@ -24,7 +24,7 @@ export default class DisabledPermissionListItem extends Component<Props, any> {
<td> <td>
<div className="gf-form"> <div className="gf-form">
<DescriptionPicker <DescriptionPicker
optionsWithDesc={permissionOptions} optionsWithDesc={dashboardPermissionLevels}
onSelected={() => {}} onSelected={() => {}}
value={item.permission} value={item.permission}
disabled={true} disabled={true}
......
...@@ -6,11 +6,10 @@ import coreModule from 'app/core/core_module'; ...@@ -6,11 +6,10 @@ import coreModule from 'app/core/core_module';
import { profiler } from 'app/core/profiler'; import { profiler } from 'app/core/profiler';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import Drop from 'tether-drop'; import Drop from 'tether-drop';
import { createStore } from 'app/stores/store';
import colors from 'app/core/utils/colors'; import colors from 'app/core/utils/colors';
import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv'; import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { configureStore } from 'app/stores/configureStore'; import { configureStore } from 'app/store/configureStore';
export class GrafanaCtrl { export class GrafanaCtrl {
/** @ngInject */ /** @ngInject */
...@@ -28,7 +27,6 @@ export class GrafanaCtrl { ...@@ -28,7 +27,6 @@ export class GrafanaCtrl {
// sets singleston instances for angular services so react components can access them // sets singleston instances for angular services so react components can access them
configureStore(); configureStore();
setBackendSrv(backendSrv); setBackendSrv(backendSrv);
createStore({ backendSrv, datasourceSrv });
$scope.init = () => { $scope.init = () => {
$scope.contextSrv = contextSrv; $scope.contextSrv = contextSrv;
......
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { store } from 'app/stores/store'; import { store } from 'app/store/configureStore';
import { store as reduxStore } from 'app/stores/configureStore';
import { reaction } from 'mobx';
import locationUtil from 'app/core/utils/location_util'; import locationUtil from 'app/core/utils/location_util';
import { updateLocation } from 'app/core/actions'; import { updateLocation } from 'app/core/actions';
...@@ -18,12 +16,9 @@ export class BridgeSrv { ...@@ -18,12 +16,9 @@ export class BridgeSrv {
init() { init() {
this.$rootScope.$on('$routeUpdate', (evt, data) => { this.$rootScope.$on('$routeUpdate', (evt, data) => {
const angularUrl = this.$location.url(); const angularUrl = this.$location.url();
if (store.view.currentUrl !== angularUrl) { const state = store.getState();
store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params);
}
const state = reduxStore.getState();
if (state.location.url !== angularUrl) { if (state.location.url !== angularUrl) {
reduxStore.dispatch( store.dispatch(
updateLocation({ updateLocation({
path: this.$location.path(), path: this.$location.path(),
query: this.$location.search(), query: this.$location.search(),
...@@ -34,8 +29,7 @@ export class BridgeSrv { ...@@ -34,8 +29,7 @@ export class BridgeSrv {
}); });
this.$rootScope.$on('$routeChangeSuccess', (evt, data) => { this.$rootScope.$on('$routeChangeSuccess', (evt, data) => {
store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); store.dispatch(
reduxStore.dispatch(
updateLocation({ updateLocation({
path: this.$location.path(), path: this.$location.path(),
query: this.$location.search(), query: this.$location.search(),
...@@ -44,24 +38,9 @@ export class BridgeSrv { ...@@ -44,24 +38,9 @@ export class BridgeSrv {
); );
}); });
// listen for mobx store changes and update angular
reaction(
() => store.view.currentUrl,
currentUrl => {
const angularUrl = this.$location.url();
const url = locationUtil.stripBaseFromUrl(currentUrl);
if (angularUrl !== url) {
this.$timeout(() => {
this.$location.url(url);
});
console.log('store updating angular $location.url', url);
}
}
);
// Listen for changes in redux location -> update angular location // Listen for changes in redux location -> update angular location
reduxStore.subscribe(() => { store.subscribe(() => {
const state = reduxStore.getState(); const state = store.getState();
const angularUrl = this.$location.url(); const angularUrl = this.$location.url();
const url = locationUtil.stripBaseFromUrl(state.location.url); const url = locationUtil.stripBaseFromUrl(state.location.url);
if (angularUrl !== url) { if (angularUrl !== url) {
......
...@@ -13,7 +13,7 @@ import { ...@@ -13,7 +13,7 @@ import {
import PermissionList from 'app/core/components/PermissionList/PermissionList'; import PermissionList from 'app/core/components/PermissionList/PermissionList';
import AddPermission from 'app/core/components/PermissionList/AddPermission'; import AddPermission from 'app/core/components/PermissionList/AddPermission';
import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo'; import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo';
import { store } from 'app/stores/configureStore'; import { store } from 'app/store/configureStore';
export interface Props { export interface Props {
dashboardId: number; dashboardId: number;
...@@ -65,7 +65,6 @@ export class DashboardPermissions extends PureComponent<Props, State> { ...@@ -65,7 +65,6 @@ export class DashboardPermissions extends PureComponent<Props, State> {
render() { render() {
const { permissions, folder } = this.props; const { permissions, folder } = this.props;
const { isAdding } = this.state; const { isAdding } = this.state;
console.log('DashboardPermissions', this.props);
return ( return (
<div> <div>
......
import { toJS } from 'mobx';
import { coreModule } from 'app/core/core'; import { coreModule } from 'app/core/core';
import { store } from 'app/stores/store'; import { store } from 'app/store/configureStore';
import { getNavModel } from 'app/core/selectors/navModel';
import { buildNavModel } from './state/navModel';
export class DataSourceDashboardsCtrl { export class DataSourceDashboardsCtrl {
datasourceMeta: any; datasourceMeta: any;
...@@ -9,11 +10,8 @@ export class DataSourceDashboardsCtrl { ...@@ -9,11 +10,8 @@ export class DataSourceDashboardsCtrl {
/** @ngInject */ /** @ngInject */
constructor(private backendSrv, private $routeParams) { constructor(private backendSrv, private $routeParams) {
if (store.nav.main === null) { const state = store.getState();
store.nav.load('cfg', 'datasources'); this.navModel = getNavModel(state.navIndex, 'datasources');
}
this.navModel = toJS(store.nav);
if (this.$routeParams.id) { if (this.$routeParams.id) {
this.getDatasourceById(this.$routeParams.id); this.getDatasourceById(this.$routeParams.id);
...@@ -30,8 +28,7 @@ export class DataSourceDashboardsCtrl { ...@@ -30,8 +28,7 @@ export class DataSourceDashboardsCtrl {
} }
updateNav() { updateNav() {
store.nav.initDatasourceEditNav(this.current, this.datasourceMeta, 'datasource-dashboards'); this.navModel = buildNavModel(this.current, this.datasourceMeta, 'datasource-dashboards');
this.navModel = toJS(store.nav);
} }
getPluginInfo() { getPluginInfo() {
......
import _ from 'lodash'; import _ from 'lodash';
import { toJS } from 'mobx';
import config from 'app/core/config'; import config from 'app/core/config';
import { coreModule, appEvents } from 'app/core/core'; import { coreModule, appEvents } from 'app/core/core';
import { store } from 'app/stores/store'; import { store } from 'app/store/configureStore';
import { getNavModel } from 'app/core/selectors/navModel';
import { buildNavModel } from './state/navModel';
let datasourceTypes = []; let datasourceTypes = [];
...@@ -31,11 +32,8 @@ export class DataSourceEditCtrl { ...@@ -31,11 +32,8 @@ export class DataSourceEditCtrl {
/** @ngInject */ /** @ngInject */
constructor(private $q, private backendSrv, private $routeParams, private $location, private datasourceSrv) { constructor(private $q, private backendSrv, private $routeParams, private $location, private datasourceSrv) {
if (store.nav.main === null) { const state = store.getState();
store.nav.load('cfg', 'datasources'); this.navModel = getNavModel(state.navIndex, 'datasources');
}
this.navModel = toJS(store.nav);
this.datasources = []; this.datasources = [];
this.loadDatasourceTypes().then(() => { this.loadDatasourceTypes().then(() => {
...@@ -101,8 +99,7 @@ export class DataSourceEditCtrl { ...@@ -101,8 +99,7 @@ export class DataSourceEditCtrl {
} }
updateNav() { updateNav() {
store.nav.initDatasourceEditNav(this.current, this.datasourceMeta, 'datasource-settings'); this.navModel = buildNavModel(this.current, this.datasourceMeta, 'datasource-settings');
this.navModel = toJS(store.nav);
} }
typeChanged() { typeChanged() {
......
import _ from 'lodash';
import { DataSource, PluginMeta, NavModel } from 'app/types';
export function buildNavModel(ds: DataSource, plugin: PluginMeta, currentPage: string): NavModel {
let title = 'New';
const subTitle = `Type: ${plugin.name}`;
if (ds.id) {
title = ds.name;
}
const main = {
img: plugin.info.logos.large,
id: 'ds-edit-' + plugin.id,
subTitle: subTitle,
url: '',
text: title,
breadcrumbs: [{ title: 'Data Sources', url: 'datasources' }],
children: [
{
active: currentPage === 'datasource-settings',
icon: 'fa fa-fw fa-sliders',
id: 'datasource-settings',
text: 'Settings',
url: `datasources/edit/${ds.id}`,
},
],
};
const hasDashboards = _.find(plugin.includes, { type: 'dashboard' }) !== undefined;
if (hasDashboards && ds.id) {
main.children.push({
active: currentPage === 'datasource-dashboards',
icon: 'fa fa-fw fa-th-large',
id: 'datasource-dashboards',
text: 'Dashboards',
url: `datasources/edit/${ds.id}/dashboards`,
});
}
return {
main: main,
node: _.find(main.children, { active: true }),
};
}
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Provider } from 'mobx-react'; import { Provider } from 'react-redux';
import { Provider as ReduxProvider } from 'react-redux';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import { store } from 'app/stores/store'; import { store } from 'app/store/configureStore';
import { store as reduxStore } from 'app/stores/configureStore';
import { BackendSrv } from 'app/core/services/backend_srv'; import { BackendSrv } from 'app/core/services/backend_srv';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { ContextSrv } from 'app/core/services/context_srv'; import { ContextSrv } from 'app/core/services/context_srv';
function WrapInProvider(store, Component, props) { function WrapInProvider(store, Component, props) {
return ( return (
<ReduxProvider store={reduxStore}> <Provider store={store}>
<Provider {...store}> <Component {...props} />
<Component {...props} /> </Provider>
</Provider>
</ReduxProvider>
); );
} }
......
import { types } from 'mobx-state-tree';
export const NavItem = types.model('NavItem', {
id: types.identifier(types.string),
text: types.string,
url: types.optional(types.string, ''),
subTitle: types.optional(types.string, ''),
icon: types.optional(types.string, ''),
img: types.optional(types.string, ''),
active: types.optional(types.boolean, false),
hideFromTabs: types.optional(types.boolean, false),
breadcrumbs: types.optional(types.array(types.late(() => Breadcrumb)), []),
children: types.optional(types.array(types.late(() => NavItem)), []),
});
export const Breadcrumb = types.model('Breadcrumb', {
title: types.string,
url: types.string,
});
import { NavStore } from './NavStore';
describe('NavStore', () => {
const folderId = 1;
const folderTitle = 'Folder Name';
const folderUrl = '/dashboards/f/uid/folder-name';
const canAdmin = true;
const folder = {
id: folderId,
url: folderUrl,
title: folderTitle,
canAdmin: canAdmin,
};
let store;
beforeEach(() => {
store = NavStore.create();
store.initFolderNav(folder, 'manage-folder-settings');
});
it('Should set text', () => {
expect(store.main.text).toBe(folderTitle);
});
it('Should load nav with tabs', () => {
expect(store.main.children.length).toBe(3);
expect(store.main.children[0].id).toBe('manage-folder-dashboards');
expect(store.main.children[1].id).toBe('manage-folder-permissions');
expect(store.main.children[2].id).toBe('manage-folder-settings');
});
it('Should set correct urls for each tab', () => {
expect(store.main.children.length).toBe(3);
expect(store.main.children[0].url).toBe(folderUrl);
expect(store.main.children[1].url).toBe(`${folderUrl}/permissions`);
expect(store.main.children[2].url).toBe(`${folderUrl}/settings`);
});
it('Should set active tab', () => {
expect(store.main.children.length).toBe(3);
expect(store.main.children[0].active).toBe(false);
expect(store.main.children[1].active).toBe(false);
expect(store.main.children[2].active).toBe(true);
});
});
import _ from 'lodash';
import { types, getEnv } from 'mobx-state-tree';
import { NavItem } from './NavItem';
export const NavStore = types
.model('NavStore', {
main: types.maybe(NavItem),
node: types.maybe(NavItem),
})
.actions(self => ({
load(...args) {
let children = getEnv(self).navTree;
let main, node;
const parents = [];
for (const id of args) {
node = children.find(el => el.id === id);
if (!node) {
throw new Error(`NavItem with id ${id} not found`);
}
children = node.children;
parents.push(node);
}
main = parents[parents.length - 2];
if (main.children) {
for (const item of main.children) {
item.active = false;
if (item.url === node.url) {
item.active = true;
}
}
}
self.main = NavItem.create(main);
self.node = NavItem.create(node);
},
initFolderNav(folder: any, activeChildId: string) {
const main = {
icon: 'fa fa-folder-open',
id: 'manage-folder',
subTitle: 'Manage folder dashboards & permissions',
url: '',
text: folder.title,
breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }],
children: [
{
active: activeChildId === 'manage-folder-dashboards',
icon: 'fa fa-fw fa-th-large',
id: 'manage-folder-dashboards',
text: 'Dashboards',
url: folder.url,
},
{
active: activeChildId === 'manage-folder-permissions',
icon: 'fa fa-fw fa-lock',
id: 'manage-folder-permissions',
text: 'Permissions',
url: `${folder.url}/permissions`,
},
{
active: activeChildId === 'manage-folder-settings',
icon: 'fa fa-fw fa-cog',
id: 'manage-folder-settings',
text: 'Settings',
url: `${folder.url}/settings`,
},
],
};
self.main = NavItem.create(main);
},
initDatasourceEditNav(ds: any, plugin: any, currentPage: string) {
let title = 'New';
const subTitle = `Type: ${plugin.name}`;
if (ds.id) {
title = ds.name;
}
const main = {
img: plugin.info.logos.large,
id: 'ds-edit-' + plugin.id,
subTitle: subTitle,
url: '',
text: title,
breadcrumbs: [{ title: 'Data Sources', url: 'datasources' }],
children: [
{
active: currentPage === 'datasource-settings',
icon: 'fa fa-fw fa-sliders',
id: 'datasource-settings',
text: 'Settings',
url: `datasources/edit/${ds.id}`,
},
],
};
const hasDashboards = _.find(plugin.includes, { type: 'dashboard' }) !== undefined;
if (hasDashboards && ds.id) {
main.children.push({
active: currentPage === 'datasource-dashboards',
icon: 'fa fa-fw fa-th-large',
id: 'datasource-dashboards',
text: 'Dashboards',
url: `datasources/edit/${ds.id}/dashboards`,
});
}
self.main = NavItem.create(main);
},
}));
import { PermissionsStore } from './PermissionsStore';
import { backendSrv } from 'test/mocks/common';
describe('PermissionsStore', () => {
let store;
beforeEach(async () => {
backendSrv.get.mockReturnValue(
Promise.resolve([
{ id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' },
{ id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' },
{
id: 4,
dashboardId: 10,
permission: 1,
permissionName: 'View',
teamId: 1,
team: 'MyTestTeam',
inherited: true,
},
{
id: 5,
dashboardId: 1,
permission: 1,
permissionName: 'View',
userId: 1,
userLogin: 'MyTestUser',
},
{
id: 6,
dashboardId: 1,
permission: 1,
permissionName: 'Edit',
teamId: 2,
team: 'MyTestTeam2',
},
])
);
backendSrv.post = jest.fn(() => Promise.resolve({}));
store = PermissionsStore.create(
{
fetching: false,
items: [],
},
{
backendSrv: backendSrv,
}
);
await store.load(1, false, false);
});
it('should save update on permission change', async () => {
expect(store.items[0].permission).toBe(1);
expect(store.items[0].permissionName).toBe('View');
await store.updatePermissionOnIndex(0, 2, 'Edit');
expect(store.items[0].permission).toBe(2);
expect(store.items[0].permissionName).toBe('Edit');
expect(backendSrv.post.mock.calls.length).toBe(1);
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions');
});
it('should save removed permissions automatically', async () => {
expect(store.items.length).toBe(5);
await store.removeStoreItem(2);
expect(store.items.length).toBe(4);
expect(backendSrv.post.mock.calls.length).toBe(1);
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions');
});
it('should be sorted by sort rank and alphabetically', async () => {
expect(store.items[0].name).toBe('MyTestTeam');
expect(store.items[0].dashboardId).toBe(10);
expect(store.items[1].name).toBe('Editor');
expect(store.items[2].name).toBe('Viewer');
expect(store.items[3].name).toBe('MyTestTeam2');
expect(store.items[4].name).toBe('MyTestUser');
});
describe('when one inherited and one not inherited team permission are added', () => {
beforeEach(async () => {
const overridingItemForChildDashboard = {
team: 'MyTestTeam',
dashboardId: 1,
teamId: 1,
permission: 2,
};
store.resetNewType();
store.newItem.setTeam(overridingItemForChildDashboard.teamId, overridingItemForChildDashboard.team);
store.newItem.setPermission(overridingItemForChildDashboard.permission);
await store.addStoreItem();
});
it('should add new overriding permission', () => {
expect(store.items.length).toBe(6);
});
it('should be sorted by sort rank and alphabetically', async () => {
expect(store.items[0].name).toBe('MyTestTeam');
expect(store.items[0].dashboardId).toBe(10);
expect(store.items[1].name).toBe('Editor');
expect(store.items[2].name).toBe('Viewer');
expect(store.items[3].name).toBe('MyTestTeam');
expect(store.items[3].dashboardId).toBe(1);
expect(store.items[4].name).toBe('MyTestTeam2');
expect(store.items[5].name).toBe('MyTestUser');
});
});
});
import { types, getEnv, flow } from 'mobx-state-tree';
import { PermissionsStoreItem } from './PermissionsStoreItem';
export const permissionOptions = [
{ value: 1, label: 'View', description: 'Can view dashboards.' },
{ value: 2, label: 'Edit', description: 'Can add, edit and delete dashboards.' },
{
value: 4,
label: 'Admin',
description: 'Can add/remove permissions and can add, edit and delete dashboards.',
},
];
export const aclTypeValues = {
GROUP: { value: 'Group', text: 'Team' },
USER: { value: 'User', text: 'User' },
VIEWER: { value: 'Viewer', text: 'Everyone With Viewer Role' },
EDITOR: { value: 'Editor', text: 'Everyone With Editor Role' },
};
export const aclTypes = Object.keys(aclTypeValues).map(item => aclTypeValues[item]);
const defaultNewType = aclTypes[0].value;
export const NewPermissionsItem = types
.model('NewPermissionsItem', {
type: types.optional(
types.enumeration(Object.keys(aclTypeValues).map(item => aclTypeValues[item].value)),
defaultNewType
),
userId: types.maybe(types.number),
userLogin: types.maybe(types.string),
userAvatarUrl: types.maybe(types.string),
teamAvatarUrl: types.maybe(types.string),
teamId: types.maybe(types.number),
team: types.maybe(types.string),
permission: types.optional(types.number, 1),
})
.views(self => ({
isValid: () => {
switch (self.type) {
case aclTypeValues.GROUP.value:
return self.teamId && self.team;
case aclTypeValues.USER.value:
return !!self.userId && !!self.userLogin;
case aclTypeValues.VIEWER.value:
case aclTypeValues.EDITOR.value:
return true;
default:
return false;
}
},
}))
.actions(self => ({
setUser(userId: number, userLogin: string, userAvatarUrl: string) {
self.userId = userId;
self.userLogin = userLogin;
self.userAvatarUrl = userAvatarUrl;
self.teamId = null;
self.team = null;
},
setTeam(teamId: number, team: string, teamAvatarUrl: string) {
self.userId = null;
self.userLogin = null;
self.teamId = teamId;
self.team = team;
self.teamAvatarUrl = teamAvatarUrl;
},
setPermission(permission: number) {
self.permission = permission;
},
}));
export const PermissionsStore = types
.model('PermissionsStore', {
fetching: types.boolean,
isFolder: types.maybe(types.boolean),
dashboardId: types.maybe(types.number),
items: types.optional(types.array(PermissionsStoreItem), []),
originalItems: types.optional(types.array(PermissionsStoreItem), []),
newType: types.optional(types.string, defaultNewType),
newItem: types.maybe(NewPermissionsItem),
isAddPermissionsVisible: types.optional(types.boolean, false),
isInRoot: types.maybe(types.boolean),
})
.views(self => ({
isValid: item => {
const dupe = self.items.find(it => {
return isDuplicate(it, item);
});
if (dupe) {
return false;
}
return true;
},
}))
.actions(self => {
const resetNewTypeInternal = () => {
self.newItem = NewPermissionsItem.create();
};
return {
load: flow(function* load(dashboardId: number, isFolder: boolean, isInRoot: boolean) {
const backendSrv = getEnv(self).backendSrv;
self.fetching = true;
self.isFolder = isFolder;
self.isInRoot = isInRoot;
self.dashboardId = dashboardId;
self.items.clear();
const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/permissions`);
const items = prepareServerResponse(res, dashboardId, isFolder, isInRoot);
self.items = items;
self.originalItems = items;
self.fetching = false;
}),
addStoreItem: flow(function* addStoreItem() {
const item = {
type: self.newItem.type,
permission: self.newItem.permission,
dashboardId: self.dashboardId,
team: undefined,
teamId: undefined,
userLogin: undefined,
userId: undefined,
userAvatarUrl: undefined,
teamAvatarUrl: undefined,
role: undefined,
};
switch (self.newItem.type) {
case aclTypeValues.GROUP.value:
item.team = self.newItem.team;
item.teamId = self.newItem.teamId;
item.teamAvatarUrl = self.newItem.teamAvatarUrl;
break;
case aclTypeValues.USER.value:
item.userLogin = self.newItem.userLogin;
item.userId = self.newItem.userId;
item.userAvatarUrl = self.newItem.userAvatarUrl;
break;
case aclTypeValues.VIEWER.value:
case aclTypeValues.EDITOR.value:
item.role = self.newItem.type;
break;
default:
throw Error('Unknown type: ' + self.newItem.type);
}
const updatedItems = self.items.peek();
const newItem = prepareItem(item, self.dashboardId, self.isFolder, self.isInRoot);
updatedItems.push(newItem);
try {
yield updateItems(self, updatedItems);
self.items.push(newItem);
const sortedItems = self.items.sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name));
self.items = sortedItems;
resetNewTypeInternal();
} catch {}
yield Promise.resolve();
}),
removeStoreItem: flow(function* removeStoreItem(idx: number) {
self.items.splice(idx, 1);
yield updateItems(self, self.items.peek());
}),
updatePermissionOnIndex: flow(function* updatePermissionOnIndex(
idx: number,
permission: number,
permissionName: string
) {
self.items[idx].updatePermission(permission, permissionName);
yield updateItems(self, self.items.peek());
}),
setNewType(newType: string) {
self.newItem = NewPermissionsItem.create({ type: newType });
},
resetNewType() {
resetNewTypeInternal();
},
toggleAddPermissions() {
self.isAddPermissionsVisible = !self.isAddPermissionsVisible;
},
hideAddPermissions() {
self.isAddPermissionsVisible = false;
},
};
});
const updateItems = (self, items) => {
const backendSrv = getEnv(self).backendSrv;
const updated = [];
for (const item of items) {
if (item.inherited) {
continue;
}
updated.push({
id: item.id,
userId: item.userId,
teamId: item.teamId,
role: item.role,
permission: item.permission,
});
}
return backendSrv.post(`/api/dashboards/id/${self.dashboardId}/permissions`, {
items: updated,
});
};
const prepareServerResponse = (response, dashboardId: number, isFolder: boolean, isInRoot: boolean) => {
return response
.map(item => {
return prepareItem(item, dashboardId, isFolder, isInRoot);
})
.sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name));
};
const prepareItem = (item, dashboardId: number, isFolder: boolean, isInRoot: boolean) => {
item.sortRank = 0;
if (item.userId > 0) {
item.name = item.userLogin;
item.sortRank = 10;
} else if (item.teamId > 0) {
item.name = item.team;
item.sortRank = 20;
} else if (item.role) {
item.icon = 'fa fa-fw fa-street-view';
item.name = item.role;
item.sortRank = 30;
if (item.role === 'Editor') {
item.sortRank += 1;
}
}
if (item.inherited) {
item.sortRank += 100;
}
return item;
};
const isDuplicate = (origItem, newItem) => {
if (origItem.inherited) {
return false;
}
return (
(origItem.role && newItem.role && origItem.role === newItem.role) ||
(origItem.userId && newItem.userId && origItem.userId === newItem.userId) ||
(origItem.teamId && newItem.teamId && origItem.teamId === newItem.teamId)
);
};
import { types } from 'mobx-state-tree';
export const PermissionsStoreItem = types
.model('PermissionsStoreItem', {
dashboardId: types.optional(types.number, -1),
permission: types.number,
permissionName: types.maybe(types.string),
role: types.maybe(types.string),
team: types.optional(types.string, ''),
teamId: types.optional(types.number, 0),
userEmail: types.optional(types.string, ''),
userId: types.optional(types.number, 0),
userLogin: types.optional(types.string, ''),
inherited: types.maybe(types.boolean),
sortRank: types.maybe(types.number),
icon: types.maybe(types.string),
name: types.maybe(types.string),
teamAvatarUrl: types.maybe(types.string),
userAvatarUrl: types.maybe(types.string),
})
.actions(self => ({
updateRole: role => {
self.role = role;
},
updatePermission(permission: number, permissionName: string) {
self.permission = permission;
self.permissionName = permissionName;
},
}));
import { types } from 'mobx-state-tree';
import { NavStore } from './../NavStore/NavStore';
import { ViewStore } from './../ViewStore/ViewStore';
import { PermissionsStore } from './../PermissionsStore/PermissionsStore';
export const RootStore = types.model({
nav: types.optional(NavStore, {}),
permissions: types.optional(PermissionsStore, {
fetching: false,
items: [],
}),
view: types.optional(ViewStore, {
path: '',
query: {},
routeParams: {},
}),
});
type RootStoreType = typeof RootStore.Type;
export interface RootStoreInterface extends RootStoreType {}
import { ViewStore } from './ViewStore';
import { toJS } from 'mobx';
describe('ViewStore', () => {
let store;
beforeAll(() => {
store = ViewStore.create({
path: '',
query: {},
routeParams: {},
});
});
it('Can update path and query', () => {
store.updatePathAndQuery('/hello', { key: 1, otherParam: 'asd' }, { key: 1, otherParam: 'asd' });
expect(store.path).toBe('/hello');
expect(store.query.get('key')).toBe(1);
expect(store.currentUrl).toBe('/hello?key=1&otherParam=asd');
});
it('Query can contain arrays', () => {
store.updatePathAndQuery('/hello', { values: ['A', 'B'] }, { key: 1, otherParam: 'asd' });
expect(toJS(store.query.get('values'))).toMatchObject(['A', 'B']);
expect(store.currentUrl).toBe('/hello?values=A&values=B');
});
it('Query can contain boolean', () => {
store.updatePathAndQuery('/hello', { abool: true }, { abool: true });
expect(toJS(store.query.get('abool'))).toBe(true);
expect(store.currentUrl).toBe('/hello?abool');
});
});
import { types } from 'mobx-state-tree';
import { toJS } from 'mobx';
import { toUrlParams } from 'app/core/utils/url';
const QueryInnerValueType = types.union(types.string, types.boolean, types.number);
const QueryValueType = types.union(QueryInnerValueType, types.array(QueryInnerValueType));
export const ViewStore = types
.model({
path: types.string,
query: types.map(QueryValueType),
routeParams: types.map(QueryValueType),
})
.views(self => ({
get currentUrl() {
let path = self.path;
if (self.query.size) {
path += '?' + toUrlParams(toJS(self.query));
}
return path;
},
}))
.actions(self => {
// querystring only
function updateQuery(query: any) {
self.query.clear();
for (const key of Object.keys(query)) {
if (query[key]) {
self.query.set(key, query[key]);
}
}
}
// needed to get route parameters like slug from the url
function updateRouteParams(routeParams: any) {
self.routeParams.clear();
for (const key of Object.keys(routeParams)) {
if (routeParams[key]) {
self.routeParams.set(key, routeParams[key]);
}
}
}
function updatePathAndQuery(path: string, query: any, routeParams: any) {
self.path = path;
updateQuery(query);
updateRouteParams(routeParams);
}
return {
updateQuery,
updatePathAndQuery,
};
});
import { RootStore, RootStoreInterface } from './RootStore/RootStore';
import config from 'app/core/config';
export let store: RootStoreInterface;
export function createStore(services) {
store = RootStore.create(
{},
{
...services,
navTree: config.bootData.navTree,
}
);
return store;
}
export interface DataSource {
id: number;
orgId: number;
name: string;
typeLogoUrl: string;
type: string;
}
...@@ -2,9 +2,11 @@ import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams'; ...@@ -2,9 +2,11 @@ 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, FolderInfo } from './folder'; import { FolderDTO, FolderState, FolderInfo } from './folders';
import { DashboardState } from './dashboard'; import { DashboardState } from './dashboard';
import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
import { DataSource } from './datasources';
import { PluginMeta } from './plugins';
export { export {
Team, Team,
...@@ -29,6 +31,8 @@ export { ...@@ -29,6 +31,8 @@ export {
DashboardAcl, DashboardAcl,
OrgRole, OrgRole,
PermissionLevel, PermissionLevel,
DataSource,
PluginMeta,
}; };
export interface StoreState { export interface StoreState {
......
export interface PluginMeta {
id: string;
name: string;
info: PluginMetaInfo;
includes: PluginInclude[];
}
export interface PluginInclude {
type: string;
name: string;
path: string;
}
export interface PluginMetaInfo {
logos: {
large: string;
small: string;
};
}
...@@ -5258,7 +5258,7 @@ hmac-drbg@^1.0.0: ...@@ -5258,7 +5258,7 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0" minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1" minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0: hoist-non-react-statics@^2.5.0:
version "2.5.5" version "2.5.5"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
...@@ -7593,24 +7593,6 @@ mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdi ...@@ -7593,24 +7593,6 @@ mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdi
dependencies: dependencies:
minimist "0.0.8" minimist "0.0.8"
mobx-react-devtools@^4.2.15:
version "4.2.15"
resolved "https://registry.yarnpkg.com/mobx-react-devtools/-/mobx-react-devtools-4.2.15.tgz#881c038fb83db4dffd1e72bbaf5374d26b2fdebb"
mobx-react@^4.3.5:
version "4.4.3"
resolved "http://registry.npmjs.org/mobx-react/-/mobx-react-4.4.3.tgz#baa9ec41165ee35ae7b9df19bca10190f36f117e"
dependencies:
hoist-non-react-statics "^2.3.1"
mobx-state-tree@^1.3.1:
version "1.4.0"
resolved "http://registry.npmjs.org/mobx-state-tree/-/mobx-state-tree-1.4.0.tgz#c914c855d5ec5c1c16e4ba6d6925679df42c8110"
mobx@^3.4.1:
version "3.6.2"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-3.6.2.tgz#fb9f5ff5090539a1ad54e75dc4c098b602693320"
mocha@^4.0.1: mocha@^4.0.1:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794" resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794"
......
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