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 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 { DashboardViewItem } from 'app/features/search/types';
|
||||||
import { useDispatch } from 'app/types';
|
import { useDispatch } from 'app/types';
|
||||||
|
|
||||||
@ -11,6 +13,7 @@ import {
|
|||||||
setItemSelectionState,
|
setItemSelectionState,
|
||||||
useChildrenByParentUIDState,
|
useChildrenByParentUIDState,
|
||||||
setAllSelection,
|
setAllSelection,
|
||||||
|
useBrowseLoadingStatus,
|
||||||
} from '../state';
|
} from '../state';
|
||||||
import { DashboardTreeSelection, SelectionState } from '../types';
|
import { DashboardTreeSelection, SelectionState } from '../types';
|
||||||
|
|
||||||
@ -24,6 +27,7 @@ interface BrowseViewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function BrowseView({ folderUID, width, height, canSelect }: BrowseViewProps) {
|
export function BrowseView({ folderUID, width, height, canSelect }: BrowseViewProps) {
|
||||||
|
const status = useBrowseLoadingStatus(folderUID);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const flatTree = useFlatTreeState(folderUID);
|
const flatTree = useFlatTreeState(folderUID);
|
||||||
const selectedItems = useCheckboxSelectionState();
|
const selectedItems = useCheckboxSelectionState();
|
||||||
@ -95,6 +99,27 @@ export function BrowseView({ folderUID, width, height, canSelect }: BrowseViewPr
|
|||||||
[selectedItems, childrenByParentUID]
|
[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 (
|
return (
|
||||||
<DashboardsTree
|
<DashboardsTree
|
||||||
canSelect={canSelect}
|
canSelect={canSelect}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback } from 'react';
|
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 { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKeyboardSelection';
|
||||||
import { SearchResultsProps, SearchResultsTable } from 'app/features/search/page/components/SearchResultsTable';
|
import { SearchResultsProps, SearchResultsTable } from 'app/features/search/page/components/SearchResultsTable';
|
||||||
import { useSearchStateManager } from 'app/features/search/state/SearchStateManager';
|
import { useSearchStateManager } from 'app/features/search/state/SearchStateManager';
|
||||||
@ -69,7 +69,18 @@ export function SearchView({ width, height, canSelect }: SearchViewProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (value.totalRows === 0) {
|
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 = {
|
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) => wholeState.browseDashboards.openFolders,
|
||||||
(wholeState: StoreState, rootFolderUID: string | undefined) => rootFolderUID,
|
(wholeState: StoreState, rootFolderUID: string | undefined) => rootFolderUID,
|
||||||
(rootItems, childrenByParentUID, openFolders, folderUID) => {
|
(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) {
|
export function useFlatTreeState(folderUID: string | undefined) {
|
||||||
return useSelector((state) => flatTreeSelector(state, folderUID));
|
return useSelector((state) => flatTreeSelector(state, folderUID));
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ export function setItemSelectionState(
|
|||||||
let nextParentUID = item.parentUID;
|
let nextParentUID = item.parentUID;
|
||||||
|
|
||||||
while (nextParentUID) {
|
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
|
// This case should not happen, but a find can theortically return undefined, and it
|
||||||
// helps limit infinite loops
|
// 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
|
// 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 }>) {
|
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;
|
state.selectedItems[child.kind][child.uid] = isSelected;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -8,7 +8,7 @@ import * as allReducers from './reducers';
|
|||||||
const { extraReducerFetchChildrenFulfilled, ...baseReducers } = allReducers;
|
const { extraReducerFetchChildrenFulfilled, ...baseReducers } = allReducers;
|
||||||
|
|
||||||
const initialState: BrowseDashboardsState = {
|
const initialState: BrowseDashboardsState = {
|
||||||
rootItems: [],
|
rootItems: undefined,
|
||||||
childrenByParentUID: {},
|
childrenByParentUID: {},
|
||||||
openFolders: {},
|
openFolders: {},
|
||||||
selectedItems: {
|
selectedItems: {
|
||||||
|
@ -7,7 +7,7 @@ export type DashboardTreeSelection = Record<DashboardViewItemKind, Record<string
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface BrowseDashboardsState {
|
export interface BrowseDashboardsState {
|
||||||
rootItems: DashboardViewItem[];
|
rootItems: DashboardViewItem[] | undefined;
|
||||||
childrenByParentUID: Record<string, DashboardViewItem[] | undefined>;
|
childrenByParentUID: Record<string, DashboardViewItem[] | undefined>;
|
||||||
selectedItems: DashboardTreeSelection;
|
selectedItems: DashboardTreeSelection;
|
||||||
|
|
||||||
|
@ -95,6 +95,7 @@ export class SearchStateManager extends StateManagerBase<SearchState> {
|
|||||||
tag: [],
|
tag: [],
|
||||||
panel_type: undefined,
|
panel_type: undefined,
|
||||||
starred: undefined,
|
starred: undefined,
|
||||||
|
sort: undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user