mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* Make log minimap draggable, tied to jumpToTarget * Link dragging minimap to scrolling w/ accuracy/performance issues * WIP * Revert back to drag-and-jump behavior * Reduce UI jankiness on jumpToTarget * Fix prettier * WIP * Add overscan to time ruler and density chart * Fix typechecking and add comment * Update typecheck * Begin implementing minimap redesign * Add target marker * Refine drag target area * Fix jumping minimap on reload * Fix unused declarations * Remove unused NegativeAreaMap * Change move cursor to grab/grabbing * Change overscan boundaries to dark gray instead of diagonal lines
This commit is contained in:
parent
82054a280d
commit
cce2274781
11 changed files with 305 additions and 78 deletions
|
@ -38,7 +38,7 @@ export const DensityChart: React.SFC<DensityChartProps> = ({
|
|||
const xMax = max(buckets.map(bucket => bucket.entriesCount)) || 0;
|
||||
const xScale = scaleLinear()
|
||||
.domain([0, xMax])
|
||||
.range([0, width / 2]);
|
||||
.range([0, width * (2 / 3)]);
|
||||
|
||||
const path = area<SummaryBucket>()
|
||||
.x0(xScale(0))
|
||||
|
@ -47,24 +47,37 @@ export const DensityChart: React.SFC<DensityChartProps> = ({
|
|||
.curve(curveMonotoneY);
|
||||
const pathData = path(buckets);
|
||||
|
||||
const highestPathCoord = String(pathData)
|
||||
.replace(/[^.0-9,]/g, ' ')
|
||||
.split(/[ ,]/)
|
||||
.reduce((result, num) => (Number(num) > result ? Number(num) : result), 0);
|
||||
return (
|
||||
<g transform={`translate(${width / 2}, 0)`}>
|
||||
<g transform={`translate(${width / 3}, 0)`}>
|
||||
<DensityChartNegativeBackground
|
||||
transform={`translate(${-width / 3}, 0)`}
|
||||
width={width / 2}
|
||||
height={highestPathCoord}
|
||||
/>
|
||||
<DensityChartPositiveBackground width={width * (2 / 3)} height={highestPathCoord} />
|
||||
<PositiveAreaPath d={pathData || ''} />
|
||||
<NegativeAreaPath transform="scale(-1, 1)" d={pathData || ''} />
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const DensityChartNegativeBackground = euiStyled.rect`
|
||||
fill: white;
|
||||
`;
|
||||
|
||||
const DensityChartPositiveBackground = euiStyled.rect`
|
||||
fill: ${props =>
|
||||
props.theme.darkMode
|
||||
? props.theme.eui.euiColorLightShade
|
||||
: props.theme.eui.euiColorLightestShade};
|
||||
`;
|
||||
|
||||
const PositiveAreaPath = euiStyled.path`
|
||||
fill: ${props =>
|
||||
props.theme.darkMode
|
||||
? props.theme.eui.euiColorMediumShade
|
||||
: props.theme.eui.euiColorLightShade};
|
||||
`;
|
||||
|
||||
const NegativeAreaPath = euiStyled.path`
|
||||
fill: ${props =>
|
||||
props.theme.darkMode
|
||||
? props.theme.eui.euiColorLightShade
|
||||
: props.theme.eui.euiColorLightestShade};
|
||||
`;
|
||||
|
|
|
@ -14,6 +14,7 @@ interface HighlightedIntervalProps {
|
|||
start: number;
|
||||
end: number;
|
||||
width: number;
|
||||
target: number | null;
|
||||
}
|
||||
|
||||
export const HighlightedInterval: React.SFC<HighlightedIntervalProps> = ({
|
||||
|
@ -22,20 +23,38 @@ export const HighlightedInterval: React.SFC<HighlightedIntervalProps> = ({
|
|||
getPositionOfTime,
|
||||
start,
|
||||
width,
|
||||
target,
|
||||
}) => {
|
||||
const yStart = getPositionOfTime(start);
|
||||
const yEnd = getPositionOfTime(end);
|
||||
const yTarget = target && getPositionOfTime(target);
|
||||
|
||||
return (
|
||||
<HighlightPolygon
|
||||
className={className}
|
||||
points={`0,${yStart} ${width},${yStart} ${width},${yEnd} 0,${yEnd}`}
|
||||
/>
|
||||
<>
|
||||
{yTarget && (
|
||||
<HighlightTargetMarker
|
||||
className={className}
|
||||
x1={0}
|
||||
x2={width / 3}
|
||||
y1={yTarget}
|
||||
y2={yTarget}
|
||||
/>
|
||||
)}
|
||||
<HighlightPolygon
|
||||
className={className}
|
||||
points={` ${width / 3},${yStart} ${width},${yStart} ${width},${yEnd} ${width / 3},${yEnd}`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
HighlightedInterval.displayName = 'HighlightedInterval';
|
||||
|
||||
const HighlightTargetMarker = euiStyled.line`
|
||||
stroke: ${props => props.theme.eui.euiColorPrimary};
|
||||
stroke-width: 1;
|
||||
`;
|
||||
|
||||
const HighlightPolygon = euiStyled.polygon`
|
||||
fill: ${props => props.theme.eui.euiColorPrimary};
|
||||
fill-opacity: 0.3;
|
||||
|
|
|
@ -15,13 +15,20 @@ import { SearchMarkers } from './search_markers';
|
|||
import { TimeRuler } from './time_ruler';
|
||||
import { SummaryBucket, SummaryHighlightBucket } from './types';
|
||||
|
||||
interface Interval {
|
||||
end: number;
|
||||
start: number;
|
||||
}
|
||||
|
||||
interface DragRecord {
|
||||
startY: number;
|
||||
currentY: number | null;
|
||||
}
|
||||
|
||||
interface LogMinimapProps {
|
||||
className?: string;
|
||||
height: number;
|
||||
highlightedInterval: {
|
||||
end: number;
|
||||
start: number;
|
||||
} | null;
|
||||
highlightedInterval: Interval | null;
|
||||
jumpToTarget: (params: LogEntryTime) => any;
|
||||
intervalSize: number;
|
||||
summaryBuckets: SummaryBucket[];
|
||||
|
@ -31,33 +38,114 @@ interface LogMinimapProps {
|
|||
}
|
||||
|
||||
interface LogMinimapState {
|
||||
target: number | null;
|
||||
drag: DragRecord | null;
|
||||
svgPosition: ClientRect;
|
||||
timeCursorY: number;
|
||||
}
|
||||
|
||||
export class LogMinimap extends React.Component<LogMinimapProps, LogMinimapState> {
|
||||
public readonly state = {
|
||||
timeCursorY: 0,
|
||||
};
|
||||
function calculateYScale(target: number | null, height: number, intervalSize: number) {
|
||||
const domainStart = target ? target - intervalSize / 2 : 0;
|
||||
const domainEnd = target ? target + intervalSize / 2 : 0;
|
||||
return scaleLinear()
|
||||
.domain([domainStart, domainEnd])
|
||||
.range([0, height]);
|
||||
}
|
||||
|
||||
public handleClick: React.MouseEventHandler<SVGSVGElement> = event => {
|
||||
const svgPosition = event.currentTarget.getBoundingClientRect();
|
||||
export class LogMinimap extends React.Component<LogMinimapProps, LogMinimapState> {
|
||||
constructor(props: LogMinimapProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
timeCursorY: 0,
|
||||
target: props.target,
|
||||
drag: null,
|
||||
svgPosition: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private dragTargetArea: SVGElement | null = null;
|
||||
|
||||
public static getDerivedStateFromProps({ target }: LogMinimapProps, { drag }: LogMinimapState) {
|
||||
if (!drag) {
|
||||
return { target };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public handleClick = (event: MouseEvent) => {
|
||||
const { svgPosition } = this.state;
|
||||
const clickedYPosition = event.clientY - svgPosition.top;
|
||||
const clickedTime = Math.floor(this.getYScale().invert(clickedYPosition));
|
||||
|
||||
this.setState({
|
||||
drag: null,
|
||||
});
|
||||
this.props.jumpToTarget({
|
||||
tiebreaker: 0,
|
||||
time: clickedTime,
|
||||
});
|
||||
};
|
||||
|
||||
public getYScale = () => {
|
||||
const { height, intervalSize, target } = this.props;
|
||||
private handleMouseDown: React.MouseEventHandler<SVGSVGElement> = event => {
|
||||
const { clientY, target } = event;
|
||||
if (target === this.dragTargetArea) {
|
||||
const svgPosition = event.currentTarget.getBoundingClientRect();
|
||||
this.setState({
|
||||
drag: {
|
||||
startY: clientY,
|
||||
currentY: null,
|
||||
},
|
||||
svgPosition,
|
||||
});
|
||||
window.addEventListener('mousemove', this.handleDragMove);
|
||||
}
|
||||
window.addEventListener('mouseup', this.handleMouseUp);
|
||||
};
|
||||
|
||||
const domainStart = target ? target - intervalSize / 2 : 0;
|
||||
const domainEnd = target ? target + intervalSize / 2 : 0;
|
||||
return scaleLinear()
|
||||
.domain([domainStart, domainEnd])
|
||||
.range([0, height]);
|
||||
private handleMouseUp = (event: MouseEvent) => {
|
||||
window.removeEventListener('mousemove', this.handleDragMove);
|
||||
window.removeEventListener('mouseup', this.handleMouseUp);
|
||||
|
||||
const { drag, svgPosition } = this.state;
|
||||
if (!drag || !drag.currentY) {
|
||||
this.handleClick(event);
|
||||
return;
|
||||
}
|
||||
const getTime = (pos: number) => Math.floor(this.getYScale().invert(pos));
|
||||
const startYPosition = drag.startY - svgPosition.top;
|
||||
const endYPosition = event.clientY - svgPosition.top;
|
||||
const startTime = getTime(startYPosition);
|
||||
const endTime = getTime(endYPosition);
|
||||
const timeDifference = endTime - startTime;
|
||||
const newTime = (this.props.target || 0) - timeDifference;
|
||||
this.setState({ drag: null, target: newTime });
|
||||
this.props.jumpToTarget({
|
||||
tiebreaker: 0,
|
||||
time: newTime,
|
||||
});
|
||||
};
|
||||
|
||||
private handleDragMove = (event: MouseEvent) => {
|
||||
const { drag } = this.state;
|
||||
if (!drag) return;
|
||||
this.setState({
|
||||
drag: {
|
||||
...drag,
|
||||
currentY: event.clientY,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
public getYScale = () => {
|
||||
const { target } = this.state;
|
||||
const { height, intervalSize } = this.props;
|
||||
return calculateYScale(target, height, intervalSize);
|
||||
};
|
||||
|
||||
public getPositionOfTime = (time: number) => {
|
||||
|
@ -65,7 +153,7 @@ export class LogMinimap extends React.Component<LogMinimapProps, LogMinimapState
|
|||
|
||||
const [minTime] = this.getYScale().domain();
|
||||
|
||||
return ((time - minTime) * height) / intervalSize;
|
||||
return ((time - minTime) * height) / intervalSize; //
|
||||
};
|
||||
|
||||
private updateTimeCursor: React.MouseEventHandler<SVGSVGElement> = event => {
|
||||
|
@ -84,12 +172,20 @@ export class LogMinimap extends React.Component<LogMinimapProps, LogMinimapState
|
|||
summaryBuckets,
|
||||
summaryHighlightBuckets,
|
||||
width,
|
||||
intervalSize,
|
||||
} = this.props;
|
||||
|
||||
const { timeCursorY } = this.state;
|
||||
|
||||
const [minTime, maxTime] = this.getYScale().domain();
|
||||
|
||||
const { timeCursorY, drag, target } = this.state;
|
||||
// Render the time ruler and density map beyond the visible range of time, so that
|
||||
// the user doesn't run out of ruler when they click and drag
|
||||
const overscanHeight = Math.round(window.screen.availHeight * 2.5) || height * 3;
|
||||
const [minTime, maxTime] = calculateYScale(
|
||||
target,
|
||||
overscanHeight,
|
||||
intervalSize * (overscanHeight / height)
|
||||
).domain();
|
||||
const tickCount = height ? Math.round((overscanHeight / height) * 144) : 12;
|
||||
const overscanTranslate = height ? -(overscanHeight - height) / 2 : 0;
|
||||
const dragTransform = !drag || !drag.currentY ? 0 : drag.currentY - drag.startY;
|
||||
return (
|
||||
<MinimapWrapper
|
||||
className={className}
|
||||
|
@ -97,25 +193,35 @@ export class LogMinimap extends React.Component<LogMinimapProps, LogMinimapState
|
|||
preserveAspectRatio="none"
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
width={width}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onMouseMove={this.updateTimeCursor}
|
||||
showOverscanBoundaries={Boolean(height && summaryBuckets.length)}
|
||||
>
|
||||
<MinimapBackground x={width / 2} y="0" width={width / 2} height={height} />
|
||||
<DensityChart
|
||||
buckets={summaryBuckets}
|
||||
start={minTime}
|
||||
end={maxTime}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
<MinimapBorder x1={width / 2} y1={0} x2={width / 2} y2={height} />
|
||||
<TimeRuler start={minTime} end={maxTime} width={width} height={height} tickCount={12} />
|
||||
<g transform={`translate(0, ${dragTransform + overscanTranslate})`}>
|
||||
<DensityChart
|
||||
buckets={summaryBuckets}
|
||||
start={minTime}
|
||||
end={maxTime}
|
||||
width={width}
|
||||
height={overscanHeight}
|
||||
/>
|
||||
|
||||
<MinimapBorder x1={width / 3} y1={0} x2={width / 3} y2={overscanHeight} />
|
||||
<TimeRuler
|
||||
start={minTime}
|
||||
end={maxTime}
|
||||
width={width}
|
||||
height={overscanHeight}
|
||||
tickCount={tickCount}
|
||||
/>
|
||||
</g>
|
||||
{highlightedInterval ? (
|
||||
<HighlightedInterval
|
||||
end={highlightedInterval.end}
|
||||
getPositionOfTime={this.getPositionOfTime}
|
||||
start={highlightedInterval.start}
|
||||
width={width}
|
||||
target={target}
|
||||
/>
|
||||
) : null}
|
||||
<g transform={`translate(${width * 0.5}, 0)`}>
|
||||
|
@ -128,14 +234,25 @@ export class LogMinimap extends React.Component<LogMinimapProps, LogMinimapState
|
|||
jumpToTarget={jumpToTarget}
|
||||
/>
|
||||
</g>
|
||||
<TimeCursor x1={0} x2={width} y1={timeCursorY} y2={timeCursorY} />
|
||||
<TimeCursor x1={width / 3} x2={width} y1={timeCursorY} y2={timeCursorY} />
|
||||
<DragTargetArea
|
||||
isGrabbing={Boolean(drag)}
|
||||
innerRef={node => {
|
||||
this.dragTargetArea = node;
|
||||
}}
|
||||
x={0}
|
||||
y={0}
|
||||
width={width / 3}
|
||||
height={height}
|
||||
/>
|
||||
</MinimapWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const MinimapBackground = euiStyled.rect`
|
||||
fill: ${props => props.theme.eui.euiColorLightestShade};
|
||||
const DragTargetArea = euiStyled.rect<{ isGrabbing: boolean }>`
|
||||
fill: transparent;
|
||||
cursor: ${({ isGrabbing }) => (isGrabbing ? 'grabbing' : 'grab')};
|
||||
`;
|
||||
|
||||
const MinimapBorder = euiStyled.line`
|
||||
|
@ -152,7 +269,9 @@ const TimeCursor = euiStyled.line`
|
|||
: props.theme.eui.euiColorDarkShade};
|
||||
`;
|
||||
|
||||
const MinimapWrapper = euiStyled.svg`
|
||||
const MinimapWrapper = euiStyled.svg<{ showOverscanBoundaries: boolean }>`
|
||||
background: ${props =>
|
||||
props.showOverscanBoundaries ? props.theme.eui.euiColorMediumShade : 'transparent'};
|
||||
& ${TimeCursor} {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
|
|
@ -25,16 +25,35 @@ export const TimeRuler: React.SFC<TimeRulerProps> = ({ end, height, start, tickC
|
|||
const ticks = yScale.ticks(tickCount);
|
||||
const formatTick = yScale.tickFormat();
|
||||
|
||||
const dateModLabel = (() => {
|
||||
for (let i = 0; i < ticks.length; i++) {
|
||||
const tickLabel = formatTick(ticks[i]);
|
||||
if (!tickLabel[0].match(/[0-9]/)) {
|
||||
return i % 12;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<g>
|
||||
{ticks.map((tick, tickIndex) => {
|
||||
const y = yScale(tick);
|
||||
const isLabeledTick = tickIndex % 12 === dateModLabel;
|
||||
const tickStartX = isLabeledTick ? 0 : width / 3 - 4;
|
||||
return (
|
||||
<g key={`tick${tickIndex}`}>
|
||||
<TimeRulerTickLabel x={2} y={y - 4}>
|
||||
{formatTick(tick)}
|
||||
</TimeRulerTickLabel>
|
||||
<TimeRulerGridLine x1={0} y1={y} x2={width} y2={y} />
|
||||
{isLabeledTick && (
|
||||
<TimeRulerTickLabel x={0} y={y - 4}>
|
||||
{formatTick(tick)}
|
||||
</TimeRulerTickLabel>
|
||||
)}
|
||||
<TimeRulerGridLine
|
||||
isDark={isLabeledTick}
|
||||
x1={tickStartX}
|
||||
y1={y}
|
||||
x2={width / 3}
|
||||
y2={y}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
@ -45,16 +64,22 @@ export const TimeRuler: React.SFC<TimeRulerProps> = ({ end, height, start, tickC
|
|||
TimeRuler.displayName = 'TimeRuler';
|
||||
|
||||
const TimeRulerTickLabel = euiStyled.text`
|
||||
font-size: ${props => props.theme.eui.euiFontSizeXS};
|
||||
font-size: 9px;
|
||||
line-height: ${props => props.theme.eui.euiLineHeight};
|
||||
fill: ${props => props.theme.eui.textColors.subdued};
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const TimeRulerGridLine = euiStyled.line`
|
||||
const TimeRulerGridLine = euiStyled.line<{ isDark: boolean }>`
|
||||
stroke: ${props =>
|
||||
props.theme.darkMode ? props.theme.eui.euiColorDarkShade : props.theme.eui.euiColorMediumShade};
|
||||
stroke-dasharray: 2, 2;
|
||||
props.isDark
|
||||
? props.theme.darkMode
|
||||
? props.theme.eui.euiColorDarkestShade
|
||||
: props.theme.eui.euiColorDarkShade
|
||||
: props.theme.darkMode
|
||||
? props.theme.eui.euiColorDarkShade
|
||||
: props.theme.eui.euiColorMediumShade};
|
||||
stroke-opacity: 0.5;
|
||||
stroke-width: 1px;
|
||||
`;
|
||||
|
|
|
@ -44,6 +44,7 @@ interface ScrollableLogTextStreamViewProps {
|
|||
startKey: TimeKey | null;
|
||||
middleKey: TimeKey | null;
|
||||
endKey: TimeKey | null;
|
||||
fromScroll: boolean;
|
||||
}) => any;
|
||||
loadNewerItems: () => void;
|
||||
setFlyoutItem: (id: string) => void;
|
||||
|
@ -253,12 +254,14 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
|
|||
bottomChild,
|
||||
pagesAbove,
|
||||
pagesBelow,
|
||||
fromScroll,
|
||||
}: {
|
||||
topChild: string;
|
||||
middleChild: string;
|
||||
bottomChild: string;
|
||||
pagesAbove: number;
|
||||
pagesBelow: number;
|
||||
fromScroll: boolean;
|
||||
}) => {
|
||||
this.props.reportVisibleInterval({
|
||||
endKey: parseStreamItemId(bottomChild),
|
||||
|
@ -266,6 +269,7 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
|
|||
pagesAfterEnd: pagesBelow,
|
||||
pagesBeforeStart: pagesAbove,
|
||||
startKey: parseStreamItemId(topChild),
|
||||
fromScroll,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -22,6 +22,7 @@ interface VerticalScrollPanelProps<Child> {
|
|||
bottomChild: Child;
|
||||
pagesAbove: number;
|
||||
pagesBelow: number;
|
||||
fromScroll: boolean;
|
||||
}) => void;
|
||||
target: Child | undefined;
|
||||
height: number;
|
||||
|
@ -52,11 +53,17 @@ export class VerticalScrollPanel<Child> extends React.PureComponent<
|
|||
public scrollRef = React.createRef<HTMLDivElement>();
|
||||
public childRefs = new Map<Child, MeasurableChild>();
|
||||
public childDimensions = new Map<Child, Rect>();
|
||||
private nextScrollEventFromCenterTarget = false;
|
||||
|
||||
public handleScroll: React.UIEventHandler<HTMLDivElement> = throttle(
|
||||
SCROLL_THROTTLE_INTERVAL,
|
||||
() => {
|
||||
this.reportVisibleChildren();
|
||||
// If this event was fired by the centerTarget method modifying the scrollTop,
|
||||
// then don't send `fromScroll: true` to reportVisibleChildren. The rest of the
|
||||
// app needs to respond differently depending on whether the user is scrolling through
|
||||
// the pane manually, versus whether the pane is updating itself in response to new data
|
||||
this.reportVisibleChildren(!this.nextScrollEventFromCenterTarget);
|
||||
this.nextScrollEventFromCenterTarget = false;
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -121,7 +128,7 @@ export class VerticalScrollPanel<Child> extends React.PureComponent<
|
|||
};
|
||||
};
|
||||
|
||||
public reportVisibleChildren = () => {
|
||||
public reportVisibleChildren = (fromScroll: boolean = false) => {
|
||||
const { onVisibleChildrenChange } = this.props;
|
||||
const visibleChildren = this.getVisibleChildren();
|
||||
const scrollPosition = this.getScrollPosition();
|
||||
|
@ -134,6 +141,7 @@ export class VerticalScrollPanel<Child> extends React.PureComponent<
|
|||
bottomChild: visibleChildren.bottomChild,
|
||||
middleChild: visibleChildren.middleChild,
|
||||
topChild: visibleChildren.topChild,
|
||||
fromScroll,
|
||||
...scrollPosition,
|
||||
});
|
||||
};
|
||||
|
@ -153,6 +161,9 @@ export class VerticalScrollPanel<Child> extends React.PureComponent<
|
|||
|
||||
if (targetDimensions) {
|
||||
const targetOffset = typeof offset === 'undefined' ? targetDimensions.height / 2 : offset;
|
||||
// Flag the scrollTop change that's about to happen as programmatic, as
|
||||
// opposed to being in direct response to user input
|
||||
this.nextScrollEventFromCenterTarget = true;
|
||||
scrollRef.current.scrollTop = targetDimensions.top + targetOffset - scrollViewHeight / 2;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -121,19 +121,30 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
|
|||
<WithSummary>
|
||||
{({ buckets }) => (
|
||||
<WithLogPosition>
|
||||
{({ jumpToTargetPosition, visibleMidpointTime, visibleTimeInterval }) => (
|
||||
<LogMinimap
|
||||
height={height}
|
||||
width={width}
|
||||
highlightedInterval={visibleTimeInterval}
|
||||
intervalSize={intervalSize}
|
||||
jumpToTarget={jumpToTargetPosition}
|
||||
summaryBuckets={buckets}
|
||||
summaryHighlightBuckets={
|
||||
logSummaryHighlights.length > 0 ? logSummaryHighlights[0].buckets : []
|
||||
}
|
||||
target={visibleMidpointTime}
|
||||
/>
|
||||
{({
|
||||
isAutoReloading,
|
||||
jumpToTargetPosition,
|
||||
visibleMidpointTime,
|
||||
visibleTimeInterval,
|
||||
}) => (
|
||||
<WithStreamItems initializeOnMount={!isAutoReloading}>
|
||||
{({ isReloading }) => (
|
||||
<LogMinimap
|
||||
height={height}
|
||||
width={width}
|
||||
highlightedInterval={isReloading ? null : visibleTimeInterval}
|
||||
intervalSize={intervalSize}
|
||||
jumpToTarget={jumpToTargetPosition}
|
||||
summaryBuckets={buckets}
|
||||
summaryHighlightBuckets={
|
||||
logSummaryHighlights.length > 0
|
||||
? logSummaryHighlights[0].buckets
|
||||
: []
|
||||
}
|
||||
target={visibleMidpointTime}
|
||||
/>
|
||||
)}
|
||||
</WithStreamItems>
|
||||
)}
|
||||
</WithLogPosition>
|
||||
)}
|
||||
|
|
|
@ -126,6 +126,7 @@ export const LogsToolbar = injectI18n(({ intl }) => {
|
|||
jumpToTargetPositionTime,
|
||||
startLiveStreaming,
|
||||
stopLiveStreaming,
|
||||
targetPosition,
|
||||
}) => (
|
||||
<LogTimeControls
|
||||
currentTime={visibleMidpointTime}
|
||||
|
|
|
@ -24,6 +24,7 @@ export interface ReportVisiblePositionsPayload {
|
|||
endKey: TimeKey | null;
|
||||
middleKey: TimeKey | null;
|
||||
startKey: TimeKey | null;
|
||||
fromScroll: boolean;
|
||||
}
|
||||
|
||||
export const reportVisiblePositions = actionCreator<ReportVisiblePositionsPayload>(
|
||||
|
|
|
@ -36,6 +36,7 @@ export interface LogPositionState {
|
|||
middleKey: TimeKey | null;
|
||||
endKey: TimeKey | null;
|
||||
};
|
||||
controlsShouldDisplayTargetPosition: boolean;
|
||||
}
|
||||
|
||||
export const initialLogPositionState: LogPositionState = {
|
||||
|
@ -48,6 +49,7 @@ export const initialLogPositionState: LogPositionState = {
|
|||
middleKey: null,
|
||||
startKey: null,
|
||||
},
|
||||
controlsShouldDisplayTargetPosition: false,
|
||||
};
|
||||
|
||||
const targetPositionReducer = reducerWithInitialState(initialLogPositionState.targetPosition).case(
|
||||
|
@ -74,8 +76,23 @@ const visiblePositionReducer = reducerWithInitialState(
|
|||
startKey,
|
||||
}));
|
||||
|
||||
// Determines whether to use the target position or the visible midpoint when
|
||||
// displaying a timestamp or time range in the toolbar and log minimap. When the
|
||||
// user jumps to a new target, the final visible midpoint is indeterminate until
|
||||
// all the new data has finished loading, so using this flag reduces the perception
|
||||
// that the UI is jumping around inaccurately
|
||||
const controlsShouldDisplayTargetPositionReducer = reducerWithInitialState(
|
||||
initialLogPositionState.controlsShouldDisplayTargetPosition
|
||||
)
|
||||
.case(jumpToTargetPosition, () => true)
|
||||
.case(reportVisiblePositions, (state, { fromScroll }) => {
|
||||
if (fromScroll) return false;
|
||||
return state;
|
||||
});
|
||||
|
||||
export const logPositionReducer = combineReducers<LogPositionState>({
|
||||
targetPosition: targetPositionReducer,
|
||||
updatePolicy: targetPositionUpdatePolicyReducer,
|
||||
visiblePositions: visiblePositionReducer,
|
||||
controlsShouldDisplayTargetPosition: controlsShouldDisplayTargetPositionReducer,
|
||||
});
|
||||
|
|
|
@ -22,11 +22,17 @@ export const selectMiddleVisiblePosition = (state: LogPositionState) =>
|
|||
export const selectLastVisiblePosition = (state: LogPositionState) =>
|
||||
state.visiblePositions.endKey ? state.visiblePositions.endKey : null;
|
||||
|
||||
export const selectControlsShouldDisplayTargetPosition = (state: LogPositionState) =>
|
||||
state.controlsShouldDisplayTargetPosition;
|
||||
|
||||
export const selectVisibleMidpointOrTarget = createSelector(
|
||||
selectMiddleVisiblePosition,
|
||||
selectTargetPosition,
|
||||
(middleVisiblePosition, targetPosition) => {
|
||||
if (middleVisiblePosition) {
|
||||
selectControlsShouldDisplayTargetPosition,
|
||||
(middleVisiblePosition, targetPosition, displayTargetPosition) => {
|
||||
if (displayTargetPosition) {
|
||||
return targetPosition;
|
||||
} else if (middleVisiblePosition) {
|
||||
return middleVisiblePosition;
|
||||
} else if (targetPosition) {
|
||||
return targetPosition;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue