Provisioned Resource Read Only: When repo is read only, disable action buttons and display badge (#109494)

* When repo is read only, disable action buttons and display badge

* browse dashboards page, disable checkbox if repo is read only

* clean up

* clean up

* i18n

* added read only status to repository page

* i18n

* fix

* readonly tooltip added local provisioning message

* i18n
This commit is contained in:
Yunwen Zheng
2025-08-13 09:08:53 +02:00
committed by GitHub
parent 85166512cb
commit 2ecc076bbf
17 changed files with 192 additions and 28 deletions
@@ -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 */
@@ -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 <Badge color="purple" icon="exchange-alt" tooltip={t('folder-repo.badge-tooltip', 'Provisioned')} />;
return (
// badge with text and icon only has different height, we will need to adjust the layout using stretch
<Stack direction="row" alignItems="stretch">
{isReadOnlyRepo && (
<Badge
color="darkgrey"
text={t('folder-repo.read-only-badge', 'Read only')}
tooltip={getReadOnlyTooltipText({ isLocal: repoType === 'local' })}
/>
)}
<Badge color="purple" icon="exchange-alt" tooltip={t('folder-repo.provisioned-badge', 'Provisioned')} />
</Stack>
);
}
@@ -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<string
const isSearching = stateManager.hasSearchFilters();
const location = useLocation();
const search = useMemo(() => 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<string
canEditDashboards,
canDeleteFolders,
canDeleteDashboards,
isReadOnlyRepo,
};
const onEditTitle = async (newValue: string) => {
if (folderDTO) {
@@ -160,12 +163,14 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
<Trans i18nKey="browse-dashboards.actions.button-to-recently-deleted">Recently deleted</Trans>
</LinkButton>
)}
{folderDTO && <FolderActionsButton folder={folderDTO} />}
{folderDTO && <FolderActionsButton folder={folderDTO} repoType={repoType} isReadOnlyRepo={isReadOnlyRepo} />}
{(canCreateDashboards || canCreateFolders) && (
<CreateNewButton
parentFolder={folderDTO}
canCreateDashboard={canCreateDashboards}
canCreateFolder={canCreateFolders}
repoType={repoType}
isReadOnlyRepo={isReadOnlyRepo}
/>
)}
</>
@@ -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<DashboardTreeSelection, 'panel' | '$all'>) {
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<DashboardTreeSele
const isCrossRepo = new Set(repoUIDs).size > 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,
};
}
@@ -118,6 +118,7 @@ describe('BulkDeleteProvisionedResource', () => {
selectedItemsRepoUID: 'test-folder',
isInLockedRepo: jest.fn().mockReturnValue(false),
isCrossRepo: false,
isUidInReadOnlyRepo: jest.fn().mockReturnValue(false),
});
});
@@ -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 <CheckboxSpacer />;
}
if ((permissions && permissions.isReadOnlyRepo) || isUidInReadOnlyRepo(item.uid)) {
// When the folder is read-only (inherited from repository), disable checkbox with tooltip
return (
<Tooltip content={getReadOnlyTooltipText({})}>
<span>
<Checkbox disabled value={false} />
</span>
</Tooltip>
);
}
// Check if user can edit this specific item type
if (permissions && !canEditItemType(item.kind, permissions)) {
return <CheckboxSpacer />;
}
// check if current item uid has different repo uid than selected items
if (selectedItemsRepoUID && !isInLockedRepo(item.uid)) {
return (
<Tooltip
@@ -3,11 +3,17 @@ import { Checkbox } from '@grafana/ui';
import { DashboardTreeHeaderProps, SelectionState } from '../types';
export default function CheckboxHeaderCell({ isSelected, onAllSelectionChange }: DashboardTreeHeaderProps) {
export default function CheckboxHeaderCell({
isSelected,
onAllSelectionChange,
permissions,
}: DashboardTreeHeaderProps) {
const state = isSelected?.('$all') ?? SelectionState.Unselected;
const isReadOnlyRepo = permissions?.isReadOnlyRepo;
return (
<Checkbox
disabled={isReadOnlyRepo}
value={state === SelectionState.Selected}
indeterminate={state === SelectionState.Mixed}
aria-label={t('browse-dashboards.dashboards-tree.select-all-header-checkbox', 'Select all')}
@@ -15,7 +15,7 @@ function render(...[ui, options]: Parameters<typeof rtlRender>) {
}
async function renderAndOpen(folder?: FolderDTO) {
render(<CreateNewButton canCreateDashboard canCreateFolder parentFolder={folder} />);
render(<CreateNewButton canCreateDashboard canCreateFolder parentFolder={folder} isReadOnlyRepo={false} />);
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(<CreateNewButton canCreateDashboard canCreateFolder parentFolder={mockParentFolder} />);
render(
<CreateNewButton canCreateDashboard canCreateFolder parentFolder={mockParentFolder} isReadOnlyRepo={false} />
);
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(<CreateNewButton canCreateDashboard canCreateFolder={false} />);
render(<CreateNewButton canCreateDashboard canCreateFolder={false} isReadOnlyRepo={false} />);
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(<CreateNewButton canCreateDashboard={false} canCreateFolder />);
render(<CreateNewButton canCreateDashboard={false} canCreateFolder isReadOnlyRepo={false} />);
const newButton = screen.getByText('New');
await userEvent.click(newButton);
@@ -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 (
<>
<Dropdown overlay={newMenu} onVisibleChange={setIsOpen}>
<Button>
<Button
disabled={isReadOnlyRepo}
tooltip={isReadOnlyRepo ? getReadOnlyTooltipText({ isLocal: repoType === 'local' }) : undefined}
variant="secondary"
>
{getNewPhrase()}
<Icon name={isOpen ? 'angle-up' : 'angle-down'} />
</Button>
@@ -6,6 +6,8 @@ import { locationService, reportInteraction } from '@grafana/runtime';
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
import { Permissions } from 'app/core/components/AccessControl';
import { appEvents } from 'app/core/core';
import { RepoType } from 'app/features/provisioning/Wizard/types';
import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository';
import { ShowModalReactEvent } from 'app/types/events';
import { FolderDTO } from 'app/types/folders';
@@ -20,9 +22,11 @@ import { DeleteProvisionedFolderForm } from './DeleteProvisionedFolderForm';
interface Props {
folder: FolderDTO;
isReadOnlyRepo?: boolean;
repoType?: RepoType;
}
export function FolderActionsButton({ folder }: Props) {
export function FolderActionsButton({ folder, repoType, isReadOnlyRepo }: Props) {
const [isOpen, setIsOpen] = useState(false);
const [showPermissionsDrawer, setShowPermissionsDrawer] = useState(false);
const [showDeleteProvisionedFolderDrawer, setShowDeleteProvisionedFolderDrawer] = useState(false);
@@ -137,7 +141,11 @@ export function FolderActionsButton({ folder }: Props) {
return (
<>
<Dropdown overlay={menu} onVisibleChange={setIsOpen}>
<Button variant="secondary">
<Button
variant="secondary"
disabled={isReadOnlyRepo}
tooltip={isReadOnlyRepo ? getReadOnlyTooltipText({ isLocal: repoType === 'local' }) : undefined}
>
<Trans i18nKey="browse-dashboards.folder-actions-button.folder-actions">Folder actions</Trans>
<Icon name={isOpen ? 'angle-up' : 'angle-down'} />
</Button>
@@ -67,4 +67,5 @@ export interface BrowseDashboardsPermissions {
canEditDashboards: boolean;
canDeleteFolders?: boolean;
canDeleteDashboards?: boolean;
isReadOnlyRepo?: boolean;
}
@@ -5,7 +5,17 @@ import { GrafanaTheme2, store } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { config, locationService } from '@grafana/runtime';
import { Button, ButtonGroup, Dropdown, Icon, Menu, ToolbarButton, ToolbarButtonRow, useStyles2 } from '@grafana/ui';
import {
Badge,
Button,
ButtonGroup,
Dropdown,
Icon,
Menu,
ToolbarButton,
ToolbarButtonRow,
useStyles2,
} from '@grafana/ui';
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator';
import grafanaConfig from 'app/core/config';
@@ -13,6 +23,8 @@ import { LS_PANEL_COPY_KEY } from 'app/core/constants';
import { contextSrv } from 'app/core/core';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository';
import { useSelector } from 'app/types/store';
import { shareDashboardType } from '../../dashboard/components/ShareModal/utils';
@@ -75,6 +87,10 @@ export function ToolbarActions({ dashboard }: Props) {
const isEditingAndShowingDashboard = isEditing && isShowingDashboard;
const folderRepo = useSelector((state) => selectFolderRepository()(state, meta.folderUid));
const isManaged = Boolean(dashboard.isManagedRepository() || folderRepo);
// Get the repository for the dashboard's folder
const { isReadOnlyRepo, repoType } = useGetResourceRepositoryView({
folderName: meta.folderUid,
});
// Internal only;
// allows viewer editing without ability to save
@@ -118,6 +134,22 @@ export function ToolbarActions({ dashboard }: Props) {
},
});
if (isReadOnlyRepo) {
toolbarActions.push({
group: 'icon-actions',
condition: true,
render: () => {
return (
<Badge
color="darkgrey"
text={t('dashboard.toolbar.read-only', 'Read only')}
tooltip={getReadOnlyTooltipText({ isLocal: repoType === 'local' })}
/>
);
},
});
}
if (dashboard.isManaged() && meta.canEdit) {
toolbarActions.push({
group: 'icon-actions',
@@ -325,12 +357,17 @@ export function ToolbarActions({ dashboard }: Props) {
onClick={() => {
dashboard.onEnterEditMode();
}}
tooltip={t('dashboard.toolbar.edit.tooltip', 'Enter edit mode')}
tooltip={
isReadOnlyRepo
? getReadOnlyTooltipText({ isLocal: repoType === 'local' })
: t('dashboard.toolbar.edit.tooltip', 'Enter edit mode')
}
key="edit"
className={styles.buttonWithExtraMargin}
variant={config.featureToggles.newDashboardSharingComponent ? 'secondary' : 'primary'}
size="sm"
data-testid={selectors.components.NavToolbar.editDashboard.editButton}
disabled={isReadOnlyRepo}
>
<Trans i18nKey="dashboard.toolbar.edit.label">Edit</Trans>
</Button>
@@ -1,10 +1,11 @@
import { Trans } from '@grafana/i18n';
import { Button, LinkButton, Stack } from '@grafana/ui';
import { t, Trans } from '@grafana/i18n';
import { Badge, Button, LinkButton, Stack } from '@grafana/ui';
import { Repository } from 'app/api/clients/provisioning/v0alpha1';
import { StatusBadge } from '../Shared/StatusBadge';
import { PROVISIONING_URL } from '../constants';
import { getRepoHrefForProvider } from '../utils/git';
import { getIsReadOnlyWorkflows } from '../utils/repository';
import { getRepositoryTypeConfig } from '../utils/repositoryTypes';
import { DeleteRepositoryButton } from './DeleteRepositoryButton';
@@ -21,9 +22,11 @@ export function RepositoryActions({ repository }: RepositoryActionsProps) {
const repoType = repository.spec?.type;
const repoConfig = repoType ? getRepositoryTypeConfig(repoType) : undefined;
const providerIcon = repoConfig?.icon || 'external-link-alt';
const isReadOnlyRepo = getIsReadOnlyWorkflows(repository.spec?.workflows);
return (
<Stack>
{isReadOnlyRepo && <Badge color="darkgrey" text={t('folder-repo.read-only-badge', 'Read only')} />}
<StatusBadge repo={repository} />
{repoHref && (
<Button variant="secondary" icon={providerIcon} onClick={() => window.open(repoHref, '_blank')}>
@@ -1,12 +1,13 @@
import { ReactNode } from 'react';
import { Trans } from '@grafana/i18n';
import { Stack, Text, TextLink, Icon, Card, LinkButton } from '@grafana/ui';
import { t, Trans } from '@grafana/i18n';
import { Stack, Text, TextLink, Icon, Card, LinkButton, Badge } from '@grafana/ui';
import { Repository, ResourceCount } from 'app/api/clients/provisioning/v0alpha1';
import { RepoIcon } from '../Shared/RepoIcon';
import { StatusBadge } from '../Shared/StatusBadge';
import { PROVISIONING_URL } from '../constants';
import { getIsReadOnlyWorkflows } from '../utils/repository';
import { DeleteRepositoryButton } from './DeleteRepositoryButton';
import { SyncRepository } from './SyncRepository';
@@ -16,6 +17,7 @@ interface Props {
}
export function RepositoryCard({ repository }: Props) {
const isReadOnlyRepo = getIsReadOnlyWorkflows(repository.spec?.workflows);
const { metadata, spec, status } = repository;
const name = metadata?.name ?? '';
@@ -63,6 +65,9 @@ export function RepositoryCard({ repository }: Props) {
<Stack gap={2} direction="row" alignItems="center">
{spec?.title && <Text variant="h3">{spec.title}</Text>}
<StatusBadge repo={repository} />
{isReadOnlyRepo && (
<Badge color="darkgrey" text={t('provisioning.repository-card.read-only-badge', 'Read only')} />
)}
</Stack>
</Card.Heading>
@@ -5,6 +5,7 @@ import { Folder, useGetFolderQuery } from 'app/api/clients/folder/v1beta1';
import { RepositoryView, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1';
import { AnnoKeyManagerIdentity } from 'app/features/apiserver/types';
import { RepoType } from '../Wizard/types';
import { getIsReadOnlyRepo } from '../utils/repository';
interface GetResourceRepositoryArgs {
@@ -14,6 +15,7 @@ interface GetResourceRepositoryArgs {
interface RepositoryViewData {
repository?: RepositoryView;
repoType?: RepoType;
folder?: Folder;
isLoading?: boolean;
isInstanceManaged: boolean;
@@ -26,6 +28,7 @@ export const useGetResourceRepositoryView = ({ name, folderName }: GetResourceRe
const { data: settingsData, isLoading: isSettingsLoading } = useGetFrontendSettingsQuery(
!provisioningEnabled ? skipToken : undefined
);
const skipFolderQuery = !folderName || !provisioningEnabled;
const { data: folder, isLoading: isFolderLoading } = useGetFolderQuery(
skipFolderQuery ? skipToken : { name: folderName }
@@ -93,5 +96,6 @@ export const useGetResourceRepositoryView = ({ name, folderName }: GetResourceRe
folder,
isInstanceManaged,
isReadOnlyRepo: getIsReadOnlyRepo(instanceRepo),
repoType: instanceRepo?.type,
};
};
@@ -1,10 +1,28 @@
import { RepositoryView } from 'app/api/clients/provisioning/v0alpha1';
import { t } from '@grafana/i18n';
import { RepositoryView, RepoWorkflows } from 'app/api/clients/provisioning/v0alpha1';
export function getIsReadOnlyWorkflows(workflows?: RepoWorkflows): boolean {
// Repository is consider read-only if it has no workflows defined (workflows are required for write operations)
return workflows?.length === 0;
}
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;
return getIsReadOnlyWorkflows(repository.workflows);
}
// Right now we only support local file provisioning message and git provisioned. This can be extend in the future as needed.
export const getReadOnlyTooltipText = ({ isLocal = false }) => {
return isLocal
? t(
'provisioning.read-only-local-tooltip',
'This folder is read-only and provisioned through file provisioning. To make any changes in the folder, update the connected file repository. To modify the folder settings go to Administration > Provisioning > Repositories.'
)
: t(
'provisioning.read-only-remote-tooltip',
'This folder is read-only and provisioned through Git. To make any changes in the folder, update the connected repository. To modify the folder settings go to Administration > Provisioning > Repositories.'
);
};
+6 -1
View File
@@ -5343,6 +5343,7 @@
"playlist-next": "Go to next dashboard",
"playlist-previous": "Go to previous dashboard",
"playlist-stop": "Stop playlist",
"read-only": "Read only",
"refresh": "Refresh dashboard",
"save": "Save dashboard",
"save-dashboard": {
@@ -7505,7 +7506,8 @@
"loading": "Loading folders..."
},
"folder-repo": {
"badge-tooltip": "Provisioned"
"provisioned-badge": "Provisioned",
"read-only-badge": "Read only"
},
"folders": {
"api": {
@@ -11413,6 +11415,8 @@
"subtitle": "Use this option if you want to sync and manage your entire Grafana instance through external storage."
}
},
"read-only-local-tooltip": "This folder is read-only and provisioned through file provisioning. To make any changes in the folder, update the connected file repository. To modify the folder settings go to Administration > Provisioning > Repositories.",
"read-only-remote-tooltip": "This folder is read-only and provisioned through Git. To make any changes in the folder, update the connected repository. To modify the folder settings go to Administration > Provisioning > Repositories.",
"recent-jobs": {
"active-jobs": "active jobs",
"column-action": "Action",
@@ -11431,6 +11435,7 @@
"get-repository-meta": {
"webhook": "Webhook"
},
"read-only-badge": "Read only",
"settings": "Settings",
"view": "View"
},