mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardScene: Alert states data layer (#77945)
* Add AlertStates data topic * DashboardScene: Alert states data layer * TMP package json * Remove duplicated function * Use latest scenes canry * Use latest scenes and add transformation test
This commit is contained in:
@@ -2873,7 +2873,9 @@ exports[`better eslint`] = {
|
|||||||
],
|
],
|
||||||
"public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts:5381": [
|
"public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||||
|
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||||
|
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||||
],
|
],
|
||||||
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts:5381": [
|
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
|
|||||||
@@ -254,7 +254,7 @@
|
|||||||
"@grafana/lezer-traceql": "0.0.10",
|
"@grafana/lezer-traceql": "0.0.10",
|
||||||
"@grafana/monaco-logql": "^0.0.7",
|
"@grafana/monaco-logql": "^0.0.7",
|
||||||
"@grafana/runtime": "workspace:*",
|
"@grafana/runtime": "workspace:*",
|
||||||
"@grafana/scenes": "^1.21.1",
|
"@grafana/scenes": "^1.22.0",
|
||||||
"@grafana/schema": "workspace:*",
|
"@grafana/schema": "workspace:*",
|
||||||
"@grafana/ui": "workspace:*",
|
"@grafana/ui": "workspace:*",
|
||||||
"@kusto/monaco-kusto": "^7.4.0",
|
"@kusto/monaco-kusto": "^7.4.0",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface DataSourceRef extends SchemaDataSourceRef {}
|
|||||||
*/
|
*/
|
||||||
export enum DataTopic {
|
export enum DataTopic {
|
||||||
Annotations = 'annotations',
|
Annotations = 'annotations',
|
||||||
|
AlertStates = 'alertStates',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import { from, map, Unsubscribable, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { AlertState, AlertStateInfo, DataTopic, LoadingState, toDataFrame } from '@grafana/data';
|
||||||
|
import { config, getBackendSrv } from '@grafana/runtime';
|
||||||
|
import {
|
||||||
|
SceneDataLayerBase,
|
||||||
|
SceneDataLayerProvider,
|
||||||
|
SceneDataLayerProviderState,
|
||||||
|
sceneGraph,
|
||||||
|
SceneTimeRangeLike,
|
||||||
|
} from '@grafana/scenes';
|
||||||
|
import { notifyApp } from 'app/core/actions';
|
||||||
|
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
import { getMessageFromError } from 'app/core/utils/errors';
|
||||||
|
import { Annotation } from 'app/features/alerting/unified/utils/constants';
|
||||||
|
import { isAlertingRule } from 'app/features/alerting/unified/utils/rules';
|
||||||
|
import { dispatch } from 'app/store/store';
|
||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
|
import { PromAlertingRuleState, PromRulesResponse } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
|
|
||||||
|
interface AlertStatesDataLayerState extends SceneDataLayerProviderState {}
|
||||||
|
|
||||||
|
export class AlertStatesDataLayer
|
||||||
|
extends SceneDataLayerBase<AlertStatesDataLayerState>
|
||||||
|
implements SceneDataLayerProvider
|
||||||
|
{
|
||||||
|
private hasAlertRules = true;
|
||||||
|
private _timeRangeSub: Unsubscribable | undefined;
|
||||||
|
public topic = DataTopic.AlertStates;
|
||||||
|
|
||||||
|
public constructor(initialState: AlertStatesDataLayerState) {
|
||||||
|
super({
|
||||||
|
isEnabled: true,
|
||||||
|
...initialState,
|
||||||
|
isHidden: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onEnable(): void {
|
||||||
|
const timeRange = sceneGraph.getTimeRange(this);
|
||||||
|
|
||||||
|
this._timeRangeSub = timeRange.subscribeToState(() => {
|
||||||
|
this.runWithTimeRange(timeRange);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDisable(): void {
|
||||||
|
this._timeRangeSub?.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
public runLayer() {
|
||||||
|
const timeRange = sceneGraph.getTimeRange(this);
|
||||||
|
this.runWithTimeRange(timeRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runWithTimeRange(timeRange: SceneTimeRangeLike) {
|
||||||
|
const dashboard = getDashboardSceneFor(this);
|
||||||
|
const { uid, id } = dashboard.state;
|
||||||
|
|
||||||
|
if (this.querySub) {
|
||||||
|
this.querySub.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.canWork(timeRange)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let alerStatesExecution: Observable<AlertStateInfo[]> | undefined;
|
||||||
|
|
||||||
|
if (this.isUsingLegacyAlerting()) {
|
||||||
|
alerStatesExecution = from(
|
||||||
|
getBackendSrv().get(
|
||||||
|
'/api/alerts/states-for-dashboard',
|
||||||
|
{
|
||||||
|
dashboardId: id,
|
||||||
|
},
|
||||||
|
`dashboard-query-runner-alert-states-${id}`
|
||||||
|
)
|
||||||
|
).pipe(map((alertStates) => alertStates));
|
||||||
|
} else {
|
||||||
|
alerStatesExecution = from(
|
||||||
|
getBackendSrv().get(
|
||||||
|
'/api/prometheus/grafana/api/v1/rules',
|
||||||
|
{
|
||||||
|
dashboard_uid: uid!,
|
||||||
|
},
|
||||||
|
`dashboard-query-runner-unified-alert-states-${id}`
|
||||||
|
)
|
||||||
|
).pipe(
|
||||||
|
map((result: PromRulesResponse) => {
|
||||||
|
if (result.status === 'success') {
|
||||||
|
this.hasAlertRules = false;
|
||||||
|
const panelIdToAlertState: Record<number, AlertStateInfo> = {};
|
||||||
|
|
||||||
|
result.data.groups.forEach((group) =>
|
||||||
|
group.rules.forEach((rule) => {
|
||||||
|
if (isAlertingRule(rule) && rule.annotations && rule.annotations[Annotation.panelID]) {
|
||||||
|
this.hasAlertRules = true;
|
||||||
|
const panelId = Number(rule.annotations[Annotation.panelID]);
|
||||||
|
const state = promAlertStateToAlertState(rule.state);
|
||||||
|
|
||||||
|
// there can be multiple alerts per panel, so we make sure we get the most severe state:
|
||||||
|
// alerting > pending > ok
|
||||||
|
if (!panelIdToAlertState[panelId]) {
|
||||||
|
panelIdToAlertState[panelId] = {
|
||||||
|
state,
|
||||||
|
id: Object.keys(panelIdToAlertState).length,
|
||||||
|
panelId,
|
||||||
|
dashboardId: id!,
|
||||||
|
};
|
||||||
|
} else if (
|
||||||
|
state === AlertState.Alerting &&
|
||||||
|
panelIdToAlertState[panelId].state !== AlertState.Alerting
|
||||||
|
) {
|
||||||
|
panelIdToAlertState[panelId].state = AlertState.Alerting;
|
||||||
|
} else if (
|
||||||
|
state === AlertState.Pending &&
|
||||||
|
panelIdToAlertState[panelId].state !== AlertState.Alerting &&
|
||||||
|
panelIdToAlertState[panelId].state !== AlertState.Pending
|
||||||
|
) {
|
||||||
|
panelIdToAlertState[panelId].state = AlertState.Pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return Object.values(panelIdToAlertState);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unexpected alert rules response.`);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.querySub = alerStatesExecution.subscribe({
|
||||||
|
next: (stateUpdate) => {
|
||||||
|
this.publishResults(
|
||||||
|
{
|
||||||
|
state: LoadingState.Done,
|
||||||
|
series: [toDataFrame(stateUpdate)],
|
||||||
|
timeRange: timeRange.state.value,
|
||||||
|
},
|
||||||
|
DataTopic.AlertStates
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.handleError(err);
|
||||||
|
this.publishResults(
|
||||||
|
{
|
||||||
|
state: LoadingState.Error,
|
||||||
|
series: [],
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: getMessageFromError(err),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timeRange: timeRange.state.value,
|
||||||
|
},
|
||||||
|
DataTopic.AlertStates
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private canWork(timeRange: SceneTimeRangeLike): boolean {
|
||||||
|
const dashboard = getDashboardSceneFor(this);
|
||||||
|
const { uid, id } = dashboard.state;
|
||||||
|
|
||||||
|
if (this.isUsingLegacyAlerting()) {
|
||||||
|
if (!id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeRange.state.value.raw.to !== 'now') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if (!uid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot fetch rules while on a public dashboard since it's unauthenticated
|
||||||
|
if (config.publicDashboardAccessToken) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeRange.state.value.raw.to !== 'now') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasAlertRules === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasRuleReadPermission =
|
||||||
|
contextSrv.hasPermission(AccessControlAction.AlertingRuleRead) &&
|
||||||
|
contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalRead);
|
||||||
|
|
||||||
|
if (!hasRuleReadPermission) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isUsingLegacyAlerting(): boolean {
|
||||||
|
return !config.unifiedAlertingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError = (err: unknown) => {
|
||||||
|
const notification = createErrorNotification('AlertStatesDataLayer', getMessageFromError(err));
|
||||||
|
dispatch(notifyApp(notification));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function promAlertStateToAlertState(state: PromAlertingRuleState): AlertState {
|
||||||
|
if (state === PromAlertingRuleState.Firing) {
|
||||||
|
return AlertState.Alerting;
|
||||||
|
} else if (state === PromAlertingRuleState.Pending) {
|
||||||
|
return AlertState.Pending;
|
||||||
|
}
|
||||||
|
return AlertState.OK;
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
|||||||
import { createPanelSaveModel } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures';
|
import { createPanelSaveModel } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures';
|
||||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||||
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
||||||
|
import { DashboardDataDTO } from 'app/types';
|
||||||
|
|
||||||
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
|
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
|
||||||
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||||
@@ -743,6 +744,34 @@ describe('transformSaveModelToScene', () => {
|
|||||||
expect(dataLayers.state.layers[3].state.isHidden).toBe(true);
|
expect(dataLayers.state.layers[3].state.isHidden).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Alerting data layer', () => {
|
||||||
|
it('Should add alert states data layer if unified alerting enabled', () => {
|
||||||
|
config.unifiedAlertingEnabled = true;
|
||||||
|
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
|
||||||
|
|
||||||
|
expect(scene.state.$data).toBeInstanceOf(SceneDataLayers);
|
||||||
|
expect(scene.state.controls![2]).toBeInstanceOf(SceneDataLayerControls);
|
||||||
|
|
||||||
|
const dataLayers = scene.state.$data as SceneDataLayers;
|
||||||
|
expect(dataLayers.state.layers).toHaveLength(5);
|
||||||
|
expect(dataLayers.state.layers[4].state.name).toBe('Alert States');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should add alert states data layer if any panel has a legacy alert defined', () => {
|
||||||
|
config.unifiedAlertingEnabled = false;
|
||||||
|
const dashboard = { ...dashboard_to_load1 } as unknown as DashboardDataDTO;
|
||||||
|
dashboard.panels![0].alert = {};
|
||||||
|
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
|
||||||
|
|
||||||
|
expect(scene.state.$data).toBeInstanceOf(SceneDataLayers);
|
||||||
|
expect(scene.state.controls![2]).toBeInstanceOf(SceneDataLayerControls);
|
||||||
|
|
||||||
|
const dataLayers = scene.state.$data as SceneDataLayers;
|
||||||
|
expect(dataLayers.state.layers).toHaveLength(5);
|
||||||
|
expect(dataLayers.state.layers[4].state.name).toBe('Alert States');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function buildGridItemForTest(saveModel: Partial<Panel>): { gridItem: SceneGridItem; vizPanel: VizPanel } {
|
function buildGridItemForTest(saveModel: Partial<Panel>): { gridItem: SceneGridItem; vizPanel: VizPanel } {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AdHocVariableModel, TypedVariableModel, VariableModel } from '@grafana/data';
|
import { AdHocVariableModel, TypedVariableModel, VariableModel } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
VizPanel,
|
VizPanel,
|
||||||
SceneTimePicker,
|
SceneTimePicker,
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||||
import { DashboardDTO } from 'app/types';
|
import { DashboardDTO } from 'app/types';
|
||||||
|
|
||||||
|
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
|
||||||
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||||
@@ -187,6 +189,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
|
|||||||
layers = oldModel.annotations?.list.map((a) => {
|
layers = oldModel.annotations?.list.map((a) => {
|
||||||
// Each annotation query is an individual data layer
|
// Each annotation query is an individual data layer
|
||||||
return new DashboardAnnotationsDataLayer({
|
return new DashboardAnnotationsDataLayer({
|
||||||
|
key: `annnotations-${a.name}`,
|
||||||
query: a,
|
query: a,
|
||||||
name: a.name,
|
name: a.name,
|
||||||
isEnabled: Boolean(a.enable),
|
isEnabled: Boolean(a.enable),
|
||||||
@@ -195,6 +198,22 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let shouldUseAlertStatesLayer = config.unifiedAlertingEnabled;
|
||||||
|
if (!shouldUseAlertStatesLayer) {
|
||||||
|
if (oldModel.panels.find((panel) => Boolean(panel.alert))) {
|
||||||
|
shouldUseAlertStatesLayer = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldUseAlertStatesLayer) {
|
||||||
|
layers.push(
|
||||||
|
new AlertStatesDataLayer({
|
||||||
|
key: 'alert-states',
|
||||||
|
name: 'Alert States',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let controls: SceneObject[] = [
|
let controls: SceneObject[] = [
|
||||||
new VariableValueSelectors({}),
|
new VariableValueSelectors({}),
|
||||||
...filtersSets,
|
...filtersSets,
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import { config, getBackendSrv } from '@grafana/runtime';
|
|||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { Annotation } from 'app/features/alerting/unified/utils/constants';
|
import { Annotation } from 'app/features/alerting/unified/utils/constants';
|
||||||
import { isAlertingRule } from 'app/features/alerting/unified/utils/rules';
|
import { isAlertingRule } from 'app/features/alerting/unified/utils/rules';
|
||||||
|
import { promAlertStateToAlertState } from 'app/features/dashboard-scene/scene/AlertStatesDataLayer';
|
||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
import { PromAlertingRuleState, PromRulesResponse } from 'app/types/unified-alerting-dto';
|
import { PromRulesResponse } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { DashboardQueryRunnerOptions, DashboardQueryRunnerWorker, DashboardQueryRunnerWorkerResult } from './types';
|
import { DashboardQueryRunnerOptions, DashboardQueryRunnerWorker, DashboardQueryRunnerWorkerResult } from './types';
|
||||||
import { emptyResult, handleDashboardQueryRunnerWorkerError } from './utils';
|
import { emptyResult, handleDashboardQueryRunnerWorkerError } from './utils';
|
||||||
@@ -105,12 +106,3 @@ export class UnifiedAlertStatesWorker implements DashboardQueryRunnerWorker {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function promAlertStateToAlertState(state: PromAlertingRuleState): AlertState {
|
|
||||||
if (state === PromAlertingRuleState.Firing) {
|
|
||||||
return AlertState.Alerting;
|
|
||||||
} else if (state === PromAlertingRuleState.Pending) {
|
|
||||||
return AlertState.Pending;
|
|
||||||
}
|
|
||||||
return AlertState.OK;
|
|
||||||
}
|
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@@ -3320,9 +3320,9 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"@grafana/scenes@npm:^1.21.1":
|
"@grafana/scenes@npm:^1.22.0":
|
||||||
version: 1.21.1
|
version: 1.22.0
|
||||||
resolution: "@grafana/scenes@npm:1.21.1"
|
resolution: "@grafana/scenes@npm:1.22.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@grafana/e2e-selectors": "npm:10.0.2"
|
"@grafana/e2e-selectors": "npm:10.0.2"
|
||||||
react-grid-layout: "npm:1.3.4"
|
react-grid-layout: "npm:1.3.4"
|
||||||
@@ -3334,7 +3334,7 @@ __metadata:
|
|||||||
"@grafana/runtime": 10.0.3
|
"@grafana/runtime": 10.0.3
|
||||||
"@grafana/schema": 10.0.3
|
"@grafana/schema": 10.0.3
|
||||||
"@grafana/ui": 10.0.3
|
"@grafana/ui": 10.0.3
|
||||||
checksum: f9621b0edcc5a9da2cfeac679bf9ea8d2ae6fc64c635f5bf8ee90c47cdf7a8e8799b4ef4d41b1fdee056371e9f5bfc73f5e5b2dc23852a1b05963748559fd6e9
|
checksum: 6067bccec76de3f5aeab15c943095cf11e407b5734ddab4dc52ed40e469f65b096d69b7b0f46e5abe0a51c6691651c8ca8609177b95a32a2419239068a337952
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -17325,7 +17325,7 @@ __metadata:
|
|||||||
"@grafana/lezer-traceql": "npm:0.0.10"
|
"@grafana/lezer-traceql": "npm:0.0.10"
|
||||||
"@grafana/monaco-logql": "npm:^0.0.7"
|
"@grafana/monaco-logql": "npm:^0.0.7"
|
||||||
"@grafana/runtime": "workspace:*"
|
"@grafana/runtime": "workspace:*"
|
||||||
"@grafana/scenes": "npm:^1.21.1"
|
"@grafana/scenes": "npm:^1.22.0"
|
||||||
"@grafana/schema": "workspace:*"
|
"@grafana/schema": "workspace:*"
|
||||||
"@grafana/tsconfig": "npm:^1.3.0-rc1"
|
"@grafana/tsconfig": "npm:^1.3.0-rc1"
|
||||||
"@grafana/ui": "workspace:*"
|
"@grafana/ui": "workspace:*"
|
||||||
|
|||||||
Reference in New Issue
Block a user