Skip to content

Commit fd2c759

Browse files
authored
NestedFolders: Add empty states for Browse and Search (grafana#67423)
* NestedFolders: Add empty states for Browse and Search * empty states * fix types * tests
1 parent 32d3e89 commit fd2c759

File tree

8 files changed

+134
-8
lines changed

8 files changed

+134
-8
lines changed

public/app/features/browse-dashboards/components/BrowseView.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React, { useCallback, useEffect } from 'react';
22

3+
import { Spinner } from '@grafana/ui';
4+
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
35
import { DashboardViewItem } from 'app/features/search/types';
46
import { useDispatch } from 'app/types';
57

@@ -11,6 +13,7 @@ import {
1113
setItemSelectionState,
1214
useChildrenByParentUIDState,
1315
setAllSelection,
16+
useBrowseLoadingStatus,
1417
} from '../state';
1518
import { DashboardTreeSelection, SelectionState } from '../types';
1619

@@ -24,6 +27,7 @@ interface BrowseViewProps {
2427
}
2528

2629
export function BrowseView({ folderUID, width, height, canSelect }: BrowseViewProps) {
30+
const status = useBrowseLoadingStatus(folderUID);
2731
const dispatch = useDispatch();
2832
const flatTree = useFlatTreeState(folderUID);
2933
const selectedItems = useCheckboxSelectionState();
@@ -95,6 +99,27 @@ export function BrowseView({ folderUID, width, height, canSelect }: BrowseViewPr
9599
[selectedItems, childrenByParentUID]
96100
);
97101

102+
if (status === 'pending') {
103+
return <Spinner />;
104+
}
105+
106+
if (status === 'fulfilled' && flatTree.length === 0) {
107+
return (
108+
<div style={{ width }}>
109+
<EmptyListCTA
110+
title={folderUID ? "This folder doesn't have any dashboards yet" : 'No dashboards yet. Create your first!'}
111+
buttonIcon="plus"
112+
buttonTitle="Create Dashboard"
113+
buttonLink={folderUID ? `dashboard/new?folderUid=${folderUID}` : 'dashboard/new'}
114+
proTip={folderUID && 'Add/move dashboards to your folder at ->'}
115+
proTipLink={folderUID && 'dashboards'}
116+
proTipLinkTitle={folderUID && 'Browse dashboards'}
117+
proTipTarget=""
118+
/>
119+
</div>
120+
);
121+
}
122+
98123
return (
99124
<DashboardsTree
100125
canSelect={canSelect}

public/app/features/browse-dashboards/components/SearchView.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useCallback } from 'react';
22

3-
import { Spinner } from '@grafana/ui';
3+
import { Button, Card, Spinner } from '@grafana/ui';
44
import { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKeyboardSelection';
55
import { SearchResultsProps, SearchResultsTable } from 'app/features/search/page/components/SearchResultsTable';
66
import { useSearchStateManager } from 'app/features/search/state/SearchStateManager';
@@ -69,7 +69,18 @@ export function SearchView({ width, height, canSelect }: SearchViewProps) {
6969
}
7070

7171
if (value.totalRows === 0) {
72-
return <div style={{ width }}>No search results</div>;
72+
return (
73+
<div style={{ width }}>
74+
<Card>
75+
<Card.Heading>No results found for your query.</Card.Heading>
76+
<Card.Actions>
77+
<Button variant="secondary" onClick={stateManager.onClearSearchAndFilters}>
78+
Clear search and filters
79+
</Button>
80+
</Card.Actions>
81+
</Card>
82+
</div>
83+
);
7384
}
7485

7586
const props: SearchResultsProps = {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { configureStore } from 'app/store/configureStore';
2+
import { useSelector } from 'app/types';
3+
4+
import { BrowseDashboardsState } from '../types';
5+
6+
import { useBrowseLoadingStatus } from './hooks';
7+
8+
jest.mock('app/types', () => {
9+
const original = jest.requireActual('app/types');
10+
return {
11+
...original,
12+
useSelector: jest.fn(),
13+
};
14+
});
15+
16+
function createInitialState(partial: Partial<BrowseDashboardsState>): BrowseDashboardsState {
17+
return {
18+
rootItems: undefined,
19+
childrenByParentUID: {},
20+
openFolders: {},
21+
selectedItems: {
22+
$all: false,
23+
dashboard: {},
24+
folder: {},
25+
panel: {},
26+
},
27+
28+
...partial,
29+
};
30+
}
31+
32+
describe('browse-dashboards state hooks', () => {
33+
const folderUID = 'abc-123';
34+
35+
function mockState(browseState: BrowseDashboardsState) {
36+
const wholeState = configureStore().getState();
37+
wholeState.browseDashboards = browseState;
38+
39+
jest.mocked(useSelector).mockImplementationOnce((callback) => {
40+
return callback(wholeState);
41+
});
42+
}
43+
44+
describe('useBrowseLoadingStatus', () => {
45+
it('returns loading when root view is loading', () => {
46+
mockState(createInitialState({ rootItems: undefined }));
47+
48+
const status = useBrowseLoadingStatus(undefined);
49+
expect(status).toEqual('pending');
50+
});
51+
52+
it('returns loading when folder view is loading', () => {
53+
mockState(createInitialState({ childrenByParentUID: {} }));
54+
55+
const status = useBrowseLoadingStatus(folderUID);
56+
expect(status).toEqual('pending');
57+
});
58+
59+
it('returns fulfilled when root view is finished loading', () => {
60+
mockState(createInitialState({ rootItems: [] }));
61+
62+
const status = useBrowseLoadingStatus(undefined);
63+
expect(status).toEqual('fulfilled');
64+
});
65+
66+
it('returns fulfilled when folder view is finished loading', () => {
67+
mockState(
68+
createInitialState({
69+
childrenByParentUID: {
70+
[folderUID]: [],
71+
},
72+
})
73+
);
74+
75+
const status = useBrowseLoadingStatus(folderUID);
76+
expect(status).toEqual('fulfilled');
77+
});
78+
});
79+
});

public/app/features/browse-dashboards/state/hooks.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const flatTreeSelector = createSelector(
1111
(wholeState: StoreState) => wholeState.browseDashboards.openFolders,
1212
(wholeState: StoreState, rootFolderUID: string | undefined) => rootFolderUID,
1313
(rootItems, childrenByParentUID, openFolders, folderUID) => {
14-
return createFlatTree(folderUID, rootItems, childrenByParentUID, openFolders);
14+
return createFlatTree(folderUID, rootItems ?? [], childrenByParentUID, openFolders);
1515
}
1616
);
1717

@@ -32,6 +32,16 @@ const selectedItemsForActionsSelector = createSelector(
3232
}
3333
);
3434

35+
export function useBrowseLoadingStatus(folderUID: string | undefined): 'pending' | 'fulfilled' {
36+
return useSelector((wholeState) => {
37+
const children = folderUID
38+
? wholeState.browseDashboards.childrenByParentUID[folderUID]
39+
: wholeState.browseDashboards.rootItems;
40+
41+
return children ? 'fulfilled' : 'pending';
42+
});
43+
}
44+
3545
export function useFlatTreeState(folderUID: string | undefined) {
3646
return useSelector((state) => flatTreeSelector(state, folderUID));
3747
}

public/app/features/browse-dashboards/state/reducers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function setItemSelectionState(
6868
let nextParentUID = item.parentUID;
6969

7070
while (nextParentUID) {
71-
const parent = findItem(state.rootItems, state.childrenByParentUID, nextParentUID);
71+
const parent = findItem(state.rootItems ?? [], state.childrenByParentUID, nextParentUID);
7272

7373
// This case should not happen, but a find can theortically return undefined, and it
7474
// helps limit infinite loops
@@ -92,7 +92,7 @@ export function setItemSelectionState(
9292
}
9393

9494
// Check to see if we should mark the header checkbox selected if all root items are selected
95-
state.selectedItems.$all = state.rootItems.every((v) => state.selectedItems[v.kind][v.uid]) ?? false;
95+
state.selectedItems.$all = state.rootItems?.every((v) => state.selectedItems[v.kind][v.uid]) ?? false;
9696
}
9797

9898
export function setAllSelection(state: BrowseDashboardsState, action: PayloadAction<{ isSelected: boolean }>) {
@@ -115,7 +115,7 @@ export function setAllSelection(state: BrowseDashboardsState, action: PayloadAct
115115
}
116116
}
117117

118-
for (const child of state.rootItems) {
118+
for (const child of state.rootItems ?? []) {
119119
state.selectedItems[child.kind][child.uid] = isSelected;
120120
}
121121
} else {

public/app/features/browse-dashboards/state/slice.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as allReducers from './reducers';
88
const { extraReducerFetchChildrenFulfilled, ...baseReducers } = allReducers;
99

1010
const initialState: BrowseDashboardsState = {
11-
rootItems: [],
11+
rootItems: undefined,
1212
childrenByParentUID: {},
1313
openFolders: {},
1414
selectedItems: {

public/app/features/browse-dashboards/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export type DashboardTreeSelection = Record<DashboardViewItemKind, Record<string
77
};
88

99
export interface BrowseDashboardsState {
10-
rootItems: DashboardViewItem[];
10+
rootItems: DashboardViewItem[] | undefined;
1111
childrenByParentUID: Record<string, DashboardViewItem[] | undefined>;
1212
selectedItems: DashboardTreeSelection;
1313

public/app/features/search/state/SearchStateManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export class SearchStateManager extends StateManagerBase<SearchState> {
9595
tag: [],
9696
panel_type: undefined,
9797
starred: undefined,
98+
sort: undefined,
9899
});
99100
};
100101

0 commit comments

Comments
 (0)