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 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PanelDataPane({
|
||||||
|
panelRef,
|
||||||
|
tabs,
|
||||||
|
tab: TabId.Queries,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFromUrl(values: SceneObjectUrlValues) {
|
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,112 +246,454 @@ 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({
|
}));
|
||||||
type: 'grafana-testdata-datasource',
|
|
||||||
uid: 'gdev-testdata',
|
const store = jest.requireMock('app/core/store');
|
||||||
|
let deactivators = [] as Array<() => void>;
|
||||||
|
|
||||||
|
describe('PanelDataQueriesTab', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setObject.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
deactivators.forEach((deactivate) => deactivate());
|
||||||
|
deactivators = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Adding queries', () => {
|
||||||
|
it('can add a new query', async () => {
|
||||||
|
const { queriesTab } = await setupScene('panel-1');
|
||||||
|
|
||||||
|
queriesTab.addQueryClick();
|
||||||
|
|
||||||
|
expect(queriesTab.queryRunner.state.queries).toHaveLength(2);
|
||||||
|
expect(queriesTab.queryRunner.state.queries[1].refId).toBe('B');
|
||||||
|
expect(queriesTab.queryRunner.state.queries[1].hide).toBe(false);
|
||||||
|
expect(queriesTab.queryRunner.state.queries[1].datasource).toEqual({
|
||||||
|
type: 'grafana-testdata-datasource',
|
||||||
|
uid: 'gdev-testdata',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can add a new query when datasource is mixed', async () => {
|
||||||
|
const { queriesTab } = await setupScene('panel-7');
|
||||||
|
|
||||||
|
expect(queriesTab.state.datasource?.uid).toBe('-- Mixed --');
|
||||||
|
expect(queriesTab.queryRunner.state.datasource?.uid).toBe('-- Mixed --');
|
||||||
|
|
||||||
|
queriesTab.addQueryClick();
|
||||||
|
|
||||||
|
expect(queriesTab.queryRunner.state.queries).toHaveLength(2);
|
||||||
|
expect(queriesTab.queryRunner.state.queries[1].refId).toBe('B');
|
||||||
|
expect(queriesTab.queryRunner.state.queries[1].hide).toBe(false);
|
||||||
|
expect(queriesTab.queryRunner.state.queries[1].datasource?.uid).toBe('gdev-testdata');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can add a new query when datasource is mixed', async () => {
|
describe('PanelDataQueriesTab', () => {
|
||||||
const vizPanelManager = setupVizPanelManger('panel-7');
|
it('renders query group top section', async () => {
|
||||||
vizPanelManager.activate();
|
const modelMock = await createModelMock();
|
||||||
await Promise.resolve();
|
|
||||||
|
|
||||||
const model = new PanelDataQueriesTab(vizPanelManager);
|
render(<PanelDataQueriesTabRendered model={modelMock}></PanelDataQueriesTabRendered>);
|
||||||
expect(vizPanelManager.state.datasource?.uid).toBe('-- Mixed --');
|
await screen.findByTestId(selectors.components.QueryTab.queryGroupTopSection);
|
||||||
expect(model.queryRunner.state.datasource?.uid).toBe('-- Mixed --');
|
});
|
||||||
model.addQueryClick();
|
|
||||||
|
|
||||||
expect(model.queryRunner.state.queries).toHaveLength(2);
|
it('renders queries rows when queries are set', async () => {
|
||||||
expect(model.queryRunner.state.queries[1].refId).toBe('B');
|
const modelMock = await createModelMock();
|
||||||
expect(model.queryRunner.state.queries[1].hide).toBe(false);
|
render(<PanelDataQueriesTabRendered model={modelMock}></PanelDataQueriesTabRendered>);
|
||||||
expect(model.queryRunner.state.queries[1].datasource?.uid).toBe('gdev-testdata');
|
|
||||||
|
await screen.findByTestId('query-editor-rows');
|
||||||
|
expect(screen.getAllByTestId('query-editor-row')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allow to add a new query when user clicks on add new', async () => {
|
||||||
|
const modelMock = await createModelMock();
|
||||||
|
jest.spyOn(modelMock, 'addQueryClick');
|
||||||
|
jest.spyOn(modelMock, 'onQueriesChange');
|
||||||
|
render(<PanelDataQueriesTabRendered model={modelMock}></PanelDataQueriesTabRendered>);
|
||||||
|
|
||||||
|
await screen.findByTestId(selectors.components.QueryTab.addQuery);
|
||||||
|
await userEvent.click(screen.getByTestId(selectors.components.QueryTab.addQuery));
|
||||||
|
|
||||||
|
const expectedQueries = [
|
||||||
|
{
|
||||||
|
datasource: { type: 'grafana-testdata-datasource', uid: 'gdev-testdata' },
|
||||||
|
refId: 'A',
|
||||||
|
scenarioId: 'random_walk',
|
||||||
|
seriesCount: 1,
|
||||||
|
},
|
||||||
|
{ datasource: { type: 'grafana-testdata-datasource', uid: 'gdev-testdata' }, hide: false, refId: 'B' },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(modelMock.addQueryClick).toHaveBeenCalled();
|
||||||
|
expect(modelMock.onQueriesChange).toHaveBeenCalledWith(expectedQueries);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allow to remove a query when user clicks on remove', async () => {
|
||||||
|
const modelMock = await createModelMock();
|
||||||
|
jest.spyOn(modelMock, 'addQueryClick');
|
||||||
|
jest.spyOn(modelMock, 'onQueriesChange');
|
||||||
|
render(<PanelDataQueriesTabRendered model={modelMock}></PanelDataQueriesTabRendered>);
|
||||||
|
|
||||||
|
await screen.findByTestId('data-testid Remove query');
|
||||||
|
await userEvent.click(screen.getByTestId('data-testid Remove query'));
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PanelDataQueriesTab', () => {
|
async function setupScene(panelId: string) {
|
||||||
it('renders query group top section', async () => {
|
const dashboard = transformSaveModelToScene({ dashboard: testDashboard as unknown as DashboardDataDTO, meta: {} });
|
||||||
const modelMock = await createModelMock();
|
const panel = findVizPanelByKey(dashboard, panelId)!;
|
||||||
|
|
||||||
render(<PanelDataQueriesTabRendered model={modelMock}></PanelDataQueriesTabRendered>);
|
const panelEditor = buildPanelEditScene(panel);
|
||||||
await screen.findByTestId(selectors.components.QueryTab.queryGroupTopSection);
|
dashboard.setState({ editPanel: panelEditor });
|
||||||
});
|
|
||||||
|
|
||||||
it('renders queries rows when queries are set', async () => {
|
deactivators.push(dashboard.activate());
|
||||||
const modelMock = await createModelMock();
|
deactivators.push(panelEditor.activate());
|
||||||
render(<PanelDataQueriesTabRendered model={modelMock}></PanelDataQueriesTabRendered>);
|
|
||||||
|
|
||||||
await screen.findByTestId('query-editor-rows');
|
const queriesTab = panelEditor.state.dataPane!.state.tabs[0] as PanelDataQueriesTab;
|
||||||
expect(screen.getAllByTestId('query-editor-row')).toHaveLength(1);
|
deactivators.push(queriesTab.activate());
|
||||||
});
|
|
||||||
|
|
||||||
it('allow to add a new query when user clicks on add new', async () => {
|
await Promise.resolve();
|
||||||
const modelMock = await createModelMock();
|
|
||||||
jest.spyOn(modelMock, 'addQueryClick');
|
|
||||||
jest.spyOn(modelMock, 'onQueriesChange');
|
|
||||||
render(<PanelDataQueriesTabRendered model={modelMock}></PanelDataQueriesTabRendered>);
|
|
||||||
|
|
||||||
await screen.findByTestId(selectors.components.QueryTab.addQuery);
|
return { panel, scene: dashboard, queriesTab };
|
||||||
await userEvent.click(screen.getByTestId(selectors.components.QueryTab.addQuery));
|
}
|
||||||
|
|
||||||
const expectedQueries = [
|
|
||||||
{
|
|
||||||
datasource: { type: 'grafana-testdata-datasource', uid: 'gdev-testdata' },
|
|
||||||
refId: 'A',
|
|
||||||
scenarioId: 'random_walk',
|
|
||||||
seriesCount: 1,
|
|
||||||
},
|
|
||||||
{ datasource: { type: 'grafana-testdata-datasource', uid: 'gdev-testdata' }, hide: false, refId: 'B' },
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(modelMock.addQueryClick).toHaveBeenCalled();
|
|
||||||
expect(modelMock.onQueriesChange).toHaveBeenCalledWith(expectedQueries);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allow to remove a query when user clicks on remove', async () => {
|
|
||||||
const modelMock = await createModelMock();
|
|
||||||
jest.spyOn(modelMock, 'addQueryClick');
|
|
||||||
jest.spyOn(modelMock, 'onQueriesChange');
|
|
||||||
render(<PanelDataQueriesTabRendered model={modelMock}></PanelDataQueriesTabRendered>);
|
|
||||||
|
|
||||||
await screen.findByTestId('data-testid Remove query');
|
|
||||||
await userEvent.click(screen.getByTestId('data-testid Remove query'));
|
|
||||||
|
|
||||||
expect(modelMock.onQueriesChange).toHaveBeenCalledWith([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const setupVizPanelManger = (panelId: string) => {
|
|
||||||
const scene = transformSaveModelToScene({ dashboard: testDashboard as unknown as DashboardDataDTO, 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;
|
|
||||||
};
|
|
||||||
|
@ -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);
|
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();
|
||||||
};
|
};
|
||||||
|
|
||||||
onQueryOptionsChange = (options: QueryGroupOptions) => {
|
public onQueryOptionsChange = (options: QueryGroupOptions) => {
|
||||||
this._panelManager.changeQueryOptions(options);
|
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();
|
||||||
};
|
};
|
||||||
|
|
||||||
onQueriesChange = (queries: DataQuery[]) => {
|
public onQueriesChange = (queries: SceneDataQuery[]) => {
|
||||||
this._panelManager.changeQueries(queries);
|
const runner = this.queryRunner;
|
||||||
|
runner.setState({ queries });
|
||||||
};
|
};
|
||||||
|
|
||||||
onRunQueries = () => {
|
public onRunQueries = () => {
|
||||||
this._panelManager.queryRunner.runQueries();
|
this.queryRunner.runQueries();
|
||||||
};
|
};
|
||||||
|
|
||||||
getQueries() {
|
public getQueries() {
|
||||||
return this._panelManager.queryRunner.state.queries;
|
return this.queryRunner.state.queries;
|
||||||
}
|
}
|
||||||
|
|
||||||
newQuery(): Partial<DataQuery> {
|
public newQuery(): Partial<DataQuery> {
|
||||||
const { dsSettings, datasource } = this._panelManager.state;
|
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;
|
describe('When initializing', () => {
|
||||||
expect(updatedPanel?.state.title).toBe('changed title');
|
it('should wait for panel plugin to load', async () => {
|
||||||
|
const { panelEditor, panel, pluginResolve, dashboard } = await setup({ skipWait: true });
|
||||||
|
|
||||||
|
expect(panel.state.options).toEqual({});
|
||||||
|
expect(panelEditor.state.isInitializing).toBe(true);
|
||||||
|
|
||||||
|
const pluginToLoad = getPanelPlugin({ id: 'text' }).setPanelOptions((build) => {
|
||||||
|
build.addBooleanSwitch({
|
||||||
|
path: 'showHeader',
|
||||||
|
name: 'Show header',
|
||||||
|
defaultValue: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
pluginResolve(pluginToLoad);
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
|
|
||||||
|
expect(panelEditor.state.isInitializing).toBe(false);
|
||||||
|
expect(panel.state.options).toEqual({ showHeader: true });
|
||||||
|
|
||||||
|
panel.onOptionsChange({ showHeader: false });
|
||||||
|
panelEditor.onDiscard();
|
||||||
|
|
||||||
|
const discardedPanel = findVizPanelByKey(dashboard, panel.state.key!)!;
|
||||||
|
expect(discardedPanel.state.options).toEqual({ showHeader: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When discarding', () => {
|
||||||
|
it('should discard changes revert all changes', async () => {
|
||||||
|
const { panelEditor, panel, dashboard } = await setup();
|
||||||
|
|
||||||
|
panel.setState({ title: 'changed title' });
|
||||||
|
panelEditor.onDiscard();
|
||||||
|
|
||||||
|
const discardedPanel = findVizPanelByKey(dashboard, panel.state.key!)!;
|
||||||
|
|
||||||
|
expect(discardedPanel.state.title).toBe('original title');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should discard changes when unmounted and discard changes is marked as true', () => {
|
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 });
|
|
||||||
|
|
||||||
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' });
|
|
||||||
|
|
||||||
editScene.onDiscard();
|
|
||||||
deactivate();
|
|
||||||
|
|
||||||
const updatedPanel = gridItem.state.body as VizPanel;
|
|
||||||
expect(updatedPanel?.state.title).toBe(panel.state.title);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should discard a newly added panel', () => {
|
it('should discard query runner changes', async () => {
|
||||||
pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true });
|
const { panelEditor, panel, dashboard } = await setup({});
|
||||||
|
|
||||||
const panel = new VizPanel({
|
const queryRunner = getQueryRunnerFor(panel);
|
||||||
key: 'panel-1',
|
queryRunner?.setState({ maxDataPoints: 123, queries: [{ refId: 'A' }, { refId: 'B' }] });
|
||||||
pluginId: 'text',
|
|
||||||
});
|
|
||||||
|
|
||||||
const gridItem = new DashboardGridItem({ body: panel });
|
panelEditor.onDiscard();
|
||||||
|
|
||||||
const editScene = buildPanelEditScene(panel, true);
|
const discardedPanel = findVizPanelByKey(dashboard, panel.state.key!)!;
|
||||||
const scene = new DashboardScene({
|
const restoredQueryRunner = getQueryRunnerFor(discardedPanel);
|
||||||
editPanel: editScene,
|
expect(restoredQueryRunner?.state.maxDataPoints).toBe(500);
|
||||||
isEditing: true,
|
expect(restoredQueryRunner?.state.queries.length).toBe(1);
|
||||||
body: new SceneGridLayout({
|
});
|
||||||
children: [gridItem],
|
});
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
const scene = new DashboardScene({
|
|
||||||
editPanel: editScene,
|
|
||||||
});
|
|
||||||
|
|
||||||
activateFullSceneTree(scene);
|
|
||||||
|
|
||||||
expect(editScene.state.dataPane).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should exist if panel is supporting querying', () => {
|
it('should exist if panel is supporting querying', async () => {
|
||||||
pluginToLoad = getTestPanelPlugin({ id: 'timeseries' });
|
const { panelEditor } = await setup({ pluginSkipDataQuery: false });
|
||||||
|
expect(panelEditor.state.dataPane).toBeDefined();
|
||||||
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._subs.add(
|
this.waitForPlugin();
|
||||||
panelManager.subscribeToState((n, p) => {
|
|
||||||
if (n.pluginId !== p.pluginId) {
|
|
||||||
this._initDataPane(n.pluginId);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this._initDataPane(panel.state.pluginId);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (!this._discardChanges) {
|
if (layoutElement instanceof DashboardGridItem) {
|
||||||
this.commitChanges();
|
layoutElement.editingCompleted();
|
||||||
} else if (this.state.isNewPanel) {
|
}
|
||||||
getDashboardSceneFor(this).removePanel(panelManager.state.sourcePanel.resolve()!);
|
if (deactivateParents) {
|
||||||
|
deactivateParents();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
private waitForPlugin(retry = 0) {
|
||||||
|
const panel = this.getPanel();
|
||||||
|
const plugin = panel.getPlugin();
|
||||||
|
|
||||||
private _initDataPane(pluginId: string) {
|
if (!plugin || plugin.meta.id !== panel.state.pluginId) {
|
||||||
const skipDataQuery = config.panels[pluginId]?.skipDataQuery;
|
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(
|
||||||
|
panel.subscribeToState((n, p) => {
|
||||||
|
if (n.pluginId !== p.pluginId) {
|
||||||
|
this.waitForPlugin();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Setup options pane
|
||||||
|
this.setState({
|
||||||
|
optionsPane: new PanelOptionsPane({
|
||||||
|
panelRef: this.state.panelRef,
|
||||||
|
searchQuery: '',
|
||||||
|
listMode: OptionFilter.All,
|
||||||
|
}),
|
||||||
|
isInitializing: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// plugin changed after first time initialization
|
||||||
|
// Just update data pane
|
||||||
|
this._updateDataPane(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateDataPane(plugin: PanelPlugin) {
|
||||||
|
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,73 +94,77 @@ export function getPanelFrameCategory2(
|
|||||||
render: () => <ScenePanelLinksEditor panelLinks={panelLinksObject ?? undefined} />,
|
render: () => <ScenePanelLinksEditor panelLinks={panelLinksObject ?? undefined} />,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
|
||||||
.addCategory(
|
|
||||||
new OptionsPaneCategoryDescriptor({
|
|
||||||
title: 'Repeat options',
|
|
||||||
id: 'Repeat options',
|
|
||||||
isOpenDefault: false,
|
|
||||||
})
|
|
||||||
.addItem(
|
|
||||||
new OptionsPaneItemDescriptor({
|
|
||||||
title: 'Repeat by variable',
|
|
||||||
description:
|
|
||||||
'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.',
|
|
||||||
render: function renderRepeatOptions() {
|
|
||||||
return (
|
|
||||||
<RepeatRowSelect2
|
|
||||||
id="repeat-by-variable-select"
|
|
||||||
parent={panel}
|
|
||||||
repeat={repeat}
|
|
||||||
onChange={(value?: string) => {
|
|
||||||
const stateUpdate: Partial<VizPanelManagerState> = { repeat: value };
|
|
||||||
if (value && !vizManager.state.repeatDirection) {
|
|
||||||
stateUpdate.repeatDirection = 'h';
|
|
||||||
}
|
|
||||||
vizManager.setState(stateUpdate);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.addItem(
|
|
||||||
new OptionsPaneItemDescriptor({
|
|
||||||
title: 'Repeat direction',
|
|
||||||
showIf: () => !!vizManager.state.repeat,
|
|
||||||
render: function renderRepeatOptions() {
|
|
||||||
const directionOptions: Array<SelectableValue<'h' | 'v'>> = [
|
|
||||||
{ label: 'Horizontal', value: 'h' },
|
|
||||||
{ label: 'Vertical', value: 'v' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RadioButtonGroup
|
|
||||||
options={directionOptions}
|
|
||||||
value={vizManager.state.repeatDirection ?? 'h'}
|
|
||||||
onChange={(value) => vizManager.setState({ repeatDirection: value })}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.addItem(
|
|
||||||
new OptionsPaneItemDescriptor({
|
|
||||||
title: 'Max per row',
|
|
||||||
showIf: () => Boolean(vizManager.state.repeat && vizManager.state.repeatDirection === 'h'),
|
|
||||||
render: function renderOption() {
|
|
||||||
const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value }));
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
options={maxPerRowOptions}
|
|
||||||
value={vizManager.state.maxPerRow}
|
|
||||||
onChange={(value) => vizManager.setState({ maxPerRow: value.value })}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (layoutElement instanceof DashboardGridItem) {
|
||||||
|
const gridItem = layoutElement;
|
||||||
|
|
||||||
|
const category = new OptionsPaneCategoryDescriptor({
|
||||||
|
title: 'Repeat options',
|
||||||
|
id: 'Repeat options',
|
||||||
|
isOpenDefault: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
category.addItem(
|
||||||
|
new OptionsPaneItemDescriptor({
|
||||||
|
title: 'Repeat by variable',
|
||||||
|
description:
|
||||||
|
'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.',
|
||||||
|
render: function renderRepeatOptions() {
|
||||||
|
return (
|
||||||
|
<RepeatRowSelect2
|
||||||
|
id="repeat-by-variable-select"
|
||||||
|
sceneContext={panel}
|
||||||
|
repeat={gridItem.state.variableName}
|
||||||
|
onChange={(value?: string) => gridItem.setRepeatByVariable(value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
category.addItem(
|
||||||
|
new OptionsPaneItemDescriptor({
|
||||||
|
title: 'Repeat direction',
|
||||||
|
showIf: () => Boolean(gridItem.state.variableName),
|
||||||
|
render: function renderRepeatOptions() {
|
||||||
|
const directionOptions: Array<SelectableValue<'h' | 'v'>> = [
|
||||||
|
{ label: 'Horizontal', value: 'h' },
|
||||||
|
{ label: 'Vertical', value: 'v' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioButtonGroup
|
||||||
|
options={directionOptions}
|
||||||
|
value={gridItem.state.repeatDirection ?? 'h'}
|
||||||
|
onChange={(value) => gridItem.setState({ repeatDirection: value })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
category.addItem(
|
||||||
|
new OptionsPaneItemDescriptor({
|
||||||
|
title: 'Max per row',
|
||||||
|
showIf: () => Boolean(gridItem.state.variableName && gridItem.state.repeatDirection === 'h'),
|
||||||
|
render: function renderOption() {
|
||||||
|
const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value }));
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
options={maxPerRowOptions}
|
||||||
|
value={gridItem.state.maxPerRow ?? 4}
|
||||||
|
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,14 +55,17 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses the current repeat item count to calculate the user intended desired itemHeight
|
* Uses the current repeat item count to calculate the user intended desired itemHeight
|
||||||
*/
|
*/
|
||||||
@ -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