@ -1,17 +1,33 @@
import { BaseQueryFn , createApi } from '@reduxjs/toolkit/query/react' ;
import { lastValueFrom } from 'rxjs' ;
import { isTruthy } from '@grafana/data' ;
import { BackendSrvRequest , getBackendSrv } from '@grafana/runtime' ;
import { DescendantCount , DescendantCountDTO , FolderDTO } from 'app/types' ;
import { isTruthy , locationUtil } from '@grafana/data' ;
import { BackendSrvRequest , getBackendSrv , locationService } from '@grafana/runtime' ;
import { notifyApp } from 'app/core/actions' ;
import { createSuccessNotification } from 'app/core/copy/appNotification' ;
import { contextSrv } from 'app/core/core' ;
import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types' ;
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher' ;
import { DashboardDTO , DescendantCount , DescendantCountDTO , FolderDTO , SaveDashboardResponseDTO } from 'app/types' ;
import { refetchChildren , refreshParents } from '../state' ;
import { DashboardTreeSelection } from '../types' ;
import { PAGE_SIZE , ROOT_PAGE_SIZE } from './services' ;
interface RequestOptions extends BackendSrvRequest {
manageError ? : ( err : unknown ) = > { error : unknown } ;
showErrorAlert? : boolean ;
}
interface DeleteItemsArgs {
selectedItems : Omit < DashboardTreeSelection , ' panel ' | ' $ all ' > ;
}
interface MoveItemsArgs extends DeleteItemsArgs {
destinationUID : string ;
}
function createBackendSrvBaseQuery ( { baseURL } : { baseURL : string } ) : BaseQueryFn < RequestOptions > {
async function backendSrvBaseQuery ( requestOptions : RequestOptions ) {
try {
@ -36,22 +52,109 @@ export const browseDashboardsAPI = createApi({
reducerPath : 'browseDashboardsAPI' ,
baseQuery : createBackendSrvBaseQuery ( { baseURL : '/api' } ) ,
endpoints : ( builder ) = > ( {
// get folder info (e.g. title, parents) but *not* children
getFolder : builder.query < FolderDTO , string > ( {
providesTags : ( _result , _error , folderUID ) = > [ { type : 'getFolder' , id : folderUID } ] ,
query : ( folderUID ) = > ( { url : ` /folders/ ${ folderUID } ` , params : { accesscontrol : true } } ) ,
providesTags : ( _result , _error , arg ) = > [ { type : 'getFolder' , id : arg } ] ,
} ) ,
// create a new folder
newFolder : builder.mutation < FolderDTO , { title : string ; parentUid ? : string } > ( {
query : ( { title , parentUid } ) = > ( {
method : 'POST' ,
url : '/folders' ,
data : {
title ,
parentUid ,
} ,
} ) ,
onQueryStarted : ( { parentUid } , { queryFulfilled , dispatch } ) = > {
queryFulfilled . then ( async ( { data : folder } ) = > {
await contextSrv . fetchUserPermissions ( ) ;
dispatch ( notifyApp ( createSuccessNotification ( 'Folder created' ) ) ) ;
dispatch (
refetchChildren ( {
parentUID : parentUid ,
pageSize : parentUid ? PAGE_SIZE : ROOT_PAGE_SIZE ,
} )
) ;
locationService . push ( locationUtil . stripBaseFromUrl ( folder . url ) ) ;
} ) ;
} ,
} ) ,
// save an existing folder (e.g. rename)
saveFolder : builder.mutation < FolderDTO , FolderDTO > ( {
invalidatesTags : ( _result , _error , args ) = > [ { type : 'getFolder' , id : args.uid } ] ,
query : ( folder ) = > ( {
// because the getFolder calls contain the parents, renaming a parent/grandparent/etc needs to invalidate all child folders
// we could do something smart and recursively invalidate these child folders but it doesn't seem worth it
// instead let's just invalidate all the getFolder calls
invalidatesTags : [ 'getFolder' ] ,
query : ( { uid , title , version } ) = > ( {
method : 'PUT' ,
showErrorAlert : false ,
url : ` /folders/ ${ folder . uid } ` ,
url : ` /folders/ ${ uid } ` ,
data : {
title : folder.title ,
version : folder.version ,
title ,
version ,
} ,
} ) ,
onQueryStarted : ( { parentUid } , { queryFulfilled , dispatch } ) = > {
queryFulfilled . then ( ( ) = > {
dispatch (
refetchChildren ( {
parentUID : parentUid ,
pageSize : parentUid ? PAGE_SIZE : ROOT_PAGE_SIZE ,
} )
) ;
} ) ;
} ,
} ) ,
// move an *individual* folder. used in the folder actions menu.
moveFolder : builder.mutation < void , { folder : FolderDTO ; destinationUID : string } > ( {
invalidatesTags : [ 'getFolder' ] ,
query : ( { folder , destinationUID } ) = > ( {
url : ` /folders/ ${ folder . uid } /move ` ,
method : 'POST' ,
data : { parentUID : destinationUID } ,
} ) ,
onQueryStarted : ( { folder , destinationUID } , { queryFulfilled , dispatch } ) = > {
const { parentUid } = folder ;
queryFulfilled . then ( ( ) = > {
dispatch (
refetchChildren ( {
parentUID : parentUid ,
pageSize : parentUid ? PAGE_SIZE : ROOT_PAGE_SIZE ,
} )
) ;
dispatch (
refetchChildren ( {
parentUID : destinationUID ,
pageSize : destinationUID ? PAGE_SIZE : ROOT_PAGE_SIZE ,
} )
) ;
} ) ;
} ,
} ) ,
// delete an *individual* folder. used in the folder actions menu.
deleteFolder : builder.mutation < void , FolderDTO > ( {
query : ( { uid } ) = > ( {
url : ` /folders/ ${ uid } ` ,
method : 'DELETE' ,
params : {
// TODO: Once backend returns alert rule counts, set this back to true
// when this is merged https://github.com/grafana/grafana/pull/67259
forceDeleteRules : false ,
} ,
} ) ,
onQueryStarted : ( { parentUid } , { queryFulfilled , dispatch } ) = > {
queryFulfilled . then ( ( ) = > {
dispatch (
refetchChildren ( {
parentUID : parentUid ,
pageSize : parentUid ? PAGE_SIZE : ROOT_PAGE_SIZE ,
} )
) ;
} ) ;
} ,
} ) ,
// gets the descendant counts for a folder. used in the move/delete modals.
getAffectedItems : builder.query < DescendantCount , DashboardTreeSelection > ( {
queryFn : async ( selectedItems ) = > {
const folderUIDs = Object . keys ( selectedItems . folder ) . filter ( ( uid ) = > selectedItems . folder [ uid ] ) ;
@ -81,17 +184,132 @@ export const browseDashboardsAPI = createApi({
return { data : totalCounts } ;
} ,
} ) ,
moveFolder : builder.mutation < void , { folderUID : string ; destinationUID : string } > ( {
query : ( { folderUID , destinationUID } ) = > ( {
url : ` /folders/ ${ folderUID } /move ` ,
// move *multiple* items (folders and dashboards). used in the move modal.
moveItems : builder.mutation < void , MoveItemsArgs > ( {
invalidatesTags : [ 'getFolder' ] ,
queryFn : async ( { selectedItems , destinationUID } , _api , _extraOptions , baseQuery ) = > {
const selectedDashboards = Object . keys ( selectedItems . dashboard ) . filter ( ( uid ) = > selectedItems . dashboard [ uid ] ) ;
const selectedFolders = Object . keys ( selectedItems . folder ) . filter ( ( uid ) = > selectedItems . folder [ uid ] ) ;
// Move all the folders sequentially
// TODO error handling here
for ( const folderUID of selectedFolders ) {
await baseQuery ( {
url : ` /folders/ ${ folderUID } /move ` ,
method : 'POST' ,
data : { parentUID : destinationUID } ,
} ) ;
}
// Move all the dashboards sequentially
// TODO error handling here
for ( const dashboardUID of selectedDashboards ) {
const fullDash : DashboardDTO = await getBackendSrv ( ) . get ( ` /api/dashboards/uid/ ${ dashboardUID } ` ) ;
const options = {
dashboard : fullDash.dashboard ,
folderUid : destinationUID ,
overwrite : false ,
message : '' ,
} ;
await baseQuery ( {
url : ` /dashboards/db ` ,
method : 'POST' ,
data : options ,
} ) ;
}
return { data : undefined } ;
} ,
onQueryStarted : ( { destinationUID , selectedItems } , { queryFulfilled , dispatch } ) = > {
const selectedDashboards = Object . keys ( selectedItems . dashboard ) . filter ( ( uid ) = > selectedItems . dashboard [ uid ] ) ;
const selectedFolders = Object . keys ( selectedItems . folder ) . filter ( ( uid ) = > selectedItems . folder [ uid ] ) ;
queryFulfilled . then ( ( ) = > {
dispatch (
refetchChildren ( {
parentUID : destinationUID ,
pageSize : destinationUID ? PAGE_SIZE : ROOT_PAGE_SIZE ,
} )
) ;
dispatch ( refreshParents ( [ . . . selectedFolders , . . . selectedDashboards ] ) ) ;
} ) ;
} ,
} ) ,
// delete *multiple* items (folders and dashboards). used in the delete modal.
deleteItems : builder.mutation < void , DeleteItemsArgs > ( {
queryFn : async ( { selectedItems } , _api , _extraOptions , baseQuery ) = > {
const selectedDashboards = Object . keys ( selectedItems . dashboard ) . filter ( ( uid ) = > selectedItems . dashboard [ uid ] ) ;
const selectedFolders = Object . keys ( selectedItems . folder ) . filter ( ( uid ) = > selectedItems . folder [ uid ] ) ;
// Delete all the folders sequentially
// TODO error handling here
for ( const folderUID of selectedFolders ) {
await baseQuery ( {
url : ` /folders/ ${ folderUID } ` ,
method : 'DELETE' ,
params : {
// TODO: Once backend returns alert rule counts, set this back to true
// when this is merged https://github.com/grafana/grafana/pull/67259
forceDeleteRules : false ,
} ,
} ) ;
}
// Delete all the dashboards sequentially
// TODO error handling here
for ( const dashboardUID of selectedDashboards ) {
await baseQuery ( {
url : ` /dashboards/uid/ ${ dashboardUID } ` ,
method : 'DELETE' ,
} ) ;
}
return { data : undefined } ;
} ,
onQueryStarted : ( { selectedItems } , { queryFulfilled , dispatch } ) = > {
const selectedDashboards = Object . keys ( selectedItems . dashboard ) . filter ( ( uid ) = > selectedItems . dashboard [ uid ] ) ;
const selectedFolders = Object . keys ( selectedItems . folder ) . filter ( ( uid ) = > selectedItems . folder [ uid ] ) ;
queryFulfilled . then ( ( ) = > {
dispatch ( refreshParents ( [ . . . selectedFolders , . . . selectedDashboards ] ) ) ;
} ) ;
} ,
} ) ,
// save an existing dashboard
saveDashboard : builder.mutation < SaveDashboardResponseDTO , SaveDashboardCommand > ( {
query : ( { dashboard , folderUid , message , overwrite } ) = > ( {
url : ` /dashboards/db ` ,
method : 'POST' ,
data : { parentUID : destinationUID } ,
data : {
dashboard ,
folderUid ,
message : message ? ? '' ,
overwrite : Boolean ( overwrite ) ,
} ,
} ) ,
invalidatesTags : ( _result , _error , arg ) = > [ { type : 'getFolder' , id : arg.folderUID } ] ,
onQueryStarted : ( { folderUid } , { queryFulfilled , dispatch } ) = > {
dashboardWatcher . ignoreNextSave ( ) ;
queryFulfilled . then ( async ( ) = > {
await contextSrv . fetchUserPermissions ( ) ;
dispatch (
refetchChildren ( {
parentUID : folderUid ,
pageSize : folderUid ? PAGE_SIZE : ROOT_PAGE_SIZE ,
} )
) ;
} ) ;
} ,
} ) ,
} ) ,
} ) ;
export const { endpoints , useGetAffectedItemsQuery , useGetFolderQuery , useMoveFolderMutation , useSaveFolderMutation } =
browseDashboardsAPI ;
export const {
endpoints ,
useDeleteFolderMutation ,
useDeleteItemsMutation ,
useGetAffectedItemsQuery ,
useGetFolderQuery ,
useMoveFolderMutation ,
useMoveItemsMutation ,
useNewFolderMutation ,
useSaveDashboardMutation ,
useSaveFolderMutation ,
} = browseDashboardsAPI ;
export { skipToken } from '@reduxjs/toolkit/query/react' ;