PoC: Copy/Paste panel styles (#116786)

This commit is contained in:
Adela Almasan 2026-02-03 12:57:19 -06:00 committed by GitHub
parent 5cef2a02f7
commit cf2481b1c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 545 additions and 4 deletions

View file

@ -1287,6 +1287,11 @@ export interface FeatureToggles {
*/
newVizSuggestions?: boolean;
/**
* Enable style actions (copy/paste) in the panel editor
* @default false
*/
panelStyleActions?: boolean;
/**
* Enable all plugins to supply visualization suggestions (including 3rd party plugins)
* @default false
*/

View file

@ -2024,6 +2024,14 @@ var (
Owner: grafanaDatavizSquad,
Expression: "false",
},
{
Name: "panelStyleActions",
Description: "Enable style actions (copy/paste) in the panel editor",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaDatavizSquad,
Expression: "false",
},
{
Name: "externalVizSuggestions",
Description: "Enable all plugins to supply visualization suggestions (including 3rd party plugins)",

View file

@ -253,6 +253,7 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly
2025-10-20,newGauge,preview,@grafana/dataviz-squad,false,false,true
2025-11-12,newVizSuggestions,preview,@grafana/dataviz-squad,false,false,true
2025-12-02,externalVizSuggestions,experimental,@grafana/dataviz-squad,false,false,true
2026-01-28,panelStyleActions,experimental,@grafana/dataviz-squad,false,false,true
2025-12-18,heatmapRowsAxisOptions,experimental,@grafana/dataviz-squad,false,false,true
2025-10-17,preventPanelChromeOverflow,preview,@grafana/grafana-frontend-platform,false,false,true
2025-10-31,jaegerEnableGrpcEndpoint,experimental,@grafana/oss-big-tent,false,false,false

1 Created Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
253 2025-10-20 newGauge preview @grafana/dataviz-squad false false true
254 2025-11-12 newVizSuggestions preview @grafana/dataviz-squad false false true
255 2025-12-02 externalVizSuggestions experimental @grafana/dataviz-squad false false true
256 2026-01-28 panelStyleActions experimental @grafana/dataviz-squad false false true
257 2025-12-18 heatmapRowsAxisOptions experimental @grafana/dataviz-squad false false true
258 2025-10-17 preventPanelChromeOverflow preview @grafana/grafana-frontend-platform false false true
259 2025-10-31 jaegerEnableGrpcEndpoint experimental @grafana/oss-big-tent false false false

View file

@ -3314,6 +3314,20 @@
"expression": "false"
}
},
{
"metadata": {
"name": "panelStyleActions",
"resourceVersion": "1769620237787",
"creationTimestamp": "2026-01-28T17:10:37Z"
},
"spec": {
"description": "Enable style actions (copy/paste) in the panel editor",
"stage": "experimental",
"codeowner": "@grafana/dataviz-squad",
"frontend": true,
"expression": "false"
}
},
{
"metadata": {
"name": "panelTimeSettings",

View file

@ -9,6 +9,7 @@ export const DEFAULT_ROW_HEIGHT = 250;
export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3;
export const LS_PANEL_COPY_KEY = 'panel-copy';
export const LS_STYLES_COPY_KEY = 'styles-copy';
export const LS_ROW_COPY_KEY = 'row-copy';
export const LS_TAB_COPY_KEY = 'tab-copy';
export const PANEL_BORDER = 2;

View file

@ -24,7 +24,7 @@ import {
import { Dashboard, DashboardCursorSync, LibraryPanel } from '@grafana/schema';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { appEvents } from 'app/core/app_events';
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
import { LS_PANEL_COPY_KEY, LS_STYLES_COPY_KEY } from 'app/core/constants';
import { AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DecoratedRevisionModel } from 'app/features/dashboard/types/revisionModels';
@ -39,6 +39,7 @@ import { createWorker } from '../saving/createDetectChangesWorker';
import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import { getCloneKey } from '../utils/clone';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { DashboardInteractions } from '../utils/interactions';
import { findVizPanelByKey, getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils';
import * as utils from '../utils/utils';
@ -632,6 +633,196 @@ describe('DashboardScene', () => {
expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false);
});
describe('Copy/Paste panel styles', () => {
const createTimeseriesPanel = () => {
return new VizPanel({
title: 'Timeseries Panel',
key: `panel-timeseries-${Math.random()}`,
pluginId: 'timeseries',
fieldConfig: {
defaults: {
color: { mode: 'palette-classic' },
custom: {
lineWidth: 1,
fillOpacity: 10,
},
},
overrides: [],
},
});
};
beforeEach(() => {
store.delete(LS_STYLES_COPY_KEY);
config.featureToggles.panelStyleActions = true;
});
afterEach(() => {
config.featureToggles.panelStyleActions = false;
});
it('Should copy panel styles when feature flag is enabled', () => {
const spy = jest.spyOn(DashboardInteractions, 'panelStylesMenuClicked');
const timeseriesPanel = createTimeseriesPanel();
scene.copyPanelStyles(timeseriesPanel);
expect(store.exists(LS_STYLES_COPY_KEY)).toBe(true);
const stored = JSON.parse(store.get(LS_STYLES_COPY_KEY) || '{}');
expect(stored.panelType).toBe('timeseries');
expect(stored.styles).toBeDefined();
expect(spy).not.toHaveBeenCalled(); // Analytics only called from menu
});
it('Should not copy panel styles when feature flag is disabled', () => {
config.featureToggles.panelStyleActions = false;
const timeseriesPanel = createTimeseriesPanel();
scene.copyPanelStyles(timeseriesPanel);
expect(store.exists(LS_STYLES_COPY_KEY)).toBe(false);
});
it('Should not copy styles for non-timeseries panels', () => {
const vizPanel = findVizPanelByKey(scene, 'panel-1')!;
scene.copyPanelStyles(vizPanel);
expect(store.exists(LS_STYLES_COPY_KEY)).toBe(false);
});
it('Should return false for hasPanelStylesToPaste when no styles copied', () => {
expect(DashboardScene.hasPanelStylesToPaste('timeseries')).toBe(false);
});
it('Should return false for hasPanelStylesToPaste when feature flag is disabled', () => {
store.set(LS_STYLES_COPY_KEY, JSON.stringify({ panelType: 'timeseries', styles: {} }));
config.featureToggles.panelStyleActions = false;
expect(DashboardScene.hasPanelStylesToPaste('timeseries')).toBe(false);
});
it('Should return true for hasPanelStylesToPaste when styles exist for matching panel type', () => {
store.set(LS_STYLES_COPY_KEY, JSON.stringify({ panelType: 'timeseries', styles: {} }));
expect(DashboardScene.hasPanelStylesToPaste('timeseries')).toBe(true);
});
it('Should return false for hasPanelStylesToPaste for different panel type', () => {
store.set(LS_STYLES_COPY_KEY, JSON.stringify({ panelType: 'timeseries', styles: {} }));
expect(DashboardScene.hasPanelStylesToPaste('table')).toBe(false);
});
it('Should paste panel styles when feature flag is enabled', () => {
const spy = jest.spyOn(DashboardInteractions, 'panelStylesMenuClicked');
const timeseriesPanel = createTimeseriesPanel();
const mockOnFieldConfigChange = jest.fn();
timeseriesPanel.onFieldConfigChange = mockOnFieldConfigChange;
const styles = {
panelType: 'timeseries',
styles: {
fieldConfig: {
defaults: {
color: { mode: 'palette-classic' },
custom: {
lineWidth: 2,
fillOpacity: 10,
},
},
},
},
};
store.set(LS_STYLES_COPY_KEY, JSON.stringify(styles));
scene.pastePanelStyles(timeseriesPanel);
expect(mockOnFieldConfigChange).toHaveBeenCalled();
expect(store.exists(LS_STYLES_COPY_KEY)).toBe(true);
expect(spy).not.toHaveBeenCalled();
});
it('Should not paste panel styles when feature flag is disabled', () => {
config.featureToggles.panelStyleActions = false;
const timeseriesPanel = createTimeseriesPanel();
const mockOnFieldConfigChange = jest.fn();
timeseriesPanel.onFieldConfigChange = mockOnFieldConfigChange;
const styles = {
panelType: 'timeseries',
styles: { fieldConfig: { defaults: {} } },
};
store.set(LS_STYLES_COPY_KEY, JSON.stringify(styles));
scene.pastePanelStyles(timeseriesPanel);
expect(mockOnFieldConfigChange).not.toHaveBeenCalled();
});
it('Should not paste styles when no styles are copied', () => {
const timeseriesPanel = createTimeseriesPanel();
const mockOnFieldConfigChange = jest.fn();
timeseriesPanel.onFieldConfigChange = mockOnFieldConfigChange;
scene.pastePanelStyles(timeseriesPanel);
expect(mockOnFieldConfigChange).not.toHaveBeenCalled();
});
it('Should not paste styles to different panel type', () => {
const spy = jest.spyOn(DashboardInteractions, 'panelStylesMenuClicked');
const timeseriesPanel = createTimeseriesPanel();
const mockOnFieldConfigChange = jest.fn();
timeseriesPanel.onFieldConfigChange = mockOnFieldConfigChange;
const styles = {
panelType: 'table',
styles: { fieldConfig: { defaults: {} } },
};
store.set(LS_STYLES_COPY_KEY, JSON.stringify(styles));
scene.pastePanelStyles(timeseriesPanel);
expect(mockOnFieldConfigChange).not.toHaveBeenCalled();
expect(spy).not.toHaveBeenCalled();
});
it('Should allow pasting styles multiple times', () => {
const spy = jest.spyOn(DashboardInteractions, 'panelStylesMenuClicked');
const timeseriesPanel1 = createTimeseriesPanel();
const timeseriesPanel2 = createTimeseriesPanel();
const mockOnFieldConfigChange1 = jest.fn();
const mockOnFieldConfigChange2 = jest.fn();
timeseriesPanel1.onFieldConfigChange = mockOnFieldConfigChange1;
timeseriesPanel2.onFieldConfigChange = mockOnFieldConfigChange2;
const styles = {
panelType: 'timeseries',
styles: { fieldConfig: { defaults: { custom: { lineWidth: 3 } } } },
};
store.set(LS_STYLES_COPY_KEY, JSON.stringify(styles));
scene.pastePanelStyles(timeseriesPanel1);
expect(mockOnFieldConfigChange1).toHaveBeenCalled();
expect(store.exists(LS_STYLES_COPY_KEY)).toBe(true);
scene.pastePanelStyles(timeseriesPanel2);
expect(mockOnFieldConfigChange2).toHaveBeenCalled();
expect(store.exists(LS_STYLES_COPY_KEY)).toBe(true);
expect(spy).not.toHaveBeenCalled();
});
it('Should report analytics on paste error', () => {
const spy = jest.spyOn(DashboardInteractions, 'panelStylesMenuClicked');
jest.spyOn(console, 'error').mockImplementation();
store.set(LS_STYLES_COPY_KEY, 'invalid json');
scene.pastePanelStyles(createTimeseriesPanel());
expect(spy).toHaveBeenCalledWith('paste', 'timeseries', expect.any(Number), true);
});
});
it('Should unlink a library panel', () => {
const libPanel = new VizPanel({
title: 'Panel B',

View file

@ -1,6 +1,6 @@
import * as H from 'history';
import { CoreApp, DataQueryRequest, locationUtil, NavIndex, NavModelItem, store } from '@grafana/data';
import { CoreApp, DataQueryRequest, FieldConfig, locationUtil, NavIndex, NavModelItem, store } from '@grafana/data';
import { t } from '@grafana/i18n';
import { config, locationService, RefreshEvent } from '@grafana/runtime';
import {
@ -19,7 +19,7 @@ import { Dashboard, DashboardLink, LibraryPanel } from '@grafana/schema';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { appEvents } from 'app/core/app_events';
import { ScrollRefElement } from 'app/core/components/NativeScrollbar';
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
import { LS_PANEL_COPY_KEY, LS_STYLES_COPY_KEY } from 'app/core/constants';
import { getNavModel } from 'app/core/selectors/navModel';
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
@ -34,6 +34,7 @@ import { DecoratedRevisionModel } from 'app/features/dashboard/types/revisionMod
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { DashboardJson } from 'app/features/manage-dashboards/types';
import { VariablesChanged } from 'app/features/variables/types';
import { defaultGraphStyleConfig } from 'app/plugins/panel/timeseries/config';
import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types/dashboard';
import { ShowConfirmModalEvent } from 'app/types/events';
@ -69,6 +70,7 @@ import { isRepeatCloneOrChildOf } from '../utils/clone';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { djb2Hash } from '../utils/djb2Hash';
import { getDashboardUrl } from '../utils/getDashboardUrl';
import { DashboardInteractions } from '../utils/interactions';
import {
getClosestVizPanel,
getDashboardSceneFor,
@ -98,6 +100,15 @@ export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'gra
export const PANEL_SEARCH_VAR = 'systemPanelFilterVar';
export const PANELS_PER_ROW_VAR = 'systemDynamicRowSizeVar';
type PanelStyles = {
fieldConfig?: { defaults: Partial<FieldConfig> };
};
type CopiedPanelStyles = {
panelType: string;
styles: PanelStyles;
};
export interface DashboardSceneState extends SceneObjectState {
/** The title */
title: string;
@ -651,6 +662,145 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
store.delete(LS_PANEL_COPY_KEY);
}
/**
* Hardcoded to Timeseries for this PoC
* @internal
*/
private static extractPanelStyles(panel: VizPanel): PanelStyles {
const styles: PanelStyles = {};
if (!panel.state.fieldConfig?.defaults) {
return styles;
}
styles.fieldConfig = { defaults: {} };
const defaults = styles.fieldConfig.defaults;
const panelDefaults = panel.state.fieldConfig.defaults;
// default props (color)
if (defaultGraphStyleConfig.fieldConfig?.defaultsProps) {
for (const key of defaultGraphStyleConfig.fieldConfig.defaultsProps) {
const value = panelDefaults[key];
if (value !== undefined) {
defaults[key] = value;
}
}
}
// custom props (lineWidth, fillOpacity, etc.)
if (panel.state.fieldConfig.defaults.custom && defaultGraphStyleConfig.fieldConfig?.defaults) {
const customDefaults: Record<string, unknown> = {};
const panelCustom: Record<string, unknown> = panel.state.fieldConfig.defaults.custom;
for (const key of defaultGraphStyleConfig.fieldConfig.defaults) {
const value = panelCustom[key];
if (value !== undefined) {
customDefaults[key] = value;
}
}
defaults.custom = customDefaults;
}
return styles;
}
/** @internal */
public copyPanelStyles(vizPanel: VizPanel) {
if (!config.featureToggles.panelStyleActions) {
return;
}
const panelType = vizPanel.state.pluginId;
if (panelType !== 'timeseries') {
return;
}
const stylesToCopy: CopiedPanelStyles = {
panelType,
styles: DashboardScene.extractPanelStyles(vizPanel),
};
store.set(LS_STYLES_COPY_KEY, JSON.stringify(stylesToCopy));
appEvents.emit('alert-success', ['Panel styles copied.']);
}
/** @internal */
public static hasPanelStylesToPaste(panelType: string): boolean {
if (!config.featureToggles.panelStyleActions) {
return false;
}
const stylesJson = store.get(LS_STYLES_COPY_KEY);
if (!stylesJson) {
return false;
}
try {
const stylesCopy: CopiedPanelStyles = JSON.parse(stylesJson);
return stylesCopy.panelType === panelType;
} catch (e) {
return false;
}
}
/** @internal */
public pastePanelStyles(vizPanel: VizPanel) {
if (!config.featureToggles.panelStyleActions) {
return;
}
const stylesJson = store.get(LS_STYLES_COPY_KEY);
if (!stylesJson) {
return;
}
try {
const stylesCopy: CopiedPanelStyles = JSON.parse(stylesJson);
const panelType = vizPanel.state.pluginId;
if (stylesCopy.panelType !== panelType) {
return;
}
if (!stylesCopy.styles.fieldConfig?.defaults) {
return;
}
const newDefaults = {
...vizPanel.state.fieldConfig?.defaults,
...stylesCopy.styles.fieldConfig.defaults,
};
if (stylesCopy.styles.fieldConfig.defaults.custom) {
newDefaults.custom = {
...vizPanel.state.fieldConfig?.defaults?.custom,
...stylesCopy.styles.fieldConfig.defaults.custom,
};
}
const newFieldConfig = {
...vizPanel.state.fieldConfig,
defaults: newDefaults,
};
vizPanel.onFieldConfigChange(newFieldConfig);
appEvents.emit('alert-success', ['Panel styles applied.']);
} catch (e) {
console.error('Error pasting panel styles:', e);
appEvents.emit('alert-error', ['Error pasting panel styles.']);
DashboardInteractions.panelStylesMenuClicked(
'paste',
vizPanel.state.pluginId,
getPanelIdForVizPanel(vizPanel) ?? -1,
true
);
}
}
public removePanel(panel: VizPanel) {
getLayoutManagerFor(panel).removePanel?.(panel);
}

View file

@ -5,6 +5,7 @@ import {
PluginExtensionPanelContext,
PluginExtensionTypes,
getDefaultTimeRange,
store,
toDataFrame,
urlUtil,
} from '@grafana/data';
@ -18,6 +19,7 @@ import {
VizPanel,
VizPanelMenu,
} from '@grafana/scenes';
import { LS_STYLES_COPY_KEY } from 'app/core/constants';
import { contextSrv } from 'app/core/services/context_srv';
import { GetExploreUrlArguments } from 'app/core/utils/explore';
import { grantUserPermissions } from 'app/features/alerting/unified/mocks';
@ -26,6 +28,7 @@ import * as storeModule from 'app/store/store';
import { AccessControlAction } from 'app/types/accessControl';
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
import { DashboardInteractions } from '../utils/interactions';
import { DashboardScene } from './DashboardScene';
import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks';
@ -849,6 +852,75 @@ describe('panelMenuBehavior', () => {
jest.restoreAllMocks();
});
});
describe('Panel styles menu', () => {
async function buildTimeseriesTestScene() {
const menu = new VizPanelMenu({ $behaviors: [panelMenuBehavior] });
const panel = new VizPanel({
title: 'Timeseries Panel',
pluginId: 'timeseries',
key: 'panel-ts',
menu,
});
panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false });
new DashboardScene({
title: 'My dashboard',
uid: 'dash-1',
meta: { canEdit: true },
body: DefaultGridLayoutManager.fromVizPanels([panel]),
});
menu.activate();
await new Promise((r) => setTimeout(r, 1));
return { menu, panel };
}
beforeEach(() => {
config.featureToggles.panelStyleActions = true;
});
afterEach(() => {
config.featureToggles.panelStyleActions = false;
store.delete(LS_STYLES_COPY_KEY);
});
it('should call analytics when copy styles is clicked', async () => {
const spy = jest.spyOn(DashboardInteractions, 'panelStylesMenuClicked');
const { menu } = await buildTimeseriesTestScene();
const copyItem = menu.state.items?.find((i) => i.text === 'Styles')?.subMenu?.[0];
copyItem?.onClick?.({} as never);
expect(spy).toHaveBeenCalledWith('copy', 'timeseries', expect.any(Number));
});
it('should call analytics when paste styles is clicked', async () => {
store.set(LS_STYLES_COPY_KEY, JSON.stringify({ panelType: 'timeseries', styles: {} }));
const spy = jest.spyOn(DashboardInteractions, 'panelStylesMenuClicked');
const { menu } = await buildTimeseriesTestScene();
const pasteItem = menu.state.items?.find((i) => i.text === 'Styles')?.subMenu?.[1];
pasteItem?.onClick?.({} as never);
expect(spy).toHaveBeenCalledWith('paste', 'timeseries', expect.any(Number));
});
it('should not show styles menu when feature flag is disabled', async () => {
config.featureToggles.panelStyleActions = false;
const { menu } = await buildTimeseriesTestScene();
expect(menu.state.items?.find((i) => i.text === 'Styles')).toBeUndefined();
});
it('should not show styles menu for non-timeseries panels', async () => {
const { menu } = await buildTestScene({});
expect(menu.state.items?.find((i) => i.text === 'Styles')).toBeUndefined();
});
});
});
interface SceneOptions {

View file

@ -347,6 +347,48 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
}
}
if (panel.state.pluginId === 'timeseries' && config.featureToggles.panelStyleActions) {
const stylesSubMenu: PanelMenuItem[] = [];
stylesSubMenu.push({
text: t('panel.header-menu.copy-styles', `Copy styles`),
iconClassName: 'copy',
onClick: () => {
DashboardInteractions.panelStylesMenuClicked(
'copy',
panel.state.pluginId,
getPanelIdForVizPanel(panel) ?? -1
);
dashboard.copyPanelStyles(panel);
},
});
if (DashboardScene.hasPanelStylesToPaste('timeseries')) {
stylesSubMenu.push({
text: t('panel.header-menu.paste-styles', `Paste styles`),
iconClassName: 'clipboard-alt',
onClick: () => {
DashboardInteractions.panelStylesMenuClicked(
'paste',
panel.state.pluginId,
getPanelIdForVizPanel(panel) ?? -1
);
dashboard.pastePanelStyles(panel);
},
});
}
items.push({
type: 'submenu',
text: t('panel.header-menu.styles', `Styles`),
iconClassName: 'palette',
subMenu: stylesSubMenu,
onClick: (e) => {
e.preventDefault();
},
});
}
if (moreSubMenu.length) {
items.push({
type: 'submenu',

View file

@ -6,7 +6,7 @@ import {
RowsLayoutRowKind,
TabsLayoutTabKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { LS_PANEL_COPY_KEY, LS_ROW_COPY_KEY, LS_TAB_COPY_KEY } from 'app/core/constants';
import { LS_PANEL_COPY_KEY, LS_ROW_COPY_KEY, LS_STYLES_COPY_KEY, LS_TAB_COPY_KEY } from 'app/core/constants';
import { deserializeAutoGridItem } from '../../serialization/layoutSerializers/AutoGridLayoutSerializer';
import { deserializeGridItem } from '../../serialization/layoutSerializers/DefaultGridLayoutSerializer';
@ -24,6 +24,7 @@ export function clearClipboard() {
store.delete(LS_PANEL_COPY_KEY);
store.delete(LS_ROW_COPY_KEY);
store.delete(LS_TAB_COPY_KEY);
store.delete(LS_STYLES_COPY_KEY);
}
export interface RowStore {

View file

@ -101,6 +101,11 @@ export const DashboardInteractions = {
reportDashboardInteraction('panel_action_clicked', { item, id, source });
},
// Panel styles copy/paste interactions
panelStylesMenuClicked(action: 'copy' | 'paste', panelType: string, panelId: number, error?: boolean) {
reportDashboardInteraction('panel_styles_menu_clicked', { action, panelType, panelId, error });
},
// Dashboard edit item actions
// dashboards_edit_action_clicked: when user adds or removes an item in edit mode
// props: { item: string } - item is one of: add_panel, group_row, group_tab, ungroup, paste_panel, remove_row, remove_tab

View file

@ -44,6 +44,54 @@ export const defaultGraphConfig: GraphFieldConfig = {
showValues: false,
};
/**
* Defines graph style configuration properties. Properties from GraphFieldConfig.
* Temporary config - PoC.
*/
export const defaultGraphStyleConfig = {
fieldConfig: {
defaultsProps: ['color'],
defaults: [
// Line config
'lineColor',
'lineInterpolation',
'lineStyle',
'lineWidth',
'spanNulls',
// Fill config
'fillBelowTo',
'fillColor',
'fillOpacity',
// Points config
'pointColor',
'pointSize',
'pointSymbol',
'showPoints',
// Axis config
'axisBorderShow',
'axisCenteredZero',
'axisColorMode',
'axisGridShow',
'axisLabel',
'axisPlacement',
'axisSoftMax',
'axisSoftMin',
'axisWidth',
// Graph field config
'drawStyle',
'gradientMode',
'insertNulls',
'showValues',
// Stacking
'stacking',
// Bar config
'barAlignment',
'barWidthFactor',
'barMaxWidth',
],
},
} as const;
export type NullEditorSettings = { isTime: boolean };
export function getGraphFieldConfig(cfg: GraphFieldConfig, isTime = true): SetFieldConfigOptionsArgs<GraphFieldConfig> {

View file

@ -11290,6 +11290,7 @@
},
"header-menu": {
"copy": "Copy",
"copy-styles": "Copy styles",
"create-library-panel": "Create library panel",
"duplicate": "Duplicate",
"edit": "Edit",
@ -11301,11 +11302,13 @@
"inspect-json": "Panel JSON",
"more": "More...",
"new-alert-rule": "New alert rule",
"paste-styles": "Paste styles",
"query": "Query",
"remove": "Remove",
"replace-library-panel": "Replace library panel",
"share": "Share",
"show-legend": "Show legend",
"styles": "Styles",
"time-settings": "Time settings",
"unlink-library-panel": "Unlink library panel",
"view": "View"