PageToolbar: Extracting navbar styles & layout into a modern emotion based component (#30588)

* Explore: Replaces navbar-button and overriden explore button css classes with ToolbarButton and cleans up scss & markup, removes ResponsiveButton

* Change live button text when paused

* For the dashboard toolbar button I need a transparent button so I refactored the states/variants into a new ToolbarButtonVariatn

* PageToolbar wip

* Progress

* Prgress

* Minor progress

* Fixed back button and responsive titles

* Fixed tv mode

* Updated

* support tv modes and playlist

* more progress

* Fixing lots of view states and responsive features

* Minor fixes

* review fixes

* Fixes to e2e tests

* Review fixes
This commit is contained in:
Torkel Ödegaard
2021-01-27 15:02:04 +01:00
committed by GitHub
parent 1c158744e8
commit 701ad79bb5
34 changed files with 464 additions and 410 deletions
@@ -1,16 +1,14 @@
// Libaries
import React, { PureComponent, FC, ReactNode } from 'react';
import { connect, MapDispatchToProps } from 'react-redux';
import { css } from 'emotion';
// Utils & Services
import { appEvents } from 'app/core/app_events';
import { PlaylistSrv } from 'app/features/playlist/playlist_srv';
// Components
import { DashNavButton } from './DashNavButton';
import { DashNavTimeControls } from './DashNavTimeControls';
import { Icon, ModalsController } from '@grafana/ui';
import { ButtonGroup, ModalsController, ToolbarButton, PageToolbar } from '@grafana/ui';
import { textUtil } from '@grafana/data';
import { BackButton } from 'app/core/components/BackButton/BackButton';
// State
import { updateLocation } from 'app/core/actions';
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
@@ -126,11 +124,23 @@ class DashNav extends PureComponent<Props> {
});
}
isInKioskMode() {
return !!this.props.location.query.kiosk;
}
isPlaylistRunning() {
return this.playlistSrv.isPlaying;
}
renderLeftActionsButton() {
const { dashboard } = this.props;
const { canStar, canShare, isStarred } = dashboard.meta;
const buttons: ReactNode[] = [];
if (this.isInKioskMode() || this.isPlaylistRunning()) {
return [];
}
if (canStar) {
buttons.push(
<DashNavButton
@@ -172,74 +182,49 @@ class DashNav extends PureComponent<Props> {
return buttons;
}
renderDashboardTitleSearchButton() {
const { dashboard, isFullscreen } = this.props;
const folderSymbol = css`
margin-right: 0 4px;
`;
const mainIconClassName = css`
margin-right: 8px;
margin-bottom: 3px;
`;
const folderTitle = dashboard.meta.folderTitle;
const haveFolder = (dashboard.meta.folderId ?? 0) > 0;
renderPlaylistControls() {
return (
<>
<div>
<div className="navbar-page-btn">
{!isFullscreen && <Icon name="apps" size="lg" className={mainIconClassName} />}
{haveFolder && (
<>
<a className="navbar-page-btn__folder" onClick={this.onFolderNameClick}>
{folderTitle} <span className={folderSymbol}>/</span>
</a>
</>
)}
<a onClick={this.onDashboardNameClick}>{dashboard.title}</a>
</div>
</div>
<div className="navbar-buttons navbar-buttons--actions">{this.renderLeftActionsButton()}</div>
<div className="navbar__spacer" />
</>
);
}
renderBackButton() {
return (
<div className="navbar-edit">
<BackButton surface="dashboard" onClick={this.onClose} />
</div>
<ButtonGroup key="playlist-buttons">
<ToolbarButton tooltip="Go to previous dashboard" icon="backward" onClick={this.onPlaylistPrev} narrow />
<ToolbarButton onClick={this.onPlaylistStop}>Stop playlist</ToolbarButton>
<ToolbarButton tooltip="Go to next dashboard" icon="forward" onClick={this.onPlaylistNext} narrow />
</ButtonGroup>
);
}
renderRightActionsButton() {
const { dashboard, onAddPanel } = this.props;
const { dashboard, onAddPanel, location, updateTimeZoneForSession, isFullscreen } = this.props;
const { canEdit, showSettings } = dashboard.meta;
const { snapshot } = dashboard;
const snapshotUrl = snapshot && snapshot.originalUrl;
const buttons: ReactNode[] = [];
if (canEdit) {
buttons.push(
<DashNavButton
classSuffix="save"
tooltip="Add panel"
icon="panel-add"
onClick={onAddPanel}
iconType="mono"
iconSize="xl"
key="button-panel-add"
/>
);
const tvButton = (
<ToolbarButton tooltip="Cycle view mode" icon="monitor" onClick={this.onToggleTVMode} key="tv-button" />
);
const timeControls = (
<DashNavTimeControls
dashboard={dashboard}
location={location}
onChangeTimeZone={updateTimeZoneForSession}
key="time-controls"
/>
);
if (this.isPlaylistRunning()) {
return [this.renderPlaylistControls(), timeControls];
}
if (this.isInKioskMode()) {
return [timeControls, tvButton];
}
if (canEdit && !isFullscreen) {
buttons.push(<ToolbarButton tooltip="Add panel" icon="panel-add" onClick={onAddPanel} key="button-panel-add" />);
buttons.push(
<ModalsController key="button-save">
{({ showModal, hideModal }) => (
<DashNavButton
<ToolbarButton
tooltip="Save dashboard"
classSuffix="save"
icon="save"
onClick={() => {
showModal(SaveDashboardModalProxy, {
@@ -255,10 +240,9 @@ class DashNav extends PureComponent<Props> {
if (snapshotUrl) {
buttons.push(
<DashNavButton
<ToolbarButton
tooltip="Open original dashboard"
classSuffix="snapshot-origin"
href={textUtil.sanitizeUrl(snapshotUrl)}
onClick={() => this.gotoSnapshotOrigin(snapshotUrl)}
icon="link"
key="button-snapshot"
/>
@@ -267,67 +251,40 @@ class DashNav extends PureComponent<Props> {
if (showSettings) {
buttons.push(
<DashNavButton
tooltip="Dashboard settings"
classSuffix="settings"
icon="cog"
onClick={this.onOpenSettings}
key="button-settings"
/>
<ToolbarButton tooltip="Dashboard settings" icon="cog" onClick={this.onOpenSettings} key="button-settings" />
);
}
this.addCustomContent(customRightActions, buttons);
if (!dashboard.timepicker.hidden) {
buttons.push(timeControls);
}
buttons.push(tvButton);
return buttons;
}
gotoSnapshotOrigin(snapshotUrl: string) {
window.location.href = textUtil.sanitizeUrl(snapshotUrl);
}
render() {
const { dashboard, location, isFullscreen, updateTimeZoneForSession } = this.props;
const { dashboard, isFullscreen } = this.props;
const onGoBack = isFullscreen ? this.onClose : undefined;
return (
<div className="navbar">
{isFullscreen && this.renderBackButton()}
{this.renderDashboardTitleSearchButton()}
{this.playlistSrv.isPlaying && (
<div className="navbar-buttons navbar-buttons--playlist">
<DashNavButton
tooltip="Go to previous dashboard"
classSuffix="tight"
icon="step-backward"
onClick={this.onPlaylistPrev}
/>
<DashNavButton
tooltip="Stop playlist"
classSuffix="tight"
icon="square-shape"
onClick={this.onPlaylistStop}
/>
<DashNavButton
tooltip="Go to next dashboard"
classSuffix="tight"
icon="forward"
onClick={this.onPlaylistNext}
/>
</div>
)}
<div className="navbar-buttons navbar-buttons--actions">{this.renderRightActionsButton()}</div>
<div className="navbar-buttons navbar-buttons--tv">
<DashNavButton tooltip="Cycle view mode" classSuffix="tv" icon="monitor" onClick={this.onToggleTVMode} />
</div>
{!dashboard.timepicker.hidden && (
<div className="navbar-buttons">
<DashNavTimeControls
dashboard={dashboard}
location={location}
onChangeTimeZone={updateTimeZoneForSession}
/>
</div>
)}
</div>
<PageToolbar
pageIcon={isFullscreen ? undefined : 'apps'}
title={dashboard.title}
parent={dashboard.meta.folderTitle}
onClickTitle={this.onDashboardNameClick}
onClickParent={this.onFolderNameClick}
onGoBack={onGoBack}
leftItems={this.renderLeftActionsButton()}
>
{this.renderRightActionsButton()}
</PageToolbar>
);
}
}
@@ -18,13 +18,6 @@ interface Props {
noBorder?: boolean;
}
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
noBorderContainer: css`
padding: 0 ${theme.spacing.xs};
display: flex;
`,
}));
export const DashNavButton: FunctionComponent<Props> = ({
icon,
iconType,
@@ -62,7 +55,7 @@ export const DashNavButton: FunctionComponent<Props> = ({
<button
className={`btn navbar-button navbar-button--${classSuffix}`}
onClick={onClick}
aria-label={selectors.pages.Dashboard.Toolbar.toolbarItems(tooltip)}
aria-label={selectors.components.PageToolbar.item(tooltip)}
>
{icon && <Icon name={icon} type={iconType} size={iconSize || 'lg'} />}
{children}
@@ -76,3 +69,10 @@ export const DashNavButton: FunctionComponent<Props> = ({
</Tooltip>
);
};
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
noBorderContainer: css`
padding: 0 ${theme.spacing.xs};
display: flex;
`,
}));
+29 -80
View File
@@ -1,88 +1,9 @@
import React from 'react';
import tinycolor from 'tinycolor2';
import { css } from 'emotion';
import { CSSTransition } from 'react-transition-group';
import { useTheme, Tooltip, stylesFactory, selectThemeVariant, ButtonGroup, ToolbarButton } from '@grafana/ui';
import { useTheme, Tooltip, stylesFactory, ButtonGroup, ToolbarButton } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
const getStyles = stylesFactory((theme: GrafanaTheme) => {
const bgColor = selectThemeVariant({ light: theme.palette.gray5, dark: theme.palette.dark1 }, theme.type);
const orangeLighter = tinycolor(theme.palette.orangeDark).lighten(10).toString();
const pulseTextColor = tinycolor(theme.palette.orangeDark).desaturate(90).toString();
return {
isLive: css`
label: isLive;
border-color: ${theme.palette.orangeDark};
color: ${theme.palette.orangeDark};
background: transparent;
&:focus {
background: transparent;
border-color: ${theme.palette.orangeDark};
color: ${theme.palette.orangeDark};
}
&:hover {
background-color: ${bgColor};
}
&:active,
&:hover {
border-color: ${orangeLighter};
color: ${orangeLighter};
}
`,
isPaused: css`
label: isPaused;
border-color: ${theme.palette.orangeDark};
background: transparent;
animation: pulse 3s ease-out 0s infinite normal forwards;
&:focus {
background: transparent;
border-color: ${theme.palette.orangeDark};
}
&:hover {
background-color: ${bgColor};
}
&:active,
&:hover {
border-color: ${orangeLighter};
}
@keyframes pulse {
0% {
color: ${pulseTextColor};
}
50% {
color: ${theme.palette.orangeDark};
}
100% {
color: ${pulseTextColor};
}
}
`,
stopButtonEnter: css`
label: stopButtonEnter;
width: 0;
opacity: 0;
overflow: hidden;
`,
stopButtonEnterActive: css`
label: stopButtonEnterActive;
opacity: 1;
width: 32px;
`,
stopButtonExit: css`
label: stopButtonExit;
width: 32px;
opacity: 1;
overflow: hidden;
`,
stopButtonExitActive: css`
label: stopButtonExitActive;
opacity: 0;
width: 0;
`,
};
});
type LiveTailButtonProps = {
splitted: boolean;
start: () => void;
@@ -92,6 +13,7 @@ type LiveTailButtonProps = {
isLive: boolean;
isPaused: boolean;
};
export function LiveTailButton(props: LiveTailButtonProps) {
const { start, pause, resume, isLive, isPaused, stop, splitted } = props;
const theme = useTheme();
@@ -134,3 +56,30 @@ export function LiveTailButton(props: LiveTailButtonProps) {
</ButtonGroup>
);
}
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
stopButtonEnter: css`
label: stopButtonEnter;
width: 0;
opacity: 0;
overflow: hidden;
`,
stopButtonEnterActive: css`
label: stopButtonEnterActive;
opacity: 1;
width: 32px;
`,
stopButtonExit: css`
label: stopButtonExit;
width: 32px;
opacity: 1;
overflow: hidden;
`,
stopButtonExitActive: css`
label: stopButtonExitActive;
opacity: 0;
width: 0;
`,
};
});
@@ -76,7 +76,6 @@ export const UnconnectedReturnToDashboardButton: FC<Props> = ({
data-testid="returnButtonWithChanges"
options={[{ label: 'Return to panel with changes', value: '' }]}
onChange={() => returnToPanel({ withChanges: true })}
maxMenuHeight={380}
/>
</ButtonGroup>
);
-44
View File
@@ -1,16 +1,3 @@
.navbar-buttons--zoom {
display: none;
}
.navbar-page-btn {
max-width: 200px;
}
.navbar-buttons--tv,
.navbar-buttons--actions {
display: none;
}
// Media queries
// ---------------------
@@ -21,34 +8,3 @@
font-size: 16px;
}
}
@include media-breakpoint-up(sm) {
.navbar-page-btn {
max-width: 250px;
}
}
@include media-breakpoint-up(md) {
.navbar-buttons--tv,
.navbar-buttons--actions {
display: flex;
}
.navbar-page-btn {
max-width: 325px;
}
}
@include media-breakpoint-up(lg) {
.navbar-buttons--zoom {
display: flex;
}
.navbar-page-btn {
max-width: 450px;
}
}
@include media-breakpoint-up(xl) {
.navbar-page-btn {
max-width: 600px;
}
}
-73
View File
@@ -1,47 +1,3 @@
.navbar {
position: relative;
z-index: $zindex-navbar-fixed;
height: $navbarHeight;
padding: 0 16px 0 60px;
display: flex;
flex-grow: 0;
flex-shrink: 0;
border-bottom: 1px solid transparent;
transition-duration: 350ms;
transition-timing-function: ease-in-out;
transition-property: box-shadow, border-bottom;
@include media-breakpoint-up(md) {
padding-left: $dashboard-padding;
margin-left: 0;
}
&--edit {
background: $panel-bg;
border-bottom: $panel-border;
box-shadow: 0 0 10px $dashboard-bg;
}
}
@mixin navbar-alt-look() {
background: $page-header-bg;
box-shadow: $search-shadow;
border-bottom: $navbarBorder;
}
.panel-in-fullscreen,
.panel-in-fullscreen.view-mode--tv {
.navbar {
padding-left: $navbar-padding;
}
.navbar-button--add-panel,
.navbar-button--star,
.navbar-button--tv {
display: none;
}
}
.navbar-page-btn {
text-overflow: ellipsis;
overflow: hidden;
@@ -75,35 +31,6 @@
}
}
.navbar-page-btn__folder {
display: none;
padding-right: 4px;
@include media-breakpoint-up(lg) {
display: inline-block;
}
}
.navbar-buttons {
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: 10px;
&--close {
display: none;
margin-right: 0;
}
&--zoom {
margin-right: 0;
}
}
.navbar__spacer {
flex-grow: 1;
}
.navbar-button {
background-color: $panel-bg;
@@ -22,10 +22,6 @@
}
.panel-in-fullscreen {
.sidemenu {
display: none;
}
.search-container {
left: 0 !important;
}
+4 -3
View File
@@ -190,10 +190,10 @@ li.sidemenu-org-switcher {
}
img {
width: 30px;
width: 26px;
position: relative;
top: 5px;
left: 4px;
left: 8px;
}
}
@@ -234,11 +234,12 @@ li.sidemenu-org-switcher {
}
.sidemenu__logo_small_breakpoint {
padding: 14px 10px 26px 13px;
padding: 13px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: baseline;
cursor: pointer;
.fa-bars {
font-size: 25px;
+16 -37
View File
@@ -2,41 +2,10 @@
.react-resizable-handle,
.add-row-panel-hint,
.dash-row-menu-container,
.navbar-buttons--actions,
.panel-info-corner--info,
.panel-info-corner--links {
display: none;
}
.navbar-page-btn {
i {
display: none;
}
i.navbar-page-btn__folder-icon {
display: inline-block;
opacity: inherit;
}
}
.navbar-button--zoom {
display: none;
}
}
.view-mode--playlist {
@extend .view-mode--inactive;
}
// https://github.com/grafana/grafana/issues/18114
.view-mode--tv.panel-in-fullscreen {
.navbar {
padding-left: $navbar-padding;
}
.navbar-page-btn {
transform: none;
}
}
.view-mode--tv {
@@ -60,8 +29,12 @@
}
}
.navbar {
.page-toolbar {
padding-left: $side-menu-width;
&--fullscreen {
padding-left: $space-md;
}
}
.submenu-controls {
@@ -73,15 +46,21 @@
@extend .view-mode--tv;
.sidemenu,
.navbar {
.page-toolbar {
display: none;
}
.scroll-canvas--dashboard {
height: 100%;
}
.submenu-controls {
display: none;
}
}
@include media-breakpoint-down(sm) {
div.page-toolbar {
padding-left: 53px;
&--fullscreen {
padding-left: $space-md;
}
}
}