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"]
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "10"]
|
||||||
],
|
],
|
||||||
"public/app/features/dashboard-scene/inspect/HelpWizard/utils.ts:5381": [
|
"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.", "0"]
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
|
||||||
],
|
],
|
||||||
"public/app/features/dashboard-scene/inspect/InspectDataTab.tsx:5381": [
|
"public/app/features/dashboard-scene/inspect/InspectDataTab.tsx:5381": [
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
[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"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts:5381": [
|
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
|
||||||
],
|
],
|
||||||
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts:5381": [
|
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[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 { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
|
||||||
|
|
||||||
import { DashboardGridItem } from '../../scene/DashboardGridItem';
|
import { DashboardGridItem } from '../../scene/DashboardGridItem';
|
||||||
import { DashboardScene } from '../../scene/DashboardScene';
|
|
||||||
import { gridItemToPanel, vizPanelToPanel } from '../../serialization/transformSceneToSaveModel';
|
import { gridItemToPanel, vizPanelToPanel } from '../../serialization/transformSceneToSaveModel';
|
||||||
import { getQueryRunnerFor, isLibraryPanel } from '../../utils/utils';
|
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) {
|
export async function getDebugDashboard(panel: VizPanel, rand: Randomize, timeRange: TimeRange) {
|
||||||
let saveModel: ReturnType<typeof gridItemToPanel> = { type: '' };
|
let saveModel: ReturnType<typeof gridItemToPanel> = { type: '' };
|
||||||
const gridItem = panel.parent as DashboardGridItem;
|
const gridItem = panel.parent as DashboardGridItem;
|
||||||
const scene = panel.getRoot() as DashboardScene;
|
|
||||||
|
|
||||||
if (isLibraryPanel(panel)) {
|
if (isLibraryPanel(panel)) {
|
||||||
saveModel = {
|
saveModel = {
|
||||||
...gridItemToPanel(gridItem),
|
...gridItemToPanel(gridItem),
|
||||||
...vizPanelToPanel(panel),
|
...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 {
|
} else {
|
||||||
saveModel = gridItemToPanel(gridItem);
|
saveModel = gridItemToPanel(gridItem);
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,6 @@ import { InspectTab } from 'app/features/inspector/types';
|
|||||||
import { getPrettyJSON } from 'app/features/inspector/utils/utils';
|
import { getPrettyJSON } from 'app/features/inspector/utils/utils';
|
||||||
import { reportPanelInspectInteraction } from 'app/features/search/page/reporting';
|
import { reportPanelInspectInteraction } from 'app/features/search/page/reporting';
|
||||||
|
|
||||||
import { VizPanelManager } from '../panel-edit/VizPanelManager';
|
|
||||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||||
import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene';
|
import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene';
|
||||||
import { gridItemToPanel, vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
import { gridItemToPanel, vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||||
@ -219,11 +218,6 @@ function getJsonText(show: ShowContent, panel: VizPanel): string {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (panel.parent instanceof VizPanelManager) {
|
|
||||||
objToStringify = panel.parent.getPanelSaveModel();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gridItem instanceof DashboardGridItem) {
|
if (gridItem instanceof DashboardGridItem) {
|
||||||
objToStringify = gridItemToPanel(gridItem);
|
objToStringify = gridItemToPanel(gridItem);
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ import { SceneInspectTab } from './types';
|
|||||||
|
|
||||||
interface PanelInspectDrawerState extends SceneObjectState {
|
interface PanelInspectDrawerState extends SceneObjectState {
|
||||||
tabs?: SceneInspectTab[];
|
tabs?: SceneInspectTab[];
|
||||||
panelRef?: SceneObjectRef<VizPanel>;
|
panelRef: SceneObjectRef<VizPanel>;
|
||||||
pluginNotLoaded?: boolean;
|
pluginNotLoaded?: boolean;
|
||||||
canEdit?: boolean;
|
canEdit?: boolean;
|
||||||
}
|
}
|
||||||
@ -51,7 +51,7 @@ export class PanelInspectDrawer extends SceneObjectBase<PanelInspectDrawerState>
|
|||||||
*/
|
*/
|
||||||
async buildTabs(retry: number) {
|
async buildTabs(retry: number) {
|
||||||
const panelRef = this.state.panelRef;
|
const panelRef = this.state.panelRef;
|
||||||
const plugin = panelRef?.resolve()?.getPlugin();
|
const plugin = panelRef.resolve()?.getPlugin();
|
||||||
const tabs: SceneInspectTab[] = [];
|
const tabs: SceneInspectTab[] = [];
|
||||||
|
|
||||||
if (!plugin) {
|
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 { createDashboardSceneFromDashboardModel } from '../../serialization/transformSaveModelToScene';
|
||||||
import * as utils from '../../utils/utils';
|
import * as utils from '../../utils/utils';
|
||||||
import { findVizPanelByKey, getVizPanelKeyForPanelId } from '../../utils/utils';
|
import { findVizPanelByKey, getVizPanelKeyForPanelId } from '../../utils/utils';
|
||||||
import { VizPanelManager } from '../VizPanelManager';
|
|
||||||
|
|
||||||
import { PanelDataAlertingTab, PanelDataAlertingTabRendered } from './PanelDataAlertingTab';
|
import { PanelDataAlertingTab, PanelDataAlertingTabRendered } from './PanelDataAlertingTab';
|
||||||
|
|
||||||
@ -361,7 +360,7 @@ async function clickNewButton() {
|
|||||||
function createModel(dashboard: DashboardModel) {
|
function createModel(dashboard: DashboardModel) {
|
||||||
const scene = createDashboardSceneFromDashboardModel(dashboard, {} as DashboardDataDTO);
|
const scene = createDashboardSceneFromDashboardModel(dashboard, {} as DashboardDataDTO);
|
||||||
const vizPanel = findVizPanelByKey(scene, getVizPanelKeyForPanelId(34))!;
|
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);
|
jest.spyOn(utils, 'getDashboardSceneFor').mockReturnValue(scene);
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
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 { Alert, LoadingPlaceholder, Tab, useStyles2 } from '@grafana/ui';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { RulesTable } from 'app/features/alerting/unified/components/rules/RulesTable';
|
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 { stringifyErrorLike } from 'app/features/alerting/unified/utils/misc';
|
||||||
|
|
||||||
import { getDashboardSceneFor, getPanelIdForVizPanel } from '../../utils/utils';
|
import { getDashboardSceneFor, getPanelIdForVizPanel } from '../../utils/utils';
|
||||||
import { VizPanelManager } from '../VizPanelManager';
|
|
||||||
|
|
||||||
import { ScenesNewRuleFromPanelButton } from './NewAlertRuleButton';
|
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 {
|
export interface PanelDataAlertingTabState extends SceneObjectState {
|
||||||
|
panelRef: SceneObjectRef<VizPanel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PanelDataAlertingTab extends SceneObjectBase<PanelDataAlertingTabState> implements PanelDataPaneTab {
|
||||||
static Component = PanelDataAlertingTabRendered;
|
static Component = PanelDataAlertingTabRendered;
|
||||||
TabComponent: (props: PanelDataTabHeaderProps) => React.JSX.Element;
|
public tabId = TabId.Alert;
|
||||||
|
|
||||||
tabId = TabId.Alert;
|
public renderTab(props: PanelDataTabHeaderProps) {
|
||||||
private _panelManager: VizPanelManager;
|
return <AlertingTab key={this.getTabLabel()} model={this} {...props} />;
|
||||||
|
|
||||||
constructor(panelManager: VizPanelManager) {
|
|
||||||
super({});
|
|
||||||
this.TabComponent = (props: PanelDataTabHeaderProps) => AlertingTab({ ...props, model: this });
|
|
||||||
this._panelManager = panelManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTabLabel() {
|
public getTabLabel() {
|
||||||
return 'Alert';
|
return 'Alert';
|
||||||
}
|
}
|
||||||
|
|
||||||
getDashboardUID() {
|
public getDashboardUID() {
|
||||||
const dashboard = this.getDashboard();
|
const dashboard = this.getDashboard();
|
||||||
return dashboard.state.uid!;
|
return dashboard.state.uid!;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDashboard() {
|
public getDashboard() {
|
||||||
return getDashboardSceneFor(this._panelManager);
|
return getDashboardSceneFor(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getLegacyPanelId() {
|
public getLegacyPanelId() {
|
||||||
return getPanelIdForVizPanel(this._panelManager.state.panel);
|
return getPanelIdForVizPanel(this.state.panelRef.resolve());
|
||||||
}
|
}
|
||||||
|
|
||||||
getCanCreateRules() {
|
public getCanCreateRules() {
|
||||||
const rulesPermissions = getRulesPermissions('grafana');
|
const rulesPermissions = getRulesPermissions('grafana');
|
||||||
return this.getDashboard().state.meta.canSave && contextSrv.hasPermission(rulesPermissions.create);
|
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>) {
|
export function PanelDataAlertingTabRendered({ model }: SceneComponentProps<PanelDataAlertingTab>) {
|
||||||
const { model } = props;
|
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const { errors, loading, rules } = usePanelCombinedRules({
|
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();
|
const canCreateRules = model.getCanCreateRules();
|
||||||
|
|
||||||
if (rules.length) {
|
if (rules.length) {
|
||||||
@ -132,7 +119,6 @@ function AlertingTab(props: PanelDataAlertingTabHeaderProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab
|
<Tab
|
||||||
key={props.key}
|
|
||||||
label={model.getTabLabel()}
|
label={model.getTabLabel()}
|
||||||
icon="bell"
|
icon="bell"
|
||||||
counter={rules.length}
|
counter={rules.length}
|
||||||
|
@ -1,46 +1,64 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { Unsubscribable } from 'rxjs';
|
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import {
|
import {
|
||||||
SceneComponentProps,
|
SceneComponentProps,
|
||||||
SceneObjectBase,
|
SceneObjectBase,
|
||||||
|
SceneObjectRef,
|
||||||
SceneObjectState,
|
SceneObjectState,
|
||||||
SceneObjectUrlSyncConfig,
|
SceneObjectUrlSyncConfig,
|
||||||
SceneObjectUrlValues,
|
SceneObjectUrlValues,
|
||||||
|
VizPanel,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { Container, CustomScrollbar, TabContent, TabsBar, useStyles2 } from '@grafana/ui';
|
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 { contextSrv } from 'app/core/core';
|
||||||
import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-control';
|
import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-control';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||||
|
|
||||||
import { VizPanelManager } from '../VizPanelManager';
|
|
||||||
|
|
||||||
import { PanelDataAlertingTab } from './PanelDataAlertingTab';
|
import { PanelDataAlertingTab } from './PanelDataAlertingTab';
|
||||||
import { PanelDataQueriesTab } from './PanelDataQueriesTab';
|
import { PanelDataQueriesTab } from './PanelDataQueriesTab';
|
||||||
import { PanelDataTransformationsTab } from './PanelDataTransformationsTab';
|
import { PanelDataTransformationsTab } from './PanelDataTransformationsTab';
|
||||||
import { PanelDataPaneTab, TabId } from './types';
|
import { PanelDataPaneTab, TabId } from './types';
|
||||||
|
|
||||||
export interface PanelDataPaneState extends SceneObjectState {
|
export interface PanelDataPaneState extends SceneObjectState {
|
||||||
tabs?: PanelDataPaneTab[];
|
tabs: PanelDataPaneTab[];
|
||||||
tab?: TabId;
|
tab: TabId;
|
||||||
|
panelRef: SceneObjectRef<VizPanel>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||||
static Component = PanelDataPaneRendered;
|
static Component = PanelDataPaneRendered;
|
||||||
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['tab'] });
|
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['tab'] });
|
||||||
private panelSubscription: Unsubscribable | undefined;
|
|
||||||
public panelManager: VizPanelManager;
|
|
||||||
|
|
||||||
getUrlState() {
|
public static createFor(panel: VizPanel) {
|
||||||
return {
|
const panelRef = panel.getRef();
|
||||||
tab: this.state.tab,
|
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) {
|
if (!values.tab) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -48,68 +66,6 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
|||||||
this.setState({ tab: values.tab as TabId });
|
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>) {
|
function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||||
@ -125,15 +81,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.dataPane} data-testid={selectors.components.PanelEditor.DataPane.content}>
|
<div className={styles.dataPane} data-testid={selectors.components.PanelEditor.DataPane.content}>
|
||||||
<TabsBar hideBorder={true} className={styles.tabsBar}>
|
<TabsBar hideBorder={true} className={styles.tabsBar}>
|
||||||
{tabs.map((t, index) => {
|
{tabs.map((t) => t.renderTab({ active: t.tabId === tab, onChangeTab: () => model.onChangeTab(t) }))}
|
||||||
return (
|
|
||||||
<t.TabComponent
|
|
||||||
key={`${t.getTabLabel()}-${index}`}
|
|
||||||
active={t.tabId === tab}
|
|
||||||
onChangeTab={() => model.onChangeTab(t)}
|
|
||||||
></t.TabComponent>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TabsBar>
|
</TabsBar>
|
||||||
<CustomScrollbar className={styles.scroll}>
|
<CustomScrollbar className={styles.scroll}>
|
||||||
<TabContent className={styles.tabContent}>
|
<TabContent className={styles.tabContent}>
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
DataQuery,
|
DataQuery,
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
|
DataSourceInstanceSettings,
|
||||||
DataSourceJsonData,
|
DataSourceJsonData,
|
||||||
DataSourceRef,
|
DataSourceRef,
|
||||||
FieldType,
|
FieldType,
|
||||||
@ -14,29 +15,28 @@ import {
|
|||||||
TimeRange,
|
TimeRange,
|
||||||
toDataFrame,
|
toDataFrame,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
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 { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||||
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
||||||
import { DashboardDataDTO } from 'app/types';
|
import { DashboardDataDTO } from 'app/types';
|
||||||
|
|
||||||
|
import { PanelTimeRange, PanelTimeRangeState } from '../../scene/PanelTimeRange';
|
||||||
import { transformSaveModelToScene } from '../../serialization/transformSaveModelToScene';
|
import { transformSaveModelToScene } from '../../serialization/transformSaveModelToScene';
|
||||||
import { DashboardModelCompatibilityWrapper } from '../../utils/DashboardModelCompatibilityWrapper';
|
|
||||||
import { findVizPanelByKey } from '../../utils/utils';
|
import { findVizPanelByKey } from '../../utils/utils';
|
||||||
import { VizPanelManager } from '../VizPanelManager';
|
import { buildPanelEditScene } from '../PanelEditor';
|
||||||
import { testDashboard } from '../testfiles/testDashboard';
|
import { testDashboard, panelWithTransformations, panelWithQueriesOnly } from '../testfiles/testDashboard';
|
||||||
|
|
||||||
import { PanelDataQueriesTab, PanelDataQueriesTabRendered } from './PanelDataQueriesTab';
|
import { PanelDataQueriesTab, PanelDataQueriesTabRendered } from './PanelDataQueriesTab';
|
||||||
|
|
||||||
async function createModelMock() {
|
async function createModelMock() {
|
||||||
const panelManager = setupVizPanelManger('panel-1');
|
const { queriesTab } = await setupScene('panel-1');
|
||||||
panelManager.activate();
|
|
||||||
await Promise.resolve();
|
|
||||||
const queryTabModel = new PanelDataQueriesTab(panelManager);
|
|
||||||
|
|
||||||
// mock queryRunner data state
|
// mock queryRunner data state
|
||||||
jest.spyOn(queryTabModel.queryRunner, 'state', 'get').mockReturnValue({
|
jest.spyOn(queriesTab.queryRunner, 'state', 'get').mockReturnValue({
|
||||||
...queryTabModel.queryRunner.state,
|
...queriesTab.queryRunner.state,
|
||||||
data: {
|
data: {
|
||||||
state: LoadingState.Done,
|
state: LoadingState.Done,
|
||||||
series: [
|
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 runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => {
|
||||||
const result: PanelData = {
|
const result: PanelData = {
|
||||||
state: LoadingState.Loading,
|
state: LoadingState.Loading,
|
||||||
@ -186,11 +192,17 @@ const MixedDsSettingsMock = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const panelPlugin = getPanelPlugin({ id: 'timeseries', skipDataQuery: false });
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
|
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
|
||||||
return runRequestMock(ds, request);
|
return runRequestMock(ds, request);
|
||||||
},
|
},
|
||||||
|
getPluginImportUtils: () => ({
|
||||||
|
getPanelPluginFromCache: jest.fn(() => panelPlugin),
|
||||||
|
}),
|
||||||
|
getPluginLinkExtensions: jest.fn(),
|
||||||
getDataSourceSrv: () => ({
|
getDataSourceSrv: () => ({
|
||||||
get: async (ref: DataSourceRef) => {
|
get: async (ref: DataSourceRef) => {
|
||||||
// Mocking the build in Grafana data source to avoid annotations data layer errors.
|
// Mocking the build in Grafana data source to avoid annotations data layer errors.
|
||||||
@ -234,52 +246,63 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
return instance1SettingsMock;
|
return instance1SettingsMock;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
locationService: {
|
|
||||||
partial: jest.fn(),
|
|
||||||
getSearchObject: jest.fn().mockReturnValue({
|
|
||||||
firstPanel: false,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
config: {
|
config: {
|
||||||
...jest.requireActual('@grafana/runtime').config,
|
...jest.requireActual('@grafana/runtime').config,
|
||||||
defaultDatasource: 'gdev-testdata',
|
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);
|
jest.mock('app/core/store', () => ({
|
||||||
model.addQueryClick();
|
exists: jest.fn(),
|
||||||
expect(model.queryRunner.state.queries).toHaveLength(2);
|
get: jest.fn(),
|
||||||
expect(model.queryRunner.state.queries[1].refId).toBe('B');
|
getObject: jest.fn((_a, b) => b),
|
||||||
expect(model.queryRunner.state.queries[1].hide).toBe(false);
|
setObject: jest.fn(),
|
||||||
expect(model.queryRunner.state.queries[1].datasource).toEqual({
|
}));
|
||||||
|
|
||||||
|
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',
|
type: 'grafana-testdata-datasource',
|
||||||
uid: 'gdev-testdata',
|
uid: 'gdev-testdata',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can add a new query when datasource is mixed', async () => {
|
it('Can add a new query when datasource is mixed', async () => {
|
||||||
const vizPanelManager = setupVizPanelManger('panel-7');
|
const { queriesTab } = await setupScene('panel-7');
|
||||||
vizPanelManager.activate();
|
|
||||||
await Promise.resolve();
|
|
||||||
|
|
||||||
const model = new PanelDataQueriesTab(vizPanelManager);
|
expect(queriesTab.state.datasource?.uid).toBe('-- Mixed --');
|
||||||
expect(vizPanelManager.state.datasource?.uid).toBe('-- Mixed --');
|
expect(queriesTab.queryRunner.state.datasource?.uid).toBe('-- Mixed --');
|
||||||
expect(model.queryRunner.state.datasource?.uid).toBe('-- Mixed --');
|
|
||||||
model.addQueryClick();
|
|
||||||
|
|
||||||
expect(model.queryRunner.state.queries).toHaveLength(2);
|
queriesTab.addQueryClick();
|
||||||
expect(model.queryRunner.state.queries[1].refId).toBe('B');
|
|
||||||
expect(model.queryRunner.state.queries[1].hide).toBe(false);
|
expect(queriesTab.queryRunner.state.queries).toHaveLength(2);
|
||||||
expect(model.queryRunner.state.queries[1].datasource?.uid).toBe('gdev-testdata');
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('PanelDataQueriesTab', () => {
|
describe('PanelDataQueriesTab', () => {
|
||||||
it('renders query group top section', async () => {
|
it('renders query group top section', async () => {
|
||||||
const modelMock = await createModelMock();
|
const modelMock = await createModelMock();
|
||||||
|
|
||||||
@ -329,17 +352,348 @@ describe('PanelDataQueriesTab', () => {
|
|||||||
|
|
||||||
expect(modelMock.onQueriesChange).toHaveBeenCalledWith([]);
|
expect(modelMock.onQueriesChange).toHaveBeenCalledWith([]);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('query options', () => {
|
||||||
|
describe('activation', () => {
|
||||||
|
it('should load data source', async () => {
|
||||||
|
const { queriesTab } = await setupScene('panel-1');
|
||||||
|
|
||||||
|
expect(queriesTab.state.datasource).toEqual(ds1Mock);
|
||||||
|
expect(queriesTab.state.dsSettings).toEqual(instance1SettingsMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should store loaded data source in local storage', async () => {
|
||||||
|
await setupScene('panel-1');
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const setupVizPanelManger = (panelId: string) => {
|
async function setupScene(panelId: string) {
|
||||||
const scene = transformSaveModelToScene({ dashboard: testDashboard as unknown as DashboardDataDTO, meta: {} });
|
const dashboard = transformSaveModelToScene({ dashboard: testDashboard as unknown as DashboardDataDTO, meta: {} });
|
||||||
const panel = findVizPanelByKey(scene, panelId)!;
|
const panel = findVizPanelByKey(dashboard, panelId)!;
|
||||||
|
|
||||||
const vizPanelManager = VizPanelManager.createFor(panel);
|
const panelEditor = buildPanelEditScene(panel);
|
||||||
|
dashboard.setState({ editPanel: panelEditor });
|
||||||
|
|
||||||
// The following happens on DahsboardScene activation. For the needs of this test this activation aint needed hence we hand-call it
|
deactivators.push(dashboard.activate());
|
||||||
// @ts-expect-error
|
deactivators.push(panelEditor.activate());
|
||||||
getDashboardSrv().setCurrent(new DashboardModelCompatibilityWrapper(scene));
|
|
||||||
|
|
||||||
return vizPanelManager;
|
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, getDataSourceRef } from '@grafana/data';
|
||||||
|
|
||||||
import { CoreApp, DataSourceApi, DataSourceInstanceSettings, IconName, getDataSourceRef } from '@grafana/data';
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { config, getDataSourceSrv } from '@grafana/runtime';
|
import { config, getDataSourceSrv, locationService } from '@grafana/runtime';
|
||||||
import { SceneObjectBase, SceneComponentProps, sceneGraph, SceneQueryRunner } from '@grafana/scenes';
|
import {
|
||||||
|
SceneObjectBase,
|
||||||
|
SceneComponentProps,
|
||||||
|
sceneGraph,
|
||||||
|
SceneQueryRunner,
|
||||||
|
SceneObjectRef,
|
||||||
|
VizPanel,
|
||||||
|
SceneObjectState,
|
||||||
|
SceneDataQuery,
|
||||||
|
} from '@grafana/scenes';
|
||||||
import { DataQuery } from '@grafana/schema';
|
import { DataQuery } from '@grafana/schema';
|
||||||
import { Button, Stack, Tab } from '@grafana/ui';
|
import { Button, Stack, Tab } from '@grafana/ui';
|
||||||
import { addQuery } from 'app/core/utils/query';
|
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 { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
|
||||||
import { GroupActionComponents } from 'app/features/query/components/QueryActionComponent';
|
import { GroupActionComponents } from 'app/features/query/components/QueryActionComponent';
|
||||||
import { QueryEditorRows } from 'app/features/query/components/QueryEditorRows';
|
import { QueryEditorRows } from 'app/features/query/components/QueryEditorRows';
|
||||||
import { QueryGroupTopSection } from 'app/features/query/components/QueryGroup';
|
import { QueryGroupTopSection } from 'app/features/query/components/QueryGroup';
|
||||||
|
import { updateQueries } from 'app/features/query/state/updateQueries';
|
||||||
import { isSharedDashboardQuery } from 'app/plugins/datasource/dashboard';
|
import { isSharedDashboardQuery } from 'app/plugins/datasource/dashboard';
|
||||||
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
|
|
||||||
import { QueryGroupOptions } from 'app/types';
|
import { QueryGroupOptions } from 'app/types';
|
||||||
|
|
||||||
import { PanelTimeRange } from '../../scene/PanelTimeRange';
|
import { PanelTimeRange, PanelTimeRangeState } from '../../scene/PanelTimeRange';
|
||||||
import { VizPanelManager } from '../VizPanelManager';
|
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;
|
datasource?: DataSourceApi;
|
||||||
dsSettings?: DataSourceInstanceSettings;
|
dsSettings?: DataSourceInstanceSettings;
|
||||||
|
panelRef: SceneObjectRef<VizPanel>;
|
||||||
}
|
}
|
||||||
export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabState> implements PanelDataPaneTab {
|
export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabState> implements PanelDataPaneTab {
|
||||||
static Component = PanelDataQueriesTabRendered;
|
static Component = PanelDataQueriesTabRendered;
|
||||||
TabComponent: (props: PanelDataTabHeaderProps) => React.JSX.Element;
|
|
||||||
|
|
||||||
tabId = TabId.Queries;
|
tabId = TabId.Queries;
|
||||||
icon: IconName = 'database';
|
|
||||||
private _panelManager: VizPanelManager;
|
|
||||||
|
|
||||||
getTabLabel() {
|
public constructor(state: PanelDataQueriesTabState) {
|
||||||
|
super(state);
|
||||||
|
this.addActivationHandler(() => this.onActivate());
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTabLabel() {
|
||||||
return 'Queries';
|
return 'Queries';
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemsCount() {
|
public getItemsCount() {
|
||||||
return this.getQueries().length;
|
return this.getQueries().length;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(panelManager: VizPanelManager) {
|
public renderTab(props: PanelDataTabHeaderProps) {
|
||||||
super({});
|
return <QueriesTab key={this.getTabLabel()} model={this} {...props} />;
|
||||||
|
|
||||||
this.TabComponent = (props: PanelDataTabHeaderProps) => {
|
|
||||||
return QueriesTab({ ...props, model: this });
|
|
||||||
};
|
|
||||||
|
|
||||||
this._panelManager = panelManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildQueryOptions(): QueryGroupOptions {
|
private onActivate() {
|
||||||
const panelManager = this._panelManager;
|
this.loadDataSource();
|
||||||
const panelObj = this._panelManager.state.panel;
|
}
|
||||||
const queryRunner = this._panelManager.queryRunner;
|
|
||||||
const timeRangeObj = sceneGraph.getTimeRange(panelObj);
|
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'] = {
|
let timeRangeOpts: QueryGroupOptions['timeRange'] = {
|
||||||
from: undefined,
|
from: undefined,
|
||||||
@ -71,19 +145,14 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
|||||||
}
|
}
|
||||||
|
|
||||||
let queries: QueryGroupOptions['queries'] = queryRunner.state.queries;
|
let queries: QueryGroupOptions['queries'] = queryRunner.state.queries;
|
||||||
|
const dsSettings = this.state.dsSettings;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cacheTimeout: panelManager.state.dsSettings?.meta.queryOptions?.cacheTimeout
|
cacheTimeout: dsSettings?.meta.queryOptions?.cacheTimeout ? queryRunner.state.cacheTimeout : undefined,
|
||||||
? queryRunner.state.cacheTimeout
|
queryCachingTTL: dsSettings?.cachingConfig?.enabled ? queryRunner.state.queryCachingTTL : undefined,
|
||||||
: undefined,
|
|
||||||
queryCachingTTL: panelManager.state.dsSettings?.cachingConfig?.enabled
|
|
||||||
? queryRunner.state.queryCachingTTL
|
|
||||||
: undefined,
|
|
||||||
dataSource: {
|
dataSource: {
|
||||||
default: panelManager.state.dsSettings?.isDefault,
|
default: dsSettings?.isDefault,
|
||||||
...(panelManager.state.dsSettings
|
...(dsSettings ? getDataSourceRef(dsSettings) : { type: undefined, uid: undefined }),
|
||||||
? getDataSourceRef(panelManager.state.dsSettings)
|
|
||||||
: { type: undefined, uid: undefined }),
|
|
||||||
},
|
},
|
||||||
queries,
|
queries,
|
||||||
maxDataPoints: queryRunner.state.maxDataPoints,
|
maxDataPoints: queryRunner.state.maxDataPoints,
|
||||||
@ -92,37 +161,98 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpenInspector = () => {
|
public onOpenInspector = () => {
|
||||||
this._panelManager.inspectPanel();
|
const panel = this.state.panelRef.resolve();
|
||||||
|
const panelId = getPanelIdForVizPanel(panel);
|
||||||
|
|
||||||
|
locationService.partial({ inspect: panelId, inspectTab: 'query' });
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeDataSource = async (
|
public onChangeDataSource = async (newSettings: DataSourceInstanceSettings, defaultQueries?: SceneDataQuery[]) => {
|
||||||
newSettings: DataSourceInstanceSettings,
|
const { dsSettings } = this.state;
|
||||||
defaultQueries?: DataQuery[] | GrafanaQuery[]
|
const queryRunner = this.queryRunner;
|
||||||
) => {
|
|
||||||
this._panelManager.changePanelDataSource(newSettings, defaultQueries);
|
|
||||||
};
|
|
||||||
|
|
||||||
onQueryOptionsChange = (options: QueryGroupOptions) => {
|
const currentDS = dsSettings ? await getDataSourceSrv().get({ uid: dsSettings.uid }) : undefined;
|
||||||
this._panelManager.changeQueryOptions(options);
|
const nextDS = await getDataSourceSrv().get({ uid: newSettings.uid });
|
||||||
};
|
|
||||||
|
|
||||||
onQueriesChange = (queries: DataQuery[]) => {
|
const currentQueries = queryRunner.state.queries;
|
||||||
this._panelManager.changeQueries(queries);
|
|
||||||
};
|
|
||||||
|
|
||||||
onRunQueries = () => {
|
// 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
|
||||||
this._panelManager.queryRunner.runQueries();
|
const queries = defaultQueries || (await updateQueries(nextDS, newSettings.uid, currentQueries, currentDS));
|
||||||
};
|
|
||||||
|
|
||||||
getQueries() {
|
queryRunner.setState({ datasource: getDataSourceRef(newSettings), queries });
|
||||||
return this._panelManager.queryRunner.state.queries;
|
|
||||||
|
if (defaultQueries) {
|
||||||
|
queryRunner.runQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
newQuery(): Partial<DataQuery> {
|
this.loadDataSource();
|
||||||
const { dsSettings, datasource } = this._panelManager.state;
|
};
|
||||||
|
|
||||||
|
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;
|
let ds;
|
||||||
|
|
||||||
if (!dsSettings?.meta.mixed) {
|
if (!dsSettings?.meta.mixed) {
|
||||||
ds = dsSettings; // Use dsSettings if it is not mixed
|
ds = dsSettings; // Use dsSettings if it is not mixed
|
||||||
} else if (!datasource?.meta.mixed) {
|
} else if (!datasource?.meta.mixed) {
|
||||||
@ -138,29 +268,30 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
addQueryClick = () => {
|
public addQueryClick = () => {
|
||||||
const queries = this.getQueries();
|
const queries = this.getQueries();
|
||||||
this.onQueriesChange(addQuery(queries, this.newQuery()));
|
this.onQueriesChange(addQuery(queries, this.newQuery()));
|
||||||
};
|
};
|
||||||
|
|
||||||
onAddQuery = (query: Partial<DataQuery>) => {
|
public onAddQuery = (query: Partial<DataQuery>) => {
|
||||||
const queries = this.getQueries();
|
const queries = this.getQueries();
|
||||||
const dsSettings = this._panelManager.state.dsSettings;
|
const dsSettings = this.state.dsSettings;
|
||||||
|
|
||||||
this.onQueriesChange(
|
this.onQueriesChange(
|
||||||
addQuery(queries, query, dsSettings ? getDataSourceRef(dsSettings) : { type: undefined, uid: undefined })
|
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;
|
return (dsSettings.meta.alerting || dsSettings.meta.mixed) === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddExpressionClick = () => {
|
public onAddExpressionClick = () => {
|
||||||
const queries = this.getQueries();
|
const queries = this.getQueries();
|
||||||
this.onQueriesChange(addQuery(queries, expressionDatasource.newQuery()));
|
this.onQueriesChange(addQuery(queries, expressionDatasource.newQuery()));
|
||||||
};
|
};
|
||||||
|
|
||||||
renderExtraActions() {
|
public renderExtraActions() {
|
||||||
return GroupActionComponents.getAllExtraRenderAction()
|
return GroupActionComponents.getAllExtraRenderAction()
|
||||||
.map((action, index) =>
|
.map((action, index) =>
|
||||||
action({
|
action({
|
||||||
@ -172,18 +303,14 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
get queryRunner(): SceneQueryRunner {
|
public get queryRunner(): SceneQueryRunner {
|
||||||
return this._panelManager.queryRunner;
|
return getQueryRunnerFor(this.state.panelRef.resolve())!;
|
||||||
}
|
|
||||||
|
|
||||||
get panelManager() {
|
|
||||||
return this._panelManager;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<PanelDataQueriesTab>) {
|
export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<PanelDataQueriesTab>) {
|
||||||
const { datasource, dsSettings } = model.panelManager.useState();
|
const { datasource, dsSettings } = model.useState();
|
||||||
const { data, queries } = model.panelManager.queryRunner.useState();
|
const { data, queries } = model.queryRunner.useState();
|
||||||
|
|
||||||
if (!datasource || !dsSettings || !data) {
|
if (!datasource || !dsSettings || !data) {
|
||||||
return null;
|
return null;
|
||||||
@ -250,7 +377,6 @@ function QueriesTab(props: QueriesTabProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab
|
<Tab
|
||||||
key={props.key}
|
|
||||||
label={model.getTabLabel()}
|
label={model.getTabLabel()}
|
||||||
icon="database"
|
icon="database"
|
||||||
counter={queryRunnerState.queries.length}
|
counter={queryRunnerState.queries.length}
|
||||||
|
@ -19,7 +19,6 @@ import { DashboardDataDTO } from 'app/types';
|
|||||||
import { transformSaveModelToScene } from '../../serialization/transformSaveModelToScene';
|
import { transformSaveModelToScene } from '../../serialization/transformSaveModelToScene';
|
||||||
import { DashboardModelCompatibilityWrapper } from '../../utils/DashboardModelCompatibilityWrapper';
|
import { DashboardModelCompatibilityWrapper } from '../../utils/DashboardModelCompatibilityWrapper';
|
||||||
import { findVizPanelByKey } from '../../utils/utils';
|
import { findVizPanelByKey } from '../../utils/utils';
|
||||||
import { VizPanelManager } from '../VizPanelManager';
|
|
||||||
import { testDashboard } from '../testfiles/testDashboard';
|
import { testDashboard } from '../testfiles/testDashboard';
|
||||||
|
|
||||||
import { PanelDataTransformationsTab, PanelDataTransformationsTabRendered } from './PanelDataTransformationsTab';
|
import { PanelDataTransformationsTab, PanelDataTransformationsTabRendered } from './PanelDataTransformationsTab';
|
||||||
@ -52,10 +51,9 @@ const mockData = {
|
|||||||
|
|
||||||
describe('PanelDataTransformationsModel', () => {
|
describe('PanelDataTransformationsModel', () => {
|
||||||
it('can change transformations', () => {
|
it('can change transformations', () => {
|
||||||
const vizPanelManager = setupVizPanelManger('panel-1');
|
const { transformsTab } = setupTabScene('panel-1');
|
||||||
const model = new PanelDataTransformationsTab(vizPanelManager);
|
transformsTab.onChangeTransformations([{ id: 'calculateField', options: {} }]);
|
||||||
model.onChangeTransformations([{ id: 'calculateField', options: {} }]);
|
expect(transformsTab.getDataTransformer().state.transformations).toEqual([{ id: 'calculateField', options: {} }]);
|
||||||
expect(model.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 scene = transformSaveModelToScene({ dashboard: testDashboard as unknown as DashboardDataDTO, meta: {} });
|
||||||
const panel = findVizPanelByKey(scene, panelId)!;
|
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
|
// The following happens on DahsboardScene activation. For the needs of this test this activation aint needed hence we hand-call it
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
getDashboardSrv().setCurrent(new DashboardModelCompatibilityWrapper(scene));
|
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 { DragDropContext, DropResult, Droppable } from '@hello-pangea/dnd';
|
||||||
import { useState } from 'react';
|
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 { 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 { Button, ButtonGroup, ConfirmModal, Tab, useStyles2 } from '@grafana/ui';
|
||||||
import { TransformationOperationRows } from 'app/features/dashboard/components/TransformationsEditor/TransformationOperationRows';
|
import { TransformationOperationRows } from 'app/features/dashboard/components/TransformationsEditor/TransformationOperationRows';
|
||||||
|
|
||||||
import { VizPanelManager } from '../VizPanelManager';
|
import { getQueryRunnerFor } from '../../utils/utils';
|
||||||
|
|
||||||
import { EmptyTransformationsMessage } from './EmptyTransformationsMessage';
|
import { EmptyTransformationsMessage } from './EmptyTransformationsMessage';
|
||||||
import { TransformationsDrawer } from './TransformationsDrawer';
|
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
|
export class PanelDataTransformationsTab
|
||||||
extends SceneObjectBase<PanelDataTransformationsTabState>
|
extends SceneObjectBase<PanelDataTransformationsTabState>
|
||||||
implements PanelDataPaneTab
|
implements PanelDataPaneTab
|
||||||
{
|
{
|
||||||
static Component = PanelDataTransformationsTabRendered;
|
static Component = PanelDataTransformationsTabRendered;
|
||||||
TabComponent: (props: PanelDataTabHeaderProps) => React.JSX.Element;
|
|
||||||
|
|
||||||
tabId = TabId.Transformations;
|
tabId = TabId.Transformations;
|
||||||
icon: IconName = 'process';
|
|
||||||
private _panelManager: VizPanelManager;
|
|
||||||
|
|
||||||
getTabLabel() {
|
getTabLabel() {
|
||||||
return 'Transformations';
|
return 'Transformations';
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(panelManager: VizPanelManager) {
|
public renderTab(props: PanelDataTabHeaderProps) {
|
||||||
super({});
|
return <TransformationsTab key={this.getTabLabel()} model={this} {...props} />;
|
||||||
this.TabComponent = (props: PanelDataTabHeaderProps) => TransformationsTab({ ...props, model: this });
|
|
||||||
|
|
||||||
this._panelManager = panelManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getQueryRunner(): SceneQueryRunner {
|
public getQueryRunner(): SceneQueryRunner {
|
||||||
return this._panelManager.queryRunner;
|
return getQueryRunnerFor(this.state.panelRef.resolve())!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDataTransformer(): SceneDataTransformer {
|
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[]) {
|
public onChangeTransformations(transformations: DataTransformerConfig[]) {
|
||||||
this._panelManager.changeTransformations(transformations);
|
const transformer = this.getDataTransformer();
|
||||||
}
|
transformer.setState({ transformations });
|
||||||
|
transformer.reprocessTransformations();
|
||||||
get panelManager() {
|
|
||||||
return this._panelManager;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,11 +206,10 @@ interface TransformationsTabProps extends PanelDataTabHeaderProps {
|
|||||||
|
|
||||||
function TransformationsTab(props: TransformationsTabProps) {
|
function TransformationsTab(props: TransformationsTabProps) {
|
||||||
const { model } = props;
|
const { model } = props;
|
||||||
|
|
||||||
const transformerState = model.getDataTransformer().useState();
|
const transformerState = model.getDataTransformer().useState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab
|
<Tab
|
||||||
key={props.key}
|
|
||||||
label={model.getTabLabel()}
|
label={model.getTabLabel()}
|
||||||
icon="process"
|
icon="process"
|
||||||
counter={transformerState.transformations.length}
|
counter={transformerState.transformations.length}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { SceneObject, SceneObjectState } from '@grafana/scenes';
|
import { SceneObject } from '@grafana/scenes';
|
||||||
|
|
||||||
export interface PanelDataPaneTabState extends SceneObjectState {}
|
|
||||||
|
|
||||||
export enum TabId {
|
export enum TabId {
|
||||||
Queries = 'queries',
|
Queries = 'queries',
|
||||||
@ -9,13 +7,12 @@ export enum TabId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PanelDataTabHeaderProps {
|
export interface PanelDataTabHeaderProps {
|
||||||
key: string;
|
|
||||||
active: boolean;
|
active: boolean;
|
||||||
onChangeTab?: (event: React.MouseEvent<HTMLElement>) => void;
|
onChangeTab?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PanelDataPaneTab extends SceneObject {
|
export interface PanelDataPaneTab extends SceneObject {
|
||||||
TabComponent: (props: PanelDataTabHeaderProps) => React.JSX.Element;
|
renderTab: (props: PanelDataTabHeaderProps) => React.JSX.Element;
|
||||||
getTabLabel(): string;
|
getTabLabel(): string;
|
||||||
tabId: TabId;
|
tabId: TabId;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { config } from '@grafana/runtime';
|
|
||||||
import { InlineSwitch } from '@grafana/ui';
|
import { InlineSwitch } from '@grafana/ui';
|
||||||
|
|
||||||
import { PanelEditor } from './PanelEditor';
|
import { PanelEditor } from './PanelEditor';
|
||||||
@ -9,19 +8,17 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function PanelEditControls({ panelEditor }: Props) {
|
export function PanelEditControls({ panelEditor }: Props) {
|
||||||
const vizManager = panelEditor.state.vizManager;
|
const { tableView, dataPane } = panelEditor.useState();
|
||||||
const { panel, tableView } = vizManager.useState();
|
|
||||||
const skipDataQuery = config.panels[panel.state.pluginId]?.skipDataQuery;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!skipDataQuery && (
|
{dataPane && (
|
||||||
<InlineSwitch
|
<InlineSwitch
|
||||||
label="Table view"
|
label="Table view"
|
||||||
showLabel={true}
|
showLabel={true}
|
||||||
id="table-view"
|
id="table-view"
|
||||||
value={tableView ? true : false}
|
value={tableView ? true : false}
|
||||||
onClick={() => vizManager.toggleTableView()}
|
onClick={panelEditor.onToggleTableView}
|
||||||
aria-label="toggle-table-view"
|
aria-label="toggle-table-view"
|
||||||
data-testid={selectors.components.PanelEditor.toggleTableView}
|
data-testid={selectors.components.PanelEditor.toggleTableView}
|
||||||
/>
|
/>
|
||||||
|
@ -1,5 +1,21 @@
|
|||||||
import { PanelPlugin, PanelPluginMeta, PluginType } from '@grafana/data';
|
import { of } from 'rxjs';
|
||||||
import { SceneGridLayout, VizPanel } from '@grafana/scenes';
|
|
||||||
|
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 * as libAPI from 'app/features/library-panels/state/api';
|
||||||
|
|
||||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||||
@ -7,14 +23,28 @@ import { DashboardScene } from '../scene/DashboardScene';
|
|||||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||||
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||||
import { activateFullSceneTree } from '../utils/test-utils';
|
import { activateFullSceneTree } from '../utils/test-utils';
|
||||||
|
import { findVizPanelByKey, getQueryRunnerFor } from '../utils/utils';
|
||||||
|
|
||||||
import { buildPanelEditScene } from './PanelEditor';
|
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.mock('@grafana/runtime', () => ({
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
|
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
|
||||||
|
return runRequestMock(ds, request);
|
||||||
|
},
|
||||||
getPluginImportUtils: () => ({
|
getPluginImportUtils: () => ({
|
||||||
getPanelPluginFromCache: jest.fn(() => pluginToLoad),
|
getPanelPluginFromCache: jest.fn(() => undefined),
|
||||||
|
importPanelPlugin: () => pluginPromise,
|
||||||
}),
|
}),
|
||||||
config: {
|
config: {
|
||||||
...jest.requireActual('@grafana/runtime').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('PanelEditor', () => {
|
||||||
describe('When closing editor', () => {
|
afterEach(() => {
|
||||||
it('should apply changes automatically', () => {
|
if (deactivate) {
|
||||||
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' });
|
|
||||||
|
|
||||||
deactivate();
|
deactivate();
|
||||||
|
deactivate = undefined;
|
||||||
const updatedPanel = gridItem.state.body as VizPanel;
|
}
|
||||||
expect(updatedPanel?.state.title).toBe('changed title');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should discard changes when unmounted and discard changes is marked as true', () => {
|
describe('When initializing', () => {
|
||||||
pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true });
|
it('should wait for panel plugin to load', async () => {
|
||||||
|
const { panelEditor, panel, pluginResolve, dashboard } = await setup({ skipWait: true });
|
||||||
|
|
||||||
const panel = new VizPanel({
|
expect(panel.state.options).toEqual({});
|
||||||
key: 'panel-1',
|
expect(panelEditor.state.isInitializing).toBe(true);
|
||||||
pluginId: 'text',
|
|
||||||
|
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);
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
const scene = new DashboardScene({
|
|
||||||
editPanel: editScene,
|
expect(panelEditor.state.isInitializing).toBe(false);
|
||||||
isEditing: true,
|
expect(panel.state.options).toEqual({ showHeader: true });
|
||||||
body: new SceneGridLayout({
|
|
||||||
children: [gridItem],
|
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();
|
const discardedPanel = findVizPanelByKey(dashboard, panel.state.key!)!;
|
||||||
deactivate();
|
|
||||||
|
|
||||||
const updatedPanel = gridItem.state.body as VizPanel;
|
expect(discardedPanel.state.title).toBe('original title');
|
||||||
expect(updatedPanel?.state.title).toBe(panel.state.title);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should discard a newly added panel', () => {
|
it('should discard a newly added panel', async () => {
|
||||||
pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true });
|
const { panelEditor, dashboard } = await setup({ isNewPanel: true });
|
||||||
|
panelEditor.onDiscard();
|
||||||
|
|
||||||
const panel = new VizPanel({
|
expect((dashboard.state.body as SceneGridLayout).state.children.length).toBe(0);
|
||||||
key: 'panel-1',
|
|
||||||
pluginId: 'text',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 queryRunner = getQueryRunnerFor(panel);
|
||||||
const scene = new DashboardScene({
|
queryRunner?.setState({ maxDataPoints: 123, queries: [{ refId: 'A' }, { refId: 'B' }] });
|
||||||
editPanel: editScene,
|
|
||||||
isEditing: true,
|
panelEditor.onDiscard();
|
||||||
body: new SceneGridLayout({
|
|
||||||
children: [gridItem],
|
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();
|
describe('When changes are made', () => {
|
||||||
const deactivate = activateFullSceneTree(scene);
|
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', () => {
|
describe('Handling library panels', () => {
|
||||||
it('should call the api with the updated panel', async () => {
|
it('should call the api with the updated panel', async () => {
|
||||||
pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true });
|
pluginPromise = Promise.resolve(getPanelPlugin({ id: 'text', skipDataQuery: true }));
|
||||||
const panel = new VizPanel({
|
|
||||||
key: 'panel-1',
|
|
||||||
pluginId: 'text',
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const panel = new VizPanel({ key: 'panel-1', pluginId: 'text' });
|
||||||
const libraryPanelModel = {
|
const libraryPanelModel = {
|
||||||
title: 'title',
|
title: 'title',
|
||||||
uid: 'uid',
|
uid: 'uid',
|
||||||
@ -143,15 +204,13 @@ describe('PanelEditor', () => {
|
|||||||
_loadedPanel: libraryPanelModel,
|
_loadedPanel: libraryPanelModel,
|
||||||
});
|
});
|
||||||
|
|
||||||
panel.setState({
|
panel.setState({ $behaviors: [libPanelBehavior] });
|
||||||
$behaviors: [libPanelBehavior],
|
|
||||||
});
|
|
||||||
|
|
||||||
const gridItem = new DashboardGridItem({ body: panel });
|
const gridItem = new DashboardGridItem({ body: panel });
|
||||||
|
|
||||||
const editScene = buildPanelEditScene(panel);
|
const editScene = buildPanelEditScene(panel);
|
||||||
const scene = new DashboardScene({
|
const scene = new DashboardScene({
|
||||||
editPanel: editScene,
|
editPanel: editScene,
|
||||||
|
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
|
||||||
isEditing: true,
|
isEditing: true,
|
||||||
body: new SceneGridLayout({
|
body: new SceneGridLayout({
|
||||||
children: [gridItem],
|
children: [gridItem],
|
||||||
@ -160,96 +219,133 @@ describe('PanelEditor', () => {
|
|||||||
|
|
||||||
activateFullSceneTree(scene);
|
activateFullSceneTree(scene);
|
||||||
|
|
||||||
editScene.state.vizManager.state.panel.setState({ title: 'changed title' });
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
(editScene.state.vizManager.state.panel.state.$behaviors![0] as LibraryPanelBehavior).setState({
|
|
||||||
name: 'changed name',
|
panel.setState({ title: 'changed title' });
|
||||||
});
|
libPanelBehavior.setState({ name: 'changed name' });
|
||||||
|
|
||||||
jest.spyOn(libAPI, 'saveLibPanel').mockImplementation(async (panel) => {
|
jest.spyOn(libAPI, 'saveLibPanel').mockImplementation(async (panel) => {
|
||||||
const updatedPanel = { ...libAPI.libraryVizPanelToSaveModel(panel), version: 2 };
|
const updatedPanel = { ...libAPI.libraryVizPanelToSaveModel(panel), version: 2 };
|
||||||
|
|
||||||
libPanelBehavior.setPanelFromLibPanel(updatedPanel);
|
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._loadedPanel?.version).toBe(2);
|
||||||
expect(libPanelBehavior.state.name).toBe('changed name');
|
expect(libPanelBehavior.state.name).toBe('changed name');
|
||||||
expect(libPanelBehavior.state.title).toBe('changed title');
|
expect(libPanelBehavior.state.title).toBe('changed title');
|
||||||
expect((gridItem.state.body as VizPanel).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', () => {
|
describe('PanelDataPane', () => {
|
||||||
it('should not exist if panel is skipDataQuery', () => {
|
it('should not exist if panel is skipDataQuery', async () => {
|
||||||
pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true });
|
const { panelEditor } = await setup({ pluginSkipDataQuery: true });
|
||||||
|
expect(panelEditor.state.dataPane).toBeUndefined();
|
||||||
const panel = new VizPanel({
|
|
||||||
key: 'panel-1',
|
|
||||||
pluginId: 'text',
|
|
||||||
});
|
|
||||||
new DashboardGridItem({
|
|
||||||
body: panel,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const editScene = buildPanelEditScene(panel);
|
it('should exist if panel is supporting querying', async () => {
|
||||||
const scene = new DashboardScene({
|
const { panelEditor } = await setup({ pluginSkipDataQuery: false });
|
||||||
editPanel: editScene,
|
expect(panelEditor.state.dataPane).toBeDefined();
|
||||||
});
|
|
||||||
|
|
||||||
activateFullSceneTree(scene);
|
|
||||||
|
|
||||||
expect(editScene.state.dataPane).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
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 {
|
interface SetupOptions {
|
||||||
const plugin = new PanelPlugin(() => null);
|
isNewPanel?: boolean;
|
||||||
plugin.meta = {
|
pluginSkipDataQuery?: boolean;
|
||||||
id: options.id!,
|
repeatByVariable?: string;
|
||||||
type: PluginType.panel,
|
skipWait?: boolean;
|
||||||
name: options.id!,
|
pluginLoadTime?: number;
|
||||||
sort: options.sort || 1,
|
}
|
||||||
info: {
|
|
||||||
author: {
|
async function setup(options: SetupOptions = {}) {
|
||||||
name: options.id + 'name',
|
const pluginToLoad = getPanelPlugin({ id: 'text', skipDataQuery: options.pluginSkipDataQuery });
|
||||||
},
|
let pluginResolve = (plugin: PanelPlugin) => {};
|
||||||
description: '',
|
|
||||||
links: [],
|
pluginPromise = new Promise<PanelPlugin>((resolve) => {
|
||||||
logos: {
|
pluginResolve = resolve;
|
||||||
large: '',
|
});
|
||||||
small: '',
|
|
||||||
},
|
const panel = new VizPanel({
|
||||||
screenshots: [],
|
key: 'panel-1',
|
||||||
updated: '',
|
pluginId: 'text',
|
||||||
version: '1.0.',
|
title: 'original title',
|
||||||
},
|
$data: new SceneDataTransformer({
|
||||||
hideFromList: options.hideFromList === true,
|
transformations: [],
|
||||||
module: options.module ?? '',
|
$data: new SceneQueryRunner({
|
||||||
baseUrl: '',
|
queries: [{ refId: 'A' }],
|
||||||
skipDataQuery: options.skipDataQuery ?? false,
|
maxDataPoints: 500,
|
||||||
};
|
datasource: { uid: 'ds1' },
|
||||||
return plugin;
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
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],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
panelEditor.debounceSaveModelDiff = false;
|
||||||
|
|
||||||
|
deactivate = activateFullSceneTree(dashboard);
|
||||||
|
|
||||||
|
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 * as H from 'history';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import { NavIndex } from '@grafana/data';
|
import { NavIndex, PanelPlugin } from '@grafana/data';
|
||||||
import { config, locationService } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
import { SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
|
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 { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
|
||||||
import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils';
|
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 { PanelDataPane } from './PanelDataPane/PanelDataPane';
|
||||||
import { PanelEditorRenderer } from './PanelEditorRenderer';
|
import { PanelEditorRenderer } from './PanelEditorRenderer';
|
||||||
import { PanelOptionsPane } from './PanelOptionsPane';
|
import { PanelOptionsPane } from './PanelOptionsPane';
|
||||||
import { VizPanelManager, VizPanelManagerState } from './VizPanelManager';
|
|
||||||
|
|
||||||
export interface PanelEditorState extends SceneObjectState {
|
export interface PanelEditorState extends SceneObjectState {
|
||||||
isNewPanel: boolean;
|
isNewPanel: boolean;
|
||||||
isDirty?: boolean;
|
isDirty?: boolean;
|
||||||
panelId: number;
|
optionsPane?: PanelOptionsPane;
|
||||||
optionsPane: PanelOptionsPane;
|
|
||||||
dataPane?: PanelDataPane;
|
dataPane?: PanelDataPane;
|
||||||
vizManager: VizPanelManager;
|
panelRef: SceneObjectRef<VizPanel>;
|
||||||
showLibraryPanelSaveModal?: boolean;
|
showLibraryPanelSaveModal?: boolean;
|
||||||
showLibraryPanelUnlinkModal?: boolean;
|
showLibraryPanelUnlinkModal?: boolean;
|
||||||
|
tableView?: VizPanel;
|
||||||
|
pluginLoadErrror?: string;
|
||||||
|
/**
|
||||||
|
* Waiting for library panel or panel plugin to load
|
||||||
|
*/
|
||||||
|
isInitializing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||||
private _initialRepeatOptions: Pick<VizPanelManagerState, 'repeat' | 'repeatDirection' | 'maxPerRow'> = {};
|
|
||||||
static Component = PanelEditorRenderer;
|
static Component = PanelEditorRenderer;
|
||||||
|
|
||||||
private _discardChanges = false;
|
private _originalLayoutElementState!: DashboardGridItemState;
|
||||||
|
private _layoutElement!: DashboardGridItem;
|
||||||
|
private _originalSaveModel!: Panel;
|
||||||
|
|
||||||
public constructor(state: PanelEditorState) {
|
public constructor(state: PanelEditorState) {
|
||||||
super(state);
|
super(state);
|
||||||
|
|
||||||
const { repeat, repeatDirection, maxPerRow } = state.vizManager.state;
|
this.setOriginalState(this.state.panelRef);
|
||||||
this._initialRepeatOptions = {
|
|
||||||
repeat,
|
|
||||||
repeatDirection,
|
|
||||||
maxPerRow,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.addActivationHandler(this._activationHandler.bind(this));
|
this.addActivationHandler(this._activationHandler.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _activationHandler() {
|
private _activationHandler() {
|
||||||
const panelManager = this.state.vizManager;
|
const panel = this.state.panelRef.resolve();
|
||||||
const panel = panelManager.state.panel;
|
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(
|
this._subs.add(
|
||||||
panelManager.subscribeToState((n, p) => {
|
panel.subscribeToState((n, p) => {
|
||||||
if (n.pluginId !== p.pluginId) {
|
if (n.pluginId !== p.pluginId) {
|
||||||
this._initDataPane(n.pluginId);
|
this.waitForPlugin();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this._initDataPane(panel.state.pluginId);
|
// Setup options pane
|
||||||
|
this.setState({
|
||||||
return () => {
|
optionsPane: new PanelOptionsPane({
|
||||||
if (!this._discardChanges) {
|
panelRef: this.state.panelRef,
|
||||||
this.commitChanges();
|
searchQuery: '',
|
||||||
} else if (this.state.isNewPanel) {
|
listMode: OptionFilter.All,
|
||||||
getDashboardSceneFor(this).removePanel(panelManager.state.sourcePanel.resolve()!);
|
}),
|
||||||
|
isInitializing: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// plugin changed after first time initialization
|
||||||
|
// Just update data pane
|
||||||
|
this._updateDataPane(plugin);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _initDataPane(pluginId: string) {
|
private _updateDataPane(plugin: PanelPlugin) {
|
||||||
const skipDataQuery = config.panels[pluginId]?.skipDataQuery;
|
const skipDataQuery = plugin.meta.skipDataQuery;
|
||||||
|
|
||||||
if (skipDataQuery && this.state.dataPane) {
|
if (skipDataQuery && this.state.dataPane) {
|
||||||
locationService.partial({ tab: null }, true);
|
locationService.partial({ tab: null }, true);
|
||||||
@ -74,12 +188,16 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!skipDataQuery && !this.state.dataPane) {
|
if (!skipDataQuery && !this.state.dataPane) {
|
||||||
this.setState({ dataPane: new PanelDataPane(this.state.vizManager) });
|
this.setState({ dataPane: PanelDataPane.createFor(this.getPanel()) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUrlKey() {
|
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) {
|
public getPageNav(location: H.Location, navIndex: NavIndex) {
|
||||||
@ -92,53 +210,23 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onDiscard = () => {
|
public onDiscard = () => {
|
||||||
this.state.vizManager.setState({ isDirty: false });
|
this.setState({ isDirty: false });
|
||||||
this._discardChanges = true;
|
|
||||||
|
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 });
|
locationService.partial({ editPanel: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
public commitChanges() {
|
public dashboardSaved() {
|
||||||
const dashboard = getDashboardSceneFor(this);
|
this.setOriginalState(this.state.panelRef);
|
||||||
|
this.setState({ isDirty: false });
|
||||||
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 onSaveLibraryPanel = () => {
|
public onSaveLibraryPanel = () => {
|
||||||
@ -146,8 +234,8 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onConfirmSaveLibraryPanel = () => {
|
public onConfirmSaveLibraryPanel = () => {
|
||||||
this.state.vizManager.commitChanges();
|
saveLibPanel(this.state.panelRef.resolve());
|
||||||
this.state.vizManager.setState({ isDirty: false });
|
this.setState({ isDirty: false });
|
||||||
locationService.partial({ editPanel: null });
|
locationService.partial({ editPanel: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -164,16 +252,43 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onConfirmUnlinkLibraryPanel = () => {
|
public onConfirmUnlinkLibraryPanel = () => {
|
||||||
this.state.vizManager.unlinkLibraryPanel();
|
const libPanelBehavior = getLibraryPanelBehavior(this.getPanel());
|
||||||
|
if (!libPanelBehavior) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
libPanelBehavior.unlink();
|
||||||
|
|
||||||
this.setState({ showLibraryPanelUnlinkModal: false });
|
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 {
|
export function buildPanelEditScene(panel: VizPanel, isNewPanel = false): PanelEditor {
|
||||||
return new PanelEditor({
|
return new PanelEditor({
|
||||||
panelId: getPanelIdForVizPanel(panel),
|
isInitializing: true,
|
||||||
optionsPane: new PanelOptionsPane({}),
|
panelRef: panel.getRef(),
|
||||||
vizManager: VizPanelManager.createFor(panel),
|
|
||||||
isNewPanel,
|
isNewPanel,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@ import { css, cx } from '@emotion/css';
|
|||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { SceneComponentProps } from '@grafana/scenes';
|
import { SceneComponentProps, VizPanel } from '@grafana/scenes';
|
||||||
import { Button, ToolbarButton, useStyles2 } from '@grafana/ui';
|
import { Button, Spinner, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||||
import { UnlinkModal } from '../scene/UnlinkModal';
|
import { UnlinkModal } from '../scene/UnlinkModal';
|
||||||
@ -54,7 +54,8 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!splitterState.collapsed && <optionsPane.Component model={optionsPane} />}
|
{!splitterState.collapsed && optionsPane && <optionsPane.Component model={optionsPane} />}
|
||||||
|
{!splitterState.collapsed && !optionsPane && <Spinner />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -63,9 +64,9 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
|||||||
|
|
||||||
function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
|
function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
|
||||||
const dashboard = getDashboardSceneFor(model);
|
const dashboard = getDashboardSceneFor(model);
|
||||||
const { vizManager, dataPane, showLibraryPanelSaveModal, showLibraryPanelUnlinkModal } = model.useState();
|
const { dataPane, showLibraryPanelSaveModal, showLibraryPanelUnlinkModal, tableView } = model.useState();
|
||||||
const { sourcePanel } = vizManager.useState();
|
const panel = model.getPanel();
|
||||||
const libraryPanel = getLibraryPanelBehavior(sourcePanel.resolve());
|
const libraryPanel = getLibraryPanelBehavior(panel);
|
||||||
const { controls } = dashboard.useState();
|
const { controls } = dashboard.useState();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
@ -94,7 +95,7 @@ function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
|
|||||||
)}
|
)}
|
||||||
<div {...containerProps}>
|
<div {...containerProps}>
|
||||||
<div {...primaryProps}>
|
<div {...primaryProps}>
|
||||||
<vizManager.Component model={vizManager} />
|
<VizWrapper panel={panel} tableView={tableView} />
|
||||||
</div>
|
</div>
|
||||||
{showLibraryPanelSaveModal && libraryPanel && (
|
{showLibraryPanelSaveModal && libraryPanel && (
|
||||||
<SaveLibraryVizPanelModal
|
<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) {
|
function getStyles(theme: GrafanaTheme2) {
|
||||||
return {
|
return {
|
||||||
pageContainer: css({
|
pageContainer: css({
|
||||||
@ -215,5 +232,10 @@ function getStyles(theme: GrafanaTheme2) {
|
|||||||
rotate: '-90deg',
|
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 * as utils from '../utils/utils';
|
||||||
|
|
||||||
import { PanelOptions } from './PanelOptions';
|
import { PanelOptions } from './PanelOptions';
|
||||||
import { VizPanelManager } from './VizPanelManager';
|
import { PanelOptionsPane } from './PanelOptionsPane';
|
||||||
|
|
||||||
const OptionsPaneSelector = selectors.components.PanelEditor.OptionsPane;
|
const OptionsPaneSelector = selectors.components.PanelEditor.OptionsPane;
|
||||||
|
|
||||||
@ -92,43 +92,47 @@ function setup(options: SetupOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// need to wait for plugin to load
|
// 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);
|
activateFullSceneTree(panelOptionsScene);
|
||||||
|
panel.activate();
|
||||||
const panelOptions = <PanelOptions vizManager={vizManager} searchQuery="" listMode={OptionFilter.All}></PanelOptions>;
|
|
||||||
|
|
||||||
|
const panelOptions = <PanelOptions panel={panel} searchQuery="" listMode={OptionFilter.All}></PanelOptions>;
|
||||||
const renderResult = render(panelOptions);
|
const renderResult = render(panelOptions);
|
||||||
|
|
||||||
return { renderResult, vizManager };
|
return { renderResult, panelOptionsScene, panel };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('PanelOptions', () => {
|
describe('PanelOptions', () => {
|
||||||
describe('Can render and edit panel frame options', () => {
|
describe('Can render and edit panel frame options', () => {
|
||||||
it('Can edit title', async () => {
|
it('Can edit title', async () => {
|
||||||
const { vizManager } = setup();
|
const { panel } = setup();
|
||||||
|
|
||||||
expect(screen.getByLabelText(OptionsPaneSelector.fieldLabel('Panel options Title'))).toBeInTheDocument();
|
expect(screen.getByLabelText(OptionsPaneSelector.fieldLabel('Panel options Title'))).toBeInTheDocument();
|
||||||
|
|
||||||
const input = screen.getByTestId(selectors.components.PanelEditor.OptionsPane.fieldInput('Title'));
|
const input = screen.getByTestId(selectors.components.PanelEditor.OptionsPane.fieldInput('Title'));
|
||||||
fireEvent.change(input, { target: { value: 'New 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 () => {
|
it('Clearing title should set hoverHeader to true', async () => {
|
||||||
const { vizManager } = setup();
|
const { panel } = setup();
|
||||||
|
|
||||||
expect(screen.getByLabelText(OptionsPaneSelector.fieldLabel('Panel options Title'))).toBeInTheDocument();
|
expect(screen.getByLabelText(OptionsPaneSelector.fieldLabel('Panel options Title'))).toBeInTheDocument();
|
||||||
|
|
||||||
const input = screen.getByTestId(selectors.components.PanelEditor.OptionsPane.fieldInput('Title'));
|
const input = screen.getByTestId(selectors.components.PanelEditor.OptionsPane.fieldInput('Title'));
|
||||||
fireEvent.change(input, { target: { value: '' } });
|
fireEvent.change(input, { target: { value: '' } });
|
||||||
|
|
||||||
expect(vizManager.state.panel.state.title).toBe('');
|
expect(panel.state.title).toBe('');
|
||||||
expect(vizManager.state.panel.state.hoverHeader).toBe(true);
|
expect(panel.state.hoverHeader).toBe(true);
|
||||||
|
|
||||||
fireEvent.change(input, { target: { value: 'Muu' } });
|
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,
|
_loadedPanel: libraryPanelModel,
|
||||||
});
|
});
|
||||||
|
|
||||||
panel.setState({
|
panel.setState({ $behaviors: [libraryPanel] });
|
||||||
$behaviors: [libraryPanel],
|
|
||||||
});
|
|
||||||
|
|
||||||
new DashboardGridItem({ body: panel });
|
new DashboardGridItem({ body: panel });
|
||||||
|
|
||||||
const { renderResult, vizManager } = setup({ panel: panel });
|
const { renderResult } = setup({ panel: panel });
|
||||||
|
|
||||||
const input = await renderResult.findByTestId('library panel name input');
|
const input = await renderResult.findByTestId('library panel name input');
|
||||||
|
|
||||||
@ -193,8 +195,6 @@ describe('PanelOptions', () => {
|
|||||||
fireEvent.blur(input, { target: { value: 'new library panel name' } });
|
fireEvent.blur(input, { target: { value: 'new library panel name' } });
|
||||||
});
|
});
|
||||||
|
|
||||||
expect((vizManager.state.panel.state.$behaviors![0] as LibraryPanelBehavior).state.name).toBe(
|
expect((panel.state.$behaviors![0] as LibraryPanelBehavior).state.name).toBe('new library panel name');
|
||||||
'new library panel name'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -13,24 +13,23 @@ import {
|
|||||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||||
import { getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils';
|
import { getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils';
|
||||||
|
|
||||||
import { VizPanelManager } from './VizPanelManager';
|
|
||||||
import { getPanelFrameCategory2 } from './getPanelFrameOptions';
|
import { getPanelFrameCategory2 } from './getPanelFrameOptions';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
vizManager: VizPanelManager;
|
panel: VizPanel;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
listMode: OptionFilter;
|
listMode: OptionFilter;
|
||||||
data?: PanelData;
|
data?: PanelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PanelOptions = React.memo<Props>(({ vizManager, searchQuery, listMode, data }) => {
|
export const PanelOptions = React.memo<Props>(({ panel, searchQuery, listMode, data }) => {
|
||||||
const { panel, repeat } = vizManager.useState();
|
|
||||||
const { options, fieldConfig, _pluginInstanceState } = panel.useState();
|
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(
|
const panelFrameOptions = useMemo(
|
||||||
() => getPanelFrameCategory2(vizManager, panel, repeat),
|
() => getPanelFrameCategory2(panel, layoutElementState),
|
||||||
[vizManager, panel, repeat]
|
[panel, layoutElementState]
|
||||||
);
|
);
|
||||||
|
|
||||||
const visualizationOptions = useMemo(() => {
|
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 { css } from '@emotion/css';
|
||||||
import { useMemo } from 'react';
|
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 { 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 { FilterInput, Stack, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||||
import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
|
import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
|
||||||
import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPluginError';
|
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 { getAllPanelPluginMeta } from 'app/features/panel/state/util';
|
||||||
|
|
||||||
import { PanelEditor } from './PanelEditor';
|
|
||||||
import { PanelOptions } from './PanelOptions';
|
import { PanelOptions } from './PanelOptions';
|
||||||
import { PanelVizTypePicker } from './PanelVizTypePicker';
|
import { PanelVizTypePicker } from './PanelVizTypePicker';
|
||||||
|
|
||||||
@ -17,21 +32,48 @@ export interface PanelOptionsPaneState extends SceneObjectState {
|
|||||||
isVizPickerOpen?: boolean;
|
isVizPickerOpen?: boolean;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
listMode: OptionFilter;
|
listMode: OptionFilter;
|
||||||
|
panelRef: SceneObjectRef<VizPanel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginOptionsCache {
|
||||||
|
options: DeepPartial<{}>;
|
||||||
|
fieldConfig: FieldConfigSource<DeepPartial<{}>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
||||||
public constructor(state: Partial<PanelOptionsPaneState>) {
|
private _cachedPluginOptions: Record<string, PluginOptionsCache | undefined> = {};
|
||||||
super({
|
|
||||||
searchQuery: '',
|
|
||||||
listMode: OptionFilter.All,
|
|
||||||
...state,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleVizPicker = () => {
|
onToggleVizPicker = () => {
|
||||||
this.setState({ isVizPickerOpen: !this.state.isVizPickerOpen });
|
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) => {
|
onSetSearchQuery = (searchQuery: string) => {
|
||||||
this.setState({ searchQuery });
|
this.setState({ searchQuery });
|
||||||
};
|
};
|
||||||
@ -41,10 +83,10 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static Component = ({ model }: SceneComponentProps<PanelOptionsPane>) => {
|
static Component = ({ model }: SceneComponentProps<PanelOptionsPane>) => {
|
||||||
const { isVizPickerOpen, searchQuery, listMode } = model.useState();
|
const { isVizPickerOpen, searchQuery, listMode, panelRef } = model.useState();
|
||||||
const vizManager = sceneGraph.getAncestor(model, PanelEditor).state.vizManager;
|
const panel = panelRef.resolve();
|
||||||
const { pluginId } = vizManager.useState();
|
const { pluginId } = panel.useState();
|
||||||
const { data } = sceneGraph.getData(vizManager.state.panel).useState();
|
const { data } = sceneGraph.getData(panel).useState();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -61,12 +103,17 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.listOfOptions}>
|
<div className={styles.listOfOptions}>
|
||||||
<PanelOptions vizManager={vizManager} searchQuery={searchQuery} listMode={listMode} data={data} />
|
<PanelOptions panel={panel} searchQuery={searchQuery} listMode={listMode} data={data} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isVizPickerOpen && (
|
{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 { GrafanaTheme2, PanelData, SelectableValue } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { VizPanel } from '@grafana/scenes';
|
||||||
import { Button, CustomScrollbar, Field, FilterInput, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
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 { LS_VISUALIZATION_SELECT_TAB_KEY, LS_WIDGET_SELECT_TAB_KEY } from 'app/core/constants';
|
||||||
import { VisualizationSelectPaneTab } from 'app/features/dashboard/components/PanelEditor/types';
|
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 { PanelModelCompatibilityWrapper } from '../utils/PanelModelCompatibilityWrapper';
|
||||||
|
|
||||||
import { VizPanelManager } from './VizPanelManager';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
data?: PanelData;
|
data?: PanelData;
|
||||||
vizManager: VizPanelManager;
|
panel: VizPanel;
|
||||||
onChange: () => void;
|
onChange: (options: VizTypeChangeDetails) => void;
|
||||||
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PanelVizTypePicker({ vizManager, data, onChange }: Props) {
|
export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) {
|
||||||
const { panel } = vizManager.useState();
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
@ -50,22 +49,8 @@ export function PanelVizTypePicker({ vizManager, data, onChange }: Props) {
|
|||||||
const radioOptions: Array<SelectableValue<VisualizationSelectPaneTab>> = [
|
const radioOptions: Array<SelectableValue<VisualizationSelectPaneTab>> = [
|
||||||
{ label: 'Visualizations', value: VisualizationSelectPaneTab.Visualizations },
|
{ label: 'Visualizations', value: VisualizationSelectPaneTab.Visualizations },
|
||||||
{ label: 'Suggestions', value: VisualizationSelectPaneTab.Suggestions },
|
{ 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 (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<div className={styles.searchRow}>
|
<div className={styles.searchRow}>
|
||||||
@ -82,7 +67,7 @@ export function PanelVizTypePicker({ vizManager, data, onChange }: Props) {
|
|||||||
icon="angle-up"
|
icon="angle-up"
|
||||||
className={styles.closeButton}
|
className={styles.closeButton}
|
||||||
data-testid={selectors.components.PanelEditor.toggleVizPicker}
|
data-testid={selectors.components.PanelEditor.toggleVizPicker}
|
||||||
onClick={onCloseVizPicker}
|
onClick={onClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Field className={styles.customFieldMargin}>
|
<Field className={styles.customFieldMargin}>
|
||||||
@ -90,18 +75,10 @@ export function PanelVizTypePicker({ vizManager, data, onChange }: Props) {
|
|||||||
</Field>
|
</Field>
|
||||||
<CustomScrollbar>
|
<CustomScrollbar>
|
||||||
{listMode === VisualizationSelectPaneTab.Visualizations && (
|
{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 && (
|
{listMode === VisualizationSelectPaneTab.Suggestions && (
|
||||||
<VisualizationSuggestions
|
<VisualizationSuggestions onChange={onChange} searchQuery={searchQuery} panel={panelModel} data={data} />
|
||||||
onChange={onVizTypeChange}
|
|
||||||
searchQuery={searchQuery}
|
|
||||||
panel={panelModel}
|
|
||||||
data={data}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
</div>
|
</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 { SelectableValue } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { VizPanel } from '@grafana/scenes';
|
import { SceneObjectState, VizPanel } from '@grafana/scenes';
|
||||||
import { RadioButtonGroup, Select, DataLinksInlineEditor, Input, TextArea, Switch } from '@grafana/ui';
|
import { DataLinksInlineEditor, Input, TextArea, Switch, RadioButtonGroup, Select } from '@grafana/ui';
|
||||||
import { GenAIPanelDescriptionButton } from 'app/features/dashboard/components/GenAI/GenAIPanelDescriptionButton';
|
import { GenAIPanelDescriptionButton } from 'app/features/dashboard/components/GenAI/GenAIPanelDescriptionButton';
|
||||||
import { GenAIPanelTitleButton } from 'app/features/dashboard/components/GenAI/GenAIPanelTitleButton';
|
import { GenAIPanelTitleButton } from 'app/features/dashboard/components/GenAI/GenAIPanelTitleButton';
|
||||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
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 { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect';
|
||||||
import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||||
|
|
||||||
|
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||||
import { VizPanelLinks } from '../scene/PanelLinks';
|
import { VizPanelLinks } from '../scene/PanelLinks';
|
||||||
import { vizPanelToPanel, transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
|
import { vizPanelToPanel, transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
|
||||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||||
import { getDashboardSceneFor } from '../utils/utils';
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
|
|
||||||
import { VizPanelManager, VizPanelManagerState } from './VizPanelManager';
|
|
||||||
|
|
||||||
export function getPanelFrameCategory2(
|
export function getPanelFrameCategory2(
|
||||||
vizManager: VizPanelManager,
|
|
||||||
panel: VizPanel,
|
panel: VizPanel,
|
||||||
repeat?: string
|
layoutElementState: SceneObjectState
|
||||||
): OptionsPaneCategoryDescriptor {
|
): OptionsPaneCategoryDescriptor {
|
||||||
const descriptor = new OptionsPaneCategoryDescriptor({
|
const descriptor = new OptionsPaneCategoryDescriptor({
|
||||||
title: 'Panel options',
|
title: 'Panel options',
|
||||||
@ -31,19 +29,20 @@ export function getPanelFrameCategory2(
|
|||||||
const panelLinksObject = dashboardSceneGraph.getPanelLinks(panel);
|
const panelLinksObject = dashboardSceneGraph.getPanelLinks(panel);
|
||||||
const links = panelLinksObject?.state.rawLinks ?? [];
|
const links = panelLinksObject?.state.rawLinks ?? [];
|
||||||
const dashboard = getDashboardSceneFor(panel);
|
const dashboard = getDashboardSceneFor(panel);
|
||||||
|
const layoutElement = panel.parent;
|
||||||
|
|
||||||
return descriptor
|
descriptor
|
||||||
.addItem(
|
.addItem(
|
||||||
new OptionsPaneItemDescriptor({
|
new OptionsPaneItemDescriptor({
|
||||||
title: 'Title',
|
title: 'Title',
|
||||||
value: panel.state.title,
|
value: panel.state.title,
|
||||||
popularRank: 1,
|
popularRank: 1,
|
||||||
render: function renderTitle() {
|
render: function renderTitle() {
|
||||||
return <PanelFrameTitle vizManager={vizManager} />;
|
return <PanelFrameTitle panel={panel} />;
|
||||||
},
|
},
|
||||||
addon: config.featureToggles.dashgpt && (
|
addon: config.featureToggles.dashgpt && (
|
||||||
<GenAIPanelTitleButton
|
<GenAIPanelTitleButton
|
||||||
onGenerate={(title) => vizManager.setPanelTitle(title)}
|
onGenerate={(title) => setPanelTitle(panel, title)}
|
||||||
panel={vizPanelToPanel(panel)}
|
panel={vizPanelToPanel(panel)}
|
||||||
dashboard={transformSceneToSaveModel(dashboard)}
|
dashboard={transformSceneToSaveModel(dashboard)}
|
||||||
/>
|
/>
|
||||||
@ -95,14 +94,18 @@ export function getPanelFrameCategory2(
|
|||||||
render: () => <ScenePanelLinksEditor panelLinks={panelLinksObject ?? undefined} />,
|
render: () => <ScenePanelLinksEditor panelLinks={panelLinksObject ?? undefined} />,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
.addCategory(
|
|
||||||
new OptionsPaneCategoryDescriptor({
|
if (layoutElement instanceof DashboardGridItem) {
|
||||||
|
const gridItem = layoutElement;
|
||||||
|
|
||||||
|
const category = new OptionsPaneCategoryDescriptor({
|
||||||
title: 'Repeat options',
|
title: 'Repeat options',
|
||||||
id: 'Repeat options',
|
id: 'Repeat options',
|
||||||
isOpenDefault: false,
|
isOpenDefault: false,
|
||||||
})
|
});
|
||||||
.addItem(
|
|
||||||
|
category.addItem(
|
||||||
new OptionsPaneItemDescriptor({
|
new OptionsPaneItemDescriptor({
|
||||||
title: 'Repeat by variable',
|
title: 'Repeat by variable',
|
||||||
description:
|
description:
|
||||||
@ -111,24 +114,19 @@ export function getPanelFrameCategory2(
|
|||||||
return (
|
return (
|
||||||
<RepeatRowSelect2
|
<RepeatRowSelect2
|
||||||
id="repeat-by-variable-select"
|
id="repeat-by-variable-select"
|
||||||
parent={panel}
|
sceneContext={panel}
|
||||||
repeat={repeat}
|
repeat={gridItem.state.variableName}
|
||||||
onChange={(value?: string) => {
|
onChange={(value?: string) => gridItem.setRepeatByVariable(value)}
|
||||||
const stateUpdate: Partial<VizPanelManagerState> = { repeat: value };
|
|
||||||
if (value && !vizManager.state.repeatDirection) {
|
|
||||||
stateUpdate.repeatDirection = 'h';
|
|
||||||
}
|
|
||||||
vizManager.setState(stateUpdate);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
.addItem(
|
|
||||||
|
category.addItem(
|
||||||
new OptionsPaneItemDescriptor({
|
new OptionsPaneItemDescriptor({
|
||||||
title: 'Repeat direction',
|
title: 'Repeat direction',
|
||||||
showIf: () => !!vizManager.state.repeat,
|
showIf: () => Boolean(gridItem.state.variableName),
|
||||||
render: function renderRepeatOptions() {
|
render: function renderRepeatOptions() {
|
||||||
const directionOptions: Array<SelectableValue<'h' | 'v'>> = [
|
const directionOptions: Array<SelectableValue<'h' | 'v'>> = [
|
||||||
{ label: 'Horizontal', value: 'h' },
|
{ label: 'Horizontal', value: 'h' },
|
||||||
@ -138,30 +136,35 @@ export function getPanelFrameCategory2(
|
|||||||
return (
|
return (
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
options={directionOptions}
|
options={directionOptions}
|
||||||
value={vizManager.state.repeatDirection ?? 'h'}
|
value={gridItem.state.repeatDirection ?? 'h'}
|
||||||
onChange={(value) => vizManager.setState({ repeatDirection: value })}
|
onChange={(value) => gridItem.setState({ repeatDirection: value })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
.addItem(
|
|
||||||
|
category.addItem(
|
||||||
new OptionsPaneItemDescriptor({
|
new OptionsPaneItemDescriptor({
|
||||||
title: 'Max per row',
|
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() {
|
render: function renderOption() {
|
||||||
const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value }));
|
const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value }));
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
options={maxPerRowOptions}
|
options={maxPerRowOptions}
|
||||||
value={vizManager.state.maxPerRow}
|
value={gridItem.state.maxPerRow ?? 4}
|
||||||
onChange={(value) => vizManager.setState({ maxPerRow: value.value })}
|
onChange={(value) => gridItem.setState({ maxPerRow: value.value })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
descriptor.addCategory(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ScenePanelLinksEditorProps {
|
interface ScenePanelLinksEditorProps {
|
||||||
@ -181,14 +184,14 @@ function ScenePanelLinksEditor({ panelLinks }: ScenePanelLinksEditorProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PanelFrameTitle({ vizManager }: { vizManager: VizPanelManager }) {
|
function PanelFrameTitle({ panel }: { panel: VizPanel }) {
|
||||||
const { title } = vizManager.state.panel.useState();
|
const { title } = panel.useState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
data-testid={selectors.components.PanelEditor.OptionsPane.fieldInput('Title')}
|
data-testid={selectors.components.PanelEditor.OptionsPane.fieldInput('Title')}
|
||||||
value={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]);
|
}, [dashboard]);
|
||||||
|
|
||||||
const onHistoryBlock = (location: H.Location) => {
|
const onHistoryBlock = (location: H.Location) => {
|
||||||
const panelInEdit = dashboard.state.editPanel;
|
const panelEditor = dashboard.state.editPanel;
|
||||||
const vizPanelManager = panelInEdit?.state.vizManager;
|
const vizPanel = panelEditor?.getPanel();
|
||||||
const vizPanel = vizPanelManager?.state.panel;
|
|
||||||
const search = new URLSearchParams(location.search);
|
const search = new URLSearchParams(location.search);
|
||||||
|
|
||||||
// Are we leaving panel edit & library panel?
|
// Are we leaving panel edit & library panel?
|
||||||
if (
|
if (panelEditor && vizPanel && isLibraryPanel(vizPanel) && panelEditor.state.isDirty && !search.has('editPanel')) {
|
||||||
panelInEdit &&
|
|
||||||
vizPanel &&
|
|
||||||
isLibraryPanel(vizPanel) &&
|
|
||||||
vizPanelManager.state.isDirty &&
|
|
||||||
!search.has('editPanel')
|
|
||||||
) {
|
|
||||||
const libPanelBehavior = getLibraryPanelBehavior(vizPanel);
|
const libPanelBehavior = getLibraryPanelBehavior(vizPanel);
|
||||||
|
|
||||||
showModal(SaveLibraryVizPanelModal, {
|
showModal(SaveLibraryVizPanelModal, {
|
||||||
@ -60,12 +53,12 @@ export const DashboardPrompt = memo(({ dashboard }: DashboardPromptProps) => {
|
|||||||
isUnsavedPrompt: true,
|
isUnsavedPrompt: true,
|
||||||
libraryPanel: libPanelBehavior!,
|
libraryPanel: libPanelBehavior!,
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
panelInEdit.onConfirmSaveLibraryPanel();
|
panelEditor.onConfirmSaveLibraryPanel();
|
||||||
hideModal();
|
hideModal();
|
||||||
moveToBlockedLocationAfterReactStateUpdate(location);
|
moveToBlockedLocationAfterReactStateUpdate(location);
|
||||||
},
|
},
|
||||||
onDiscard: () => {
|
onDiscard: () => {
|
||||||
panelInEdit.onDiscard();
|
panelEditor.onDiscard();
|
||||||
hideModal();
|
hideModal();
|
||||||
moveToBlockedLocationAfterReactStateUpdate(location);
|
moveToBlockedLocationAfterReactStateUpdate(location);
|
||||||
},
|
},
|
||||||
|
@ -14,7 +14,6 @@ import {
|
|||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { createWorker } from 'app/features/dashboard-scene/saving/createDetectChangesWorker';
|
import { createWorker } from 'app/features/dashboard-scene/saving/createDetectChangesWorker';
|
||||||
|
|
||||||
import { VizPanelManager } from '../panel-edit/VizPanelManager';
|
|
||||||
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
||||||
import { DashboardControls } from '../scene/DashboardControls';
|
import { DashboardControls } from '../scene/DashboardControls';
|
||||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||||
@ -43,7 +42,6 @@ export class DashboardSceneChangeTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Any change in the panel should trigger a change detection
|
// 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
|
// The PanelTimeRange includes the overrides configuration
|
||||||
if (
|
if (
|
||||||
payload.changedObject instanceof VizPanel ||
|
payload.changedObject instanceof VizPanel ||
|
||||||
@ -52,16 +50,6 @@ export class DashboardSceneChangeTracker {
|
|||||||
) {
|
) {
|
||||||
return true;
|
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
|
// SceneQueryRunner includes the DS configuration
|
||||||
if (payload.changedObject instanceof SceneQueryRunner) {
|
if (payload.changedObject instanceof SceneQueryRunner) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'data')) {
|
if (!Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'data')) {
|
||||||
|
@ -237,8 +237,7 @@ describe('getDashboardChangesFromScene', () => {
|
|||||||
dashboard.onEnterEditMode();
|
dashboard.onEnterEditMode();
|
||||||
dashboard.setState({ editPanel: editScene });
|
dashboard.setState({ editPanel: editScene });
|
||||||
|
|
||||||
editScene.state.vizManager.state.panel.setState({ title: 'changed title' });
|
editScene.state.panelRef.resolve().setState({ title: 'changed title' });
|
||||||
editScene.commitChanges();
|
|
||||||
|
|
||||||
const result = getDashboardChangesFromScene(dashboard, false, true);
|
const result = getDashboardChangesFromScene(dashboard, false, true);
|
||||||
const panelSaveModel = result.changedSaveModel.panels![0];
|
const panelSaveModel = result.changedSaveModel.panels![0];
|
||||||
|
@ -11,11 +11,10 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||||
import { setPluginImportUtils } from '@grafana/runtime';
|
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 { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||||
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
||||||
|
|
||||||
import { VizPanelManager } from '../panel-edit/VizPanelManager';
|
|
||||||
import { activateFullSceneTree } from '../utils/test-utils';
|
import { activateFullSceneTree } from '../utils/test-utils';
|
||||||
|
|
||||||
import { DashboardDatasourceBehaviour } from './DashboardDatasourceBehaviour';
|
import { DashboardDatasourceBehaviour } from './DashboardDatasourceBehaviour';
|
||||||
@ -275,14 +274,12 @@ describe('DashboardDatasourceBehaviour', () => {
|
|||||||
// spy on runQueries
|
// spy on runQueries
|
||||||
const spy = jest.spyOn(dashboardDSPanel.state.$data!.state.$data as SceneQueryRunner, 'runQueries');
|
const spy = jest.spyOn(dashboardDSPanel.state.$data!.state.$data as SceneQueryRunner, 'runQueries');
|
||||||
|
|
||||||
const vizPanelManager = new VizPanelManager({
|
const scene = new SceneFlexLayout({
|
||||||
panel: dashboardDSPanel.clone(),
|
|
||||||
$data: dashboardDSPanel.state.$data?.clone(),
|
$data: dashboardDSPanel.state.$data?.clone(),
|
||||||
sourcePanel: dashboardDSPanel.getRef(),
|
children: [],
|
||||||
pluginId: dashboardDSPanel.state.pluginId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
vizPanelManager.activate();
|
scene.activate();
|
||||||
|
|
||||||
expect(spy).not.toHaveBeenCalled();
|
expect(spy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
SceneVariable,
|
SceneVariable,
|
||||||
SceneVariableDependencyConfigLike,
|
SceneVariableDependencyConfigLike,
|
||||||
} from '@grafana/scenes';
|
} 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';
|
import { getMultiVariableValues, getQueryRunnerFor } from '../utils/utils';
|
||||||
|
|
||||||
@ -41,7 +41,8 @@ export type RepeatDirection = 'v' | 'h';
|
|||||||
|
|
||||||
export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> implements SceneGridItemLike {
|
export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> implements SceneGridItemLike {
|
||||||
private _prevRepeatValues?: VariableValueSingle[];
|
private _prevRepeatValues?: VariableValueSingle[];
|
||||||
private _oldBody?: VizPanel;
|
private _prevPanelState: VizPanelState | undefined;
|
||||||
|
private _prevGridItemState: DashboardGridItemState | undefined;
|
||||||
|
|
||||||
protected _variableDependency = new DashboardGridItemVariableDependencyHandler(this);
|
protected _variableDependency = new DashboardGridItemVariableDependencyHandler(this);
|
||||||
|
|
||||||
@ -54,11 +55,14 @@ export class DashboardGridItem extends SceneObjectBase<DashboardGridItemState> i
|
|||||||
private _activationHandler() {
|
private _activationHandler() {
|
||||||
if (this.state.variableName) {
|
if (this.state.variableName) {
|
||||||
this._subs.add(this.subscribeToState((newState, prevState) => this._handleGridResize(newState, prevState)));
|
this._subs.add(this.subscribeToState((newState, prevState) => this._handleGridResize(newState, prevState)));
|
||||||
if (this._oldBody !== this.state.body) {
|
this.clearCachedStateIfBodyOrOptionsChanged();
|
||||||
this._prevRepeatValues = undefined;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._oldBody = this.state.body;
|
|
||||||
this._prevRepeatValues = values;
|
|
||||||
|
|
||||||
const panelToRepeat = this.state.body;
|
const panelToRepeat = this.state.body;
|
||||||
const repeatedPanels: VizPanel[] = [];
|
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
|
// Used from dashboard url sync
|
||||||
this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true);
|
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) {
|
public notifyRepeatedPanelsWaitingForVariables(variable: SceneVariable) {
|
||||||
for (const panel of this.state.repeatedPanels ?? []) {
|
for (const panel of this.state.repeatedPanels ?? []) {
|
||||||
const queryRunner = getQueryRunnerFor(panel);
|
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 { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
import { VariablesChanged } from 'app/features/variables/types';
|
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 { createWorker } from '../saving/createDetectChangesWorker';
|
||||||
import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||||
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
|
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
|
||||||
@ -197,18 +197,16 @@ describe('DashboardScene', () => {
|
|||||||
expect(resoredLayout.state.children.map((c) => c.state.key)).toEqual(originalPanelOrder);
|
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', () => {
|
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 panel = findVizPanelByKey(scene, 'panel-1')!;
|
||||||
const editPanel = buildPanelEditScene(panel!);
|
const editPanel = buildPanelEditScene(panel!);
|
||||||
scene.setState({
|
scene.setState({ editPanel });
|
||||||
editPanel,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(scene.state.editPanel!['_discardChanges']).toBe(false);
|
|
||||||
|
|
||||||
|
panel.setState({ title: 'new title' });
|
||||||
scene.exitEditMode({ skipConfirm: true });
|
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`
|
it.each`
|
||||||
@ -1023,14 +1021,14 @@ describe('DashboardScene', () => {
|
|||||||
panelPluginId: 'table',
|
panelPluginId: 'table',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('when editing', () => {
|
test('when editing', () => {
|
||||||
const panel = findVizPanelByKey(scene, 'panel-1');
|
const panel = findVizPanelByKey(scene, 'panel-1');
|
||||||
const editPanel = buildPanelEditScene(panel!);
|
const editPanel = buildPanelEditScene(panel!);
|
||||||
scene.setState({
|
scene.setState({ editPanel });
|
||||||
editPanel,
|
|
||||||
});
|
const queryRunner = editPanel.getPanel().state.$data!;
|
||||||
|
|
||||||
const queryRunner = (scene.state.editPanel as PanelEditor).state.vizManager.queryRunner;
|
|
||||||
expect(scene.enrichDataRequest(queryRunner)).toEqual({
|
expect(scene.enrichDataRequest(queryRunner)).toEqual({
|
||||||
app: CoreApp.Dashboard,
|
app: CoreApp.Dashboard,
|
||||||
dashboardUID: 'dash-1',
|
dashboardUID: 'dash-1',
|
||||||
|
@ -252,6 +252,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this._changeTracker.stopTrackingChanges();
|
this._changeTracker.stopTrackingChanges();
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
version: result.version,
|
version: result.version,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
@ -267,6 +268,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.state.editPanel?.dashboardSaved();
|
||||||
this._changeTracker.startTrackingChanges();
|
this._changeTracker.startTrackingChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -801,7 +803,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
|||||||
let panel = getClosestVizPanel(sceneObject);
|
let panel = getClosestVizPanel(sceneObject);
|
||||||
|
|
||||||
if (dashboard.state.isEditing && dashboard.state.editPanel) {
|
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;
|
let panelId = 0;
|
||||||
|
@ -2,14 +2,7 @@ import { Unsubscribable } from 'rxjs';
|
|||||||
|
|
||||||
import { AppEvents } from '@grafana/data';
|
import { AppEvents } from '@grafana/data';
|
||||||
import { config, locationService } from '@grafana/runtime';
|
import { config, locationService } from '@grafana/runtime';
|
||||||
import {
|
import { SceneGridLayout, SceneObjectUrlSyncHandler, SceneObjectUrlValues, VizPanel } from '@grafana/scenes';
|
||||||
SceneGridLayout,
|
|
||||||
SceneObjectBase,
|
|
||||||
SceneObjectState,
|
|
||||||
SceneObjectUrlSyncHandler,
|
|
||||||
SceneObjectUrlValues,
|
|
||||||
VizPanel,
|
|
||||||
} from '@grafana/scenes';
|
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { KioskMode } from 'app/types';
|
import { KioskMode } from 'app/types';
|
||||||
|
|
||||||
@ -18,7 +11,7 @@ import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
|||||||
import { createDashboardEditViewFor } from '../settings/utils';
|
import { createDashboardEditViewFor } from '../settings/utils';
|
||||||
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
|
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
|
||||||
import { ShareModal } from '../sharing/ShareModal';
|
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 { DashboardScene, DashboardSceneState } from './DashboardScene';
|
||||||
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
|
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
|
||||||
@ -78,9 +71,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update.inspectPanelKey = values.inspect;
|
update.inspectPanelKey = values.inspect;
|
||||||
update.overlay = new PanelInspectDrawer({
|
update.overlay = new PanelInspectDrawer({ panelRef: panel.getRef() });
|
||||||
$behaviors: [new ResolveInspectPanelByKey({ panelKey: values.inspect })],
|
|
||||||
});
|
|
||||||
} else if (inspectPanelKey) {
|
} else if (inspectPanelKey) {
|
||||||
update.inspectPanelKey = undefined;
|
update.inspectPanelKey = undefined;
|
||||||
update.overlay = 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() {
|
private async loadLibraryPanelFromPanelModel() {
|
||||||
let vizPanel = this.parent;
|
let vizPanel = this.parent;
|
||||||
|
|
||||||
|
@ -61,8 +61,8 @@ export function ToolbarActions({ dashboard }: Props) {
|
|||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const isEditingPanel = Boolean(editPanel);
|
const isEditingPanel = Boolean(editPanel);
|
||||||
const isViewingPanel = Boolean(viewPanelScene);
|
const isViewingPanel = Boolean(viewPanelScene);
|
||||||
const isEditedPanelDirty = useVizManagerDirty(editPanel);
|
const isEditedPanelDirty = usePanelEditDirty(editPanel);
|
||||||
const isEditingLibraryPanel = useEditingLibraryPanel(editPanel);
|
const isEditingLibraryPanel = editPanel && isLibraryPanel(editPanel.state.panelRef.resolve());
|
||||||
const hasCopiedPanel = store.exists(LS_PANEL_COPY_KEY);
|
const hasCopiedPanel = store.exists(LS_PANEL_COPY_KEY);
|
||||||
// Means we are not in settings view, fullscreen panel or edit panel
|
// Means we are not in settings view, fullscreen panel or edit panel
|
||||||
const isShowingDashboard = !editview && !isViewingPanel && !isEditingPanel;
|
const isShowingDashboard = !editview && !isViewingPanel && !isEditingPanel;
|
||||||
@ -422,7 +422,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
|||||||
onClick={editPanel?.onDiscard}
|
onClick={editPanel?.onDiscard}
|
||||||
tooltip={editPanel?.state.isNewPanel ? 'Discard panel' : 'Discard panel changes'}
|
tooltip={editPanel?.state.isNewPanel ? 'Discard panel' : 'Discard panel changes'}
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={!isEditedPanelDirty || !isDirty}
|
disabled={!isEditedPanelDirty}
|
||||||
key="discard"
|
key="discard"
|
||||||
fill="outline"
|
fill="outline"
|
||||||
variant="destructive"
|
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
|
// This hook handles when panelEditor is not defined to avoid conditionally hook usage
|
||||||
function useVizManagerDirty(panelEditor?: PanelEditor) {
|
function usePanelEditDirty(panelEditor?: PanelEditor) {
|
||||||
const [isDirty, setIsDirty] = useState<Boolean>(false);
|
const [isDirty, setIsDirty] = useState<Boolean | undefined>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (panelEditor) {
|
if (panelEditor) {
|
||||||
const unsub = panelEditor.state.vizManager.subscribeToState((vizManagerState) =>
|
const unsub = panelEditor.subscribeToState((state) => {
|
||||||
setIsDirty(vizManagerState.isDirty || false)
|
if (state.isDirty !== isDirty) {
|
||||||
);
|
setIsDirty(state.isDirty);
|
||||||
return () => {
|
}
|
||||||
unsub.unsubscribe();
|
});
|
||||||
};
|
|
||||||
|
return () => unsub.unsubscribe();
|
||||||
}
|
}
|
||||||
setIsDirty(false);
|
|
||||||
return;
|
return;
|
||||||
}, [panelEditor]);
|
}, [panelEditor, isDirty]);
|
||||||
|
|
||||||
return isDirty;
|
return isDirty;
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ describe('DashboardRow', () => {
|
|||||||
<TestProvider>
|
<TestProvider>
|
||||||
<RowOptionsForm
|
<RowOptionsForm
|
||||||
repeat={'3'}
|
repeat={'3'}
|
||||||
parent={scene}
|
sceneContext={scene}
|
||||||
title=""
|
title=""
|
||||||
onCancel={jest.fn()}
|
onCancel={jest.fn()}
|
||||||
onUpdate={jest.fn()}
|
onUpdate={jest.fn()}
|
||||||
@ -40,7 +40,7 @@ describe('DashboardRow', () => {
|
|||||||
it('Should not show warning component when does not have warningMessage prop', () => {
|
it('Should not show warning component when does not have warningMessage prop', () => {
|
||||||
render(
|
render(
|
||||||
<TestProvider>
|
<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>
|
</TestProvider>
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
|
@ -12,13 +12,13 @@ export type OnRowOptionsUpdate = (title: string, repeat?: string | null) => void
|
|||||||
export interface Props {
|
export interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
repeat?: string;
|
repeat?: string;
|
||||||
parent: SceneObject;
|
sceneContext: SceneObject;
|
||||||
onUpdate: OnRowOptionsUpdate;
|
onUpdate: OnRowOptionsUpdate;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
warning?: React.ReactNode;
|
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 [newRepeat, setNewRepeat] = useState<string | undefined>(repeat);
|
||||||
const onChangeRepeat = useCallback((name?: string) => setNewRepeat(name), [setNewRepeat]);
|
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" />
|
<Input {...register('title')} type="text" />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Repeat for">
|
<Field label="Repeat for">
|
||||||
<RepeatRowSelect2 parent={parent} repeat={newRepeat} onChange={onChangeRepeat} />
|
<RepeatRowSelect2 sceneContext={sceneContext} repeat={newRepeat} onChange={onChangeRepeat} />
|
||||||
</Field>
|
</Field>
|
||||||
{warning && (
|
{warning && (
|
||||||
<Alert
|
<Alert
|
||||||
|
@ -21,7 +21,7 @@ export const RowOptionsModal = ({ repeat, title, parent, onDismiss, onUpdate, wa
|
|||||||
return (
|
return (
|
||||||
<Modal isOpen={true} title="Row options" onDismiss={onDismiss} className={styles.modal}>
|
<Modal isOpen={true} title="Row options" onDismiss={onDismiss} className={styles.modal}>
|
||||||
<RowOptionsForm
|
<RowOptionsForm
|
||||||
parent={parent}
|
sceneContext={parent}
|
||||||
repeat={repeat}
|
repeat={repeat}
|
||||||
title={title}
|
title={title}
|
||||||
onCancel={onDismiss}
|
onCancel={onDismiss}
|
||||||
|
@ -15,14 +15,7 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||||
import { getPluginLinkExtensions, setPluginImportUtils } from '@grafana/runtime';
|
import { getPluginLinkExtensions, setPluginImportUtils } from '@grafana/runtime';
|
||||||
import {
|
import { MultiValueVariable, sceneGraph, SceneGridLayout, SceneGridRow, VizPanel } from '@grafana/scenes';
|
||||||
MultiValueVariable,
|
|
||||||
sceneGraph,
|
|
||||||
SceneGridLayout,
|
|
||||||
SceneGridRow,
|
|
||||||
SceneTimeRange,
|
|
||||||
VizPanel,
|
|
||||||
} from '@grafana/scenes';
|
|
||||||
import { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema';
|
import { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema';
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
|
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 { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||||
import { DashboardDataDTO } from 'app/types';
|
import { DashboardDataDTO } from 'app/types';
|
||||||
|
|
||||||
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
|
||||||
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
|
||||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
|
||||||
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||||
import { NEW_LINK } from '../settings/links/utils';
|
import { NEW_LINK } from '../settings/links/utils';
|
||||||
@ -804,7 +795,7 @@ describe('transformSceneToSaveModel', () => {
|
|||||||
activateFullSceneTree(scene);
|
activateFullSceneTree(scene);
|
||||||
|
|
||||||
expect(repeater.state.repeatedPanels?.length).toBe(2);
|
expect(repeater.state.repeatedPanels?.length).toBe(2);
|
||||||
const result = panelRepeaterToPanels(repeater, undefined, true);
|
const result = panelRepeaterToPanels(repeater, true);
|
||||||
|
|
||||||
expect(result).toHaveLength(2);
|
expect(result).toHaveLength(2);
|
||||||
|
|
||||||
@ -861,7 +852,7 @@ describe('transformSceneToSaveModel', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
activateFullSceneTree(scene);
|
activateFullSceneTree(scene);
|
||||||
const result = panelRepeaterToPanels(repeater, undefined, true);
|
const result = panelRepeaterToPanels(repeater, true);
|
||||||
|
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
|
|
||||||
@ -886,7 +877,7 @@ describe('transformSceneToSaveModel', () => {
|
|||||||
activateFullSceneTree(scene);
|
activateFullSceneTree(scene);
|
||||||
|
|
||||||
let panels: Panel[] = [];
|
let panels: Panel[] = [];
|
||||||
gridRowToSaveModel(row, panels, undefined, true);
|
gridRowToSaveModel(row, panels, true);
|
||||||
|
|
||||||
expect(panels).toHaveLength(2);
|
expect(panels).toHaveLength(2);
|
||||||
expect(panels[0].repeat).toBe('handler');
|
expect(panels[0].repeat).toBe('handler');
|
||||||
@ -914,7 +905,7 @@ describe('transformSceneToSaveModel', () => {
|
|||||||
activateFullSceneTree(scene);
|
activateFullSceneTree(scene);
|
||||||
|
|
||||||
let panels: Panel[] = [];
|
let panels: Panel[] = [];
|
||||||
gridRowToSaveModel(row, panels, undefined, true);
|
gridRowToSaveModel(row, panels, true);
|
||||||
|
|
||||||
expect(panels[0].repeat).toBe('handler');
|
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', () => {
|
describe('Given a scene with repeated panels and non-repeated panels', () => {
|
||||||
it('should save repeated panels itemHeight as height', () => {
|
it('should save repeated panels itemHeight as height', () => {
|
||||||
const scene = transformSaveModelToScene({
|
const scene = transformSaveModelToScene({
|
||||||
|
@ -33,7 +33,7 @@ import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
|
|||||||
|
|
||||||
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||||
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
import { DashboardGridItem } from '../scene/DashboardGridItem';
|
||||||
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||||
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||||
@ -58,9 +58,9 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
|
|||||||
if (child instanceof DashboardGridItem) {
|
if (child instanceof DashboardGridItem) {
|
||||||
// handle panel repeater scenario
|
// handle panel repeater scenario
|
||||||
if (child.state.variableName) {
|
if (child.state.variableName) {
|
||||||
panels = panels.concat(panelRepeaterToPanels(child, state, isSnapshot));
|
panels = panels.concat(panelRepeaterToPanels(child, isSnapshot));
|
||||||
} else {
|
} 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) {
|
if (child.state.key!.indexOf('-clone-') > 0 && !isSnapshot) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
gridRowToSaveModel(child, panels, state, isSnapshot);
|
gridRowToSaveModel(child, panels, isSnapshot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,11 +139,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
|
|||||||
return sortedDeepCloneWithoutNulls(dashboard);
|
return sortedDeepCloneWithoutNulls(dashboard);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function gridItemToPanel(
|
export function gridItemToPanel(gridItem: DashboardGridItem, isSnapshot = false): Panel {
|
||||||
gridItem: DashboardGridItem,
|
|
||||||
sceneState?: DashboardSceneState,
|
|
||||||
isSnapshot = false
|
|
||||||
): Panel {
|
|
||||||
let vizPanel: VizPanel | undefined;
|
let vizPanel: VizPanel | undefined;
|
||||||
let x = 0,
|
let x = 0,
|
||||||
y = 0,
|
y = 0,
|
||||||
@ -152,19 +148,6 @@ export function gridItemToPanel(
|
|||||||
|
|
||||||
let gridItem_ = gridItem;
|
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)) {
|
if (!(gridItem_.state.body instanceof VizPanel)) {
|
||||||
throw new Error('DashboardGridItem body expected to be VizPanel');
|
throw new Error('DashboardGridItem body expected to be VizPanel');
|
||||||
}
|
}
|
||||||
@ -325,13 +308,9 @@ function vizPanelDataToPanel(
|
|||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function panelRepeaterToPanels(
|
export function panelRepeaterToPanels(repeater: DashboardGridItem, isSnapshot = false): Panel[] {
|
||||||
repeater: DashboardGridItem,
|
|
||||||
sceneState?: DashboardSceneState,
|
|
||||||
isSnapshot = false
|
|
||||||
): Panel[] {
|
|
||||||
if (!isSnapshot) {
|
if (!isSnapshot) {
|
||||||
return [gridItemToPanel(repeater, sceneState)];
|
return [gridItemToPanel(repeater)];
|
||||||
} else {
|
} else {
|
||||||
// return early if the repeated panel is a library panel
|
// return early if the repeated panel is a library panel
|
||||||
if (repeater.state.body instanceof VizPanel && isLibraryPanel(repeater.state.body)) {
|
if (repeater.state.body instanceof VizPanel && isLibraryPanel(repeater.state.body)) {
|
||||||
@ -388,12 +367,7 @@ export function panelRepeaterToPanels(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function gridRowToSaveModel(
|
export function gridRowToSaveModel(gridRow: SceneGridRow, panelsArray: Array<Panel | RowPanel>, isSnapshot = false) {
|
||||||
gridRow: SceneGridRow,
|
|
||||||
panelsArray: Array<Panel | RowPanel>,
|
|
||||||
sceneState?: DashboardSceneState,
|
|
||||||
isSnapshot = false
|
|
||||||
) {
|
|
||||||
const collapsed = Boolean(gridRow.state.isCollapsed);
|
const collapsed = Boolean(gridRow.state.isCollapsed);
|
||||||
const rowPanel: RowPanel = {
|
const rowPanel: RowPanel = {
|
||||||
type: 'row',
|
type: 'row',
|
||||||
@ -443,10 +417,10 @@ export function gridRowToSaveModel(
|
|||||||
if (c instanceof DashboardGridItem) {
|
if (c instanceof DashboardGridItem) {
|
||||||
if (c.state.variableName) {
|
if (c.state.variableName) {
|
||||||
// Perform snapshot only for uncollapsed rows
|
// Perform snapshot only for uncollapsed rows
|
||||||
panelsInsideRow = panelsInsideRow.concat(panelRepeaterToPanels(c, sceneState, !collapsed));
|
panelsInsideRow = panelsInsideRow.concat(panelRepeaterToPanels(c, !collapsed));
|
||||||
} else {
|
} else {
|
||||||
// Perform snapshot only for uncollapsed panels
|
// 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)) {
|
if (!(c instanceof DashboardGridItem)) {
|
||||||
throw new Error('Row child expected to be 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);
|
onOverrideChange(idx, override);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDynamicConfigValueAdd = (o: ConfigOverrideRule, value: SelectableValue<string>) => {
|
const onDynamicConfigValueAdd = (override: ConfigOverrideRule, value: SelectableValue<string>) => {
|
||||||
const registryItem = registry.get(value.value!);
|
const registryItem = registry.get(value.value!);
|
||||||
const propertyConfig: DynamicConfigValue = {
|
const propertyConfig: DynamicConfigValue = {
|
||||||
id: registryItem.id,
|
id: registryItem.id,
|
||||||
value: registryItem.defaultValue,
|
value: registryItem.defaultValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (override.properties) {
|
const properties = override.properties ?? [];
|
||||||
o.properties.push(propertyConfig);
|
properties.push(propertyConfig);
|
||||||
} else {
|
|
||||||
o.properties = [propertyConfig];
|
|
||||||
}
|
|
||||||
|
|
||||||
onOverrideChange(idx, o);
|
onOverrideChange(idx, { ...override, properties });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -158,13 +155,23 @@ export function getFieldOverrideCategories(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onPropertyChange = (value: DynamicConfigValue) => {
|
const onPropertyChange = (value: DynamicConfigValue) => {
|
||||||
override.properties[propIdx].value = value;
|
onOverrideChange(idx, {
|
||||||
onOverrideChange(idx, override);
|
...override,
|
||||||
|
properties: override.properties.map((prop, i) => {
|
||||||
|
if (i === propIdx) {
|
||||||
|
return { ...prop, value: value };
|
||||||
|
}
|
||||||
|
|
||||||
|
return prop;
|
||||||
|
}),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPropertyRemove = () => {
|
const onPropertyRemove = () => {
|
||||||
override.properties.splice(propIdx, 1);
|
onOverrideChange(idx, {
|
||||||
onOverrideChange(idx, override);
|
...override,
|
||||||
|
properties: override.properties.filter((_, i) => i !== propIdx),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,14 +44,14 @@ export const RepeatRowSelect = ({ repeat, onChange, id }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface Props2 {
|
interface Props2 {
|
||||||
parent: SceneObject;
|
sceneContext: SceneObject;
|
||||||
repeat: string | undefined;
|
repeat: string | undefined;
|
||||||
id?: string;
|
id?: string;
|
||||||
onChange: (name?: string) => void;
|
onChange: (name?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RepeatRowSelect2 = ({ parent, repeat, id, onChange }: Props2) => {
|
export const RepeatRowSelect2 = ({ sceneContext, repeat, id, onChange }: Props2) => {
|
||||||
const sceneVars = useMemo(() => sceneGraph.getVariables(parent), [parent]);
|
const sceneVars = useMemo(() => sceneGraph.getVariables(sceneContext.getRoot()), [sceneContext]);
|
||||||
const variables = sceneVars.useState().variables;
|
const variables = sceneVars.useState().variables;
|
||||||
|
|
||||||
const variableOptions = useMemo(() => {
|
const variableOptions = useMemo(() => {
|
||||||
|
@ -3,7 +3,6 @@ import { lastValueFrom } from 'rxjs';
|
|||||||
import { VizPanel } from '@grafana/scenes';
|
import { VizPanel } from '@grafana/scenes';
|
||||||
import { LibraryPanel, defaultDashboard } from '@grafana/schema';
|
import { LibraryPanel, defaultDashboard } from '@grafana/schema';
|
||||||
import { DashboardModel } from 'app/features/dashboard/state';
|
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 { DashboardGridItem } from 'app/features/dashboard-scene/scene/DashboardGridItem';
|
||||||
import { vizPanelToPanel } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModel';
|
import { vizPanelToPanel } from 'app/features/dashboard-scene/serialization/transformSceneToSaveModel';
|
||||||
import { getLibraryPanelBehavior } from 'app/features/dashboard-scene/utils/utils';
|
import { getLibraryPanelBehavior } from 'app/features/dashboard-scene/utils/utils';
|
||||||
@ -146,10 +145,6 @@ export function libraryVizPanelToSaveModel(vizPanel: VizPanel) {
|
|||||||
|
|
||||||
let gridItem = vizPanel.parent;
|
let gridItem = vizPanel.parent;
|
||||||
|
|
||||||
if (gridItem instanceof VizPanelManager) {
|
|
||||||
gridItem = gridItem.state.sourcePanel.resolve().parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!gridItem || !(gridItem instanceof DashboardGridItem)) {
|
if (!gridItem || !(gridItem instanceof DashboardGridItem)) {
|
||||||
throw new Error('Trying to save a library panel that does not have a DashboardGridItem parent');
|
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',
|
name: 'from history',
|
||||||
input: { from: '2024-07-22T18:30:00.000Z', to: '2024-07-22T19:30:00.000Z' },
|
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',
|
name: 'time change event with timezone',
|
||||||
@ -33,7 +33,7 @@ describe('DataTrailsHistory', () => {
|
|||||||
},
|
},
|
||||||
])('$name', ({ input, expected }) => {
|
])('$name', ({ input, expected }) => {
|
||||||
const result = parseTimeTooltip(input);
|
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) {
|
function isPanelInEdit(panelId: number, panelInEditId?: number) {
|
||||||
let idToCompareWith = panelInEditId;
|
let idToCompareWith = panelInEditId;
|
||||||
|
|
||||||
if (window.__grafanaSceneContext && window.__grafanaSceneContext instanceof DashboardScene) {
|
if (window.__grafanaSceneContext && window.__grafanaSceneContext instanceof DashboardScene) {
|
||||||
idToCompareWith = window.__grafanaSceneContext.state.editPanel?.state.panelId;
|
idToCompareWith = window.__grafanaSceneContext.state.editPanel?.getPanelId();
|
||||||
}
|
}
|
||||||
|
|
||||||
return panelId === idToCompareWith;
|
return panelId === idToCompareWith;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user