diff --git a/public/app/features/browse-dashboards/components/BrowseView.tsx b/public/app/features/browse-dashboards/components/BrowseView.tsx
index 021e02e5e83..8afd4cb9d51 100644
--- a/public/app/features/browse-dashboards/components/BrowseView.tsx
+++ b/public/app/features/browse-dashboards/components/BrowseView.tsx
@@ -1,5 +1,7 @@
import React, { useCallback, useEffect } from 'react';
+import { Spinner } from '@grafana/ui';
+import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { DashboardViewItem } from 'app/features/search/types';
import { useDispatch } from 'app/types';
@@ -11,6 +13,7 @@ import {
setItemSelectionState,
useChildrenByParentUIDState,
setAllSelection,
+ useBrowseLoadingStatus,
} from '../state';
import { DashboardTreeSelection, SelectionState } from '../types';
@@ -24,6 +27,7 @@ interface BrowseViewProps {
}
export function BrowseView({ folderUID, width, height, canSelect }: BrowseViewProps) {
+ const status = useBrowseLoadingStatus(folderUID);
const dispatch = useDispatch();
const flatTree = useFlatTreeState(folderUID);
const selectedItems = useCheckboxSelectionState();
@@ -95,6 +99,27 @@ export function BrowseView({ folderUID, width, height, canSelect }: BrowseViewPr
[selectedItems, childrenByParentUID]
);
+ if (status === 'pending') {
+ return ;
+ }
+
+ if (status === 'fulfilled' && flatTree.length === 0) {
+ return (
+
+ '}
+ proTipLink={folderUID && 'dashboards'}
+ proTipLinkTitle={folderUID && 'Browse dashboards'}
+ proTipTarget=""
+ />
+
+ );
+ }
+
return (
No search results;
+ return (
+
+
+ No results found for your query.
+
+
+
+
+
+ );
}
const props: SearchResultsProps = {
diff --git a/public/app/features/browse-dashboards/state/hooks.test.ts b/public/app/features/browse-dashboards/state/hooks.test.ts
new file mode 100644
index 00000000000..87d302558d9
--- /dev/null
+++ b/public/app/features/browse-dashboards/state/hooks.test.ts
@@ -0,0 +1,79 @@
+import { configureStore } from 'app/store/configureStore';
+import { useSelector } from 'app/types';
+
+import { BrowseDashboardsState } from '../types';
+
+import { useBrowseLoadingStatus } from './hooks';
+
+jest.mock('app/types', () => {
+ const original = jest.requireActual('app/types');
+ return {
+ ...original,
+ useSelector: jest.fn(),
+ };
+});
+
+function createInitialState(partial: Partial): BrowseDashboardsState {
+ return {
+ rootItems: undefined,
+ childrenByParentUID: {},
+ openFolders: {},
+ selectedItems: {
+ $all: false,
+ dashboard: {},
+ folder: {},
+ panel: {},
+ },
+
+ ...partial,
+ };
+}
+
+describe('browse-dashboards state hooks', () => {
+ const folderUID = 'abc-123';
+
+ function mockState(browseState: BrowseDashboardsState) {
+ const wholeState = configureStore().getState();
+ wholeState.browseDashboards = browseState;
+
+ jest.mocked(useSelector).mockImplementationOnce((callback) => {
+ return callback(wholeState);
+ });
+ }
+
+ describe('useBrowseLoadingStatus', () => {
+ it('returns loading when root view is loading', () => {
+ mockState(createInitialState({ rootItems: undefined }));
+
+ const status = useBrowseLoadingStatus(undefined);
+ expect(status).toEqual('pending');
+ });
+
+ it('returns loading when folder view is loading', () => {
+ mockState(createInitialState({ childrenByParentUID: {} }));
+
+ const status = useBrowseLoadingStatus(folderUID);
+ expect(status).toEqual('pending');
+ });
+
+ it('returns fulfilled when root view is finished loading', () => {
+ mockState(createInitialState({ rootItems: [] }));
+
+ const status = useBrowseLoadingStatus(undefined);
+ expect(status).toEqual('fulfilled');
+ });
+
+ it('returns fulfilled when folder view is finished loading', () => {
+ mockState(
+ createInitialState({
+ childrenByParentUID: {
+ [folderUID]: [],
+ },
+ })
+ );
+
+ const status = useBrowseLoadingStatus(folderUID);
+ expect(status).toEqual('fulfilled');
+ });
+ });
+});
diff --git a/public/app/features/browse-dashboards/state/hooks.ts b/public/app/features/browse-dashboards/state/hooks.ts
index 097ef3ebb7e..1523ea2ec91 100644
--- a/public/app/features/browse-dashboards/state/hooks.ts
+++ b/public/app/features/browse-dashboards/state/hooks.ts
@@ -11,7 +11,7 @@ const flatTreeSelector = createSelector(
(wholeState: StoreState) => wholeState.browseDashboards.openFolders,
(wholeState: StoreState, rootFolderUID: string | undefined) => rootFolderUID,
(rootItems, childrenByParentUID, openFolders, folderUID) => {
- return createFlatTree(folderUID, rootItems, childrenByParentUID, openFolders);
+ return createFlatTree(folderUID, rootItems ?? [], childrenByParentUID, openFolders);
}
);
@@ -32,6 +32,16 @@ const selectedItemsForActionsSelector = createSelector(
}
);
+export function useBrowseLoadingStatus(folderUID: string | undefined): 'pending' | 'fulfilled' {
+ return useSelector((wholeState) => {
+ const children = folderUID
+ ? wholeState.browseDashboards.childrenByParentUID[folderUID]
+ : wholeState.browseDashboards.rootItems;
+
+ return children ? 'fulfilled' : 'pending';
+ });
+}
+
export function useFlatTreeState(folderUID: string | undefined) {
return useSelector((state) => flatTreeSelector(state, folderUID));
}
diff --git a/public/app/features/browse-dashboards/state/reducers.ts b/public/app/features/browse-dashboards/state/reducers.ts
index 86414f423ec..1fcd31ff6df 100644
--- a/public/app/features/browse-dashboards/state/reducers.ts
+++ b/public/app/features/browse-dashboards/state/reducers.ts
@@ -68,7 +68,7 @@ export function setItemSelectionState(
let nextParentUID = item.parentUID;
while (nextParentUID) {
- const parent = findItem(state.rootItems, state.childrenByParentUID, nextParentUID);
+ const parent = findItem(state.rootItems ?? [], state.childrenByParentUID, nextParentUID);
// This case should not happen, but a find can theortically return undefined, and it
// helps limit infinite loops
@@ -92,7 +92,7 @@ export function setItemSelectionState(
}
// Check to see if we should mark the header checkbox selected if all root items are selected
- state.selectedItems.$all = state.rootItems.every((v) => state.selectedItems[v.kind][v.uid]) ?? false;
+ state.selectedItems.$all = state.rootItems?.every((v) => state.selectedItems[v.kind][v.uid]) ?? false;
}
export function setAllSelection(state: BrowseDashboardsState, action: PayloadAction<{ isSelected: boolean }>) {
@@ -115,7 +115,7 @@ export function setAllSelection(state: BrowseDashboardsState, action: PayloadAct
}
}
- for (const child of state.rootItems) {
+ for (const child of state.rootItems ?? []) {
state.selectedItems[child.kind][child.uid] = isSelected;
}
} else {
diff --git a/public/app/features/browse-dashboards/state/slice.ts b/public/app/features/browse-dashboards/state/slice.ts
index 39db1edbdb9..3f8f0db9862 100644
--- a/public/app/features/browse-dashboards/state/slice.ts
+++ b/public/app/features/browse-dashboards/state/slice.ts
@@ -8,7 +8,7 @@ import * as allReducers from './reducers';
const { extraReducerFetchChildrenFulfilled, ...baseReducers } = allReducers;
const initialState: BrowseDashboardsState = {
- rootItems: [],
+ rootItems: undefined,
childrenByParentUID: {},
openFolders: {},
selectedItems: {
diff --git a/public/app/features/browse-dashboards/types.ts b/public/app/features/browse-dashboards/types.ts
index aa97eb5d737..f9681e83872 100644
--- a/public/app/features/browse-dashboards/types.ts
+++ b/public/app/features/browse-dashboards/types.ts
@@ -7,7 +7,7 @@ export type DashboardTreeSelection = Record;
selectedItems: DashboardTreeSelection;
diff --git a/public/app/features/search/state/SearchStateManager.ts b/public/app/features/search/state/SearchStateManager.ts
index 104511ad2e0..211d0742884 100644
--- a/public/app/features/search/state/SearchStateManager.ts
+++ b/public/app/features/search/state/SearchStateManager.ts
@@ -95,6 +95,7 @@ export class SearchStateManager extends StateManagerBase {
tag: [],
panel_type: undefined,
starred: undefined,
+ sort: undefined,
});
};