Chore: remove legacy dashboard and folder permission pages (#77143)

remove legacy dashboard and folder permission pages
This commit is contained in:
Ieva 2023-10-31 14:23:37 +00:00 committed by GitHub
parent 75005a8482
commit ed88289984
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 11 additions and 1216 deletions

View File

@ -1349,9 +1349,6 @@ exports[`better eslint`] = {
"public/app/core/components/PasswordField/PasswordField.tsx:5381": [
[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": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
@ -4197,9 +4194,6 @@ exports[`better eslint`] = {
"public/app/features/expressions/guards.ts:5381": [
[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": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],

View File

@ -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;

View File

@ -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>
);
}
}

View File

@ -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;

View File

@ -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>
);
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -35,8 +35,6 @@ const mockFolder = (folderOverride: Partial<FolderState> = {}): FolderState => {
canSave: false,
url: '/folder-1',
version: 1,
permissions: [],
canViewFolderPermissions: false,
canDelete: false,
...folderOverride,
};

View File

@ -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);

View File

@ -50,7 +50,6 @@ export const renderSharePublicDashboard = async (
const store = configureStore({
dashboard: {
getModel: () => props?.dashboard || mockDashboard,
permissions: [],
initError: null,
initPhase: DashboardInitPhase.Completed,
},

View File

@ -5,100 +5,14 @@ import { createSuccessNotification } from 'app/core/copy/appNotification';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { removeAllPanels } from 'app/features/panel/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 { cancelVariables } from '../../variables/state/actions';
import { getDashboardSrv } from '../services/DashboardSrv';
import { getTimeSrv } from '../services/TimeSrv';
import { cleanUpDashboard, loadDashboardPermissions } 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));
};
}
import { cleanUpDashboard } from './reducers';
export function importDashboard(data: any, dashboardTitle: string): ThunkResult<void> {
return async (dispatch) => {

View File

@ -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 {
dashboardInitCompleted,
dashboardInitFailed,
dashboardInitFetching,
loadDashboardPermissions,
dashboardReducer,
initialState,
} from './reducers';
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', () => {
let state: DashboardState;

View File

@ -3,8 +3,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { PanelPlugin } from '@grafana/data';
import { AngularComponent } from '@grafana/runtime';
import { defaultDashboard } from '@grafana/schema';
import { processAclItems } from 'app/core/utils/acl';
import { DashboardAclDTO, DashboardInitError, DashboardInitPhase, DashboardState } from 'app/types';
import { DashboardInitError, DashboardInitPhase, DashboardState } from 'app/types';
import { DashboardModel } from './DashboardModel';
import { PanelModel } from './PanelModel';
@ -12,7 +11,6 @@ import { PanelModel } from './PanelModel';
export const initialState: DashboardState = {
initPhase: DashboardInitPhase.NotStarted,
getModel: () => null,
permissions: [],
initError: null,
initialDatasource: undefined,
};
@ -21,9 +19,6 @@ const dashboardSlice = createSlice({
name: 'dashboard',
initialState,
reducers: {
loadDashboardPermissions: (state, action: PayloadAction<DashboardAclDTO[]>) => {
state.permissions = processAclItems(action.payload);
},
dashboardInitFetching: (state) => {
state.initPhase = DashboardInitPhase.Fetching;
},
@ -74,7 +69,6 @@ export interface SetPanelInstanceStatePayload {
}
export const {
loadDashboardPermissions,
dashboardInitFetching,
dashboardInitFailed,
dashboardInitCompleted,

View File

@ -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);

View File

@ -25,8 +25,6 @@ const setup = (propOverrides?: object) => {
url: 'url',
hasChanged: false,
version: 1,
permissions: [],
canViewFolderPermissions: true,
},
getFolderByUid: jest.fn(),
setFolderTitle: mockToolkitActionCreator(setFolderTitle),

View File

@ -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),
]);
});
});
});

View File

@ -1,16 +1,13 @@
import { lastValueFrom } from 'rxjs';
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 { createSuccessNotification, createWarningNotification } from 'app/core/copy/appNotification';
import { createSuccessNotification } from 'app/core/copy/appNotification';
import { contextSrv } from 'app/core/core';
import { backendSrv } from 'app/core/services/backend_srv';
import { FolderDTO, FolderState, ThunkResult } from 'app/types';
import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel } from 'app/types/acl';
import { buildNavModel } from './navModel';
import { loadFolder, loadFolderPermissions, setCanViewFolderPermissions } from './reducers';
import { loadFolder } from './reducers';
export function getFolderByUid(uid: string): ThunkResult<Promise<FolderDTO>> {
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> {
return async (dispatch) => {
const newFolder = await getBackendSrv().post('/api/folders', { title: folderName, parentUid: uid });

View File

@ -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 {
folderReducer,
initialState,
loadFolder,
loadFolderPermissions,
setCanViewFolderPermissions,
setFolderTitle,
} from './reducers';
import { folderReducer, initialState, loadFolder, setFolderTitle } from './reducers';
function getTestFolder(): FolderDTO {
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,
});
});
});
});

View File

@ -1,8 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { processAclItems } from 'app/core/utils/acl';
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 = {
id: 0,
@ -13,8 +12,6 @@ export const initialState: FolderState = {
canDelete: false,
hasChanged: false,
version: 1,
permissions: [],
canViewFolderPermissions: false,
};
const loadFolderReducer = (state: FolderState, action: PayloadAction<FolderDTO>): FolderState => {
@ -37,23 +34,13 @@ const folderSlice = createSlice({
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) => {
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;

View File

@ -9,65 +9,6 @@ export enum TeamPermissionLevel {
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 {
View = 'View',
Edit = 'Edit',
@ -79,61 +20,3 @@ export enum SearchQueryType {
Dashboard = 'dash-db',
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.',
},
];

View File

@ -2,8 +2,6 @@ import { DataQuery } from '@grafana/data';
import { Dashboard, DataSourceRef } from '@grafana/schema';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { DashboardAcl } from './acl';
export interface DashboardDTO {
redirectUri?: string;
dashboard: DashboardDataDTO;
@ -112,5 +110,4 @@ export interface DashboardState {
initPhase: DashboardInitPhase;
initialDatasource?: DataSourceRef['uid'];
initError: DashboardInitError | null;
permissions: DashboardAcl[];
}

View File

@ -1,7 +1,5 @@
import { WithAccessControlMetadata } from '@grafana/data';
import { DashboardAcl } from './acl';
export interface FolderDTO extends WithAccessControlMetadata {
canAdmin: boolean;
canDelete: boolean;
@ -30,8 +28,6 @@ export interface FolderState {
canDelete: boolean;
hasChanged: boolean;
version: number;
permissions: DashboardAcl[];
canViewFolderPermissions: boolean;
}
export interface DescendantCountDTO {
@ -57,5 +53,4 @@ export interface FolderInfo {
uid?: string;
title?: string;
url?: string;
canViewFolderPermissions?: boolean;
}