Get team folder and preselect it

This commit is contained in:
Andrej Ocenas
2025-12-11 11:26:51 +01:00
parent 726ac07372
commit 2d83ad6e2a
3 changed files with 103 additions and 4 deletions
@@ -249,6 +249,7 @@ const injectedRtkApi = api
sort: queryArg.sort,
limit: queryArg.limit,
explain: queryArg.explain,
owner: queryArg.owner,
},
}),
providesTags: ['Search'],
@@ -606,6 +607,8 @@ export type GetSearchApiArg = {
type?: 'folder' | 'dashboard';
/** search/list within a folder (not recursive) */
folder?: string;
/** filter by owner reference name or UID */
owner?: string;
/** count distinct terms for selected fields */
facet?: string[];
/** tag query filter */
@@ -20,6 +20,7 @@ import { FolderRepo } from './FolderRepo';
import { getDOMId, NestedFolderList } from './NestedFolderList';
import Trigger from './Trigger';
import { useFoldersQuery } from './useFoldersQuery';
import { useTeamOwnedFolder } from './useTeamOwnedFolder';
import { useTreeInteractions } from './useTreeInteractions';
import { getRootFolderItem } from './utils';
@@ -82,7 +83,10 @@ export function NestedFolderPicker({
id,
}: NestedFolderPickerProps) {
const styles = useStyles2(getStyles);
const selectedFolder = useGetFolderQueryFacade(value);
const { folder: teamFolder } = useTeamOwnedFolder();
const effectiveValue = value || teamFolder?.name;
const selectedFolder = useGetFolderQueryFacade(effectiveValue);
// user might not have access to the folder, but they have access to the dashboard
// in this case we disable the folder picker - this is an edge case when user has edit access to a dashboard
// but doesn't have access to the folder
@@ -112,6 +116,12 @@ export function NestedFolderPicker({
rootFolderItem,
});
useEffect(() => {
if (value === undefined && teamFolder && onChange) {
onChange(teamFolder.name, teamFolder.title);
}
}, [onChange, teamFolder, value]);
useEffect(() => {
if (!search) {
setSearchResults(null);
@@ -209,6 +219,23 @@ export function NestedFolderPicker({
[search, fetchFolderPage]
);
const teamFolderTreeItem = useMemo(() => {
if (!teamFolder) {
return undefined;
}
return {
isOpen: false,
level: 0,
item: {
kind: 'folder' as const,
title: teamFolder.title,
uid: teamFolder.name,
parentUID: teamFolder.folder,
},
};
}, [teamFolder]);
const flatTree = useMemo(() => {
let flatTree: Array<DashboardsTreeItem<DashboardViewItemWithUIItems>> = [];
@@ -229,6 +256,10 @@ export function NestedFolderPicker({
})) ?? [];
}
if (teamFolderTreeItem) {
flatTree = [teamFolderTreeItem, ...flatTree];
}
// It's not super optimal to filter these in an additional iteration, but
// these options are used infrequently that its not a big deal
if (!showRootFolder || excludeUIDs?.length) {
@@ -246,7 +277,7 @@ export function NestedFolderPicker({
}
return flatTree;
}, [browseFlatTree, excludeUIDs, isBrowsing, searchResults?.items, showRootFolder]);
}, [browseFlatTree, excludeUIDs, isBrowsing, searchResults?.items, showRootFolder, teamFolderTreeItem]);
const isItemLoaded = useCallback(
(itemIndex: number) => {
@@ -276,7 +307,7 @@ export function NestedFolderPicker({
});
let label = selectedFolder.data?.title;
if (value === '') {
if (!label) {
label = t('browse-dashboards.folder-picker.root-title', 'Dashboards');
}
@@ -362,7 +393,7 @@ export function NestedFolderPicker({
<NestedFolderList
items={flatTree}
selectedFolder={value}
selectedFolder={effectiveValue}
focusedItemIndex={focusedItemIndex}
onFolderExpand={handleFolderExpand}
onFolderSelect={handleFolderSelect}
@@ -0,0 +1,65 @@
import { skipToken } from '@reduxjs/toolkit/query';
import { useEffect, useMemo, useState } from 'react';
import { TeamDto } from '@grafana/api-clients/rtkq/legacy';
import { useGetSearchQuery } from 'app/api/clients/dashboard/v0alpha1';
import { api as profileApi } from 'app/features/profile/api';
/**
* Returns the first folder owned by any team the current user belongs to.
* Uses the dashboard v0alpha1 search API with the `owner` filter.
*/
export function useTeamOwnedFolder() {
const [teams, setTeams] = useState<TeamDto[] | null>(null);
const [teamError, setTeamError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
profileApi
.loadTeams()
.then((result) => {
if (!cancelled) {
setTeams(result);
}
})
.catch((err) => {
if (!cancelled) {
setTeamError(err);
}
});
return () => {
cancelled = true;
};
}, []);
const owner = useMemo(() => {
if (!teams || teams.length === 0) {
return undefined;
}
const firstTeam = teams[0];
// Prefer UID if available, otherwise fallback to name
return firstTeam.uid ?? firstTeam.name;
}, [teams]);
const searchArgs =
owner !== undefined
? {
owner,
type: 'folder' as const,
// Now with the dummy backend this wouldn't work as we filter for owner after we get results
// from the DB and this would be only applied to the DB call.
// limit: 1,
}
: skipToken;
const { data, isFetching, error: searchError } = useGetSearchQuery(searchArgs);
const folder = data?.hits?.[0];
return {
folder,
isLoading: (teams === null && !teamError) || isFetching,
error: teamError ?? searchError,
};
}