diff --git a/CHANGELOG.md b/CHANGELOG.md index b11ae5473f..47cdc2e541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes merged into master +### [#221](https://github.com/jaegertracing/jaeger-ui/pull/221) Timeline Expand and Collapse Features + +* Partially addresses [#160](https://github.com/jaegertracing/jaeger-ui/issues/160) - Heuristics for collapsing spans + ### [#191](https://github.com/jaegertracing/jaeger-ui/pull/191) Add GA event tracking for actions in trace view * Partially addresses [#157](https://github.com/jaegertracing/jaeger-ui/issues/157) - Enhanced Google Analytics integration diff --git a/packages/jaeger-ui/src/components/TracePage/KeyboardShortcutsHelp.js b/packages/jaeger-ui/src/components/TracePage/KeyboardShortcutsHelp.js index f2037ecf61..30328eef43 100644 --- a/packages/jaeger-ui/src/components/TracePage/KeyboardShortcutsHelp.js +++ b/packages/jaeger-ui/src/components/TracePage/KeyboardShortcutsHelp.js @@ -49,6 +49,10 @@ const descriptions = { zoomInFast: 'Zoom in — Large', zoomOut: 'Zoom out', zoomOutFast: 'Zoom out — Large', + collapseAll: 'Collapse All', + expandAll: 'Expand All', + collapseOne: 'Collapse One Level', + expandOne: 'Expand One Level', }; function convertKeys(keyConfig: string | string[]): string[][] { diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.css new file mode 100644 index 0000000000..6be2e0d87a --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.css @@ -0,0 +1,30 @@ +/* +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. +*/ + +.TimelineCollapser { + float: right; + margin: 0 0.8rem 0 0; + display: inline-block; +} + +.TimelineCollapser--btn, +.TimelineCollapser--btn-expand { + margin-right: 0.3rem; +} + +.TimelineCollapser--btn-expand { + transform: rotate(90deg); +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.js new file mode 100644 index 0000000000..2e905d72d5 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.js @@ -0,0 +1,48 @@ +// @flow + +// 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 React from 'react'; + +import { Tooltip, Icon } from 'antd'; + +import './TimelineCollapser.css'; + +type CollapserProps = { + onCollapseAll: () => void, + onCollapseOne: () => void, + onExpandOne: () => void, + onExpandAll: () => void, +}; + +export default function TimelineCollapser(props: CollapserProps) { + const { onExpandAll, onExpandOne, onCollapseAll, onCollapseOne } = props; + return ( + + + + + + + + + + + + + + + ); +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.test.js new file mode 100644 index 0000000000..56b8bff95a --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineCollapser.test.js @@ -0,0 +1,32 @@ +// 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 React from 'react'; +import { shallow } from 'enzyme'; + +import TimelineCollapser from './TimelineCollapser'; + +describe('', () => { + it('renders without exploding', () => { + const props = { + onCollapseAll: () => {}, + onCollapseOne: () => {}, + onExpandAll: () => {}, + onExpandOne: () => {}, + }; + const wrapper = shallow(); + expect(wrapper).toBeDefined(); + expect(wrapper.find('.TimelineCollapser').length).toBe(1); + }); +}); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.css index fe615e0762..45eb9c5c2d 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.css @@ -26,8 +26,9 @@ limitations under the License. } .TimelineHeaderRow--title { + display: inline-block; overflow: hidden; - margin: 0 0.75rem 0 0.5rem; + margin: 0 0 0 0.5rem; text-overflow: ellipsis; white-space: nowrap; } diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.js index ba3fc2838a..9c0a1e922a 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.js @@ -16,6 +16,7 @@ import * as React from 'react'; +import TimelineCollapser from './TimelineCollapser'; import TimelineColumnResizer from './TimelineColumnResizer'; import TimelineViewingLayer from './TimelineViewingLayer'; import Ticks from '../Ticks'; @@ -28,7 +29,11 @@ type TimelineHeaderRowProps = { duration: number, nameColumnWidth: number, numTicks: number, + onCollapseAll: () => void, + onCollapseOne: () => void, onColummWidthChange: number => void, + onExpandAll: () => void, + onExpandOne: () => void, updateNextViewRangeTime: ViewRangeTimeUpdate => void, updateViewRangeTime: (number, number, ?string) => void, viewRangeTime: ViewRangeTime, @@ -39,7 +44,11 @@ export default function TimelineHeaderRow(props: TimelineHeaderRowProps) { duration, nameColumnWidth, numTicks, + onCollapseAll, + onCollapseOne, onColummWidthChange, + onExpandAll, + onExpandOne, updateViewRangeTime, updateNextViewRangeTime, viewRangeTime, @@ -49,6 +58,12 @@ export default function TimelineHeaderRow(props: TimelineHeaderRowProps) {

Service & Operation

+
', () => { let wrapper; @@ -28,7 +29,11 @@ describe('', () => { nameColumnWidth, duration: 1234, numTicks: 5, + onCollapseAll: () => {}, + onCollapseOne: () => {}, onColummWidthChange: () => {}, + onExpandAll: () => {}, + onExpandOne: () => {}, updateNextViewRangeTime: () => {}, updateViewRangeTime: () => {}, viewRangeTime: { @@ -92,4 +97,16 @@ describe('', () => { ); expect(wrapper.containsMatchingElement(elm)).toBe(true); }); + + it('renders the TimelineCollapser', () => { + const elm = ( + + ); + expect(wrapper.containsMatchingElement(elm)).toBe(true); + }); }); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.js index ed8cda9300..d4e1db9b49 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.js @@ -49,6 +49,10 @@ export const actionTypes = generateActionTypes('@jaeger-ui/trace-timeline-viewer 'SET_TRACE', 'SET_SPAN_NAME_COLUMN_WIDTH', 'CHILDREN_TOGGLE', + 'EXPAND_ALL', + 'COLLAPSE_ALL', + 'EXPAND_ONE', + 'COLLAPSE_ONE', 'DETAIL_TOGGLE', 'DETAIL_TAGS_TOGGLE', 'DETAIL_PROCESS_TOGGLE', @@ -61,6 +65,10 @@ const fullActions = createActions({ [actionTypes.SET_TRACE]: traceID => ({ traceID }), [actionTypes.SET_SPAN_NAME_COLUMN_WIDTH]: width => ({ width }), [actionTypes.CHILDREN_TOGGLE]: spanID => ({ spanID }), + [actionTypes.EXPAND_ALL]: () => ({}), + [actionTypes.EXPAND_ONE]: spans => ({ spans }), + [actionTypes.COLLAPSE_ALL]: spans => ({ spans }), + [actionTypes.COLLAPSE_ONE]: spans => ({ spans }), [actionTypes.DETAIL_TOGGLE]: spanID => ({ spanID }), [actionTypes.DETAIL_TAGS_TOGGLE]: spanID => ({ spanID }), [actionTypes.DETAIL_PROCESS_TOGGLE]: spanID => ({ spanID }), @@ -97,6 +105,70 @@ function childrenToggle(state, { payload }) { return { ...state, childrenHiddenIDs }; } +function shouldDisableCollapse(allSpans, hiddenSpansIds) { + const allParentSpans = allSpans.filter(s => s.hasChildren); + return allParentSpans.length === hiddenSpansIds.size; +} + +export function expandAll(state) { + const childrenHiddenIDs = new Set(); + return { ...state, childrenHiddenIDs }; +} + +export function collapseAll(state, { payload }) { + const { spans } = payload; + if (shouldDisableCollapse(spans, state.childrenHiddenIDs)) { + return state; + } + const childrenHiddenIDs = spans.reduce((res, s) => { + if (s.hasChildren) { + res.add(s.spanID); + } + return res; + }, new Set()); + return { ...state, childrenHiddenIDs }; +} + +export function collapseOne(state, { payload }) { + const { spans } = payload; + if (shouldDisableCollapse(spans, state.childrenHiddenIDs)) { + return state; + } + let nearestCollapsedAncestor; + const childrenHiddenIDs = spans.reduce((res, curSpan) => { + if (nearestCollapsedAncestor && curSpan.depth <= nearestCollapsedAncestor.depth) { + res.add(nearestCollapsedAncestor.spanID); + nearestCollapsedAncestor = curSpan; + } else if (curSpan.hasChildren && !res.has(curSpan.spanID)) { + nearestCollapsedAncestor = curSpan; + } + return res; + }, new Set(state.childrenHiddenIDs)); + childrenHiddenIDs.add(nearestCollapsedAncestor.spanID); + return { ...state, childrenHiddenIDs }; +} + +export function expandOne(state, { payload }) { + const { spans } = payload; + if (state.childrenHiddenIDs.size === 0) { + return state; + } + let prevExpandedDepth = -1; + let expandNextHiddenSpan = true; + const childrenHiddenIDs = spans.reduce((res, s) => { + if (s.depth <= prevExpandedDepth) { + expandNextHiddenSpan = true; + } + if (expandNextHiddenSpan && res.has(s.spanID)) { + res.delete(s.spanID); + expandNextHiddenSpan = false; + prevExpandedDepth = s.depth; + } + return res; + }, new Set(state.childrenHiddenIDs)); + return { ...state, childrenHiddenIDs }; +} + function detailToggle(state, { payload }) { const { spanID } = payload; const detailStates = new Map(state.detailStates); @@ -149,6 +221,10 @@ export default handleActions( [actionTypes.SET_TRACE]: setTrace, [actionTypes.SET_SPAN_NAME_COLUMN_WIDTH]: setColumnWidth, [actionTypes.CHILDREN_TOGGLE]: childrenToggle, + [actionTypes.EXPAND_ALL]: expandAll, + [actionTypes.EXPAND_ONE]: expandOne, + [actionTypes.COLLAPSE_ALL]: collapseAll, + [actionTypes.COLLAPSE_ONE]: collapseOne, [actionTypes.DETAIL_TOGGLE]: detailToggle, [actionTypes.DETAIL_TAGS_TOGGLE]: detailTagsToggle, [actionTypes.DETAIL_PROCESS_TOGGLE]: detailProcessToggle, diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.test.js index b63c488a4d..499a668fb3 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.test.js @@ -14,7 +14,7 @@ import { createStore } from 'redux'; -import reducer, { actions, newInitialState } from './duck'; +import reducer, { actions, newInitialState, collapseAll, collapseOne, expandAll, expandOne } from './duck'; import DetailState from './SpanDetail/DetailState'; import transformTraceData from '../../../model/transform-trace-data'; import traceGenerator from '../../../demo/trace-generators'; @@ -129,6 +129,110 @@ describe('TraceTimelineViewer/duck', () => { }); }); + describe('expands and collapses all spans', () => { + // 0 + // - 1 + // --- 2 + // - 3 + // --- 4 + const spans = [ + { spanID: 0, depth: 0, hasChildren: true }, + { spanID: 1, depth: 1, hasChildren: true }, + { spanID: 2, depth: 2, hasChildren: false }, + { spanID: 3, depth: 1, hasChildren: true }, + { spanID: 4, depth: 2, hasChildren: false }, + ]; + + const oneSpanCollapsed = new Set([1]); + const allSpansCollapsed = new Set([0, 1, 3]); + const oneLevelCollapsed = new Set([1, 3]); + + // Tests for corner cases of reducers + const tests = [ + { + msg: 'expand all', + action: expandAll, + initial: allSpansCollapsed, + resultant: new Set(), + }, + { + msg: 'collapse all, no-op', + action: collapseAll, + initial: allSpansCollapsed, + resultant: allSpansCollapsed, + }, + { + msg: 'expand one', + action: expandOne, + initial: allSpansCollapsed, + resultant: oneLevelCollapsed, + }, + { + msg: 'expand one, one collapsed', + action: expandOne, + initial: oneSpanCollapsed, + resultant: new Set(), + }, + { + msg: 'collapse one, no-op', + action: collapseOne, + initial: allSpansCollapsed, + resultant: allSpansCollapsed, + }, + { + msg: 'collapse one, one collapsed', + action: collapseOne, + initial: oneSpanCollapsed, + resultant: oneLevelCollapsed, + }, + ]; + + tests.forEach(info => { + const { msg, action, initial, resultant } = info; + + it(msg, () => { + const { childrenHiddenIDs } = action({ childrenHiddenIDs: initial }, { payload: { spans } }); + expect(childrenHiddenIDs).toEqual(resultant); + }); + }); + + // Tests to verify correct behaviour of actions + const dispatchTests = [ + { + msg: 'expand all, no-op', + action: actions.expandAll(), + resultant: new Set(), + }, + { + msg: 'collapse all', + action: actions.collapseAll(spans), + resultant: allSpansCollapsed, + }, + { + msg: 'expand one, no-op', + action: actions.expandOne(spans), + resultant: new Set(), + }, + { + msg: 'collapse one', + action: actions.collapseOne(spans), + resultant: oneLevelCollapsed, + }, + ]; + + dispatchTests.forEach(info => { + const { msg, action, resultant } = info; + + it(msg, () => { + const st0 = store.getState(); + store.dispatch(action); + const st1 = store.getState(); + expect(st0.childrenHiddenIDs).toEqual(new Set()); + expect(st1.childrenHiddenIDs).toEqual(resultant); + }); + }); + }); + describe("toggles a detail's sub-sections", () => { const id = trace.spans[0].spanID; const baseDetail = new DetailState(); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/index.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/index.js index c3bc9a0469..28e4e92095 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/index.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/index.js @@ -16,19 +16,25 @@ import React from 'react'; import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; import { actions } from './duck'; import TimelineHeaderRow from './TimelineHeaderRow'; import VirtualizedTraceView from './VirtualizedTraceView'; +import { merge as mergeShortcuts } from '../keyboard-shortcuts'; import type { Accessors } from '../ScrollManager'; import type { ViewRange, ViewRangeTimeUpdate } from '../types'; -import type { Trace } from '../../../types'; +import type { Span, Trace } from '../../../types'; import './index.css'; type TraceTimelineViewerProps = { registerAccessors: Accessors => void, setSpanNameColumnWidth: number => void, + collapseAll: (Span[]) => void, + collapseOne: (Span[]) => void, + expandAll: () => void, + expandOne: (Span[]) => void, spanNameColumnWidth: number, textFilter: ?string, trace: Trace, @@ -45,23 +51,63 @@ const NUM_TICKS = 5; * re-render the ListView every time the cursor is moved on the trace minimap * or `TimelineHeaderRow`. */ -function TraceTimelineViewer(props: TraceTimelineViewerProps) { - const { setSpanNameColumnWidth, updateNextViewRangeTime, updateViewRangeTime, viewRange, ...rest } = props; - const { spanNameColumnWidth, trace } = rest; - return ( -
- - -
- ); +export class TraceTimelineViewerImpl extends React.PureComponent { + props: TraceTimelineViewerProps; + + componentDidMount() { + mergeShortcuts({ + collapseAll: this.collapseAll, + expandAll: this.expandAll, + collapseOne: this.collapseOne, + expandOne: this.expandOne, + }); + } + + collapseAll = () => { + this.props.collapseAll(this.props.trace.spans); + }; + + collapseOne = () => { + this.props.collapseOne(this.props.trace.spans); + }; + + expandAll = () => { + this.props.expandAll(); + }; + + expandOne = () => { + this.props.expandOne(this.props.trace.spans); + }; + + render() { + const { + setSpanNameColumnWidth, + updateNextViewRangeTime, + updateViewRangeTime, + viewRange, + ...rest + } = this.props; + const { spanNameColumnWidth, trace } = rest; + + return ( +
+ + +
+ ); + } } function mapStateToProps(state, ownProps) { @@ -70,11 +116,11 @@ function mapStateToProps(state, ownProps) { } function mapDispatchToProps(dispatch) { - const setSpanNameColumnWidth = (...args) => { - const action = actions.setSpanNameColumnWidth(...args); - return dispatch(action); - }; - return { setSpanNameColumnWidth }; + const { setSpanNameColumnWidth, expandAll, expandOne, collapseAll, collapseOne } = bindActionCreators( + actions, + dispatch + ); + return { setSpanNameColumnWidth, expandAll, expandOne, collapseAll, collapseOne }; } -export default connect(mapStateToProps, mapDispatchToProps)(TraceTimelineViewer); +export default connect(mapStateToProps, mapDispatchToProps)(TraceTimelineViewerImpl); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/index.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/index.test.js index a0a3ebd8a7..01f86a1690 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/index.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/index.test.js @@ -15,9 +15,10 @@ import React from 'react'; import { shallow } from 'enzyme'; -import TraceTimelineViewer from './index'; +import TraceTimelineViewer, { TraceTimelineViewerImpl } from './index'; import traceGenerator from '../../../demo/trace-generators'; import transformTraceData from '../../../model/transform-trace-data'; +import TimelineHeaderRow from './TimelineHeaderRow'; describe('', () => { const trace = transformTraceData(traceGenerator.trace({})); @@ -29,6 +30,11 @@ describe('', () => { current: [0, 1], }, }, + spanNameColumnWidth: 0.5, + expandAll: jest.fn(), + collapseAll: jest.fn(), + expandOne: jest.fn(), + collapseOne: jest.fn(), }; const options = { context: { @@ -37,17 +43,33 @@ describe('', () => { return { traceTimeline: { spanNameColumnWidth: 0.25 } }; }, subscribe() {}, + dispatch() {}, }, }, }; let wrapper; + let connectedWrapper; beforeEach(() => { - wrapper = shallow(, options); + wrapper = shallow(, options); + connectedWrapper = shallow(, options); }); it('it does not explode', () => { expect(wrapper).toBeDefined(); + expect(connectedWrapper).toBeDefined(); + }); + + it('it sets up actions', () => { + const headerRow = wrapper.find(TimelineHeaderRow); + headerRow.props().onCollapseAll(); + headerRow.props().onExpandAll(); + headerRow.props().onExpandOne(); + headerRow.props().onCollapseOne(); + expect(props.collapseAll.mock.calls.length).toBe(1); + expect(props.expandAll.mock.calls.length).toBe(1); + expect(props.expandOne.mock.calls.length).toBe(1); + expect(props.collapseOne.mock.calls.length).toBe(1); }); }); diff --git a/packages/jaeger-ui/src/components/TracePage/index.js b/packages/jaeger-ui/src/components/TracePage/index.js index 6344585ec5..faec98da1b 100644 --- a/packages/jaeger-ui/src/components/TracePage/index.js +++ b/packages/jaeger-ui/src/components/TracePage/index.js @@ -27,7 +27,7 @@ import ArchiveNotifier from './ArchiveNotifier'; import { actions as archiveActions } from './ArchiveNotifier/duck'; import { trackFilter, trackRange } from './index.track'; import type { CombokeysHandler, ShortcutCallbacks } from './keyboard-shortcuts'; -import { init as initShortcuts, reset as resetShortcuts } from './keyboard-shortcuts'; +import { merge as mergeShortcuts, reset as resetShortcuts } from './keyboard-shortcuts'; import { cancel as cancelScroll, scrollBy, scrollTo } from './scroll-page'; import ScrollManager from './ScrollManager'; import SpanGraph from './SpanGraph'; @@ -117,6 +117,7 @@ export default class TracePage extends React.PureComponent', () => { }); it('performs misc cleanup when unmounting', () => { + resetShortcuts.mockReset(); wrapper = shallow(); const scrollManager = wrapper.instance()._scrollManager; scrollManager.destroy = jest.fn(); wrapper.unmount(); expect(scrollManager.destroy.mock.calls).toEqual([[]]); - expect(resetShortcuts.mock.calls).toEqual([[]]); + expect(resetShortcuts.mock.calls).toEqual([[], []]); expect(cancelScroll.mock.calls).toEqual([[]]); }); diff --git a/packages/jaeger-ui/src/components/TracePage/keyboard-shortcuts.js b/packages/jaeger-ui/src/components/TracePage/keyboard-shortcuts.js index eef93268a4..b8d2281486 100644 --- a/packages/jaeger-ui/src/components/TracePage/keyboard-shortcuts.js +++ b/packages/jaeger-ui/src/components/TracePage/keyboard-shortcuts.js @@ -27,19 +27,24 @@ type CombokeysType = { }; export type ShortcutCallbacks = { - scrollPageDown: CombokeysHandler, - scrollPageUp: CombokeysHandler, - scrollToNextVisibleSpan: CombokeysHandler, - scrollToPrevVisibleSpan: CombokeysHandler, + scrollPageDown?: CombokeysHandler, + scrollPageUp?: CombokeysHandler, + scrollToNextVisibleSpan?: CombokeysHandler, + scrollToPrevVisibleSpan?: CombokeysHandler, // view range - panLeft: CombokeysHandler, - panLeftFast: CombokeysHandler, - panRight: CombokeysHandler, - panRightFast: CombokeysHandler, - zoomIn: CombokeysHandler, - zoomInFast: CombokeysHandler, - zoomOut: CombokeysHandler, - zoomOutFast: CombokeysHandler, + panLeft?: CombokeysHandler, + panLeftFast?: CombokeysHandler, + panRight?: CombokeysHandler, + panRightFast?: CombokeysHandler, + zoomIn?: CombokeysHandler, + zoomInFast?: CombokeysHandler, + zoomOut?: CombokeysHandler, + zoomOutFast?: CombokeysHandler, + // collapse/expand + collapseAll?: CombokeysHandler, + expandAll?: CombokeysHandler, + collapseOne?: CombokeysHandler, + expandOne?: CombokeysHandler, }; export const kbdMappings = { @@ -55,6 +60,10 @@ export const kbdMappings = { zoomInFast: 'shift+up', zoomOut: 'down', zoomOutFast: 'shift+down', + collapseAll: ']', + expandAll: '[', + collapseOne: 'p', + expandOne: 'o', }; let instance: ?CombokeysType; @@ -66,11 +75,13 @@ function getInstance(): CombokeysType { return instance; } -export function init(callbacks: ShortcutCallbacks) { - const combokeys = getInstance(); - combokeys.reset(); - Object.keys(kbdMappings).forEach(name => { - combokeys.bind(kbdMappings[name], callbacks[name]); +export function merge(callbacks: ShortcutCallbacks) { + const inst = getInstance(); + Object.keys(callbacks).forEach(name => { + const keysHandler = callbacks[name]; + if (keysHandler) { + inst.bind(kbdMappings[name], keysHandler); + } }); }