From b5fca60bdb0f477f2785da90cda70777e5d6b6c1 Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Thu, 7 Aug 2025 09:19:04 -0400 Subject: [PATCH] Provisioning Folder: Bugfix cancel action drawer should not reset selection state; Read only alert reoganize (#109087) * Read only message on drawer, fix cancel drawer reset selection state. * Bugfix when select provisioned sub items from root, we are missing repo info --- .../BrowseActions/BrowseActions.tsx | 2 - .../BulkDeleteProvisionedResource.test.tsx | 4 +- .../BulkDeleteProvisionedResource.tsx | 9 ++-- .../BulkMoveProvisionedResource.tsx | 10 ++-- .../BulkActions/RepoInvalidStateBanner.tsx | 44 +++++++++++++++++ .../BulkActions/useFolderNameFromSelection.ts | 49 +++++++++++++++++++ .../DeleteProvisionedFolderForm.test.tsx | 1 + .../DeleteProvisionedFolderForm.tsx | 16 ++++-- .../NewProvisionedFolderForm.test.tsx | 9 ++-- .../components/NewProvisionedFolderForm.tsx | 16 ++++-- .../hooks/useProvisionedFolderFormData.ts | 4 +- .../SaveProvisionedDashboardForm.tsx | 25 +++++----- .../saving/provisioned/hooks.ts | 5 +- .../DeleteProvisionedDashboardForm.tsx | 19 +++---- .../hooks/useGetResourceRepositoryView.ts | 13 +++-- .../features/provisioning/utils/repository.ts | 10 ++++ public/locales/en-US/grafana.json | 19 +++++-- 17 files changed, 197 insertions(+), 58 deletions(-) create mode 100644 public/app/features/browse-dashboards/components/BulkActions/RepoInvalidStateBanner.tsx create mode 100644 public/app/features/browse-dashboards/components/BulkActions/useFolderNameFromSelection.ts create mode 100644 public/app/features/provisioning/utils/repository.ts 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({
{readOnly && ( - - - If you have direct access to the target, copy the JSON and paste it there. - - + )} {isNew && ( diff --git a/public/app/features/dashboard-scene/saving/provisioned/hooks.ts b/public/app/features/dashboard-scene/saving/provisioned/hooks.ts index 8edec0ec7a2..1b87fc88be3 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/hooks.ts +++ b/public/app/features/dashboard-scene/saving/provisioned/hooks.ts @@ -4,6 +4,7 @@ import { RepositoryView } from 'app/api/clients/provisioning/v0alpha1'; import { useUrlParams } from 'app/core/navigation/hooks'; import { AnnoKeyManagerIdentity, AnnoKeyManagerKind, AnnoKeySourcePath } from 'app/features/apiserver/types'; import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView'; +import { getIsReadOnlyRepo } from 'app/features/provisioning/utils/repository'; import { DashboardMeta } from 'app/types/dashboard'; import { DashboardScene } from '../../scene/DashboardScene'; @@ -110,8 +111,6 @@ export function useProvisionedDashboardData(dashboard: DashboardScene): Provisio const { values, isNew, repository } = defaultValuesResult; const workflowOptions = getWorkflowOptions(repository, loadedFromRef); - const readOnly = !repository?.workflows?.length; - return { isReady: true, defaultValues: values, @@ -119,7 +118,7 @@ export function useProvisionedDashboardData(dashboard: DashboardScene): Provisio loadedFromRef, workflowOptions, isNew, - readOnly, + readOnly: getIsReadOnlyRepo(repository), isLoading, setIsLoading, }; diff --git a/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx index 9d101b2f8c5..05ffdaf91f6 100644 --- a/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx +++ b/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx @@ -4,8 +4,9 @@ import { useNavigate } from 'react-router-dom-v5-compat'; import { AppEvents } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { getAppEvents } from '@grafana/runtime'; -import { Alert, Button, Drawer, Stack } from '@grafana/ui'; +import { Button, Drawer, Stack } from '@grafana/ui'; import { RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; +import { RepoInvalidStateBanner } from 'app/features/browse-dashboards/components/BulkActions/RepoInvalidStateBanner'; import { PROVISIONING_URL } from 'app/features/provisioning/constants'; import { ResourceEditFormSharedFields } from '../components/Provisioned/ResourceEditFormSharedFields'; @@ -118,17 +119,11 @@ export function DeleteProvisionedDashboardForm({ {readOnly && ( - - - This dashboard cannot be deleted directly from Grafana because the repository is read-only. To delete - this dashboard, please remove the file from your Git repository. - - + )} repo.target === 'instance'); @@ -52,6 +55,7 @@ export const useGetResourceRepositoryView = ({ name, folderName }: GetResourceRe repository, folder, isInstanceManaged, + isReadOnlyRepo: getIsReadOnlyRepo(repository), }; } } @@ -65,6 +69,7 @@ export const useGetResourceRepositoryView = ({ name, folderName }: GetResourceRe repository, folder, isInstanceManaged, + isReadOnlyRepo: getIsReadOnlyRepo(repository), }; } @@ -77,6 +82,7 @@ export const useGetResourceRepositoryView = ({ name, folderName }: GetResourceRe repository, folder, isInstanceManaged, + isReadOnlyRepo: getIsReadOnlyRepo(repository), }; } } @@ -86,5 +92,6 @@ export const useGetResourceRepositoryView = ({ name, folderName }: GetResourceRe repository: instanceRepo, folder, isInstanceManaged, + isReadOnlyRepo: getIsReadOnlyRepo(instanceRepo), }; }; diff --git a/public/app/features/provisioning/utils/repository.ts b/public/app/features/provisioning/utils/repository.ts new file mode 100644 index 00000000000..4e33d5e3922 --- /dev/null +++ b/public/app/features/provisioning/utils/repository.ts @@ -0,0 +1,10 @@ +import { RepositoryView } from 'app/api/clients/provisioning/v0alpha1'; + +export function getIsReadOnlyRepo(repository: RepositoryView | undefined): boolean { + if (!repository) { + return false; + } + + // Repository is consider read-only if it has no workflows defined (workflows are required for write operations) + return repository.workflows.length === 0; +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 2c040613239..06a31f3f782 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3539,6 +3539,13 @@ "button-move": "Move", "button-moving": "Moving...", "deleting": "Deleting", + "error": { + "read-only-message": "If you have direct access to the target, please make modifications directly in the target repository.", + "read-only-saving-message": "Repository is read-only and provisioned in git. {{readOnlyMessage}}", + "read-only-title": "This repository is read only", + "repository-not-found-message": "The repository for the selected folder could not be found. Please ensure that the folder is provisioned correctly.", + "repository-not-found-title": "Repository not found" + }, "error-file-content-not-found": "File content not found", "error-path-not-found": "Path not found", "error-target-folder-path-missing": "Target folder path is missing", @@ -3568,6 +3575,9 @@ "select-checkbox": "Select", "tags-column": "Tags" }, + "delete-folder": { + "read-only-message": "To delete this folder, please remove the folder from your repository." + }, "delete-provisioned-folder-form": { "api-error": "Failed to delete folder", "button-cancel": "Cancel", @@ -3611,6 +3621,9 @@ "name-cell": { "no-items": "No items" }, + "new-folder": { + "read-only-message": "To create this folder, please add the resource in your repository directly." + }, "new-folder-form": { "cancel-label": "Cancel", "create-label": "Create", @@ -5691,11 +5704,9 @@ "api-error": "Failed to delete dashboard", "cancel-action": "Cancel", "delete-action": "Delete dashboard", - "delete-read-only-file-message": "This dashboard cannot be deleted directly from Grafana because the repository is read-only. To delete this dashboard, please remove the file from your Git repository.", "deleting": "Deleting...", "drawer-title": "Delete Provisioned Dashboard", - "success-message": "Dashboard deleted successfully", - "title-this-repository-is-read-only": "This repository is read only" + "success-message": "Dashboard deleted successfully" }, "description-label": { "description": "Description" @@ -6067,7 +6078,6 @@ "api-error": "Error saving dashboard", "cancel": "Cancel", "cannot-be-saved": "This dashboard cannot be saved from the Grafana UI because it has been provisioned from another source. Copy the JSON or save it to a file below, then you can update your dashboard in the provisioning source.", - "copy-json-message": "If you have direct access to the target, copy the JSON and paste it there.", "copy-json-to-clipboard": "Copy JSON to clipboard", "file-path": "<0>File path: {{filePath}}", "label-description": "Description", @@ -6079,7 +6089,6 @@ "see-docs": "See <2>documentation for more information about provisioning.", "title-required": "Dashboard title is required", "title-same-as-folder": "Dashboard name cannot be the same as the folder name", - "title-this-repository-is-read-only": "This repository is read only", "title-validation-failed": "Dashboard title validation failed." }, "scenes-new-rule-from-panel-button": {