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, }); };