mirror of https://github.com/grafana/grafana.git
Provisioning: Retry loading dashboard if ref is missing (#111811)
* Provisioning: Retry loading dashboard if ref is missing * Fix tests * Remove any
This commit is contained in:
parent
73cf4b0895
commit
bbb9c4660c
|
@ -1,10 +1,15 @@
|
|||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { advanceBy } from 'jest-date-mock';
|
||||
import { UnknownAction } from 'redux';
|
||||
import { of } from 'rxjs';
|
||||
import { createFetchResponse } from 'test/helpers/createFetchResponse';
|
||||
|
||||
import { BackendSrv, config, locationService, setBackendSrv } from '@grafana/runtime';
|
||||
import {
|
||||
Spec as DashboardV2Spec,
|
||||
defaultSpec as defaultDashboardV2Spec,
|
||||
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
import { provisioningAPIv0alpha1 } from 'app/api/clients/provisioning/v0alpha1';
|
||||
import store from 'app/core/store';
|
||||
import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
|
||||
import { DashboardVersionError, DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
|
||||
|
@ -26,18 +31,26 @@ import {
|
|||
UnifiedDashboardScenePageStateManager,
|
||||
DASHBOARD_CACHE_TTL,
|
||||
} from './DashboardScenePageStateManager';
|
||||
const fetchMock = jest.fn();
|
||||
|
||||
// Mock the config module
|
||||
jest.mock('@grafana/runtime', () => {
|
||||
const original = jest.requireActual('@grafana/runtime');
|
||||
const originalGetBackendSrv = original.getBackendSrv;
|
||||
return {
|
||||
...original,
|
||||
getBackendSrv: () => {
|
||||
const originalSrv = originalGetBackendSrv();
|
||||
return {
|
||||
...originalSrv,
|
||||
fetch: fetchMock,
|
||||
};
|
||||
},
|
||||
config: {
|
||||
...original.config,
|
||||
featureToggles: {
|
||||
...original.config.featureToggles,
|
||||
dashboardNewLayouts: false, // Default value
|
||||
reloadDashboardsOnParamsChange: false, // Default value
|
||||
dashboardNewLayouts: false,
|
||||
reloadDashboardsOnParamsChange: false,
|
||||
},
|
||||
datasources: {
|
||||
'gdev-testdata': {
|
||||
|
@ -81,6 +94,29 @@ jest.mock('app/features/playlist/PlaylistSrv', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
const createTestStore = () =>
|
||||
configureStore({
|
||||
reducer: {
|
||||
[provisioningAPIv0alpha1.reducerPath]: provisioningAPIv0alpha1.reducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(provisioningAPIv0alpha1.middleware),
|
||||
});
|
||||
|
||||
let testStore: ReturnType<typeof createTestStore>;
|
||||
|
||||
jest.mock('app/store/store', () => {
|
||||
const actual = jest.requireActual('app/store/store');
|
||||
return {
|
||||
...actual,
|
||||
dispatch: jest.fn((action: UnknownAction) => {
|
||||
if (testStore) {
|
||||
return testStore.dispatch(action);
|
||||
}
|
||||
return action;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const setupDashboardAPI = (
|
||||
d: DashboardWithAccessInfo<DashboardV2Spec> | undefined,
|
||||
spy: jest.Mock,
|
||||
|
@ -161,10 +197,13 @@ beforeEach(() => {
|
|||
jest.clearAllMocks();
|
||||
mockDashboardLoader.loadDashboard.mockReset();
|
||||
mockDashboardLoader.loadSnapshot.mockReset();
|
||||
fetchMock.mockReset();
|
||||
|
||||
// Reset locationService mocks
|
||||
locationService.getSearch = jest.fn().mockReturnValue(new URLSearchParams());
|
||||
locationService.getSearchObject = jest.fn().mockReturnValue({});
|
||||
|
||||
testStore = createTestStore();
|
||||
});
|
||||
|
||||
describe('DashboardScenePageStateManager v1', () => {
|
||||
|
@ -1558,10 +1597,9 @@ describe('UnifiedDashboardScenePageStateManager', () => {
|
|||
|
||||
describe('Provisioned dashboard', () => {
|
||||
it('should load a provisioned v1 dashboard', async () => {
|
||||
fetchMock.mockImplementation(() => of(createFetchResponse(v1ProvisionedDashboardResource)));
|
||||
|
||||
const loader = new UnifiedDashboardScenePageStateManager({});
|
||||
setBackendSrv({
|
||||
get: () => Promise.resolve(v1ProvisionedDashboardResource),
|
||||
} as unknown as BackendSrv);
|
||||
await loader.loadDashboard({ uid: 'blah-blah', route: DashboardRoutes.Provisioning });
|
||||
|
||||
expect(loader.state.dashboard).toBeDefined();
|
||||
|
@ -1572,10 +1610,9 @@ describe('UnifiedDashboardScenePageStateManager', () => {
|
|||
});
|
||||
|
||||
it('should load a provisioned v2 dashboard', async () => {
|
||||
fetchMock.mockImplementation(() => of(createFetchResponse(v2ProvisionedDashboardResource)));
|
||||
|
||||
const loader = new UnifiedDashboardScenePageStateManager({});
|
||||
setBackendSrv({
|
||||
get: () => Promise.resolve(v2ProvisionedDashboardResource),
|
||||
} as unknown as BackendSrv);
|
||||
await loader.loadDashboard({ uid: 'blah-blah', route: DashboardRoutes.Provisioning });
|
||||
|
||||
expect(loader.state.dashboard).toBeDefined();
|
||||
|
|
|
@ -3,7 +3,7 @@ import { t } from '@grafana/i18n';
|
|||
import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
|
||||
import { sceneGraph } from '@grafana/scenes';
|
||||
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
import { BASE_URL } from 'app/api/clients/provisioning/v0alpha1/baseAPI';
|
||||
import { GetRepositoryFilesWithPathApiResponse, provisioningAPIv0alpha1 } from 'app/api/clients/provisioning/v0alpha1';
|
||||
import { StateManagerBase } from 'app/core/services/StateManagerBase';
|
||||
import { getMessageFromError, getMessageIdFromError, getStatusFromError } from 'app/core/utils/errors';
|
||||
import { startMeasure, stopMeasure } from 'app/core/utils/metrics';
|
||||
|
@ -23,6 +23,7 @@ import { emitDashboardViewEvent } from 'app/features/dashboard/state/analyticsPr
|
|||
import { trackDashboardSceneLoaded } from 'app/features/dashboard/utils/tracking';
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
import { ProvisioningPreview } from 'app/features/provisioning/types';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import {
|
||||
DashboardDataDTO,
|
||||
DashboardDTO,
|
||||
|
@ -178,26 +179,46 @@ abstract class DashboardScenePageStateManagerBase<T>
|
|||
const params = new URLSearchParams(window.location.search);
|
||||
const ref = params.get('ref') ?? undefined; // commit hash or branch
|
||||
|
||||
const url = `${BASE_URL}/repositories/${repo}/files/${path}`;
|
||||
return getBackendSrv()
|
||||
.get(url, ref ? { ref } : undefined)
|
||||
.then((v) => {
|
||||
// Load the results from dryRun
|
||||
const dryRun = v.resource.dryRun;
|
||||
if (!dryRun) {
|
||||
return Promise.reject('failed to read provisioned dashboard');
|
||||
}
|
||||
const loadWithRef = async (refParam: string | undefined) => {
|
||||
const result = await dispatch(
|
||||
provisioningAPIv0alpha1.endpoints.getRepositoryFilesWithPath.initiate({
|
||||
name: repo,
|
||||
path: path,
|
||||
ref: refParam,
|
||||
})
|
||||
);
|
||||
|
||||
if (!dryRun.apiVersion.startsWith('dashboard.grafana.app')) {
|
||||
return Promise.reject('unexpected resource type: ' + dryRun.apiVersion);
|
||||
}
|
||||
if (result && 'error' in result) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
return this.processDashboardFromProvisioning(repo, path, dryRun, {
|
||||
file: url,
|
||||
ref: ref,
|
||||
repo: repo,
|
||||
});
|
||||
const v: GetRepositoryFilesWithPathApiResponse = structuredClone(result.data);
|
||||
// Load the results from dryRun
|
||||
const dryRun = v.resource.dryRun;
|
||||
if (!dryRun) {
|
||||
return Promise.reject('failed to read provisioned dashboard');
|
||||
}
|
||||
|
||||
if (!dryRun.apiVersion.startsWith('dashboard.grafana.app')) {
|
||||
return Promise.reject('unexpected resource type: ' + dryRun.apiVersion);
|
||||
}
|
||||
|
||||
return this.processDashboardFromProvisioning(repo, path, dryRun, {
|
||||
file: v.path ?? '',
|
||||
ref: refParam,
|
||||
repo: repo,
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
return await loadWithRef(ref);
|
||||
} catch (err) {
|
||||
// If ref is not found (404), retry without ref to default to the main branch
|
||||
if (ref && isFetchError(err) && err.status === 404) {
|
||||
return await loadWithRef(undefined);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private processDashboardFromProvisioning(
|
||||
|
|
|
@ -13,8 +13,7 @@ export interface SaveProvisionedDashboardProps {
|
|||
}
|
||||
|
||||
export function SaveProvisionedDashboard({ drawer, changeInfo, dashboard }: SaveProvisionedDashboardProps) {
|
||||
const { isNew, defaultValues, loadedFromRef, workflowOptions, readOnly, repository } =
|
||||
useProvisionedDashboardData(dashboard);
|
||||
const { isNew, defaultValues, workflowOptions, readOnly, repository } = useProvisionedDashboardData(dashboard);
|
||||
|
||||
if (!defaultValues) {
|
||||
return null;
|
||||
|
@ -27,7 +26,6 @@ export function SaveProvisionedDashboard({ drawer, changeInfo, dashboard }: Save
|
|||
changeInfo={changeInfo}
|
||||
isNew={isNew}
|
||||
defaultValues={defaultValues}
|
||||
loadedFromRef={loadedFromRef}
|
||||
repository={repository}
|
||||
workflowOptions={workflowOptions}
|
||||
readOnly={readOnly}
|
||||
|
|
|
@ -33,7 +33,6 @@ import { SaveProvisionedDashboardProps } from './SaveProvisionedDashboard';
|
|||
export interface Props extends SaveProvisionedDashboardProps {
|
||||
isNew: boolean;
|
||||
defaultValues: ProvisionedDashboardFormData;
|
||||
loadedFromRef?: string;
|
||||
workflowOptions: Array<{ label: string; value: string }>;
|
||||
readOnly: boolean;
|
||||
repository?: RepositoryView;
|
||||
|
@ -45,7 +44,6 @@ export function SaveProvisionedDashboardForm({
|
|||
drawer,
|
||||
changeInfo,
|
||||
isNew,
|
||||
loadedFromRef,
|
||||
workflowOptions,
|
||||
readOnly,
|
||||
repository,
|
||||
|
|
Loading…
Reference in New Issue