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": [
[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": [
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"],
[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"
>
{parentFolder?.managedBy === ManagerKind.Repo || isProvisionedInstance ? (
<NewProvisionedFolderForm
onSubmit={() => setShowNewFolderDrawer(false)}
onCancel={() => setShowNewFolderDrawer(false)}
parentFolder={parentFolder}
/>
<NewProvisionedFolderForm onDismiss={() => setShowNewFolderDrawer(false)} parentFolder={parentFolder} />
) : (
<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>,
}));
jest.mock('app/features/dashboard-scene/components/Provisioned/DashboardEditFormSharedFields', () => ({
DashboardEditFormSharedFields: () => <div data-testid="shared-fields" />,
jest.mock('app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields', () => ({
ResourceEditFormSharedFields: () => <div data-testid="shared-fields" />,
}));
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 { RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1';
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 { FolderDTO } from 'app/types';
@ -121,7 +121,7 @@ function FormContent({
/>
</Box>
<DashboardEditFormSharedFields
<ResourceEditFormSharedFields
resourceType="folder"
isNew={false}
workflow={workflow}

View File

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

View File

@ -1,73 +1,52 @@
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 { AppEvents } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { getAppEvents } from '@grafana/runtime';
import { Alert, Button, Field, Input, RadioButtonGroup, Spinner, Stack, TextArea } from '@grafana/ui';
import { useCreateRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1';
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 { 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 { BranchValidationError } from 'app/features/provisioning/Shared/BranchValidationError';
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 { WorkflowOption } from 'app/features/provisioning/types';
import { validateBranchName } from 'app/features/provisioning/utils/git';
import { FolderDTO } from 'app/types';
type FormData = {
ref?: string;
path: string;
comment?: string;
repo: string;
workflow?: WorkflowOption;
title: string;
};
import { useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData';
interface FormProps extends Props {
initialValues: BaseProvisionedFormData;
repository?: RepositoryView;
workflowOptions: Array<{ label: string; value: string }>;
folder?: Folder;
isGitHub: boolean;
}
interface Props {
onSubmit: () => void;
onCancel: () => void;
parentFolder?: FolderDTO;
onDismiss?: () => void;
}
const initialFormValues: Partial<FormData> = {
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 });
function FormContent({ initialValues, repository, workflowOptions, folder, isGitHub, onDismiss }: FormProps) {
const prURL = usePullRequestParam();
const navigate = useNavigate();
const [create, request] = useCreateRepositoryFilesWithPathMutation();
const isGitHub = Boolean(repository?.type === 'github');
const {
register,
handleSubmit,
watch,
formState: { errors },
control,
setValue,
} = useForm<FormData>({ defaultValues: { ...initialFormValues, workflow: getDefaultWorkflow(repository) } });
const methods = useForm<BaseProvisionedFormData>({
defaultValues: initialValues,
mode: 'onBlur', // Validates when user leaves the field
});
const { handleSubmit, watch, register, formState } = methods;
const [workflow, ref] = watch(['workflow', 'ref']);
useEffect(() => {
setValue('workflow', getDefaultWorkflow(repository));
}, [repository, setValue]);
// TODO: replace with useProvisionedRequestHandler hook
useEffect(() => {
const appEvents = getAppEvents();
if (request.isSuccess && repository) {
onSubmit();
onDismiss?.();
appEvents.publish({
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]);
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"
/>
);
}
}, [request.isSuccess, request.isError, request.error, ref, request.data, workflow, navigate, repository, onDismiss]);
const validateFolderName = async (folderName: string) => {
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;
if (!title || !repoName) {
return;
@ -163,107 +129,103 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
};
return (
<form onSubmit={handleSubmit(doSave)}>
<Stack direction="column" gap={2}>
{!repository?.workflows?.length && (
<Alert
title={t(
'browse-dashboards.new-provisioned-folder-form.title-this-repository-is-read-only',
'This repository is read only'
)}
<FormProvider {...methods}>
<form onSubmit={handleSubmit(doSave)}>
<Stack direction="column" gap={2}>
{!repository?.workflows?.length && (
<Alert
title={t(
'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">
If you have direct access to the target, copy the JSON and paste it there.
</Trans>
</Alert>
)}
<Input
{...register('title', {
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>
<Field
label={t('browse-dashboards.new-provisioned-folder-form.label-folder-name', 'Folder name')}
invalid={!!errors.title}
error={errors.title?.message}
>
<Input
{...register('title', {
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"
<ResourceEditFormSharedFields
resourceType="folder"
isNew={false}
workflow={workflow}
workflowOptions={workflowOptions}
isGitHub={isGitHub}
hidePath
/>
</Field>
{/* TODO: use DashboardEditFormSharedFields to replace comment and workflow input*/}
<Field label={t('browse-dashboards.new-provisioned-folder-form.label-comment', 'Comment')}>
<TextArea
{...register('comment')}
placeholder={t(
'browse-dashboards.new-provisioned-folder-form.folder-comment-input-placeholder-describe-changes-optional',
'Add a note to describe your changes (optional)'
)}
id="folder-comment-input"
rows={5}
/>
</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>
)}
{isGitHub && (
<>
<Field label={t('browse-dashboards.new-provisioned-folder-form.label-workflow', 'Workflow')}>
<Controller
control={control}
name="workflow"
render={({ field: { ref, ...field } }) => (
<RadioButtonGroup {...field} options={getWorkflowOptions(repository)} id={'folder-workflow'} />
)}
/>
</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 gap={2}>
<Button variant="secondary" fill="outline" onClick={onDismiss}>
<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 { DashboardEditFormSharedFields } from './DashboardEditFormSharedFields';
import { ResourceEditFormSharedFields } from './ResourceEditFormSharedFields';
// Mock the i18n hook since it's used in the component
jest.mock('@grafana/i18n', () => ({
@ -65,13 +65,13 @@ function setup(options: SetupOptions = {}) {
user,
...render(
<FormWrapper>
<DashboardEditFormSharedFields {...componentProps} resourceType="dashboard" />
<ResourceEditFormSharedFields {...componentProps} resourceType="dashboard" />
</FormWrapper>
),
};
}
describe('DashboardEditFormSharedFields', () => {
describe('ResourceEditFormSharedFields', () => {
describe('Basic Rendering', () => {
it('should render path and comment fields by default', () => {
setup();
@ -186,7 +186,7 @@ describe('DashboardEditFormSharedFields', () => {
return (
<FormProvider {...methods}>
<DashboardEditFormSharedFields
<ResourceEditFormSharedFields
workflowOptions={[
{ label: 'Write directly', value: 'write' },
{ label: 'Create branch', value: 'branch' },

View File

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

View File

@ -34,8 +34,8 @@ jest.mock('react-router-dom-v5-compat', () => ({
const mockNavigate = jest.fn();
// Mock shared form components
jest.mock('../components/Provisioned/DashboardEditFormSharedFields', () => ({
DashboardEditFormSharedFields: ({ disabled }: { disabled: boolean }) => (
jest.mock('../components/Provisioned/ResourceEditFormSharedFields', () => ({
ResourceEditFormSharedFields: ({ disabled }: { disabled: boolean }) => (
<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 { 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 { DashboardScene } from '../scene/DashboardScene';
import { useProvisionedRequestHandler } from '../utils/useProvisionedRequestHandler';
@ -121,7 +121,7 @@ export function DeleteProvisionedDashboardForm({
</Alert>
)}
<DashboardEditFormSharedFields
<ResourceEditFormSharedFields
resourceType="dashboard"
isNew={isNew}
readOnly={readOnly}

View File

@ -3473,18 +3473,12 @@
"button-create": "Create",
"button-creating": "Creating...",
"cancel": "Cancel",
"description-branch-name-in-git-hub": "Branch name in GitHub",
"error-invalid-folder-name": "Invalid folder name",
"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",
"label-branch": "Branch",
"label-comment": "Comment",
"label-folder-name": "Folder name",
"label-workflow": "Workflow",
"text-pull-request-created": "A pull request has been created with changes to this folder:",
"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"
},
"no-results": {