mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PanelEdit: Edit the source panel, refactor out VizPanelManager, simplify (#93045)
* Options pane, data pane queries tab and transformations tab working * Update * Discard works * Panel inspect working * Table viw works * Repeat options * Began fixing tests * More tests fixed * Progress on tests * no errors * init full width when enabling repeat * Began moving VizPanelManager tests to where the code was moved * Unlink libray panel code and unit test * Fixes and unit tests for change tracking and resetting original state and dirty flag when saving * migrating and improving unit tests * Done with VizPanelManager tests refactoring * Update * Update * remove console.log * Removed unnesssary behavior and fixed test * Update * Fix unrelated test * conditional options fix * remove * Fixing issue with editing repeated panels and scoping variable to first value * Minor fix * Fix discard query runner changes * Review comment changes * fix discard issue with new panels * Add loading state to panel edit * Fix test * Update * fix test * fix lint * lint * Fix * Fix overrides editing mutating fieldConfig --------- Co-authored-by: alexandra vargas <alexa1866@gmail.com>
This commit is contained in:
parent
9adb7b03a7
commit
038d9cabde
@ -2665,8 +2665,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "10"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/inspect/HelpWizard/utils.ts: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.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/inspect/InspectDataTab.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
@ -2793,9 +2792,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"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.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
|
@ -15,7 +15,6 @@ import { VizPanel } from '@grafana/scenes';
|
||||
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
|
||||
|
||||
import { DashboardGridItem } from '../../scene/DashboardGridItem';
|
||||
import { DashboardScene } from '../../scene/DashboardScene';
|
||||
import { gridItemToPanel, vizPanelToPanel } from '../../serialization/transformSceneToSaveModel';
|
||||
import { getQueryRunnerFor, isLibraryPanel } from '../../utils/utils';
|
||||
|
||||
@ -64,25 +63,12 @@ export function getGithubMarkdown(panel: VizPanel, snapshot: string): string {
|
||||
export async function getDebugDashboard(panel: VizPanel, rand: Randomize, timeRange: TimeRange) {
|
||||
let saveModel: ReturnType<typeof gridItemToPanel> = { type: '' };
|
||||
const gridItem = panel.parent as DashboardGridItem;
|
||||
const scene = panel.getRoot() as DashboardScene;
|
||||
|
||||
if (isLibraryPanel(panel)) {
|
||||
saveModel = {
|
||||
...gridItemToPanel(gridItem),
|
||||
...vizPanelToPanel(panel),
|
||||
};
|
||||
} else if (scene.state.editPanel) {
|
||||
// If panel edit mode is open when the user chooses the "get help" panel menu option
|
||||
// we want the debug dashboard to include the panel with any changes that were made while
|
||||
// in panel edit mode.
|
||||
const sourcePanel = scene.state.editPanel.state.vizManager.state.sourcePanel.resolve();
|
||||
const dashGridItem = sourcePanel.parent;
|
||||
if (dashGridItem instanceof DashboardGridItem) {
|
||||
saveModel = {
|
||||
...gridItemToPanel(dashGridItem),
|
||||
...vizPanelToPanel(scene.state.editPanel.state.vizManager.state.panel.clone()),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
saveModel = gridItemToPanel(gridItem);
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ import { InspectTab } from 'app/features/inspector/types';
|
||||
import { getPrettyJSON } from 'app/features/inspector/utils/utils';
|
||||
import { reportPanelInspectInteraction } from 'app/features/search/page/reporting';
|
||||
|
||||
import { VizPanelManager } from '../panel-edit/VizPanelManager';
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene';
|
||||
import { gridItemToPanel, vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
@ -219,11 +218,6 @@ function getJsonText(show: ShowContent, panel: VizPanel): string {
|
||||
break;
|
||||
}
|
||||
|
||||
if (panel.parent instanceof VizPanelManager) {
|
||||
objToStringify = panel.parent.getPanelSaveModel();
|
||||
break;
|
||||
}
|
||||
|
||||
if (gridItem instanceof DashboardGridItem) {
|
||||
objToStringify = gridItemToPanel(gridItem);
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ import { SceneInspectTab } from './types';
|
||||
|
||||
interface PanelInspectDrawerState extends SceneObjectState {
|
||||
tabs?: SceneInspectTab[];
|
||||
panelRef?: SceneObjectRef<VizPanel>;
|
||||
panelRef: SceneObjectRef<VizPanel>;
|
||||
pluginNotLoaded?: boolean;
|
||||
canEdit?: boolean;
|
||||
}
|
||||
@ -51,7 +51,7 @@ export class PanelInspectDrawer extends SceneObjectBase<PanelInspectDrawerState>
|
||||
*/
|
||||
async buildTabs(retry: number) {
|
||||
const panelRef = this.state.panelRef;
|
||||
const plugin = panelRef?.resolve()?.getPlugin();
|
||||
const plugin = panelRef.resolve()?.getPlugin();
|
||||
const tabs: SceneInspectTab[] = [];
|
||||
|
||||
if (!plugin) {
|
||||
|
@ -0,0 +1,48 @@
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import {
|
||||
SceneDataProvider,
|
||||
SceneDataProviderResult,
|
||||
SceneDataState,
|
||||
SceneObjectBase,
|
||||
SceneObjectRef,
|
||||
} from '@grafana/scenes';
|
||||
|
||||
export interface DataProviderSharerState extends SceneDataState {
|
||||
source: SceneObjectRef<SceneDataProvider>;
|
||||
}
|
||||
|
||||
export class DataProviderSharer extends SceneObjectBase<DataProviderSharerState> implements SceneDataProvider {
|
||||
public constructor(state: DataProviderSharerState) {
|
||||
super({
|
||||
source: state.source,
|
||||
data: state.source.resolve().state.data,
|
||||
});
|
||||
|
||||
this.addActivationHandler(() => {
|
||||
this._subs.add(
|
||||
this.state.source.resolve().subscribeToState((newState, oldState) => {
|
||||
if (newState.data !== oldState.data) {
|
||||
this.setState({ data: newState.data });
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public setContainerWidth(width: number) {
|
||||
this.state.source.resolve().setContainerWidth?.(width);
|
||||
}
|
||||
|
||||
public isDataReadyToDisplay() {
|
||||
return this.state.source.resolve().isDataReadyToDisplay?.() ?? true;
|
||||
}
|
||||
|
||||
public cancelQuery() {
|
||||
this.state.source.resolve().cancelQuery?.();
|
||||
}
|
||||
|
||||
public getResultsStream(): Observable<SceneDataProviderResult> {
|
||||
return this.state.source.resolve().getResultsStream();
|
||||
}
|
||||
}
|
@ -34,7 +34,6 @@ import { AlertQuery, PromRulesResponse } from 'app/types/unified-alerting-dto';
|
||||
import { createDashboardSceneFromDashboardModel } from '../../serialization/transformSaveModelToScene';
|
||||
import * as utils from '../../utils/utils';
|
||||
import { findVizPanelByKey, getVizPanelKeyForPanelId } from '../../utils/utils';
|
||||
import { VizPanelManager } from '../VizPanelManager';
|
||||
|
||||
import { PanelDataAlertingTab, PanelDataAlertingTabRendered } from './PanelDataAlertingTab';
|
||||
|
||||
@ -361,7 +360,7 @@ async function clickNewButton() {
|
||||
function createModel(dashboard: DashboardModel) {
|
||||
const scene = createDashboardSceneFromDashboardModel(dashboard, {} as DashboardDataDTO);
|
||||
const vizPanel = findVizPanelByKey(scene, getVizPanelKeyForPanelId(34))!;
|
||||
const model = new PanelDataAlertingTab(VizPanelManager.createFor(vizPanel));
|
||||
const model = new PanelDataAlertingTab({ panelRef: vizPanel.getRef() });
|
||||
jest.spyOn(utils, 'getDashboardSceneFor').mockReturnValue(scene);
|
||||
return model;
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState, VizPanel } from '@grafana/scenes';
|
||||
import { Alert, LoadingPlaceholder, Tab, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { RulesTable } from 'app/features/alerting/unified/components/rules/RulesTable';
|
||||
@ -11,58 +10,46 @@ import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-
|
||||
import { stringifyErrorLike } from 'app/features/alerting/unified/utils/misc';
|
||||
|
||||
import { getDashboardSceneFor, getPanelIdForVizPanel } from '../../utils/utils';
|
||||
import { VizPanelManager } from '../VizPanelManager';
|
||||
|
||||
import { ScenesNewRuleFromPanelButton } from './NewAlertRuleButton';
|
||||
import { PanelDataPaneTab, PanelDataPaneTabState, PanelDataTabHeaderProps, TabId } from './types';
|
||||
import { PanelDataPaneTab, PanelDataTabHeaderProps, TabId } from './types';
|
||||
|
||||
export class PanelDataAlertingTab extends SceneObjectBase<PanelDataPaneTabState> implements PanelDataPaneTab {
|
||||
static Component = PanelDataAlertingTabRendered;
|
||||
TabComponent: (props: PanelDataTabHeaderProps) => React.JSX.Element;
|
||||
|
||||
tabId = TabId.Alert;
|
||||
private _panelManager: VizPanelManager;
|
||||
|
||||
constructor(panelManager: VizPanelManager) {
|
||||
super({});
|
||||
this.TabComponent = (props: PanelDataTabHeaderProps) => AlertingTab({ ...props, model: this });
|
||||
this._panelManager = panelManager;
|
||||
export interface PanelDataAlertingTabState extends SceneObjectState {
|
||||
panelRef: SceneObjectRef<VizPanel>;
|
||||
}
|
||||
|
||||
getTabLabel() {
|
||||
export class PanelDataAlertingTab extends SceneObjectBase<PanelDataAlertingTabState> implements PanelDataPaneTab {
|
||||
static Component = PanelDataAlertingTabRendered;
|
||||
public tabId = TabId.Alert;
|
||||
|
||||
public renderTab(props: PanelDataTabHeaderProps) {
|
||||
return <AlertingTab key={this.getTabLabel()} model={this} {...props} />;
|
||||
}
|
||||
|
||||
public getTabLabel() {
|
||||
return 'Alert';
|
||||
}
|
||||
|
||||
getDashboardUID() {
|
||||
public getDashboardUID() {
|
||||
const dashboard = this.getDashboard();
|
||||
return dashboard.state.uid!;
|
||||
}
|
||||
|
||||
getDashboard() {
|
||||
return getDashboardSceneFor(this._panelManager);
|
||||
public getDashboard() {
|
||||
return getDashboardSceneFor(this);
|
||||
}
|
||||
|
||||
getLegacyPanelId() {
|
||||
return getPanelIdForVizPanel(this._panelManager.state.panel);
|
||||
public getLegacyPanelId() {
|
||||
return getPanelIdForVizPanel(this.state.panelRef.resolve());
|
||||
}
|
||||
|
||||
getCanCreateRules() {
|
||||
public getCanCreateRules() {
|
||||
const rulesPermissions = getRulesPermissions('grafana');
|
||||
return this.getDashboard().state.meta.canSave && contextSrv.hasPermission(rulesPermissions.create);
|
||||
}
|
||||
|
||||
get panelManager() {
|
||||
return this._panelManager;
|
||||
}
|
||||
|
||||
get panel() {
|
||||
return this._panelManager.state.panel;
|
||||
}
|
||||
}
|
||||
|
||||
export function PanelDataAlertingTabRendered(props: SceneComponentProps<PanelDataAlertingTab>) {
|
||||
const { model } = props;
|
||||
|
||||
export function PanelDataAlertingTabRendered({ model }: SceneComponentProps<PanelDataAlertingTab>) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { errors, loading, rules } = usePanelCombinedRules({
|
||||
@ -87,7 +74,7 @@ export function PanelDataAlertingTabRendered(props: SceneComponentProps<PanelDat
|
||||
);
|
||||
}
|
||||
|
||||
const { panel } = model;
|
||||
const panel = model.state.panelRef.resolve();
|
||||
const canCreateRules = model.getCanCreateRules();
|
||||
|
||||
if (rules.length) {
|
||||
@ -132,7 +119,6 @@ function AlertingTab(props: PanelDataAlertingTabHeaderProps) {
|
||||
|
||||
return (
|
||||
<Tab
|
||||
key={props.key}
|
||||
label={model.getTabLabel()}
|
||||
icon="bell"
|
||||
counter={rules.length}
|
||||
|
@ -1,46 +1,64 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import {
|
||||
SceneComponentProps,
|
||||
SceneObjectBase,
|
||||
SceneObjectRef,
|
||||
SceneObjectState,
|
||||
SceneObjectUrlSyncConfig,
|
||||
SceneObjectUrlValues,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { Container, CustomScrollbar, TabContent, TabsBar, useStyles2 } from '@grafana/ui';
|
||||
import { config, getConfig } from 'app/core/config';
|
||||
import { getConfig } from 'app/core/config';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-control';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
|
||||
import { VizPanelManager } from '../VizPanelManager';
|
||||
|
||||
import { PanelDataAlertingTab } from './PanelDataAlertingTab';
|
||||
import { PanelDataQueriesTab } from './PanelDataQueriesTab';
|
||||
import { PanelDataTransformationsTab } from './PanelDataTransformationsTab';
|
||||
import { PanelDataPaneTab, TabId } from './types';
|
||||
|
||||
export interface PanelDataPaneState extends SceneObjectState {
|
||||
tabs?: PanelDataPaneTab[];
|
||||
tab?: TabId;
|
||||
tabs: PanelDataPaneTab[];
|
||||
tab: TabId;
|
||||
panelRef: SceneObjectRef<VizPanel>;
|
||||
}
|
||||
|
||||
export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||
static Component = PanelDataPaneRendered;
|
||||
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['tab'] });
|
||||
private panelSubscription: Unsubscribable | undefined;
|
||||
public panelManager: VizPanelManager;
|
||||
|
||||
getUrlState() {
|
||||
return {
|
||||
tab: this.state.tab,
|
||||
};
|
||||
public static createFor(panel: VizPanel) {
|
||||
const panelRef = panel.getRef();
|
||||
const tabs: PanelDataPaneTab[] = [
|
||||
new PanelDataQueriesTab({ panelRef }),
|
||||
new PanelDataTransformationsTab({ panelRef }),
|
||||
];
|
||||
|
||||
if (shouldShowAlertingTab(panel.state.pluginId)) {
|
||||
tabs.push(new PanelDataAlertingTab({ panelRef }));
|
||||
}
|
||||
|
||||
updateFromUrl(values: SceneObjectUrlValues) {
|
||||
return new PanelDataPane({
|
||||
panelRef,
|
||||
tabs,
|
||||
tab: TabId.Queries,
|
||||
});
|
||||
}
|
||||
|
||||
public onChangeTab = (tab: PanelDataPaneTab) => {
|
||||
this.setState({ tab: tab.tabId });
|
||||
};
|
||||
|
||||
public getUrlState() {
|
||||
return { tab: this.state.tab };
|
||||
}
|
||||
|
||||
public updateFromUrl(values: SceneObjectUrlValues) {
|
||||
if (!values.tab) {
|
||||
return;
|
||||
}
|
||||
@ -48,68 +66,6 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||
this.setState({ tab: values.tab as TabId });
|
||||
}
|
||||
}
|
||||
|
||||
constructor(panelMgr: VizPanelManager) {
|
||||
super({
|
||||
tab: TabId.Queries,
|
||||
tabs: [],
|
||||
});
|
||||
|
||||
this.panelManager = panelMgr;
|
||||
this.addActivationHandler(() => this.onActivate());
|
||||
}
|
||||
|
||||
private onActivate() {
|
||||
this.buildTabs();
|
||||
|
||||
this._subs.add(
|
||||
// Setup subscription for the case when panel type changed
|
||||
this.panelManager.subscribeToState((n, p) => {
|
||||
if (n.pluginId !== p.pluginId) {
|
||||
this.buildTabs();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (this.panelSubscription) {
|
||||
this.panelSubscription.unsubscribe();
|
||||
this.panelSubscription = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private buildTabs() {
|
||||
const panelManager = this.panelManager;
|
||||
const panel = panelManager.state.panel;
|
||||
const pluginId = panelManager.state.pluginId;
|
||||
|
||||
const runner = this.panelManager.queryRunner;
|
||||
const tabs: PanelDataPaneTab[] = [];
|
||||
|
||||
if (panel) {
|
||||
if (config.panels[pluginId]?.skipDataQuery) {
|
||||
this.setState({ tabs });
|
||||
return;
|
||||
} else {
|
||||
if (runner) {
|
||||
tabs.push(new PanelDataQueriesTab(this.panelManager));
|
||||
}
|
||||
|
||||
tabs.push(new PanelDataTransformationsTab(this.panelManager));
|
||||
|
||||
if (shouldShowAlertingTab(panelManager.state.pluginId)) {
|
||||
tabs.push(new PanelDataAlertingTab(this.panelManager));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ tabs });
|
||||
}
|
||||
|
||||
onChangeTab = (tab: PanelDataPaneTab) => {
|
||||
this.setState({ tab: tab.tabId });
|
||||
};
|
||||
}
|
||||
|
||||
function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
@ -125,15 +81,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
return (
|
||||
<div className={styles.dataPane} data-testid={selectors.components.PanelEditor.DataPane.content}>
|
||||
<TabsBar hideBorder={true} className={styles.tabsBar}>
|
||||
{tabs.map((t, index) => {
|
||||
return (
|
||||
<t.TabComponent
|
||||
key={`${t.getTabLabel()}-${index}`}
|
||||
active={t.tabId === tab}
|
||||
onChangeTab={() => model.onChangeTab(t)}
|
||||
></t.TabComponent>
|
||||
);
|
||||
})}
|
||||
{tabs.map((t) => t.renderTab({ active: t.tabId === tab, onChangeTab: () => model.onChangeTab(t) }))}
|
||||
</TabsBar>
|
||||
<CustomScrollbar className={styles.scroll}>
|
||||
<TabContent className={styles.tabContent}>
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
DataQuery,
|
||||
DataQueryRequest,
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
DataSourceJsonData,
|
||||
DataSourceRef,
|
||||
FieldType,
|
||||
@ -14,29 +15,28 @@ import {
|
||||
TimeRange,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { config, locationService, setPluginExtensionsHook } from '@grafana/runtime';
|
||||
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 { DashboardDataDTO } from 'app/types';
|
||||
|
||||
import { PanelTimeRange, PanelTimeRangeState } from '../../scene/PanelTimeRange';
|
||||
import { transformSaveModelToScene } from '../../serialization/transformSaveModelToScene';
|
||||
import { DashboardModelCompatibilityWrapper } from '../../utils/DashboardModelCompatibilityWrapper';
|
||||
import { findVizPanelByKey } from '../../utils/utils';
|
||||
import { VizPanelManager } from '../VizPanelManager';
|
||||
import { testDashboard } from '../testfiles/testDashboard';
|
||||
import { buildPanelEditScene } from '../PanelEditor';
|
||||
import { testDashboard, panelWithTransformations, panelWithQueriesOnly } from '../testfiles/testDashboard';
|
||||
|
||||
import { PanelDataQueriesTab, PanelDataQueriesTabRendered } from './PanelDataQueriesTab';
|
||||
|
||||
async function createModelMock() {
|
||||
const panelManager = setupVizPanelManger('panel-1');
|
||||
panelManager.activate();
|
||||
await Promise.resolve();
|
||||
const queryTabModel = new PanelDataQueriesTab(panelManager);
|
||||
const { queriesTab } = await setupScene('panel-1');
|
||||
|
||||
// mock queryRunner data state
|
||||
jest.spyOn(queryTabModel.queryRunner, 'state', 'get').mockReturnValue({
|
||||
...queryTabModel.queryRunner.state,
|
||||
jest.spyOn(queriesTab.queryRunner, 'state', 'get').mockReturnValue({
|
||||
...queriesTab.queryRunner.state,
|
||||
data: {
|
||||
state: LoadingState.Done,
|
||||
series: [
|
||||
@ -52,8 +52,14 @@ async function createModelMock() {
|
||||
},
|
||||
});
|
||||
|
||||
return queryTabModel;
|
||||
return queriesTab;
|
||||
}
|
||||
|
||||
setPluginExtensionsHook(() => ({
|
||||
extensions: [],
|
||||
isLoading: false,
|
||||
}));
|
||||
|
||||
const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => {
|
||||
const result: PanelData = {
|
||||
state: LoadingState.Loading,
|
||||
@ -186,11 +192,17 @@ const MixedDsSettingsMock = {
|
||||
},
|
||||
};
|
||||
|
||||
const panelPlugin = getPanelPlugin({ id: 'timeseries', skipDataQuery: false });
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
|
||||
return runRequestMock(ds, request);
|
||||
},
|
||||
getPluginImportUtils: () => ({
|
||||
getPanelPluginFromCache: jest.fn(() => panelPlugin),
|
||||
}),
|
||||
getPluginLinkExtensions: jest.fn(),
|
||||
getDataSourceSrv: () => ({
|
||||
get: async (ref: DataSourceRef) => {
|
||||
// Mocking the build in Grafana data source to avoid annotations data layer errors.
|
||||
@ -234,48 +246,59 @@ jest.mock('@grafana/runtime', () => ({
|
||||
return instance1SettingsMock;
|
||||
},
|
||||
}),
|
||||
locationService: {
|
||||
partial: jest.fn(),
|
||||
getSearchObject: jest.fn().mockReturnValue({
|
||||
firstPanel: false,
|
||||
}),
|
||||
},
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
defaultDatasource: 'gdev-testdata',
|
||||
},
|
||||
}));
|
||||
describe('PanelDataQueriesModel', () => {
|
||||
it('can add a new query', async () => {
|
||||
const vizPanelManager = setupVizPanelManger('panel-1');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
const model = new PanelDataQueriesTab(vizPanelManager);
|
||||
model.addQueryClick();
|
||||
expect(model.queryRunner.state.queries).toHaveLength(2);
|
||||
expect(model.queryRunner.state.queries[1].refId).toBe('B');
|
||||
expect(model.queryRunner.state.queries[1].hide).toBe(false);
|
||||
expect(model.queryRunner.state.queries[1].datasource).toEqual({
|
||||
jest.mock('app/core/store', () => ({
|
||||
exists: jest.fn(),
|
||||
get: jest.fn(),
|
||||
getObject: jest.fn((_a, b) => b),
|
||||
setObject: jest.fn(),
|
||||
}));
|
||||
|
||||
const store = jest.requireMock('app/core/store');
|
||||
let deactivators = [] as Array<() => void>;
|
||||
|
||||
describe('PanelDataQueriesTab', () => {
|
||||
beforeEach(() => {
|
||||
store.setObject.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
deactivators.forEach((deactivate) => deactivate());
|
||||
deactivators = [];
|
||||
});
|
||||
|
||||
describe('Adding queries', () => {
|
||||
it('can add a new query', async () => {
|
||||
const { queriesTab } = await setupScene('panel-1');
|
||||
|
||||
queriesTab.addQueryClick();
|
||||
|
||||
expect(queriesTab.queryRunner.state.queries).toHaveLength(2);
|
||||
expect(queriesTab.queryRunner.state.queries[1].refId).toBe('B');
|
||||
expect(queriesTab.queryRunner.state.queries[1].hide).toBe(false);
|
||||
expect(queriesTab.queryRunner.state.queries[1].datasource).toEqual({
|
||||
type: 'grafana-testdata-datasource',
|
||||
uid: 'gdev-testdata',
|
||||
});
|
||||
});
|
||||
|
||||
it('can add a new query when datasource is mixed', async () => {
|
||||
const vizPanelManager = setupVizPanelManger('panel-7');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
it('Can add a new query when datasource is mixed', async () => {
|
||||
const { queriesTab } = await setupScene('panel-7');
|
||||
|
||||
const model = new PanelDataQueriesTab(vizPanelManager);
|
||||
expect(vizPanelManager.state.datasource?.uid).toBe('-- Mixed --');
|
||||
expect(model.queryRunner.state.datasource?.uid).toBe('-- Mixed --');
|
||||
model.addQueryClick();
|
||||
expect(queriesTab.state.datasource?.uid).toBe('-- Mixed --');
|
||||
expect(queriesTab.queryRunner.state.datasource?.uid).toBe('-- Mixed --');
|
||||
|
||||
expect(model.queryRunner.state.queries).toHaveLength(2);
|
||||
expect(model.queryRunner.state.queries[1].refId).toBe('B');
|
||||
expect(model.queryRunner.state.queries[1].hide).toBe(false);
|
||||
expect(model.queryRunner.state.queries[1].datasource?.uid).toBe('gdev-testdata');
|
||||
queriesTab.addQueryClick();
|
||||
|
||||
expect(queriesTab.queryRunner.state.queries).toHaveLength(2);
|
||||
expect(queriesTab.queryRunner.state.queries[1].refId).toBe('B');
|
||||
expect(queriesTab.queryRunner.state.queries[1].hide).toBe(false);
|
||||
expect(queriesTab.queryRunner.state.queries[1].datasource?.uid).toBe('gdev-testdata');
|
||||
});
|
||||
});
|
||||
|
||||
@ -331,15 +354,346 @@ describe('PanelDataQueriesTab', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const setupVizPanelManger = (panelId: string) => {
|
||||
const scene = transformSaveModelToScene({ dashboard: testDashboard as unknown as DashboardDataDTO, meta: {} });
|
||||
const panel = findVizPanelByKey(scene, panelId)!;
|
||||
describe('query options', () => {
|
||||
describe('activation', () => {
|
||||
it('should load data source', async () => {
|
||||
const { queriesTab } = await setupScene('panel-1');
|
||||
|
||||
const vizPanelManager = VizPanelManager.createFor(panel);
|
||||
expect(queriesTab.state.datasource).toEqual(ds1Mock);
|
||||
expect(queriesTab.state.dsSettings).toEqual(instance1SettingsMock);
|
||||
});
|
||||
|
||||
// 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));
|
||||
it('should store loaded data source in local storage', async () => {
|
||||
await setupScene('panel-1');
|
||||
|
||||
return vizPanelManager;
|
||||
};
|
||||
expect(store.setObject).toHaveBeenCalledWith('grafana.dashboards.panelEdit.lastUsedDatasource', {
|
||||
dashboardUid: 'ffbe00e2-803c-4d49-adb7-41aad336234f',
|
||||
datasourceUid: 'gdev-testdata',
|
||||
});
|
||||
});
|
||||
|
||||
it('should load default datasource if the datasource passed is not found', async () => {
|
||||
const { queriesTab } = await setupScene('panel-6');
|
||||
|
||||
expect(queriesTab.queryRunner.state.datasource).toEqual({
|
||||
uid: 'abc',
|
||||
type: 'datasource',
|
||||
});
|
||||
|
||||
expect(config.defaultDatasource).toBe('gdev-testdata');
|
||||
expect(queriesTab.state.datasource).toEqual(defaultDsMock);
|
||||
expect(queriesTab.state.dsSettings).toEqual(instance1SettingsMock);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data source change', () => {
|
||||
it('should load new data source', async () => {
|
||||
const { queriesTab, panel } = await setupScene('panel-1');
|
||||
panel.state.$data?.activate();
|
||||
|
||||
await queriesTab.onChangeDataSource(
|
||||
{ type: 'grafana-prometheus-datasource', uid: 'gdev-prometheus' } as DataSourceInstanceSettings,
|
||||
[]
|
||||
);
|
||||
|
||||
expect(store.setObject).toHaveBeenCalledTimes(2);
|
||||
expect(store.setObject).toHaveBeenLastCalledWith('grafana.dashboards.panelEdit.lastUsedDatasource', {
|
||||
dashboardUid: 'ffbe00e2-803c-4d49-adb7-41aad336234f',
|
||||
datasourceUid: 'gdev-prometheus',
|
||||
});
|
||||
|
||||
expect(queriesTab.state.datasource).toEqual(ds2Mock);
|
||||
expect(queriesTab.state.dsSettings).toEqual(instance2SettingsMock);
|
||||
});
|
||||
});
|
||||
|
||||
describe('query options change', () => {
|
||||
describe('time overrides', () => {
|
||||
it('should create PanelTimeRange object', async () => {
|
||||
const { queriesTab, panel } = await setupScene('panel-1');
|
||||
|
||||
panel.state.$data?.activate();
|
||||
|
||||
expect(panel.state.$timeRange).toBeUndefined();
|
||||
|
||||
queriesTab.onQueryOptionsChange({
|
||||
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 { queriesTab, panel } = await setupScene('panel-1');
|
||||
|
||||
expect(panel.state.$timeRange).toBeUndefined();
|
||||
|
||||
queriesTab.onQueryOptionsChange({
|
||||
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');
|
||||
|
||||
queriesTab.onQueryOptionsChange({
|
||||
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 { queriesTab, panel } = await setupScene('panel-1');
|
||||
|
||||
expect(panel.state.$timeRange).toBeUndefined();
|
||||
|
||||
queriesTab.onQueryOptionsChange({
|
||||
dataSource: { name: 'grafana-testdata', type: 'grafana-testdata-datasource', default: true },
|
||||
queries: [],
|
||||
timeRange: { from: '1h' },
|
||||
});
|
||||
|
||||
queriesTab.onQueryOptionsChange({
|
||||
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('should update max data points', async () => {
|
||||
const { queriesTab } = await setupScene('panel-1');
|
||||
const dataObj = queriesTab.queryRunner;
|
||||
|
||||
expect(dataObj.state.maxDataPoints).toBeUndefined();
|
||||
|
||||
queriesTab.onQueryOptionsChange({
|
||||
dataSource: { name: 'grafana-testdata', type: 'grafana-testdata-datasource', default: true },
|
||||
queries: [],
|
||||
maxDataPoints: 100,
|
||||
});
|
||||
|
||||
expect(dataObj.state.maxDataPoints).toBe(100);
|
||||
});
|
||||
|
||||
it('should update min interval', async () => {
|
||||
const { queriesTab } = await setupScene('panel-1');
|
||||
const dataObj = queriesTab.queryRunner;
|
||||
|
||||
expect(dataObj.state.maxDataPoints).toBeUndefined();
|
||||
|
||||
queriesTab.onQueryOptionsChange({
|
||||
dataSource: { name: 'grafana-testdata', type: 'grafana-testdata-datasource', default: true },
|
||||
queries: [],
|
||||
minInterval: '1s',
|
||||
});
|
||||
expect(dataObj.state.minInterval).toBe('1s');
|
||||
});
|
||||
});
|
||||
|
||||
describe('query caching', () => {
|
||||
it('updates cacheTimeout and queryCachingTTL', async () => {
|
||||
const { queriesTab } = await setupScene('panel-1');
|
||||
const dataObj = queriesTab.queryRunner;
|
||||
|
||||
queriesTab.onQueryOptionsChange({
|
||||
cacheTimeout: '60',
|
||||
queryCachingTTL: 200000,
|
||||
dataSource: { name: 'grafana-testdata', type: 'grafana-testdata-datasource', default: true },
|
||||
queries: [],
|
||||
});
|
||||
|
||||
expect(dataObj.state.cacheTimeout).toBe('60');
|
||||
expect(dataObj.state.queryCachingTTL).toBe(200000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('query inspection', () => {
|
||||
it('allows query inspection from the tab', async () => {
|
||||
const { queriesTab } = await setupScene('panel-1');
|
||||
queriesTab.onOpenInspector();
|
||||
|
||||
const params = locationService.getSearchObject();
|
||||
expect(params.inspect).toBe('1');
|
||||
expect(params.inspectTab).toBe(InspectTab.Query);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data source change', () => {
|
||||
it('changing from one plugin to another', async () => {
|
||||
const { queriesTab } = await setupScene('panel-1');
|
||||
|
||||
expect(queriesTab.queryRunner.state.datasource).toEqual({
|
||||
uid: 'gdev-testdata',
|
||||
type: 'grafana-testdata-datasource',
|
||||
});
|
||||
|
||||
await queriesTab.onChangeDataSource({
|
||||
name: 'grafana-prometheus',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
uid: 'gdev-prometheus',
|
||||
meta: {
|
||||
name: 'Prometheus',
|
||||
module: 'prometheus',
|
||||
id: 'grafana-prometheus-datasource',
|
||||
},
|
||||
} as DataSourceInstanceSettings);
|
||||
|
||||
expect(queriesTab.queryRunner.state.datasource).toEqual({
|
||||
uid: 'gdev-prometheus',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
});
|
||||
});
|
||||
|
||||
it('changing from a plugin to a dashboard data source', async () => {
|
||||
const { queriesTab } = await setupScene('panel-1');
|
||||
|
||||
expect(queriesTab.queryRunner.state.datasource).toEqual({
|
||||
uid: 'gdev-testdata',
|
||||
type: 'grafana-testdata-datasource',
|
||||
});
|
||||
|
||||
await queriesTab.onChangeDataSource({
|
||||
name: SHARED_DASHBOARD_QUERY,
|
||||
type: 'datasource',
|
||||
uid: SHARED_DASHBOARD_QUERY,
|
||||
meta: {
|
||||
name: 'Prometheus',
|
||||
module: 'prometheus',
|
||||
id: DASHBOARD_DATASOURCE_PLUGIN_ID,
|
||||
},
|
||||
} as DataSourceInstanceSettings);
|
||||
|
||||
expect(queriesTab.queryRunner.state.datasource).toEqual({
|
||||
uid: SHARED_DASHBOARD_QUERY,
|
||||
type: 'datasource',
|
||||
});
|
||||
});
|
||||
|
||||
it('changing from dashboard data source to a plugin', async () => {
|
||||
const { queriesTab } = await setupScene('panel-3');
|
||||
|
||||
expect(queriesTab.queryRunner.state.datasource).toEqual({ uid: SHARED_DASHBOARD_QUERY, type: 'datasource' });
|
||||
|
||||
await queriesTab.onChangeDataSource({
|
||||
name: 'grafana-prometheus',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
uid: 'gdev-prometheus',
|
||||
meta: {
|
||||
name: 'Prometheus',
|
||||
module: 'prometheus',
|
||||
id: 'grafana-prometheus-datasource',
|
||||
},
|
||||
} as DataSourceInstanceSettings);
|
||||
|
||||
expect(queriesTab.queryRunner.state.datasource).toEqual({
|
||||
uid: 'gdev-prometheus',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('change queries', () => {
|
||||
describe('plugin queries', () => {
|
||||
it('should update queries', async () => {
|
||||
const { queriesTab, panel } = await setupScene('panel-1');
|
||||
|
||||
panel.state.$data?.activate();
|
||||
|
||||
queriesTab.onQueriesChange([
|
||||
{
|
||||
datasource: { type: 'grafana-testdata-datasource', uid: 'gdev-testdata' },
|
||||
refId: 'A',
|
||||
scenarioId: 'random_walk',
|
||||
seriesCount: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(queriesTab.queryRunner.state.queries).toEqual([
|
||||
{
|
||||
datasource: { type: 'grafana-testdata-datasource', uid: 'gdev-testdata' },
|
||||
refId: 'A',
|
||||
scenarioId: 'random_walk',
|
||||
seriesCount: 5,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dashboard queries', () => {
|
||||
it('should update queries', async () => {
|
||||
const { queriesTab, panel } = await setupScene('panel-3');
|
||||
|
||||
panel.state.$data?.activate();
|
||||
|
||||
// Changing dashboard query to a panel with transformations
|
||||
queriesTab.onQueriesChange([
|
||||
{
|
||||
refId: 'A',
|
||||
datasource: { type: DASHBOARD_DATASOURCE_PLUGIN_ID },
|
||||
panelId: panelWithTransformations.id,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(queriesTab.queryRunner.state.queries[0].panelId).toEqual(panelWithTransformations.id);
|
||||
|
||||
// Changing dashboard query to a panel with queries only
|
||||
queriesTab.onQueriesChange([
|
||||
{
|
||||
refId: 'A',
|
||||
datasource: { type: DASHBOARD_DATASOURCE_PLUGIN_ID },
|
||||
panelId: panelWithQueriesOnly.id,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(queriesTab.queryRunner.state.queries[0].panelId).toBe(panelWithQueriesOnly.id);
|
||||
});
|
||||
|
||||
it('should load last used data source if no data source specified for a panel', async () => {
|
||||
store.exists.mockReturnValue(true);
|
||||
store.getObject.mockReturnValue({
|
||||
dashboardUid: 'ffbe00e2-803c-4d49-adb7-41aad336234f',
|
||||
datasourceUid: 'gdev-testdata',
|
||||
});
|
||||
|
||||
const { queriesTab } = await setupScene('panel-5');
|
||||
|
||||
expect(queriesTab.state.datasource).toBe(ds1Mock);
|
||||
expect(queriesTab.state.dsSettings).toBe(instance1SettingsMock);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function setupScene(panelId: string) {
|
||||
const dashboard = transformSaveModelToScene({ dashboard: testDashboard as unknown as DashboardDataDTO, meta: {} });
|
||||
const panel = findVizPanelByKey(dashboard, panelId)!;
|
||||
|
||||
const panelEditor = buildPanelEditScene(panel);
|
||||
dashboard.setState({ editPanel: panelEditor });
|
||||
|
||||
deactivators.push(dashboard.activate());
|
||||
deactivators.push(panelEditor.activate());
|
||||
|
||||
const queriesTab = panelEditor.state.dataPane!.state.tabs[0] as PanelDataQueriesTab;
|
||||
deactivators.push(queriesTab.activate());
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
return { panel, scene: dashboard, queriesTab };
|
||||
}
|
||||
|
@ -1,60 +1,134 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { CoreApp, DataSourceApi, DataSourceInstanceSettings, IconName, getDataSourceRef } from '@grafana/data';
|
||||
import { CoreApp, DataSourceApi, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { SceneObjectBase, SceneComponentProps, sceneGraph, SceneQueryRunner } from '@grafana/scenes';
|
||||
import { config, getDataSourceSrv, locationService } from '@grafana/runtime';
|
||||
import {
|
||||
SceneObjectBase,
|
||||
SceneComponentProps,
|
||||
sceneGraph,
|
||||
SceneQueryRunner,
|
||||
SceneObjectRef,
|
||||
VizPanel,
|
||||
SceneObjectState,
|
||||
SceneDataQuery,
|
||||
} from '@grafana/scenes';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { Button, Stack, Tab } from '@grafana/ui';
|
||||
import { addQuery } from 'app/core/utils/query';
|
||||
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard';
|
||||
import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils';
|
||||
import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
|
||||
import { GroupActionComponents } from 'app/features/query/components/QueryActionComponent';
|
||||
import { QueryEditorRows } from 'app/features/query/components/QueryEditorRows';
|
||||
import { QueryGroupTopSection } from 'app/features/query/components/QueryGroup';
|
||||
import { updateQueries } from 'app/features/query/state/updateQueries';
|
||||
import { isSharedDashboardQuery } from 'app/plugins/datasource/dashboard';
|
||||
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
|
||||
import { QueryGroupOptions } from 'app/types';
|
||||
|
||||
import { PanelTimeRange } from '../../scene/PanelTimeRange';
|
||||
import { VizPanelManager } from '../VizPanelManager';
|
||||
import { PanelTimeRange, PanelTimeRangeState } from '../../scene/PanelTimeRange';
|
||||
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../../utils/utils';
|
||||
|
||||
import { PanelDataPaneTabState, PanelDataPaneTab, TabId, PanelDataTabHeaderProps } from './types';
|
||||
import { PanelDataPaneTab, TabId, PanelDataTabHeaderProps } from './types';
|
||||
|
||||
interface PanelDataQueriesTabState extends PanelDataPaneTabState {
|
||||
interface PanelDataQueriesTabState extends SceneObjectState {
|
||||
datasource?: DataSourceApi;
|
||||
dsSettings?: DataSourceInstanceSettings;
|
||||
panelRef: SceneObjectRef<VizPanel>;
|
||||
}
|
||||
export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabState> implements PanelDataPaneTab {
|
||||
static Component = PanelDataQueriesTabRendered;
|
||||
TabComponent: (props: PanelDataTabHeaderProps) => React.JSX.Element;
|
||||
|
||||
tabId = TabId.Queries;
|
||||
icon: IconName = 'database';
|
||||
private _panelManager: VizPanelManager;
|
||||
|
||||
getTabLabel() {
|
||||
public constructor(state: PanelDataQueriesTabState) {
|
||||
super(state);
|
||||
this.addActivationHandler(() => this.onActivate());
|
||||
}
|
||||
|
||||
public getTabLabel() {
|
||||
return 'Queries';
|
||||
}
|
||||
|
||||
getItemsCount() {
|
||||
public getItemsCount() {
|
||||
return this.getQueries().length;
|
||||
}
|
||||
|
||||
constructor(panelManager: VizPanelManager) {
|
||||
super({});
|
||||
|
||||
this.TabComponent = (props: PanelDataTabHeaderProps) => {
|
||||
return QueriesTab({ ...props, model: this });
|
||||
};
|
||||
|
||||
this._panelManager = panelManager;
|
||||
public renderTab(props: PanelDataTabHeaderProps) {
|
||||
return <QueriesTab key={this.getTabLabel()} model={this} {...props} />;
|
||||
}
|
||||
|
||||
buildQueryOptions(): QueryGroupOptions {
|
||||
const panelManager = this._panelManager;
|
||||
const panelObj = this._panelManager.state.panel;
|
||||
const queryRunner = this._panelManager.queryRunner;
|
||||
const timeRangeObj = sceneGraph.getTimeRange(panelObj);
|
||||
private onActivate() {
|
||||
this.loadDataSource();
|
||||
}
|
||||
|
||||
private async loadDataSource() {
|
||||
const panel = this.state.panelRef.resolve();
|
||||
const dataObj = panel.state.$data;
|
||||
|
||||
if (!dataObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
let datasourceToLoad = this.queryRunner.state.datasource;
|
||||
|
||||
try {
|
||||
let datasource: DataSourceApi | undefined;
|
||||
let dsSettings: DataSourceInstanceSettings | undefined;
|
||||
|
||||
if (!datasourceToLoad) {
|
||||
const dashboardScene = getDashboardSceneFor(this);
|
||||
const dashboardUid = dashboardScene.state.uid ?? '';
|
||||
const lastUsedDatasource = getLastUsedDatasourceFromStorage(dashboardUid!);
|
||||
|
||||
// do we have a last used datasource for this dashboard
|
||||
if (lastUsedDatasource?.datasourceUid !== null) {
|
||||
// get datasource from dashbopard uid
|
||||
dsSettings = getDataSourceSrv().getInstanceSettings({ uid: lastUsedDatasource?.datasourceUid });
|
||||
if (dsSettings) {
|
||||
datasource = await getDataSourceSrv().get({
|
||||
uid: lastUsedDatasource?.datasourceUid,
|
||||
type: dsSettings.type,
|
||||
});
|
||||
|
||||
this.queryRunner.setState({
|
||||
datasource: {
|
||||
...getDataSourceRef(dsSettings),
|
||||
uid: lastUsedDatasource?.datasourceUid,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
datasource = await getDataSourceSrv().get(datasourceToLoad);
|
||||
dsSettings = getDataSourceSrv().getInstanceSettings(datasourceToLoad);
|
||||
}
|
||||
|
||||
if (datasource && dsSettings) {
|
||||
this.setState({ datasource, dsSettings });
|
||||
storeLastUsedDataSourceInLocalStorage(getDataSourceRef(dsSettings) || { default: true });
|
||||
}
|
||||
} catch (err) {
|
||||
//set default datasource if we fail to load the datasource
|
||||
const datasource = await getDataSourceSrv().get(config.defaultDatasource);
|
||||
const dsSettings = getDataSourceSrv().getInstanceSettings(config.defaultDatasource);
|
||||
|
||||
if (datasource && dsSettings) {
|
||||
this.setState({
|
||||
datasource,
|
||||
dsSettings,
|
||||
});
|
||||
|
||||
this.queryRunner.setState({
|
||||
datasource: getDataSourceRef(dsSettings),
|
||||
});
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
public buildQueryOptions(): QueryGroupOptions {
|
||||
const panel = this.state.panelRef.resolve();
|
||||
const queryRunner = getQueryRunnerFor(panel)!;
|
||||
const timeRangeObj = sceneGraph.getTimeRange(panel);
|
||||
|
||||
let timeRangeOpts: QueryGroupOptions['timeRange'] = {
|
||||
from: undefined,
|
||||
@ -71,19 +145,14 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
||||
}
|
||||
|
||||
let queries: QueryGroupOptions['queries'] = queryRunner.state.queries;
|
||||
const dsSettings = this.state.dsSettings;
|
||||
|
||||
return {
|
||||
cacheTimeout: panelManager.state.dsSettings?.meta.queryOptions?.cacheTimeout
|
||||
? queryRunner.state.cacheTimeout
|
||||
: undefined,
|
||||
queryCachingTTL: panelManager.state.dsSettings?.cachingConfig?.enabled
|
||||
? queryRunner.state.queryCachingTTL
|
||||
: undefined,
|
||||
cacheTimeout: dsSettings?.meta.queryOptions?.cacheTimeout ? queryRunner.state.cacheTimeout : undefined,
|
||||
queryCachingTTL: dsSettings?.cachingConfig?.enabled ? queryRunner.state.queryCachingTTL : undefined,
|
||||
dataSource: {
|
||||
default: panelManager.state.dsSettings?.isDefault,
|
||||
...(panelManager.state.dsSettings
|
||||
? getDataSourceRef(panelManager.state.dsSettings)
|
||||
: { type: undefined, uid: undefined }),
|
||||
default: dsSettings?.isDefault,
|
||||
...(dsSettings ? getDataSourceRef(dsSettings) : { type: undefined, uid: undefined }),
|
||||
},
|
||||
queries,
|
||||
maxDataPoints: queryRunner.state.maxDataPoints,
|
||||
@ -92,37 +161,98 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
||||
};
|
||||
}
|
||||
|
||||
onOpenInspector = () => {
|
||||
this._panelManager.inspectPanel();
|
||||
public onOpenInspector = () => {
|
||||
const panel = this.state.panelRef.resolve();
|
||||
const panelId = getPanelIdForVizPanel(panel);
|
||||
|
||||
locationService.partial({ inspect: panelId, inspectTab: 'query' });
|
||||
};
|
||||
|
||||
onChangeDataSource = async (
|
||||
newSettings: DataSourceInstanceSettings,
|
||||
defaultQueries?: DataQuery[] | GrafanaQuery[]
|
||||
) => {
|
||||
this._panelManager.changePanelDataSource(newSettings, defaultQueries);
|
||||
};
|
||||
public onChangeDataSource = async (newSettings: DataSourceInstanceSettings, defaultQueries?: SceneDataQuery[]) => {
|
||||
const { dsSettings } = this.state;
|
||||
const queryRunner = this.queryRunner;
|
||||
|
||||
onQueryOptionsChange = (options: QueryGroupOptions) => {
|
||||
this._panelManager.changeQueryOptions(options);
|
||||
};
|
||||
const currentDS = dsSettings ? await getDataSourceSrv().get({ uid: dsSettings.uid }) : undefined;
|
||||
const nextDS = await getDataSourceSrv().get({ uid: newSettings.uid });
|
||||
|
||||
onQueriesChange = (queries: DataQuery[]) => {
|
||||
this._panelManager.changeQueries(queries);
|
||||
};
|
||||
const currentQueries = queryRunner.state.queries;
|
||||
|
||||
onRunQueries = () => {
|
||||
this._panelManager.queryRunner.runQueries();
|
||||
};
|
||||
// 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));
|
||||
|
||||
getQueries() {
|
||||
return this._panelManager.queryRunner.state.queries;
|
||||
queryRunner.setState({ datasource: getDataSourceRef(newSettings), queries });
|
||||
|
||||
if (defaultQueries) {
|
||||
queryRunner.runQueries();
|
||||
}
|
||||
|
||||
newQuery(): Partial<DataQuery> {
|
||||
const { dsSettings, datasource } = this._panelManager.state;
|
||||
this.loadDataSource();
|
||||
};
|
||||
|
||||
public onQueryOptionsChange = (options: QueryGroupOptions) => {
|
||||
const panelObj = this.state.panelRef.resolve();
|
||||
const dataObj = this.queryRunner;
|
||||
const timeRangeObj = panelObj.state.$timeRange;
|
||||
|
||||
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) });
|
||||
}
|
||||
|
||||
if (options.cacheTimeout !== dataObj?.state.cacheTimeout) {
|
||||
dataObjStateUpdate.cacheTimeout = options.cacheTimeout;
|
||||
}
|
||||
|
||||
if (options.queryCachingTTL !== dataObj?.state.queryCachingTTL) {
|
||||
dataObjStateUpdate.queryCachingTTL = options.queryCachingTTL;
|
||||
}
|
||||
|
||||
dataObj.setState(dataObjStateUpdate);
|
||||
dataObj.runQueries();
|
||||
};
|
||||
|
||||
public onQueriesChange = (queries: SceneDataQuery[]) => {
|
||||
const runner = this.queryRunner;
|
||||
runner.setState({ queries });
|
||||
};
|
||||
|
||||
public onRunQueries = () => {
|
||||
this.queryRunner.runQueries();
|
||||
};
|
||||
|
||||
public getQueries() {
|
||||
return this.queryRunner.state.queries;
|
||||
}
|
||||
|
||||
public newQuery(): Partial<DataQuery> {
|
||||
const { dsSettings, datasource } = this.state;
|
||||
let ds;
|
||||
|
||||
if (!dsSettings?.meta.mixed) {
|
||||
ds = dsSettings; // Use dsSettings if it is not mixed
|
||||
} else if (!datasource?.meta.mixed) {
|
||||
@ -138,29 +268,30 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
||||
};
|
||||
}
|
||||
|
||||
addQueryClick = () => {
|
||||
public addQueryClick = () => {
|
||||
const queries = this.getQueries();
|
||||
this.onQueriesChange(addQuery(queries, this.newQuery()));
|
||||
};
|
||||
|
||||
onAddQuery = (query: Partial<DataQuery>) => {
|
||||
public onAddQuery = (query: Partial<DataQuery>) => {
|
||||
const queries = this.getQueries();
|
||||
const dsSettings = this._panelManager.state.dsSettings;
|
||||
const dsSettings = this.state.dsSettings;
|
||||
|
||||
this.onQueriesChange(
|
||||
addQuery(queries, query, dsSettings ? getDataSourceRef(dsSettings) : { type: undefined, uid: undefined })
|
||||
);
|
||||
};
|
||||
|
||||
isExpressionsSupported(dsSettings: DataSourceInstanceSettings): boolean {
|
||||
public isExpressionsSupported(dsSettings: DataSourceInstanceSettings): boolean {
|
||||
return (dsSettings.meta.alerting || dsSettings.meta.mixed) === true;
|
||||
}
|
||||
|
||||
onAddExpressionClick = () => {
|
||||
public onAddExpressionClick = () => {
|
||||
const queries = this.getQueries();
|
||||
this.onQueriesChange(addQuery(queries, expressionDatasource.newQuery()));
|
||||
};
|
||||
|
||||
renderExtraActions() {
|
||||
public renderExtraActions() {
|
||||
return GroupActionComponents.getAllExtraRenderAction()
|
||||
.map((action, index) =>
|
||||
action({
|
||||
@ -172,18 +303,14 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
get queryRunner(): SceneQueryRunner {
|
||||
return this._panelManager.queryRunner;
|
||||
}
|
||||
|
||||
get panelManager() {
|
||||
return this._panelManager;
|
||||
public get queryRunner(): SceneQueryRunner {
|
||||
return getQueryRunnerFor(this.state.panelRef.resolve())!;
|
||||
}
|
||||
}
|
||||
|
||||
export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<PanelDataQueriesTab>) {
|
||||
const { datasource, dsSettings } = model.panelManager.useState();
|
||||
const { data, queries } = model.panelManager.queryRunner.useState();
|
||||
const { datasource, dsSettings } = model.useState();
|
||||
const { data, queries } = model.queryRunner.useState();
|
||||
|
||||
if (!datasource || !dsSettings || !data) {
|
||||
return null;
|
||||
@ -250,7 +377,6 @@ function QueriesTab(props: QueriesTabProps) {
|
||||
|
||||
return (
|
||||
<Tab
|
||||
key={props.key}
|
||||
label={model.getTabLabel()}
|
||||
icon="database"
|
||||
counter={queryRunnerState.queries.length}
|
||||
|
@ -19,7 +19,6 @@ import { DashboardDataDTO } from 'app/types';
|
||||
import { transformSaveModelToScene } from '../../serialization/transformSaveModelToScene';
|
||||
import { DashboardModelCompatibilityWrapper } from '../../utils/DashboardModelCompatibilityWrapper';
|
||||
import { findVizPanelByKey } from '../../utils/utils';
|
||||
import { VizPanelManager } from '../VizPanelManager';
|
||||
import { testDashboard } from '../testfiles/testDashboard';
|
||||
|
||||
import { PanelDataTransformationsTab, PanelDataTransformationsTabRendered } from './PanelDataTransformationsTab';
|
||||
@ -52,10 +51,9 @@ const mockData = {
|
||||
|
||||
describe('PanelDataTransformationsModel', () => {
|
||||
it('can change transformations', () => {
|
||||
const vizPanelManager = setupVizPanelManger('panel-1');
|
||||
const model = new PanelDataTransformationsTab(vizPanelManager);
|
||||
model.onChangeTransformations([{ id: 'calculateField', options: {} }]);
|
||||
expect(model.getDataTransformer().state.transformations).toEqual([{ id: 'calculateField', options: {} }]);
|
||||
const { transformsTab } = setupTabScene('panel-1');
|
||||
transformsTab.onChangeTransformations([{ id: 'calculateField', options: {} }]);
|
||||
expect(transformsTab.getDataTransformer().state.transformations).toEqual([{ id: 'calculateField', options: {} }]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -169,15 +167,16 @@ describe('PanelDataTransformationsTab', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const setupVizPanelManger = (panelId: string) => {
|
||||
function setupTabScene(panelId: string) {
|
||||
const scene = transformSaveModelToScene({ dashboard: testDashboard as unknown as DashboardDataDTO, meta: {} });
|
||||
const panel = findVizPanelByKey(scene, panelId)!;
|
||||
|
||||
const vizPanelManager = VizPanelManager.createFor(panel);
|
||||
const transformsTab = new PanelDataTransformationsTab({ panelRef: panel.getRef() });
|
||||
transformsTab.activate();
|
||||
|
||||
// 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));
|
||||
|
||||
return vizPanelManager;
|
||||
};
|
||||
return { transformsTab };
|
||||
}
|
||||
|
@ -2,56 +2,62 @@ import { css } from '@emotion/css';
|
||||
import { DragDropContext, DropResult, Droppable } from '@hello-pangea/dnd';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { DataTransformerConfig, GrafanaTheme2, IconName, PanelData } from '@grafana/data';
|
||||
import { DataTransformerConfig, GrafanaTheme2, PanelData } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { SceneObjectBase, SceneComponentProps, SceneDataTransformer, SceneQueryRunner } from '@grafana/scenes';
|
||||
import {
|
||||
SceneObjectBase,
|
||||
SceneComponentProps,
|
||||
SceneDataTransformer,
|
||||
SceneQueryRunner,
|
||||
SceneObjectRef,
|
||||
VizPanel,
|
||||
SceneObjectState,
|
||||
} from '@grafana/scenes';
|
||||
import { Button, ButtonGroup, ConfirmModal, Tab, useStyles2 } from '@grafana/ui';
|
||||
import { TransformationOperationRows } from 'app/features/dashboard/components/TransformationsEditor/TransformationOperationRows';
|
||||
|
||||
import { VizPanelManager } from '../VizPanelManager';
|
||||
import { getQueryRunnerFor } from '../../utils/utils';
|
||||
|
||||
import { EmptyTransformationsMessage } from './EmptyTransformationsMessage';
|
||||
import { TransformationsDrawer } from './TransformationsDrawer';
|
||||
import { PanelDataPaneTabState, PanelDataPaneTab, TabId, PanelDataTabHeaderProps } from './types';
|
||||
import { PanelDataPaneTab, TabId, PanelDataTabHeaderProps } from './types';
|
||||
|
||||
interface PanelDataTransformationsTabState extends PanelDataPaneTabState {}
|
||||
interface PanelDataTransformationsTabState extends SceneObjectState {
|
||||
panelRef: SceneObjectRef<VizPanel>;
|
||||
}
|
||||
|
||||
export class PanelDataTransformationsTab
|
||||
extends SceneObjectBase<PanelDataTransformationsTabState>
|
||||
implements PanelDataPaneTab
|
||||
{
|
||||
static Component = PanelDataTransformationsTabRendered;
|
||||
TabComponent: (props: PanelDataTabHeaderProps) => React.JSX.Element;
|
||||
|
||||
tabId = TabId.Transformations;
|
||||
icon: IconName = 'process';
|
||||
private _panelManager: VizPanelManager;
|
||||
|
||||
getTabLabel() {
|
||||
return 'Transformations';
|
||||
}
|
||||
|
||||
constructor(panelManager: VizPanelManager) {
|
||||
super({});
|
||||
this.TabComponent = (props: PanelDataTabHeaderProps) => TransformationsTab({ ...props, model: this });
|
||||
|
||||
this._panelManager = panelManager;
|
||||
public renderTab(props: PanelDataTabHeaderProps) {
|
||||
return <TransformationsTab key={this.getTabLabel()} model={this} {...props} />;
|
||||
}
|
||||
|
||||
public getQueryRunner(): SceneQueryRunner {
|
||||
return this._panelManager.queryRunner;
|
||||
return getQueryRunnerFor(this.state.panelRef.resolve())!;
|
||||
}
|
||||
|
||||
public getDataTransformer(): SceneDataTransformer {
|
||||
return this._panelManager.dataTransformer;
|
||||
const provider = this.state.panelRef.resolve().state.$data;
|
||||
|
||||
if (!provider || !(provider instanceof SceneDataTransformer)) {
|
||||
throw new Error('Could not find SceneDataTransformer for panel');
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
public onChangeTransformations(transformations: DataTransformerConfig[]) {
|
||||
this._panelManager.changeTransformations(transformations);
|
||||
}
|
||||
|
||||
get panelManager() {
|
||||
return this._panelManager;
|
||||
const transformer = this.getDataTransformer();
|
||||
transformer.setState({ transformations });
|
||||
transformer.reprocessTransformations();
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,11 +206,10 @@ interface TransformationsTabProps extends PanelDataTabHeaderProps {
|
||||
|
||||
function TransformationsTab(props: TransformationsTabProps) {
|
||||
const { model } = props;
|
||||
|
||||
const transformerState = model.getDataTransformer().useState();
|
||||
|
||||
return (
|
||||
<Tab
|
||||
key={props.key}
|
||||
label={model.getTabLabel()}
|
||||
icon="process"
|
||||
counter={transformerState.transformations.length}
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { SceneObject, SceneObjectState } from '@grafana/scenes';
|
||||
|
||||
export interface PanelDataPaneTabState extends SceneObjectState {}
|
||||
import { SceneObject } from '@grafana/scenes';
|
||||
|
||||
export enum TabId {
|
||||
Queries = 'queries',
|
||||
@ -9,13 +7,12 @@ export enum TabId {
|
||||
}
|
||||
|
||||
export interface PanelDataTabHeaderProps {
|
||||
key: string;
|
||||
active: boolean;
|
||||
onChangeTab?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
}
|
||||
|
||||
export interface PanelDataPaneTab extends SceneObject {
|
||||
TabComponent: (props: PanelDataTabHeaderProps) => React.JSX.Element;
|
||||
renderTab: (props: PanelDataTabHeaderProps) => React.JSX.Element;
|
||||
getTabLabel(): string;
|
||||
tabId: TabId;
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { InlineSwitch } from '@grafana/ui';
|
||||
|
||||
import { PanelEditor } from './PanelEditor';
|
||||
@ -9,19 +8,17 @@ export interface Props {
|
||||
}
|
||||
|
||||
export function PanelEditControls({ panelEditor }: Props) {
|
||||
const vizManager = panelEditor.state.vizManager;
|
||||
const { panel, tableView } = vizManager.useState();
|
||||
const skipDataQuery = config.panels[panel.state.pluginId]?.skipDataQuery;
|
||||
const { tableView, dataPane } = panelEditor.useState();
|
||||
|
||||
return (
|
||||
<>
|
||||
{!skipDataQuery && (
|
||||
{dataPane && (
|
||||
<InlineSwitch
|
||||
label="Table view"
|
||||
showLabel={true}
|
||||
id="table-view"
|
||||
value={tableView ? true : false}
|
||||
onClick={() => vizManager.toggleTableView()}
|
||||
onClick={panelEditor.onToggleTableView}
|
||||
aria-label="toggle-table-view"
|
||||
data-testid={selectors.components.PanelEditor.toggleTableView}
|
||||
/>
|
||||
|
@ -1,5 +1,21 @@
|
||||
import { PanelPlugin, PanelPluginMeta, PluginType } from '@grafana/data';
|
||||
import { SceneGridLayout, VizPanel } from '@grafana/scenes';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { DataQueryRequest, DataSourceApi, LoadingState, PanelPlugin } from '@grafana/data';
|
||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import {
|
||||
CancelActivationHandler,
|
||||
CustomVariable,
|
||||
SceneDataTransformer,
|
||||
sceneGraph,
|
||||
SceneGridLayout,
|
||||
SceneQueryRunner,
|
||||
SceneTimeRange,
|
||||
SceneVariableSet,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { mockDataSource, MockDataSourceSrv } from 'app/features/alerting/unified/mocks';
|
||||
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
|
||||
import * as libAPI from 'app/features/library-panels/state/api';
|
||||
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
@ -7,14 +23,28 @@ import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import { activateFullSceneTree } from '../utils/test-utils';
|
||||
import { findVizPanelByKey, getQueryRunnerFor } from '../utils/utils';
|
||||
|
||||
import { buildPanelEditScene } from './PanelEditor';
|
||||
|
||||
let pluginToLoad: PanelPlugin | undefined;
|
||||
const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => {
|
||||
return of({
|
||||
state: LoadingState.Loading,
|
||||
series: [],
|
||||
timeRange: request.range,
|
||||
});
|
||||
});
|
||||
|
||||
let pluginPromise: Promise<PanelPlugin> | undefined;
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
|
||||
return runRequestMock(ds, request);
|
||||
},
|
||||
getPluginImportUtils: () => ({
|
||||
getPanelPluginFromCache: jest.fn(() => pluginToLoad),
|
||||
getPanelPluginFromCache: jest.fn(() => undefined),
|
||||
importPanelPlugin: () => pluginPromise,
|
||||
}),
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
@ -29,103 +59,134 @@ jest.mock('@grafana/runtime', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const dataSources = {
|
||||
ds1: mockDataSource({
|
||||
uid: 'ds1',
|
||||
type: DataSourceType.Prometheus,
|
||||
}),
|
||||
};
|
||||
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
|
||||
let deactivate: CancelActivationHandler | undefined;
|
||||
|
||||
describe('PanelEditor', () => {
|
||||
describe('When closing editor', () => {
|
||||
it('should apply changes automatically', () => {
|
||||
pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true });
|
||||
|
||||
const panel = new VizPanel({
|
||||
key: 'panel-1',
|
||||
pluginId: 'text',
|
||||
});
|
||||
|
||||
const gridItem = new DashboardGridItem({ body: panel });
|
||||
|
||||
const editScene = buildPanelEditScene(panel);
|
||||
const scene = new DashboardScene({
|
||||
editPanel: editScene,
|
||||
isEditing: true,
|
||||
body: new SceneGridLayout({
|
||||
children: [gridItem],
|
||||
}),
|
||||
});
|
||||
|
||||
const deactivate = activateFullSceneTree(scene);
|
||||
|
||||
editScene.state.vizManager.state.panel.setState({ title: 'changed title' });
|
||||
|
||||
afterEach(() => {
|
||||
if (deactivate) {
|
||||
deactivate();
|
||||
|
||||
const updatedPanel = gridItem.state.body as VizPanel;
|
||||
expect(updatedPanel?.state.title).toBe('changed title');
|
||||
deactivate = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
it('should discard changes when unmounted and discard changes is marked as true', () => {
|
||||
pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true });
|
||||
describe('When initializing', () => {
|
||||
it('should wait for panel plugin to load', async () => {
|
||||
const { panelEditor, panel, pluginResolve, dashboard } = await setup({ skipWait: true });
|
||||
|
||||
const panel = new VizPanel({
|
||||
key: 'panel-1',
|
||||
pluginId: 'text',
|
||||
expect(panel.state.options).toEqual({});
|
||||
expect(panelEditor.state.isInitializing).toBe(true);
|
||||
|
||||
const pluginToLoad = getPanelPlugin({ id: 'text' }).setPanelOptions((build) => {
|
||||
build.addBooleanSwitch({
|
||||
path: 'showHeader',
|
||||
name: 'Show header',
|
||||
defaultValue: true,
|
||||
});
|
||||
});
|
||||
|
||||
const gridItem = new DashboardGridItem({ body: panel });
|
||||
pluginResolve(pluginToLoad);
|
||||
|
||||
const editScene = buildPanelEditScene(panel);
|
||||
const scene = new DashboardScene({
|
||||
editPanel: editScene,
|
||||
isEditing: true,
|
||||
body: new SceneGridLayout({
|
||||
children: [gridItem],
|
||||
}),
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(panelEditor.state.isInitializing).toBe(false);
|
||||
expect(panel.state.options).toEqual({ showHeader: true });
|
||||
|
||||
panel.onOptionsChange({ showHeader: false });
|
||||
panelEditor.onDiscard();
|
||||
|
||||
const discardedPanel = findVizPanelByKey(dashboard, panel.state.key!)!;
|
||||
expect(discardedPanel.state.options).toEqual({ showHeader: true });
|
||||
});
|
||||
});
|
||||
|
||||
const deactivate = activateFullSceneTree(scene);
|
||||
describe('When discarding', () => {
|
||||
it('should discard changes revert all changes', async () => {
|
||||
const { panelEditor, panel, dashboard } = await setup();
|
||||
|
||||
editScene.state.vizManager.state.panel.setState({ title: 'changed title' });
|
||||
panel.setState({ title: 'changed title' });
|
||||
panelEditor.onDiscard();
|
||||
|
||||
editScene.onDiscard();
|
||||
deactivate();
|
||||
const discardedPanel = findVizPanelByKey(dashboard, panel.state.key!)!;
|
||||
|
||||
const updatedPanel = gridItem.state.body as VizPanel;
|
||||
expect(updatedPanel?.state.title).toBe(panel.state.title);
|
||||
expect(discardedPanel.state.title).toBe('original title');
|
||||
});
|
||||
|
||||
it('should discard a newly added panel', () => {
|
||||
pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true });
|
||||
it('should discard a newly added panel', async () => {
|
||||
const { panelEditor, dashboard } = await setup({ isNewPanel: true });
|
||||
panelEditor.onDiscard();
|
||||
|
||||
const panel = new VizPanel({
|
||||
key: 'panel-1',
|
||||
pluginId: 'text',
|
||||
expect((dashboard.state.body as SceneGridLayout).state.children.length).toBe(0);
|
||||
});
|
||||
|
||||
const gridItem = new DashboardGridItem({ body: panel });
|
||||
it('should discard query runner changes', async () => {
|
||||
const { panelEditor, panel, dashboard } = await setup({});
|
||||
|
||||
const editScene = buildPanelEditScene(panel, true);
|
||||
const scene = new DashboardScene({
|
||||
editPanel: editScene,
|
||||
isEditing: true,
|
||||
body: new SceneGridLayout({
|
||||
children: [gridItem],
|
||||
}),
|
||||
const queryRunner = getQueryRunnerFor(panel);
|
||||
queryRunner?.setState({ maxDataPoints: 123, queries: [{ refId: 'A' }, { refId: 'B' }] });
|
||||
|
||||
panelEditor.onDiscard();
|
||||
|
||||
const discardedPanel = findVizPanelByKey(dashboard, panel.state.key!)!;
|
||||
const restoredQueryRunner = getQueryRunnerFor(discardedPanel);
|
||||
expect(restoredQueryRunner?.state.maxDataPoints).toBe(500);
|
||||
expect(restoredQueryRunner?.state.queries.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
editScene.onDiscard();
|
||||
const deactivate = activateFullSceneTree(scene);
|
||||
describe('When changes are made', () => {
|
||||
it('Should set state to dirty', async () => {
|
||||
const { panelEditor, panel } = await setup({});
|
||||
|
||||
deactivate();
|
||||
expect(panelEditor.state.isDirty).toBe(undefined);
|
||||
|
||||
expect((scene.state.body as SceneGridLayout).state.children.length).toBe(0);
|
||||
panel.setState({ title: 'changed title' });
|
||||
|
||||
expect(panelEditor.state.isDirty).toBe(true);
|
||||
});
|
||||
|
||||
it('Should reset dirty and orginal state when dashboard is saved', async () => {
|
||||
const { panelEditor, panel } = await setup({});
|
||||
|
||||
expect(panelEditor.state.isDirty).toBe(undefined);
|
||||
|
||||
panel.setState({ title: 'changed title' });
|
||||
|
||||
panelEditor.dashboardSaved();
|
||||
|
||||
expect(panelEditor.state.isDirty).toBe(false);
|
||||
|
||||
panel.setState({ title: 'changed title 2' });
|
||||
|
||||
expect(panelEditor.state.isDirty).toBe(true);
|
||||
|
||||
// Change back to already saved state
|
||||
panel.setState({ title: 'changed title' });
|
||||
expect(panelEditor.state.isDirty).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When opening a repeated panel', () => {
|
||||
it('Should default to the first variable value if panel is repeated', async () => {
|
||||
const { panel } = await setup({ repeatByVariable: 'server' });
|
||||
const variable = sceneGraph.lookupVariable('server', panel);
|
||||
expect(variable?.getValue()).toBe('A');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Handling library panels', () => {
|
||||
it('should call the api with the updated panel', async () => {
|
||||
pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true });
|
||||
const panel = new VizPanel({
|
||||
key: 'panel-1',
|
||||
pluginId: 'text',
|
||||
});
|
||||
pluginPromise = Promise.resolve(getPanelPlugin({ id: 'text', skipDataQuery: true }));
|
||||
|
||||
const panel = new VizPanel({ key: 'panel-1', pluginId: 'text' });
|
||||
const libraryPanelModel = {
|
||||
title: 'title',
|
||||
uid: 'uid',
|
||||
@ -143,15 +204,13 @@ describe('PanelEditor', () => {
|
||||
_loadedPanel: libraryPanelModel,
|
||||
});
|
||||
|
||||
panel.setState({
|
||||
$behaviors: [libPanelBehavior],
|
||||
});
|
||||
panel.setState({ $behaviors: [libPanelBehavior] });
|
||||
|
||||
const gridItem = new DashboardGridItem({ body: panel });
|
||||
|
||||
const editScene = buildPanelEditScene(panel);
|
||||
const scene = new DashboardScene({
|
||||
editPanel: editScene,
|
||||
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
|
||||
isEditing: true,
|
||||
body: new SceneGridLayout({
|
||||
children: [gridItem],
|
||||
@ -160,96 +219,133 @@ describe('PanelEditor', () => {
|
||||
|
||||
activateFullSceneTree(scene);
|
||||
|
||||
editScene.state.vizManager.state.panel.setState({ title: 'changed title' });
|
||||
(editScene.state.vizManager.state.panel.state.$behaviors![0] as LibraryPanelBehavior).setState({
|
||||
name: 'changed name',
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
panel.setState({ title: 'changed title' });
|
||||
libPanelBehavior.setState({ name: 'changed name' });
|
||||
|
||||
jest.spyOn(libAPI, 'saveLibPanel').mockImplementation(async (panel) => {
|
||||
const updatedPanel = { ...libAPI.libraryVizPanelToSaveModel(panel), version: 2 };
|
||||
|
||||
libPanelBehavior.setPanelFromLibPanel(updatedPanel);
|
||||
});
|
||||
|
||||
editScene.state.vizManager.commitChanges();
|
||||
editScene.onConfirmSaveLibraryPanel();
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
await new Promise(process.nextTick); // Wait for mock api to return and update the library panel
|
||||
// Wait for mock api to return and update the library panel
|
||||
expect(libPanelBehavior.state._loadedPanel?.version).toBe(2);
|
||||
expect(libPanelBehavior.state.name).toBe('changed name');
|
||||
expect(libPanelBehavior.state.title).toBe('changed title');
|
||||
expect((gridItem.state.body as VizPanel).state.title).toBe('changed title');
|
||||
});
|
||||
|
||||
it('unlinks library panel', () => {
|
||||
const libraryPanelModel = {
|
||||
title: 'title',
|
||||
uid: 'uid',
|
||||
name: 'libraryPanelName',
|
||||
model: {
|
||||
title: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
type: 'panel',
|
||||
version: 1,
|
||||
};
|
||||
|
||||
const libPanelBehavior = new LibraryPanelBehavior({
|
||||
isLoaded: true,
|
||||
title: libraryPanelModel.title,
|
||||
uid: libraryPanelModel.uid,
|
||||
name: libraryPanelModel.name,
|
||||
_loadedPanel: libraryPanelModel,
|
||||
});
|
||||
|
||||
// Just adding an extra stateless behavior to verify unlinking does not remvoe it
|
||||
const otherBehavior = jest.fn();
|
||||
const panel = new VizPanel({ key: 'panel-1', pluginId: 'text', $behaviors: [libPanelBehavior, otherBehavior] });
|
||||
const editScene = buildPanelEditScene(panel);
|
||||
editScene.onConfirmUnlinkLibraryPanel();
|
||||
|
||||
expect(panel.state.$behaviors?.length).toBe(1);
|
||||
expect(panel.state.$behaviors![0]).toBe(otherBehavior);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PanelDataPane', () => {
|
||||
it('should not exist if panel is skipDataQuery', () => {
|
||||
pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true });
|
||||
it('should not exist if panel is skipDataQuery', async () => {
|
||||
const { panelEditor } = await setup({ pluginSkipDataQuery: true });
|
||||
expect(panelEditor.state.dataPane).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should exist if panel is supporting querying', async () => {
|
||||
const { panelEditor } = await setup({ pluginSkipDataQuery: false });
|
||||
expect(panelEditor.state.dataPane).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
interface SetupOptions {
|
||||
isNewPanel?: boolean;
|
||||
pluginSkipDataQuery?: boolean;
|
||||
repeatByVariable?: string;
|
||||
skipWait?: boolean;
|
||||
pluginLoadTime?: number;
|
||||
}
|
||||
|
||||
async function setup(options: SetupOptions = {}) {
|
||||
const pluginToLoad = getPanelPlugin({ id: 'text', skipDataQuery: options.pluginSkipDataQuery });
|
||||
let pluginResolve = (plugin: PanelPlugin) => {};
|
||||
|
||||
pluginPromise = new Promise<PanelPlugin>((resolve) => {
|
||||
pluginResolve = resolve;
|
||||
});
|
||||
|
||||
const panel = new VizPanel({
|
||||
key: 'panel-1',
|
||||
pluginId: 'text',
|
||||
});
|
||||
new DashboardGridItem({
|
||||
body: panel,
|
||||
title: 'original title',
|
||||
$data: new SceneDataTransformer({
|
||||
transformations: [],
|
||||
$data: new SceneQueryRunner({
|
||||
queries: [{ refId: 'A' }],
|
||||
maxDataPoints: 500,
|
||||
datasource: { uid: 'ds1' },
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const editScene = buildPanelEditScene(panel);
|
||||
const scene = new DashboardScene({
|
||||
editPanel: editScene,
|
||||
const gridItem = new DashboardGridItem({ body: panel, variableName: options.repeatByVariable });
|
||||
|
||||
const panelEditor = buildPanelEditScene(panel, options.isNewPanel);
|
||||
const dashboard = new DashboardScene({
|
||||
editPanel: panelEditor,
|
||||
isEditing: true,
|
||||
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
|
||||
$variables: new SceneVariableSet({
|
||||
variables: [
|
||||
new CustomVariable({
|
||||
name: 'server',
|
||||
query: 'A,B,C',
|
||||
isMulti: true,
|
||||
value: ['A', 'B', 'C'],
|
||||
text: ['A', 'B', 'C'],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
body: new SceneGridLayout({
|
||||
children: [gridItem],
|
||||
}),
|
||||
});
|
||||
|
||||
activateFullSceneTree(scene);
|
||||
panelEditor.debounceSaveModelDiff = false;
|
||||
|
||||
expect(editScene.state.dataPane).toBeUndefined();
|
||||
});
|
||||
deactivate = activateFullSceneTree(dashboard);
|
||||
|
||||
it('should exist if panel is supporting querying', () => {
|
||||
pluginToLoad = getTestPanelPlugin({ id: 'timeseries' });
|
||||
|
||||
const panel = new VizPanel({
|
||||
key: 'panel-1',
|
||||
pluginId: 'timeseries',
|
||||
});
|
||||
|
||||
new DashboardGridItem({
|
||||
body: panel,
|
||||
});
|
||||
const editScene = buildPanelEditScene(panel);
|
||||
const scene = new DashboardScene({
|
||||
editPanel: editScene,
|
||||
});
|
||||
|
||||
activateFullSceneTree(scene);
|
||||
expect(editScene.state.dataPane).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export function getTestPanelPlugin(options: Partial<PanelPluginMeta>): PanelPlugin {
|
||||
const plugin = new PanelPlugin(() => null);
|
||||
plugin.meta = {
|
||||
id: options.id!,
|
||||
type: PluginType.panel,
|
||||
name: options.id!,
|
||||
sort: options.sort || 1,
|
||||
info: {
|
||||
author: {
|
||||
name: options.id + 'name',
|
||||
},
|
||||
description: '',
|
||||
links: [],
|
||||
logos: {
|
||||
large: '',
|
||||
small: '',
|
||||
},
|
||||
screenshots: [],
|
||||
updated: '',
|
||||
version: '1.0.',
|
||||
},
|
||||
hideFromList: options.hideFromList === true,
|
||||
module: options.module ?? '',
|
||||
baseUrl: '',
|
||||
skipDataQuery: options.skipDataQuery ?? false,
|
||||
};
|
||||
return plugin;
|
||||
if (!options.skipWait) {
|
||||
//console.log('pluginResolve(pluginToLoad)');
|
||||
pluginResolve(pluginToLoad);
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
}
|
||||
|
||||
return { dashboard, panel, gridItem, panelEditor, pluginResolve };
|
||||
}
|
||||
|
@ -1,72 +1,186 @@
|
||||
import * as H from 'history';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { NavIndex } from '@grafana/data';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
|
||||
import { NavIndex, PanelPlugin } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import {
|
||||
PanelBuilders,
|
||||
SceneObjectBase,
|
||||
SceneObjectRef,
|
||||
SceneObjectState,
|
||||
SceneObjectStateChangedEvent,
|
||||
sceneUtils,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { Panel } from '@grafana/schema/dist/esm/index.gen';
|
||||
import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
|
||||
import { saveLibPanel } from 'app/features/library-panels/state/api';
|
||||
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils';
|
||||
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
|
||||
import { getPanelChanges } from '../saving/getDashboardChanges';
|
||||
import { DashboardGridItem, DashboardGridItemState } from '../scene/DashboardGridItem';
|
||||
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import {
|
||||
activateInActiveParents,
|
||||
getDashboardSceneFor,
|
||||
getLibraryPanelBehavior,
|
||||
getPanelIdForVizPanel,
|
||||
} from '../utils/utils';
|
||||
|
||||
import { DataProviderSharer } from './PanelDataPane/DataProviderSharer';
|
||||
import { PanelDataPane } from './PanelDataPane/PanelDataPane';
|
||||
import { PanelEditorRenderer } from './PanelEditorRenderer';
|
||||
import { PanelOptionsPane } from './PanelOptionsPane';
|
||||
import { VizPanelManager, VizPanelManagerState } from './VizPanelManager';
|
||||
|
||||
export interface PanelEditorState extends SceneObjectState {
|
||||
isNewPanel: boolean;
|
||||
isDirty?: boolean;
|
||||
panelId: number;
|
||||
optionsPane: PanelOptionsPane;
|
||||
optionsPane?: PanelOptionsPane;
|
||||
dataPane?: PanelDataPane;
|
||||
vizManager: VizPanelManager;
|
||||
panelRef: SceneObjectRef<VizPanel>;
|
||||
showLibraryPanelSaveModal?: boolean;
|
||||
showLibraryPanelUnlinkModal?: boolean;
|
||||
tableView?: VizPanel;
|
||||
pluginLoadErrror?: string;
|
||||
/**
|
||||
* Waiting for library panel or panel plugin to load
|
||||
*/
|
||||
isInitializing?: boolean;
|
||||
}
|
||||
|
||||
export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
private _initialRepeatOptions: Pick<VizPanelManagerState, 'repeat' | 'repeatDirection' | 'maxPerRow'> = {};
|
||||
static Component = PanelEditorRenderer;
|
||||
|
||||
private _discardChanges = false;
|
||||
private _originalLayoutElementState!: DashboardGridItemState;
|
||||
private _layoutElement!: DashboardGridItem;
|
||||
private _originalSaveModel!: Panel;
|
||||
|
||||
public constructor(state: PanelEditorState) {
|
||||
super(state);
|
||||
|
||||
const { repeat, repeatDirection, maxPerRow } = state.vizManager.state;
|
||||
this._initialRepeatOptions = {
|
||||
repeat,
|
||||
repeatDirection,
|
||||
maxPerRow,
|
||||
};
|
||||
|
||||
this.setOriginalState(this.state.panelRef);
|
||||
this.addActivationHandler(this._activationHandler.bind(this));
|
||||
}
|
||||
|
||||
private _activationHandler() {
|
||||
const panelManager = this.state.vizManager;
|
||||
const panel = panelManager.state.panel;
|
||||
const panel = this.state.panelRef.resolve();
|
||||
const deactivateParents = activateInActiveParents(panel);
|
||||
const layoutElement = panel.parent;
|
||||
|
||||
this.waitForPlugin();
|
||||
|
||||
return () => {
|
||||
if (layoutElement instanceof DashboardGridItem) {
|
||||
layoutElement.editingCompleted();
|
||||
}
|
||||
if (deactivateParents) {
|
||||
deactivateParents();
|
||||
}
|
||||
};
|
||||
}
|
||||
private waitForPlugin(retry = 0) {
|
||||
const panel = this.getPanel();
|
||||
const plugin = panel.getPlugin();
|
||||
|
||||
if (!plugin || plugin.meta.id !== panel.state.pluginId) {
|
||||
if (retry < 100) {
|
||||
setTimeout(() => this.waitForPlugin(retry + 1), retry * 10);
|
||||
} else {
|
||||
this.setState({ pluginLoadErrror: 'Failed to load panel plugin' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.gotPanelPlugin(plugin);
|
||||
}
|
||||
|
||||
private setOriginalState(panelRef: SceneObjectRef<VizPanel>) {
|
||||
const panel = panelRef.resolve();
|
||||
|
||||
this._originalSaveModel = vizPanelToPanel(panel);
|
||||
|
||||
if (panel.parent instanceof DashboardGridItem) {
|
||||
this._originalLayoutElementState = sceneUtils.cloneSceneObjectState(panel.parent.state);
|
||||
this._layoutElement = panel.parent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful for testing to turn on debounce
|
||||
*/
|
||||
public debounceSaveModelDiff = true;
|
||||
|
||||
/**
|
||||
* Subscribe to state changes and check if the save model has changed
|
||||
*/
|
||||
private _setupChangeDetection() {
|
||||
const panel = this.state.panelRef.resolve();
|
||||
const performSaveModelDiff = () => {
|
||||
const { hasChanges } = getPanelChanges(this._originalSaveModel, vizPanelToPanel(panel));
|
||||
this.setState({ isDirty: hasChanges });
|
||||
};
|
||||
|
||||
const performSaveModelDiffDebounced = this.debounceSaveModelDiff
|
||||
? debounce(performSaveModelDiff, 250)
|
||||
: performSaveModelDiff;
|
||||
|
||||
const handleStateChange = (event: SceneObjectStateChangedEvent) => {
|
||||
if (DashboardSceneChangeTracker.isUpdatingPersistedState(event)) {
|
||||
performSaveModelDiffDebounced();
|
||||
}
|
||||
};
|
||||
|
||||
this._subs.add(panel.subscribeToEvent(SceneObjectStateChangedEvent, handleStateChange));
|
||||
// Repeat options live on the layout element (DashboardGridItem)
|
||||
this._subs.add(this._layoutElement.subscribeToEvent(SceneObjectStateChangedEvent, handleStateChange));
|
||||
}
|
||||
|
||||
public getPanel(): VizPanel {
|
||||
return this.state.panelRef?.resolve();
|
||||
}
|
||||
|
||||
private gotPanelPlugin(plugin: PanelPlugin) {
|
||||
const panel = this.getPanel();
|
||||
const layoutElement = panel.parent;
|
||||
|
||||
// First time initialization
|
||||
if (this.state.isInitializing) {
|
||||
this.setOriginalState(this.state.panelRef);
|
||||
|
||||
if (layoutElement instanceof DashboardGridItem) {
|
||||
layoutElement.editingStarted();
|
||||
}
|
||||
|
||||
this._setupChangeDetection();
|
||||
this._updateDataPane(plugin);
|
||||
|
||||
// Listen for panel plugin changes
|
||||
this._subs.add(
|
||||
panelManager.subscribeToState((n, p) => {
|
||||
panel.subscribeToState((n, p) => {
|
||||
if (n.pluginId !== p.pluginId) {
|
||||
this._initDataPane(n.pluginId);
|
||||
this.waitForPlugin();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this._initDataPane(panel.state.pluginId);
|
||||
|
||||
return () => {
|
||||
if (!this._discardChanges) {
|
||||
this.commitChanges();
|
||||
} else if (this.state.isNewPanel) {
|
||||
getDashboardSceneFor(this).removePanel(panelManager.state.sourcePanel.resolve()!);
|
||||
// Setup options pane
|
||||
this.setState({
|
||||
optionsPane: new PanelOptionsPane({
|
||||
panelRef: this.state.panelRef,
|
||||
searchQuery: '',
|
||||
listMode: OptionFilter.All,
|
||||
}),
|
||||
isInitializing: false,
|
||||
});
|
||||
} else {
|
||||
// plugin changed after first time initialization
|
||||
// Just update data pane
|
||||
this._updateDataPane(plugin);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _initDataPane(pluginId: string) {
|
||||
const skipDataQuery = config.panels[pluginId]?.skipDataQuery;
|
||||
private _updateDataPane(plugin: PanelPlugin) {
|
||||
const skipDataQuery = plugin.meta.skipDataQuery;
|
||||
|
||||
if (skipDataQuery && this.state.dataPane) {
|
||||
locationService.partial({ tab: null }, true);
|
||||
@ -74,12 +188,16 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
}
|
||||
|
||||
if (!skipDataQuery && !this.state.dataPane) {
|
||||
this.setState({ dataPane: new PanelDataPane(this.state.vizManager) });
|
||||
this.setState({ dataPane: PanelDataPane.createFor(this.getPanel()) });
|
||||
}
|
||||
}
|
||||
|
||||
public getUrlKey() {
|
||||
return this.state.panelId.toString();
|
||||
return this.getPanelId().toString();
|
||||
}
|
||||
|
||||
public getPanelId() {
|
||||
return getPanelIdForVizPanel(this.state.panelRef.resolve());
|
||||
}
|
||||
|
||||
public getPageNav(location: H.Location, navIndex: NavIndex) {
|
||||
@ -92,53 +210,23 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
}
|
||||
|
||||
public onDiscard = () => {
|
||||
this.state.vizManager.setState({ isDirty: false });
|
||||
this._discardChanges = true;
|
||||
this.setState({ isDirty: false });
|
||||
|
||||
const panel = this.state.panelRef.resolve();
|
||||
|
||||
if (this.state.isNewPanel) {
|
||||
getDashboardSceneFor(this).removePanel(panel);
|
||||
} else {
|
||||
// Revert any layout element changes
|
||||
this._layoutElement.setState(this._originalLayoutElementState!);
|
||||
}
|
||||
|
||||
locationService.partial({ editPanel: null });
|
||||
};
|
||||
|
||||
public commitChanges() {
|
||||
const dashboard = getDashboardSceneFor(this);
|
||||
|
||||
if (!dashboard.state.isEditing) {
|
||||
dashboard.onEnterEditMode();
|
||||
}
|
||||
|
||||
const panelManager = this.state.vizManager;
|
||||
const sourcePanel = panelManager.state.sourcePanel.resolve();
|
||||
const gridItem = sourcePanel!.parent;
|
||||
|
||||
if (!(gridItem instanceof DashboardGridItem)) {
|
||||
console.error('Unsupported scene object type');
|
||||
return;
|
||||
}
|
||||
|
||||
this.commitChangesToSource(gridItem);
|
||||
}
|
||||
|
||||
private commitChangesToSource(gridItem: DashboardGridItem) {
|
||||
let width = gridItem.state.width ?? 1;
|
||||
let height = gridItem.state.height;
|
||||
|
||||
const panelManager = this.state.vizManager;
|
||||
const horizontalToVertical =
|
||||
this._initialRepeatOptions.repeatDirection === 'h' && panelManager.state.repeatDirection === 'v';
|
||||
const verticalToHorizontal =
|
||||
this._initialRepeatOptions.repeatDirection === 'v' && panelManager.state.repeatDirection === 'h';
|
||||
if (horizontalToVertical) {
|
||||
width = Math.floor(width / (gridItem.state.maxPerRow ?? 1));
|
||||
} else if (verticalToHorizontal) {
|
||||
width = 24;
|
||||
}
|
||||
|
||||
gridItem.setState({
|
||||
body: panelManager.state.panel.clone(),
|
||||
repeatDirection: panelManager.state.repeatDirection,
|
||||
variableName: panelManager.state.repeat,
|
||||
maxPerRow: panelManager.state.maxPerRow,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
public dashboardSaved() {
|
||||
this.setOriginalState(this.state.panelRef);
|
||||
this.setState({ isDirty: false });
|
||||
}
|
||||
|
||||
public onSaveLibraryPanel = () => {
|
||||
@ -146,8 +234,8 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
};
|
||||
|
||||
public onConfirmSaveLibraryPanel = () => {
|
||||
this.state.vizManager.commitChanges();
|
||||
this.state.vizManager.setState({ isDirty: false });
|
||||
saveLibPanel(this.state.panelRef.resolve());
|
||||
this.setState({ isDirty: false });
|
||||
locationService.partial({ editPanel: null });
|
||||
};
|
||||
|
||||
@ -164,16 +252,43 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
};
|
||||
|
||||
public onConfirmUnlinkLibraryPanel = () => {
|
||||
this.state.vizManager.unlinkLibraryPanel();
|
||||
const libPanelBehavior = getLibraryPanelBehavior(this.getPanel());
|
||||
if (!libPanelBehavior) {
|
||||
return;
|
||||
}
|
||||
|
||||
libPanelBehavior.unlink();
|
||||
|
||||
this.setState({ showLibraryPanelUnlinkModal: false });
|
||||
};
|
||||
|
||||
public onToggleTableView = () => {
|
||||
if (this.state.tableView) {
|
||||
this.setState({ tableView: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
const panel = this.state.panelRef.resolve();
|
||||
const dataProvider = panel.state.$data;
|
||||
if (!dataProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
tableView: PanelBuilders.table()
|
||||
.setTitle('')
|
||||
.setOption('showTypeIcons', true)
|
||||
.setOption('showHeader', true)
|
||||
.setData(new DataProviderSharer({ source: dataProvider.getRef() }))
|
||||
.build(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPanelEditScene(panel: VizPanel, isNewPanel = false): PanelEditor {
|
||||
return new PanelEditor({
|
||||
panelId: getPanelIdForVizPanel(panel),
|
||||
optionsPane: new PanelOptionsPane({}),
|
||||
vizManager: VizPanelManager.createFor(panel),
|
||||
isInitializing: true,
|
||||
panelRef: panel.getRef(),
|
||||
isNewPanel,
|
||||
});
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ import { css, cx } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { SceneComponentProps } from '@grafana/scenes';
|
||||
import { Button, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||
import { SceneComponentProps, VizPanel } from '@grafana/scenes';
|
||||
import { Button, Spinner, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||
import { UnlinkModal } from '../scene/UnlinkModal';
|
||||
@ -54,7 +54,8 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!splitterState.collapsed && <optionsPane.Component model={optionsPane} />}
|
||||
{!splitterState.collapsed && optionsPane && <optionsPane.Component model={optionsPane} />}
|
||||
{!splitterState.collapsed && !optionsPane && <Spinner />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@ -63,9 +64,9 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
||||
|
||||
function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
const { vizManager, dataPane, showLibraryPanelSaveModal, showLibraryPanelUnlinkModal } = model.useState();
|
||||
const { sourcePanel } = vizManager.useState();
|
||||
const libraryPanel = getLibraryPanelBehavior(sourcePanel.resolve());
|
||||
const { dataPane, showLibraryPanelSaveModal, showLibraryPanelUnlinkModal, tableView } = model.useState();
|
||||
const panel = model.getPanel();
|
||||
const libraryPanel = getLibraryPanelBehavior(panel);
|
||||
const { controls } = dashboard.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
@ -94,7 +95,7 @@ function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
|
||||
)}
|
||||
<div {...containerProps}>
|
||||
<div {...primaryProps}>
|
||||
<vizManager.Component model={vizManager} />
|
||||
<VizWrapper panel={panel} tableView={tableView} />
|
||||
</div>
|
||||
{showLibraryPanelSaveModal && libraryPanel && (
|
||||
<SaveLibraryVizPanelModal
|
||||
@ -137,6 +138,22 @@ function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
|
||||
);
|
||||
}
|
||||
|
||||
interface VizWrapperProps {
|
||||
panel: VizPanel;
|
||||
tableView?: VizPanel;
|
||||
}
|
||||
|
||||
function VizWrapper({ panel, tableView }: VizWrapperProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const panelToShow = tableView ?? panel;
|
||||
|
||||
return (
|
||||
<div className={styles.vizWrapper}>
|
||||
<panelToShow.Component model={panelToShow} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
pageContainer: css({
|
||||
@ -215,5 +232,10 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
rotate: '-90deg',
|
||||
},
|
||||
}),
|
||||
vizWrapper: css({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
paddingLeft: theme.spacing(2),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import { activateFullSceneTree } from '../utils/test-utils';
|
||||
import * as utils from '../utils/utils';
|
||||
|
||||
import { PanelOptions } from './PanelOptions';
|
||||
import { VizPanelManager } from './VizPanelManager';
|
||||
import { PanelOptionsPane } from './PanelOptionsPane';
|
||||
|
||||
const OptionsPaneSelector = selectors.components.PanelEditor.OptionsPane;
|
||||
|
||||
@ -92,43 +92,47 @@ function setup(options: SetupOptions = {}) {
|
||||
}
|
||||
|
||||
// need to wait for plugin to load
|
||||
const vizManager = VizPanelManager.createFor(panel);
|
||||
const panelOptionsScene = new PanelOptionsPane({
|
||||
panelRef: panel.getRef(),
|
||||
searchQuery: '',
|
||||
listMode: OptionFilter.All,
|
||||
});
|
||||
|
||||
activateFullSceneTree(vizManager);
|
||||
|
||||
const panelOptions = <PanelOptions vizManager={vizManager} searchQuery="" listMode={OptionFilter.All}></PanelOptions>;
|
||||
activateFullSceneTree(panelOptionsScene);
|
||||
panel.activate();
|
||||
|
||||
const panelOptions = <PanelOptions panel={panel} searchQuery="" listMode={OptionFilter.All}></PanelOptions>;
|
||||
const renderResult = render(panelOptions);
|
||||
|
||||
return { renderResult, vizManager };
|
||||
return { renderResult, panelOptionsScene, panel };
|
||||
}
|
||||
|
||||
describe('PanelOptions', () => {
|
||||
describe('Can render and edit panel frame options', () => {
|
||||
it('Can edit title', async () => {
|
||||
const { vizManager } = setup();
|
||||
const { panel } = setup();
|
||||
|
||||
expect(screen.getByLabelText(OptionsPaneSelector.fieldLabel('Panel options Title'))).toBeInTheDocument();
|
||||
|
||||
const input = screen.getByTestId(selectors.components.PanelEditor.OptionsPane.fieldInput('Title'));
|
||||
fireEvent.change(input, { target: { value: 'New title' } });
|
||||
|
||||
expect(vizManager.state.panel.state.title).toBe('New title');
|
||||
expect(panel.state.title).toBe('New title');
|
||||
});
|
||||
|
||||
it('Clearing title should set hoverHeader to true', async () => {
|
||||
const { vizManager } = setup();
|
||||
const { panel } = setup();
|
||||
|
||||
expect(screen.getByLabelText(OptionsPaneSelector.fieldLabel('Panel options Title'))).toBeInTheDocument();
|
||||
|
||||
const input = screen.getByTestId(selectors.components.PanelEditor.OptionsPane.fieldInput('Title'));
|
||||
fireEvent.change(input, { target: { value: '' } });
|
||||
|
||||
expect(vizManager.state.panel.state.title).toBe('');
|
||||
expect(vizManager.state.panel.state.hoverHeader).toBe(true);
|
||||
expect(panel.state.title).toBe('');
|
||||
expect(panel.state.hoverHeader).toBe(true);
|
||||
|
||||
fireEvent.change(input, { target: { value: 'Muu' } });
|
||||
expect(vizManager.state.panel.state.hoverHeader).toBe(false);
|
||||
expect(panel.state.hoverHeader).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -179,13 +183,11 @@ describe('PanelOptions', () => {
|
||||
_loadedPanel: libraryPanelModel,
|
||||
});
|
||||
|
||||
panel.setState({
|
||||
$behaviors: [libraryPanel],
|
||||
});
|
||||
panel.setState({ $behaviors: [libraryPanel] });
|
||||
|
||||
new DashboardGridItem({ body: panel });
|
||||
|
||||
const { renderResult, vizManager } = setup({ panel: panel });
|
||||
const { renderResult } = setup({ panel: panel });
|
||||
|
||||
const input = await renderResult.findByTestId('library panel name input');
|
||||
|
||||
@ -193,8 +195,6 @@ describe('PanelOptions', () => {
|
||||
fireEvent.blur(input, { target: { value: 'new library panel name' } });
|
||||
});
|
||||
|
||||
expect((vizManager.state.panel.state.$behaviors![0] as LibraryPanelBehavior).state.name).toBe(
|
||||
'new library panel name'
|
||||
);
|
||||
expect((panel.state.$behaviors![0] as LibraryPanelBehavior).state.name).toBe('new library panel name');
|
||||
});
|
||||
});
|
||||
|
@ -13,24 +13,23 @@ import {
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils';
|
||||
|
||||
import { VizPanelManager } from './VizPanelManager';
|
||||
import { getPanelFrameCategory2 } from './getPanelFrameOptions';
|
||||
|
||||
interface Props {
|
||||
vizManager: VizPanelManager;
|
||||
panel: VizPanel;
|
||||
searchQuery: string;
|
||||
listMode: OptionFilter;
|
||||
data?: PanelData;
|
||||
}
|
||||
|
||||
export const PanelOptions = React.memo<Props>(({ vizManager, searchQuery, listMode, data }) => {
|
||||
const { panel, repeat } = vizManager.useState();
|
||||
export const PanelOptions = React.memo<Props>(({ panel, searchQuery, listMode, data }) => {
|
||||
const { options, fieldConfig, _pluginInstanceState } = panel.useState();
|
||||
const layoutElement = panel.parent!;
|
||||
const layoutElementState = layoutElement.useState();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const panelFrameOptions = useMemo(
|
||||
() => getPanelFrameCategory2(vizManager, panel, repeat),
|
||||
[vizManager, panel, repeat]
|
||||
() => getPanelFrameCategory2(panel, layoutElementState),
|
||||
[panel, layoutElementState]
|
||||
);
|
||||
|
||||
const visualizationOptions = useMemo(() => {
|
||||
|
@ -0,0 +1,101 @@
|
||||
import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper';
|
||||
import { findVizPanelByKey } from '../utils/utils';
|
||||
|
||||
import { PanelOptionsPane } from './PanelOptionsPane';
|
||||
import { testDashboard } from './testfiles/testDashboard';
|
||||
|
||||
describe('PanelOptionsPane', () => {
|
||||
describe('When changing plugin', () => {
|
||||
it('Should set the cache', () => {
|
||||
const { optionsPane, panel } = setupTest('panel-1');
|
||||
panel.changePluginType = jest.fn();
|
||||
|
||||
expect(panel.state.pluginId).toBe('timeseries');
|
||||
|
||||
optionsPane.onChangePanelPlugin({ pluginId: 'table' });
|
||||
|
||||
expect(optionsPane['_cachedPluginOptions']['timeseries']?.options).toBe(panel.state.options);
|
||||
expect(optionsPane['_cachedPluginOptions']['timeseries']?.fieldConfig).toBe(panel.state.fieldConfig);
|
||||
});
|
||||
|
||||
it('Should preserve correct field config', () => {
|
||||
const { optionsPane, panel } = setupTest('panel-1');
|
||||
|
||||
const mockFn = jest.fn();
|
||||
panel.changePluginType = mockFn;
|
||||
|
||||
const fieldConfig = panel.state.fieldConfig;
|
||||
|
||||
fieldConfig.defaults = {
|
||||
...fieldConfig.defaults,
|
||||
unit: 'flop',
|
||||
decimals: 2,
|
||||
};
|
||||
|
||||
fieldConfig.overrides = [
|
||||
{
|
||||
matcher: {
|
||||
id: 'byName',
|
||||
options: 'A-series',
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
id: 'displayName',
|
||||
value: 'test',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
matcher: { id: 'byName', options: 'D-series' },
|
||||
//should be removed because it's custom
|
||||
properties: [
|
||||
{
|
||||
id: 'custom.customPropNoExist',
|
||||
value: 'google',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
panel.setState({ fieldConfig: fieldConfig });
|
||||
|
||||
expect(panel.state.fieldConfig.defaults.color?.mode).toBe('palette-classic');
|
||||
expect(panel.state.fieldConfig.defaults.thresholds?.mode).toBe('absolute');
|
||||
expect(panel.state.fieldConfig.defaults.unit).toBe('flop');
|
||||
expect(panel.state.fieldConfig.defaults.decimals).toBe(2);
|
||||
expect(panel.state.fieldConfig.overrides).toHaveLength(2);
|
||||
expect(panel.state.fieldConfig.overrides[1].properties).toHaveLength(1);
|
||||
expect(panel.state.fieldConfig.defaults.custom).toHaveProperty('axisBorderShow');
|
||||
|
||||
optionsPane.onChangePanelPlugin({ pluginId: 'table' });
|
||||
|
||||
expect(mockFn).toHaveBeenCalled();
|
||||
expect(mockFn.mock.calls[0][2].defaults.color?.mode).toBe('palette-classic');
|
||||
expect(mockFn.mock.calls[0][2].defaults.thresholds?.mode).toBe('absolute');
|
||||
expect(mockFn.mock.calls[0][2].defaults.unit).toBe('flop');
|
||||
expect(mockFn.mock.calls[0][2].defaults.decimals).toBe(2);
|
||||
expect(mockFn.mock.calls[0][2].overrides).toHaveLength(2);
|
||||
//removed custom property
|
||||
expect(mockFn.mock.calls[0][2].overrides[1].properties).toHaveLength(0);
|
||||
//removed fieldConfig custom values as well
|
||||
expect(mockFn.mock.calls[0][2].defaults.custom).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupTest(panelId: string) {
|
||||
const scene = transformSaveModelToScene({ dashboard: testDashboard, meta: {} });
|
||||
const panel = findVizPanelByKey(scene, panelId)!;
|
||||
|
||||
const optionsPane = new PanelOptionsPane({ panelRef: panel.getRef(), listMode: OptionFilter.All, searchQuery: '' });
|
||||
|
||||
// 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));
|
||||
|
||||
return { optionsPane, scene, panel };
|
||||
}
|
@ -1,15 +1,30 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { GrafanaTheme2, PanelPluginMeta } from '@grafana/data';
|
||||
import {
|
||||
FieldConfigSource,
|
||||
filterFieldConfigOverrides,
|
||||
GrafanaTheme2,
|
||||
isStandardFieldProp,
|
||||
PanelPluginMeta,
|
||||
restoreCustomOverrideRules,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState, sceneGraph } from '@grafana/scenes';
|
||||
import {
|
||||
DeepPartial,
|
||||
SceneComponentProps,
|
||||
SceneObjectBase,
|
||||
SceneObjectRef,
|
||||
SceneObjectState,
|
||||
VizPanel,
|
||||
sceneGraph,
|
||||
} from '@grafana/scenes';
|
||||
import { FilterInput, Stack, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||
import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
|
||||
import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPluginError';
|
||||
import { VizTypeChangeDetails } from 'app/features/panel/components/VizTypePicker/types';
|
||||
import { getAllPanelPluginMeta } from 'app/features/panel/state/util';
|
||||
|
||||
import { PanelEditor } from './PanelEditor';
|
||||
import { PanelOptions } from './PanelOptions';
|
||||
import { PanelVizTypePicker } from './PanelVizTypePicker';
|
||||
|
||||
@ -17,21 +32,48 @@ export interface PanelOptionsPaneState extends SceneObjectState {
|
||||
isVizPickerOpen?: boolean;
|
||||
searchQuery: string;
|
||||
listMode: OptionFilter;
|
||||
panelRef: SceneObjectRef<VizPanel>;
|
||||
}
|
||||
|
||||
interface PluginOptionsCache {
|
||||
options: DeepPartial<{}>;
|
||||
fieldConfig: FieldConfigSource<DeepPartial<{}>>;
|
||||
}
|
||||
|
||||
export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
||||
public constructor(state: Partial<PanelOptionsPaneState>) {
|
||||
super({
|
||||
searchQuery: '',
|
||||
listMode: OptionFilter.All,
|
||||
...state,
|
||||
});
|
||||
}
|
||||
private _cachedPluginOptions: Record<string, PluginOptionsCache | undefined> = {};
|
||||
|
||||
onToggleVizPicker = () => {
|
||||
this.setState({ isVizPickerOpen: !this.state.isVizPickerOpen });
|
||||
};
|
||||
|
||||
onChangePanelPlugin = (options: VizTypeChangeDetails) => {
|
||||
const panel = this.state.panelRef.resolve();
|
||||
const { options: prevOptions, fieldConfig: prevFieldConfig, pluginId: prevPluginId } = panel.state;
|
||||
const pluginId = options.pluginId;
|
||||
|
||||
// clear custom options
|
||||
let newFieldConfig: FieldConfigSource = {
|
||||
defaults: {
|
||||
...prevFieldConfig.defaults,
|
||||
custom: {},
|
||||
},
|
||||
overrides: filterFieldConfigOverrides(prevFieldConfig.overrides, isStandardFieldProp),
|
||||
};
|
||||
|
||||
this._cachedPluginOptions[prevPluginId] = { options: prevOptions, fieldConfig: prevFieldConfig };
|
||||
|
||||
const cachedOptions = this._cachedPluginOptions[pluginId]?.options;
|
||||
const cachedFieldConfig = this._cachedPluginOptions[pluginId]?.fieldConfig;
|
||||
|
||||
if (cachedFieldConfig) {
|
||||
newFieldConfig = restoreCustomOverrideRules(newFieldConfig, cachedFieldConfig);
|
||||
}
|
||||
|
||||
panel.changePluginType(pluginId, cachedOptions, newFieldConfig);
|
||||
this.onToggleVizPicker();
|
||||
};
|
||||
|
||||
onSetSearchQuery = (searchQuery: string) => {
|
||||
this.setState({ searchQuery });
|
||||
};
|
||||
@ -41,10 +83,10 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
||||
};
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<PanelOptionsPane>) => {
|
||||
const { isVizPickerOpen, searchQuery, listMode } = model.useState();
|
||||
const vizManager = sceneGraph.getAncestor(model, PanelEditor).state.vizManager;
|
||||
const { pluginId } = vizManager.useState();
|
||||
const { data } = sceneGraph.getData(vizManager.state.panel).useState();
|
||||
const { isVizPickerOpen, searchQuery, listMode, panelRef } = model.useState();
|
||||
const panel = panelRef.resolve();
|
||||
const { pluginId } = panel.useState();
|
||||
const { data } = sceneGraph.getData(panel).useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
@ -61,12 +103,17 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.listOfOptions}>
|
||||
<PanelOptions vizManager={vizManager} searchQuery={searchQuery} listMode={listMode} data={data} />
|
||||
<PanelOptions panel={panel} searchQuery={searchQuery} listMode={listMode} data={data} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isVizPickerOpen && (
|
||||
<PanelVizTypePicker vizManager={vizManager} onChange={model.onToggleVizPicker} data={data} />
|
||||
<PanelVizTypePicker
|
||||
panel={panel}
|
||||
onChange={model.onChangePanelPlugin}
|
||||
onClose={model.onToggleVizPicker}
|
||||
data={data}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -4,6 +4,7 @@ import { useLocalStorage } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2, PanelData, SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
import { Button, CustomScrollbar, Field, FilterInput, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
import { LS_VISUALIZATION_SELECT_TAB_KEY, LS_WIDGET_SELECT_TAB_KEY } from 'app/core/constants';
|
||||
import { VisualizationSelectPaneTab } from 'app/features/dashboard/components/PanelEditor/types';
|
||||
@ -13,16 +14,14 @@ import { VizTypeChangeDetails } from 'app/features/panel/components/VizTypePicke
|
||||
|
||||
import { PanelModelCompatibilityWrapper } from '../utils/PanelModelCompatibilityWrapper';
|
||||
|
||||
import { VizPanelManager } from './VizPanelManager';
|
||||
|
||||
export interface Props {
|
||||
data?: PanelData;
|
||||
vizManager: VizPanelManager;
|
||||
onChange: () => void;
|
||||
panel: VizPanel;
|
||||
onChange: (options: VizTypeChangeDetails) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function PanelVizTypePicker({ vizManager, data, onChange }: Props) {
|
||||
const { panel } = vizManager.useState();
|
||||
export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
@ -50,22 +49,8 @@ export function PanelVizTypePicker({ vizManager, data, onChange }: Props) {
|
||||
const radioOptions: Array<SelectableValue<VisualizationSelectPaneTab>> = [
|
||||
{ label: 'Visualizations', value: VisualizationSelectPaneTab.Visualizations },
|
||||
{ label: 'Suggestions', value: VisualizationSelectPaneTab.Suggestions },
|
||||
// {
|
||||
// label: 'Library panels',
|
||||
// value: VisualizationSelectPaneTab.LibraryPanels,
|
||||
// description: 'Reusable panels you can share between multiple dashboards.',
|
||||
// },
|
||||
];
|
||||
|
||||
const onVizTypeChange = (options: VizTypeChangeDetails) => {
|
||||
vizManager.changePluginType(options.pluginId);
|
||||
onChange();
|
||||
};
|
||||
|
||||
const onCloseVizPicker = () => {
|
||||
onChange();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.searchRow}>
|
||||
@ -82,7 +67,7 @@ export function PanelVizTypePicker({ vizManager, data, onChange }: Props) {
|
||||
icon="angle-up"
|
||||
className={styles.closeButton}
|
||||
data-testid={selectors.components.PanelEditor.toggleVizPicker}
|
||||
onClick={onCloseVizPicker}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
<Field className={styles.customFieldMargin}>
|
||||
@ -90,18 +75,10 @@ export function PanelVizTypePicker({ vizManager, data, onChange }: Props) {
|
||||
</Field>
|
||||
<CustomScrollbar>
|
||||
{listMode === VisualizationSelectPaneTab.Visualizations && (
|
||||
<VizTypePicker pluginId={panel.state.pluginId} searchQuery={searchQuery} onChange={onVizTypeChange} />
|
||||
<VizTypePicker pluginId={panel.state.pluginId} searchQuery={searchQuery} onChange={onChange} />
|
||||
)}
|
||||
{/* {listMode === VisualizationSelectPaneTab.Widgets && (
|
||||
<VizTypePicker pluginId={plugin.meta.id} onChange={onVizChange} searchQuery={searchQuery} isWidget />
|
||||
)} */}
|
||||
{listMode === VisualizationSelectPaneTab.Suggestions && (
|
||||
<VisualizationSuggestions
|
||||
onChange={onVizTypeChange}
|
||||
searchQuery={searchQuery}
|
||||
panel={panelModel}
|
||||
data={data}
|
||||
/>
|
||||
<VisualizationSuggestions onChange={onChange} searchQuery={searchQuery} panel={panelModel} data={data} />
|
||||
)}
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
|
@ -1,864 +0,0 @@
|
||||
import { map, of } from 'rxjs';
|
||||
|
||||
import { DataQueryRequest, DataSourceApi, DataSourceInstanceSettings, LoadingState, PanelData } from '@grafana/data';
|
||||
import { calculateFieldTransformer } from '@grafana/data/src/transformations/transformers/calculateField';
|
||||
import { mockTransformationsRegistry } from '@grafana/data/src/utils/tests/mockTransformationsRegistry';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import {
|
||||
CustomVariable,
|
||||
LocalValueVariable,
|
||||
SceneGridRow,
|
||||
SceneVariableSet,
|
||||
VizPanel,
|
||||
sceneGraph,
|
||||
} 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 * as libAPI from 'app/features/library-panels/state/api';
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
||||
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange';
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper';
|
||||
import { findVizPanelByKey } from '../utils/utils';
|
||||
|
||||
import { buildPanelEditScene } from './PanelEditor';
|
||||
import { VizPanelManager } from './VizPanelManager';
|
||||
import { panelWithQueriesOnly, panelWithTransformations, testDashboard } from './testfiles/testDashboard';
|
||||
|
||||
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 defaultDsMock: 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 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',
|
||||
},
|
||||
};
|
||||
|
||||
// Mocking the build in Grafana data source to avoid annotations data layer errors.
|
||||
const grafanaDs = {
|
||||
id: 1,
|
||||
uid: '-- Grafana --',
|
||||
name: 'grafana',
|
||||
type: 'grafana',
|
||||
meta: {
|
||||
id: 'grafana',
|
||||
},
|
||||
};
|
||||
|
||||
// Mock the store module
|
||||
jest.mock('app/core/store', () => ({
|
||||
exists: jest.fn(),
|
||||
get: jest.fn(),
|
||||
getObject: jest.fn((_a, b) => b),
|
||||
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) => {
|
||||
// Mocking the build in Grafana data source to avoid annotations data layer errors.
|
||||
|
||||
if (ref.uid === '-- Grafana --') {
|
||||
return grafanaDs;
|
||||
}
|
||||
|
||||
if (ref.uid === 'gdev-testdata') {
|
||||
return ds1Mock;
|
||||
}
|
||||
|
||||
if (ref.uid === 'gdev-prometheus') {
|
||||
return ds2Mock;
|
||||
}
|
||||
|
||||
if (ref.uid === SHARED_DASHBOARD_QUERY) {
|
||||
return ds3Mock;
|
||||
}
|
||||
|
||||
// if datasource is not found, return default datasource
|
||||
return defaultDsMock;
|
||||
},
|
||||
getInstanceSettings: (ref: DataSourceRef) => {
|
||||
if (ref.uid === 'gdev-testdata') {
|
||||
return instance1SettingsMock;
|
||||
}
|
||||
|
||||
if (ref.uid === 'gdev-prometheus') {
|
||||
return instance2SettingsMock;
|
||||
}
|
||||
|
||||
// if datasource is not found, return default instance settings
|
||||
return instance1SettingsMock;
|
||||
},
|
||||
}),
|
||||
locationService: {
|
||||
partial: jest.fn(),
|
||||
},
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
defaultDatasource: 'gdev-testdata',
|
||||
},
|
||||
}));
|
||||
|
||||
mockTransformationsRegistry([calculateFieldTransformer]);
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('VizPanelManager', () => {
|
||||
describe('When changing plugin', () => {
|
||||
it('Should set the cache', () => {
|
||||
const { vizPanelManager } = setupTest('panel-1');
|
||||
vizPanelManager.state.panel.changePluginType = jest.fn();
|
||||
|
||||
expect(vizPanelManager.state.panel.state.pluginId).toBe('timeseries');
|
||||
|
||||
vizPanelManager.changePluginType('table');
|
||||
|
||||
expect(vizPanelManager['_cachedPluginOptions']['timeseries']?.options).toBe(
|
||||
vizPanelManager.state.panel.state.options
|
||||
);
|
||||
expect(vizPanelManager['_cachedPluginOptions']['timeseries']?.fieldConfig).toBe(
|
||||
vizPanelManager.state.panel.state.fieldConfig
|
||||
);
|
||||
});
|
||||
|
||||
it('Should preserve correct field config', () => {
|
||||
const { vizPanelManager } = setupTest('panel-1');
|
||||
const mockFn = jest.fn();
|
||||
vizPanelManager.state.panel.changePluginType = mockFn;
|
||||
const fieldConfig = vizPanelManager.state.panel.state.fieldConfig;
|
||||
fieldConfig.defaults = {
|
||||
...fieldConfig.defaults,
|
||||
unit: 'flop',
|
||||
decimals: 2,
|
||||
};
|
||||
fieldConfig.overrides = [
|
||||
{
|
||||
matcher: {
|
||||
id: 'byName',
|
||||
options: 'A-series',
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
id: 'displayName',
|
||||
value: 'test',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
matcher: { id: 'byName', options: 'D-series' },
|
||||
//should be removed because it's custom
|
||||
properties: [
|
||||
{
|
||||
id: 'custom.customPropNoExist',
|
||||
value: 'google',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
vizPanelManager.state.panel.setState({
|
||||
fieldConfig: fieldConfig,
|
||||
});
|
||||
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.color?.mode).toBe('palette-classic');
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.thresholds?.mode).toBe('absolute');
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.unit).toBe('flop');
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.decimals).toBe(2);
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.overrides).toHaveLength(2);
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.overrides[1].properties).toHaveLength(1);
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.custom).toHaveProperty('axisBorderShow');
|
||||
|
||||
vizPanelManager.changePluginType('table');
|
||||
|
||||
expect(mockFn).toHaveBeenCalled();
|
||||
expect(mockFn.mock.calls[0][2].defaults.color?.mode).toBe('palette-classic');
|
||||
expect(mockFn.mock.calls[0][2].defaults.thresholds?.mode).toBe('absolute');
|
||||
expect(mockFn.mock.calls[0][2].defaults.unit).toBe('flop');
|
||||
expect(mockFn.mock.calls[0][2].defaults.decimals).toBe(2);
|
||||
expect(mockFn.mock.calls[0][2].overrides).toHaveLength(2);
|
||||
//removed custom property
|
||||
expect(mockFn.mock.calls[0][2].overrides[1].properties).toHaveLength(0);
|
||||
//removed fieldConfig custom values as well
|
||||
expect(mockFn.mock.calls[0][2].defaults.custom).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('library panels', () => {
|
||||
it('saves library panels on commit', () => {
|
||||
const panel = new VizPanel({
|
||||
key: 'panel-1',
|
||||
pluginId: 'text',
|
||||
});
|
||||
|
||||
const libraryPanelModel = {
|
||||
title: 'title',
|
||||
uid: 'uid',
|
||||
name: 'libraryPanelName',
|
||||
model: vizPanelToPanel(panel),
|
||||
type: 'panel',
|
||||
version: 1,
|
||||
};
|
||||
|
||||
const libPanelBehavior = new LibraryPanelBehavior({
|
||||
isLoaded: true,
|
||||
title: libraryPanelModel.title,
|
||||
uid: libraryPanelModel.uid,
|
||||
name: libraryPanelModel.name,
|
||||
_loadedPanel: libraryPanelModel,
|
||||
});
|
||||
|
||||
panel.setState({
|
||||
$behaviors: [libPanelBehavior],
|
||||
});
|
||||
|
||||
new DashboardGridItem({ body: panel });
|
||||
|
||||
const panelManager = VizPanelManager.createFor(panel);
|
||||
|
||||
const apiCall = jest.spyOn(libAPI, 'saveLibPanel');
|
||||
|
||||
panelManager.state.panel.setState({ title: 'new title' });
|
||||
panelManager.commitChanges();
|
||||
|
||||
expect(apiCall.mock.calls[0][0].state.title).toBe('new title');
|
||||
});
|
||||
|
||||
it('unlinks library panel', () => {
|
||||
const panel = new VizPanel({
|
||||
key: 'panel-1',
|
||||
pluginId: 'text',
|
||||
});
|
||||
|
||||
const libraryPanelModel = {
|
||||
title: 'title',
|
||||
uid: 'uid',
|
||||
name: 'libraryPanelName',
|
||||
model: vizPanelToPanel(panel),
|
||||
type: 'panel',
|
||||
version: 1,
|
||||
};
|
||||
|
||||
const libPanelBehavior = new LibraryPanelBehavior({
|
||||
isLoaded: true,
|
||||
title: libraryPanelModel.title,
|
||||
uid: libraryPanelModel.uid,
|
||||
name: libraryPanelModel.name,
|
||||
_loadedPanel: libraryPanelModel,
|
||||
});
|
||||
|
||||
panel.setState({
|
||||
$behaviors: [libPanelBehavior],
|
||||
});
|
||||
|
||||
new DashboardGridItem({ body: panel });
|
||||
|
||||
const panelManager = VizPanelManager.createFor(panel);
|
||||
panelManager.unlinkLibraryPanel();
|
||||
|
||||
const sourcePanel = panelManager.state.sourcePanel.resolve();
|
||||
expect(sourcePanel.state.$behaviors).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
});
|
||||
});
|
||||
|
||||
it('should load default datasource if the datasource passed is not found', async () => {
|
||||
const { vizPanelManager } = setupTest('panel-6');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(vizPanelManager.queryRunner.state.datasource).toEqual({
|
||||
uid: 'abc',
|
||||
type: 'datasource',
|
||||
});
|
||||
|
||||
expect(config.defaultDatasource).toBe('gdev-testdata');
|
||||
expect(vizPanelManager.state.datasource).toEqual(defaultDsMock);
|
||||
expect(vizPanelManager.state.dsSettings).toEqual(instance1SettingsMock);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data source change', () => {
|
||||
it('should load new data source', async () => {
|
||||
const { vizPanelManager } = setupTest('panel-1');
|
||||
vizPanelManager.activate();
|
||||
vizPanelManager.state.panel.state.$data?.activate();
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
await vizPanelManager.changePanelDataSource(
|
||||
{ type: 'grafana-prometheus-datasource', uid: 'gdev-prometheus' } as DataSourceInstanceSettings,
|
||||
[]
|
||||
);
|
||||
|
||||
expect(store.setObject).toHaveBeenCalledTimes(2);
|
||||
expect(store.setObject).toHaveBeenLastCalledWith('grafana.dashboards.panelEdit.lastUsedDatasource', {
|
||||
dashboardUid: 'ffbe00e2-803c-4d49-adb7-41aad336234f',
|
||||
datasourceUid: 'gdev-prometheus',
|
||||
});
|
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true);
|
||||
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();
|
||||
vizPanelManager.state.panel.state.$data?.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',
|
||||
},
|
||||
});
|
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true);
|
||||
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('should update 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,
|
||||
});
|
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true);
|
||||
expect(dataObj.state.maxDataPoints).toBe(100);
|
||||
});
|
||||
|
||||
it('should update min interval', 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',
|
||||
});
|
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true);
|
||||
expect(dataObj.state.minInterval).toBe('1s');
|
||||
});
|
||||
});
|
||||
|
||||
describe('query caching', () => {
|
||||
it('updates cacheTimeout and queryCachingTTL', async () => {
|
||||
const { vizPanelManager } = setupTest('panel-1');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
const dataObj = vizPanelManager.queryRunner;
|
||||
|
||||
vizPanelManager.changeQueryOptions({
|
||||
cacheTimeout: '60',
|
||||
queryCachingTTL: 200000,
|
||||
dataSource: {
|
||||
name: 'grafana-testdata',
|
||||
type: 'grafana-testdata-datasource',
|
||||
default: true,
|
||||
},
|
||||
queries: [],
|
||||
});
|
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true);
|
||||
expect(dataObj.state.cacheTimeout).toBe('60');
|
||||
expect(dataObj.state.queryCachingTTL).toBe(200000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
expect(vizPanelManager.queryRunner.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 DataSourceInstanceSettings);
|
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true);
|
||||
expect(vizPanelManager.queryRunner.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();
|
||||
|
||||
expect(vizPanelManager.queryRunner.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 DataSourceInstanceSettings);
|
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true);
|
||||
expect(vizPanelManager.queryRunner.state.datasource).toEqual({
|
||||
uid: SHARED_DASHBOARD_QUERY,
|
||||
type: 'datasource',
|
||||
});
|
||||
});
|
||||
|
||||
it('changing from dashboard data source to a plugin', async () => {
|
||||
const { vizPanelManager } = setupTest('panel-3');
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(vizPanelManager.queryRunner.state.datasource).toEqual({
|
||||
uid: SHARED_DASHBOARD_QUERY,
|
||||
type: 'datasource',
|
||||
});
|
||||
|
||||
await vizPanelManager.changePanelDataSource({
|
||||
name: 'grafana-prometheus',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
uid: 'gdev-prometheus',
|
||||
meta: {
|
||||
name: 'Prometheus',
|
||||
module: 'prometheus',
|
||||
id: 'grafana-prometheus-datasource',
|
||||
},
|
||||
} as DataSourceInstanceSettings);
|
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true);
|
||||
expect(vizPanelManager.queryRunner.state.datasource).toEqual({
|
||||
uid: 'gdev-prometheus',
|
||||
type: 'grafana-prometheus-datasource',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('change transformations', () => {
|
||||
it('should update and reprocess transformations', () => {
|
||||
const { scene, panel } = setupTest('panel-3');
|
||||
scene.setState({ editPanel: buildPanelEditScene(panel) });
|
||||
|
||||
const vizPanelManager = scene.state.editPanel!.state.vizManager;
|
||||
vizPanelManager.activate();
|
||||
vizPanelManager.state.panel.state.$data?.activate();
|
||||
|
||||
const reprocessMock = jest.fn();
|
||||
vizPanelManager.dataTransformer.reprocessTransformations = reprocessMock;
|
||||
vizPanelManager.changeTransformations([{ id: 'calculateField', options: {} }]);
|
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true);
|
||||
expect(reprocessMock).toHaveBeenCalledTimes(1);
|
||||
expect(vizPanelManager.dataTransformer.state.transformations).toEqual([{ id: 'calculateField', options: {} }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change queries', () => {
|
||||
describe('plugin queries', () => {
|
||||
it('should update queries', () => {
|
||||
const { vizPanelManager } = setupTest('panel-1');
|
||||
|
||||
vizPanelManager.activate();
|
||||
vizPanelManager.state.panel.state.$data?.activate();
|
||||
|
||||
vizPanelManager.changeQueries([
|
||||
{
|
||||
datasource: {
|
||||
type: 'grafana-testdata-datasource',
|
||||
uid: 'gdev-testdata',
|
||||
},
|
||||
refId: 'A',
|
||||
scenarioId: 'random_walk',
|
||||
seriesCount: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true);
|
||||
expect(vizPanelManager.queryRunner.state.queries).toEqual([
|
||||
{
|
||||
datasource: {
|
||||
type: 'grafana-testdata-datasource',
|
||||
uid: 'gdev-testdata',
|
||||
},
|
||||
refId: 'A',
|
||||
scenarioId: 'random_walk',
|
||||
seriesCount: 5,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dashboard queries', () => {
|
||||
it('should update queries', () => {
|
||||
const { scene, panel } = setupTest('panel-3');
|
||||
scene.setState({ editPanel: buildPanelEditScene(panel) });
|
||||
|
||||
const vizPanelManager = scene.state.editPanel!.state.vizManager;
|
||||
vizPanelManager.activate();
|
||||
vizPanelManager.state.panel.state.$data?.activate();
|
||||
|
||||
// Changing dashboard query to a panel with transformations
|
||||
vizPanelManager.changeQueries([
|
||||
{
|
||||
refId: 'A',
|
||||
datasource: {
|
||||
type: DASHBOARD_DATASOURCE_PLUGIN_ID,
|
||||
},
|
||||
panelId: panelWithTransformations.id,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(vizPanelManager.queryRunner.state.queries[0].panelId).toEqual(panelWithTransformations.id);
|
||||
|
||||
// Changing dashboard query to a panel with queries only
|
||||
vizPanelManager.changeQueries([
|
||||
{
|
||||
refId: 'A',
|
||||
datasource: {
|
||||
type: DASHBOARD_DATASOURCE_PLUGIN_ID,
|
||||
},
|
||||
panelId: panelWithQueriesOnly.id,
|
||||
},
|
||||
]);
|
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true);
|
||||
expect(vizPanelManager.queryRunner.state.queries[0].panelId).toBe(panelWithQueriesOnly.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should load last used data source if no data source specified for a panel', async () => {
|
||||
store.exists.mockReturnValue(true);
|
||||
store.getObject.mockReturnValue({
|
||||
dashboardUid: 'ffbe00e2-803c-4d49-adb7-41aad336234f',
|
||||
datasourceUid: 'gdev-testdata',
|
||||
});
|
||||
const { scene, panel } = setupTest('panel-5');
|
||||
scene.setState({ editPanel: buildPanelEditScene(panel) });
|
||||
|
||||
const vizPanelManager = scene.state.editPanel!.state.vizManager;
|
||||
vizPanelManager.activate();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(vizPanelManager.state.datasource).toEqual(ds1Mock);
|
||||
expect(vizPanelManager.state.dsSettings).toEqual(instance1SettingsMock);
|
||||
});
|
||||
|
||||
it('Should default to the first variable value if panel is repeated', async () => {
|
||||
const { scene, panel } = setupTest('panel-10');
|
||||
|
||||
scene.setState({
|
||||
$variables: new SceneVariableSet({
|
||||
variables: [
|
||||
new CustomVariable({ name: 'custom', query: 'A,B,C', value: ['A', 'B', 'C'], text: ['A', 'B', 'C'] }),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
scene.setState({ editPanel: buildPanelEditScene(panel) });
|
||||
|
||||
const vizPanelManager = scene.state.editPanel!.state.vizManager;
|
||||
vizPanelManager.activate();
|
||||
|
||||
const variable = sceneGraph.lookupVariable('custom', vizPanelManager);
|
||||
expect(variable?.getValue()).toBe('A');
|
||||
});
|
||||
|
||||
describe('Given a panel inside repeated row', () => {
|
||||
it('Should include row variable scope', () => {
|
||||
const { panel } = setupTest('panel-9');
|
||||
|
||||
const row = panel.parent?.parent;
|
||||
if (!(row instanceof SceneGridRow)) {
|
||||
throw new Error('Did not find parent row');
|
||||
}
|
||||
|
||||
row.setState({
|
||||
$variables: new SceneVariableSet({ variables: [new LocalValueVariable({ name: 'hello', value: 'A' })] }),
|
||||
});
|
||||
|
||||
const editor = buildPanelEditScene(panel);
|
||||
const variable = sceneGraph.lookupVariable('hello', editor.state.vizManager);
|
||||
expect(variable?.getValue()).toBe('A');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const setupTest = (panelId: string) => {
|
||||
const scene = transformSaveModelToScene({ dashboard: testDashboard, meta: {} });
|
||||
|
||||
const panel = findVizPanelByKey(scene, panelId)!;
|
||||
|
||||
const vizPanelManager = VizPanelManager.createFor(panel);
|
||||
// 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));
|
||||
|
||||
return { vizPanelManager, scene, panel };
|
||||
};
|
@ -1,504 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { debounce } from 'lodash';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
FieldConfigSource,
|
||||
GrafanaTheme2,
|
||||
filterFieldConfigOverrides,
|
||||
getDataSourceRef,
|
||||
isStandardFieldProp,
|
||||
restoreCustomOverrideRules,
|
||||
} from '@grafana/data';
|
||||
import { config, getDataSourceSrv, locationService } from '@grafana/runtime';
|
||||
import {
|
||||
DeepPartial,
|
||||
LocalValueVariable,
|
||||
MultiValueVariable,
|
||||
PanelBuilders,
|
||||
SceneComponentProps,
|
||||
SceneDataTransformer,
|
||||
SceneObjectBase,
|
||||
SceneObjectRef,
|
||||
SceneObjectState,
|
||||
SceneObjectStateChangedEvent,
|
||||
SceneQueryRunner,
|
||||
SceneVariableSet,
|
||||
SceneVariables,
|
||||
VizPanel,
|
||||
sceneGraph,
|
||||
} from '@grafana/scenes';
|
||||
import { DataQuery, DataTransformerConfig, Panel } from '@grafana/schema';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard';
|
||||
import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils';
|
||||
import { saveLibPanel } from 'app/features/library-panels/state/api';
|
||||
import { updateQueries } from 'app/features/query/state/updateQueries';
|
||||
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
|
||||
import { QueryGroupOptions } from 'app/types';
|
||||
|
||||
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
|
||||
import { getPanelChanges } from '../saving/getDashboardChanges';
|
||||
import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem';
|
||||
import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange';
|
||||
import { gridItemToPanel, vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import {
|
||||
getDashboardSceneFor,
|
||||
getMultiVariableValues,
|
||||
getPanelIdForVizPanel,
|
||||
getQueryRunnerFor,
|
||||
isLibraryPanel,
|
||||
} from '../utils/utils';
|
||||
|
||||
export interface VizPanelManagerState extends SceneObjectState {
|
||||
panel: VizPanel;
|
||||
sourcePanel: SceneObjectRef<VizPanel>;
|
||||
pluginId: string;
|
||||
datasource?: DataSourceApi;
|
||||
dsSettings?: DataSourceInstanceSettings;
|
||||
tableView?: VizPanel;
|
||||
repeat?: string;
|
||||
repeatDirection?: RepeatDirection;
|
||||
maxPerRow?: number;
|
||||
isDirty?: boolean;
|
||||
}
|
||||
|
||||
export enum DisplayMode {
|
||||
Fill = 0,
|
||||
Fit = 1,
|
||||
Exact = 2,
|
||||
}
|
||||
|
||||
// VizPanelManager serves as an API to manipulate VizPanel state from the outside. It allows panel type, options and data manipulation.
|
||||
export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
private _cachedPluginOptions: Record<
|
||||
string,
|
||||
{ options: DeepPartial<{}>; fieldConfig: FieldConfigSource<DeepPartial<{}>> } | undefined
|
||||
> = {};
|
||||
|
||||
public constructor(state: VizPanelManagerState) {
|
||||
super(state);
|
||||
this.addActivationHandler(() => this._onActivate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Will clone the source panel and move the data provider to
|
||||
* live on the VizPanelManager level instead of the VizPanel level
|
||||
*/
|
||||
public static createFor(sourcePanel: VizPanel) {
|
||||
let repeatOptions: Pick<VizPanelManagerState, 'repeat' | 'repeatDirection' | 'maxPerRow'> = {};
|
||||
|
||||
const gridItem = sourcePanel.parent;
|
||||
|
||||
if (!(gridItem instanceof DashboardGridItem)) {
|
||||
console.error('VizPanel is not a child of a dashboard grid item');
|
||||
throw new Error('VizPanel is not a child of a dashboard grid item');
|
||||
}
|
||||
|
||||
const { variableName: repeat, repeatDirection, maxPerRow } = gridItem.state;
|
||||
repeatOptions = { repeat, repeatDirection, maxPerRow };
|
||||
|
||||
let variables: SceneVariables | undefined;
|
||||
|
||||
if (gridItem.parent?.state.$variables) {
|
||||
variables = gridItem.parent.state.$variables.clone();
|
||||
}
|
||||
|
||||
if (repeatOptions.repeat) {
|
||||
const variable = sceneGraph.lookupVariable(repeatOptions.repeat, gridItem);
|
||||
|
||||
if (variable instanceof MultiValueVariable && variable.state.value.length) {
|
||||
const { values, texts } = getMultiVariableValues(variable);
|
||||
|
||||
const varWithDefaultValue = new LocalValueVariable({
|
||||
name: variable.state.name,
|
||||
value: values[0],
|
||||
text: String(texts[0]),
|
||||
});
|
||||
|
||||
if (!variables) {
|
||||
variables = new SceneVariableSet({
|
||||
variables: [varWithDefaultValue],
|
||||
});
|
||||
} else {
|
||||
variables.setState({ variables: [varWithDefaultValue] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new VizPanelManager({
|
||||
$variables: variables,
|
||||
panel: sourcePanel.clone(),
|
||||
sourcePanel: sourcePanel.getRef(),
|
||||
pluginId: sourcePanel.state.pluginId,
|
||||
...repeatOptions,
|
||||
});
|
||||
}
|
||||
|
||||
private _onActivate() {
|
||||
this.loadDataSource();
|
||||
const changesSub = this.subscribeToEvent(SceneObjectStateChangedEvent, this._handleStateChange);
|
||||
|
||||
return () => {
|
||||
changesSub.unsubscribe();
|
||||
};
|
||||
}
|
||||
|
||||
private _detectPanelModelChanges = debounce(() => {
|
||||
const { hasChanges } = getPanelChanges(
|
||||
vizPanelToPanel(this.state.sourcePanel.resolve().clone({ $behaviors: undefined })),
|
||||
vizPanelToPanel(this.state.panel.clone({ $behaviors: undefined }))
|
||||
);
|
||||
this.setState({ isDirty: hasChanges });
|
||||
}, 250);
|
||||
|
||||
private _handleStateChange = (event: SceneObjectStateChangedEvent) => {
|
||||
if (DashboardSceneChangeTracker.isUpdatingPersistedState(event)) {
|
||||
this._detectPanelModelChanges();
|
||||
}
|
||||
};
|
||||
|
||||
private async loadDataSource() {
|
||||
const dataObj = this.state.panel.state.$data;
|
||||
|
||||
if (!dataObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
let datasourceToLoad = this.queryRunner.state.datasource;
|
||||
|
||||
try {
|
||||
let datasource: DataSourceApi | undefined;
|
||||
let dsSettings: DataSourceInstanceSettings | undefined;
|
||||
|
||||
if (!datasourceToLoad) {
|
||||
const dashboardScene = getDashboardSceneFor(this);
|
||||
const dashboardUid = dashboardScene.state.uid ?? '';
|
||||
const lastUsedDatasource = getLastUsedDatasourceFromStorage(dashboardUid!);
|
||||
|
||||
// do we have a last used datasource for this dashboard
|
||||
if (lastUsedDatasource?.datasourceUid !== null) {
|
||||
// get datasource from dashbopard uid
|
||||
dsSettings = getDataSourceSrv().getInstanceSettings({ uid: lastUsedDatasource?.datasourceUid });
|
||||
if (dsSettings) {
|
||||
datasource = await getDataSourceSrv().get({
|
||||
uid: lastUsedDatasource?.datasourceUid,
|
||||
type: dsSettings.type,
|
||||
});
|
||||
|
||||
this.queryRunner.setState({
|
||||
datasource: {
|
||||
...getDataSourceRef(dsSettings),
|
||||
uid: lastUsedDatasource?.datasourceUid,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
datasource = await getDataSourceSrv().get(datasourceToLoad);
|
||||
dsSettings = getDataSourceSrv().getInstanceSettings(datasourceToLoad);
|
||||
}
|
||||
|
||||
if (datasource && dsSettings) {
|
||||
this.setState({ datasource, dsSettings });
|
||||
|
||||
storeLastUsedDataSourceInLocalStorage(getDataSourceRef(dsSettings) || { default: true });
|
||||
}
|
||||
} catch (err) {
|
||||
//set default datasource if we fail to load the datasource
|
||||
const datasource = await getDataSourceSrv().get(config.defaultDatasource);
|
||||
const dsSettings = getDataSourceSrv().getInstanceSettings(config.defaultDatasource);
|
||||
|
||||
if (datasource && dsSettings) {
|
||||
this.setState({
|
||||
datasource,
|
||||
dsSettings,
|
||||
});
|
||||
|
||||
this.queryRunner.setState({
|
||||
datasource: getDataSourceRef(dsSettings),
|
||||
});
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
public changePluginType(pluginId: string) {
|
||||
const { options: prevOptions, fieldConfig: prevFieldConfig, pluginId: prevPluginId } = this.state.panel.state;
|
||||
|
||||
// clear custom options
|
||||
let newFieldConfig: FieldConfigSource = {
|
||||
defaults: {
|
||||
...prevFieldConfig.defaults,
|
||||
custom: {},
|
||||
},
|
||||
overrides: filterFieldConfigOverrides(prevFieldConfig.overrides, isStandardFieldProp),
|
||||
};
|
||||
|
||||
this._cachedPluginOptions[prevPluginId] = { options: prevOptions, fieldConfig: prevFieldConfig };
|
||||
|
||||
const cachedOptions = this._cachedPluginOptions[pluginId]?.options;
|
||||
const cachedFieldConfig = this._cachedPluginOptions[pluginId]?.fieldConfig;
|
||||
|
||||
if (cachedFieldConfig) {
|
||||
newFieldConfig = restoreCustomOverrideRules(newFieldConfig, cachedFieldConfig);
|
||||
}
|
||||
|
||||
// When changing from non-data to data panel, we need to add a new data provider
|
||||
if (!this.state.panel.state.$data && !config.panels[pluginId].skipDataQuery) {
|
||||
let ds = getLastUsedDatasourceFromStorage(getDashboardSceneFor(this).state.uid!)?.datasourceUid;
|
||||
|
||||
if (!ds) {
|
||||
ds = config.defaultDatasource;
|
||||
}
|
||||
|
||||
this.state.panel.setState({
|
||||
$data: new SceneDataTransformer({
|
||||
$data: new SceneQueryRunner({
|
||||
datasource: {
|
||||
uid: ds,
|
||||
},
|
||||
queries: [{ refId: 'A' }],
|
||||
}),
|
||||
transformations: [],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ pluginId });
|
||||
this.state.panel.changePluginType(pluginId, cachedOptions, newFieldConfig);
|
||||
|
||||
this.loadDataSource();
|
||||
}
|
||||
|
||||
public async changePanelDataSource(
|
||||
newSettings: DataSourceInstanceSettings,
|
||||
defaultQueries?: DataQuery[] | GrafanaQuery[]
|
||||
) {
|
||||
const { dsSettings } = this.state;
|
||||
const queryRunner = this.queryRunner;
|
||||
|
||||
const currentDS = dsSettings ? await getDataSourceSrv().get({ uid: dsSettings.uid }) : undefined;
|
||||
const nextDS = await getDataSourceSrv().get({ uid: newSettings.uid });
|
||||
|
||||
const currentQueries = queryRunner.state.queries;
|
||||
|
||||
// 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));
|
||||
|
||||
queryRunner.setState({
|
||||
datasource: getDataSourceRef(newSettings),
|
||||
queries,
|
||||
});
|
||||
if (defaultQueries) {
|
||||
queryRunner.runQueries();
|
||||
}
|
||||
|
||||
this.loadDataSource();
|
||||
}
|
||||
|
||||
public changeQueryOptions(options: QueryGroupOptions) {
|
||||
const panelObj = this.state.panel;
|
||||
const dataObj = this.queryRunner;
|
||||
const timeRangeObj = panelObj.state.$timeRange;
|
||||
|
||||
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) });
|
||||
}
|
||||
|
||||
if (options.cacheTimeout !== dataObj?.state.cacheTimeout) {
|
||||
dataObjStateUpdate.cacheTimeout = options.cacheTimeout;
|
||||
}
|
||||
|
||||
if (options.queryCachingTTL !== dataObj?.state.queryCachingTTL) {
|
||||
dataObjStateUpdate.queryCachingTTL = options.queryCachingTTL;
|
||||
}
|
||||
|
||||
dataObj.setState(dataObjStateUpdate);
|
||||
dataObj.runQueries();
|
||||
}
|
||||
|
||||
public changeQueries<T extends DataQuery>(queries: T[]) {
|
||||
const runner = this.queryRunner;
|
||||
runner.setState({ queries });
|
||||
}
|
||||
|
||||
public changeTransformations(transformations: DataTransformerConfig[]) {
|
||||
const dataprovider = this.dataTransformer;
|
||||
dataprovider.setState({ transformations });
|
||||
dataprovider.reprocessTransformations();
|
||||
}
|
||||
|
||||
public inspectPanel() {
|
||||
const panel = this.state.panel;
|
||||
const panelId = getPanelIdForVizPanel(panel);
|
||||
|
||||
locationService.partial({
|
||||
inspect: panelId,
|
||||
inspectTab: 'query',
|
||||
});
|
||||
}
|
||||
|
||||
get queryRunner(): SceneQueryRunner {
|
||||
// Panel data object is always SceneQueryRunner wrapped in a SceneDataTransformer
|
||||
const runner = getQueryRunnerFor(this.state.panel);
|
||||
|
||||
if (!runner) {
|
||||
throw new Error('Query runner not found');
|
||||
}
|
||||
|
||||
return runner;
|
||||
}
|
||||
|
||||
get dataTransformer(): SceneDataTransformer {
|
||||
const provider = this.state.panel.state.$data;
|
||||
if (!provider || !(provider instanceof SceneDataTransformer)) {
|
||||
throw new Error('Could not find SceneDataTransformer for panel');
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
public toggleTableView() {
|
||||
if (this.state.tableView) {
|
||||
this.setState({ tableView: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
tableView: PanelBuilders.table()
|
||||
.setTitle('')
|
||||
.setOption('showTypeIcons', true)
|
||||
.setOption('showHeader', true)
|
||||
// Here we are breaking a scene rule and changing the parent of the main panel data provider
|
||||
// But we need to share this same instance as the queries tab is subscribing to it
|
||||
.setData(this.dataTransformer)
|
||||
.build(),
|
||||
});
|
||||
}
|
||||
|
||||
public unlinkLibraryPanel() {
|
||||
const sourcePanel = this.state.sourcePanel.resolve();
|
||||
if (!isLibraryPanel(sourcePanel)) {
|
||||
throw new Error('VizPanel is not a library panel');
|
||||
}
|
||||
|
||||
const gridItem = sourcePanel.parent;
|
||||
|
||||
if (!(gridItem instanceof DashboardGridItem)) {
|
||||
throw new Error('Library panel not a child of a grid item');
|
||||
}
|
||||
|
||||
const newSourcePanel = this.state.panel.clone({ $data: sourcePanel.state.$data?.clone(), $behaviors: undefined });
|
||||
gridItem.setState({
|
||||
body: newSourcePanel,
|
||||
});
|
||||
|
||||
this.state.panel.setState({ $behaviors: undefined });
|
||||
this.setState({ sourcePanel: newSourcePanel.getRef() });
|
||||
}
|
||||
|
||||
public commitChanges() {
|
||||
const sourcePanel = this.state.sourcePanel.resolve();
|
||||
this.commitChangesTo(sourcePanel);
|
||||
}
|
||||
|
||||
public commitChangesTo(sourcePanel: VizPanel) {
|
||||
const repeatUpdate = {
|
||||
variableName: this.state.repeat,
|
||||
repeatDirection: this.state.repeatDirection,
|
||||
maxPerRow: this.state.maxPerRow,
|
||||
};
|
||||
|
||||
const vizPanel = this.state.panel.clone();
|
||||
|
||||
if (sourcePanel.parent instanceof DashboardGridItem) {
|
||||
sourcePanel.parent.setState({
|
||||
...repeatUpdate,
|
||||
body: vizPanel,
|
||||
});
|
||||
}
|
||||
|
||||
if (isLibraryPanel(vizPanel)) {
|
||||
saveLibPanel(vizPanel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used from inspect json tab to view the current persisted model
|
||||
*/
|
||||
public getPanelSaveModel(): Panel | object {
|
||||
const sourcePanel = this.state.sourcePanel.resolve();
|
||||
const gridItem = sourcePanel.parent;
|
||||
|
||||
if (!(gridItem instanceof DashboardGridItem)) {
|
||||
return { error: 'Unsupported panel parent' };
|
||||
}
|
||||
|
||||
const parentClone = gridItem.clone({
|
||||
body: this.state.panel.clone(),
|
||||
});
|
||||
|
||||
return gridItemToPanel(parentClone);
|
||||
}
|
||||
|
||||
public setPanelTitle(newTitle: string) {
|
||||
this.state.panel.setState({ title: newTitle, hoverHeader: newTitle === '' });
|
||||
}
|
||||
|
||||
public static Component = ({ model }: SceneComponentProps<VizPanelManager>) => {
|
||||
const { panel, tableView } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const panelToShow = tableView ?? panel;
|
||||
const dataProvider = panelToShow.state.$data;
|
||||
|
||||
// This is to preserve SceneQueryRunner stays alive when switching between visualizations and table view
|
||||
useEffect(() => {
|
||||
return dataProvider?.activate();
|
||||
}, [dataProvider]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.wrapper}>{<panelToShow.Component model={panelToShow} />}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
wrapper: css({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
paddingLeft: theme.spacing(2),
|
||||
}),
|
||||
};
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
import { RadioButtonGroup, Select, DataLinksInlineEditor, Input, TextArea, Switch } from '@grafana/ui';
|
||||
import { SceneObjectState, VizPanel } from '@grafana/scenes';
|
||||
import { DataLinksInlineEditor, Input, TextArea, Switch, RadioButtonGroup, Select } from '@grafana/ui';
|
||||
import { GenAIPanelDescriptionButton } from 'app/features/dashboard/components/GenAI/GenAIPanelDescriptionButton';
|
||||
import { GenAIPanelTitleButton } from 'app/features/dashboard/components/GenAI/GenAIPanelTitleButton';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
@ -10,17 +10,15 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan
|
||||
import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect';
|
||||
import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { VizPanelLinks } from '../scene/PanelLinks';
|
||||
import { vizPanelToPanel, transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
import { VizPanelManager, VizPanelManagerState } from './VizPanelManager';
|
||||
|
||||
export function getPanelFrameCategory2(
|
||||
vizManager: VizPanelManager,
|
||||
panel: VizPanel,
|
||||
repeat?: string
|
||||
layoutElementState: SceneObjectState
|
||||
): OptionsPaneCategoryDescriptor {
|
||||
const descriptor = new OptionsPaneCategoryDescriptor({
|
||||
title: 'Panel options',
|
||||
@ -31,19 +29,20 @@ export function getPanelFrameCategory2(
|
||||
const panelLinksObject = dashboardSceneGraph.getPanelLinks(panel);
|
||||
const links = panelLinksObject?.state.rawLinks ?? [];
|
||||
const dashboard = getDashboardSceneFor(panel);
|
||||
const layoutElement = panel.parent;
|
||||
|
||||
return descriptor
|
||||
descriptor
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Title',
|
||||
value: panel.state.title,
|
||||
popularRank: 1,
|
||||
render: function renderTitle() {
|
||||
return <PanelFrameTitle vizManager={vizManager} />;
|
||||
return <PanelFrameTitle panel={panel} />;
|
||||
},
|
||||
addon: config.featureToggles.dashgpt && (
|
||||
<GenAIPanelTitleButton
|
||||
onGenerate={(title) => vizManager.setPanelTitle(title)}
|
||||
onGenerate={(title) => setPanelTitle(panel, title)}
|
||||
panel={vizPanelToPanel(panel)}
|
||||
dashboard={transformSceneToSaveModel(dashboard)}
|
||||
/>
|
||||
@ -95,14 +94,18 @@ export function getPanelFrameCategory2(
|
||||
render: () => <ScenePanelLinksEditor panelLinks={panelLinksObject ?? undefined} />,
|
||||
})
|
||||
)
|
||||
)
|
||||
.addCategory(
|
||||
new OptionsPaneCategoryDescriptor({
|
||||
);
|
||||
|
||||
if (layoutElement instanceof DashboardGridItem) {
|
||||
const gridItem = layoutElement;
|
||||
|
||||
const category = new OptionsPaneCategoryDescriptor({
|
||||
title: 'Repeat options',
|
||||
id: 'Repeat options',
|
||||
isOpenDefault: false,
|
||||
})
|
||||
.addItem(
|
||||
});
|
||||
|
||||
category.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Repeat by variable',
|
||||
description:
|
||||
@ -111,24 +114,19 @@ export function getPanelFrameCategory2(
|
||||
return (
|
||||
<RepeatRowSelect2
|
||||
id="repeat-by-variable-select"
|
||||
parent={panel}
|
||||
repeat={repeat}
|
||||
onChange={(value?: string) => {
|
||||
const stateUpdate: Partial<VizPanelManagerState> = { repeat: value };
|
||||
if (value && !vizManager.state.repeatDirection) {
|
||||
stateUpdate.repeatDirection = 'h';
|
||||
}
|
||||
vizManager.setState(stateUpdate);
|
||||
}}
|
||||
sceneContext={panel}
|
||||
repeat={gridItem.state.variableName}
|
||||
onChange={(value?: string) => gridItem.setRepeatByVariable(value)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
);
|
||||
|
||||
category.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Repeat direction',
|
||||
showIf: () => !!vizManager.state.repeat,
|
||||
showIf: () => Boolean(gridItem.state.variableName),
|
||||
render: function renderRepeatOptions() {
|
||||
const directionOptions: Array<SelectableValue<'h' | 'v'>> = [
|
||||
{ label: 'Horizontal', value: 'h' },
|
||||
@ -138,30 +136,35 @@ export function getPanelFrameCategory2(
|
||||
return (
|
||||
<RadioButtonGroup
|
||||
options={directionOptions}
|
||||
value={vizManager.state.repeatDirection ?? 'h'}
|
||||
onChange={(value) => vizManager.setState({ repeatDirection: value })}
|
||||
value={gridItem.state.repeatDirection ?? 'h'}
|
||||
onChange={(value) => gridItem.setState({ repeatDirection: value })}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
);
|
||||
|
||||
category.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Max per row',
|
||||
showIf: () => Boolean(vizManager.state.repeat && vizManager.state.repeatDirection === 'h'),
|
||||
showIf: () => Boolean(gridItem.state.variableName && gridItem.state.repeatDirection === 'h'),
|
||||
render: function renderOption() {
|
||||
const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value }));
|
||||
return (
|
||||
<Select
|
||||
options={maxPerRowOptions}
|
||||
value={vizManager.state.maxPerRow}
|
||||
onChange={(value) => vizManager.setState({ maxPerRow: value.value })}
|
||||
value={gridItem.state.maxPerRow ?? 4}
|
||||
onChange={(value) => gridItem.setState({ maxPerRow: value.value })}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
descriptor.addCategory(category);
|
||||
}
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
interface ScenePanelLinksEditorProps {
|
||||
@ -181,14 +184,14 @@ function ScenePanelLinksEditor({ panelLinks }: ScenePanelLinksEditorProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function PanelFrameTitle({ vizManager }: { vizManager: VizPanelManager }) {
|
||||
const { title } = vizManager.state.panel.useState();
|
||||
function PanelFrameTitle({ panel }: { panel: VizPanel }) {
|
||||
const { title } = panel.useState();
|
||||
|
||||
return (
|
||||
<Input
|
||||
data-testid={selectors.components.PanelEditor.OptionsPane.fieldInput('Title')}
|
||||
value={title}
|
||||
onChange={(e) => vizManager.setPanelTitle(e.currentTarget.value)}
|
||||
onChange={(e) => setPanelTitle(panel, e.currentTarget.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -204,3 +207,7 @@ function DescriptionTextArea({ panel }: { panel: VizPanel }) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function setPanelTitle(panel: VizPanel, title: string) {
|
||||
panel.setState({ title: title, hoverHeader: title === '' });
|
||||
}
|
||||
|
@ -40,19 +40,12 @@ export const DashboardPrompt = memo(({ dashboard }: DashboardPromptProps) => {
|
||||
}, [dashboard]);
|
||||
|
||||
const onHistoryBlock = (location: H.Location) => {
|
||||
const panelInEdit = dashboard.state.editPanel;
|
||||
const vizPanelManager = panelInEdit?.state.vizManager;
|
||||
const vizPanel = vizPanelManager?.state.panel;
|
||||
const panelEditor = dashboard.state.editPanel;
|
||||
const vizPanel = panelEditor?.getPanel();
|
||||
const search = new URLSearchParams(location.search);
|
||||
|
||||
// Are we leaving panel edit & library panel?
|
||||
if (
|
||||
panelInEdit &&
|
||||
vizPanel &&
|
||||
isLibraryPanel(vizPanel) &&
|
||||
vizPanelManager.state.isDirty &&
|
||||
!search.has('editPanel')
|
||||
) {
|
||||
if (panelEditor && vizPanel && isLibraryPanel(vizPanel) && panelEditor.state.isDirty && !search.has('editPanel')) {
|
||||
const libPanelBehavior = getLibraryPanelBehavior(vizPanel);
|
||||
|
||||
showModal(SaveLibraryVizPanelModal, {
|
||||
@ -60,12 +53,12 @@ export const DashboardPrompt = memo(({ dashboard }: DashboardPromptProps) => {
|
||||
isUnsavedPrompt: true,
|
||||
libraryPanel: libPanelBehavior!,
|
||||
onConfirm: () => {
|
||||
panelInEdit.onConfirmSaveLibraryPanel();
|
||||
panelEditor.onConfirmSaveLibraryPanel();
|
||||
hideModal();
|
||||
moveToBlockedLocationAfterReactStateUpdate(location);
|
||||
},
|
||||
onDiscard: () => {
|
||||
panelInEdit.onDiscard();
|
||||
panelEditor.onDiscard();
|
||||
hideModal();
|
||||
moveToBlockedLocationAfterReactStateUpdate(location);
|
||||
},
|
||||
|
@ -14,7 +14,6 @@ import {
|
||||
} from '@grafana/scenes';
|
||||
import { createWorker } from 'app/features/dashboard-scene/saving/createDetectChangesWorker';
|
||||
|
||||
import { VizPanelManager } from '../panel-edit/VizPanelManager';
|
||||
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
||||
import { DashboardControls } from '../scene/DashboardControls';
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
@ -43,7 +42,6 @@ export class DashboardSceneChangeTracker {
|
||||
}
|
||||
|
||||
// Any change in the panel should trigger a change detection
|
||||
// The VizPanelManager includes configuration for the panel like repeat
|
||||
// The PanelTimeRange includes the overrides configuration
|
||||
if (
|
||||
payload.changedObject instanceof VizPanel ||
|
||||
@ -52,16 +50,6 @@ export class DashboardSceneChangeTracker {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// VizPanelManager includes the repeat configuration
|
||||
if (payload.changedObject instanceof VizPanelManager) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'repeat') ||
|
||||
Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'repeatDirection') ||
|
||||
Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'maxPerRow')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// SceneQueryRunner includes the DS configuration
|
||||
if (payload.changedObject instanceof SceneQueryRunner) {
|
||||
if (!Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'data')) {
|
||||
|
@ -237,8 +237,7 @@ describe('getDashboardChangesFromScene', () => {
|
||||
dashboard.onEnterEditMode();
|
||||
dashboard.setState({ editPanel: editScene });
|
||||
|
||||
editScene.state.vizManager.state.panel.setState({ title: 'changed title' });
|
||||
editScene.commitChanges();
|
||||
editScene.state.panelRef.resolve().setState({ title: 'changed title' });
|
||||
|
||||
const result = getDashboardChangesFromScene(dashboard, false, true);
|
||||
const panelSaveModel = result.changedSaveModel.panels![0];
|
||||
|
@ -11,11 +11,10 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||
import { setPluginImportUtils } from '@grafana/runtime';
|
||||
import { SceneDataTransformer, SceneGridLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes';
|
||||
import { SceneDataTransformer, SceneFlexLayout, SceneGridLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes';
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
||||
|
||||
import { VizPanelManager } from '../panel-edit/VizPanelManager';
|
||||
import { activateFullSceneTree } from '../utils/test-utils';
|
||||
|
||||
import { DashboardDatasourceBehaviour } from './DashboardDatasourceBehaviour';
|
||||
@ -275,14 +274,12 @@ describe('DashboardDatasourceBehaviour', () => {
|
||||
// spy on runQueries
|
||||
const spy = jest.spyOn(dashboardDSPanel.state.$data!.state.$data as SceneQueryRunner, 'runQueries');
|
||||
|
||||
const vizPanelManager = new VizPanelManager({
|
||||
panel: dashboardDSPanel.clone(),
|
||||
const scene = new SceneFlexLayout({
|
||||
$data: dashboardDSPanel.state.$data?.clone(),
|
||||
sourcePanel: dashboardDSPanel.getRef(),
|
||||
pluginId: dashboardDSPanel.state.pluginId,
|
||||
children: [],
|
||||
});
|
||||
|
||||
vizPanelManager.activate();
|
||||
scene.activate();
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -21,7 +21,7 @@ import {
|
||||
SceneVariable,
|
||||
SceneVariableDependencyConfigLike,
|
||||
} from '@grafana/scenes';
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||
|
||||
import { getMultiVariableValues, getQueryRunnerFor } from '../utils/utils';
|
||||
|
||||
@ -41,7 +41,8 @@ export type RepeatDirection = 'v' | 'h';
|
||||
|
||||
export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> implements SceneGridItemLike {
|
||||
private _prevRepeatValues?: VariableValueSingle[];
|
||||
private _oldBody?: VizPanel;
|
||||
private _prevPanelState: VizPanelState | undefined;
|
||||
private _prevGridItemState: DashboardGridItemState | undefined;
|
||||
|
||||
protected _variableDependency = new DashboardGridItemVariableDependencyHandler(this);
|
||||
|
||||
@ -54,11 +55,14 @@ export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> i
|
||||
private _activationHandler() {
|
||||
if (this.state.variableName) {
|
||||
this._subs.add(this.subscribeToState((newState, prevState) => this._handleGridResize(newState, prevState)));
|
||||
if (this._oldBody !== this.state.body) {
|
||||
this._prevRepeatValues = undefined;
|
||||
this.clearCachedStateIfBodyOrOptionsChanged();
|
||||
this.performRepeat();
|
||||
}
|
||||
}
|
||||
|
||||
this.performRepeat();
|
||||
private clearCachedStateIfBodyOrOptionsChanged() {
|
||||
if (this._prevGridItemState !== this.state || this._prevPanelState !== this.state.body.state) {
|
||||
this._prevRepeatValues = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,9 +120,6 @@ export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> i
|
||||
return;
|
||||
}
|
||||
|
||||
this._oldBody = this.state.body;
|
||||
this._prevRepeatValues = values;
|
||||
|
||||
const panelToRepeat = this.state.body;
|
||||
const repeatedPanels: VizPanel[] = [];
|
||||
|
||||
@ -178,10 +179,54 @@ export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> i
|
||||
}
|
||||
}
|
||||
|
||||
this._prevGridItemState = this.state;
|
||||
this._prevPanelState = this.state.body.state;
|
||||
this._prevRepeatValues = values;
|
||||
|
||||
// Used from dashboard url sync
|
||||
this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true);
|
||||
}
|
||||
|
||||
public setRepeatByVariable(variableName: string | undefined) {
|
||||
const stateUpdate: Partial<DashboardGridItemState> = { variableName };
|
||||
|
||||
if (variableName && !this.state.repeatDirection) {
|
||||
stateUpdate.repeatDirection = 'h';
|
||||
}
|
||||
|
||||
if (this.state.body.state.$variables) {
|
||||
this.state.body.setState({ $variables: undefined });
|
||||
}
|
||||
|
||||
this.setState(stateUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logic to prep panel for panel edit
|
||||
*/
|
||||
public editingStarted() {
|
||||
if (!this.state.variableName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.repeatedPanels?.length ?? 0 > 1) {
|
||||
this.state.body.setState({
|
||||
$variables: this.state.repeatedPanels![0].state.$variables?.clone(),
|
||||
$data: this.state.repeatedPanels![0].state.$data?.clone(),
|
||||
});
|
||||
this._prevPanelState = this.state.body.state;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Going back to dashboards logic
|
||||
*/
|
||||
public editingCompleted() {
|
||||
if (this.state.variableName && this.state.repeatDirection === 'h' && this.state.width !== GRID_COLUMN_COUNT) {
|
||||
this.setState({ width: GRID_COLUMN_COUNT });
|
||||
}
|
||||
}
|
||||
|
||||
public notifyRepeatedPanelsWaitingForVariables(variable: SceneVariable) {
|
||||
for (const panel of this.state.repeatedPanels ?? []) {
|
||||
const queryRunner = getQueryRunnerFor(panel);
|
||||
|
@ -18,7 +18,7 @@ import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { VariablesChanged } from 'app/features/variables/types';
|
||||
|
||||
import { PanelEditor, buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
import { createWorker } from '../saving/createDetectChangesWorker';
|
||||
import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
|
||||
@ -197,18 +197,16 @@ describe('DashboardScene', () => {
|
||||
expect(resoredLayout.state.children.map((c) => c.state.key)).toEqual(originalPanelOrder);
|
||||
});
|
||||
|
||||
it('Should exit edit mode and discard panel changes if leaving the dashboard while in panel edit', () => {
|
||||
const panel = findVizPanelByKey(scene, 'panel-1');
|
||||
it('Should exit edit mode and discard panel changes if leaving the dashboard while in panel edit', async () => {
|
||||
const panel = findVizPanelByKey(scene, 'panel-1')!;
|
||||
const editPanel = buildPanelEditScene(panel!);
|
||||
scene.setState({
|
||||
editPanel,
|
||||
});
|
||||
|
||||
expect(scene.state.editPanel!['_discardChanges']).toBe(false);
|
||||
scene.setState({ editPanel });
|
||||
|
||||
panel.setState({ title: 'new title' });
|
||||
scene.exitEditMode({ skipConfirm: true });
|
||||
|
||||
expect(scene.state.editPanel!['_discardChanges']).toBe(true);
|
||||
const discardPanel = findVizPanelByKey(scene, panel.state.key)!;
|
||||
expect(discardPanel.state.title).toBe('Panel A');
|
||||
});
|
||||
|
||||
it.each`
|
||||
@ -1023,14 +1021,14 @@ describe('DashboardScene', () => {
|
||||
panelPluginId: 'table',
|
||||
});
|
||||
});
|
||||
|
||||
test('when editing', () => {
|
||||
const panel = findVizPanelByKey(scene, 'panel-1');
|
||||
const editPanel = buildPanelEditScene(panel!);
|
||||
scene.setState({
|
||||
editPanel,
|
||||
});
|
||||
scene.setState({ editPanel });
|
||||
|
||||
const queryRunner = editPanel.getPanel().state.$data!;
|
||||
|
||||
const queryRunner = (scene.state.editPanel as PanelEditor).state.vizManager.queryRunner;
|
||||
expect(scene.enrichDataRequest(queryRunner)).toEqual({
|
||||
app: CoreApp.Dashboard,
|
||||
dashboardUID: 'dash-1',
|
||||
|
@ -252,6 +252,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
};
|
||||
|
||||
this._changeTracker.stopTrackingChanges();
|
||||
|
||||
this.setState({
|
||||
version: result.version,
|
||||
isDirty: false,
|
||||
@ -267,6 +268,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
},
|
||||
});
|
||||
|
||||
this.state.editPanel?.dashboardSaved();
|
||||
this._changeTracker.startTrackingChanges();
|
||||
}
|
||||
|
||||
@ -801,7 +803,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
let panel = getClosestVizPanel(sceneObject);
|
||||
|
||||
if (dashboard.state.isEditing && dashboard.state.editPanel) {
|
||||
panel = dashboard.state.editPanel.state.vizManager.state.panel;
|
||||
panel = dashboard.state.editPanel.state.panelRef.resolve();
|
||||
}
|
||||
|
||||
let panelId = 0;
|
||||
|
@ -2,14 +2,7 @@ import { Unsubscribable } from 'rxjs';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import {
|
||||
SceneGridLayout,
|
||||
SceneObjectBase,
|
||||
SceneObjectState,
|
||||
SceneObjectUrlSyncHandler,
|
||||
SceneObjectUrlValues,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { SceneGridLayout, SceneObjectUrlSyncHandler, SceneObjectUrlValues, VizPanel } from '@grafana/scenes';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { KioskMode } from 'app/types';
|
||||
|
||||
@ -18,7 +11,7 @@ import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
import { createDashboardEditViewFor } from '../settings/utils';
|
||||
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
|
||||
import { ShareModal } from '../sharing/ShareModal';
|
||||
import { findVizPanelByKey, getDashboardSceneFor, getLibraryPanelBehavior, isPanelClone } from '../utils/utils';
|
||||
import { findVizPanelByKey, getLibraryPanelBehavior, isPanelClone } from '../utils/utils';
|
||||
|
||||
import { DashboardScene, DashboardSceneState } from './DashboardScene';
|
||||
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
|
||||
@ -78,9 +71,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
}
|
||||
|
||||
update.inspectPanelKey = values.inspect;
|
||||
update.overlay = new PanelInspectDrawer({
|
||||
$behaviors: [new ResolveInspectPanelByKey({ panelKey: values.inspect })],
|
||||
});
|
||||
update.overlay = new PanelInspectDrawer({ panelRef: panel.getRef() });
|
||||
} else if (inspectPanelKey) {
|
||||
update.inspectPanelKey = undefined;
|
||||
update.overlay = undefined;
|
||||
@ -196,37 +187,3 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface ResolveInspectPanelByKeyState extends SceneObjectState {
|
||||
panelKey: string;
|
||||
}
|
||||
|
||||
class ResolveInspectPanelByKey extends SceneObjectBase<ResolveInspectPanelByKeyState> {
|
||||
constructor(state: ResolveInspectPanelByKeyState) {
|
||||
super(state);
|
||||
this.addActivationHandler(this._onActivate);
|
||||
}
|
||||
|
||||
private _onActivate = () => {
|
||||
const parent = this.parent;
|
||||
|
||||
if (!parent || !(parent instanceof PanelInspectDrawer)) {
|
||||
throw new Error('ResolveInspectPanelByKey must be attached to a PanelInspectDrawer');
|
||||
}
|
||||
|
||||
const dashboard = getDashboardSceneFor(parent);
|
||||
if (!dashboard) {
|
||||
return;
|
||||
}
|
||||
const panelId = this.state.panelKey;
|
||||
let panel = findVizPanelByKey(dashboard, panelId);
|
||||
|
||||
if (dashboard.state.editPanel) {
|
||||
panel = dashboard.state.editPanel.state.vizManager.state.panel;
|
||||
}
|
||||
|
||||
if (panel) {
|
||||
parent.setState({ panelRef: panel.getRef() });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -77,6 +77,16 @@ export class LibraryPanelBehavior extends SceneObjectBase<LibraryPanelBehaviorSt
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes itself from the parent panel's behaviors array
|
||||
*/
|
||||
public unlink() {
|
||||
const panel = this.parent;
|
||||
if (panel instanceof VizPanel) {
|
||||
panel.setState({ $behaviors: panel.state.$behaviors?.filter((b) => b !== this) });
|
||||
}
|
||||
}
|
||||
|
||||
private async loadLibraryPanelFromPanelModel() {
|
||||
let vizPanel = this.parent;
|
||||
|
||||
|
@ -61,8 +61,8 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const isEditingPanel = Boolean(editPanel);
|
||||
const isViewingPanel = Boolean(viewPanelScene);
|
||||
const isEditedPanelDirty = useVizManagerDirty(editPanel);
|
||||
const isEditingLibraryPanel = useEditingLibraryPanel(editPanel);
|
||||
const isEditedPanelDirty = usePanelEditDirty(editPanel);
|
||||
const isEditingLibraryPanel = editPanel && isLibraryPanel(editPanel.state.panelRef.resolve());
|
||||
const hasCopiedPanel = store.exists(LS_PANEL_COPY_KEY);
|
||||
// Means we are not in settings view, fullscreen panel or edit panel
|
||||
const isShowingDashboard = !editview && !isViewingPanel && !isEditingPanel;
|
||||
@ -422,7 +422,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
onClick={editPanel?.onDiscard}
|
||||
tooltip={editPanel?.state.isNewPanel ? 'Discard panel' : 'Discard panel changes'}
|
||||
size="sm"
|
||||
disabled={!isEditedPanelDirty || !isDirty}
|
||||
disabled={!isEditedPanelDirty}
|
||||
key="discard"
|
||||
fill="outline"
|
||||
variant="destructive"
|
||||
@ -613,41 +613,22 @@ function addDynamicActions(
|
||||
}
|
||||
}
|
||||
|
||||
function useEditingLibraryPanel(panelEditor?: PanelEditor) {
|
||||
const [isEditingLibraryPanel, setEditingLibraryPanel] = useState<Boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (panelEditor) {
|
||||
const unsub = panelEditor.state.vizManager.subscribeToState((vizManagerState) =>
|
||||
setEditingLibraryPanel(isLibraryPanel(vizManagerState.sourcePanel.resolve()))
|
||||
);
|
||||
return () => {
|
||||
unsub.unsubscribe();
|
||||
};
|
||||
}
|
||||
setEditingLibraryPanel(false);
|
||||
return;
|
||||
}, [panelEditor]);
|
||||
|
||||
return isEditingLibraryPanel;
|
||||
}
|
||||
|
||||
// This hook handles when panelEditor is not defined to avoid conditionally hook usage
|
||||
function useVizManagerDirty(panelEditor?: PanelEditor) {
|
||||
const [isDirty, setIsDirty] = useState<Boolean>(false);
|
||||
function usePanelEditDirty(panelEditor?: PanelEditor) {
|
||||
const [isDirty, setIsDirty] = useState<Boolean | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (panelEditor) {
|
||||
const unsub = panelEditor.state.vizManager.subscribeToState((vizManagerState) =>
|
||||
setIsDirty(vizManagerState.isDirty || false)
|
||||
);
|
||||
return () => {
|
||||
unsub.unsubscribe();
|
||||
};
|
||||
const unsub = panelEditor.subscribeToState((state) => {
|
||||
if (state.isDirty !== isDirty) {
|
||||
setIsDirty(state.isDirty);
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsub.unsubscribe();
|
||||
}
|
||||
setIsDirty(false);
|
||||
return;
|
||||
}, [panelEditor]);
|
||||
}, [panelEditor, isDirty]);
|
||||
|
||||
return isDirty;
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ describe('DashboardRow', () => {
|
||||
<TestProvider>
|
||||
<RowOptionsForm
|
||||
repeat={'3'}
|
||||
parent={scene}
|
||||
sceneContext={scene}
|
||||
title=""
|
||||
onCancel={jest.fn()}
|
||||
onUpdate={jest.fn()}
|
||||
@ -40,7 +40,7 @@ describe('DashboardRow', () => {
|
||||
it('Should not show warning component when does not have warningMessage prop', () => {
|
||||
render(
|
||||
<TestProvider>
|
||||
<RowOptionsForm repeat={'3'} parent={scene} title="" onCancel={jest.fn()} onUpdate={jest.fn()} />
|
||||
<RowOptionsForm repeat={'3'} sceneContext={scene} title="" onCancel={jest.fn()} onUpdate={jest.fn()} />
|
||||
</TestProvider>
|
||||
);
|
||||
expect(
|
||||
|
@ -12,13 +12,13 @@ export type OnRowOptionsUpdate = (title: string, repeat?: string | null) => void
|
||||
export interface Props {
|
||||
title: string;
|
||||
repeat?: string;
|
||||
parent: SceneObject;
|
||||
sceneContext: SceneObject;
|
||||
onUpdate: OnRowOptionsUpdate;
|
||||
onCancel: () => void;
|
||||
warning?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const RowOptionsForm = ({ repeat, title, parent, warning, onUpdate, onCancel }: Props) => {
|
||||
export const RowOptionsForm = ({ repeat, title, sceneContext, warning, onUpdate, onCancel }: Props) => {
|
||||
const [newRepeat, setNewRepeat] = useState<string | undefined>(repeat);
|
||||
const onChangeRepeat = useCallback((name?: string) => setNewRepeat(name), [setNewRepeat]);
|
||||
|
||||
@ -38,7 +38,7 @@ export const RowOptionsForm = ({ repeat, title, parent, warning, onUpdate, onCan
|
||||
<Input {...register('title')} type="text" />
|
||||
</Field>
|
||||
<Field label="Repeat for">
|
||||
<RepeatRowSelect2 parent={parent} repeat={newRepeat} onChange={onChangeRepeat} />
|
||||
<RepeatRowSelect2 sceneContext={sceneContext} repeat={newRepeat} onChange={onChangeRepeat} />
|
||||
</Field>
|
||||
{warning && (
|
||||
<Alert
|
||||
|
@ -21,7 +21,7 @@ export const RowOptionsModal = ({ repeat, title, parent, onDismiss, onUpdate, wa
|
||||
return (
|
||||
<Modal isOpen={true} title="Row options" onDismiss={onDismiss} className={styles.modal}>
|
||||
<RowOptionsForm
|
||||
parent={parent}
|
||||
sceneContext={parent}
|
||||
repeat={repeat}
|
||||
title={title}
|
||||
onCancel={onDismiss}
|
||||
|
@ -15,14 +15,7 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||
import { getPluginLinkExtensions, setPluginImportUtils } from '@grafana/runtime';
|
||||
import {
|
||||
MultiValueVariable,
|
||||
sceneGraph,
|
||||
SceneGridLayout,
|
||||
SceneGridRow,
|
||||
SceneTimeRange,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { MultiValueVariable, sceneGraph, SceneGridLayout, SceneGridRow, VizPanel } from '@grafana/scenes';
|
||||
import { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
|
||||
@ -30,10 +23,8 @@ import { reduceTransformRegistryItem } from 'app/features/transformers/editors/R
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||
import { DashboardDataDTO } from 'app/types';
|
||||
|
||||
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||
import { NEW_LINK } from '../settings/links/utils';
|
||||
@ -804,7 +795,7 @@ describe('transformSceneToSaveModel', () => {
|
||||
activateFullSceneTree(scene);
|
||||
|
||||
expect(repeater.state.repeatedPanels?.length).toBe(2);
|
||||
const result = panelRepeaterToPanels(repeater, undefined, true);
|
||||
const result = panelRepeaterToPanels(repeater, true);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
|
||||
@ -861,7 +852,7 @@ describe('transformSceneToSaveModel', () => {
|
||||
);
|
||||
|
||||
activateFullSceneTree(scene);
|
||||
const result = panelRepeaterToPanels(repeater, undefined, true);
|
||||
const result = panelRepeaterToPanels(repeater, true);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
|
||||
@ -886,7 +877,7 @@ describe('transformSceneToSaveModel', () => {
|
||||
activateFullSceneTree(scene);
|
||||
|
||||
let panels: Panel[] = [];
|
||||
gridRowToSaveModel(row, panels, undefined, true);
|
||||
gridRowToSaveModel(row, panels, true);
|
||||
|
||||
expect(panels).toHaveLength(2);
|
||||
expect(panels[0].repeat).toBe('handler');
|
||||
@ -914,7 +905,7 @@ describe('transformSceneToSaveModel', () => {
|
||||
activateFullSceneTree(scene);
|
||||
|
||||
let panels: Panel[] = [];
|
||||
gridRowToSaveModel(row, panels, undefined, true);
|
||||
gridRowToSaveModel(row, panels, true);
|
||||
|
||||
expect(panels[0].repeat).toBe('handler');
|
||||
|
||||
@ -1024,94 +1015,6 @@ describe('transformSceneToSaveModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Given a scene with an open panel editor', () => {
|
||||
it('should persist changes to panel model', async () => {
|
||||
const panel = new VizPanel({
|
||||
key: 'panel-1',
|
||||
pluginId: 'text',
|
||||
});
|
||||
|
||||
const gridItem = new DashboardGridItem({ body: panel });
|
||||
|
||||
const editScene = buildPanelEditScene(panel);
|
||||
const scene = new DashboardScene({
|
||||
editPanel: editScene,
|
||||
isEditing: true,
|
||||
body: new SceneGridLayout({
|
||||
children: [gridItem],
|
||||
}),
|
||||
$timeRange: new SceneTimeRange({
|
||||
from: 'now-6h',
|
||||
to: 'now',
|
||||
timeZone: '',
|
||||
}),
|
||||
});
|
||||
|
||||
editScene!.state.vizManager.state.panel.setState({
|
||||
options: {
|
||||
mode: 'markdown',
|
||||
code: {
|
||||
language: 'plaintext',
|
||||
showLineNumbers: false,
|
||||
showMiniMap: false,
|
||||
},
|
||||
content: 'new content',
|
||||
},
|
||||
});
|
||||
activateFullSceneTree(scene);
|
||||
const saveModel = transformSceneToSaveModel(scene);
|
||||
expect((saveModel.panels![0] as any).options.content).toBe('new content');
|
||||
});
|
||||
|
||||
it('should persist changes to panel model in row', async () => {
|
||||
const panel = new VizPanel({
|
||||
key: 'panel-1',
|
||||
pluginId: 'text',
|
||||
options: {
|
||||
content: 'old content',
|
||||
},
|
||||
});
|
||||
|
||||
const gridItem = new DashboardGridItem({ body: panel });
|
||||
|
||||
const editScene = buildPanelEditScene(panel);
|
||||
const scene = new DashboardScene({
|
||||
editPanel: editScene,
|
||||
isEditing: true,
|
||||
body: new SceneGridLayout({
|
||||
children: [
|
||||
new SceneGridRow({
|
||||
key: '23',
|
||||
isCollapsed: false,
|
||||
children: [gridItem],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
$timeRange: new SceneTimeRange({
|
||||
from: 'now-6h',
|
||||
to: 'now',
|
||||
timeZone: '',
|
||||
}),
|
||||
});
|
||||
activateFullSceneTree(scene);
|
||||
|
||||
editScene!.state.vizManager.state.panel.setState({
|
||||
options: {
|
||||
mode: 'markdown',
|
||||
code: {
|
||||
language: 'plaintext',
|
||||
showLineNumbers: false,
|
||||
showMiniMap: false,
|
||||
},
|
||||
content: 'new content',
|
||||
},
|
||||
});
|
||||
|
||||
const saveModel = transformSceneToSaveModel(scene);
|
||||
expect((saveModel.panels![1] as any).options.content).toBe('new content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Given a scene with repeated panels and non-repeated panels', () => {
|
||||
it('should save repeated panels itemHeight as height', () => {
|
||||
const scene = transformSaveModelToScene({
|
||||
|
@ -33,7 +33,7 @@ import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
|
||||
|
||||
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
@ -58,9 +58,9 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
|
||||
if (child instanceof DashboardGridItem) {
|
||||
// handle panel repeater scenario
|
||||
if (child.state.variableName) {
|
||||
panels = panels.concat(panelRepeaterToPanels(child, state, isSnapshot));
|
||||
panels = panels.concat(panelRepeaterToPanels(child, isSnapshot));
|
||||
} else {
|
||||
panels.push(gridItemToPanel(child, state, isSnapshot));
|
||||
panels.push(gridItemToPanel(child, isSnapshot));
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,7 +69,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
|
||||
if (child.state.key!.indexOf('-clone-') > 0 && !isSnapshot) {
|
||||
continue;
|
||||
}
|
||||
gridRowToSaveModel(child, panels, state, isSnapshot);
|
||||
gridRowToSaveModel(child, panels, isSnapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -139,11 +139,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
|
||||
return sortedDeepCloneWithoutNulls(dashboard);
|
||||
}
|
||||
|
||||
export function gridItemToPanel(
|
||||
gridItem: DashboardGridItem,
|
||||
sceneState?: DashboardSceneState,
|
||||
isSnapshot = false
|
||||
): Panel {
|
||||
export function gridItemToPanel(gridItem: DashboardGridItem, isSnapshot = false): Panel {
|
||||
let vizPanel: VizPanel | undefined;
|
||||
let x = 0,
|
||||
y = 0,
|
||||
@ -152,19 +148,6 @@ export function gridItemToPanel(
|
||||
|
||||
let gridItem_ = gridItem;
|
||||
|
||||
// If we're saving while the panel editor is open, we need to persist those changes in the panel model
|
||||
if (
|
||||
sceneState &&
|
||||
sceneState.editPanel?.state.vizManager &&
|
||||
sceneState.editPanel.state.vizManager.state.sourcePanel.resolve() === gridItem.state.body
|
||||
) {
|
||||
const gridItemClone = gridItem.clone();
|
||||
if (gridItemClone.state.body instanceof VizPanel && !isLibraryPanel(gridItemClone.state.body)) {
|
||||
sceneState.editPanel.state.vizManager.commitChangesTo(gridItemClone.state.body);
|
||||
gridItem_ = gridItemClone;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(gridItem_.state.body instanceof VizPanel)) {
|
||||
throw new Error('DashboardGridItem body expected to be VizPanel');
|
||||
}
|
||||
@ -325,13 +308,9 @@ function vizPanelDataToPanel(
|
||||
return panel;
|
||||
}
|
||||
|
||||
export function panelRepeaterToPanels(
|
||||
repeater: DashboardGridItem,
|
||||
sceneState?: DashboardSceneState,
|
||||
isSnapshot = false
|
||||
): Panel[] {
|
||||
export function panelRepeaterToPanels(repeater: DashboardGridItem, isSnapshot = false): Panel[] {
|
||||
if (!isSnapshot) {
|
||||
return [gridItemToPanel(repeater, sceneState)];
|
||||
return [gridItemToPanel(repeater)];
|
||||
} else {
|
||||
// return early if the repeated panel is a library panel
|
||||
if (repeater.state.body instanceof VizPanel && isLibraryPanel(repeater.state.body)) {
|
||||
@ -388,12 +367,7 @@ export function panelRepeaterToPanels(
|
||||
}
|
||||
}
|
||||
|
||||
export function gridRowToSaveModel(
|
||||
gridRow: SceneGridRow,
|
||||
panelsArray: Array<Panel | RowPanel>,
|
||||
sceneState?: DashboardSceneState,
|
||||
isSnapshot = false
|
||||
) {
|
||||
export function gridRowToSaveModel(gridRow: SceneGridRow, panelsArray: Array<Panel | RowPanel>, isSnapshot = false) {
|
||||
const collapsed = Boolean(gridRow.state.isCollapsed);
|
||||
const rowPanel: RowPanel = {
|
||||
type: 'row',
|
||||
@ -443,10 +417,10 @@ export function gridRowToSaveModel(
|
||||
if (c instanceof DashboardGridItem) {
|
||||
if (c.state.variableName) {
|
||||
// Perform snapshot only for uncollapsed rows
|
||||
panelsInsideRow = panelsInsideRow.concat(panelRepeaterToPanels(c, sceneState, !collapsed));
|
||||
panelsInsideRow = panelsInsideRow.concat(panelRepeaterToPanels(c, !collapsed));
|
||||
} else {
|
||||
// Perform snapshot only for uncollapsed panels
|
||||
panelsInsideRow.push(gridItemToPanel(c, sceneState, !collapsed));
|
||||
panelsInsideRow.push(gridItemToPanel(c, !collapsed));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -455,7 +429,7 @@ export function gridRowToSaveModel(
|
||||
if (!(c instanceof DashboardGridItem)) {
|
||||
throw new Error('Row child expected to be DashboardGridItem');
|
||||
}
|
||||
return gridItemToPanel(c, sceneState);
|
||||
return gridItemToPanel(c);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -110,20 +110,17 @@ export function getFieldOverrideCategories(
|
||||
onOverrideChange(idx, override);
|
||||
};
|
||||
|
||||
const onDynamicConfigValueAdd = (o: ConfigOverrideRule, value: SelectableValue<string>) => {
|
||||
const onDynamicConfigValueAdd = (override: ConfigOverrideRule, value: SelectableValue<string>) => {
|
||||
const registryItem = registry.get(value.value!);
|
||||
const propertyConfig: DynamicConfigValue = {
|
||||
id: registryItem.id,
|
||||
value: registryItem.defaultValue,
|
||||
};
|
||||
|
||||
if (override.properties) {
|
||||
o.properties.push(propertyConfig);
|
||||
} else {
|
||||
o.properties = [propertyConfig];
|
||||
}
|
||||
const properties = override.properties ?? [];
|
||||
properties.push(propertyConfig);
|
||||
|
||||
onOverrideChange(idx, o);
|
||||
onOverrideChange(idx, { ...override, properties });
|
||||
};
|
||||
|
||||
/**
|
||||
@ -158,13 +155,23 @@ export function getFieldOverrideCategories(
|
||||
}
|
||||
|
||||
const onPropertyChange = (value: DynamicConfigValue) => {
|
||||
override.properties[propIdx].value = value;
|
||||
onOverrideChange(idx, override);
|
||||
onOverrideChange(idx, {
|
||||
...override,
|
||||
properties: override.properties.map((prop, i) => {
|
||||
if (i === propIdx) {
|
||||
return { ...prop, value: value };
|
||||
}
|
||||
|
||||
return prop;
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const onPropertyRemove = () => {
|
||||
override.properties.splice(propIdx, 1);
|
||||
onOverrideChange(idx, override);
|
||||
onOverrideChange(idx, {
|
||||
...override,
|
||||
properties: override.properties.filter((_, i) => i !== propIdx),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -44,14 +44,14 @@ export const RepeatRowSelect = ({ repeat, onChange, id }: Props) => {
|
||||
};
|
||||
|
||||
interface Props2 {
|
||||
parent: SceneObject;
|
||||
sceneContext: SceneObject;
|
||||
repeat: string | undefined;
|
||||
id?: string;
|
||||
onChange: (name?: string) => void;
|
||||
}
|
||||
|
||||
export const RepeatRowSelect2 = ({ parent, repeat, id, onChange }: Props2) => {
|
||||
const sceneVars = useMemo(() => sceneGraph.getVariables(parent), [parent]);
|
||||
export const RepeatRowSelect2 = ({ sceneContext, repeat, id, onChange }: Props2) => {
|
||||
const sceneVars = useMemo(() => sceneGraph.getVariables(sceneContext.getRoot()), [sceneContext]);
|
||||
const variables = sceneVars.useState().variables;
|
||||
|
||||
const variableOptions = useMemo(() => {
|
||||
|
@ -3,7 +3,6 @@ import { lastValueFrom } from 'rxjs';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
import { LibraryPanel, defaultDashboard } from '@grafana/schema';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { VizPanelManager } from 'app/features/dashboard-scene/panel-edit/VizPanelManager';
|
||||
import { DashboardGridItem } from 'app/features/dashboard-scene/scene/DashboardGridItem';
|
||||
import { vizPanelToPanel } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModel';
|
||||
import { getLibraryPanelBehavior } from 'app/features/dashboard-scene/utils/utils';
|
||||
@ -146,10 +145,6 @@ export function libraryVizPanelToSaveModel(vizPanel: VizPanel) {
|
||||
|
||||
let gridItem = vizPanel.parent;
|
||||
|
||||
if (gridItem instanceof VizPanelManager) {
|
||||
gridItem = gridItem.state.sourcePanel.resolve().parent;
|
||||
}
|
||||
|
||||
if (!gridItem || !(gridItem instanceof DashboardGridItem)) {
|
||||
throw new Error('Trying to save a library panel that does not have a DashboardGridItem parent');
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ describe('DataTrailsHistory', () => {
|
||||
{
|
||||
name: 'from history',
|
||||
input: { from: '2024-07-22T18:30:00.000Z', to: '2024-07-22T19:30:00.000Z' },
|
||||
expected: '2024-07-22 12:30:00 - 2024-07-22 13:30:00',
|
||||
expected: '2024-07-22 13:30:00 - 2024-07-22 14:30:00',
|
||||
},
|
||||
{
|
||||
name: 'time change event with timezone',
|
||||
@ -33,7 +33,7 @@ describe('DataTrailsHistory', () => {
|
||||
},
|
||||
])('$name', ({ input, expected }) => {
|
||||
const result = parseTimeTooltip(input);
|
||||
expect(result).toBe(expected);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -23,9 +23,11 @@ function getQueryDisplayText(query: DataQuery): string {
|
||||
|
||||
function isPanelInEdit(panelId: number, panelInEditId?: number) {
|
||||
let idToCompareWith = panelInEditId;
|
||||
|
||||
if (window.__grafanaSceneContext && window.__grafanaSceneContext instanceof DashboardScene) {
|
||||
idToCompareWith = window.__grafanaSceneContext.state.editPanel?.state.panelId;
|
||||
idToCompareWith = window.__grafanaSceneContext.state.editPanel?.getPanelId();
|
||||
}
|
||||
|
||||
return panelId === idToCompareWith;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user