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
7 changed files with 138 additions and 89 deletions
@@ -17,7 +17,7 @@ import { css } from '@emotion/css';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import memoizeOne from 'memoize-one'; 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 { GrafanaTheme2, LinkModel } from '@grafana/data';
import ListView from './ListView'; import ListView from './ListView';
@@ -38,6 +38,7 @@ import { SpanLinkFunc, TNil } from '../types';
import { TraceLog, TraceSpan, Trace, TraceKeyValuePair, TraceLink, TraceSpanReference } from '../types/trace'; import { TraceLog, TraceSpan, Trace, TraceKeyValuePair, TraceLink, TraceSpanReference } from '../types/trace';
import TTraceTimeline from '../types/TTraceTimeline'; import TTraceTimeline from '../types/TTraceTimeline';
import { PEER_SERVICE } from '../constants/tag-keys'; import { PEER_SERVICE } from '../constants/tag-keys';
import { createRef, RefObject } from 'react';
type TExtractUiFindFromStateReturn = { type TExtractUiFindFromStateReturn = {
uiFind: string | undefined; uiFind: string | undefined;
@@ -51,6 +52,18 @@ const getStyles = stylesFactory(() => {
row: css` row: css`
width: 100%; 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; scrollElement?: Element;
focusedSpanId?: string; focusedSpanId?: string;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel; createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
}; };
type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TExtractUiFindFromStateReturn & TTraceTimeline; type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TExtractUiFindFromStateReturn & TTraceTimeline;
@@ -168,6 +182,7 @@ const memoizedGetClipping = memoizeOne(getClipping, isEqual);
// export from tests // export from tests
export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTraceViewProps> { export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTraceViewProps> {
listView: ListView | TNil; listView: ListView | TNil;
topTraceViewRef = createRef<HTMLDivElement>();
constructor(props: VirtualizedTraceViewProps) { constructor(props: VirtualizedTraceViewProps) {
super(props); super(props);
@@ -495,11 +510,16 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
); );
} }
scrollToTop = () => {
const { topOfExploreViewRef } = this.props;
topOfExploreViewRef?.current?.scrollIntoView({ behavior: 'smooth' });
};
render() { render() {
const styles = getStyles(); const styles = getStyles();
const { scrollElement } = this.props; const { scrollElement } = this.props;
return ( return (
<div> <>
<ListView <ListView
ref={this.setListView} ref={this.setListView}
dataLength={this.getRowStates().length} dataLength={this.getRowStates().length}
@@ -513,7 +533,14 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
windowScroller={false} windowScroller={false}
scrollElement={scrollElement} scrollElement={scrollElement}
/> />
</div>
<ToolbarButton
className={styles.scrollToTopButton}
onClick={this.scrollToTop}
title="Scroll to top"
icon="arrow-up"
></ToolbarButton>
</>
); );
} }
} }
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import React from 'react'; import React, { RefObject } from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme2, LinkModel } from '@grafana/data'; import { GrafanaTheme2, LinkModel } from '@grafana/data';
import { stylesFactory, withTheme2 } from '@grafana/ui'; import { stylesFactory, withTheme2 } from '@grafana/ui';
@@ -106,6 +106,7 @@ type TProps = TExtractUiFindFromStateReturn & {
scrollElement?: Element; scrollElement?: Element;
focusedSpanId?: string; focusedSpanId?: string;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel; createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
}; };
type State = { type State = {
@@ -161,6 +162,7 @@ export class UnthemedTraceTimelineViewer extends React.PureComponent<TProps, Sta
createLinkToExternalSpan, createLinkToExternalSpan,
traceTimeline, traceTimeline,
theme, theme,
topOfExploreViewRef,
...rest ...rest
} = this.props; } = this.props;
const { trace } = rest; const { trace } = rest;
@@ -191,6 +193,7 @@ export class UnthemedTraceTimelineViewer extends React.PureComponent<TProps, Sta
{...traceTimeline} {...traceTimeline}
setSpanNameColumnWidth={setSpanNameColumnWidth} setSpanNameColumnWidth={setSpanNameColumnWidth}
currentViewRangeTime={viewRange.time.current} currentViewRangeTime={viewRange.time.current}
topOfExploreViewRef={topOfExploreViewRef}
/> />
</div> </div>
</ExternalLinkContext.Provider> </ExternalLinkContext.Provider>
+8 -2
View File
@@ -1,4 +1,4 @@
import React from 'react'; import React, { createRef } from 'react';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { compose } from 'redux'; import { compose } from 'redux';
import { connect, ConnectedProps } from 'react-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> { export class Explore extends React.PureComponent<Props, ExploreState> {
scrollElement: HTMLDivElement | undefined; scrollElement: HTMLDivElement | undefined;
absoluteTimeUnsubsciber: Unsubscribable | undefined; absoluteTimeUnsubsciber: Unsubscribable | undefined;
topOfExploreViewRef = createRef<HTMLDivElement>();
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@@ -305,6 +306,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
dataFrames={dataFrames} dataFrames={dataFrames}
splitOpenFn={splitOpen} splitOpenFn={splitOpen}
scrollElement={this.scrollElement} scrollElement={this.scrollElement}
topOfExploreViewRef={this.topOfExploreViewRef}
/> />
) )
); );
@@ -337,7 +339,11 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
autoHeightMin={'100%'} autoHeightMin={'100%'}
scrollRefCallback={(scrollElement) => (this.scrollElement = scrollElement || undefined)} 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} {datasourceMissing ? this.renderEmptyState() : null}
{datasourceInstance && ( {datasourceInstance && (
<div className="explore-container"> <div className="explore-container">
+84 -80
View File
@@ -1,4 +1,4 @@
import React, { PureComponent } from 'react'; import React, { PureComponent, RefObject } from 'react';
import { connect, ConnectedProps } from 'react-redux'; import { connect, ConnectedProps } from 'react-redux';
import { ExploreId } from 'app/types/explore'; import { ExploreId } from 'app/types/explore';
import { PageToolbar, SetInterval, ToolbarButton, ToolbarButtonRow } from '@grafana/ui'; import { PageToolbar, SetInterval, ToolbarButton, ToolbarButtonRow } from '@grafana/ui';
@@ -23,6 +23,7 @@ import { AddToDashboard } from './AddToDashboard';
interface OwnProps { interface OwnProps {
exploreId: ExploreId; exploreId: ExploreId;
onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void; onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
} }
type Props = OwnProps & ConnectedProps<typeof connector>; type Props = OwnProps & ConnectedProps<typeof connector>;
@@ -71,94 +72,97 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
containerWidth, containerWidth,
onChangeTimeZone, onChangeTimeZone,
onChangeFiscalYearStartMonth, onChangeFiscalYearStartMonth,
topOfExploreViewRef,
} = this.props; } = this.props;
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;
return ( return (
<PageToolbar <div ref={topOfExploreViewRef}>
title={exploreId === ExploreId.left ? 'Explore' : undefined} <PageToolbar
pageIcon={exploreId === ExploreId.left ? 'compass' : undefined} title={exploreId === ExploreId.left ? 'Explore' : undefined}
leftItems={[ pageIcon={exploreId === ExploreId.left ? 'compass' : undefined}
exploreId === ExploreId.left && ( leftItems={[
<DashNavButton exploreId === ExploreId.left && (
key="share" <DashNavButton
tooltip="Copy shortened link" key="share"
icon="share-alt" tooltip="Copy shortened link"
onClick={() => createAndCopyShortLink(window.location.href)} icon="share-alt"
aria-label="Copy shortened link" 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 && ( {refreshInterval && <SetInterval func={this.onRunQuery} interval={refreshInterval} loading={loading} />}
<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} /> {hasLiveOption && (
<LiveTailControls exploreId={exploreId}>
<RunButton {(controls) => (
refreshInterval={refreshInterval} <LiveTailButton
onChangeRefreshInterval={this.onChangeRefreshInterval} splitted={splitted}
isSmall={splitted || showSmallTimePicker} isLive={isLive}
isLive={isLive} isPaused={isPaused}
loading={loading || (isLive && !isPaused)} start={controls.start}
onRun={this.onRunQuery} pause={controls.pause}
showDropdown={!isLive} resume={controls.resume}
/> stop={controls.stop}
/>
{refreshInterval && <SetInterval func={this.onRunQuery} interval={refreshInterval} loading={loading} />} )}
</LiveTailControls>
{hasLiveOption && ( )}
<LiveTailControls exploreId={exploreId}> </ToolbarButtonRow>
{(controls) => ( </PageToolbar>
<LiveTailButton </div>
splitted={splitted}
isLive={isLive}
isPaused={isPaused}
start={controls.start}
pause={controls.pause}
resume={controls.resume}
stop={controls.stop}
/>
)}
</LiveTailControls>
)}
</ToolbarButtonRow>
</PageToolbar>
); );
} }
} }
@@ -24,7 +24,7 @@ import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { getTimeZone } from 'app/features/profile/state/selectors'; import { getTimeZone } from 'app/features/profile/state/selectors';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { ExploreId } from 'app/types/explore'; 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 { useDispatch, useSelector } from 'react-redux';
import { changePanelState } from '../state/explorePane'; import { changePanelState } from '../state/explorePane';
import { createSpanLinkFactory } from './createSpanLink'; import { createSpanLinkFactory } from './createSpanLink';
@@ -43,6 +43,7 @@ type Props = {
splitOpenFn: SplitOpen; splitOpenFn: SplitOpen;
exploreId: ExploreId; exploreId: ExploreId;
scrollElement?: Element; scrollElement?: Element;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
}; };
export function TraceView(props: Props) { export function TraceView(props: Props) {
@@ -176,6 +177,7 @@ export function TraceView(props: Props) {
scrollElement={props.scrollElement} scrollElement={props.scrollElement}
focusedSpanId={focusedSpanId} focusedSpanId={focusedSpanId}
createFocusSpanLink={createFocusSpanLink} createFocusSpanLink={createFocusSpanLink}
topOfExploreViewRef={props.topOfExploreViewRef}
/> />
</> </>
); );
@@ -1,4 +1,4 @@
import React from 'react'; import React, { RefObject } from 'react';
import { Collapse } from '@grafana/ui'; import { Collapse } from '@grafana/ui';
import { DataFrame, SplitOpen } from '@grafana/data'; import { DataFrame, SplitOpen } from '@grafana/data';
import { TraceView } from './TraceView'; import { TraceView } from './TraceView';
@@ -9,9 +9,10 @@ interface Props {
splitOpenFn: SplitOpen; splitOpenFn: SplitOpen;
exploreId: ExploreId; exploreId: ExploreId;
scrollElement?: Element; scrollElement?: Element;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
} }
export function TraceViewContainer(props: Props) { export function TraceViewContainer(props: Props) {
const { dataFrames, splitOpenFn, exploreId, scrollElement } = props; const { dataFrames, splitOpenFn, exploreId, scrollElement, topOfExploreViewRef } = props;
return ( return (
<Collapse label="Trace View" isOpen> <Collapse label="Trace View" isOpen>
@@ -20,6 +21,7 @@ export function TraceViewContainer(props: Props) {
dataFrames={dataFrames} dataFrames={dataFrames}
splitOpenFn={splitOpenFn} splitOpenFn={splitOpenFn}
scrollElement={scrollElement} scrollElement={scrollElement}
topOfExploreViewRef={topOfExploreViewRef}
/> />
</Collapse> </Collapse>
); );
@@ -8,6 +8,11 @@ exports[`Explore should render component 1`] = `
<Connect(UnConnectedExploreToolbar) <Connect(UnConnectedExploreToolbar)
exploreId="left" exploreId="left"
onChangeTime={[Function]} onChangeTime={[Function]}
topOfExploreViewRef={
Object {
"current": null,
}
}
/> />
<div <div
className="explore-container" className="explore-container"