grafana/public/app/features/explore/TraceView/components/ScrollManager.tsx
Ashley Harrison 53186c14a4
Chore: Improve some types (#64675)
* some type fixes

* few more

* more type fixes

* fix the majority of (window as any) calls

* don't make new variable for event

* few more

* MOAR
2023-03-14 09:51:44 +00:00

273 lines
9.3 KiB
TypeScript

// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { TNil } from './types';
import { TraceSpan, TraceSpanReference, Trace } from './types/trace';
/**
* `Accessors` is necessary because `ScrollManager` needs to be created by
* `TracePage` so it can be passed into the keyboard shortcut manager. But,
* `ScrollManager` needs to know about the state of `ListView` and `Positions`,
* which are very low-level. And, storing their state info in redux or
* `TracePage#state` would be inefficient because the state info only rarely
* needs to be accessed (when a keyboard shortcut is triggered). `Accessors`
* allows that state info to be accessed in a loosely coupled fashion on an
* as-needed basis.
*/
export type Accessors = {
getViewRange: () => [number, number];
getSearchedSpanIDs: () => Set<string> | TNil;
getCollapsedChildren: () => Set<string> | TNil;
getViewHeight: () => number;
getBottomRowIndexVisible: () => number;
getTopRowIndexVisible: () => number;
getRowPosition: (rowIndex: number) => { height: number; y: number };
mapRowIndexToSpanIndex: (rowIndex: number) => number;
mapSpanIndexToRowIndex: (spanIndex: number) => number;
};
interface Scroller {
scrollTo: (rowIndex: number) => void;
// TODO arg names throughout
scrollBy: (rowIndex: number, opt?: boolean) => void;
}
/**
* Returns `{ isHidden: true, ... }` if one of the parents of `span` is
* collapsed, e.g. has children hidden.
*
* @param {TraceSpan} span The Span to check for.
* @param {Set<string>} childrenAreHidden The set of Spans known to have hidden
* children, either because it is
* collapsed or has a collapsed parent.
* @param {Map<string, TraceSpan | TNil} spansMap Mapping from spanID to Span.
* @returns {{ isHidden: boolean, parentIds: Set<string> }}
*/
function isSpanHidden(span: TraceSpan, childrenAreHidden: Set<string>, spansMap: Map<string, TraceSpan | TNil>) {
const parentIDs = new Set<string>();
let { references }: { references: TraceSpanReference[] | TNil } = span;
let parentID: undefined | string;
const checkRef = (ref: TraceSpanReference) => {
if (ref.refType === 'CHILD_OF' || ref.refType === 'FOLLOWS_FROM') {
parentID = ref.spanID;
parentIDs.add(parentID);
return childrenAreHidden.has(parentID);
}
return false;
};
while (Array.isArray(references) && references.length) {
const isHidden = references.some(checkRef);
if (isHidden) {
return { isHidden, parentIDs };
}
if (!parentID) {
break;
}
const parent = spansMap.get(parentID);
parentID = undefined;
references = parent && parent.references;
}
return { parentIDs, isHidden: false };
}
/**
* ScrollManager is intended for scrolling the TracePage. Has two modes, paging
* and scrolling to the previous or next visible span.
*/
export default class ScrollManager {
_trace: Trace | TNil;
_scroller: Scroller | TNil;
_accessors: Accessors | TNil;
constructor(trace: Trace | TNil, scroller: Scroller) {
this._trace = trace;
this._scroller = scroller;
this._accessors = undefined;
}
_scrollPast(rowIndex: number, direction: 1 | -1) {
const xrs = this._accessors;
/* istanbul ignore next */
if (!xrs) {
throw new Error('Accessors not set');
}
const isUp = direction < 0;
const position = xrs.getRowPosition(rowIndex);
if (!position) {
// eslint-disable-next-line no-console
console.warn('Invalid row index');
return;
}
let { y } = position;
const vh = xrs.getViewHeight();
if (!isUp) {
y += position.height;
// scrollTop is based on the top of the window
y -= vh;
}
y += direction * 0.5 * vh;
this._scroller?.scrollTo(y);
}
_scrollToVisibleSpan(direction: 1 | -1, startRow?: number) {
const xrs = this._accessors;
/* istanbul ignore next */
if (!xrs) {
throw new Error('Accessors not set');
}
if (!this._trace) {
return;
}
const { duration, spans, startTime: traceStartTime } = this._trace;
const isUp = direction < 0;
let boundaryRow: number;
if (startRow != null) {
boundaryRow = startRow;
} else if (isUp) {
boundaryRow = xrs.getTopRowIndexVisible();
} else {
boundaryRow = xrs.getBottomRowIndexVisible();
}
const spanIndex = xrs.mapRowIndexToSpanIndex(boundaryRow);
if ((spanIndex === 0 && isUp) || (spanIndex === spans.length - 1 && !isUp)) {
return;
}
// fullViewSpanIndex is one row inside the view window unless already at the top or bottom
let fullViewSpanIndex = spanIndex;
if (spanIndex !== 0 && spanIndex !== spans.length - 1) {
fullViewSpanIndex -= direction;
}
const [viewStart, viewEnd] = xrs.getViewRange();
const checkVisibility = viewStart !== 0 || viewEnd !== 1;
// use NaN as fallback to make flow happy
const startTime = checkVisibility ? traceStartTime + duration * viewStart : NaN;
const endTime = checkVisibility ? traceStartTime + duration * viewEnd : NaN;
const findMatches = xrs.getSearchedSpanIDs();
const _collapsed = xrs.getCollapsedChildren();
const childrenAreHidden = _collapsed ? new Set(_collapsed) : null;
// use empty Map as fallback to make flow happy
const spansMap: Map<string, TraceSpan> = childrenAreHidden ? new Map(spans.map((s) => [s.spanID, s])) : new Map();
const boundary = direction < 0 ? -1 : spans.length;
let nextSpanIndex: number | undefined;
for (let i = fullViewSpanIndex + direction; i !== boundary; i += direction) {
const span = spans[i];
const { duration: spanDuration, spanID, startTime: spanStartTime } = span;
const spanEndTime = spanStartTime + spanDuration;
if (checkVisibility && (spanStartTime > endTime || spanEndTime < startTime)) {
// span is not visible within the view range
continue;
}
if (findMatches && !findMatches.has(spanID)) {
// skip to search matches (when searching)
continue;
}
if (childrenAreHidden) {
// make sure the span is not collapsed
const { isHidden, parentIDs } = isSpanHidden(span, childrenAreHidden, spansMap);
if (isHidden) {
parentIDs.forEach((id) => childrenAreHidden.add(id));
continue;
}
}
nextSpanIndex = i;
break;
}
if (!nextSpanIndex || nextSpanIndex === boundary) {
// might as well scroll to the top or bottom
nextSpanIndex = boundary - direction;
// If there are hidden children, scroll to the last visible span
if (childrenAreHidden) {
let isFallbackHidden: boolean;
do {
const { isHidden, parentIDs } = isSpanHidden(spans[nextSpanIndex], childrenAreHidden, spansMap);
if (isHidden) {
parentIDs.forEach((id) => childrenAreHidden.add(id));
nextSpanIndex--;
}
isFallbackHidden = isHidden;
} while (isFallbackHidden);
}
}
const nextRow = xrs.mapSpanIndexToRowIndex(nextSpanIndex);
this._scrollPast(nextRow, direction);
}
/**
* Sometimes the ScrollManager is created before the trace is loaded. This
* setter allows the trace to be set asynchronously.
*/
setTrace(trace: Trace | TNil) {
this._trace = trace;
}
/**
* `setAccessors` is bound in the ctor, so it can be passed as a prop to
* children components.
*/
setAccessors = (accessors: Accessors) => {
this._accessors = accessors;
};
/**
* Scrolls around one page down (0.95x). It is bounds in the ctor, so it can
* be used as a keyboard shortcut handler.
*/
scrollPageDown = () => {
if (!this._scroller || !this._accessors) {
return;
}
this._scroller.scrollBy(0.95 * this._accessors.getViewHeight(), true);
};
/**
* Scrolls around one page up (0.95x). It is bounds in the ctor, so it can
* be used as a keyboard shortcut handler.
*/
scrollPageUp = () => {
if (!this._scroller || !this._accessors) {
return;
}
this._scroller.scrollBy(-0.95 * this._accessors.getViewHeight(), true);
};
/**
* Scrolls to the next visible span, ignoring spans that do not match the
* text filter, if there is one. It is bounds in the ctor, so it can
* be used as a keyboard shortcut handler.
*/
scrollToNextVisibleSpan = () => {
this._scrollToVisibleSpan(1);
};
/**
* Scrolls to the previous visible span, ignoring spans that do not match the
* text filter, if there is one. It is bounds in the ctor, so it can
* be used as a keyboard shortcut handler.
*/
scrollToPrevVisibleSpan = () => {
this._scrollToVisibleSpan(-1);
};
scrollToFirstVisibleSpan = () => {
this._scrollToVisibleSpan(1, 0);
};
destroy() {
this._trace = undefined;
this._scroller = undefined;
this._accessors = undefined;
}
}