mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* 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
273 lines
9.3 KiB
TypeScript
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;
|
|
}
|
|
}
|