Provisioning components organize (#109741)

* Provisioning components organize
This commit is contained in:
Yunwen Zheng
2025-08-21 12:17:43 -04:00
committed by GitHub
parent 56c8e53a99
commit 6d538f62c8
56 changed files with 220 additions and 272 deletions
@@ -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 { ProvisionedFolderPreviewBanner } from '../provisioning/components/Folders/ProvisionedFolderPreviewBanner';
import { useGetResourceRepositoryView } from '../provisioning/hooks/useGetResourceRepositoryView';
import { useSearchStateManager } from '../search/state/SearchStateManager';
import { getSearchPlaceholder } from '../search/tempI18nPhrases';
@@ -26,7 +27,6 @@ import { BrowseFilters } from './components/BrowseFilters';
import { BrowseView } from './components/BrowseView';
import CreateNewButton from './components/CreateNewButton';
import { FolderActionsButton } from './components/FolderActionsButton';
import { ProvisionedFolderPreviewBanner } from './components/ProvisionedFolderPreviewBanner';
import { SearchView } from './components/SearchView';
import { getFolderPermissions } from './permissions';
import { useHasSelection } from './state/hooks';
@@ -5,6 +5,9 @@ import { config, reportInteraction } from '@grafana/runtime';
import { Button, Drawer, Stack } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { ManagerKind } from 'app/features/apiserver/types';
import { BulkDeleteProvisionedResource } from 'app/features/provisioning/components/BulkActions/BulkDeleteProvisionedResource';
import { BulkMoveProvisionedResource } from 'app/features/provisioning/components/BulkActions/BulkMoveProvisionedResource';
import { useSelectionProvisioningStatus } from 'app/features/provisioning/hooks/useSelectionProvisioningStatus';
import { useSearchStateManager } from 'app/features/search/state/SearchStateManager';
import { ShowModalReactEvent } from 'app/types/events';
import { FolderDTO } from 'app/types/folders';
@@ -14,13 +17,10 @@ import { useDeleteItemsMutation, useMoveItemsMutation } from '../../api/browseDa
import { useActionSelectionState } from '../../state/hooks';
import { setAllSelection } from '../../state/slice';
import { DashboardTreeSelection } from '../../types';
import { BulkDeleteProvisionedResource } from '../BulkActions/BulkDeleteProvisionedResource';
import { BulkMoveProvisionedResource } from '../BulkActions/BulkMoveProvisionedResource';
import { DeleteModal } from './DeleteModal';
import { MoveModal } from './MoveModal';
import { SelectedMixResourcesMsgModal } from './SelectedMixResourcesMsgModal';
import { useSelectionProvisioningStatus } from './useSelectionProvisioningStatus';
export interface Props {
folderDTO?: FolderDTO;
@@ -1,8 +1,4 @@
import { t } from '@grafana/i18n';
import { DashboardViewItem } from 'app/features/search/types';
import { findItem } from '../../state/utils';
import { DashboardViewItemCollection } from '../../types';
export function buildBreakdownString(
folderCount: number,
@@ -30,40 +26,3 @@ export function buildBreakdownString(
}
return breakdownString;
}
// Utility: Get root folder for any item (reusing existing pattern from reducers.ts)
export function getItemRootFolder(
item: { uid: string; parentUID?: string; kind?: string },
browseState: {
rootItems?: { items: DashboardViewItem[] };
childrenByParentUID: Record<string, DashboardViewItemCollection>;
}
): string | undefined {
const rootItems = browseState.rootItems?.items || [];
// If it's already a root-level item, return its UID (only for folders)
if (!item.parentUID) {
return item.kind === 'folder' ? item.uid : undefined;
}
// For nested items, traverse up to find root folder (same pattern as reducers.ts)
let nextParentUID = item.parentUID;
while (nextParentUID) {
const parent = findItem(rootItems, browseState.childrenByParentUID, nextParentUID);
// Safety check to prevent infinite loops (same as reducers.ts)
if (!parent) {
break;
}
// Found the root folder (no parent)
if (!parent.parentUID) {
return parent.uid;
}
nextParentUID = parent.parentUID;
}
return undefined;
}
@@ -6,12 +6,12 @@ import { t } from '@grafana/i18n';
import { Checkbox, Tooltip, useStyles2 } from '@grafana/ui';
import { ManagerKind } from 'app/features/apiserver/types';
import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance';
import { useSelectionRepoValidation } from 'app/features/provisioning/hooks/useSelectionRepoValidation';
import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository';
import { useSelector } from 'app/types/store';
import { DashboardsTreeCellProps, SelectionState } from '../types';
import { useSelectionRepoValidation } from './BrowseActions/useSelectionRepoValidation';
import { isSharedWithMe, canEditItemType } from './utils';
export default function CheckboxCell({
@@ -6,6 +6,7 @@ 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 { NewProvisionedFolderForm } from 'app/features/provisioning/components/Folders/NewProvisionedFolderForm';
import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance';
import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository';
import {
@@ -20,7 +21,6 @@ import { ManagerKind } from '../../apiserver/types';
import { useNewFolderMutation } from '../api/browseDashboardsAPI';
import { NewFolderForm } from './NewFolderForm';
import { NewProvisionedFolderForm } from './NewProvisionedFolderForm';
interface Props {
parentFolder?: FolderDTO;
@@ -7,6 +7,7 @@ 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 { DeleteProvisionedFolderForm } from 'app/features/provisioning/components/Folders/DeleteProvisionedFolderForm';
import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository';
import { ShowModalReactEvent } from 'app/types/events';
import { FolderDTO } from 'app/types/folders';
@@ -18,7 +19,6 @@ import { getFolderPermissions } from '../permissions';
import { DeleteModal } from './BrowseActions/DeleteModal';
import { MoveModal } from './BrowseActions/MoveModal';
import { DeleteProvisionedFolderForm } from './DeleteProvisionedFolderForm';
interface Props {
folder: FolderDTO;
@@ -1,17 +1,8 @@
import { config } from '@grafana/runtime';
import { contextSrv } from 'app/core/core';
import { ManagerKind } from 'app/features/apiserver/types';
import { DashboardViewItem } from 'app/features/search/types';
import { ResourceRef } from 'app/features/provisioning/components/BulkActions/useBulkActionJob';
import { findItem } from '../state/utils';
import {
DashboardTreeSelection,
DashboardViewItemWithUIItems,
BrowseDashboardsPermissions,
BrowseDashboardsState,
} from '../types';
import { ResourceRef } from './BulkActions/useBulkActionJob';
import { DashboardTreeSelection, DashboardViewItemWithUIItems, BrowseDashboardsPermissions } from '../types';
export function makeRowID(baseId: string, item: DashboardViewItemWithUIItems) {
return baseId + item.uid;
@@ -69,35 +60,3 @@ export function canSelectItems(permissions: BrowseDashboardsPermissions) {
const canSelectDashboards = canEditDashboards || canDeleteDashboards;
return Boolean(canSelectFolders || canSelectDashboards);
}
/**
* Finds the repository name for an item by traversing up the tree to find the root provisioned folder (managed by ManagerKind.Repo)
* This should be an edge case where user have multiple provisioned folders and try to managing resources on root folder
*/
export function getItemRepositoryUid(
item: DashboardViewItem,
rootItems: DashboardViewItem[],
childrenByParentUID: BrowseDashboardsState['childrenByParentUID']
): string {
// For root provisioned folders, the UID is the repository name
if (item.managedBy === ManagerKind.Repo && !item.parentUID && item.kind === 'folder') {
return item.uid;
}
// Traverse up the tree to find the root provisioned folder
let currentItem = item;
while (currentItem.parentUID) {
const parent = findItem(rootItems, childrenByParentUID, currentItem.parentUID);
if (!parent) {
break;
}
if (parent.managedBy === ManagerKind.Repo && !parent.parentUID) {
return currentItem.parentUID;
}
currentItem = parent;
}
return 'non_provisioned';
}
@@ -12,10 +12,10 @@ import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { DashboardPageError } from 'app/features/dashboard/containers/DashboardPageError';
import { DashboardPageRouteParams, DashboardPageRouteSearchParams } from 'app/features/dashboard/containers/types';
import { getDashboardSceneProfiler } from 'app/features/dashboard/services/DashboardProfiler';
import { DashboardPreviewBanner } from 'app/features/provisioning/components/Dashboards/DashboardPreviewBanner';
import { DashboardRoutes } from 'app/types/dashboard';
import { DashboardPrompt } from '../saving/DashboardPrompt';
import { DashboardPreviewBanner } from '../saving/provisioned/DashboardPreviewBanner';
import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState';
import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager';
@@ -7,10 +7,10 @@ import { Trans, t } from '@grafana/i18n';
import { Button, Input, Switch, Field, Label, TextArea, Stack, Alert, Box } from '@grafana/ui';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv';
import { getProvisionedMeta } from 'app/features/provisioning/components/utils/getProvisionedMeta';
import { DashboardScene } from '../scene/DashboardScene';
import { getProvisionedMeta } from './provisioned/utils/getProvisionedMeta';
import { DashboardChangeInfo, NameAlreadyExistsError, SaveButton, isNameExistsError } from './shared';
import { useSaveDashboard } from './useSaveDashboard';
@@ -2,6 +2,7 @@ import { t } from '@grafana/i18n';
import { SceneComponentProps, SceneObjectBase, SceneObjectState, SceneObjectRef } from '@grafana/scenes';
import { Drawer, Tab, TabsBar } from '@grafana/ui';
import { SaveDashboardDiff } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDiff';
import { SaveProvisionedDashboard } from 'app/features/provisioning/components/Dashboards/SaveProvisionedDashboard';
import { useIsProvisionedNG } from 'app/features/provisioning/hooks/useIsProvisionedNG';
import { DashboardScene } from '../scene/DashboardScene';
@@ -9,7 +10,6 @@ import { DashboardScene } from '../scene/DashboardScene';
import { SaveDashboardAsForm } from './SaveDashboardAsForm';
import { SaveDashboardForm } from './SaveDashboardForm';
import { SaveProvisionedDashboardForm } from './SaveProvisionedDashboardForm';
import { SaveProvisionedDashboard } from './provisioned/SaveProvisionedDashboard';
interface SaveDashboardDrawerState extends SceneObjectState {
dashboardRef: SceneObjectRef<DashboardScene>;
@@ -6,7 +6,6 @@ import { config, isFetchError } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { Alert, Box, Button, Stack } from '@grafana/ui';
import { WorkflowOption } from 'app/features/provisioning/types';
import { Diffs } from '../settings/version-history/utils';
@@ -24,23 +23,6 @@ export interface DashboardChangeInfo {
hasMigratedToV2?: boolean;
}
export interface BaseProvisionedFormData {
ref?: string;
path: string;
comment?: string;
repo: string;
workflow?: WorkflowOption;
title: string;
}
export interface ProvisionedDashboardFormData extends BaseProvisionedFormData {
description: string;
folder: {
uid?: string;
title?: string;
};
}
export function isVersionMismatchError(error?: Error) {
return isFetchError(error) && error.data && error.data.status === 'version-mismatch';
}
@@ -4,12 +4,11 @@ import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { config, reportInteraction } from '@grafana/runtime';
import { Button, ConfirmModal, Modal, Space, Text, TextLink } from '@grafana/ui';
import { DeleteProvisionedDashboardDrawer } from 'app/features/provisioning/components/Dashboards/DeleteProvisionedDashboardDrawer';
import { useDeleteItemsMutation } from '../../browse-dashboards/api/browseDashboardsAPI';
import { DashboardScene } from '../scene/DashboardScene';
import { DeleteProvisionedDashboardDrawer } from './DeleteProvisionedDashboardDrawer';
interface ButtonProps {
dashboard: DashboardScene;
}
@@ -23,6 +23,7 @@ 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 { updateNavModel } from '../pages/utils';
import { DashboardScene } from '../scene/DashboardScene';
@@ -31,7 +32,6 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getDashboardSceneFor } from '../utils/utils';
import { DeleteDashboardButton } from './DeleteDashboardButton';
import { MoveProvisionedDashboardDrawer } from './MoveProvisionedDashboardDrawer';
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
export interface GeneralSettingsEditViewState extends DashboardEditViewState {
@@ -111,30 +111,3 @@ export function createDashboardEditViewFor(editview: string): DashboardEditView
return new GeneralSettingsEditView({});
}
}
export type ResourceBranchUrlOptions = {
baseUrl?: string;
paramName?: string;
paramValue?: string;
repoType?: string;
};
export function buildResourceBranchRedirectUrl({
baseUrl = '/dashboards',
paramName,
paramValue,
repoType,
}: ResourceBranchUrlOptions): string {
const params = new URLSearchParams();
if (paramName && paramValue) {
params.set(paramName, paramValue);
}
if (repoType) {
params.set('repo_type', repoType);
}
const queryString = params.toString();
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
}
@@ -3,12 +3,12 @@ import { render } from 'test/test-utils';
import { Job, RepositoryView } from 'app/api/clients/provisioning/v0alpha1';
import { useSelectionRepoValidation } from '../BrowseActions/useSelectionRepoValidation';
import { useSelectionRepoValidation } from '../../hooks/useSelectionRepoValidation';
import { BulkDeleteProvisionedResource } from './BulkDeleteProvisionedResource';
import { ResponseType } from './useBulkActionJob';
jest.mock('../BrowseActions/DescendantCount', () => ({
jest.mock('app/features/browse-dashboards/components/BrowseActions/DescendantCount', () => ({
DescendantCount: jest.fn(({ selectedItems }) => (
<div data-testid="descendant-count">
Mocked descendant count for {Object.keys(selectedItems.folder).length} folders and{' '}
@@ -21,7 +21,7 @@ jest.mock('app/features/provisioning/hooks/useGetResourceRepositoryView', () =>
useGetResourceRepositoryView: jest.fn(),
}));
jest.mock('../BrowseActions/useSelectionRepoValidation', () => ({
jest.mock('../../hooks/useSelectionRepoValidation', () => ({
useSelectionRepoValidation: jest.fn(),
}));
@@ -6,18 +6,18 @@ import { Trans, t } from '@grafana/i18n';
import { getAppEvents } from '@grafana/runtime';
import { Box, Button, Stack } from '@grafana/ui';
import { Job, RepositoryView } from 'app/api/clients/provisioning/v0alpha1';
import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields';
import { getDefaultWorkflow, getWorkflowOptions } from 'app/features/dashboard-scene/saving/provisioned/defaults';
import { generateTimestamp } from 'app/features/dashboard-scene/saving/provisioned/utils/timestamp';
import { DescendantCount } from 'app/features/browse-dashboards/components/BrowseActions/DescendantCount';
import { collectSelectedItems } from 'app/features/browse-dashboards/components/utils';
import { JobStatus } from 'app/features/provisioning/Job/JobStatus';
import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
import { GENERAL_FOLDER_UID } from 'app/features/search/constants';
import { DescendantCount } from '../BrowseActions/DescendantCount';
import { useSelectionRepoValidation } from '../BrowseActions/useSelectionRepoValidation';
import { collectSelectedItems } from '../utils';
import { useSelectionRepoValidation } from '../../hooks/useSelectionRepoValidation';
import { RepoInvalidStateBanner } from '../Shared/RepoInvalidStateBanner';
import { ResourceEditFormSharedFields } from '../Shared/ResourceEditFormSharedFields';
import { getDefaultWorkflow, getWorkflowOptions } from '../defaults';
import { generateTimestamp } from '../utils/timestamp';
import { RepoInvalidStateBanner } from './RepoInvalidStateBanner';
import { DeleteJobSpec, useBulkActionJob } from './useBulkActionJob';
import { BulkActionFormData, BulkActionProvisionResourceProps } from './utils';
@@ -9,18 +9,18 @@ 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';
import { AnnoKeySourcePath } from 'app/features/apiserver/types';
import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields';
import { getDefaultWorkflow, getWorkflowOptions } from 'app/features/dashboard-scene/saving/provisioned/defaults';
import { generateTimestamp } from 'app/features/dashboard-scene/saving/provisioned/utils/timestamp';
import { DescendantCount } from 'app/features/browse-dashboards/components/BrowseActions/DescendantCount';
import { collectSelectedItems } from 'app/features/browse-dashboards/components/utils';
import { JobStatus } from 'app/features/provisioning/Job/JobStatus';
import { getDefaultWorkflow, getWorkflowOptions } from 'app/features/provisioning/components/defaults';
import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
import { GENERAL_FOLDER_UID } from 'app/features/search/constants';
import { DescendantCount } from '../BrowseActions/DescendantCount';
import { useSelectionRepoValidation } from '../BrowseActions/useSelectionRepoValidation';
import { collectSelectedItems } from '../utils';
import { useSelectionRepoValidation } from '../../hooks/useSelectionRepoValidation';
import { RepoInvalidStateBanner } from '../Shared/RepoInvalidStateBanner';
import { ResourceEditFormSharedFields } from '../Shared/ResourceEditFormSharedFields';
import { generateTimestamp } from '../utils/timestamp';
import { RepoInvalidStateBanner } from './RepoInvalidStateBanner';
import { MoveJobSpec, useBulkActionJob } from './useBulkActionJob';
import { BulkActionFormData, BulkActionProvisionResourceProps, getTargetFolderPathInRepo } from './utils';
@@ -2,10 +2,9 @@ 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';
import { rootItemsSelector, useChildrenByParentUIDState } from 'app/features/browse-dashboards/state/hooks';
import { findItem } from 'app/features/browse-dashboards/state/utils';
import { DashboardTreeSelection } from 'app/features/browse-dashboards/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
@@ -1,9 +1,8 @@
import { Folder } from 'app/api/clients/folder/v1beta1';
import { AnnoKeySourcePath } from 'app/features/apiserver/types';
import { DashboardTreeSelection } from 'app/features/browse-dashboards/types';
import { WorkflowOption } from 'app/features/provisioning/types';
import { DashboardTreeSelection } from '../../types';
export type BulkActionFormData = {
comment: string;
ref: string;
@@ -6,7 +6,7 @@ import { DashboardPageRouteSearchParams } from 'app/features/dashboard/container
import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
import { DashboardRoutes } from 'app/types/dashboard';
import { PreviewBannerViewPR } from './PreviewBannerViewPR';
import { PreviewBannerViewPR } from '../Shared/PreviewBannerViewPR';
export interface CommonBannerProps {
queryParams: DashboardPageRouteSearchParams;
@@ -1,5 +1,6 @@
import { useProvisionedDashboardData } from '../saving/provisioned/hooks';
import { DashboardScene } from '../scene/DashboardScene';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { useProvisionedDashboardData } from '../../hooks/useProvisionedDashboardData';
import { DeleteProvisionedDashboardForm } from './DeleteProvisionedDashboardForm';
@@ -4,13 +4,16 @@ import userEvent from '@testing-library/user-event';
import { AppEvents } from '@grafana/data';
import { getAppEvents } from '@grafana/runtime';
import { useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { useProvisionedDashboardData, ProvisionedDashboardData } from '../saving/provisioned/hooks';
import { DashboardScene } from '../scene/DashboardScene';
import { ProvisionedDashboardData, useProvisionedDashboardData } from '../../hooks/useProvisionedDashboardData';
import { DeleteProvisionedDashboardDrawer, Props } from './DeleteProvisionedDashboardDrawer';
// Mock the hooks and dependencies
jest.mock('../../hooks/useProvisionedDashboardData', () => ({
useProvisionedDashboardData: jest.fn(),
}));
jest.mock('app/api/clients/provisioning/v0alpha1', () => ({
useDeleteRepositoryFilesWithPathMutation: jest.fn(),
provisioningAPIv0alpha1: {
@@ -28,7 +31,13 @@ jest.mock('react-redux', () => {
useDispatch: jest.fn(),
};
});
jest.mock('../saving/provisioned/hooks');
jest.mock('../../hooks/useProvisionedRequestHandler', () => ({
useProvisionedRequestHandler: jest.fn(({ request, handlers }) => {
if (request.isError && handlers.onError) {
handlers.onError(request.error, { repoType: 'github', resourceType: 'dashboard', workflow: 'branch' });
}
}),
}));
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getAppEvents: jest.fn(),
@@ -41,7 +50,7 @@ jest.mock('react-router-dom-v5-compat', () => ({
const mockNavigate = jest.fn();
// Mock shared form components
jest.mock('../components/Provisioned/ResourceEditFormSharedFields', () => ({
jest.mock('../Shared/ResourceEditFormSharedFields', () => ({
ResourceEditFormSharedFields: ({ disabled }: { disabled: boolean }) => (
<textarea data-testid="shared-fields" disabled={disabled} />
),
@@ -6,16 +6,15 @@ import { Trans, t } from '@grafana/i18n';
import { getAppEvents } from '@grafana/runtime';
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 { getFolderURL } from 'app/features/browse-dashboards/components/utils';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { PROVISIONING_URL } from 'app/features/provisioning/constants';
import { ResourceEditFormSharedFields } from '../components/Provisioned/ResourceEditFormSharedFields';
import { ProvisionedDashboardFormData } from '../saving/shared';
import { DashboardScene } from '../scene/DashboardScene';
import { useProvisionedRequestHandler, ProvisionedOperationInfo } from '../utils/useProvisionedRequestHandler';
import { buildResourceBranchRedirectUrl } from './utils';
import { ProvisionedOperationInfo, useProvisionedRequestHandler } from '../../hooks/useProvisionedRequestHandler';
import { ProvisionedDashboardFormData } from '../../types/form';
import { buildResourceBranchRedirectUrl } from '../../utils/redirect';
import { RepoInvalidStateBanner } from '../Shared/RepoInvalidStateBanner';
import { ResourceEditFormSharedFields } from '../Shared/ResourceEditFormSharedFields';
export interface Props {
dashboard: DashboardScene;
@@ -1,5 +1,6 @@
import { useProvisionedDashboardData } from '../saving/provisioned/hooks';
import { DashboardScene } from '../scene/DashboardScene';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { useProvisionedDashboardData } from '../../hooks/useProvisionedDashboardData';
import { MoveProvisionedDashboardForm } from './MoveProvisionedDashboardForm';
@@ -8,9 +8,9 @@ import {
useGetRepositoryFilesWithPathQuery,
} from 'app/api/clients/provisioning/v0alpha1';
import { AnnoKeySourcePath } from 'app/features/apiserver/types';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { DashboardScene } from '../scene/DashboardScene';
import { useProvisionedRequestHandler } from '../utils/useProvisionedRequestHandler';
import { useProvisionedRequestHandler } from '../../hooks/useProvisionedRequestHandler';
import { MoveProvisionedDashboardForm, Props } from './MoveProvisionedDashboardForm';
@@ -38,7 +38,7 @@ jest.mock('app/api/clients/folder/v1beta1', () => ({
useGetFolderQuery: jest.fn(),
}));
jest.mock('../utils/useProvisionedRequestHandler', () => ({
jest.mock('../../hooks/useProvisionedRequestHandler', () => ({
useProvisionedRequestHandler: jest.fn(),
}));
@@ -46,7 +46,7 @@ jest.mock('react-router-dom-v5-compat', () => ({
useNavigate: () => jest.fn(),
}));
jest.mock('../components/Provisioned/ResourceEditFormSharedFields', () => ({
jest.mock('../Shared/ResourceEditFormSharedFields', () => ({
ResourceEditFormSharedFields: () => <div data-testid="resource-edit-form" />,
}));
@@ -14,14 +14,13 @@ import {
useGetRepositoryFilesWithPathQuery,
} from 'app/api/clients/provisioning/v0alpha1';
import { AnnoKeySourcePath } from 'app/features/apiserver/types';
import { getTargetFolderPathInRepo } from 'app/features/browse-dashboards/components/BulkActions/utils';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { ResourceEditFormSharedFields } from '../components/Provisioned/ResourceEditFormSharedFields';
import { ProvisionedDashboardFormData } from '../saving/shared';
import { DashboardScene } from '../scene/DashboardScene';
import { useProvisionedRequestHandler, ProvisionedOperationInfo } from '../utils/useProvisionedRequestHandler';
import { buildResourceBranchRedirectUrl } from './utils';
import { ProvisionedOperationInfo, useProvisionedRequestHandler } from '../../hooks/useProvisionedRequestHandler';
import { ProvisionedDashboardFormData } from '../../types/form';
import { buildResourceBranchRedirectUrl } from '../../utils/redirect';
import { getTargetFolderPathInRepo } from '../BulkActions/utils';
import { ResourceEditFormSharedFields } from '../Shared/ResourceEditFormSharedFields';
export interface Props {
dashboard: DashboardScene;
@@ -1,9 +1,10 @@
import { DashboardScene } from '../../scene/DashboardScene';
import { SaveDashboardDrawer } from '../SaveDashboardDrawer';
import { DashboardChangeInfo } from '../shared';
import { SaveDashboardDrawer } from 'app/features/dashboard-scene/saving/SaveDashboardDrawer';
import { DashboardChangeInfo } from 'app/features/dashboard-scene/saving/shared';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { useProvisionedDashboardData } from '../../hooks/useProvisionedDashboardData';
import { SaveProvisionedDashboardForm } from './SaveProvisionedDashboardForm';
import { useProvisionedDashboardData } from './hooks';
export interface SaveProvisionedDashboardProps {
dashboard: DashboardScene;
@@ -4,12 +4,11 @@ import userEvent from '@testing-library/user-event';
import { getAppEvents } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
import { AnnoKeyFolder, AnnoKeySourcePath } from 'app/features/apiserver/types';
import { SaveDashboardDrawer } from 'app/features/dashboard-scene/saving/SaveDashboardDrawer';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv';
import { useCreateOrUpdateRepositoryFile } from 'app/features/provisioning/hooks/useCreateOrUpdateRepositoryFile';
import { DashboardScene } from '../../scene/DashboardScene';
import { SaveDashboardDrawer } from '../SaveDashboardDrawer';
import { SaveProvisionedDashboardForm, Props } from './SaveProvisionedDashboardForm';
jest.mock('@grafana/runtime', () => {
@@ -31,7 +30,7 @@ jest.mock('@grafana/runtime', () => {
};
});
jest.mock('app/features/dashboard-scene/utils/useProvisionedRequestHandler', () => {
jest.mock('../../hooks/useProvisionedRequestHandler', () => {
return {
useProvisionedRequestHandler: jest.fn(),
};
@@ -77,8 +76,8 @@ jest.mock('react-router-dom-v5-compat', () => {
};
});
jest.mock('../SaveDashboardForm', () => {
const actual = jest.requireActual('../SaveDashboardForm');
jest.mock('app/features/dashboard-scene/saving/SaveDashboardForm', () => {
const actual = jest.requireActual('app/features/dashboard-scene/saving/SaveDashboardForm');
return {
...actual,
SaveDashboardFormCommonOptions: () => <div data-testid="common-options">Common Options</div>,
@@ -11,20 +11,23 @@ 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 { SaveDashboardFormCommonOptions } from 'app/features/dashboard-scene/saving/SaveDashboardForm';
import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl';
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';
import {
ProvisionedOperationInfo,
useProvisionedRequestHandler,
} from 'app/features/provisioning/hooks/useProvisionedRequestHandler';
import { ResourceEditFormSharedFields } from '../../components/Provisioned/ResourceEditFormSharedFields';
import { buildResourceBranchRedirectUrl } from '../../settings/utils';
import { getDashboardUrl } from '../../utils/getDashboardUrl';
import { ProvisionedOperationInfo, useProvisionedRequestHandler } from '../../utils/useProvisionedRequestHandler';
import { SaveDashboardFormCommonOptions } from '../SaveDashboardForm';
import { ProvisionedDashboardFormData } from '../shared';
import { ProvisionedDashboardFormData } from '../../types/form';
import { buildResourceBranchRedirectUrl } from '../../utils/redirect';
import { RepoInvalidStateBanner } from '../Shared/RepoInvalidStateBanner';
import { ResourceEditFormSharedFields } from '../Shared/ResourceEditFormSharedFields';
import { getProvisionedMeta } from '../utils/getProvisionedMeta';
import { SaveProvisionedDashboardProps } from './SaveProvisionedDashboard';
import { getProvisionedMeta } from './utils/getProvisionedMeta';
export interface Props extends SaveProvisionedDashboardProps {
isNew: boolean;
@@ -4,7 +4,10 @@ import userEvent from '@testing-library/user-event';
import { RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1';
import { FolderDTO } from 'app/types/folders';
import { ProvisionedFolderFormDataResult, useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData';
import {
ProvisionedFolderFormDataResult,
useProvisionedFolderFormData,
} from '../../hooks/useProvisionedFolderFormData';
import { DeleteProvisionedFolderForm } from './DeleteProvisionedFolderForm';
@@ -47,13 +50,13 @@ jest.mock('app/api/clients/provisioning/v0alpha1', () => ({
},
}));
jest.mock('../hooks/useProvisionedFolderFormData');
jest.mock('../../hooks/useProvisionedFolderFormData');
jest.mock('./BrowseActions/DescendantCount', () => ({
jest.mock('app/features/browse-dashboards/components/BrowseActions/DescendantCount', () => ({
DescendantCount: () => <div data-testid="descendant-count">2 folders, 5 dashboards</div>,
}));
jest.mock('app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields', () => ({
jest.mock('../Shared/ResourceEditFormSharedFields', () => ({
ResourceEditFormSharedFields: () => <div data-testid="shared-fields" />,
}));
@@ -8,20 +8,16 @@ import { Box, Button, Stack } from '@grafana/ui';
import { Folder } from 'app/api/clients/folder/v1beta1';
import { RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1';
import { AnnoKeySourcePath } from 'app/features/apiserver/types';
import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields';
import { BaseProvisionedFormData } from 'app/features/dashboard-scene/saving/shared';
import { buildResourceBranchRedirectUrl } from 'app/features/dashboard-scene/settings/utils';
import {
useProvisionedRequestHandler,
ProvisionedOperationInfo,
} from 'app/features/dashboard-scene/utils/useProvisionedRequestHandler';
import { DescendantCount } from 'app/features/browse-dashboards/components/BrowseActions/DescendantCount';
import { getFolderURL } from 'app/features/browse-dashboards/components/utils';
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';
import { useProvisionedFolderFormData } from '../../hooks/useProvisionedFolderFormData';
import { ProvisionedOperationInfo, useProvisionedRequestHandler } from '../../hooks/useProvisionedRequestHandler';
import { BaseProvisionedFormData } from '../../types/form';
import { buildResourceBranchRedirectUrl } from '../../utils/redirect';
import { RepoInvalidStateBanner } from '../Shared/RepoInvalidStateBanner';
import { ResourceEditFormSharedFields } from '../Shared/ResourceEditFormSharedFields';
interface FormProps extends DeleteProvisionedFolderFormProps {
initialValues: BaseProvisionedFormData;
@@ -8,7 +8,10 @@ import { validationSrv } from 'app/features/manage-dashboards/services/Validatio
import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
import { FolderDTO } from 'app/types/folders';
import { ProvisionedFolderFormDataResult, useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData';
import {
ProvisionedFolderFormDataResult,
useProvisionedFolderFormData,
} from '../../hooks/useProvisionedFolderFormData';
import { NewProvisionedFolderForm } from './NewProvisionedFolderForm';
@@ -47,7 +50,7 @@ jest.mock('app/api/clients/provisioning/v0alpha1', () => {
};
});
jest.mock('../hooks/useProvisionedFolderFormData', () => {
jest.mock('../../hooks/useProvisionedFolderFormData', () => {
return {
useProvisionedFolderFormData: jest.fn(),
};
@@ -75,17 +78,6 @@ jest.mock('react-router-dom-v5-compat', () => {
};
});
// Mock the defaults
jest.mock('../../dashboard-scene/saving/provisioned/defaults', () => {
return {
getDefaultWorkflow: jest.fn().mockReturnValue('write'),
getWorkflowOptions: jest.fn().mockReturnValue([
{ label: 'Commit directly', value: 'write' },
{ label: 'Create a branch', value: 'branch' },
]),
};
});
interface Props {
onDismiss?: () => void;
parentFolder?: FolderDTO;
@@ -8,20 +8,16 @@ import { Alert, Button, Field, Input, Stack } from '@grafana/ui';
import { Folder } from 'app/api/clients/folder/v1beta1';
import { RepositoryView, useCreateRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1';
import { AnnoKeySourcePath, Resource } from 'app/features/apiserver/types';
import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields';
import { BaseProvisionedFormData } from 'app/features/dashboard-scene/saving/shared';
import { buildResourceBranchRedirectUrl } from 'app/features/dashboard-scene/settings/utils';
import {
useProvisionedRequestHandler,
ProvisionedOperationInfo,
} from 'app/features/dashboard-scene/utils/useProvisionedRequestHandler';
import { PROVISIONING_URL } from 'app/features/provisioning/constants';
import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
import { FolderDTO } from 'app/types/folders';
import { useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData';
import { RepoInvalidStateBanner } from './BulkActions/RepoInvalidStateBanner';
import { useProvisionedFolderFormData } from '../../hooks/useProvisionedFolderFormData';
import { ProvisionedOperationInfo, useProvisionedRequestHandler } from '../../hooks/useProvisionedRequestHandler';
import { BaseProvisionedFormData } from '../../types/form';
import { buildResourceBranchRedirectUrl } from '../../utils/redirect';
import { RepoInvalidStateBanner } from '../Shared/RepoInvalidStateBanner';
import { ResourceEditFormSharedFields } from '../Shared/ResourceEditFormSharedFields';
interface FormProps extends Props {
initialValues: BaseProvisionedFormData;
@@ -1,6 +1,6 @@
import { config } from '@grafana/runtime';
import { CommonBannerProps } from 'app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner';
import { PreviewBannerViewPR } from 'app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR';
import { CommonBannerProps } from 'app/features/provisioning/components/Dashboards/DashboardPreviewBanner';
import { PreviewBannerViewPR } from 'app/features/provisioning/components/Shared/PreviewBannerViewPR';
import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
export function ProvisionedFolderPreviewBanner({ queryParams }: CommonBannerProps) {
@@ -4,7 +4,7 @@ import { Alert, Icon, Stack } from '@grafana/ui';
import { RepoTypeDisplay, RepoType } from 'app/features/provisioning/Wizard/types';
import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
import { commonAlertProps } from './DashboardPreviewBanner';
import { commonAlertProps } from '../Dashboards/DashboardPreviewBanner';
// TODO: We have this https://github.com/grafana/git-ui-sync-project/issues/166 to add more details about the PR.
@@ -3,8 +3,9 @@ import userEvent from '@testing-library/user-event';
import { ReactNode } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { RepositoryView } from '../../../../api/clients/provisioning/v0alpha1';
import { ProvisionedDashboardFormData } from '../../saving/shared';
import { RepositoryView } from 'app/api/clients/provisioning/v0alpha1';
import { ProvisionedDashboardFormData } from '../../types/form';
import { ResourceEditFormSharedFields } from './ResourceEditFormSharedFields';
@@ -3,16 +3,15 @@ import { useState } from 'react';
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 { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
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';
import { ProvisionedDashboardFormData } from '../shared';
import { getDefaultWorkflow, getWorkflowOptions } from './defaults';
import { generatePath } from './utils/path';
import { generateTimestamp } from './utils/timestamp';
import { getDefaultWorkflow, getWorkflowOptions } from '../components/defaults';
import { generatePath } from '../components/utils/path';
import { generateTimestamp } from '../components/utils/timestamp';
import { ProvisionedDashboardFormData } from '../types/form';
interface UseDefaultValuesParams {
meta: DashboardMeta;
@@ -3,13 +3,13 @@ import { useMemo } from 'react';
import { Folder } from 'app/api/clients/folder/v1beta1';
import { RepositoryView } from 'app/api/clients/provisioning/v0alpha1';
import { AnnoKeySourcePath } from 'app/features/apiserver/types';
import { getDefaultWorkflow, getWorkflowOptions } from 'app/features/dashboard-scene/saving/provisioned/defaults';
import { generateTimestamp } from 'app/features/dashboard-scene/saving/provisioned/utils/timestamp';
import { getDefaultWorkflow, getWorkflowOptions } from 'app/features/provisioning/components/defaults';
import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
import { BaseProvisionedFormData } from '../../dashboard-scene/saving/shared';
import { generateTimestamp } from '../components/utils/timestamp';
import { BaseProvisionedFormData } from '../types/form';
export interface UseProvisionedFolderFormDataProps {
interface UseProvisionedFolderFormDataProps {
folderUid?: string;
action: 'create' | 'delete';
title?: string;
@@ -9,8 +9,8 @@ import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsP
import { useSearchStateManager } from 'app/features/search/state/SearchStateManager';
import { useSelector } from 'app/types/store';
import { findItem } from '../../state/utils';
import { DashboardTreeSelection } from '../../types';
import { findItem } from '../../browse-dashboards/state/utils';
import { DashboardTreeSelection } from '../../browse-dashboards/types';
// TODO: This will soon be remove after bulk action is merged in
@@ -2,14 +2,13 @@ import { skipToken } from '@reduxjs/toolkit/query';
import { config } from '@grafana/runtime';
import { useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1';
import { findItem } from 'app/features/browse-dashboards/state/utils';
import { DashboardTreeSelection } from 'app/features/browse-dashboards/types';
import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance';
import { getIsReadOnlyRepo } from 'app/features/provisioning/utils/repository';
import { getIsReadOnlyRepo, getItemRepositoryUid } from 'app/features/provisioning/utils/repository';
import { useSelector } from 'app/types/store';
import { useChildrenByParentUIDState, rootItemsSelector } from '../../state/hooks';
import { findItem } from '../../state/utils';
import { DashboardTreeSelection } from '../../types';
import { getItemRepositoryUid } from '../utils';
import { useChildrenByParentUIDState, rootItemsSelector } from '../../browse-dashboards/state/hooks';
// 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'>) {
@@ -0,0 +1,18 @@
import { WorkflowOption } from '../types';
export interface BaseProvisionedFormData {
ref?: string;
path: string;
comment?: string;
repo: string;
workflow?: WorkflowOption;
title: string;
}
export interface ProvisionedDashboardFormData extends BaseProvisionedFormData {
description: string;
folder: {
uid?: string;
title?: string;
};
}
@@ -0,0 +1,26 @@
type ResourceBranchUrlOptions = {
baseUrl?: string;
paramName?: string;
paramValue?: string;
repoType?: string;
};
export function buildResourceBranchRedirectUrl({
baseUrl = '/dashboards',
paramName,
paramValue,
repoType,
}: ResourceBranchUrlOptions): string {
const params = new URLSearchParams();
if (paramName && paramValue) {
params.set(paramName, paramValue);
}
if (repoType) {
params.set('repo_type', repoType);
}
const queryString = params.toString();
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
}
@@ -1,5 +1,9 @@
import { t } from '@grafana/i18n';
import { RepositoryView } from 'app/api/clients/provisioning/v0alpha1';
import { ManagerKind } from 'app/features/apiserver/types';
import { findItem } from 'app/features/browse-dashboards/state/utils';
import { BrowseDashboardsState } from 'app/features/browse-dashboards/types';
import { DashboardViewItem } from 'app/features/search/types';
import { RepoWorkflows } from '../types';
@@ -28,3 +32,35 @@ export const getReadOnlyTooltipText = ({ isLocal = false }) => {
'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.'
);
};
/**
* Finds the repository name for an item by traversing up the tree to find the root provisioned folder (managed by ManagerKind.Repo)
* This should be an edge case where user have multiple provisioned folders and try to managing resources on root folder
*/
export function getItemRepositoryUid(
item: DashboardViewItem,
rootItems: DashboardViewItem[],
childrenByParentUID: BrowseDashboardsState['childrenByParentUID']
): string {
// For root provisioned folders, the UID is the repository name
if (item.managedBy === ManagerKind.Repo && !item.parentUID && item.kind === 'folder') {
return item.uid;
}
// Traverse up the tree to find the root provisioned folder
let currentItem = item;
while (currentItem.parentUID) {
const parent = findItem(rootItems, childrenByParentUID, currentItem.parentUID);
if (!parent) {
break;
}
if (parent.managedBy === ManagerKind.Repo && !parent.parentUID) {
return currentItem.parentUID;
}
currentItem = parent;
}
return 'non_provisioned';
}