Trace view scroll to top button (#46077)

* Trace view scroll to top button

* Updated button css selector

* Updates for scrolToTop

* Updated snapshot

* Updated render

* Add smooth scrolling
This commit is contained in:
Joey Tawadrous 2022-03-03 16:19:44 +00:00 committed by GitHub
parent cfbf58fb92
commit 6f3ebbe6cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 138 additions and 89 deletions

View File

@ -17,7 +17,7 @@ import { css } from '@emotion/css';
import { isEqual } from 'lodash';
import memoizeOne from 'memoize-one';
import { stylesFactory, withTheme2 } from '@grafana/ui';
import { stylesFactory, withTheme2, ToolbarButton } from '@grafana/ui';
import { GrafanaTheme2, LinkModel } from '@grafana/data';
import ListView from './ListView';
@ -38,6 +38,7 @@ import { SpanLinkFunc, TNil } from '../types';
import { TraceLog, TraceSpan, Trace, TraceKeyValuePair, TraceLink, TraceSpanReference } from '../types/trace';
import TTraceTimeline from '../types/TTraceTimeline';
import { PEER_SERVICE } from '../constants/tag-keys';
import { createRef, RefObject } from 'react';
type TExtractUiFindFromStateReturn = {
uiFind: string | undefined;
@ -51,6 +52,18 @@ const getStyles = stylesFactory(() => {
row: css`
width: 100%;
`,
scrollToTopButton: css`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
position: fixed;
bottom: 30px;
right: 30px;
z-index: 1;
`,
};
});
@ -89,6 +102,7 @@ type TVirtualizedTraceViewOwnProps = {
scrollElement?: Element;
focusedSpanId?: string;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
};
type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TExtractUiFindFromStateReturn & TTraceTimeline;
@ -168,6 +182,7 @@ const memoizedGetClipping = memoizeOne(getClipping, isEqual);
// export from tests
export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTraceViewProps> {
listView: ListView | TNil;
topTraceViewRef = createRef<HTMLDivElement>();
constructor(props: VirtualizedTraceViewProps) {
super(props);
@ -495,11 +510,16 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
);
}
scrollToTop = () => {
const { topOfExploreViewRef } = this.props;
topOfExploreViewRef?.current?.scrollIntoView({ behavior: 'smooth' });
};
render() {
const styles = getStyles();
const { scrollElement } = this.props;
return (
<div>
<>
<ListView
ref={this.setListView}
dataLength={this.getRowStates().length}
@ -513,7 +533,14 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
windowScroller={false}
scrollElement={scrollElement}
/>
</div>
<ToolbarButton
className={styles.scrollToTopButton}
onClick={this.scrollToTop}
title="Scroll to top"
icon="arrow-up"
></ToolbarButton>
</>
);
}
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import React, { RefObject } from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme2, LinkModel } from '@grafana/data';
import { stylesFactory, withTheme2 } from '@grafana/ui';
@ -106,6 +106,7 @@ type TProps = TExtractUiFindFromStateReturn & {
scrollElement?: Element;
focusedSpanId?: string;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
};
type State = {
@ -161,6 +162,7 @@ export class UnthemedTraceTimelineViewer extends React.PureComponent<TProps, Sta
createLinkToExternalSpan,
traceTimeline,
theme,
topOfExploreViewRef,
...rest
} = this.props;
const { trace } = rest;
@ -191,6 +193,7 @@ export class UnthemedTraceTimelineViewer extends React.PureComponent<TProps, Sta
{...traceTimeline}
setSpanNameColumnWidth={setSpanNameColumnWidth}
currentViewRangeTime={viewRange.time.current}
topOfExploreViewRef={topOfExploreViewRef}
/>
</div>
</ExternalLinkContext.Provider>

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { createRef } from 'react';
import { css, cx } from '@emotion/css';
import { compose } from 'redux';
import { connect, ConnectedProps } from 'react-redux';
@ -101,6 +101,7 @@ export type Props = ExploreProps & ConnectedProps<typeof connector>;
export class Explore extends React.PureComponent<Props, ExploreState> {
scrollElement: HTMLDivElement | undefined;
absoluteTimeUnsubsciber: Unsubscribable | undefined;
topOfExploreViewRef = createRef<HTMLDivElement>();
constructor(props: Props) {
super(props);
@ -305,6 +306,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
dataFrames={dataFrames}
splitOpenFn={splitOpen}
scrollElement={this.scrollElement}
topOfExploreViewRef={this.topOfExploreViewRef}
/>
)
);
@ -337,7 +339,11 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
autoHeightMin={'100%'}
scrollRefCallback={(scrollElement) => (this.scrollElement = scrollElement || undefined)}
>
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} />
<ExploreToolbar
exploreId={exploreId}
onChangeTime={this.onChangeTime}
topOfExploreViewRef={this.topOfExploreViewRef}
/>
{datasourceMissing ? this.renderEmptyState() : null}
{datasourceInstance && (
<div className="explore-container">

View File

@ -1,4 +1,4 @@
import React, { PureComponent } from 'react';
import React, { PureComponent, RefObject } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { ExploreId } from 'app/types/explore';
import { PageToolbar, SetInterval, ToolbarButton, ToolbarButtonRow } from '@grafana/ui';
@ -23,6 +23,7 @@ import { AddToDashboard } from './AddToDashboard';
interface OwnProps {
exploreId: ExploreId;
onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
}
type Props = OwnProps & ConnectedProps<typeof connector>;
@ -71,94 +72,97 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
containerWidth,
onChangeTimeZone,
onChangeFiscalYearStartMonth,
topOfExploreViewRef,
} = this.props;
const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false;
const showSmallTimePicker = splitted || containerWidth < 1210;
return (
<PageToolbar
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={() => createAndCopyShortLink(window.location.href)}
aria-label="Copy shortened link"
<div ref={topOfExploreViewRef}>
<PageToolbar
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={() => createAndCopyShortLink(window.location.href)}
aria-label="Copy shortened link"
/>
),
!datasourceMissing && (
<DataSourcePicker
key={`${exploreId}-ds-picker`}
onChange={this.onChangeDatasource}
current={this.props.datasourceName}
hideTextValue={showSmallDataSourcePicker}
width={showSmallDataSourcePicker ? 8 : undefined}
/>
),
].filter(Boolean)}
>
<ToolbarButtonRow>
{!splitted ? (
<ToolbarButton title="Split" onClick={() => split()} icon="columns" disabled={isLive}>
Split
</ToolbarButton>
) : (
<ToolbarButton title="Close split pane" onClick={() => closeSplit(exploreId)} icon="times">
Close
</ToolbarButton>
)}
{!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}
/>
)}
<AddToDashboard exploreId={exploreId} />
<RunButton
refreshInterval={refreshInterval}
onChangeRefreshInterval={this.onChangeRefreshInterval}
isSmall={splitted || showSmallTimePicker}
isLive={isLive}
loading={loading || (isLive && !isPaused)}
onRun={this.onRunQuery}
showDropdown={!isLive}
/>
),
!datasourceMissing && (
<DataSourcePicker
key={`${exploreId}-ds-picker`}
onChange={this.onChangeDatasource}
current={this.props.datasourceName}
hideTextValue={showSmallDataSourcePicker}
width={showSmallDataSourcePicker ? 8 : undefined}
/>
),
].filter(Boolean)}
>
<ToolbarButtonRow>
{!splitted ? (
<ToolbarButton title="Split" onClick={() => split()} icon="columns" disabled={isLive}>
Split
</ToolbarButton>
) : (
<ToolbarButton title="Close split pane" onClick={() => closeSplit(exploreId)} icon="times">
Close
</ToolbarButton>
)}
{!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}
/>
)}
{refreshInterval && <SetInterval func={this.onRunQuery} interval={refreshInterval} loading={loading} />}
<AddToDashboard exploreId={exploreId} />
<RunButton
refreshInterval={refreshInterval}
onChangeRefreshInterval={this.onChangeRefreshInterval}
isSmall={splitted || showSmallTimePicker}
isLive={isLive}
loading={loading || (isLive && !isPaused)}
onRun={this.onRunQuery}
showDropdown={!isLive}
/>
{refreshInterval && <SetInterval func={this.onRunQuery} interval={refreshInterval} loading={loading} />}
{hasLiveOption && (
<LiveTailControls exploreId={exploreId}>
{(controls) => (
<LiveTailButton
splitted={splitted}
isLive={isLive}
isPaused={isPaused}
start={controls.start}
pause={controls.pause}
resume={controls.resume}
stop={controls.stop}
/>
)}
</LiveTailControls>
)}
</ToolbarButtonRow>
</PageToolbar>
{hasLiveOption && (
<LiveTailControls exploreId={exploreId}>
{(controls) => (
<LiveTailButton
splitted={splitted}
isLive={isLive}
isPaused={isPaused}
start={controls.start}
pause={controls.pause}
resume={controls.resume}
stop={controls.stop}
/>
)}
</LiveTailControls>
)}
</ToolbarButtonRow>
</PageToolbar>
</div>
);
}
}

View File

@ -24,7 +24,7 @@ import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { getTimeZone } from 'app/features/profile/state/selectors';
import { StoreState } from 'app/types';
import { ExploreId } from 'app/types/explore';
import React, { useCallback, useMemo, useState } from 'react';
import React, { RefObject, useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { changePanelState } from '../state/explorePane';
import { createSpanLinkFactory } from './createSpanLink';
@ -43,6 +43,7 @@ type Props = {
splitOpenFn: SplitOpen;
exploreId: ExploreId;
scrollElement?: Element;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
};
export function TraceView(props: Props) {
@ -176,6 +177,7 @@ export function TraceView(props: Props) {
scrollElement={props.scrollElement}
focusedSpanId={focusedSpanId}
createFocusSpanLink={createFocusSpanLink}
topOfExploreViewRef={props.topOfExploreViewRef}
/>
</>
);

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { RefObject } from 'react';
import { Collapse } from '@grafana/ui';
import { DataFrame, SplitOpen } from '@grafana/data';
import { TraceView } from './TraceView';
@ -9,9 +9,10 @@ interface Props {
splitOpenFn: SplitOpen;
exploreId: ExploreId;
scrollElement?: Element;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
}
export function TraceViewContainer(props: Props) {
const { dataFrames, splitOpenFn, exploreId, scrollElement } = props;
const { dataFrames, splitOpenFn, exploreId, scrollElement, topOfExploreViewRef } = props;
return (
<Collapse label="Trace View" isOpen>
@ -20,6 +21,7 @@ export function TraceViewContainer(props: Props) {
dataFrames={dataFrames}
splitOpenFn={splitOpenFn}
scrollElement={scrollElement}
topOfExploreViewRef={topOfExploreViewRef}
/>
</Collapse>
);

View File

@ -8,6 +8,11 @@ exports[`Explore should render component 1`] = `
<Connect(UnConnectedExploreToolbar)
exploreId="left"
onChangeTime={[Function]}
topOfExploreViewRef={
Object {
"current": null,
}
}
/>
<div
className="explore-container"