diff --git a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.test.tsx b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.test.tsx
index c67d94a2c75..caa0da74f8c 100644
--- a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.test.tsx
+++ b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.test.tsx
@@ -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,
diff --git a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx
index 9b082028799..2cb4bf4837e 100644
--- a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx
+++ b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx
@@ -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)'
)}
>
-
+
diff --git a/public/app/features/dashboard-scene/saving/provisioned/utils/path.test.ts b/public/app/features/dashboard-scene/saving/provisioned/utils/path.test.ts
index fc25878d2fa..234f799d6da 100644
--- a/public/app/features/dashboard-scene/saving/provisioned/utils/path.test.ts
+++ b/public/app/features/dashboard-scene/saving/provisioned/utils/path.test.ts
@@ -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', () => {
diff --git a/public/app/features/dashboard-scene/saving/provisioned/utils/path.ts b/public/app/features/dashboard-scene/saving/provisioned/utils/path.ts
index 5464d13dbee..7a25d663065 100644
--- a/public/app/features/dashboard-scene/saving/provisioned/utils/path.ts
+++ b/public/app/features/dashboard-scene/saving/provisioned/utils/path.ts
@@ -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}`;
diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx
index c7a2504f1c0..b2e91341c82 100644
--- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx
+++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx
@@ -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 impleme
return this.serializer.getSaveModel(this);
}
+ // Get the dashboard in native K8s form (using the appropriate apiVersion)
+ getSaveResource(options: SaveDashboardAsOptions): ResourceForCreate {
+ 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);
}
diff --git a/public/app/features/provisioning/Shared/FolderRepositoryList.tsx b/public/app/features/provisioning/Shared/FolderRepositoryList.tsx
index 7af68d68f61..512d5ef53d1 100644
--- a/public/app/features/provisioning/Shared/FolderRepositoryList.tsx
+++ b/public/app/features/provisioning/Shared/FolderRepositoryList.tsx
@@ -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 (
- {!instanceProvisioned && (
+ {!instanceConnected && (