Commit 80294b2d by Torkel Ödegaard Committed by GitHub

TimeRangePicker: Updates components to use new ToolbarButton & ButtonGroup (#30570)

* WIP: Using new components

* Progress

* Everything looks to be working

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

* 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

* Fixed story

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

* Changing my mind on the transparent variant

* review fixes

* Review fixes
parent 951b11a9
...@@ -71,7 +71,7 @@ export const Variants: Story<ButtonProps> = ({ children, ...args }) => { ...@@ -71,7 +71,7 @@ export const Variants: Story<ButtonProps> = ({ children, ...args }) => {
<div /> <div />
<HorizontalGroup spacing="lg"> <HorizontalGroup spacing="lg">
<div>Inside ButtonGroup</div> <div>Inside ButtonGroup</div>
<ButtonGroup noSpacing> <ButtonGroup>
<Button icon="sync">Run query</Button> <Button icon="sync">Run query</Button>
<Button icon="angle-down" /> <Button icon="angle-down" />
</ButtonGroup> </ButtonGroup>
......
...@@ -5,21 +5,13 @@ import { useStyles } from '../../themes'; ...@@ -5,21 +5,13 @@ import { useStyles } from '../../themes';
export interface Props extends HTMLAttributes<HTMLDivElement> { export interface Props extends HTMLAttributes<HTMLDivElement> {
className?: string; className?: string;
noSpacing?: boolean;
} }
export const ButtonGroup = forwardRef<HTMLDivElement, Props>(({ noSpacing, className, children, ...rest }, ref) => { export const ButtonGroup = forwardRef<HTMLDivElement, Props>(({ className, children, ...rest }, ref) => {
const styles = useStyles(getStyles); const styles = useStyles(getStyles);
const mainClass = cx(
{
[styles.wrapper]: !noSpacing,
[styles.wrapperNoSpacing]: noSpacing,
},
className
);
return ( return (
<div ref={ref} className={mainClass} {...rest}> <div ref={ref} className={cx('button-group', styles.wrapper, className)} {...rest}>
{children} {children}
</div> </div>
); );
...@@ -31,26 +23,17 @@ const getStyles = (theme: GrafanaTheme) => ({ ...@@ -31,26 +23,17 @@ const getStyles = (theme: GrafanaTheme) => ({
wrapper: css` wrapper: css`
display: flex; display: flex;
> a,
> button { > button {
margin-left: ${theme.spacing.sm}; border-radius: 0;
border-right-width: 0;
&:first-child { &.toolbar-button {
margin-left: 0; margin-left: 0;
} }
}
`,
wrapperNoSpacing: css`
display: flex;
> a,
> button {
border-radius: 0;
border-right: 0;
&:last-of-type { &:last-of-type {
border-radius: 0 ${theme.border.radius.sm} ${theme.border.radius.sm} 0; border-radius: 0 ${theme.border.radius.sm} ${theme.border.radius.sm} 0;
border-right: 1px solid ${theme.colors.border2}; border-right-width: 1px;
} }
&:first-child { &:first-child {
......
import React from 'react'; import React from 'react';
import { ToolbarButton, ButtonGroup, useTheme, VerticalGroup, HorizontalGroup } from '@grafana/ui'; import { ToolbarButton, ButtonGroup, useTheme, VerticalGroup, HorizontalGroup } from '@grafana/ui';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { ToolbarButtonRow } from './ToolbarButtonRow';
import { ToolbarButtonVariant } from './ToolbarButton';
export default { export default {
title: 'Buttons/ToolbarButton', title: 'Buttons/ToolbarButton',
...@@ -11,12 +13,13 @@ export default { ...@@ -11,12 +13,13 @@ export default {
export const List = () => { export const List = () => {
const theme = useTheme(); const theme = useTheme();
const variants: ToolbarButtonVariant[] = ['default', 'active', 'primary', 'destructive'];
return ( return (
<div style={{ background: theme.colors.dashboardBg, padding: '32px' }}> <div style={{ background: theme.colors.dashboardBg, padding: '32px' }}>
<VerticalGroup> <VerticalGroup>
Wrapped in normal ButtonGroup (md spacing) Button states
<ButtonGroup> <ToolbarButtonRow>
<ToolbarButton>Just text</ToolbarButton> <ToolbarButton>Just text</ToolbarButton>
<ToolbarButton icon="sync" tooltip="Sync" /> <ToolbarButton icon="sync" tooltip="Sync" />
<ToolbarButton imgSrc="./grafana_icon.svg">With imgSrc</ToolbarButton> <ToolbarButton imgSrc="./grafana_icon.svg">With imgSrc</ToolbarButton>
...@@ -26,31 +29,46 @@ export const List = () => { ...@@ -26,31 +29,46 @@ export const List = () => {
<ToolbarButton icon="cloud" isOpen={false}> <ToolbarButton icon="cloud" isOpen={false}>
isOpen = false isOpen = false
</ToolbarButton> </ToolbarButton>
</ButtonGroup> </ToolbarButtonRow>
<br />
disabled
<ToolbarButtonRow>
<ToolbarButton icon="sync" disabled>
Disabled
</ToolbarButton>
</ToolbarButtonRow>
<br />
Variants
<ToolbarButtonRow>
{variants.map((variant) => (
<ToolbarButton icon="sync" tooltip="Sync" variant={variant} key={variant}>
{variant}
</ToolbarButton>
))}
</ToolbarButtonRow>
<br /> <br />
Wrapped in noSpacing ButtonGroup Wrapped in noSpacing ButtonGroup
<ButtonGroup noSpacing> <ButtonGroup>
<ToolbarButton icon="clock-nine" tooltip="Time picker"> <ToolbarButton icon="clock-nine" tooltip="Time picker">
2020-10-02 2020-10-02
</ToolbarButton> </ToolbarButton>
<ToolbarButton icon="search-minus" /> <ToolbarButton icon="search-minus" />
</ButtonGroup> </ButtonGroup>
<br /> <br />
Wrapped in noSpacing ButtonGroup <ButtonGroup>
<ButtonGroup noSpacing>
<ToolbarButton icon="sync" /> <ToolbarButton icon="sync" />
<ToolbarButton isOpen={false} narrow /> <ToolbarButton isOpen={false} narrow />
</ButtonGroup> </ButtonGroup>
<br /> <br />
As primary and destructive variant As primary and destructive variant
<HorizontalGroup> <HorizontalGroup>
<ButtonGroup noSpacing> <ButtonGroup>
<ToolbarButton variant="primary" icon="sync"> <ToolbarButton variant="primary" icon="sync">
Run query Run query
</ToolbarButton> </ToolbarButton>
<ToolbarButton isOpen={false} narrow variant="primary" /> <ToolbarButton isOpen={false} narrow variant="primary" />
</ButtonGroup> </ButtonGroup>
<ButtonGroup noSpacing> <ButtonGroup>
<ToolbarButton variant="destructive" icon="sync"> <ToolbarButton variant="destructive" icon="sync">
Run query Run query
</ToolbarButton> </ToolbarButton>
......
import React, { forwardRef, HTMLAttributes } from 'react'; import React, { forwardRef, ButtonHTMLAttributes } from 'react';
import { cx, css } from 'emotion'; import { cx, css } from 'emotion';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { styleMixins, useStyles } from '../../themes'; import { styleMixins, useStyles } from '../../themes';
import { IconName } from '../../types/icon'; import { IconName } from '../../types/icon';
import { Tooltip } from '../Tooltip/Tooltip'; import { Tooltip } from '../Tooltip/Tooltip';
import { Icon } from '../Icon/Icon'; import { Icon } from '../Icon/Icon';
import { ButtonVariant, getPropertiesForVariant } from './Button'; import { getPropertiesForVariant } from './Button';
import { isString } from 'lodash';
export interface Props extends HTMLAttributes<HTMLButtonElement> { export interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Icon name */ /** Icon name */
icon?: IconName; icon?: IconName | React.ReactNode;
/** Tooltip */ /** Tooltip */
tooltip?: string; tooltip?: string;
/** For image icons */ /** For image icons */
...@@ -21,21 +22,28 @@ export interface Props extends HTMLAttributes<HTMLButtonElement> { ...@@ -21,21 +22,28 @@ export interface Props extends HTMLAttributes<HTMLButtonElement> {
/** reduces padding to xs */ /** reduces padding to xs */
narrow?: boolean; narrow?: boolean;
/** variant */ /** variant */
variant?: ButtonVariant; variant?: ToolbarButtonVariant;
/** Hide any children and only show icon */
iconOnly?: boolean;
} }
export type ToolbarButtonVariant = 'default' | 'primary' | 'destructive' | 'active';
export const ToolbarButton = forwardRef<HTMLButtonElement, Props>( export const ToolbarButton = forwardRef<HTMLButtonElement, Props>(
({ tooltip, icon, className, children, imgSrc, fullWidth, isOpen, narrow, variant, ...rest }, ref) => { (
{ tooltip, icon, className, children, imgSrc, fullWidth, isOpen, narrow, variant = 'default', iconOnly, ...rest },
ref
) => {
const styles = useStyles(getStyles); const styles = useStyles(getStyles);
const buttonStyles = cx( const buttonStyles = cx(
'toolbar-button',
{ {
[styles.button]: true, [styles.button]: true,
[styles.buttonFullWidth]: fullWidth, [styles.buttonFullWidth]: fullWidth,
[styles.narrow]: narrow, [styles.narrow]: narrow,
[styles.primaryVariant]: variant === 'primary',
[styles.destructiveVariant]: variant === 'destructive',
}, },
(styles as any)[variant],
className className
); );
...@@ -47,9 +55,9 @@ export const ToolbarButton = forwardRef<HTMLButtonElement, Props>( ...@@ -47,9 +55,9 @@ export const ToolbarButton = forwardRef<HTMLButtonElement, Props>(
const body = ( const body = (
<button ref={ref} className={buttonStyles} {...rest}> <button ref={ref} className={buttonStyles} {...rest}>
{icon && <Icon name={icon} size={'lg'} />} {renderIcon(icon)}
{imgSrc && <img className={styles.img} src={imgSrc} />} {imgSrc && <img className={styles.img} src={imgSrc} />}
{children && <span className={contentStyles}>{children}</span>} {children && !iconOnly && <span className={contentStyles}>{children}</span>}
{isOpen === false && <Icon name="angle-down" />} {isOpen === false && <Icon name="angle-down" />}
{isOpen === true && <Icon name="angle-up" />} {isOpen === true && <Icon name="angle-up" />}
</button> </button>
...@@ -65,31 +73,76 @@ export const ToolbarButton = forwardRef<HTMLButtonElement, Props>( ...@@ -65,31 +73,76 @@ export const ToolbarButton = forwardRef<HTMLButtonElement, Props>(
} }
); );
function renderIcon(icon: IconName | React.ReactNode) {
if (!icon) {
return null;
}
if (isString(icon)) {
return <Icon name={icon as IconName} size={'lg'} />;
}
return icon;
}
const getStyles = (theme: GrafanaTheme) => { const getStyles = (theme: GrafanaTheme) => {
const primaryVariant = getPropertiesForVariant(theme, 'primary'); const primaryVariant = getPropertiesForVariant(theme, 'primary');
const destructiveVariant = getPropertiesForVariant(theme, 'destructive'); const destructiveVariant = getPropertiesForVariant(theme, 'destructive');
return { return {
button: css` button: css`
background: ${theme.colors.bg1}; label: toolbar-button;
border: 1px solid ${theme.colors.border2}; display: flex;
align-items: center;
height: ${theme.height.md}px; height: ${theme.height.md}px;
padding: 0 ${theme.spacing.sm}; padding: 0 ${theme.spacing.sm};
color: ${theme.colors.textWeak};
border-radius: ${theme.border.radius.sm}; border-radius: ${theme.border.radius.sm};
line-height: ${theme.height.md - 2}px; line-height: ${theme.height.md - 2}px;
display: flex; font-weight: ${theme.typography.weight.semibold};
align-items: center; border: 1px solid ${theme.colors.border2};
&:focus { &:focus {
outline: none; outline: none;
} }
&[disabled],
&:disabled {
cursor: not-allowed;
opacity: 0.5;
&:hover {
color: ${theme.colors.textWeak};
background: ${theme.colors.bg1};
}
}
`,
default: css`
color: ${theme.colors.textWeak};
background-color: ${theme.colors.bg1};
&:hover { &:hover {
color: ${theme.colors.text}; color: ${theme.colors.text};
background: ${styleMixins.hoverColor(theme.colors.bg1, theme)}; background: ${styleMixins.hoverColor(theme.colors.bg1, theme)};
} }
`, `,
active: css`
color: ${theme.palette.orangeDark};
border-color: ${theme.palette.orangeDark};
background-color: transparent;
&:hover {
color: ${theme.colors.text};
background: ${styleMixins.hoverColor(theme.colors.bg1, theme)};
}
`,
primary: css`
border-color: ${primaryVariant.borderColor};
${primaryVariant.variantStyles}
`,
destructive: css`
border-color: ${destructiveVariant.borderColor};
${destructiveVariant.variantStyles}
`,
narrow: css` narrow: css`
padding: 0 ${theme.spacing.xs}; padding: 0 ${theme.spacing.xs};
`, `,
...@@ -103,6 +156,11 @@ const getStyles = (theme: GrafanaTheme) => { ...@@ -103,6 +156,11 @@ const getStyles = (theme: GrafanaTheme) => {
`, `,
content: css` content: css`
flex-grow: 1; flex-grow: 1;
display: none;
@media only screen and (min-width: ${theme.breakpoints.md}) {
display: block;
}
`, `,
contentWithIcon: css` contentWithIcon: css`
padding-left: ${theme.spacing.sm}; padding-left: ${theme.spacing.sm};
...@@ -110,13 +168,5 @@ const getStyles = (theme: GrafanaTheme) => { ...@@ -110,13 +168,5 @@ const getStyles = (theme: GrafanaTheme) => {
contentWithRightIcon: css` contentWithRightIcon: css`
padding-right: ${theme.spacing.xs}; padding-right: ${theme.spacing.xs};
`, `,
primaryVariant: css`
border-color: ${primaryVariant.borderColor};
${primaryVariant.variantStyles}
`,
destructiveVariant: css`
border-color: ${destructiveVariant.borderColor};
${destructiveVariant.variantStyles}
`,
}; };
}; };
import React, { forwardRef, HTMLAttributes } from 'react';
import { css, cx } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { useStyles } from '../../themes';
export interface Props extends HTMLAttributes<HTMLDivElement> {
className?: string;
}
export const ToolbarButtonRow = forwardRef<HTMLDivElement, Props>(({ className, children, ...rest }, ref) => {
const styles = useStyles(getStyles);
return (
<div ref={ref} className={cx(styles.wrapper, className)} {...rest}>
{children}
</div>
);
});
ToolbarButtonRow.displayName = 'ToolbarButtonRow';
const getStyles = (theme: GrafanaTheme) => ({
wrapper: css`
display: flex;
.button-group,
.toolbar-button {
margin-left: ${theme.spacing.sm};
&:first-child {
margin-left: 0;
}
}
`,
});
export * from './Button'; export * from './Button';
export { ButtonGroup } from './ButtonGroup'; export { ButtonGroup } from './ButtonGroup';
export { ToolbarButton } from './ToolbarButton'; export { ToolbarButton, ToolbarButtonVariant } from './ToolbarButton';
export { ToolbarButtonRow } from './ToolbarButtonRow';
import React, { useState, HTMLAttributes } from 'react'; import React, { useState, HTMLAttributes } from 'react';
import { PopoverContent } from '../Tooltip/Tooltip'; import { PopoverContent } from '../Tooltip/Tooltip';
import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { ButtonVariant, ToolbarButton } from '../Button'; import { ToolbarButtonVariant, ToolbarButton } from '../Button';
import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper'; import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper';
import { css } from 'emotion'; import { css } from 'emotion';
import { useStyles } from '../../themes/ThemeContext'; import { useStyles } from '../../themes/ThemeContext';
...@@ -15,7 +15,7 @@ export interface Props<T> extends HTMLAttributes<HTMLButtonElement> { ...@@ -15,7 +15,7 @@ export interface Props<T> extends HTMLAttributes<HTMLButtonElement> {
onChange: (item: SelectableValue<T>) => void; onChange: (item: SelectableValue<T>) => void;
tooltipContent?: PopoverContent; tooltipContent?: PopoverContent;
narrow?: boolean; narrow?: boolean;
variant?: ButtonVariant; variant?: ToolbarButtonVariant;
} }
/** /**
......
...@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; ...@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { Tooltip } from '../Tooltip/Tooltip'; import { Tooltip } from '../Tooltip/Tooltip';
import { ButtonSelect } from '../Dropdown/ButtonSelect'; import { ButtonSelect } from '../Dropdown/ButtonSelect';
import { ButtonGroup, ButtonVariant, ToolbarButton } from '../Button'; import { ButtonGroup, ToolbarButton, ToolbarButtonVariant } from '../Button';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
// Default intervals used in the refresh picker component // Default intervals used in the refresh picker component
...@@ -39,7 +39,7 @@ export class RefreshPicker extends PureComponent<Props> { ...@@ -39,7 +39,7 @@ export class RefreshPicker extends PureComponent<Props> {
} }
}; };
getVariant(): ButtonVariant | undefined { getVariant(): ToolbarButtonVariant {
if (this.props.isLive) { if (this.props.isLive) {
return 'primary'; return 'primary';
} }
...@@ -49,7 +49,7 @@ export class RefreshPicker extends PureComponent<Props> { ...@@ -49,7 +49,7 @@ export class RefreshPicker extends PureComponent<Props> {
if (this.props.primary) { if (this.props.primary) {
return 'primary'; return 'primary';
} }
return undefined; return 'default';
} }
render() { render() {
...@@ -67,7 +67,7 @@ export class RefreshPicker extends PureComponent<Props> { ...@@ -67,7 +67,7 @@ export class RefreshPicker extends PureComponent<Props> {
return ( return (
<div className="refresh-picker"> <div className="refresh-picker">
<ButtonGroup className="refresh-picker-buttons" noSpacing={true}> <ButtonGroup className="refresh-picker-buttons">
<Tooltip placement="bottom" content={tooltip!}> <Tooltip placement="bottom" content={tooltip!}>
<ToolbarButton <ToolbarButton
onClick={onRefresh} onClick={onRefresh}
......
...@@ -119,6 +119,7 @@ const getStyles = (theme: GrafanaTheme) => { ...@@ -119,6 +119,7 @@ const getStyles = (theme: GrafanaTheme) => {
justify-content: space-between; justify-content: space-between;
cursor: pointer; cursor: pointer;
padding-right: 0; padding-right: 0;
line-height: ${theme.spacing.formInputHeight - 2}px;
${getFocusStyle(theme)}; ${getFocusStyle(theme)};
` `
), ),
......
...@@ -7,6 +7,8 @@ import { UseState } from '../../utils/storybook/UseState'; ...@@ -7,6 +7,8 @@ import { UseState } from '../../utils/storybook/UseState';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { dateTime, TimeRange, DefaultTimeZone, TimeZone, isDateTime } from '@grafana/data'; import { dateTime, TimeRange, DefaultTimeZone, TimeZone, isDateTime } from '@grafana/data';
import { TimeRangePickerProps } from './TimeRangePicker'; import { TimeRangePickerProps } from './TimeRangePicker';
import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas';
import { HorizontalGroup, VerticalGroup } from '../Layout/Layout';
export default { export default {
title: 'Pickers and Editors/TimePickers/TimeRangePicker', title: 'Pickers and Editors/TimePickers/TimeRangePicker',
...@@ -24,7 +26,9 @@ const getComponentWithState = (initialState: State, props: TimeRangePickerProps) ...@@ -24,7 +26,9 @@ const getComponentWithState = (initialState: State, props: TimeRangePickerProps)
<UseState initialState={initialState}> <UseState initialState={initialState}>
{(state, updateValue) => { {(state, updateValue) => {
return ( return (
<> <DashboardStoryCanvas>
<VerticalGroup>
<HorizontalGroup justify="flex-end">
<TimeRangePicker <TimeRangePicker
{...props} {...props}
timeZone={state.timeZone} timeZone={state.timeZone}
...@@ -36,7 +40,9 @@ const getComponentWithState = (initialState: State, props: TimeRangePickerProps) ...@@ -36,7 +40,9 @@ const getComponentWithState = (initialState: State, props: TimeRangePickerProps)
...state, ...state,
value, value,
history: history:
isDateTime(value.raw.from) && isDateTime(value.raw.to) ? [...state.history, value] : state.history, isDateTime(value.raw.from) && isDateTime(value.raw.to)
? [...state.history, value]
: state.history,
}); });
}} }}
onChangeTimeZone={(timeZone) => { onChangeTimeZone={(timeZone) => {
...@@ -56,6 +62,10 @@ const getComponentWithState = (initialState: State, props: TimeRangePickerProps) ...@@ -56,6 +62,10 @@ const getComponentWithState = (initialState: State, props: TimeRangePickerProps)
action('onZoom fired')(); action('onZoom fired')();
}} }}
/> />
</HorizontalGroup>
<br />
<br />
<br />
<Button <Button
onClick={() => { onClick={() => {
updateValue({ updateValue({
...@@ -66,7 +76,8 @@ const getComponentWithState = (initialState: State, props: TimeRangePickerProps) ...@@ -66,7 +76,8 @@ const getComponentWithState = (initialState: State, props: TimeRangePickerProps)
> >
Clear history Clear history
</Button> </Button>
</> </VerticalGroup>
</DashboardStoryCanvas>
); );
}} }}
</UseState> </UseState>
......
// Libraries // Libraries
import React, { PureComponent, memo, FormEvent } from 'react'; import React, { PureComponent, memo, FormEvent } from 'react';
import { css, cx } from 'emotion'; import { css } from 'emotion';
// Components // Components
import { Tooltip } from '../Tooltip/Tooltip'; import { Tooltip } from '../Tooltip/Tooltip';
import { Icon } from '../Icon/Icon';
import { TimePickerContent } from './TimeRangePicker/TimePickerContent'; import { TimePickerContent } from './TimeRangePicker/TimePickerContent';
import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper'; import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper';
...@@ -17,44 +16,7 @@ import { isDateTime, rangeUtil, GrafanaTheme, dateTimeFormat, timeZoneFormatUser ...@@ -17,44 +16,7 @@ import { isDateTime, rangeUtil, GrafanaTheme, dateTimeFormat, timeZoneFormatUser
import { TimeRange, TimeZone, dateMath } from '@grafana/data'; import { TimeRange, TimeZone, dateMath } from '@grafana/data';
import { Themeable } from '../../types'; import { Themeable } from '../../types';
import { otherOptions, quickOptions } from './rangeOptions'; import { otherOptions, quickOptions } from './rangeOptions';
import { ButtonGroup, ToolbarButton } from '../Button';
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
container: css`
position: relative;
display: flex;
flex-flow: column nowrap;
`,
buttons: css`
display: flex;
`,
caretIcon: css`
margin-left: ${theme.spacing.xs};
`,
clockIcon: css`
margin-left: ${theme.spacing.xs};
margin-right: ${theme.spacing.xs};
`,
noRightBorderStyle: css`
label: noRightBorderStyle;
border-right: 0;
`,
};
});
const getLabelStyles = stylesFactory((theme: GrafanaTheme) => {
return {
container: css`
display: inline-block;
`,
utc: css`
color: ${theme.palette.orange};
font-size: 75%;
padding: 3px;
font-weight: ${theme.typography.weight.semibold};
`,
};
});
export interface TimeRangePickerProps extends Themeable { export interface TimeRangePickerProps extends Themeable {
hideText?: boolean; hideText?: boolean;
...@@ -114,28 +76,22 @@ export class UnthemedTimeRangePicker extends PureComponent<TimeRangePickerProps, ...@@ -114,28 +76,22 @@ export class UnthemedTimeRangePicker extends PureComponent<TimeRangePickerProps,
const { isOpen } = this.state; const { isOpen } = this.state;
const styles = getStyles(theme); const styles = getStyles(theme);
const hasAbsolute = isDateTime(value.raw.from) || isDateTime(value.raw.to); const hasAbsolute = isDateTime(value.raw.from) || isDateTime(value.raw.to);
const syncedTimePicker = timeSyncButton && isSynced; const variant = isSynced ? 'active' : 'default';
const timePickerIconClass = cx({ ['icon-brand-gradient']: syncedTimePicker });
const timePickerButtonClass = cx('btn navbar-button navbar-button--tight', {
[`btn--radius-right-0 ${styles.noRightBorderStyle}`]: !!timeSyncButton,
[`explore-active-button`]: syncedTimePicker,
});
return ( return (
<div className={styles.container}> <ButtonGroup className={styles.container}>
<div className={styles.buttons}> {hasAbsolute && <ToolbarButton variant={variant} onClick={onMoveBackward} icon="angle-left" narrow />}
{hasAbsolute && (
<button className="btn navbar-button navbar-button--tight" onClick={onMoveBackward}>
<Icon name="angle-left" size="lg" />
</button>
)}
<div>
<Tooltip content={<TimePickerTooltip timeRange={value} timeZone={timeZone} />} placement="bottom"> <Tooltip content={<TimePickerTooltip timeRange={value} timeZone={timeZone} />} placement="bottom">
<button aria-label="TimePicker Open Button" className={timePickerButtonClass} onClick={this.onOpen}> <ToolbarButton
<Icon name="clock-nine" className={cx(styles.clockIcon, timePickerIconClass)} size="lg" /> aria-label="TimePicker Open Button"
onClick={this.onOpen}
icon="clock-nine"
isOpen={isOpen}
variant={variant}
>
<TimePickerButtonLabel {...this.props} /> <TimePickerButtonLabel {...this.props} />
<span className={styles.caretIcon}>{<Icon name={isOpen ? 'angle-up' : 'angle-down'} size="lg" />}</span> </ToolbarButton>
</button>
</Tooltip> </Tooltip>
{isOpen && ( {isOpen && (
<ClickOutsideWrapper includeButtonPress={false} onClick={this.onClose}> <ClickOutsideWrapper includeButtonPress={false} onClick={this.onClose}>
...@@ -152,23 +108,15 @@ export class UnthemedTimeRangePicker extends PureComponent<TimeRangePickerProps, ...@@ -152,23 +108,15 @@ export class UnthemedTimeRangePicker extends PureComponent<TimeRangePickerProps,
/> />
</ClickOutsideWrapper> </ClickOutsideWrapper>
)} )}
</div>
{timeSyncButton} {timeSyncButton}
{hasAbsolute && ( {hasAbsolute && <ToolbarButton onClick={onMoveForward} icon="angle-right" narrow variant={variant} />}
<button className="btn navbar-button navbar-button--tight" onClick={onMoveForward}>
<Icon name="angle-right" size="lg" />
</button>
)}
<Tooltip content={ZoomOutTooltip} placement="bottom"> <Tooltip content={ZoomOutTooltip} placement="bottom">
<button className="btn navbar-button navbar-button--zoom" onClick={onZoom}> <ToolbarButton onClick={onZoom} icon="search-minus" variant={variant} />
<Icon name="search-minus" size="lg" />
</button>
</Tooltip> </Tooltip>
</div> </ButtonGroup>
</div>
); );
} }
} }
...@@ -224,3 +172,29 @@ const formattedRange = (value: TimeRange, timeZone?: TimeZone) => { ...@@ -224,3 +172,29 @@ const formattedRange = (value: TimeRange, timeZone?: TimeZone) => {
}; };
export const TimeRangePicker = withTheme(UnthemedTimeRangePicker); export const TimeRangePicker = withTheme(UnthemedTimeRangePicker);
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
container: css`
position: relative;
display: flex;
vertical-align: middle;
`,
};
});
const getLabelStyles = stylesFactory((theme: GrafanaTheme) => {
return {
container: css`
display: inline-block;
`,
utc: css`
color: ${theme.palette.orange};
font-size: ${theme.typography.size.sm};
padding-left: 6px;
line-height: 28px;
vertical-align: bottom;
font-weight: ${theme.typography.weight.semibold};
`,
};
});
...@@ -91,6 +91,7 @@ const getFullScreenStyles = stylesFactory((theme: GrafanaTheme, hideQuickRanges? ...@@ -91,6 +91,7 @@ const getFullScreenStyles = stylesFactory((theme: GrafanaTheme, hideQuickRanges?
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-end;
padding-top: ${theme.spacing.sm};
`, `,
}; };
}); });
......
...@@ -132,7 +132,7 @@ export { FieldConfigItemHeaderTitle } from './FieldConfigs/FieldConfigItemHeader ...@@ -132,7 +132,7 @@ export { FieldConfigItemHeaderTitle } from './FieldConfigs/FieldConfigItemHeader
// Next-gen forms // Next-gen forms
export { Form } from './Forms/Form'; export { Form } from './Forms/Form';
export { InputControl } from './InputControl'; export { InputControl } from './InputControl';
export { Button, LinkButton, ButtonVariant, ToolbarButton, ButtonGroup } from './Button'; export { Button, LinkButton, ButtonVariant, ToolbarButton, ButtonGroup, ToolbarButtonRow } from './Button';
export { ValuePicker } from './ValuePicker/ValuePicker'; export { ValuePicker } from './ValuePicker/ValuePicker';
export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI'; export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI';
export { getFormStyles } from './Forms/getFormStyles'; export { getFormStyles } from './Forms/getFormStyles';
......
...@@ -16,15 +16,6 @@ import { TimePickerWithHistory } from 'app/core/components/TimePicker/TimePicker ...@@ -16,15 +16,6 @@ import { TimePickerWithHistory } from 'app/core/components/TimePicker/TimePicker
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { appEvents } from 'app/core/core'; import { appEvents } from 'app/core/core';
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
container: css`
position: relative;
display: flex;
`,
};
});
export interface Props extends Themeable { export interface Props extends Themeable {
dashboard: DashboardModel; dashboard: DashboardModel;
location: LocationState; location: LocationState;
...@@ -126,3 +117,12 @@ class UnthemedDashNavTimeControls extends Component<Props> { ...@@ -126,3 +117,12 @@ class UnthemedDashNavTimeControls extends Component<Props> {
} }
export const DashNavTimeControls = withTheme(UnthemedDashNavTimeControls); export const DashNavTimeControls = withTheme(UnthemedDashNavTimeControls);
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
container: css`
position: relative;
display: flex;
`,
};
});
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';
import memoizeOne from 'memoize-one';
import classNames from 'classnames'; import classNames from 'classnames';
import { css } from 'emotion'; import { css } from 'emotion';
import { ExploreId, ExploreItemState } from 'app/types/explore'; import { ExploreId, ExploreItemState } from 'app/types/explore';
import { Icon, IconButton, SetInterval, Tooltip } from '@grafana/ui'; import { Icon, IconButton, SetInterval, ToolbarButton, ToolbarButtonRow, Tooltip } from '@grafana/ui';
import { DataSourceInstanceSettings, RawTimeRange, TimeRange, TimeZone } from '@grafana/data'; import { DataSourceInstanceSettings, RawTimeRange, TimeRange, TimeZone } from '@grafana/data';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { StoreState } from 'app/types/store'; import { StoreState } from 'app/types/store';
...@@ -18,23 +17,11 @@ import { getTimeZone } from '../profile/state/selectors'; ...@@ -18,23 +17,11 @@ import { getTimeZone } from '../profile/state/selectors';
import { updateTimeZoneForSession } from '../profile/state/reducers'; import { updateTimeZoneForSession } from '../profile/state/reducers';
import { ExploreTimeControls } from './ExploreTimeControls'; import { ExploreTimeControls } from './ExploreTimeControls';
import { LiveTailButton } from './LiveTailButton'; import { LiveTailButton } from './LiveTailButton';
import { ResponsiveButton } from './ResponsiveButton';
import { RunButton } from './RunButton'; import { RunButton } from './RunButton';
import { LiveTailControls } from './useLiveTailControls'; import { LiveTailControls } from './useLiveTailControls';
import { cancelQueries, clearQueries, runQueries } from './state/query'; import { cancelQueries, clearQueries, runQueries } from './state/query';
import ReturnToDashboardButton from './ReturnToDashboardButton'; import ReturnToDashboardButton from './ReturnToDashboardButton';
const getStyles = memoizeOne(() => {
return {
liveTailButtons: css`
margin-left: 10px;
@media (max-width: 1110px) {
margin-left: 4px;
}
`,
};
});
interface OwnProps { interface OwnProps {
exploreId: ExploreId; exploreId: ExploreId;
onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void; onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void;
...@@ -117,7 +104,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> { ...@@ -117,7 +104,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
onChangeTimeZone, onChangeTimeZone,
} = this.props; } = this.props;
const styles = getStyles();
const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false; const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false;
const showSmallTimePicker = splitted || containerWidth < 1210; const showSmallTimePicker = splitted || containerWidth < 1210;
...@@ -163,31 +149,29 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> { ...@@ -163,31 +149,29 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
</div> </div>
</div> </div>
) : null} ) : null}
<ToolbarButtonRow>
<ReturnToDashboardButton exploreId={exploreId} /> <ReturnToDashboardButton exploreId={exploreId} />
{exploreId === 'left' && !splitted ? ( {exploreId === 'left' && !splitted ? (
<div className="explore-toolbar-content-item explore-icon-align"> <ToolbarButton
<ResponsiveButton iconOnly={splitted}
splitted={splitted}
title="Split" title="Split"
/* This way ResponsiveButton doesn't add event as a parameter when invoking split function /* This way ToolbarButton doesn't add event as a parameter when invoking split function
* which breaks splitting functionality * which breaks splitting functionality
*/ */
onClick={() => split()} onClick={() => split()}
icon="columns" icon="columns"
iconClassName="icon-margin-right"
disabled={isLive} disabled={isLive}
/> >
</div> Split
</ToolbarButton>
) : null} ) : null}
<div className={'explore-toolbar-content-item'}>
<Tooltip content={'Copy shortened link'} placement="bottom"> <Tooltip content={'Copy shortened link'} placement="bottom">
<button className={'btn navbar-button'} onClick={() => createAndCopyShortLink(window.location.href)}> <ToolbarButton icon="share-alt" onClick={() => createAndCopyShortLink(window.location.href)} />
<Icon name="share-alt" />
</button>
</Tooltip> </Tooltip>
</div>
{!isLive && ( {!isLive && (
<div className="explore-toolbar-content-item">
<ExploreTimeControls <ExploreTimeControls
exploreId={exploreId} exploreId={exploreId}
range={range} range={range}
...@@ -199,21 +183,14 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> { ...@@ -199,21 +183,14 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
hideText={showSmallTimePicker} hideText={showSmallTimePicker}
onChangeTimeZone={onChangeTimeZone} onChangeTimeZone={onChangeTimeZone}
/> />
</div>
)} )}
{!isLive && ( {!isLive && (
<div className="explore-toolbar-content-item explore-icon-align"> <ToolbarButton title="Clear all" onClick={this.onClearAll} icon="trash-alt" iconOnly={splitted}>
<ResponsiveButton Clear all
splitted={splitted} </ToolbarButton>
title="Clear All"
onClick={this.onClearAll}
icon="trash-alt"
iconClassName="icon-margin-right"
/>
</div>
)} )}
<div className="explore-toolbar-content-item">
<RunButton <RunButton
refreshInterval={refreshInterval} refreshInterval={refreshInterval}
onChangeRefreshInterval={this.onChangeRefreshInterval} onChangeRefreshInterval={this.onChangeRefreshInterval}
...@@ -223,11 +200,10 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> { ...@@ -223,11 +200,10 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
onRun={this.onRunQuery} onRun={this.onRunQuery}
showDropdown={!isLive} showDropdown={!isLive}
/> />
{refreshInterval && <SetInterval func={this.onRunQuery} interval={refreshInterval} loading={loading} />} {refreshInterval && <SetInterval func={this.onRunQuery} interval={refreshInterval} loading={loading} />}
</div>
{hasLiveOption && ( {hasLiveOption && (
<div className={`explore-toolbar-content-item ${styles.liveTailButtons}`}>
<LiveTailControls exploreId={exploreId}> <LiveTailControls exploreId={exploreId}>
{(controls) => ( {(controls) => (
<LiveTailButton <LiveTailButton
...@@ -241,8 +217,8 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> { ...@@ -241,8 +217,8 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
/> />
)} )}
</LiveTailControls> </LiveTailControls>
</div>
)} )}
</ToolbarButtonRow>
</div> </div>
</div> </div>
</div> </div>
......
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { css } from 'emotion'; import { css } from 'emotion';
import { CSSTransition } from 'react-transition-group'; import { CSSTransition } from 'react-transition-group';
import { useTheme, Tooltip, stylesFactory, selectThemeVariant, Icon } from '@grafana/ui'; import { useTheme, Tooltip, stylesFactory, selectThemeVariant, ButtonGroup, ToolbarButton } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
//Components
import { ResponsiveButton } from './ResponsiveButton';
const getStyles = stylesFactory((theme: GrafanaTheme) => { const getStyles = stylesFactory((theme: GrafanaTheme) => {
const bgColor = selectThemeVariant({ light: theme.palette.gray5, dark: theme.palette.dark1 }, theme.type); const bgColor = selectThemeVariant({ light: theme.palette.gray5, dark: theme.palette.dark1 }, theme.type);
const orangeLighter = tinycolor(theme.palette.orangeDark).lighten(10).toString(); const orangeLighter = tinycolor(theme.palette.orangeDark).lighten(10).toString();
const pulseTextColor = tinycolor(theme.palette.orangeDark).desaturate(90).toString(); const pulseTextColor = tinycolor(theme.palette.orangeDark).desaturate(90).toString();
return { return {
noRightBorderStyle: css`
label: noRightBorderStyle;
border-right: 0;
`,
liveButton: css`
label: liveButton;
margin: 0;
`,
isLive: css` isLive: css`
label: isLive; label: isLive;
border-color: ${theme.palette.orangeDark}; border-color: ${theme.palette.orangeDark};
...@@ -107,25 +96,25 @@ export function LiveTailButton(props: LiveTailButtonProps) { ...@@ -107,25 +96,25 @@ export function LiveTailButton(props: LiveTailButtonProps) {
const { start, pause, resume, isLive, isPaused, stop, splitted } = props; const { start, pause, resume, isLive, isPaused, stop, splitted } = props;
const theme = useTheme(); const theme = useTheme();
const styles = getStyles(theme); const styles = getStyles(theme);
const buttonVariant = isLive && !isPaused ? 'active' : 'default';
const onClickMain = isLive ? (isPaused ? resume : pause) : start; const onClickMain = isLive ? (isPaused ? resume : pause) : start;
return ( return (
<> <ButtonGroup>
<Tooltip content={isLive ? <>Pause the live stream</> : <>Live stream your logs</>} placement="bottom"> <Tooltip
<ResponsiveButton content={isLive && !isPaused ? <>Pause the live stream</> : <>Start live stream your logs</>}
splitted={splitted} placement="bottom"
buttonClassName={classNames('btn navbar-button', styles.liveButton, { >
[`btn--radius-right-0 explore-active-button ${styles.noRightBorderStyle}`]: isLive, <ToolbarButton
[styles.isLive]: isLive && !isPaused, iconOnly={splitted}
[styles.isPaused]: isLive && isPaused, variant={buttonVariant}
})} icon={!isLive || isPaused ? 'play' : 'pause'}
icon={!isLive ? 'play' : 'pause'}
iconClassName={isLive ? 'icon-brand-gradient' : undefined}
onClick={onClickMain} onClick={onClickMain}
title={'\xa0Live'} >
/> {isLive && isPaused ? 'Paused' : 'Live'}
</ToolbarButton>
</Tooltip> </Tooltip>
<CSSTransition <CSSTransition
mountOnEnter={true} mountOnEnter={true}
unmountOnExit={true} unmountOnExit={true}
...@@ -138,17 +127,10 @@ export function LiveTailButton(props: LiveTailButtonProps) { ...@@ -138,17 +127,10 @@ export function LiveTailButton(props: LiveTailButtonProps) {
exitActive: styles.stopButtonExitActive, exitActive: styles.stopButtonExitActive,
}} }}
> >
<div>
<Tooltip content={<>Stop and exit the live stream</>} placement="bottom"> <Tooltip content={<>Stop and exit the live stream</>} placement="bottom">
<button <ToolbarButton variant={buttonVariant} onClick={stop} icon="square-shape" />
className={`btn navbar-button navbar-button--attached explore-active-button ${styles.isLive}`}
onClick={stop}
>
<Icon className="icon-brand-gradient" name="square-shape" size="lg" type="mono" />
</button>
</Tooltip> </Tooltip>
</div>
</CSSTransition> </CSSTransition>
</> </ButtonGroup>
); );
} }
import React, { forwardRef } from 'react';
import { IconName, Icon } from '@grafana/ui';
export enum IconSide {
left = 'left',
right = 'right',
}
interface Props extends React.HTMLAttributes<HTMLDivElement> {
splitted: boolean;
title: string;
onClick?: () => void;
buttonClassName?: string;
icon?: IconName;
iconClassName?: string;
iconSide?: IconSide;
disabled?: boolean;
}
function formatBtnTitle(title: string, iconSide?: string): string {
return iconSide === IconSide.left ? '\xA0' + title : iconSide === IconSide.right ? title + '\xA0' : title;
}
export const ResponsiveButton = forwardRef<HTMLButtonElement, Props>((props, ref) => {
const defaultProps = {
iconSide: IconSide.left,
};
props = { ...defaultProps, ...props };
const {
title,
onClick,
buttonClassName,
icon,
iconClassName,
splitted,
iconSide,
disabled,
...divElementProps
} = props;
return (
<div {...divElementProps}>
<button
ref={ref}
className={`btn navbar-button ${buttonClassName ? buttonClassName : ''}`}
onClick={onClick ?? undefined}
disabled={disabled || false}
>
{icon && iconSide === IconSide.left ? <Icon name={icon} className={iconClassName} size="lg" /> : null}
<span className="btn-title">{!splitted ? formatBtnTitle(title, iconSide) : ''}</span>
{icon && iconSide === IconSide.right ? <Icon name={icon} className={iconClassName} size="lg" /> : null}
</button>
</div>
);
});
ResponsiveButton.displayName = 'ResponsiveButton';
...@@ -66,7 +66,7 @@ export const UnconnectedReturnToDashboardButton: FC<Props> = ({ ...@@ -66,7 +66,7 @@ export const UnconnectedReturnToDashboardButton: FC<Props> = ({
}; };
return ( return (
<ButtonGroup className="explore-toolbar-content-item" noSpacing> <ButtonGroup>
<Tooltip content={'Return to panel'} placement="bottom"> <Tooltip content={'Return to panel'} placement="bottom">
<ToolbarButton data-testid="returnButton" title={'Return to panel'} onClick={() => returnToPanel()}> <ToolbarButton data-testid="returnButton" title={'Return to panel'} onClick={() => returnToPanel()}>
<Icon name="arrow-left" /> <Icon name="arrow-left" />
......
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import { Tooltip, ToolbarButton } from '@grafana/ui';
import { Tooltip, Icon } from '@grafana/ui';
interface TimeSyncButtonProps { interface TimeSyncButtonProps {
isSynced: boolean; isSynced: boolean;
...@@ -18,15 +17,12 @@ export function TimeSyncButton(props: TimeSyncButtonProps) { ...@@ -18,15 +17,12 @@ export function TimeSyncButton(props: TimeSyncButtonProps) {
return ( return (
<Tooltip content={syncTimesTooltip} placement="bottom"> <Tooltip content={syncTimesTooltip} placement="bottom">
<button <ToolbarButton
className={classNames('btn navbar-button navbar-button--attached', { icon="link"
[`explore-active-button`]: isSynced, variant={isSynced ? 'active' : 'default'}
})}
aria-label={isSynced ? 'Synced times' : 'Unsynced times'} aria-label={isSynced ? 'Synced times' : 'Unsynced times'}
onClick={() => onClick()} onClick={onClick}
> />
<Icon name="link" className={isSynced ? 'icon-brand-gradient' : ''} size="lg" />
</button>
</Tooltip> </Tooltip>
); );
} }
...@@ -114,33 +114,7 @@ ...@@ -114,33 +114,7 @@
} }
} }
@media only screen and (max-width: 1545px) {
.explore-toolbar.splitted {
.timepicker-rangestring {
display: none;
}
}
}
@media only screen and (max-width: 1400px) {
.explore-toolbar.splitted {
.explore-toolbar-content-item {
.navbar-button {
span {
display: none;
}
}
}
}
}
@media only screen and (max-width: 1070px) { @media only screen and (max-width: 1070px) {
.timepicker {
.timepicker-rangestring {
display: none;
}
}
.explore-toolbar-content { .explore-toolbar-content {
justify-content: flex-start; justify-content: flex-start;
} }
...@@ -152,16 +126,6 @@ ...@@ -152,16 +126,6 @@
} }
} }
@media only screen and (max-width: 897px) {
.explore-toolbar {
.explore-toolbar-content-item {
.navbar-button span {
display: none;
}
}
}
}
@media only screen and (max-width: 810px) { @media only screen and (max-width: 810px) {
.explore-toolbar { .explore-toolbar {
.explore-toolbar-content-item { .explore-toolbar-content-item {
...@@ -173,29 +137,6 @@ ...@@ -173,29 +137,6 @@
} }
} }
.explore-icon-align {
.navbar-button {
i {
position: relative;
top: -1px;
@media only screen and (max-width: 1320px) {
margin: 0 -3px;
}
}
}
}
.explore-toolbar.splitted {
.explore-icon-align {
.navbar-button {
i {
margin: 0 -3px;
}
}
}
}
.explore { .explore {
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment