From e3da5ed35dbe3945b9bc1f094872b2809f7a95cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Jun 2024 10:23:51 +0200 Subject: [PATCH 01/11] Scenes: Upgrade to latest URL sync system (#88836) * Urlsync updates * Update * Fixing tests * Update to latest canary * fix * Update * Update * Update * Fix data trails issue * Data trails fixes * Update * correctly sync scene object graph with url state * Update --- package.json | 6 +- .../pages/DashboardScenePage.tsx | 5 +- .../DashboardScenePageStateManager.test.ts | 37 +- .../pages/DashboardScenePageStateManager.ts | 4 - .../pages/PublicDashboardScenePage.tsx | 13 +- .../saving/useSaveDashboard.ts | 3 - .../dashboard-scene/scene/DashboardScene.tsx | 19 +- .../scene/NavToolbarActions.test.tsx | 14 +- .../utils/dashboardSessionState.test.ts | 11 +- .../utils/dashboardSessionState.ts | 8 +- public/app/features/trails/DataTrail.test.tsx | 20 +- public/app/features/trails/DataTrail.tsx | 62 +- public/app/features/trails/DataTrailsApp.tsx | 75 +-- .../trails/TrailStore/TrailStore.test.ts | 2 + .../features/trails/TrailStore/TrailStore.ts | 22 +- yarn.lock | 608 ++++++++---------- 16 files changed, 338 insertions(+), 571 deletions(-) diff --git a/package.json b/package.json index bb49dde03d4..78218f9eced 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", + "@types/add": "^2", "@types/angular": "1.8.9", "@types/angular-route": "1.7.6", "@types/babel__core": "^7", @@ -258,7 +259,7 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "4.29.0", + "@grafana/scenes": "^5.0.2", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", @@ -398,7 +399,8 @@ "uuid": "9.0.1", "visjs-network": "4.25.0", "whatwg-fetch": "3.6.20", - "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz", + "yarn": "^1.22.22" }, "resolutions": { "underscore": "1.13.6", diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx index 84ca43a33e3..a1b4f0f46f3 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo } from 'react'; import { PageLayoutType } from '@grafana/data'; +import { UrlSyncContextProvider } from '@grafana/scenes'; import { Page } from 'app/core/components/Page/Page'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; @@ -78,10 +79,10 @@ export function DashboardScenePage({ match, route, queryParams, history }: Props } return ( - <> + - + ); } diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index 2d67ff875e9..8f87474a151 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -1,7 +1,6 @@ import { advanceBy } from 'jest-date-mock'; -import { BackendSrv, locationService, setBackendSrv } from '@grafana/runtime'; -import { getUrlSyncManager } from '@grafana/scenes'; +import { BackendSrv, setBackendSrv } from '@grafana/runtime'; import store from 'app/core/store'; import { DASHBOARD_FROM_LS_KEY } from 'app/features/dashboard/state/initDashboard'; import { DashboardRoutes } from 'app/types'; @@ -95,40 +94,6 @@ describe('DashboardScenePageStateManager', () => { expect(loader.state.isLoading).toBe(false); }); - it('should initialize url sync', async () => { - setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} }); - - locationService.partial({ from: 'now-5m', to: 'now' }); - - const loader = new DashboardScenePageStateManager({}); - await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); - const dash = loader.state.dashboard; - - expect(dash!.state.$timeRange?.state.from).toEqual('now-5m'); - - getUrlSyncManager().cleanUp(dash!); - - // try loading again (and hitting cache) - locationService.partial({ from: 'now-10m', to: 'now' }); - - await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); - const dash2 = loader.state.dashboard; - - expect(dash2!.state.$timeRange?.state.from).toEqual('now-10m'); - }); - - it('should not initialize url sync for embedded dashboards', async () => { - setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} }); - - locationService.partial({ from: 'now-5m', to: 'now' }); - - const loader = new DashboardScenePageStateManager({}); - await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Embedded }); - const dash = loader.state.dashboard; - - expect(dash!.state.$timeRange?.state.from).toEqual('now-6h'); - }); - describe('New dashboards', () => { it('Should have new empty model with meta.isNew and should not be cached', async () => { const loader = new DashboardScenePageStateManager({}); diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index a39c5ff1c1c..f0235de5a61 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -182,10 +182,6 @@ export class DashboardScenePageStateManager extends StateManagerBase; } - return ; + // if no time picker render without url sync + if (dashboard.state.controls?.state.hideTimeControls) { + return ; + } + + return ( + + + + ); } function PublicDashboardSceneRenderer({ model }: SceneComponentProps) { diff --git a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts index c961e4a44db..ebd71b6ca63 100644 --- a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts +++ b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts @@ -61,10 +61,7 @@ export function useSaveDashboard(isCopy = false) { if (newUrl !== currentLocation.pathname) { setTimeout(() => { - // Because the path changes we need to stop and restart url sync - scene.stopUrlSync(); locationService.push({ pathname: newUrl, search: currentLocation.search }); - scene.startUrlSync(); }); } diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index c050e66ec37..13e6ec7dff7 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -12,7 +12,6 @@ import { } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { - getUrlSyncManager, SceneFlexLayout, sceneGraph, SceneGridLayout, @@ -212,27 +211,15 @@ export class DashboardScene extends SceneObjectBase { window.__grafanaSceneContext = prevSceneContext; clearKeyBindings(); this._changeTracker.terminate(); - this.stopUrlSync(); oldDashboardWrapper.destroy(); dashboardWatcher.leave(); }; } - public startUrlSync() { - if (!this.state.meta.isEmbedded) { - getUrlSyncManager().initSync(this); - } - } - - public stopUrlSync() { - getUrlSyncManager().cleanUp(this); - } - public onEnterEditMode = (fromExplore = false) => { this._fromExplore = fromExplore; // Save this state this._initialState = sceneUtils.cloneSceneObjectState(this.state); - this._initialUrlState = locationService.getLocation(); // Switch to edit mode @@ -303,10 +290,7 @@ export class DashboardScene extends SceneObjectBase { private exitEditModeConfirmed(restoreInitialState = true) { // No need to listen to changes anymore this._changeTracker.stopTrackingChanges(); - // Stop url sync before updating url - this.stopUrlSync(); - // Now we can update urls // We are updating url and removing editview and editPanel. // The initial url may be including edit view, edit panel or inspect query params if the user pasted the url, // hence we need to cleanup those query params to get back to the dashboard view. Otherwise url sync can trigger overlays. @@ -330,8 +314,7 @@ export class DashboardScene extends SceneObjectBase { // Do not restore this.setState({ isEditing: false }); } - // and start url sync again - this.startUrlSync(); + // Disable grid dragging this.propagateEditModeChange(); } diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx index d081dbae492..797a80fe51d 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx @@ -5,8 +5,8 @@ import { TestProvider } from 'test/helpers/TestProvider'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { selectors } from '@grafana/e2e-selectors'; -import { config } from '@grafana/runtime'; -import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, VizPanel } from '@grafana/scenes'; +import { config, locationService } from '@grafana/runtime'; +import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, UrlSyncContextProvider, VizPanel } from '@grafana/scenes'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; @@ -103,9 +103,8 @@ describe('NavToolbarActions', () => { }); it('Should show correct buttons when in settings menu', async () => { - const { dashboard } = setup(); + setup(); - dashboard.startUrlSync(); await userEvent.click(await screen.findByText('Edit')); await userEvent.click(await screen.findByText('Settings')); @@ -118,6 +117,7 @@ describe('NavToolbarActions', () => { it('Should show correct buttons when editing a new panel', async () => { const { dashboard } = setup(); + await act(() => { dashboard.onEnterEditMode(); const editingPanel = ((dashboard.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state @@ -205,9 +205,13 @@ function setup() { const context = getGrafanaContextMock(); + locationService.push('/'); + render( - + + + ); diff --git a/public/app/features/dashboard-scene/utils/dashboardSessionState.test.ts b/public/app/features/dashboard-scene/utils/dashboardSessionState.test.ts index f7052f2203a..2d1ec2cccbe 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSessionState.test.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSessionState.test.ts @@ -1,5 +1,5 @@ import { config, locationService } from '@grafana/runtime'; -import { CustomVariable } from '@grafana/scenes'; +import { CustomVariable, getUrlSyncManager } from '@grafana/scenes'; import { DashboardDataDTO } from 'app/types'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; @@ -50,7 +50,8 @@ describe('dashboardSessionState', () => { restoreDashboardStateFromLocalStorage(scene); const variable = scene.state.$variables!.getByName('customVar') as CustomVariable; const timeRange = scene.state.$timeRange; - scene.startUrlSync(); + + getUrlSyncManager().initSync(scene); expect(variable!.state!.value).toEqual(['b']); expect(variable!.state!.text).toEqual(['b']); @@ -63,11 +64,12 @@ describe('dashboardSessionState', () => { PRESERVED_SCENE_STATE_KEY, '?var-customVar=b&var-nonApplicableVar=b&from=now-5m&to=now&timezone=browser' ); + const scene = buildTestScene(); restoreDashboardStateFromLocalStorage(scene); - expect(locationService.getSearch().toString()).toBe('var-customVar=b&from=now-5m&to=now&timezone=browser'); + expect(locationService.getLocation().search).toBe('?var-customVar=b&from=now-5m&to=now&timezone=browser'); }); // handles case when user navigates back to a dashboard with the same state, i.e. using back button @@ -79,7 +81,7 @@ describe('dashboardSessionState', () => { restoreDashboardStateFromLocalStorage(scene); - expect(locationService.getSearch().toString()).toBe('var-customVar=b&from=now-6h&to=now&timezone=browser'); + expect(locationService.getLocation().search).toBe('?var-customVar=b&from=now-6h&to=now&timezone=browser'); }); }); }); @@ -116,6 +118,7 @@ function buildTestScene() { version: 24, weekStart: '', }; + const scene = transformSaveModelToScene({ dashboard: testDashboard, meta: {} }); // Removing data layers to avoid mocking built-in Grafana data source diff --git a/public/app/features/dashboard-scene/utils/dashboardSessionState.ts b/public/app/features/dashboard-scene/utils/dashboardSessionState.ts index c93e530a530..6ad764e2222 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSessionState.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSessionState.ts @@ -17,10 +17,6 @@ export function restoreDashboardStateFromLocalStorage(dashboard: DashboardScene) preservedQueryParams.forEach((value, key) => { if (!currentQueryParams.has(key)) { currentQueryParams.append(key, value); - } else { - if (!currentQueryParams.getAll(key).includes(value)) { - currentQueryParams.append(key, value); - } } }); @@ -38,9 +34,7 @@ export function restoreDashboardStateFromLocalStorage(dashboard: DashboardScene) const finalParams = currentQueryParams.toString(); if (finalParams) { - locationService.replace({ - search: finalParams, - }); + locationService.replace({ search: finalParams }); } } } diff --git a/public/app/features/trails/DataTrail.test.tsx b/public/app/features/trails/DataTrail.test.tsx index 04dbcf2a047..833058cc5b5 100644 --- a/public/app/features/trails/DataTrail.test.tsx +++ b/public/app/features/trails/DataTrail.test.tsx @@ -71,7 +71,7 @@ describe('DataTrail', () => { }); it('should sync state with url', () => { - expect(locationService.getSearchObject().metric).toBe('metric_bucket'); + expect(trail.getUrlState().metric).toBe('metric_bucket'); }); it('should add history step', () => { @@ -104,10 +104,6 @@ describe('DataTrail', () => { trail.state.$timeRange?.setState({ from: 'now-1h' }); }); - it('should sync state with url', () => { - expect(locationService.getSearchObject().from).toBe('now-1h'); - }); - it('should add history step', () => { expect(trail.state.history.state.steps[2].type).toBe('time'); }); @@ -154,10 +150,6 @@ describe('DataTrail', () => { trail.state.$timeRange?.setState({ from: 'now-15m' }); }); - it('should sync state with url', () => { - expect(locationService.getSearchObject().from).toBe('now-15m'); - }); - it('should add history step', () => { expect(trail.state.history.state.steps[3].type).toBe('time'); }); @@ -224,10 +216,6 @@ describe('DataTrail', () => { getFilterVar().setState({ filters: [{ key: 'zone', operator: '=', value: 'a' }] }); }); - it('should sync state with url', () => { - expect(decodeURIComponent(locationService.getSearchObject()['var-filters']?.toString()!)).toBe('zone|=|a'); - }); - it('should add history step', () => { expect(trail.state.history.state.steps[2].type).toBe('filters'); }); @@ -276,12 +264,6 @@ describe('DataTrail', () => { getFilterVar().setState({ filters: [{ key: 'zone', operator: '=', value: 'b' }] }); }); - it('should sync state with url', () => { - expect(decodeURIComponent(locationService.getSearchObject()['var-filters']?.toString()!)).toBe( - 'zone|=|b' - ); - }); - it('should add history step', () => { expect(trail.state.history.state.steps[3].type).toBe('filters'); }); diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index fda4703db5a..3295af66bc8 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import React from 'react'; -import { AdHocVariableFilter, GrafanaTheme2, VariableHide, urlUtil } from '@grafana/data'; +import { AdHocVariableFilter, GrafanaTheme2, PageLayoutType, VariableHide, urlUtil } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { AdHocFiltersVariable, @@ -25,6 +25,7 @@ import { VariableValueSelectors, } from '@grafana/scenes'; import { useStyles2 } from '@grafana/ui'; +import { Page } from 'app/core/components/Page/Page'; import { DataTrailSettings } from './DataTrailSettings'; import { DataTrailHistory } from './DataTrailsHistory'; @@ -35,6 +36,7 @@ import { getTrailStore } from './TrailStore/TrailStore'; import { MetricDatasourceHelper } from './helpers/MetricDatasourceHelper'; import { reportChangeInLabelFilters } from './interactions'; import { MetricSelectedEvent, trailDS, VAR_DATASOURCE, VAR_FILTERS } from './shared'; +import { getMetricName } from './utils'; export interface DataTrailState extends SceneObjectState { topScene?: SceneObject; @@ -93,21 +95,11 @@ export class DataTrail extends SceneObjectBase { ); } - // Disconnects the current step history state from the current state, to prevent changes affecting history state - const currentState = this.state.history.state.steps[this.state.history.state.currentStep]?.trailState; - if (currentState) { - this.restoreFromHistoryStep(currentState); - } - - this.enableUrlSync(); - // Save the current trail as a recent if the browser closes or reloads const saveRecentTrail = () => getTrailStore().setRecentTrail(this); window.addEventListener('unload', saveRecentTrail); return () => { - this.disableUrlSync(); - if (!this.state.embedded) { saveRecentTrail(); } @@ -115,18 +107,6 @@ export class DataTrail extends SceneObjectBase { }; } - private enableUrlSync() { - if (!this.state.embedded) { - getUrlSyncManager().initSync(this); - } - } - - private disableUrlSync() { - if (!this.state.embedded) { - getUrlSyncManager().cleanUp(this); - } - } - protected _variableDependency = new VariableDependencyConfig(this, { variableNames: [VAR_DATASOURCE], onReferencedVariableValueChanged: (variable: SceneVariable) => { @@ -167,8 +147,6 @@ export class DataTrail extends SceneObjectBase { } public restoreFromHistoryStep(state: DataTrailState) { - this.disableUrlSync(); - if (!state.topScene && !state.metric) { // If the top scene for an is missing, correct it. state.topScene = new MetricSelectScene({}); @@ -184,8 +162,6 @@ export class DataTrail extends SceneObjectBase { const urlState = getUrlSyncManager().getUrlState(this); const fullUrl = urlUtil.renderUrl(locationService.getLocation().pathname, urlState); locationService.replace(fullUrl); - - this.enableUrlSync(); } private _handleMetricSelectedEvent(evt: MetricSelectedEvent) { @@ -227,24 +203,26 @@ export class DataTrail extends SceneObjectBase { } static Component = ({ model }: SceneComponentProps) => { - const { controls, topScene, history, settings } = model.useState(); + const { controls, topScene, history, settings, metric } = model.useState(); const styles = useStyles2(getStyles); const showHeaderForFirstTimeUsers = getTrailStore().recent.length < 2; return ( -
- {showHeaderForFirstTimeUsers && } - - {controls && ( -
- {controls.map((control) => ( - - ))} - -
- )} -
{topScene && }
-
+ +
+ {showHeaderForFirstTimeUsers && } + + {controls && ( +
+ {controls.map((control) => ( + + ))} + +
+ )} +
{topScene && }
+
+
); }; } @@ -288,6 +266,8 @@ function getStyles(theme: GrafanaTheme2) { gap: theme.spacing(1), minHeight: '100%', flexDirection: 'column', + background: theme.isLight ? theme.colors.background.primary : theme.colors.background.canvas, + padding: theme.spacing(2, 3, 2, 3), }), body: css({ flexGrow: 1, diff --git a/public/app/features/trails/DataTrailsApp.tsx b/public/app/features/trails/DataTrailsApp.tsx index 9f2511306be..f23361f1f51 100644 --- a/public/app/features/trails/DataTrailsApp.tsx +++ b/public/app/features/trails/DataTrailsApp.tsx @@ -1,11 +1,9 @@ -import { css } from '@emotion/css'; import React, { useEffect } from 'react'; import { Route, Switch } from 'react-router-dom'; -import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; +import { PageLayoutType } from '@grafana/data'; import { locationService } from '@grafana/runtime'; -import { SceneComponentProps, SceneObjectBase, SceneObjectState, getUrlSyncManager } from '@grafana/scenes'; -import { useStyles2 } from '@grafana/ui'; +import { SceneComponentProps, SceneObjectBase, SceneObjectState, UrlSyncContextProvider } from '@grafana/scenes'; import { Page } from 'app/core/components/Page/Page'; import { DataTrail } from './DataTrail'; @@ -13,7 +11,7 @@ import { DataTrailsHome } from './DataTrailsHome'; import { MetricsHeader } from './MetricsHeader'; import { getTrailStore } from './TrailStore/TrailStore'; import { HOME_ROUTE, TRAILS_ROUTE } from './shared'; -import { getMetricName, getUrlForTrail, newMetricsTrail } from './utils'; +import { getUrlForTrail, newMetricsTrail } from './utils'; export interface DataTrailsAppState extends SceneObjectState { trail: DataTrail; @@ -26,13 +24,12 @@ export class DataTrailsApp extends SceneObjectBase { } goToUrlForTrail(trail: DataTrail) { - this.setState({ trail }); locationService.push(getUrlForTrail(trail)); + this.setState({ trail }); } static Component = ({ model }: SceneComponentProps) => { const { trail, home } = model.useState(); - const styles = useStyles2(getStyles); return ( @@ -50,21 +47,7 @@ export class DataTrailsApp extends SceneObjectBase { )} /> - ( - -
- -
-
- )} - /> + } />
); }; @@ -84,7 +67,11 @@ function DataTrailView({ trail }: { trail: DataTrail }) { return null; } - return ; + return ( + + + + ); } let dataTrailsApp: DataTrailsApp; @@ -92,50 +79,10 @@ let dataTrailsApp: DataTrailsApp; export function getDataTrailsApp() { if (!dataTrailsApp) { dataTrailsApp = new DataTrailsApp({ - trail: getInitialTrail(), + trail: newMetricsTrail(), home: new DataTrailsHome({}), }); } return dataTrailsApp; } - -/** - * Get the initial trail for the app to work with based on the current URL - * - * It will either be a new trail that will be started based on the state represented - * in the URL parameters, or it will be the most recently used trail (according to the trail store) - * which has its current history step matching the URL parameters. - * - * The reason for trying to reinitialize from the recent trail is to resolve an issue - * where refreshing the browser would wipe the step history. This allows you to preserve - * it between browser refreshes, or when reaccessing the same URL. - */ -function getInitialTrail() { - const newTrail = newMetricsTrail(); - - // Set the initial state of the newTrail based on the URL, - // In case we are initializing from an externally created URL or a page reload - getUrlSyncManager().initSync(newTrail); - // Remove the URL sync for now. It will be restored on the trail if it is activated. - getUrlSyncManager().cleanUp(newTrail); - - // If one of the recent trails is a match to the newTrail derived from the current URL, - // let's restore that trail so that a page refresh doesn't create a new trail. - const recentMatchingTrail = getTrailStore().findMatchingRecentTrail(newTrail)?.resolve(); - - // If there is a matching trail, initialize with that. Otherwise, use the new trail. - return recentMatchingTrail || newTrail; -} - -function getStyles(theme: GrafanaTheme2) { - return { - customPage: css({ - padding: theme.spacing(2, 3, 2, 3), - background: theme.isLight ? theme.colors.background.primary : theme.colors.background.canvas, - flexGrow: 1, - display: 'flex', - flexDirection: 'column', - }), - }; -} diff --git a/public/app/features/trails/TrailStore/TrailStore.test.ts b/public/app/features/trails/TrailStore/TrailStore.test.ts index 1a3bccd567c..871dd2fc55c 100644 --- a/public/app/features/trails/TrailStore/TrailStore.test.ts +++ b/public/app/features/trails/TrailStore/TrailStore.test.ts @@ -349,6 +349,7 @@ describe('TrailStore', () => { describe('And time range is changed to now-15m to now', () => { let trail: DataTrail; + beforeEach(() => { localStorage.clear(); localStorage.setItem(RECENT_TRAILS_KEY, JSON.stringify([{ history, currentStep: 1 }])); @@ -357,6 +358,7 @@ describe('TrailStore', () => { trail = store.recent[0].resolve(); const urlState = getUrlSyncManager().getUrlState(trail); locationService.partial(urlState); + trail.activate(); trail.state.history.activate(); trail.state.$timeRange?.setState({ from: 'now-15m' }); diff --git a/public/app/features/trails/TrailStore/TrailStore.ts b/public/app/features/trails/TrailStore/TrailStore.ts index 02c7cd92446..4eeea064b32 100644 --- a/public/app/features/trails/TrailStore/TrailStore.ts +++ b/public/app/features/trails/TrailStore/TrailStore.ts @@ -1,5 +1,6 @@ import { debounce, isEqual } from 'lodash'; +import { urlUtil } from '@grafana/data'; import { getUrlSyncManager, SceneObject, SceneObjectRef, SceneObjectUrlValues, sceneUtils } from '@grafana/scenes'; import { dispatch } from 'app/store/store'; @@ -77,9 +78,14 @@ export class TrailStore { }); const currentStep = t.currentStep ?? trail.state.history.state.steps.length - 1; + trail.state.history.setState({ currentStep }); - // The state change listeners aren't activated yet, so maually change to the current step state - trail.setState(trail.state.history.state.steps[currentStep].trailState); + + trail.setState( + sceneUtils.cloneSceneObjectState(trail.state.history.state.steps[currentStep].trailState, { + history: trail.state.history, + }) + ); return trail; } @@ -102,8 +108,8 @@ export class TrailStore { } private _loadFromUrl(node: SceneObject, urlValues: SceneObjectUrlValues) { - node.urlSync?.updateFromUrl(urlValues); - node.forEachChild((child) => this._loadFromUrl(child, urlValues)); + const urlState = urlUtil.renderUrl('', urlValues); + sceneUtils.syncStateFromSearchParams(node, new URLSearchParams(urlState)); } // Recent Trails @@ -140,14 +146,6 @@ export class TrailStore { this._save(); } - findMatchingRecentTrail(trail: DataTrail) { - const matchUrlState = getUrlStateForComparison(trail); - return this._recent.find((t) => { - const urlState = getUrlStateForComparison(t.resolve()); - return isEqual(matchUrlState, urlState); - }); - } - // Bookmarked Trails get bookmarks() { return this._bookmarks; diff --git a/yarn.lock b/yarn.lock index 365f011f4a0..53a7df821f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -134,7 +134,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-annotate-as-pure@npm:^7.24.7": +"@babel/helper-annotate-as-pure@npm:^7.22.5, @babel/helper-annotate-as-pure@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-annotate-as-pure@npm:7.24.7" dependencies: @@ -166,7 +166,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.24.7": +"@babel/helper-create-class-features-plugin@npm:^7.24.5, @babel/helper-create-class-features-plugin@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-create-class-features-plugin@npm:7.24.7" dependencies: @@ -285,7 +285,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.24.7, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.18.9, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.24.0, @babel/helper-plugin-utils@npm:^7.24.5, @babel/helper-plugin-utils@npm:^7.24.7, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": version: 7.24.7 resolution: "@babel/helper-plugin-utils@npm:7.24.7" checksum: 10/dad51622f0123fdba4e2d40a81a6b7d6ef4b1491b2f92fd9749447a36bde809106cf117358705057a2adc8fd73d5dc090222e0561b1213dae8601c8367f5aac8 @@ -361,7 +361,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.24.7": +"@babel/helper-validator-option@npm:^7.23.5, @babel/helper-validator-option@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-validator-option@npm:7.24.7" checksum: 10/9689166bf3f777dd424c026841c8cd651e41b21242dbfd4569a53086179a3e744c8eddd56e9d10b54142270141c91581b53af0d7c00c82d552d2540e2a919f7e @@ -546,14 +546,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-flow@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-syntax-flow@npm:7.24.7" +"@babel/plugin-syntax-flow@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-syntax-flow@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.24.7" + "@babel/helper-plugin-utils": "npm:^7.24.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/0a83bde6736110d68f3b20eda44ca020a6d34c336a342f84369207f5514e17779b9c3d3ebc2f1c94b595c13819f46bf7af367c4b1382bda182e1764655fd6a5a + checksum: 10/87dfe32f3a3ea77941034fb2a39fdfc9ea18a994b8df40c3659a11c8787b2bc5adea029259c4eafc03cd35f11628f6533aa2a06381db7fcbe3b2cc3c2a2bb54f languageName: node linkType: hard @@ -601,7 +601,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.24.7, @babel/plugin-syntax-jsx@npm:^7.7.2": +"@babel/plugin-syntax-jsx@npm:^7.24.1, @babel/plugin-syntax-jsx@npm:^7.24.7, @babel/plugin-syntax-jsx@npm:^7.7.2": version: 7.24.7 resolution: "@babel/plugin-syntax-jsx@npm:7.24.7" dependencies: @@ -700,7 +700,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.24.7, @babel/plugin-syntax-typescript@npm:^7.7.2": +"@babel/plugin-syntax-typescript@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-syntax-typescript@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.24.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/bf4bd70788d5456b5f75572e47a2e31435c7c4e43609bd4dffd2cc0c7a6cf90aabcf6cd389e351854de9a64412a07d30effef5373251fe8f6a4c9db0c0163bda + languageName: node + linkType: hard + +"@babel/plugin-syntax-typescript@npm:^7.7.2": version: 7.24.7 resolution: "@babel/plugin-syntax-typescript@npm:7.24.7" dependencies: @@ -908,15 +919,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-flow-strip-types@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-flow-strip-types@npm:7.24.7" +"@babel/plugin-transform-flow-strip-types@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-flow-strip-types@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.24.7" - "@babel/plugin-syntax-flow": "npm:^7.24.7" + "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/plugin-syntax-flow": "npm:^7.24.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/234390eb09f0c1d5a2001c9e48c6440c30f9f188939004e07aa8c0cb946f04793a2e058fa1737b1c56041a7d3ea1510593c39220cc43bba85a017bfcc1c89c4d + checksum: 10/6e1db557d7d34a8dbfdf430557f47c75930a9044b838bb3cc706f9c816e11cd68a61c68239478dd05bbe3ec197113ad0c22c5be1bdddac8723040dd9e9cb9dc0 languageName: node linkType: hard @@ -1003,7 +1014,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.22.5, @babel/plugin-transform-modules-commonjs@npm:^7.23.0, @babel/plugin-transform-modules-commonjs@npm:^7.24.7": +"@babel/plugin-transform-modules-commonjs@npm:^7.22.5, @babel/plugin-transform-modules-commonjs@npm:^7.23.0, @babel/plugin-transform-modules-commonjs@npm:^7.24.1, @babel/plugin-transform-modules-commonjs@npm:^7.24.7": version: 7.24.7 resolution: "@babel/plugin-transform-modules-commonjs@npm:7.24.7" dependencies: @@ -1316,17 +1327,17 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-transform-typescript@npm:7.24.7" +"@babel/plugin-transform-typescript@npm:^7.24.1": + version: 7.24.5 + resolution: "@babel/plugin-transform-typescript@npm:7.24.5" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.24.7" - "@babel/helper-create-class-features-plugin": "npm:^7.24.7" - "@babel/helper-plugin-utils": "npm:^7.24.7" - "@babel/plugin-syntax-typescript": "npm:^7.24.7" + "@babel/helper-annotate-as-pure": "npm:^7.22.5" + "@babel/helper-create-class-features-plugin": "npm:^7.24.5" + "@babel/helper-plugin-utils": "npm:^7.24.5" + "@babel/plugin-syntax-typescript": "npm:^7.24.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/6a4af5a96a90f08ea679829abc558b8478b8b31b40c84b887f2859110b75ab2c8c48a2cf80193621d988a6b064aefef2a74ea3ccc310166219f87959d06a3033 + checksum: 10/3d35accd6d7ae075509e01ce2cc3921ef3b44159b8ec15dd6201050c56dab4cfe14c5c0538e26e3beffb14c33731527041b60444cfba1ceae740f0748caf0aa0 languageName: node linkType: hard @@ -1479,15 +1490,15 @@ __metadata: linkType: hard "@babel/preset-flow@npm:^7.22.15": - version: 7.24.7 - resolution: "@babel/preset-flow@npm:7.24.7" + version: 7.24.1 + resolution: "@babel/preset-flow@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.24.7" - "@babel/helper-validator-option": "npm:^7.24.7" - "@babel/plugin-transform-flow-strip-types": "npm:^7.24.7" + "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/helper-validator-option": "npm:^7.23.5" + "@babel/plugin-transform-flow-strip-types": "npm:^7.24.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/20fe02b5bc3a9d5b353d164d5ef89841032605434ae351d14309a041d6dc5bd0df3417d0510a6468813392d54793825ba6b04d8c5a5377eee31fc2b55503bf26 + checksum: 10/f1402746050a1c03af9509791bb88e90d1d56a3063374278a80b030c6d1f48a462a822a1a66826d0a631cb5424fc70bf91a25de5f7f31ff519553a3e190a0b7e languageName: node linkType: hard @@ -1521,23 +1532,23 @@ __metadata: linkType: hard "@babel/preset-typescript@npm:^7.23.0": - version: 7.24.7 - resolution: "@babel/preset-typescript@npm:7.24.7" + version: 7.24.1 + resolution: "@babel/preset-typescript@npm:7.24.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.24.7" - "@babel/helper-validator-option": "npm:^7.24.7" - "@babel/plugin-syntax-jsx": "npm:^7.24.7" - "@babel/plugin-transform-modules-commonjs": "npm:^7.24.7" - "@babel/plugin-transform-typescript": "npm:^7.24.7" + "@babel/helper-plugin-utils": "npm:^7.24.0" + "@babel/helper-validator-option": "npm:^7.23.5" + "@babel/plugin-syntax-jsx": "npm:^7.24.1" + "@babel/plugin-transform-modules-commonjs": "npm:^7.24.1" + "@babel/plugin-transform-typescript": "npm:^7.24.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/995e9783f8e474581e7533d6b10ec1fbea69528cc939ad8582b5937e13548e5215d25a8e2c845e7b351fdaa13139896b5e42ab3bde83918ea4e41773f10861ac + checksum: 10/ba774bd427c9f376769ddbc2723f5801a6b30113a7c3aaa14c36215508e347a527fdae98cfc294f0ecb283d800ee0c1f74e66e38e84c9bc9ed2fe6ed50dcfaf8 languageName: node linkType: hard "@babel/register@npm:^7.22.15": - version: 7.24.6 - resolution: "@babel/register@npm:7.24.6" + version: 7.23.7 + resolution: "@babel/register@npm:7.23.7" dependencies: clone-deep: "npm:^4.0.1" find-cache-dir: "npm:^2.0.0" @@ -1546,7 +1557,7 @@ __metadata: source-map-support: "npm:^0.5.16" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/94580678ee541218475d605720ea1c3b4a647c504c8a08124373efad24a523f219dd7441de92f09c692c22362ea4422c5f3c51a3b3048b7a64deb1f6daea96b6 + checksum: 10/c72a6d4856ef04f13490370d805854d2d98a77786bfaec7d85e2c585e1217011c4f3df18197a890e14520906c9111bef95551ba1a9b59c88df4dfc2dfe2c8d1b languageName: node linkType: hard @@ -3524,15 +3535,15 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes@npm:4.29.0": - version: 4.29.0 - resolution: "@grafana/scenes@npm:4.29.0" +"@grafana/scenes@npm:^5.0.2": + version: 5.0.2 + resolution: "@grafana/scenes@npm:5.0.2" dependencies: "@grafana/e2e-selectors": "npm:^11.0.0" "@leeoniya/ufuzzy": "npm:^1.0.14" react-grid-layout: "npm:1.3.4" - react-use: "npm:17.4.0" - react-virtualized-auto-sizer: "npm:1.0.7" + react-use: "npm:17.5.0" + react-virtualized-auto-sizer: "npm:1.0.24" uuid: "npm:^9.0.0" peerDependencies: "@grafana/data": ^10.4.1 @@ -3541,7 +3552,7 @@ __metadata: "@grafana/ui": ^10.4.1 react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/5da9b2ae9093f4c3ed012eeea58ce0dc66ddeabb02d2b0d4a87917f17463c857ccab5df23142cfc62323991e714a1a3f4d38d5956cdfe455ce57edcc82fe6893 + checksum: 10/a7bb83c5d0aca6e47ce8fb50ff5952da1dc1ea1b209148cbc7795bc480606df09012ff13a309510fcb4fa75e94af26700aa53bb0fadeb77d8215d23288b46b47 languageName: node linkType: hard @@ -4664,13 +4675,13 @@ __metadata: linkType: hard "@ndelangen/get-tarball@npm:^3.0.7": - version: 3.0.9 - resolution: "@ndelangen/get-tarball@npm:3.0.9" + version: 3.0.7 + resolution: "@ndelangen/get-tarball@npm:3.0.7" dependencies: gunzip-maybe: "npm:^1.4.2" pump: "npm:^3.0.0" tar-fs: "npm:^2.1.1" - checksum: 10/39697cef2b92f6e08e3590467cc6da88cd6757b2a27cb9208879c2316ed71d6be4608892ee0a86eb0343140da1a5df498f93a32c2aaf8f1fbd90f883f08b5f63 + checksum: 10/382689de9d4a8e5e5eca0647c958969b4debd977f7acf398388c4515baf242a13f57f22adb6d02c558f68c4945834c2470982828da7247caaee90a2c9053a396 languageName: node linkType: hard @@ -5688,7 +5699,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-portal@npm:1.0.4, @radix-ui/react-portal@npm:^1.0.1": +"@radix-ui/react-portal@npm:1.0.4": version: 1.0.4 resolution: "@radix-ui/react-portal@npm:1.0.4" dependencies: @@ -5708,6 +5719,26 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-portal@npm:^1.0.1": + version: 1.0.3 + resolution: "@radix-ui/react-portal@npm:1.0.3" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-primitive": "npm:1.0.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10/d352bcd6ad65eb43c9e0d72d0755c2aae85e03fb287770866262be3a2d5302b2885aee3cd99f2bbf62ecd14fcb1460703f1dcdc40351f77ad887b931c6f0012a + languageName: node + linkType: hard + "@radix-ui/react-presence@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-presence@npm:1.0.1" @@ -6605,12 +6636,12 @@ __metadata: linkType: hard "@storybook/addon-webpack5-compiler-swc@npm:^1.0.2": - version: 1.0.3 - resolution: "@storybook/addon-webpack5-compiler-swc@npm:1.0.3" + version: 1.0.2 + resolution: "@storybook/addon-webpack5-compiler-swc@npm:1.0.2" dependencies: - "@swc/core": "npm:1.5.7" + "@swc/core": "npm:^1.3.102" swc-loader: "npm:^0.2.3" - checksum: 10/21d5fbf6021a1cb1c7a0c6b0d5d17a475d4afd29610fa1c43e2b21a015cee4446b9711c991812eca17e5ff78c3e9d402762d93584e5fecf2de303f261fb0db59 + checksum: 10/890b321888b844326b72cdb98b6627dcefe2fa3c0314816dd31b93fbc7a6ec6a3e3db7fb44505bef1155994874f2c47d1d75fd4c22a29a233d110852577ccc62 languageName: node linkType: hard @@ -7515,9 +7546,9 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.5.7": - version: 1.5.7 - resolution: "@swc/core-darwin-arm64@npm:1.5.7" +"@swc/core-darwin-arm64@npm:1.5.5": + version: 1.5.5 + resolution: "@swc/core-darwin-arm64@npm:1.5.5" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -7529,9 +7560,9 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.5.7": - version: 1.5.7 - resolution: "@swc/core-darwin-x64@npm:1.5.7" +"@swc/core-darwin-x64@npm:1.5.5": + version: 1.5.5 + resolution: "@swc/core-darwin-x64@npm:1.5.5" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -7543,9 +7574,9 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.5.7": - version: 1.5.7 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.5.7" +"@swc/core-linux-arm-gnueabihf@npm:1.5.5": + version: 1.5.5 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.5.5" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -7557,9 +7588,9 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.5.7": - version: 1.5.7 - resolution: "@swc/core-linux-arm64-gnu@npm:1.5.7" +"@swc/core-linux-arm64-gnu@npm:1.5.5": + version: 1.5.5 + resolution: "@swc/core-linux-arm64-gnu@npm:1.5.5" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard @@ -7571,9 +7602,9 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.5.7": - version: 1.5.7 - resolution: "@swc/core-linux-arm64-musl@npm:1.5.7" +"@swc/core-linux-arm64-musl@npm:1.5.5": + version: 1.5.5 + resolution: "@swc/core-linux-arm64-musl@npm:1.5.5" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard @@ -7585,9 +7616,9 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.5.7": - version: 1.5.7 - resolution: "@swc/core-linux-x64-gnu@npm:1.5.7" +"@swc/core-linux-x64-gnu@npm:1.5.5": + version: 1.5.5 + resolution: "@swc/core-linux-x64-gnu@npm:1.5.5" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard @@ -7599,9 +7630,9 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.5.7": - version: 1.5.7 - resolution: "@swc/core-linux-x64-musl@npm:1.5.7" +"@swc/core-linux-x64-musl@npm:1.5.5": + version: 1.5.5 + resolution: "@swc/core-linux-x64-musl@npm:1.5.5" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard @@ -7613,9 +7644,9 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.5.7": - version: 1.5.7 - resolution: "@swc/core-win32-arm64-msvc@npm:1.5.7" +"@swc/core-win32-arm64-msvc@npm:1.5.5": + version: 1.5.5 + resolution: "@swc/core-win32-arm64-msvc@npm:1.5.5" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -7627,9 +7658,9 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.5.7": - version: 1.5.7 - resolution: "@swc/core-win32-ia32-msvc@npm:1.5.7" +"@swc/core-win32-ia32-msvc@npm:1.5.5": + version: 1.5.5 + resolution: "@swc/core-win32-ia32-msvc@npm:1.5.5" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -7641,9 +7672,9 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.5.7": - version: 1.5.7 - resolution: "@swc/core-win32-x64-msvc@npm:1.5.7" +"@swc/core-win32-x64-msvc@npm:1.5.5": + version: 1.5.5 + resolution: "@swc/core-win32-x64-msvc@npm:1.5.5" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -7694,22 +7725,22 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:1.5.7": - version: 1.5.7 - resolution: "@swc/core@npm:1.5.7" +"@swc/core@npm:^1.3.102": + version: 1.5.5 + resolution: "@swc/core@npm:1.5.5" dependencies: - "@swc/core-darwin-arm64": "npm:1.5.7" - "@swc/core-darwin-x64": "npm:1.5.7" - "@swc/core-linux-arm-gnueabihf": "npm:1.5.7" - "@swc/core-linux-arm64-gnu": "npm:1.5.7" - "@swc/core-linux-arm64-musl": "npm:1.5.7" - "@swc/core-linux-x64-gnu": "npm:1.5.7" - "@swc/core-linux-x64-musl": "npm:1.5.7" - "@swc/core-win32-arm64-msvc": "npm:1.5.7" - "@swc/core-win32-ia32-msvc": "npm:1.5.7" - "@swc/core-win32-x64-msvc": "npm:1.5.7" + "@swc/core-darwin-arm64": "npm:1.5.5" + "@swc/core-darwin-x64": "npm:1.5.5" + "@swc/core-linux-arm-gnueabihf": "npm:1.5.5" + "@swc/core-linux-arm64-gnu": "npm:1.5.5" + "@swc/core-linux-arm64-musl": "npm:1.5.5" + "@swc/core-linux-x64-gnu": "npm:1.5.5" + "@swc/core-linux-x64-musl": "npm:1.5.5" + "@swc/core-win32-arm64-msvc": "npm:1.5.5" + "@swc/core-win32-ia32-msvc": "npm:1.5.5" + "@swc/core-win32-x64-msvc": "npm:1.5.5" "@swc/counter": "npm:^0.1.2" - "@swc/types": "npm:0.1.7" + "@swc/types": "npm:^0.1.5" peerDependencies: "@swc/helpers": ^0.5.0 dependenciesMeta: @@ -7736,7 +7767,7 @@ __metadata: peerDependenciesMeta: "@swc/helpers": optional: true - checksum: 10/83e03908db40f2133c3624a83d4550336d7a56e64af7d42fd959c746b8da950a253f3c6d9eaa3467e10abeda024aa6b039a987adc839326f969e1d26625f14ef + checksum: 10/31e6dc50eeb549d1e52f21e1c5ae63b714bf4488c70cc4106264de378048ca35c0264c640590fbbb760c0cbe7f2c91291ef20e6af5c46dc31b5da88e8b946f0c languageName: node linkType: hard @@ -7756,12 +7787,10 @@ __metadata: languageName: node linkType: hard -"@swc/types@npm:0.1.7, @swc/types@npm:^0.1.5": - version: 0.1.7 - resolution: "@swc/types@npm:0.1.7" - dependencies: - "@swc/counter": "npm:^0.1.3" - checksum: 10/ed66c26b36972a74f852c1781fadc75946578abfeeea58f110684833b5d1e70f28a77ddb82fd5bf3cf3c4dad0e1b6a1c924d7e2cc7a99f9b16ed16fe266bba25 +"@swc/types@npm:^0.1.5": + version: 0.1.5 + resolution: "@swc/types@npm:0.1.5" + checksum: 10/5f4de8c60d2623bed607c7fa1e0cee4ffc682af28d5ffe88dc9ed9903a1c2088ccc39f684689d6bb314595c9fbb560beaec773d633be515fb856ffc81d738822 languageName: node linkType: hard @@ -7942,6 +7971,13 @@ __metadata: languageName: node linkType: hard +"@types/add@npm:^2": + version: 2.0.3 + resolution: "@types/add@npm:2.0.3" + checksum: 10/c15815ef0e4903113f7fda84b4c2704a1b694fd6f53a6e3c9327b5b7067aba676c75e696b950401de45be7646b37b7059303a7bb5c4363de3eb3e0c06d25f565 + languageName: node + linkType: hard + "@types/angular-route@npm:1.7.6": version: 1.7.6 resolution: "@types/angular-route@npm:1.7.6" @@ -8073,11 +8109,11 @@ __metadata: linkType: hard "@types/cross-spawn@npm:^6.0.2": - version: 6.0.6 - resolution: "@types/cross-spawn@npm:6.0.6" + version: 6.0.3 + resolution: "@types/cross-spawn@npm:6.0.3" dependencies: "@types/node": "npm:*" - checksum: 10/b4172927cd1387cf037c3ade785ef46c87537b7bc2803d7f6663b4904d0c5d6f726415d1adb2fee4fecb21746738f11336076449265d46be4ce110cc3a8c8436 + checksum: 10/06d50fa1e1370ef60b9c9085b76adec7d7bc20728fbb02b3c2061d4d922312acf1ba56a7c94d88c27a22fc6241ab6b970c936f3294038a9c97a719fbc8eb8a76 languageName: node linkType: hard @@ -8402,9 +8438,9 @@ __metadata: linkType: hard "@types/detect-port@npm:^1.3.0": - version: 1.3.5 - resolution: "@types/detect-port@npm:1.3.5" - checksum: 10/923cf04c6a05af59090743baeb9948f1938ceb98c1f7ea93db7ac310210426b385aa00005d23039ebb8019a9d13e141f5246e9c733b290885018d722a4787921 + version: 1.3.2 + resolution: "@types/detect-port@npm:1.3.2" + checksum: 10/e4678244fbe8801014798b3efb967c886e6fc0fe94fb771a1be9558b35c68910b23bd30984df4a276b927820ce436b244506fb0972116d1b18506ac96bfd1a50 languageName: node linkType: hard @@ -8439,9 +8475,9 @@ __metadata: linkType: hard "@types/ejs@npm:^3.1.1": - version: 3.1.5 - resolution: "@types/ejs@npm:3.1.5" - checksum: 10/918898fd279108087722c1713e2ddb0c152ab839397946d164db8a18b5bbd732af9746373882a9bcf4843d35c6b191a8f569a7a4e51e90726d24501b39f40367 + version: 3.1.2 + resolution: "@types/ejs@npm:3.1.2" + checksum: 10/8be94c952dc06525a5fb229a469db66f2240425ce137a1f5108a6a11f500cac9190691e924e6d18df34c5c2be40ef36b537921d2833262467ee1dfb7a7c919ac languageName: node linkType: hard @@ -8768,18 +8804,18 @@ __metadata: linkType: hard "@types/mdast@npm:^4.0.0": - version: 4.0.4 - resolution: "@types/mdast@npm:4.0.4" + version: 4.0.3 + resolution: "@types/mdast@npm:4.0.3" dependencies: "@types/unist": "npm:*" - checksum: 10/efe3ec11b9ee0015a396c4fb4cd1b6f31b51b8ae9783c59560e6fc0bf6c2fa1dcc7fccaf45fa09a6c8b3397fab9dc8d431433935cae3835caa70a18f7fc775f8 + checksum: 10/6d2d8f00ffaff6663dd67ea9ab999a5e52066c001432a9b99947fa9e76bccba819dfca40e419588a637a70d42cd405071f5b76efd4ddeb1dc721353b7cc73623 languageName: node linkType: hard "@types/mdx@npm:^2.0.0": - version: 2.0.13 - resolution: "@types/mdx@npm:2.0.13" - checksum: 10/b73ed5f08114879b9590dc6a9ee8b648643c57c708583cd24b2bc3cc8961361fc63139ac7e9291e7b3b6e6b45707749d01d6f9727ddec5533df75dc3b90871a4 + version: 2.0.4 + resolution: "@types/mdx@npm:2.0.4" + checksum: 10/79e011ea17751741f69fa0a8bada8c596ca273c5510d4b37e8fa0dd8b5f93c6b8eb4a351da426454df7b4ddbaa8dfc3aae88562417300b968cc8964afac3a6ca languageName: node linkType: hard @@ -8908,9 +8944,9 @@ __metadata: linkType: hard "@types/pretty-hrtime@npm:^1.0.0": - version: 1.0.3 - resolution: "@types/pretty-hrtime@npm:1.0.3" - checksum: 10/288061dff992c8107d5c7b5a1277bbb0a314a27eb10087dea628a08fa37694a655191a69e25a212c95e61e498363c48ad9e281d23964a448f6c14100a6be0910 + version: 1.0.1 + resolution: "@types/pretty-hrtime@npm:1.0.1" + checksum: 10/a6cdee417eea6f7af914e4fcd13e05822864ce10b5d7646525632e86d69b79123eec55a5d3fff0155ba46b61902775e1644bcb80e1e4dffdac28e7febb089083 languageName: node linkType: hard @@ -10205,7 +10241,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.11.3, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.2, acorn@npm:^8.9.0": +"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.10.0, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.2, acorn@npm:^8.9.0": version: 8.11.3 resolution: "acorn@npm:8.11.3" bin: @@ -10238,9 +10274,9 @@ __metadata: linkType: hard "address@npm:^1.0.1": - version: 1.2.2 - resolution: "address@npm:1.2.2" - checksum: 10/57d80a0c6ccadc8769ad3aeb130c1599e8aee86a8d25f671216c40df9b8489d6c3ef879bc2752b40d1458aa768f947c2d91e5b2fedfe63cf702c40afdfda9ba9 + version: 1.1.2 + resolution: "address@npm:1.1.2" + checksum: 10/be4c16c4874a347c8f6dec28b5b5c891f7c3ca2234bdd734b6e4b0f28310b07a9ae8359937a8d92a4ae682019d375d70de920203d9acc839167674e3a525970a languageName: node linkType: hard @@ -11250,9 +11286,9 @@ __metadata: linkType: hard "big-integer@npm:^1.6.44": - version: 1.6.52 - resolution: "big-integer@npm:1.6.52" - checksum: 10/4bc6ae152a96edc9f95020f5fc66b13d26a9ad9a021225a9f0213f7e3dc44269f423aa8c42e19d6ac4a63bb2b22140b95d10be8f9ca7a6d9aa1b22b330d1f514 + version: 1.6.51 + resolution: "big-integer@npm:1.6.51" + checksum: 10/c7a12640901906d6f6b6bdb42a4eaba9578397b6d9a0dd090cf001ec813ff2bfcd441e364068ea0416db6175d2615f8ed19cff7d1a795115bf7c92d44993f991 languageName: node linkType: hard @@ -11992,15 +12028,6 @@ __metadata: languageName: node linkType: hard -"citty@npm:^0.1.6": - version: 0.1.6 - resolution: "citty@npm:0.1.6" - dependencies: - consola: "npm:^3.2.3" - checksum: 10/3208947e73abb699a12578ee2bfee254bf8dd1ce0d5698e8a298411cabf16bd3620d63433aef5bd88cdb2b9da71aef18adefa3b4ffd18273bb62dd1d28c344f5 - languageName: node - linkType: hard - "cjs-module-lexer@npm:^1.0.0, cjs-module-lexer@npm:^1.2.3": version: 1.3.1 resolution: "cjs-module-lexer@npm:1.3.1" @@ -12055,15 +12082,15 @@ __metadata: linkType: hard "cli-table3@npm:^0.6.1, cli-table3@npm:~0.6.1": - version: 0.6.5 - resolution: "cli-table3@npm:0.6.5" + version: 0.6.3 + resolution: "cli-table3@npm:0.6.3" dependencies: "@colors/colors": "npm:1.5.0" string-width: "npm:^4.2.0" dependenciesMeta: "@colors/colors": optional: true - checksum: 10/8dca71256f6f1367bab84c33add3f957367c7c43750a9828a4212ebd31b8df76bd7419d386e3391ac7419698a8540c25f1a474584028f35b170841cde2e055c5 + checksum: 10/8d82b75be7edc7febb1283dc49582a521536527cba80af62a2e4522a0ee39c252886a1a2f02d05ae9d753204dbcffeb3a40d1358ee10dccd7fe8d935cfad3f85 languageName: node linkType: hard @@ -12268,7 +12295,7 @@ __metadata: languageName: node linkType: hard -"colorette@npm:^2.0.10, colorette@npm:^2.0.14, colorette@npm:^2.0.16": +"colorette@npm:^2.0.10, colorette@npm:^2.0.14, colorette@npm:^2.0.16, colorette@npm:^2.0.19": version: 2.0.20 resolution: "colorette@npm:2.0.20" checksum: 10/0b8de48bfa5d10afc160b8eaa2b9938f34a892530b2f7d7897e0458d9535a066e3998b49da9d21161c78225b272df19ae3a64d6df28b4c9734c0e55bbd02406f @@ -14005,10 +14032,10 @@ __metadata: languageName: node linkType: hard -"defu@npm:^6.1.4": - version: 6.1.4 - resolution: "defu@npm:6.1.4" - checksum: 10/aeffdb47300f45b4fdef1c5bd3880ac18ea7a1fd5b8a8faf8df29350ff03bf16dd34f9800205cab513d476e4c0a3783aa0cff0a433aff0ac84a67ddc4c8a2d64 +"defu@npm:^6.1.2": + version: 6.1.2 + resolution: "defu@npm:6.1.2" + checksum: 10/5704aa6ea0b503004ee25b2ce909af8e6dc7c472d2d41e293f5a879534a0a7827a37e6692e0ca0c6e8d3ef6b00651d50089be681c814832cbed98f0f206ef25b languageName: node linkType: hard @@ -14115,15 +14142,15 @@ __metadata: linkType: hard "detect-port@npm:^1.3.0": - version: 1.6.1 - resolution: "detect-port@npm:1.6.1" + version: 1.5.1 + resolution: "detect-port@npm:1.5.1" dependencies: address: "npm:^1.0.1" debug: "npm:4" bin: detect: bin/detect-port.js detect-port: bin/detect-port.js - checksum: 10/0429fa423abb15fc453face64e6ffa406e375f51f5b4421a7886962e680dc05824eae9b6ee4594ba273685c3add415ad00982b5da54802ac3de6f846173284c3 + checksum: 10/b48da9340481742547263d5d985e65d078592557863402ecf538511735e83575867e94f91fe74405ea19b61351feb99efccae7e55de9a151d5654e3417cea05b languageName: node linkType: hard @@ -15556,23 +15583,6 @@ __metadata: languageName: node linkType: hard -"execa@npm:^8.0.1": - version: 8.0.1 - resolution: "execa@npm:8.0.1" - dependencies: - cross-spawn: "npm:^7.0.3" - get-stream: "npm:^8.0.1" - human-signals: "npm:^5.0.0" - is-stream: "npm:^3.0.0" - merge-stream: "npm:^2.0.0" - npm-run-path: "npm:^5.1.0" - onetime: "npm:^6.0.0" - signal-exit: "npm:^4.1.0" - strip-final-newline: "npm:^3.0.0" - checksum: 10/d2ab5fe1e2bb92b9788864d0713f1fce9a07c4594e272c0c97bc18c90569897ab262e4ea58d27a694d288227a2e24f16f5e2575b44224ad9983b799dc7f1098d - languageName: node - linkType: hard - "executable@npm:^4.1.1": version: 4.1.1 resolution: "executable@npm:4.1.1" @@ -15868,9 +15878,9 @@ __metadata: linkType: hard "fetch-retry@npm:^5.0.2": - version: 5.0.6 - resolution: "fetch-retry@npm:5.0.6" - checksum: 10/9d64b37f9d179fecf486725ada210d169375803b731304a9500754e094a2a6aa81630d946adbb313d7f9d54457ad0d17c3ed5c115034961a719e8a65faa8b77c + version: 5.0.4 + resolution: "fetch-retry@npm:5.0.4" + checksum: 10/1c9fd7046b5b525e18cabdc34693887f0c410a8564eebf09bb8d8625d33bec4a9fa7f39660076ef99e36a77f152b233d26742be4b3b04583f41640c063152914 languageName: node linkType: hard @@ -16126,9 +16136,9 @@ __metadata: linkType: hard "flow-parser@npm:0.*": - version: 0.237.2 - resolution: "flow-parser@npm:0.237.2" - checksum: 10/8be3dd71f6838b812b6030e3a31d93b873564ab1deb0ebf060c346f5887d478510ee6c0c758ecd9ef288068c91e66f0d83fd30ae5ead674e3ea150c5589aa049 + version: 0.203.1 + resolution: "flow-parser@npm:0.203.1" + checksum: 10/7e00c89a13500b53b30e0013269f4f5e04a6a03e5f99bc91bb6fe0311fbecce1094b56c05cab35fb767bfb408af262e02e7ad1fbc47161d1709d1204bbb00fee languageName: node linkType: hard @@ -16555,9 +16565,9 @@ __metadata: linkType: hard "get-npm-tarball-url@npm:^2.0.3": - version: 2.1.0 - resolution: "get-npm-tarball-url@npm:2.1.0" - checksum: 10/02b96993ad5a04cbd0ef0577ac3cc9e2e78a7c60db6bb5e6c8fe78950fc1fc3d093314987629a2fda3083228d91a93670bde321767ca2cf89ce7f463c9e44071 + version: 2.0.3 + resolution: "get-npm-tarball-url@npm:2.0.3" + checksum: 10/8ad48a6f1126697665e12ebf053e0d1c3b15b3c4f29ea6c458387ac68d044ea1c08f0f2eb5c0fe35447fdd2da4f2fb5c9882feb5a2ea195c773f94e762c9b886 languageName: node linkType: hard @@ -16612,13 +16622,6 @@ __metadata: languageName: node linkType: hard -"get-stream@npm:^8.0.1": - version: 8.0.1 - resolution: "get-stream@npm:8.0.1" - checksum: 10/dde5511e2e65a48e9af80fea64aff11b4921b14b6e874c6f8294c50975095af08f41bfb0b680c887f28b566dd6ec2cb2f960f9d36a323359be324ce98b766e9e - languageName: node - linkType: hard - "get-symbol-description@npm:^1.0.2": version: 1.0.2 resolution: "get-symbol-description@npm:1.0.2" @@ -16677,20 +16680,19 @@ __metadata: linkType: hard "giget@npm:^1.0.0": - version: 1.2.3 - resolution: "giget@npm:1.2.3" + version: 1.1.2 + resolution: "giget@npm:1.1.2" dependencies: - citty: "npm:^0.1.6" - consola: "npm:^3.2.3" - defu: "npm:^6.1.4" - node-fetch-native: "npm:^1.6.3" - nypm: "npm:^0.3.8" - ohash: "npm:^1.1.3" - pathe: "npm:^1.1.2" - tar: "npm:^6.2.0" + colorette: "npm:^2.0.19" + defu: "npm:^6.1.2" + https-proxy-agent: "npm:^5.0.1" + mri: "npm:^1.2.0" + node-fetch-native: "npm:^1.0.2" + pathe: "npm:^1.1.0" + tar: "npm:^6.1.13" bin: giget: dist/cli.mjs - checksum: 10/85bdcf380566fc9c4299f029acbe78a706f1825912c6cea39b675d08064399988f5de30d17238246f725183ac7504e7b9d3000c417f1df7ebb52ab26c7d3ab8c + checksum: 10/f5080b18437fcd4cb92eb8bc90f69f57008460416c5a919a5d50e94889c0a5bfec641f203bd32f91b3de3058c8be48f23ab4b4f2d29f785dcb311b5f10c51bf4 languageName: node linkType: hard @@ -17015,7 +17017,7 @@ __metadata: "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" - "@grafana/scenes": "npm:4.29.0" + "@grafana/scenes": "npm:^5.0.2" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^1.3.0-rc1" @@ -17057,6 +17059,7 @@ __metadata: "@testing-library/react": "npm:15.0.2" "@testing-library/react-hooks": "npm:^8.0.1" "@testing-library/user-event": "npm:14.5.2" + "@types/add": "npm:^2" "@types/angular": "npm:1.8.9" "@types/angular-route": "npm:1.7.6" "@types/babel__core": "npm:^7" @@ -17314,6 +17317,7 @@ __metadata: xlsx: "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz" yaml: "npm:^2.0.0" yargs: "npm:^17.5.1" + yarn: "npm:^1.22.22" dependenciesMeta: prettier@3.3.1: unplugged: true @@ -17989,13 +17993,6 @@ __metadata: languageName: node linkType: hard -"human-signals@npm:^5.0.0": - version: 5.0.0 - resolution: "human-signals@npm:5.0.0" - checksum: 10/30f8870d831cdcd2d6ec0486a7d35d49384996742052cee792854273fa9dd9e7d5db06bb7985d4953e337e10714e994e0302e90dc6848069171b05ec836d65b0 - languageName: node - linkType: hard - "humanize-ms@npm:^1.2.1": version: 1.2.1 resolution: "humanize-ms@npm:1.2.1" @@ -20982,8 +20979,8 @@ __metadata: linkType: hard "mdast-util-from-markdown@npm:^2.0.0": - version: 2.0.1 - resolution: "mdast-util-from-markdown@npm:2.0.1" + version: 2.0.0 + resolution: "mdast-util-from-markdown@npm:2.0.0" dependencies: "@types/mdast": "npm:^4.0.0" "@types/unist": "npm:^3.0.0" @@ -20997,7 +20994,7 @@ __metadata: micromark-util-symbol: "npm:^2.0.0" micromark-util-types: "npm:^2.0.0" unist-util-stringify-position: "npm:^4.0.0" - checksum: 10/4172759cdd8cf9990701796c5617c8b6a4bd3f9863e730bb4e9689189daec80af3122e77eed2ab09090f1a2d396c4f5754416a41769d7c49efd165a1c0a033c8 + checksum: 10/960e28a8ff3d989cc25a615d14e9a1d95d145b938dc08323ce44689be6dd052ece544d2acf5242cedb8ad6ccdc3ffe854989b7c2516c6e62f2fca42b6d11a2da languageName: node linkType: hard @@ -21621,13 +21618,6 @@ __metadata: languageName: node linkType: hard -"mimic-fn@npm:^4.0.0": - version: 4.0.0 - resolution: "mimic-fn@npm:4.0.0" - checksum: 10/995dcece15ee29aa16e188de6633d43a3db4611bcf93620e7e62109ec41c79c0f34277165b8ce5e361205049766e371851264c21ac64ca35499acb5421c2ba56 - languageName: node - linkType: hard - "min-indent@npm:^1.0.0, min-indent@npm:^1.0.1": version: 1.0.1 resolution: "min-indent@npm:1.0.1" @@ -22045,6 +22035,13 @@ __metadata: languageName: node linkType: hard +"mri@npm:^1.2.0": + version: 1.2.0 + resolution: "mri@npm:1.2.0" + checksum: 10/6775a1d2228bb9d191ead4efc220bd6be64f943ad3afd4dcb3b3ac8fc7b87034443f666e38805df38e8d047b29f910c3cc7810da0109af83e42c82c73bd3f6bc + languageName: node + linkType: hard + "mrmime@npm:^2.0.0": version: 2.0.0 resolution: "mrmime@npm:2.0.0" @@ -22151,7 +22148,7 @@ __metadata: languageName: node linkType: hard -"nano-css@npm:^5.3.1, nano-css@npm:^5.6.1": +"nano-css@npm:^5.6.1": version: 5.6.1 resolution: "nano-css@npm:5.6.1" dependencies: @@ -22254,10 +22251,10 @@ __metadata: languageName: node linkType: hard -"node-fetch-native@npm:^1.6.3": - version: 1.6.4 - resolution: "node-fetch-native@npm:1.6.4" - checksum: 10/39c4c6d0c2a4bed1444943e1647ad0d79eb6638cf159bc37dffeafd22cffcf6a998e006aa1f3dd1d9d2258db7d78dee96b44bee4ba0bbaf0440ed348794f2543 +"node-fetch-native@npm:^1.0.2": + version: 1.0.2 + resolution: "node-fetch-native@npm:1.0.2" + checksum: 10/50bf4732d8c1115f626b663d7ce066a8ef7b22786813c45e38fde1594cdc2f61a3e1f3b457d1c5a576ac5beb9369912b671002ba273493841a33b8a57b553e81 languageName: node linkType: hard @@ -22624,15 +22621,6 @@ __metadata: languageName: node linkType: hard -"npm-run-path@npm:^5.1.0": - version: 5.3.0 - resolution: "npm-run-path@npm:5.3.0" - dependencies: - path-key: "npm:^4.0.0" - checksum: 10/ae8e7a89da9594fb9c308f6555c73f618152340dcaae423e5fb3620026fefbec463618a8b761920382d666fa7a2d8d240b6fe320e8a6cdd54dc3687e2b659d25 - languageName: node - linkType: hard - "npmlog@npm:^4.1.2": version: 4.1.2 resolution: "npmlog@npm:4.1.2" @@ -22764,21 +22752,6 @@ __metadata: languageName: node linkType: hard -"nypm@npm:^0.3.8": - version: 0.3.8 - resolution: "nypm@npm:0.3.8" - dependencies: - citty: "npm:^0.1.6" - consola: "npm:^3.2.3" - execa: "npm:^8.0.1" - pathe: "npm:^1.1.2" - ufo: "npm:^1.4.0" - bin: - nypm: dist/cli.mjs - checksum: 10/fc3fcf4f2c9837d09c1b9b976c205e1538a9378b5ac40ea0d3bac0bcaeb554d0a8d17e4b42c1b8b6079fb6bf760f0d94b576084c032f862433a915739a54e327 - languageName: node - linkType: hard - "oas-kit-common@npm:^1.0.8": version: 1.0.8 resolution: "oas-kit-common@npm:1.0.8" @@ -22996,13 +22969,6 @@ __metadata: languageName: node linkType: hard -"ohash@npm:^1.1.3": - version: 1.1.3 - resolution: "ohash@npm:1.1.3" - checksum: 10/80a3528285f61588600c8c4f091a67f55fbc141f4eec4b3c30182468053042eef5a9684780e963f98a71ec068f3de56d42920c6417bf8f79ab14aeb75ac0bb39 - languageName: node - linkType: hard - "ol-ext@npm:4.0.18": version: 4.0.18 resolution: "ol-ext@npm:4.0.18" @@ -23070,15 +23036,6 @@ __metadata: languageName: node linkType: hard -"onetime@npm:^6.0.0": - version: 6.0.0 - resolution: "onetime@npm:6.0.0" - dependencies: - mimic-fn: "npm:^4.0.0" - checksum: 10/0846ce78e440841335d4e9182ef69d5762e9f38aa7499b19f42ea1c4cd40f0b4446094c455c713f9adac3f4ae86f613bb5e30c99e52652764d06a89f709b3788 - languageName: node - linkType: hard - "open@npm:^10.0.3": version: 10.0.3 resolution: "open@npm:10.0.3" @@ -23568,13 +23525,6 @@ __metadata: languageName: node linkType: hard -"path-key@npm:^4.0.0": - version: 4.0.0 - resolution: "path-key@npm:4.0.0" - checksum: 10/8e6c314ae6d16b83e93032c61020129f6f4484590a777eed709c4a01b50e498822b00f76ceaf94bc64dbd90b327df56ceadce27da3d83393790f1219e07721d7 - languageName: node - linkType: hard - "path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" @@ -23645,10 +23595,10 @@ __metadata: languageName: node linkType: hard -"pathe@npm:^1.1.2": - version: 1.1.2 - resolution: "pathe@npm:1.1.2" - checksum: 10/f201d796351bf7433d147b92c20eb154a4e0ea83512017bf4ec4e492a5d6e738fb45798be4259a61aa81270179fce11026f6ff0d3fa04173041de044defe9d80 +"pathe@npm:^1.1.0": + version: 1.1.0 + resolution: "pathe@npm:1.1.0" + checksum: 10/7cd4e00d9991a2454cccc575fd0ebdd0fe0caf257e5a6690af542d41c63e4d7033e580677395c54e0e4addbd9e297c0ef4e5de02906decc93b48c1a58a1acb0c languageName: node linkType: hard @@ -23820,11 +23770,11 @@ __metadata: linkType: hard "polished@npm:^4.2.2": - version: 4.3.1 - resolution: "polished@npm:4.3.1" + version: 4.2.2 + resolution: "polished@npm:4.2.2" dependencies: "@babel/runtime": "npm:^7.17.8" - checksum: 10/0902fe2eb16aecde1587a00efee7db8081b1331ac7bcfb6e61214d266388723a84858d732ad9395028e0aecd2bb8d0c39cc03d14b4c24c22329a0e40c38141eb + checksum: 10/da71b15c1e1d98b7f55e143bbf9ebb1b0934286c74c333522e571e52f89e42a61d7d44c5b4f941dc927355c7ae09780877aeb8f23707376fa9f006ab861e758b languageName: node linkType: hard @@ -25786,31 +25736,6 @@ __metadata: languageName: node linkType: hard -"react-use@npm:17.4.0": - version: 17.4.0 - resolution: "react-use@npm:17.4.0" - dependencies: - "@types/js-cookie": "npm:^2.2.6" - "@xobotyi/scrollbar-width": "npm:^1.9.5" - copy-to-clipboard: "npm:^3.3.1" - fast-deep-equal: "npm:^3.1.3" - fast-shallow-equal: "npm:^1.0.0" - js-cookie: "npm:^2.2.1" - nano-css: "npm:^5.3.1" - react-universal-interface: "npm:^0.6.2" - resize-observer-polyfill: "npm:^1.5.1" - screenfull: "npm:^5.1.0" - set-harmonic-interval: "npm:^1.0.1" - throttle-debounce: "npm:^3.0.1" - ts-easing: "npm:^0.2.0" - tslib: "npm:^2.1.0" - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 10/98566c4817b00251107824743ea9dff41f167b548bd5f249f6eb9e2ec09388a2de1e89988e4432cead3f8aa83cf706e0255db8a20c0615768c670751973d2761 - languageName: node - linkType: hard - "react-use@npm:17.5.0, react-use@npm:^17.4.2": version: 17.5.0 resolution: "react-use@npm:17.5.0" @@ -25857,16 +25782,6 @@ __metadata: languageName: node linkType: hard -"react-virtualized-auto-sizer@npm:1.0.7": - version: 1.0.7 - resolution: "react-virtualized-auto-sizer@npm:1.0.7" - peerDependencies: - react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc - react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc - checksum: 10/a16d8ac11a0efa20d44f36a5cf3854027895b806363a409f2ddc07ff92bfc92aaf5b8eee21f3e1b153cee2273db5e5383b6334d4b530d934ed08808ecfb45b9f - languageName: node - linkType: hard - "react-window-infinite-loader@npm:1.0.9": version: 1.0.9 resolution: "react-window-infinite-loader@npm:1.0.9" @@ -26069,15 +25984,15 @@ __metadata: linkType: hard "recast@npm:^0.23.3, recast@npm:^0.23.5": - version: 0.23.9 - resolution: "recast@npm:0.23.9" + version: 0.23.7 + resolution: "recast@npm:0.23.7" dependencies: ast-types: "npm:^0.16.1" esprima: "npm:~4.0.0" source-map: "npm:~0.6.1" tiny-invariant: "npm:^1.3.3" tslib: "npm:^2.0.1" - checksum: 10/d60584be179d81a82fbe58b5bbe009aa42831ee114a21a3e3a22bda91334e0b8a1a4610dca8ecb7f9ea1426da4febc08134d3003085ad6ecee478d1808eb8796 + checksum: 10/f9f6436a8c7d2dcdaab81314a79094ad7079838988e008af8a07bf05f7aca5c13d2830f4f5b076c58633b6857260fa9f744fd24be8934de6bb8bee3961b7bd3c languageName: node linkType: hard @@ -28015,9 +27930,9 @@ __metadata: linkType: hard "stream-shift@npm:^1.0.0": - version: 1.0.3 - resolution: "stream-shift@npm:1.0.3" - checksum: 10/a24c0a3f66a8f9024bd1d579a533a53be283b4475d4e6b4b3211b964031447bdf6532dd1f3c2b0ad66752554391b7c62bd7ca4559193381f766534e723d50242 + version: 1.0.1 + resolution: "stream-shift@npm:1.0.1" + checksum: 10/59b82b44b29ec3699b5519a49b3cedcc6db58c72fb40c04e005525dfdcab1c75c4e0c180b923c380f204bed78211b9bad8faecc7b93dece4d004c3f6ec75737b languageName: node linkType: hard @@ -28225,13 +28140,6 @@ __metadata: languageName: node linkType: hard -"strip-final-newline@npm:^3.0.0": - version: 3.0.0 - resolution: "strip-final-newline@npm:3.0.0" - checksum: 10/23ee263adfa2070cd0f23d1ac14e2ed2f000c9b44229aec9c799f1367ec001478469560abefd00c5c99ee6f0b31c137d53ec6029c53e9f32a93804e18c201050 - languageName: node - linkType: hard - "strip-indent@npm:^3.0.0": version: 3.0.0 resolution: "strip-indent@npm:3.0.0" @@ -28609,7 +28517,7 @@ __metadata: languageName: node linkType: hard -"tar@npm:6.2.1, tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.2, tar@npm:^6.2.0": +"tar@npm:6.2.1, tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.13, tar@npm:^6.1.2": version: 6.2.1 resolution: "tar@npm:6.2.1" dependencies: @@ -28936,9 +28844,9 @@ __metadata: linkType: hard "tocbot@npm:^4.20.1": - version: 4.28.2 - resolution: "tocbot@npm:4.28.2" - checksum: 10/8b403d9c87b9a15d816f44ad6350325db4cc573e1dc0a50fa7921531225e83464909897589e109ad5922b60b276ac9bcea7eeae3ea774df5080ceda65eca3425 + version: 4.21.1 + resolution: "tocbot@npm:4.21.1" + checksum: 10/96ea72a9a36719595aebbfc79c6976c7f0e9a83ee8dfa2f0ce9f0a5ad744ee9a913d1d68d1e1c896e49ee771c21fe2c13b3275c26e545d100214178f0b110a49 languageName: node linkType: hard @@ -29336,9 +29244,9 @@ __metadata: linkType: hard "type-fest@npm:^4.18.2, type-fest@npm:^4.9.0": - version: 4.19.0 - resolution: "type-fest@npm:4.19.0" - checksum: 10/746294c1f463913425a4b40e9160ed1ea5cd1f877587789cbd42e01c6b67be9c55daf75e0d4787da584b98a09487df69e89b44c934d497e5d035800e115dc1e9 + version: 4.18.3 + resolution: "type-fest@npm:4.18.3" + checksum: 10/eb750920d0ef3639177f581edd6489d972c5c5827abb602a9c9662889aad148a7d558257e36c563f1beb81a2e417faec52ecec9799b28531d8335856f91e6dff languageName: node linkType: hard @@ -29492,13 +29400,6 @@ __metadata: languageName: node linkType: hard -"ufo@npm:^1.4.0": - version: 1.5.3 - resolution: "ufo@npm:1.5.3" - checksum: 10/2b30dddd873c643efecdb58cfe457183cd4d95937ccdacca6942c697b87a2c578232c25a5149fda85436696bf0fdbc213bf2b220874712bc3e58c0fb00a2c950 - languageName: node - linkType: hard - "uglify-js@npm:^3.1.4": version: 3.17.4 resolution: "uglify-js@npm:3.17.4" @@ -29728,14 +29629,14 @@ __metadata: linkType: hard "unplugin@npm:^1.3.1": - version: 1.10.1 - resolution: "unplugin@npm:1.10.1" + version: 1.5.0 + resolution: "unplugin@npm:1.5.0" dependencies: - acorn: "npm:^8.11.3" - chokidar: "npm:^3.6.0" + acorn: "npm:^8.10.0" + chokidar: "npm:^3.5.3" webpack-sources: "npm:^3.2.3" - webpack-virtual-modules: "npm:^0.6.1" - checksum: 10/d9819fad8a177c080f7f2b80744d633101935a8a6cc26b42e6a46648cccc1c5de83b7763233d56e11af53f34e6c5074816262897c9048a31e5d697bef5bb57e7 + webpack-virtual-modules: "npm:^0.5.0" + checksum: 10/4a867f2630b932c34983a4b7c741624789752677ea1a6e3e73ca68a716e7214d50d7855fb4d0e02b4a58f81aaee2def021d54365929be03a57c4b25911ae89c1 languageName: node linkType: hard @@ -30460,13 +30361,6 @@ __metadata: languageName: node linkType: hard -"webpack-virtual-modules@npm:^0.6.1": - version: 0.6.2 - resolution: "webpack-virtual-modules@npm:0.6.2" - checksum: 10/d9a0d035f7ec0c7f1055aaf88bfe48b7f96458043916a1b2926d9012fd61de3810a6b768e31a8cd4b3c84a9b6d55824361a9dd20aaf9f5ccfb6f017af216a178 - languageName: node - linkType: hard - "webpack@npm:5, webpack@npm:5.91.0, webpack@npm:^5": version: 5.91.0 resolution: "webpack@npm:5.91.0" @@ -30809,8 +30703,8 @@ __metadata: linkType: hard "ws@npm:^8.16.0, ws@npm:^8.2.3, ws@npm:^8.9.0": - version: 8.17.0 - resolution: "ws@npm:8.17.0" + version: 8.16.0 + resolution: "ws@npm:8.16.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -30819,7 +30713,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10/5e1dcb0ae70c6e2f158f5b446e0a72a2cd335b07aba73ee1872e9bae1285382286a10e53ed479db21bdd690a5dfd05641a768611ebb236253c62fefa43ef58b4 + checksum: 10/7c511c59e979bd37b63c3aea4a8e4d4163204f00bd5633c053b05ed67835481995f61a523b0ad2b603566f9a89b34cb4965cb9fab9649fbfebd8f740cea57f17 languageName: node linkType: hard @@ -30969,6 +30863,16 @@ __metadata: languageName: node linkType: hard +"yarn@npm:^1.22.22": + version: 1.22.22 + resolution: "yarn@npm:1.22.22" + bin: + yarn: bin/yarn.js + yarnpkg: bin/yarn.js + checksum: 10/98d80230beaa81f186b2256dff5ef9dce2dd6073c94299209f8e562da9018cff4275c95c27c788aaa4a9c3c186ea8a9aee9a5b83570696a4c8a9d1fff2d4da3a + languageName: node + linkType: hard + "yauzl@npm:^2.10.0": version: 2.10.0 resolution: "yauzl@npm:2.10.0" From fd44f2ee4f64270318070666d8d2ff8b00b42ecb Mon Sep 17 00:00:00 2001 From: Leonor Oliveira <9090754+leonorfmartins@users.noreply.github.com> Date: Fri, 14 Jun 2024 10:01:49 +0100 Subject: [PATCH 02/11] Init dualwriter metrics (#89003) * Pass prometheus registerer to the dual writer * Fix tests * Remove unused var * Fix tests * Uncomment test * Remove leading line * Fix tests. Reuse registerer if there's already one * Lint * Improve double registering logic * Rebase main --- pkg/apiserver/builder/common.go | 2 ++ pkg/apiserver/builder/helper.go | 4 +++- pkg/apiserver/rest/dualwriter.go | 19 ++++++++++++------- pkg/apiserver/rest/dualwriter_mode1.go | 6 ++---- pkg/apiserver/rest/dualwriter_mode1_test.go | 16 ++++++++++------ pkg/apiserver/rest/dualwriter_mode2.go | 9 +++------ pkg/apiserver/rest/dualwriter_mode2_test.go | 12 ++++++------ pkg/apiserver/rest/dualwriter_mode3.go | 6 ++---- pkg/apiserver/rest/dualwriter_mode4.go | 6 ++---- pkg/apiserver/rest/dualwriter_test.go | 4 +++- pkg/apiserver/rest/metrics.go | 10 +++++++++- pkg/cmd/grafana/apiserver/server.go | 2 +- pkg/registry/apis/dashboard/register.go | 5 ++++- .../apis/dashboardsnapshot/register.go | 3 +++ pkg/registry/apis/datasource/register.go | 3 +++ pkg/registry/apis/example/register.go | 4 +++- pkg/registry/apis/featuretoggle/register.go | 3 +++ pkg/registry/apis/folders/register.go | 5 ++++- pkg/registry/apis/peakq/register.go | 4 +++- pkg/registry/apis/playlist/register.go | 6 +++++- pkg/registry/apis/query/metrics.go | 6 +++--- pkg/registry/apis/query/register.go | 5 +++-- pkg/registry/apis/scope/register.go | 4 +++- pkg/registry/apis/service/register.go | 4 +++- .../apiserver/aggregator/aggregator.go | 11 +++++++++-- pkg/services/apiserver/service.go | 11 ++++++++--- .../apiserver/standalone/options/metrics.go | 2 +- .../apiserver/standalone/options/options.go | 2 +- 28 files changed, 114 insertions(+), 60 deletions(-) diff --git a/pkg/apiserver/builder/common.go b/pkg/apiserver/builder/common.go index d4a0d661c0e..e8741100bd5 100644 --- a/pkg/apiserver/builder/common.go +++ b/pkg/apiserver/builder/common.go @@ -4,6 +4,7 @@ import ( "net/http" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/prometheus/client_golang/prometheus" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" @@ -29,6 +30,7 @@ type APIGroupBuilder interface { codecs serializer.CodecFactory, optsGetter generic.RESTOptionsGetter, desiredMode grafanarest.DualWriterMode, + reg prometheus.Registerer, ) (*genericapiserver.APIGroupInfo, error) // Get OpenAPI definitions diff --git a/pkg/apiserver/builder/helper.go b/pkg/apiserver/builder/helper.go index ed223a7667e..1fe320aceb5 100644 --- a/pkg/apiserver/builder/helper.go +++ b/pkg/apiserver/builder/helper.go @@ -25,6 +25,7 @@ import ( "github.com/grafana/grafana/pkg/apiserver/endpoints/filters" "github.com/grafana/grafana/pkg/services/apiserver/options" + "github.com/prometheus/client_golang/prometheus" ) // TODO: this is a temporary hack to make rest.Connecter work with resource level routes @@ -128,6 +129,7 @@ func InstallAPIs( optsGetter generic.RESTOptionsGetter, builders []APIGroupBuilder, storageOpts *options.StorageOptions, + reg prometheus.Registerer, ) error { // dual writing is only enabled when the storage type is not legacy. // this is needed to support setting a default RESTOptionsGetter for new APIs that don't @@ -136,7 +138,7 @@ func InstallAPIs( for _, b := range builders { mode := b.GetDesiredDualWriterMode(dualWriteEnabled, storageOpts.DualWriterDesiredModes) - g, err := b.GetAPIGroupInfo(scheme, codecs, optsGetter, mode) + g, err := b.GetAPIGroupInfo(scheme, codecs, optsGetter, mode, reg) if err != nil { return err } diff --git a/pkg/apiserver/rest/dualwriter.go b/pkg/apiserver/rest/dualwriter.go index 9031229b999..f979c932d6b 100644 --- a/pkg/apiserver/rest/dualwriter.go +++ b/pkg/apiserver/rest/dualwriter.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/grafana/grafana/pkg/infra/kvstore" + "github.com/prometheus/client_golang/prometheus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" @@ -95,25 +96,28 @@ const ( Mode4 ) +// TODO: make this function private as there should only be one public way of setting the dual writing mode // NewDualWriter returns a new DualWriter. -func NewDualWriter(mode DualWriterMode, legacy LegacyStorage, storage Storage) DualWriter { +func NewDualWriter(mode DualWriterMode, legacy LegacyStorage, storage Storage, reg prometheus.Registerer) DualWriter { + metrics := &dualWriterMetrics{} + metrics.init(reg) switch mode { // It is not possible to initialize a mode 0 dual writer. Mode 0 represents // writing to legacy storage without `unifiedStorage` enabled. case Mode1: // read and write only from legacy storage - return newDualWriterMode1(legacy, storage) + return newDualWriterMode1(legacy, storage, metrics) case Mode2: // write to both, read from storage but use legacy as backup - return newDualWriterMode2(legacy, storage) + return newDualWriterMode2(legacy, storage, metrics) case Mode3: // write to both, read from storage only - return newDualWriterMode3(legacy, storage) + return newDualWriterMode3(legacy, storage, metrics) case Mode4: // read and write only from storage - return newDualWriterMode4(legacy, storage) + return newDualWriterMode4(legacy, storage, metrics) default: - return newDualWriterMode1(legacy, storage) + return newDualWriterMode1(legacy, storage, metrics) } } @@ -142,6 +146,7 @@ func SetDualWritingMode( storage Storage, entity string, desiredMode DualWriterMode, + reg prometheus.Registerer, ) (DualWriter, error) { toMode := map[string]DualWriterMode{ // It is not possible to initialize a mode 0 dual writer. Mode 0 represents @@ -200,5 +205,5 @@ func SetDualWritingMode( // #TODO add support for other combinations of desired and current modes - return NewDualWriter(currentMode, legacy, storage), nil + return NewDualWriter(currentMode, legacy, storage, reg), nil } diff --git a/pkg/apiserver/rest/dualwriter_mode1.go b/pkg/apiserver/rest/dualwriter_mode1.go index 15f79f5b711..03b9e226ed2 100644 --- a/pkg/apiserver/rest/dualwriter_mode1.go +++ b/pkg/apiserver/rest/dualwriter_mode1.go @@ -24,10 +24,8 @@ const mode1Str = "1" // NewDualWriterMode1 returns a new DualWriter in mode 1. // Mode 1 represents writing to and reading from LegacyStorage. -func newDualWriterMode1(legacy LegacyStorage, storage Storage) *DualWriterMode1 { - metrics := &dualWriterMetrics{} - metrics.init() - return &DualWriterMode1{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode1"), dualWriterMetrics: metrics} +func newDualWriterMode1(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics) *DualWriterMode1 { + return &DualWriterMode1{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode1"), dualWriterMetrics: dwm} } // Mode returns the mode of the dual writer. diff --git a/pkg/apiserver/rest/dualwriter_mode1_test.go b/pkg/apiserver/rest/dualwriter_mode1_test.go index d248e631373..998d683bedf 100644 --- a/pkg/apiserver/rest/dualwriter_mode1_test.go +++ b/pkg/apiserver/rest/dualwriter_mode1_test.go @@ -5,6 +5,7 @@ import ( "errors" "testing" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "k8s.io/apimachinery/pkg/api/meta" @@ -22,6 +23,8 @@ var failingObj = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta var exampleList = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, Items: []example.Pod{*exampleObj}} var anotherList = &example.PodList{Items: []example.Pod{*anotherObj}} +var p = prometheus.NewRegistry() + func TestMode1_Create(t *testing.T) { type testCase struct { input runtime.Object @@ -68,7 +71,7 @@ func TestMode1_Create(t *testing.T) { tt.setupStorageFn(m) } - dw := NewDualWriter(Mode1, ls, us) + dw := NewDualWriter(Mode1, ls, us, p) obj, err := dw.Create(context.Background(), tt.input, func(context.Context, runtime.Object) error { return nil }, &metav1.CreateOptions{}) @@ -131,7 +134,8 @@ func TestMode1_Get(t *testing.T) { tt.setupStorageFn(m, tt.input) } - dw := NewDualWriter(Mode1, ls, us) + p := prometheus.NewRegistry() + dw := NewDualWriter(Mode1, ls, us, p) obj, err := dw.Get(context.Background(), tt.input, &metav1.GetOptions{}) @@ -182,7 +186,7 @@ func TestMode1_List(t *testing.T) { tt.setupStorageFn(m) } - dw := NewDualWriter(Mode1, ls, us) + dw := NewDualWriter(Mode1, ls, us, p) _, err := dw.List(context.Background(), &metainternalversion.ListOptions{}) @@ -237,7 +241,7 @@ func TestMode1_Delete(t *testing.T) { tt.setupStorageFn(m, tt.input) } - dw := NewDualWriter(Mode1, ls, us) + dw := NewDualWriter(Mode1, ls, us, p) obj, _, err := dw.Delete(context.Background(), tt.input, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{}) @@ -296,7 +300,7 @@ func TestMode1_DeleteCollection(t *testing.T) { tt.setupStorageFn(m, tt.input) } - dw := NewDualWriter(Mode1, ls, us) + dw := NewDualWriter(Mode1, ls, us, p) obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, tt.input, &metainternalversion.ListOptions{}) @@ -372,7 +376,7 @@ func TestMode1_Update(t *testing.T) { tt.setupGetFn(m, tt.input) } - dw := NewDualWriter(Mode1, ls, us) + dw := NewDualWriter(Mode1, ls, us, p) obj, _, err := dw.Update(context.Background(), tt.input, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) diff --git a/pkg/apiserver/rest/dualwriter_mode2.go b/pkg/apiserver/rest/dualwriter_mode2.go index 8ff4a653cc9..36051dd7eaa 100644 --- a/pkg/apiserver/rest/dualwriter_mode2.go +++ b/pkg/apiserver/rest/dualwriter_mode2.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/grafana/grafana/pkg/apimachinery/utils" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" @@ -13,8 +14,6 @@ import ( "k8s.io/apimachinery/pkg/selection" "k8s.io/apiserver/pkg/registry/rest" "k8s.io/klog/v2" - - "github.com/grafana/grafana/pkg/apimachinery/utils" ) type DualWriterMode2 struct { @@ -28,10 +27,8 @@ const mode2Str = "2" // NewDualWriterMode2 returns a new DualWriter in mode 2. // Mode 2 represents writing to LegacyStorage and Storage and reading from LegacyStorage. -func newDualWriterMode2(legacy LegacyStorage, storage Storage) *DualWriterMode2 { - metrics := &dualWriterMetrics{} - metrics.init() - return &DualWriterMode2{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode2"), dualWriterMetrics: metrics} +func newDualWriterMode2(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics) *DualWriterMode2 { + return &DualWriterMode2{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode2"), dualWriterMetrics: dwm} } // Mode returns the mode of the dual writer. diff --git a/pkg/apiserver/rest/dualwriter_mode2_test.go b/pkg/apiserver/rest/dualwriter_mode2_test.go index ef028fbafa1..6b0a10e1537 100644 --- a/pkg/apiserver/rest/dualwriter_mode2_test.go +++ b/pkg/apiserver/rest/dualwriter_mode2_test.go @@ -67,7 +67,7 @@ func TestMode2_Create(t *testing.T) { tt.setupStorageFn(m, tt.input) } - dw := NewDualWriter(Mode2, ls, us) + dw := NewDualWriter(Mode2, ls, us, p) obj, err := dw.Create(context.Background(), tt.input, createFn, &metav1.CreateOptions{}) @@ -143,7 +143,7 @@ func TestMode2_Get(t *testing.T) { tt.setupStorageFn(m, tt.input) } - dw := NewDualWriter(Mode2, ls, us) + dw := NewDualWriter(Mode2, ls, us, p) obj, err := dw.Get(context.Background(), tt.input, &metav1.GetOptions{}) @@ -196,7 +196,7 @@ func TestMode2_List(t *testing.T) { tt.setupStorageFn(m) } - dw := NewDualWriter(Mode2, ls, us) + dw := NewDualWriter(Mode2, ls, us, p) obj, err := dw.List(context.Background(), &metainternalversion.ListOptions{}) @@ -289,7 +289,7 @@ func TestMode2_Delete(t *testing.T) { tt.setupStorageFn(m, tt.input) } - dw := NewDualWriter(Mode2, ls, us) + dw := NewDualWriter(Mode2, ls, us, p) obj, _, err := dw.Delete(context.Background(), tt.input, func(context.Context, runtime.Object) error { return nil }, &metav1.DeleteOptions{}) @@ -361,7 +361,7 @@ func TestMode2_DeleteCollection(t *testing.T) { tt.setupStorageFn(m) } - dw := NewDualWriter(Mode2, ls, us) + dw := NewDualWriter(Mode2, ls, us, p) obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: tt.input}}, &metainternalversion.ListOptions{}) @@ -469,7 +469,7 @@ func TestMode2_Update(t *testing.T) { tt.setupStorageFn(m, tt.input) } - dw := NewDualWriter(Mode2, ls, us) + dw := NewDualWriter(Mode2, ls, us, p) obj, _, err := dw.Update(context.Background(), tt.input, updatedObjInfoObj{}, func(ctx context.Context, obj runtime.Object) error { return nil }, func(ctx context.Context, obj, old runtime.Object) error { return nil }, false, &metav1.UpdateOptions{}) diff --git a/pkg/apiserver/rest/dualwriter_mode3.go b/pkg/apiserver/rest/dualwriter_mode3.go index 50f21fd39ba..f24d58627ae 100644 --- a/pkg/apiserver/rest/dualwriter_mode3.go +++ b/pkg/apiserver/rest/dualwriter_mode3.go @@ -20,10 +20,8 @@ type DualWriterMode3 struct { // newDualWriterMode3 returns a new DualWriter in mode 3. // Mode 3 represents writing to LegacyStorage and Storage and reading from Storage. -func newDualWriterMode3(legacy LegacyStorage, storage Storage) *DualWriterMode3 { - metrics := &dualWriterMetrics{} - metrics.init() - return &DualWriterMode3{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode3"), dualWriterMetrics: metrics} +func newDualWriterMode3(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics) *DualWriterMode3 { + return &DualWriterMode3{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode3"), dualWriterMetrics: dwm} } // Mode returns the mode of the dual writer. diff --git a/pkg/apiserver/rest/dualwriter_mode4.go b/pkg/apiserver/rest/dualwriter_mode4.go index de15ee8d5c7..7985e69332e 100644 --- a/pkg/apiserver/rest/dualwriter_mode4.go +++ b/pkg/apiserver/rest/dualwriter_mode4.go @@ -19,10 +19,8 @@ type DualWriterMode4 struct { // newDualWriterMode4 returns a new DualWriter in mode 4. // Mode 4 represents writing and reading from Storage. -func newDualWriterMode4(legacy LegacyStorage, storage Storage) *DualWriterMode4 { - metrics := &dualWriterMetrics{} - metrics.init() - return &DualWriterMode4{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode4"), dualWriterMetrics: metrics} +func newDualWriterMode4(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics) *DualWriterMode4 { + return &DualWriterMode4{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode4"), dualWriterMetrics: dwm} } // Mode returns the mode of the dual writer. diff --git a/pkg/apiserver/rest/dualwriter_test.go b/pkg/apiserver/rest/dualwriter_test.go index 10cb16644d5..6c9a8da9619 100644 --- a/pkg/apiserver/rest/dualwriter_test.go +++ b/pkg/apiserver/rest/dualwriter_test.go @@ -7,6 +7,7 @@ import ( playlist "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" "github.com/grafana/grafana/pkg/infra/kvstore" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -45,7 +46,8 @@ func TestSetDualWritingMode(t *testing.T) { kvStore := kvstore.WithNamespace(kvstore.NewFakeKVStore(), 0, "storage.dualwriting."+tt.stackID) - dw, err := SetDualWritingMode(context.Background(), kvStore, ls, us, playlist.GROUPRESOURCE, tt.desiredMode) + p := prometheus.NewRegistry() + dw, err := SetDualWritingMode(context.Background(), kvStore, ls, us, playlist.GROUPRESOURCE, tt.desiredMode, p) assert.NoError(t, err) assert.Equal(t, tt.expectedMode, dw.Mode()) diff --git a/pkg/apiserver/rest/metrics.go b/pkg/apiserver/rest/metrics.go index a4e72457a8f..4e5db41e0e2 100644 --- a/pkg/apiserver/rest/metrics.go +++ b/pkg/apiserver/rest/metrics.go @@ -5,6 +5,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + "k8s.io/klog/v2" ) type dualWriterMetrics struct { @@ -37,10 +38,17 @@ var DualWriterOutcome = prometheus.NewHistogramVec(prometheus.HistogramOpts{ NativeHistogramBucketFactor: 1.1, }, []string{"mode", "name", "method"}) -func (m *dualWriterMetrics) init() { +func (m *dualWriterMetrics) init(reg prometheus.Registerer) { + log := klog.NewKlogr() m.legacy = DualWriterLegacyDuration m.storage = DualWriterStorageDuration m.outcome = DualWriterOutcome + errLegacy := reg.Register(m.legacy) + errStorage := reg.Register(m.storage) + errOutcome := reg.Register(m.outcome) + if errLegacy != nil || errStorage != nil || errOutcome != nil { + log.Info("cloud migration metrics already registered") + } } func (m *dualWriterMetrics) recordLegacyDuration(isError bool, mode string, name string, method string, startFrom time.Time) { diff --git a/pkg/cmd/grafana/apiserver/server.go b/pkg/cmd/grafana/apiserver/server.go index 8278868f451..0963337bea4 100644 --- a/pkg/cmd/grafana/apiserver/server.go +++ b/pkg/cmd/grafana/apiserver/server.go @@ -165,7 +165,7 @@ func (o *APIServerOptions) RunAPIServer(config *genericapiserver.RecommendedConf // Install the API Group+version // #TODO figure out how to configure storage type in o.Options.StorageOptions - err = builder.InstallAPIs(grafanaAPIServer.Scheme, grafanaAPIServer.Codecs, server, config.RESTOptionsGetter, o.builders, o.Options.StorageOptions) + err = builder.InstallAPIs(grafanaAPIServer.Scheme, grafanaAPIServer.Codecs, server, config.RESTOptionsGetter, o.builders, o.Options.StorageOptions, o.Options.MetricsOptions.MetricsRegisterer) if err != nil { return err } diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index 731939ce1bf..fe60abed4fb 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -25,6 +25,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/setting" + "github.com/prometheus/client_golang/prometheus" ) var _ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil) @@ -49,6 +50,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, accessControl accesscontrol.AccessControl, provisioning provisioning.ProvisioningService, dashStore dashboards.Store, + reg prometheus.Registerer, sql db.DB, ) *DashboardsAPIBuilder { if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { @@ -115,6 +117,7 @@ func (b *DashboardsAPIBuilder) GetAPIGroupInfo( codecs serializer.CodecFactory, // pointer? optsGetter generic.RESTOptionsGetter, desiredMode grafanarest.DualWriterMode, + reg prometheus.Registerer, ) (*genericapiserver.APIGroupInfo, error) { apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(v0alpha1.GROUP, scheme, metav1.ParameterCodec, codecs) @@ -145,7 +148,7 @@ func (b *DashboardsAPIBuilder) GetAPIGroupInfo( if err := store.CompleteWithOptions(options); err != nil { return nil, err } - storage[resourceInfo.StoragePath()] = grafanarest.NewDualWriter(grafanarest.Mode1, legacyStore, store) + storage[resourceInfo.StoragePath()] = grafanarest.NewDualWriter(grafanarest.Mode1, legacyStore, store, reg) } // Summary diff --git a/pkg/registry/apis/dashboardsnapshot/register.go b/pkg/registry/apis/dashboardsnapshot/register.go index 7d884889ad1..67db0abfa70 100644 --- a/pkg/registry/apis/dashboardsnapshot/register.go +++ b/pkg/registry/apis/dashboardsnapshot/register.go @@ -8,6 +8,7 @@ import ( "time" "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -71,6 +72,7 @@ func RegisterAPIService( cfg *setting.Cfg, features featuremgmt.FeatureToggles, sql db.DB, + reg prometheus.Registerer, ) *SnapshotsAPIBuilder { if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { return nil // skip registration unless opting into experimental apis @@ -129,6 +131,7 @@ func (b *SnapshotsAPIBuilder) GetAPIGroupInfo( codecs serializer.CodecFactory, // pointer? optsGetter generic.RESTOptionsGetter, _ grafanarest.DualWriterMode, + _ prometheus.Registerer, ) (*genericapiserver.APIGroupInfo, error) { apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(dashboardsnapshot.GROUP, scheme, metav1.ParameterCodec, codecs) storage := map[string]rest.Storage{} diff --git a/pkg/registry/apis/datasource/register.go b/pkg/registry/apis/datasource/register.go index c1eab887823..93370ed49d7 100644 --- a/pkg/registry/apis/datasource/register.go +++ b/pkg/registry/apis/datasource/register.go @@ -7,6 +7,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/prometheus/client_golang/prometheus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -56,6 +57,7 @@ func RegisterAPIService( contextProvider PluginContextWrapper, pluginStore pluginstore.Store, accessControl accesscontrol.AccessControl, + reg prometheus.Registerer, ) (*DataSourceAPIBuilder, error) { // This requires devmode! if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { @@ -205,6 +207,7 @@ func (b *DataSourceAPIBuilder) GetAPIGroupInfo( codecs serializer.CodecFactory, // pointer? _ generic.RESTOptionsGetter, _ grafanarest.DualWriterMode, + _ prometheus.Registerer, ) (*genericapiserver.APIGroupInfo, error) { storage := map[string]rest.Storage{} diff --git a/pkg/registry/apis/example/register.go b/pkg/registry/apis/example/register.go index 9b7da8ff1ef..edb2a90bbad 100644 --- a/pkg/registry/apis/example/register.go +++ b/pkg/registry/apis/example/register.go @@ -25,6 +25,7 @@ import ( grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/prometheus/client_golang/prometheus" ) var _ builder.APIGroupBuilder = (*TestingAPIBuilder)(nil) @@ -41,7 +42,7 @@ func NewTestingAPIBuilder() *TestingAPIBuilder { } } -func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar) *TestingAPIBuilder { +func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, reg prometheus.Registerer) *TestingAPIBuilder { if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { return nil // skip registration unless opting into experimental apis } @@ -92,6 +93,7 @@ func (b *TestingAPIBuilder) GetAPIGroupInfo( codecs serializer.CodecFactory, // pointer? _ generic.RESTOptionsGetter, _ grafanarest.DualWriterMode, + _ prometheus.Registerer, ) (*genericapiserver.APIGroupInfo, error) { b.codecs = codecs apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(b.gv.Group, scheme, metav1.ParameterCodec, codecs) diff --git a/pkg/registry/apis/featuretoggle/register.go b/pkg/registry/apis/featuretoggle/register.go index 72d135d451c..0173057954a 100644 --- a/pkg/registry/apis/featuretoggle/register.go +++ b/pkg/registry/apis/featuretoggle/register.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" + "github.com/prometheus/client_golang/prometheus" ) var _ builder.APIGroupBuilder = (*FeatureFlagAPIBuilder)(nil) @@ -40,6 +41,7 @@ func RegisterAPIService(features *featuremgmt.FeatureManager, accessControl accesscontrol.AccessControl, apiregistration builder.APIRegistrar, cfg *setting.Cfg, + registerer prometheus.Registerer, ) *FeatureFlagAPIBuilder { builder := NewFeatureFlagAPIBuilder(features, accessControl, cfg) apiregistration.RegisterAPI(builder) @@ -89,6 +91,7 @@ func (b *FeatureFlagAPIBuilder) GetAPIGroupInfo( codecs serializer.CodecFactory, // pointer? _ generic.RESTOptionsGetter, _ grafanarest.DualWriterMode, + _ prometheus.Registerer, ) (*genericapiserver.APIGroupInfo, error) { apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(v0alpha1.GROUP, scheme, metav1.ParameterCodec, codecs) diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index 62eafeb7290..d658c68db49 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/setting" + "github.com/prometheus/client_golang/prometheus" ) var _ builder.APIGroupBuilder = (*FolderAPIBuilder)(nil) @@ -47,6 +48,7 @@ func RegisterAPIService(cfg *setting.Cfg, apiregistration builder.APIRegistrar, folderSvc folder.Service, accessControl accesscontrol.AccessControl, + registerer prometheus.Registerer, ) *FolderAPIBuilder { if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { return nil // skip registration unless opting into experimental apis @@ -106,6 +108,7 @@ func (b *FolderAPIBuilder) GetAPIGroupInfo( codecs serializer.CodecFactory, // pointer? optsGetter generic.RESTOptionsGetter, desiredMode grafanarest.DualWriterMode, + reg prometheus.Registerer, ) (*genericapiserver.APIGroupInfo, error) { apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(v0alpha1.GROUP, scheme, metav1.ParameterCodec, codecs) @@ -145,7 +148,7 @@ func (b *FolderAPIBuilder) GetAPIGroupInfo( if err != nil { return nil, err } - storage[resourceInfo.StoragePath()] = grafanarest.NewDualWriter(grafanarest.Mode1, legacyStore, store) + storage[resourceInfo.StoragePath()] = grafanarest.NewDualWriter(grafanarest.Mode1, legacyStore, store, reg) } apiGroupInfo.VersionedResourcesStorageMap[v0alpha1.VERSION] = storage diff --git a/pkg/registry/apis/peakq/register.go b/pkg/registry/apis/peakq/register.go index 718c9746edf..21e1aa072d9 100644 --- a/pkg/registry/apis/peakq/register.go +++ b/pkg/registry/apis/peakq/register.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/apiserver/builder" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/prometheus/client_golang/prometheus" ) var _ builder.APIGroupBuilder = (*PeakQAPIBuilder)(nil) @@ -28,7 +29,7 @@ func NewPeakQAPIBuilder() *PeakQAPIBuilder { return &PeakQAPIBuilder{} } -func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar) *PeakQAPIBuilder { +func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, reg prometheus.Registerer) *PeakQAPIBuilder { if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { return nil // skip registration unless opting into experimental apis } @@ -73,6 +74,7 @@ func (b *PeakQAPIBuilder) GetAPIGroupInfo( codecs serializer.CodecFactory, optsGetter generic.RESTOptionsGetter, _ grafanarest.DualWriterMode, // dual write desired mode (not relevant) + _ prometheus.Registerer, // prometheus registerer ) (*genericapiserver.APIGroupInfo, error) { apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(peakq.GROUP, scheme, metav1.ParameterCodec, codecs) diff --git a/pkg/registry/apis/playlist/register.go b/pkg/registry/apis/playlist/register.go index b9902946a6c..1c3666777ca 100644 --- a/pkg/registry/apis/playlist/register.go +++ b/pkg/registry/apis/playlist/register.go @@ -23,6 +23,7 @@ import ( gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" playlistsvc "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/setting" + "github.com/prometheus/client_golang/prometheus" ) var _ builder.APIGroupBuilder = (*PlaylistAPIBuilder)(nil) @@ -39,12 +40,14 @@ func RegisterAPIService(p playlistsvc.Service, apiregistration builder.APIRegistrar, cfg *setting.Cfg, kvStore kvstore.KVStore, + registerer prometheus.Registerer, ) *PlaylistAPIBuilder { builder := &PlaylistAPIBuilder{ service: p, namespacer: request.GetNamespaceMapper(cfg), gv: playlist.PlaylistResourceInfo.GroupVersion(), kvStore: kvstore.WithNamespace(kvStore, 0, "storage.dualwriting"), + // register: newMetrics(registerer), } apiregistration.RegisterAPI(builder) return builder @@ -94,6 +97,7 @@ func (b *PlaylistAPIBuilder) GetAPIGroupInfo( codecs serializer.CodecFactory, // pointer? optsGetter generic.RESTOptionsGetter, desiredMode grafanarest.DualWriterMode, + reg prometheus.Registerer, ) (*genericapiserver.APIGroupInfo, error) { apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(playlist.GROUP, scheme, metav1.ParameterCodec, codecs) storage := map[string]rest.Storage{} @@ -133,7 +137,7 @@ func (b *PlaylistAPIBuilder) GetAPIGroupInfo( return nil, err } - dualWriter, err := grafanarest.SetDualWritingMode(context.Background(), b.kvStore, legacyStore, store, playlist.GROUPRESOURCE, desiredMode) + dualWriter, err := grafanarest.SetDualWritingMode(context.Background(), b.kvStore, legacyStore, store, playlist.GROUPRESOURCE, desiredMode, reg) if err != nil { return nil, err } diff --git a/pkg/registry/apis/query/metrics.go b/pkg/registry/apis/query/metrics.go index e13525917e2..f3b8212f9db 100644 --- a/pkg/registry/apis/query/metrics.go +++ b/pkg/registry/apis/query/metrics.go @@ -9,15 +9,15 @@ const ( metricsNamespace = "grafana" ) -type metrics struct { +type queryMetrics struct { dsRequests *prometheus.CounterVec // older metric expressionsQuerySummary *prometheus.SummaryVec } -func newMetrics(reg prometheus.Registerer) *metrics { - m := &metrics{ +func newQueryMetrics(reg prometheus.Registerer) *queryMetrics { + m := &queryMetrics{ dsRequests: prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: metricsNamespace, Subsystem: metricsSubSystem, diff --git a/pkg/registry/apis/query/register.go b/pkg/registry/apis/query/register.go index 897f045c102..9ae0b235cc0 100644 --- a/pkg/registry/apis/query/register.go +++ b/pkg/registry/apis/query/register.go @@ -42,7 +42,7 @@ type QueryAPIBuilder struct { features featuremgmt.FeatureToggles tracer tracing.Tracer - metrics *metrics + metrics *queryMetrics parser *queryParser client DataSourceClientSupplier registry query.DataSourceApiServerRegistry @@ -81,7 +81,7 @@ func NewQueryAPIBuilder(features featuremgmt.FeatureToggles, client: client, registry: registry, parser: newQueryParser(reader, legacy, tracer), - metrics: newMetrics(registerer), + metrics: newQueryMetrics(registerer), tracer: tracer, features: features, queryTypes: queryTypes, @@ -151,6 +151,7 @@ func (b *QueryAPIBuilder) GetAPIGroupInfo( codecs serializer.CodecFactory, // pointer? optsGetter generic.RESTOptionsGetter, _ grafanarest.DualWriterMode, + _ prometheus.Registerer, ) (*genericapiserver.APIGroupInfo, error) { gv := query.SchemeGroupVersion apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(gv.Group, scheme, metav1.ParameterCodec, codecs) diff --git a/pkg/registry/apis/scope/register.go b/pkg/registry/apis/scope/register.go index 91215ce7a7e..f86839f8591 100644 --- a/pkg/registry/apis/scope/register.go +++ b/pkg/registry/apis/scope/register.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/apiserver/builder" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/prometheus/client_golang/prometheus" ) var _ builder.APIGroupBuilder = (*ScopeAPIBuilder)(nil) @@ -30,7 +31,7 @@ func NewScopeAPIBuilder() *ScopeAPIBuilder { return &ScopeAPIBuilder{} } -func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar) *ScopeAPIBuilder { +func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, reg prometheus.Registerer) *ScopeAPIBuilder { if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { return nil // skip registration unless opting into experimental apis } @@ -121,6 +122,7 @@ func (b *ScopeAPIBuilder) GetAPIGroupInfo( codecs serializer.CodecFactory, optsGetter generic.RESTOptionsGetter, _ grafanarest.DualWriterMode, // dual write desired mode (not relevant) + _ prometheus.Registerer, // prometheus registerer ) (*genericapiserver.APIGroupInfo, error) { apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(scope.GROUP, scheme, metav1.ParameterCodec, codecs) diff --git a/pkg/registry/apis/service/register.go b/pkg/registry/apis/service/register.go index 69b01a2707d..6635233d1fd 100644 --- a/pkg/registry/apis/service/register.go +++ b/pkg/registry/apis/service/register.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/apiserver/builder" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/prometheus/client_golang/prometheus" ) var _ builder.APIGroupBuilder = (*ServiceAPIBuilder)(nil) @@ -26,7 +27,7 @@ func NewServiceAPIBuilder() *ServiceAPIBuilder { return &ServiceAPIBuilder{} } -func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar) *ServiceAPIBuilder { +func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, registerer prometheus.Registerer) *ServiceAPIBuilder { if !features.IsEnabledGlobally(featuremgmt.FlagKubernetesAggregator) { return nil // skip registration unless opting into aggregator mode } @@ -79,6 +80,7 @@ func (b *ServiceAPIBuilder) GetAPIGroupInfo( codecs serializer.CodecFactory, optsGetter generic.RESTOptionsGetter, _ grafanarest.DualWriterMode, + _ prometheus.Registerer, ) (*genericapiserver.APIGroupInfo, error) { apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(service.GROUP, scheme, metav1.ParameterCodec, codecs) diff --git a/pkg/services/apiserver/aggregator/aggregator.go b/pkg/services/apiserver/aggregator/aggregator.go index 7107b2d77f5..8c97479c17b 100644 --- a/pkg/services/apiserver/aggregator/aggregator.go +++ b/pkg/services/apiserver/aggregator/aggregator.go @@ -24,6 +24,7 @@ import ( servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/service" + "github.com/prometheus/client_golang/prometheus" "gopkg.in/yaml.v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -172,7 +173,7 @@ func CreateAggregatorConfig(commandOptions *options.Options, sharedConfig generi return NewConfig(aggregatorConfig, sharedInformerFactory, []builder.APIGroupBuilder{serviceAPIBuilder}, remoteServicesConfig), nil } -func CreateAggregatorServer(config *Config, delegateAPIServer genericapiserver.DelegationTarget) (*aggregatorapiserver.APIAggregator, error) { +func CreateAggregatorServer(config *Config, delegateAPIServer genericapiserver.DelegationTarget, reg prometheus.Registerer) (*aggregatorapiserver.APIAggregator, error) { aggregatorConfig := config.KubeAggregatorConfig sharedInformerFactory := config.Informers remoteServicesConfig := config.RemoteServicesConfig @@ -285,7 +286,13 @@ func CreateAggregatorServer(config *Config, delegateAPIServer genericapiserver.D }) for _, b := range config.Builders { - serviceAPIGroupInfo, err := b.GetAPIGroupInfo(aggregatorscheme.Scheme, aggregatorscheme.Codecs, aggregatorConfig.GenericConfig.RESTOptionsGetter, grafanarest.Mode0) + serviceAPIGroupInfo, err := b.GetAPIGroupInfo( + aggregatorscheme.Scheme, + aggregatorscheme.Codecs, + aggregatorConfig.GenericConfig.RESTOptionsGetter, + grafanarest.Mode0, + reg, + ) if err != nil { return nil, err } diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 2a61456d206..339b52bfd05 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -7,6 +7,7 @@ import ( "path" "github.com/grafana/dskit/services" + "github.com/prometheus/client_golang/prometheus" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,6 +25,7 @@ import ( filestorage "github.com/grafana/grafana/pkg/apiserver/storage/file" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/modules" @@ -109,6 +111,7 @@ type service struct { builders []builder.APIGroupBuilder tracing *tracing.TracingService + metrics prometheus.Registerer authorizer *authorizer.GrafanaAuthorizer } @@ -131,6 +134,7 @@ func ProvideService( authorizer: authorizer.NewGrafanaAuthorizer(cfg, orgService), tracing: tracing, db: db, // For Unified storage + metrics: metrics.ProvideRegisterer(), } // This will be used when running as a dskit service @@ -314,7 +318,7 @@ func (s *service) start(ctx context.Context) error { } // Install the API group+version - err = builder.InstallAPIs(Scheme, Codecs, server, serverConfig.RESTOptionsGetter, builders, o.StorageOptions) + err = builder.InstallAPIs(Scheme, Codecs, server, serverConfig.RESTOptionsGetter, builders, o.StorageOptions, s.metrics) if err != nil { return err } @@ -324,7 +328,7 @@ func (s *service) start(ctx context.Context) error { var runningServer *genericapiserver.GenericAPIServer if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAggregator) { - runningServer, err = s.startAggregator(transport, serverConfig, server) + runningServer, err = s.startAggregator(transport, serverConfig, server, s.metrics) if err != nil { return err } @@ -374,6 +378,7 @@ func (s *service) startAggregator( transport *roundTripperFunc, serverConfig *genericapiserver.RecommendedConfig, server *genericapiserver.GenericAPIServer, + reg prometheus.Registerer, ) (*genericapiserver.GenericAPIServer, error) { namespaceMapper := request.GetNamespaceMapper(s.cfg) @@ -382,7 +387,7 @@ func (s *service) startAggregator( return nil, err } - aggregatorServer, err := aggregator.CreateAggregatorServer(aggregatorConfig, server) + aggregatorServer, err := aggregator.CreateAggregatorServer(aggregatorConfig, server, reg) if err != nil { return nil, err } diff --git a/pkg/services/apiserver/standalone/options/metrics.go b/pkg/services/apiserver/standalone/options/metrics.go index 4a213bd4606..11f6bdc9391 100644 --- a/pkg/services/apiserver/standalone/options/metrics.go +++ b/pkg/services/apiserver/standalone/options/metrics.go @@ -15,7 +15,7 @@ type MetricsOptions struct { MetricsRegisterer prometheus.Registerer } -func NewMetrcicsOptions(logger log.Logger) *MetricsOptions { +func NewMetricsOptions(logger log.Logger) *MetricsOptions { return &MetricsOptions{ logger: logger, } diff --git a/pkg/services/apiserver/standalone/options/options.go b/pkg/services/apiserver/standalone/options/options.go index 76085fc1558..7d1f25efdcd 100644 --- a/pkg/services/apiserver/standalone/options/options.go +++ b/pkg/services/apiserver/standalone/options/options.go @@ -25,7 +25,7 @@ func New(logger log.Logger, codec runtime.Codec) *Options { ExtraOptions: options.NewExtraOptions(), RecommendedOptions: options.NewRecommendedOptions(codec), TracingOptions: NewTracingOptions(logger), - MetricsOptions: NewMetrcicsOptions(logger), + MetricsOptions: NewMetricsOptions(logger), ServerRunOptions: genericoptions.NewServerRunOptions(), StorageOptions: options.NewStorageOptions(), } From 063082acbc9cedd261361708fb6d6492cf98541d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Jun 2024 08:52:04 +0000 Subject: [PATCH 03/11] Update dependency @grafana/scenes to v5.0.3 --- yarn.lock | 41 +++++------------------------------------ 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/yarn.lock b/yarn.lock index 53a7df821f0..25b3244e9e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -700,18 +700,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.24.1": - version: 7.24.1 - resolution: "@babel/plugin-syntax-typescript@npm:7.24.1" - dependencies: - "@babel/helper-plugin-utils": "npm:^7.24.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 10/bf4bd70788d5456b5f75572e47a2e31435c7c4e43609bd4dffd2cc0c7a6cf90aabcf6cd389e351854de9a64412a07d30effef5373251fe8f6a4c9db0c0163bda - languageName: node - linkType: hard - -"@babel/plugin-syntax-typescript@npm:^7.7.2": +"@babel/plugin-syntax-typescript@npm:^7.24.1, @babel/plugin-syntax-typescript@npm:^7.7.2": version: 7.24.7 resolution: "@babel/plugin-syntax-typescript@npm:7.24.7" dependencies: @@ -3536,8 +3525,8 @@ __metadata: linkType: soft "@grafana/scenes@npm:^5.0.2": - version: 5.0.2 - resolution: "@grafana/scenes@npm:5.0.2" + version: 5.0.3 + resolution: "@grafana/scenes@npm:5.0.3" dependencies: "@grafana/e2e-selectors": "npm:^11.0.0" "@leeoniya/ufuzzy": "npm:^1.0.14" @@ -3552,7 +3541,7 @@ __metadata: "@grafana/ui": ^10.4.1 react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/a7bb83c5d0aca6e47ce8fb50ff5952da1dc1ea1b209148cbc7795bc480606df09012ff13a309510fcb4fa75e94af26700aa53bb0fadeb77d8215d23288b46b47 + checksum: 10/d99e88ba26f6df34fa595656be20cdaee8e9e59a8928e80691bea5cd9646ef6534c9929f38a5fcca1b09891a2609d4ee8e96f898799c5ec73b1993abe09b094a languageName: node linkType: hard @@ -5699,7 +5688,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-portal@npm:1.0.4": +"@radix-ui/react-portal@npm:1.0.4, @radix-ui/react-portal@npm:^1.0.1": version: 1.0.4 resolution: "@radix-ui/react-portal@npm:1.0.4" dependencies: @@ -5719,26 +5708,6 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-portal@npm:^1.0.1": - version: 1.0.3 - resolution: "@radix-ui/react-portal@npm:1.0.3" - dependencies: - "@babel/runtime": "npm:^7.13.10" - "@radix-ui/react-primitive": "npm:1.0.3" - peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - peerDependenciesMeta: - "@types/react": - optional: true - "@types/react-dom": - optional: true - checksum: 10/d352bcd6ad65eb43c9e0d72d0755c2aae85e03fb287770866262be3a2d5302b2885aee3cd99f2bbf62ecd14fcb1460703f1dcdc40351f77ad887b931c6f0012a - languageName: node - linkType: hard - "@radix-ui/react-presence@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-presence@npm:1.0.1" From 84590901e5716cd5bd381102b87ff477c1e83ee2 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 14 Jun 2024 10:46:44 +0100 Subject: [PATCH 04/11] Chore: Migrate some more deprecated SCSS (#89160) * migrate pull-right/pull-left * remove fa-rotate and fa-flip * remove _stacked * remove font-awesome _list * remove font-awesome _larger * remove fa fixed-width --- .betterer.results | 39 ++--- public/app/features/admin/UserOrgs.tsx | 158 +++++++++--------- public/app/features/admin/UserSessions.tsx | 20 +-- .../admin/ldap/LdapConnectionStatus.tsx | 2 +- .../sharing/ShareSnapshotTab.tsx | 8 +- .../components/ShareModal/ShareSnapshot.tsx | 8 +- public/app/features/storage/RootView.tsx | 17 +- public/app/features/teams/TeamGroupSync.tsx | 2 +- public/sass/_angular.scss | 14 ++ public/sass/base/_font_awesome.scss | 6 - .../base/font-awesome/_bordered-pulled.scss | 41 ----- .../sass/base/font-awesome/_fixed-width.scss | 6 - public/sass/base/font-awesome/_larger.scss | 21 --- public/sass/base/font-awesome/_list.scss | 21 --- .../base/font-awesome/_rotated-flipped.scss | 30 ---- public/sass/base/font-awesome/_stacked.scss | 27 --- 16 files changed, 128 insertions(+), 292 deletions(-) delete mode 100644 public/sass/base/font-awesome/_bordered-pulled.scss delete mode 100644 public/sass/base/font-awesome/_fixed-width.scss delete mode 100644 public/sass/base/font-awesome/_larger.scss delete mode 100644 public/sass/base/font-awesome/_list.scss delete mode 100644 public/sass/base/font-awesome/_rotated-flipped.scss delete mode 100644 public/sass/base/font-awesome/_stacked.scss diff --git a/.betterer.results b/.betterer.results index 2cdb78262b7..0266d833b36 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1490,33 +1490,16 @@ exports[`better eslint`] = { "public/app/features/admin/UserOrgs.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"], - [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"], - [0, 0, 0, "Styles should be written using objects.", "7"], - [0, 0, 0, "Styles should be written using objects.", "8"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], - [0, 0, 0, "Styles should be written using objects.", "10"], - [0, 0, 0, "Styles should be written using objects.", "11"], - [0, 0, 0, "Styles should be written using objects.", "12"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "13"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "14"], - [0, 0, 0, "Styles should be written using objects.", "15"], - [0, 0, 0, "Styles should be written using objects.", "16"], - [0, 0, 0, "Styles should be written using objects.", "17"], - [0, 0, 0, "Styles should be written using objects.", "18"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "19"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "20"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "21"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "22"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "23"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "24"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "25"], - [0, 0, 0, "Styles should be written using objects.", "26"], - [0, 0, 0, "Styles should be written using objects.", "27"], - [0, 0, 0, "Styles should be written using objects.", "28"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "10"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "11"] ], "public/app/features/admin/UserPermissions.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], @@ -5417,9 +5400,7 @@ exports[`better eslint`] = { ], "public/app/features/storage/RootView.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/features/storage/StorageFolderPage.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] diff --git a/public/app/features/admin/UserOrgs.tsx b/public/app/features/admin/UserOrgs.tsx index 644d42d7eb7..2cfa498ebfd 100644 --- a/public/app/features/admin/UserOrgs.tsx +++ b/public/app/features/admin/UserOrgs.tsx @@ -99,30 +99,30 @@ export class UserOrgs extends PureComponent { const getOrgRowStyles = stylesFactory((theme: GrafanaTheme2) => { return { - removeButton: css` - margin-right: 0.6rem; - text-decoration: underline; - color: ${theme.v1.palette.blue95}; - `, - label: css` - font-weight: 500; - `, - disabledTooltip: css` - display: flex; - `, - tooltipItem: css` - margin-left: 5px; - `, - tooltipItemLink: css` - color: ${theme.v1.palette.blue95}; - `, - rolePickerWrapper: css` - display: flex; - `, - rolePicker: css` - flex: auto; - margin-right: ${theme.spacing(1)}; - `, + removeButton: css({ + marginRight: '0.6rem', + textDecoration: 'underline', + color: theme.v1.palette.blue95, + }), + label: css({ + fontWeight: 500, + }), + disabledTooltip: css({ + display: 'flex', + }), + tooltipItem: css({ + marginLeft: '5px', + }), + tooltipItemLink: css({ + color: theme.v1.palette.blue95, + }), + rolePickerWrapper: css({ + display: 'flex', + }), + rolePicker: css({ + flex: 'auto', + marginRight: theme.spacing(1), + }), }; }); @@ -222,33 +222,29 @@ class UnThemedOrgRow extends PureComponent { {org.role} )} -
- {canChangeRole && ( - - )} -
+ {canChangeRole && ( + + )} )} -
- {canRemoveFromOrg && ( - - Remove from organization - - )} -
+ {canRemoveFromOrg && ( + + Remove from organization + + )} ); @@ -258,15 +254,15 @@ class UnThemedOrgRow extends PureComponent { const OrgRow = withTheme2(UnThemedOrgRow); const getAddToOrgModalStyles = stylesFactory(() => ({ - modal: css` - width: 500px; - `, - buttonRow: css` - text-align: center; - `, - modalContent: css` - overflow: visible; - `, + modal: css({ + width: '500px', + }), + buttonRow: css({ + textAlign: 'center', + }), + modalContent: css({ + overflow: 'visible', + }), })); interface AddToOrgModalProps { @@ -408,20 +404,20 @@ interface ChangeOrgButtonProps { } const getChangeOrgButtonTheme = (theme: GrafanaTheme2) => ({ - disabledTooltip: css` - display: flex; - `, - tooltipItemLink: css` - color: ${theme.v1.palette.blue95}; - `, - lockMessageClass: css` - font-style: italic; - margin-left: 1.8rem; - margin-right: 0.6rem; - `, - icon: css` - line-height: 2; - `, + disabledTooltip: css({ + display: 'flex', + }), + tooltipItemLink: css({ + color: theme.v1.palette.blue95, + }), + lockMessageClass: css({ + fontStyle: 'italic', + marginLeft: '1.8rem', + marginRight: '0.6rem', + }), + icon: css({ + lineHeight: 2, + }), }); export function ChangeOrgButton({ @@ -511,15 +507,15 @@ export const ExternalUserTooltip = ({ lockMessage }: ExternalUserTooltipProps) = }; const getTooltipStyles = (theme: GrafanaTheme2) => ({ - disabledTooltip: css` - display: flex; - `, - tooltipItemLink: css` - color: ${theme.v1.palette.blue95}; - `, - lockMessageClass: css` - font-style: italic; - margin-left: 1.8rem; - margin-right: 0.6rem; - `, + disabledTooltip: css({ + display: 'flex', + }), + tooltipItemLink: css({ + color: theme.v1.palette.blue95, + }), + lockMessageClass: css({ + fontStyle: 'italic', + marginLeft: '1.8rem', + marginRight: '0.6rem', + }), }); diff --git a/public/app/features/admin/UserSessions.tsx b/public/app/features/admin/UserSessions.tsx index 1129dc7201f..465072ba34d 100644 --- a/public/app/features/admin/UserSessions.tsx +++ b/public/app/features/admin/UserSessions.tsx @@ -72,17 +72,15 @@ class BaseUserSessions extends PureComponent { {session.clientIp} {`${session.browser} on ${session.os} ${session.osVersion}`} -
- {canLogout && ( - - Force logout - - )} -
+ {canLogout && ( + + Force logout + + )} ))} diff --git a/public/app/features/admin/ldap/LdapConnectionStatus.tsx b/public/app/features/admin/ldap/LdapConnectionStatus.tsx index bf4fc842fc8..2616a50436f 100644 --- a/public/app/features/admin/ldap/LdapConnectionStatus.tsx +++ b/public/app/features/admin/ldap/LdapConnectionStatus.tsx @@ -32,7 +32,7 @@ export const LdapConnectionStatus = ({ ldapConnectionInfo }: Props) => { return serverInfo.cell.value ? ( - + ) : ( diff --git a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx index a039db09064..5331b22a93b 100644 --- a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx @@ -5,7 +5,7 @@ import { SelectableValue } from '@grafana/data'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { getBackendSrv } from '@grafana/runtime'; import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes'; -import { Button, ClipboardButton, Field, Input, Modal, RadioButtonGroup } from '@grafana/ui'; +import { Button, ClipboardButton, Field, Input, Modal, RadioButtonGroup, Stack } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; import { getDashboardSnapshotSrv, SnapshotSharingOptions } from 'app/features/dashboard/services/SnapshotSrv'; @@ -231,7 +231,7 @@ function ShareSnapshoTabRenderer({ model }: SceneComponentProps + -
+
Did you make a mistake?  
- + )} ); diff --git a/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx b/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx index e3309e2547e..9ef4546c674 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import { isEmptyObject, SelectableValue, VariableRefresh } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; -import { Button, ClipboardButton, Field, Input, LinkButton, Modal, Select, Spinner } from '@grafana/ui'; +import { Button, ClipboardButton, Field, Input, LinkButton, Modal, Select, Spinner, Stack } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; @@ -290,7 +290,7 @@ export class ShareSnapshot extends PureComponent { const { snapshotUrl } = this.state; return ( - <> + { /> -
+
Did you make a mistake?   Delete snapshot.
- + ); } diff --git a/public/app/features/storage/RootView.tsx b/public/app/features/storage/RootView.tsx index c6f855a7024..332f2e503da 100644 --- a/public/app/features/storage/RootView.tsx +++ b/public/app/features/storage/RootView.tsx @@ -83,9 +83,8 @@ export function RootView({ root, onPathChange }: Props) { - +
+
{renderRoots('', roots.base)}
@@ -100,12 +99,12 @@ export function RootView({ root, onPathChange }: Props) { function getStyles(theme: GrafanaTheme2) { return { - secondaryTextColor: css` - color: ${theme.colors.text.secondary}; - `, - clickable: css` - pointer-events: none; - `, + secondaryTextColor: css({ + color: theme.colors.text.secondary, + }), + clickable: css({ + pointerEvents: 'none', + }), }; } diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index 5dd0e5f8e8d..3d8b2bbca30 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -121,7 +121,7 @@ export class TeamGroupSync extends PureComponent { )}
{groups.length > 0 && ( - )} diff --git a/public/sass/_angular.scss b/public/sass/_angular.scss index d5b8abf0bcd..dbe49f7b96d 100644 --- a/public/sass/_angular.scss +++ b/public/sass/_angular.scss @@ -1832,3 +1832,17 @@ $easing: cubic-bezier(0, 0, 0.265, 1); } } } + +.pull-right { + float: right; +} +.pull-left { + float: left; +} + +/* makes the font 33% larger relative to the icon container */ +.#{$fa-css-prefix}-lg { + font-size: calc(4em / 3); + line-height: calc(3em / 4); + vertical-align: -15%; +} diff --git a/public/sass/base/_font_awesome.scss b/public/sass/base/_font_awesome.scss index c3521338143..59bf17154fa 100644 --- a/public/sass/base/_font_awesome.scss +++ b/public/sass/base/_font_awesome.scss @@ -7,12 +7,6 @@ @import './font-awesome/mixins'; @import './font-awesome/path'; @import './font-awesome/core'; -@import './font-awesome/larger'; -@import './font-awesome/fixed-width'; -@import './font-awesome/list'; -@import './font-awesome/bordered-pulled'; @import './font-awesome/animated'; -@import './font-awesome/rotated-flipped'; -@import './font-awesome/stacked'; @import './font-awesome/icons'; @import './font-awesome/screen-reader'; diff --git a/public/sass/base/font-awesome/_bordered-pulled.scss b/public/sass/base/font-awesome/_bordered-pulled.scss deleted file mode 100644 index c9b6b38ea6f..00000000000 --- a/public/sass/base/font-awesome/_bordered-pulled.scss +++ /dev/null @@ -1,41 +0,0 @@ -// Bordered & Pulled -// ------------------------- - -.#{$fa-css-prefix}-border { - padding: 0.2em 0.25em 0.15em; - border: solid 0.08em $fa-border-color; - border-radius: 0.1em; -} - -.#{$fa-css-prefix}-pull-left { - float: left; -} -.#{$fa-css-prefix}-pull-right { - float: right; -} - -.#{$fa-css-prefix} { - &.#{$fa-css-prefix}-pull-left { - margin-right: 0.3em; - } - &.#{$fa-css-prefix}-pull-right { - margin-left: 0.3em; - } -} - -/* Deprecated as of 4.4.0 */ -.pull-right { - float: right; -} -.pull-left { - float: left; -} - -.#{$fa-css-prefix} { - &.pull-left { - margin-right: 0.3em; - } - &.pull-right { - margin-left: 0.3em; - } -} diff --git a/public/sass/base/font-awesome/_fixed-width.scss b/public/sass/base/font-awesome/_fixed-width.scss deleted file mode 100644 index 7102bbac0ca..00000000000 --- a/public/sass/base/font-awesome/_fixed-width.scss +++ /dev/null @@ -1,6 +0,0 @@ -// Fixed Width Icons -// ------------------------- -.#{$fa-css-prefix}-fw { - width: calc(18em / 14); - text-align: center; -} diff --git a/public/sass/base/font-awesome/_larger.scss b/public/sass/base/font-awesome/_larger.scss deleted file mode 100644 index 9e47f7720bb..00000000000 --- a/public/sass/base/font-awesome/_larger.scss +++ /dev/null @@ -1,21 +0,0 @@ -// Icon Sizes -// ------------------------- - -/* makes the font 33% larger relative to the icon container */ -.#{$fa-css-prefix}-lg { - font-size: calc(4em / 3); - line-height: calc(3em / 4); - vertical-align: -15%; -} -.#{$fa-css-prefix}-2x { - font-size: 2em !important; -} -.#{$fa-css-prefix}-3x { - font-size: 3em; -} -.#{$fa-css-prefix}-4x { - font-size: 4em; -} -.#{$fa-css-prefix}-5x { - font-size: 5em; -} diff --git a/public/sass/base/font-awesome/_list.scss b/public/sass/base/font-awesome/_list.scss deleted file mode 100644 index 728bc3fe678..00000000000 --- a/public/sass/base/font-awesome/_list.scss +++ /dev/null @@ -1,21 +0,0 @@ -// List Icons -// ------------------------- - -.#{$fa-css-prefix}-ul { - padding-left: 0; - margin-left: $fa-li-width; - list-style-type: none; - > li { - position: relative; - } -} -.#{$fa-css-prefix}-li { - position: absolute; - left: -$fa-li-width; - width: $fa-li-width; - top: calc(2em / 14); - text-align: center; - &.#{$fa-css-prefix}-lg { - left: -$fa-li-width + calc(4em / 14); - } -} diff --git a/public/sass/base/font-awesome/_rotated-flipped.scss b/public/sass/base/font-awesome/_rotated-flipped.scss deleted file mode 100644 index 07c11a8b299..00000000000 --- a/public/sass/base/font-awesome/_rotated-flipped.scss +++ /dev/null @@ -1,30 +0,0 @@ -// Rotated & Flipped Icons -// ------------------------- - -.#{$fa-css-prefix}-rotate-90 { - @include fa-icon-rotate(90deg, 1); -} -.#{$fa-css-prefix}-rotate-180 { - @include fa-icon-rotate(180deg, 2); -} -.#{$fa-css-prefix}-rotate-270 { - @include fa-icon-rotate(270deg, 3); -} - -.#{$fa-css-prefix}-flip-horizontal { - @include fa-icon-flip(-1, 1, 0); -} -.#{$fa-css-prefix}-flip-vertical { - @include fa-icon-flip(1, -1, 2); -} - -// Hook for IE8-9 -// ------------------------- - -:root .#{$fa-css-prefix}-rotate-90, -:root .#{$fa-css-prefix}-rotate-180, -:root .#{$fa-css-prefix}-rotate-270, -:root .#{$fa-css-prefix}-flip-horizontal, -:root .#{$fa-css-prefix}-flip-vertical { - filter: none; -} diff --git a/public/sass/base/font-awesome/_stacked.scss b/public/sass/base/font-awesome/_stacked.scss deleted file mode 100644 index 33fbe76979c..00000000000 --- a/public/sass/base/font-awesome/_stacked.scss +++ /dev/null @@ -1,27 +0,0 @@ -// Stacked Icons -// ------------------------- - -.#{$fa-css-prefix}-stack { - position: relative; - display: inline-block; - width: 2em; - height: 2em; - line-height: 2em; - vertical-align: middle; -} -.#{$fa-css-prefix}-stack-1x, -.#{$fa-css-prefix}-stack-2x { - position: absolute; - left: 0; - width: 100%; - text-align: center; -} -.#{$fa-css-prefix}-stack-1x { - line-height: inherit; -} -.#{$fa-css-prefix}-stack-2x { - font-size: 2em; -} -.#{$fa-css-prefix}-inverse { - color: $fa-inverse; -} From 26233e98a8f6d5de37ca2958257964b95884863f Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Fri, 14 Jun 2024 10:56:25 +0100 Subject: [PATCH 05/11] Alerting: Fix saving telegram contact point to Cloud AM config (#89182) * Add test for saving telegram contact point to AM config * Fix default value for select options in AM config --- .../alerting/unified/Receivers.test.tsx | 74 +++++++++++++++++++ .../features/alerting/unified/Receivers.tsx | 3 +- .../receivers/form/fields/OptionField.tsx | 2 +- .../components/settings/__mocks__/server.ts | 38 ++++++++-- 4 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 public/app/features/alerting/unified/Receivers.test.tsx diff --git a/public/app/features/alerting/unified/Receivers.test.tsx b/public/app/features/alerting/unified/Receivers.test.tsx new file mode 100644 index 00000000000..ac966805830 --- /dev/null +++ b/public/app/features/alerting/unified/Receivers.test.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; +import { render, screen, waitFor, userEvent } from 'test/test-utils'; + +import { + EXTERNAL_VANILLA_ALERTMANAGER_UID, + setupVanillaAlertmanagerServer, +} from 'app/features/alerting/unified/components/settings/__mocks__/server'; +import { setupMswServer } from 'app/features/alerting/unified/mockApi'; +import { grantUserPermissions, mockDataSource } from 'app/features/alerting/unified/mocks'; +import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; +import { DataSourceType } from 'app/features/alerting/unified/utils/datasource'; +import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; +import { AccessControlAction } from 'app/types'; + +import ContactPoints from './Receivers'; + +import 'core-js/stable/structured-clone'; + +const server = setupMswServer(); + +const mockDataSources = { + [EXTERNAL_VANILLA_ALERTMANAGER_UID]: mockDataSource({ + uid: EXTERNAL_VANILLA_ALERTMANAGER_UID, + name: EXTERNAL_VANILLA_ALERTMANAGER_UID, + type: DataSourceType.Alertmanager, + jsonData: { + implementation: AlertManagerImplementation.prometheus, + }, + }), +}; + +beforeEach(() => { + grantUserPermissions([ + AccessControlAction.AlertingNotificationsRead, + AccessControlAction.AlertingNotificationsWrite, + AccessControlAction.AlertingNotificationsExternalRead, + AccessControlAction.AlertingNotificationsExternalWrite, + ]); +}); + +it('can save a contact point with a select dropdown', async () => { + setupVanillaAlertmanagerServer(server); + setupDataSources(mockDataSources[EXTERNAL_VANILLA_ALERTMANAGER_UID]); + + const user = userEvent.setup(); + + render(, { + historyOptions: { + initialEntries: [`/alerting/notifications/receivers/new?alertmanager=${EXTERNAL_VANILLA_ALERTMANAGER_UID}`], + }, + }); + + // Fill out contact point name + const contactPointName = await screen.findByPlaceholderText(/name/i); + await user.type(contactPointName, 'contact point with select'); + + // Select Telegram option (this is we expect the form to contain a dropdown) + const integrationDropdown = screen.getByLabelText(/integration/i); + await selectOptionInTest(integrationDropdown, /telegram/i); + + // Fill out basic fields necessary for contact point to be saved + const botToken = await screen.findByLabelText(/bot token/i); + const chatId = await screen.findByLabelText(/chat id/i); + + await user.type(botToken, 'sometoken'); + await user.type(chatId, '-123'); + + await user.click(await screen.findByRole('button', { name: /save contact point/i })); + + // TODO: Have a better way to assert that the contact point was saved. This is instead asserting on some + // text that's present on the list page, as there's a lot of overlap in text between the form and the list page + await waitFor(() => expect(screen.getByText(/search by name or type/i)).toBeInTheDocument(), { timeout: 2000 }); +}); diff --git a/public/app/features/alerting/unified/Receivers.tsx b/public/app/features/alerting/unified/Receivers.tsx index fc37703fba1..367a1f21376 100644 --- a/public/app/features/alerting/unified/Receivers.tsx +++ b/public/app/features/alerting/unified/Receivers.tsx @@ -3,7 +3,6 @@ import { Route, Switch } from 'react-router-dom'; import { withErrorBoundary } from '@grafana/ui'; import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper'; @@ -12,7 +11,7 @@ const EditContactPoint = SafeDynamicImport(() => import('./components/contact-po const NewContactPoint = SafeDynamicImport(() => import('./components/contact-points/NewContactPoint')); const GlobalConfig = SafeDynamicImport(() => import('./components/contact-points/components/GlobalConfig')); -const ContactPoints = (_props: GrafanaRouteComponentProps): JSX.Element => ( +const ContactPoints = (): JSX.Element => ( diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx index 05bc1c00a5d..705ddc79eae 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx @@ -161,7 +161,7 @@ const OptionInput: FC = ({ )} control={control} name={name} - defaultValue={option.defaultValue} + defaultValue={option.defaultValue?.value} rules={{ validate: { customValidator: (v) => (customValidator ? customValidator(v) : true), diff --git a/public/app/features/alerting/unified/components/settings/__mocks__/server.ts b/public/app/features/alerting/unified/components/settings/__mocks__/server.ts index 428b6f54de6..41ead147008 100644 --- a/public/app/features/alerting/unified/components/settings/__mocks__/server.ts +++ b/public/app/features/alerting/unified/components/settings/__mocks__/server.ts @@ -2,7 +2,13 @@ import { delay, http, HttpResponse } from 'msw'; import { SetupServerApi } from 'msw/lib/node'; import { setDataSourceSrv } from '@grafana/runtime'; -import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; +import { + AlertManagerCortexConfig, + AlertManagerDataSourceJsonData, + AlertManagerImplementation, + AlertmanagerReceiver, + Receiver, +} from 'app/plugins/datasource/alertmanager/types'; import { mockDataSource, MockDataSourceSrv } from '../../../mocks'; import * as config from '../../../utils/config'; @@ -70,7 +76,7 @@ export function setupVanillaAlertmanagerServer(server: SetupServerApi) { server.use( createVanillaAlertmanagerConfigurationHandler(EXTERNAL_VANILLA_ALERTMANAGER_UID), - ...createAlertmanagerConfigurationHandlers(PROVISIONED_MIMIR_ALERTMANAGER_UID) + ...createAlertmanagerConfigurationHandlers() ); return server; @@ -82,11 +88,33 @@ const createExternalAlertmanagersHandler = () => { return http.get('/api/v1/ngalert/alertmanagers', () => HttpResponse.json(alertmanagers)); }; -const createAlertmanagerConfigurationHandlers = (name = 'grafana') => { +const createAlertmanagerConfigurationHandlers = () => { + // Dirty check to type guard against us having a non-Grafana managed receiver + const contactPointIsAMReceiver = (receiver: Receiver): receiver is AlertmanagerReceiver => { + return !receiver.grafana_managed_receiver_configs; + }; + return [ - http.get(`/api/alertmanager/${name}/config/api/v1/alerts`, () => HttpResponse.json(internalAlertmanagerConfig)), - http.post(`/api/alertmanager/${name}/config/api/v1/alerts`, async () => { + http.get(`/api/alertmanager/:name/config/api/v1/alerts`, () => HttpResponse.json(internalAlertmanagerConfig)), + http.post(`/api/alertmanager/:name/config/api/v1/alerts`, async ({ request }) => { await delay(1000); // simulate some time + + // Specifically mock and check for the case of an invalid telegram config, + // and return a 400 error in this case + // This is to test against us accidentally sending a `{label, value}` object instead of a string + const body = await request.json(); + const invalidConfig = body.alertmanager_config.receivers?.some((receiver) => { + if (!contactPointIsAMReceiver(receiver)) { + return false; + } + + return (receiver.telegram_configs || []).some((config) => typeof config.parse_mode === 'object'); + }); + + if (invalidConfig) { + return HttpResponse.json({ message: 'bad request data' }, { status: 400 }); + } + return HttpResponse.json({ message: 'configuration created' }); }), ]; From 7c5c01cb6c43c87487232357311f6b9aa9abe0a4 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 14 Jun 2024 11:35:32 +0100 Subject: [PATCH 06/11] Chore: Tidy up more font-awesome SCSS (#89207) * migrate pull-right/pull-left * remove fa-rotate and fa-flip * remove _stacked * remove font-awesome _list * remove font-awesome _larger * remove fa fixed-width * remove fa animated styles * move sr-only to global styles * condense remaining font-awesome stuff into 1 file * extract out spin animation into util folder --- .../src/components/Button/Button.test.tsx | 11 - .../grafana-ui/src/components/Icon/Icon.tsx | 10 +- .../src/components/Spinner/Spinner.tsx | 16 +- .../src/themes/GlobalStyles/GlobalStyles.tsx | 2 + .../src/themes/GlobalStyles/accessibility.ts | 18 + packages/grafana-ui/src/utils/keyframes.ts | 10 + public/sass/base/_font_awesome.scss | 2848 ++++++++++++++++- public/sass/base/font-awesome/_animated.scss | 34 - public/sass/base/font-awesome/_core.scss | 11 - public/sass/base/font-awesome/_icons.scss | 2139 ------------- public/sass/base/font-awesome/_mixins.scss | 58 - public/sass/base/font-awesome/_path.scss | 16 - .../base/font-awesome/_screen-reader.scss | 9 - public/sass/base/font-awesome/_variables.scss | 799 ----- 14 files changed, 2888 insertions(+), 3093 deletions(-) delete mode 100644 packages/grafana-ui/src/components/Button/Button.test.tsx create mode 100644 packages/grafana-ui/src/themes/GlobalStyles/accessibility.ts create mode 100644 packages/grafana-ui/src/utils/keyframes.ts delete mode 100644 public/sass/base/font-awesome/_animated.scss delete mode 100644 public/sass/base/font-awesome/_core.scss delete mode 100644 public/sass/base/font-awesome/_icons.scss delete mode 100644 public/sass/base/font-awesome/_mixins.scss delete mode 100644 public/sass/base/font-awesome/_path.scss delete mode 100644 public/sass/base/font-awesome/_screen-reader.scss delete mode 100644 public/sass/base/font-awesome/_variables.scss diff --git a/packages/grafana-ui/src/components/Button/Button.test.tsx b/packages/grafana-ui/src/components/Button/Button.test.tsx deleted file mode 100644 index 68d4e1e55c4..00000000000 --- a/packages/grafana-ui/src/components/Button/Button.test.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { render } from '@testing-library/react'; -import React from 'react'; - -import { Button } from './Button'; - -describe('Button', () => { - it('spins the spinner when specified as an icon', () => { - const { container } = render(); - expect(container.querySelector('.fa-spin')).toBeInTheDocument(); - }); -}); diff --git a/packages/grafana-ui/src/components/Icon/Icon.tsx b/packages/grafana-ui/src/components/Icon/Icon.tsx index d33ed422b4d..d4ade1c4796 100644 --- a/packages/grafana-ui/src/components/Icon/Icon.tsx +++ b/packages/grafana-ui/src/components/Icon/Icon.tsx @@ -6,6 +6,7 @@ import { GrafanaTheme2, isIconName } from '@grafana/data'; import { useStyles2 } from '../../themes/ThemeContext'; import { IconName, IconType, IconSize } from '../../types/icon'; +import { spin } from '../../utils/keyframes'; import { getIconRoot, getIconSubDir, getSvgSize } from './utils'; @@ -33,6 +34,11 @@ const getIconStyles = (theme: GrafanaTheme2) => { orange: css({ fill: theme.v1.palette.orange, }), + spin: css({ + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + animation: `${spin} 2s infinite linear`, + }, + }), }; }; @@ -58,7 +64,9 @@ export const Icon = React.forwardRef( styles.icon, className, type === 'mono' ? { [styles.orange]: name === 'favorite' } : '', - iconName === 'spinner' && 'fa-spin' + { + [styles.spin]: iconName === 'spinner', + } ); return ( diff --git a/packages/grafana-ui/src/components/Spinner/Spinner.tsx b/packages/grafana-ui/src/components/Spinner/Spinner.tsx index a4d45805f29..013f7332a2a 100644 --- a/packages/grafana-ui/src/components/Spinner/Spinner.tsx +++ b/packages/grafana-ui/src/components/Spinner/Spinner.tsx @@ -7,6 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes'; import { IconSize, isIconSize } from '../../types'; import { t } from '../../utils/i18n'; +import { spin } from '../../utils/keyframes'; import { Icon } from '../Icon/Icon'; import { getIconRoot, getIconSubDir } from '../Icon/utils'; @@ -46,7 +47,6 @@ export const Spinner = ({ // TODO remove once we fully remove the deprecated type if (typeof size !== 'string' || !isIconSize(size)) { const iconRoot = getIconRoot(); - const iconName = 'spinner'; const subDir = getIconSubDir(iconName, 'default'); const svgPath = `${iconRoot}${subDir}/${iconName}.svg`; return ( @@ -65,7 +65,7 @@ export const Spinner = ({ src={svgPath} width={size} height={size} - className={cx('fa-spin', deprecatedStyles.icon, className)} + className={cx(styles.spin, deprecatedStyles.icon, className)} style={style} />
@@ -84,12 +84,7 @@ export const Spinner = ({ )} > ({ inline: css({ display: 'inline-block', }), + spin: css({ + [theme.transitions.handleMotion('no-preference')]: { + animation: `${spin} 2s infinite linear`, + }, + }), }); // TODO remove once we fully remove the deprecated type diff --git a/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx b/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx index 377318481c3..f8cddcb4de8 100644 --- a/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx +++ b/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { useTheme2 } from '../ThemeContext'; +import { getAccessibilityStyles } from './accessibility'; import { getAgularPanelStyles } from './angularPanelStyles'; import { getCardStyles } from './card'; import { getCodeStyles } from './code'; @@ -24,6 +25,7 @@ export function GlobalStyles() { return ( Date: Fri, 14 Jun 2024 12:13:49 +0100 Subject: [PATCH 07/11] For now, only compare if names are the same in playlists (#89206) --- pkg/registry/apis/playlist/storage.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/registry/apis/playlist/storage.go b/pkg/registry/apis/playlist/storage.go index 0b68612e7b3..d3cb8e10ce8 100644 --- a/pkg/registry/apis/playlist/storage.go +++ b/pkg/registry/apis/playlist/storage.go @@ -1,6 +1,7 @@ package playlist import ( + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/generic" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" @@ -41,6 +42,14 @@ func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, le // Compare asserts on the equality of objects returned from both stores (object storage and legacy storage) func (s *storage) Compare(storageObj, legacyObj runtime.Object) bool { - //TODO: define the comparison logic between a playlist returned by the storage and a playlist returned by the legacy storage - return false + accStr, err := meta.Accessor(storageObj) + if err != nil { + return false + } + accLegacy, err := meta.Accessor(legacyObj) + if err != nil { + return false + } + + return accStr.GetName() == accLegacy.GetName() } From 1691d80412f23402f6ad882d1b26bd233c85db5b Mon Sep 17 00:00:00 2001 From: Leonor Oliveira <9090754+leonorfmartins@users.noreply.github.com> Date: Fri, 14 Jun 2024 12:13:59 +0100 Subject: [PATCH 08/11] Refactor enrichObj function (#89210) --- pkg/apiserver/rest/dualwriter_mode1.go | 8 +++----- pkg/apiserver/rest/dualwriter_mode2.go | 17 ++++++++--------- pkg/apiserver/rest/dualwriter_mode2_test.go | 4 ++-- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/pkg/apiserver/rest/dualwriter_mode1.go b/pkg/apiserver/rest/dualwriter_mode1.go index 03b9e226ed2..b008f0bff82 100644 --- a/pkg/apiserver/rest/dualwriter_mode1.go +++ b/pkg/apiserver/rest/dualwriter_mode1.go @@ -54,13 +54,12 @@ func (d *DualWriterMode1) Create(ctx context.Context, original runtime.Object, c ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("storage create timeout")) defer cancel() - objStorage, errEnrichObj := enrichLegacyObject(original, createdCopy, true) - if errEnrichObj != nil { + if err := enrichLegacyObject(original, createdCopy, true); err != nil { cancel() } startStorage := time.Now() - _, errObjectSt := d.Storage.Create(ctx, objStorage, createValidation, options) + _, errObjectSt := d.Storage.Create(ctx, createdCopy, createValidation, options) d.recordStorageDuration(errObjectSt != nil, mode1Str, options.Kind, method, startStorage) }() @@ -202,8 +201,7 @@ func (d *DualWriterMode1) Update(ctx context.Context, name string, objInfo rest. // if the object is found, create a new updateWrapper with the object found if foundObj != nil { - resCopy, err := enrichLegacyObject(foundObj, resCopy, false) - if err != nil { + if err := enrichLegacyObject(foundObj, resCopy, false); err != nil { log.Error(err, "could not enrich object") cancel() } diff --git a/pkg/apiserver/rest/dualwriter_mode2.go b/pkg/apiserver/rest/dualwriter_mode2.go index 36051dd7eaa..12e4703d829 100644 --- a/pkg/apiserver/rest/dualwriter_mode2.go +++ b/pkg/apiserver/rest/dualwriter_mode2.go @@ -51,9 +51,8 @@ func (d *DualWriterMode2) Create(ctx context.Context, original runtime.Object, c } d.recordLegacyDuration(false, mode2Str, options.Kind, method, startLegacy) - createdLegacy, err := enrichLegacyObject(original, created, true) - if err != nil { - return createdLegacy, err + if err := enrichLegacyObject(original, created, true); err != nil { + return created, err } startStorage := time.Now() @@ -263,7 +262,7 @@ func (d *DualWriterMode2) Update(ctx context.Context, name string, objInfo rest. // if the object is found, create a new updateWrapper with the object found if foundObj != nil { - obj, err = enrichLegacyObject(foundObj, obj, false) + err = enrichLegacyObject(foundObj, obj, false) if err != nil { return obj, false, err } @@ -344,15 +343,15 @@ func parseList(legacyList []runtime.Object) (metainternalversion.ListOptions, ma return options, indexMap, nil } -func enrichLegacyObject(originalObj, returnedObj runtime.Object, created bool) (runtime.Object, error) { +func enrichLegacyObject(originalObj, returnedObj runtime.Object, created bool) error { accessorReturned, err := meta.Accessor(returnedObj) if err != nil { - return nil, err + return err } accessorOriginal, err := meta.Accessor(originalObj) if err != nil { - return nil, err + return err } accessorReturned.SetLabels(accessorOriginal.GetLabels()) @@ -371,10 +370,10 @@ func enrichLegacyObject(originalObj, returnedObj runtime.Object, created bool) ( if created { accessorReturned.SetResourceVersion("") accessorReturned.SetUID("") - return returnedObj, nil + return nil } // otherwise, we propagate the original RV and UID accessorReturned.SetResourceVersion(accessorOriginal.GetResourceVersion()) accessorReturned.SetUID(accessorOriginal.GetUID()) - return returnedObj, nil + return nil } diff --git a/pkg/apiserver/rest/dualwriter_mode2_test.go b/pkg/apiserver/rest/dualwriter_mode2_test.go index 6b0a10e1537..5a3d3969638 100644 --- a/pkg/apiserver/rest/dualwriter_mode2_test.go +++ b/pkg/apiserver/rest/dualwriter_mode2_test.go @@ -658,13 +658,13 @@ func TestEnrichReturnedObject(t *testing.T) { for _, tt := range testCase { t.Run(tt.name, func(t *testing.T) { - returned, err := enrichLegacyObject(tt.inputOriginal, tt.inputReturned, tt.isCreated) + err := enrichLegacyObject(tt.inputOriginal, tt.inputReturned, tt.isCreated) if tt.wantErr { assert.Error(t, err) return } - accessorReturned, err := meta.Accessor(returned) + accessorReturned, err := meta.Accessor(tt.inputReturned) assert.NoError(t, err) accessorExpected, err := meta.Accessor(tt.expectedObject) From a9171aa9fe8a55ee3ad4107b2fc2aef5c477175e Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Fri, 14 Jun 2024 12:41:01 +0100 Subject: [PATCH 09/11] Alerting: Fix display of falsy alert values in state history (#89156) --- public/app/features/alerting/unified/components/Label.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/public/app/features/alerting/unified/components/Label.tsx b/public/app/features/alerting/unified/components/Label.tsx index 3d1b4d11d39..bef7c86d408 100644 --- a/public/app/features/alerting/unified/components/Label.tsx +++ b/public/app/features/alerting/unified/components/Label.tsx @@ -33,11 +33,9 @@ const Label = ({ label, value, icon, color, size = 'md' }: Props) => { )}
- {value && ( -
- {value} -
- )} +
+ {value ?? '-'} +
); From 72241dbf5f26664cb8c30e35b2e59baabb11319f Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Fri, 14 Jun 2024 13:41:17 +0200 Subject: [PATCH 10/11] Plugin extensions: Add e2e tests (#89048) * add custom plugins * update bundles * provision app plugins and their dashboards * add one more script that run e2e tests using e2e test server * add e2e tests * regenerate jsonnet dashboards * ignore custom plugins and playwright report * use minified * cleanup tests * update codeowners * add leading slash * document new script * document custom-plugins * cleanup * twist modules * add readme --- .eslintignore | 2 + .github/CODEOWNERS | 1 + contribute/developer-guide.md | 14 +- contribute/style-guides/e2e-plugins.md | 2 + .../extensions/link-onclick-extensions.json | 342 ++++++++++++++++++ .../extensions/link-path-extensions.json | 237 ++++++++++++ devenv/jsonnet/dev-dashboards.libsonnet | 2 + devenv/plugins.yaml | 19 + e2e/custom-plugins/README.md | 5 + .../app-with-extension-point/README.md | 12 + .../app-with-extension-point/module.js | 141 ++++++++ .../app-with-extension-point/plugin.json | 38 ++ .../plugins/myorg-a-app/module.js | 26 ++ .../plugins/myorg-a-app/plugin.json | 45 +++ .../plugins/myorg-b-app/module.js | 27 ++ .../plugins/myorg-b-app/plugin.json | 45 +++ .../app-with-extensions/README.md | 12 + .../app-with-extensions/module.js | 216 +++++++++++ .../app-with-extensions/plugin.json | 49 +++ .../as-admin-user/extensionPoints.spec.ts | 35 ++ .../as-admin-user/extensions.spec.ts | 38 ++ e2e/plugin-e2e/start-and-run-suite | 23 ++ package.json | 3 +- scripts/grafana-server/start-server | 2 + 24 files changed, 1324 insertions(+), 12 deletions(-) create mode 100644 devenv/dev-dashboards/extensions/link-onclick-extensions.json create mode 100644 devenv/dev-dashboards/extensions/link-path-extensions.json create mode 100644 devenv/plugins.yaml create mode 100644 e2e/custom-plugins/README.md create mode 100644 e2e/custom-plugins/app-with-extension-point/README.md create mode 100644 e2e/custom-plugins/app-with-extension-point/module.js create mode 100644 e2e/custom-plugins/app-with-extension-point/plugin.json create mode 100644 e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/module.js create mode 100644 e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/plugin.json create mode 100644 e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/module.js create mode 100644 e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/plugin.json create mode 100644 e2e/custom-plugins/app-with-extensions/README.md create mode 100644 e2e/custom-plugins/app-with-extensions/module.js create mode 100644 e2e/custom-plugins/app-with-extensions/plugin.json create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensionPoints.spec.ts create mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions.spec.ts create mode 100755 e2e/plugin-e2e/start-and-run-suite diff --git a/.eslintignore b/.eslintignore index 01e0c6ec342..95ace0ec535 100644 --- a/.eslintignore +++ b/.eslintignore @@ -13,6 +13,8 @@ node_modules /public/lib/monaco /scripts/grafana-server/tmp vendor +e2e/custom-plugins +playwright-report # TS generate from cue by cuetsy **/*.gen.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4310fea2fbb..e87f3dc4109 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -223,6 +223,7 @@ /devenv/local-npm/ @grafana/frontend-ops /devenv/vscode/ @grafana/frontend-ops /devenv/setup.sh @grafana/grafana-backend-services-squad +/devenv/plugins.yaml @grafana/plugins-platform-frontend # Emails /emails/ @grafana/alerting-frontend diff --git a/contribute/developer-guide.md b/contribute/developer-guide.md index 2607b91f1a9..bcdb979b790 100644 --- a/contribute/developer-guide.md +++ b/contribute/developer-guide.md @@ -235,7 +235,7 @@ yarn e2e:dev #### To run the Playwright tests: -**Note:** If you're using VS Code as your development editor, it's recommended to install the [Playwright test extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). It allows you to run, debug and generate Playwright tests from within the editor. For more information about the extension and how to install it, refer to the [Playwright documentation](https://playwright.dev/docs/getting-started-vscode). +**Note:** If you're using VS Code as your development editor, it's recommended to install the [Playwright test extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). It allows you to run, debug and generate Playwright tests from within the editor. For more information about the extension and how to use reports to analyze failing tests, refer to the [Playwright documentation](https://playwright.dev/docs/getting-started-vscode). Each version of Playwright needs specific versions of browser binaries to operate. You need to use the Playwright CLI to install these browsers. @@ -243,22 +243,16 @@ Each version of Playwright needs specific versions of browser binaries to operat yarn playwright install chromium ``` -To run all tests in a headless Chromium browser and display results in the terminal: +To run all tests in a headless Chromium browser and display results in the terminal. This assumes you have Grafana running on port 3000. ``` yarn e2e:playwright ``` -For a better developer experience, open the Playwright UI where you can visually walk through each step of the test and see what was happening before, during, and after each step. +The following script starts a Grafana [development server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) (same server that is being used when running e2e tests in Drone CI) on port 3001 and runs the Playwright tests. The development server is provisioned with the [devenv](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#add-data-sources) dashboards, data sources and apps. ``` -yarn e2e:playwright:ui -``` - -To open the HTML reporter for the last test run session: - -``` -yarn e2e:playwright:report +yarn e2e:playwright:server ``` ## Configure Grafana for development diff --git a/contribute/style-guides/e2e-plugins.md b/contribute/style-guides/e2e-plugins.md index f675db96784..6324204c4f6 100644 --- a/contribute/style-guides/e2e-plugins.md +++ b/contribute/style-guides/e2e-plugins.md @@ -33,3 +33,5 @@ Playwright end-to-end tests for plugins should be added to the [`e2e/plugin-e2e` The script above assumes you have Grafana running on `localhost:3000`. You may change this by providing environment variables. `HOST=127.0.0.1 PORT=3001 yarn e2e:playwright` + +- `yarn e2e:playwright:server` will start a Grafana [development server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) on port 3001 and run the Playwright tests. The development server is provisioned with the [devenv](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#add-data-sources) dashboards, data sources and apps. diff --git a/devenv/dev-dashboards/extensions/link-onclick-extensions.json b/devenv/dev-dashboards/extensions/link-onclick-extensions.json new file mode 100644 index 00000000000..5119eb4c1b9 --- /dev/null +++ b/devenv/dev-dashboards/extensions/link-onclick-extensions.json @@ -0,0 +1,342 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Link with one query", + "type": "timeseries" + }, + { + "datasource": { + "type": "testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 8 + }, + "id": 6, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "10.1.0-55406pre", + "title": "No extensions", + "type": "table" + }, + { + "datasource": { + "type": "testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 4 + }, + { + "datasource": { + "type": "testdata" + }, + "hide": false, + "refId": "B", + "scenarioId": "random_walk", + "seriesCount": 1 + } + ], + "title": "Link with new name", + "type": "piechart" + }, + { + "datasource": { + "type": "testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 16 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 1 + }, + { + "datasource": { + "type": "testdata" + }, + "hide": false, + "refId": "B", + "scenarioId": "random_walk", + "seriesCount": 1 + } + ], + "title": "Link with defaults", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 38, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Link Extensions (onClick)", + "uid": "dbfb47c5-e5e5-4d28-8ac7-35f349b95946", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/devenv/dev-dashboards/extensions/link-path-extensions.json b/devenv/dev-dashboards/extensions/link-path-extensions.json new file mode 100644 index 00000000000..6ef0669e371 --- /dev/null +++ b/devenv/dev-dashboards/extensions/link-path-extensions.json @@ -0,0 +1,237 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 6, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "showRowNums": false + }, + "pluginVersion": "9.5.0-53420pre", + "title": "No extensions", + "type": "table" + }, + { + "datasource": { + "type": "testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 0 + }, + "id": 4, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 4 + } + ], + "title": "Link with new name", + "type": "piechart" + }, + { + "datasource": { + "type": "testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 8 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Link with defaults", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 38, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Link Extensions (path)", + "uid": "d1fbb077-cd44-4738-8c8a-d4e66748b719", + "version": 3, + "weekStart": "" + } \ No newline at end of file diff --git a/devenv/jsonnet/dev-dashboards.libsonnet b/devenv/jsonnet/dev-dashboards.libsonnet index 6c9eb8afb30..8eabc329c5f 100644 --- a/devenv/jsonnet/dev-dashboards.libsonnet +++ b/devenv/jsonnet/dev-dashboards.libsonnet @@ -59,6 +59,8 @@ "join-by-field": (import '../dev-dashboards/transforms/join-by-field.json'), "join-by-labels": (import '../dev-dashboards/transforms/join-by-labels.json'), "lazy_loading": (import '../dev-dashboards/panel-common/lazy_loading.json'), + "link-onclick-extensions": (import '../dev-dashboards/extensions/link-onclick-extensions.json'), + "link-path-extensions": (import '../dev-dashboards/extensions/link-path-extensions.json'), "linked-viz": (import '../dev-dashboards/panel-common/linked-viz.json'), "live-flakey": (import '../dev-dashboards/live/live-flakey.json'), "live-flakey-refresh": (import '../dev-dashboards/live/live-flakey-refresh.json'), diff --git a/devenv/plugins.yaml b/devenv/plugins.yaml new file mode 100644 index 00000000000..9e488cc065f --- /dev/null +++ b/devenv/plugins.yaml @@ -0,0 +1,19 @@ +apiVersion: 1 + +apps: + - type: myorg-extensions-app + org_id: 1 + org_name: Main Org. + disabled: false + - type: myorg-a-app + org_id: 1 + org_name: Main Org. + disabled: false + - type: myorg-b-app + org_id: 1 + org_name: Main Org. + disabled: false + - type: myorg-extensionpoint-app + org_id: 1 + org_name: Main Org. + disabled: false diff --git a/e2e/custom-plugins/README.md b/e2e/custom-plugins/README.md new file mode 100644 index 00000000000..9fef8ba9aaf --- /dev/null +++ b/e2e/custom-plugins/README.md @@ -0,0 +1,5 @@ +# Custom plugins + +Plugins in this directory will be installed when the e2e [test server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) is started. Optionally, you can provision the plugin by adding configuration to the [datasources.yaml](https://github.com/grafana/grafana/blob/extensions/add-e2e-tests/devenv/datasources.yaml) or to the [plugins.yaml](https://github.com/grafana/grafana/blob/extensions/add-e2e-tests/devenv/plugins.yaml). + +These plugins are not being built as part of CI. Plugins in this directory are being version controlled, so make sure the bundle size is small. Only use dependencies provided by the runtime (see list of runtime dependencies [here](https://github.com/grafana/plugin-tools/blob/08b67179bdbf8847788c54aadb22654aa1a7c060/packages/create-plugin/templates/common/.config/webpack/webpack.config.ts#L36)). diff --git a/e2e/custom-plugins/app-with-extension-point/README.md b/e2e/custom-plugins/app-with-extension-point/README.md new file mode 100644 index 00000000000..99b656fa109 --- /dev/null +++ b/e2e/custom-plugins/app-with-extension-point/README.md @@ -0,0 +1,12 @@ +# App with extension point + +This app was initially copied from the [app-with-extension-point](https://github.com/grafana/grafana-plugin-examples/tree/main/examples/app-with-extension-point) example plugin. The plugin bundle is using AMD, but it's not minified and the plugin feature set is small so it should be possible to make changes in this file if necessary. + +To test this app: + +```sh +# start e2e test instance (it will install this plugin) +PORT=3000 ./scripts/grafana-server/start-server +# run Playwright tests using Playwright VSCode extension or with the following script +yarn e2e:playwright +``` diff --git a/e2e/custom-plugins/app-with-extension-point/module.js b/e2e/custom-plugins/app-with-extension-point/module.js new file mode 100644 index 00000000000..f08f08a2587 --- /dev/null +++ b/e2e/custom-plugins/app-with-extension-point/module.js @@ -0,0 +1,141 @@ +define(['@grafana/data', 'react', '@grafana/ui', '@grafana/runtime'], function (data, React, UI, runtime) { + 'use strict'; + + const styles = { + container: 'main-app-body', + actions: { button: 'action-button' }, + modal: { container: 'container', open: 'open-link' }, + appA: { container: 'a-app-body' }, + appB: { modal: 'b-app-modal' }, + }; + + function ModalComponent({ onDismiss, title, path }) { + return React.createElement( + UI.Modal, + { 'data-testid': styles.modal.container, title, isOpen: true, onDismiss }, + React.createElement( + UI.VerticalGroup, + { spacing: 'sm' }, + React.createElement('p', null, 'Do you want to proceed in the current tab or open a new tab?') + ), + React.createElement( + UI.Modal.ButtonRow, + null, + React.createElement(UI.Button, { onClick: onDismiss, fill: 'outline', variant: 'secondary' }, 'Cancel'), + React.createElement( + UI.Button, + { + type: 'submit', + variant: 'secondary', + onClick: function () { + window.open(data.locationUtil.assureBaseUrl(path), '_blank'); + onDismiss(); + }, + icon: 'external-link-alt', + }, + 'Open in new tab' + ), + React.createElement( + UI.Button, + { + 'data-testid': styles.modal.open, + type: 'submit', + variant: 'primary', + onClick: function () { + runtime.locationService.push(path); + }, + icon: 'apps', + }, + 'Open' + ) + ) + ); + } + + function ActionComponent({ extensions }) { + const options = React.useMemo( + function () { + return extensions.reduce(function (acc, extension) { + if (runtime.isPluginExtensionLink(extension)) { + acc.push({ label: extension.title, title: extension.title, value: extension }); + } + return acc; + }, []); + }, + [extensions] + ); + + const [selected, setSelected] = React.useState(); + + return options.length === 0 + ? React.createElement(UI.Button, null, 'Run default action') + : React.createElement( + React.Fragment, + null, + React.createElement( + UI.ButtonGroup, + null, + React.createElement( + UI.ToolbarButton, + { + key: 'default-action', + variant: 'canvas', + onClick: function () { + alert('You triggered the default action'); + }, + }, + 'Run default action' + ), + React.createElement(UI.ButtonSelect, { + 'data-testid': styles.actions.button, + key: 'select-extension', + variant: 'canvas', + options: options, + onChange: function (e) { + const extension = e.value; + if (runtime.isPluginExtensionLink(extension)) { + if (extension.path) setSelected(extension); + if (extension.onClick) extension.onClick(); + } + }, + }) + ), + selected && + selected.path && + React.createElement(ModalComponent, { + title: selected.title, + path: selected.path, + onDismiss: function () { + setSelected(undefined); + }, + }) + ); + } + + class RootComponent extends React.PureComponent { + render() { + const { extensions } = runtime.getPluginExtensions({ + extensionPointId: 'plugins/myorg-extensionpoint-app/actions', + context: {}, + }); + + return React.createElement( + 'div', + { 'data-testid': styles.container, style: { marginTop: '5%' } }, + React.createElement( + UI.HorizontalGroup, + { align: 'flex-start', justify: 'center' }, + React.createElement( + UI.HorizontalGroup, + null, + React.createElement('span', null, 'Hello Grafana! These are the actions you can trigger from this plugin'), + React.createElement(ActionComponent, { extensions: extensions }) + ) + ) + ); + } + } + + const plugin = new data.AppPlugin().setRootPage(RootComponent); + return { plugin: plugin }; +}); diff --git a/e2e/custom-plugins/app-with-extension-point/plugin.json b/e2e/custom-plugins/app-with-extension-point/plugin.json new file mode 100644 index 00000000000..b1d3c396384 --- /dev/null +++ b/e2e/custom-plugins/app-with-extension-point/plugin.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", + "type": "app", + "name": "Extension Point App", + "id": "myorg-extensionpoint-app", + "preload": true, + "info": { + "keywords": ["app"], + "description": "Show case how to add an extension point to your plugin", + "author": { + "name": "Myorg" + }, + "logos": { + "small": "img/logo.svg", + "large": "img/logo.svg" + }, + "screenshots": [], + "version": "1.0.0", + "updated": "2024-06-11" + }, + "includes": [ + { + "type": "page", + "name": "Default", + "path": "/a/myorg-extensionpoint-app", + "role": "Admin", + "addToNav": true, + "defaultNav": true + } + ], + "dependencies": { + "grafanaDependency": ">=10.3.3", + "plugins": [] + }, + "generated": { + "extensions": [] + } +} diff --git a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/module.js b/e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/module.js new file mode 100644 index 00000000000..055db7959fb --- /dev/null +++ b/e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/module.js @@ -0,0 +1,26 @@ +define(['@grafana/data', 'react'], function (data, React) { + 'use strict'; + + const styles = { + container: 'a-app-body', + }; + + class RootComponent extends React.PureComponent { + render() { + return React.createElement( + 'div', + { 'data-testid': styles.container, className: 'page-container' }, + 'Hello Grafana!' + ); + } + } + + const plugin = new data.AppPlugin().setRootPage(RootComponent).configureExtensionLink({ + title: 'Go to A', + description: 'Navigating to plugin A', + extensionPointId: 'plugins/myorg-extensionpoint-app/actions', + path: '/a/myorg-a-app/', + }); + + return { plugin: plugin }; +}); diff --git a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/plugin.json b/e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/plugin.json new file mode 100644 index 00000000000..a37ec4e1710 --- /dev/null +++ b/e2e/custom-plugins/app-with-extension-point/plugins/myorg-a-app/plugin.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", + "type": "app", + "name": "A App", + "id": "myorg-a-app", + "preload": true, + "info": { + "keywords": ["app"], + "description": "Will extend root app with ui extensions", + "author": { + "name": "Myorg" + }, + "logos": { + "small": "img/logo.svg", + "large": "img/logo.svg" + }, + "screenshots": [], + "version": "%VERSION%", + "updated": "%TODAY%" + }, + "includes": [ + { + "type": "page", + "name": "Default", + "path": "/a/myorg-a-app", + "role": "Admin", + "addToNav": false, + "defaultNav": false + } + ], + "dependencies": { + "grafanaDependency": ">=10.3.3", + "plugins": [] + }, + "generated": { + "extensions": [ + { + "extensionPointId": "plugins/myorg-extensionpoint-app/actions", + "title": "Go to A", + "description": "Navigating to pluging A", + "type": "link" + } + ] + } +} diff --git a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/module.js b/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/module.js new file mode 100644 index 00000000000..03c7468e6ae --- /dev/null +++ b/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/module.js @@ -0,0 +1,27 @@ +define(['react', '@grafana/data'], function (React, data) { + 'use strict'; + + class RootComponent extends React.PureComponent { + render() { + return React.createElement('div', { className: 'page-container' }, 'Hello Grafana!'); + } + } + + const modalId = 'b-app-modal'; + + const plugin = new data.AppPlugin().setRootPage(RootComponent).configureExtensionLink({ + title: 'Open from B', + description: 'Open a modal from plugin B', + extensionPointId: 'plugins/myorg-extensionpoint-app/actions', + onClick: function (e, { openModal }) { + openModal({ + title: 'Modal from app B', + body: function () { + return React.createElement('div', { 'data-testid': modalId }, 'From plugin B'); + }, + }); + }, + }); + + return { plugin: plugin }; +}); diff --git a/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/plugin.json b/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/plugin.json new file mode 100644 index 00000000000..ac501bc7f56 --- /dev/null +++ b/e2e/custom-plugins/app-with-extension-point/plugins/myorg-b-app/plugin.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", + "type": "app", + "name": "B App", + "id": "myorg-b-app", + "preload": true, + "info": { + "keywords": ["app"], + "description": "Will extend root app with ui extensions", + "author": { + "name": "Myorg" + }, + "logos": { + "small": "img/logo.svg", + "large": "img/logo.svg" + }, + "screenshots": [], + "version": "%VERSION%", + "updated": "%TODAY%" + }, + "includes": [ + { + "type": "page", + "name": "Default", + "path": "/a/myorg-b-app", + "role": "Admin", + "addToNav": false, + "defaultNav": false + } + ], + "dependencies": { + "grafanaDependency": ">=10.3.3", + "plugins": [] + }, + "generated": { + "extensions": [ + { + "extensionPointId": "plugins/myorg-extensionpoint-app/actions", + "title": "Open from B", + "description": "Open a modal from plugin B", + "type": "link" + } + ] + } +} diff --git a/e2e/custom-plugins/app-with-extensions/README.md b/e2e/custom-plugins/app-with-extensions/README.md new file mode 100644 index 00000000000..62ceac9ced4 --- /dev/null +++ b/e2e/custom-plugins/app-with-extensions/README.md @@ -0,0 +1,12 @@ +# App with extensions + +This app was initially copied from the [app-with-extensions](https://github.com/grafana/grafana-plugin-examples/tree/main/examples/app-with-extensions) example plugin. The plugin bundle is using AMD, but it's not minified and the plugin feature set is small so it should be possible to make changes in this file if necessary. + +To test this app: + +```sh +# start e2e test instance (it will install this plugin) +PORT=3000 ./scripts/grafana-server/start-server +# run Playwright tests using Playwright VSCode extension or with the following script +yarn e2e:playwright +``` diff --git a/e2e/custom-plugins/app-with-extensions/module.js b/e2e/custom-plugins/app-with-extensions/module.js new file mode 100644 index 00000000000..82469a3e211 --- /dev/null +++ b/e2e/custom-plugins/app-with-extensions/module.js @@ -0,0 +1,216 @@ +define(['react', '@grafana/data', '@grafana/ui', '@grafana/runtime', '@emotion/css', 'rxjs'], function ( + React, + data, + ui, + runtime, + css, + rxjs +) { + 'use strict'; + + const styles = { + modalBody: 'ape-modal-body', + mainPageContainer: 'ape-main-page-container', + }; + + class RootComponent extends React.PureComponent { + render() { + return React.createElement( + 'div', + { 'data-testid': styles.mainPageContainer, className: 'page-container' }, + 'Hello Grafana!' + ); + } + } + + const asyncWrapper = (fn) => { + return function () { + const gen = fn.apply(this, arguments); + return new Promise((resolve, reject) => { + function step(key, arg) { + let info, value; + try { + info = gen[key](arg); + value = info.value; + } catch (error) { + reject(error); + return; + } + if (info.done) { + resolve(value); + } else { + Promise.resolve(value).then(next, throw_); + } + } + function next(value) { + step('next', value); + } + function throw_(value) { + step('throw', value); + } + next(); + }); + }; + }; + + const getStyles = (theme) => ({ + colorWeak: css.css`color: ${theme.colors.text.secondary};`, + marginTop: css.css`margin-top: ${theme.spacing(3)};`, + }); + + const updatePlugin = asyncWrapper(function* (pluginId, settings) { + const response = runtime + .getBackendSrv() + .fetch({ url: `/api/plugins/${pluginId}/settings`, method: 'POST', data: settings }); + return rxjs.lastValueFrom(response); + }); + + const handleUpdate = asyncWrapper(function* (pluginId, settings) { + try { + yield updatePlugin(pluginId, settings); + window.location.reload(); + } catch (error) { + console.error('Error while updating the plugin', error); + } + }); + + const configPageBody = ({ plugin }) => { + const styles = getStyles(ui.useStyles2()); + const { enabled, jsonData } = plugin.meta; + return React.createElement( + 'div', + null, + React.createElement(ui.Legend, null, 'Enable / Disable '), + !enabled && + React.createElement( + React.Fragment, + null, + React.createElement('div', { className: styles.colorWeak }, 'The plugin is currently not enabled.'), + React.createElement( + ui.Button, + { + className: styles.marginTop, + variant: 'primary', + onClick: () => handleUpdate(plugin.meta.id, { enabled: true, pinned: true, jsonData: jsonData }), + }, + 'Enable plugin' + ) + ), + enabled && + React.createElement( + React.Fragment, + null, + React.createElement('div', { className: styles.colorWeak }, 'The plugin is currently enabled.'), + React.createElement( + ui.Button, + { + className: styles.marginTop, + variant: 'destructive', + onClick: () => handleUpdate(plugin.meta.id, { enabled: false, pinned: false, jsonData: jsonData }), + }, + 'Disable plugin' + ) + ) + ); + }; + + const selectQueryModal = ({ targets = [], onDismiss }) => { + const [selectedQuery, setSelectedQuery] = React.useState(targets[0]); + return React.createElement( + 'div', + { 'data-testid': styles.modalBody }, + React.createElement( + 'p', + null, + 'Please select the query you would like to use to create "something" in the plugin.' + ), + React.createElement( + ui.HorizontalGroup, + null, + targets.map((query) => + React.createElement(ui.FilterPill, { + key: query.refId, + label: query.refId, + selected: query.refId === (selectedQuery ? selectedQuery.refId : null), + onClick: () => setSelectedQuery(query), + }) + ) + ), + React.createElement( + ui.Modal.ButtonRow, + null, + React.createElement(ui.Button, { variant: 'secondary', fill: 'outline', onClick: onDismiss }, 'Cancel'), + React.createElement( + ui.Button, + { + disabled: !Boolean(selectedQuery), + onClick: () => { + onDismiss && onDismiss(); + alert(`You selected query "${selectedQuery.refId}"`); + }, + }, + 'OK' + ) + ) + ); + }; + + const plugin = new data.AppPlugin() + .setRootPage(RootComponent) + .addConfigPage({ + title: 'Configuration', + icon: 'cog', + body: configPageBody, + id: 'configuration', + }) + .configureExtensionLink({ + title: 'Open from time series or pie charts (path)', + description: 'This link will only be visible on time series and pie charts', + extensionPointId: data.PluginExtensionPoints.DashboardPanelMenu, + path: `/a/myorg-extensions-app/`, + configure: (context) => { + if (context.dashboard?.title === 'Link Extensions (path)') { + switch (context.pluginId) { + case 'timeseries': + return {}; + case 'piechart': + return { title: `Open from ${context.pluginId}` }; + default: + return; + } + } + }, + }) + .configureExtensionLink({ + title: 'Open from time series or pie charts (onClick)', + description: 'This link will only be visible on time series and pie charts', + extensionPointId: data.PluginExtensionPoints.DashboardPanelMenu, + onClick: (_, { context, openModal }) => { + const targets = context?.targets || []; + const title = context?.title; + if (!targets.length) return; + if (targets.length > 1) { + openModal({ + title: `Select query from "${title}"`, + body: (props) => React.createElement(selectQueryModal, { ...props, targets: targets }), + }); + } else { + alert(`You selected query "${targets[0].refId}"`); + } + }, + configure: (context) => { + if (context.dashboard?.title === 'Link Extensions (onClick)') { + switch (context.pluginId) { + case 'timeseries': + return {}; + case 'piechart': + return { title: `Open from ${context.pluginId}` }; + default: + return; + } + } + }, + }); + + return { plugin: plugin }; +}); diff --git a/e2e/custom-plugins/app-with-extensions/plugin.json b/e2e/custom-plugins/app-with-extensions/plugin.json new file mode 100644 index 00000000000..d54f150b836 --- /dev/null +++ b/e2e/custom-plugins/app-with-extensions/plugin.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", + "type": "app", + "name": "Extensions App", + "id": "myorg-extensions-app", + "preload": true, + "info": { + "keywords": ["app"], + "description": "Example on how to extend grafana ui from a plugin", + "author": { + "name": "Myorg" + }, + "logos": { + "small": "img/logo.svg", + "large": "img/logo.svg" + }, + "screenshots": [], + "version": "1.0.0", + "updated": "2024-06-11" + }, + "includes": [ + { + "type": "page", + "name": "Default", + "path": "/a/myorg-extensions-app", + "role": "Admin", + "addToNav": true, + "defaultNav": true + } + ], + "dependencies": { + "grafanaDependency": ">=10.3.3", + "plugins": [] + }, + "extensions": [ + { + "extensionPointId": "grafana/dashboard/panel/menu", + "type": "link", + "title": "Open from time series or pie charts (path)", + "description": "This link will only be visible on time series and pie charts" + }, + { + "extensionPointId": "grafana/dashboard/panel/menu", + "type": "link", + "title": "Open from time series or pie charts (onClick)", + "description": "This link will only be visible on time series and pie charts" + } + ] +} diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensionPoints.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensionPoints.spec.ts new file mode 100644 index 00000000000..bdd51f67d7e --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensionPoints.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const testIds = { + container: 'main-app-body', + actions: { + button: 'action-button', + }, + modal: { + container: 'container', + open: 'open-link', + }, + appA: { + container: 'a-app-body', + }, + appB: { + modal: 'b-app-modal', + }, +}; + +const pluginId = 'myorg-extensionpoint-app'; + +test('should extend the actions menu with a link to a-app plugin', async ({ page }) => { + await page.goto(`/a/${pluginId}/one`); + await page.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Go to A').click(); + await page.getByTestId(testIds.modal.open).click(); + await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); +}); + +test('should extend the actions menu with a command triggered from b-app plugin', async ({ page }) => { + await page.goto(`/a/${pluginId}/one`); + await page.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Open from B').click(); + await expect(page.getByTestId(testIds.appB.modal)).toBeVisible(); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions.spec.ts new file mode 100644 index 00000000000..71ada2f4ee5 --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/extensions.spec.ts @@ -0,0 +1,38 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +const panelTitle = 'Link with defaults'; +const extensionTitle = 'Open from time series...'; +const testIds = { + modal: { + container: 'ape-modal-body', + }, + mainPage: { + container: 'ape-main-page-container', + }, +}; + +const linkOnClickDashboardUid = 'dbfb47c5-e5e5-4d28-8ac7-35f349b95946'; +const linkPathDashboardUid = 'd1fbb077-cd44-4738-8c8a-d4e66748b719'; + +test('should add link extension (path) with defaults to time series panel', async ({ gotoDashboardPage, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: linkPathDashboardUid }); + const panel = await dashboardPage.getPanelByTitle(panelTitle); + await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); + await expect(page.getByTestId(testIds.mainPage.container)).toBeVisible(); +}); + +test('should add link extension (onclick) with defaults to time series panel', async ({ gotoDashboardPage, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); + const panel = await dashboardPage.getPanelByTitle(panelTitle); + await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); + await expect(page.getByRole('dialog')).toContainText('Select query from "Link with defaults"'); +}); + +test('should add link extension (onclick) with new title to pie chart panel', async ({ gotoDashboardPage, page }) => { + const panelTitle = 'Link with new name'; + const extensionTitle = 'Open from piechart'; + const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); + const panel = await dashboardPage.getPanelByTitle(panelTitle); + await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); + await expect(page.getByRole('dialog')).toContainText('Select query from "Link with new name"'); +}); diff --git a/e2e/plugin-e2e/start-and-run-suite b/e2e/plugin-e2e/start-and-run-suite new file mode 100755 index 00000000000..3322f449345 --- /dev/null +++ b/e2e/plugin-e2e/start-and-run-suite @@ -0,0 +1,23 @@ +#!/bin/bash + +. scripts/grafana-server/variables + +LICENSE_PATH="" + +if [ "$1" = "enterprise" ]; then + if [ "$2" != "dev" ] && [ "$2" != "debug" ]; then + LICENSE_PATH=$2/license.jwt + else + LICENSE_PATH=$3/license.jwt + fi +fi + +if [ "$BASE_URL" != "" ]; then + echo -e "BASE_URL set, skipping starting server" +else + # Start it in the background + ./scripts/grafana-server/start-server $LICENSE_PATH 2>&1 > scripts/grafana-server/server.log & + ./scripts/grafana-server/wait-for-grafana +fi + +PORT=3001 HOST=localhost yarn playwright test diff --git a/package.json b/package.json index 78218f9eced..b999b4f1c53 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,7 @@ "e2e:enterprise:dev": "./e2e/start-and-run-suite enterprise dev", "e2e:enterprise:debug": "./e2e/start-and-run-suite enterprise debug", "e2e:playwright": "yarn playwright test", - "e2e:playwright:ui": "yarn playwright test --ui", - "e2e:playwright:report": "yarn playwright show-report", + "e2e:playwright:server": "./e2e/plugin-e2e/start-and-run-suite", "test": "jest --notify --watch", "test:coverage": "jest --coverage", "test:coverage:changes": "jest --coverage --changedSince=origin/main", diff --git a/scripts/grafana-server/start-server b/scripts/grafana-server/start-server index 75259985cc1..83e74b7508a 100755 --- a/scripts/grafana-server/start-server +++ b/scripts/grafana-server/start-server @@ -31,6 +31,7 @@ mkdir $PROV_DIR mkdir $PROV_DIR/datasources mkdir $PROV_DIR/dashboards mkdir $PROV_DIR/alerting +mkdir $PROV_DIR/plugins cp ./scripts/grafana-server/custom.ini $RUNDIR/conf/custom.ini cp ./conf/defaults.ini $RUNDIR/conf/defaults.ini @@ -51,6 +52,7 @@ echo -e "Copy provisioning setup from devenv" cp devenv/datasources.yaml $PROV_DIR/datasources cp devenv/dashboards.yaml $PROV_DIR/dashboards cp devenv/alert_rules.yaml $PROV_DIR/alerting +cp devenv/plugins.yaml $PROV_DIR/plugins cp -r devenv $RUNDIR From 2dd44e2f58d040552105c37232b11a41abcce5a7 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Fri, 14 Jun 2024 12:42:00 +0100 Subject: [PATCH 11/11] Alerting: Remove duplicated alertRule handlers file and usage (#89100) --- .../unified/mocks/server/all-handlers.ts | 1 - .../mocks/server/handlers/alertRule.ts | 75 ------------------- 2 files changed, 76 deletions(-) delete mode 100644 public/app/features/alerting/unified/mocks/server/handlers/alertRule.ts diff --git a/public/app/features/alerting/unified/mocks/server/all-handlers.ts b/public/app/features/alerting/unified/mocks/server/all-handlers.ts index 8aa4831116d..908f4d77acc 100644 --- a/public/app/features/alerting/unified/mocks/server/all-handlers.ts +++ b/public/app/features/alerting/unified/mocks/server/all-handlers.ts @@ -21,7 +21,6 @@ const allHandlers = [ ...folderHandlers, ...pluginsHandlers, ...silenceHandlers, - ...alertRuleHandlers, ]; export default allHandlers; diff --git a/public/app/features/alerting/unified/mocks/server/handlers/alertRule.ts b/public/app/features/alerting/unified/mocks/server/handlers/alertRule.ts deleted file mode 100644 index bfa2afd5536..00000000000 --- a/public/app/features/alerting/unified/mocks/server/handlers/alertRule.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { http, HttpResponse } from 'msw'; - -import { - RulerGrafanaRuleDTO, - RulerRuleGroupDTO, - RulerRulesConfigDTO, -} from '../../../../../../types/unified-alerting-dto'; -import { grafanaRulerRule, namespaceByUid, namespaces } from '../../alertRuleApi'; - -export const rulerRulesHandler = () => { - return http.get(`/api/ruler/grafana/api/v1/rules`, () => { - const response = Object.entries(namespaces).reduce((acc, [namespaceUid, groups]) => { - acc[namespaceByUid[namespaceUid].name] = groups; - return acc; - }, {}); - - return HttpResponse.json(response); - }); -}; - -export const rulerRuleNamespaceHandler = () => { - return http.get<{ folderUid: string }>(`/api/ruler/grafana/api/v1/rules/:folderUid`, ({ params: { folderUid } }) => { - // This mimic API response as closely as possible - Invalid folderUid returns 403 - const namespace = namespaces[folderUid]; - if (!namespace) { - return new HttpResponse(null, { status: 403 }); - } - - return HttpResponse.json({ - [namespaceByUid[folderUid].name]: namespaces[folderUid], - }); - }); -}; - -export const rulerRuleGroupHandler = () => { - return http.get<{ folderUid: string; groupName: string }>( - `/api/ruler/grafana/api/v1/rules/:folderUid/:groupName`, - ({ params: { folderUid, groupName } }) => { - // This mimic API response as closely as possible. - // Invalid folderUid returns 403 but invalid group will return 202 with empty list of rules - const namespace = namespaces[folderUid]; - if (!namespace) { - return new HttpResponse(null, { status: 403 }); - } - - const matchingGroup = namespace.find((group) => group.name === groupName); - return HttpResponse.json({ - name: groupName, - interval: matchingGroup?.interval, - rules: matchingGroup?.rules ?? [], - }); - } - ); -}; - -export const getAlertRuleHandler = () => { - const grafanaRules = new Map( - [grafanaRulerRule].map((rule) => [rule.grafana_alert.uid, rule]) - ); - - return http.get<{ uid: string }>(`/api/ruler/grafana/api/v1/rule/:uid`, ({ params: { uid } }) => { - const rule = grafanaRules.get(uid); - if (!rule) { - return new HttpResponse(null, { status: 404 }); - } - return HttpResponse.json(rule); - }); -}; - -export const alertRuleHandlers = [ - rulerRulesHandler(), - rulerRuleNamespaceHandler(), - rulerRuleGroupHandler(), - getAlertRuleHandler(), -];