mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 14:12:26 +08:00
Access Control: hiding annotation edition and deletion without permissions (#46904)
* Access Control: disabling annotation edition without FGAC permissions
This commit is contained in:

committed by
GitHub

parent
f8d11fbef9
commit
76b221e9d5
@ -30,6 +30,8 @@ export interface PanelContext {
|
|||||||
onToggleSeriesVisibility?: (label: string, mode: SeriesVisibilityChangeMode) => void;
|
onToggleSeriesVisibility?: (label: string, mode: SeriesVisibilityChangeMode) => void;
|
||||||
|
|
||||||
canAddAnnotations?: () => boolean;
|
canAddAnnotations?: () => boolean;
|
||||||
|
canEditAnnotations?: (dashboardId: number) => boolean;
|
||||||
|
canDeleteAnnotations?: (dashboardId: number) => boolean;
|
||||||
onAnnotationCreate?: (annotation: AnnotationEventUIModel) => void;
|
onAnnotationCreate?: (annotation: AnnotationEventUIModel) => void;
|
||||||
onAnnotationUpdate?: (annotation: AnnotationEventUIModel) => void;
|
onAnnotationUpdate?: (annotation: AnnotationEventUIModel) => void;
|
||||||
onAnnotationDelete?: (id: string) => void;
|
onAnnotationDelete?: (id: string) => void;
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/guardian"
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
@ -115,25 +116,33 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
|
|||||||
creator = hs.getUserLogin(c.Req.Context(), dash.CreatedBy)
|
creator = hs.getUserLogin(c.Req.Context(), dash.CreatedBy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
annotationPermissions := &dtos.AnnotationPermission{}
|
||||||
|
|
||||||
|
if !hs.AccessControl.IsDisabled() {
|
||||||
|
hs.getAnnotationPermissionsByScope(c, &annotationPermissions.Dashboard, accesscontrol.ScopeAnnotationsTypeDashboard)
|
||||||
|
hs.getAnnotationPermissionsByScope(c, &annotationPermissions.Organization, accesscontrol.ScopeAnnotationsTypeOrganization)
|
||||||
|
}
|
||||||
|
|
||||||
meta := dtos.DashboardMeta{
|
meta := dtos.DashboardMeta{
|
||||||
IsStarred: isStarred,
|
IsStarred: isStarred,
|
||||||
Slug: dash.Slug,
|
Slug: dash.Slug,
|
||||||
Type: models.DashTypeDB,
|
Type: models.DashTypeDB,
|
||||||
CanStar: c.IsSignedIn,
|
CanStar: c.IsSignedIn,
|
||||||
CanSave: canSave,
|
CanSave: canSave,
|
||||||
CanEdit: canEdit,
|
CanEdit: canEdit,
|
||||||
CanAdmin: canAdmin,
|
CanAdmin: canAdmin,
|
||||||
CanDelete: canDelete,
|
CanDelete: canDelete,
|
||||||
Created: dash.Created,
|
Created: dash.Created,
|
||||||
Updated: dash.Updated,
|
Updated: dash.Updated,
|
||||||
UpdatedBy: updater,
|
UpdatedBy: updater,
|
||||||
CreatedBy: creator,
|
CreatedBy: creator,
|
||||||
Version: dash.Version,
|
Version: dash.Version,
|
||||||
HasAcl: dash.HasAcl,
|
HasAcl: dash.HasAcl,
|
||||||
IsFolder: dash.IsFolder,
|
IsFolder: dash.IsFolder,
|
||||||
FolderId: dash.FolderId,
|
FolderId: dash.FolderId,
|
||||||
Url: dash.GetUrl(),
|
Url: dash.GetUrl(),
|
||||||
FolderTitle: "General",
|
FolderTitle: "General",
|
||||||
|
AnnotationsPermissions: annotationPermissions,
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup folder title
|
// lookup folder title
|
||||||
@ -190,6 +199,22 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
|
|||||||
return response.JSON(200, dto)
|
return response.JSON(200, dto)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hs *HTTPServer) getAnnotationPermissionsByScope(c *models.ReqContext, actions *dtos.AnnotationActions, scope string) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
evaluate := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, scope)
|
||||||
|
actions.CanDelete, err = hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluate)
|
||||||
|
if err != nil {
|
||||||
|
hs.log.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsDelete, "scope", scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluate = accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsWrite, scope)
|
||||||
|
actions.CanEdit, err = hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluate)
|
||||||
|
if err != nil {
|
||||||
|
hs.log.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsWrite, "scope", scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) getUserLogin(ctx context.Context, userID int64) string {
|
func (hs *HTTPServer) getUserLogin(ctx context.Context, userID int64) string {
|
||||||
query := models.GetUserByIdQuery{Id: userID}
|
query := models.GetUserByIdQuery{Id: userID}
|
||||||
err := hs.SQLStore.GetUserById(ctx, &query)
|
err := hs.SQLStore.GetUserById(ctx, &query)
|
||||||
|
@ -118,10 +118,11 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
|||||||
mockSQLStore.ExpectedDashboard = fakeDash
|
mockSQLStore.ExpectedDashboard = fakeDash
|
||||||
|
|
||||||
hs := &HTTPServer{
|
hs := &HTTPServer{
|
||||||
Cfg: setting.NewCfg(),
|
Cfg: setting.NewCfg(),
|
||||||
pluginStore: &fakePluginStore{},
|
pluginStore: &fakePluginStore{},
|
||||||
SQLStore: mockSQLStore,
|
SQLStore: mockSQLStore,
|
||||||
Features: featuremgmt.WithFeatures(),
|
AccessControl: accesscontrolmock.New(),
|
||||||
|
Features: featuremgmt.WithFeatures(),
|
||||||
}
|
}
|
||||||
hs.SQLStore = mockSQLStore
|
hs.SQLStore = mockSQLStore
|
||||||
|
|
||||||
@ -224,6 +225,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
|||||||
LibraryPanelService: &mockLibraryPanelService{},
|
LibraryPanelService: &mockLibraryPanelService{},
|
||||||
LibraryElementService: &mockLibraryElementService{},
|
LibraryElementService: &mockLibraryElementService{},
|
||||||
SQLStore: mockSQLStore,
|
SQLStore: mockSQLStore,
|
||||||
|
AccessControl: accesscontrolmock.New(),
|
||||||
dashboardService: service.ProvideDashboardService(
|
dashboardService: service.ProvideDashboardService(
|
||||||
cfg, dashboardStore, nil, features, accesscontrolmock.NewPermissionsServicesMock(),
|
cfg, dashboardStore, nil, features, accesscontrolmock.NewPermissionsServicesMock(),
|
||||||
),
|
),
|
||||||
@ -890,6 +892,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
|||||||
LibraryElementService: &mockLibraryElementService{},
|
LibraryElementService: &mockLibraryElementService{},
|
||||||
dashboardProvisioningService: mockDashboardProvisioningService{},
|
dashboardProvisioningService: mockDashboardProvisioningService{},
|
||||||
SQLStore: mockSQLStore,
|
SQLStore: mockSQLStore,
|
||||||
|
AccessControl: accesscontrolmock.New(),
|
||||||
}
|
}
|
||||||
hs.callGetDashboard(sc)
|
hs.callGetDashboard(sc)
|
||||||
|
|
||||||
@ -927,6 +930,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
|
|||||||
LibraryElementService: &libraryElementsService,
|
LibraryElementService: &libraryElementsService,
|
||||||
SQLStore: sc.sqlStore,
|
SQLStore: sc.sqlStore,
|
||||||
ProvisioningService: provisioningService,
|
ProvisioningService: provisioningService,
|
||||||
|
AccessControl: accesscontrolmock.New(),
|
||||||
dashboardProvisioningService: service.ProvideDashboardService(
|
dashboardProvisioningService: service.ProvideDashboardService(
|
||||||
cfg, dashboardStore, nil, features, accesscontrolmock.NewPermissionsServicesMock(),
|
cfg, dashboardStore, nil, features, accesscontrolmock.NewPermissionsServicesMock(),
|
||||||
),
|
),
|
||||||
|
@ -7,31 +7,41 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DashboardMeta struct {
|
type DashboardMeta struct {
|
||||||
IsStarred bool `json:"isStarred,omitempty"`
|
IsStarred bool `json:"isStarred,omitempty"`
|
||||||
IsHome bool `json:"isHome,omitempty"`
|
IsHome bool `json:"isHome,omitempty"`
|
||||||
IsSnapshot bool `json:"isSnapshot,omitempty"`
|
IsSnapshot bool `json:"isSnapshot,omitempty"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
CanSave bool `json:"canSave"`
|
CanSave bool `json:"canSave"`
|
||||||
CanEdit bool `json:"canEdit"`
|
CanEdit bool `json:"canEdit"`
|
||||||
CanAdmin bool `json:"canAdmin"`
|
CanAdmin bool `json:"canAdmin"`
|
||||||
CanStar bool `json:"canStar"`
|
CanStar bool `json:"canStar"`
|
||||||
CanDelete bool `json:"canDelete"`
|
CanDelete bool `json:"canDelete"`
|
||||||
Slug string `json:"slug"`
|
Slug string `json:"slug"`
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
Expires time.Time `json:"expires"`
|
Expires time.Time `json:"expires"`
|
||||||
Created time.Time `json:"created"`
|
Created time.Time `json:"created"`
|
||||||
Updated time.Time `json:"updated"`
|
Updated time.Time `json:"updated"`
|
||||||
UpdatedBy string `json:"updatedBy"`
|
UpdatedBy string `json:"updatedBy"`
|
||||||
CreatedBy string `json:"createdBy"`
|
CreatedBy string `json:"createdBy"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
HasAcl bool `json:"hasAcl"`
|
HasAcl bool `json:"hasAcl"`
|
||||||
IsFolder bool `json:"isFolder"`
|
IsFolder bool `json:"isFolder"`
|
||||||
FolderId int64 `json:"folderId"`
|
FolderId int64 `json:"folderId"`
|
||||||
FolderUid string `json:"folderUid"`
|
FolderUid string `json:"folderUid"`
|
||||||
FolderTitle string `json:"folderTitle"`
|
FolderTitle string `json:"folderTitle"`
|
||||||
FolderUrl string `json:"folderUrl"`
|
FolderUrl string `json:"folderUrl"`
|
||||||
Provisioned bool `json:"provisioned"`
|
Provisioned bool `json:"provisioned"`
|
||||||
ProvisionedExternalId string `json:"provisionedExternalId"`
|
ProvisionedExternalId string `json:"provisionedExternalId"`
|
||||||
|
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
|
||||||
|
}
|
||||||
|
type AnnotationPermission struct {
|
||||||
|
Dashboard AnnotationActions `json:"dashboard"`
|
||||||
|
Organization AnnotationActions `json:"organization"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnnotationActions struct {
|
||||||
|
CanEdit bool `json:"canEdit"`
|
||||||
|
CanDelete bool `json:"canDelete"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DashboardFullWithMeta struct {
|
type DashboardFullWithMeta struct {
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
<div class="gf-form-button-row">
|
<div class="gf-form-button-row">
|
||||||
<button type="submit" class="btn btn-primary" ng-click="ctrl.save()">Save</button>
|
<button type="submit" class="btn btn-primary" ng-click="ctrl.save()">Save</button>
|
||||||
<button ng-if="ctrl.event.id" type="submit" class="btn btn-danger" ng-click="ctrl.delete()">Delete</button>
|
<button ng-if="ctrl.event.id && ctrl.canDelete()" type="submit" class="btn btn-danger" ng-click="ctrl.delete()">Delete</button>
|
||||||
<a class="btn-text" ng-click="ctrl.close();">Cancel</a>
|
<a class="btn-text" ng-click="ctrl.close();">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -124,6 +124,7 @@ const alertEventAndAnnotationFields: AnnotationFieldInfo[] = [
|
|||||||
{ key: 'data' as any },
|
{ key: 'data' as any },
|
||||||
{ key: 'panelId' },
|
{ key: 'panelId' },
|
||||||
{ key: 'alertId' },
|
{ key: 'alertId' },
|
||||||
|
{ key: 'dashboardId' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getAnnotationsFromData(
|
export function getAnnotationsFromData(
|
||||||
|
@ -38,6 +38,7 @@ import { deleteAnnotation, saveAnnotation, updateAnnotation } from '../../annota
|
|||||||
import { getDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner';
|
import { getDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner';
|
||||||
import { liveTimer } from './liveTimer';
|
import { liveTimer } from './liveTimer';
|
||||||
import { isSoloRoute } from '../../../routes/utils';
|
import { isSoloRoute } from '../../../routes/utils';
|
||||||
|
import { contextSrv } from '../../../core/services/context_srv';
|
||||||
|
|
||||||
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
|
||||||
|
|
||||||
@ -90,11 +91,39 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
canAddAnnotations: () => Boolean(props.dashboard.meta.canEdit || props.dashboard.meta.canMakeEditable),
|
canAddAnnotations: () => Boolean(props.dashboard.meta.canEdit || props.dashboard.meta.canMakeEditable),
|
||||||
onInstanceStateChange: this.onInstanceStateChange,
|
onInstanceStateChange: this.onInstanceStateChange,
|
||||||
onToggleLegendSort: this.onToggleLegendSort,
|
onToggleLegendSort: this.onToggleLegendSort,
|
||||||
|
canEditAnnotations: this.canEditAnnotation,
|
||||||
|
canDeleteAnnotations: this.canDeleteAnnotation,
|
||||||
},
|
},
|
||||||
data: this.getInitialPanelDataState(),
|
data: this.getInitialPanelDataState(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canEditAnnotation = (dashboardId: number) => {
|
||||||
|
let canEdit = true;
|
||||||
|
|
||||||
|
if (contextSrv.accessControlEnabled()) {
|
||||||
|
if (dashboardId !== 0) {
|
||||||
|
canEdit = !!this.props.dashboard.meta.annotationsPermissions?.dashboard.canEdit;
|
||||||
|
} else {
|
||||||
|
canEdit = !!this.props.dashboard.meta.annotationsPermissions?.organization.canEdit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return canEdit && Boolean(this.props.dashboard.meta.canEdit || this.props.dashboard.meta.canMakeEditable);
|
||||||
|
};
|
||||||
|
|
||||||
|
canDeleteAnnotation = (dashboardId: number) => {
|
||||||
|
let canDelete = true;
|
||||||
|
|
||||||
|
if (contextSrv.accessControlEnabled()) {
|
||||||
|
if (dashboardId !== 0) {
|
||||||
|
canDelete = !!this.props.dashboard.meta.annotationsPermissions?.dashboard.canDelete;
|
||||||
|
} else {
|
||||||
|
canDelete = !!this.props.dashboard.meta.annotationsPermissions?.organization.canDelete;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return canDelete && Boolean(this.props.dashboard.meta.canEdit || this.props.dashboard.meta.canMakeEditable);
|
||||||
|
};
|
||||||
|
|
||||||
// Due to a mutable panel model we get the sync settings via function that proactively reads from the model
|
// Due to a mutable panel model we get the sync settings via function that proactively reads from the model
|
||||||
getSync = () => (this.props.isEditing ? DashboardCursorSync.Off : this.props.dashboard.graphTooltip);
|
getSync = () => (this.props.isEditing ? DashboardCursorSync.Off : this.props.dashboard.graphTooltip);
|
||||||
|
|
||||||
|
@ -1178,6 +1178,20 @@ export class DashboardModel implements TimeModel {
|
|||||||
return this.getVariablesFromState(this.uid);
|
return this.getVariablesFromState(this.uid);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
canEditAnnotations(dashboardId: number) {
|
||||||
|
let canEdit = true;
|
||||||
|
|
||||||
|
// if FGAC is enabled there are additional conditions to check
|
||||||
|
if (contextSrv.accessControlEnabled()) {
|
||||||
|
if (dashboardId === 0) {
|
||||||
|
canEdit = !!this.meta.annotationsPermissions?.organization.canEdit;
|
||||||
|
} else {
|
||||||
|
canEdit = !!this.meta.annotationsPermissions?.dashboard.canEdit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.canAddAnnotations() && canEdit;
|
||||||
|
}
|
||||||
|
|
||||||
canAddAnnotations() {
|
canAddAnnotations() {
|
||||||
return this.meta.canEdit || this.meta.canMakeEditable;
|
return this.meta.canEdit || this.meta.canMakeEditable;
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ export function annotationTooltipDirective(
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Show edit icon only for users with at least Editor role
|
// Show edit icon only for users with at least Editor role
|
||||||
if (event.id && dashboard?.canAddAnnotations()) {
|
if (event.id && dashboard?.canEditAnnotations(event.dashboardId)) {
|
||||||
header += `
|
header += `
|
||||||
<span class="pointer graph-annotation__edit-icon" ng-click="onEdit()">
|
<span class="pointer graph-annotation__edit-icon" ng-click="onEdit()">
|
||||||
<i class="fa fa-pencil-square"></i>
|
<i class="fa fa-pencil-square"></i>
|
||||||
|
@ -4,6 +4,7 @@ import { AnnotationEvent, dateTime } from '@grafana/data';
|
|||||||
import { MetricsPanelCtrl } from 'app/angular/panel/metrics_panel_ctrl';
|
import { MetricsPanelCtrl } from 'app/angular/panel/metrics_panel_ctrl';
|
||||||
import { deleteAnnotation, saveAnnotation, updateAnnotation } from '../../../features/annotations/api';
|
import { deleteAnnotation, saveAnnotation, updateAnnotation } from '../../../features/annotations/api';
|
||||||
import { getDashboardQueryRunner } from '../../../features/query/state/DashboardQueryRunner/DashboardQueryRunner';
|
import { getDashboardQueryRunner } from '../../../features/query/state/DashboardQueryRunner/DashboardQueryRunner';
|
||||||
|
import { contextSrv } from '../../../core/services/context_srv';
|
||||||
|
|
||||||
export class EventEditorCtrl {
|
export class EventEditorCtrl {
|
||||||
// @ts-ignore initialized through Angular not constructor
|
// @ts-ignore initialized through Angular not constructor
|
||||||
@ -31,6 +32,16 @@ export class EventEditorCtrl {
|
|||||||
this.timeFormated = this.panelCtrl.dashboard.formatDate(this.event.time!);
|
this.timeFormated = this.panelCtrl.dashboard.formatDate(this.event.time!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canDelete(): boolean {
|
||||||
|
if (contextSrv.accessControlEnabled()) {
|
||||||
|
if (this.event.source.type === 'dashboard') {
|
||||||
|
return !!this.panelCtrl.dashboard.meta.annotationsPermissions?.dashboard.canDelete;
|
||||||
|
}
|
||||||
|
return !!this.panelCtrl.dashboard.meta.annotationsPermissions?.organization.canDelete;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async save(): Promise<void> {
|
async save(): Promise<void> {
|
||||||
if (!this.form.$valid) {
|
if (!this.form.$valid) {
|
||||||
return;
|
return;
|
||||||
|
@ -27,7 +27,7 @@ const POPPER_CONFIG = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function AnnotationMarker({ annotation, timeZone, style }: Props) {
|
export function AnnotationMarker({ annotation, timeZone, style }: Props) {
|
||||||
const { canAddAnnotations, ...panelCtx } = usePanelContext();
|
const { canAddAnnotations, canEditAnnotations, canDeleteAnnotations, ...panelCtx } = usePanelContext();
|
||||||
const commonStyles = useStyles2(getCommonAnnotationStyles);
|
const commonStyles = useStyles2(getCommonAnnotationStyles);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
@ -89,10 +89,11 @@ export function AnnotationMarker({ annotation, timeZone, style }: Props) {
|
|||||||
timeFormatter={timeFormatter}
|
timeFormatter={timeFormatter}
|
||||||
onEdit={onAnnotationEdit}
|
onEdit={onAnnotationEdit}
|
||||||
onDelete={onAnnotationDelete}
|
onDelete={onAnnotationDelete}
|
||||||
editable={Boolean(canAddAnnotations && canAddAnnotations())}
|
canEdit={canEditAnnotations!(annotation.dashboardId)}
|
||||||
|
canDelete={canDeleteAnnotations!(annotation.dashboardId)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [canAddAnnotations, onAnnotationDelete, onAnnotationEdit, timeFormatter, annotation]);
|
}, [canEditAnnotations, canDeleteAnnotations, onAnnotationDelete, onAnnotationEdit, timeFormatter, annotation]);
|
||||||
|
|
||||||
const isRegionAnnotation = Boolean(annotation.isRegion);
|
const isRegionAnnotation = Boolean(annotation.isRegion);
|
||||||
|
|
||||||
|
@ -10,7 +10,8 @@ import config from 'app/core/config';
|
|||||||
interface AnnotationTooltipProps {
|
interface AnnotationTooltipProps {
|
||||||
annotation: AnnotationsDataFrameViewDTO;
|
annotation: AnnotationsDataFrameViewDTO;
|
||||||
timeFormatter: (v: number) => string;
|
timeFormatter: (v: number) => string;
|
||||||
editable: boolean;
|
canEdit: boolean;
|
||||||
|
canDelete: boolean;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}
|
}
|
||||||
@ -18,7 +19,8 @@ interface AnnotationTooltipProps {
|
|||||||
export const AnnotationTooltip = ({
|
export const AnnotationTooltip = ({
|
||||||
annotation,
|
annotation,
|
||||||
timeFormatter,
|
timeFormatter,
|
||||||
editable,
|
canEdit,
|
||||||
|
canDelete,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: AnnotationTooltipProps) => {
|
}: AnnotationTooltipProps) => {
|
||||||
@ -51,11 +53,11 @@ export const AnnotationTooltip = ({
|
|||||||
text = annotation.title + '<br />' + (typeof text === 'string' ? text : '');
|
text = annotation.title + '<br />' + (typeof text === 'string' ? text : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editable) {
|
if (canEdit || canDelete) {
|
||||||
editControls = (
|
editControls = (
|
||||||
<div className={styles.editControls}>
|
<div className={styles.editControls}>
|
||||||
<IconButton name={'pen'} size={'sm'} onClick={onEdit} />
|
{canEdit && <IconButton name={'pen'} size={'sm'} onClick={onEdit} />}
|
||||||
<IconButton name={'trash-alt'} size={'sm'} onClick={onDelete} />
|
{canDelete && <IconButton name={'trash-alt'} size={'sm'} onClick={onDelete} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
interface AnnotationsDataFrameViewDTO {
|
interface AnnotationsDataFrameViewDTO {
|
||||||
id: string;
|
id: string;
|
||||||
|
dashboardId: number;
|
||||||
time: number;
|
time: number;
|
||||||
timeEnd: number;
|
timeEnd: number;
|
||||||
text: string;
|
text: string;
|
||||||
|
@ -35,6 +35,17 @@ export interface DashboardMeta {
|
|||||||
fromScript?: boolean;
|
fromScript?: boolean;
|
||||||
fromFile?: boolean;
|
fromFile?: boolean;
|
||||||
hasUnsavedFolderChange?: boolean;
|
hasUnsavedFolderChange?: boolean;
|
||||||
|
annotationsPermissions?: AnnotationsPermissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnnotationActions {
|
||||||
|
canEdit: boolean;
|
||||||
|
canDelete: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnnotationsPermissions {
|
||||||
|
dashboard: AnnotationActions;
|
||||||
|
organization: AnnotationActions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardDataDTO {
|
export interface DashboardDataDTO {
|
||||||
|
Reference in New Issue
Block a user