mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 22:12:34 +08:00
NewPanelEditor: Angular panel options, and angular component state to redux major change (#22448)
* NewPanelEdit: Added angular options to new panel editor and started looking and angular component state * Moved angular component state to redux * Close to working 100% * Think everything is working * AlertTab: Alert tab now gets angularComponent from redux * Fixed panel menu access to angular panel component * Added new tests * Fixed unit test * Fixed strict null errors * Fixed typescript issues * fixed issues
This commit is contained in:
@ -176,37 +176,50 @@ function buildFormats() {
|
||||
hasBuiltIndex = true;
|
||||
}
|
||||
|
||||
export function getValueFormat(id: string): ValueFormatter {
|
||||
export function getValueFormat(id?: string | null): ValueFormatter {
|
||||
if (!id) {
|
||||
return toFixedUnit('');
|
||||
}
|
||||
|
||||
if (!hasBuiltIndex) {
|
||||
buildFormats();
|
||||
}
|
||||
|
||||
const fmt = index[id];
|
||||
|
||||
if (!fmt && id) {
|
||||
const idx = id.indexOf(':');
|
||||
|
||||
if (idx > 0) {
|
||||
const key = id.substring(0, idx);
|
||||
const sub = id.substring(idx + 1);
|
||||
|
||||
if (key === 'prefix') {
|
||||
return toFixedUnit(sub, true);
|
||||
}
|
||||
|
||||
if (key === 'time') {
|
||||
return toDateTimeValueFormatter(sub);
|
||||
}
|
||||
|
||||
if (key === 'si') {
|
||||
const offset = getOffsetFromSIPrefix(sub.charAt(0));
|
||||
const unit = offset === 0 ? sub : sub.substring(1);
|
||||
return decimalSIPrefix(unit, offset);
|
||||
}
|
||||
|
||||
if (key === 'count') {
|
||||
return simpleCountUnit(sub);
|
||||
}
|
||||
|
||||
if (key === 'currency') {
|
||||
return currency(sub);
|
||||
}
|
||||
}
|
||||
|
||||
return toFixedUnit(id);
|
||||
}
|
||||
|
||||
return fmt;
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ import { DataLinkEditor } from './DataLinkEditor';
|
||||
import { useTheme } from '../../themes/ThemeContext';
|
||||
|
||||
interface DataLinksEditorProps {
|
||||
value: DataLink[];
|
||||
value?: DataLink[];
|
||||
onChange: (links: DataLink[], callback?: () => void) => void;
|
||||
suggestions: VariableSuggestion[];
|
||||
maxLinks?: number;
|
||||
@ -25,59 +25,61 @@ export const enableDatalinksPrismSyntax = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const DataLinksEditor: FC<DataLinksEditorProps> = React.memo(({ value, onChange, suggestions, maxLinks }) => {
|
||||
const theme = useTheme();
|
||||
enableDatalinksPrismSyntax();
|
||||
export const DataLinksEditor: FC<DataLinksEditorProps> = React.memo(
|
||||
({ value = [], onChange, suggestions, maxLinks }) => {
|
||||
const theme = useTheme();
|
||||
enableDatalinksPrismSyntax();
|
||||
|
||||
const onAdd = () => {
|
||||
onChange(value ? [...value, { url: '', title: '' }] : [{ url: '', title: '' }]);
|
||||
};
|
||||
const onAdd = () => {
|
||||
onChange(value ? [...value, { url: '', title: '' }] : [{ url: '', title: '' }]);
|
||||
};
|
||||
|
||||
const onLinkChanged = (linkIndex: number, newLink: DataLink, callback?: () => void) => {
|
||||
onChange(
|
||||
value.map((item, listIndex) => {
|
||||
if (linkIndex === listIndex) {
|
||||
return newLink;
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
callback
|
||||
const onLinkChanged = (linkIndex: number, newLink: DataLink, callback?: () => void) => {
|
||||
onChange(
|
||||
value.map((item, listIndex) => {
|
||||
if (linkIndex === listIndex) {
|
||||
return newLink;
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
const onRemove = (link: DataLink) => {
|
||||
onChange(value.filter(item => item !== link));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{value && value.length > 0 && (
|
||||
<div
|
||||
className={css`
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
`}
|
||||
>
|
||||
{value.map((link, index) => (
|
||||
<DataLinkEditor
|
||||
key={index.toString()}
|
||||
index={index}
|
||||
isLast={index === value.length - 1}
|
||||
value={link}
|
||||
onChange={onLinkChanged}
|
||||
onRemove={onRemove}
|
||||
suggestions={suggestions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!value || (value && value.length < (maxLinks || Infinity))) && (
|
||||
<Button variant="inverse" icon="fa fa-plus" onClick={() => onAdd()}>
|
||||
Add link
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const onRemove = (link: DataLink) => {
|
||||
onChange(value.filter(item => item !== link));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{value && value.length > 0 && (
|
||||
<div
|
||||
className={css`
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
`}
|
||||
>
|
||||
{value.map((link, index) => (
|
||||
<DataLinkEditor
|
||||
key={index.toString()}
|
||||
index={index}
|
||||
isLast={index === value.length - 1}
|
||||
value={link}
|
||||
onChange={onLinkChanged}
|
||||
onRemove={onRemove}
|
||||
suggestions={suggestions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!value || (value && value.length < (maxLinks || Infinity))) && (
|
||||
<Button variant="inverse" icon="fa fa-plus" onClick={() => onAdd()}>
|
||||
Add link
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
DataLinksEditor.displayName = 'DataLinksEditor';
|
||||
|
@ -105,10 +105,10 @@ export const BarGaugeCell = () => {
|
||||
{
|
||||
matcher: { id: FieldMatcherID.byName, options: 'Progress' },
|
||||
properties: [
|
||||
{ path: 'custom.width', value: '200' },
|
||||
{ path: 'custom.displayMode', value: 'gradient-gauge' },
|
||||
{ path: 'min', value: '0' },
|
||||
{ path: 'max', value: '100' },
|
||||
{ prop: 'width', value: '200', custom: true },
|
||||
{ prop: 'displayMode', value: 'gradient-gauge', custom: true },
|
||||
{ prop: 'min', value: '0' },
|
||||
{ prop: 'max', value: '100' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
@ -141,11 +141,11 @@ export const ColoredCells = () => {
|
||||
{
|
||||
matcher: { id: FieldMatcherID.byName, options: 'Progress' },
|
||||
properties: [
|
||||
{ path: 'custom.width', value: '80' },
|
||||
{ path: 'custom.displayMode', value: 'color-background' },
|
||||
{ path: 'min', value: '0' },
|
||||
{ path: 'max', value: '100' },
|
||||
{ path: 'thresholds', value: defaultThresholds },
|
||||
{ prop: 'width', value: '80', custom: true },
|
||||
{ prop: 'displayMode', value: 'color-background', custom: true },
|
||||
{ prop: 'min', value: '0' },
|
||||
{ prop: 'max', value: '100' },
|
||||
{ prop: 'thresholds', value: defaultThresholds },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
@ -6,7 +6,7 @@ import { action } from '@storybook/addon-actions';
|
||||
import { DataFrame } from '@grafana/data';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
|
||||
const TableInputStories = storiesOf('General/Table/Input', module);
|
||||
const TableInputStories = storiesOf('General/Experimental/TableInputCSV', module);
|
||||
|
||||
TableInputStories.addDecorator(withCenteredStory);
|
||||
|
||||
|
@ -6,21 +6,10 @@ import { ThemeContext } from '../../themes/ThemeContext';
|
||||
import { Input } from '../Input/Input';
|
||||
import { ColorPicker } from '../ColorPicker/ColorPicker';
|
||||
import { css } from 'emotion';
|
||||
import Select from '../Select/Select';
|
||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
||||
|
||||
const modes: Array<SelectableValue<ThresholdsMode>> = [
|
||||
{ value: ThresholdsMode.Absolute, label: 'Absolute', description: 'Pick thresholds based on the absolute values' },
|
||||
{
|
||||
value: ThresholdsMode.Percentage,
|
||||
label: 'Percentage',
|
||||
description: 'Pick threshold based on the percent between min/max',
|
||||
},
|
||||
];
|
||||
|
||||
export interface Props {
|
||||
showAlphaUI?: boolean;
|
||||
thresholds: ThresholdsConfig;
|
||||
thresholds?: ThresholdsConfig;
|
||||
onChange: (thresholds: ThresholdsConfig) => void;
|
||||
}
|
||||
|
||||
@ -34,25 +23,11 @@ interface ThresholdWithKey extends Threshold {
|
||||
|
||||
let counter = 100;
|
||||
|
||||
function toThresholdsWithKey(steps?: Threshold[]): ThresholdWithKey[] {
|
||||
if (!steps || steps.length === 0) {
|
||||
steps = [{ value: -Infinity, color: 'green' }];
|
||||
}
|
||||
|
||||
return steps.map(t => {
|
||||
return {
|
||||
color: t.color,
|
||||
value: t.value === null ? -Infinity : t.value,
|
||||
key: counter++,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const steps = toThresholdsWithKey(props.thresholds!.steps);
|
||||
const steps = toThresholdsWithKey(props.thresholds);
|
||||
steps[0].value = -Infinity;
|
||||
|
||||
this.state = { steps };
|
||||
@ -165,14 +140,16 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
onModeChanged = (item: SelectableValue<ThresholdsMode>) => {
|
||||
if (item.value) {
|
||||
this.props.onChange({
|
||||
...this.props.thresholds,
|
||||
...getThresholdOrDefault(this.props.thresholds),
|
||||
mode: item.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderInput = (threshold: ThresholdWithKey) => {
|
||||
const isPercent = this.props.thresholds.mode === ThresholdsMode.Percentage;
|
||||
const config = getThresholdOrDefault(this.props.thresholds);
|
||||
const isPercent = config.mode === ThresholdsMode.Percentage;
|
||||
|
||||
return (
|
||||
<div className="thresholds-row-input-inner">
|
||||
<span className="thresholds-row-input-inner-arrow" />
|
||||
@ -218,7 +195,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { steps } = this.state;
|
||||
const t = this.props.thresholds;
|
||||
|
||||
return (
|
||||
<PanelOptionsGroup title="Thresholds">
|
||||
<ThemeContext.Consumer>
|
||||
@ -243,12 +220,6 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{this.props.showAlphaUI && (
|
||||
<div>
|
||||
<Select options={modes} value={modes.filter(m => m.value === t.mode)} onChange={this.onModeChanged} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ThemeContext.Consumer>
|
||||
@ -257,8 +228,14 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export function thresholdsWithoutKey(thresholds: ThresholdsConfig, steps: ThresholdWithKey[]): ThresholdsConfig {
|
||||
export function thresholdsWithoutKey(
|
||||
thresholds: ThresholdsConfig | undefined,
|
||||
steps: ThresholdWithKey[]
|
||||
): ThresholdsConfig {
|
||||
thresholds = getThresholdOrDefault(thresholds);
|
||||
|
||||
const mode = thresholds.mode ?? ThresholdsMode.Absolute;
|
||||
|
||||
return {
|
||||
mode,
|
||||
steps: steps.map(t => {
|
||||
@ -267,3 +244,25 @@ export function thresholdsWithoutKey(thresholds: ThresholdsConfig, steps: Thresh
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function getThresholdOrDefault(thresholds?: ThresholdsConfig): ThresholdsConfig {
|
||||
return thresholds ?? { steps: [], mode: ThresholdsMode.Absolute };
|
||||
}
|
||||
|
||||
function toThresholdsWithKey(thresholds?: ThresholdsConfig): ThresholdWithKey[] {
|
||||
thresholds = getThresholdOrDefault(thresholds);
|
||||
|
||||
let steps: Threshold[] = thresholds.steps || [];
|
||||
|
||||
if (thresholds.steps && thresholds.steps.length === 0) {
|
||||
steps = [{ value: -Infinity, color: 'green' }];
|
||||
}
|
||||
|
||||
return steps.map(t => {
|
||||
return {
|
||||
color: t.color,
|
||||
value: t.value === null ? -Infinity : t.value,
|
||||
key: counter++,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { GrafanaThemeType, ThresholdsMode } from '@grafana/data';
|
||||
import { ThresholdsMode } from '@grafana/data';
|
||||
import { ThresholdsEditor, Props, thresholdsWithoutKey } from './ThresholdsEditor';
|
||||
import { colors } from '../../utils';
|
||||
import { mockThemeContext } from '../../themes/ThemeContext';
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import { connect, MapStateToProps, MapDispatchToProps } from 'react-redux';
|
||||
import { css } from 'emotion';
|
||||
import { Alert, Button } from '@grafana/ui';
|
||||
|
||||
@ -14,19 +13,28 @@ import StateHistory from './StateHistory';
|
||||
import 'app/features/alerting/AlertTabCtrl';
|
||||
|
||||
import { DashboardModel } from '../dashboard/state/DashboardModel';
|
||||
import { PanelModel, angularPanelUpdated } from '../dashboard/state/PanelModel';
|
||||
import { PanelModel } from '../dashboard/state/PanelModel';
|
||||
import { TestRuleResult } from './TestRuleResult';
|
||||
import { AppNotificationSeverity, StoreState } from 'app/types';
|
||||
import { PanelEditorTabIds, getPanelEditorTab } from '../dashboard/panel_editor/state/reducers';
|
||||
import { changePanelEditorTab } from '../dashboard/panel_editor/state/actions';
|
||||
import { CoreEvents } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
interface OwnProps {
|
||||
dashboard: DashboardModel;
|
||||
panel: PanelModel;
|
||||
}
|
||||
|
||||
interface ConnectedProps {
|
||||
angularPanelComponent: AngularComponent;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
changePanelEditorTab: typeof changePanelEditorTab;
|
||||
}
|
||||
|
||||
export type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||
|
||||
interface State {
|
||||
validatonMessage: string;
|
||||
}
|
||||
@ -42,7 +50,6 @@ class UnConnectedAlertTab extends PureComponent<Props, State> {
|
||||
|
||||
componentDidMount() {
|
||||
this.loadAlertTab();
|
||||
this.props.panel.events.on(angularPanelUpdated, this.onAngularPanelUpdated);
|
||||
}
|
||||
|
||||
onAngularPanelUpdated = () => {
|
||||
@ -60,13 +67,13 @@ class UnConnectedAlertTab extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
async loadAlertTab() {
|
||||
const { panel } = this.props;
|
||||
const { panel, angularPanelComponent } = this.props;
|
||||
|
||||
if (!this.element || !panel.angularPanel || this.component) {
|
||||
if (!this.element || !angularPanelComponent || this.component) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = panel.angularPanel.getScope();
|
||||
const scope = angularPanelComponent.getScope();
|
||||
|
||||
// When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet
|
||||
if (!scope.$$childHead) {
|
||||
@ -213,8 +220,12 @@ class UnConnectedAlertTab extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export const mapStateToProps = (state: StoreState) => ({});
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
|
||||
return {
|
||||
angularPanelComponent: state.dashboard.panels[props.panel.id].angularComponent,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = { changePanelEditorTab };
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { changePanelEditorTab };
|
||||
|
||||
export const AlertTab = hot(module)(connect(mapStateToProps, mapDispatchToProps)(UnConnectedAlertTab));
|
||||
export const AlertTab = connect(mapStateToProps, mapDispatchToProps)(UnConnectedAlertTab);
|
||||
|
@ -0,0 +1,122 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect, MapStateToProps, MapDispatchToProps } from 'react-redux';
|
||||
|
||||
// Utils & Services
|
||||
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
|
||||
|
||||
// Types
|
||||
import { PanelModel, DashboardModel } from '../../state';
|
||||
import { PanelPlugin, PanelPluginMeta } from '@grafana/data';
|
||||
import { PanelCtrl } from 'app/plugins/sdk';
|
||||
import { changePanelPlugin } from '../../state/actions';
|
||||
import { StoreState } from 'app/types';
|
||||
|
||||
interface OwnProps {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
plugin: PanelPlugin;
|
||||
}
|
||||
|
||||
interface ConnectedProps {
|
||||
angularPanelComponent: AngularComponent;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
changePanelPlugin: typeof changePanelPlugin;
|
||||
}
|
||||
|
||||
type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||
|
||||
export class AngularPanelOptionsUnconnected extends PureComponent<Props> {
|
||||
element?: HTMLElement;
|
||||
angularOptions: AngularComponent;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadAngularOptions();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (this.props.plugin !== prevProps.plugin) {
|
||||
this.cleanUpAngularOptions();
|
||||
}
|
||||
|
||||
this.loadAngularOptions();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.cleanUpAngularOptions();
|
||||
}
|
||||
|
||||
cleanUpAngularOptions() {
|
||||
if (this.angularOptions) {
|
||||
this.angularOptions.destroy();
|
||||
this.angularOptions = null;
|
||||
}
|
||||
}
|
||||
|
||||
loadAngularOptions() {
|
||||
const { panel, angularPanelComponent, changePanelPlugin } = this.props;
|
||||
|
||||
if (!this.element || !angularPanelComponent || this.angularOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = angularPanelComponent.getScope();
|
||||
|
||||
// When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet
|
||||
if (!scope.$$childHead) {
|
||||
setTimeout(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const panelCtrl: PanelCtrl = scope.$$childHead.ctrl;
|
||||
panelCtrl.initEditMode();
|
||||
panelCtrl.onPluginTypeChange = (plugin: PanelPluginMeta) => {
|
||||
changePanelPlugin(panel, plugin.id);
|
||||
};
|
||||
|
||||
let template = '';
|
||||
for (let i = 0; i < panelCtrl.editorTabs.length; i++) {
|
||||
template +=
|
||||
`
|
||||
<div class="panel-options-group" ng-cloak>` +
|
||||
(i > 0
|
||||
? `<div class="panel-options-group__header">
|
||||
<span class="panel-options-group__title">{{ctrl.editorTabs[${i}].title}}
|
||||
</span>
|
||||
</div>`
|
||||
: '') +
|
||||
`<div class="panel-options-group__body">
|
||||
<panel-editor-tab editor-tab="ctrl.editorTabs[${i}]" ctrl="ctrl"></panel-editor-tab>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const loader = getAngularLoader();
|
||||
const scopeProps = { ctrl: panelCtrl };
|
||||
|
||||
this.angularOptions = loader.load(this.element, scopeProps, template);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div ref={elem => (this.element = elem)} />;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
|
||||
return {
|
||||
angularPanelComponent: state.dashboard.panels[props.panel.id].angularComponent,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { changePanelPlugin };
|
||||
|
||||
export const AngularPanelOptions = connect(mapStateToProps, mapDispatchToProps)(AngularPanelOptionsUnconnected);
|
@ -27,6 +27,7 @@ import { FieldConfigEditor } from './FieldConfigEditor';
|
||||
import { OptionsGroup } from './OptionsGroup';
|
||||
import { getPanelEditorTabs } from './state/selectors';
|
||||
import { getPanelStateById } from '../../state/selectors';
|
||||
import { AngularPanelOptions } from './AngularPanelOptions';
|
||||
|
||||
enum Pane {
|
||||
Right,
|
||||
@ -99,12 +100,12 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
renderFieldOptions() {
|
||||
const { plugin, panel, data } = this.props;
|
||||
renderFieldOptions(plugin: PanelPlugin) {
|
||||
const { panel, data } = this.props;
|
||||
|
||||
const fieldOptions = panel.options['fieldOptions'] as FieldConfigSource;
|
||||
|
||||
if (!fieldOptions || !plugin) {
|
||||
if (!fieldOptions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -123,16 +124,8 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
/**
|
||||
* The existing visualization tab
|
||||
*/
|
||||
renderVisSettings() {
|
||||
const { data, panel } = this.props;
|
||||
const { plugin } = this.props;
|
||||
|
||||
if (!plugin) {
|
||||
return null;
|
||||
}
|
||||
renderPanelSettings(plugin: PanelPlugin) {
|
||||
const { data, panel, dashboard } = this.props;
|
||||
|
||||
if (plugin.editor && panel) {
|
||||
return (
|
||||
@ -142,7 +135,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
return <div>No editor (angular?)</div>;
|
||||
return <AngularPanelOptions panel={panel} dashboard={dashboard} plugin={plugin} />;
|
||||
}
|
||||
|
||||
onDragFinished = (pane: Pane, size: number) => {
|
||||
@ -260,11 +253,17 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
renderOptionsPane(styles: any) {
|
||||
const { plugin } = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.panelOptionsPane}>
|
||||
<CustomScrollbar>
|
||||
{this.renderFieldOptions()}
|
||||
<OptionsGroup title="Old settings">{this.renderVisSettings()}</OptionsGroup>
|
||||
{plugin && (
|
||||
<>
|
||||
{this.renderFieldOptions(plugin)}
|
||||
<OptionsGroup title={`${plugin.meta.name} options`}>{this.renderPanelSettings(plugin)}</OptionsGroup>
|
||||
</>
|
||||
)}
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
);
|
||||
|
@ -2,8 +2,9 @@ import { thunkTester } from '../../../../../../test/core/thunk/thunkTester';
|
||||
import { initialState } from './reducers';
|
||||
import { initPanelEditor, panelEditorCleanUp } from './actions';
|
||||
import { PanelEditorStateNew, closeCompleted } from './reducers';
|
||||
import { cleanUpEditPanel } from '../../../state/reducers';
|
||||
import { cleanUpEditPanel, panelModelAndPluginReady } from '../../../state/reducers';
|
||||
import { PanelModel, DashboardModel } from '../../../state';
|
||||
import { getPanelPlugin } from 'app/features/plugins/__mocks__/pluginMocks';
|
||||
|
||||
describe('panelEditor actions', () => {
|
||||
describe('initPanelEditor', () => {
|
||||
@ -27,7 +28,7 @@ describe('panelEditor actions', () => {
|
||||
});
|
||||
|
||||
describe('panelEditorCleanUp', () => {
|
||||
it('create update source panel', async () => {
|
||||
it('should update source panel', async () => {
|
||||
const sourcePanel = new PanelModel({ id: 12, type: 'graph' });
|
||||
const dashboard = new DashboardModel({
|
||||
panels: [{ id: 12, type: 'graph' }],
|
||||
@ -58,5 +59,66 @@ describe('panelEditor actions', () => {
|
||||
expect(sourcePanel.getOptions()).toEqual({ prop: true });
|
||||
expect(sourcePanel.id).toEqual(12);
|
||||
});
|
||||
|
||||
it('should dispatch panelModelAndPluginReady if type changed', async () => {
|
||||
const sourcePanel = new PanelModel({ id: 12, type: 'graph' });
|
||||
const dashboard = new DashboardModel({
|
||||
panels: [{ id: 12, type: 'graph' }],
|
||||
});
|
||||
|
||||
const panel = sourcePanel.getEditClone();
|
||||
panel.type = 'table';
|
||||
panel.plugin = getPanelPlugin({ id: 'table' });
|
||||
panel.updateOptions({ prop: true });
|
||||
|
||||
const state: PanelEditorStateNew = {
|
||||
...initialState,
|
||||
getPanel: () => panel,
|
||||
getSourcePanel: () => sourcePanel,
|
||||
querySubscription: { unsubscribe: jest.fn() },
|
||||
};
|
||||
|
||||
const dispatchedActions = await thunkTester({
|
||||
panelEditorNew: state,
|
||||
dashboard: {
|
||||
getModel: () => dashboard,
|
||||
},
|
||||
})
|
||||
.givenThunk(panelEditorCleanUp)
|
||||
.whenThunkIsDispatched();
|
||||
|
||||
expect(dispatchedActions.length).toBe(3);
|
||||
expect(dispatchedActions[0].type).toBe(panelModelAndPluginReady.type);
|
||||
});
|
||||
|
||||
it('should discard changes when shouldDiscardChanges is true', async () => {
|
||||
const sourcePanel = new PanelModel({ id: 12, type: 'graph' });
|
||||
const dashboard = new DashboardModel({
|
||||
panels: [{ id: 12, type: 'graph' }],
|
||||
});
|
||||
|
||||
const panel = sourcePanel.getEditClone();
|
||||
panel.updateOptions({ prop: true });
|
||||
|
||||
const state: PanelEditorStateNew = {
|
||||
...initialState,
|
||||
shouldDiscardChanges: true,
|
||||
getPanel: () => panel,
|
||||
getSourcePanel: () => sourcePanel,
|
||||
querySubscription: { unsubscribe: jest.fn() },
|
||||
};
|
||||
|
||||
const dispatchedActions = await thunkTester({
|
||||
panelEditorNew: state,
|
||||
dashboard: {
|
||||
getModel: () => dashboard,
|
||||
},
|
||||
})
|
||||
.givenThunk(panelEditorCleanUp)
|
||||
.whenThunkIsDispatched();
|
||||
|
||||
expect(dispatchedActions.length).toBe(2);
|
||||
expect(sourcePanel.getOptions()).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
setPanelEditorUIState,
|
||||
PANEL_EDITOR_UI_STATE_STORAGE_KEY,
|
||||
} from './reducers';
|
||||
import { cleanUpEditPanel } from '../../../state/reducers';
|
||||
import { cleanUpEditPanel, panelModelAndPluginReady } from '../../../state/reducers';
|
||||
import store from '../../../../../core/store';
|
||||
|
||||
export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardModel): ThunkResult<void> {
|
||||
@ -40,17 +40,22 @@ export function panelEditorCleanUp(): ThunkResult<void> {
|
||||
const panel = getPanel();
|
||||
const modifiedSaveModel = panel.getSaveModel();
|
||||
const sourcePanel = getSourcePanel();
|
||||
const panelTypeChanged = sourcePanel.type !== panel.type;
|
||||
|
||||
// restore the source panel id before we update source panel
|
||||
modifiedSaveModel.id = sourcePanel.id;
|
||||
|
||||
sourcePanel.restoreModel(modifiedSaveModel);
|
||||
|
||||
if (panelTypeChanged) {
|
||||
dispatch(panelModelAndPluginReady({ panelId: sourcePanel.id, plugin: panel.plugin }));
|
||||
}
|
||||
|
||||
// Resend last query result on source panel query runner
|
||||
// But do this after the panel edit editor exit process has completed
|
||||
setTimeout(() => {
|
||||
sourcePanel.getQueryRunner().pipeDataToSubject(panel.getQueryRunner().getLastResult());
|
||||
});
|
||||
}, 20);
|
||||
}
|
||||
|
||||
dashboard.exitPanelEditor();
|
||||
|
@ -67,7 +67,6 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
||||
},
|
||||
"id": 1,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -181,7 +180,6 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
||||
},
|
||||
"id": 1,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -275,7 +273,6 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
||||
},
|
||||
"id": 1,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -401,7 +398,6 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
},
|
||||
"id": 1,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -513,7 +509,6 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
},
|
||||
"id": 1,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -610,7 +605,6 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
},
|
||||
"id": 1,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -704,7 +698,6 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
},
|
||||
"id": 1,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
|
@ -160,8 +160,13 @@ export class DashboardPanelUnconnected extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
|
||||
const panelState = state.dashboard.panels[props.panel.id];
|
||||
if (!panelState) {
|
||||
return { plugin: null };
|
||||
}
|
||||
|
||||
return {
|
||||
plugin: state.plugins.panels[props.panel.type],
|
||||
plugin: panelState.plugin,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -2,16 +2,22 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
import { connect, MapStateToProps, MapDispatchToProps } from 'react-redux';
|
||||
|
||||
// Components
|
||||
import { PanelHeader } from './PanelHeader/PanelHeader';
|
||||
|
||||
// Utils & Services
|
||||
import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
|
||||
import { getAngularLoader } from '@grafana/runtime';
|
||||
import { getAngularLoader, AngularComponent } from '@grafana/runtime';
|
||||
import { setPanelAngularComponent } from '../state/reducers';
|
||||
|
||||
// Types
|
||||
import { DashboardModel, PanelModel } from '../state';
|
||||
import { StoreState } from 'app/types';
|
||||
import { LoadingState, DefaultTimeRange, PanelData, PanelPlugin, PanelEvents } from '@grafana/data';
|
||||
|
||||
export interface Props {
|
||||
interface OwnProps {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
plugin: PanelPlugin;
|
||||
@ -21,6 +27,16 @@ export interface Props {
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface ConnectedProps {
|
||||
angularComponent: AngularComponent;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
setPanelAngularComponent: typeof setPanelAngularComponent;
|
||||
}
|
||||
|
||||
export type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||
|
||||
export interface State {
|
||||
data: PanelData;
|
||||
errorMessage?: string;
|
||||
@ -36,7 +52,7 @@ interface AngularScopeProps {
|
||||
};
|
||||
}
|
||||
|
||||
export class PanelChromeAngular extends PureComponent<Props, State> {
|
||||
export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
|
||||
element: HTMLElement | null = null;
|
||||
timeSrv: TimeSrv = getTimeSrv();
|
||||
scopeProps?: AngularScopeProps;
|
||||
@ -127,10 +143,10 @@ export class PanelChromeAngular extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
loadAngularPanel() {
|
||||
const { panel, dashboard, height, width } = this.props;
|
||||
const { panel, dashboard, height, width, setPanelAngularComponent } = this.props;
|
||||
|
||||
// if we have no element or already have loaded the panel return
|
||||
if (!this.element || panel.angularPanel) {
|
||||
if (!this.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -143,19 +159,23 @@ export class PanelChromeAngular extends PureComponent<Props, State> {
|
||||
size: { width, height },
|
||||
};
|
||||
|
||||
// compile angular template and get back handle to scope
|
||||
panel.setAngularPanel(loader.load(this.element, this.scopeProps, template));
|
||||
setPanelAngularComponent({
|
||||
panelId: panel.id,
|
||||
angularComponent: loader.load(this.element, this.scopeProps, template),
|
||||
});
|
||||
|
||||
// need to to this every time we load an angular as all events are unsubscribed when panel is destroyed
|
||||
this.subscribeToRenderEvent();
|
||||
}
|
||||
|
||||
cleanUpAngularPanel() {
|
||||
const { panel } = this.props;
|
||||
const { angularComponent, setPanelAngularComponent, panel } = this.props;
|
||||
|
||||
if (panel.angularPanel) {
|
||||
panel.setAngularPanel(undefined);
|
||||
if (angularComponent) {
|
||||
angularComponent.destroy();
|
||||
}
|
||||
|
||||
setPanelAngularComponent({ panelId: panel.id, angularComponent: null });
|
||||
}
|
||||
|
||||
hasOverlayHeader() {
|
||||
@ -176,7 +196,7 @@ export class PanelChromeAngular extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard, panel, isFullscreen, plugin } = this.props;
|
||||
const { dashboard, panel, isFullscreen, plugin, angularComponent } = this.props;
|
||||
const { errorMessage, data, alertState } = this.state;
|
||||
const { transparent } = panel;
|
||||
|
||||
@ -203,6 +223,7 @@ export class PanelChromeAngular extends PureComponent<Props, State> {
|
||||
title={panel.title}
|
||||
description={panel.description}
|
||||
scopedVars={panel.scopedVars}
|
||||
angularComponent={angularComponent}
|
||||
links={panel.links}
|
||||
error={errorMessage}
|
||||
isFullscreen={isFullscreen}
|
||||
@ -215,3 +236,13 @@ export class PanelChromeAngular extends PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
|
||||
return {
|
||||
angularComponent: state.dashboard.panels[props.panel.id].angularComponent,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { setPanelAngularComponent };
|
||||
|
||||
export const PanelChromeAngular = connect(mapStateToProps, mapDispatchToProps)(PanelChromeAngularUnconnected);
|
||||
|
@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash';
|
||||
import { DataLink, ScopedVars, PanelMenuItem } from '@grafana/data';
|
||||
import { AngularComponent } from '@grafana/runtime';
|
||||
import { ClickOutsideWrapper } from '@grafana/ui';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
@ -21,6 +22,7 @@ export interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
scopedVars?: ScopedVars;
|
||||
angularComponent?: AngularComponent;
|
||||
links?: DataLink[];
|
||||
error?: string;
|
||||
isFullscreen: boolean;
|
||||
@ -67,8 +69,8 @@ export class PanelHeader extends Component<Props, State> {
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
const { dashboard, panel } = this.props;
|
||||
const menuItems = getPanelMenu(dashboard, panel);
|
||||
const { dashboard, panel, angularComponent } = this.props;
|
||||
const menuItems = getPanelMenu(dashboard, panel, angularComponent);
|
||||
|
||||
this.setState({
|
||||
panelMenuOpen: !this.state.panelMenuOpen,
|
||||
|
@ -143,7 +143,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 1,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -171,7 +170,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 2,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -199,7 +197,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 3,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -227,7 +224,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 4,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -278,7 +274,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 1,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -390,7 +385,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 1,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -418,7 +412,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 2,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -446,7 +439,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 3,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -474,7 +466,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 4,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -525,7 +516,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 2,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -637,7 +627,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 1,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -665,7 +654,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 2,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -693,7 +681,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 3,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -721,7 +708,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 4,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -772,7 +758,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 3,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -884,7 +869,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 1,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -912,7 +896,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 2,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -940,7 +923,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 3,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -968,7 +950,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 4,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
@ -1019,7 +1000,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
"id": 4,
|
||||
"isInView": false,
|
||||
"options": Object {},
|
||||
"restoreModel": [Function],
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
|
@ -1,21 +1,32 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect, MapStateToProps, MapDispatchToProps } from 'react-redux';
|
||||
// Utils & Services
|
||||
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
|
||||
// Types
|
||||
import { PanelModel, DashboardModel } from '../state';
|
||||
import { angularPanelUpdated } from '../state/PanelModel';
|
||||
import { PanelPlugin, PanelPluginMeta } from '@grafana/data';
|
||||
import { PanelCtrl } from 'app/plugins/sdk';
|
||||
import { changePanelPlugin } from '../state/actions';
|
||||
import { StoreState } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
interface OwnProps {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
plugin: PanelPlugin;
|
||||
onPluginTypeChange: (newType: PanelPluginMeta) => void;
|
||||
}
|
||||
|
||||
export class AngularPanelOptions extends PureComponent<Props> {
|
||||
interface ConnectedProps {
|
||||
angularPanelComponent: AngularComponent;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
changePanelPlugin: typeof changePanelPlugin;
|
||||
}
|
||||
|
||||
type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||
|
||||
export class AngularPanelOptionsUnconnected extends PureComponent<Props> {
|
||||
element?: HTMLElement;
|
||||
angularOptions: AngularComponent;
|
||||
|
||||
@ -25,13 +36,8 @@ export class AngularPanelOptions extends PureComponent<Props> {
|
||||
|
||||
componentDidMount() {
|
||||
this.loadAngularOptions();
|
||||
this.props.panel.events.on(angularPanelUpdated, this.onAngularPanelUpdated);
|
||||
}
|
||||
|
||||
onAngularPanelUpdated = () => {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (this.props.plugin !== prevProps.plugin) {
|
||||
this.cleanUpAngularOptions();
|
||||
@ -42,7 +48,6 @@ export class AngularPanelOptions extends PureComponent<Props> {
|
||||
|
||||
componentWillUnmount() {
|
||||
this.cleanUpAngularOptions();
|
||||
this.props.panel.events.off(angularPanelUpdated, this.onAngularPanelUpdated);
|
||||
}
|
||||
|
||||
cleanUpAngularOptions() {
|
||||
@ -53,13 +58,13 @@ export class AngularPanelOptions extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
loadAngularOptions() {
|
||||
const { panel } = this.props;
|
||||
const { panel, angularPanelComponent, changePanelPlugin } = this.props;
|
||||
|
||||
if (!this.element || !panel.angularPanel || this.angularOptions) {
|
||||
if (!this.element || !angularPanelComponent || this.angularOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = panel.angularPanel.getScope();
|
||||
const scope = angularPanelComponent.getScope();
|
||||
|
||||
// When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet
|
||||
if (!scope.$$childHead) {
|
||||
@ -71,7 +76,9 @@ export class AngularPanelOptions extends PureComponent<Props> {
|
||||
|
||||
const panelCtrl: PanelCtrl = scope.$$childHead.ctrl;
|
||||
panelCtrl.initEditMode();
|
||||
panelCtrl.onPluginTypeChange = this.props.onPluginTypeChange;
|
||||
panelCtrl.onPluginTypeChange = (plugin: PanelPluginMeta) => {
|
||||
changePanelPlugin(panel, plugin.id);
|
||||
};
|
||||
|
||||
let template = '';
|
||||
for (let i = 0; i < panelCtrl.editorTabs.length; i++) {
|
||||
@ -101,3 +108,13 @@ export class AngularPanelOptions extends PureComponent<Props> {
|
||||
return <div ref={elem => (this.element = elem)} />;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
|
||||
return {
|
||||
angularPanelComponent: state.dashboard.panels[props.panel.id].angularComponent,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { changePanelPlugin };
|
||||
|
||||
export const AngularPanelOptions = connect(mapStateToProps, mapDispatchToProps)(AngularPanelOptionsUnconnected);
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
// Utils & Services
|
||||
import { AngularComponent } from '@grafana/runtime';
|
||||
import { connect } from 'react-redux';
|
||||
import { StoreState } from 'app/types';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
@ -37,7 +36,6 @@ interface State {
|
||||
|
||||
export class VisualizationTab extends PureComponent<Props, State> {
|
||||
element: HTMLElement;
|
||||
angularOptions: AngularComponent;
|
||||
querySubscription: Unsubscribable;
|
||||
|
||||
constructor(props: Props) {
|
||||
@ -65,14 +63,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
const { plugin, dashboard, panel } = this.props;
|
||||
|
||||
if (plugin.angularPanelCtrl) {
|
||||
return (
|
||||
<AngularPanelOptions
|
||||
plugin={plugin}
|
||||
dashboard={dashboard}
|
||||
panel={panel}
|
||||
onPluginTypeChange={this.onPluginTypeChange}
|
||||
/>
|
||||
);
|
||||
return <AngularPanelOptions plugin={plugin} dashboard={dashboard} panel={panel} />;
|
||||
}
|
||||
|
||||
if (plugin.editor) {
|
||||
|
@ -146,23 +146,6 @@ describe('PanelModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changing from angular panel', () => {
|
||||
const angularPanel = {
|
||||
scope: {},
|
||||
destroy: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
model.angularPanel = angularPanel;
|
||||
model.changePlugin(getPanelPlugin({ id: 'graph' }));
|
||||
});
|
||||
|
||||
it('should set angularPanel to undefined and call destory', () => {
|
||||
expect(angularPanel.destroy.mock.calls.length).toBe(1);
|
||||
expect(model.angularPanel).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changing to react panel from angular panel', () => {
|
||||
let panelQueryRunner: any;
|
||||
|
||||
|
@ -13,7 +13,6 @@ import {
|
||||
DataTransformerConfig,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
import { AngularComponent } from '@grafana/runtime';
|
||||
import { EDIT_PANEL_ID } from 'app/core/constants';
|
||||
|
||||
import config from 'app/core/config';
|
||||
@ -24,7 +23,6 @@ import { take } from 'rxjs/operators';
|
||||
|
||||
export const panelAdded = eventFactory<PanelModel | undefined>('panel-added');
|
||||
export const panelRemoved = eventFactory<PanelModel | undefined>('panel-removed');
|
||||
export const angularPanelUpdated = eventFactory('panel-angular-panel-updated');
|
||||
|
||||
export interface GridPos {
|
||||
x: number;
|
||||
@ -43,8 +41,6 @@ const notPersistedProperties: { [str: string]: boolean } = {
|
||||
cachedPluginOptions: true,
|
||||
plugin: true,
|
||||
queryRunner: true,
|
||||
angularPanel: true,
|
||||
restoreModel: true,
|
||||
};
|
||||
|
||||
// For angular panels we need to clean up properties when changing type
|
||||
@ -139,7 +135,6 @@ export class PanelModel {
|
||||
cachedPluginOptions?: any;
|
||||
legend?: { show: boolean };
|
||||
plugin?: PanelPlugin;
|
||||
angularPanel?: AngularComponent;
|
||||
|
||||
private queryRunner?: PanelQueryRunner;
|
||||
|
||||
@ -152,7 +147,7 @@ export class PanelModel {
|
||||
}
|
||||
|
||||
/** Given a persistened PanelModel restores property values */
|
||||
restoreModel = (model: any) => {
|
||||
restoreModel(model: any) {
|
||||
// copy properties from persisted model
|
||||
for (const property in model) {
|
||||
(this as any)[property] = model[property];
|
||||
@ -163,7 +158,7 @@ export class PanelModel {
|
||||
|
||||
// queries must have refId
|
||||
this.ensureQueryIds();
|
||||
};
|
||||
}
|
||||
|
||||
ensureQueryIds() {
|
||||
if (this.targets && _.isArray(this.targets)) {
|
||||
@ -296,10 +291,6 @@ export class PanelModel {
|
||||
const oldPluginId = this.type;
|
||||
const wasAngular = !!this.plugin.angularPanelCtrl;
|
||||
|
||||
if (this.angularPanel) {
|
||||
this.setAngularPanel(undefined);
|
||||
}
|
||||
|
||||
// remove panel type specific options
|
||||
for (const key of _.keys(this)) {
|
||||
if (mustKeepProps[key]) {
|
||||
@ -395,26 +386,12 @@ export class PanelModel {
|
||||
this.queryRunner.destroy();
|
||||
this.queryRunner = null;
|
||||
}
|
||||
|
||||
if (this.angularPanel) {
|
||||
this.angularPanel.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
setTransformations(transformations: DataTransformerConfig[]) {
|
||||
this.transformations = transformations;
|
||||
this.getQueryRunner().setTransformations(transformations);
|
||||
}
|
||||
|
||||
setAngularPanel(component: AngularComponent) {
|
||||
if (this.angularPanel) {
|
||||
// this will remove all event listeners
|
||||
this.angularPanel.destroy();
|
||||
}
|
||||
|
||||
this.angularPanel = component;
|
||||
this.events.emit(angularPanelUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
function getPluginVersion(plugin: PanelPlugin): string {
|
||||
|
@ -3,7 +3,7 @@ import { getBackendSrv } from '@grafana/runtime';
|
||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||
// Actions
|
||||
import { loadPluginDashboards } from '../../plugins/state/actions';
|
||||
import { loadDashboardPermissions, panelModelAndPluginReady } from './reducers';
|
||||
import { loadDashboardPermissions, panelModelAndPluginReady, setPanelAngularComponent } from './reducers';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { loadPanelPlugin } from 'app/features/plugins/state/actions';
|
||||
// Types
|
||||
@ -134,12 +134,20 @@ export function changePanelPlugin(panel: PanelModel, pluginId: string): ThunkRes
|
||||
return;
|
||||
}
|
||||
|
||||
let plugin = getStore().plugins.panels[pluginId];
|
||||
const store = getStore();
|
||||
let plugin = store.plugins.panels[pluginId];
|
||||
|
||||
if (!plugin) {
|
||||
plugin = await dispatch(loadPanelPlugin(pluginId));
|
||||
}
|
||||
|
||||
// clean up angular component (scope / ctrl state)
|
||||
const angularComponent = store.dashboard.panels[panel.id].angularComponent;
|
||||
if (angularComponent) {
|
||||
angularComponent.destroy();
|
||||
dispatch(setPanelAngularComponent({ panelId: panel.id, angularComponent: null }));
|
||||
}
|
||||
|
||||
panel.changePlugin(plugin);
|
||||
|
||||
dispatch(panelModelAndPluginReady({ panelId: panel.id, plugin }));
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
PanelState,
|
||||
QueriesToUpdateOnDashboardLoad,
|
||||
} from 'app/types';
|
||||
import { AngularComponent } from '@grafana/runtime';
|
||||
import { EDIT_PANEL_ID } from 'app/core/constants';
|
||||
import { processAclItems } from 'app/core/utils/acl';
|
||||
import { panelEditorReducer } from '../panel_editor/state/reducers';
|
||||
@ -82,6 +83,9 @@ const dashbardSlice = createSlice({
|
||||
cleanUpEditPanel: (state, action: PayloadAction) => {
|
||||
delete state.panels[EDIT_PANEL_ID];
|
||||
},
|
||||
setPanelAngularComponent: (state: DashboardState, action: PayloadAction<SetPanelAngularComponentPayload>) => {
|
||||
updatePanelState(state, action.payload.panelId, { angularComponent: action.payload.angularComponent });
|
||||
},
|
||||
addPanel: (state, action: PayloadAction<PanelModel>) => {
|
||||
state.panels[action.payload.id] = { pluginId: action.payload.type };
|
||||
},
|
||||
@ -101,6 +105,11 @@ export interface PanelModelAndPluginReadyPayload {
|
||||
plugin: PanelPlugin;
|
||||
}
|
||||
|
||||
export interface SetPanelAngularComponentPayload {
|
||||
panelId: number;
|
||||
angularComponent: AngularComponent | null;
|
||||
}
|
||||
|
||||
export const {
|
||||
loadDashboardPermissions,
|
||||
dashboardInitFetching,
|
||||
@ -114,6 +123,7 @@ export const {
|
||||
panelModelAndPluginReady,
|
||||
addPanel,
|
||||
cleanUpEditPanel,
|
||||
setPanelAngularComponent,
|
||||
} = dashbardSlice.actions;
|
||||
|
||||
export const dashboardReducer = dashbardSlice.reducer;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { store } from 'app/store/store';
|
||||
import config from 'app/core/config';
|
||||
import { getDataSourceSrv, getLocationSrv } from '@grafana/runtime';
|
||||
import { getDataSourceSrv, getLocationSrv, AngularComponent } from '@grafana/runtime';
|
||||
import { PanelMenuItem } from '@grafana/data';
|
||||
import { copyPanel, duplicatePanel, editPanelJson, removePanel, sharePanel } from 'app/features/dashboard/utils/panel';
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
@ -12,7 +12,11 @@ import { getExploreUrl } from '../../../core/utils/explore';
|
||||
import { getTimeSrv } from '../services/TimeSrv';
|
||||
import { PanelCtrl } from '../../panel/panel_ctrl';
|
||||
|
||||
export function getPanelMenu(dashboard: DashboardModel, panel: PanelModel): PanelMenuItem[] {
|
||||
export function getPanelMenu(
|
||||
dashboard: DashboardModel,
|
||||
panel: PanelModel,
|
||||
angularComponent?: AngularComponent
|
||||
): PanelMenuItem[] {
|
||||
const onViewPanel = (event: React.MouseEvent<any>) => {
|
||||
event.preventDefault();
|
||||
store.dispatch(
|
||||
@ -171,8 +175,8 @@ export function getPanelMenu(dashboard: DashboardModel, panel: PanelModel): Pane
|
||||
});
|
||||
|
||||
// add old angular panel options
|
||||
if (panel.angularPanel) {
|
||||
const scope = panel.angularPanel.getScope();
|
||||
if (angularComponent) {
|
||||
const scope = angularComponent.getScope();
|
||||
const panelCtrl: PanelCtrl = scope.$$childHead.ctrl;
|
||||
const angularMenuItems = panelCtrl.getExtendedMenu();
|
||||
|
||||
|
@ -27,7 +27,6 @@ import {
|
||||
getDataLinksVariableSuggestions,
|
||||
getCalculationValueDataLinksVariableSuggestions,
|
||||
} from 'app/features/panel/panellinks/link_srv';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
|
||||
onThresholdsChanged = (thresholds: ThresholdsConfig) => {
|
||||
@ -124,11 +123,7 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
|
||||
/>
|
||||
</PanelOptionsGroup>
|
||||
|
||||
<ThresholdsEditor
|
||||
onChange={this.onThresholdsChanged}
|
||||
thresholds={defaults.thresholds}
|
||||
showAlphaUI={config.featureToggles.newEdit}
|
||||
/>
|
||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
|
||||
|
@ -24,7 +24,6 @@ import {
|
||||
getCalculationValueDataLinksVariableSuggestions,
|
||||
getDataLinksVariableSuggestions,
|
||||
} from 'app/features/panel/panellinks/link_srv';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
|
||||
labelWidth = 6;
|
||||
@ -130,11 +129,7 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
|
||||
/>
|
||||
</PanelOptionsGroup>
|
||||
|
||||
<ThresholdsEditor
|
||||
onChange={this.onThresholdsChanged}
|
||||
thresholds={defaults.thresholds}
|
||||
showAlphaUI={config.featureToggles.newEdit}
|
||||
/>
|
||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
|
||||
|
@ -29,7 +29,6 @@ import {
|
||||
getDataLinksVariableSuggestions,
|
||||
getCalculationValueDataLinksVariableSuggestions,
|
||||
} from 'app/features/panel/panellinks/link_srv';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
export class StatPanelEditor extends PureComponent<PanelEditorProps<StatPanelOptions>> {
|
||||
onThresholdsChanged = (thresholds: ThresholdsConfig) => {
|
||||
@ -137,11 +136,7 @@ export class StatPanelEditor extends PureComponent<PanelEditorProps<StatPanelOpt
|
||||
/>
|
||||
</PanelOptionsGroup>
|
||||
|
||||
<ThresholdsEditor
|
||||
onChange={this.onThresholdsChanged}
|
||||
thresholds={defaults.thresholds}
|
||||
showAlphaUI={config.featureToggles.newEdit}
|
||||
/>
|
||||
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { DashboardAcl } from './acl';
|
||||
import { DataQuery, PanelPlugin } from '@grafana/data';
|
||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { AngularComponent } from '@grafana/runtime';
|
||||
|
||||
export interface DashboardDTO {
|
||||
redirectUri?: string;
|
||||
@ -70,6 +71,7 @@ export interface QueriesToUpdateOnDashboardLoad {
|
||||
export interface PanelState {
|
||||
pluginId: string;
|
||||
plugin?: PanelPlugin;
|
||||
angularComponent?: AngularComponent | null;
|
||||
}
|
||||
|
||||
export interface DashboardState {
|
||||
|
Reference in New Issue
Block a user