diff --git a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
index ca3952427e6..5e538d6e91b 100644
--- a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
+++ b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
@@ -148,7 +148,6 @@ export function BrowseActions({ folderDTO }: Props) {
folderUid={folderDTO?.uid || ''}
onDismiss={() => {
setShowBulkDeleteProvisionedResource(false);
- onActionComplete();
}}
/>
@@ -166,7 +165,6 @@ export function BrowseActions({ folderDTO }: Props) {
folderUid={folderDTO?.uid}
onDismiss={() => {
setShowBulkMoveProvisionedResource(false);
- onActionComplete();
}}
/>
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 045efed7e54..b4fc6013446 100644
--- a/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.test.tsx
+++ b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.test.tsx
@@ -194,8 +194,8 @@ describe('BulkDeleteProvisionedResource', () => {
});
it('returns null when repository is not available', () => {
- const { container } = setup(null);
+ setup(null);
- expect(container.firstChild).toBeNull();
+ expect(screen.getByLabelText('Repository not found')).toBeInTheDocument();
});
});
diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx
index 14ea787fc81..8bcb95e6a2d 100644
--- a/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx
+++ b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx
@@ -25,7 +25,9 @@ import { collectSelectedItems, fetchProvisionedDashboardPath } from '../utils';
import { MoveResultFailed } from './BulkActionFailureBanner';
import { BulkActionPostSubmitStep } from './BulkActionPostSubmitStep';
import { ProgressState } from './BulkActionProgress';
+import { RepoInvalidStateBanner } from './RepoInvalidStateBanner';
import { useBulkActionRequest } from './useBulkActionRequest';
+import { useFolderNameFromSelection } from './useFolderNameFromSelection';
import {
BulkActionFormData,
BulkActionProvisionResourceProps,
@@ -202,7 +204,8 @@ export function BulkDeleteProvisionedResource({
selectedItems,
onDismiss,
}: BulkActionProvisionResourceProps) {
- const { repository, folder } = useGetResourceRepositoryView({ folderName: folderUid });
+ const folderName = useFolderNameFromSelection({ folderUid, selectedItems });
+ const { repository, folder, isReadOnlyRepo } = useGetResourceRepositoryView({ folderName });
const workflowOptions = getWorkflowOptions(repository);
const folderPath = folder?.metadata?.annotations?.[AnnoKeySourcePath] || '';
@@ -214,8 +217,8 @@ export function BulkDeleteProvisionedResource({
workflow: getDefaultWorkflow(repository),
};
- if (!repository) {
- return null;
+ if (!repository || isReadOnlyRepo) {
+ return ;
}
return (
diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkMoveProvisionedResource.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkMoveProvisionedResource.tsx
index a45ad80eabe..883ec38c578 100644
--- a/public/app/features/browse-dashboards/components/BulkActions/BulkMoveProvisionedResource.tsx
+++ b/public/app/features/browse-dashboards/components/BulkActions/BulkMoveProvisionedResource.tsx
@@ -30,7 +30,9 @@ import { collectSelectedItems, fetchProvisionedDashboardPath } from '../utils';
import { MoveResultFailed } from './BulkActionFailureBanner';
import { BulkActionPostSubmitStep } from './BulkActionPostSubmitStep';
import { ProgressState } from './BulkActionProgress';
+import { RepoInvalidStateBanner } from './RepoInvalidStateBanner';
import { useBulkActionRequest } from './useBulkActionRequest';
+import { useFolderNameFromSelection } from './useFolderNameFromSelection';
import {
BulkActionFormData,
BulkActionProvisionResourceProps,
@@ -39,6 +41,7 @@ import {
getResourceTargetPath,
MoveResultSuccessState,
} from './utils';
+
interface FormProps extends BulkActionProvisionResourceProps {
initialValues: BulkActionFormData;
repository: RepositoryView;
@@ -277,7 +280,8 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions
}
export function BulkMoveProvisionedResource({ folderUid, selectedItems, onDismiss }: BulkActionProvisionResourceProps) {
- const { repository, folder } = useGetResourceRepositoryView({ folderName: folderUid });
+ const folderName = useFolderNameFromSelection({ folderUid, selectedItems });
+ const { repository, folder, isReadOnlyRepo } = useGetResourceRepositoryView({ folderName });
const workflowOptions = getWorkflowOptions(repository);
const folderPath = folder?.metadata?.annotations?.[AnnoKeySourcePath] || '';
@@ -289,8 +293,8 @@ export function BulkMoveProvisionedResource({ folderUid, selectedItems, onDismis
workflow: getDefaultWorkflow(repository),
};
- if (!repository) {
- return null;
+ if (!repository || isReadOnlyRepo) {
+ return ;
}
return (
diff --git a/public/app/features/browse-dashboards/components/BulkActions/RepoInvalidStateBanner.tsx b/public/app/features/browse-dashboards/components/BulkActions/RepoInvalidStateBanner.tsx
new file mode 100644
index 00000000000..10381234514
--- /dev/null
+++ b/public/app/features/browse-dashboards/components/BulkActions/RepoInvalidStateBanner.tsx
@@ -0,0 +1,44 @@
+import { Trans, t } from '@grafana/i18n';
+import { Alert } from '@grafana/ui';
+
+interface Props {
+ noRepository: boolean;
+ isReadOnlyRepo: boolean;
+ readOnlyMessage?: string;
+}
+
+export function RepoInvalidStateBanner({ noRepository, isReadOnlyRepo, readOnlyMessage }: Props) {
+ if (noRepository) {
+ return (
+
+
+ The repository for the selected folder could not be found. Please ensure that the folder is provisioned
+ correctly.
+
+
+ );
+ }
+
+ if (isReadOnlyRepo) {
+ return (
+
+ {readOnlyMessage
+ ? t(
+ 'browse-dashboards.bulk-move-resources-form.error.read-only-saving-message',
+ 'Repository is read-only and provisioned in git. {{readOnlyMessage}}',
+ { readOnlyMessage }
+ )
+ : t(
+ 'browse-dashboards.bulk-move-resources-form.error.read-only-message',
+ 'If you have direct access to the target, please make modifications directly in the target repository.'
+ )}
+
+ );
+ }
+
+ return null;
+}
diff --git a/public/app/features/browse-dashboards/components/BulkActions/useFolderNameFromSelection.ts b/public/app/features/browse-dashboards/components/BulkActions/useFolderNameFromSelection.ts
new file mode 100644
index 00000000000..a21b74129fa
--- /dev/null
+++ b/public/app/features/browse-dashboards/components/BulkActions/useFolderNameFromSelection.ts
@@ -0,0 +1,49 @@
+import { useMemo } from 'react';
+import { useSelector } from 'react-redux';
+
+import { ManagerKind } from 'app/features/apiserver/types';
+
+import { rootItemsSelector, useChildrenByParentUIDState } from '../../state/hooks';
+import { findItem } from '../../state/utils';
+import { DashboardTreeSelection } from '../../types';
+
+// This hook retrieves the folder UID from the selection state. Because search endpoint currently does not return resource metadata
+// NOTE: This is a temporary workaround until the search endpoint is updated
+interface Props {
+ folderUid?: string;
+ selectedItems: Omit;
+}
+export function useFolderNameFromSelection({ folderUid, selectedItems }: Props) {
+ const rootItems = useSelector(rootItemsSelector);
+ const childrenByParentUID = useChildrenByParentUIDState();
+
+ return useMemo(() => {
+ // if we already have a folderUid, return it;
+ if (folderUid) {
+ return folderUid;
+ }
+
+ // Helper to walk up tree and find provisioned folder
+ const findProvisionedParent = (itemUid: string): string | undefined => {
+ const item = findItem(rootItems?.items || [], childrenByParentUID, itemUid);
+ if (!item) {
+ return undefined;
+ }
+
+ if (item.managedBy === ManagerKind.Repo) {
+ return item.uid;
+ }
+ if (item.parentUID) {
+ return findProvisionedParent(item.parentUID);
+ }
+ return undefined;
+ };
+
+ // Try folders, then dashboards
+ const firstSelectedUid =
+ Object.keys(selectedItems.folder).find((uid) => selectedItems.folder[uid]) ||
+ Object.keys(selectedItems.dashboard).find((uid) => selectedItems.dashboard[uid]);
+
+ return firstSelectedUid ? findProvisionedParent(firstSelectedUid) : undefined;
+ }, [folderUid, selectedItems, rootItems, childrenByParentUID]);
+}
diff --git a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx
index a3ddd150785..23108ee102c 100644
--- a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx
+++ b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx
@@ -134,6 +134,7 @@ const defaultHookData: ProvisionedFolderFormDataResult = {
repository: mockRepository,
folder: mockFolder,
initialValues: mockFormData,
+ isReadOnlyRepo: false,
};
function setup(
diff --git a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx
index f6fb26101f8..87f1d6bca1e 100644
--- a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx
+++ b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx
@@ -20,6 +20,7 @@ import { FolderDTO } from 'app/types/folders';
import { useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData';
import { DescendantCount } from './BrowseActions/DescendantCount';
+import { RepoInvalidStateBanner } from './BulkActions/RepoInvalidStateBanner';
import { getFolderURL } from './utils';
interface FormProps extends DeleteProvisionedFolderFormProps {
@@ -150,14 +151,23 @@ function FormContent({ initialValues, parentFolder, repository, workflowOptions,
}
export function DeleteProvisionedFolderForm({ parentFolder, onDismiss }: DeleteProvisionedFolderFormProps) {
- const { workflowOptions, repository, folder, initialValues } = useProvisionedFolderFormData({
+ const { workflowOptions, repository, folder, initialValues, isReadOnlyRepo } = useProvisionedFolderFormData({
folderUid: parentFolder?.uid,
action: 'delete',
title: parentFolder?.title,
});
- if (!initialValues) {
- return null;
+ if (isReadOnlyRepo || !initialValues) {
+ return (
+
+ );
}
return (
diff --git a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx
index 2242a8965ae..9ee6d728426 100644
--- a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx
+++ b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx
@@ -131,6 +131,7 @@ const mockHookData: ProvisionedFolderFormDataResult = {
workflows: ['write', 'branch'],
target: 'folder',
},
+ isReadOnlyRepo: false,
folder: {
metadata: {
annotations: {
@@ -188,18 +189,18 @@ describe('NewProvisionedFolderForm', () => {
});
it('should return null when initialValues is not available', () => {
- const { container } = setup(
+ setup(
{},
{
...mockHookData,
initialValues: undefined,
}
);
- expect(container.firstChild).toBeNull();
+ expect(screen.getByLabelText('Repository not found')).toBeInTheDocument();
});
it('should show error when repository is not found', () => {
- const { container } = setup(
+ setup(
{},
{
...mockHookData,
@@ -207,7 +208,7 @@ describe('NewProvisionedFolderForm', () => {
initialValues: undefined,
}
);
- expect(container.firstChild).toBeNull();
+ expect(screen.getByLabelText('Repository not found')).toBeInTheDocument();
});
it('should show branch field when branch workflow is selected', async () => {
diff --git a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx
index 797f4bb98ba..bd452e3ba72 100644
--- a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx
+++ b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx
@@ -22,6 +22,7 @@ import { FolderDTO } from 'app/types/folders';
import { useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData';
+import { RepoInvalidStateBanner } from './BulkActions/RepoInvalidStateBanner';
import { validateFolderName } from './NewFolderForm';
import { formatFolderName, hasFolderNameCharactersToReplace } from './utils';
@@ -213,14 +214,23 @@ function FormContent({ initialValues, repository, workflowOptions, folder, onDis
}
export function NewProvisionedFolderForm({ parentFolder, onDismiss }: Props) {
- const { workflowOptions, repository, folder, initialValues } = useProvisionedFolderFormData({
+ const { workflowOptions, repository, folder, initialValues, isReadOnlyRepo } = useProvisionedFolderFormData({
folderUid: parentFolder?.uid,
action: 'create',
title: '', // Empty title for new folders
});
- if (!initialValues) {
- return null;
+ if (isReadOnlyRepo || !initialValues) {
+ return (
+
+ );
}
return (
diff --git a/public/app/features/browse-dashboards/hooks/useProvisionedFolderFormData.ts b/public/app/features/browse-dashboards/hooks/useProvisionedFolderFormData.ts
index 8ea8c4835f0..342808e6520 100644
--- a/public/app/features/browse-dashboards/hooks/useProvisionedFolderFormData.ts
+++ b/public/app/features/browse-dashboards/hooks/useProvisionedFolderFormData.ts
@@ -20,6 +20,7 @@ export interface ProvisionedFolderFormDataResult {
folder?: Folder;
workflowOptions: Array<{ label: string; value: string }>;
initialValues?: BaseProvisionedFormData;
+ isReadOnlyRepo: boolean;
}
/**
@@ -30,7 +31,7 @@ export function useProvisionedFolderFormData({
action,
title,
}: UseProvisionedFolderFormDataProps): ProvisionedFolderFormDataResult {
- const { repository, folder, isLoading } = useGetResourceRepositoryView({ folderName: folderUid });
+ const { repository, folder, isLoading, isReadOnlyRepo } = useGetResourceRepositoryView({ folderName: folderUid });
const workflowOptions = getWorkflowOptions(repository);
const timestamp = generateTimestamp();
@@ -56,5 +57,6 @@ export function useProvisionedFolderFormData({
folder,
workflowOptions,
initialValues,
+ isReadOnlyRepo,
};
}
diff --git a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx
index 260c491bb57..d69c9b6984c 100644
--- a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx
+++ b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx
@@ -6,11 +6,12 @@ import { AppEvents, locationUtil } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { getAppEvents, locationService } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
-import { Alert, Button, Field, Input, Stack, TextArea } from '@grafana/ui';
+import { Button, Field, Input, Stack, TextArea } from '@grafana/ui';
import { RepositoryView } from 'app/api/clients/provisioning/v0alpha1';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import kbn from 'app/core/utils/kbn';
import { Resource } from 'app/features/apiserver/types';
+import { RepoInvalidStateBanner } from 'app/features/browse-dashboards/components/BulkActions/RepoInvalidStateBanner';
import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv';
import { PROVISIONING_URL } from 'app/features/provisioning/constants';
import { useCreateOrUpdateRepositoryFile } from 'app/features/provisioning/hooks/useCreateOrUpdateRepositoryFile';
@@ -60,7 +61,7 @@ export function SaveProvisionedDashboardForm({
reset(defaultValues);
}, [defaultValues, reset]);
- const onRequestError = (error: unknown, info: ProvisionedOperationInfo) => {
+ const onRequestError = (error: unknown) => {
appEvents.publish({
type: AppEvents.alertError.name,
payload: [t('dashboard-scene.save-provisioned-dashboard-form.api-error', 'Error saving dashboard'), error],
@@ -80,6 +81,7 @@ export function SaveProvisionedDashboardForm({
};
const onWriteSuccess = (_: ProvisionedOperationInfo, upsert: Resource) => {
+ handleDismiss();
if (isNew && upsert?.metadata.name) {
handleNewDashboard(upsert);
} else {
@@ -91,6 +93,7 @@ export function SaveProvisionedDashboardForm({
};
const onBranchSuccess = (ref: string, path: string, info: ProvisionedOperationInfo, upsert: Resource) => {
+ handleDismiss();
if (isNew && upsert?.metadata?.name) {
handleNewDashboard(upsert);
} else {
@@ -104,7 +107,7 @@ export function SaveProvisionedDashboardForm({
}
};
- const onDismiss = () => {
+ const handleDismiss = () => {
dashboard.setState({ isDirty: false });
panelEditor?.onDiscard();
drawer.onClose();
@@ -118,7 +121,6 @@ export function SaveProvisionedDashboardForm({
onBranchSuccess: ({ ref, path }, info, resource) => onBranchSuccess(ref, path, info, resource),
onWriteSuccess,
onError: onRequestError,
- onDismiss,
},
});
@@ -157,16 +159,11 @@ export function SaveProvisionedDashboardForm({