@ -1,13 +1,11 @@
import { css , cx } from '@emotion/css' ;
import * as H from 'history' ;
import { useDialog } from '@react-aria/dialog' ;
import React , { useMemo } from 'react' ;
import { FocusScope } from '@react-aria/focus' ;
import { useLocation } from 'react-router-dom' ;
import { useOverlay } from '@react-aria/overlays' ;
import React , { useCallback , useMemo , useRef } from 'react' ;
import { locationUtil , NavModel , NavModelItem } from '@grafana/data' ;
import { Link } from 'react-router-dom' ;
import { locationService } from '@grafana/runtime' ;
import { Button , PageToolbar } from '@grafana/ui' ;
import { GrafanaTheme2 , locationUtil } from '@grafana/data' ;
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate' ;
import { locationService , reportInteraction } from '@grafana/runtime' ;
import { Button , CustomScrollbar , Icon , IconName , PageToolbar , stylesFactory , useForceUpdate } from '@grafana/ui' ;
import config from 'app/core/config' ;
import config from 'app/core/config' ;
import { contextSrv } from 'app/core/services/context_srv' ;
import { contextSrv } from 'app/core/services/context_srv' ;
import { AccessControlAction } from 'app/types' ;
import { AccessControlAction } from 'app/types' ;
@ -23,204 +21,205 @@ import { GeneralSettings } from './GeneralSettings';
import { JsonEditorSettings } from './JsonEditorSettings' ;
import { JsonEditorSettings } from './JsonEditorSettings' ;
import { LinksSettings } from './LinksSettings' ;
import { LinksSettings } from './LinksSettings' ;
import { VersionsSettings } from './VersionsSettings' ;
import { VersionsSettings } from './VersionsSettings' ;
import { SettingsPage , SettingsPageProps } from './types' ;
export interface Props {
export interface Props {
dashboard : DashboardModel ;
dashboard : DashboardModel ;
sectionNav : NavModel ;
pageNav : NavModelItem ;
editview : string ;
editview : string ;
}
}
export interface SettingsPage {
const onClose = ( ) = > locationService . partial ( { editview : null , editIndex : null } ) ;
id : string ;
title : string ;
icon : IconName ;
component : React.ReactNode ;
}
const onClose = ( ) = > locationService . partial ( { editview : null } ) ;
export function DashboardSettings ( { dashboard , editview , pageNav , sectionNav } : Props ) {
const pages = useMemo ( ( ) = > getSettingsPages ( dashboard ) , [ dashboard ] ) ;
const MakeEditable = ( props : { onMakeEditable : ( ) = > any } ) = > (
< div >
const onPostSave = ( ) = > {
< div className = "dashboard-settings__header" > Dashboard not editable < / div >
dashboard . meta . hasUnsavedFolderChange = false ;
< Button type = "submit" onClick = { props . onMakeEditable } >
} ;
Make editable
< / Button >
const folderTitle = dashboard . meta . folderTitle ;
< / div >
const currentPage = pages . find ( ( page ) = > page . id === editview ) ? ? pages [ 0 ] ;
) ;
const canSaveAs = contextSrv . hasEditPermissionInFolders ;
const canSave = dashboard . meta . canSave ;
export function DashboardSettings ( { dashboard , editview } : Props ) {
const location = useLocation ( ) ;
const ref = useRef < HTMLDivElement > ( null ) ;
const editIndex = getEditIndex ( location ) ;
const { overlayProps } = useOverlay (
const subSectionNav = getSectionNav ( pageNav , sectionNav , pages , currentPage , location ) ;
{
isOpen : true ,
const actions = [
onClose ,
canSaveAs && (
} ,
< SaveDashboardAsButton dashboard = { dashboard } onSaveSuccess = { onPostSave } variant = "secondary" key = "save as" / >
ref
) ,
) ;
canSave && < SaveDashboardButton dashboard = { dashboard } onSaveSuccess = { onPostSave } key = "Save" / > ,
const { dialogProps } = useDialog (
] ;
{
'aria-label' : 'Dashboard settings' ,
return (
} ,
< >
ref
{ ! config . featureToggles . topnav ? (
< PageToolbar title = { ` ${ dashboard . title } / Settings ` } parent = { folderTitle } onGoBack = { onClose } >
{ actions }
< / PageToolbar >
) : (
< AppChromeUpdate actions = { actions } / >
) }
< currentPage.component sectionNav = { subSectionNav } dashboard = { dashboard } editIndex = { editIndex } / >
< / >
) ;
) ;
const forceUpdate = useForceUpdate ( ) ;
}
const onMakeEditable = useCallback ( ( ) = > {
dashboard . editable = true ;
dashboard . meta . canMakeEditable = false ;
dashboard . meta . canEdit = true ;
dashboard . meta . canSave = true ;
forceUpdate ( ) ;
} , [ dashboard , forceUpdate ] ) ;
const pages = useMemo ( ( ) : SettingsPage [ ] = > {
function getSettingsPages ( dashboard : DashboardModel ) {
const pages : SettingsPage [ ] = [ ] ;
const pages : SettingsPage [ ] = [ ] ;
if ( dashboard . meta . canEdit ) {
if ( dashboard . meta . canEdit ) {
pages . push ( {
pages . push ( {
title : 'General' ,
title : 'General' ,
id : 'settings' ,
id : 'settings' ,
icon : 'sliders-v-alt' ,
icon : 'sliders-v-alt' ,
component : < GeneralSettings dashboard = { dashboard } / > ,
component : GeneralSettings ,
} ) ;
} ) ;
pages . push ( {
pages . push ( {
title : 'Annotations' ,
title : 'Annotations' ,
id : 'annotations' ,
id : 'annotations' ,
icon : 'comment-alt' ,
icon : 'comment-alt' ,
component : < AnnotationsSettings dashboard = { dashboard } / > ,
component : AnnotationsSettings ,
} ) ;
subTitle :
'Annotation queries return events that can be visualized as event markers in graphs across the dashboard.' ,
} ) ;
pages . push ( {
pages . push ( {
title : 'Variables' ,
title : 'Variables' ,
id : 'templating' ,
id : 'templating' ,
icon : 'calculator-alt' ,
icon : 'calculator-alt' ,
component : < VariableEditorContainer dashboard = { dashboard } / > ,
component : VariableEditorContainer ,
} ) ;
subTitle : 'Variables can make your dashboard more dynamic and act as global filters.' ,
} ) ;
pages . push ( {
pages . push ( {
title : 'Links' ,
title : 'Links' ,
id : 'links' ,
id : 'links' ,
icon : 'link' ,
icon : 'link' ,
component : < LinksSettings dashboard = { dashboard } / > ,
component : LinksSettings ,
} ) ;
} ) ;
}
}
if ( dashboard . meta . canMakeEditable ) {
pages . push ( {
title : 'General' ,
icon : 'sliders-v-alt' ,
id : 'settings' ,
component : MakeEditable ,
} ) ;
}
if ( dashboard . meta . canMakeEditable ) {
if ( dashboard . id && dashboard . meta . canSave ) {
pages . push ( {
title : 'Versions' ,
id : 'versions' ,
icon : 'history' ,
component : VersionsSettings ,
} ) ;
}
if ( dashboard . id && dashboard . meta . canAdmin ) {
if ( ! config . rbacEnabled ) {
pages . push ( {
pages . push ( {
title : 'General' ,
title : 'Permissions ' ,
icon : 'sliders-v-alt' ,
id : 'permissions ' ,
id : 'settings' ,
icon : 'lock ' ,
component : < MakeEditable onMakeEditable = { onMakeEditable } / > ,
component : DashboardPermissions ,
} ) ;
} ) ;
}
} else if ( contextSrv . hasPermission ( AccessControlAction . DashboardsPermissionsRead ) ) {
if ( dashboard . id && dashboard . meta . canSave ) {
pages . push ( {
pages . push ( {
title : 'Ver sions' ,
title : 'Permissions' ,
id : 'ver sions' ,
id : 'permis sions' ,
icon : 'history ' ,
icon : 'lock ' ,
component : < VersionsSettings dashboard = { dashboard } / > ,
component : AccessControlDashboardPermissions ,
} ) ;
} ) ;
}
}
}
if ( dashboard . id && dashboard . meta . canAdmin ) {
pages . push ( {
if ( ! config . rbacEnabled ) {
title : 'JSON Model' ,
pages . push ( {
id : 'dashboard_json' ,
title : 'Permissions' ,
icon : 'arrow' ,
id : 'permissions' ,
component : JsonEditorSettings ,
icon : 'lock' ,
} ) ;
component : < DashboardPermissions dashboard = { dashboard } / > ,
} ) ;
} else if ( contextSrv . hasPermission ( AccessControlAction . DashboardsPermissionsRead ) ) {
pages . push ( {
title : 'Permissions' ,
id : 'permissions' ,
icon : 'lock' ,
component : < AccessControlDashboardPermissions dashboard = { dashboard } / > ,
} ) ;
}
}
pages . push ( {
return pages ;
title : 'JSON Model' ,
}
id : 'dashboard_json' ,
icon : 'arrow' ,
component : < JsonEditorSettings dashboard = { dashboard } / > ,
} ) ;
return pages ;
function getSectionNav (
} , [ dashboard , onMakeEditable ] ) ;
pageNav : NavModelItem ,
sectionNav : NavModel ,
pages : SettingsPage [ ] ,
currentPage : SettingsPage ,
location : H.Location
) : NavModel {
const main : NavModelItem = {
text : 'Settings' ,
children : [ ] ,
icon : 'apps' ,
hideFromBreadcrumbs : true ,
} ;
const onPostSave = ( ) = > {
main . children = pages . map ( ( page ) = > ( {
dashboard . meta . hasUnsavedFolderChange = false ;
text : page.title ,
icon : page.icon ,
id : page.id ,
url : locationUtil.getUrlForPartial ( location , { editview : page.id , editIndex : null } ) ,
active : page === currentPage ,
parentItem : main ,
subTitle : page.subTitle ,
} ) ) ;
if ( pageNav . parentItem ) {
pageNav = {
. . . pageNav ,
parentItem : {
. . . pageNav . parentItem ,
parentItem : sectionNav.node ,
} ,
} ;
} else {
pageNav = {
. . . pageNav ,
parentItem : sectionNav.node ,
} ;
}
main . parentItem = pageNav ;
return {
main ,
node : main.children.find ( ( x ) = > x . active ) ! ,
} ;
} ;
}
const folderTitle = dashboard . meta . folderTitle ;
function MakeEditable ( { dashboard } : SettingsPageProps ) {
const currentPage = pages . find ( ( page ) = > page . id === editview ) ? ? pages [ 0 ] ;
const onMakeEditable = ( ) = > {
const canSaveAs = contextSrv . hasEditPermissionInFolders ;
dashboard . editable = true ;
const canSave = dashboard . meta . canSave ;
dashboard . meta . canMakeEditable = false ;
const styles = getStyles ( config . theme2 ) ;
dashboard . meta . canEdit = true ;
dashboard . meta . canSave = true ;
// TODO add some kind of reload
} ;
return (
return (
< FocusScope contain autoFocus >
< div >
< div className = "dashboard-settings" ref = { ref } { ...overlayProps } { ...dialogProps } >
< div className = "dashboard-settings__header" > Dashboard not editable < / div >
< PageToolbar
< Button type = "submit" onClick = { onMakeEditable } >
className = { styles . toolbar }
Make editable
title = { dashboard . title }
< / Button >
section = "Settings"
< / div >
parent = { folderTitle }
onGoBack = { onClose }
/ >
< CustomScrollbar >
< div className = { styles . scrollInner } >
< div className = { styles . settingsWrapper } >
< aside className = "dashboard-settings__aside" >
{ pages . map ( ( page ) = > (
< Link
onClick = { ( ) = > reportInteraction ( ` Dashboard settings navigation to ${ page . id } ` ) }
to = { ( loc ) = > locationUtil . getUrlForPartial ( loc , { editview : page.id } ) }
className = { cx ( 'dashboard-settings__nav-item' , { active : page.id === editview } ) }
key = { page . id }
>
< Icon name = { page . icon } style = { { marginRight : '4px' } } / >
{ page . title }
< / Link >
) ) }
< div className = "dashboard-settings__aside-actions" >
{ canSave && < SaveDashboardButton dashboard = { dashboard } onSaveSuccess = { onPostSave } / > }
{ canSaveAs && (
< SaveDashboardAsButton dashboard = { dashboard } onSaveSuccess = { onPostSave } variant = "secondary" / >
) }
< / div >
< / aside >
< div className = { styles . settingsContent } > { currentPage . component } < / div >
< / div >
< / div >
< / CustomScrollbar >
< / div >
< / FocusScope >
) ;
) ;
}
}
const getStyles = stylesFactory ( ( theme : GrafanaTheme2 ) = > ( {
function getEditIndex ( location : H.Location ) : number | undefined {
scrollInner : css `
const editIndex = new URLSearchParams ( location . search ) . get ( 'editIndex' ) ;
min - width : 100 % ;
if ( editIndex != null ) {
display : flex ;
return parseInt ( editIndex , 10 ) ;
` ,
}
toolbar : css `
return undefined ;
width : 60vw ;
}
min - width : min - content ;
` ,
settingsWrapper : css `
margin : $ { theme . spacing ( 0 , 2 , 2 ) } ;
display : flex ;
flex - grow : 1 ;
` ,
settingsContent : css `
flex - grow : 1 ;
height : 100 % ;
padding : 32px ;
border : 1px solid $ { theme . colors . border . weak } ;
background : $ { theme . colors . background . primary } ;
border - radius : $ { theme . shape . borderRadius ( ) } ;
` ,
} ) ) ;