FolderPicker: Don't show expand button for empty folders and move search icon (#111872)

* Remove expand indicator if empty

* Simplify the empty folder checking

* Move search icon

* Remove commented code
This commit is contained in:
Andrej Ocenas
2025-10-01 15:03:45 +02:00
committed by GitHub
parent 03326f9ffd
commit bb7358be29
4 changed files with 51 additions and 7 deletions
@@ -32,6 +32,7 @@ interface NestedFolderListProps {
onFolderSelect: (item: DashboardViewItem) => void;
isItemLoaded: (itemIndex: number) => boolean;
requestLoadMore: (folderUid: string | undefined) => void;
emptyFolders: Set<string>;
}
export function NestedFolderList({
@@ -44,6 +45,7 @@ export function NestedFolderList({
onFolderSelect,
isItemLoaded,
requestLoadMore,
emptyFolders,
}: NestedFolderListProps) {
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
const styles = useStyles2(getStyles);
@@ -57,8 +59,18 @@ export function NestedFolderList({
onFolderExpand,
onFolderSelect,
idPrefix,
emptyFolders,
}),
[items, focusedItemIndex, foldersAreOpenable, selectedFolder, onFolderExpand, onFolderSelect, idPrefix]
[
items,
focusedItemIndex,
foldersAreOpenable,
selectedFolder,
onFolderExpand,
onFolderSelect,
idPrefix,
emptyFolders,
]
);
const handleIsItemLoaded = useCallback(
@@ -119,8 +131,16 @@ interface RowProps {
const SKELETON_WIDTHS = [100, 200, 130, 160, 150];
function Row({ index, style: virtualStyles, data }: RowProps) {
const { items, focusedItemIndex, foldersAreOpenable, selectedFolder, onFolderExpand, onFolderSelect, idPrefix } =
data;
const {
items,
focusedItemIndex,
foldersAreOpenable,
selectedFolder,
onFolderExpand,
onFolderSelect,
idPrefix,
emptyFolders,
} = data;
const { item, isOpen, level, parentUID } = items[index];
const rowRef = useRef<HTMLDivElement>(null);
const labelId = useId();
@@ -203,7 +223,7 @@ function Row({ index, style: virtualStyles, data }: RowProps) {
<div className={styles.rowBody}>
<Indent level={level} spacing={2} />
{foldersAreOpenable ? (
{foldersAreOpenable && !emptyFolders.has(item.uid) ? (
<IconButton
size={CHEVRON_SIZE}
// by using onMouseDown here instead of onClick we can stop focus moving
@@ -257,7 +277,7 @@ const getStyles = (theme: GrafanaTheme2) => {
}),
folderButtonSpacer: css({
paddingLeft: theme.spacing(0.5),
paddingLeft: theme.spacing(2.5),
}),
row: css({
@@ -111,6 +111,7 @@ export function NestedFolderPicker({
const isBrowsing = Boolean(overlayOpen && !(search && searchResults));
const {
emptyFolders,
items: browseFlatTree,
isLoading: isBrowseLoading,
requestNextPage: fetchFolderPage,
@@ -328,7 +329,7 @@ export function NestedFolderPicker({
<Input
ref={refs.setReference}
autoFocus
prefix={label ? <Icon name="folder" /> : null}
prefix={label ? <Icon name="folder" /> : <Icon name="search" />}
placeholder={label ?? t('browse-dashboards.folder-picker.search-placeholder', 'Search folders')}
value={search}
invalid={invalid}
@@ -341,7 +342,6 @@ export function NestedFolderPicker({
aria-owns={overlayId}
aria-activedescendant={getDOMId(overlayId, flatTree[focusedItemIndex]?.item.uid)}
role="combobox"
suffix={<Icon name="search" />}
{...getReferenceProps()}
onKeyDown={handleKeyDown}
/>
@@ -381,6 +381,7 @@ export function NestedFolderPicker({
foldersAreOpenable={!(search && searchResults)}
isItemLoaded={isItemLoaded}
requestLoadMore={handleLoadMore}
emptyFolders={emptyFolders}
/>
</div>
)}
@@ -40,6 +40,9 @@ export function useFoldersQueryAppPlatform({
// Keep a list of all request subscriptions so we can unsubscribe from them when the component is unmounted
const requestsRef = useRef<GetFolderChildrenRequest[]>([]);
// Set of UIDs for which children were requested but were empty.
const [emptyFolders, setEmptyFolders] = useState<Set<string>>(new Set());
// Keep a list of selectors for dynamic state selection
const [selectors, setSelectors] = useState<Array<ReturnType<typeof dashboardAPIv0alpha1.endpoints.getSearch.select>>>(
[]
@@ -145,6 +148,14 @@ export function useFoldersQueryAppPlatform({
const childResponse = folderIsOpen && state.responseByParent[name];
if (childResponse) {
// If we finished loading and there are no children add to empty list
if (
childResponse.data &&
childResponse.status !== QueryStatus.pending &&
childResponse.data.hits.length === 0
) {
setEmptyFolders((prev) => new Set(prev).add(name));
}
const childFlatItems = createFlatList(name, childResponse, level + 1);
return [flatItem, ...childFlatItems];
}
@@ -168,6 +179,7 @@ export function useFoldersQueryAppPlatform({
}, [state, isBrowsing, openFolders, rootFolderUID, rootFolderItem]);
return {
emptyFolders,
items: treeList,
isLoading: state.isLoading,
requestNextPage,
@@ -58,6 +58,9 @@ export function useFoldersQueryLegacy({
// Keep a list of all request subscriptions so we can unsubscribe from them when the component is unmounted
const requestsRef = useRef<ListFoldersRequest[]>([]);
// Set of UIDs for which children were requested but were empty.
const [emptyFolders, setEmptyFolders] = useState<Set<string>>(new Set());
// Keep a list of selectors for dynamic state selection
const [selectors, setSelectors] = useState<
Array<ReturnType<typeof browseDashboardsAPI.endpoints.listFolders.select>>
@@ -165,8 +168,15 @@ export function useFoldersQueryLegacy({
};
const childPages = folderIsOpen && state.pagesByParent[item.uid];
if (childPages) {
const childFlatItems = createFlatList(item.uid, childPages, level + 1);
// If we finished loading and there are no children add to empty list
if (childPages[0] && childPages[0].status !== PENDING_STATUS && childFlatItems.length === 0) {
setEmptyFolders((prev) => new Set(prev).add(item.uid));
}
return [flatItem, ...childFlatItems];
}
@@ -191,6 +201,7 @@ export function useFoldersQueryLegacy({
}, [state, isBrowsing, openFolders, rootFolderUID, rootFolderItem]);
return {
emptyFolders,
items: treeList,
isLoading: state.isLoading,
requestNextPage,