mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #13273 from grafana/permissions-to-redux
Permissions to redux
This commit is contained in:
commit
693f2fd8e9
@ -5,7 +5,6 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
|
|||||||
import { SearchResult } from './components/search/SearchResult';
|
import { SearchResult } from './components/search/SearchResult';
|
||||||
import { TagFilter } from './components/TagFilter/TagFilter';
|
import { TagFilter } from './components/TagFilter/TagFilter';
|
||||||
import { SideMenu } from './components/sidemenu/SideMenu';
|
import { SideMenu } from './components/sidemenu/SideMenu';
|
||||||
import DashboardPermissions from './components/Permissions/DashboardPermissions';
|
|
||||||
|
|
||||||
export function registerAngularDirectives() {
|
export function registerAngularDirectives() {
|
||||||
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
||||||
@ -18,5 +17,4 @@ export function registerAngularDirectives() {
|
|||||||
['onSelect', { watchDepth: 'reference' }],
|
['onSelect', { watchDepth: 'reference' }],
|
||||||
['tagOptions', { watchDepth: 'reference' }],
|
['tagOptions', { watchDepth: 'reference' }],
|
||||||
]);
|
]);
|
||||||
react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']);
|
|
||||||
}
|
}
|
||||||
|
@ -1,77 +1,90 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { observer } from 'mobx-react';
|
|
||||||
import { aclTypes } from 'app/stores/PermissionsStore/PermissionsStore';
|
|
||||||
import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
|
import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
|
||||||
import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker';
|
import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker';
|
||||||
import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker';
|
import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker';
|
||||||
import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
|
import {
|
||||||
|
dashboardPermissionLevels,
|
||||||
|
dashboardAclTargets,
|
||||||
|
AclTarget,
|
||||||
|
PermissionLevel,
|
||||||
|
NewDashboardAclItem,
|
||||||
|
OrgRole,
|
||||||
|
} from 'app/types/acl';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
permissions: any;
|
onAddPermission: (item: NewDashboardAclItem) => void;
|
||||||
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
class AddPermissions extends Component<Props, NewDashboardAclItem> {
|
||||||
class AddPermissions extends Component<Props, any> {
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.state = this.getCleanState();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
getCleanState() {
|
||||||
const { permissions } = this.props;
|
return {
|
||||||
permissions.resetNewType();
|
userId: 0,
|
||||||
|
teamId: 0,
|
||||||
|
type: AclTarget.Team,
|
||||||
|
permission: PermissionLevel.View,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onTypeChanged = evt => {
|
onTypeChanged = evt => {
|
||||||
const { value } = evt.target;
|
const type = evt.target.value as AclTarget;
|
||||||
const { permissions } = this.props;
|
|
||||||
|
|
||||||
permissions.setNewType(value);
|
switch (type) {
|
||||||
|
case AclTarget.User:
|
||||||
|
case AclTarget.Team:
|
||||||
|
this.setState({ type: type, userId: 0, teamId: 0, role: undefined });
|
||||||
|
break;
|
||||||
|
case AclTarget.Editor:
|
||||||
|
this.setState({ type: type, userId: 0, teamId: 0, role: OrgRole.Editor });
|
||||||
|
break;
|
||||||
|
case AclTarget.Viewer:
|
||||||
|
this.setState({ type: type, userId: 0, teamId: 0, role: OrgRole.Viewer });
|
||||||
|
break;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onUserSelected = (user: User) => {
|
onUserSelected = (user: User) => {
|
||||||
const { permissions } = this.props;
|
this.setState({ userId: user ? user.id : 0 });
|
||||||
if (!user) {
|
|
||||||
permissions.newItem.setUser(null, null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return permissions.newItem.setUser(user.id, user.login, user.avatarUrl);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onTeamSelected = (team: Team) => {
|
onTeamSelected = (team: Team) => {
|
||||||
const { permissions } = this.props;
|
this.setState({ teamId: team ? team.id : 0 });
|
||||||
if (!team) {
|
|
||||||
permissions.newItem.setTeam(null, null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return permissions.newItem.setTeam(team.id, team.name, team.avatarUrl);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onPermissionChanged = (permission: OptionWithDescription) => {
|
onPermissionChanged = (permission: OptionWithDescription) => {
|
||||||
const { permissions } = this.props;
|
this.setState({ permission: permission.value });
|
||||||
return permissions.newItem.setPermission(permission.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
resetNewType() {
|
onSubmit = async evt => {
|
||||||
const { permissions } = this.props;
|
evt.preventDefault();
|
||||||
return permissions.resetNewType();
|
await this.props.onAddPermission(this.state);
|
||||||
|
this.setState(this.getCleanState());
|
||||||
|
};
|
||||||
|
|
||||||
|
isValid() {
|
||||||
|
switch (this.state.type) {
|
||||||
|
case AclTarget.Team:
|
||||||
|
return this.state.teamId > 0;
|
||||||
|
case AclTarget.User:
|
||||||
|
return this.state.userId > 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit = evt => {
|
|
||||||
evt.preventDefault();
|
|
||||||
const { permissions } = this.props;
|
|
||||||
permissions.addStoreItem();
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { permissions } = this.props;
|
const { onCancel } = this.props;
|
||||||
const newItem = permissions.newItem;
|
const newItem = this.state;
|
||||||
const pickerClassName = 'width-20';
|
const pickerClassName = 'width-20';
|
||||||
|
const isValid = this.isValid();
|
||||||
const isValid = newItem.isValid();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gf-form-inline cta-form">
|
<div className="gf-form-inline cta-form">
|
||||||
<button className="cta-form__close btn btn-transparent" onClick={permissions.hideAddPermissions}>
|
<button className="cta-form__close btn btn-transparent" onClick={onCancel}>
|
||||||
<i className="fa fa-close" />
|
<i className="fa fa-close" />
|
||||||
</button>
|
</button>
|
||||||
<form name="addPermission" onSubmit={this.onSubmit}>
|
<form name="addPermission" onSubmit={this.onSubmit}>
|
||||||
@ -80,7 +93,7 @@ class AddPermissions extends Component<Props, any> {
|
|||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<div className="gf-form-select-wrapper">
|
<div className="gf-form-select-wrapper">
|
||||||
<select className="gf-form-input gf-size-auto" value={newItem.type} onChange={this.onTypeChanged}>
|
<select className="gf-form-input gf-size-auto" value={newItem.type} onChange={this.onTypeChanged}>
|
||||||
{aclTypes.map((option, idx) => {
|
{dashboardAclTargets.map((option, idx) => {
|
||||||
return (
|
return (
|
||||||
<option key={idx} value={option.value}>
|
<option key={idx} value={option.value}>
|
||||||
{option.text}
|
{option.text}
|
||||||
@ -91,21 +104,29 @@ class AddPermissions extends Component<Props, any> {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{newItem.type === 'User' ? (
|
{newItem.type === AclTarget.User ? (
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<UserPicker onSelected={this.onUserSelected} value={newItem.userId} className={pickerClassName} />
|
<UserPicker
|
||||||
|
onSelected={this.onUserSelected}
|
||||||
|
value={newItem.userId.toString()}
|
||||||
|
className={pickerClassName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{newItem.type === 'Group' ? (
|
{newItem.type === AclTarget.Team ? (
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<TeamPicker onSelected={this.onTeamSelected} value={newItem.teamId} className={pickerClassName} />
|
<TeamPicker
|
||||||
|
onSelected={this.onTeamSelected}
|
||||||
|
value={newItem.teamId.toString()}
|
||||||
|
className={pickerClassName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<DescriptionPicker
|
<DescriptionPicker
|
||||||
optionsWithDesc={permissionOptions}
|
optionsWithDesc={dashboardPermissionLevels}
|
||||||
onSelected={this.onPermissionChanged}
|
onSelected={this.onPermissionChanged}
|
||||||
value={newItem.permission}
|
value={newItem.permission}
|
||||||
disabled={false}
|
disabled={false}
|
@ -1,21 +1,20 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import PermissionsListItem from './PermissionsListItem';
|
import PermissionsListItem from './PermissionListItem';
|
||||||
import DisabledPermissionsListItem from './DisabledPermissionsListItem';
|
import DisabledPermissionsListItem from './DisabledPermissionListItem';
|
||||||
import { observer } from 'mobx-react';
|
import { FolderInfo } from 'app/types';
|
||||||
import { FolderInfo } from './FolderInfo';
|
import { DashboardAcl } from 'app/types/acl';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
permissions: any[];
|
items: DashboardAcl[];
|
||||||
removeItem: any;
|
onRemoveItem: (item: DashboardAcl) => void;
|
||||||
permissionChanged: any;
|
onPermissionChanged: any;
|
||||||
fetching: boolean;
|
isFetching: boolean;
|
||||||
folderInfo?: FolderInfo;
|
folderInfo?: FolderInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
class PermissionList extends PureComponent<Props> {
|
||||||
class PermissionsList extends Component<Props, any> {
|
|
||||||
render() {
|
render() {
|
||||||
const { permissions, removeItem, permissionChanged, fetching, folderInfo } = this.props;
|
const { items, onRemoveItem, onPermissionChanged, isFetching, folderInfo } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="filter-table gf-form-group">
|
<table className="filter-table gf-form-group">
|
||||||
@ -28,19 +27,18 @@ class PermissionsList extends Component<Props, any> {
|
|||||||
icon: 'fa fa-fw fa-street-view',
|
icon: 'fa fa-fw fa-street-view',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{permissions.map((item, idx) => {
|
{items.map((item, idx) => {
|
||||||
return (
|
return (
|
||||||
<PermissionsListItem
|
<PermissionsListItem
|
||||||
key={idx + 1}
|
key={idx + 1}
|
||||||
item={item}
|
item={item}
|
||||||
itemIndex={idx}
|
onRemoveItem={onRemoveItem}
|
||||||
removeItem={removeItem}
|
onPermissionChanged={onPermissionChanged}
|
||||||
permissionChanged={permissionChanged}
|
|
||||||
folderInfo={folderInfo}
|
folderInfo={folderInfo}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{fetching === true && permissions.length < 1 ? (
|
{isFetching === true && items.length < 1 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={4}>
|
<td colSpan={4}>
|
||||||
<em>Loading permissions...</em>
|
<em>Loading permissions...</em>
|
||||||
@ -48,7 +46,7 @@ class PermissionsList extends Component<Props, any> {
|
|||||||
</tr>
|
</tr>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{fetching === false && permissions.length < 1 ? (
|
{isFetching === false && items.length < 1 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={4}>
|
<td colSpan={4}>
|
||||||
<em>No permissions are set. Will only be accessible by admins.</em>
|
<em>No permissions are set. Will only be accessible by admins.</em>
|
||||||
@ -61,4 +59,4 @@ class PermissionsList extends Component<Props, any> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PermissionsList;
|
export default PermissionList;
|
100
public/app/core/components/PermissionList/PermissionListItem.tsx
Normal file
100
public/app/core/components/PermissionList/PermissionListItem.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
|
||||||
|
import { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl';
|
||||||
|
import { FolderInfo } from 'app/types';
|
||||||
|
|
||||||
|
const setClassNameHelper = inherited => {
|
||||||
|
return inherited ? 'gf-form-disabled' : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
function ItemAvatar({ item }) {
|
||||||
|
if (item.userAvatarUrl) {
|
||||||
|
return <img className="filter-table__avatar" src={item.userAvatarUrl} />;
|
||||||
|
}
|
||||||
|
if (item.teamAvatarUrl) {
|
||||||
|
return <img className="filter-table__avatar" src={item.teamAvatarUrl} />;
|
||||||
|
}
|
||||||
|
if (item.role === 'Editor') {
|
||||||
|
return <i style={{ width: '25px', height: '25px' }} className="gicon gicon-editor" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <i style={{ width: '25px', height: '25px' }} className="gicon gicon-viewer" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ItemDescription({ item }) {
|
||||||
|
if (item.userId) {
|
||||||
|
return <span className="filter-table__weak-italic">(User)</span>;
|
||||||
|
}
|
||||||
|
if (item.teamId) {
|
||||||
|
return <span className="filter-table__weak-italic">(Team)</span>;
|
||||||
|
}
|
||||||
|
return <span className="filter-table__weak-italic">(Role)</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
item: DashboardAcl;
|
||||||
|
onRemoveItem: (item: DashboardAcl) => void;
|
||||||
|
onPermissionChanged: (item: DashboardAcl, level: PermissionLevel) => void;
|
||||||
|
folderInfo?: FolderInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class PermissionsListItem extends PureComponent<Props> {
|
||||||
|
onPermissionChanged = option => {
|
||||||
|
this.props.onPermissionChanged(this.props.item, option.value as PermissionLevel);
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveItem = () => {
|
||||||
|
this.props.onRemoveItem(this.props.item);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { item, folderInfo } = this.props;
|
||||||
|
const inheritedFromRoot = item.dashboardId === -1 && !item.inherited;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className={setClassNameHelper(item.inherited)}>
|
||||||
|
<td style={{ width: '1%' }}>
|
||||||
|
<ItemAvatar item={item} />
|
||||||
|
</td>
|
||||||
|
<td style={{ width: '90%' }}>
|
||||||
|
{item.name} <ItemDescription item={item} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{item.inherited &&
|
||||||
|
folderInfo && (
|
||||||
|
<em className="muted no-wrap">
|
||||||
|
Inherited from folder{' '}
|
||||||
|
<a className="text-link" href={`${folderInfo.url}/permissions`}>
|
||||||
|
{folderInfo.title}
|
||||||
|
</a>{' '}
|
||||||
|
</em>
|
||||||
|
)}
|
||||||
|
{inheritedFromRoot && <em className="muted no-wrap">Default Permission</em>}
|
||||||
|
</td>
|
||||||
|
<td className="query-keyword">Can</td>
|
||||||
|
<td>
|
||||||
|
<div className="gf-form">
|
||||||
|
<DescriptionPicker
|
||||||
|
optionsWithDesc={dashboardPermissionLevels}
|
||||||
|
onSelected={this.onPermissionChanged}
|
||||||
|
value={item.permission}
|
||||||
|
disabled={item.inherited}
|
||||||
|
className={'gf-form-input--form-dropdown-right'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{!item.inherited ? (
|
||||||
|
<a className="btn btn-danger btn-small" onClick={this.onRemoveItem}>
|
||||||
|
<i className="fa fa-remove" />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<button className="btn btn-inverse btn-small">
|
||||||
|
<i className="fa fa-lock" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,90 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import AddPermissions from './AddPermissions';
|
|
||||||
import { RootStore } from 'app/stores/RootStore/RootStore';
|
|
||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
|
||||||
|
|
||||||
jest.mock('app/core/services/backend_srv', () => ({
|
|
||||||
getBackendSrv: () => {
|
|
||||||
return {
|
|
||||||
get: () => {
|
|
||||||
return Promise.resolve([
|
|
||||||
{ id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' },
|
|
||||||
{ id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' },
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
post: jest.fn(() => Promise.resolve({})),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('AddPermissions', () => {
|
|
||||||
let wrapper;
|
|
||||||
let store;
|
|
||||||
let instance;
|
|
||||||
const backendSrv: any = getBackendSrv();
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
store = RootStore.create({}, { backendSrv: backendSrv });
|
|
||||||
wrapper = shallow(<AddPermissions permissions={store.permissions} />);
|
|
||||||
instance = wrapper.instance();
|
|
||||||
return store.permissions.load(1, true, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when permission for a user is added', () => {
|
|
||||||
it('should save permission to db', () => {
|
|
||||||
const evt = {
|
|
||||||
target: {
|
|
||||||
value: 'User',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const userItem = {
|
|
||||||
id: 2,
|
|
||||||
login: 'user2',
|
|
||||||
};
|
|
||||||
|
|
||||||
instance.onTypeChanged(evt);
|
|
||||||
instance.onUserSelected(userItem);
|
|
||||||
|
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
expect(wrapper.find('[data-save-permission]').prop('disabled')).toBe(false);
|
|
||||||
|
|
||||||
wrapper.find('form').simulate('submit', { preventDefault() {} });
|
|
||||||
|
|
||||||
expect(backendSrv.post.mock.calls.length).toBe(1);
|
|
||||||
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when permission for team is added', () => {
|
|
||||||
it('should save permission to db', () => {
|
|
||||||
const evt = {
|
|
||||||
target: {
|
|
||||||
value: 'Group',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const teamItem = {
|
|
||||||
id: 2,
|
|
||||||
name: 'ug1',
|
|
||||||
};
|
|
||||||
|
|
||||||
instance.onTypeChanged(evt);
|
|
||||||
instance.onTeamSelected(teamItem);
|
|
||||||
|
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
expect(wrapper.find('[data-save-permission]').prop('disabled')).toBe(false);
|
|
||||||
|
|
||||||
wrapper.find('form').simulate('submit', { preventDefault() {} });
|
|
||||||
|
|
||||||
expect(backendSrv.post.mock.calls.length).toBe(1);
|
|
||||||
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
backendSrv.post.mockClear();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,71 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { observer } from 'mobx-react';
|
|
||||||
import { store } from 'app/stores/store';
|
|
||||||
import Permissions from 'app/core/components/Permissions/Permissions';
|
|
||||||
import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
|
||||||
import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo';
|
|
||||||
import AddPermissions from 'app/core/components/Permissions/AddPermissions';
|
|
||||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
|
||||||
import { FolderInfo } from './FolderInfo';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
dashboardId: number;
|
|
||||||
folder?: FolderInfo;
|
|
||||||
backendSrv: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
class DashboardPermissions extends Component<Props, any> {
|
|
||||||
permissions: any;
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.handleAddPermission = this.handleAddPermission.bind(this);
|
|
||||||
this.permissions = store.permissions;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAddPermission() {
|
|
||||||
this.permissions.toggleAddPermissions();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.permissions.hideAddPermissions();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { dashboardId, folder, backendSrv } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="dashboard-settings__header">
|
|
||||||
<div className="page-action-bar">
|
|
||||||
<h3 className="d-inline-block">Permissions</h3>
|
|
||||||
<Tooltip className="page-sub-heading-icon" placement="auto" content={PermissionsInfo}>
|
|
||||||
<i className="gicon gicon-question gicon--has-hover" />
|
|
||||||
</Tooltip>
|
|
||||||
<div className="page-action-bar__spacer" />
|
|
||||||
<button
|
|
||||||
className="btn btn-success pull-right"
|
|
||||||
onClick={this.handleAddPermission}
|
|
||||||
disabled={this.permissions.isAddPermissionsVisible}
|
|
||||||
>
|
|
||||||
<i className="fa fa-plus" /> Add Permission
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SlideDown in={this.permissions.isAddPermissionsVisible}>
|
|
||||||
<AddPermissions permissions={this.permissions} />
|
|
||||||
</SlideDown>
|
|
||||||
<Permissions
|
|
||||||
permissions={this.permissions}
|
|
||||||
isFolder={false}
|
|
||||||
dashboardId={dashboardId}
|
|
||||||
folderInfo={folder}
|
|
||||||
backendSrv={backendSrv}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DashboardPermissions;
|
|
@ -1,5 +0,0 @@
|
|||||||
export interface FolderInfo {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PermissionsList from './PermissionsList';
|
|
||||||
import { observer } from 'mobx-react';
|
|
||||||
import { FolderInfo } from './FolderInfo';
|
|
||||||
|
|
||||||
export interface DashboardAcl {
|
|
||||||
id?: number;
|
|
||||||
dashboardId?: number;
|
|
||||||
userId?: number;
|
|
||||||
userLogin?: string;
|
|
||||||
userEmail?: string;
|
|
||||||
teamId?: number;
|
|
||||||
team?: string;
|
|
||||||
permission?: number;
|
|
||||||
permissionName?: string;
|
|
||||||
role?: string;
|
|
||||||
icon?: string;
|
|
||||||
name?: string;
|
|
||||||
inherited?: boolean;
|
|
||||||
sortRank?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
dashboardId: number;
|
|
||||||
folderInfo?: FolderInfo;
|
|
||||||
permissions?: any;
|
|
||||||
isFolder: boolean;
|
|
||||||
backendSrv: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
class Permissions extends Component<Props, any> {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
const { dashboardId, isFolder, folderInfo } = this.props;
|
|
||||||
this.permissionChanged = this.permissionChanged.bind(this);
|
|
||||||
this.typeChanged = this.typeChanged.bind(this);
|
|
||||||
this.removeItem = this.removeItem.bind(this);
|
|
||||||
this.loadStore(dashboardId, isFolder, folderInfo && folderInfo.id === 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadStore(dashboardId, isFolder, isInRoot = false) {
|
|
||||||
return this.props.permissions.load(dashboardId, isFolder, isInRoot);
|
|
||||||
}
|
|
||||||
|
|
||||||
permissionChanged(index: number, permission: number, permissionName: string) {
|
|
||||||
const { permissions } = this.props;
|
|
||||||
permissions.updatePermissionOnIndex(index, permission, permissionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeItem(index: number) {
|
|
||||||
const { permissions } = this.props;
|
|
||||||
permissions.removeStoreItem(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetNewType() {
|
|
||||||
const { permissions } = this.props;
|
|
||||||
permissions.resetNewType();
|
|
||||||
}
|
|
||||||
|
|
||||||
typeChanged(evt) {
|
|
||||||
const { value } = evt.target;
|
|
||||||
const { permissions, dashboardId } = this.props;
|
|
||||||
|
|
||||||
if (value === 'Viewer' || value === 'Editor') {
|
|
||||||
permissions.addStoreItem({ permission: 1, role: value, dashboardId: dashboardId }, dashboardId);
|
|
||||||
this.resetNewType();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
permissions.setNewType(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { permissions, folderInfo } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="gf-form-group">
|
|
||||||
<PermissionsList
|
|
||||||
permissions={permissions.items}
|
|
||||||
removeItem={this.removeItem}
|
|
||||||
permissionChanged={this.permissionChanged}
|
|
||||||
fetching={permissions.fetching}
|
|
||||||
folderInfo={folderInfo}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Permissions;
|
|
@ -1,91 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { observer } from 'mobx-react';
|
|
||||||
import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
|
|
||||||
import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
|
|
||||||
|
|
||||||
const setClassNameHelper = inherited => {
|
|
||||||
return inherited ? 'gf-form-disabled' : '';
|
|
||||||
};
|
|
||||||
|
|
||||||
function ItemAvatar({ item }) {
|
|
||||||
if (item.userAvatarUrl) {
|
|
||||||
return <img className="filter-table__avatar" src={item.userAvatarUrl} />;
|
|
||||||
}
|
|
||||||
if (item.teamAvatarUrl) {
|
|
||||||
return <img className="filter-table__avatar" src={item.teamAvatarUrl} />;
|
|
||||||
}
|
|
||||||
if (item.role === 'Editor') {
|
|
||||||
return <i style={{ width: '25px', height: '25px' }} className="gicon gicon-editor" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <i style={{ width: '25px', height: '25px' }} className="gicon gicon-viewer" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemDescription({ item }) {
|
|
||||||
if (item.userId) {
|
|
||||||
return <span className="filter-table__weak-italic">(User)</span>;
|
|
||||||
}
|
|
||||||
if (item.teamId) {
|
|
||||||
return <span className="filter-table__weak-italic">(Team)</span>;
|
|
||||||
}
|
|
||||||
return <span className="filter-table__weak-italic">(Role)</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default observer(({ item, removeItem, permissionChanged, itemIndex, folderInfo }) => {
|
|
||||||
const handleRemoveItem = evt => {
|
|
||||||
evt.preventDefault();
|
|
||||||
removeItem(itemIndex);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangePermission = permissionOption => {
|
|
||||||
permissionChanged(itemIndex, permissionOption.value, permissionOption.label);
|
|
||||||
};
|
|
||||||
|
|
||||||
const inheritedFromRoot = item.dashboardId === -1 && !item.inherited;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr className={setClassNameHelper(item.inherited)}>
|
|
||||||
<td style={{ width: '1%' }}>
|
|
||||||
<ItemAvatar item={item} />
|
|
||||||
</td>
|
|
||||||
<td style={{ width: '90%' }}>
|
|
||||||
{item.name} <ItemDescription item={item} />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{item.inherited &&
|
|
||||||
folderInfo && (
|
|
||||||
<em className="muted no-wrap">
|
|
||||||
Inherited from folder{' '}
|
|
||||||
<a className="text-link" href={`${folderInfo.url}/permissions`}>
|
|
||||||
{folderInfo.title}
|
|
||||||
</a>{' '}
|
|
||||||
</em>
|
|
||||||
)}
|
|
||||||
{inheritedFromRoot && <em className="muted no-wrap">Default Permission</em>}
|
|
||||||
</td>
|
|
||||||
<td className="query-keyword">Can</td>
|
|
||||||
<td>
|
|
||||||
<div className="gf-form">
|
|
||||||
<DescriptionPicker
|
|
||||||
optionsWithDesc={permissionOptions}
|
|
||||||
onSelected={handleChangePermission}
|
|
||||||
value={item.permission}
|
|
||||||
disabled={item.inherited}
|
|
||||||
className={'gf-form-input--form-dropdown-right'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{!item.inherited ? (
|
|
||||||
<a className="btn btn-danger btn-small" onClick={handleRemoveItem}>
|
|
||||||
<i className="fa fa-remove" />
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<button className="btn btn-inverse btn-small">
|
|
||||||
<i className="fa fa-lock" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
});
|
|
31
public/app/core/reducers/processsAclItems.ts
Normal file
31
public/app/core/reducers/processsAclItems.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { DashboardAcl, DashboardAclDTO } from 'app/types/acl';
|
||||||
|
|
||||||
|
export function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] {
|
||||||
|
return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function processAclItem(dto: DashboardAclDTO): DashboardAcl {
|
||||||
|
const item = dto as DashboardAcl;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
31
public/app/core/utils/acl.ts
Normal file
31
public/app/core/utils/acl.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { DashboardAcl, DashboardAclDTO } from 'app/types/acl';
|
||||||
|
|
||||||
|
export function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] {
|
||||||
|
return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function processAclItem(dto: DashboardAclDTO): DashboardAcl {
|
||||||
|
const item = dto as DashboardAcl;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
@ -30,6 +30,12 @@ import './settings/settings';
|
|||||||
import './panellinks/module';
|
import './panellinks/module';
|
||||||
import './dashlinks/module';
|
import './dashlinks/module';
|
||||||
|
|
||||||
|
// angular wrappers
|
||||||
|
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||||
|
import DashboardPermissions from './permissions/DashboardPermissions';
|
||||||
|
|
||||||
|
react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']);
|
||||||
|
|
||||||
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 { DashboardImportCtrl } from './dashboard_import_ctrl';
|
import { DashboardImportCtrl } from './dashboard_import_ctrl';
|
||||||
|
@ -0,0 +1,117 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
||||||
|
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||||
|
import { StoreState, FolderInfo } from 'app/types';
|
||||||
|
import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';
|
||||||
|
import {
|
||||||
|
getDashboardPermissions,
|
||||||
|
addDashboardPermission,
|
||||||
|
removeDashboardPermission,
|
||||||
|
updateDashboardPermission,
|
||||||
|
} from '../state/actions';
|
||||||
|
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';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
dashboardId: number;
|
||||||
|
folder?: FolderInfo;
|
||||||
|
permissions: DashboardAcl[];
|
||||||
|
getDashboardPermissions: typeof getDashboardPermissions;
|
||||||
|
updateDashboardPermission: typeof updateDashboardPermission;
|
||||||
|
removeDashboardPermission: typeof removeDashboardPermission;
|
||||||
|
addDashboardPermission: typeof addDashboardPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
isAdding: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DashboardPermissions extends PureComponent<Props, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isAdding: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.getDashboardPermissions(this.props.dashboardId);
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpenAddPermissions = () => {
|
||||||
|
this.setState({ isAdding: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveItem = (item: DashboardAcl) => {
|
||||||
|
this.props.removeDashboardPermission(this.props.dashboardId, item);
|
||||||
|
};
|
||||||
|
|
||||||
|
onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => {
|
||||||
|
this.props.updateDashboardPermission(this.props.dashboardId, item, level);
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddPermission = (newItem: NewDashboardAclItem) => {
|
||||||
|
return this.props.addDashboardPermission(this.props.dashboardId, newItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancelAddPermission = () => {
|
||||||
|
this.setState({ isAdding: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { permissions, folder } = this.props;
|
||||||
|
const { isAdding } = this.state;
|
||||||
|
console.log('DashboardPermissions', this.props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="dashboard-settings__header">
|
||||||
|
<div className="page-action-bar">
|
||||||
|
<h3 className="d-inline-block">Permissions</h3>
|
||||||
|
<Tooltip className="page-sub-heading-icon" placement="auto" content={PermissionsInfo}>
|
||||||
|
<i className="gicon gicon-question gicon--has-hover" />
|
||||||
|
</Tooltip>
|
||||||
|
<div className="page-action-bar__spacer" />
|
||||||
|
<button className="btn btn-success pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
|
||||||
|
<i className="fa fa-plus" /> Add Permission
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SlideDown in={isAdding}>
|
||||||
|
<AddPermission onAddPermission={this.onAddPermission} onCancel={this.onCancelAddPermission} />
|
||||||
|
</SlideDown>
|
||||||
|
<PermissionList
|
||||||
|
items={permissions}
|
||||||
|
onRemoveItem={this.onRemoveItem}
|
||||||
|
onPermissionChanged={this.onPermissionChanged}
|
||||||
|
isFetching={false}
|
||||||
|
folderInfo={folder}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWithStore(WrappedComponent, ...args) {
|
||||||
|
const ConnectedWrappedComponent = connect(...args)(WrappedComponent);
|
||||||
|
return props => {
|
||||||
|
return <ConnectedWrappedComponent {...props} store={store} />;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StoreState) => ({
|
||||||
|
permissions: state.dashboard.permissions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
getDashboardPermissions,
|
||||||
|
addDashboardPermission,
|
||||||
|
removeDashboardPermission,
|
||||||
|
updateDashboardPermission,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connectWithStore(DashboardPermissions, mapStateToProps, mapDispatchToProps);
|
115
public/app/features/dashboard/state/actions.ts
Normal file
115
public/app/features/dashboard/state/actions.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { StoreState } from 'app/types';
|
||||||
|
import { ThunkAction } from 'redux-thunk';
|
||||||
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DashboardAcl,
|
||||||
|
DashboardAclDTO,
|
||||||
|
PermissionLevel,
|
||||||
|
DashboardAclUpdateDTO,
|
||||||
|
NewDashboardAclItem,
|
||||||
|
} from 'app/types/acl';
|
||||||
|
|
||||||
|
export enum ActionTypes {
|
||||||
|
LoadDashboardPermissions = 'LOAD_DASHBOARD_PERMISSIONS',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadDashboardPermissionsAction {
|
||||||
|
type: ActionTypes.LoadDashboardPermissions;
|
||||||
|
payload: DashboardAcl[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Action = LoadDashboardPermissionsAction;
|
||||||
|
|
||||||
|
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
|
||||||
|
|
||||||
|
export const loadDashboardPermissions = (items: DashboardAclDTO[]): LoadDashboardPermissionsAction => ({
|
||||||
|
type: ActionTypes.LoadDashboardPermissions,
|
||||||
|
payload: items,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function getDashboardPermissions(id: number): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
const permissions = await getBackendSrv().get(`/api/dashboards/id/${id}/permissions`);
|
||||||
|
dispatch(loadDashboardPermissions(permissions));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUpdateItem(item: DashboardAcl): DashboardAclUpdateDTO {
|
||||||
|
return {
|
||||||
|
userId: item.userId,
|
||||||
|
teamId: item.teamId,
|
||||||
|
role: item.role,
|
||||||
|
permission: item.permission,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateDashboardPermission(
|
||||||
|
dashboardId: number,
|
||||||
|
itemToUpdate: DashboardAcl,
|
||||||
|
level: PermissionLevel
|
||||||
|
): ThunkResult<void> {
|
||||||
|
return async (dispatch, getStore) => {
|
||||||
|
const { dashboard } = getStore();
|
||||||
|
const itemsToUpdate = [];
|
||||||
|
|
||||||
|
for (const item of dashboard.permissions) {
|
||||||
|
if (item.inherited) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = toUpdateItem(itemToUpdate);
|
||||||
|
|
||||||
|
// if this is the item we want to update, update it's permisssion
|
||||||
|
if (itemToUpdate === item) {
|
||||||
|
updated.permission = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsToUpdate.push(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
await getBackendSrv().post(`/api/dashboards/id/${dashboardId}/permissions`, { items: itemsToUpdate });
|
||||||
|
await dispatch(getDashboardPermissions(dashboardId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeDashboardPermission(dashboardId: number, itemToDelete: DashboardAcl): ThunkResult<void> {
|
||||||
|
return async (dispatch, getStore) => {
|
||||||
|
const dashboard = getStore().dashboard;
|
||||||
|
const itemsToUpdate = [];
|
||||||
|
|
||||||
|
for (const item of dashboard.permissions) {
|
||||||
|
if (item.inherited || item === itemToDelete) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
itemsToUpdate.push(toUpdateItem(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
await getBackendSrv().post(`/api/dashboards/id/${dashboardId}/permissions`, { items: itemsToUpdate });
|
||||||
|
await dispatch(getDashboardPermissions(dashboardId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addDashboardPermission(dashboardId: number, newItem: NewDashboardAclItem): ThunkResult<void> {
|
||||||
|
return async (dispatch, getStore) => {
|
||||||
|
const { dashboard } = getStore();
|
||||||
|
const itemsToUpdate = [];
|
||||||
|
|
||||||
|
for (const item of dashboard.permissions) {
|
||||||
|
if (item.inherited) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
itemsToUpdate.push(toUpdateItem(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsToUpdate.push({
|
||||||
|
userId: newItem.userId,
|
||||||
|
teamId: newItem.teamId,
|
||||||
|
role: newItem.role,
|
||||||
|
permission: newItem.permission,
|
||||||
|
});
|
||||||
|
|
||||||
|
await getBackendSrv().post(`/api/dashboards/id/${dashboardId}/permissions`, { items: itemsToUpdate });
|
||||||
|
await dispatch(getDashboardPermissions(dashboardId));
|
||||||
|
};
|
||||||
|
}
|
24
public/app/features/dashboard/state/reducers.test.ts
Normal file
24
public/app/features/dashboard/state/reducers.test.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Action, ActionTypes } from './actions';
|
||||||
|
import { OrgRole, PermissionLevel, DashboardState } from 'app/types';
|
||||||
|
import { inititalState, dashboardReducer } from './reducers';
|
||||||
|
|
||||||
|
describe('dashboard reducer', () => {
|
||||||
|
describe('loadDashboardPermissions', () => {
|
||||||
|
let state: DashboardState;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const action: Action = {
|
||||||
|
type: ActionTypes.LoadDashboardPermissions,
|
||||||
|
payload: [
|
||||||
|
{ id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View },
|
||||||
|
{ id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
state = dashboardReducer(inititalState, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add permissions to state', async () => {
|
||||||
|
expect(state.permissions.length).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
22
public/app/features/dashboard/state/reducers.ts
Normal file
22
public/app/features/dashboard/state/reducers.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { DashboardState } from 'app/types';
|
||||||
|
import { Action, ActionTypes } from './actions';
|
||||||
|
import { processAclItems } from 'app/core/utils/acl';
|
||||||
|
|
||||||
|
export const inititalState: DashboardState = {
|
||||||
|
permissions: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dashboardReducer = (state = inititalState, action: Action): DashboardState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.LoadDashboardPermissions:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
permissions: processAclItems(action.payload),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
dashboard: dashboardReducer,
|
||||||
|
};
|
@ -1,58 +1,82 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import { inject, observer } from 'mobx-react';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
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 Tooltip from 'app/core/components/Tooltip/Tooltip';
|
import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
||||||
import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo';
|
|
||||||
import AddPermissions from 'app/core/components/Permissions/AddPermissions';
|
|
||||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import { NavModel, StoreState, FolderState } from 'app/types';
|
import { NavModel, StoreState, FolderState } from 'app/types';
|
||||||
import { getFolderByUid } from './state/actions';
|
import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';
|
||||||
import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore';
|
import {
|
||||||
|
getFolderByUid,
|
||||||
|
getFolderPermissions,
|
||||||
|
updateFolderPermission,
|
||||||
|
removeFolderPermission,
|
||||||
|
addFolderPermission,
|
||||||
|
} from './state/actions';
|
||||||
import { getLoadingNav } from './state/navModel';
|
import { getLoadingNav } from './state/navModel';
|
||||||
|
import PermissionList from 'app/core/components/PermissionList/PermissionList';
|
||||||
|
import AddPermission from 'app/core/components/PermissionList/AddPermission';
|
||||||
|
import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
getFolderByUid: typeof getFolderByUid;
|
|
||||||
folderUid: string;
|
folderUid: string;
|
||||||
folder: FolderState;
|
folder: FolderState;
|
||||||
permissions: typeof PermissionsStore.Type;
|
getFolderByUid: typeof getFolderByUid;
|
||||||
backendSrv: any;
|
getFolderPermissions: typeof getFolderPermissions;
|
||||||
|
updateFolderPermission: typeof updateFolderPermission;
|
||||||
|
removeFolderPermission: typeof removeFolderPermission;
|
||||||
|
addFolderPermission: typeof addFolderPermission;
|
||||||
}
|
}
|
||||||
|
|
||||||
@inject('permissions')
|
export interface State {
|
||||||
@observer
|
isAdding: boolean;
|
||||||
export class FolderPermissions extends Component<Props> {
|
}
|
||||||
|
|
||||||
|
export class FolderPermissions extends PureComponent<Props, State> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.handleAddPermission = this.handleAddPermission.bind(this);
|
|
||||||
|
this.state = {
|
||||||
|
isAdding: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.getFolderByUid(this.props.folderUid);
|
this.props.getFolderByUid(this.props.folderUid);
|
||||||
|
this.props.getFolderPermissions(this.props.folderUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
onOpenAddPermissions = () => {
|
||||||
const { permissions } = this.props;
|
this.setState({ isAdding: true });
|
||||||
permissions.hideAddPermissions();
|
};
|
||||||
}
|
|
||||||
|
|
||||||
handleAddPermission() {
|
onRemoveItem = (item: DashboardAcl) => {
|
||||||
const { permissions } = this.props;
|
this.props.removeFolderPermission(item);
|
||||||
permissions.toggleAddPermissions();
|
};
|
||||||
}
|
|
||||||
|
onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => {
|
||||||
|
this.props.updateFolderPermission(item, level);
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddPermission = (newItem: NewDashboardAclItem) => {
|
||||||
|
return this.props.addFolderPermission(newItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancelAddPermission = () => {
|
||||||
|
this.setState({ isAdding: false });
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { navModel, permissions, backendSrv, folder } = this.props;
|
const { navModel, folder } = this.props;
|
||||||
|
const { isAdding } = this.state;
|
||||||
|
|
||||||
if (folder.id === 0) {
|
if (folder.id === 0) {
|
||||||
return <PageHeader model={navModel} />;
|
return <PageHeader model={navModel} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dashboardId = folder.id;
|
const folderInfo = { title: folder.title, url: folder.url, id: folder.id };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -64,18 +88,20 @@ export class FolderPermissions extends Component<Props> {
|
|||||||
<i className="gicon gicon-question gicon--has-hover" />
|
<i className="gicon gicon-question gicon--has-hover" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div className="page-action-bar__spacer" />
|
<div className="page-action-bar__spacer" />
|
||||||
<button
|
<button className="btn btn-success pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
|
||||||
className="btn btn-success pull-right"
|
|
||||||
onClick={this.handleAddPermission}
|
|
||||||
disabled={permissions.isAddPermissionsVisible}
|
|
||||||
>
|
|
||||||
<i className="fa fa-plus" /> Add Permission
|
<i className="fa fa-plus" /> Add Permission
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<SlideDown in={permissions.isAddPermissionsVisible}>
|
<SlideDown in={isAdding}>
|
||||||
<AddPermissions permissions={permissions} />
|
<AddPermission onAddPermission={this.onAddPermission} onCancel={this.onCancelAddPermission} />
|
||||||
</SlideDown>
|
</SlideDown>
|
||||||
<Permissions permissions={permissions} isFolder={true} dashboardId={dashboardId} backendSrv={backendSrv} />
|
<PermissionList
|
||||||
|
items={folder.permissions}
|
||||||
|
onRemoveItem={this.onRemoveItem}
|
||||||
|
onPermissionChanged={this.onPermissionChanged}
|
||||||
|
isFetching={false}
|
||||||
|
folderInfo={folderInfo}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -93,6 +119,10 @@ const mapStateToProps = (state: StoreState) => {
|
|||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
getFolderByUid,
|
getFolderByUid,
|
||||||
|
getFolderPermissions,
|
||||||
|
updateFolderPermission,
|
||||||
|
removeFolderPermission,
|
||||||
|
addFolderPermission,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions));
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions));
|
||||||
|
@ -15,6 +15,7 @@ const setup = (propOverrides?: object) => {
|
|||||||
url: 'url',
|
url: 'url',
|
||||||
hasChanged: false,
|
hasChanged: false,
|
||||||
version: 1,
|
version: 1,
|
||||||
|
permissions: [],
|
||||||
},
|
},
|
||||||
getFolderByUid: jest.fn(),
|
getFolderByUid: jest.fn(),
|
||||||
setFolderTitle: jest.fn(),
|
setFolderTitle: jest.fn(),
|
||||||
|
@ -2,6 +2,14 @@ import { getBackendSrv } from 'app/core/services/backend_srv';
|
|||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { ThunkAction } from 'redux-thunk';
|
import { ThunkAction } from 'redux-thunk';
|
||||||
import { FolderDTO, FolderState } from 'app/types';
|
import { FolderDTO, FolderState } from 'app/types';
|
||||||
|
import {
|
||||||
|
DashboardAcl,
|
||||||
|
DashboardAclDTO,
|
||||||
|
PermissionLevel,
|
||||||
|
DashboardAclUpdateDTO,
|
||||||
|
NewDashboardAclItem,
|
||||||
|
} from 'app/types/acl';
|
||||||
|
|
||||||
import { updateNavIndex, updateLocation } from 'app/core/actions';
|
import { updateNavIndex, updateLocation } from 'app/core/actions';
|
||||||
import { buildNavModel } from './navModel';
|
import { buildNavModel } from './navModel';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
@ -10,6 +18,7 @@ export enum ActionTypes {
|
|||||||
LoadFolder = 'LOAD_FOLDER',
|
LoadFolder = 'LOAD_FOLDER',
|
||||||
SetFolderTitle = 'SET_FOLDER_TITLE',
|
SetFolderTitle = 'SET_FOLDER_TITLE',
|
||||||
SaveFolder = 'SAVE_FOLDER',
|
SaveFolder = 'SAVE_FOLDER',
|
||||||
|
LoadFolderPermissions = 'LOAD_FOLDER_PERMISSONS',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadFolderAction {
|
export interface LoadFolderAction {
|
||||||
@ -22,6 +31,15 @@ export interface SetFolderTitleAction {
|
|||||||
payload: string;
|
payload: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LoadFolderPermissionsAction {
|
||||||
|
type: ActionTypes.LoadFolderPermissions;
|
||||||
|
payload: DashboardAcl[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Action = LoadFolderAction | SetFolderTitleAction | LoadFolderPermissionsAction;
|
||||||
|
|
||||||
|
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
|
||||||
|
|
||||||
export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({
|
export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({
|
||||||
type: ActionTypes.LoadFolder,
|
type: ActionTypes.LoadFolder,
|
||||||
payload: folder,
|
payload: folder,
|
||||||
@ -32,10 +50,10 @@ export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({
|
|||||||
payload: newTitle,
|
payload: newTitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Action = LoadFolderAction | SetFolderTitleAction;
|
export const loadFolderPermissions = (items: DashboardAclDTO[]): LoadFolderPermissionsAction => ({
|
||||||
|
type: ActionTypes.LoadFolderPermissions,
|
||||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
|
payload: items,
|
||||||
|
});
|
||||||
|
|
||||||
export function getFolderByUid(uid: string): ThunkResult<void> {
|
export function getFolderByUid(uid: string): ThunkResult<void> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
@ -65,3 +83,85 @@ export function deleteFolder(uid: string): ThunkResult<void> {
|
|||||||
dispatch(updateLocation({ path: `dashboards` }));
|
dispatch(updateLocation({ path: `dashboards` }));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFolderPermissions(uid: string): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
const permissions = await getBackendSrv().get(`/api/folders/${uid}/permissions`);
|
||||||
|
dispatch(loadFolderPermissions(permissions));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toUpdateItem(item: DashboardAcl): DashboardAclUpdateDTO {
|
||||||
|
return {
|
||||||
|
userId: item.userId,
|
||||||
|
teamId: item.teamId,
|
||||||
|
role: item.role,
|
||||||
|
permission: item.permission,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateFolderPermission(itemToUpdate: DashboardAcl, level: PermissionLevel): ThunkResult<void> {
|
||||||
|
return async (dispatch, getStore) => {
|
||||||
|
const folder = getStore().folder;
|
||||||
|
const itemsToUpdate = [];
|
||||||
|
|
||||||
|
for (const item of folder.permissions) {
|
||||||
|
if (item.inherited) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = toUpdateItem(itemToUpdate);
|
||||||
|
|
||||||
|
// if this is the item we want to update, update it's permisssion
|
||||||
|
if (itemToUpdate === item) {
|
||||||
|
updated.permission = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsToUpdate.push(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate });
|
||||||
|
await dispatch(getFolderPermissions(folder.uid));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeFolderPermission(itemToDelete: DashboardAcl): ThunkResult<void> {
|
||||||
|
return async (dispatch, getStore) => {
|
||||||
|
const folder = getStore().folder;
|
||||||
|
const itemsToUpdate = [];
|
||||||
|
|
||||||
|
for (const item of folder.permissions) {
|
||||||
|
if (item.inherited || item === itemToDelete) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
itemsToUpdate.push(toUpdateItem(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate });
|
||||||
|
await dispatch(getFolderPermissions(folder.uid));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addFolderPermission(newItem: NewDashboardAclItem): ThunkResult<void> {
|
||||||
|
return async (dispatch, getStore) => {
|
||||||
|
const folder = getStore().folder;
|
||||||
|
const itemsToUpdate = [];
|
||||||
|
|
||||||
|
for (const item of folder.permissions) {
|
||||||
|
if (item.inherited) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
itemsToUpdate.push(toUpdateItem(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsToUpdate.push({
|
||||||
|
userId: newItem.userId,
|
||||||
|
teamId: newItem.teamId,
|
||||||
|
role: newItem.role,
|
||||||
|
permission: newItem.permission,
|
||||||
|
});
|
||||||
|
|
||||||
|
await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate });
|
||||||
|
await dispatch(getFolderPermissions(folder.uid));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Action, ActionTypes } from './actions';
|
import { Action, ActionTypes } from './actions';
|
||||||
import { FolderDTO } from 'app/types';
|
import { FolderDTO, OrgRole, PermissionLevel, FolderState } from 'app/types';
|
||||||
import { inititalState, folderReducer } from './reducers';
|
import { inititalState, folderReducer } from './reducers';
|
||||||
|
|
||||||
function getTestFolder(): FolderDTO {
|
function getTestFolder(): FolderDTO {
|
||||||
@ -14,6 +14,7 @@ function getTestFolder(): FolderDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('folder reducer', () => {
|
describe('folder reducer', () => {
|
||||||
|
describe('loadFolder', () => {
|
||||||
it('should load folder and set hasChanged to false', () => {
|
it('should load folder and set hasChanged to false', () => {
|
||||||
const folder = getTestFolder();
|
const folder = getTestFolder();
|
||||||
|
|
||||||
@ -27,7 +28,9 @@ describe('folder reducer', () => {
|
|||||||
expect(state.hasChanged).toEqual(false);
|
expect(state.hasChanged).toEqual(false);
|
||||||
expect(state.title).toEqual('test folder');
|
expect(state.title).toEqual('test folder');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('detFolderTitle', () => {
|
||||||
it('should set title', () => {
|
it('should set title', () => {
|
||||||
const action: Action = {
|
const action: Action = {
|
||||||
type: ActionTypes.SetFolderTitle,
|
type: ActionTypes.SetFolderTitle,
|
||||||
@ -40,3 +43,55 @@ describe('folder reducer', () => {
|
|||||||
expect(state.title).toEqual('new title');
|
expect(state.title).toEqual('new title');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('loadFolderPermissions', () => {
|
||||||
|
let state: FolderState;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const action: Action = {
|
||||||
|
type: ActionTypes.LoadFolderPermissions,
|
||||||
|
payload: [
|
||||||
|
{ id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View },
|
||||||
|
{ id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit },
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
dashboardId: 10,
|
||||||
|
permission: PermissionLevel.View,
|
||||||
|
teamId: 1,
|
||||||
|
team: 'MyTestTeam',
|
||||||
|
inherited: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
dashboardId: 1,
|
||||||
|
permission: PermissionLevel.View,
|
||||||
|
userId: 1,
|
||||||
|
userLogin: 'MyTestUser',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
dashboardId: 1,
|
||||||
|
permission: PermissionLevel.Edit,
|
||||||
|
teamId: 2,
|
||||||
|
team: 'MyTestTeam2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
state = folderReducer(inititalState, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add permissions to state', async () => {
|
||||||
|
expect(state.permissions.length).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be sorted by sort rank and alphabetically', async () => {
|
||||||
|
expect(state.permissions[0].name).toBe('MyTestTeam');
|
||||||
|
expect(state.permissions[0].dashboardId).toBe(10);
|
||||||
|
expect(state.permissions[1].name).toBe('Editor');
|
||||||
|
expect(state.permissions[2].name).toBe('Viewer');
|
||||||
|
expect(state.permissions[3].name).toBe('MyTestTeam2');
|
||||||
|
expect(state.permissions[4].name).toBe('MyTestUser');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { FolderState } from 'app/types';
|
import { FolderState } from 'app/types';
|
||||||
import { Action, ActionTypes } from './actions';
|
import { Action, ActionTypes } from './actions';
|
||||||
|
import { processAclItems } from 'app/core/utils/acl';
|
||||||
|
|
||||||
export const inititalState: FolderState = {
|
export const inititalState: FolderState = {
|
||||||
id: 0,
|
id: 0,
|
||||||
@ -8,13 +9,15 @@ export const inititalState: FolderState = {
|
|||||||
url: '',
|
url: '',
|
||||||
canSave: false,
|
canSave: false,
|
||||||
hasChanged: false,
|
hasChanged: false,
|
||||||
version: 0,
|
version: 1,
|
||||||
|
permissions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const folderReducer = (state = inititalState, action: Action): FolderState => {
|
export const folderReducer = (state = inititalState, action: Action): FolderState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.LoadFolder:
|
case ActionTypes.LoadFolder:
|
||||||
return {
|
return {
|
||||||
|
...state,
|
||||||
...action.payload,
|
...action.payload,
|
||||||
hasChanged: false,
|
hasChanged: false,
|
||||||
};
|
};
|
||||||
@ -24,6 +27,11 @@ export const folderReducer = (state = inititalState, action: Action): FolderStat
|
|||||||
title: action.payload,
|
title: action.payload,
|
||||||
hasChanged: action.payload.trim().length > 0,
|
hasChanged: action.payload.trim().length > 0,
|
||||||
};
|
};
|
||||||
|
case ActionTypes.LoadFolderPermissions:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
permissions: processAclItems(action.payload),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
@ -5,12 +5,14 @@ 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';
|
import foldersReducers from 'app/features/folders/state/reducers';
|
||||||
|
import dashboardReducers from 'app/features/dashboard/state/reducers';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
...sharedReducers,
|
...sharedReducers,
|
||||||
...alertingReducers,
|
...alertingReducers,
|
||||||
...teamsReducers,
|
...teamsReducers,
|
||||||
...foldersReducers,
|
...foldersReducers,
|
||||||
|
...dashboardReducers,
|
||||||
});
|
});
|
||||||
|
|
||||||
export let store;
|
export let store;
|
||||||
|
91
public/app/types/acl.ts
Normal file
91
public/app/types/acl.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
export enum OrgRole {
|
||||||
|
Viewer = 'Viewer',
|
||||||
|
Editor = 'Editor',
|
||||||
|
Admin = 'Admin',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardAclDTO {
|
||||||
|
id?: number;
|
||||||
|
dashboardId?: number;
|
||||||
|
userId?: number;
|
||||||
|
userLogin?: string;
|
||||||
|
userEmail?: string;
|
||||||
|
teamId?: number;
|
||||||
|
team?: string;
|
||||||
|
permission?: PermissionLevel;
|
||||||
|
role?: OrgRole;
|
||||||
|
icon?: string;
|
||||||
|
inherited?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardAclUpdateDTO {
|
||||||
|
userId: number;
|
||||||
|
teamId: number;
|
||||||
|
role: OrgRole;
|
||||||
|
permission: PermissionLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardAcl {
|
||||||
|
id?: number;
|
||||||
|
dashboardId?: number;
|
||||||
|
userId?: number;
|
||||||
|
userLogin?: string;
|
||||||
|
userEmail?: string;
|
||||||
|
teamId?: number;
|
||||||
|
team?: string;
|
||||||
|
permission?: PermissionLevel;
|
||||||
|
role?: OrgRole;
|
||||||
|
icon?: string;
|
||||||
|
name?: string;
|
||||||
|
inherited?: boolean;
|
||||||
|
sortRank?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardPermissionInfo {
|
||||||
|
value: PermissionLevel;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NewDashboardAclItem {
|
||||||
|
teamId: number;
|
||||||
|
userId: number;
|
||||||
|
role?: OrgRole;
|
||||||
|
permission: PermissionLevel;
|
||||||
|
type: AclTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PermissionLevel {
|
||||||
|
View = 1,
|
||||||
|
Edit = 2,
|
||||||
|
Admin = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AclTarget {
|
||||||
|
Team = 'Team',
|
||||||
|
User = 'User',
|
||||||
|
Viewer = 'Viewer',
|
||||||
|
Editor = 'Editor',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AclTargetInfo {
|
||||||
|
value: AclTarget;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dashboardAclTargets: AclTargetInfo[] = [
|
||||||
|
{ value: AclTarget.Team, text: 'Team' },
|
||||||
|
{ value: AclTarget.User, text: 'User' },
|
||||||
|
{ value: AclTarget.Viewer, text: 'Everyone With Viewer Role' },
|
||||||
|
{ value: AclTarget.Editor, text: 'Everyone With Editor Role' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const dashboardPermissionLevels: DashboardPermissionInfo[] = [
|
||||||
|
{ value: PermissionLevel.View, label: 'View', description: 'Can view dashboards.' },
|
||||||
|
{ value: PermissionLevel.Edit, label: 'Edit', description: 'Can add, edit and delete dashboards.' },
|
||||||
|
{
|
||||||
|
value: PermissionLevel.Admin,
|
||||||
|
label: 'Admin',
|
||||||
|
description: 'Can add/remove permissions and can add, edit and delete dashboards.',
|
||||||
|
},
|
||||||
|
];
|
5
public/app/types/dashboard.ts
Normal file
5
public/app/types/dashboard.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { DashboardAcl } from './acl';
|
||||||
|
|
||||||
|
export interface DashboardState {
|
||||||
|
permissions: DashboardAcl[];
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
import { DashboardAcl } from './acl';
|
||||||
|
|
||||||
export interface FolderDTO {
|
export interface FolderDTO {
|
||||||
id: number;
|
id: number;
|
||||||
uid: string;
|
uid: string;
|
||||||
@ -12,7 +14,14 @@ export interface FolderState {
|
|||||||
uid: string;
|
uid: string;
|
||||||
title: string;
|
title: string;
|
||||||
url: string;
|
url: string;
|
||||||
version: number;
|
|
||||||
canSave: boolean;
|
canSave: boolean;
|
||||||
hasChanged: boolean;
|
hasChanged: boolean;
|
||||||
|
version: number;
|
||||||
|
permissions: DashboardAcl[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FolderInfo {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,9 @@ 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';
|
import { FolderDTO, FolderState, FolderInfo } from './folder';
|
||||||
|
import { DashboardState } from './dashboard';
|
||||||
|
import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Team,
|
Team,
|
||||||
@ -22,6 +24,11 @@ export {
|
|||||||
UrlQueryValue,
|
UrlQueryValue,
|
||||||
FolderDTO,
|
FolderDTO,
|
||||||
FolderState,
|
FolderState,
|
||||||
|
FolderInfo,
|
||||||
|
DashboardState,
|
||||||
|
DashboardAcl,
|
||||||
|
OrgRole,
|
||||||
|
PermissionLevel,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
@ -31,4 +38,5 @@ export interface StoreState {
|
|||||||
teams: TeamsState;
|
teams: TeamsState;
|
||||||
team: TeamState;
|
team: TeamState;
|
||||||
folder: FolderState;
|
folder: FolderState;
|
||||||
|
dashboard: DashboardState;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,9 @@ module.exports = {
|
|||||||
path.resolve('node_modules')
|
path.resolve('node_modules')
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
stats: {
|
||||||
|
warningsFilter: /export .* was not found in/
|
||||||
|
},
|
||||||
node: {
|
node: {
|
||||||
fs: 'empty',
|
fs: 'empty',
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user