mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TopNav: Panel edit changes (#54746)
* Progress * Progress * Things are working * More tweaks * Fixing unit test * Tweaks and fixing e2e tests * Remove ... in Save as * Fixing unit test * Fixing e2e test * Fixes Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
@@ -1,21 +1,19 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||
|
||||
export interface AppChromeUpdateProps {
|
||||
pageNav?: NavModelItem;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
/**
|
||||
* This needs to be moved to @grafana/ui or runtime.
|
||||
* This is the way core pages and plugins update the breadcrumbs and page toolbar actions
|
||||
*/
|
||||
export const AppChromeUpdate = React.memo<AppChromeUpdateProps>(({ pageNav, actions }: AppChromeUpdateProps) => {
|
||||
export const AppChromeUpdate = React.memo<AppChromeUpdateProps>(({ actions }: AppChromeUpdateProps) => {
|
||||
const { chrome } = useGrafana();
|
||||
|
||||
useEffect(() => {
|
||||
chrome.update({ pageNav, actions });
|
||||
chrome.update({ actions });
|
||||
});
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
pageToolbar: css({
|
||||
height: TOP_BAR_LEVEL_HEIGHT,
|
||||
display: 'flex',
|
||||
padding: theme.spacing(0, 2),
|
||||
padding: theme.spacing(0, 1, 0, 2),
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}),
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
import { Location as HistoryLocation } from 'history';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { GrafanaTheme2, NavModelItem, NavSection } from '@grafana/data';
|
||||
import { config, locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { CustomScrollbar, Icon, useTheme2 } from '@grafana/ui';
|
||||
import { config, locationSearchToObject, locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { Icon, useTheme2, CustomScrollbar } from '@grafana/ui';
|
||||
import { getKioskMode } from 'app/core/navigation/kiosk';
|
||||
import { KioskMode, StoreState } from 'app/types';
|
||||
|
||||
@@ -38,7 +39,6 @@ export const NavBar = React.memo(() => {
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
const location = useLocation();
|
||||
const kiosk = getKioskMode();
|
||||
const [showSwitcherModal, setShowSwitcherModal] = useState(false);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [menuAnimationInProgress, setMenuAnimationInProgress] = useState(false);
|
||||
@@ -85,9 +85,10 @@ export const NavBar = React.memo(() => {
|
||||
|
||||
const activeItem = isSearchActive(location) ? searchItem : getActiveItem(navTree, location.pathname);
|
||||
|
||||
if (kiosk !== KioskMode.Off) {
|
||||
if (shouldHideNavBar(location)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.navWrapper}>
|
||||
<nav className={cx(styles.sidemenu, 'sidemenu')} data-testid="sidemenu" aria-label="Main menu">
|
||||
@@ -174,6 +175,22 @@ export const NavBar = React.memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
function shouldHideNavBar(location: HistoryLocation) {
|
||||
const queryParams = locationSearchToObject(location.search);
|
||||
const kiosk = getKioskMode(queryParams);
|
||||
|
||||
if (kiosk !== KioskMode.Off) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Temporary, can be removed after topnav is made permanent
|
||||
if ((location.pathname.indexOf('/d/') === 0 && queryParams.editview) || queryParams.editPanel) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
NavBar.displayName = 'NavBar';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
|
||||
@@ -26,6 +26,8 @@ export const OldPage: PageType = ({
|
||||
scrollRef,
|
||||
scrollTop,
|
||||
layout = PageLayoutType.Standard,
|
||||
subTitle,
|
||||
...otherProps
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const navModel = usePageNav(navId, oldNavProp);
|
||||
@@ -35,10 +37,10 @@ export const OldPage: PageType = ({
|
||||
const pageHeaderNav = pageNav ?? navModel?.main;
|
||||
|
||||
return (
|
||||
<div className={cx(styles.wrapper, className)}>
|
||||
<div className={cx(styles.wrapper, className)} {...otherProps}>
|
||||
{layout === PageLayoutType.Standard && (
|
||||
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
||||
<div className="page-scrollbar-content">
|
||||
<div className={cx('page-scrollbar-content', className)}>
|
||||
{pageHeaderNav && <PageHeader navItem={pageHeaderNav} />}
|
||||
{children}
|
||||
<Footer />
|
||||
@@ -55,6 +57,12 @@ export const OldPage: PageType = ({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{layout === PageLayoutType.Custom && (
|
||||
<>
|
||||
{toolbar}
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ export const Page: PageType = ({
|
||||
toolbar,
|
||||
scrollTop,
|
||||
scrollRef,
|
||||
...otherProps
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const navModel = usePageNav(navId, oldNavProp);
|
||||
@@ -46,7 +47,7 @@ export const Page: PageType = ({
|
||||
}, [navModel, pageNav, chrome]);
|
||||
|
||||
return (
|
||||
<div className={cx(styles.wrapper, className)}>
|
||||
<div className={cx(styles.wrapper, className)} {...otherProps}>
|
||||
{layout === PageLayoutType.Standard && (
|
||||
<div className={styles.panes}>
|
||||
{navModel && navModel.main.children && <SectionNav model={navModel} />}
|
||||
@@ -70,6 +71,12 @@ export const Page: PageType = ({
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
)}
|
||||
{layout === PageLayoutType.Custom && (
|
||||
<>
|
||||
{toolbar}
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { AppEvents, UrlQueryMap } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
|
||||
import { KioskMode } from '../../types';
|
||||
@@ -27,10 +27,8 @@ export function toggleKioskMode() {
|
||||
locationService.partial({ kiosk });
|
||||
}
|
||||
|
||||
export function getKioskMode(): KioskMode {
|
||||
const kiosk = locationService.getSearchObject().kiosk;
|
||||
|
||||
switch (kiosk) {
|
||||
export function getKioskMode(queryParams: UrlQueryMap): KioskMode {
|
||||
switch (queryParams.kiosk) {
|
||||
case 'tv':
|
||||
return KioskMode.TV;
|
||||
// legacy support
|
||||
|
||||
@@ -325,7 +325,6 @@ export const DashNav = React.memo<Props>((props) => {
|
||||
if (config.featureToggles.topnav) {
|
||||
return (
|
||||
<AppChromeUpdate
|
||||
pageNav={{ text: title }}
|
||||
actions={
|
||||
<>
|
||||
{renderLeftActions()}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { DashboardModel } from '../../state';
|
||||
export interface Props {
|
||||
dashboard: DashboardModel;
|
||||
onChangeTimeZone: (timeZone: TimeZone) => void;
|
||||
isOnCanvas?: boolean;
|
||||
}
|
||||
|
||||
export class DashNavTimeControls extends Component<Props> {
|
||||
@@ -77,7 +78,7 @@ export class DashNavTimeControls extends Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dashboard } = this.props;
|
||||
const { dashboard, isOnCanvas } = this.props;
|
||||
const { refresh_intervals } = dashboard.timepicker;
|
||||
const intervals = getTimeSrv().getValidIntervals(refresh_intervals || defaultIntervals);
|
||||
|
||||
@@ -98,12 +99,14 @@ export class DashNavTimeControls extends Component<Props> {
|
||||
onZoom={this.onZoom}
|
||||
onChangeTimeZone={this.onChangeTimeZone}
|
||||
onChangeFiscalYearStartMonth={this.onChangeFiscalYearStartMonth}
|
||||
isOnCanvas={isOnCanvas}
|
||||
/>
|
||||
<RefreshPicker
|
||||
onIntervalChanged={this.onChangeRefreshInterval}
|
||||
onRefresh={this.onRefresh}
|
||||
value={dashboard.refresh}
|
||||
intervals={intervals}
|
||||
isOnCanvas={isOnCanvas}
|
||||
tooltip={t({ id: 'dashboard.toolbar.refresh', message: 'Refresh dashboard' })}
|
||||
noIntervalPicker={hideIntervalPicker}
|
||||
offDescriptionAriaLabelMsg={t({
|
||||
|
||||
@@ -46,12 +46,19 @@ export function DashboardSettings({ dashboard, editview, pageNav, sectionNav }:
|
||||
const location = useLocation();
|
||||
const editIndex = getEditIndex(location);
|
||||
const subSectionNav = getSectionNav(pageNav, sectionNav, pages, currentPage, location);
|
||||
const size = config.featureToggles.topnav ? 'sm' : 'md';
|
||||
|
||||
const actions = [
|
||||
canSaveAs && (
|
||||
<SaveDashboardAsButton dashboard={dashboard} onSaveSuccess={onPostSave} variant="secondary" key="save as" />
|
||||
<SaveDashboardAsButton
|
||||
dashboard={dashboard}
|
||||
onSaveSuccess={onPostSave}
|
||||
variant="secondary"
|
||||
key="save as"
|
||||
size={size}
|
||||
/>
|
||||
),
|
||||
canSave && <SaveDashboardButton dashboard={dashboard} onSaveSuccess={onPostSave} key="Save" />,
|
||||
canSave && <SaveDashboardButton dashboard={dashboard} onSaveSuccess={onPostSave} key="Save" size={size} />,
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,20 +4,25 @@ import { connect, ConnectedProps } from 'react-redux';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { FieldConfigSource, GrafanaTheme2 } from '@grafana/data';
|
||||
import { FieldConfigSource, GrafanaTheme2, NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import {
|
||||
Button,
|
||||
HorizontalGroup,
|
||||
InlineSwitch,
|
||||
ModalsController,
|
||||
PageToolbar,
|
||||
RadioButtonGroup,
|
||||
Stack,
|
||||
stylesFactory,
|
||||
Themeable2,
|
||||
ToolbarButton,
|
||||
ToolbarButtonRow,
|
||||
withTheme2,
|
||||
} from '@grafana/ui';
|
||||
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper';
|
||||
import { appEvents } from 'app/core/core';
|
||||
import { SubMenuItems } from 'app/features/dashboard/components/SubMenu/SubMenuItems';
|
||||
@@ -50,6 +55,9 @@ import { calculatePanelSize } from './utils';
|
||||
interface OwnProps {
|
||||
dashboard: DashboardModel;
|
||||
sourcePanel: PanelModel;
|
||||
sectionNav: NavModel;
|
||||
pageNav: NavModelItem;
|
||||
className?: string;
|
||||
tab?: string;
|
||||
}
|
||||
|
||||
@@ -127,12 +135,6 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
this.onBack();
|
||||
};
|
||||
|
||||
onOpenDashboardSettings = () => {
|
||||
locationService.partial({
|
||||
editview: 'settings',
|
||||
});
|
||||
};
|
||||
|
||||
onSaveDashboard = () => {
|
||||
appEvents.publish(
|
||||
new ShowModalReactEvent({
|
||||
@@ -293,7 +295,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
<div className={styles.panelToolbar}>
|
||||
<HorizontalGroup justify={variables.length > 0 ? 'space-between' : 'flex-end'} align="flex-start">
|
||||
{this.renderTemplateVariables(styles)}
|
||||
<HorizontalGroup>
|
||||
<Stack gap={1}>
|
||||
<InlineSwitch
|
||||
label="Table view"
|
||||
showLabel={true}
|
||||
@@ -303,47 +305,58 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
aria-label={selectors.components.PanelEditor.toggleTableView}
|
||||
/>
|
||||
<RadioButtonGroup value={uiState.mode} options={displayModes} onChange={this.onDisplayModeChange} />
|
||||
<DashNavTimeControls dashboard={dashboard} onChangeTimeZone={updateTimeZoneForSession} />
|
||||
<DashNavTimeControls dashboard={dashboard} onChangeTimeZone={updateTimeZoneForSession} isOnCanvas={true} />
|
||||
{!uiState.isPanelOptionsVisible && <VisualizationButton panel={panel} />}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderEditorActions() {
|
||||
const size = config.featureToggles.topnav ? 'sm' : 'md';
|
||||
let editorActions = [
|
||||
<ToolbarButton
|
||||
icon="cog"
|
||||
onClick={this.onOpenDashboardSettings}
|
||||
tooltip="Open dashboard settings"
|
||||
key="settings"
|
||||
/>,
|
||||
<ToolbarButton onClick={this.onDiscard} tooltip="Undo all changes" key="discard">
|
||||
<Button
|
||||
onClick={this.onDiscard}
|
||||
title="Undo all changes"
|
||||
key="discard"
|
||||
size={size}
|
||||
variant="destructive"
|
||||
fill="outline"
|
||||
>
|
||||
Discard
|
||||
</ToolbarButton>,
|
||||
</Button>,
|
||||
this.props.panel.libraryPanel ? (
|
||||
<ToolbarButton
|
||||
<Button
|
||||
onClick={this.onSaveLibraryPanel}
|
||||
variant="primary"
|
||||
tooltip="Apply changes and save library panel"
|
||||
size={size}
|
||||
title="Apply changes and save library panel"
|
||||
key="save-panel"
|
||||
>
|
||||
Save library panel
|
||||
</ToolbarButton>
|
||||
</Button>
|
||||
) : (
|
||||
<ToolbarButton onClick={this.onSaveDashboard} tooltip="Apply changes and save dashboard" key="save">
|
||||
<Button
|
||||
onClick={this.onSaveDashboard}
|
||||
title="Apply changes and save dashboard"
|
||||
key="save"
|
||||
size={size}
|
||||
variant="secondary"
|
||||
>
|
||||
Save
|
||||
</ToolbarButton>
|
||||
</Button>
|
||||
),
|
||||
<ToolbarButton
|
||||
<Button
|
||||
onClick={this.onBack}
|
||||
variant="primary"
|
||||
tooltip="Apply changes and go back to dashboard"
|
||||
title="Apply changes and go back to dashboard"
|
||||
data-testid={selectors.components.PanelEditor.applyButton}
|
||||
key="apply"
|
||||
size={size}
|
||||
>
|
||||
Apply
|
||||
</ToolbarButton>,
|
||||
</Button>,
|
||||
];
|
||||
|
||||
if (this.props.panel.libraryPanel) {
|
||||
@@ -410,8 +423,24 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
this.setState({ showSaveLibraryPanelModal: false });
|
||||
};
|
||||
|
||||
renderToolbar() {
|
||||
if (config.featureToggles.topnav) {
|
||||
return (
|
||||
<AppChromeUpdate
|
||||
actions={<ToolbarButtonRow alignment="right">{this.renderEditorActions()}</ToolbarButtonRow>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageToolbar title={this.props.dashboard.title} section="Edit Panel" onGoBack={this.onGoBackToDashboard}>
|
||||
{this.renderEditorActions()}
|
||||
</PageToolbar>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard, initDone, updatePanelEditorUIState, uiState, theme } = this.props;
|
||||
const { initDone, updatePanelEditorUIState, uiState, theme, sectionNav, pageNav, className } = this.props;
|
||||
const styles = getStyles(theme, this.props);
|
||||
|
||||
if (!initDone) {
|
||||
@@ -419,29 +448,35 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper} aria-label={selectors.components.PanelEditor.General.content}>
|
||||
<PageToolbar title={dashboard.title} section="Edit Panel" onGoBack={this.onGoBackToDashboard}>
|
||||
{this.renderEditorActions()}
|
||||
</PageToolbar>
|
||||
<div className={styles.verticalSplitPanesWrapper}>
|
||||
<SplitPaneWrapper
|
||||
leftPaneComponents={this.renderPanelAndEditor(styles)}
|
||||
rightPaneComponents={this.renderOptionsPane()}
|
||||
uiState={uiState}
|
||||
updateUiState={updatePanelEditorUIState}
|
||||
rightPaneVisible={uiState.isPanelOptionsVisible}
|
||||
/>
|
||||
<Page
|
||||
navModel={sectionNav}
|
||||
pageNav={pageNav}
|
||||
aria-label={selectors.components.PanelEditor.General.content}
|
||||
layout={PageLayoutType.Custom}
|
||||
toolbar={this.renderToolbar()}
|
||||
className={className}
|
||||
>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.verticalSplitPanesWrapper}>
|
||||
<SplitPaneWrapper
|
||||
leftPaneComponents={this.renderPanelAndEditor(styles)}
|
||||
rightPaneComponents={this.renderOptionsPane()}
|
||||
uiState={uiState}
|
||||
updateUiState={updatePanelEditorUIState}
|
||||
rightPaneVisible={uiState.isPanelOptionsVisible}
|
||||
/>
|
||||
</div>
|
||||
{this.state.showSaveLibraryPanelModal && (
|
||||
<SaveLibraryPanelModal
|
||||
panel={this.props.panel as PanelModelWithLibraryPanel}
|
||||
folderId={this.props.dashboard.meta.folderId as number}
|
||||
onConfirm={this.onConfirmAndDismissLibarayPanelModel}
|
||||
onDiscard={this.onDiscard}
|
||||
onDismiss={this.onConfirmAndDismissLibarayPanelModel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{this.state.showSaveLibraryPanelModal && (
|
||||
<SaveLibraryPanelModal
|
||||
panel={this.props.panel as PanelModelWithLibraryPanel}
|
||||
folderId={this.props.dashboard.meta.folderId as number}
|
||||
onConfirm={this.onConfirmAndDismissLibarayPanelModel}
|
||||
onDiscard={this.onDiscard}
|
||||
onDismiss={this.onConfirmAndDismissLibarayPanelModel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -456,19 +491,13 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2, props: Props) => {
|
||||
const paneSpacing = theme.spacing(2);
|
||||
|
||||
return {
|
||||
wrapper: css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
z-index: ${theme.zIndex.sidemenu};
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: ${theme.colors.background.canvas};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`,
|
||||
wrapper: css({
|
||||
width: '100%',
|
||||
flexGrow: 1,
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
paddingTop: config.featureToggles.topnav ? theme.spacing(2) : 0,
|
||||
}),
|
||||
verticalSplitPanesWrapper: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -45,6 +45,7 @@ export const VisualizationButton: FC<Props> = ({ panel }) => {
|
||||
isOpen={isVizPickerOpen}
|
||||
onClick={onToggleOpen}
|
||||
aria-label={selectors.components.PanelEditor.toggleVizPicker}
|
||||
variant="canvas"
|
||||
fullWidth
|
||||
>
|
||||
{plugin.meta.name}
|
||||
@@ -53,6 +54,7 @@ export const VisualizationButton: FC<Props> = ({ panel }) => {
|
||||
tooltip={isPanelOptionsVisible ? 'Close options pane' : 'Show options pane'}
|
||||
icon={isPanelOptionsVisible ? 'angle-right' : 'angle-left'}
|
||||
onClick={onToggleOptionsPane}
|
||||
variant="canvas"
|
||||
aria-label={selectors.components.PanelEditor.toggleVizOptions}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Button, ButtonVariant, ModalsController } from '@grafana/ui';
|
||||
import { Button, ButtonVariant, ComponentSize, ModalsController } from '@grafana/ui';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
|
||||
import { SaveDashboardDrawer } from './SaveDashboardDrawer';
|
||||
@@ -9,14 +9,16 @@ import { SaveDashboardDrawer } from './SaveDashboardDrawer';
|
||||
interface SaveDashboardButtonProps {
|
||||
dashboard: DashboardModel;
|
||||
onSaveSuccess?: () => void;
|
||||
size?: ComponentSize;
|
||||
}
|
||||
|
||||
export const SaveDashboardButton: React.FC<SaveDashboardButtonProps> = ({ dashboard, onSaveSuccess }) => {
|
||||
export const SaveDashboardButton: React.FC<SaveDashboardButtonProps> = ({ dashboard, onSaveSuccess, size }) => {
|
||||
return (
|
||||
<ModalsController>
|
||||
{({ showModal, hideModal }) => {
|
||||
return (
|
||||
<Button
|
||||
size={size}
|
||||
onClick={() => {
|
||||
showModal(SaveDashboardDrawer, {
|
||||
dashboard,
|
||||
@@ -38,12 +40,14 @@ export const SaveDashboardAsButton: React.FC<SaveDashboardButtonProps & { varian
|
||||
dashboard,
|
||||
onSaveSuccess,
|
||||
variant,
|
||||
size,
|
||||
}) => {
|
||||
return (
|
||||
<ModalsController>
|
||||
{({ showModal, hideModal }) => {
|
||||
return (
|
||||
<Button
|
||||
size={size}
|
||||
onClick={() => {
|
||||
showModal(SaveDashboardDrawer, {
|
||||
dashboard,
|
||||
@@ -55,7 +59,7 @@ export const SaveDashboardAsButton: React.FC<SaveDashboardButtonProps & { varian
|
||||
variant={variant}
|
||||
aria-label={selectors.pages.Dashboard.Settings.General.saveAsDashBoard}
|
||||
>
|
||||
Save As...
|
||||
Save as
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -241,7 +241,7 @@ describe('DashboardPage', () => {
|
||||
});
|
||||
|
||||
it('Should render panel editor', () => {
|
||||
expect(screen.getByLabelText('Apply changes and go back to dashboard')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Apply changes and go back to dashboard')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should reset state when leaving', () => {
|
||||
@@ -294,9 +294,8 @@ describe('DashboardPage', () => {
|
||||
|
||||
dashboardPageScenario('When in full kiosk mode', (ctx) => {
|
||||
ctx.setup(() => {
|
||||
locationService.partial({ kiosk: true });
|
||||
ctx.mount({
|
||||
queryParams: {},
|
||||
queryParams: { kiosk: true },
|
||||
dashboard: getTestDashboard(),
|
||||
});
|
||||
ctx.rerender({ dashboard: ctx.dashboard });
|
||||
|
||||
@@ -53,6 +53,7 @@ export type DashboardPageRouteSearchParams = {
|
||||
from?: string;
|
||||
to?: string;
|
||||
refresh?: string;
|
||||
kiosk?: string | true;
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state: StoreState) => ({
|
||||
@@ -85,7 +86,7 @@ export interface State {
|
||||
editPanel: PanelModel | null;
|
||||
viewPanel: PanelModel | null;
|
||||
updateScrollTop?: number;
|
||||
rememberScrollTop: number;
|
||||
rememberScrollTop?: number;
|
||||
showLoadingState: boolean;
|
||||
panelNotFound: boolean;
|
||||
editPanelAccessDenied: boolean;
|
||||
@@ -103,7 +104,6 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
editPanel: null,
|
||||
viewPanel: null,
|
||||
showLoadingState: false,
|
||||
rememberScrollTop: 0,
|
||||
panelNotFound: false,
|
||||
editPanelAccessDenied: false,
|
||||
};
|
||||
@@ -227,54 +227,58 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
return state;
|
||||
}
|
||||
|
||||
state = updateStatePageNavFromProps(props, state);
|
||||
const updatedState = { ...state };
|
||||
|
||||
// Entering edit mode
|
||||
if (!state.editPanel && urlEditPanelId) {
|
||||
const panel = dashboard.getPanelByUrlId(urlEditPanelId);
|
||||
if (!panel) {
|
||||
return { ...state, panelNotFound: true };
|
||||
}
|
||||
|
||||
if (dashboard.canEditPanel(panel)) {
|
||||
return { ...state, editPanel: panel, rememberScrollTop: state.scrollElement?.scrollTop };
|
||||
if (panel) {
|
||||
if (dashboard.canEditPanel(panel)) {
|
||||
updatedState.editPanel = panel;
|
||||
updatedState.rememberScrollTop = state.scrollElement?.scrollTop;
|
||||
} else {
|
||||
updatedState.editPanelAccessDenied = true;
|
||||
}
|
||||
} else {
|
||||
return { ...state, editPanelAccessDenied: true };
|
||||
updatedState.panelNotFound = true;
|
||||
}
|
||||
}
|
||||
// Leaving edit mode
|
||||
else if (state.editPanel && !urlEditPanelId) {
|
||||
return { ...state, editPanel: null, updateScrollTop: state.rememberScrollTop };
|
||||
updatedState.editPanel = null;
|
||||
updatedState.updateScrollTop = state.rememberScrollTop;
|
||||
}
|
||||
|
||||
// Entering view mode
|
||||
if (!state.viewPanel && urlViewPanelId) {
|
||||
const panel = dashboard.getPanelByUrlId(urlViewPanelId);
|
||||
if (!panel) {
|
||||
return { ...state, panelNotFound: urlEditPanelId };
|
||||
if (panel) {
|
||||
// This mutable state feels wrong to have in getDerivedStateFromProps
|
||||
// Should move this state out of dashboard in the future
|
||||
dashboard.initViewPanel(panel);
|
||||
updatedState.viewPanel = panel;
|
||||
updatedState.rememberScrollTop = state.scrollElement?.scrollTop;
|
||||
updatedState.updateScrollTop = 0;
|
||||
} else {
|
||||
updatedState.panelNotFound = true;
|
||||
}
|
||||
|
||||
// This mutable state feels wrong to have in getDerivedStateFromProps
|
||||
// Should move this state out of dashboard in the future
|
||||
dashboard.initViewPanel(panel);
|
||||
|
||||
return { ...state, viewPanel: panel, rememberScrollTop: state.scrollElement?.scrollTop, updateScrollTop: 0 };
|
||||
}
|
||||
// Leaving view mode
|
||||
else if (state.viewPanel && !urlViewPanelId) {
|
||||
// This mutable state feels wrong to have in getDerivedStateFromProps
|
||||
// Should move this state out of dashboard in the future
|
||||
dashboard.exitViewPanel(state.viewPanel);
|
||||
|
||||
return { ...state, viewPanel: null, updateScrollTop: state.rememberScrollTop };
|
||||
updatedState.viewPanel = null;
|
||||
updatedState.updateScrollTop = state.rememberScrollTop;
|
||||
}
|
||||
|
||||
// if we removed url edit state, clear any panel not found state
|
||||
if (state.panelNotFound || (state.editPanelAccessDenied && !urlEditPanelId)) {
|
||||
return { ...state, panelNotFound: false, editPanelAccessDenied: false };
|
||||
updatedState.panelNotFound = false;
|
||||
updatedState.editPanelAccessDenied = false;
|
||||
}
|
||||
|
||||
return state;
|
||||
return updateStatePageNavFromProps(props, updatedState);
|
||||
}
|
||||
|
||||
onAddPanel = () => {
|
||||
@@ -325,7 +329,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
render() {
|
||||
const { dashboard, initError, queryParams, isPublic } = this.props;
|
||||
const { editPanel, viewPanel, updateScrollTop, pageNav, sectionNav } = this.state;
|
||||
const kioskMode = !isPublic ? getKioskMode() : KioskMode.Full;
|
||||
const kioskMode = !isPublic ? getKioskMode(this.props.queryParams) : KioskMode.Full;
|
||||
|
||||
if (!dashboard || !pageNav || !sectionNav) {
|
||||
return <DashboardLoading initPhase={this.props.initPhase} />;
|
||||
@@ -348,6 +352,11 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
</header>
|
||||
);
|
||||
|
||||
const pageClassName = cx({
|
||||
'panel-in-fullscreen': Boolean(viewPanel),
|
||||
'page-hidden': Boolean(queryParams.editview || editPanel),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Page
|
||||
@@ -355,7 +364,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
pageNav={pageNav}
|
||||
layout={PageLayoutType.Canvas}
|
||||
toolbar={toolbar}
|
||||
className={cx(viewPanel && 'panel-in-fullscreen', queryParams.editview && 'dashboard-content--hidden')}
|
||||
className={pageClassName}
|
||||
scrollRef={this.setScrollRef}
|
||||
scrollTop={updateScrollTop}
|
||||
>
|
||||
@@ -371,8 +380,16 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
<DashboardGrid dashboard={dashboard} viewPanel={viewPanel} editPanel={editPanel} />
|
||||
|
||||
{inspectPanel && <PanelInspector dashboard={dashboard} panel={inspectPanel} />}
|
||||
{editPanel && <PanelEditor dashboard={dashboard} sourcePanel={editPanel} tab={this.props.queryParams.tab} />}
|
||||
</Page>
|
||||
{editPanel && (
|
||||
<PanelEditor
|
||||
dashboard={dashboard}
|
||||
sourcePanel={editPanel}
|
||||
tab={this.props.queryParams.tab}
|
||||
sectionNav={sectionNav}
|
||||
pageNav={pageNav}
|
||||
/>
|
||||
)}
|
||||
{queryParams.editview && (
|
||||
<DashboardSettings
|
||||
dashboard={dashboard}
|
||||
@@ -429,6 +446,14 @@ function updateStatePageNavFromProps(props: Props, state: State): State {
|
||||
sectionNav = getNavModel(props.navIndex, config.featureToggles.topnav ? 'dashboards/browse' : 'dashboards');
|
||||
}
|
||||
|
||||
if (state.editPanel || state.viewPanel) {
|
||||
pageNav = {
|
||||
...pageNav,
|
||||
text: `${state.editPanel ? 'Edit' : 'View'} panel`,
|
||||
parentItem: pageNav,
|
||||
};
|
||||
}
|
||||
|
||||
if (state.pageNav === pageNav && state.sectionNav === sectionNav) {
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -48,13 +48,13 @@ function SceneRenderer({ model }: SceneComponentProps<Scene>) {
|
||||
}
|
||||
|
||||
const pageToolbar = config.featureToggles.topnav ? (
|
||||
<AppChromeUpdate pageNav={{ text: title }} actions={toolbarActions} />
|
||||
<AppChromeUpdate actions={toolbarActions} />
|
||||
) : (
|
||||
<PageToolbar title={title}>{toolbarActions}</PageToolbar>
|
||||
);
|
||||
|
||||
return (
|
||||
<Page navId="scenes" layout={PageLayoutType.Canvas} toolbar={pageToolbar}>
|
||||
<Page navId="scenes" pageNav={{ text: title }} layout={PageLayoutType.Canvas} toolbar={pageToolbar}>
|
||||
<div style={{ flexGrow: 1, display: 'flex', gap: '8px', overflow: 'auto' }}>
|
||||
<layout.Component model={layout} isEditing={isEditing} />
|
||||
{$editor && <$editor.Component model={$editor} isEditing={isEditing} />}
|
||||
|
||||
Reference in New Issue
Block a user