Git sync create folder flow refactor to use new hook (#107422)
Actionlint / Lint GitHub Actions files (push) Waiting to run Details
Backend Code Checks / Validate Backend Configs (push) Waiting to run Details
Backend Unit Tests / Detect whether code changed (push) Waiting to run Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (1/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (2/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (3/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (4/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (5/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (6/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (7/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana (${{ matrix.shard }}) (8/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (1/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (2/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (3/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (4/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (5/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (6/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (7/8) (push) Blocked by required conditions Details
Backend Unit Tests / Grafana Enterprise (${{ matrix.shard }}) (8/8) (push) Blocked by required conditions Details
Backend Unit Tests / All backend unit tests complete (push) Blocked by required conditions Details
CodeQL checks / Analyze (actions) (push) Waiting to run Details
CodeQL checks / Analyze (go) (push) Waiting to run Details
CodeQL checks / Analyze (javascript) (push) Waiting to run Details
Lint Frontend / Detect whether code changed (push) Waiting to run Details
Lint Frontend / Lint (push) Blocked by required conditions Details
Lint Frontend / Typecheck (push) Blocked by required conditions Details
Lint Frontend / Betterer (push) Blocked by required conditions Details
golangci-lint / lint-go (push) Waiting to run Details
Verify i18n / verify-i18n (push) Waiting to run Details
Documentation / Build & Verify Docs (push) Waiting to run Details
End-to-end tests / Detect whether code changed (push) Waiting to run Details
End-to-end tests / Build & Package Grafana (push) Blocked by required conditions Details
End-to-end tests / Build E2E test runner (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (--flags="--env dashboardScene=false", e2e/old-arch/dashboards-suite, dashboards-suite (old arch)) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (--flags="--env dashboardScene=false", e2e/old-arch/panels-suite, panels-suite (old arch)) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (--flags="--env dashboardScene=false", e2e/old-arch/smoke-tests-suite, smoke-tests-suite (old arch)) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (--flags="--env dashboardScene=false", e2e/old-arch/various-suite, various-suite (old arch)) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (e2e/dashboards-suite, dashboards-suite) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (e2e/panels-suite, panels-suite) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (e2e/smoke-tests-suite, smoke-tests-suite) (push) Blocked by required conditions Details
End-to-end tests / ${{ matrix.suite }} (e2e/various-suite, various-suite) (push) Blocked by required conditions Details
End-to-end tests / A11y test (push) Blocked by required conditions Details
End-to-end tests / All E2E tests complete (push) Blocked by required conditions Details
Frontend tests / Detect whether code changed (push) Waiting to run Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (1) (push) Blocked by required conditions Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (2) (push) Blocked by required conditions Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (3) (push) Blocked by required conditions Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (4) (push) Blocked by required conditions Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (5) (push) Blocked by required conditions Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (6) (push) Blocked by required conditions Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (7) (push) Blocked by required conditions Details
Frontend tests / Unit tests (${{ matrix.chunk }} / 8) (8) (push) Blocked by required conditions Details
Frontend tests / All frontend unit tests complete (push) Blocked by required conditions Details
Integration Tests / Sqlite (${{ matrix.shard }}) (1/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (2/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (3/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (4/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (5/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (6/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (7/8) (push) Waiting to run Details
Integration Tests / Sqlite (${{ matrix.shard }}) (8/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (1/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (2/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (3/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (4/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (5/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (6/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (7/8) (push) Waiting to run Details
Integration Tests / MySQL (${{ matrix.shard }}) (8/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (1/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (2/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (3/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (4/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (5/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (6/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (7/8) (push) Waiting to run Details
Integration Tests / Postgres (${{ matrix.shard }}) (8/8) (push) Waiting to run Details
Integration Tests / All backend integration tests complete (push) Blocked by required conditions Details
publish-technical-documentation-next / sync (push) Waiting to run Details
Reject GitHub secrets / reject-gh-secrets (push) Waiting to run Details
Build Release Packages / setup (push) Waiting to run Details
Build Release Packages / Dispatch grafana-enterprise build (push) Blocked by required conditions Details
Build Release Packages / ${{ needs.setup.outputs.version }} / ${{ matrix.name }} (targz:grafana:darwin/amd64, darwin-amd64) (push) Blocked by required conditions Details
Build Release Packages / ${{ needs.setup.outputs.version }} / ${{ matrix.name }} (targz:grafana:darwin/arm64, darwin-arm64) (push) Blocked by required conditions Details
Build Release Packages / ${{ needs.setup.outputs.version }} / ${{ matrix.name }} (targz:grafana:linux/amd64,deb:grafana:linux/amd64,rpm:grafana:linux/amd64,docker:grafana:linux/amd64,docker:grafana:linux/amd64:ubuntu,npm:grafana,storybook, linux-amd64) (push) Blocked by required conditions Details
Build Release Packages / ${{ needs.setup.outputs.version }} / ${{ matrix.name }} (targz:grafana:linux/arm/v6,deb:grafana:linux/arm/v6, linux-armv6) (push) Blocked by required conditions Details
Build Release Packages / ${{ needs.setup.outputs.version }} / ${{ matrix.name }} (targz:grafana:linux/arm/v7,deb:grafana:linux/arm/v7,docker:grafana:linux/arm/v7,docker:grafana:linux/arm/v7:ubuntu, linux-armv7) (push) Blocked by required conditions Details
Build Release Packages / ${{ needs.setup.outputs.version }} / ${{ matrix.name }} (targz:grafana:linux/arm64,deb:grafana:linux/arm64,rpm:grafana:linux/arm64,docker:grafana:linux/arm64,docker:grafana:linux/arm64:ubuntu, linux-arm64) (push) Blocked by required conditions Details
Build Release Packages / ${{ needs.setup.outputs.version }} / ${{ matrix.name }} (targz:grafana:linux/s390x,deb:grafana:linux/s390x,rpm:grafana:linux/s390x,docker:grafana:linux/s390x,docker:grafana:linux/s390x:ubuntu, linux-s390x) (push) Blocked by required conditions Details
Build Release Packages / ${{ needs.setup.outputs.version }} / ${{ matrix.name }} (targz:grafana:windows/amd64,zip:grafana:windows/amd64,msi:grafana:windows/amd64, windows-amd64) (push) Blocked by required conditions Details
Build Release Packages / ${{ needs.setup.outputs.version }} / ${{ matrix.name }} (targz:grafana:windows/arm64,zip:grafana:windows/arm64, windows-arm64) (push) Blocked by required conditions Details
Build Release Packages / Upload artifacts (push) Blocked by required conditions Details
Run dashboard schema v2 e2e / dashboard-schema-v2-e2e (push) Waiting to run Details
Shellcheck / Shellcheck scripts (push) Waiting to run Details
Verify Storybook / Verify Storybook (push) Waiting to run Details
Swagger generated code / Verify committed API specs match (push) Waiting to run Details
Dispatch sync to mirror / dispatch-job (push) Waiting to run Details
trigger-dashboard-search-e2e / trigger-search-e2e (push) Waiting to run Details
Crowdin Upload Action / upload-sources-to-crowdin (push) Has been cancelled Details

* Git sync create folder flow refactor to use new hook

---------

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
Yunwen Zheng 2025-07-01 12:19:48 -04:00 committed by GitHub
parent 8b9e57f2f6
commit fdf4935e42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 225 additions and 275 deletions

View File

@ -1493,12 +1493,6 @@ exports[`better eslint`] = {
"public/app/features/browse-dashboards/components/NewFolderForm.tsx:5381": [ "public/app/features/browse-dashboards/components/NewFolderForm.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
], ],
"public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"],
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"],
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "2"],
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "3"]
],
"public/app/features/browse-dashboards/state/index.ts:5381": [ "public/app/features/browse-dashboards/state/index.ts:5381": [
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"], [0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"],
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"], [0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"],

View File

@ -107,11 +107,7 @@ export default function CreateNewButton({ parentFolder, canCreateDashboard, canC
size="sm" size="sm"
> >
{parentFolder?.managedBy === ManagerKind.Repo || isProvisionedInstance ? ( {parentFolder?.managedBy === ManagerKind.Repo || isProvisionedInstance ? (
<NewProvisionedFolderForm <NewProvisionedFolderForm onDismiss={() => setShowNewFolderDrawer(false)} parentFolder={parentFolder} />
onSubmit={() => setShowNewFolderDrawer(false)}
onCancel={() => setShowNewFolderDrawer(false)}
parentFolder={parentFolder}
/>
) : ( ) : (
<NewFolderForm onConfirm={onCreateFolder} onCancel={() => setShowNewFolderDrawer(false)} /> <NewFolderForm onConfirm={onCreateFolder} onCancel={() => setShowNewFolderDrawer(false)} />
)} )}

View File

@ -33,8 +33,8 @@ jest.mock('./BrowseActions/DescendantCount', () => ({
DescendantCount: () => <div data-testid="descendant-count">2 folders, 5 dashboards</div>, DescendantCount: () => <div data-testid="descendant-count">2 folders, 5 dashboards</div>,
})); }));
jest.mock('app/features/dashboard-scene/components/Provisioned/DashboardEditFormSharedFields', () => ({ jest.mock('app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields', () => ({
DashboardEditFormSharedFields: () => <div data-testid="shared-fields" />, ResourceEditFormSharedFields: () => <div data-testid="shared-fields" />,
})); }));
const mockUseDeleteRepositoryFilesMutation = useDeleteRepositoryFilesWithPathMutation as jest.MockedFunction< const mockUseDeleteRepositoryFilesMutation = useDeleteRepositoryFilesWithPathMutation as jest.MockedFunction<

View File

@ -8,7 +8,7 @@ import { Box, Button, Stack } from '@grafana/ui';
import { Folder } from 'app/api/clients/folder/v1beta1'; import { Folder } from 'app/api/clients/folder/v1beta1';
import { RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; import { RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1';
import { AnnoKeySourcePath } from 'app/features/apiserver/types'; import { AnnoKeySourcePath } from 'app/features/apiserver/types';
import { DashboardEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/DashboardEditFormSharedFields'; import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields';
import { BaseProvisionedFormData } from 'app/features/dashboard-scene/saving/shared'; import { BaseProvisionedFormData } from 'app/features/dashboard-scene/saving/shared';
import { FolderDTO } from 'app/types'; import { FolderDTO } from 'app/types';
@ -121,7 +121,7 @@ function FormContent({
/> />
</Box> </Box>
<DashboardEditFormSharedFields <ResourceEditFormSharedFields
resourceType="folder" resourceType="folder"
isNew={false} isNew={false}
workflow={workflow} workflow={workflow}

View File

@ -3,13 +3,12 @@ import userEvent from '@testing-library/user-event';
import { AppEvents } from '@grafana/data'; import { AppEvents } from '@grafana/data';
import { getAppEvents } from '@grafana/runtime'; import { getAppEvents } from '@grafana/runtime';
import { useGetFolderQuery } from 'app/api/clients/folder/v1beta1';
import { useCreateRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; import { useCreateRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1';
import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv'; import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv';
import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam'; import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
import { FolderDTO } from '../../../types'; import { FolderDTO } from '../../../types';
import { ProvisionedFolderFormDataResult, useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData';
import { NewProvisionedFolderForm } from './NewProvisionedFolderForm'; import { NewProvisionedFolderForm } from './NewProvisionedFolderForm';
@ -41,9 +40,9 @@ jest.mock('app/api/clients/provisioning/v0alpha1', () => {
}; };
}); });
jest.mock('app/api/clients/folder/v1beta1', () => { jest.mock('../hooks/useProvisionedFolderFormData', () => {
return { return {
useGetFolderQuery: jest.fn(), useProvisionedFolderFormData: jest.fn(),
}; };
}); });
@ -53,12 +52,6 @@ jest.mock('app/features/provisioning/hooks/usePullRequestParam', () => {
}; };
}); });
jest.mock('app/features/provisioning/hooks/useGetResourceRepositoryView', () => {
return {
useGetResourceRepositoryView: jest.fn(),
};
});
jest.mock('react-router-dom-v5-compat', () => { jest.mock('react-router-dom-v5-compat', () => {
const actual = jest.requireActual('react-router-dom-v5-compat'); const actual = jest.requireActual('react-router-dom-v5-compat');
return { return {
@ -79,17 +72,15 @@ jest.mock('../../dashboard-scene/saving/provisioned/defaults', () => {
}); });
interface Props { interface Props {
onSubmit: () => void; onDismiss?: () => void;
onCancel: () => void; parentFolder?: FolderDTO;
parentFolder: FolderDTO;
} }
function setup(props: Partial<Props> = {}) { function setup(props: Partial<Props> = {}, hookData = mockHookData) {
const user = userEvent.setup(); const user = userEvent.setup();
const defaultProps: Props = { const defaultProps: Props = {
onSubmit: jest.fn(), onDismiss: jest.fn(),
onCancel: jest.fn(),
parentFolder: { parentFolder: {
id: 1, id: 1,
uid: 'folder-uid', uid: 'folder-uid',
@ -108,6 +99,8 @@ function setup(props: Partial<Props> = {}) {
...props, ...props,
}; };
(useProvisionedFolderFormData as jest.Mock).mockReturnValue(hookData);
return { return {
user, user,
...render(<NewProvisionedFolderForm {...defaultProps} />), ...render(<NewProvisionedFolderForm {...defaultProps} />),
@ -123,6 +116,40 @@ const mockRequest = {
data: { resource: { upsert: { metadata: { name: 'new-folder' } } } }, data: { resource: { upsert: { metadata: { name: 'new-folder' } } } },
}; };
const mockHookData: ProvisionedFolderFormDataResult = {
repository: {
name: 'test-repo',
title: 'Test Repository',
type: 'github',
workflows: ['write', 'branch'],
target: 'folder',
},
folder: {
metadata: {
annotations: {
'grafana.app/sourcePath': '/dashboards',
},
},
spec: {
title: '',
},
status: {},
},
workflowOptions: [
{ label: 'Commit directly', value: 'write' },
{ label: 'Create a branch', value: 'branch' },
],
isGitHub: true,
initialValues: {
title: '',
comment: '',
ref: 'folder/test-timestamp',
repo: 'test-repo',
path: '/dashboards',
workflow: 'write',
},
};
describe('NewProvisionedFolderForm', () => { describe('NewProvisionedFolderForm', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
@ -133,39 +160,11 @@ describe('NewProvisionedFolderForm', () => {
}; };
(getAppEvents as jest.Mock).mockReturnValue(mockAppEvents); (getAppEvents as jest.Mock).mockReturnValue(mockAppEvents);
(useGetResourceRepositoryView as jest.Mock).mockReturnValue({
isLoading: false,
repository: {
name: 'test-repo',
title: 'Test Repository',
type: 'github',
github: {
url: 'https://github.com/grafana/grafana',
branch: 'main',
},
workflows: [{ name: 'default', path: 'workflows/default.json' }],
},
});
// Mock useGetFolderQuery
(useGetFolderQuery as jest.Mock).mockReturnValue({
data: {
metadata: {
annotations: {
'source.path': '/dashboards',
},
},
},
isLoading: false,
isError: false,
});
// Mock usePullRequestParam // Mock usePullRequestParam
(usePullRequestParam as jest.Mock).mockReturnValue(null); (usePullRequestParam as jest.Mock).mockReturnValue(null);
// Mock useCreateRepositoryFilesWithPathMutation // Mock useCreateRepositoryFilesWithPathMutation
const mockCreate = jest.fn(); const mockCreate = jest.fn();
(useCreateRepositoryFilesWithPathMutation as jest.Mock).mockReturnValue([mockCreate, mockRequest]); (useCreateRepositoryFilesWithPathMutation as jest.Mock).mockReturnValue([mockCreate, mockRequest]);
(validationSrv.validateNewFolderName as jest.Mock).mockResolvedValue(true); (validationSrv.validateNewFolderName as jest.Mock).mockResolvedValue(true);
@ -182,25 +181,27 @@ describe('NewProvisionedFolderForm', () => {
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
}); });
it('should show loading state when repository data is loading', () => { it('should return null when initialValues is not available', () => {
(useGetResourceRepositoryView as jest.Mock).mockReturnValue({ const { container } = setup(
isLoading: true, {},
}); {
...mockHookData,
setup(); initialValues: undefined,
}
expect(screen.getByTestId('Spinner')).toBeInTheDocument(); );
expect(container.firstChild).toBeNull();
}); });
it('should show error when repository is not found', () => { it('should show error when repository is not found', () => {
(useGetResourceRepositoryView as jest.Mock).mockReturnValue({ const { container } = setup(
isLoading: false, {},
repository: undefined, {
}); ...mockHookData,
repository: undefined,
setup(); initialValues: undefined,
}
expect(screen.getByText('Repository not found')).toBeInTheDocument(); );
expect(container.firstChild).toBeNull();
}); });
it('should show branch field when branch workflow is selected', async () => { it('should show branch field when branch workflow is selected', async () => {
@ -289,6 +290,7 @@ describe('NewProvisionedFolderForm', () => {
expect.objectContaining({ expect.objectContaining({
ref: undefined, // write workflow uses undefined ref ref: undefined, // write workflow uses undefined ref
name: 'test-repo', name: 'test-repo',
path: '/dashboards/new-test-folder/',
message: 'Creating a new test folder', message: 'Creating a new test folder',
body: { body: {
title: 'New Test Folder', title: 'New Test Folder',
@ -298,8 +300,8 @@ describe('NewProvisionedFolderForm', () => {
); );
}); });
// Check if onSubmit was called // Check if onDismiss was called
expect(props.onSubmit).toHaveBeenCalled(); expect(props.onDismiss).toHaveBeenCalled();
}); });
it('should create folder with branch workflow', async () => { it('should create folder with branch workflow', async () => {
@ -341,6 +343,7 @@ describe('NewProvisionedFolderForm', () => {
expect.objectContaining({ expect.objectContaining({
ref: 'feature/new-folder', ref: 'feature/new-folder',
name: 'test-repo', name: 'test-repo',
path: '/dashboards/branch-folder/',
message: 'Create folder: Branch Folder', message: 'Create folder: Branch Folder',
body: { body: {
title: 'Branch Folder', title: 'Branch Folder',
@ -415,33 +418,31 @@ describe('NewProvisionedFolderForm', () => {
expect(screen.getByRole('link')).toHaveTextContent('https://github.com/grafana/grafana/pull/1234'); expect(screen.getByRole('link')).toHaveTextContent('https://github.com/grafana/grafana/pull/1234');
}); });
it('should call onCancel when cancel button is clicked', async () => { it('should call onDismiss when cancel button is clicked', async () => {
const { user, props } = setup(); const { user, props } = setup();
// Click cancel button // Click cancel button
const cancelButton = screen.getByRole('button', { name: /cancel/i }); const cancelButton = screen.getByRole('button', { name: /cancel/i });
await user.click(cancelButton); await user.click(cancelButton);
// Check if onCancel was called expect(props.onDismiss).toHaveBeenCalled();
expect(props.onCancel).toHaveBeenCalled();
}); });
it('should show read-only alert when repository has no workflows', () => { it('should show read-only alert when repository has no workflows', () => {
// Mock repository with empty workflows array // Mock repository with empty workflows array
(useGetResourceRepositoryView as jest.Mock).mockReturnValue({ setup(
repository: { {},
name: 'test-repo', {
title: 'Test Repository', ...mockHookData,
type: 'github', repository: {
github: { name: 'test-repo',
url: 'https://github.com/grafana/grafana', title: 'Test Repository',
branch: 'main', type: 'github',
workflows: [],
target: 'folder',
}, },
workflows: [], }
}, );
});
setup();
// Read-only alert should be visible // Read-only alert should be visible
expect(screen.getByText('This repository is read only')).toBeInTheDocument(); expect(screen.getByText('This repository is read only')).toBeInTheDocument();

View File

@ -1,73 +1,52 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom-v5-compat'; import { useNavigate } from 'react-router-dom-v5-compat';
import { AppEvents } from '@grafana/data'; import { AppEvents } from '@grafana/data';
import { Trans, t } from '@grafana/i18n'; import { Trans, t } from '@grafana/i18n';
import { getAppEvents } from '@grafana/runtime'; import { getAppEvents } from '@grafana/runtime';
import { Alert, Button, Field, Input, RadioButtonGroup, Spinner, Stack, TextArea } from '@grafana/ui'; import { Alert, Button, Field, Input, Stack } from '@grafana/ui';
import { useCreateRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; 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 { AnnoKeySourcePath, Resource } from 'app/features/apiserver/types';
import { getDefaultWorkflow, getWorkflowOptions } from 'app/features/dashboard-scene/saving/provisioned/defaults'; import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields';
import { BaseProvisionedFormData } from 'app/features/dashboard-scene/saving/shared';
import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv'; import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv';
import { BranchValidationError } from 'app/features/provisioning/Shared/BranchValidationError';
import { PROVISIONING_URL } from 'app/features/provisioning/constants'; import { PROVISIONING_URL } from 'app/features/provisioning/constants';
import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam'; import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
import { WorkflowOption } from 'app/features/provisioning/types';
import { validateBranchName } from 'app/features/provisioning/utils/git';
import { FolderDTO } from 'app/types'; import { FolderDTO } from 'app/types';
type FormData = { import { useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData';
ref?: string; interface FormProps extends Props {
path: string; initialValues: BaseProvisionedFormData;
comment?: string; repository?: RepositoryView;
repo: string; workflowOptions: Array<{ label: string; value: string }>;
workflow?: WorkflowOption; folder?: Folder;
title: string; isGitHub: boolean;
}; }
interface Props { interface Props {
onSubmit: () => void;
onCancel: () => void;
parentFolder?: FolderDTO; parentFolder?: FolderDTO;
onDismiss?: () => void;
} }
const initialFormValues: Partial<FormData> = { function FormContent({ initialValues, repository, workflowOptions, folder, isGitHub, onDismiss }: FormProps) {
title: '',
comment: '',
ref: `folder/${Date.now()}`,
};
// TODO: use useProvisionedFolderFormData hook to manage form data and repository state
export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: Props) {
const { repository, folder, isLoading } = useGetResourceRepositoryView({ folderName: parentFolder?.uid });
const prURL = usePullRequestParam(); const prURL = usePullRequestParam();
const navigate = useNavigate(); const navigate = useNavigate();
const [create, request] = useCreateRepositoryFilesWithPathMutation(); const [create, request] = useCreateRepositoryFilesWithPathMutation();
const isGitHub = Boolean(repository?.type === 'github'); const methods = useForm<BaseProvisionedFormData>({
defaultValues: initialValues,
const { mode: 'onBlur', // Validates when user leaves the field
register, });
handleSubmit, const { handleSubmit, watch, register, formState } = methods;
watch,
formState: { errors },
control,
setValue,
} = useForm<FormData>({ defaultValues: { ...initialFormValues, workflow: getDefaultWorkflow(repository) } });
const [workflow, ref] = watch(['workflow', 'ref']); const [workflow, ref] = watch(['workflow', 'ref']);
useEffect(() => {
setValue('workflow', getDefaultWorkflow(repository));
}, [repository, setValue]);
// TODO: replace with useProvisionedRequestHandler hook // TODO: replace with useProvisionedRequestHandler hook
useEffect(() => { useEffect(() => {
const appEvents = getAppEvents(); const appEvents = getAppEvents();
if (request.isSuccess && repository) { if (request.isSuccess && repository) {
onSubmit(); onDismiss?.();
appEvents.publish({ appEvents.publish({
type: AppEvents.alertSuccess.name, type: AppEvents.alertSuccess.name,
@ -101,20 +80,7 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
], ],
}); });
} }
}, [request.isSuccess, request.isError, request.error, onSubmit, ref, request.data, workflow, navigate, repository]); }, [request.isSuccess, request.isError, request.error, ref, request.data, workflow, navigate, repository, onDismiss]);
if (isLoading) {
return <Spinner />;
}
if (!repository) {
return (
<Alert
title={t('browse-dashboards.new-provisioned-folder-form.title-repository-not-found', 'Repository not found')}
severity="error"
/>
);
}
const validateFolderName = async (folderName: string) => { const validateFolderName = async (folderName: string) => {
try { try {
@ -128,7 +94,7 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
} }
}; };
const doSave = async ({ ref, title, workflow, comment }: FormData) => { const doSave = async ({ ref, title, workflow, comment }: BaseProvisionedFormData) => {
const repoName = repository?.name; const repoName = repository?.name;
if (!title || !repoName) { if (!title || !repoName) {
return; return;
@ -163,107 +129,103 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
}; };
return ( return (
<form onSubmit={handleSubmit(doSave)}> <FormProvider {...methods}>
<Stack direction="column" gap={2}> <form onSubmit={handleSubmit(doSave)}>
{!repository?.workflows?.length && ( <Stack direction="column" gap={2}>
<Alert {!repository?.workflows?.length && (
title={t( <Alert
'browse-dashboards.new-provisioned-folder-form.title-this-repository-is-read-only', title={t(
'This repository is read only' 'browse-dashboards.new-provisioned-folder-form.title-this-repository-is-read-only',
)} 'This repository is read only'
)}
>
<Trans i18nKey="browse-dashboards.text-this-repository-is-read-only">
If you have direct access to the target, copy the JSON and paste it there.
</Trans>
</Alert>
)}
<Field
noMargin
label={t('browse-dashboards.new-provisioned-folder-form.label-folder-name', 'Folder name')}
invalid={!!formState.errors.title}
error={formState.errors.title?.message}
> >
<Trans i18nKey="browse-dashboards.text-this-repository-is-read-only"> <Input
If you have direct access to the target, copy the JSON and paste it there. {...register('title', {
</Trans> required: t('browse-dashboards.new-provisioned-folder-form.error-required', 'Folder name is required'),
</Alert> validate: validateFolderName,
)} })}
placeholder={t(
'browse-dashboards.new-provisioned-folder-form.folder-name-input-placeholder-enter-folder-name',
'Enter folder name'
)}
id="folder-name-input"
/>
</Field>
<Field <ResourceEditFormSharedFields
label={t('browse-dashboards.new-provisioned-folder-form.label-folder-name', 'Folder name')} resourceType="folder"
invalid={!!errors.title} isNew={false}
error={errors.title?.message} workflow={workflow}
> workflowOptions={workflowOptions}
<Input isGitHub={isGitHub}
{...register('title', { hidePath
required: t('browse-dashboards.new-provisioned-folder-form.error-required', 'Folder name is required'),
validate: validateFolderName,
})}
placeholder={t(
'browse-dashboards.new-provisioned-folder-form.folder-name-input-placeholder-enter-folder-name',
'Enter folder name'
)}
id="folder-name-input"
/> />
</Field>
{/* TODO: use DashboardEditFormSharedFields to replace comment and workflow input*/} {prURL && (
<Field label={t('browse-dashboards.new-provisioned-folder-form.label-comment', 'Comment')}> <Alert
<TextArea severity="info"
{...register('comment')} title={t(
placeholder={t( 'browse-dashboards.new-provisioned-folder-form.title-pull-request-created',
'browse-dashboards.new-provisioned-folder-form.folder-comment-input-placeholder-describe-changes-optional', 'Pull request created'
'Add a note to describe your changes (optional)' )}
)} >
id="folder-comment-input" <Trans i18nKey="browse-dashboards.new-provisioned-folder-form.text-pull-request-created">
rows={5} A pull request has been created with changes to this folder:
/> </Trans>{' '}
</Field> <a href={prURL} target="_blank" rel="noopener noreferrer">
{prURL}
</a>
</Alert>
)}
{isGitHub && ( <Stack gap={2}>
<> <Button variant="secondary" fill="outline" onClick={onDismiss}>
<Field label={t('browse-dashboards.new-provisioned-folder-form.label-workflow', 'Workflow')}> <Trans i18nKey="browse-dashboards.new-provisioned-folder-form.cancel">Cancel</Trans>
<Controller </Button>
control={control} <Button type="submit" disabled={request.isLoading}>
name="workflow" {request.isLoading
render={({ field: { ref, ...field } }) => ( ? t('browse-dashboards.new-provisioned-folder-form.button-creating', 'Creating...')
<RadioButtonGroup {...field} options={getWorkflowOptions(repository)} id={'folder-workflow'} /> : t('browse-dashboards.new-provisioned-folder-form.button-create', 'Create')}
)} </Button>
/> </Stack>
</Field>
{workflow === 'branch' && (
<Field
label={t('browse-dashboards.new-provisioned-folder-form.label-branch', 'Branch')}
description={t(
'browse-dashboards.new-provisioned-folder-form.description-branch-name-in-git-hub',
'Branch name in GitHub'
)}
invalid={!!errors?.ref}
error={errors.ref ? <BranchValidationError /> : ''}
>
<Input {...register('ref', { validate: validateBranchName })} id="branch-name-input" />
</Field>
)}
</>
)}
{prURL && (
<Alert
severity="info"
title={t(
'browse-dashboards.new-provisioned-folder-form.title-pull-request-created',
'Pull request created'
)}
>
<Trans i18nKey="browse-dashboards.new-provisioned-folder-form.text-pull-request-created">
A pull request has been created with changes to this folder:
</Trans>{' '}
<a href={prURL} target="_blank" rel="noopener noreferrer">
{prURL}
</a>
</Alert>
)}
<Stack gap={2}>
<Button variant="secondary" fill="outline" onClick={onCancel}>
<Trans i18nKey="browse-dashboards.new-provisioned-folder-form.cancel">Cancel</Trans>
</Button>
<Button type="submit" disabled={request.isLoading}>
{request.isLoading
? t('browse-dashboards.new-provisioned-folder-form.button-creating', 'Creating...')
: t('browse-dashboards.new-provisioned-folder-form.button-create', 'Create')}
</Button>
</Stack> </Stack>
</Stack> </form>
</form> </FormProvider>
);
}
export function NewProvisionedFolderForm({ parentFolder, onDismiss }: Props) {
const { workflowOptions, isGitHub, repository, folder, initialValues } = useProvisionedFolderFormData({
folderUid: parentFolder?.uid,
action: 'create',
title: parentFolder?.title,
});
if (!initialValues) {
return null;
}
return (
<FormContent
parentFolder={parentFolder}
onDismiss={onDismiss}
initialValues={initialValues}
repository={repository}
workflowOptions={workflowOptions}
folder={folder}
isGitHub={isGitHub}
/>
); );
} }

View File

@ -5,7 +5,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import { ProvisionedDashboardFormData } from '../../saving/shared'; import { ProvisionedDashboardFormData } from '../../saving/shared';
import { DashboardEditFormSharedFields } from './DashboardEditFormSharedFields'; import { ResourceEditFormSharedFields } from './ResourceEditFormSharedFields';
// Mock the i18n hook since it's used in the component // Mock the i18n hook since it's used in the component
jest.mock('@grafana/i18n', () => ({ jest.mock('@grafana/i18n', () => ({
@ -65,13 +65,13 @@ function setup(options: SetupOptions = {}) {
user, user,
...render( ...render(
<FormWrapper> <FormWrapper>
<DashboardEditFormSharedFields {...componentProps} resourceType="dashboard" /> <ResourceEditFormSharedFields {...componentProps} resourceType="dashboard" />
</FormWrapper> </FormWrapper>
), ),
}; };
} }
describe('DashboardEditFormSharedFields', () => { describe('ResourceEditFormSharedFields', () => {
describe('Basic Rendering', () => { describe('Basic Rendering', () => {
it('should render path and comment fields by default', () => { it('should render path and comment fields by default', () => {
setup(); setup();
@ -186,7 +186,7 @@ describe('DashboardEditFormSharedFields', () => {
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<DashboardEditFormSharedFields <ResourceEditFormSharedFields
workflowOptions={[ workflowOptions={[
{ label: 'Write directly', value: 'write' }, { label: 'Write directly', value: 'write' },
{ label: 'Create branch', value: 'branch' }, { label: 'Create branch', value: 'branch' },

View File

@ -14,10 +14,11 @@ interface DashboardEditFormSharedFieldsProps {
readOnly?: boolean; readOnly?: boolean;
workflow?: WorkflowOption; workflow?: WorkflowOption;
isGitHub?: boolean; isGitHub?: boolean;
hidePath?: boolean;
} }
export const DashboardEditFormSharedFields = memo<DashboardEditFormSharedFieldsProps>( export const ResourceEditFormSharedFields = memo<DashboardEditFormSharedFieldsProps>(
({ readOnly = false, workflow, workflowOptions, isGitHub, isNew, resourceType }) => { ({ readOnly = false, workflow, workflowOptions, isGitHub, isNew, resourceType, hidePath = false }) => {
const { const {
control, control,
register, register,
@ -32,16 +33,18 @@ export const DashboardEditFormSharedFields = memo<DashboardEditFormSharedFieldsP
return ( return (
<> <>
{/* Path */} {/* Path */}
<Field {!hidePath && (
noMargin <Field
label={t('provisioned-resource-form.save-or-delete-resource-shared-fields.label-path', 'Path')} noMargin
description={t( label={t('provisioned-resource-form.save-or-delete-resource-shared-fields.label-path', 'Path')}
'provisioned-resource-form.save-or-delete-resource-shared-fields.description-inside-repository', description={t(
pathText 'provisioned-resource-form.save-or-delete-resource-shared-fields.description-inside-repository',
)} pathText
> )}
<Input id="dashboard-path" type="text" {...register('path')} readOnly={!isNew} /> >
</Field> <Input id="dashboard-path" type="text" {...register('path')} readOnly={!isNew} />
</Field>
)}
{/* Comment */} {/* Comment */}
<Field <Field

View File

@ -14,7 +14,7 @@ import { validationSrv } from 'app/features/manage-dashboards/services/Validatio
import { PROVISIONING_URL } from 'app/features/provisioning/constants'; import { PROVISIONING_URL } from 'app/features/provisioning/constants';
import { useCreateOrUpdateRepositoryFile } from 'app/features/provisioning/hooks/useCreateOrUpdateRepositoryFile'; import { useCreateOrUpdateRepositoryFile } from 'app/features/provisioning/hooks/useCreateOrUpdateRepositoryFile';
import { DashboardEditFormSharedFields } from '../../components/Provisioned/DashboardEditFormSharedFields'; import { ResourceEditFormSharedFields } from '../../components/Provisioned/ResourceEditFormSharedFields';
import { getDashboardUrl } from '../../utils/getDashboardUrl'; import { getDashboardUrl } from '../../utils/getDashboardUrl';
import { useProvisionedRequestHandler } from '../../utils/useProvisionedRequestHandler'; import { useProvisionedRequestHandler } from '../../utils/useProvisionedRequestHandler';
import { SaveDashboardFormCommonOptions } from '../SaveDashboardForm'; import { SaveDashboardFormCommonOptions } from '../SaveDashboardForm';
@ -214,7 +214,7 @@ export function SaveProvisionedDashboardForm({
{!isNew && !readOnly && <SaveDashboardFormCommonOptions drawer={drawer} changeInfo={changeInfo} />} {!isNew && !readOnly && <SaveDashboardFormCommonOptions drawer={drawer} changeInfo={changeInfo} />}
<DashboardEditFormSharedFields <ResourceEditFormSharedFields
resourceType="dashboard" resourceType="dashboard"
readOnly={readOnly} readOnly={readOnly}
workflow={workflow} workflow={workflow}

View File

@ -34,8 +34,8 @@ jest.mock('react-router-dom-v5-compat', () => ({
const mockNavigate = jest.fn(); const mockNavigate = jest.fn();
// Mock shared form components // Mock shared form components
jest.mock('../components/Provisioned/DashboardEditFormSharedFields', () => ({ jest.mock('../components/Provisioned/ResourceEditFormSharedFields', () => ({
DashboardEditFormSharedFields: ({ disabled }: { disabled: boolean }) => ( ResourceEditFormSharedFields: ({ disabled }: { disabled: boolean }) => (
<textarea data-testid="shared-fields" disabled={disabled} /> <textarea data-testid="shared-fields" disabled={disabled} />
), ),
})); }));

View File

@ -8,7 +8,7 @@ import { Alert, Button, Drawer, Stack } from '@grafana/ui';
import { useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; import { useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1';
import { PROVISIONING_URL } from 'app/features/provisioning/constants'; import { PROVISIONING_URL } from 'app/features/provisioning/constants';
import { DashboardEditFormSharedFields } from '../components/Provisioned/DashboardEditFormSharedFields'; import { ResourceEditFormSharedFields } from '../components/Provisioned/ResourceEditFormSharedFields';
import { ProvisionedDashboardFormData } from '../saving/shared'; import { ProvisionedDashboardFormData } from '../saving/shared';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { useProvisionedRequestHandler } from '../utils/useProvisionedRequestHandler'; import { useProvisionedRequestHandler } from '../utils/useProvisionedRequestHandler';
@ -121,7 +121,7 @@ export function DeleteProvisionedDashboardForm({
</Alert> </Alert>
)} )}
<DashboardEditFormSharedFields <ResourceEditFormSharedFields
resourceType="dashboard" resourceType="dashboard"
isNew={isNew} isNew={isNew}
readOnly={readOnly} readOnly={readOnly}

View File

@ -3473,18 +3473,12 @@
"button-create": "Create", "button-create": "Create",
"button-creating": "Creating...", "button-creating": "Creating...",
"cancel": "Cancel", "cancel": "Cancel",
"description-branch-name-in-git-hub": "Branch name in GitHub",
"error-invalid-folder-name": "Invalid folder name", "error-invalid-folder-name": "Invalid folder name",
"error-required": "Folder name is required", "error-required": "Folder name is required",
"folder-comment-input-placeholder-describe-changes-optional": "Add a note to describe your changes (optional)",
"folder-name-input-placeholder-enter-folder-name": "Enter folder name", "folder-name-input-placeholder-enter-folder-name": "Enter folder name",
"label-branch": "Branch",
"label-comment": "Comment",
"label-folder-name": "Folder name", "label-folder-name": "Folder name",
"label-workflow": "Workflow",
"text-pull-request-created": "A pull request has been created with changes to this folder:", "text-pull-request-created": "A pull request has been created with changes to this folder:",
"title-pull-request-created": "Pull request created", "title-pull-request-created": "Pull request created",
"title-repository-not-found": "Repository not found",
"title-this-repository-is-read-only": "This repository is read only" "title-this-repository-is-read-only": "This repository is read only"
}, },
"no-results": { "no-results": {