Dashboards: hide playlist edit functionality from viewers and snapshots link from unauthenticated users (#28992)

* feat: hide snapshots menu item from viewers

* feat(playlists): prevent viewers from creating/editing playlists

* feat: prevent viewers seeing playlist nav link if no playlists

* refactor(playlist): rename isViewer property to canEditPlaylists

* revert(playlists): put back note if viewer and no playlists

* refactor(snapshots): consider admin/editor permission in folders/dashboards for displaying menu item

* feat(snapshots): only show snapshot nav item if user is signed in

* fix(snapshots): revert snapshots to previous state if delete snapshot api error
This commit is contained in:
Jack Westbrook 2020-12-02 15:51:22 +01:00 committed by GitHub
parent 2d49d3f5b2
commit aa70a38391
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 42 additions and 21 deletions

View File

@ -164,7 +164,15 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
{Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true}, {Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true},
{Text: "Manage", Id: "manage-dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "sitemap"}, {Text: "Manage", Id: "manage-dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "sitemap"},
{Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "presentation-play"}, {Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "presentation-play"},
{Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "camera"}, }
if c.IsSignedIn {
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{
Text: "Snapshots",
Id: "snapshots",
Url: setting.AppSubUrl + "/dashboard/snapshots",
Icon: "camera",
})
} }
navTree = append(navTree, &dtos.NavLink{ navTree = append(navTree, &dtos.NavLink{

View File

@ -1,7 +1,6 @@
import React, { FC, useState, useCallback, useEffect } from 'react'; import React, { FC, useState, useCallback, useEffect } from 'react';
import { ConfirmModal, Button, LinkButton } from '@grafana/ui'; import { ConfirmModal, Button, LinkButton } from '@grafana/ui';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
import { noop } from 'rxjs';
import { Snapshot } from '../types'; import { Snapshot } from '../types';
interface Props { interface Props {
@ -28,11 +27,12 @@ export const SnapshotListTable: FC<Props> = ({ url }) => {
const doRemoveSnapshot = useCallback( const doRemoveSnapshot = useCallback(
async (snapshot: Snapshot) => { async (snapshot: Snapshot) => {
setSnapshots(snapshots.filter(ss => ss.key !== snapshot.key)); const filteredSnapshots = snapshots.filter(ss => ss.key !== snapshot.key);
setSnapshots(filteredSnapshots);
await getBackendSrv() await getBackendSrv()
.delete(`/api/snapshots/${snapshot.key}`) .delete(`/api/snapshots/${snapshot.key}`)
.then(noop, () => { .catch(() => {
setSnapshots(snapshots.concat(snapshot)); setSnapshots(snapshots);
}); });
}, },
[snapshots] [snapshots]
@ -59,9 +59,9 @@ export const SnapshotListTable: FC<Props> = ({ url }) => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{snapshots.map((snapshot, key) => { {snapshots.map(snapshot => {
return ( return (
<tr key={key}> <tr key={snapshot.key}>
<td> <td>
<a href={snapshot.url}>{snapshot.name}</a> <a href={snapshot.url}>{snapshot.name}</a>
</td> </td>

View File

@ -2,7 +2,7 @@
<div class="page-container page-body"> <div class="page-container page-body">
<div ng-if="ctrl.playlists.length > 0"> <div ng-if="ctrl.playlists.length > 0">
<div class="page-action-bar"> <div class="page-action-bar" ng-if="ctrl.canEditPlaylists">
<div class="page-action-bar__spacer"></div> <div class="page-action-bar__spacer"></div>
<a class="btn btn-primary pull-right" href="playlists/create"> <a class="btn btn-primary pull-right" href="playlists/create">
New playlist New playlist
@ -13,11 +13,12 @@
<thead> <thead>
<th><strong>Name</strong></th> <th><strong>Name</strong></th>
<th style="width: 100px"></th> <th style="width: 100px"></th>
<th style="width: 78px"></th> <th ng-if="ctrl.canEditPlaylists" style="width: 78px"></th>
</thead> </thead>
<tr ng-repeat="playlist in ctrl.playlists"> <tr ng-repeat="playlist in ctrl.playlists">
<td class="link-td"> <td ng-class="{'link-td': ctrl.canEditPlaylists}">
<a href="playlists/edit/{{playlist.id}}">{{playlist.name}}</a> <a ng-if="ctrl.canEditPlaylists" href="playlists/edit/{{playlist.id}}">{{playlist.name}}</a>
<span ng-if="!ctrl.canEditPlaylists">{{playlist.name}}</span>
</td> </td>
<td class="dropdown"> <td class="dropdown">
<button class="btn btn-inverse btn-small" data-toggle="dropdown"> <button class="btn btn-inverse btn-small" data-toggle="dropdown">
@ -44,7 +45,7 @@
</li> </li>
</ul> </ul>
</td> </td>
<td class="text-right"> <td ng-if="ctrl.canEditPlaylists" class="text-right">
<a ng-click="ctrl.removePlaylist(playlist)" class="btn btn-danger btn-small"> <a ng-click="ctrl.removePlaylist(playlist)" class="btn btn-danger btn-small">
<icon name="'times'"></icon> <icon name="'times'"></icon>
</a> </a>
@ -53,16 +54,23 @@
</table> </table>
</div> </div>
<div ng-if="ctrl.playlists.length === 0"> <div ng-if="ctrl.playlists.length === 0">
<empty-list-cta <div ng-if="ctrl.canEditPlaylists">
title="'There are no playlists created yet'" <empty-list-cta
buttonIcon="'plus'" title="'There are no playlists created yet'"
buttonLink="'playlists/create'" buttonIcon="'plus'"
buttonTitle="'Create Playlist'" buttonLink="'playlists/create'"
proTip="'You can use playlists to cycle dashboards on TVs without user control'" buttonTitle="'Create Playlist'"
proTipLink="'http://docs.grafana.org/reference/playlist/'" proTip="'You can use playlists to cycle dashboards on TVs without user control'"
proTipLinkTitle="'Learn more'" proTipLink="'http://docs.grafana.org/reference/playlist/'"
proTipTarget="'_blank'" /> proTipLinkTitle="'Learn more'"
proTipTarget="'_blank'" />
</div>
<div class="grafana-info-box" ng-if="!ctrl.canEditPlaylists">
<h5>There are no playlists created yet</h5>
<p>Unfortunately you don't have permission to create playlists.</p>
</div>
</div> </div>
</div> </div>
<footer /> <footer />

View File

@ -1,9 +1,11 @@
import { IScope } from 'angular'; import { IScope } from 'angular';
import _ from 'lodash'; import _ from 'lodash';
import { AppEvents } from '@grafana/data'; import { AppEvents } from '@grafana/data';
import { OrgRole } from 'app/types';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
import coreModule from '../../core/core_module'; import coreModule from '../../core/core_module';
import config from '../../core/config';
import { NavModelSrv } from 'app/core/nav_model_srv'; import { NavModelSrv } from 'app/core/nav_model_srv';
import { AppEventEmitter, CoreEvents } from 'app/types'; import { AppEventEmitter, CoreEvents } from 'app/types';
import { promiseToDigest } from '../../core/utils/promiseToDigest'; import { promiseToDigest } from '../../core/utils/promiseToDigest';
@ -11,10 +13,13 @@ import { promiseToDigest } from '../../core/utils/promiseToDigest';
export class PlaylistsCtrl { export class PlaylistsCtrl {
playlists: any; playlists: any;
navModel: any; navModel: any;
canEditPlaylists: boolean;
/** @ngInject */ /** @ngInject */
constructor(private $scope: IScope & AppEventEmitter, navModelSrv: NavModelSrv) { constructor(private $scope: IScope & AppEventEmitter, navModelSrv: NavModelSrv) {
this.navModel = navModelSrv.getNav('dashboards', 'playlists', 0); this.navModel = navModelSrv.getNav('dashboards', 'playlists', 0);
this.canEditPlaylists = config.bootData.user.orgRole !== OrgRole.Viewer;
promiseToDigest($scope)( promiseToDigest($scope)(
getBackendSrv() getBackendSrv()
.get('/api/playlists') .get('/api/playlists')