FolderPicker: Allow customizing root item display (#110319)

FolderPicker: Allow customize root item display item
This commit is contained in:
Yunwen Zheng
2025-08-29 10:43:48 -04:00
committed by GitHub
parent be3fa041a5
commit 6952461362
6 changed files with 106 additions and 28 deletions
@@ -39,6 +39,9 @@ export interface NestedFolderPickerProps {
/* Start tree from this folder instead of root */
rootFolderUID?: string;
/* Custom root folder item, default is "Dashboards" */
rootFolderItem?: DashboardsTreeItem;
/* Show folders matching this permission, mainly used to also show folders user can view. Defaults to showing only folders user has Edit */
permission?: 'view' | 'edit';
@@ -73,6 +76,7 @@ export function NestedFolderPicker({
clearable = false,
excludeUIDs,
rootFolderUID,
rootFolderItem,
permission = 'edit',
onChange,
id,
@@ -110,7 +114,13 @@ export function NestedFolderPicker({
items: browseFlatTree,
isLoading: isBrowseLoading,
requestNextPage: fetchFolderPage,
} = useFoldersQuery(isBrowsing, foldersOpenState, permissionLevel, rootFolderUID);
} = useFoldersQuery({
isBrowsing,
openFolders: foldersOpenState,
permission: permissionLevel,
rootFolderUID,
rootFolderItem,
});
useEffect(() => {
if (!search) {
@@ -5,11 +5,12 @@ import * as runtime from '@grafana/runtime';
import { setupMockServer } from '@grafana/test-utils/server';
import { getFolderFixtures } from '@grafana/test-utils/unstable';
import { backendSrv } from 'app/core/services/backend_srv';
import { ManagerKind } from 'app/features/apiserver/types';
import { DashboardViewItem } from '../../../features/search/types';
import { useFoldersQuery } from './useFoldersQuery';
import { getRootFolderItem } from './utils';
import { getCustomRootFolderItem, getRootFolderItem } from './utils';
const [_, { folderA, folderB, folderC }] = getFolderFixtures();
@@ -47,11 +48,42 @@ describe('useFoldersQuery', () => {
expect(sortedItemTitles).toEqual(expectedTitles);
});
it('uses custom root folder display name when rootFolderItem is provided', async () => {
runtime.config.featureToggles.foldersAppPlatformAPI = featureToggleState;
const { result } = renderHook(
() =>
useFoldersQuery({
isBrowsing: true,
openFolders: {},
rootFolderItem: getCustomRootFolderItem({
title: 'Test Repo',
managedBy: ManagerKind.Repo,
}),
}),
{ wrapper }
);
// Test that root folder item uses the custom display name
expect(result.current.items[0]).toEqual(
getCustomRootFolderItem({
title: 'Test Repo',
managedBy: ManagerKind.Repo,
})
);
});
});
});
async function testFn() {
const { result } = renderHook(() => useFoldersQuery(true, {}), { wrapper });
const { result } = renderHook(
() =>
useFoldersQuery({
isBrowsing: true,
openFolders: {},
}),
{ wrapper }
);
expect(result.current.items[0]).toEqual(getRootFolderItem());
expect(result.current.isLoading).toBe(false);
@@ -1,18 +1,28 @@
import { config } from '@grafana/runtime';
import { DashboardsTreeItem } from 'app/features/browse-dashboards/types';
import { PermissionLevelString } from 'app/types/acl';
import { useFoldersQueryAppPlatform } from './useFoldersQueryAppPlatform';
import { useFoldersQueryLegacy } from './useFoldersQueryLegacy';
export function useFoldersQuery(
isBrowsing: boolean,
openFolders: Record<string, boolean>,
permission?: PermissionLevelString,
export interface UseFoldersQueryProps {
isBrowsing: boolean;
openFolders: Record<string, boolean>;
permission?: PermissionLevelString;
rootFolderUID?: string;
rootFolderItem?: DashboardsTreeItem;
}
export function useFoldersQuery({
isBrowsing,
openFolders,
permission,
/* Start tree from this folder instead of root */
rootFolderUID?: string
) {
const resultLegacy = useFoldersQueryLegacy(isBrowsing, openFolders, permission, rootFolderUID);
const resultAppPlatform = useFoldersQueryAppPlatform(isBrowsing, openFolders, rootFolderUID);
rootFolderUID,
rootFolderItem,
}: UseFoldersQueryProps) {
const resultLegacy = useFoldersQueryLegacy({ isBrowsing, openFolders, permission, rootFolderUID, rootFolderItem });
const resultAppPlatform = useFoldersQueryAppPlatform({ isBrowsing, openFolders, rootFolderUID, rootFolderItem });
// Running the hooks themselves don't have any side effects, so we can just conditionally use one or the other
// requestNextPage function from the result
@@ -10,6 +10,7 @@ import { AnnoKeyManagerKind, ManagerKind } from '../../../features/apiserver/typ
import { PAGE_SIZE } from '../../../features/browse-dashboards/api/services';
import { getPaginationPlaceholders } from '../../../features/browse-dashboards/state/utils';
import { UseFoldersQueryProps } from './useFoldersQuery';
import { getRootFolderItem } from './utils';
type GetFolderChildrenQuery = ReturnType<ReturnType<typeof dashboardAPIv0alpha1.endpoints.getSearch.select>>;
@@ -25,12 +26,15 @@ const collator = new Intl.Collator();
* This version uses the getFolderChildren API from the folder v1beta1 API. Compared to legacy API, the v1beta1 API
* does not have pagination at the moment.
*/
export function useFoldersQueryAppPlatform(
isBrowsing: boolean,
openFolders: Record<string, boolean>,
type Props = Omit<UseFoldersQueryProps, 'permission'>;
export function useFoldersQueryAppPlatform({
isBrowsing,
openFolders,
/* rootFolderUID: configure which folder to start browsing from */
rootFolderUID?: string
) {
rootFolderUID,
rootFolderItem,
}: Props) {
const dispatch = useDispatch();
// Keep a list of all request subscriptions so we can unsubscribe from them when the component is unmounted
@@ -157,10 +161,10 @@ export function useFoldersQueryAppPlatform(
const startingToken = rootFolderUID ?? rootFolderToken;
const rootFlatTree = createFlatList(startingToken, state.responseByParent[startingToken], 1);
rootFlatTree.unshift(getRootFolderItem());
rootFlatTree.unshift(rootFolderItem || getRootFolderItem());
return rootFlatTree;
}, [state, isBrowsing, openFolders, rootFolderUID]);
}, [state, isBrowsing, openFolders, rootFolderUID, rootFolderItem]);
return {
items: treeList,
@@ -7,10 +7,10 @@ import { ListFolderQueryArgs, browseDashboardsAPI } from 'app/features/browse-da
import { PAGE_SIZE } from 'app/features/browse-dashboards/api/services';
import { getPaginationPlaceholders } from 'app/features/browse-dashboards/state/utils';
import { DashboardViewItemWithUIItems, DashboardsTreeItem } from 'app/features/browse-dashboards/types';
import { PermissionLevelString } from 'app/types/acl';
import { FolderListItemDTO } from 'app/types/folders';
import { useDispatch, useSelector } from 'app/types/store';
import { UseFoldersQueryProps } from './useFoldersQuery';
import { getRootFolderItem } from './utils';
type ListFoldersQuery = ReturnType<ReturnType<typeof browseDashboardsAPI.endpoints.listFolders.select>>;
@@ -45,13 +45,14 @@ function getPagesLoadStatus(pages: ListFoldersQuery[]): [boolean, number | undef
/**
* Returns a loaded folder hierarchy as a flat list and a function to load more pages.
*/
export function useFoldersQueryLegacy(
isBrowsing: boolean,
openFolders: Record<string, boolean>,
permission?: PermissionLevelString,
export function useFoldersQueryLegacy({
isBrowsing,
openFolders,
permission,
/* rootFolderUID: configure which folder to start browsing from */
rootFolderUID?: string
) {
rootFolderUID,
rootFolderItem,
}: UseFoldersQueryProps) {
const dispatch = useDispatch();
// Keep a list of all request subscriptions so we can unsubscribe from them when the component is unmounted
@@ -183,10 +184,10 @@ export function useFoldersQueryLegacy(
const startingPages = rootFolderUID ? state.pagesByParent[rootFolderUID] : state.rootPages;
const rootFlatTree = createFlatList(rootFolderUID ?? undefined, startingPages ?? [], 1);
rootFlatTree.unshift(getRootFolderItem());
rootFlatTree.unshift(rootFolderItem || getRootFolderItem());
return rootFlatTree;
}, [state, isBrowsing, openFolders, rootFolderUID]);
}, [state, isBrowsing, openFolders, rootFolderUID, rootFolderItem]);
return {
items: treeList,
@@ -1,6 +1,8 @@
import { t } from '@grafana/i18n';
import { ManagerKind } from 'app/features/apiserver/types';
import { DashboardsTreeItem } from 'app/features/browse-dashboards/types';
export const getRootFolderItem = () => ({
export const getRootFolderItem = (): DashboardsTreeItem => ({
isOpen: true,
level: 0,
item: {
@@ -9,3 +11,22 @@ export const getRootFolderItem = () => ({
uid: '',
},
});
export const getCustomRootFolderItem = ({
title,
managedBy,
uid,
}: {
title: string;
managedBy?: ManagerKind;
uid?: string;
}): DashboardsTreeItem => ({
isOpen: true,
level: 0,
item: {
kind: 'folder' as const,
title,
uid: uid || '',
managedBy,
},
});