FolderRepo: Prevent no basic role user getting infinite api calls on provisioning settings endpoint (#110486)
This commit is contained in:
@@ -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<Partial<RepositoryView>>) {
|
||||
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<Partial<RepositoryView>>;
|
||||
}) {
|
||||
mockSettings(settingsMock);
|
||||
mockRepoView(repoViewMock);
|
||||
|
||||
return {
|
||||
...render(<FolderRepo folder={folder} />),
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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' })}
|
||||
/>
|
||||
)}
|
||||
<Badge color="purple" icon="exchange-alt" tooltip={t('folder-repo.provisioned-badge', 'Provisioned')} />
|
||||
<Badge
|
||||
title={t('folder-repo.provisioned-badge', 'Provisioned')}
|
||||
color="purple"
|
||||
icon="exchange-alt"
|
||||
tooltip={t('folder-repo.provisioned-badge', 'Provisioned')}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user