NewPanelEditor: Enable new edit mode (#23405)

* WIP: initial commit to transition to new edit mode

* More old edit cleanup

* Minor update

* Refactoring url edit/fullscreen state to simplify logic, now seperate states

* Fixed tests and part of the explore integration

* Updated snapshot

* Fix alert rule links

* Fixed issue going back from explore

* Updated snapshots

* Fixes and changes

* Fixed bridge srv issue

* Fixed add panel issue

* Removed console log

* Removed render

* Tests: fixes e2e smoketest

* Make description optional

* Fixed typings

* e2e fixes

* removed import

* updated snapshot

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
This commit is contained in:
Torkel Ödegaard 2020-04-10 16:37:26 +02:00 committed by GitHub
parent f9c0c22d85
commit 5a4f690807
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 250 additions and 481 deletions

View File

@ -18,6 +18,7 @@ else
cp -r ./bin $RUNDIR
cp -r ./public $RUNDIR
cp -r ./tools $RUNDIR
mkdir $RUNDIR/conf
mkdir $PROV_DIR
@ -25,7 +26,6 @@ else
mkdir $PROV_DIR/dashboards
cp ./conf/defaults.ini $RUNDIR/conf/defaults.ini
cp ./conf/sample.ini $RUNDIR/conf/custom.ini
fi
echo -e "Copy provisioning setup from devenv"

View File

@ -209,7 +209,7 @@ const assertAdding3dependantQueryVariablesScenario = (queryVariables: QueryVaria
e2e.pages.SaveDashboardModal.save().click();
e2e.flows.assertSuccessNotification();
e2e.pages.Dashboard.Toolbar.backArrow().click();
e2e.pages.Components.BackButton.backArrow().click();
assertVariableLabelsAndComponents(asserts);
@ -264,7 +264,7 @@ const assertDuplicateItem = (queryVariables: QueryVariableData[]) => {
e2e.pages.SaveDashboardModal.save().click();
e2e.flows.assertSuccessNotification();
e2e.pages.Dashboard.Toolbar.backArrow().click();
e2e.pages.Components.BackButton.backArrow().click();
e2e.pages.Dashboard.SubMenu.submenuItemLabels(newItem.label).should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(newItem.selectedOption)
@ -300,7 +300,7 @@ const assertDeleteItem = (queryVariables: QueryVariableData[]) => {
e2e.pages.SaveDashboardModal.save().click();
e2e.flows.assertSuccessNotification();
e2e.pages.Dashboard.Toolbar.backArrow().click();
e2e.pages.Components.BackButton.backArrow().click();
e2e.pages.Dashboard.SubMenu.submenuItemLabels(itemToDelete.label).should('not.exist');
@ -347,7 +347,7 @@ const assertUpdateItem = (data: QueryVariableData[]) => {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelect().select('');
e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInput().type(updatedItem.query);
e2e.pages.Dashboard.Toolbar.backArrow().click();
e2e.pages.Components.BackButton.backArrow().click();
e2e()
.window()
@ -407,7 +407,7 @@ const assertMoveDownItem = (data: QueryVariableData[]) => {
});
});
e2e.pages.Dashboard.Toolbar.backArrow().click();
e2e.pages.Components.BackButton.backArrow().click();
assertVariableLabelsAndComponents(queryVariables);
@ -552,7 +552,7 @@ const assertMoveUpItem = (data: QueryVariableData[]) => {
});
});
e2e.pages.Dashboard.Toolbar.backArrow().click();
e2e.pages.Components.BackButton.backArrow().click();
assertVariableLabelsAndComponents(queryVariables);

View File

@ -15,48 +15,15 @@ e2e.scenario({
e2e.pages.Dashboard.Toolbar.toolbarItems('Add panel').click();
e2e.pages.AddDashboard.ctaButtons('Add Query').click();
e2e.pages.Dashboard.Panels.EditPanel.tabItems('Queries').click();
e2e.pages.Dashboard.Panels.DataSource.TestData.QueryTab.scenarioSelect().select('CSV Metric Values');
e2e.pages.Dashboard.Panels.EditPanel.tabItems('Visualization').click();
e2e.pages.Dashboard.Panels.Visualization.Graph.VisualizationTab.xAxisSection()
.contains('Show')
.click();
e2e.flows.saveDashboard();
e2e.pages.Components.BackButton.backArrow().click();
e2e.pages.Dashboard.Toolbar.backArrow().click();
e2e.pages.Dashboard.Panels.Panel.title('Panel Title').click();
e2e.pages.Dashboard.Panels.Panel.headerItems('Share').click();
e2e.pages.SharePanelModal.linkToRenderedImage().then(($a: any) => {
// extract the fully qualified href property
const url = $a.prop('href');
// Test that the image renderer returns 200 OK
e2e().request({ method: 'GET', url, timeout: 120000 });
// Download image
if (!e2e.env('CIRCLE_SHA1')) {
return;
}
const theOutputImage = `${e2e.config().screenshotsFolder}/received/smoke-test-scenario.png`;
const theTruthImage = `${e2e.config().screenshotsFolder}/expected/smoke-test-scenario.png`;
e2e().wrap(
e2e.imgSrcToBlob(url).then((blob: any) => {
e2e.blobToBase64String(blob).then((base64String: string) => {
const data = base64String.replace(/^data:image\/\w+;base64,/, '');
e2e().writeFile(theOutputImage, data, 'base64');
});
})
);
e2e().wait(1000); // give the io a chance to flush image to disk
e2e().compareSnapshot({ pathToFileA: theOutputImage, pathToFileB: theTruthImage });
});
// e2e.pages.Dashboard.Panels.Panel.title('Panel Title').click();
// e2e.pages.Dashboard.Panels.Panel.headerItems('Inspect').click();
},
});

View File

@ -49,7 +49,7 @@ export interface OptionsEditorItem<TOptions, TSettings, TEditorProps, TValue> ex
interface OptionEditorConfig<TOptions, TSettings, TValue = any> {
id: keyof TOptions & string;
name: string;
description: string;
description?: string;
settings?: TSettings;
defaultValue?: TValue;
}

View File

@ -77,7 +77,7 @@ export interface FieldConfigEditorConfig<TOptions, TSettings = any, TValue = any
/**
* Description of the field config property. Will be displayed in the UI as form element description.
*/
description: string;
description?: string;
/**
* Array of strings representing category of the field config property. First element in the array will make option render as collapsible section.
*/

View File

@ -142,7 +142,7 @@ export interface PanelOptionsEditorConfig<TOptions, TSettings = any, TValue = an
/**
* Description of the option. Will be displayed in the UI as form element description.
*/
description: string;
description?: string;
/**al
* Custom settings of the editor.
*/

View File

@ -3,7 +3,7 @@ import { SelectableValue } from '../types/select';
export interface RegistryItem {
id: string; // Unique Key -- saved in configs
name: string; // Display Name, can change without breaking configs
description: string;
description?: string;
aliasIds?: string[]; // when the ID changes, we may want backwards compatibility ('current' => 'last')
/**

View File

@ -4,7 +4,6 @@ export const Dashboard = pageFactory({
url: (uid: string) => `/d/${uid}`,
selectors: {
toolbarItems: (button: string) => `Dashboard navigation bar button ${button}`,
backArrow: 'Dashboard settings Go Back button',
navBar: () => '.navbar',
},
});

View File

@ -15,6 +15,7 @@ import { SaveDashboardModal } from './saveDashboardModal';
import { Panel } from './panel';
import { SharePanelModal } from './sharePanelModal';
import { ConstantVariable, QueryVariable, VariableGeneral, Variables, VariablesSubMenu } from './variables';
import { pageFactory } from '../support';
export const Pages = {
Login,
@ -53,4 +54,11 @@ export const Pages = {
SaveDashboardAsModal,
SaveDashboardModal,
SharePanelModal,
Components: {
BackButton: pageFactory({
selectors: {
backArrow: 'Go Back button',
},
}),
},
};

View File

@ -100,7 +100,7 @@ export const DataLinksInlineEditor: React.FC<DataLinksInlineEditorProps> = ({ li
)}
<FullWidthButtonContainer>
<Button size="sm" icon="plus-circle" onClick={onDataLinkAdd} variant="secondary">
<Button size="sm" icon="plus" onClick={onDataLinkAdd} variant="secondary">
Add link
</Button>
</FullWidthButtonContainer>

View File

@ -200,7 +200,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
return (
<div className={styles.wrapper}>
<FullWidthButtonContainer className={styles.addButton}>
<Button size="sm" icon="plus-circle" onClick={() => this.onAddThreshold()} variant="secondary">
<Button size="sm" icon="plus" onClick={() => this.onAddThreshold()} variant="secondary">
Add threshold
</Button>
</FullWidthButtonContainer>

View File

@ -68,7 +68,7 @@ export const ValueMappingsEditor: React.FC<Props> = ({ valueMappings, onChange,
<FullWidthButtonContainer>
<Button
size="sm"
icon="plus-circle"
icon="plus"
onClick={onAdd}
aria-label="ValueMappingsEditor add mapping button"
variant="secondary"

View File

@ -112,7 +112,7 @@ func (c *EvalContext) GetDashboardUID() (*models.DashboardRef, error) {
return c.dashboardRef, nil
}
const urlFormat = "%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d"
const urlFormat = "%s?tab=alert&editPanel=%d&orgId=%d"
// GetRuleURL returns the url to the dashboard containing the alert.
func (c *EvalContext) GetRuleURL() (string, error) {

View File

@ -1,7 +1,8 @@
import React, { ButtonHTMLAttributes } from 'react';
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { stylesFactory, useTheme, Tooltip, selectThemeVariant } from '@grafana/ui';
import { selectThemeVariant, stylesFactory, Tooltip, useTheme } from '@grafana/ui';
import { e2e } from '@grafana/e2e';
export type Props = ButtonHTMLAttributes<HTMLButtonElement>;
@ -11,7 +12,7 @@ export const BackButton: React.FC<Props> = props => {
return (
<Tooltip content="Go back (Esc)" placement="bottom">
<button className={styles.wrapper} {...props}>
<button className={styles.wrapper} {...props} aria-label={e2e.pages.Components.BackButton.selectors.backArrow}>
<i className="gicon gicon-arrow-left" />
</button>
</Tooltip>

View File

@ -20,7 +20,7 @@ export class SideMenu extends PureComponent {
<Branding.MenuLogo />
</a>,
<div className="sidemenu__logo_small_breakpoint" onClick={this.toggleSideMenuSmallBreakpoint} key="hamburger">
<Icon name="bars" />
<Icon name="bars" size="xl" />
<span className="sidemenu__close">
<Icon name="times" />
&nbsp;Close

View File

@ -16,6 +16,7 @@ Array [
>
<Icon
name="bars"
size="xl"
/>
<span
className="sidemenu__close"

View File

@ -15,6 +15,7 @@ export class BridgeSrv {
private fullPageReloadRoutes: string[];
private lastQuery: UrlQueryMap = {};
private lastPath = '';
private angularUrl: string;
/** @ngInject */
constructor(
@ -26,13 +27,16 @@ export class BridgeSrv {
private variableSrv: VariableSrv
) {
this.fullPageReloadRoutes = ['/logout'];
this.angularUrl = $location.url();
}
init() {
this.$rootScope.$on('$routeUpdate', (evt, data) => {
const angularUrl = this.$location.url();
const state = store.getState();
if (state.location.url !== angularUrl) {
this.angularUrl = this.$location.url();
if (state.location.url !== this.angularUrl) {
store.dispatch(
updateLocation({
path: this.$location.path(),
@ -44,6 +48,8 @@ export class BridgeSrv {
});
this.$rootScope.$on('$routeChangeSuccess', (evt, data) => {
this.angularUrl = this.$location.url();
store.dispatch(
updateLocation({
path: this.$location.path(),
@ -56,9 +62,12 @@ export class BridgeSrv {
// Listen for changes in redux location -> update angular location
store.subscribe(() => {
const state = store.getState();
const angularUrl = this.$location.url();
const url = state.location.url;
if (angularUrl !== url) {
if (this.angularUrl !== url) {
// store angular url right away as otherwise we end up syncing multiple times
this.angularUrl = url;
this.$timeout(() => {
this.$location.url(url);
// some state changes should not trigger new browser history
@ -66,6 +75,7 @@ export class BridgeSrv {
this.$location.replace();
}
});
console.log('store updating angular $location.url', url);
}

View File

@ -8,7 +8,6 @@ import { store } from 'app/store/store';
import { AppEventEmitter, CoreEvents } from 'app/types';
import Mousetrap from 'mousetrap';
import { PanelEvents } from '@grafana/data';
import 'mousetrap-global-bind';
import { ContextSrv } from './context_srv';
import { ILocationService, IRootScopeService, ITimeoutService } from 'angular';
@ -133,8 +132,9 @@ export class KeybindingSrv {
return;
}
if (search.fullscreen) {
appEvents.emit(PanelEvents.panelChangeView, { fullscreen: false, edit: false });
if (search.viewPanel) {
delete search.viewPanel;
this.$location.search(search);
return;
}
@ -213,23 +213,16 @@ export class KeybindingSrv {
// edit panel
this.bind('e', () => {
if (dashboard.canEditPanelById(dashboard.meta.focusPanelId)) {
appEvents.emit(PanelEvents.panelChangeView, {
fullscreen: true,
edit: true,
panelId: dashboard.meta.focusPanelId,
toggle: true,
});
const search = _.extend(this.$location.search(), { editPanel: dashboard.meta.focusPanelId });
this.$location.search(search);
}
});
// view panel
this.bind('v', () => {
if (dashboard.meta.focusPanelId) {
appEvents.emit(PanelEvents.panelChangeView, {
fullscreen: true,
panelId: dashboard.meta.focusPanelId,
toggle: true,
});
const search = _.extend(this.$location.search(), { viewPanel: dashboard.meta.focusPanelId });
this.$location.search(search);
}
});

View File

@ -15,7 +15,6 @@ import {
LogRowModel,
LogsDedupStrategy,
LogsModel,
PanelModel,
RawTimeRange,
TimeFragment,
TimeRange,
@ -33,6 +32,7 @@ import { ExploreUrlState, QueryOptions, QueryTransaction } from 'app/types/explo
import { config } from '../config';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { DataSourceSrv } from '@grafana/runtime';
import { PanelModel } from 'app/features/dashboard/state';
export const DEFAULT_RANGE = {
from: 'now-1h',
@ -105,7 +105,7 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise<strin
};
}
const exploreState = JSON.stringify({ ...state, originPanelId: panel.id });
const exploreState = JSON.stringify({ ...state, originPanelId: panel.getSavedId() });
url = renderUrl('/explore', { left: exploreState });
}
return url;

View File

@ -24,7 +24,7 @@ class AlertRuleItem extends PureComponent<Props> {
render() {
const { rule, onTogglePause } = this.props;
const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen&edit&tab=alert`;
const ruleUrl = `${rule.url}?editPanel=${rule.panelId}&tab=alert`;
return (
<li className="alert-rule-item">

View File

@ -19,7 +19,7 @@ exports[`Render should render component 1`] = `
className="alert-rule-item__name"
>
<a
href="https://something.something.darkside?panelId=1&fullscreen&edit&tab=alert"
href="https://something.something.darkside?editPanel=1&tab=alert"
>
<Highlighter
highlightClassName="highlight-search-match"
@ -71,7 +71,7 @@ exports[`Render should render component 1`] = `
</button>
<a
className="btn btn-small btn-inverse alert-list__btn width-2"
href="https://something.something.darkside?panelId=1&fullscreen&edit&tab=alert"
href="https://something.something.darkside?editPanel=1&tab=alert"
title="Edit alert rule"
>
<Icon

View File

@ -36,8 +36,8 @@ export class AnnotationsSrv {
.then(results => {
// combine the annotations and flatten results
let annotations: AnnotationEvent[] = flattenDeep(results[0]);
// when in edit mode we need to use editSourceId
let panelFilterId = options.panel.editSourceId ?? options.panel.id;
// when in edit mode we need to use this function to get the saved id
let panelFilterId = options.panel.getSavedId();
// filter out annotations that do not belong to requesting panel
annotations = annotations.filter(item => {

View File

@ -86,16 +86,13 @@ export class AddPanelWidgetUnconnected extends React.Component<Props, State> {
const location: LocationUpdate = {
query: {
panelId: newPanel.id,
edit: true,
fullscreen: true,
editPanel: newPanel.id,
},
partial: true,
};
if (tab === 'visualization') {
location.query.tab = 'visualization';
location.query.openVizPicker = true;
if (tab === 'visualize') {
location.query.tab = 'visualize';
}
reduxStore.dispatch(updateLocation(location));
@ -173,10 +170,8 @@ export class AddPanelWidgetUnconnected extends React.Component<Props, State> {
</div>
<div className="add-panel-widget__btn-container">
<div className="add-panel-widget__create">
{this.renderOptionLink('search', 'Add Query', this.onCreateNewPanel)}
{this.renderOptionLink('chart-line', 'Choose Visualization', () =>
this.onCreateNewPanel('visualization')
)}
{this.renderOptionLink('database', 'Add Query', this.onCreateNewPanel)}
{this.renderOptionLink('chart-line', 'Choose Visualization', () => this.onCreateNewPanel('visualize'))}
</div>
<div className="add-panel-widget__actions">
<button className="btn btn-inverse add-panel-widget__action" onClick={this.onCreateNewRow}>

View File

@ -37,7 +37,7 @@
.add-panel-widget__link {
margin: 0 $space-sm;
width: 160px;
width: 170px;
height: 88px !important;
flex-direction: column !important;
}

View File

@ -52,7 +52,7 @@ exports[`Render should render component 1`] = `
className="add-panel-widget__icon"
>
<Icon
name="search"
name="database"
size="xl"
/>
</div>

View File

@ -2,7 +2,6 @@
import React, { PureComponent, FC } from 'react';
import { connect } from 'react-redux';
import { css } from 'emotion';
import { e2e } from '@grafana/e2e';
// Utils & Services
import { appEvents } from 'app/core/app_events';
import { PlaylistSrv } from 'app/features/playlist/playlist_srv';
@ -22,7 +21,6 @@ import { sanitizeUrl } from 'app/core/utils/text';
export interface OwnProps {
dashboard: DashboardModel;
isEditing: boolean;
isFullscreen: boolean;
$injector: any;
updateLocation: typeof updateLocation;
@ -61,7 +59,7 @@ class DashNav extends PureComponent<Props> {
onClose = () => {
this.props.updateLocation({
query: { panelId: null, edit: null, fullscreen: null, tab: null },
query: { edit: null, viewPanel: null },
partial: true,
});
};
@ -140,7 +138,7 @@ class DashNav extends PureComponent<Props> {
renderBackButton() {
return (
<div className="navbar-edit">
<BackButton onClick={this.onClose} aria-label={e2e.pages.Dashboard.Toolbar.selectors.backArrow} />
<BackButton onClick={this.onClose} />
</div>
);
}

View File

@ -7,7 +7,6 @@ import { AngularComponent, getAngularLoader } from '@grafana/runtime';
// Types
import { DashboardModel } from '../../state/DashboardModel';
import { BackButton } from 'app/core/components/BackButton/BackButton';
import { e2e } from '@grafana/e2e';
import { updateLocation } from 'app/core/actions';
import { CustomScrollbar } from '@grafana/ui';
@ -51,7 +50,7 @@ export class DashboardSettings extends PureComponent<Props> {
<div className="dashboard-settings">
<div className="navbar navbar--shadow">
<div className="navbar-edit">
<BackButton onClick={this.onClose} aria-label={e2e.pages.Dashboard.Toolbar.selectors.backArrow} />
<BackButton onClick={this.onClose} />
</div>
<div className="navbar-page-btn">
{haveFolder && <div className="navbar-page-btn__folder">{folderTitle} / </div>}

View File

@ -89,7 +89,7 @@ export const OverrideFieldConfigEditor: React.FC<Props> = props => {
const renderAddOverride = () => {
return (
<ValuePicker
icon="plus-circle"
icon="plus"
label="Add override"
variant="secondary"
options={fieldMatchersUI

View File

@ -169,9 +169,8 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
<DashboardPanel
dashboard={dashboard}
panel={panel}
isEditing={false}
isInEditMode
isFullscreen={false}
isEditing={true}
isViewing={false}
isInView={true}
/>
</div>
@ -410,6 +409,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => {
cursor: col-resize;
width: ${paneSpaceing};
border-right-width: 1px;
margin-top: 18px;
`
),
resizerH: cx(
@ -418,9 +418,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => {
height: ${paneSpaceing};
cursor: row-resize;
position: relative;
top: 49px;
top: 0px;
z-index: 1;
border-top-width: 1px;
margin-left: ${paneSpaceing};
`
),
tabsWrapper: css`

View File

@ -14,7 +14,7 @@ import store from '../../../../../core/store';
export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardModel): ThunkResult<void> {
return dispatch => {
const panel = dashboard.initPanelEditor(sourcePanel);
const panel = dashboard.initEditPanel(sourcePanel);
const queryRunner = panel.getQueryRunner();
const querySubscription = queryRunner.getData(false).subscribe({

View File

@ -62,8 +62,6 @@ function dashboardPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
urlUid: '11',
$injector: {},
routeInfo: DashboardRouteInfo.Normal,
urlEdit: false,
urlFullscreen: false,
initPhase: DashboardInitPhase.NotStarted,
isInitSlow: false,
initDashboard: jest.fn(),
@ -133,55 +131,39 @@ describe('DashboardPage', () => {
ctx.mount();
ctx.setDashboardProp();
ctx.wrapper?.setProps({
urlFullscreen: true,
urlEdit: true,
urlPanelId: '1',
urlEditPanelId: '1',
});
});
it('Should update model state to fullscreen & edit', () => {
expect(ctx.dashboard).not.toBe(null);
expect(ctx.dashboard?.meta.fullscreen).toBe(true);
expect(ctx.dashboard?.meta.isEditing).toBe(true);
});
it('Should update component state to fullscreen and edit', () => {
const state = ctx.wrapper?.state();
expect(state).not.toBe(null);
expect(state?.isEditing).toBe(true);
expect(state?.isFullscreen).toBe(true);
expect(state?.editPanel).toBeDefined();
});
});
dashboardPageScenario('When user goes back to dashboard from panel edit', ctx => {
dashboardPageScenario('When user goes back to dashboard from view panel', ctx => {
ctx.setup(() => {
ctx.mount();
ctx.setDashboardProp();
ctx.wrapper?.setState({ scrollTop: 100 });
ctx.wrapper?.setProps({
urlFullscreen: true,
urlEdit: true,
urlPanelId: '1',
urlEditPanelId: '1',
});
ctx.wrapper?.setProps({
urlFullscreen: false,
urlEdit: false,
urlPanelId: (null as unknown) as string,
urlEditPanelId: undefined,
});
});
it('Should update model state normal state', () => {
expect(ctx.dashboard).not.toBe(null);
expect(ctx.dashboard?.meta.fullscreen).toBe(false);
expect(ctx.dashboard?.meta.isEditing).toBe(false);
expect(ctx.dashboard.panelInEdit).toBeUndefined();
});
it('Should update component state to normal and restore scrollTop', () => {
const state = ctx.wrapper?.state();
expect(ctx.wrapper).not.toBe(null);
expect(state).not.toBe(null);
expect(state?.isEditing).toBe(false);
expect(state?.isFullscreen).toBe(false);
expect(state?.editPanel).toBe(null);
expect(state?.scrollTop).toBe(100);
});
});
@ -228,9 +210,7 @@ describe('DashboardPage', () => {
schemaVersion: 17,
});
ctx.wrapper?.setProps({
urlEdit: true,
urlFullscreen: true,
urlPanelId: '0',
urlEditPanelId: '0',
});
});
@ -238,8 +218,7 @@ describe('DashboardPage', () => {
const state = ctx.wrapper?.state();
expect(ctx.wrapper).not.toBe(null);
expect(state).not.toBe(null);
expect(state?.fullscreenPanel).not.toBe(null);
expect(state?.fullscreenPanel?.id).toBe(0);
expect(state?.editPanel).not.toBe(null);
});
});
@ -258,13 +237,12 @@ describe('DashboardPage', () => {
});
});
describe('mapStateToProps with bool fullscreen', () => {
describe('mapStateToProps with editPanel', () => {
const props = mapStateToProps({
location: {
routeParams: {},
query: {
fullscreen: true,
edit: false,
editPanel: '1',
},
},
panelEditorNew: {},
@ -273,8 +251,7 @@ describe('DashboardPage', () => {
},
} as any);
expect(props.urlFullscreen).toBe(true);
expect(props.urlEdit).toBe(false);
expect(props.urlEditPanelId).toBe('1');
});
describe('mapStateToProps with string edit true', () => {
@ -282,8 +259,7 @@ describe('DashboardPage', () => {
location: {
routeParams: {},
query: {
fullscreen: false,
edit: 'true',
viewPanel: '2',
},
},
panelEditorNew: {},
@ -292,7 +268,6 @@ describe('DashboardPage', () => {
},
} as any);
expect(props.urlFullscreen).toBe(false);
expect(props.urlEdit).toBe(true);
expect(props.urlViewPanelId).toBe('2');
});
});

View File

@ -15,7 +15,7 @@ import { DashNav } from '../components/DashNav';
import { AngularSubMenu } from '../components/SubMenu';
import { DashboardSettings } from '../components/DashboardSettings';
import { PanelEditor } from '../components/PanelEditor/PanelEditor';
import { Alert, CustomScrollbar, Portal } from '@grafana/ui';
import { Alert, CustomScrollbar } from '@grafana/ui';
// Redux
import { initDashboard } from '../state/initDashboard';
import { cleanUpDashboard } from '../state/reducers';
@ -41,13 +41,12 @@ export interface Props {
editview?: string;
urlPanelId?: string;
urlFolderId?: string;
urlEditPanel?: string;
inspectPanelId?: string;
$scope: any;
$injector: any;
routeInfo: DashboardRouteInfo;
urlEdit: boolean;
urlFullscreen: boolean;
urlEditPanelId?: string;
urlViewPanelId?: string;
initPhase: DashboardInitPhase;
isInitSlow: boolean;
dashboard: DashboardModel | null;
@ -61,9 +60,8 @@ export interface Props {
}
export interface State {
isEditing: boolean;
isFullscreen: boolean;
fullscreenPanel: PanelModel | null;
editPanel: PanelModel | null;
viewPanel: PanelModel | null;
scrollTop: number;
updateScrollTop?: number;
rememberScrollTop: number;
@ -72,10 +70,9 @@ export interface State {
export class DashboardPage extends PureComponent<Props, State> {
state: State = {
isEditing: false,
isFullscreen: false,
editPanel: null,
viewPanel: null,
showLoadingState: false,
fullscreenPanel: null,
scrollTop: 0,
rememberScrollTop: 0,
};
@ -101,7 +98,8 @@ export class DashboardPage extends PureComponent<Props, State> {
}
componentDidUpdate(prevProps: Props) {
const { dashboard, urlEdit, urlFullscreen, urlPanelId, urlUid } = this.props;
const { dashboard, urlEditPanelId, urlViewPanelId, urlUid } = this.props;
const { editPanel, viewPanel } = this.state;
if (!dashboard) {
return;
@ -118,56 +116,64 @@ export class DashboardPage extends PureComponent<Props, State> {
return;
}
// Sync url state with model
if (urlFullscreen !== dashboard.meta.fullscreen || urlEdit !== dashboard.meta.isEditing) {
if (urlPanelId && !isNaN(parseInt(urlPanelId, 10))) {
this.onEnterFullscreen(dashboard, urlPanelId);
} else {
this.onLeaveFullscreen(dashboard);
}
// entering edit mode
if (!editPanel && urlEditPanelId) {
this.getPanelByIdFromUrlParam(urlEditPanelId, panel => {
this.setState({ editPanel: panel });
});
}
// leaving edit mode
if (editPanel && !urlEditPanelId) {
this.setState({ editPanel: null });
}
// entering view mode
if (!viewPanel && urlViewPanelId) {
this.getPanelByIdFromUrlParam(urlViewPanelId, panel => {
this.setPanelFullscreenClass(true);
dashboard.initViewPanel(panel);
this.setState({
viewPanel: panel,
rememberScrollTop: this.state.scrollTop,
});
});
}
// leaving view mode
if (viewPanel && !urlViewPanelId) {
this.setPanelFullscreenClass(false);
dashboard.exitViewPanel(viewPanel);
this.setState(
{ viewPanel: null, updateScrollTop: this.state.rememberScrollTop },
this.triggerPanelsRendering.bind(this)
);
}
}
onEnterFullscreen(dashboard: DashboardModel, urlPanelId: string) {
const { urlEdit, urlFullscreen } = this.props;
getPanelByIdFromUrlParam(urlPanelId: string, callback: (panel: PanelModel) => void) {
const { dashboard } = this.props;
const panelId = parseInt(urlPanelId!, 10);
dashboard;
// need to expand parent row if this panel is inside a row
dashboard.expandParentRowFor(panelId);
const panel = dashboard!.getPanelById(panelId);
const panel = dashboard.getPanelById(panelId);
if (panel) {
dashboard.setViewMode(panel, urlFullscreen, urlEdit);
this.setState({
isEditing: urlEdit && dashboard.meta.canEdit === true,
isFullscreen: urlFullscreen,
fullscreenPanel: panel,
rememberScrollTop: this.state.scrollTop,
if (!panel) {
// Panel not found
this.props.notifyApp(createErrorNotification(`Panel with id ${urlPanelId} not found`));
// Clear url state
this.props.updateLocation({
query: {
edit: null,
fullscreen: null,
panelId: null,
},
partial: true,
});
this.setPanelFullscreenClass(urlFullscreen);
} else {
this.handleFullscreenPanelNotFound(urlPanelId);
}
}
onLeaveFullscreen(dashboard: DashboardModel) {
if (this.state.fullscreenPanel) {
dashboard.setViewMode(this.state.fullscreenPanel, false, false);
return;
}
this.setState(
{
isEditing: false,
isFullscreen: false,
fullscreenPanel: null,
updateScrollTop: this.state.rememberScrollTop,
},
this.triggerPanelsRendering.bind(this)
);
this.setPanelFullscreenClass(false);
dashboard!.expandParentRowFor(panelId);
callback(panel);
}
triggerPanelsRendering() {
@ -179,20 +185,6 @@ export class DashboardPage extends PureComponent<Props, State> {
}
}
handleFullscreenPanelNotFound(urlPanelId: string) {
// Panel not found
this.props.notifyApp(createErrorNotification(`Panel with id ${urlPanelId} not found`));
// Clear url state
this.props.updateLocation({
query: {
edit: null,
fullscreen: null,
panelId: null,
},
partial: true,
});
}
setPanelFullscreenClass(isFullscreen: boolean) {
$('body').toggleClass('panel-in-fullscreen', isFullscreen);
}
@ -252,13 +244,13 @@ export class DashboardPage extends PureComponent<Props, State> {
isInitSlow,
initError,
inspectPanelId,
urlEditPanel,
inspectTab,
isNewEditorOpen,
updateLocation,
} = this.props;
const { isEditing, isFullscreen, scrollTop, updateScrollTop } = this.state;
const { editPanel, viewPanel, scrollTop, updateScrollTop } = this.state;
const { featureToggles } = getConfig();
if (!dashboard) {
if (isInitSlow) {
@ -274,21 +266,13 @@ export class DashboardPage extends PureComponent<Props, State> {
// Find the panel to inspect
const inspectPanel = inspectPanelId ? dashboard.getPanelById(parseInt(inspectPanelId, 10)) : null;
// find panel being edited
const editPanel = urlEditPanel ? dashboard.getPanelById(parseInt(urlEditPanel, 10)) : null;
// Only trigger render when the scroll has moved by 25
const approximateScrollTop = Math.round(scrollTop / 25) * 25;
return (
<div>
<DashNav
dashboard={dashboard}
isEditing={isEditing}
isFullscreen={isFullscreen}
$injector={$injector}
onAddPanel={this.onAddPanel}
/>
<DashNav dashboard={dashboard} isFullscreen={!!viewPanel} $injector={$injector} onAddPanel={this.onAddPanel} />
<div className="scroll-canvas scroll-canvas--dashboard">
<CustomScrollbar
autoHeightMin="100%"
@ -300,12 +284,12 @@ export class DashboardPage extends PureComponent<Props, State> {
{initError && this.renderInitFailedState()}
<div className={gridWrapperClasses}>
{!getConfig().featureToggles.newVariables && <AngularSubMenu dashboard={dashboard} />}
{!editPanel && getConfig().featureToggles.newVariables && <SubMenu dashboard={dashboard} />}
{!editPanel && !featureToggles.newVariables && <AngularSubMenu dashboard={dashboard} />}
{!editPanel && featureToggles.newVariables && <SubMenu dashboard={dashboard} />}
<DashboardGrid
dashboard={dashboard}
isEditing={isEditing}
isFullscreen={isFullscreen}
viewPanel={viewPanel}
editPanel={editPanel}
isNewEditorOpen={isNewEditorOpen}
scrollTop={approximateScrollTop}
/>
@ -314,11 +298,7 @@ export class DashboardPage extends PureComponent<Props, State> {
</div>
{inspectPanel && <PanelInspector dashboard={dashboard} panel={inspectPanel} selectedTab={inspectTab} />}
{editPanel && (
<Portal>
<PanelEditor dashboard={dashboard} sourcePanel={editPanel} />
</Portal>
)}
{editPanel && <PanelEditor dashboard={dashboard} sourcePanel={editPanel} />}
{editview && <DashboardSettings dashboard={dashboard} updateLocation={updateLocation} />}
</div>
);
@ -331,10 +311,9 @@ export const mapStateToProps = (state: StoreState) => ({
urlType: state.location.routeParams.type,
editview: state.location.query.editview,
urlPanelId: state.location.query.panelId,
urlEditPanel: state.location.query.editPanel,
urlFolderId: state.location.query.folderId,
urlFullscreen: !!state.location.query.fullscreen,
urlEdit: !!state.location.query.edit,
urlEditPanelId: state.location.query.editPanel,
urlViewPanelId: state.location.query.viewPanel,
inspectPanelId: state.location.query.inspect,
initPhase: state.dashboard.initPhase,
isInitSlow: state.dashboard.isInitSlow,

View File

@ -89,7 +89,7 @@ export class SoloPanelPage extends Component<Props, State> {
return (
<div className="panel-solo">
<DashboardPanel dashboard={dashboard} panel={panel} isEditing={false} isFullscreen={false} isInView={true} />
<DashboardPanel dashboard={dashboard} panel={panel} isEditing={false} isViewing={false} isInView={true} />
</div>
);
}

View File

@ -40,8 +40,6 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
"canSave": true,
"canShare": true,
"canStar": true,
"fullscreen": false,
"isEditing": false,
"showSettings": true,
},
"originalTemplating": Array [],
@ -98,7 +96,6 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
"version": 0,
}
}
isEditing={false}
isFullscreen={false}
onAddPanel={[Function]}
/>
@ -156,8 +153,6 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
"canSave": true,
"canShare": true,
"canStar": true,
"fullscreen": false,
"isEditing": false,
"showSettings": true,
},
"originalTemplating": Array [],
@ -252,8 +247,6 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
"canSave": true,
"canShare": true,
"canStar": true,
"fullscreen": false,
"isEditing": false,
"showSettings": true,
},
"originalTemplating": Array [],
@ -310,9 +303,9 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
"version": 0,
}
}
isEditing={false}
isFullscreen={false}
editPanel={null}
scrollTop={0}
viewPanel={null}
/>
</div>
</CustomScrollbar>
@ -378,8 +371,6 @@ exports[`DashboardPage When dashboard has editview url state should render setti
"canSave": true,
"canShare": true,
"canStar": true,
"fullscreen": false,
"isEditing": false,
"showSettings": true,
},
"originalTemplating": Array [],
@ -436,7 +427,6 @@ exports[`DashboardPage When dashboard has editview url state should render setti
"version": 0,
}
}
isEditing={false}
isFullscreen={false}
onAddPanel={[Function]}
/>
@ -494,8 +484,6 @@ exports[`DashboardPage When dashboard has editview url state should render setti
"canSave": true,
"canShare": true,
"canStar": true,
"fullscreen": false,
"isEditing": false,
"showSettings": true,
},
"originalTemplating": Array [],
@ -590,8 +578,6 @@ exports[`DashboardPage When dashboard has editview url state should render setti
"canSave": true,
"canShare": true,
"canStar": true,
"fullscreen": false,
"isEditing": false,
"showSettings": true,
},
"originalTemplating": Array [],
@ -648,9 +634,9 @@ exports[`DashboardPage When dashboard has editview url state should render setti
"version": 0,
}
}
isEditing={false}
isFullscreen={false}
editPanel={null}
scrollTop={0}
viewPanel={null}
/>
</div>
</CustomScrollbar>
@ -692,8 +678,6 @@ exports[`DashboardPage When dashboard has editview url state should render setti
"canSave": true,
"canShare": true,
"canStar": true,
"fullscreen": false,
"isEditing": false,
"showSettings": true,
},
"originalTemplating": Array [],

View File

@ -57,8 +57,8 @@ function dashboardGridScenario(description: string, scenarioFn: (ctx: ScenarioCo
setupFn = fn;
},
props: {
isEditing: false,
isFullscreen: false,
editPanel: null,
viewPanel: null,
scrollTop: 0,
dashboard: getTestDashboard(),
},

View File

@ -15,7 +15,6 @@ import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core
import { DashboardPanel } from './DashboardPanel';
import { DashboardModel, PanelModel } from '../state';
import { CoreEvents } from 'app/types';
import { PanelEvents } from '@grafana/data';
import { panelAdded, panelRemoved } from '../state/PanelModel';
let lastGridWidth = 1200;
@ -33,7 +32,7 @@ interface GridWrapperProps {
className: string;
isResizable?: boolean;
isDraggable?: boolean;
isFullscreen?: boolean;
viewPanel: PanelModel | null;
}
function GridWrapper({
@ -48,7 +47,7 @@ function GridWrapper({
className,
isResizable,
isDraggable,
isFullscreen,
viewPanel,
}: GridWrapperProps) {
const width = size.width > 0 ? size.width : lastGridWidth;
@ -56,7 +55,7 @@ function GridWrapper({
if (width !== lastGridWidth) {
if (ignoreNextWidthChange) {
ignoreNextWidthChange = false;
} else if (!isFullscreen && Math.abs(width - lastGridWidth) > 8) {
} else if (!viewPanel && Math.abs(width - lastGridWidth) > 8) {
onWidthChange();
lastGridWidth = width;
}
@ -95,8 +94,8 @@ const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper);
export interface Props {
dashboard: DashboardModel;
isEditing: boolean;
isFullscreen: boolean;
editPanel: PanelModel | null;
viewPanel: PanelModel | null;
scrollTop: number;
isNewEditorOpen?: boolean;
}
@ -111,7 +110,6 @@ export class DashboardGrid extends PureComponent<Props> {
dashboard.on(panelAdded, this.triggerForceUpdate);
dashboard.on(panelRemoved, this.triggerForceUpdate);
dashboard.on(CoreEvents.repeatsProcessed, this.triggerForceUpdate);
dashboard.on(PanelEvents.viewModeChanged, this.onViewModeChanged);
dashboard.on(CoreEvents.rowCollapsed, this.triggerForceUpdate);
dashboard.on(CoreEvents.rowExpanded, this.triggerForceUpdate);
}
@ -121,7 +119,6 @@ export class DashboardGrid extends PureComponent<Props> {
dashboard.off(panelAdded, this.triggerForceUpdate);
dashboard.off(panelRemoved, this.triggerForceUpdate);
dashboard.off(CoreEvents.repeatsProcessed, this.triggerForceUpdate);
dashboard.off(PanelEvents.viewModeChanged, this.onViewModeChanged);
dashboard.off(CoreEvents.rowCollapsed, this.triggerForceUpdate);
dashboard.off(CoreEvents.rowExpanded, this.triggerForceUpdate);
}
@ -181,10 +178,6 @@ export class DashboardGrid extends PureComponent<Props> {
}
};
onViewModeChanged = () => {
ignoreNextWidthChange = true;
};
updateGridPos = (item: ReactGridLayout.Layout, layout: ReactGridLayout.Layout[]) => {
this.panelMap[item.i!].updateGridPos(item);
@ -207,7 +200,7 @@ export class DashboardGrid extends PureComponent<Props> {
};
isInView = (panel: PanelModel): boolean => {
if (panel.fullscreen || panel.isEditing) {
if (panel.isViewing || panel.isEditing) {
return true;
}
@ -248,7 +241,7 @@ export class DashboardGrid extends PureComponent<Props> {
const panelElements = [];
for (const panel of this.props.dashboard.panels) {
const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.fullscreen });
const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.isViewing });
const id = panel.id.toString();
panel.isInView = this.isInView(panel);
@ -276,14 +269,14 @@ export class DashboardGrid extends PureComponent<Props> {
panel={panel}
dashboard={this.props.dashboard}
isEditing={panel.isEditing}
isFullscreen={panel.fullscreen}
isViewing={panel.isViewing}
isInView={panel.isInView}
/>
);
}
render() {
const { dashboard, isFullscreen } = this.props;
const { dashboard, viewPanel } = this.props;
return (
<SizedReactLayoutGrid
@ -296,7 +289,7 @@ export class DashboardGrid extends PureComponent<Props> {
onDragStop={this.onDragStop}
onResize={this.onResize}
onResizeStop={this.onResizeStop}
isFullscreen={isFullscreen}
viewPanel={viewPanel}
>
{this.renderPanels()}
</SizedReactLayoutGrid>

View File

@ -6,8 +6,6 @@ import { connect, MapStateToProps, MapDispatchToProps } from 'react-redux';
// Components
import { PanelChrome } from './PanelChrome';
import { PanelEditor } from '../panel_editor/PanelEditor';
import { PanelResizer } from './PanelResizer';
import { PanelChromeAngular } from './PanelChromeAngular';
// Actions
@ -23,8 +21,7 @@ export interface OwnProps {
panel: PanelModel;
dashboard: DashboardModel;
isEditing: boolean;
isInEditMode?: boolean;
isFullscreen: boolean;
isViewing: boolean;
isInView: boolean;
}
@ -74,12 +71,10 @@ export class DashboardPanelUnconnected extends PureComponent<Props, State> {
};
renderPanel(plugin: PanelPlugin) {
const { dashboard, panel, isFullscreen, isEditing, isInView, isInEditMode, updateLocation } = this.props;
const autoSizerStyle = { height: isEditing ? '100%' : '' };
const { dashboard, panel, isViewing, isInView, isEditing, updateLocation } = this.props;
return (
<AutoSizer style={autoSizerStyle}>
<AutoSizer>
{({ width, height }) => {
if (width === 0) {
return null;
@ -91,7 +86,8 @@ export class DashboardPanelUnconnected extends PureComponent<Props, State> {
plugin={plugin}
panel={panel}
dashboard={dashboard}
isFullscreen={isFullscreen}
isViewing={isViewing}
isEditing={isEditing}
isInView={isInView}
width={width}
height={height}
@ -104,9 +100,9 @@ export class DashboardPanelUnconnected extends PureComponent<Props, State> {
plugin={plugin}
panel={panel}
dashboard={dashboard}
isFullscreen={isFullscreen}
isViewing={isViewing}
isEditing={isEditing}
isInView={isInView}
isInEditMode={isInEditMode}
width={width}
height={height}
updateLocation={updateLocation}
@ -118,7 +114,7 @@ export class DashboardPanelUnconnected extends PureComponent<Props, State> {
}
render() {
const { panel, dashboard, isFullscreen, isEditing, plugin } = this.props;
const { isViewing, plugin } = this.props;
const { isLazy } = this.state;
// if we have not loaded plugin exports yet, wait
@ -131,34 +127,14 @@ export class DashboardPanelUnconnected extends PureComponent<Props, State> {
return null;
}
const editorContainerClasses = classNames({
'panel-editor-container': isEditing,
'panel-height-helper': !isEditing,
});
const panelWrapperClass = classNames({
'panel-wrapper': true,
'panel-wrapper--edit': isEditing,
'panel-wrapper--view': isFullscreen && !isEditing,
'panel-wrapper--view': isViewing,
});
return (
<div className={editorContainerClasses}>
<PanelResizer
isEditing={isEditing}
panel={panel}
render={styles => (
<div
className={panelWrapperClass}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
style={styles}
>
{this.renderPanel(plugin)}
</div>
)}
/>
{panel.isEditing && <PanelEditor panel={panel} plugin={plugin} dashboard={dashboard} />}
<div className={panelWrapperClass} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
{this.renderPanel(plugin)}
</div>
);
}

View File

@ -33,9 +33,9 @@ export interface Props {
panel: PanelModel;
dashboard: DashboardModel;
plugin: PanelPlugin;
isFullscreen: boolean;
isViewing: boolean;
isEditing?: boolean;
isInView: boolean;
isInEditMode?: boolean;
width: number;
height: number;
updateLocation: typeof updateLocation;
@ -69,7 +69,8 @@ export class PanelChrome extends PureComponent<Props, State> {
}
componentDidMount() {
const { panel, dashboard, isInEditMode } = this.props;
const { panel, dashboard, isEditing } = this.props;
panel.events.on(PanelEvents.refresh, this.onRefresh);
panel.events.on(PanelEvents.render, this.onRender);
dashboard.panelInitialized(this.props.panel);
@ -85,7 +86,7 @@ export class PanelChrome extends PureComponent<Props, State> {
isFirstLoad: false,
});
} else {
if (isInEditMode) {
if (isEditing) {
this.querySubscription = panel
.getQueryRunner()
.getData()
@ -319,7 +320,7 @@ export class PanelChrome extends PureComponent<Props, State> {
}
render() {
const { dashboard, panel, isFullscreen, width, height, updateLocation } = this.props;
const { dashboard, panel, isViewing, isEditing, width, height, updateLocation } = this.props;
const { errorMessage, data } = this.state;
const { transparent } = panel;
@ -340,7 +341,8 @@ export class PanelChrome extends PureComponent<Props, State> {
scopedVars={panel.scopedVars}
links={panel.links}
error={errorMessage}
isFullscreen={isFullscreen}
isEditing={isEditing}
isViewing={isViewing}
data={data}
updateLocation={updateLocation}
/>

View File

@ -24,7 +24,8 @@ interface OwnProps {
panel: PanelModel;
dashboard: DashboardModel;
plugin: PanelPlugin;
isFullscreen: boolean;
isViewing: boolean;
isEditing: boolean;
isInView: boolean;
width: number;
height: number;
@ -217,7 +218,7 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
}
render() {
const { dashboard, panel, isFullscreen, plugin, angularComponent, updateLocation } = this.props;
const { dashboard, panel, isViewing, isEditing, plugin, angularComponent, updateLocation } = this.props;
const { errorMessage, data, alertState } = this.state;
const { transparent } = panel;
@ -246,7 +247,8 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
angularComponent={angularComponent}
links={panel.links}
error={errorMessage}
isFullscreen={isFullscreen}
isViewing={isViewing}
isEditing={isEditing}
data={data}
updateLocation={updateLocation}
/>

View File

@ -25,7 +25,8 @@ export interface Props {
angularComponent?: AngularComponent | null;
links?: DataLink[];
error?: string;
isFullscreen: boolean;
isViewing: boolean;
isEditing: boolean;
data: PanelData;
updateLocation: typeof updateLocation;
}
@ -121,13 +122,13 @@ export class PanelHeader extends Component<Props, State> {
};
render() {
const { panel, scopedVars, error, isFullscreen, data } = this.props;
const { panel, scopedVars, error, isViewing, isEditing, data } = this.props;
const { menuItems } = this.state;
const title = templateSrv.replaceWithText(panel.title, scopedVars);
const panelHeaderClass = classNames({
'panel-header': true,
'grid-drag-handle': !isFullscreen,
'grid-drag-handle': !(isViewing || isEditing),
});
// dedupe on severity

View File

@ -4,7 +4,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
<SizeMe(GridWrapper)
className="layout"
isDraggable={true}
isFullscreen={false}
isResizable={true}
layout={
Array [
@ -43,6 +42,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
onResize={[Function]}
onResizeStop={[Function]}
onWidthChange={[Function]}
viewPanel={null}
>
<div
className=""
@ -96,13 +96,8 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
"fn": [Function],
"once": false,
},
"view-mode-changed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
},
"_eventsCount": 6,
"_eventsCount": 5,
},
},
"getVariables": [Function],
@ -117,8 +112,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
"canSave": true,
"canShare": true,
"canStar": true,
"fullscreen": false,
"isEditing": false,
"showSettings": true,
},
"originalTemplating": Array [],
@ -345,13 +338,8 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
"fn": [Function],
"once": false,
},
"view-mode-changed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
},
"_eventsCount": 6,
"_eventsCount": 5,
},
},
"getVariables": [Function],
@ -366,8 +354,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
"canSave": true,
"canShare": true,
"canStar": true,
"fullscreen": false,
"isEditing": false,
"showSettings": true,
},
"originalTemplating": Array [],
@ -594,13 +580,8 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
"fn": [Function],
"once": false,
},
"view-mode-changed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
},
"_eventsCount": 6,
"_eventsCount": 5,
},
},
"getVariables": [Function],
@ -615,8 +596,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
"canSave": true,
"canShare": true,
"canStar": true,
"fullscreen": false,
"isEditing": false,
"showSettings": true,
},
"originalTemplating": Array [],
@ -843,13 +822,8 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
"fn": [Function],
"once": false,
},
"view-mode-changed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
},
"_eventsCount": 6,
"_eventsCount": 5,
},
},
"getVariables": [Function],
@ -864,8 +838,6 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
"canSave": true,
"canShare": true,
"canStar": true,
"fullscreen": false,
"isEditing": false,
"showSettings": true,
},
"originalTemplating": Array [],

View File

@ -53,6 +53,7 @@ export class ChangeTracker {
if (this.originalPath === $location.path()) {
return true;
}
if (this.ignoreChanges()) {
return true;
}

View File

@ -1,6 +1,3 @@
import { ILocationService } from 'angular';
import { PanelEvents } from '@grafana/data';
import coreModule from 'app/core/core_module';
import { appEvents } from 'app/core/app_events';
import { DashboardModel } from '../state/DashboardModel';
@ -14,8 +11,7 @@ export class DashboardSrv {
dashboard: DashboardModel;
/** @ngInject */
constructor(private $rootScope: GrafanaRootScope, private $location: ILocationService) {
appEvents.on(PanelEvents.panelChangeView, this.onPanelChangeView);
constructor(private $rootScope: GrafanaRootScope) {
appEvents.on(CoreEvents.removePanel, this.onRemovePanel);
}
@ -36,48 +32,6 @@ export class DashboardSrv {
removePanel(dashboard, dashboard.getPanelById(panelId), true);
};
onPanelChangeView = ({
fullscreen = false,
edit = false,
panelId,
}: {
fullscreen?: boolean;
edit?: boolean;
panelId?: number;
}) => {
const urlParams = this.$location.search();
// handle toggle logic
// I hate using these truthy converters (!!) but in this case
// I think it's appropriate. edit can be null/false/undefined and
// here i want all of those to compare the same
if (fullscreen === urlParams.fullscreen && edit === !!urlParams.edit) {
const paramsToRemove = ['fullscreen', 'edit', 'panelId', 'tab'];
for (const key of paramsToRemove) {
delete urlParams[key];
}
this.$location.search(urlParams);
return;
}
const newUrlParams = {
...urlParams,
fullscreen: fullscreen || undefined,
edit: edit || undefined,
tab: edit ? urlParams.tab : undefined,
panelId,
};
Object.keys(newUrlParams).forEach(key => {
if (newUrlParams[key] === undefined) {
delete newUrlParams[key];
}
});
this.$location.search(newUrlParams);
};
saveJSONDashboard(json: string) {
const parsedJson = JSON.parse(json);
return getBackendSrv().saveDashboard(parsedJson, {

View File

@ -51,6 +51,7 @@ export class DashboardModel {
gnetId: any;
panels: PanelModel[];
panelInEdit?: PanelModel;
panelInView: PanelModel;
// ------------------
// not persisted
@ -69,6 +70,7 @@ export class DashboardModel {
originalTime: true,
originalTemplating: true,
panelInEdit: true,
panelInView: true,
getVariablesFromState: true,
};
@ -144,8 +146,6 @@ export class DashboardModel {
meta.canEdit = meta.canEdit !== false;
meta.showSettings = meta.canEdit;
meta.canMakeEditable = meta.canSave && !this.editable;
meta.fullscreen = false;
meta.isEditing = false;
if (!this.editable) {
meta.canEdit = false;
@ -263,15 +263,6 @@ export class DashboardModel {
}
}
setViewMode(panel: PanelModel, fullscreen: boolean, isEditing: boolean) {
this.meta.fullscreen = fullscreen;
this.meta.isEditing = isEditing && this.meta.canEdit;
panel.setViewMode(fullscreen, this.meta.isEditing);
this.events.emit(PanelEvents.viewModeChanged, panel);
}
timeRangeUpdated(timeRange: TimeRange) {
this.events.emit(CoreEvents.timeRangeUpdated, timeRange);
if (getConfig().featureToggles.newVariables) {
@ -317,14 +308,24 @@ export class DashboardModel {
}
otherPanelInFullscreen(panel: PanelModel) {
return (this.meta.fullscreen && !panel.fullscreen) || this.panelInEdit;
return (this.panelInEdit || this.panelInView) && !(panel.isViewing || panel.isEditing);
}
initPanelEditor(sourcePanel: PanelModel): PanelModel {
initEditPanel(sourcePanel: PanelModel): PanelModel {
this.panelInEdit = sourcePanel.getEditClone();
return this.panelInEdit;
}
initViewPanel(panel: PanelModel) {
this.panelInView = panel;
panel.setIsViewing(true);
}
exitViewPanel(panel: PanelModel) {
this.panelInView = undefined;
panel.setIsViewing(false);
}
exitPanelEditor() {
this.panelInEdit = undefined;
}

View File

@ -37,7 +37,7 @@ export interface GridPos {
const notPersistedProperties: { [str: string]: boolean } = {
events: true,
fullscreen: true,
isViewing: true,
isEditing: true,
isInView: true,
hasRefreshed: true,
@ -134,7 +134,7 @@ export class PanelModel implements DataConfigSource {
transparent: boolean;
// non persisted
fullscreen: boolean;
isViewing: boolean;
isEditing: boolean;
isInView: boolean;
hasRefreshed: boolean;
@ -215,10 +215,8 @@ export class PanelModel implements DataConfigSource {
return model;
}
setViewMode(fullscreen: boolean, isEditing: boolean) {
this.fullscreen = fullscreen;
this.isEditing = isEditing;
this.events.emit(PanelEvents.viewModeChanged);
setIsViewing(isViewing: boolean) {
this.isViewing = isViewing;
}
updateGridPos(newPos: GridPos) {
@ -394,6 +392,7 @@ export class PanelModel implements DataConfigSource {
sourceModel.editSourceId = this.id;
const clone = new PanelModel(sourceModel);
clone.isEditing = true;
const sourceQueryRunner = this.getQueryRunner();
// pipe last result to new clone query runner
@ -466,6 +465,14 @@ export class PanelModel implements DataConfigSource {
this.getQueryRunner().resendLastResult();
}
/*
* Panel have a different id while in edit mode (to more easily be able to discard changes)
* Use this to always get the underlying source id
* */
getSavedId(): number {
return this.editSourceId ?? this.id;
}
}
function getPluginVersion(plugin: PanelPlugin): string {

View File

@ -1,6 +1,5 @@
import { updateLocation } from 'app/core/actions';
import { store } from 'app/store/store';
import config from 'app/core/config';
import { getDataSourceSrv, getLocationSrv, AngularComponent } from '@grafana/runtime';
import { PanelMenuItem } from '@grafana/data';
import { copyPanel, duplicatePanel, editPanelJson, removePanel, sharePanel } from 'app/features/dashboard/utils/panel';
@ -22,9 +21,7 @@ export function getPanelMenu(
store.dispatch(
updateLocation({
query: {
panelId: panel.id,
edit: null,
fullscreen: true,
viewPanel: panel.id,
},
partial: true,
})
@ -32,20 +29,6 @@ export function getPanelMenu(
};
const onEditPanel = (event: React.MouseEvent<any>) => {
event.preventDefault();
store.dispatch(
updateLocation({
query: {
panelId: panel.id,
edit: true,
fullscreen: true,
},
partial: true,
})
);
};
const onNewEditPanel = (event: React.MouseEvent<any>) => {
event.preventDefault();
store.dispatch(
updateLocation({
@ -143,18 +126,9 @@ export function getPanelMenu(
shortcut: 'p i',
});
if (config.featureToggles.newEdit) {
menu.push({
text: 'New edit',
iconClassName: 'edit',
onClick: onNewEditPanel,
shortcut: 'p i',
});
}
const subMenu: PanelMenuItem[] = [];
if (!panel.fullscreen && dashboard.canEditPanel(panel)) {
if (dashboard.canEditPanel(panel) && !(panel.isViewing || panel.isEditing)) {
subMenu.push({
text: 'Duplicate',
onClick: onDuplicatePanel,

View File

@ -1,4 +1,3 @@
import omitBy from 'lodash/omitBy';
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { hot } from 'react-hot-loader';
@ -133,18 +132,15 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
});
}
const dashViewOptions = {
fullscreen: withChanges || dash.meta.fullscreen,
edit: withChanges || dash.meta.isEditing,
};
const query: any = {};
this.props.updateLocation({
path: `/d/${dash.uid}/:${titleSlug}`,
query: {
...omitBy(dashViewOptions, v => !v),
panelId: originPanelId,
},
});
if (withChanges || dash.panelInEdit) {
query.editPanel = originPanelId;
} else if (dash.panelInView) {
query.viewPanel = originPanelId;
}
this.props.updateLocation({ path: `/d/${dash.uid}/:${titleSlug}`, query });
};
// Remove explore specific parameters from queries

View File

@ -95,7 +95,7 @@ export class PanelCtrl {
}
otherPanelInFullscreenMode() {
return this.dashboard.meta.fullscreen && !this.panel.fullscreen;
return this.dashboard.otherPanelInFullscreen(this.panel);
}
render(payload?: any) {

View File

@ -65,17 +65,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
});
}
function onViewModeChanged() {
// first wait one pass for dashboard fullscreen view mode to take effect (classses being applied)
setTimeout(() => {
// then wait another cycle (this might not be needed)
$timeout(() => {
ctrl.render();
resizeScrollableContent();
});
}, 10);
}
function onPanelModelRender(payload?: any) {
ctrl.height = scope.$parent.$parent.size.height;
ctrl.width = scope.$parent.$parent.size.width;
@ -89,7 +78,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
panel.events.on(PanelEvents.refresh, onPanelModelRefresh);
panel.events.on(PanelEvents.render, onPanelModelRender);
panel.events.on(PanelEvents.panelSizeChanged, onPanelSizeChanged);
panel.events.on(PanelEvents.viewModeChanged, onViewModeChanged);
scope.$on('$destroy', () => {
elem.off();

View File

@ -43,7 +43,7 @@ export function runSharedRequest(options: QueryRunnerOptions): Observable<PanelD
// If we are in fullscreen the other panel will not execute any queries
// So we have to trigger it from here
if (currentPanel.fullscreen) {
if (currentPanel.isViewing || currentPanel.isEditing) {
const { datasource, targets } = listenToPanel;
const modified = {
...options,

View File

@ -13,7 +13,6 @@ export const plugin = new PanelPlugin<BarGaugeOptions>(BarGaugePanel)
.addRadio({
path: 'displayMode',
name: 'Display mode',
description: 'Controls the bar style',
settings: {
options: displayModes,
},
@ -24,7 +23,7 @@ export const plugin = new PanelPlugin<BarGaugeOptions>(BarGaugePanel)
name: 'Show unfilled area',
description: 'When enabled renders the unfilled region as gray',
defaultValue: true,
showIf: options => options.displayMode !== 'lcd',
showIf: (options: BarGaugeOptions) => options.displayMode !== 'lcd',
});
})
.setPanelChangeHandler(sharedSingleStatPanelChangedHandler)

View File

@ -18,9 +18,7 @@ export interface DashboardMeta {
canAdmin?: boolean;
url?: string;
folderId?: number;
fullscreen?: boolean;
fromExplore?: boolean;
isEditing?: boolean;
canMakeEditable?: boolean;
submenuEnabled?: boolean;
provisioned?: boolean;

View File

@ -8,11 +8,6 @@
height: 100%;
position: relative;
&--edit {
height: 40%;
margin: 0 $dashboard-padding;
}
&--view {
flex: 1 1 0;
height: 90%;