Navigation: Integrate Explore actions into topnav (#56381)

* integrate Explore actions into topnav toolbar

* add keys

* fix unit test

* fix bug with ToolbarButtonRow overflow

* make the render function a bit more readable

* fix overflow not updating when children change
This commit is contained in:
Ashley Harrison 2022-10-11 12:59:17 +01:00 committed by GitHub
parent d6fa12c5bc
commit 90cf76e05e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 174 additions and 108 deletions

View File

@ -105,7 +105,7 @@ export const PageToolbar: FC<Props> = React.memo(
</>
)}
{(title || leftItems?.length) && (
{(title || Boolean(leftItems?.length)) && (
<div className={styles.titleWrapper}>
{title && (
<h1 className={styles.h1Styles}>

View File

@ -15,6 +15,8 @@ export interface Props extends HTMLAttributes<HTMLDivElement> {
alignment?: 'left' | 'right';
}
const OVERFLOW_BUTTON_ID = 'overflow-button';
export const ToolbarButtonRow = forwardRef<HTMLDivElement, Props>(
({ alignment = 'left', className, children, ...rest }, ref) => {
const [childVisibility, setChildVisibility] = useState<boolean[]>(
@ -53,7 +55,10 @@ export const ToolbarButtonRow = forwardRef<HTMLDivElement, Props>(
);
if (containerRef.current) {
Array.from(containerRef.current.children).forEach((item) => {
intersectionObserver.observe(item);
// don't observe the overflow button
if (item instanceof HTMLElement && item.dataset.testid !== OVERFLOW_BUTTON_ID) {
intersectionObserver.observe(item);
}
});
}
return () => intersectionObserver.disconnect();
@ -70,12 +75,11 @@ export const ToolbarButtonRow = forwardRef<HTMLDivElement, Props>(
</div>
))}
{childVisibility.includes(false) && (
<>
<div data-testid={OVERFLOW_BUTTON_ID} className={styles.overflowButton}>
<ToolbarButton
variant={showOverflowItems ? 'active' : 'default'}
tooltip="Show more items"
onClick={() => setShowOverflowItems(!showOverflowItems)}
className={styles.overflowButton}
icon="ellipsis-v"
iconOnly
narrow
@ -87,7 +91,7 @@ export const ToolbarButtonRow = forwardRef<HTMLDivElement, Props>(
</div>
</FocusScope>
)}
</>
</div>
)}
</div>
);

View File

@ -82,6 +82,7 @@ const dummyProps: Props = {
showNodeGraph: true,
showFlameGraph: true,
splitOpen: (() => {}) as any,
splitted: false,
changeGraphStyle: () => {},
graphStyle: 'lines',
};

View File

@ -38,6 +38,7 @@ import { TraceViewContainer } from './TraceView/TraceViewContainer';
import { changeSize, changeGraphStyle } from './state/explorePane';
import { splitOpen } from './state/main';
import { addQueryRow, modifyQueries, scanStart, scanStopAction, setQueries } from './state/query';
import { isSplit } from './state/selectors';
import { makeAbsoluteTime, updateTimeRange } from './state/time';
const getStyles = (theme: GrafanaTheme2) => {
@ -67,6 +68,9 @@ const getStyles = (theme: GrafanaTheme2) => {
padding: ${theme.spacing(2)};
padding-top: 0;
`,
exploreContainerTopnav: css`
padding-top: ${theme.spacing(2)};
`,
};
};
@ -349,6 +353,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
showTrace,
showNodeGraph,
showFlameGraph,
splitted,
timeZone,
} = this.props;
const { openDrawer } = this.state;
@ -377,7 +382,11 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} topOfViewRef={this.topOfViewRef} />
{datasourceMissing ? this.renderEmptyState(styles.exploreContainer) : null}
{datasourceInstance && (
<div className={cx(styles.exploreContainer)}>
<div
className={cx(styles.exploreContainer, {
[styles.exploreContainerTopnav]: Boolean(config.featureToggles.topnav && !splitted),
})}
>
<PanelContainer className={styles.queryContainer}>
<QueryRows exploreId={exploreId} />
<SecondaryActions
@ -486,6 +495,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
showTrace,
showNodeGraph,
showFlameGraph,
splitted: isSplit(state),
loading,
graphStyle,
};

View File

@ -3,7 +3,15 @@ import { connect, ConnectedProps } from 'react-redux';
import { DataSourceInstanceSettings, RawTimeRange } from '@grafana/data';
import { config, DataSourcePicker, reportInteraction } from '@grafana/runtime';
import { defaultIntervals, PageToolbar, RefreshPicker, SetInterval, ToolbarButton } from '@grafana/ui';
import {
defaultIntervals,
PageToolbar,
RefreshPicker,
SetInterval,
ToolbarButton,
ToolbarButtonRow,
} from '@grafana/ui';
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { contextSrv } from 'app/core/core';
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
import { AccessControlAction } from 'app/types';
@ -92,6 +100,7 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
return (
<RefreshPicker
key="refreshPicker"
onIntervalChanged={this.onChangeRefreshInterval}
value={refreshInterval}
isLoading={loading}
@ -107,126 +116,168 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
);
};
render() {
renderActions = () => {
const {
datasourceMissing,
splitted,
isLive,
exploreId,
loading,
range,
timeZone,
fiscalYearStartMonth,
splitted,
syncedTimes,
refreshInterval,
onChangeTime,
hasLiveOption,
isLive,
isPaused,
containerWidth,
syncedTimes,
onChangeTimeZone,
onChangeFiscalYearStartMonth,
topOfViewRef,
refreshInterval,
loading,
isPaused,
hasLiveOption,
containerWidth,
} = this.props;
const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false;
const showSmallTimePicker = splitted || containerWidth < 1210;
const showExploreToDashboard =
contextSrv.hasAccess(AccessControlAction.DashboardsCreate, contextSrv.isEditor) ||
contextSrv.hasAccess(AccessControlAction.DashboardsWrite, contextSrv.isEditor);
return (
return [
!splitted ? (
<ToolbarButton
key="split"
tooltip="Split the pane"
onClick={this.onOpenSplitView}
icon="columns"
disabled={isLive}
>
Split
</ToolbarButton>
) : (
<ToolbarButton key="split" tooltip="Close split pane" onClick={this.onCloseSplitView} icon="times">
Close
</ToolbarButton>
),
config.featureToggles.explore2Dashboard && showExploreToDashboard && (
<Suspense key="addToDashboard" fallback={null}>
<AddToDashboard exploreId={exploreId} />
</Suspense>
),
!isLive && (
<ExploreTimeControls
key="timeControls"
exploreId={exploreId}
range={range}
timeZone={timeZone}
fiscalYearStartMonth={fiscalYearStartMonth}
onChangeTime={onChangeTime}
splitted={splitted}
syncedTimes={syncedTimes}
onChangeTimeSync={this.onChangeTimeSync}
hideText={showSmallTimePicker}
onChangeTimeZone={onChangeTimeZone}
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
/>
),
this.renderRefreshPicker(showSmallTimePicker),
refreshInterval && (
<SetInterval key="setInterval" func={this.onRunQuery} interval={refreshInterval} loading={loading} />
),
hasLiveOption && (
<LiveTailControls key="liveControls" exploreId={exploreId}>
{(c) => {
const controls = {
...c,
start: () => {
reportInteraction('grafana_explore_logs_live_tailing_clicked', {
datasourceType: this.props.datasourceType,
});
c.start();
},
};
return (
<LiveTailButton
splitted={splitted}
isLive={isLive}
isPaused={isPaused}
start={controls.start}
pause={controls.pause}
resume={controls.resume}
stop={controls.stop}
/>
);
}}
</LiveTailControls>
),
].filter(Boolean);
};
render() {
const { datasourceMissing, exploreId, splitted, containerWidth, topOfViewRef } = this.props;
const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false;
const isTopnav = config.featureToggles.topnav;
const getDashNav = () => (
<DashNavButton
key="share"
tooltip="Copy shortened link"
icon="share-alt"
onClick={this.onCopyShortLink}
aria-label="Copy shortened link"
/>
);
const getDataSourcePicker = () =>
!datasourceMissing && (
<DataSourcePicker
key={`${exploreId}-ds-picker`}
mixed={config.featureToggles.exploreMixedDatasource === true}
onChange={this.onChangeDatasource}
current={this.props.datasourceRef}
hideTextValue={showSmallDataSourcePicker}
width={showSmallDataSourcePicker ? 8 : undefined}
/>
);
const topNavActions = [
getDashNav(),
!splitted && getDataSourcePicker(),
<div style={{ flex: 1 }} key="spacer" />,
<ToolbarButtonRow key="actions" alignment="right">
{this.renderActions()}
</ToolbarButtonRow>,
].filter(Boolean);
const toolbarLeftItems = [exploreId === ExploreId.left && getDashNav(), getDataSourcePicker()].filter(Boolean);
const toolbarLeftItemsTopNav = [
exploreId === ExploreId.left && (
<AppChromeUpdate
actions={[getDashNav(), !splitted && getDataSourcePicker(), <div style={{ flex: 1 }} key="spacer" />].filter(
Boolean
)}
/>
),
getDataSourcePicker(),
].filter(Boolean);
return isTopnav && !splitted ? (
<div ref={topOfViewRef}>
<AppChromeUpdate actions={topNavActions} />
</div>
) : (
<div ref={topOfViewRef}>
<PageToolbar
aria-label="Explore toolbar"
title={exploreId === ExploreId.left ? 'Explore' : undefined}
pageIcon={exploreId === ExploreId.left ? 'compass' : undefined}
leftItems={[
exploreId === ExploreId.left && (
<DashNavButton
key="share"
tooltip="Copy shortened link"
icon="share-alt"
onClick={this.onCopyShortLink}
aria-label="Copy shortened link"
/>
),
!datasourceMissing && (
<DataSourcePicker
key={`${exploreId}-ds-picker`}
mixed={config.featureToggles.exploreMixedDatasource === true}
onChange={this.onChangeDatasource}
current={this.props.datasourceRef}
hideTextValue={showSmallDataSourcePicker}
width={showSmallDataSourcePicker ? 8 : undefined}
/>
),
].filter(Boolean)}
title={exploreId === ExploreId.left && !isTopnav ? 'Explore' : undefined}
pageIcon={exploreId === ExploreId.left && !isTopnav ? 'compass' : undefined}
leftItems={isTopnav ? toolbarLeftItemsTopNav : toolbarLeftItems}
>
<>
{!splitted ? (
<ToolbarButton tooltip="Split the pane" onClick={this.onOpenSplitView} icon="columns" disabled={isLive}>
Split
</ToolbarButton>
) : (
<ToolbarButton tooltip="Close split pane" onClick={this.onCloseSplitView} icon="times">
Close
</ToolbarButton>
)}
{config.featureToggles.explore2Dashboard && showExploreToDashboard && (
<Suspense fallback={null}>
<AddToDashboard exploreId={exploreId} />
</Suspense>
)}
{!isLive && (
<ExploreTimeControls
exploreId={exploreId}
range={range}
timeZone={timeZone}
fiscalYearStartMonth={fiscalYearStartMonth}
onChangeTime={onChangeTime}
splitted={splitted}
syncedTimes={syncedTimes}
onChangeTimeSync={this.onChangeTimeSync}
hideText={showSmallTimePicker}
onChangeTimeZone={onChangeTimeZone}
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
/>
)}
{this.renderRefreshPicker(showSmallTimePicker)}
{refreshInterval && <SetInterval func={this.onRunQuery} interval={refreshInterval} loading={loading} />}
{hasLiveOption && (
<LiveTailControls exploreId={exploreId}>
{(c) => {
const controls = {
...c,
start: () => {
reportInteraction('grafana_explore_logs_live_tailing_clicked', {
datasourceType: this.props.datasourceType,
});
c.start();
},
};
return (
<LiveTailButton
splitted={splitted}
isLive={isLive}
isPaused={isPaused}
start={controls.start}
pause={controls.pause}
resume={controls.resume}
stop={controls.stop}
/>
);
}}
</LiveTailControls>
)}
</>
{this.renderActions()}
</PageToolbar>
</div>
);