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

View File

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

View File

@ -15,48 +15,15 @@ e2e.scenario({
e2e.pages.Dashboard.Toolbar.toolbarItems('Add panel').click(); e2e.pages.Dashboard.Toolbar.toolbarItems('Add panel').click();
e2e.pages.AddDashboard.ctaButtons('Add Query').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.DataSource.TestData.QueryTab.scenarioSelect().select('CSV Metric Values');
e2e.pages.Dashboard.Panels.EditPanel.tabItems('Visualization').click();
e2e.pages.Dashboard.Panels.Visualization.Graph.VisualizationTab.xAxisSection() e2e.pages.Dashboard.Panels.Visualization.Graph.VisualizationTab.xAxisSection()
.contains('Show') .contains('Show')
.click(); .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('Inspect').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 });
});
}, },
}); });

View File

@ -49,7 +49,7 @@ export interface OptionsEditorItem<TOptions, TSettings, TEditorProps, TValue> ex
interface OptionEditorConfig<TOptions, TSettings, TValue = any> { interface OptionEditorConfig<TOptions, TSettings, TValue = any> {
id: keyof TOptions & string; id: keyof TOptions & string;
name: string; name: string;
description: string; description?: string;
settings?: TSettings; settings?: TSettings;
defaultValue?: TValue; 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 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. * 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 of the option. Will be displayed in the UI as form element description.
*/ */
description: string; description?: string;
/**al /**al
* Custom settings of the editor. * Custom settings of the editor.
*/ */

View File

@ -3,7 +3,7 @@ import { SelectableValue } from '../types/select';
export interface RegistryItem { export interface RegistryItem {
id: string; // Unique Key -- saved in configs id: string; // Unique Key -- saved in configs
name: string; // Display Name, can change without breaking 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') 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}`, url: (uid: string) => `/d/${uid}`,
selectors: { selectors: {
toolbarItems: (button: string) => `Dashboard navigation bar button ${button}`, toolbarItems: (button: string) => `Dashboard navigation bar button ${button}`,
backArrow: 'Dashboard settings Go Back button',
navBar: () => '.navbar', navBar: () => '.navbar',
}, },
}); });

View File

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

View File

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

View File

@ -200,7 +200,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<FullWidthButtonContainer className={styles.addButton}> <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 Add threshold
</Button> </Button>
</FullWidthButtonContainer> </FullWidthButtonContainer>

View File

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

View File

@ -112,7 +112,7 @@ func (c *EvalContext) GetDashboardUID() (*models.DashboardRef, error) {
return c.dashboardRef, nil 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. // GetRuleURL returns the url to the dashboard containing the alert.
func (c *EvalContext) GetRuleURL() (string, error) { func (c *EvalContext) GetRuleURL() (string, error) {

View File

@ -1,7 +1,8 @@
import React, { ButtonHTMLAttributes } from 'react'; import React, { ButtonHTMLAttributes } from 'react';
import { css } from 'emotion'; import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data'; 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>; export type Props = ButtonHTMLAttributes<HTMLButtonElement>;
@ -11,7 +12,7 @@ export const BackButton: React.FC<Props> = props => {
return ( return (
<Tooltip content="Go back (Esc)" placement="bottom"> <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" /> <i className="gicon gicon-arrow-left" />
</button> </button>
</Tooltip> </Tooltip>

View File

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

View File

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

View File

@ -15,6 +15,7 @@ export class BridgeSrv {
private fullPageReloadRoutes: string[]; private fullPageReloadRoutes: string[];
private lastQuery: UrlQueryMap = {}; private lastQuery: UrlQueryMap = {};
private lastPath = ''; private lastPath = '';
private angularUrl: string;
/** @ngInject */ /** @ngInject */
constructor( constructor(
@ -26,13 +27,16 @@ export class BridgeSrv {
private variableSrv: VariableSrv private variableSrv: VariableSrv
) { ) {
this.fullPageReloadRoutes = ['/logout']; this.fullPageReloadRoutes = ['/logout'];
this.angularUrl = $location.url();
} }
init() { init() {
this.$rootScope.$on('$routeUpdate', (evt, data) => { this.$rootScope.$on('$routeUpdate', (evt, data) => {
const angularUrl = this.$location.url();
const state = store.getState(); const state = store.getState();
if (state.location.url !== angularUrl) {
this.angularUrl = this.$location.url();
if (state.location.url !== this.angularUrl) {
store.dispatch( store.dispatch(
updateLocation({ updateLocation({
path: this.$location.path(), path: this.$location.path(),
@ -44,6 +48,8 @@ export class BridgeSrv {
}); });
this.$rootScope.$on('$routeChangeSuccess', (evt, data) => { this.$rootScope.$on('$routeChangeSuccess', (evt, data) => {
this.angularUrl = this.$location.url();
store.dispatch( store.dispatch(
updateLocation({ updateLocation({
path: this.$location.path(), path: this.$location.path(),
@ -56,9 +62,12 @@ export class BridgeSrv {
// Listen for changes in redux location -> update angular location // Listen for changes in redux location -> update angular location
store.subscribe(() => { store.subscribe(() => {
const state = store.getState(); const state = store.getState();
const angularUrl = this.$location.url();
const url = state.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.$timeout(() => {
this.$location.url(url); this.$location.url(url);
// some state changes should not trigger new browser history // some state changes should not trigger new browser history
@ -66,6 +75,7 @@ export class BridgeSrv {
this.$location.replace(); this.$location.replace();
} }
}); });
console.log('store updating angular $location.url', url); 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 { AppEventEmitter, CoreEvents } from 'app/types';
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import { PanelEvents } from '@grafana/data';
import 'mousetrap-global-bind'; import 'mousetrap-global-bind';
import { ContextSrv } from './context_srv'; import { ContextSrv } from './context_srv';
import { ILocationService, IRootScopeService, ITimeoutService } from 'angular'; import { ILocationService, IRootScopeService, ITimeoutService } from 'angular';
@ -133,8 +132,9 @@ export class KeybindingSrv {
return; return;
} }
if (search.fullscreen) { if (search.viewPanel) {
appEvents.emit(PanelEvents.panelChangeView, { fullscreen: false, edit: false }); delete search.viewPanel;
this.$location.search(search);
return; return;
} }
@ -213,23 +213,16 @@ export class KeybindingSrv {
// edit panel // edit panel
this.bind('e', () => { this.bind('e', () => {
if (dashboard.canEditPanelById(dashboard.meta.focusPanelId)) { if (dashboard.canEditPanelById(dashboard.meta.focusPanelId)) {
appEvents.emit(PanelEvents.panelChangeView, { const search = _.extend(this.$location.search(), { editPanel: dashboard.meta.focusPanelId });
fullscreen: true, this.$location.search(search);
edit: true,
panelId: dashboard.meta.focusPanelId,
toggle: true,
});
} }
}); });
// view panel // view panel
this.bind('v', () => { this.bind('v', () => {
if (dashboard.meta.focusPanelId) { if (dashboard.meta.focusPanelId) {
appEvents.emit(PanelEvents.panelChangeView, { const search = _.extend(this.$location.search(), { viewPanel: dashboard.meta.focusPanelId });
fullscreen: true, this.$location.search(search);
panelId: dashboard.meta.focusPanelId,
toggle: true,
});
} }
}); });

View File

@ -15,7 +15,6 @@ import {
LogRowModel, LogRowModel,
LogsDedupStrategy, LogsDedupStrategy,
LogsModel, LogsModel,
PanelModel,
RawTimeRange, RawTimeRange,
TimeFragment, TimeFragment,
TimeRange, TimeRange,
@ -33,6 +32,7 @@ import { ExploreUrlState, QueryOptions, QueryTransaction } from 'app/types/explo
import { config } from '../config'; import { config } from '../config';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { DataSourceSrv } from '@grafana/runtime'; import { DataSourceSrv } from '@grafana/runtime';
import { PanelModel } from 'app/features/dashboard/state';
export const DEFAULT_RANGE = { export const DEFAULT_RANGE = {
from: 'now-1h', 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 }); url = renderUrl('/explore', { left: exploreState });
} }
return url; return url;

View File

@ -24,7 +24,7 @@ class AlertRuleItem extends PureComponent<Props> {
render() { render() {
const { rule, onTogglePause } = this.props; 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 ( return (
<li className="alert-rule-item"> <li className="alert-rule-item">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -89,7 +89,7 @@ export class SoloPanelPage extends Component<Props, State> {
return ( return (
<div className="panel-solo"> <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> </div>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -53,6 +53,7 @@ export class ChangeTracker {
if (this.originalPath === $location.path()) { if (this.originalPath === $location.path()) {
return true; return true;
} }
if (this.ignoreChanges()) { if (this.ignoreChanges()) {
return true; 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 coreModule from 'app/core/core_module';
import { appEvents } from 'app/core/app_events'; import { appEvents } from 'app/core/app_events';
import { DashboardModel } from '../state/DashboardModel'; import { DashboardModel } from '../state/DashboardModel';
@ -14,8 +11,7 @@ export class DashboardSrv {
dashboard: DashboardModel; dashboard: DashboardModel;
/** @ngInject */ /** @ngInject */
constructor(private $rootScope: GrafanaRootScope, private $location: ILocationService) { constructor(private $rootScope: GrafanaRootScope) {
appEvents.on(PanelEvents.panelChangeView, this.onPanelChangeView);
appEvents.on(CoreEvents.removePanel, this.onRemovePanel); appEvents.on(CoreEvents.removePanel, this.onRemovePanel);
} }
@ -36,48 +32,6 @@ export class DashboardSrv {
removePanel(dashboard, dashboard.getPanelById(panelId), true); 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) { saveJSONDashboard(json: string) {
const parsedJson = JSON.parse(json); const parsedJson = JSON.parse(json);
return getBackendSrv().saveDashboard(parsedJson, { return getBackendSrv().saveDashboard(parsedJson, {

View File

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

View File

@ -37,7 +37,7 @@ export interface GridPos {
const notPersistedProperties: { [str: string]: boolean } = { const notPersistedProperties: { [str: string]: boolean } = {
events: true, events: true,
fullscreen: true, isViewing: true,
isEditing: true, isEditing: true,
isInView: true, isInView: true,
hasRefreshed: true, hasRefreshed: true,
@ -134,7 +134,7 @@ export class PanelModel implements DataConfigSource {
transparent: boolean; transparent: boolean;
// non persisted // non persisted
fullscreen: boolean; isViewing: boolean;
isEditing: boolean; isEditing: boolean;
isInView: boolean; isInView: boolean;
hasRefreshed: boolean; hasRefreshed: boolean;
@ -215,10 +215,8 @@ export class PanelModel implements DataConfigSource {
return model; return model;
} }
setViewMode(fullscreen: boolean, isEditing: boolean) { setIsViewing(isViewing: boolean) {
this.fullscreen = fullscreen; this.isViewing = isViewing;
this.isEditing = isEditing;
this.events.emit(PanelEvents.viewModeChanged);
} }
updateGridPos(newPos: GridPos) { updateGridPos(newPos: GridPos) {
@ -394,6 +392,7 @@ export class PanelModel implements DataConfigSource {
sourceModel.editSourceId = this.id; sourceModel.editSourceId = this.id;
const clone = new PanelModel(sourceModel); const clone = new PanelModel(sourceModel);
clone.isEditing = true;
const sourceQueryRunner = this.getQueryRunner(); const sourceQueryRunner = this.getQueryRunner();
// pipe last result to new clone query runner // pipe last result to new clone query runner
@ -466,6 +465,14 @@ export class PanelModel implements DataConfigSource {
this.getQueryRunner().resendLastResult(); 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 { function getPluginVersion(plugin: PanelPlugin): string {

View File

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

View File

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

View File

@ -95,7 +95,7 @@ export class PanelCtrl {
} }
otherPanelInFullscreenMode() { otherPanelInFullscreenMode() {
return this.dashboard.meta.fullscreen && !this.panel.fullscreen; return this.dashboard.otherPanelInFullscreen(this.panel);
} }
render(payload?: any) { 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) { function onPanelModelRender(payload?: any) {
ctrl.height = scope.$parent.$parent.size.height; ctrl.height = scope.$parent.$parent.size.height;
ctrl.width = scope.$parent.$parent.size.width; 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.refresh, onPanelModelRefresh);
panel.events.on(PanelEvents.render, onPanelModelRender); panel.events.on(PanelEvents.render, onPanelModelRender);
panel.events.on(PanelEvents.panelSizeChanged, onPanelSizeChanged); panel.events.on(PanelEvents.panelSizeChanged, onPanelSizeChanged);
panel.events.on(PanelEvents.viewModeChanged, onViewModeChanged);
scope.$on('$destroy', () => { scope.$on('$destroy', () => {
elem.off(); 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 // If we are in fullscreen the other panel will not execute any queries
// So we have to trigger it from here // So we have to trigger it from here
if (currentPanel.fullscreen) { if (currentPanel.isViewing || currentPanel.isEditing) {
const { datasource, targets } = listenToPanel; const { datasource, targets } = listenToPanel;
const modified = { const modified = {
...options, ...options,

View File

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

View File

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

View File

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