Commit c002a394 by Torkel Ödegaard Committed by GitHub

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
parent 60dbf728
......@@ -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));
};
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>
)}
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>
)}
</>
);
});
{(!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++,
};
});
}
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';
......
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);
// 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",
......
// 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);
// 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;
......
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} />
......
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 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment