Skip to content

Commit

Permalink
Merge pull request jaegertracing#93 from uber/issue-23-trace-minimap-…
Browse files Browse the repository at this point in the history
…navigate-zoom

Keyboard shortcuts and minimap UX
  • Loading branch information
tiffon committed Oct 20, 2017
2 parents 61cd543 + 2f22b02 commit b922904
Show file tree
Hide file tree
Showing 66 changed files with 4,948 additions and 1,078 deletions.
4 changes: 2 additions & 2 deletions README.md
@@ -1,6 +1,6 @@
[![ReadTheDocs][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov]

# Jaeger UI
# Jaeger UI

Visualize distributed tracing with Jaeger.

Expand Down Expand Up @@ -28,7 +28,7 @@ Install dependencies via `npm` or `yarn`:
```
npm install
# or
yarn install
yarn
```

Make sure you have the Jaeger Query service running on http://localhost:16686.
Expand Down
6 changes: 4 additions & 2 deletions package.json
Expand Up @@ -29,6 +29,7 @@
"basscss": "^8.0.3",
"chance": "^1.0.10",
"classnames": "^2.2.5",
"combokeys": "^3.0.0",
"cytoscape": "^3.2.1",
"cytoscape-dagre": "^2.0.0",
"d3-scale": "^1.0.6",
Expand Down Expand Up @@ -65,7 +66,8 @@
"reselect": "^3.0.1",
"semantic-ui-css": "^2.2.12",
"semantic-ui-react": "^0.71.4",
"store": "^2.0.12"
"store": "^2.0.12",
"tween-functions": "^1.2.0"
},
"scripts": {
"start": "react-scripts start",
Expand All @@ -83,7 +85,7 @@
"clear-homepage": "json -I -f package.json -e 'delete this.homepage'",
"deploy-docs": "./bin/deploy-docs.sh",
"postpublish": "npm run build:docs && npm run deploy-docs",
"add-license": "uber-licence",
"add-license": "cd src && uber-licence",
"precommit": "lint-staged"
},
"lint-staged": {
Expand Down
5 changes: 2 additions & 3 deletions src/components/SearchTracePage/SearchDropdownInput.js
Expand Up @@ -37,7 +37,6 @@ export default class SearchDropdownInput extends Component {
this.state = {
currentItems: props.items.slice(0, props.maxResults),
};
this.onSearch = this.onSearch.bind(this);
}
componentWillReceiveProps(nextProps) {
if (this.props.items.map(i => i.text).join(',') !== nextProps.items.map(i => i.text).join(',')) {
Expand All @@ -46,12 +45,12 @@ export default class SearchDropdownInput extends Component {
});
}
}
onSearch(_, searchText) {
onSearch = (_, searchText) => {
const { items, maxResults } = this.props;
const regexStr = regexpEscape(searchText);
const regex = new RegExp(regexStr, 'i');
return items.filter(v => regex.test(v.text)).slice(0, maxResults);
}
};
render() {
const { input: { value, onChange } } = this.props;
const { currentItems } = this.state;
Expand Down
251 changes: 251 additions & 0 deletions src/components/TracePage/ScrollManager.js
@@ -0,0 +1,251 @@
// @flow

// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import type { Span, Trace } from '../../types';

/**
* `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>,
getCollapsedChildren: () => ?Set<string>,
getViewHeight: () => number,
getBottomRowIndexVisible: () => number,
getTopRowIndexVisible: () => number,
getRowPosition: number => { height: number, y: number },
mapRowIndexToSpanIndex: number => number,
mapSpanIndexToRowIndex: number => number,
};

interface Scroller {
scrollTo: number => void,
scrollBy: (number, ?boolean) => void,
}

/**
* Returns `{ isHidden: true, ... }` if one of the parents of `span` is
* collapsed, e.g. has children hidden.
*
* @param {Span} 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, ?Span} spansMap Mapping from spanID to Span.
* @returns {{ isHidden: boolean, parentIds: Set<string> }}
*/
function isSpanHidden(span: Span, childrenAreHidden: Set<string>, spansMap: Map<string, ?Span>) {
const parentIDs = new Set();
let { references } = span;
let parentID: ?string;
const checkRef = ref => {
if (ref.refType === 'CHILD_OF') {
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;
_scroller: Scroller;
_accessors: ?Accessors;

constructor(trace: ?Trace, scroller: Scroller) {
this._trace = trace;
this._scroller = scroller;
this._accessors = undefined;
}

_scrollPast(rowIndex: number, direction: 1 | -1) {
const xrs = this._accessors;
if (!xrs) {
throw new Error('Accessors not set');
}
const isUp = direction < 0;
const position = xrs.getRowPosition(rowIndex);
if (!position) {
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) {
const xrs = this._accessors;
if (!xrs) {
throw new Error('Accessors not set');
}
if (!this._trace) {
return;
}
const { duration, spans, startTime: traceStartTime } = this._trace;
const isUp = direction < 0;
const boundaryRow = isUp ? xrs.getTopRowIndexVisible() : 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 = childrenAreHidden ? new Map(spans.map(s => [s.spanID, s])) : new Map();
const boundary = direction < 0 ? -1 : spans.length;
let nextSpanIndex: number;
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) {
childrenAreHidden.add(...parentIDs);
continue;
}
}
nextSpanIndex = i;
break;
}
if (!nextSpanIndex || nextSpanIndex === boundary) {
// might as well scroll to the top or bottom
nextSpanIndex = boundary - direction;
}
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) {
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);
};

destroy() {
this._trace = undefined;
this._scroller = (undefined: any);
this._accessors = undefined;
}
}

0 comments on commit b922904

Please sign in to comment.