1
1
import { css , cx } from '@emotion/css' ;
2
2
import { isEqual } from 'lodash' ;
3
- import { Link } from 'react-router-dom' ;
4
3
5
- import { GrafanaTheme2 , urlUtil } from '@grafana/data' ;
4
+ import { GrafanaTheme2 , ScopeDashboardBinding } from '@grafana/data' ;
6
5
import { SceneComponentProps , SceneObjectBase , SceneObjectRef , SceneObjectState } from '@grafana/scenes' ;
7
- import { Button , CustomScrollbar , FilterInput , LoadingPlaceholder , useStyles2 } from '@grafana/ui' ;
8
- import { useQueryParams } from 'app/core/hooks/useQueryParams' ;
6
+ import { Button , CustomScrollbar , LoadingPlaceholder , useStyles2 } from '@grafana/ui' ;
9
7
import { t , Trans } from 'app/core/internationalization' ;
10
8
9
+ import { ScopesDashboardsTree } from './ScopesDashboardsTree' ;
10
+ import { ScopesDashboardsTreeSearch } from './ScopesDashboardsTreeSearch' ;
11
11
import { ScopesSelectorScene } from './ScopesSelectorScene' ;
12
- import { fetchSuggestedDashboards } from './api' ;
12
+ import { fetchDashboards } from './api' ;
13
13
import { DASHBOARDS_OPENED_KEY } from './const' ;
14
- import { SuggestedDashboard } from './types' ;
15
- import { getScopeNamesFromSelectedScopes } from './utils' ;
14
+ import { SuggestedDashboardsFoldersMap } from './types' ;
15
+ import { filterFolders , getScopeNamesFromSelectedScopes , groupDashboards } from './utils' ;
16
16
17
17
export interface ScopesDashboardsSceneState extends SceneObjectState {
18
18
selector : SceneObjectRef < ScopesSelectorScene > | null ;
19
- dashboards : SuggestedDashboard [ ] ;
20
- filteredDashboards : SuggestedDashboard [ ] ;
19
+ // by keeping a track of the raw response, it's much easier to check if we got any dashboards for the currently selected scopes
20
+ dashboards : ScopeDashboardBinding [ ] ;
21
+ // this is a grouping in folders of the `dashboards` property. it is used for filtering the dashboards and folders when the search query changes
22
+ folders : SuggestedDashboardsFoldersMap ;
23
+ // a filtered version of the `folders` property. this prevents a lot of unnecessary parsings in React renders
24
+ filteredFolders : SuggestedDashboardsFoldersMap ;
21
25
forScopeNames : string [ ] ;
22
26
isLoading : boolean ;
23
27
isPanelOpened : boolean ;
@@ -28,7 +32,8 @@ export interface ScopesDashboardsSceneState extends SceneObjectState {
28
32
29
33
export const getInitialDashboardsState : ( ) => Omit < ScopesDashboardsSceneState , 'selector' > = ( ) => ( {
30
34
dashboards : [ ] ,
31
- filteredDashboards : [ ] ,
35
+ folders : { } ,
36
+ filteredFolders : { } ,
32
37
forScopeNames : [ ] ,
33
38
isLoading : false ,
34
39
isPanelOpened : localStorage . getItem ( DASHBOARDS_OPENED_KEY ) === 'true' ,
@@ -80,7 +85,8 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
80
85
if ( scopeNames . length === 0 ) {
81
86
return this . setState ( {
82
87
dashboards : [ ] ,
83
- filteredDashboards : [ ] ,
88
+ folders : { } ,
89
+ filteredFolders : { } ,
84
90
forScopeNames : [ ] ,
85
91
isLoading : false ,
86
92
scopesSelected : false ,
@@ -89,26 +95,50 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
89
95
90
96
this . setState ( { isLoading : true } ) ;
91
97
92
- const dashboards = await fetchSuggestedDashboards ( scopeNames ) ;
98
+ const dashboards = await fetchDashboards ( scopeNames ) ;
99
+ const folders = groupDashboards ( dashboards ) ;
100
+ const filteredFolders = filterFolders ( folders , this . state . searchQuery ) ;
93
101
94
102
this . setState ( {
95
103
dashboards,
96
- filteredDashboards : this . filterDashboards ( dashboards , this . state . searchQuery ) ,
104
+ folders,
105
+ filteredFolders,
97
106
forScopeNames : scopeNames ,
98
107
isLoading : false ,
99
108
scopesSelected : scopeNames . length > 0 ,
100
109
} ) ;
101
110
}
102
111
103
112
public changeSearchQuery ( searchQuery : string ) {
113
+ searchQuery = searchQuery ?? '' ;
114
+
104
115
this . setState ( {
105
- filteredDashboards : searchQuery
106
- ? this . filterDashboards ( this . state . dashboards , searchQuery )
107
- : this . state . dashboards ,
108
- searchQuery : searchQuery ?? '' ,
116
+ filteredFolders : filterFolders ( this . state . folders , searchQuery ) ,
117
+ searchQuery,
109
118
} ) ;
110
119
}
111
120
121
+ public updateFolder ( path : string [ ] , isExpanded : boolean ) {
122
+ let folders = { ...this . state . folders } ;
123
+ let filteredFolders = { ...this . state . filteredFolders } ;
124
+ let currentLevelFolders : SuggestedDashboardsFoldersMap = folders ;
125
+ let currentLevelFilteredFolders : SuggestedDashboardsFoldersMap = filteredFolders ;
126
+
127
+ for ( let idx = 0 ; idx < path . length - 1 ; idx ++ ) {
128
+ currentLevelFolders = currentLevelFolders [ path [ idx ] ] . folders ;
129
+ currentLevelFilteredFolders = currentLevelFilteredFolders [ path [ idx ] ] . folders ;
130
+ }
131
+
132
+ const name = path [ path . length - 1 ] ;
133
+ const currentFolder = currentLevelFolders [ name ] ;
134
+ const currentFilteredFolder = currentLevelFilteredFolders [ name ] ;
135
+
136
+ currentFolder . isExpanded = isExpanded ;
137
+ currentFilteredFolder . isExpanded = isExpanded ;
138
+
139
+ this . setState ( { folders, filteredFolders } ) ;
140
+ }
141
+
112
142
public togglePanel ( ) {
113
143
if ( this . state . isPanelOpened ) {
114
144
this . closePanel ( ) ;
@@ -135,20 +165,13 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
135
165
public disable ( ) {
136
166
this . setState ( { isEnabled : false } ) ;
137
167
}
138
-
139
- private filterDashboards ( dashboards : SuggestedDashboard [ ] , searchQuery : string ) : SuggestedDashboard [ ] {
140
- const lowerCasedSearchQuery = searchQuery . toLowerCase ( ) ;
141
-
142
- return dashboards . filter ( ( { dashboardTitle } ) => dashboardTitle . toLowerCase ( ) . includes ( lowerCasedSearchQuery ) ) ;
143
- }
144
168
}
145
169
146
170
export function ScopesDashboardsSceneRenderer ( { model } : SceneComponentProps < ScopesDashboardsScene > ) {
147
- const { dashboards, filteredDashboards , isLoading, isPanelOpened, isEnabled, searchQuery, scopesSelected } =
171
+ const { dashboards, filteredFolders , isLoading, isPanelOpened, isEnabled, searchQuery, scopesSelected } =
148
172
model . useState ( ) ;
149
- const styles = useStyles2 ( getStyles ) ;
150
173
151
- const [ queryParams ] = useQueryParams ( ) ;
174
+ const styles = useStyles2 ( getStyles ) ;
152
175
153
176
if ( ! isEnabled || ! isPanelOpened ) {
154
177
return null ;
@@ -178,34 +201,25 @@ export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps<Sco
178
201
179
202
return (
180
203
< div className = { styles . container } data-testid = "scopes-dashboards-container" >
181
- < div className = { styles . searchInputContainer } >
182
- < FilterInput
183
- disabled = { isLoading }
184
- placeholder = { t ( 'scopes.dashboards.search' , 'Search' ) }
185
- value = { searchQuery }
186
- data-testid = "scopes-dashboards-search"
187
- onChange = { ( value ) => model . changeSearchQuery ( value ) }
188
- />
189
- </ div >
204
+ < ScopesDashboardsTreeSearch
205
+ disabled = { isLoading }
206
+ query = { searchQuery }
207
+ onChange = { ( value ) => model . changeSearchQuery ( value ) }
208
+ />
190
209
191
210
{ isLoading ? (
192
211
< LoadingPlaceholder
193
212
className = { styles . loadingIndicator }
194
213
text = { t ( 'scopes.dashboards.loading' , 'Loading dashboards' ) }
195
214
data-testid = "scopes-dashboards-loading"
196
215
/>
197
- ) : filteredDashboards . length > 0 ? (
216
+ ) : filteredFolders [ '' ] ? (
198
217
< CustomScrollbar >
199
- { filteredDashboards . map ( ( { dashboard, dashboardTitle } ) => (
200
- < Link
201
- key = { dashboard }
202
- to = { urlUtil . renderUrl ( `/d/${ dashboard } /` , queryParams ) }
203
- className = { styles . dashboardItem }
204
- data-testid = { `scopes-dashboards-${ dashboard } ` }
205
- >
206
- { dashboardTitle }
207
- </ Link >
208
- ) ) }
218
+ < ScopesDashboardsTree
219
+ folders = { filteredFolders }
220
+ folderPath = { [ '' ] }
221
+ onFolderUpdate = { ( path , isExpanded ) => model . updateFolder ( path , isExpanded ) }
222
+ />
209
223
</ CustomScrollbar >
210
224
) : (
211
225
< p className = { styles . noResultsContainer } data-testid = "scopes-dashboards-notFoundForFilter" >
@@ -246,19 +260,8 @@ const getStyles = (theme: GrafanaTheme2) => {
246
260
margin : 0 ,
247
261
textAlign : 'center' ,
248
262
} ) ,
249
- searchInputContainer : css ( {
250
- flex : '0 1 auto' ,
251
- } ) ,
252
263
loadingIndicator : css ( {
253
264
alignSelf : 'center' ,
254
265
} ) ,
255
- dashboardItem : css ( {
256
- padding : theme . spacing ( 1 , 0 ) ,
257
- borderBottom : `1px solid ${ theme . colors . border . weak } ` ,
258
-
259
- '& :is(:first-child)' : {
260
- paddingTop : 0 ,
261
- } ,
262
- } ) ,
263
266
} ;
264
267
} ;
0 commit comments