mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 10:57:19 +08:00
Provisioning: Fix the referenced path and generate name (#103424)
This commit is contained in:
@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event';
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { getAppEvents, locationService } from '@grafana/runtime';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import { AnnoKeyFolder, AnnoKeySourcePath } from 'app/features/apiserver/types';
|
||||
import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv';
|
||||
import { useCreateOrUpdateRepositoryFile } from 'app/features/provisioning/hooks/useCreateOrUpdateRepositoryFile';
|
||||
|
||||
@ -196,12 +197,20 @@ describe('SaveProvisionedDashboardForm', () => {
|
||||
it('should save a new dashboard successfully', async () => {
|
||||
const { user, props } = setup();
|
||||
const newDashboard = {
|
||||
title: 'New Dashboard',
|
||||
description: 'New Description',
|
||||
panels: [],
|
||||
schemaVersion: 36,
|
||||
apiVersion: 'dashboard.grafana.app/v1alpha1',
|
||||
kind: 'Dashboard',
|
||||
metadata: {
|
||||
generateName: 'p',
|
||||
name: undefined,
|
||||
},
|
||||
spec: {
|
||||
title: 'New Dashboard',
|
||||
description: 'New Description',
|
||||
panels: [],
|
||||
schemaVersion: 36,
|
||||
},
|
||||
};
|
||||
props.dashboard.getSaveAsModel = jest.fn().mockReturnValue(newDashboard);
|
||||
props.dashboard.getSaveResource = jest.fn().mockReturnValue(newDashboard);
|
||||
const mockAction = jest.fn();
|
||||
const mockRequest = { ...mockRequestBase, isSuccess: true };
|
||||
(useCreateOrUpdateRepositoryFile as jest.Mock).mockReturnValue([mockAction, mockRequest]);
|
||||
@ -245,14 +254,26 @@ describe('SaveProvisionedDashboardForm', () => {
|
||||
});
|
||||
|
||||
it('should update an existing dashboard successfully', async () => {
|
||||
const updatedDashboard = {
|
||||
apiVersion: 'dashboard.grafana.app/vXyz',
|
||||
metadata: {
|
||||
name: 'test-dashboard',
|
||||
annotations: {
|
||||
[AnnoKeyFolder]: 'folder-uid',
|
||||
[AnnoKeySourcePath]: 'path/to/file.json',
|
||||
},
|
||||
},
|
||||
spec: { title: 'Test Dashboard', description: 'Test Description' },
|
||||
};
|
||||
const { user, props } = setup({
|
||||
isNew: false,
|
||||
dashboard: {
|
||||
useState: () => ({
|
||||
meta: {
|
||||
folderUid: 'folder-uid',
|
||||
folderUid: updatedDashboard.metadata.annotations[AnnoKeyFolder],
|
||||
slug: 'test-dashboard',
|
||||
k8s: { name: 'test-dashboard' },
|
||||
uid: updatedDashboard.metadata.name,
|
||||
k8s: updatedDashboard.metadata,
|
||||
},
|
||||
title: 'Test Dashboard',
|
||||
description: 'Test Description',
|
||||
@ -260,7 +281,7 @@ describe('SaveProvisionedDashboardForm', () => {
|
||||
}),
|
||||
setState: jest.fn(),
|
||||
closeModal: jest.fn(),
|
||||
getSaveAsModel: jest.fn().mockReturnValue({ title: 'Test Dashboard', description: 'Test Description' }),
|
||||
getSaveResource: jest.fn().mockReturnValue(updatedDashboard),
|
||||
setManager: jest.fn(),
|
||||
} as unknown as DashboardScene,
|
||||
});
|
||||
@ -268,10 +289,13 @@ describe('SaveProvisionedDashboardForm', () => {
|
||||
const mockRequest = { ...mockRequestBase, isSuccess: true };
|
||||
(useCreateOrUpdateRepositoryFile as jest.Mock).mockReturnValue([mockAction, mockRequest]);
|
||||
const pathInput = screen.getByRole('textbox', { name: /path/i });
|
||||
const commentInput = screen.getByRole('textbox', { name: /comment/i });
|
||||
expect(pathInput).toHaveAttribute('readonly'); // can not edit the path value
|
||||
pathInput.removeAttribute('readonly'); // save won't get called unless we have a value
|
||||
await user.clear(pathInput);
|
||||
await user.type(pathInput, 'path/to/file.json');
|
||||
|
||||
const commentInput = screen.getByRole('textbox', { name: /comment/i });
|
||||
await user.clear(commentInput);
|
||||
await user.type(pathInput, 'test-dashboard.json');
|
||||
await user.type(commentInput, 'Update dashboard');
|
||||
const submitButton = screen.getByRole('button', { name: /save/i });
|
||||
await user.click(submitButton);
|
||||
@ -282,9 +306,9 @@ describe('SaveProvisionedDashboardForm', () => {
|
||||
expect(mockAction).toHaveBeenCalledWith({
|
||||
ref: undefined,
|
||||
name: 'test-repo',
|
||||
path: 'test-dashboard.json',
|
||||
path: 'path/to/file.json',
|
||||
message: 'Update dashboard',
|
||||
body: expect.any(Object),
|
||||
body: updatedDashboard,
|
||||
});
|
||||
});
|
||||
expect(props.dashboard.closeModal).toHaveBeenCalled();
|
||||
@ -294,12 +318,20 @@ describe('SaveProvisionedDashboardForm', () => {
|
||||
it('should show error when save fails', async () => {
|
||||
const { user, props } = setup();
|
||||
const newDashboard = {
|
||||
title: 'New Dashboard',
|
||||
description: 'New Description',
|
||||
panels: [],
|
||||
schemaVersion: 36,
|
||||
apiVersion: 'dashboard.grafana.app/v1alpha1',
|
||||
kind: 'Dashboard',
|
||||
metadata: {
|
||||
generateName: 'p',
|
||||
name: undefined,
|
||||
},
|
||||
spec: {
|
||||
title: 'New Dashboard',
|
||||
description: 'New Description',
|
||||
panels: [],
|
||||
schemaVersion: 36,
|
||||
},
|
||||
};
|
||||
props.dashboard.getSaveAsModel = jest.fn().mockReturnValue(newDashboard);
|
||||
props.dashboard.getSaveResource = jest.fn().mockReturnValue(newDashboard);
|
||||
const mockAction = jest.fn();
|
||||
const mockRequest = {
|
||||
...mockRequestBase,
|
||||
|
@ -59,7 +59,7 @@ export function SaveProvisionedDashboardForm({
|
||||
}: Props) {
|
||||
const navigate = useNavigate();
|
||||
const appEvents = getAppEvents();
|
||||
const { meta, isDirty, editPanel: panelEditor } = dashboard.useState();
|
||||
const { isDirty, editPanel: panelEditor } = dashboard.useState();
|
||||
|
||||
const [createOrUpdateFile, request] = useCreateOrUpdateRepositoryFile(isNew ? undefined : defaultValues.path);
|
||||
|
||||
@ -134,24 +134,23 @@ export function SaveProvisionedDashboardForm({
|
||||
return;
|
||||
}
|
||||
|
||||
// The dashboard spec
|
||||
const saveModel = dashboard.getSaveAsModel({
|
||||
isNew,
|
||||
title,
|
||||
description,
|
||||
});
|
||||
|
||||
// If user is writing to the original branch, override ref with whatever we loaded from
|
||||
if (workflow === 'write') {
|
||||
ref = loadedFromRef;
|
||||
}
|
||||
|
||||
const body = dashboard.getSaveResource({
|
||||
isNew,
|
||||
title,
|
||||
description,
|
||||
});
|
||||
|
||||
createOrUpdateFile({
|
||||
ref,
|
||||
name: repo,
|
||||
path,
|
||||
message: comment,
|
||||
body: { ...saveModel, uid: meta.uid },
|
||||
body,
|
||||
});
|
||||
};
|
||||
|
||||
@ -238,7 +237,7 @@ export function SaveProvisionedDashboardForm({
|
||||
'File path inside the repository (.json or .yaml)'
|
||||
)}
|
||||
>
|
||||
<Input id="dashboard-path" {...register('path')} />
|
||||
<Input id="dashboard-path" {...register('path')} readOnly={!isNew} />
|
||||
</Field>
|
||||
|
||||
<Field label={t('dashboard-scene.save-provisioned-dashboard-form.label-comment', 'Comment')}>
|
||||
|
@ -49,14 +49,14 @@ describe('generatePath', () => {
|
||||
expect(result).toBe('folder/path/my-dashboard.json');
|
||||
});
|
||||
|
||||
it('should prepend folderPath to pathFromAnnotation when both are provided', () => {
|
||||
it('should use pathFromAnnotation when both are provided', () => {
|
||||
const result = generatePath({
|
||||
timestamp,
|
||||
pathFromAnnotation: 'my-custom-path.json',
|
||||
folderPath: 'folder/path',
|
||||
pathFromAnnotation: 'full/path/my-custom-path.json', // this is always the full path
|
||||
folderPath: 'full/path', // this will be a substring
|
||||
});
|
||||
|
||||
expect(result).toBe('folder/path/my-custom-path.json');
|
||||
expect(result).toBe('full/path/my-custom-path.json');
|
||||
});
|
||||
|
||||
it('should handle empty folderPath', () => {
|
||||
|
@ -19,12 +19,12 @@ export function generatePath({ timestamp, pathFromAnnotation, slug, folderPath =
|
||||
|
||||
if (pathFromAnnotation) {
|
||||
const hashIndex = pathFromAnnotation.indexOf('#');
|
||||
path = hashIndex > 0 ? pathFromAnnotation.substring(0, hashIndex) : pathFromAnnotation;
|
||||
} else {
|
||||
const pathSlug = slug || `new-dashboard-${timestamp}`;
|
||||
path = `${pathSlug}.json`;
|
||||
return hashIndex > 0 ? pathFromAnnotation.substring(0, hashIndex) : pathFromAnnotation;
|
||||
}
|
||||
|
||||
const pathSlug = slug || `new-dashboard-${timestamp}`;
|
||||
path = `${pathSlug}.json`;
|
||||
|
||||
// Add folder path if it exists
|
||||
if (folderPath) {
|
||||
return `${folderPath}/${path}`;
|
||||
|
@ -33,7 +33,7 @@ import { VariablesChanged } from 'app/features/variables/types';
|
||||
import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types';
|
||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||
|
||||
import { AnnoKeyManagerKind, AnnoKeySourcePath, ManagerKind } from '../../apiserver/types';
|
||||
import { AnnoKeyManagerKind, AnnoKeySourcePath, ManagerKind, ResourceForCreate } from '../../apiserver/types';
|
||||
import { DashboardEditPane } from '../edit-pane/DashboardEditPane';
|
||||
import { PanelEditor } from '../panel-edit/PanelEditor';
|
||||
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
|
||||
@ -763,6 +763,22 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
return this.serializer.getSaveModel(this);
|
||||
}
|
||||
|
||||
// Get the dashboard in native K8s form (using the appropriate apiVersion)
|
||||
getSaveResource(options: SaveDashboardAsOptions): ResourceForCreate<unknown> {
|
||||
const { meta } = this.state;
|
||||
const spec = this.getSaveAsModel(options);
|
||||
return {
|
||||
apiVersion: 'dashboard.grafana.app/v1alpha1', // get from the dashboard?
|
||||
kind: 'Dashboard',
|
||||
metadata: {
|
||||
...meta.k8s,
|
||||
name: meta.uid, // ideally the name is preserved
|
||||
generateName: options.isNew ? 'd' : undefined,
|
||||
},
|
||||
spec,
|
||||
};
|
||||
}
|
||||
|
||||
getSaveAsModel(options: SaveDashboardAsOptions): Dashboard | DashboardV2Spec {
|
||||
return this.serializer.getSaveAsModel(this, options);
|
||||
}
|
||||
|
@ -16,10 +16,10 @@ interface Props {
|
||||
export function FolderRepositoryList({ items }: Props) {
|
||||
const [query, setQuery] = useState('');
|
||||
const filteredItems = items.filter((item) => item.metadata?.name?.includes(query));
|
||||
const instanceProvisioned = checkSyncSettings(items);
|
||||
const { instanceConnected } = checkSyncSettings(items);
|
||||
return (
|
||||
<Stack direction={'column'} gap={3}>
|
||||
{!instanceProvisioned && (
|
||||
{!instanceConnected && (
|
||||
<Stack gap={2}>
|
||||
<FilterInput
|
||||
placeholder={t('provisioning.folder-repository-list.placeholder-search', 'Search')}
|
||||
|
Reference in New Issue
Block a user