mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'remove-mobx'
This commit is contained in:
commit
dd0b1d8410
@ -60,7 +60,6 @@
|
||||
"lint-staged": "^6.0.0",
|
||||
"load-grunt-tasks": "3.5.2",
|
||||
"mini-css-extract-plugin": "^0.4.0",
|
||||
"mobx-react-devtools": "^4.2.15",
|
||||
"mocha": "^4.0.1",
|
||||
"ng-annotate-loader": "^0.6.1",
|
||||
"ng-annotate-webpack-plugin": "^0.3.0",
|
||||
@ -146,9 +145,6 @@
|
||||
"immutable": "^3.8.2",
|
||||
"jquery": "^3.2.1",
|
||||
"lodash": "^4.17.10",
|
||||
"mobx": "^3.4.1",
|
||||
"mobx-react": "^4.3.5",
|
||||
"mobx-state-tree": "^1.3.1",
|
||||
"moment": "^2.22.2",
|
||||
"mousetrap": "^1.6.0",
|
||||
"mousetrap-global-bind": "^1.1.0",
|
||||
|
@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { NavModel, NavModelItem } from 'app/types';
|
||||
import classNames from 'classnames';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { toJS } from 'mobx';
|
||||
|
||||
export interface Props {
|
||||
model: NavModel;
|
||||
@ -81,7 +79,6 @@ const Navigation = ({ main }: { main: NavModelItem }) => {
|
||||
);
|
||||
};
|
||||
|
||||
@observer
|
||||
export default class PageHeader extends React.Component<Props, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -148,7 +145,7 @@ export default class PageHeader extends React.Component<Props, any> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const main = toJS(model.main); // Convert to JS if its a mobx observable
|
||||
const main = model.main;
|
||||
|
||||
return (
|
||||
<div className="page-header-canvas">
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
|
||||
import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
|
||||
import { dashboardPermissionLevels } from 'app/types/acl';
|
||||
|
||||
export interface Props {
|
||||
item: any;
|
||||
@ -24,7 +24,7 @@ export default class DisabledPermissionListItem extends Component<Props, any> {
|
||||
<td>
|
||||
<div className="gf-form">
|
||||
<DescriptionPicker
|
||||
optionsWithDesc={permissionOptions}
|
||||
optionsWithDesc={dashboardPermissionLevels}
|
||||
onSelected={() => {}}
|
||||
value={item.permission}
|
||||
disabled={true}
|
||||
|
@ -6,11 +6,10 @@ import coreModule from 'app/core/core_module';
|
||||
import { profiler } from 'app/core/profiler';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import Drop from 'tether-drop';
|
||||
import { createStore } from 'app/stores/store';
|
||||
import colors from 'app/core/utils/colors';
|
||||
import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { configureStore } from 'app/stores/configureStore';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
export class GrafanaCtrl {
|
||||
/** @ngInject */
|
||||
@ -28,7 +27,6 @@ export class GrafanaCtrl {
|
||||
// sets singleston instances for angular services so react components can access them
|
||||
configureStore();
|
||||
setBackendSrv(backendSrv);
|
||||
createStore({ backendSrv, datasourceSrv });
|
||||
|
||||
$scope.init = () => {
|
||||
$scope.contextSrv = contextSrv;
|
||||
|
@ -1,8 +1,6 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { store } from 'app/stores/store';
|
||||
import { store as reduxStore } from 'app/stores/configureStore';
|
||||
import { reaction } from 'mobx';
|
||||
import { store } from 'app/store/configureStore';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
@ -18,12 +16,9 @@ export class BridgeSrv {
|
||||
init() {
|
||||
this.$rootScope.$on('$routeUpdate', (evt, data) => {
|
||||
const angularUrl = this.$location.url();
|
||||
if (store.view.currentUrl !== angularUrl) {
|
||||
store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params);
|
||||
}
|
||||
const state = reduxStore.getState();
|
||||
const state = store.getState();
|
||||
if (state.location.url !== angularUrl) {
|
||||
reduxStore.dispatch(
|
||||
store.dispatch(
|
||||
updateLocation({
|
||||
path: this.$location.path(),
|
||||
query: this.$location.search(),
|
||||
@ -34,8 +29,7 @@ export class BridgeSrv {
|
||||
});
|
||||
|
||||
this.$rootScope.$on('$routeChangeSuccess', (evt, data) => {
|
||||
store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params);
|
||||
reduxStore.dispatch(
|
||||
store.dispatch(
|
||||
updateLocation({
|
||||
path: this.$location.path(),
|
||||
query: this.$location.search(),
|
||||
@ -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
|
||||
reduxStore.subscribe(() => {
|
||||
const state = reduxStore.getState();
|
||||
store.subscribe(() => {
|
||||
const state = store.getState();
|
||||
const angularUrl = this.$location.url();
|
||||
const url = locationUtil.stripBaseFromUrl(state.location.url);
|
||||
if (angularUrl !== url) {
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
import PermissionList from 'app/core/components/PermissionList/PermissionList';
|
||||
import AddPermission from 'app/core/components/PermissionList/AddPermission';
|
||||
import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo';
|
||||
import { store } from 'app/stores/configureStore';
|
||||
import { store } from 'app/store/configureStore';
|
||||
|
||||
export interface Props {
|
||||
dashboardId: number;
|
||||
@ -65,7 +65,6 @@ export class DashboardPermissions extends PureComponent<Props, State> {
|
||||
render() {
|
||||
const { permissions, folder } = this.props;
|
||||
const { isAdding } = this.state;
|
||||
console.log('DashboardPermissions', this.props);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { toJS } from 'mobx';
|
||||
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 {
|
||||
datasourceMeta: any;
|
||||
@ -9,11 +10,8 @@ export class DataSourceDashboardsCtrl {
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private $routeParams) {
|
||||
if (store.nav.main === null) {
|
||||
store.nav.load('cfg', 'datasources');
|
||||
}
|
||||
|
||||
this.navModel = toJS(store.nav);
|
||||
const state = store.getState();
|
||||
this.navModel = getNavModel(state.navIndex, 'datasources');
|
||||
|
||||
if (this.$routeParams.id) {
|
||||
this.getDatasourceById(this.$routeParams.id);
|
||||
@ -30,8 +28,7 @@ export class DataSourceDashboardsCtrl {
|
||||
}
|
||||
|
||||
updateNav() {
|
||||
store.nav.initDatasourceEditNav(this.current, this.datasourceMeta, 'datasource-dashboards');
|
||||
this.navModel = toJS(store.nav);
|
||||
this.navModel = buildNavModel(this.current, this.datasourceMeta, 'datasource-dashboards');
|
||||
}
|
||||
|
||||
getPluginInfo() {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import { toJS } from 'mobx';
|
||||
import config from 'app/core/config';
|
||||
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 = [];
|
||||
|
||||
@ -31,11 +32,8 @@ export class DataSourceEditCtrl {
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $q, private backendSrv, private $routeParams, private $location, private datasourceSrv) {
|
||||
if (store.nav.main === null) {
|
||||
store.nav.load('cfg', 'datasources');
|
||||
}
|
||||
|
||||
this.navModel = toJS(store.nav);
|
||||
const state = store.getState();
|
||||
this.navModel = getNavModel(state.navIndex, 'datasources');
|
||||
this.datasources = [];
|
||||
|
||||
this.loadDatasourceTypes().then(() => {
|
||||
@ -101,8 +99,7 @@ export class DataSourceEditCtrl {
|
||||
}
|
||||
|
||||
updateNav() {
|
||||
store.nav.initDatasourceEditNav(this.current, this.datasourceMeta, 'datasource-settings');
|
||||
this.navModel = toJS(store.nav);
|
||||
this.navModel = buildNavModel(this.current, this.datasourceMeta, 'datasource-settings');
|
||||
}
|
||||
|
||||
typeChanged() {
|
||||
|
45
public/app/features/plugins/state/navModel.ts
Normal file
45
public/app/features/plugins/state/navModel.ts
Normal file
@ -0,0 +1,45 @@
|
||||
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 }),
|
||||
};
|
||||
}
|
@ -1,22 +1,18 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'mobx-react';
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { store } from 'app/stores/store';
|
||||
import { store as reduxStore } from 'app/stores/configureStore';
|
||||
import { store } from 'app/store/configureStore';
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { ContextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
function WrapInProvider(store, Component, props) {
|
||||
return (
|
||||
<ReduxProvider store={reduxStore}>
|
||||
<Provider {...store}>
|
||||
<Component {...props} />
|
||||
</Provider>
|
||||
</ReduxProvider>
|
||||
<Provider store={store}>
|
||||
<Component {...props} />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,19 +0,0 @@
|
||||
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,
|
||||
});
|
@ -1,47 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
@ -1,118 +0,0 @@
|
||||
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);
|
||||
},
|
||||
}));
|
@ -1,116 +0,0 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,259 +0,0 @@
|
||||
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)
|
||||
);
|
||||
};
|
@ -1,29 +0,0 @@
|
||||
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;
|
||||
},
|
||||
}));
|
@ -1,20 +0,0 @@
|
||||
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 {}
|
@ -1,33 +0,0 @@
|
||||
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');
|
||||
});
|
||||
});
|
@ -1,55 +0,0 @@
|
||||
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,
|
||||
};
|
||||
});
|
@ -1,16 +0,0 @@
|
||||
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;
|
||||
}
|
7
public/app/types/datasources.ts
Normal file
7
public/app/types/datasources.ts
Normal file
@ -0,0 +1,7 @@
|
||||
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';
|
||||
import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting';
|
||||
import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location';
|
||||
import { NavModel, NavModelItem, NavIndex } from './navModel';
|
||||
import { FolderDTO, FolderState, FolderInfo } from './folder';
|
||||
import { FolderDTO, FolderState, FolderInfo } from './folders';
|
||||
import { DashboardState } from './dashboard';
|
||||
import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
|
||||
import { DataSource } from './datasources';
|
||||
import { PluginMeta } from './plugins';
|
||||
|
||||
export {
|
||||
Team,
|
||||
@ -29,6 +31,8 @@ export {
|
||||
DashboardAcl,
|
||||
OrgRole,
|
||||
PermissionLevel,
|
||||
DataSource,
|
||||
PluginMeta,
|
||||
};
|
||||
|
||||
export interface StoreState {
|
||||
|
19
public/app/types/plugins.ts
Normal file
19
public/app/types/plugins.ts
Normal file
@ -0,0 +1,19 @@
|
||||
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;
|
||||
};
|
||||
}
|
20
yarn.lock
20
yarn.lock
@ -5258,7 +5258,7 @@ hmac-drbg@^1.0.0:
|
||||
minimalistic-assert "^1.0.0"
|
||||
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"
|
||||
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
|
||||
dependencies:
|
||||
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:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794"
|
||||
|
Loading…
Reference in New Issue
Block a user