mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardScene: Data source and query options edit for a panel (#79267)
* Scenes bump * Refactor query group components so that they are reusable * Basic queries rendering and datasource change * Store last used ds in local storage * maxDataPoints and minInterval queries options * Progress on query options * Query options tests * Allow panel inspection / queries inspection from panel edit * Add counters to data pane tabs * Betterer update * Handle data source changes * Minor fixes * Data source change tests * Handle scenario of data source change when transformations are in place * Review comment * Fix test
This commit is contained in:
parent
96b5b10e20
commit
73eab8fcd6
@ -2438,6 +2438,20 @@ exports[`better eslint`] = {
|
||||
"public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx:5381": [
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx:5381": [
|
||||
[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.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/scene/DashboardScene.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
@ -4176,14 +4190,14 @@ exports[`better eslint`] = {
|
||||
],
|
||||
"public/app/features/query/components/QueryGroup.tsx:5381": [
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"],
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "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, "Use data-testid for E2E selectors instead of aria-label", "8"]
|
||||
],
|
||||
"public/app/features/query/components/QueryGroupOptions.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||
|
@ -255,7 +255,7 @@
|
||||
"@grafana/lezer-traceql": "0.0.12",
|
||||
"@grafana/monaco-logql": "^0.0.7",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/scenes": "1.27.0",
|
||||
"@grafana/scenes": "1.28.0",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
"@kusto/monaco-kusto": "^7.4.0",
|
||||
|
@ -3,16 +3,32 @@ import React from 'react';
|
||||
import { IconName } from '@grafana/data';
|
||||
import { SceneObjectBase, SceneComponentProps } from '@grafana/scenes';
|
||||
|
||||
import { VizPanelManager } from '../VizPanelManager';
|
||||
|
||||
import { PanelDataPaneTabState, PanelDataPaneTab } from './types';
|
||||
|
||||
export class PanelDataAlertingTab extends SceneObjectBase<PanelDataPaneTabState> implements PanelDataPaneTab {
|
||||
static Component = PanelDataAlertingTabRendered;
|
||||
tabId = 'alert';
|
||||
icon: IconName = 'bell';
|
||||
private _panelManager: VizPanelManager;
|
||||
|
||||
constructor(panelManager: VizPanelManager) {
|
||||
super({});
|
||||
|
||||
this._panelManager = panelManager;
|
||||
}
|
||||
getTabLabel() {
|
||||
return 'Alert';
|
||||
}
|
||||
|
||||
getItemsCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
get panelManager() {
|
||||
return this._panelManager;
|
||||
}
|
||||
}
|
||||
|
||||
function PanelDataAlertingTabRendered(props: SceneComponentProps<PanelDataAlertingTab>) {
|
||||
|
@ -1,24 +1,29 @@
|
||||
import React from 'react';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
|
||||
import {
|
||||
SceneComponentProps,
|
||||
SceneDataTransformer,
|
||||
SceneObjectBase,
|
||||
SceneObjectRef,
|
||||
SceneObjectState,
|
||||
SceneObjectUrlSyncConfig,
|
||||
SceneObjectUrlValues,
|
||||
SceneQueryRunner,
|
||||
VizPanel,
|
||||
sceneGraph,
|
||||
} from '@grafana/scenes';
|
||||
import { Tab, TabContent, TabsBar } from '@grafana/ui';
|
||||
import { shouldShowAlertingTab } from 'app/features/dashboard/components/PanelEditor/state/selectors';
|
||||
|
||||
import { ShareQueryDataProvider } from '../../scene/ShareQueryDataProvider';
|
||||
import { VizPanelManager } from '../VizPanelManager';
|
||||
|
||||
import { PanelDataAlertingTab } from './PanelDataAlertingTab';
|
||||
import { PanelDataQueriesTab } from './PanelDataQueriesTab';
|
||||
import { PanelDataTransformationsTab } from './PanelDataTransformationsTab';
|
||||
import { PanelDataPaneTab } from './types';
|
||||
|
||||
export interface PanelDataPaneState extends SceneObjectState {
|
||||
panelRef: SceneObjectRef<VizPanel>;
|
||||
tabs?: PanelDataPaneTab[];
|
||||
tab?: string;
|
||||
}
|
||||
@ -26,6 +31,9 @@ export interface PanelDataPaneState extends SceneObjectState {
|
||||
export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||
static Component = PanelDataPaneRendered;
|
||||
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['tab'] });
|
||||
private _initialTabsBuilt = false;
|
||||
private panelSubscription: Unsubscribable | undefined;
|
||||
public panelManager: VizPanelManager;
|
||||
|
||||
getUrlState() {
|
||||
return {
|
||||
@ -42,36 +50,81 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(state: Omit<PanelDataPaneState, 'tab'> & { tab?: string }) {
|
||||
constructor(panelMgr: VizPanelManager) {
|
||||
super({
|
||||
tab: 'queries',
|
||||
...state,
|
||||
});
|
||||
|
||||
const { panelRef } = this.state;
|
||||
const panel = panelRef.resolve();
|
||||
this.panelManager = panelMgr;
|
||||
this.addActivationHandler(() => this.onActivate());
|
||||
}
|
||||
|
||||
if (panel) {
|
||||
// The subscription below is needed because the plugin may not be loaded when this pane is mounted.
|
||||
// This can happen i.e. when the user opens the panel editor directly via an URL.
|
||||
this._subs.add(
|
||||
panel.subscribeToState((n, p) => {
|
||||
if (n.pluginVersion || p.pluginId !== n.pluginId) {
|
||||
this.buildTabs();
|
||||
}
|
||||
})
|
||||
);
|
||||
private onActivate() {
|
||||
const panel = this.panelManager.state.panel;
|
||||
this.setupPanelSubscription(panel);
|
||||
this.buildTabs();
|
||||
|
||||
this._subs.add(
|
||||
// Setup subscription for the case when panel type changed
|
||||
this.panelManager.subscribeToState((n, p) => {
|
||||
if (n.panel !== p.panel) {
|
||||
this.buildTabs();
|
||||
this.setupPanelSubscription(n.panel);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (this.panelSubscription) {
|
||||
this.panelSubscription.unsubscribe();
|
||||
this.panelSubscription = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private setupPanelSubscription(panel: VizPanel) {
|
||||
if (this.panelSubscription) {
|
||||
this._initialTabsBuilt = false;
|
||||
this.panelSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
this.addActivationHandler(() => this.buildTabs());
|
||||
this.panelSubscription = panel.subscribeToState(() => {
|
||||
if (panel.getPlugin() && !this._initialTabsBuilt) {
|
||||
this.buildTabs();
|
||||
this._initialTabsBuilt = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getDataObjects(): [SceneQueryRunner | ShareQueryDataProvider | undefined, SceneDataTransformer | undefined] {
|
||||
const dataObj = sceneGraph.getData(this.panelManager.state.panel);
|
||||
|
||||
let runner: SceneQueryRunner | ShareQueryDataProvider | undefined;
|
||||
let transformer: SceneDataTransformer | undefined;
|
||||
|
||||
if (dataObj instanceof SceneQueryRunner || dataObj instanceof ShareQueryDataProvider) {
|
||||
runner = dataObj;
|
||||
}
|
||||
|
||||
if (dataObj instanceof SceneDataTransformer) {
|
||||
transformer = dataObj;
|
||||
if (transformer.state.$data instanceof SceneQueryRunner) {
|
||||
runner = transformer.state.$data;
|
||||
}
|
||||
}
|
||||
|
||||
return [runner, transformer];
|
||||
}
|
||||
|
||||
private buildTabs() {
|
||||
const { panelRef } = this.state;
|
||||
const panelManager = this.panelManager;
|
||||
const panel = panelManager.state.panel;
|
||||
const [runner] = this.getDataObjects();
|
||||
const tabs: PanelDataPaneTab[] = [];
|
||||
|
||||
if (panelRef) {
|
||||
const plugin = panelRef.resolve().getPlugin();
|
||||
if (panel) {
|
||||
const plugin = panel.getPlugin();
|
||||
|
||||
if (!plugin) {
|
||||
this.setState({ tabs });
|
||||
return;
|
||||
@ -80,11 +133,14 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||
this.setState({ tabs });
|
||||
return;
|
||||
} else {
|
||||
tabs.push(new PanelDataQueriesTab({}));
|
||||
tabs.push(new PanelDataTransformationsTab({}));
|
||||
if (runner) {
|
||||
tabs.push(new PanelDataQueriesTab(this.panelManager));
|
||||
}
|
||||
|
||||
tabs.push(new PanelDataTransformationsTab(this.panelManager));
|
||||
|
||||
if (shouldShowAlertingTab(plugin)) {
|
||||
tabs.push(new PanelDataAlertingTab({}));
|
||||
tabs.push(new PanelDataAlertingTab(this.panelManager));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -115,7 +171,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
key={`${t.getTabLabel()}-${index}`}
|
||||
label={t.getTabLabel()}
|
||||
icon={t.icon}
|
||||
// suffix={}
|
||||
counter={t.getItemsCount?.()}
|
||||
active={t.tabId === tab}
|
||||
onChangeTab={() => model.onChangeTab(t)}
|
||||
/>
|
||||
|
@ -1,20 +1,172 @@
|
||||
import React from 'react';
|
||||
|
||||
import { IconName } from '@grafana/data';
|
||||
import { SceneObjectBase, SceneComponentProps } from '@grafana/scenes';
|
||||
import { DataSourceApi, DataSourceInstanceSettings, IconName } from '@grafana/data';
|
||||
import { SceneObjectBase, SceneComponentProps, SceneQueryRunner, sceneGraph } from '@grafana/scenes';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { QueryEditorRows } from 'app/features/query/components/QueryEditorRows';
|
||||
import { QueryGroupTopSection } from 'app/features/query/components/QueryGroup';
|
||||
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
|
||||
import { QueryGroupOptions } from 'app/types';
|
||||
|
||||
import { PanelTimeRange } from '../../scene/PanelTimeRange';
|
||||
import { ShareQueryDataProvider } from '../../scene/ShareQueryDataProvider';
|
||||
import { VizPanelManager } from '../VizPanelManager';
|
||||
|
||||
import { PanelDataPaneTabState, PanelDataPaneTab } from './types';
|
||||
|
||||
export class PanelDataQueriesTab extends SceneObjectBase<PanelDataPaneTabState> implements PanelDataPaneTab {
|
||||
interface PanelDataQueriesTabState extends PanelDataPaneTabState {
|
||||
// dataRef: SceneObjectRef<SceneQueryRunner | ShareQueryDataProvider>;
|
||||
datasource?: DataSourceApi;
|
||||
dsSettings?: DataSourceInstanceSettings;
|
||||
}
|
||||
export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabState> implements PanelDataPaneTab {
|
||||
static Component = PanelDataQueriesTabRendered;
|
||||
tabId = 'queries';
|
||||
icon: IconName = 'database';
|
||||
private _panelManager: VizPanelManager;
|
||||
|
||||
getTabLabel() {
|
||||
return 'Queries';
|
||||
}
|
||||
|
||||
getItemsCount() {
|
||||
const dataObj = this._panelManager.state.panel.state.$data!;
|
||||
|
||||
if (dataObj instanceof ShareQueryDataProvider) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (dataObj instanceof SceneQueryRunner) {
|
||||
return dataObj.state.queries.length;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
constructor(panelManager: VizPanelManager) {
|
||||
super({});
|
||||
|
||||
this._panelManager = panelManager;
|
||||
}
|
||||
|
||||
buildQueryOptions(): QueryGroupOptions {
|
||||
const panelManager = this._panelManager;
|
||||
const panelObj = this._panelManager.state.panel;
|
||||
const dataObj = panelObj.state.$data!;
|
||||
const queryRunner = this._panelManager.queryRunner;
|
||||
|
||||
const timeRangeObj = sceneGraph.getTimeRange(panelObj);
|
||||
|
||||
let timeRangeOpts: QueryGroupOptions['timeRange'] = {
|
||||
from: undefined,
|
||||
shift: undefined,
|
||||
hide: undefined,
|
||||
};
|
||||
|
||||
if (timeRangeObj instanceof PanelTimeRange) {
|
||||
timeRangeOpts = {
|
||||
from: timeRangeObj.state.timeFrom,
|
||||
shift: timeRangeObj.state.timeShift,
|
||||
hide: timeRangeObj.state.hideTimeOverride,
|
||||
};
|
||||
}
|
||||
|
||||
let queries: QueryGroupOptions['queries'] = [];
|
||||
if (dataObj instanceof ShareQueryDataProvider) {
|
||||
queries = [dataObj.state.query];
|
||||
}
|
||||
|
||||
if (dataObj instanceof SceneQueryRunner) {
|
||||
queries = dataObj.state.queries;
|
||||
}
|
||||
|
||||
return {
|
||||
// TODO
|
||||
// cacheTimeout: dsSettings?.meta.queryOptions?.cacheTimeout ? panel.cacheTimeout : undefined,
|
||||
// queryCachingTTL: dsSettings?.cachingConfig?.enabled ? panel.queryCachingTTL : undefined,
|
||||
dataSource: {
|
||||
default: panelManager.state.dsSettings?.isDefault,
|
||||
type: panelManager.state.dsSettings?.type,
|
||||
uid: panelManager.state.dsSettings?.uid,
|
||||
},
|
||||
queries,
|
||||
maxDataPoints: queryRunner.state.maxDataPoints,
|
||||
minInterval: queryRunner.state.minInterval,
|
||||
timeRange: timeRangeOpts,
|
||||
};
|
||||
}
|
||||
|
||||
onOpenInspector = () => {
|
||||
this._panelManager.inspectPanel();
|
||||
};
|
||||
|
||||
onChangeDataSource = async (
|
||||
newSettings: DataSourceInstanceSettings,
|
||||
defaultQueries?: DataQuery[] | GrafanaQuery[]
|
||||
) => {
|
||||
this._panelManager.changePanelDataSource(newSettings, defaultQueries);
|
||||
};
|
||||
|
||||
onQueryOptionsChange = (options: QueryGroupOptions) => {
|
||||
this._panelManager.changeQueryOptions(options);
|
||||
};
|
||||
|
||||
onQueriesChange = (queries: DataQuery[]) => {
|
||||
this._panelManager.changeQueries(queries);
|
||||
};
|
||||
|
||||
getQueries() {
|
||||
const dataObj = this._panelManager.state.panel.state.$data!;
|
||||
|
||||
if (dataObj instanceof ShareQueryDataProvider) {
|
||||
return [dataObj.state.query];
|
||||
}
|
||||
return this._panelManager.queryRunner.state.queries;
|
||||
}
|
||||
|
||||
get panelManager() {
|
||||
return this._panelManager;
|
||||
}
|
||||
}
|
||||
|
||||
function PanelDataQueriesTabRendered(props: SceneComponentProps<PanelDataQueriesTab>) {
|
||||
return <div>TODO Queries</div>;
|
||||
function PanelDataQueriesTabRendered({ model }: SceneComponentProps<PanelDataQueriesTab>) {
|
||||
const { panel, datasource, dsSettings } = model.panelManager.useState();
|
||||
const { $data: dataObj } = panel.useState();
|
||||
|
||||
if (!dataObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = dataObj!.useState();
|
||||
|
||||
if (!datasource || !dsSettings || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<QueryGroupTopSection
|
||||
data={data}
|
||||
dsSettings={dsSettings}
|
||||
dataSource={datasource}
|
||||
options={model.buildQueryOptions()}
|
||||
onDataSourceChange={model.onChangeDataSource}
|
||||
onOptionsChange={model.onQueryOptionsChange}
|
||||
onOpenQueryInspector={model.onOpenInspector}
|
||||
/>
|
||||
|
||||
{dataObj instanceof ShareQueryDataProvider ? (
|
||||
<h1>TODO: DashboardQueryEditor</h1>
|
||||
) : (
|
||||
<QueryEditorRows
|
||||
data={data}
|
||||
queries={model.getQueries()}
|
||||
dsSettings={dsSettings}
|
||||
onAddQuery={() => {}}
|
||||
onQueriesChange={model.onQueriesChange}
|
||||
onRunQueries={() => {}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -3,18 +3,44 @@ import React from 'react';
|
||||
import { IconName } from '@grafana/data';
|
||||
import { SceneObjectBase, SceneComponentProps } from '@grafana/scenes';
|
||||
|
||||
import { VizPanelManager } from '../VizPanelManager';
|
||||
|
||||
import { PanelDataPaneTabState, PanelDataPaneTab } from './types';
|
||||
|
||||
export class PanelDataTransformationsTab extends SceneObjectBase<PanelDataPaneTabState> implements PanelDataPaneTab {
|
||||
interface PanelDataTransformationsTabState extends PanelDataPaneTabState {}
|
||||
|
||||
export class PanelDataTransformationsTab
|
||||
extends SceneObjectBase<PanelDataTransformationsTabState>
|
||||
implements PanelDataPaneTab
|
||||
{
|
||||
static Component = PanelDataTransformationsTabRendered;
|
||||
tabId = 'transformations';
|
||||
icon: IconName = 'process';
|
||||
private _panelManager: VizPanelManager;
|
||||
|
||||
getTabLabel() {
|
||||
return 'Transformations';
|
||||
}
|
||||
|
||||
getItemsCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
constructor(panelManager: VizPanelManager) {
|
||||
super({});
|
||||
|
||||
this._panelManager = panelManager;
|
||||
}
|
||||
|
||||
get panelManager() {
|
||||
return this._panelManager;
|
||||
}
|
||||
}
|
||||
|
||||
function PanelDataTransformationsTabRendered(props: SceneComponentProps<PanelDataTransformationsTab>) {
|
||||
function PanelDataTransformationsTabRendered({ model }: SceneComponentProps<PanelDataTransformationsTab>) {
|
||||
// const { dataRef } = model.useState();
|
||||
// const dataObj = dataRef.resolve();
|
||||
// // const { transformations } = dataObj.useState();
|
||||
|
||||
return <div>TODO Transformations</div>;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ export interface PanelDataPaneTabState extends SceneObjectState {}
|
||||
|
||||
export interface PanelDataPaneTab extends SceneObject {
|
||||
getTabLabel(): string;
|
||||
getItemsCount?(): number | null;
|
||||
tabId: string;
|
||||
icon: IconName;
|
||||
}
|
||||
|
@ -14,12 +14,15 @@ import {
|
||||
SplitLayout,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper';
|
||||
import { getDashboardUrl } from '../utils/urlBuilders';
|
||||
|
||||
import { PanelDataPane } from './PanelDataPane/PanelDataPane';
|
||||
import { PanelEditorRenderer } from './PanelEditorRenderer';
|
||||
import { PanelEditorUrlSync } from './PanelEditorUrlSync';
|
||||
import { PanelOptionsPane } from './PanelOptionsPane';
|
||||
import { PanelVizTypePicker } from './PanelVizTypePicker';
|
||||
import { VizPanelManager } from './VizPanelManager';
|
||||
@ -29,9 +32,9 @@ export interface PanelEditorState extends SceneObjectState {
|
||||
controls?: SceneObject[];
|
||||
isDirty?: boolean;
|
||||
/** Panel to inspect */
|
||||
inspectPanelId?: string;
|
||||
inspectPanelKey?: string;
|
||||
/** Scene object that handles the current drawer */
|
||||
drawer?: SceneObject;
|
||||
overlay?: SceneObject;
|
||||
|
||||
dashboardRef: SceneObjectRef<DashboardScene>;
|
||||
sourcePanelRef: SceneObjectRef<VizPanel>;
|
||||
@ -41,6 +44,11 @@ export interface PanelEditorState extends SceneObjectState {
|
||||
export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
static Component = PanelEditorRenderer;
|
||||
|
||||
/**
|
||||
* Handles url sync
|
||||
*/
|
||||
protected _urlSync = new PanelEditorUrlSync(this);
|
||||
|
||||
public constructor(state: PanelEditorState) {
|
||||
super(state);
|
||||
|
||||
@ -48,6 +56,10 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
}
|
||||
|
||||
private _activationHandler() {
|
||||
const oldDashboardWrapper = new DashboardModelCompatibilityWrapper(this.state.dashboardRef.resolve());
|
||||
// @ts-expect-error
|
||||
getDashboardSrv().setCurrent(oldDashboardWrapper);
|
||||
|
||||
// Deactivation logic
|
||||
return () => {
|
||||
getUrlSyncManager().cleanUp(this);
|
||||
@ -85,13 +97,15 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
private _commitChanges() {
|
||||
const dashboard = this.state.dashboardRef.resolve();
|
||||
const sourcePanel = this.state.sourcePanelRef.resolve();
|
||||
const panel = this.state.panelRef.resolve();
|
||||
|
||||
const panelMngr = this.state.panelRef.resolve();
|
||||
|
||||
if (!dashboard.state.isEditing) {
|
||||
dashboard.onEnterEditMode();
|
||||
}
|
||||
|
||||
const newState = sceneUtils.cloneSceneObjectState(panel.state);
|
||||
const newState = sceneUtils.cloneSceneObjectState(panelMngr.state.panel.state);
|
||||
|
||||
sourcePanel.setState(newState);
|
||||
|
||||
// preserve time range and variables state
|
||||
@ -107,6 +121,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
getDashboardUrl({
|
||||
uid: this.state.dashboardRef.resolve().state.uid,
|
||||
currentQueryParams: locationService.getLocation().search,
|
||||
useExperimentalURL: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -114,7 +129,8 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
|
||||
export function buildPanelEditScene(dashboard: DashboardScene, panel: VizPanel): PanelEditor {
|
||||
const panelClone = panel.clone();
|
||||
const vizPanelMgr = new VizPanelManager(panelClone);
|
||||
|
||||
const vizPanelMgr = new VizPanelManager(panelClone, dashboard.getRef());
|
||||
const dashboardStateCloned = sceneUtils.cloneSceneObjectState(dashboard.state);
|
||||
|
||||
return new PanelEditor({
|
||||
@ -130,10 +146,10 @@ export function buildPanelEditScene(dashboard: DashboardScene, panel: VizPanel):
|
||||
direction: 'column',
|
||||
primary: new SceneFlexLayout({
|
||||
direction: 'column',
|
||||
children: [panelClone],
|
||||
children: [vizPanelMgr],
|
||||
}),
|
||||
secondary: new SceneFlexItem({
|
||||
body: new PanelDataPane({ panelRef: panelClone.getRef() }),
|
||||
body: new PanelDataPane(vizPanelMgr),
|
||||
}),
|
||||
}),
|
||||
secondary: new SceneFlexLayout({
|
||||
|
@ -13,7 +13,7 @@ import { useSelector } from 'app/types/store';
|
||||
import { PanelEditor } from './PanelEditor';
|
||||
|
||||
export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>) {
|
||||
const { body, controls, drawer } = model.useState();
|
||||
const { body, controls, overlay } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const location = useLocation();
|
||||
const navIndex = useSelector((state) => state.navIndex);
|
||||
@ -34,7 +34,7 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
||||
<body.Component model={body} />
|
||||
</div>
|
||||
</div>
|
||||
{drawer && <drawer.Component model={drawer} />}
|
||||
{overlay && <overlay.Component model={overlay} />}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,49 @@
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { SceneObjectUrlSyncHandler, SceneObjectUrlValues } from '@grafana/scenes';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
|
||||
import { findVizPanelByKey } from '../utils/utils';
|
||||
|
||||
import { PanelEditor, PanelEditorState } from './PanelEditor';
|
||||
|
||||
export class PanelEditorUrlSync implements SceneObjectUrlSyncHandler {
|
||||
constructor(private _scene: PanelEditor) {}
|
||||
|
||||
getKeys(): string[] {
|
||||
return ['inspect'];
|
||||
}
|
||||
|
||||
getUrlState(): SceneObjectUrlValues {
|
||||
const state = this._scene.state;
|
||||
return {
|
||||
inspect: state.inspectPanelKey,
|
||||
};
|
||||
}
|
||||
|
||||
updateFromUrl(values: SceneObjectUrlValues): void {
|
||||
const { inspectPanelKey } = this._scene.state;
|
||||
const update: Partial<PanelEditorState> = {};
|
||||
|
||||
// Handle inspect object state
|
||||
if (typeof values.inspect === 'string') {
|
||||
const panel = findVizPanelByKey(this._scene, values.inspect);
|
||||
if (!panel) {
|
||||
appEvents.emit(AppEvents.alertError, ['Panel not found']);
|
||||
locationService.partial({ inspect: null });
|
||||
return;
|
||||
}
|
||||
|
||||
update.inspectPanelKey = values.inspect;
|
||||
update.overlay = new PanelInspectDrawer({ panelRef: panel.getRef() });
|
||||
} else if (inspectPanelKey) {
|
||||
update.inspectPanelKey = undefined;
|
||||
update.overlay = undefined;
|
||||
}
|
||||
|
||||
if (Object.keys(update).length > 0) {
|
||||
this._scene.setState(update);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,31 +1,178 @@
|
||||
import { FieldConfigSource } from '@grafana/data';
|
||||
import { DeepPartial, SceneQueryRunner, VizPanel } from '@grafana/scenes';
|
||||
import { map, of } from 'rxjs';
|
||||
|
||||
import { DataQueryRequest, DataSourceApi, LoadingState, PanelData } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { SceneDataTransformer, SceneQueryRunner, VizPanel } from '@grafana/scenes';
|
||||
import { DataQuery, DataSourceJsonData, DataSourceRef } from '@grafana/schema';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { InspectTab } from 'app/features/inspector/types';
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange';
|
||||
import { ShareQueryDataProvider } from '../scene/ShareQueryDataProvider';
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper';
|
||||
import { findVizPanelByKey } from '../utils/utils';
|
||||
|
||||
import { VizPanelManager } from './VizPanelManager';
|
||||
import testDashboard from './testfiles/testDashboard.json';
|
||||
|
||||
const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => {
|
||||
const result: PanelData = {
|
||||
state: LoadingState.Loading,
|
||||
series: [],
|
||||
timeRange: request.range,
|
||||
};
|
||||
|
||||
return of([]).pipe(
|
||||
map(() => {
|
||||
result.state = LoadingState.Done;
|
||||
result.series = [];
|
||||
|
||||
return result;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const ds1Mock: DataSourceApi = {
|
||||
meta: {
|
||||
id: 'grafana-testdata-datasource',
|
||||
},
|
||||
name: 'grafana-testdata-datasource',
|
||||
type: 'grafana-testdata-datasource',
|
||||
uid: 'gdev-testdata',
|
||||
getRef: () => {
|
||||
return { type: 'grafana-testdata-datasource', uid: 'gdev-testdata' };
|
||||
},
|
||||
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>;
|
||||
|
||||
const ds2Mock: DataSourceApi = {
|
||||
meta: {
|
||||
id: 'grafana-prometheus-datasource',
|
||||
},
|
||||
name: 'grafana-prometheus-datasource',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
uid: 'gdev-prometheus',
|
||||
getRef: () => {
|
||||
return { type: 'grafana-prometheus-datasource', uid: 'gdev-prometheus' };
|
||||
},
|
||||
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>;
|
||||
|
||||
const ds3Mock: DataSourceApi = {
|
||||
meta: {
|
||||
id: DASHBOARD_DATASOURCE_PLUGIN_ID,
|
||||
},
|
||||
name: SHARED_DASHBOARD_QUERY,
|
||||
type: SHARED_DASHBOARD_QUERY,
|
||||
uid: SHARED_DASHBOARD_QUERY,
|
||||
getRef: () => {
|
||||
return { type: SHARED_DASHBOARD_QUERY, uid: SHARED_DASHBOARD_QUERY };
|
||||
},
|
||||
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>;
|
||||
|
||||
const instance1SettingsMock = {
|
||||
id: 1,
|
||||
uid: 'gdev-testdata',
|
||||
name: 'testDs1',
|
||||
type: 'grafana-testdata-datasource',
|
||||
meta: {
|
||||
id: 'grafana-testdata-datasource',
|
||||
},
|
||||
};
|
||||
|
||||
const instance2SettingsMock = {
|
||||
id: 1,
|
||||
uid: 'gdev-prometheus',
|
||||
name: 'testDs2',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
meta: {
|
||||
id: 'grafana-prometheus-datasource',
|
||||
},
|
||||
};
|
||||
|
||||
// Mock the store module
|
||||
jest.mock('app/core/store', () => ({
|
||||
exists: jest.fn(),
|
||||
get: jest.fn(),
|
||||
getObject: jest.fn(),
|
||||
setObject: jest.fn(),
|
||||
}));
|
||||
|
||||
const store = jest.requireMock('app/core/store');
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
|
||||
return runRequestMock(ds, request);
|
||||
},
|
||||
getDataSourceSrv: () => ({
|
||||
get: async (ref: DataSourceRef) => {
|
||||
if (ref.uid === 'gdev-testdata') {
|
||||
return ds1Mock;
|
||||
}
|
||||
|
||||
if (ref.uid === 'gdev-prometheus') {
|
||||
return ds2Mock;
|
||||
}
|
||||
|
||||
if (ref.uid === SHARED_DASHBOARD_QUERY) {
|
||||
return ds3Mock;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
getInstanceSettings: (ref: DataSourceRef) => {
|
||||
if (ref.uid === 'gdev-testdata') {
|
||||
return instance1SettingsMock;
|
||||
}
|
||||
|
||||
if (ref.uid === 'gdev-prometheus') {
|
||||
return instance2SettingsMock;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
locationService: {
|
||||
partial: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('VizPanelManager', () => {
|
||||
describe('changePluginType', () => {
|
||||
it('Should successfully change from one viz type to another', () => {
|
||||
const vizPanelManager = getVizPanelManager();
|
||||
expect(vizPanelManager.state.panel.state.pluginId).toBe('table');
|
||||
vizPanelManager.changePluginType('timeseries');
|
||||
const vizPanelManager = setupTest('panel-1');
|
||||
expect(vizPanelManager.state.panel.state.pluginId).toBe('timeseries');
|
||||
vizPanelManager.changePluginType('table');
|
||||
expect(vizPanelManager.state.panel.state.pluginId).toBe('table');
|
||||
});
|
||||
|
||||
it('Should clear custom options', () => {
|
||||
const dashboardSceneMock = new DashboardScene({});
|
||||
const overrides = [
|
||||
{
|
||||
matcher: { id: 'matcherOne' },
|
||||
properties: [{ id: 'custom.propertyOne' }, { id: 'custom.propertyTwo' }, { id: 'standardProperty' }],
|
||||
},
|
||||
];
|
||||
const vizPanelManager = getVizPanelManager(undefined, {
|
||||
defaults: {
|
||||
custom: 'Custom',
|
||||
const vizPanel = new VizPanel({
|
||||
title: 'Panel A',
|
||||
key: 'panel-1',
|
||||
pluginId: 'table',
|
||||
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
|
||||
options: undefined,
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
custom: 'Custom',
|
||||
},
|
||||
overrides,
|
||||
},
|
||||
overrides,
|
||||
});
|
||||
|
||||
const vizPanelManager = new VizPanelManager(vizPanel, dashboardSceneMock.getRef());
|
||||
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.custom).toBe('Custom');
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.overrides).toBe(overrides);
|
||||
|
||||
@ -37,14 +184,21 @@ describe('VizPanelManager', () => {
|
||||
});
|
||||
|
||||
it('Should restore cached options/fieldConfig if they exist', () => {
|
||||
const vizPanelManager = getVizPanelManager(
|
||||
{
|
||||
const dashboardSceneMock = new DashboardScene({});
|
||||
const vizPanel = new VizPanel({
|
||||
title: 'Panel A',
|
||||
key: 'panel-1',
|
||||
pluginId: 'table',
|
||||
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
|
||||
options: {
|
||||
customOption: 'A',
|
||||
},
|
||||
{ defaults: { custom: 'Custom' }, overrides: [] }
|
||||
);
|
||||
fieldConfig: { defaults: { custom: 'Custom' }, overrides: [] },
|
||||
});
|
||||
|
||||
vizPanelManager.changePluginType('timeseries');
|
||||
const vizPanelManager = new VizPanelManager(vizPanel, dashboardSceneMock.getRef());
|
||||
|
||||
vizPanelManager.changePluginType('timeseties');
|
||||
//@ts-ignore
|
||||
expect(vizPanelManager.state.panel.state.options['customOption']).toBeUndefined();
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.custom).toStrictEqual({});
|
||||
@ -56,22 +210,412 @@ describe('VizPanelManager', () => {
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.custom).toBe('Custom');
|
||||
});
|
||||
});
|
||||
|
||||
describe('query options', () => {
|
||||
beforeEach(() => {
|
||||
store.setObject.mockClear();
|
||||
});
|
||||
|
||||
describe('activation', () => {
|
||||
it('should load data source', async () => {
|
||||
const vizPanelManager = setupTest('panel-1');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(vizPanelManager.state.datasource).toEqual(ds1Mock);
|
||||
expect(vizPanelManager.state.dsSettings).toEqual(instance1SettingsMock);
|
||||
});
|
||||
|
||||
it('should store loaded data source in local storage', async () => {
|
||||
const vizPanelManager = setupTest('panel-1');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(store.setObject).toHaveBeenCalledWith('grafana.dashboards.panelEdit.lastUsedDatasource', {
|
||||
dashboardUid: 'ffbe00e2-803c-4d49-adb7-41aad336234f',
|
||||
datasourceUid: 'gdev-testdata',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('data source change', () => {
|
||||
it('should load new data source', async () => {
|
||||
const vizPanelManager = setupTest('panel-1');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
const dataObj = vizPanelManager.queryRunner;
|
||||
dataObj.setState({
|
||||
datasource: {
|
||||
type: 'grafana-prometheus-datasource',
|
||||
uid: 'gdev-prometheus',
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
expect(store.setObject).toHaveBeenCalledTimes(2);
|
||||
expect(store.setObject).toHaveBeenLastCalledWith('grafana.dashboards.panelEdit.lastUsedDatasource', {
|
||||
dashboardUid: 'ffbe00e2-803c-4d49-adb7-41aad336234f',
|
||||
datasourceUid: 'gdev-prometheus',
|
||||
});
|
||||
|
||||
expect(vizPanelManager.state.datasource).toEqual(ds2Mock);
|
||||
expect(vizPanelManager.state.dsSettings).toEqual(instance2SettingsMock);
|
||||
});
|
||||
});
|
||||
|
||||
describe('query options change', () => {
|
||||
describe('time overrides', () => {
|
||||
it('should create PanelTimeRange object', async () => {
|
||||
const vizPanelManager = setupTest('panel-1');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
const panel = vizPanelManager.state.panel;
|
||||
|
||||
expect(panel.state.$timeRange).toBeUndefined();
|
||||
|
||||
vizPanelManager.changeQueryOptions({
|
||||
dataSource: {
|
||||
name: 'grafana-testdata',
|
||||
type: 'grafana-testdata-datasource',
|
||||
default: true,
|
||||
},
|
||||
queries: [],
|
||||
timeRange: {
|
||||
from: '1h',
|
||||
},
|
||||
});
|
||||
|
||||
expect(panel.state.$timeRange).toBeInstanceOf(PanelTimeRange);
|
||||
});
|
||||
it('should update PanelTimeRange object on time options update', async () => {
|
||||
const vizPanelManager = setupTest('panel-1');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
const panel = vizPanelManager.state.panel;
|
||||
|
||||
expect(panel.state.$timeRange).toBeUndefined();
|
||||
|
||||
vizPanelManager.changeQueryOptions({
|
||||
dataSource: {
|
||||
name: 'grafana-testdata',
|
||||
type: 'grafana-testdata-datasource',
|
||||
default: true,
|
||||
},
|
||||
queries: [],
|
||||
timeRange: {
|
||||
from: '1h',
|
||||
},
|
||||
});
|
||||
|
||||
expect(panel.state.$timeRange).toBeInstanceOf(PanelTimeRange);
|
||||
expect((panel.state.$timeRange?.state as PanelTimeRangeState).timeFrom).toBe('1h');
|
||||
|
||||
vizPanelManager.changeQueryOptions({
|
||||
dataSource: {
|
||||
name: 'grafana-testdata',
|
||||
type: 'grafana-testdata-datasource',
|
||||
default: true,
|
||||
},
|
||||
queries: [],
|
||||
timeRange: {
|
||||
from: '2h',
|
||||
},
|
||||
});
|
||||
|
||||
expect((panel.state.$timeRange?.state as PanelTimeRangeState).timeFrom).toBe('2h');
|
||||
});
|
||||
|
||||
it('should remove PanelTimeRange object on time options cleared', async () => {
|
||||
const vizPanelManager = setupTest('panel-1');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
const panel = vizPanelManager.state.panel;
|
||||
|
||||
expect(panel.state.$timeRange).toBeUndefined();
|
||||
|
||||
vizPanelManager.changeQueryOptions({
|
||||
dataSource: {
|
||||
name: 'grafana-testdata',
|
||||
type: 'grafana-testdata-datasource',
|
||||
default: true,
|
||||
},
|
||||
queries: [],
|
||||
timeRange: {
|
||||
from: '1h',
|
||||
},
|
||||
});
|
||||
|
||||
expect(panel.state.$timeRange).toBeInstanceOf(PanelTimeRange);
|
||||
|
||||
vizPanelManager.changeQueryOptions({
|
||||
dataSource: {
|
||||
name: 'grafana-testdata',
|
||||
type: 'grafana-testdata-datasource',
|
||||
default: true,
|
||||
},
|
||||
queries: [],
|
||||
timeRange: {
|
||||
from: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(panel.state.$timeRange).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('max data points and interval', () => {
|
||||
it('max data points', async () => {
|
||||
const vizPanelManager = setupTest('panel-1');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
const dataObj = vizPanelManager.queryRunner;
|
||||
|
||||
expect(dataObj.state.maxDataPoints).toBeUndefined();
|
||||
|
||||
vizPanelManager.changeQueryOptions({
|
||||
dataSource: {
|
||||
name: 'grafana-testdata',
|
||||
type: 'grafana-testdata-datasource',
|
||||
default: true,
|
||||
},
|
||||
queries: [],
|
||||
maxDataPoints: 100,
|
||||
});
|
||||
|
||||
expect(dataObj.state.maxDataPoints).toBe(100);
|
||||
});
|
||||
|
||||
it('max data points', async () => {
|
||||
const vizPanelManager = setupTest('panel-1');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
const dataObj = vizPanelManager.queryRunner;
|
||||
|
||||
expect(dataObj.state.maxDataPoints).toBeUndefined();
|
||||
|
||||
vizPanelManager.changeQueryOptions({
|
||||
dataSource: {
|
||||
name: 'grafana-testdata',
|
||||
type: 'grafana-testdata-datasource',
|
||||
default: true,
|
||||
},
|
||||
queries: [],
|
||||
minInterval: '1s',
|
||||
});
|
||||
|
||||
expect(dataObj.state.minInterval).toBe('1s');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('query inspection', () => {
|
||||
it('allows query inspection from the tab', async () => {
|
||||
const vizPanelManager = setupTest('panel-1');
|
||||
vizPanelManager.inspectPanel();
|
||||
|
||||
expect(locationService.partial).toHaveBeenCalledWith({ inspect: 1, inspectTab: InspectTab.Query });
|
||||
});
|
||||
});
|
||||
|
||||
describe('data source change', () => {
|
||||
it('changing from one plugin to another', async () => {
|
||||
const vizPanelManager = setupTest('panel-1');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
const panel = vizPanelManager.state.panel;
|
||||
|
||||
expect(panel.state.$data).toBeInstanceOf(SceneQueryRunner);
|
||||
|
||||
expect((panel.state.$data as SceneQueryRunner).state.datasource).toEqual({
|
||||
uid: 'gdev-testdata',
|
||||
type: 'grafana-testdata-datasource',
|
||||
});
|
||||
|
||||
await vizPanelManager.changePanelDataSource({
|
||||
name: 'grafana-prometheus',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
uid: 'gdev-prometheus',
|
||||
meta: {
|
||||
name: 'Prometheus',
|
||||
module: 'prometheus',
|
||||
id: 'grafana-prometheus-datasource',
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect((panel.state.$data as SceneQueryRunner).state.datasource).toEqual({
|
||||
uid: 'gdev-prometheus',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
});
|
||||
});
|
||||
|
||||
it('changing from a plugin to a dashboard data source', async () => {
|
||||
const vizPanelManager = setupTest('panel-1');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
const panel = vizPanelManager.state.panel;
|
||||
|
||||
expect(panel.state.$data).toBeInstanceOf(SceneQueryRunner);
|
||||
|
||||
expect((panel.state.$data as SceneQueryRunner).state.datasource).toEqual({
|
||||
uid: 'gdev-testdata',
|
||||
type: 'grafana-testdata-datasource',
|
||||
});
|
||||
|
||||
await vizPanelManager.changePanelDataSource({
|
||||
name: SHARED_DASHBOARD_QUERY,
|
||||
type: 'datasource',
|
||||
uid: SHARED_DASHBOARD_QUERY,
|
||||
meta: {
|
||||
name: 'Prometheus',
|
||||
module: 'prometheus',
|
||||
id: DASHBOARD_DATASOURCE_PLUGIN_ID,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(panel.state.$data).toBeInstanceOf(ShareQueryDataProvider);
|
||||
});
|
||||
|
||||
it('changing from dashboard data source to a plugin', async () => {
|
||||
const vizPanelManager = setupTest('panel-3');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
const panel = vizPanelManager.state.panel;
|
||||
|
||||
expect(panel.state.$data).toBeInstanceOf(ShareQueryDataProvider);
|
||||
|
||||
await vizPanelManager.changePanelDataSource({
|
||||
name: 'grafana-prometheus',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
uid: 'gdev-prometheus',
|
||||
meta: {
|
||||
name: 'Prometheus',
|
||||
module: 'prometheus',
|
||||
id: 'grafana-prometheus-datasource',
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(panel.state.$data).toBeInstanceOf(SceneQueryRunner);
|
||||
expect((panel.state.$data as SceneQueryRunner).state.datasource).toEqual({
|
||||
uid: 'gdev-prometheus',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
});
|
||||
});
|
||||
|
||||
describe('with transformations', () => {
|
||||
it('changing from one plugin to another', async () => {
|
||||
const vizPanelManager = setupTest('panel-2');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
const panel = vizPanelManager.state.panel;
|
||||
|
||||
expect(panel.state.$data).toBeInstanceOf(SceneDataTransformer);
|
||||
|
||||
expect((panel.state.$data?.state.$data as SceneQueryRunner).state.datasource).toEqual({
|
||||
uid: 'gdev-testdata',
|
||||
type: 'grafana-testdata-datasource',
|
||||
});
|
||||
|
||||
await vizPanelManager.changePanelDataSource({
|
||||
name: 'grafana-prometheus',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
uid: 'gdev-prometheus',
|
||||
meta: {
|
||||
name: 'Prometheus',
|
||||
module: 'prometheus',
|
||||
id: 'grafana-prometheus-datasource',
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(panel.state.$data).toBeInstanceOf(SceneDataTransformer);
|
||||
expect((panel.state.$data?.state.$data as SceneQueryRunner).state.datasource).toEqual({
|
||||
uid: 'gdev-prometheus',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('changing from a plugin to dashboard data source', async () => {
|
||||
const vizPanelManager = setupTest('panel-2');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
const panel = vizPanelManager.state.panel;
|
||||
|
||||
expect(panel.state.$data).toBeInstanceOf(SceneDataTransformer);
|
||||
|
||||
expect((panel.state.$data?.state.$data as SceneQueryRunner).state.datasource).toEqual({
|
||||
uid: 'gdev-testdata',
|
||||
type: 'grafana-testdata-datasource',
|
||||
});
|
||||
|
||||
await vizPanelManager.changePanelDataSource({
|
||||
name: SHARED_DASHBOARD_QUERY,
|
||||
type: 'datasource',
|
||||
uid: SHARED_DASHBOARD_QUERY,
|
||||
meta: {
|
||||
name: 'Prometheus',
|
||||
module: 'prometheus',
|
||||
id: DASHBOARD_DATASOURCE_PLUGIN_ID,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(panel.state.$data).toBeInstanceOf(SceneDataTransformer);
|
||||
expect(panel.state.$data?.state.$data).toBeInstanceOf(ShareQueryDataProvider);
|
||||
});
|
||||
|
||||
it('changing from a dashboard data source to a plugin', async () => {
|
||||
const vizPanelManager = setupTest('panel-4');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
const panel = vizPanelManager.state.panel;
|
||||
|
||||
expect(panel.state.$data).toBeInstanceOf(SceneDataTransformer);
|
||||
expect(panel.state.$data?.state.$data).toBeInstanceOf(ShareQueryDataProvider);
|
||||
|
||||
await vizPanelManager.changePanelDataSource({
|
||||
name: 'grafana-prometheus',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
uid: 'gdev-prometheus',
|
||||
meta: {
|
||||
name: 'Prometheus',
|
||||
module: 'prometheus',
|
||||
id: 'grafana-prometheus-datasource',
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(panel.state.$data).toBeInstanceOf(SceneDataTransformer);
|
||||
expect(panel.state.$data?.state.$data).toBeInstanceOf(SceneQueryRunner);
|
||||
expect((panel.state.$data?.state.$data as SceneQueryRunner).state.datasource).toEqual({
|
||||
uid: 'gdev-prometheus',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getVizPanelManager(
|
||||
options: {} = {},
|
||||
fieldConfig: FieldConfigSource<DeepPartial<{}>> = { overrides: [], defaults: {} }
|
||||
) {
|
||||
const vizPanel = new VizPanel({
|
||||
title: 'Panel A',
|
||||
key: 'panel-1',
|
||||
pluginId: 'table',
|
||||
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
|
||||
options,
|
||||
fieldConfig,
|
||||
});
|
||||
const setupTest = (panelId: string) => {
|
||||
const scene = transformSaveModelToScene({ dashboard: testDashboard as any, meta: {} });
|
||||
|
||||
const vizPanelManager = new VizPanelManager(vizPanel);
|
||||
// The following happens on DahsboardScene activation. For the needs of this test this activation aint needed hence we hand-call it
|
||||
// @ts-expect-error
|
||||
getDashboardSrv().setCurrent(new DashboardModelCompatibilityWrapper(scene));
|
||||
|
||||
const panel = findVizPanelByKey(scene, panelId)!;
|
||||
|
||||
const vizPanelManager = new VizPanelManager(panel.clone(), scene.getRef());
|
||||
|
||||
return vizPanelManager;
|
||||
}
|
||||
};
|
||||
|
@ -1,12 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
|
||||
import {
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
FieldConfigSource,
|
||||
LoadingState,
|
||||
PanelModel,
|
||||
filterFieldConfigOverrides,
|
||||
getDefaultTimeRange,
|
||||
isStandardFieldProp,
|
||||
restoreCustomOverrideRules,
|
||||
} from '@grafana/data';
|
||||
import { getDataSourceSrv, locationService } from '@grafana/runtime';
|
||||
import {
|
||||
SceneObjectState,
|
||||
VizPanel,
|
||||
@ -14,21 +20,143 @@ import {
|
||||
SceneComponentProps,
|
||||
sceneUtils,
|
||||
DeepPartial,
|
||||
SceneObjectRef,
|
||||
SceneObject,
|
||||
SceneQueryRunner,
|
||||
sceneGraph,
|
||||
SceneDataTransformer,
|
||||
SceneDataProvider,
|
||||
} from '@grafana/scenes';
|
||||
import { DataQuery, DataSourceRef } from '@grafana/schema';
|
||||
import { getPluginVersion } from 'app/features/dashboard/state/PanelModel';
|
||||
import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils';
|
||||
import { updateQueries } from 'app/features/query/state/updateQueries';
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
||||
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
|
||||
import { QueryGroupOptions } from 'app/types';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange';
|
||||
import { ShareQueryDataProvider, findObjectInScene } from '../scene/ShareQueryDataProvider';
|
||||
import { getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../utils/utils';
|
||||
|
||||
interface VizPanelManagerState extends SceneObjectState {
|
||||
panel: VizPanel;
|
||||
datasource?: DataSourceApi;
|
||||
dsSettings?: DataSourceInstanceSettings;
|
||||
}
|
||||
|
||||
// VizPanelManager serves as an API to manipulate VizPanel state from the outside. It allows panel type, options and data maniulation.
|
||||
export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
public static Component = ({ model }: SceneComponentProps<VizPanelManager>) => {
|
||||
const { panel } = model.useState();
|
||||
|
||||
return <panel.Component model={panel} />;
|
||||
};
|
||||
|
||||
private _cachedPluginOptions: Record<
|
||||
string,
|
||||
{ options: DeepPartial<{}>; fieldConfig: FieldConfigSource<DeepPartial<{}>> } | undefined
|
||||
> = {};
|
||||
|
||||
public constructor(panel: VizPanel) {
|
||||
private _dataObjectSubscription: Unsubscribable | undefined;
|
||||
|
||||
public constructor(panel: VizPanel, dashboardRef: SceneObjectRef<DashboardScene>) {
|
||||
super({ panel });
|
||||
|
||||
/**
|
||||
* If the panel uses a shared query, we clone the source runner and attach it as a data provider for the shared one.
|
||||
* This way the source panel does not to be present in the edit scene hierarchy.
|
||||
*/
|
||||
if (panel.state.$data instanceof ShareQueryDataProvider) {
|
||||
const sharedProvider = panel.state.$data;
|
||||
if (sharedProvider.state.query.panelId) {
|
||||
const keyToFind = getVizPanelKeyForPanelId(sharedProvider.state.query.panelId);
|
||||
const source = findObjectInScene(dashboardRef.resolve(), (scene: SceneObject) => scene.state.key === keyToFind);
|
||||
if (source) {
|
||||
sharedProvider.setState({
|
||||
$data: source.state.$data!.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.addActivationHandler(() => this._onActivate());
|
||||
}
|
||||
|
||||
private _onActivate() {
|
||||
this.setupDataObjectSubscription();
|
||||
|
||||
this.loadDataSource();
|
||||
|
||||
return () => {
|
||||
this._dataObjectSubscription?.unsubscribe();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The subscription is updated whenever the data source type is changed so that we can update manager's stored
|
||||
* data source and data source instance settings, which are needed for the query options and editors
|
||||
*/
|
||||
private setupDataObjectSubscription() {
|
||||
const runner = this.queryRunner;
|
||||
|
||||
if (this._dataObjectSubscription) {
|
||||
this._dataObjectSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
this._dataObjectSubscription = runner.subscribeToState((n, p) => {
|
||||
if (n.datasource !== p.datasource) {
|
||||
this.loadDataSource();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async loadDataSource() {
|
||||
const dataObj = this.state.panel.state.$data;
|
||||
|
||||
if (!dataObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
let datasourceToLoad: DataSourceRef | undefined;
|
||||
|
||||
if (dataObj instanceof ShareQueryDataProvider) {
|
||||
datasourceToLoad = {
|
||||
uid: SHARED_DASHBOARD_QUERY,
|
||||
type: DASHBOARD_DATASOURCE_PLUGIN_ID,
|
||||
};
|
||||
} else {
|
||||
datasourceToLoad = this.queryRunner.state.datasource;
|
||||
}
|
||||
|
||||
if (!datasourceToLoad) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Handle default/last used datasource selection for new panel
|
||||
// Ref: PanelEditorQueries / componentDidMount
|
||||
const datasource = await getDataSourceSrv().get(datasourceToLoad);
|
||||
const dsSettings = getDataSourceSrv().getInstanceSettings(datasourceToLoad);
|
||||
|
||||
if (datasource && dsSettings) {
|
||||
this.setState({
|
||||
datasource,
|
||||
dsSettings,
|
||||
});
|
||||
|
||||
storeLastUsedDataSourceInLocalStorage(
|
||||
{
|
||||
type: dsSettings.type,
|
||||
uid: dsSettings.uid,
|
||||
} || { default: true }
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
public changePluginType(pluginType: string) {
|
||||
@ -79,11 +207,172 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
}
|
||||
|
||||
this.setState({ panel: newPanel });
|
||||
this.setupDataObjectSubscription();
|
||||
}
|
||||
|
||||
public static Component = ({ model }: SceneComponentProps<VizPanelManager>) => {
|
||||
const { panel } = model.useState();
|
||||
public async changePanelDataSource(
|
||||
newSettings: DataSourceInstanceSettings,
|
||||
defaultQueries?: DataQuery[] | GrafanaQuery[]
|
||||
) {
|
||||
const { panel, dsSettings } = this.state;
|
||||
const dataObj = panel.state.$data;
|
||||
if (!dataObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <panel.Component model={panel} />;
|
||||
};
|
||||
const currentDS = dsSettings ? await getDataSourceSrv().get({ uid: dsSettings.uid }) : undefined;
|
||||
const nextDS = await getDataSourceSrv().get({ uid: newSettings.uid });
|
||||
|
||||
const currentQueries = [];
|
||||
if (dataObj instanceof SceneQueryRunner) {
|
||||
currentQueries.push(...dataObj.state.queries);
|
||||
} else if (dataObj instanceof ShareQueryDataProvider) {
|
||||
currentQueries.push(dataObj.state.query);
|
||||
}
|
||||
|
||||
// We need to pass in newSettings.uid as well here as that can be a variable expression and we want to store that in the query model not the current ds variable value
|
||||
const queries = defaultQueries || (await updateQueries(nextDS, newSettings.uid, currentQueries, currentDS));
|
||||
|
||||
if (dataObj instanceof SceneQueryRunner) {
|
||||
// Changing to Dashboard data source
|
||||
if (newSettings.uid === SHARED_DASHBOARD_QUERY) {
|
||||
// Changing from one plugin to another
|
||||
const sharedProvider = new ShareQueryDataProvider({
|
||||
query: queries[0],
|
||||
$data: new SceneQueryRunner({
|
||||
queries: [],
|
||||
}),
|
||||
data: {
|
||||
series: [],
|
||||
state: LoadingState.NotStarted,
|
||||
timeRange: getDefaultTimeRange(),
|
||||
},
|
||||
});
|
||||
panel.setState({ $data: sharedProvider });
|
||||
this.setupDataObjectSubscription();
|
||||
this.loadDataSource();
|
||||
} else {
|
||||
dataObj.setState({
|
||||
datasource: {
|
||||
type: newSettings.type,
|
||||
uid: newSettings.uid,
|
||||
},
|
||||
queries,
|
||||
});
|
||||
if (defaultQueries) {
|
||||
dataObj.runQueries();
|
||||
}
|
||||
}
|
||||
} else if (dataObj instanceof ShareQueryDataProvider && newSettings.uid !== SHARED_DASHBOARD_QUERY) {
|
||||
const dataProvider = new SceneQueryRunner({
|
||||
datasource: {
|
||||
type: newSettings.type,
|
||||
uid: newSettings.uid,
|
||||
},
|
||||
queries,
|
||||
});
|
||||
panel.setState({ $data: dataProvider });
|
||||
this.setupDataObjectSubscription();
|
||||
this.loadDataSource();
|
||||
} else if (dataObj instanceof SceneDataTransformer) {
|
||||
const data = dataObj.clone();
|
||||
|
||||
let provider: SceneDataProvider = new SceneQueryRunner({
|
||||
datasource: {
|
||||
type: newSettings.type,
|
||||
uid: newSettings.uid,
|
||||
},
|
||||
queries,
|
||||
});
|
||||
|
||||
if (newSettings.uid === SHARED_DASHBOARD_QUERY) {
|
||||
provider = new ShareQueryDataProvider({
|
||||
query: queries[0],
|
||||
$data: new SceneQueryRunner({
|
||||
queries: [],
|
||||
}),
|
||||
data: {
|
||||
series: [],
|
||||
state: LoadingState.NotStarted,
|
||||
timeRange: getDefaultTimeRange(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
data.setState({
|
||||
$data: provider,
|
||||
});
|
||||
|
||||
panel.setState({ $data: data });
|
||||
|
||||
this.setupDataObjectSubscription();
|
||||
this.loadDataSource();
|
||||
}
|
||||
}
|
||||
|
||||
public changeQueryOptions(options: QueryGroupOptions) {
|
||||
const panelObj = this.state.panel;
|
||||
const dataObj = this.queryRunner;
|
||||
let timeRangeObj = sceneGraph.getTimeRange(panelObj);
|
||||
|
||||
const dataObjStateUpdate: Partial<SceneQueryRunner['state']> = {};
|
||||
const timeRangeObjStateUpdate: Partial<PanelTimeRangeState> = {};
|
||||
|
||||
if (options.maxDataPoints !== dataObj.state.maxDataPoints) {
|
||||
dataObjStateUpdate.maxDataPoints = options.maxDataPoints ?? undefined;
|
||||
}
|
||||
if (options.minInterval !== dataObj.state.minInterval && options.minInterval !== null) {
|
||||
dataObjStateUpdate.minInterval = options.minInterval;
|
||||
}
|
||||
if (options.timeRange) {
|
||||
timeRangeObjStateUpdate.timeFrom = options.timeRange.from ?? undefined;
|
||||
timeRangeObjStateUpdate.timeShift = options.timeRange.shift ?? undefined;
|
||||
timeRangeObjStateUpdate.hideTimeOverride = options.timeRange.hide;
|
||||
}
|
||||
if (timeRangeObj instanceof PanelTimeRange) {
|
||||
if (timeRangeObjStateUpdate.timeFrom !== undefined || timeRangeObjStateUpdate.timeShift !== undefined) {
|
||||
// update time override
|
||||
timeRangeObj.setState(timeRangeObjStateUpdate);
|
||||
} else {
|
||||
// remove time override
|
||||
panelObj.setState({ $timeRange: undefined });
|
||||
}
|
||||
} else {
|
||||
// no time override present on the panel, let's create one first
|
||||
panelObj.setState({ $timeRange: new PanelTimeRange(timeRangeObjStateUpdate) });
|
||||
}
|
||||
|
||||
dataObj.setState(dataObjStateUpdate);
|
||||
dataObj.runQueries();
|
||||
}
|
||||
|
||||
public changeQueries(queries: DataQuery[]) {
|
||||
const dataObj = this.queryRunner;
|
||||
dataObj.setState({ queries });
|
||||
// TODO: Handle dashboard query
|
||||
}
|
||||
|
||||
public inspectPanel() {
|
||||
const panel = this.state.panel;
|
||||
const panelId = getPanelIdForVizPanel(panel);
|
||||
|
||||
locationService.partial({
|
||||
inspect: panelId,
|
||||
inspectTab: 'query',
|
||||
});
|
||||
}
|
||||
|
||||
get queryRunner(): SceneQueryRunner {
|
||||
const dataObj = this.state.panel.state.$data;
|
||||
|
||||
if (dataObj instanceof ShareQueryDataProvider) {
|
||||
return dataObj.state.$data as SceneQueryRunner;
|
||||
}
|
||||
|
||||
if (dataObj instanceof SceneDataTransformer) {
|
||||
return dataObj.state.$data as SceneQueryRunner;
|
||||
}
|
||||
|
||||
return dataObj as SceneQueryRunner;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,372 @@
|
||||
{
|
||||
"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": 2378,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": 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": 1,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk",
|
||||
"seriesCount": 1
|
||||
}
|
||||
],
|
||||
"title": "Source panel",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-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": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "10.3.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "gdev-testdata"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk",
|
||||
"seriesCount": 1
|
||||
}
|
||||
],
|
||||
"title": "Panel with transforms",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "reduce",
|
||||
"options": {}
|
||||
}
|
||||
],
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": 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": 8
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Dashboard query",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"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": 12,
|
||||
"x": 12,
|
||||
"y": 8
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "10.3.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Dashboard query with transformations",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "reduce",
|
||||
"options": {}
|
||||
}
|
||||
],
|
||||
"type": "table"
|
||||
}
|
||||
],
|
||||
"refresh": "",
|
||||
"schemaVersion": 39,
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Scenes/PanelEdit/Queries: Edit",
|
||||
"uid": "ffbe00e2-803c-4d49-adb7-41aad336234f",
|
||||
"version": 6,
|
||||
"weekStart": ""
|
||||
}
|
@ -12,7 +12,7 @@ import {
|
||||
import { Icon, PanelChrome, TimePickerTooltip, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { TimeOverrideResult } from 'app/features/dashboard/utils/panel';
|
||||
|
||||
interface PanelTimeRangeState extends SceneTimeRangeState {
|
||||
export interface PanelTimeRangeState extends SceneTimeRangeState {
|
||||
timeFrom?: string;
|
||||
timeShift?: string;
|
||||
hideTimeOverride?: boolean;
|
||||
@ -30,8 +30,21 @@ export class PanelTimeRange extends SceneTimeRangeTransformerBase<PanelTimeRange
|
||||
to: 'now',
|
||||
value: getDefaultTimeRange(),
|
||||
});
|
||||
|
||||
this.addActivationHandler(() => this._onActivate());
|
||||
}
|
||||
|
||||
private _onActivate() {
|
||||
this._subs.add(
|
||||
this.subscribeToState((n, p) => {
|
||||
// Listen to own changes and update time info when required
|
||||
if (n.timeFrom !== p.timeFrom || n.timeShift !== p.timeShift) {
|
||||
const { timeInfo } = this.getTimeOverride(this.getAncestorTimeRange().state.value);
|
||||
this.setState({ timeInfo });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
protected ancestorTimeRangeChanged(timeRange: SceneTimeRangeState): void {
|
||||
const overrideResult = this.getTimeOverride(timeRange.value);
|
||||
this.setState({ value: overrideResult.timeRange, timeInfo: overrideResult.timeInfo });
|
||||
|
@ -56,22 +56,27 @@ export class ShareQueryDataProvider extends SceneObjectBase<ShareQueryDataProvid
|
||||
this._querySub.unsubscribe();
|
||||
}
|
||||
|
||||
if (!query.panelId) {
|
||||
return;
|
||||
}
|
||||
if (this.state.$data) {
|
||||
this._sourceProvider = this.state.$data;
|
||||
this._passContainerWidth = true;
|
||||
} else {
|
||||
if (!query.panelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keyToFind = getVizPanelKeyForPanelId(query.panelId);
|
||||
const source = findObjectInScene(this.getRoot(), (scene: SceneObject) => scene.state.key === keyToFind);
|
||||
const keyToFind = getVizPanelKeyForPanelId(query.panelId);
|
||||
const source = findObjectInScene(this.getRoot(), (scene: SceneObject) => scene.state.key === keyToFind);
|
||||
|
||||
if (!source) {
|
||||
console.log('Shared dashboard query refers to a panel that does not exist in the scene');
|
||||
return;
|
||||
}
|
||||
if (!source) {
|
||||
console.log('Shared dashboard query refers to a panel that does not exist in the scene');
|
||||
return;
|
||||
}
|
||||
|
||||
this._sourceProvider = source.state.$data;
|
||||
if (!this._sourceProvider) {
|
||||
console.log('No source data found for shared dashboard query');
|
||||
return;
|
||||
this._sourceProvider = source.state.$data;
|
||||
if (!this._sourceProvider) {
|
||||
console.log('No source data found for shared dashboard query');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If the source is not active we need to pass the container width
|
||||
|
@ -2,18 +2,14 @@ import React, { PureComponent } from 'react';
|
||||
|
||||
import { DataQuery, getDataSourceRef } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { QueryGroup } from 'app/features/query/components/QueryGroup';
|
||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||
import { QueryGroupDataSource, QueryGroupOptions } from 'app/types';
|
||||
|
||||
import { getDashboardSrv } from '../../services/DashboardSrv';
|
||||
import { PanelModel } from '../../state';
|
||||
import {
|
||||
getLastUsedDatasourceFromStorage,
|
||||
initLastUsedDatasourceKeyForDashboard,
|
||||
setLastUsedDatasourceKeyForDashboard,
|
||||
} from '../../utils/dashboard';
|
||||
import { getLastUsedDatasourceFromStorage } from '../../utils/dashboard';
|
||||
|
||||
interface Props {
|
||||
/** Current panel */
|
||||
@ -29,16 +25,7 @@ export class PanelEditorQueries extends PureComponent<Props> {
|
||||
|
||||
// store last used datasource in local storage
|
||||
updateLastUsedDatasource = (datasource: QueryGroupDataSource) => {
|
||||
if (!datasource.uid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dashboardUid = getDashboardSrv().getCurrent()?.uid ?? '';
|
||||
// if datasource is MIXED reset datasource uid in storage, because Mixed datasource can contain multiple ds
|
||||
if (datasource.uid === MIXED_DATASOURCE_NAME) {
|
||||
return initLastUsedDatasourceKeyForDashboard(dashboardUid!);
|
||||
}
|
||||
setLastUsedDatasourceKeyForDashboard(dashboardUid, datasource.uid);
|
||||
storeLastUsedDataSourceInLocalStorage(datasource);
|
||||
};
|
||||
|
||||
buildQueryOptions(panel: PanelModel): QueryGroupOptions {
|
||||
|
@ -1,4 +1,11 @@
|
||||
import { DataSourceInstanceSettings, DataSourceJsonData, DataSourceRef } from '@grafana/data';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import {
|
||||
initLastUsedDatasourceKeyForDashboard,
|
||||
setLastUsedDatasourceKeyForDashboard,
|
||||
} from 'app/features/dashboard/utils/dashboard';
|
||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||
import { QueryGroupDataSource } from 'app/types';
|
||||
|
||||
export function isDataSourceMatch(
|
||||
ds: DataSourceInstanceSettings | undefined,
|
||||
@ -97,3 +104,17 @@ export function getDataSourceCompareFn(
|
||||
export function matchDataSourceWithSearch(ds: DataSourceInstanceSettings, searchTerm = '') {
|
||||
return ds.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
}
|
||||
|
||||
export function storeLastUsedDataSourceInLocalStorage(datasource: QueryGroupDataSource) {
|
||||
if (!datasource.uid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dashboardUid = getDashboardSrv().getCurrent()?.uid ?? '';
|
||||
// if datasource is MIXED reset datasource uid in storage, because Mixed datasource can contain multiple ds
|
||||
if (datasource.uid === MIXED_DATASOURCE_NAME) {
|
||||
return initLastUsedDatasourceKeyForDashboard(dashboardUid!);
|
||||
}
|
||||
|
||||
setLastUsedDatasourceKeyForDashboard(dashboardUid, datasource.uid);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { PureComponent, useEffect, useState } from 'react';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
|
||||
import {
|
||||
@ -84,11 +84,6 @@ export class QueryGroup extends PureComponent<Props, State> {
|
||||
});
|
||||
|
||||
this.setNewQueriesAndDatasource(options);
|
||||
|
||||
// Clean up the first panel flag since the modal is now open
|
||||
if (!!locationService.getSearchObject().firstPanel) {
|
||||
locationService.partial({ firstPanel: null }, true);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -213,58 +208,21 @@ export class QueryGroup extends PureComponent<Props, State> {
|
||||
|
||||
renderTopSection(styles: QueriesTabStyles) {
|
||||
const { onOpenQueryInspector, options } = this.props;
|
||||
const { dataSource, data } = this.state;
|
||||
const { dataSource, data, dsSettings } = this.state;
|
||||
|
||||
if (!dsSettings || !dataSource) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.dataSourceRow}>
|
||||
<InlineFormLabel htmlFor="data-source-picker" width={'auto'}>
|
||||
Data source
|
||||
</InlineFormLabel>
|
||||
<div className={styles.dataSourceRowItem}>{this.renderDataSourcePickerWithPrompt()}</div>
|
||||
{dataSource && (
|
||||
<>
|
||||
<div className={styles.dataSourceRowItem}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="question-circle"
|
||||
title="Open data source help"
|
||||
onClick={this.onOpenHelp}
|
||||
data-testid="query-tab-help-button"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.dataSourceRowItemOptions}>
|
||||
<QueryGroupOptionsEditor
|
||||
options={options}
|
||||
dataSource={dataSource}
|
||||
data={data}
|
||||
onChange={this.onUpdateAndRun}
|
||||
/>
|
||||
</div>
|
||||
{onOpenQueryInspector && (
|
||||
<div className={styles.dataSourceRowItem}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onOpenQueryInspector}
|
||||
aria-label={selectors.components.QueryTab.queryInspectorButton}
|
||||
>
|
||||
Query inspector
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{dataSource && isAngularDatasourcePluginAndNotHidden(dataSource.uid) && (
|
||||
<AngularDeprecationPluginNotice
|
||||
pluginId={dataSource.type}
|
||||
pluginType={PluginType.datasource}
|
||||
angularSupportEnabled={config?.angularSupportEnabled}
|
||||
showPluginDetailsLink={true}
|
||||
interactionElementId="datasource-query"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<QueryGroupTopSection
|
||||
data={data}
|
||||
dataSource={dataSource}
|
||||
options={options}
|
||||
dsSettings={dsSettings}
|
||||
onOptionsChange={this.onUpdateAndRun}
|
||||
onDataSourceChange={this.onChangeDataSource}
|
||||
onOpenQueryInspector={onOpenQueryInspector}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -280,32 +238,6 @@ export class QueryGroup extends PureComponent<Props, State> {
|
||||
this.setState({ isDataSourceModalOpen: false });
|
||||
};
|
||||
|
||||
renderDataSourcePickerWithPrompt = () => {
|
||||
const { isDataSourceModalOpen } = this.state;
|
||||
|
||||
const commonProps = {
|
||||
metrics: true,
|
||||
mixed: true,
|
||||
dashboard: true,
|
||||
variables: true,
|
||||
current: this.props.options.dataSource,
|
||||
uploadFile: true,
|
||||
onChange: async (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => {
|
||||
await this.onChangeDataSource(ds, defaultQueries);
|
||||
this.onCloseDataSourceModal();
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDataSourceModalOpen && config.featureToggles.advancedDataSourcePicker && (
|
||||
<DataSourceModal {...commonProps} onDismiss={this.onCloseDataSourceModal}></DataSourceModal>
|
||||
)}
|
||||
<DataSourcePicker {...commonProps} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
onAddQuery = (query: Partial<DataQuery>) => {
|
||||
const { dsSettings, queries } = this.state;
|
||||
this.onQueriesChange(addQuery(queries, query, { type: dsSettings?.type, uid: dsSettings?.uid }));
|
||||
@ -452,3 +384,134 @@ const getStyles = stylesFactory(() => {
|
||||
});
|
||||
|
||||
type QueriesTabStyles = ReturnType<typeof getStyles>;
|
||||
|
||||
interface QueryGroupTopSectionProps {
|
||||
data: PanelData;
|
||||
dataSource: DataSourceApi;
|
||||
dsSettings: DataSourceInstanceSettings;
|
||||
options: QueryGroupOptions;
|
||||
onOpenQueryInspector?: () => void;
|
||||
onOptionsChange?: (options: QueryGroupOptions) => void;
|
||||
onDataSourceChange?: (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => Promise<void>;
|
||||
}
|
||||
|
||||
export function QueryGroupTopSection({
|
||||
dataSource,
|
||||
options,
|
||||
data,
|
||||
dsSettings,
|
||||
onDataSourceChange,
|
||||
onOptionsChange,
|
||||
onOpenQueryInspector,
|
||||
}: QueryGroupTopSectionProps) {
|
||||
const styles = getStyles();
|
||||
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className={styles.dataSourceRow}>
|
||||
<InlineFormLabel htmlFor="data-source-picker" width={'auto'}>
|
||||
Data source
|
||||
</InlineFormLabel>
|
||||
<div className={styles.dataSourceRowItem}>
|
||||
<DataSourcePickerWithPrompt
|
||||
options={options}
|
||||
onChange={async (ds, defaultQueries) => {
|
||||
return await onDataSourceChange?.(ds, defaultQueries);
|
||||
}}
|
||||
isDataSourceModalOpen={Boolean(locationService.getSearchObject().firstPanel)}
|
||||
/>
|
||||
</div>
|
||||
{dataSource && (
|
||||
<>
|
||||
<div className={styles.dataSourceRowItem}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="question-circle"
|
||||
title="Open data source help"
|
||||
onClick={() => setIsHelpOpen(true)}
|
||||
data-testid="query-tab-help-button"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.dataSourceRowItemOptions}>
|
||||
<QueryGroupOptionsEditor
|
||||
options={options}
|
||||
dataSource={dataSource}
|
||||
data={data}
|
||||
onChange={(opts) => {
|
||||
onOptionsChange?.(opts);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{onOpenQueryInspector && (
|
||||
<div className={styles.dataSourceRowItem}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onOpenQueryInspector}
|
||||
aria-label={selectors.components.QueryTab.queryInspectorButton}
|
||||
>
|
||||
Query inspector
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{dataSource && isAngularDatasourcePluginAndNotHidden(dataSource.uid) && (
|
||||
<AngularDeprecationPluginNotice
|
||||
pluginId={dataSource.type}
|
||||
pluginType={PluginType.datasource}
|
||||
angularSupportEnabled={config?.angularSupportEnabled}
|
||||
showPluginDetailsLink={true}
|
||||
interactionElementId="datasource-query"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isHelpOpen && (
|
||||
<Modal title="Data source help" isOpen={true} onDismiss={() => setIsHelpOpen(false)}>
|
||||
<PluginHelp pluginId={dsSettings.meta.id} />
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface DataSourcePickerWithPromptProps {
|
||||
isDataSourceModalOpen?: boolean;
|
||||
options: QueryGroupOptions;
|
||||
onChange: (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => Promise<void>;
|
||||
}
|
||||
|
||||
function DataSourcePickerWithPrompt({ options, onChange, ...otherProps }: DataSourcePickerWithPromptProps) {
|
||||
const [isDataSourceModalOpen, setIsDataSourceModalOpen] = useState(Boolean(otherProps.isDataSourceModalOpen));
|
||||
|
||||
useEffect(() => {
|
||||
// Clean up the first panel flag since the modal is now open
|
||||
if (!!locationService.getSearchObject().firstPanel) {
|
||||
locationService.partial({ firstPanel: null }, true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const commonProps = {
|
||||
metrics: true,
|
||||
mixed: true,
|
||||
dashboard: true,
|
||||
variables: true,
|
||||
current: options.dataSource,
|
||||
uploadFile: true,
|
||||
onChange: async (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => {
|
||||
await onChange(ds, defaultQueries);
|
||||
setIsDataSourceModalOpen(false);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDataSourceModalOpen && config.featureToggles.advancedDataSourcePicker && (
|
||||
<DataSourceModal {...commonProps} onDismiss={() => setIsDataSourceModalOpen(false)}></DataSourceModal>
|
||||
)}
|
||||
|
||||
<DataSourcePicker {...commonProps} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ jest.mock('@grafana/runtime', () => ({
|
||||
jest.mock('app/core/store', () => {
|
||||
return {
|
||||
set() {},
|
||||
get() {},
|
||||
getBool(key: string, defaultValue?: boolean) {
|
||||
const item = window.localStorage.getItem(key);
|
||||
if (item === null) {
|
||||
|
10
yarn.lock
10
yarn.lock
@ -3290,9 +3290,9 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@grafana/scenes@npm:1.27.0":
|
||||
version: 1.27.0
|
||||
resolution: "@grafana/scenes@npm:1.27.0"
|
||||
"@grafana/scenes@npm:1.28.0":
|
||||
version: 1.28.0
|
||||
resolution: "@grafana/scenes@npm:1.28.0"
|
||||
dependencies:
|
||||
"@grafana/e2e-selectors": "npm:10.0.2"
|
||||
react-grid-layout: "npm:1.3.4"
|
||||
@ -3304,7 +3304,7 @@ __metadata:
|
||||
"@grafana/runtime": 10.0.3
|
||||
"@grafana/schema": 10.0.3
|
||||
"@grafana/ui": 10.0.3
|
||||
checksum: e19aa28d6297316676cb4805dde9bc3a52d2d28a3231e2fcdedf610356fc5c1b24eecfec36e8089cf4defc388705d47fce32736f8a86c9176e5bd4e4b7da9acb
|
||||
checksum: 0973206c4485cad15ceb41f031e96e0f1f075be24570f527bbcb17dd56d5cd362385c04acef8f7aa240c3bb8b045d2270fab2dbb2f18e7e2850ab67a13a3d268
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -17317,7 +17317,7 @@ __metadata:
|
||||
"@grafana/lezer-traceql": "npm:0.0.12"
|
||||
"@grafana/monaco-logql": "npm:^0.0.7"
|
||||
"@grafana/runtime": "workspace:*"
|
||||
"@grafana/scenes": "npm:1.27.0"
|
||||
"@grafana/scenes": "npm:1.28.0"
|
||||
"@grafana/schema": "workspace:*"
|
||||
"@grafana/tsconfig": "npm:^1.3.0-rc1"
|
||||
"@grafana/ui": "workspace:*"
|
||||
|
Loading…
Reference in New Issue
Block a user