mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: remove legacy dashboard and folder permission pages (#77143)
remove legacy dashboard and folder permission pages
This commit is contained in:
parent
75005a8482
commit
ed88289984
@ -1349,9 +1349,6 @@ exports[`better eslint`] = {
|
|||||||
"public/app/core/components/PasswordField/PasswordField.tsx:5381": [
|
"public/app/core/components/PasswordField/PasswordField.tsx:5381": [
|
||||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
||||||
],
|
],
|
||||||
"public/app/core/components/PermissionList/AddPermission.tsx:5381": [
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
|
||||||
],
|
|
||||||
"public/app/core/components/QueryOperationRow/OperationRowHelp.tsx:5381": [
|
"public/app/core/components/QueryOperationRow/OperationRowHelp.tsx:5381": [
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||||
],
|
],
|
||||||
@ -4197,9 +4194,6 @@ exports[`better eslint`] = {
|
|||||||
"public/app/features/expressions/guards.ts:5381": [
|
"public/app/features/expressions/guards.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/features/folders/state/actions.test.ts:5381": [
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
|
||||||
],
|
|
||||||
"public/app/features/geo/editor/GazetteerPathEditor.tsx:5381": [
|
"public/app/features/geo/editor/GazetteerPathEditor.tsx:5381": [
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||||
],
|
],
|
||||||
|
@ -1,148 +0,0 @@
|
|||||||
import { css } from '@emotion/css';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
|
||||||
import { Button, Form, HorizontalGroup, Select, stylesFactory } from '@grafana/ui';
|
|
||||||
import { TeamPicker } from 'app/core/components/Select/TeamPicker';
|
|
||||||
import { UserPicker } from 'app/core/components/Select/UserPicker';
|
|
||||||
import config from 'app/core/config';
|
|
||||||
import { OrgUser, Team } from 'app/types';
|
|
||||||
import {
|
|
||||||
dashboardPermissionLevels,
|
|
||||||
dashboardAclTargets,
|
|
||||||
AclTarget,
|
|
||||||
PermissionLevel,
|
|
||||||
NewDashboardAclItem,
|
|
||||||
OrgRole,
|
|
||||||
} from 'app/types/acl';
|
|
||||||
|
|
||||||
import { CloseButton } from '../CloseButton/CloseButton';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
onAddPermission: (item: NewDashboardAclItem) => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AddPermissions extends Component<Props, NewDashboardAclItem> {
|
|
||||||
static defaultProps = {
|
|
||||||
showPermissionLevels: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
this.state = this.getCleanState();
|
|
||||||
}
|
|
||||||
|
|
||||||
getCleanState() {
|
|
||||||
return {
|
|
||||||
userId: 0,
|
|
||||||
teamId: 0,
|
|
||||||
role: undefined,
|
|
||||||
type: AclTarget.Team,
|
|
||||||
permission: PermissionLevel.View,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onTypeChanged = (item: SelectableValue<AclTarget>) => {
|
|
||||||
const type = item.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: SelectableValue<OrgUser['userId']>) => {
|
|
||||||
this.setState({ userId: user && !Array.isArray(user) ? user.id : 0 });
|
|
||||||
};
|
|
||||||
|
|
||||||
onTeamSelected = (team: SelectableValue<Team>) => {
|
|
||||||
this.setState({ teamId: team.value?.id && !Array.isArray(team.value) ? team.value.id : 0 });
|
|
||||||
};
|
|
||||||
|
|
||||||
onPermissionChanged = (permission: SelectableValue<PermissionLevel>) => {
|
|
||||||
this.setState({ permission: permission.value! });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSubmit = async () => {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { onCancel } = this.props;
|
|
||||||
const newItem = this.state;
|
|
||||||
const pickerClassName = 'min-width-20';
|
|
||||||
const isValid = this.isValid();
|
|
||||||
const styles = getStyles(config.theme2);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="cta-form">
|
|
||||||
<CloseButton onClick={onCancel} />
|
|
||||||
<h5>Add Permission For</h5>
|
|
||||||
<Form maxWidth="none" onSubmit={this.onSubmit}>
|
|
||||||
{() => (
|
|
||||||
<HorizontalGroup>
|
|
||||||
<Select
|
|
||||||
aria-label="Role to add new permission to"
|
|
||||||
isSearchable={false}
|
|
||||||
value={this.state.type}
|
|
||||||
options={dashboardAclTargets}
|
|
||||||
onChange={this.onTypeChanged}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{newItem.type === AclTarget.User ? (
|
|
||||||
<UserPicker onSelected={this.onUserSelected} className={pickerClassName} />
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{newItem.type === AclTarget.Team ? (
|
|
||||||
<TeamPicker onSelected={this.onTeamSelected} className={pickerClassName} />
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<span className={styles.label}>Can</span>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
aria-label="Permission level"
|
|
||||||
isSearchable={false}
|
|
||||||
value={this.state.permission}
|
|
||||||
options={dashboardPermissionLevels}
|
|
||||||
onChange={this.onPermissionChanged}
|
|
||||||
width={25}
|
|
||||||
/>
|
|
||||||
<Button data-save-permission type="submit" disabled={!isValid}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</HorizontalGroup>
|
|
||||||
)}
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
|
|
||||||
label: css`
|
|
||||||
color: ${theme.colors.primary.text};
|
|
||||||
font-weight: bold;
|
|
||||||
`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default AddPermissions;
|
|
@ -1,43 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
import { Select, Icon, Button } from '@grafana/ui';
|
|
||||||
import { DashboardAcl, dashboardPermissionLevels } from 'app/types/acl';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
item: DashboardAcl;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DisabledPermissionListItem extends Component<Props> {
|
|
||||||
render() {
|
|
||||||
const { item } = this.props;
|
|
||||||
const currentPermissionLevel = dashboardPermissionLevels.find((dp) => dp.value === item.permission);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr className="gf-form-disabled">
|
|
||||||
<td style={{ width: '1%' }}>
|
|
||||||
<Icon size="lg" name="shield" />
|
|
||||||
</td>
|
|
||||||
<td style={{ width: '90%' }}>
|
|
||||||
{item.name}
|
|
||||||
<span className="filter-table__weak-italic"> (Role)</span>
|
|
||||||
</td>
|
|
||||||
<td />
|
|
||||||
<td className="query-keyword">Can</td>
|
|
||||||
<td>
|
|
||||||
<div className="gf-form">
|
|
||||||
<Select
|
|
||||||
aria-label={`Permission level for "${item.name}"`}
|
|
||||||
options={dashboardPermissionLevels}
|
|
||||||
onChange={() => {}}
|
|
||||||
disabled={true}
|
|
||||||
value={currentPermissionLevel}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Button aria-label={`Remove permission for "${item.name}"`} size="sm" icon="lock" disabled />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { FolderInfo } from 'app/types';
|
|
||||||
import { DashboardAcl, PermissionLevel } from 'app/types/acl';
|
|
||||||
|
|
||||||
import DisabledPermissionsListItem from './DisabledPermissionListItem';
|
|
||||||
import PermissionsListItem from './PermissionListItem';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
items: DashboardAcl[];
|
|
||||||
onRemoveItem: (item: DashboardAcl) => void;
|
|
||||||
onPermissionChanged: (item: DashboardAcl, level: PermissionLevel) => void;
|
|
||||||
isFetching: boolean;
|
|
||||||
folderInfo?: FolderInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
class PermissionList extends PureComponent<Props> {
|
|
||||||
render() {
|
|
||||||
const { items, onRemoveItem, onPermissionChanged, isFetching, folderInfo } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table className="filter-table gf-form-group">
|
|
||||||
<tbody>
|
|
||||||
<DisabledPermissionsListItem
|
|
||||||
key={0}
|
|
||||||
item={{
|
|
||||||
name: 'Admin',
|
|
||||||
permission: 4,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{items.map((item, idx) => {
|
|
||||||
return (
|
|
||||||
<PermissionsListItem
|
|
||||||
key={idx + 1}
|
|
||||||
item={item}
|
|
||||||
onRemoveItem={onRemoveItem}
|
|
||||||
onPermissionChanged={onPermissionChanged}
|
|
||||||
folderInfo={folderInfo}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{isFetching === true && items.length < 1 ? (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={4}>
|
|
||||||
<em>Loading permissions...</em>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{isFetching === false && items.length < 1 ? (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={4}>
|
|
||||||
<em>No permissions are set. Will only be accessible by admins.</em>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : null}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PermissionList;
|
|
@ -1,108 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
|
||||||
import { Select, Icon, Button } from '@grafana/ui';
|
|
||||||
import { FolderInfo } from 'app/types';
|
|
||||||
import { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl';
|
|
||||||
|
|
||||||
const setClassNameHelper = (inherited: boolean) => {
|
|
||||||
return inherited ? 'gf-form-disabled' : '';
|
|
||||||
};
|
|
||||||
|
|
||||||
function ItemAvatar({ item }: { item: DashboardAcl }) {
|
|
||||||
if (item.userAvatarUrl) {
|
|
||||||
return <img className="filter-table__avatar" src={item.userAvatarUrl} alt="User avatar" />;
|
|
||||||
}
|
|
||||||
if (item.teamAvatarUrl) {
|
|
||||||
return <img className="filter-table__avatar" src={item.teamAvatarUrl} alt="Team avatar" />;
|
|
||||||
}
|
|
||||||
if (item.role === 'Editor') {
|
|
||||||
return <Icon size="lg" name="edit" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Icon size="lg" name="eye" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemDescription({ item }: { item: DashboardAcl }) {
|
|
||||||
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: SelectableValue<PermissionLevel>) => {
|
|
||||||
this.props.onPermissionChanged(this.props.item, option.value!);
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveItem = () => {
|
|
||||||
this.props.onRemoveItem(this.props.item);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { item, folderInfo } = this.props;
|
|
||||||
const inheritedFromRoot = item.dashboardId === -1 && !item.inherited;
|
|
||||||
const currentPermissionLevel = dashboardPermissionLevels.find((dp) => dp.value === item.permission);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr className={setClassNameHelper(Boolean(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{' '}
|
|
||||||
{folderInfo.canViewFolderPermissions ? (
|
|
||||||
<a className="text-link" href={`${folderInfo.url}/permissions`}>
|
|
||||||
{folderInfo.title}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
folderInfo.title
|
|
||||||
)}
|
|
||||||
</em>
|
|
||||||
)}
|
|
||||||
{inheritedFromRoot && <em className="muted no-wrap">Default Permission</em>}
|
|
||||||
</td>
|
|
||||||
<td className="query-keyword">Can</td>
|
|
||||||
<td>
|
|
||||||
<Select
|
|
||||||
aria-label={`Permission level for "${item.name}"`}
|
|
||||||
isSearchable={false}
|
|
||||||
options={dashboardPermissionLevels}
|
|
||||||
onChange={this.onPermissionChanged}
|
|
||||||
disabled={item.inherited}
|
|
||||||
value={currentPermissionLevel}
|
|
||||||
width={25}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{!item.inherited ? (
|
|
||||||
<Button
|
|
||||||
aria-label={`Remove permission for "${item.name}"`}
|
|
||||||
size="sm"
|
|
||||||
variant="destructive"
|
|
||||||
icon="times"
|
|
||||||
onClick={this.onRemoveItem}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button aria-label={`Remove permission for "${item.name}" (Disabled)`} size="sm" disabled icon="times" />
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const PermissionsInfo = () => (
|
|
||||||
<div>
|
|
||||||
<h5>What are Permissions?</h5>
|
|
||||||
<p>
|
|
||||||
An Access Control List (ACL) model is used to limit access to Dashboard Folders. A user or a Team can be assigned
|
|
||||||
permissions for a folder or for a single dashboard.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default PermissionsInfo;
|
|
@ -1,32 +0,0 @@
|
|||||||
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: DashboardAcl = dto;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
@ -35,8 +35,6 @@ const mockFolder = (folderOverride: Partial<FolderState> = {}): FolderState => {
|
|||||||
canSave: false,
|
canSave: false,
|
||||||
url: '/folder-1',
|
url: '/folder-1',
|
||||||
version: 1,
|
version: 1,
|
||||||
permissions: [],
|
|
||||||
canViewFolderPermissions: false,
|
|
||||||
canDelete: false,
|
canDelete: false,
|
||||||
...folderOverride,
|
...folderOverride,
|
||||||
};
|
};
|
||||||
|
@ -1,130 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import { connect, ConnectedProps } from 'react-redux';
|
|
||||||
|
|
||||||
import { config } from '@grafana/runtime';
|
|
||||||
import { Tooltip, Icon, Button } from '@grafana/ui';
|
|
||||||
import { SlideDown } from 'app/core/components/Animations/SlideDown';
|
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
|
||||||
import AddPermission from 'app/core/components/PermissionList/AddPermission';
|
|
||||||
import PermissionList from 'app/core/components/PermissionList/PermissionList';
|
|
||||||
import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo';
|
|
||||||
import { StoreState } from 'app/types';
|
|
||||||
import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';
|
|
||||||
|
|
||||||
import { checkFolderPermissions } from '../../../folders/state/actions';
|
|
||||||
import {
|
|
||||||
getDashboardPermissions,
|
|
||||||
addDashboardPermission,
|
|
||||||
removeDashboardPermission,
|
|
||||||
updateDashboardPermission,
|
|
||||||
} from '../../state/actions';
|
|
||||||
import { SettingsPageProps } from '../DashboardSettings/types';
|
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState) => ({
|
|
||||||
permissions: state.dashboard.permissions,
|
|
||||||
canViewFolderPermissions: state.folder.canViewFolderPermissions,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
getDashboardPermissions,
|
|
||||||
addDashboardPermission,
|
|
||||||
removeDashboardPermission,
|
|
||||||
updateDashboardPermission,
|
|
||||||
checkFolderPermissions,
|
|
||||||
};
|
|
||||||
|
|
||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
|
||||||
|
|
||||||
export type Props = SettingsPageProps & ConnectedProps<typeof connector>;
|
|
||||||
|
|
||||||
export interface State {
|
|
||||||
isAdding: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DashboardPermissionsUnconnected extends PureComponent<Props, State> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isAdding: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.getDashboardPermissions(this.props.dashboard.id);
|
|
||||||
if (this.props.dashboard.meta.folderUid) {
|
|
||||||
this.props.checkFolderPermissions(this.props.dashboard.meta.folderUid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpenAddPermissions = () => {
|
|
||||||
this.setState({ isAdding: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveItem = (item: DashboardAcl) => {
|
|
||||||
this.props.removeDashboardPermission(this.props.dashboard.id, item);
|
|
||||||
};
|
|
||||||
|
|
||||||
onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => {
|
|
||||||
this.props.updateDashboardPermission(this.props.dashboard.id, item, level);
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddPermission = (newItem: NewDashboardAclItem) => {
|
|
||||||
return this.props.addDashboardPermission(this.props.dashboard.id, newItem);
|
|
||||||
};
|
|
||||||
|
|
||||||
onCancelAddPermission = () => {
|
|
||||||
this.setState({ isAdding: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
getFolder() {
|
|
||||||
const { dashboard, canViewFolderPermissions } = this.props;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: dashboard.meta.folderId,
|
|
||||||
title: dashboard.meta.folderTitle,
|
|
||||||
url: dashboard.meta.folderUrl,
|
|
||||||
canViewFolderPermissions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { permissions, dashboard, sectionNav } = this.props;
|
|
||||||
const { isAdding } = this.state;
|
|
||||||
const pageNav = config.featureToggles.dockedMegaMenu ? sectionNav.node.parentItem : undefined;
|
|
||||||
|
|
||||||
if (dashboard.meta.hasUnsavedFolderChange) {
|
|
||||||
return (
|
|
||||||
<Page navModel={sectionNav} pageNav={pageNav}>
|
|
||||||
<h5>You have changed a folder, please save to view permissions.</h5>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page navModel={sectionNav} pageNav={pageNav}>
|
|
||||||
<div className="page-action-bar">
|
|
||||||
<Tooltip placement="auto" content={<PermissionsInfo />}>
|
|
||||||
<Icon className="icon--has-hover page-sub-heading-icon" name="question-circle" />
|
|
||||||
</Tooltip>
|
|
||||||
<div className="page-action-bar__spacer" />
|
|
||||||
<Button className="pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
|
|
||||||
Add permission
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<SlideDown in={isAdding}>
|
|
||||||
<AddPermission onAddPermission={this.onAddPermission} onCancel={this.onCancelAddPermission} />
|
|
||||||
</SlideDown>
|
|
||||||
<PermissionList
|
|
||||||
items={permissions}
|
|
||||||
onRemoveItem={this.onRemoveItem}
|
|
||||||
onPermissionChanged={this.onPermissionChanged}
|
|
||||||
isFetching={false}
|
|
||||||
folderInfo={this.getFolder()}
|
|
||||||
/>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DashboardPermissions = connector(DashboardPermissionsUnconnected);
|
|
@ -50,7 +50,6 @@ export const renderSharePublicDashboard = async (
|
|||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
dashboard: {
|
dashboard: {
|
||||||
getModel: () => props?.dashboard || mockDashboard,
|
getModel: () => props?.dashboard || mockDashboard,
|
||||||
permissions: [],
|
|
||||||
initError: null,
|
initError: null,
|
||||||
initPhase: DashboardInitPhase.Completed,
|
initPhase: DashboardInitPhase.Completed,
|
||||||
},
|
},
|
||||||
|
@ -5,100 +5,14 @@ import { createSuccessNotification } from 'app/core/copy/appNotification';
|
|||||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||||
import { removeAllPanels } from 'app/features/panel/state/reducers';
|
import { removeAllPanels } from 'app/features/panel/state/reducers';
|
||||||
import { updateTimeZoneForSession, updateWeekStartForSession } from 'app/features/profile/state/reducers';
|
import { updateTimeZoneForSession, updateWeekStartForSession } from 'app/features/profile/state/reducers';
|
||||||
import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel, ThunkResult } from 'app/types';
|
import { ThunkResult } from 'app/types';
|
||||||
|
|
||||||
import { loadPluginDashboards } from '../../plugins/admin/state/actions';
|
import { loadPluginDashboards } from '../../plugins/admin/state/actions';
|
||||||
import { cancelVariables } from '../../variables/state/actions';
|
import { cancelVariables } from '../../variables/state/actions';
|
||||||
import { getDashboardSrv } from '../services/DashboardSrv';
|
import { getDashboardSrv } from '../services/DashboardSrv';
|
||||||
import { getTimeSrv } from '../services/TimeSrv';
|
import { getTimeSrv } from '../services/TimeSrv';
|
||||||
|
|
||||||
import { cleanUpDashboard, loadDashboardPermissions } from './reducers';
|
import { cleanUpDashboard } from './reducers';
|
||||||
|
|
||||||
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(item);
|
|
||||||
|
|
||||||
// if this is the item we want to update, update its permission
|
|
||||||
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));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function importDashboard(data: any, dashboardTitle: string): ThunkResult<void> {
|
export function importDashboard(data: any, dashboardTitle: string): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
|
@ -1,32 +1,15 @@
|
|||||||
import { DashboardInitPhase, DashboardState, OrgRole, PermissionLevel } from 'app/types';
|
import { DashboardInitPhase, DashboardState } from 'app/types';
|
||||||
|
|
||||||
import { createDashboardModelFixture, createPanelSaveModel } from './__fixtures__/dashboardFixtures';
|
import { createDashboardModelFixture, createPanelSaveModel } from './__fixtures__/dashboardFixtures';
|
||||||
import {
|
import {
|
||||||
dashboardInitCompleted,
|
dashboardInitCompleted,
|
||||||
dashboardInitFailed,
|
dashboardInitFailed,
|
||||||
dashboardInitFetching,
|
dashboardInitFetching,
|
||||||
loadDashboardPermissions,
|
|
||||||
dashboardReducer,
|
dashboardReducer,
|
||||||
initialState,
|
initialState,
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
|
|
||||||
describe('dashboard reducer', () => {
|
describe('dashboard reducer', () => {
|
||||||
describe('loadDashboardPermissions', () => {
|
|
||||||
let state: DashboardState;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const action = loadDashboardPermissions([
|
|
||||||
{ id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View },
|
|
||||||
{ id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit },
|
|
||||||
]);
|
|
||||||
state = dashboardReducer(initialState, action);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add permissions to state', async () => {
|
|
||||||
expect(state.permissions?.length).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('dashboardInitCompleted', () => {
|
describe('dashboardInitCompleted', () => {
|
||||||
let state: DashboardState;
|
let state: DashboardState;
|
||||||
|
|
||||||
|
@ -3,8 +3,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|||||||
import { PanelPlugin } from '@grafana/data';
|
import { PanelPlugin } from '@grafana/data';
|
||||||
import { AngularComponent } from '@grafana/runtime';
|
import { AngularComponent } from '@grafana/runtime';
|
||||||
import { defaultDashboard } from '@grafana/schema';
|
import { defaultDashboard } from '@grafana/schema';
|
||||||
import { processAclItems } from 'app/core/utils/acl';
|
import { DashboardInitError, DashboardInitPhase, DashboardState } from 'app/types';
|
||||||
import { DashboardAclDTO, DashboardInitError, DashboardInitPhase, DashboardState } from 'app/types';
|
|
||||||
|
|
||||||
import { DashboardModel } from './DashboardModel';
|
import { DashboardModel } from './DashboardModel';
|
||||||
import { PanelModel } from './PanelModel';
|
import { PanelModel } from './PanelModel';
|
||||||
@ -12,7 +11,6 @@ import { PanelModel } from './PanelModel';
|
|||||||
export const initialState: DashboardState = {
|
export const initialState: DashboardState = {
|
||||||
initPhase: DashboardInitPhase.NotStarted,
|
initPhase: DashboardInitPhase.NotStarted,
|
||||||
getModel: () => null,
|
getModel: () => null,
|
||||||
permissions: [],
|
|
||||||
initError: null,
|
initError: null,
|
||||||
initialDatasource: undefined,
|
initialDatasource: undefined,
|
||||||
};
|
};
|
||||||
@ -21,9 +19,6 @@ const dashboardSlice = createSlice({
|
|||||||
name: 'dashboard',
|
name: 'dashboard',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
loadDashboardPermissions: (state, action: PayloadAction<DashboardAclDTO[]>) => {
|
|
||||||
state.permissions = processAclItems(action.payload);
|
|
||||||
},
|
|
||||||
dashboardInitFetching: (state) => {
|
dashboardInitFetching: (state) => {
|
||||||
state.initPhase = DashboardInitPhase.Fetching;
|
state.initPhase = DashboardInitPhase.Fetching;
|
||||||
},
|
},
|
||||||
@ -74,7 +69,6 @@ export interface SetPanelInstanceStatePayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
loadDashboardPermissions,
|
|
||||||
dashboardInitFetching,
|
dashboardInitFetching,
|
||||||
dashboardInitFailed,
|
dashboardInitFailed,
|
||||||
dashboardInitCompleted,
|
dashboardInitCompleted,
|
||||||
|
@ -1,130 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import { connect, ConnectedProps } from 'react-redux';
|
|
||||||
|
|
||||||
import { Tooltip, Icon, Button } from '@grafana/ui';
|
|
||||||
import { SlideDown } from 'app/core/components/Animations/SlideDown';
|
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
|
||||||
import AddPermission from 'app/core/components/PermissionList/AddPermission';
|
|
||||||
import PermissionList from 'app/core/components/PermissionList/PermissionList';
|
|
||||||
import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo';
|
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
|
||||||
import { StoreState } from 'app/types';
|
|
||||||
import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getFolderByUid,
|
|
||||||
getFolderPermissions,
|
|
||||||
updateFolderPermission,
|
|
||||||
removeFolderPermission,
|
|
||||||
addFolderPermission,
|
|
||||||
} from './state/actions';
|
|
||||||
import { getLoadingNav } from './state/navModel';
|
|
||||||
|
|
||||||
export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {}
|
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState, props: OwnProps) => {
|
|
||||||
const uid = props.match.params.uid;
|
|
||||||
return {
|
|
||||||
pageNav: getNavModel(state.navIndex, `folder-permissions-${uid}`, getLoadingNav(1)),
|
|
||||||
folderUid: uid,
|
|
||||||
folder: state.folder,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
getFolderByUid,
|
|
||||||
getFolderPermissions,
|
|
||||||
updateFolderPermission,
|
|
||||||
removeFolderPermission,
|
|
||||||
addFolderPermission,
|
|
||||||
};
|
|
||||||
|
|
||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
|
||||||
|
|
||||||
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
|
||||||
|
|
||||||
export interface State {
|
|
||||||
isAdding: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FolderPermissions extends PureComponent<Props, State> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isAdding: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.getFolderByUid(this.props.folderUid);
|
|
||||||
this.props.getFolderPermissions(this.props.folderUid);
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpenAddPermissions = () => {
|
|
||||||
this.setState({ isAdding: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveItem = (item: DashboardAcl) => {
|
|
||||||
this.props.removeFolderPermission(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => {
|
|
||||||
this.props.updateFolderPermission(item, level);
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddPermission = (newItem: NewDashboardAclItem) => {
|
|
||||||
return this.props.addFolderPermission(newItem);
|
|
||||||
};
|
|
||||||
|
|
||||||
onCancelAddPermission = () => {
|
|
||||||
this.setState({ isAdding: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { pageNav, folder } = this.props;
|
|
||||||
const { isAdding } = this.state;
|
|
||||||
|
|
||||||
if (folder.id === 0) {
|
|
||||||
return (
|
|
||||||
<Page navId="dashboards/browse" pageNav={pageNav.main}>
|
|
||||||
<Page.Contents isLoading={true}>
|
|
||||||
<span />
|
|
||||||
</Page.Contents>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const folderInfo = { title: folder.title, url: folder.url, id: folder.id };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page navId="dashboards/browse" pageNav={pageNav.main}>
|
|
||||||
<Page.Contents>
|
|
||||||
<div className="page-action-bar">
|
|
||||||
<h3 className="page-sub-heading">Folder Permissions</h3>
|
|
||||||
<Tooltip placement="auto" content={<PermissionsInfo />}>
|
|
||||||
<Icon className="icon--has-hover page-sub-heading-icon" name="question-circle" />
|
|
||||||
</Tooltip>
|
|
||||||
<div className="page-action-bar__spacer" />
|
|
||||||
<Button className="pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
|
|
||||||
Add Permission
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<SlideDown in={isAdding}>
|
|
||||||
<AddPermission onAddPermission={this.onAddPermission} onCancel={this.onCancelAddPermission} />
|
|
||||||
</SlideDown>
|
|
||||||
<PermissionList
|
|
||||||
items={folder.permissions}
|
|
||||||
onRemoveItem={this.onRemoveItem}
|
|
||||||
onPermissionChanged={this.onPermissionChanged}
|
|
||||||
isFetching={false}
|
|
||||||
folderInfo={folderInfo}
|
|
||||||
/>
|
|
||||||
</Page.Contents>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connector(FolderPermissions);
|
|
@ -25,8 +25,6 @@ const setup = (propOverrides?: object) => {
|
|||||||
url: 'url',
|
url: 'url',
|
||||||
hasChanged: false,
|
hasChanged: false,
|
||||||
version: 1,
|
version: 1,
|
||||||
permissions: [],
|
|
||||||
canViewFolderPermissions: true,
|
|
||||||
},
|
},
|
||||||
getFolderByUid: jest.fn(),
|
getFolderByUid: jest.fn(),
|
||||||
setFolderTitle: mockToolkitActionCreator(setFolderTitle),
|
setFolderTitle: mockToolkitActionCreator(setFolderTitle),
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
import { Observable, of, throwError } from 'rxjs';
|
|
||||||
import { thunkTester } from 'test/core/thunk/thunkTester';
|
|
||||||
|
|
||||||
import { FetchResponse } from '@grafana/runtime';
|
|
||||||
import { notifyApp } from 'app/core/actions';
|
|
||||||
import { createWarningNotification } from 'app/core/copy/appNotification';
|
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
|
||||||
|
|
||||||
import { checkFolderPermissions } from './actions';
|
|
||||||
import { setCanViewFolderPermissions } from './reducers';
|
|
||||||
|
|
||||||
describe('folder actions', () => {
|
|
||||||
let fetchSpy: jest.SpyInstance<Observable<FetchResponse<unknown>>>;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
fetchSpy = jest.spyOn(backendSrv, 'fetch');
|
|
||||||
});
|
|
||||||
afterAll(() => {
|
|
||||||
fetchSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
function mockFetch(resp: Observable<any>) {
|
|
||||||
fetchSpy.mockReturnValueOnce(resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
const folderUid = 'abc123';
|
|
||||||
|
|
||||||
describe('checkFolderPermissions', () => {
|
|
||||||
it('should dispatch true when the api call is successful', async () => {
|
|
||||||
mockFetch(of({}));
|
|
||||||
|
|
||||||
const dispatchedActions = await thunkTester({})
|
|
||||||
.givenThunk(checkFolderPermissions)
|
|
||||||
.whenThunkIsDispatched(folderUid);
|
|
||||||
|
|
||||||
expect(dispatchedActions).toEqual([setCanViewFolderPermissions(true)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should dispatch just "false" when the api call fails with 403', async () => {
|
|
||||||
mockFetch(throwError(() => ({ status: 403, data: { message: 'Access denied' } })));
|
|
||||||
|
|
||||||
const dispatchedActions = await thunkTester({})
|
|
||||||
.givenThunk(checkFolderPermissions)
|
|
||||||
.whenThunkIsDispatched(folderUid);
|
|
||||||
|
|
||||||
expect(dispatchedActions).toEqual([setCanViewFolderPermissions(false)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should also dispatch a notification when the api call fails with an error other than 403', async () => {
|
|
||||||
mockFetch(throwError(() => ({ status: 500, data: { message: 'Server error' } })));
|
|
||||||
|
|
||||||
const dispatchedActions = await thunkTester({})
|
|
||||||
.givenThunk(checkFolderPermissions)
|
|
||||||
.whenThunkIsDispatched(folderUid);
|
|
||||||
|
|
||||||
const notificationAction = notifyApp(
|
|
||||||
createWarningNotification('Error checking folder permissions', 'Server error')
|
|
||||||
);
|
|
||||||
notificationAction.payload.id = expect.any(String);
|
|
||||||
notificationAction.payload.timestamp = expect.any(Number);
|
|
||||||
|
|
||||||
expect(dispatchedActions).toEqual([
|
|
||||||
expect.objectContaining(notificationAction),
|
|
||||||
setCanViewFolderPermissions(false),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,16 +1,13 @@
|
|||||||
import { lastValueFrom } from 'rxjs';
|
|
||||||
|
|
||||||
import { locationUtil } from '@grafana/data';
|
import { locationUtil } from '@grafana/data';
|
||||||
import { getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
|
import { getBackendSrv, locationService } from '@grafana/runtime';
|
||||||
import { notifyApp, updateNavIndex } from 'app/core/actions';
|
import { notifyApp, updateNavIndex } from 'app/core/actions';
|
||||||
import { createSuccessNotification, createWarningNotification } from 'app/core/copy/appNotification';
|
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
import { backendSrv } from 'app/core/services/backend_srv';
|
||||||
import { FolderDTO, FolderState, ThunkResult } from 'app/types';
|
import { FolderDTO, FolderState, ThunkResult } from 'app/types';
|
||||||
import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel } from 'app/types/acl';
|
|
||||||
|
|
||||||
import { buildNavModel } from './navModel';
|
import { buildNavModel } from './navModel';
|
||||||
import { loadFolder, loadFolderPermissions, setCanViewFolderPermissions } from './reducers';
|
import { loadFolder } from './reducers';
|
||||||
|
|
||||||
export function getFolderByUid(uid: string): ThunkResult<Promise<FolderDTO>> {
|
export function getFolderByUid(uid: string): ThunkResult<Promise<FolderDTO>> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
@ -41,110 +38,6 @@ export function deleteFolder(uid: string): ThunkResult<void> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFolderPermissions(uid: string): ThunkResult<void> {
|
|
||||||
return async (dispatch) => {
|
|
||||||
const permissions = await backendSrv.get(`/api/folders/${uid}/permissions`);
|
|
||||||
dispatch(loadFolderPermissions(permissions));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkFolderPermissions(uid: string): ThunkResult<void> {
|
|
||||||
return async (dispatch) => {
|
|
||||||
try {
|
|
||||||
await lastValueFrom(
|
|
||||||
backendSrv.fetch({
|
|
||||||
method: 'GET',
|
|
||||||
showErrorAlert: false,
|
|
||||||
showSuccessAlert: false,
|
|
||||||
url: `/api/folders/${uid}/permissions`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
dispatch(setCanViewFolderPermissions(true));
|
|
||||||
} catch (err) {
|
|
||||||
if (isFetchError(err) && err.status !== 403) {
|
|
||||||
dispatch(notifyApp(createWarningNotification('Error checking folder permissions', err.data?.message)));
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(setCanViewFolderPermissions(false));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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(item);
|
|
||||||
|
|
||||||
// if this is the item we want to update, update its permission
|
|
||||||
if (itemToUpdate === item) {
|
|
||||||
updated.permission = level;
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsToUpdate.push(updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
await backendSrv.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 backendSrv.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 backendSrv.post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate });
|
|
||||||
await dispatch(getFolderPermissions(folder.uid));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createNewFolder(folderName: string, uid?: string): ThunkResult<void> {
|
export function createNewFolder(folderName: string, uid?: string): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
const newFolder = await getBackendSrv().post('/api/folders', { title: folderName, parentUid: uid });
|
const newFolder = await getBackendSrv().post('/api/folders', { title: folderName, parentUid: uid });
|
||||||
|
@ -1,15 +1,8 @@
|
|||||||
import { FolderDTO, FolderState, OrgRole, PermissionLevel } from 'app/types';
|
import { FolderDTO, FolderState } from 'app/types';
|
||||||
|
|
||||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||||
|
|
||||||
import {
|
import { folderReducer, initialState, loadFolder, setFolderTitle } from './reducers';
|
||||||
folderReducer,
|
|
||||||
initialState,
|
|
||||||
loadFolder,
|
|
||||||
loadFolderPermissions,
|
|
||||||
setCanViewFolderPermissions,
|
|
||||||
setFolderTitle,
|
|
||||||
} from './reducers';
|
|
||||||
|
|
||||||
function getTestFolder(): FolderDTO {
|
function getTestFolder(): FolderDTO {
|
||||||
return {
|
return {
|
||||||
@ -71,102 +64,4 @@ describe('folder reducer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when loadFolderPermissions is dispatched', () => {
|
|
||||||
it('then state should be correct', () => {
|
|
||||||
reducerTester<FolderState>()
|
|
||||||
.givenReducer(folderReducer, { ...initialState })
|
|
||||||
.whenActionIsDispatched(
|
|
||||||
loadFolderPermissions([
|
|
||||||
{ 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',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
)
|
|
||||||
.thenStateShouldEqual({
|
|
||||||
...initialState,
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
dashboardId: 10,
|
|
||||||
id: 4,
|
|
||||||
inherited: true,
|
|
||||||
name: 'MyTestTeam',
|
|
||||||
permission: 1,
|
|
||||||
sortRank: 120,
|
|
||||||
team: 'MyTestTeam',
|
|
||||||
teamId: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dashboardId: 1,
|
|
||||||
icon: 'fa fa-fw fa-street-view',
|
|
||||||
id: 3,
|
|
||||||
name: 'Editor',
|
|
||||||
permission: 2,
|
|
||||||
role: OrgRole.Editor,
|
|
||||||
sortRank: 31,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dashboardId: 1,
|
|
||||||
icon: 'fa fa-fw fa-street-view',
|
|
||||||
id: 2,
|
|
||||||
name: 'Viewer',
|
|
||||||
permission: 1,
|
|
||||||
role: OrgRole.Viewer,
|
|
||||||
sortRank: 30,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dashboardId: 1,
|
|
||||||
id: 6,
|
|
||||||
name: 'MyTestTeam2',
|
|
||||||
permission: 2,
|
|
||||||
sortRank: 20,
|
|
||||||
team: 'MyTestTeam2',
|
|
||||||
teamId: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dashboardId: 1,
|
|
||||||
id: 5,
|
|
||||||
name: 'MyTestUser',
|
|
||||||
permission: 1,
|
|
||||||
sortRank: 10,
|
|
||||||
userId: 1,
|
|
||||||
userLogin: 'MyTestUser',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('setCanViewFolderPermissions', () => {
|
|
||||||
it('should set the canViewFolderPermissions value', () => {
|
|
||||||
reducerTester<FolderState>()
|
|
||||||
.givenReducer(folderReducer, { ...initialState })
|
|
||||||
.whenActionIsDispatched(setCanViewFolderPermissions(true))
|
|
||||||
.thenStateShouldEqual({
|
|
||||||
...initialState,
|
|
||||||
canViewFolderPermissions: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { processAclItems } from 'app/core/utils/acl';
|
|
||||||
import { endpoints } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
import { endpoints } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
||||||
import { DashboardAclDTO, FolderDTO, FolderState } from 'app/types';
|
import { FolderDTO, FolderState } from 'app/types';
|
||||||
|
|
||||||
export const initialState: FolderState = {
|
export const initialState: FolderState = {
|
||||||
id: 0,
|
id: 0,
|
||||||
@ -13,8 +12,6 @@ export const initialState: FolderState = {
|
|||||||
canDelete: false,
|
canDelete: false,
|
||||||
hasChanged: false,
|
hasChanged: false,
|
||||||
version: 1,
|
version: 1,
|
||||||
permissions: [],
|
|
||||||
canViewFolderPermissions: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadFolderReducer = (state: FolderState, action: PayloadAction<FolderDTO>): FolderState => {
|
const loadFolderReducer = (state: FolderState, action: PayloadAction<FolderDTO>): FolderState => {
|
||||||
@ -37,23 +34,13 @@ const folderSlice = createSlice({
|
|||||||
hasChanged: action.payload.trim().length > 0,
|
hasChanged: action.payload.trim().length > 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
loadFolderPermissions: (state, action: PayloadAction<DashboardAclDTO[]>): FolderState => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
permissions: processAclItems(action.payload),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
setCanViewFolderPermissions: (state, action: PayloadAction<boolean>): FolderState => {
|
|
||||||
state.canViewFolderPermissions = action.payload;
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addMatcher(endpoints.getFolder.matchFulfilled, loadFolderReducer);
|
builder.addMatcher(endpoints.getFolder.matchFulfilled, loadFolderReducer);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { loadFolderPermissions, loadFolder, setFolderTitle, setCanViewFolderPermissions } = folderSlice.actions;
|
export const { loadFolder, setFolderTitle } = folderSlice.actions;
|
||||||
|
|
||||||
export const folderReducer = folderSlice.reducer;
|
export const folderReducer = folderSlice.reducer;
|
||||||
|
|
||||||
|
@ -9,65 +9,6 @@ export enum TeamPermissionLevel {
|
|||||||
|
|
||||||
export { OrgRole as OrgRole };
|
export { OrgRole as OrgRole };
|
||||||
|
|
||||||
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;
|
|
||||||
userAvatarUrl?: string;
|
|
||||||
teamAvatarUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 PermissionLevelString {
|
export enum PermissionLevelString {
|
||||||
View = 'View',
|
View = 'View',
|
||||||
Edit = 'Edit',
|
Edit = 'Edit',
|
||||||
@ -79,61 +20,3 @@ export enum SearchQueryType {
|
|||||||
Dashboard = 'dash-db',
|
Dashboard = 'dash-db',
|
||||||
AlertFolder = 'dash-folder-alerting',
|
AlertFolder = 'dash-folder-alerting',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DataSourcePermissionLevel {
|
|
||||||
Query = 1,
|
|
||||||
Admin = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AclTarget {
|
|
||||||
Team = 'Team',
|
|
||||||
User = 'User',
|
|
||||||
ServiceAccount = 'ServiceAccount',
|
|
||||||
Viewer = 'Viewer',
|
|
||||||
Editor = 'Editor',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AclTargetInfo {
|
|
||||||
value: AclTarget;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dataSourceAclLevels = [
|
|
||||||
{ value: DataSourcePermissionLevel.Query, label: 'Query', description: 'Can query data source.' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const dashboardAclTargets: AclTargetInfo[] = [
|
|
||||||
{ value: AclTarget.Team, label: 'Team' },
|
|
||||||
{ value: AclTarget.User, label: 'User' },
|
|
||||||
{ value: AclTarget.Viewer, label: 'Everyone With Viewer Role' },
|
|
||||||
{ value: AclTarget.Editor, label: 'Everyone With Editor Role' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const dashboardPermissionLevels: DashboardPermissionInfo[] = [
|
|
||||||
{ value: PermissionLevel.View, label: PermissionLevelString.View, description: 'Can view dashboards.' },
|
|
||||||
{
|
|
||||||
value: PermissionLevel.Edit,
|
|
||||||
label: PermissionLevelString.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.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface TeamPermissionInfo {
|
|
||||||
value: TeamPermissionLevel;
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const teamsPermissionLevels: TeamPermissionInfo[] = [
|
|
||||||
{ value: TeamPermissionLevel.Member, label: 'Member', description: 'Is team member' },
|
|
||||||
{
|
|
||||||
value: TeamPermissionLevel.Admin,
|
|
||||||
label: 'Admin',
|
|
||||||
description: 'Can add/remove permissions, members and delete team.',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
@ -2,8 +2,6 @@ import { DataQuery } from '@grafana/data';
|
|||||||
import { Dashboard, DataSourceRef } from '@grafana/schema';
|
import { Dashboard, DataSourceRef } from '@grafana/schema';
|
||||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||||
|
|
||||||
import { DashboardAcl } from './acl';
|
|
||||||
|
|
||||||
export interface DashboardDTO {
|
export interface DashboardDTO {
|
||||||
redirectUri?: string;
|
redirectUri?: string;
|
||||||
dashboard: DashboardDataDTO;
|
dashboard: DashboardDataDTO;
|
||||||
@ -112,5 +110,4 @@ export interface DashboardState {
|
|||||||
initPhase: DashboardInitPhase;
|
initPhase: DashboardInitPhase;
|
||||||
initialDatasource?: DataSourceRef['uid'];
|
initialDatasource?: DataSourceRef['uid'];
|
||||||
initError: DashboardInitError | null;
|
initError: DashboardInitError | null;
|
||||||
permissions: DashboardAcl[];
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { WithAccessControlMetadata } from '@grafana/data';
|
import { WithAccessControlMetadata } from '@grafana/data';
|
||||||
|
|
||||||
import { DashboardAcl } from './acl';
|
|
||||||
|
|
||||||
export interface FolderDTO extends WithAccessControlMetadata {
|
export interface FolderDTO extends WithAccessControlMetadata {
|
||||||
canAdmin: boolean;
|
canAdmin: boolean;
|
||||||
canDelete: boolean;
|
canDelete: boolean;
|
||||||
@ -30,8 +28,6 @@ export interface FolderState {
|
|||||||
canDelete: boolean;
|
canDelete: boolean;
|
||||||
hasChanged: boolean;
|
hasChanged: boolean;
|
||||||
version: number;
|
version: number;
|
||||||
permissions: DashboardAcl[];
|
|
||||||
canViewFolderPermissions: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DescendantCountDTO {
|
export interface DescendantCountDTO {
|
||||||
@ -57,5 +53,4 @@ export interface FolderInfo {
|
|||||||
uid?: string;
|
uid?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
canViewFolderPermissions?: boolean;
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user