mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
NestedFolders: Add empty states for Browse and Search (#67423)
* NestedFolders: Add empty states for Browse and Search * empty states * fix types * tests
This commit is contained in:
parent
32d3e895b3
commit
fd2c7594cf
@ -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 <Spinner />;
|
||||
}
|
||||
|
||||
if (status === 'fulfilled' && flatTree.length === 0) {
|
||||
return (
|
||||
<div style={{ width }}>
|
||||
<EmptyListCTA
|
||||
title={folderUID ? "This folder doesn't have any dashboards yet" : 'No dashboards yet. Create your first!'}
|
||||
buttonIcon="plus"
|
||||
buttonTitle="Create Dashboard"
|
||||
buttonLink={folderUID ? `dashboard/new?folderUid=${folderUID}` : 'dashboard/new'}
|
||||
proTip={folderUID && 'Add/move dashboards to your folder at ->'}
|
||||
proTipLink={folderUID && 'dashboards'}
|
||||
proTipLinkTitle={folderUID && 'Browse dashboards'}
|
||||
proTipTarget=""
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardsTree
|
||||
canSelect={canSelect}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { Spinner } from '@grafana/ui';
|
||||
import { Button, Card, Spinner } from '@grafana/ui';
|
||||
import { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKeyboardSelection';
|
||||
import { SearchResultsProps, SearchResultsTable } from 'app/features/search/page/components/SearchResultsTable';
|
||||
import { useSearchStateManager } from 'app/features/search/state/SearchStateManager';
|
||||
@ -69,7 +69,18 @@ export function SearchView({ width, height, canSelect }: SearchViewProps) {
|
||||
}
|
||||
|
||||
if (value.totalRows === 0) {
|
||||
return <div style={{ width }}>No search results</div>;
|
||||
return (
|
||||
<div style={{ width }}>
|
||||
<Card>
|
||||
<Card.Heading>No results found for your query.</Card.Heading>
|
||||
<Card.Actions>
|
||||
<Button variant="secondary" onClick={stateManager.onClearSearchAndFilters}>
|
||||
Clear search and filters
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const props: SearchResultsProps = {
|
||||
|
79
public/app/features/browse-dashboards/state/hooks.test.ts
Normal file
79
public/app/features/browse-dashboards/state/hooks.test.ts
Normal file
@ -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>): 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');
|
||||
});
|
||||
});
|
||||
});
|
@ -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));
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -8,7 +8,7 @@ import * as allReducers from './reducers';
|
||||
const { extraReducerFetchChildrenFulfilled, ...baseReducers } = allReducers;
|
||||
|
||||
const initialState: BrowseDashboardsState = {
|
||||
rootItems: [],
|
||||
rootItems: undefined,
|
||||
childrenByParentUID: {},
|
||||
openFolders: {},
|
||||
selectedItems: {
|
||||
|
@ -7,7 +7,7 @@ export type DashboardTreeSelection = Record<DashboardViewItemKind, Record<string
|
||||
};
|
||||
|
||||
export interface BrowseDashboardsState {
|
||||
rootItems: DashboardViewItem[];
|
||||
rootItems: DashboardViewItem[] | undefined;
|
||||
childrenByParentUID: Record<string, DashboardViewItem[] | undefined>;
|
||||
selectedItems: DashboardTreeSelection;
|
||||
|
||||
|
@ -95,6 +95,7 @@ export class SearchStateManager extends StateManagerBase<SearchState> {
|
||||
tag: [],
|
||||
panel_type: undefined,
|
||||
starred: undefined,
|
||||
sort: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user