diff --git a/public/app/core/components/NestedFolderPicker/FolderRepo.test.tsx b/public/app/core/components/NestedFolderPicker/FolderRepo.test.tsx new file mode 100644 index 00000000000..316ae8c51e7 --- /dev/null +++ b/public/app/core/components/NestedFolderPicker/FolderRepo.test.tsx @@ -0,0 +1,91 @@ +import { render, screen, cleanup } from '@testing-library/react'; + +import { RepositoryView } from 'app/api/clients/provisioning/v0alpha1'; +import { ManagerKind } from 'app/features/apiserver/types'; +import { DashboardViewItem } from 'app/features/search/types'; + +import { FolderRepo } from './FolderRepo'; + +jest.mock('@grafana/runtime', () => ({ + config: { featureToggles: { provisioning: true } }, +})); + +const mockUseGetFrontendSettingsQuery = jest.fn(); +jest.mock('app/api/clients/provisioning/v0alpha1', () => ({ + useGetFrontendSettingsQuery: () => mockUseGetFrontendSettingsQuery(), +})); + +const mockUseGetResourceRepositoryView = jest.fn(); +jest.mock('app/features/provisioning/hooks/useGetResourceRepositoryView', () => ({ + useGetResourceRepositoryView: () => mockUseGetResourceRepositoryView(), +})); + +function mockSettings(items: Array>) { + mockUseGetFrontendSettingsQuery.mockReturnValue({ data: { items } }); +} + +function mockRepoView({ isReadOnlyRepo = false, repoType = 'github' }) { + mockUseGetResourceRepositoryView.mockReturnValue({ isReadOnlyRepo, repoType }); +} + +const MOCK_FOLDER: DashboardViewItem = { + uid: 'A', + managedBy: ManagerKind.Repo, + parentUID: undefined, + kind: 'folder', + title: 'test', +}; + +function setup({ + folder = undefined, + repoViewMock = {}, + settingsMock = [], +}: { + folder?: DashboardViewItem; + repoViewMock?: { isReadOnlyRepo?: boolean; repoType?: string }; + settingsMock?: Array>; +}) { + mockSettings(settingsMock); + mockRepoView(repoViewMock); + + return { + ...render(), + }; +} + +describe('FolderRepo', () => { + afterEach(() => { + cleanup(); + jest.clearAllMocks(); + }); + + it('returns null when folder is undefined', () => { + setup({ folder: undefined }); + expect(screen.queryByText('Read only')).not.toBeInTheDocument(); + expect(screen.queryByTitle('Provisioned')).not.toBeInTheDocument(); + }); + + it('returns null when folder has parentUID', () => { + setup({ folder: { ...MOCK_FOLDER, parentUID: 'repo-123' } }); + expect(screen.queryByText('Read only')).not.toBeInTheDocument(); + expect(screen.queryByTitle('Provisioned')).not.toBeInTheDocument(); + }); + + it('returns null when folder is not managed', () => { + setup({ folder: { ...MOCK_FOLDER, managedBy: undefined } }); + expect(screen.queryByText('Read only')).not.toBeInTheDocument(); + expect(screen.queryByTitle('Provisioned')).not.toBeInTheDocument(); + }); + + it('returns null when whole instance is provisioned', () => { + setup({ folder: MOCK_FOLDER, settingsMock: [{ target: 'instance' }] }); + expect(screen.queryByText('Read only')).not.toBeInTheDocument(); + expect(screen.queryByTitle('Provisioned')).not.toBeInTheDocument(); + }); + + it('renders Read only badge when repo is read-only (empty workflows)', () => { + setup({ folder: MOCK_FOLDER, repoViewMock: { isReadOnlyRepo: true, repoType: 'github' } }); + expect(screen.getByText('Read only')).toBeInTheDocument(); + expect(screen.queryByTitle('Provisioned')).toBeInTheDocument(); + }); +}); diff --git a/public/app/core/components/NestedFolderPicker/FolderRepo.tsx b/public/app/core/components/NestedFolderPicker/FolderRepo.tsx index b0f750d991d..932f33bfabe 100644 --- a/public/app/core/components/NestedFolderPicker/FolderRepo.tsx +++ b/public/app/core/components/NestedFolderPicker/FolderRepo.tsx @@ -1,13 +1,14 @@ import { t } from '@grafana/i18n'; import { Badge, Stack } from '@grafana/ui'; +import { ManagerKind } from 'app/features/apiserver/types'; import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView'; import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance'; import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository'; -import { NestedFolderDTO } from 'app/features/search/service/types'; -import { FolderDTO, FolderListItemDTO } from 'app/types/folders'; +import { DashboardViewItem } from 'app/features/search/types'; +import { FolderDTO } from 'app/types/folders'; export interface Props { - folder?: FolderListItemDTO | NestedFolderDTO | FolderDTO; + folder?: FolderDTO | DashboardViewItem; } export function FolderRepo({ folder }: Props) { @@ -17,11 +18,11 @@ export function FolderRepo({ folder }: Props) { // folder is not managed // if whole instance is provisioned const isProvisionedInstance = useIsProvisionedInstance(); - const skipRender = - !folder || ('parentUID' in folder && folder.parentUID) || !folder.managedBy || isProvisionedInstance; + const skipRender = getShouldSkipRender(folder, isProvisionedInstance); const { isReadOnlyRepo, repoType } = useGetResourceRepositoryView({ folderName: skipRender ? undefined : folder?.uid, + skipQuery: skipRender, }); if (skipRender) { @@ -38,7 +39,21 @@ export function FolderRepo({ folder }: Props) { tooltip={getReadOnlyTooltipText({ isLocal: repoType === 'local' })} /> )} - + ); } + +function getShouldSkipRender(folder: FolderDTO | DashboardViewItem | undefined, isProvisionedInstance?: boolean) { + // Skip render if parentUID is present, then we should skip rendering. we only display icon for root folders + const hasParent = folder && Boolean('parentUID' in folder && folder.parentUID); + // Skip render if folder is not managed by Repo + const isNotManaged = folder && folder.managedBy !== ManagerKind.Repo; + + return !folder || hasParent || isNotManaged || isProvisionedInstance; +} diff --git a/public/app/features/browse-dashboards/components/BrowseView.tsx b/public/app/features/browse-dashboards/components/BrowseView.tsx index b5895c5e376..b1bbc45ba3d 100644 --- a/public/app/features/browse-dashboards/components/BrowseView.tsx +++ b/public/app/features/browse-dashboards/components/BrowseView.tsx @@ -1,10 +1,12 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useCallback, useMemo } from 'react'; +import { OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { CallToActionCard, EmptyState, LinkButton, TextLink } from '@grafana/ui'; import { useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1'; +import { contextSrv } from 'app/core/core'; import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance'; import { DashboardViewItem } from 'app/features/search/types'; import { useDispatch, useSelector } from 'app/types/store'; @@ -41,7 +43,8 @@ export function BrowseView({ folderUID, width, height, permissions }: BrowseView const canSelect = canSelectItems(permissions); const isProvisionedInstance = useIsProvisionedInstance(); const provisioningEnabled = config.featureToggles.provisioning; - const { data: settingsData } = useGetFrontendSettingsQuery(!provisioningEnabled ? skipToken : undefined); + const hasNoRole = contextSrv.user.orgRole === OrgRole.None; + const { data: settingsData } = useGetFrontendSettingsQuery(!provisioningEnabled || hasNoRole ? skipToken : undefined); const rootItems = useSelector(rootItemsSelector); const excludeUIDs = useMemo(() => { diff --git a/public/app/features/provisioning/hooks/useGetResourceRepositoryView.ts b/public/app/features/provisioning/hooks/useGetResourceRepositoryView.ts index 46f1be54526..7f57c5c1231 100644 --- a/public/app/features/provisioning/hooks/useGetResourceRepositoryView.ts +++ b/public/app/features/provisioning/hooks/useGetResourceRepositoryView.ts @@ -1,8 +1,10 @@ import { skipToken } from '@reduxjs/toolkit/query/react'; +import { OrgRole } from '@grafana/data'; import { config } from '@grafana/runtime'; import { Folder, useGetFolderQuery } from 'app/api/clients/folder/v1beta1'; import { RepositoryView, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1'; +import { contextSrv } from 'app/core/core'; import { AnnoKeyManagerIdentity } from 'app/features/apiserver/types'; import { RepoType } from '../Wizard/types'; @@ -11,6 +13,7 @@ import { getIsReadOnlyRepo } from '../utils/repository'; interface GetResourceRepositoryArgs { name?: string; // the repository name folderName?: string; // folder we are targeting + skipQuery?: boolean; } interface RepositoryViewData { @@ -23,13 +26,19 @@ interface RepositoryViewData { } // This is safe to call as a viewer (you do not need full access to the Repository configs) -export const useGetResourceRepositoryView = ({ name, folderName }: GetResourceRepositoryArgs): RepositoryViewData => { +export const useGetResourceRepositoryView = ({ + name, + folderName, + skipQuery, +}: GetResourceRepositoryArgs): RepositoryViewData => { + const hasNoRole = contextSrv.user.orgRole === OrgRole.None; + const provisioningEnabled = config.featureToggles.provisioning; const { data: settingsData, isLoading: isSettingsLoading } = useGetFrontendSettingsQuery( - !provisioningEnabled ? skipToken : undefined + !provisioningEnabled || skipQuery || hasNoRole ? skipToken : undefined ); - const skipFolderQuery = !folderName || !provisioningEnabled; + const skipFolderQuery = !folderName || !provisioningEnabled || skipQuery || hasNoRole; const { data: folder, isLoading: isFolderLoading } = useGetFolderQuery( skipFolderQuery ? skipToken : { name: folderName } ); diff --git a/public/app/features/provisioning/hooks/useIsProvisionedInstance.ts b/public/app/features/provisioning/hooks/useIsProvisionedInstance.ts index daa2ce8da6f..7ee0b83a3ec 100644 --- a/public/app/features/provisioning/hooks/useIsProvisionedInstance.ts +++ b/public/app/features/provisioning/hooks/useIsProvisionedInstance.ts @@ -1,12 +1,15 @@ import { skipToken } from '@reduxjs/toolkit/query'; +import { OrgRole } from '@grafana/data'; import { config } from '@grafana/runtime'; import { RepositoryViewList, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1'; +import { contextSrv } from 'app/core/core'; export function useIsProvisionedInstance(settings?: RepositoryViewList) { - const settingsQuery = useGetFrontendSettingsQuery( - settings || !config.featureToggles.provisioning ? skipToken : undefined - ); + const hasNoRole = contextSrv.user.orgRole === OrgRole.None; + const skip = !config.featureToggles.provisioning || hasNoRole; + + const settingsQuery = useGetFrontendSettingsQuery(settings || skip ? skipToken : undefined); if (!settings) { settings = settingsQuery.data; }