The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx

493 lines
14 KiB

import React, { PureComponent } from 'react';
import { FieldConfigSource, GrafanaTheme, PanelData, PanelPlugin } from '@grafana/data';
import { Button, stylesFactory, Icon, RadioButtonGroup } from '@grafana/ui';
import { css, cx } from 'emotion';
import config from 'app/core/config';
import AutoSizer from 'react-virtualized-auto-sizer';
import { PanelModel } from '../../state/PanelModel';
import { DashboardModel } from '../../state/DashboardModel';
import { DashboardPanel } from '../../dashgrid/DashboardPanel';
import SplitPane from 'react-split-pane';
import { StoreState } from '../../../../types/store';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { updateLocation } from '../../../../core/reducers/location';
import { Unsubscribable } from 'rxjs';
import { DisplayMode, displayModes, PanelEditorTab } from './types';
import { PanelEditorTabs } from './PanelEditorTabs';
import { DashNavTimeControls } from '../DashNav/DashNavTimeControls';
import { LocationState, CoreEvents } from 'app/types';
import { calculatePanelSize } from './utils';
import { initPanelEditor, panelEditorCleanUp, updatePanelEditorUIState } from './state/actions';
import { PanelEditorUIState, setDiscardChanges } from './state/reducers';
import { getPanelEditorTabs } from './state/selectors';
import { getPanelStateById } from '../../state/selectors';
import { OptionsPaneContent } from './OptionsPaneContent';
import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton';
import { VariableModel } from 'app/features/templating/types';
import { getVariables } from 'app/features/variables/state/selectors';
import { SubMenuItems } from 'app/features/dashboard/components/SubMenu/SubMenuItems';
import { BackButton } from 'app/core/components/BackButton/BackButton';
import { appEvents } from 'app/core/core';
import { SaveDashboardModalProxy } from '../SaveDashboard/SaveDashboardModalProxy';
interface OwnProps {
dashboard: DashboardModel;
sourcePanel: PanelModel;
}
interface ConnectedProps {
location: LocationState;
plugin?: PanelPlugin;
panel: PanelModel;
data: PanelData;
initDone: boolean;
tabs: PanelEditorTab[];
uiState: PanelEditorUIState;
variables: VariableModel[];
}
interface DispatchProps {
updateLocation: typeof updateLocation;
initPanelEditor: typeof initPanelEditor;
panelEditorCleanUp: typeof panelEditorCleanUp;
setDiscardChanges: typeof setDiscardChanges;
updatePanelEditorUIState: typeof updatePanelEditorUIState;
}
type Props = OwnProps & ConnectedProps & DispatchProps;
export class PanelEditorUnconnected extends PureComponent<Props> {
querySubscription: Unsubscribable;
componentDidMount() {
this.props.initPanelEditor(this.props.sourcePanel, this.props.dashboard);
}
componentWillUnmount() {
this.props.panelEditorCleanUp();
}
onPanelExit = () => {
this.props.updateLocation({
query: { editPanel: null, tab: null },
partial: true,
});
};
onDiscard = () => {
this.props.setDiscardChanges(true);
this.props.updateLocation({
query: { editPanel: null, tab: null },
partial: true,
});
};
onOpenDashboardSettings = () => {
this.props.updateLocation({ query: { editview: 'settings' }, partial: true });
};
onSaveDashboard = () => {
appEvents.emit(CoreEvents.showModalReact, {
component: SaveDashboardModalProxy,
props: { dashboard: this.props.dashboard },
});
};
onChangeTab = (tab: PanelEditorTab) => {
this.props.updateLocation({ query: { tab: tab.id }, partial: true });
};
FieldOverrides: Move FieldConfigSource from fieldOptions to PanelModel.fieldConfig (#22600) * Apply field overrides in PanelChrome * Move applyFieldOverrides to panel query runner * Review updates * Make sure overrides are applied back on souce panel when exiting the new edit mode * TS ignores in est * Make field display work in viz repeater * Review updates * Review and test updates * Change the way overrides and trransformations are retrieved in PQR * Add fieldConfig property to PanelModel * Dashboard migration v1 * Use field config when exiting new panel edit mode * Gauge - use fieldConfig from panel model * FieldDisplayOptions - don's extend FieldConfigSource * Fix fieldDisplay ts * StatPanel updated * Stat panel defaults applied * Table2 panel options update * React graph updates * BarGauge updated * PieChart, Gauge, BarGauge and Stat updates * PieChart - remove field config defaults from options * FieldDisplayEditor - remove unused methos * PanelModel - remove debugger * Remove fieldConfig from field options when migrating dashboard * Update data links migrations * Update fieldDisaplay tests to respect new fieldConfig * Update dashboard schema version in snapshots * Fix BarGaugePanel test * Rebase fixes * Add onFieldConfigChange to PanelProps type * Update shared single stat migration * Pass PanelModel instead of options only for panel type change handler [breaking] * Renames * Don't mutate panel options * Migrations update * Remove obsolete snap * Minor updates after review * Fix null checks * Temporarily (until we decide to switch to new pane edit) bring back old aditors * Temporarily rename ValueMappingEditor and MappingRow to Legacy* * Migrations update * Updae setFieldConfigDefaults API * Update the way field config defaults are applied * Use standard field config for gauge, bar gauge and stat panels * refactoring * Revert dashboard fieldOptions migrations as those are handled by single stat migrator * Fix ts in tests * Strict null fix and some minor fixes Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
5 years ago
onFieldConfigChange = (config: FieldConfigSource) => {
const { panel } = this.props;
FieldOverrides: Move FieldConfigSource from fieldOptions to PanelModel.fieldConfig (#22600) * Apply field overrides in PanelChrome * Move applyFieldOverrides to panel query runner * Review updates * Make sure overrides are applied back on souce panel when exiting the new edit mode * TS ignores in est * Make field display work in viz repeater * Review updates * Review and test updates * Change the way overrides and trransformations are retrieved in PQR * Add fieldConfig property to PanelModel * Dashboard migration v1 * Use field config when exiting new panel edit mode * Gauge - use fieldConfig from panel model * FieldDisplayOptions - don's extend FieldConfigSource * Fix fieldDisplay ts * StatPanel updated * Stat panel defaults applied * Table2 panel options update * React graph updates * BarGauge updated * PieChart, Gauge, BarGauge and Stat updates * PieChart - remove field config defaults from options * FieldDisplayEditor - remove unused methos * PanelModel - remove debugger * Remove fieldConfig from field options when migrating dashboard * Update data links migrations * Update fieldDisaplay tests to respect new fieldConfig * Update dashboard schema version in snapshots * Fix BarGaugePanel test * Rebase fixes * Add onFieldConfigChange to PanelProps type * Update shared single stat migration * Pass PanelModel instead of options only for panel type change handler [breaking] * Renames * Don't mutate panel options * Migrations update * Remove obsolete snap * Minor updates after review * Fix null checks * Temporarily (until we decide to switch to new pane edit) bring back old aditors * Temporarily rename ValueMappingEditor and MappingRow to Legacy* * Migrations update * Updae setFieldConfigDefaults API * Update the way field config defaults are applied * Use standard field config for gauge, bar gauge and stat panels * refactoring * Revert dashboard fieldOptions migrations as those are handled by single stat migrator * Fix ts in tests * Strict null fix and some minor fixes Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
5 years ago
panel.updateFieldConfig({
...config,
});
this.forceUpdate();
};
onPanelOptionsChanged = (options: any) => {
this.props.panel.updateOptions(options);
this.forceUpdate();
};
onPanelConfigChanged = (configKey: string, value: any) => {
// @ts-ignore
this.props.panel[configKey] = value;
this.props.panel.render();
this.forceUpdate();
};
onDragFinished = (pane: Pane, size?: number) => {
document.body.style.cursor = 'auto';
// When the drag handle is just clicked size is undefined
if (!size) {
return;
}
const targetPane = pane === Pane.Top ? 'topPaneSize' : 'rightPaneSize';
const { updatePanelEditorUIState } = this.props;
updatePanelEditorUIState({
[targetPane]: size,
});
};
onDragStarted = () => {
document.body.style.cursor = 'row-resize';
};
onDiplayModeChange = (mode: DisplayMode) => {
const { updatePanelEditorUIState } = this.props;
updatePanelEditorUIState({
mode: mode,
});
};
onTogglePanelOptions = () => {
const { uiState, updatePanelEditorUIState } = this.props;
updatePanelEditorUIState({ isPanelOptionsVisible: !uiState.isPanelOptionsVisible });
};
renderHorizontalSplit(styles: EditorStyles) {
const { dashboard, panel, tabs, data, uiState } = this.props;
return (
<SplitPane
split="horizontal"
minSize={50}
primary="first"
/* Use persisted state for default size */
defaultSize={uiState.topPaneSize}
pane2Style={{ minHeight: 0 }}
resizerClassName={styles.resizerH}
onDragStarted={this.onDragStarted}
onDragFinished={size => this.onDragFinished(Pane.Top, size)}
>
<div className={styles.mainPaneWrapper}>
{this.renderPanelToolbar(styles)}
<div className={styles.panelWrapper}>
<AutoSizer>
{({ width, height }) => {
if (width < 3 || height < 3) {
return null;
}
return (
<div className={styles.centeringContainer} style={{ width, height }}>
<div style={calculatePanelSize(uiState.mode, width, height, panel)}>
<DashboardPanel
dashboard={dashboard}
panel={panel}
isEditing={true}
isViewing={false}
isInView={true}
/>
</div>
</div>
);
}}
</AutoSizer>
</div>
</div>
<div className={styles.tabsWrapper}>
<PanelEditorTabs panel={panel} dashboard={dashboard} tabs={tabs} onChangeTab={this.onChangeTab} data={data} />
</div>
</SplitPane>
);
}
renderTemplateVariables(styles: EditorStyles) {
const { variables } = this.props;
if (!variables.length) {
return null;
}
return (
<div className={styles.variablesWrapper}>
<SubMenuItems variables={variables} />
</div>
);
}
renderPanelToolbar(styles: EditorStyles) {
const { dashboard, location, uiState } = this.props;
return (
<div className={styles.panelToolbar}>
{this.renderTemplateVariables(styles)}
<div className="flex-grow-1" />
<div className={styles.toolbarItem}>
<RadioButtonGroup value={uiState.mode} options={displayModes} onChange={this.onDiplayModeChange} />
</div>
<div className={styles.toolbarItem}>
<DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} />
</div>
{!uiState.isPanelOptionsVisible && (
<div className={styles.toolbarItem}>
<DashNavButton onClick={this.onTogglePanelOptions} tooltip="Open options pane" classSuffix="close-options">
@grafana/ui: Create Icon component and replace part of the icons (#23402) * Part1: Unicons implementation (#23197) * Create a new Icon component * Update icons in main sidebar * Update icons in Useful links and in react components on main site * Update icons in Useful links and in main top navigation * Adjust sizing * Update panel navigation and timepicker * Update icons in Panel menu * NewPanelEditor: Fixed so that test alert rule works in new edit mode (#23179) * Update icons in add panel widget * Resolve merge conflict * Fix part of the test errors and type errors * Fix storybook errors * Update getAvailableIcons import in storybook knobs * Fix import path * Fix SyntaxError: Cannot use import statement outside a module in test environment error * Remove dynamic imports * Remove types as using @ts-ignore * Update snapshot test * Add @iconscout/react-unicons to the shouldExclude list as it is blundled with es2015 syntax * Remove color prop from icon, remove color implemetation in mono icons * Update navbar styling * Move toPascalCase to utils/string Co-authored-by: Torkel Ödegaard <torkel@grafana.com> * Resolve type errors resulted from merge * Part2: Unicons implementation (#23266) * Create a new Icon component * Update icons in main sidebar * Update icons in Useful links and in react components on main site * Update icons in Useful links and in main top navigation * Adjust sizing * Update panel navigation and timepicker * Update icons in Panel menu * Update icons in add panel widget * Resolve merge conflict * Fix part of the test errors and type errors * Fix storybook errors * Update getAvailableIcons import in storybook knobs * Fix import path * Fix SyntaxError: Cannot use import statement outside a module in test environment error * Remove dynamic imports * Remove types as using @ts-ignore * Update snapshot test * Add @iconscout/react-unicons to the shouldExclude list as it is blundled with es2015 syntax * Implment icons in Tabs * Implement icons in search items and empty list * Update buttons * Update button-related snapshot tests * Update icons in modals and page headers * Create anfular wrapper and update all icons on search screen * Update sizing, remove colors, update snapshot tests * Remove color prop from icon, remove color implemetation in mono icons * Remove color props from monochrome icons * Complete update of icons for search screen * Update icons for infor tooltips, playlist, permissions * Support temporarly font awesome icons used in enterprise grafana * Part1: Unicons implementation (#23197) * Create a new Icon component * Update icons in main sidebar * Update icons in Useful links and in react components on main site * Update icons in Useful links and in main top navigation * Adjust sizing * Update panel navigation and timepicker * Update icons in Panel menu * NewPanelEditor: Fixed so that test alert rule works in new edit mode (#23179) * Update icons in add panel widget * Resolve merge conflict * Fix part of the test errors and type errors * Fix storybook errors * Update getAvailableIcons import in storybook knobs * Fix import path * Fix SyntaxError: Cannot use import statement outside a module in test environment error * Remove dynamic imports * Remove types as using @ts-ignore * Update snapshot test * Add @iconscout/react-unicons to the shouldExclude list as it is blundled with es2015 syntax * Remove color prop from icon, remove color implemetation in mono icons * Update navbar styling * Move toPascalCase to utils/string Co-authored-by: Torkel Ödegaard <torkel@grafana.com> * Icons update * Add optional chaining to for isFontAwesome variable Co-authored-by: Torkel Ödegaard <torkel@grafana.com> * Part3: Unicons implementation (#23356) * Create a new Icon component * Update icons in main sidebar * Update icons in Useful links and in react components on main site * Update icons in Useful links and in main top navigation * Adjust sizing * Update panel navigation and timepicker * Update icons in Panel menu * Update icons in add panel widget * Resolve merge conflict * Fix part of the test errors and type errors * Fix storybook errors * Update getAvailableIcons import in storybook knobs * Fix import path * Fix SyntaxError: Cannot use import statement outside a module in test environment error * Remove dynamic imports * Remove types as using @ts-ignore * Update snapshot test * Add @iconscout/react-unicons to the shouldExclude list as it is blundled with es2015 syntax * Implment icons in Tabs * Implement icons in search items and empty list * Update buttons * Update button-related snapshot tests * Update icons in modals and page headers * Create anfular wrapper and update all icons on search screen * Update sizing, remove colors, update snapshot tests * Remove color prop from icon, remove color implemetation in mono icons * Remove color props from monochrome icons * Complete update of icons for search screen * Update icons for infor tooltips, playlist, permissions * Support temporarly font awesome icons used in enterprise grafana * Part1: Unicons implementation (#23197) * Create a new Icon component * Update icons in main sidebar * Update icons in Useful links and in react components on main site * Update icons in Useful links and in main top navigation * Adjust sizing * Update panel navigation and timepicker * Update icons in Panel menu * NewPanelEditor: Fixed so that test alert rule works in new edit mode (#23179) * Update icons in add panel widget * Resolve merge conflict * Fix part of the test errors and type errors * Fix storybook errors * Update getAvailableIcons import in storybook knobs * Fix import path * Fix SyntaxError: Cannot use import statement outside a module in test environment error * Remove dynamic imports * Remove types as using @ts-ignore * Update snapshot test * Add @iconscout/react-unicons to the shouldExclude list as it is blundled with es2015 syntax * Remove color prop from icon, remove color implemetation in mono icons * Update navbar styling * Move toPascalCase to utils/string Co-authored-by: Torkel Ödegaard <torkel@grafana.com> * Update icons in Explore * Update icons in alerting * Update + and x buttons * Update icons in configurations and settings * Update close icons * Update icons in rich history * Update alert messages * Add optional chaining to for isFontAwesome variable * Remove icon mock, set up jest.config * Fix navbar plus icon * Fir enable-bacground to enableBackgournd Co-authored-by: Torkel Ödegaard <torkel@grafana.com> * Merge remote branch origin master to icons-unicons * Revert "Merge remote branch origin master to icons-unicons" This reverts commit 3f25d50a39a940883fefe73ce51219139c1ed37f. * Size-up dashnav icons * Fix alerting icons, panel headers, update tests * Fix typecheck error * Adjustments - add panel icon, spacing * Set TerserPlugin sourceMap to false to prevent running out of memory when publishing storybook Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
5 years ago
<Icon name="angle-left" /> <span style={{ paddingLeft: '6px' }}>Show options</span>
</DashNavButton>
</div>
)}
</div>
);
}
editorToolbar(styles: EditorStyles) {
const { dashboard } = this.props;
return (
<div className={styles.editorToolbar}>
<div className={styles.toolbarLeft}>
<BackButton onClick={this.onPanelExit} surface="panel" />
<span className={styles.editorTitle}>{dashboard.title} / Edit Panel</span>
</div>
<div className={styles.toolbarLeft}>
<div className={styles.toolbarItem}>
<Button
icon="cog"
onClick={this.onOpenDashboardSettings}
variant="secondary"
title="Open dashboad settings"
/>
</div>
<div className={styles.toolbarItem}>
<Button onClick={this.onDiscard} variant="secondary" title="Undo all changes">
Discard
</Button>
</div>
<div className={styles.toolbarItem}>
<Button onClick={this.onSaveDashboard} variant="secondary" title="Apply changes and save dashboard">
Save
</Button>
</div>
<div className={styles.toolbarItem}>
<Button onClick={this.onPanelExit} title="Apply changes and go back to dashboard">
Apply
</Button>
</div>
</div>
</div>
);
}
renderOptionsPane() {
const { plugin, dashboard, data, panel, uiState } = this.props;
if (!plugin) {
return <div />;
}
return (
<OptionsPaneContent
plugin={plugin}
dashboard={dashboard}
data={data}
panel={panel}
width={uiState.rightPaneSize as number}
onClose={this.onTogglePanelOptions}
onFieldConfigsChange={this.onFieldConfigChange}
onPanelOptionsChanged={this.onPanelOptionsChanged}
onPanelConfigChange={this.onPanelConfigChanged}
/>
);
}
renderWithOptionsPane(styles: EditorStyles) {
const { uiState } = this.props;
return (
<SplitPane
split="vertical"
minSize={100}
primary="second"
/* Use persisted state for default size */
defaultSize={uiState.rightPaneSize}
resizerClassName={styles.resizerV}
onDragStarted={() => (document.body.style.cursor = 'col-resize')}
onDragFinished={size => this.onDragFinished(Pane.Right, size)}
>
{this.renderHorizontalSplit(styles)}
{this.renderOptionsPane()}
</SplitPane>
);
}
render() {
const { initDone, uiState } = this.props;
const styles = getStyles(config.theme, this.props);
if (!initDone) {
return null;
}
return (
<div className={styles.wrapper}>
{this.editorToolbar(styles)}
<div className={styles.verticalSplitPanesWrapper}>
{uiState.isPanelOptionsVisible ? this.renderWithOptionsPane(styles) : this.renderHorizontalSplit(styles)}
</div>
</div>
);
}
}
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
const panel = state.panelEditorNew.getPanel();
const { plugin } = getPanelStateById(state.dashboard, panel.id);
return {
location: state.location,
plugin: plugin,
panel: state.panelEditorNew.getPanel(),
data: state.panelEditorNew.getData(),
initDone: state.panelEditorNew.initDone,
tabs: getPanelEditorTabs(state.location, plugin),
uiState: state.panelEditorNew.ui,
variables: getVariables(state),
};
};
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
updateLocation,
initPanelEditor,
panelEditorCleanUp,
setDiscardChanges,
updatePanelEditorUIState,
};
export const PanelEditor = connect(mapStateToProps, mapDispatchToProps)(PanelEditorUnconnected);
enum Pane {
Right,
Top,
}
/*
* Styles
*/
export const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => {
const { uiState } = props;
const handleColor = theme.palette.blue95;
const paneSpaceing = theme.spacing.md;
const resizer = css`
font-style: italic;
background: transparent;
border-top: 0;
border-right: 0;
border-bottom: 0;
border-left: 0;
border-color: transparent;
border-style: solid;
transition: 0.2s border-color ease-in-out;
&:hover {
border-color: ${handleColor};
}
`;
return {
wrapper: css`
width: 100%;
height: 100%;
position: fixed;
z-index: ${theme.zIndex.sidemenu};
top: 0;
left: 0;
right: 0;
bottom: 0;
background: ${theme.colors.dashboardBg};
display: flex;
flex-direction: column;
`,
verticalSplitPanesWrapper: css`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
position: relative;
`,
mainPaneWrapper: css`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
padding-right: ${uiState.isPanelOptionsVisible ? 0 : paneSpaceing};
`,
variablesWrapper: css`
display: flex;
`,
panelWrapper: css`
flex: 1 1 0;
min-height: 0;
width: 100%;
padding-left: ${paneSpaceing};
`,
resizerV: cx(
resizer,
css`
cursor: col-resize;
width: ${paneSpaceing};
border-right-width: 1px;
margin-top: 18px;
`
),
resizerH: cx(
resizer,
css`
height: ${paneSpaceing};
cursor: row-resize;
position: relative;
top: 0px;
z-index: 1;
border-top-width: 1px;
margin-left: ${paneSpaceing};
`
),
tabsWrapper: css`
height: 100%;
width: 100%;
`,
editorToolbar: css`
display: flex;
padding: ${theme.spacing.sm};
background: ${theme.colors.panelBg};
justify-content: space-between;
border-bottom: 1px solid ${theme.colors.panelBorder};
`,
panelToolbar: css`
display: flex;
padding: ${paneSpaceing} 0 ${paneSpaceing} ${paneSpaceing};
justify-content: space-between;
flex-wrap: wrap;
`,
toolbarLeft: css`
padding-left: ${theme.spacing.sm};
display: flex;
align-items: center;
`,
toolbarItem: css`
margin-right: ${theme.spacing.sm};
&:last-child {
margin-right: 0;
}
`,
centeringContainer: css`
display: flex;
justify-content: center;
align-items: center;
`,
editorTitle: css`
font-size: ${theme.typography.size.lg};
padding-left: ${theme.spacing.md};
`,
};
});
type EditorStyles = ReturnType<typeof getStyles>;