Provisioning: Add ProvisioningAwareFolderPicker to prevent cross-repository folder moves (#110136)
* Added ProvisioningAwareFolderPicker component to prevent cross repo resource move
This commit is contained in:
@@ -17,7 +17,7 @@ export const getCustomRootFolderItem = ({
|
||||
managedBy,
|
||||
uid,
|
||||
}: {
|
||||
title: string;
|
||||
title?: string;
|
||||
managedBy?: ManagerKind;
|
||||
uid?: string;
|
||||
}): DashboardsTreeItem => ({
|
||||
@@ -25,7 +25,7 @@ export const getCustomRootFolderItem = ({
|
||||
level: 0,
|
||||
item: {
|
||||
kind: 'folder' as const,
|
||||
title,
|
||||
title: title || '',
|
||||
uid: uid || '',
|
||||
managedBy,
|
||||
},
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Alert, Button, Field, Modal, Text, Space } from '@grafana/ui';
|
||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||
import { Alert, Button, Field, Modal, Text, Space, Box } from '@grafana/ui';
|
||||
import { MoveActionAvailableTargetWarning } from 'app/features/provisioning/components/Shared/MoveActionAvailableTargetWarning';
|
||||
import { ProvisioningAwareFolderPicker } from 'app/features/provisioning/components/Shared/ProvisioningAwareFolderPicker';
|
||||
|
||||
import { DashboardTreeSelection } from '../../types';
|
||||
|
||||
@@ -18,7 +19,6 @@ export interface Props {
|
||||
export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Props) => {
|
||||
const [moveTarget, setMoveTarget] = useState<string>();
|
||||
const [isMoving, setIsMoving] = useState(false);
|
||||
|
||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
|
||||
const onMove = async () => {
|
||||
@@ -43,16 +43,25 @@ export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Pro
|
||||
/>
|
||||
)}
|
||||
|
||||
<Text element="p">
|
||||
<Trans i18nKey="browse-dashboards.action.move-modal-text">This action will move the following content:</Trans>
|
||||
</Text>
|
||||
<MoveActionAvailableTargetWarning />
|
||||
|
||||
<DescendantCount selectedItems={selectedItems} />
|
||||
<Box paddingTop={2}>
|
||||
<Text element="p">
|
||||
<Trans i18nKey="browse-dashboards.action.move-modal-text">This action will move the following content:</Trans>
|
||||
</Text>
|
||||
|
||||
<DescendantCount selectedItems={selectedItems} />
|
||||
</Box>
|
||||
|
||||
<Space v={3} />
|
||||
|
||||
<Field label={t('browse-dashboards.action.move-modal-field-label', 'Folder name')}>
|
||||
<FolderPicker value={moveTarget} excludeUIDs={selectedFolders} onChange={setMoveTarget} />
|
||||
<ProvisioningAwareFolderPicker
|
||||
value={moveTarget}
|
||||
excludeUIDs={selectedFolders}
|
||||
onChange={setMoveTarget}
|
||||
repositoryName={undefined} // is non-provisioned folder
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Modal.ButtonRow>
|
||||
|
||||
@@ -36,6 +36,7 @@ import { ShowConfirmModalEvent } from 'app/types/events';
|
||||
|
||||
import {
|
||||
AnnoKeyManagerAllowsEdits,
|
||||
AnnoKeyManagerIdentity,
|
||||
AnnoKeyManagerKind,
|
||||
AnnoKeySourcePath,
|
||||
ManagerKind,
|
||||
@@ -773,6 +774,11 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
return this.state.meta.k8s?.annotations?.[AnnoKeyManagerKind];
|
||||
}
|
||||
|
||||
getManagerIdentity(): string | undefined {
|
||||
// get repo name if any
|
||||
return this.state.meta.k8s?.annotations?.[AnnoKeyManagerIdentity];
|
||||
}
|
||||
|
||||
isManaged() {
|
||||
return Boolean(this.getManagerKind());
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ import {
|
||||
WeekStart,
|
||||
} from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||
import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings';
|
||||
import { GenAIDashDescriptionButton } from 'app/features/dashboard/components/GenAI/GenAIDashDescriptionButton';
|
||||
import { GenAIDashTitleButton } from 'app/features/dashboard/components/GenAI/GenAIDashTitleButton';
|
||||
import { MoveProvisionedDashboardDrawer } from 'app/features/provisioning/components/Dashboards/MoveProvisionedDashboardDrawer';
|
||||
import { ProvisioningAwareFolderPicker } from 'app/features/provisioning/components/Shared/ProvisioningAwareFolderPicker';
|
||||
|
||||
import { updateNavModel } from '../pages/utils';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
@@ -286,9 +286,10 @@ function GeneralSettingsEditViewComponent({ model }: SceneComponentProps<General
|
||||
<TagsInput id="tags-input" tags={tags} onChange={model.onTagsChange} width={40} />
|
||||
</Field>
|
||||
<Field noMargin label={t('dashboard-settings.general.folder-label', 'Folder')}>
|
||||
<FolderPicker
|
||||
<ProvisioningAwareFolderPicker
|
||||
value={meta.folderUid}
|
||||
onChange={dashboard.isManagedRepository() ? model.onProvisionedFolderChange : model.onFolderChange}
|
||||
repositoryName={dashboard.getManagerIdentity()}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
|
||||
+7
-5
@@ -4,7 +4,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { FolderPicker, getAppEvents } from '@grafana/runtime';
|
||||
import { getAppEvents } from '@grafana/runtime';
|
||||
import { Box, Button, Field, Stack } from '@grafana/ui';
|
||||
import { useGetFolderQuery } from 'app/api/clients/folder/v1beta1';
|
||||
import { RepositoryView, Job } from 'app/api/clients/provisioning/v0alpha1';
|
||||
@@ -17,6 +17,8 @@ import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/us
|
||||
import { GENERAL_FOLDER_UID } from 'app/features/search/constants';
|
||||
|
||||
import { useSelectionRepoValidation } from '../../hooks/useSelectionRepoValidation';
|
||||
import { MoveActionAvailableTargetWarning } from '../Shared/MoveActionAvailableTargetWarning';
|
||||
import { ProvisioningAwareFolderPicker } from '../Shared/ProvisioningAwareFolderPicker';
|
||||
import { RepoInvalidStateBanner } from '../Shared/RepoInvalidStateBanner';
|
||||
import { ResourceEditFormSharedFields } from '../Shared/ResourceEditFormSharedFields';
|
||||
import { generateTimestamp } from '../utils/timestamp';
|
||||
@@ -111,10 +113,9 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(handleSubmitForm)}>
|
||||
<Stack direction="column" gap={2}>
|
||||
<MoveActionAvailableTargetWarning />
|
||||
<Box paddingBottom={2}>
|
||||
<Trans i18nKey="browse-dashboards.bulk-move-resources-form.move-warning">
|
||||
This will move selected folders and their descendants. In total, this will affect:
|
||||
</Trans>
|
||||
<Trans i18nKey="browse-dashboards.bulk-move-resources-form.move-total">In total, this will affect:</Trans>
|
||||
<DescendantCount selectedItems={{ ...selectedItems, panel: {}, $all: false }} />
|
||||
</Box>
|
||||
|
||||
@@ -129,12 +130,13 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions
|
||||
error={errors.targetFolderUID?.message}
|
||||
invalid={!!errors.targetFolderUID}
|
||||
>
|
||||
<FolderPicker
|
||||
<ProvisioningAwareFolderPicker
|
||||
value={targetFolderUID}
|
||||
onChange={(uid) => {
|
||||
setTargetFolderUID(uid || '');
|
||||
clearErrors('targetFolderUID');
|
||||
}}
|
||||
repositoryName={repository.name}
|
||||
/>
|
||||
</Field>
|
||||
<ResourceEditFormSharedFields
|
||||
|
||||
+6
-10
@@ -36,16 +36,6 @@ jest.mock('../../hooks/useProvisionedRequestHandler', () => {
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('app/core/components/Select/FolderPicker', () => {
|
||||
const actual = jest.requireActual('app/core/components/Select/FolderPicker');
|
||||
return {
|
||||
...actual,
|
||||
FolderPicker: function MockFolderPicker() {
|
||||
return <div data-testid="folder-picker">Folder Picker</div>;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('app/features/provisioning/hooks/useCreateOrUpdateRepositoryFile', () => {
|
||||
return {
|
||||
useCreateOrUpdateRepositoryFile: jest.fn(),
|
||||
@@ -58,6 +48,12 @@ jest.mock('app/features/provisioning/hooks/useGetResourceRepositoryView', () =>
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('app/features/provisioning/components/Shared/ProvisioningAwareFolderPicker', () => {
|
||||
return {
|
||||
ProvisioningAwareFolderPicker: () => <div data-testid="folder-picker">Mocked Folder Picker</div>,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('app/features/manage-dashboards/services/ValidationSrv', () => {
|
||||
const actual = jest.requireActual('app/features/manage-dashboards/services/ValidationSrv');
|
||||
return {
|
||||
|
||||
+5
-4
@@ -8,7 +8,6 @@ import { getAppEvents, locationService } from '@grafana/runtime';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
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 { SaveDashboardFormCommonOptions } from 'app/features/dashboard-scene/saving/SaveDashboardForm';
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
|
||||
import { ProvisionedDashboardFormData } from '../../types/form';
|
||||
import { buildResourceBranchRedirectUrl } from '../../utils/redirect';
|
||||
import { ProvisioningAwareFolderPicker } from '../Shared/ProvisioningAwareFolderPicker';
|
||||
import { RepoInvalidStateBanner } from '../Shared/RepoInvalidStateBanner';
|
||||
import { ResourceEditFormSharedFields } from '../Shared/ResourceEditFormSharedFields';
|
||||
import { getProvisionedMeta } from '../utils/getProvisionedMeta';
|
||||
@@ -208,10 +208,9 @@ export function SaveProvisionedDashboardForm({
|
||||
name={'folder'}
|
||||
render={({ field: { ref, value, onChange, ...field } }) => {
|
||||
return (
|
||||
<FolderPicker
|
||||
<ProvisioningAwareFolderPicker
|
||||
onChange={async (uid?: string, title?: string) => {
|
||||
onChange({ uid, title });
|
||||
// Update folderUid URL param
|
||||
updateURLParams('folderUid', uid);
|
||||
const meta = await getProvisionedMeta(uid);
|
||||
dashboard.setState({
|
||||
@@ -223,6 +222,7 @@ export function SaveProvisionedDashboardForm({
|
||||
}}
|
||||
value={value.uid}
|
||||
{...field}
|
||||
showAllFolders
|
||||
/>
|
||||
);
|
||||
}}
|
||||
@@ -284,7 +284,8 @@ async function validateTitle(title: string, formValues: ProvisionedDashboardForm
|
||||
|
||||
// Update the URL params without reloading the page
|
||||
function updateURLParams(param: string, value?: string) {
|
||||
if (!value) {
|
||||
// only check undefine and null, empty string = root folder, we still want to update the URL
|
||||
if (value === undefined || value === null) {
|
||||
return;
|
||||
}
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { Box, Icon, Tooltip } from '@grafana/ui';
|
||||
|
||||
export function MoveActionAvailableTargetWarning() {
|
||||
return (
|
||||
<Box>
|
||||
<Trans i18nKey="browse-dashboards.bulk-move-resources-form.move-warning">
|
||||
This will move selected folders and their descendants. Available target folders depend on the selected
|
||||
resources.
|
||||
</Trans>
|
||||
<Tooltip
|
||||
content={t(
|
||||
'browse-dashboards.bulk-move-resources-form.move-warning-tooltip',
|
||||
'You can only move provisioned resources within their provisioned folder, and local resources to local folders.'
|
||||
)}
|
||||
>
|
||||
<span style={{ marginLeft: '4px' }}>
|
||||
<Icon name="info-circle" size="sm" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1';
|
||||
|
||||
import { useIsProvisionedInstance } from '../../hooks/useIsProvisionedInstance';
|
||||
|
||||
import { ProvisioningAwareFolderPicker } from './ProvisioningAwareFolderPicker';
|
||||
|
||||
jest.mock('app/api/clients/provisioning/v0alpha1', () => ({
|
||||
useGetFrontendSettingsQuery: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../hooks/useIsProvisionedInstance', () => ({
|
||||
useIsProvisionedInstance: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('app/core/components/Select/FolderPicker', () => ({
|
||||
FolderPicker: (props: { rootFolderUID?: string; excludeUIDs?: string[] }) => (
|
||||
<div data-testid="folder-picker">
|
||||
<div data-testid="root-folder-uid">{props.rootFolderUID || 'undefined'}</div>
|
||||
<div data-testid="exclude-uids">{JSON.stringify(props.excludeUIDs || [])}</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockUseGetFrontendSettingsQuery = useGetFrontendSettingsQuery as Partial<
|
||||
ReturnType<typeof useGetFrontendSettingsQuery>
|
||||
>;
|
||||
const mockUseIsProvisionedInstance = useIsProvisionedInstance as jest.MockedFunction<typeof useIsProvisionedInstance>;
|
||||
|
||||
const setup = ({
|
||||
repoName = undefined,
|
||||
excludeUIDs = undefined,
|
||||
}: {
|
||||
repoName?: string;
|
||||
isNonProvisionedFolder?: boolean;
|
||||
excludeUIDs?: string[];
|
||||
}) => {
|
||||
render(<ProvisioningAwareFolderPicker repositoryName={repoName} onChange={jest.fn()} excludeUIDs={excludeUIDs} />);
|
||||
};
|
||||
|
||||
describe('ProvisioningAwareFolderPicker', () => {
|
||||
const mockSettingsData = {
|
||||
items: [{ name: 'repo1' }, { name: 'repo2' }, { name: 'repo3' }],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUseGetFrontendSettingsQuery.mockReturnValue({
|
||||
data: mockSettingsData,
|
||||
isLoading: false,
|
||||
error: undefined,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
|
||||
config.featureToggles = { provisioning: true };
|
||||
});
|
||||
|
||||
describe('Provisioned Instance', () => {
|
||||
beforeEach(() => {
|
||||
mockUseIsProvisionedInstance.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('should not restrict folders', () => {
|
||||
setup({});
|
||||
|
||||
expect(screen.getByTestId('root-folder-uid')).toHaveTextContent('undefined');
|
||||
expect(screen.getByTestId('exclude-uids')).toHaveTextContent('[]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Non-Provisioned Instance', () => {
|
||||
beforeEach(() => {
|
||||
mockUseIsProvisionedInstance.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('should set root folder for repository context', () => {
|
||||
setup({ repoName: 'my-repo' });
|
||||
expect(screen.getByTestId('root-folder-uid')).toHaveTextContent('my-repo');
|
||||
});
|
||||
|
||||
it('should exclude provisioned folders for non-provisioned context', () => {
|
||||
setup({ repoName: undefined });
|
||||
expect(screen.getByTestId('exclude-uids')).toHaveTextContent('["repo1","repo2","repo3"]');
|
||||
});
|
||||
|
||||
it('should merge excludeUIDs', () => {
|
||||
setup({ repoName: undefined, excludeUIDs: ['custom1'] });
|
||||
|
||||
const excludeUIDs = JSON.parse(screen.getByTestId('exclude-uids').textContent || '[]');
|
||||
expect(excludeUIDs).toEqual(['repo1', 'repo2', 'repo3', 'custom1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature Toggle Disabled', () => {
|
||||
beforeEach(() => {
|
||||
mockUseIsProvisionedInstance.mockReturnValue(false);
|
||||
config.featureToggles.provisioning = false;
|
||||
});
|
||||
|
||||
it('should not apply restrictions', () => {
|
||||
setup({ isNonProvisionedFolder: true });
|
||||
expect(screen.getByTestId('root-folder-uid')).toHaveTextContent('undefined');
|
||||
expect(screen.getByTestId('exclude-uids')).toHaveTextContent('[]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
beforeEach(() => {
|
||||
mockUseIsProvisionedInstance.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('should handle missing settings data', () => {
|
||||
mockUseGetFrontendSettingsQuery.mockReturnValue({ data: undefined });
|
||||
setup({ isNonProvisionedFolder: true });
|
||||
expect(screen.getByTestId('exclude-uids')).toHaveTextContent('[]');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { RepositoryView, RepositoryViewList, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1';
|
||||
import { NestedFolderPickerProps } from 'app/core/components/NestedFolderPicker/NestedFolderPicker';
|
||||
import { getCustomRootFolderItem } from 'app/core/components/NestedFolderPicker/utils';
|
||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||
import { ManagerKind } from 'app/features/apiserver/types';
|
||||
|
||||
import { useIsProvisionedInstance } from '../../hooks/useIsProvisionedInstance';
|
||||
|
||||
interface Props extends NestedFolderPickerProps {
|
||||
/* Repository name (uid) or undefined (when it's non-provisioned folder). This decides when to show only one provisioned folder */
|
||||
repositoryName?: string;
|
||||
showAllFolders?: boolean;
|
||||
}
|
||||
|
||||
export function ProvisioningAwareFolderPicker({ repositoryName, showAllFolders, ...props }: Props) {
|
||||
const isProvisionedInstance = useIsProvisionedInstance();
|
||||
const provisioningEnabled = config.featureToggles.provisioning;
|
||||
const { data: settingsData } = useGetFrontendSettingsQuery(provisioningEnabled ? undefined : skipToken);
|
||||
const isNonProvisionedResource = !repositoryName;
|
||||
|
||||
const rootFolderUID = getRootFolderUID({
|
||||
isProvisionedInstance,
|
||||
provisioningEnabled,
|
||||
repositoryName,
|
||||
});
|
||||
const excludeUIDs = getExcludeUIDs({
|
||||
isProvisionedInstance,
|
||||
isNonProvisionedResource,
|
||||
provisioningEnabled,
|
||||
settingsData,
|
||||
});
|
||||
const rootFolderDisplayItem = getRootFolderDisplayItem({
|
||||
isProvisionedInstance,
|
||||
rootFolderUID,
|
||||
settingsDataItem: settingsData?.items,
|
||||
});
|
||||
|
||||
return (
|
||||
<FolderPicker
|
||||
{...props}
|
||||
rootFolderUID={showAllFolders ? undefined : rootFolderUID}
|
||||
excludeUIDs={showAllFolders ? undefined : [...excludeUIDs, ...(props.excludeUIDs || [])]}
|
||||
rootFolderItem={showAllFolders ? undefined : rootFolderDisplayItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getRootFolderUID({
|
||||
isProvisionedInstance,
|
||||
provisioningEnabled,
|
||||
repositoryName,
|
||||
}: {
|
||||
isProvisionedInstance?: boolean;
|
||||
provisioningEnabled?: boolean;
|
||||
repositoryName?: string;
|
||||
}) {
|
||||
if (isProvisionedInstance) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (provisioningEnabled && repositoryName) {
|
||||
return repositoryName;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getExcludeUIDs({
|
||||
isProvisionedInstance,
|
||||
isNonProvisionedResource,
|
||||
provisioningEnabled,
|
||||
settingsData,
|
||||
}: {
|
||||
isProvisionedInstance?: boolean;
|
||||
isNonProvisionedResource?: boolean;
|
||||
provisioningEnabled?: boolean;
|
||||
settingsData?: RepositoryViewList;
|
||||
}) {
|
||||
if (isProvisionedInstance) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isNonProvisionedResource) {
|
||||
// If provisioning is disabled, we don't want to exclude any folders
|
||||
if (!provisioningEnabled) {
|
||||
return [];
|
||||
}
|
||||
// If provisioning is enabled, we want to exclude all provisioned folders
|
||||
return settingsData?.items.map((repo) => repo.name) || [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getRootFolderDisplayItem({
|
||||
isProvisionedInstance,
|
||||
rootFolderUID,
|
||||
settingsDataItem,
|
||||
}: {
|
||||
isProvisionedInstance?: boolean;
|
||||
rootFolderUID?: string;
|
||||
settingsDataItem?: RepositoryView[];
|
||||
}) {
|
||||
if (isProvisionedInstance) {
|
||||
// If it's a provisioned instance, we use default root display ("Dashboards")
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const repoFolder = settingsDataItem?.find((item: RepositoryView) => item.name === rootFolderUID);
|
||||
return repoFolder
|
||||
? getCustomRootFolderItem({ title: repoFolder.title, uid: repoFolder.name, managedBy: ManagerKind.Repo })
|
||||
: undefined;
|
||||
}
|
||||
@@ -3565,7 +3565,9 @@
|
||||
},
|
||||
"error-moving-resources": "Error moving resources",
|
||||
"error-no-target-folder-path": "Target folder path is invalid or empty, please select again.",
|
||||
"move-warning": "This will move selected folders and their descendants. In total, this will affect:",
|
||||
"move-total": "In total, this will affect:",
|
||||
"move-warning": "This will move selected folders and their descendants. Available target folders depend on the selected resources.",
|
||||
"move-warning-tooltip": "You can only move provisioned resources within their provisioned folder, and local resources to local folders.",
|
||||
"target-folder": "Target Folder"
|
||||
},
|
||||
"counts": {
|
||||
|
||||
Reference in New Issue
Block a user