diff --git a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts
index 33ee7549eb7..bc262a42bae 100644
--- a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts
+++ b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts
@@ -1013,7 +1013,7 @@ export type RepositorySpec = {
- `"local"` */
type: 'bitbucket' | 'git' | 'github' | 'gitlab' | 'local';
/** UI driven Workflow that allow changes to the contends of the repository. The order is relevant for defining the precedence of the workflows. When empty, the repository does not support any edits (eg, readonly) */
- workflows: ('branch' | 'write')[];
+ workflows: RepoWorkflows;
};
export type HealthStatus = {
/** When the health was checked last time */
@@ -1258,6 +1258,7 @@ export type WebhookResponse = {
/** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
kind?: string;
};
+export type RepoWorkflows = ('branch' | 'write')[]
export type RepositoryView = {
/** For git, this is the target branch */
branch?: string;
@@ -1281,7 +1282,7 @@ export type RepositoryView = {
- `"local"` */
type: 'bitbucket' | 'git' | 'github' | 'gitlab' | 'local';
/** The supported workflows */
- workflows: ('branch' | 'write')[];
+ workflows: RepoWorkflows;
};
export type RepositoryViewList = {
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
diff --git a/public/app/core/components/NestedFolderPicker/FolderRepo.tsx b/public/app/core/components/NestedFolderPicker/FolderRepo.tsx
index b4d8c2bcf26..be50e30b5be 100644
--- a/public/app/core/components/NestedFolderPicker/FolderRepo.tsx
+++ b/public/app/core/components/NestedFolderPicker/FolderRepo.tsx
@@ -1,6 +1,7 @@
import { t } from '@grafana/i18n';
-import { Badge } from '@grafana/ui';
-import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance';
+import { Badge, Stack } from '@grafana/ui';
+import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
+import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository';
import { NestedFolderDTO } from 'app/features/search/service/types';
import { FolderDTO, FolderListItemDTO } from 'app/types/folders';
@@ -9,11 +10,31 @@ export interface Props {
}
export function FolderRepo({ folder }: Props) {
- const isProvisionedInstance = useIsProvisionedInstance();
+ // skip rendering if:
+ // folder is not present
+ // folder have parentUID
+ // folder is not managed
+ const skipRender = !folder || ('parentUID' in folder && folder.parentUID) || !folder.managedBy;
- if (!folder || ('parentUID' in folder && folder.parentUID) || !folder.managedBy || isProvisionedInstance) {
+ const { isReadOnlyRepo, repoType } = useGetResourceRepositoryView({
+ folderName: skipRender ? undefined : folder?.uid,
+ });
+
+ if (skipRender) {
return null;
}
- return ;
+ return (
+ // badge with text and icon only has different height, we will need to adjust the layout using stretch
+
+ {isReadOnlyRepo && (
+
+ )}
+
+
+ );
}
diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
index 1574433972d..038a32d1af6 100644
--- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
+++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
@@ -16,6 +16,7 @@ import { FolderRepo } from '../../core/components/NestedFolderPicker/FolderRepo'
import { contextSrv } from '../../core/services/context_srv';
import { ManagerKind } from '../apiserver/types';
import { buildNavModel, getDashboardsTabID } from '../folders/state/navModel';
+import { useGetResourceRepositoryView } from '../provisioning/hooks/useGetResourceRepositoryView';
import { useSearchStateManager } from '../search/state/SearchStateManager';
import { getSearchPlaceholder } from '../search/tempI18nPhrases';
@@ -41,6 +42,7 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record new URLSearchParams(location.search), [location.search]);
+ const { isReadOnlyRepo, repoType } = useGetResourceRepositoryView({ folderName: folderUID });
useEffect(() => {
stateManager.initStateFromUrl(folderUID);
@@ -109,6 +111,7 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record {
if (folderDTO) {
@@ -160,12 +163,14 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: RecordRecently deleted
)}
- {folderDTO && }
+ {folderDTO && }
{(canCreateDashboards || canCreateFolders) && (
)}
>
diff --git a/public/app/features/browse-dashboards/components/BrowseActions/useSelectionRepoValidation.ts b/public/app/features/browse-dashboards/components/BrowseActions/useSelectionRepoValidation.ts
index 30ee0957c17..cde7e9d56e5 100644
--- a/public/app/features/browse-dashboards/components/BrowseActions/useSelectionRepoValidation.ts
+++ b/public/app/features/browse-dashboards/components/BrowseActions/useSelectionRepoValidation.ts
@@ -1,3 +1,8 @@
+import { skipToken } from '@reduxjs/toolkit/query';
+
+import { config } from '@grafana/runtime';
+import { useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1';
+import { getIsReadOnlyRepo } from 'app/features/provisioning/utils/repository';
import { useSelector } from 'app/types/store';
import { useChildrenByParentUIDState, rootItemsSelector } from '../../state/hooks';
@@ -7,9 +12,19 @@ import { getItemRepositoryUid } from '../utils';
// This hook is responsible for validating if all selected resources (dashboard folders and dashboards) are in the same repository
export function useSelectionRepoValidation(selectedItems: Omit) {
+ const provisioningEnabled = config.featureToggles.provisioning;
const childrenByParentUID = useChildrenByParentUIDState();
const rootItems = useSelector(rootItemsSelector)?.items ?? [];
+ const { data: settingsData } = useGetFrontendSettingsQuery(!provisioningEnabled ? skipToken : undefined);
+ // Function to grab repository configuration by UID
+ const getRepositoryByUid = (repoUid: string) => {
+ if (!settingsData?.items || repoUid === 'non_provisioned') {
+ return undefined;
+ }
+ return settingsData.items.find((repo) => repo.name === repoUid);
+ };
+
const getRepoUid = (uid: string) => {
const item = findItem(rootItems, childrenByParentUID, uid);
return item ? getItemRepositoryUid(item, rootItems, childrenByParentUID) : 'non_provisioned';
@@ -26,10 +41,15 @@ export function useSelectionRepoValidation(selectedItems: Omit 1;
const isInLockedRepo = (uid: string) => !selectedItemsRepoUID || getRepoUid(uid) === selectedItemsRepoUID;
+ const isUidInReadOnlyRepo = (uid: string) => {
+ const repo = getRepositoryByUid(getRepoUid(uid));
+ return repo ? getIsReadOnlyRepo(repo) : false;
+ };
return {
selectedItemsRepoUID,
isInLockedRepo,
isCrossRepo, // true if items are from different repositories
+ isUidInReadOnlyRepo,
};
}
diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.test.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.test.tsx
index 65b6686ef14..2a5be2aa309 100644
--- a/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.test.tsx
+++ b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.test.tsx
@@ -118,6 +118,7 @@ describe('BulkDeleteProvisionedResource', () => {
selectedItemsRepoUID: 'test-folder',
isInLockedRepo: jest.fn().mockReturnValue(false),
isCrossRepo: false,
+ isUidInReadOnlyRepo: jest.fn().mockReturnValue(false),
});
});
diff --git a/public/app/features/browse-dashboards/components/CheckboxCell.tsx b/public/app/features/browse-dashboards/components/CheckboxCell.tsx
index 7867510d940..736e98bbf44 100644
--- a/public/app/features/browse-dashboards/components/CheckboxCell.tsx
+++ b/public/app/features/browse-dashboards/components/CheckboxCell.tsx
@@ -5,6 +5,7 @@ import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
import { Checkbox, Tooltip, useStyles2 } from '@grafana/ui';
import { ManagerKind } from 'app/features/apiserver/types';
+import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository';
import { useSelector } from 'app/types/store';
import { DashboardsTreeCellProps, SelectionState } from '../types';
@@ -22,7 +23,7 @@ export default function CheckboxCell({
// Get current selection state for repository validation
const selectedItems = useSelector((state) => state.browseDashboards.selectedItems);
- const { selectedItemsRepoUID, isInLockedRepo } = useSelectionRepoValidation(selectedItems);
+ const { selectedItemsRepoUID, isInLockedRepo, isUidInReadOnlyRepo } = useSelectionRepoValidation(selectedItems);
// Early returns for cases where we should show a spacer instead of checkbox
if (!isSelected) {
@@ -46,11 +47,23 @@ export default function CheckboxCell({
return ;
}
+ if ((permissions && permissions.isReadOnlyRepo) || isUidInReadOnlyRepo(item.uid)) {
+ // When the folder is read-only (inherited from repository), disable checkbox with tooltip
+ return (
+
+
+
+
+
+ );
+ }
+
// Check if user can edit this specific item type
if (permissions && !canEditItemType(item.kind, permissions)) {
return ;
}
+ // check if current item uid has different repo uid than selected items
if (selectedItemsRepoUID && !isInLockedRepo(item.uid)) {
return (
) {
}
async function renderAndOpen(folder?: FolderDTO) {
- render();
+ render();
const newButton = screen.getByText('New');
await userEvent.click(newButton);
}
@@ -42,7 +42,9 @@ describe('NewActionsButton', () => {
});
it('clicking the "New folder" button opens the drawer', async () => {
- render();
+ render(
+
+ );
const newButton = screen.getByText('New');
await userEvent.click(newButton);
@@ -55,7 +57,7 @@ describe('NewActionsButton', () => {
});
it('should only render dashboard items when folder creation is disabled', async () => {
- render();
+ render();
const newButton = screen.getByText('New');
await userEvent.click(newButton);
@@ -65,7 +67,7 @@ describe('NewActionsButton', () => {
});
it('should only render folder item when dashboard creation is disabled', async () => {
- render();
+ render();
const newButton = screen.getByText('New');
await userEvent.click(newButton);
diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.tsx
index 027916de85e..bc8b896b8ae 100644
--- a/public/app/features/browse-dashboards/components/CreateNewButton.tsx
+++ b/public/app/features/browse-dashboards/components/CreateNewButton.tsx
@@ -5,7 +5,9 @@ import { locationUtil } from '@grafana/data';
import { config, locationService, reportInteraction } from '@grafana/runtime';
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
+import { RepoType } from 'app/features/provisioning/Wizard/types';
import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance';
+import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository';
import {
getImportPhrase,
getNewDashboardPhrase,
@@ -24,9 +26,17 @@ interface Props {
parentFolder?: FolderDTO;
canCreateFolder: boolean;
canCreateDashboard: boolean;
+ isReadOnlyRepo: boolean;
+ repoType?: RepoType;
}
-export default function CreateNewButton({ parentFolder, canCreateDashboard, canCreateFolder }: Props) {
+export default function CreateNewButton({
+ parentFolder,
+ canCreateDashboard,
+ canCreateFolder,
+ isReadOnlyRepo,
+ repoType,
+}: Props) {
const [isOpen, setIsOpen] = useState(false);
const location = useLocation();
const [newFolder] = useNewFolderMutation();
@@ -94,7 +104,11 @@ export default function CreateNewButton({ parentFolder, canCreateDashboard, canC
return (
<>
-