diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 55e8d553b36..4bf6a94b273 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -153,6 +153,7 @@ Experimental features might be changed or removed without prior notice. | `idForwarding` | Generate signed id token for identity that can be forwarded to plugins and external services | | `enableNativeHTTPHistogram` | Enables native HTTP Histograms | | `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint | +| `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards | | `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) | | `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query | | `queryServiceRewrite` | Rewrite requests targeting /ds/query to the query service | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 272d45a9abf..0d2f5c0d41f 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -117,6 +117,7 @@ export interface FeatureToggles { transformationsVariableSupport?: boolean; kubernetesPlaylists?: boolean; kubernetesSnapshots?: boolean; + kubernetesDashboards?: boolean; datasourceQueryTypes?: boolean; queryService?: boolean; queryServiceRewrite?: boolean; diff --git a/pkg/registry/apis/dashboard/access/sql_dashboards.go b/pkg/registry/apis/dashboard/access/sql_dashboards.go index 30462de1a62..ea16a6af7f4 100644 --- a/pkg/registry/apis/dashboard/access/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/access/sql_dashboards.go @@ -9,6 +9,7 @@ import ( "time" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/components/simplejson" @@ -177,8 +178,9 @@ func (a *dashboardSqlAccess) GetDashboards(ctx context.Context, query *Dashboard func (a *dashboardSqlAccess) GetDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, error) { r, err := a.GetDashboards(ctx, &DashboardQuery{ - OrgID: orgId, - UID: uid, + OrgID: orgId, + UID: uid, + Labels: labels.Everything(), }) if err != nil { return nil, err diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index cbe98b57e7e..de358003dc2 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -740,6 +740,13 @@ var ( Owner: grafanaAppPlatformSquad, RequiresRestart: true, // changes the API routing }, + { + Name: "kubernetesDashboards", + Description: "Use the kubernetes API in the frontend for dashboards", + Stage: FeatureStageExperimental, + Owner: grafanaAppPlatformSquad, + FrontendOnly: true, + }, { Name: "datasourceQueryTypes", Description: "Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 37a96386636..9413100c0a6 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -98,6 +98,7 @@ formatString,preview,@grafana/dataviz-squad,false,false,true transformationsVariableSupport,preview,@grafana/dataviz-squad,false,false,true kubernetesPlaylists,GA,@grafana/grafana-app-platform-squad,false,true,false kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false +kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,true datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false queryService,experimental,@grafana/grafana-app-platform-squad,false,true,false queryServiceRewrite,experimental,@grafana/grafana-app-platform-squad,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 487fb6dc8b2..9b6626a175f 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -403,6 +403,10 @@ const ( // Routes snapshot requests from /api to the /apis endpoint FlagKubernetesSnapshots = "kubernetesSnapshots" + // FlagKubernetesDashboards + // Use the kubernetes API in the frontend for dashboards + FlagKubernetesDashboards = "kubernetesDashboards" + // FlagDatasourceQueryTypes // Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) FlagDatasourceQueryTypes = "datasourceQueryTypes" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 7e47611e0ba..f04d77a6de7 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2224,6 +2224,19 @@ "stage": "GA", "codeowner": "@grafana/plugins-platform-backend" } + }, + { + "metadata": { + "name": "kubernetesDashboards", + "resourceVersion": "1717593661635", + "creationTimestamp": "2024-06-05T13:21:01Z" + }, + "spec": { + "description": "Use the kubernetes API in the frontend for dashboards", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad", + "frontend": true + } } ] } \ No newline at end of file diff --git a/public/app/core/components/Select/DashboardPicker.tsx b/public/app/core/components/Select/DashboardPicker.tsx index 41e783fe337..7f89920b43a 100644 --- a/public/app/core/components/Select/DashboardPicker.tsx +++ b/public/app/core/components/Select/DashboardPicker.tsx @@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { AsyncSelectProps, AsyncSelect } from '@grafana/ui'; import { backendSrv } from 'app/core/services/backend_srv'; +import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { DashboardSearchItem } from 'app/features/search/types'; import { DashboardDTO } from 'app/types'; @@ -54,7 +55,7 @@ export const DashboardPicker = ({ (async () => { // value was manually changed from outside or we are rendering for the first time. // We need to fetch dashboard information. - const res = await backendSrv.getDashboardByUid(value); + const res = await getDashboardAPI().getDashboardDTO(value); if (res.dashboard) { setCurrent({ value: { diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.test.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.test.tsx index 949fe893309..8bf26101828 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.test.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.test.tsx @@ -7,21 +7,26 @@ import { Preferences as UserPreferencesDTO } from '@grafana/schema/src/raw/prefe import SharedPreferences from './SharedPreferences'; +jest.mock('app/features/dashboard/api/dashboard_api', () => ({ + getDashboardAPI: () => ({ + getDashboardDTO: jest.fn().mockResolvedValue({ + dashboard: { + id: 2, + title: 'My Dashboard', + uid: 'myDash', + templating: { + list: [], + }, + panels: [], + }, + meta: {}, + }), + }), +})); + jest.mock('app/core/services/backend_srv', () => { return { backendSrv: { - getDashboardByUid: jest.fn().mockResolvedValue({ - dashboard: { - id: 2, - title: 'My Dashboard', - uid: 'myDash', - templating: { - list: [], - }, - panels: [], - }, - meta: {}, - }), search: jest.fn().mockResolvedValue([ { id: 2, diff --git a/public/app/core/services/__mocks__/backend_srv.ts b/public/app/core/services/__mocks__/backend_srv.ts index 9659824f263..21aa8c138e1 100644 --- a/public/app/core/services/__mocks__/backend_srv.ts +++ b/public/app/core/services/__mocks__/backend_srv.ts @@ -28,13 +28,15 @@ function makePromResponse() { export const backendSrv = { get: jest.fn(), - getDashboardByUid: jest.fn(), getFolderByUid: jest.fn(), post: jest.fn(), resolveCancelerIfExists: jest.fn(), search: jest.fn(), datasourceRequest: jest.fn(() => Promise.resolve(makePromResponse())), + /** @deprecated Use getDashboardAPI().getDashboardDTO(uid) */ + getDashboardByUid: jest.fn(), + // Observable support fetch: (options: BackendSrvRequest) => { return of(makePromResponse() as FetchResponse); diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 5fa66df994e..3ceffdec347 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -15,12 +15,13 @@ import { } from 'rxjs/operators'; import { v4 as uuidv4 } from 'uuid'; -import { AppEvents, DataQueryErrorType } from '@grafana/data'; +import { AppEvents, DataQueryErrorType, deprecationWarning } from '@grafana/data'; import { BackendSrv as BackendService, BackendSrvRequest, config, FetchError, FetchResponse } from '@grafana/runtime'; import appEvents from 'app/core/app_events'; import { getConfig } from 'app/core/config'; import { getSessionExpiry, hasSessionExpiry } from 'app/core/utils/auth'; import { loadUrlToken } from 'app/core/utils/urlToken'; +import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { DashboardModel } from 'app/features/dashboard/state'; import { DashboardSearchItem } from 'app/features/search/types'; import { TokenRevokedModal } from 'app/features/users/TokenRevokedModal'; @@ -512,8 +513,12 @@ export class BackendSrv implements BackendService { return this.get('/api/search', query); } + /** @deprecated */ getDashboardByUid(uid: string): Promise { - return this.get(`/api/dashboards/uid/${uid}`); + // NOTE: When this is removed, we can also remove most instances of: + // jest.mock('app/features/live/dashboard/dashboardWatcher + deprecationWarning('backend_srv', 'getDashboardByUid(uid)', 'getDashboardAPI().getDashboardDTO(uid)'); + return getDashboardAPI().getDashboardDTO(uid); } validateDashboard(dashboard: DashboardModel): Promise { @@ -522,7 +527,7 @@ export class BackendSrv implements BackendService { // config.featureToggles.showDashboardValidationWarnings return Promise.resolve({ isValid: false, - message: 'dashboard validation is supported', + message: 'dashboard validation is not supported', }); } diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index c749b642d0f..82bac2e5764 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -62,6 +62,11 @@ jest.mock('@grafana/runtime', () => ({ getDataSourceSrv: () => getDataSourceSrvMock(), })); +// Avoids errors caused by circular dependencies +jest.mock('app/features/live/dashboard/dashboardWatcher', () => ({ + ignoreNextSave: jest.fn(), +})); + describe('state functions', () => { describe('serializeStateToUrlParam', () => { it('returns url parameter value for a state object', () => { diff --git a/public/app/features/apiserver/client.ts b/public/app/features/apiserver/client.ts index d033a093df7..a98ed0e2431 100644 --- a/public/app/features/apiserver/client.ts +++ b/public/app/features/apiserver/client.ts @@ -37,6 +37,10 @@ export class ScopedResourceClient implements ResourceCli return getBackendSrv().get>(`${this.url}/${name}`); } + public async subresource(name: string, path: string): Promise { + return getBackendSrv().get(`${this.url}/${name}/${path}`); + } + public async list(opts?: ListOptions | undefined): Promise> { const finalOpts = opts || {}; finalOpts.labelSelector = this.parseListOptionsSelector(finalOpts?.labelSelector); diff --git a/public/app/features/apiserver/types.ts b/public/app/features/apiserver/types.ts index 102e9a79ee6..467d23fd3c5 100644 --- a/public/app/features/apiserver/types.ts +++ b/public/app/features/apiserver/types.ts @@ -144,6 +144,7 @@ export interface MetaStatus { export interface ResourceClient { create(obj: ResourceForCreate): Promise; get(name: string): Promise>; + subresource(name: string, path: string): Promise; list(opts?: ListOptions): Promise>; update(obj: ResourceForCreate): Promise>; delete(name: string): Promise; diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index 264b1806785..422eab15d93 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -7,6 +7,7 @@ import { Dashboard } from '@grafana/schema'; import { notifyApp } from 'app/core/actions'; import { createSuccessNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/core'; +import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { @@ -244,7 +245,7 @@ export const browseDashboardsAPI = createApi({ // Move all the dashboards sequentially // TODO error handling here for (const dashboardUID of selectedDashboards) { - const fullDash: DashboardDTO = await getBackendSrv().get(`/api/dashboards/uid/${dashboardUID}`); + const fullDash: DashboardDTO = await getDashboardAPI().getDashboardDTO(dashboardUID); const options = { dashboard: fullDash.dashboard, diff --git a/public/app/features/dashboard/api/dashboard_api.ts b/public/app/features/dashboard/api/dashboard_api.ts new file mode 100644 index 00000000000..146349d8a10 --- /dev/null +++ b/public/app/features/dashboard/api/dashboard_api.ts @@ -0,0 +1,84 @@ +import { config, getBackendSrv } from '@grafana/runtime'; +import { ScopedResourceClient } from 'app/features/apiserver/client'; +import { ResourceClient } from 'app/features/apiserver/types'; +import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; +import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; +import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; +import { DashboardDTO, DashboardDataDTO } from 'app/types'; + +export interface DashboardAPI { + /** Get a dashboard with the access control metadata */ + getDashboardDTO(uid: string): Promise; + /** Save dashboard */ + saveDashboard(options: SaveDashboardCommand): Promise; + /** Delete a dashboard */ + deleteDashboard(uid: string, showSuccessAlert: boolean): Promise; +} + +// Implemented using /api/dashboards/* +class LegacyDashboardAPI implements DashboardAPI { + constructor() {} + + saveDashboard(options: SaveDashboardCommand): Promise { + dashboardWatcher.ignoreNextSave(); + + return getBackendSrv().post('/api/dashboards/db/', { + dashboard: options.dashboard, + message: options.message ?? '', + overwrite: options.overwrite ?? false, + folderUid: options.folderUid, + }); + } + + deleteDashboard(uid: string, showSuccessAlert: boolean): Promise { + return getBackendSrv().delete(`/api/dashboards/uid/${uid}`, { showSuccessAlert }); + } + + getDashboardDTO(uid: string): Promise { + return getBackendSrv().get(`/api/dashboards/uid/${uid}`); + } +} + +// Implemented using /apis/dashboards.grafana.app/* +class K8sDashboardAPI implements DashboardAPI { + private client: ResourceClient; + constructor(private legacy: DashboardAPI) { + this.client = new ScopedResourceClient({ + group: 'dashboard.grafana.app', + version: 'v0alpha1', + resource: 'dashboards', + }); + } + + saveDashboard(options: SaveDashboardCommand): Promise { + return this.legacy.saveDashboard(options); + } + + deleteDashboard(uid: string, showSuccessAlert: boolean): Promise { + return this.legacy.deleteDashboard(uid, showSuccessAlert); + } + + async getDashboardDTO(uid: string): Promise { + const d = await this.client.get(uid); + const m = await this.client.subresource(uid, 'access'); + return { + meta: { + ...m, + isNew: false, + isFolder: false, + uid: d.metadata.name, + }, + dashboard: d.spec, + }; + } +} + +let instance: DashboardAPI | undefined = undefined; + +export function getDashboardAPI() { + if (!instance) { + const legacy = new LegacyDashboardAPI(); + instance = config.featureToggles.kubernetesDashboards ? new K8sDashboardAPI(legacy) : legacy; + } + return instance; +} diff --git a/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx b/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx index 28df23c5a1c..a89cd34d749 100644 --- a/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx +++ b/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx @@ -78,6 +78,12 @@ jest.mock('react-virtualized-auto-sizer', () => { }); }); +jest.mock('app/features/dashboard/api/dashboard_api', () => ({ + getDashboardAPI: () => ({ + getDashboardDTO: jest.fn().mockResolvedValue(dashMock), + }), +})); + function setup(props: Partial) { const context = getGrafanaContextMock(); const store = configureStore({}); diff --git a/public/app/features/dashboard/services/DashboardLoaderSrv.ts b/public/app/features/dashboard/services/DashboardLoaderSrv.ts index 2d35c27c57d..22c5b789d75 100644 --- a/public/app/features/dashboard/services/DashboardLoaderSrv.ts +++ b/public/app/features/dashboard/services/DashboardLoaderSrv.ts @@ -12,6 +12,7 @@ import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { DashboardDTO } from 'app/types'; import { appEvents } from '../../../core/core'; +import { getDashboardAPI } from '../api/dashboard_api'; import { getDashboardSrv } from './DashboardSrv'; import { getDashboardSnapshotSrv } from './SnapshotSrv'; @@ -81,8 +82,8 @@ export class DashboardLoaderSrv { return Promise.resolve(cachedDashboard); } - promise = backendSrv - .getDashboardByUid(uid) + promise = getDashboardAPI() + .getDashboardDTO(uid) .then((result) => { if (result.meta.isFolder) { appEvents.emit(AppEvents.alertError, ['Dashboard not found']); diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index d79ebf2253c..8482ea555ff 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -6,7 +6,7 @@ import { Dashboard } from '@grafana/schema'; import { appEvents } from 'app/core/app_events'; import { t } from 'app/core/internationalization'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { saveDashboard } from 'app/features/manage-dashboards/state/actions'; +import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { DashboardMeta } from 'app/types'; import { RemovePanelEvent } from '../../../types/events'; @@ -65,7 +65,7 @@ export class DashboardSrv { saveJSONDashboard(json: string) { const parsedJson = JSON.parse(json); - return saveDashboard({ + return getDashboardAPI().saveDashboard({ dashboard: parsedJson, folderUid: this.dashboard?.meta.folderUid || parsedJson.folderUid, }); diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index 184d9251d45..3f9b8d529f8 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -2,6 +2,7 @@ import { TimeZone } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; import { notifyApp } from 'app/core/actions'; import { createSuccessNotification } from 'app/core/copy/appNotification'; +import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { removeAllPanels } from 'app/features/panel/state/reducers'; import { updateTimeZoneForSession, updateWeekStartForSession } from 'app/features/profile/state/reducers'; @@ -24,7 +25,7 @@ export function importDashboard(data: any, dashboardTitle: string): ThunkResult< export function removeDashboard(uid: string): ThunkResult { return async (dispatch) => { - await getBackendSrv().delete(`/api/dashboards/uid/${uid}`); + await getDashboardAPI().deleteDashboard(uid, false); dispatch(loadPluginDashboards()); }; } diff --git a/public/app/features/explore/Logs/LiveLogs.test.tsx b/public/app/features/explore/Logs/LiveLogs.test.tsx index d1c097251b0..9606d6c0858 100644 --- a/public/app/features/explore/Logs/LiveLogs.test.tsx +++ b/public/app/features/explore/Logs/LiveLogs.test.tsx @@ -7,6 +7,11 @@ import { makeLogs } from '../__mocks__/makeLogs'; import { LiveLogsWithTheme } from './LiveLogs'; +// Avoids errors caused by circular dependencies +jest.mock('app/features/live/dashboard/dashboardWatcher', () => ({ + ignoreNextSave: jest.fn(), +})); + const setup = (rows: LogRowModel[]) => render( ({ + getDashboardAPI: () => ({ + getDashboardDTO: () => { + return Promise.resolve(mockDashboard); + }, + }), +})); + describe('addPanelToDashboard', () => { let spy: jest.SpyInstance; beforeAll(() => { @@ -71,7 +79,9 @@ describe('addPanelToDashboard', () => { it('Previous panels should not be removed', async () => { const queries: DataQuery[] = [{ refId: 'A' }]; const existingPanel = { prop: 'this should be kept' }; - jest.spyOn(backendSrv, 'getDashboardByUid').mockResolvedValue({ + + // Set the mocked dashboard + mockDashboard = { dashboard: { ...defaultDashboard, templating: { list: [] }, @@ -80,7 +90,7 @@ describe('addPanelToDashboard', () => { panels: [existingPanel], }, meta: {}, - }); + }; await setDashboardInLocalStorage({ queries, diff --git a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts index 7dad742e5ef..51c927801c7 100644 --- a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts +++ b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts @@ -1,7 +1,7 @@ import { DataFrame, ExplorePanelsState } from '@grafana/data'; import { Dashboard, DataQuery, DataSourceRef } from '@grafana/schema'; import { DataTransformerConfig } from '@grafana/schema/dist/esm/raw/dashboard/x/dashboard_types.gen'; -import { backendSrv } from 'app/core/services/backend_srv'; +import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { setDashboardToFetchFromLocalStorage } from 'app/features/dashboard/state/initDashboard'; import { buildNewDashboardSaveModel } from 'app/features/dashboard-scene/serialization/buildNewDashboardSaveModel'; import { DashboardDTO, ExplorePanelData } from 'app/types'; @@ -80,7 +80,7 @@ export async function setDashboardInLocalStorage(options: AddPanelToDashboardOpt if (options.dashboardUid) { try { - dto = await backendSrv.getDashboardByUid(options.dashboardUid); + dto = await getDashboardAPI().getDashboardDTO(options.dashboardUid); } catch (e) { throw AddToDashboardError.FETCH_DASHBOARD; } diff --git a/public/app/features/explore/hooks/useStateSync/migrators/v0.test.ts b/public/app/features/explore/hooks/useStateSync/migrators/v0.test.ts index c437c603715..bde44b7c2e3 100644 --- a/public/app/features/explore/hooks/useStateSync/migrators/v0.test.ts +++ b/public/app/features/explore/hooks/useStateSync/migrators/v0.test.ts @@ -2,6 +2,11 @@ import { DEFAULT_RANGE } from 'app/features/explore/state/utils'; import { v0Migrator } from './v0'; +// Avoids errors caused by circular dependencies +jest.mock('app/features/live/dashboard/dashboardWatcher', () => ({ + ignoreNextSave: jest.fn(), +})); + describe('v0 migrator', () => { describe('parse', () => { it('returns default state on empty string', () => { diff --git a/public/app/features/explore/hooks/useStateSync/migrators/v1.test.ts b/public/app/features/explore/hooks/useStateSync/migrators/v1.test.ts index 0f87f1bb4b5..f436ecc5f00 100644 --- a/public/app/features/explore/hooks/useStateSync/migrators/v1.test.ts +++ b/public/app/features/explore/hooks/useStateSync/migrators/v1.test.ts @@ -2,6 +2,11 @@ import { DEFAULT_RANGE } from 'app/features/explore/state/utils'; import { v1Migrator } from './v1'; +// Avoids errors caused by circular dependencies +jest.mock('app/features/live/dashboard/dashboardWatcher', () => ({ + ignoreNextSave: jest.fn(), +})); + jest.mock('app/core/utils/explore', () => ({ ...jest.requireActual('app/core/utils/explore'), generateExploreId: () => 'ID', diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index 40cdeb80270..a3c6df8cd87 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -3,8 +3,7 @@ import { getBackendSrv, getDataSourceSrv, isFetchError } from '@grafana/runtime' import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; import { browseDashboardsAPI, ImportInputs } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; -import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; -import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; +import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { FolderInfo, PermissionLevelString, SearchQueryType, ThunkResult } from 'app/types'; import { @@ -16,7 +15,7 @@ import { import { getLibraryPanel } from '../../library-panels/state/api'; import { LibraryElementDTO, LibraryElementKind } from '../../library-panels/types'; import { DashboardSearchHit } from '../../search/types'; -import { DashboardJson, DeleteDashboardResponse } from '../types'; +import { DashboardJson } from '../types'; import { clearDashboard, @@ -311,17 +310,6 @@ export function deleteFoldersAndDashboards(folderUids: string[], dashboardUids: return executeInOrder(tasks); } -export function saveDashboard(options: SaveDashboardCommand) { - dashboardWatcher.ignoreNextSave(); - - return getBackendSrv().post('/api/dashboards/db/', { - dashboard: options.dashboard, - message: options.message ?? '', - overwrite: options.overwrite ?? false, - folderUid: options.folderUid, - }); -} - function deleteFolder(uid: string, showSuccessAlert: boolean) { return getBackendSrv().delete(`/api/folders/${uid}?forceDeleteRules=false`, undefined, { showSuccessAlert }); } @@ -360,7 +348,7 @@ export function getFolderById(id: number): Promise<{ id: number; title: string } } export function deleteDashboard(uid: string, showSuccessAlert: boolean) { - return getBackendSrv().delete(`/api/dashboards/uid/${uid}`, { showSuccessAlert }); + return getDashboardAPI().deleteDashboard(uid, showSuccessAlert); } function executeInOrder(tasks: any[]): Promise { diff --git a/public/app/features/org/OrgDetailsPage.test.tsx b/public/app/features/org/OrgDetailsPage.test.tsx index ae104700620..a092fc63731 100644 --- a/public/app/features/org/OrgDetailsPage.test.tsx +++ b/public/app/features/org/OrgDetailsPage.test.tsx @@ -53,6 +53,12 @@ const setup = (propOverrides?: object) => { ); }; +jest.mock('app/features/dashboard/api/dashboard_api', () => ({ + getDashboardAPI: () => ({ + getDashboardDTO: jest.fn().mockResolvedValue({}), + }), +})); + describe('Render', () => { beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => {}); diff --git a/public/app/features/profile/UserProfileEditPage.test.tsx b/public/app/features/profile/UserProfileEditPage.test.tsx index e81b5bfc50e..4a96dc9a473 100644 --- a/public/app/features/profile/UserProfileEditPage.test.tsx +++ b/public/app/features/profile/UserProfileEditPage.test.tsx @@ -22,6 +22,12 @@ jest.mock('app/core/hooks/useQueryParams', () => ({ useQueryParams: () => [{}], })); +jest.mock('app/features/dashboard/api/dashboard_api', () => ({ + getDashboardAPI: () => ({ + getDashboardDTO: jest.fn().mockResolvedValue({}), + }), +})); + const defaultProps: Props = { ...initialUserState, user: {