diff --git a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts index d2cc21d31e6..d0238d40946 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts @@ -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 */ diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx index 7967f867ad5..9ef4cb8a9d2 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx @@ -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> = []; @@ -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({ (null); + const [teamError, setTeamError] = useState(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, + }; +}