NestedFolders: Paginate nested folder picker (#71489)

* Paginate nested folder picker items

* Prevent dashboards from appearing in the folder picker

* fix tests

* remove SkeletonGroup

* more excludeKinds

* cleanup
This commit is contained in:
Josh Hunt
2023-07-19 13:10:43 +00:00
committed by GitHub
parent c3695270b1
commit 34a2a7f3cb
8 changed files with 211 additions and 119 deletions

View File

@@ -3,11 +3,15 @@ import { DashboardViewItem, DashboardViewItemKind } from 'app/features/search/ty
import { createAsyncThunk } from 'app/types';
import { listDashboards, listFolders, PAGE_SIZE } from '../api/services';
import { DashboardViewItemWithUIItems, UIDashboardViewItem } from '../types';
import { findItem } from './utils';
interface FetchNextChildrenPageArgs {
parentUID: string | undefined;
// Allow UI items to be excluded (they're always excluded) for convenience for callers
excludeKinds?: Array<DashboardViewItemWithUIItems['kind'] | UIDashboardViewItem['uiKind']>;
pageSize: number;
}
@@ -87,9 +91,12 @@ export const refetchChildren = createAsyncThunk(
export const fetchNextChildrenPage = createAsyncThunk(
'browseDashboards/fetchNextChildrenPage',
async (
{ parentUID, pageSize }: FetchNextChildrenPageArgs,
{ parentUID, excludeKinds = [], pageSize }: FetchNextChildrenPageArgs,
thunkAPI
): Promise<undefined | FetchNextChildrenPageResult> => {
// TODO: invert prop to `includeKinds`, but also support not loading folders
const loadDashboards = !excludeKinds.includes('dashboard');
const uid = parentUID === GENERAL_FOLDER_UID ? undefined : parentUID;
const state = thunkAPI.getState().browseDashboards;
@@ -107,13 +114,13 @@ export const fetchNextChildrenPage = createAsyncThunk(
fetchKind = 'folder';
} else if (collection.lastFetchedKind === 'dashboard' && !collection.lastKindHasMoreItems) {
// There's nothing to load at all
console.warn(`FetchedChildren called for ${uid} but that collection is fully loaded`);
console.warn(`fetchNextChildrenPage called for ${uid} but that collection is fully loaded`);
// return;
} else if (collection.lastFetchedKind === 'folder' && collection.lastKindHasMoreItems) {
// Load additional pages of folders
page = collection.lastFetchedPage + 1;
fetchKind = 'folder';
} else {
} else if (loadDashboards) {
// We've already checked if there's more folders to load, so if the last fetched is folder
// then we fetch first page of dashboards
page = collection.lastFetchedKind === 'folder' ? 1 : collection.lastFetchedPage + 1;
@@ -133,7 +140,7 @@ export const fetchNextChildrenPage = createAsyncThunk(
// If we've loaded all folders, load the first page of dashboards.
// This ensures dashboards are loaded if a folder contains only dashboards.
if (fetchKind === 'folder' && lastPageOfKind) {
if (fetchKind === 'folder' && lastPageOfKind && loadDashboards) {
fetchKind = 'dashboard';
page = 1;

View File

@@ -5,9 +5,16 @@ import { DashboardViewItem } from 'app/features/search/types';
import { useSelector, StoreState, useDispatch } from 'app/types';
import { PAGE_SIZE } from '../api/services';
import { BrowseDashboardsState, DashboardsTreeItem, DashboardTreeSelection } from '../types';
import {
BrowseDashboardsState,
DashboardsTreeItem,
DashboardTreeSelection,
DashboardViewItemWithUIItems,
UIDashboardViewItem,
} from '../types';
import { fetchNextChildrenPage } from './actions';
import { getPaginationPlaceholders } from './utils';
export const rootItemsSelector = (wholeState: StoreState) => wholeState.browseDashboards.rootItems;
export const childrenByParentUIDSelector = (wholeState: StoreState) => wholeState.browseDashboards.childrenByParentUID;
@@ -97,7 +104,9 @@ export function useActionSelectionState() {
return useSelector((state) => selectedItemsForActionsSelector(state));
}
export function useLoadNextChildrenPage() {
export function useLoadNextChildrenPage(
excludeKinds: Array<DashboardViewItemWithUIItems['kind'] | UIDashboardViewItem['uiKind']> = []
) {
const dispatch = useDispatch();
const requestInFlightRef = useRef(false);
@@ -109,12 +118,12 @@ export function useLoadNextChildrenPage() {
requestInFlightRef.current = true;
const promise = dispatch(fetchNextChildrenPage({ parentUID: folderUID, pageSize: PAGE_SIZE }));
const promise = dispatch(fetchNextChildrenPage({ parentUID: folderUID, excludeKinds, pageSize: PAGE_SIZE }));
promise.finally(() => (requestInFlightRef.current = false));
return promise;
},
[dispatch]
[dispatch, excludeKinds]
);
return handleLoadMore;
@@ -135,21 +144,25 @@ export function createFlatTree(
childrenByUID: BrowseDashboardsState['childrenByParentUID'],
openFolders: Record<string, boolean>,
level = 0,
insertEmptyFolderIndicator = true
excludeKinds: Array<DashboardViewItemWithUIItems['kind'] | UIDashboardViewItem['uiKind']> = []
): DashboardsTreeItem[] {
function mapItem(item: DashboardViewItem, parentUID: string | undefined, level: number): DashboardsTreeItem[] {
if (excludeKinds.includes(item.kind)) {
return [];
}
const mappedChildren = createFlatTree(
item.uid,
rootCollection,
childrenByUID,
openFolders,
level + 1,
insertEmptyFolderIndicator
excludeKinds
);
const isOpen = Boolean(openFolders[item.uid]);
const emptyFolder = childrenByUID[item.uid]?.items.length === 0;
if (isOpen && emptyFolder && insertEmptyFolderIndicator) {
if (isOpen && emptyFolder && !excludeKinds.includes('empty-folder')) {
mappedChildren.push({
isOpen: false,
level: level + 1,
@@ -186,18 +199,3 @@ export function createFlatTree(
return children;
}
function getPaginationPlaceholders(amount: number, parentUID: string | undefined, level: number) {
return new Array(amount).fill(null).map((_, index) => {
return {
parentUID,
level,
isOpen: false,
item: {
kind: 'ui' as const,
uiKind: 'pagination-placeholder' as const,
uid: `${parentUID}-pagination-${index}`,
},
};
});
}

View File

@@ -40,7 +40,7 @@ export function fetchNextChildrenPageFulfilled(
}
const { children, page, kind, lastPageOfKind } = payload;
const { parentUID } = action.meta.arg;
const { parentUID, excludeKinds = [] } = action.meta.arg;
const collection = parentUID ? state.childrenByParentUID[parentUID] : state.rootItems;
const prevItems = collection?.items ?? [];
@@ -50,7 +50,7 @@ export function fetchNextChildrenPageFulfilled(
lastFetchedKind: kind,
lastFetchedPage: page,
lastKindHasMoreItems: !lastPageOfKind,
isFullyLoaded: kind === 'dashboard' && lastPageOfKind,
isFullyLoaded: !excludeKinds.includes('dashboard') ? kind === 'dashboard' && lastPageOfKind : lastPageOfKind,
};
if (!parentUID) {

View File

@@ -28,3 +28,18 @@ export function findItem(
return undefined;
}
export function getPaginationPlaceholders(amount: number, parentUID: string | undefined, level: number) {
return new Array(amount).fill(null).map((_, index) => {
return {
parentUID,
level,
isOpen: false,
item: {
kind: 'ui' as const,
uiKind: 'pagination-placeholder' as const,
uid: `${parentUID}-pagination-${index}`,
},
};
});
}

View File

@@ -33,7 +33,7 @@ export interface UIDashboardViewItem {
uid: string;
}
type DashboardViewItemWithUIItems = DashboardViewItem | UIDashboardViewItem;
export type DashboardViewItemWithUIItems = DashboardViewItem | UIDashboardViewItem;
export interface DashboardsTreeItem<T extends DashboardViewItemWithUIItems = DashboardViewItemWithUIItems> {
item: T;