Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timeline Expand and Collapse Features #221

Merged
merged 17 commits into from
Jul 6, 2018
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[][] {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is 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);
}
Original file line number Diff line number Diff line change
@@ -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 (
<span className="TimelineCollapser">
<Tooltip title="Expand +1">
<Icon type="right" onClick={onExpandOne} className="TimelineCollapser--btn-expand" />
</Tooltip>
<Tooltip title="Collapse +1">
<Icon type="right" onClick={onCollapseOne} className="TimelineCollapser--btn" />
</Tooltip>
<Tooltip title="Expand All">
<Icon type="double-right" onClick={onExpandAll} className="TimelineCollapser--btn-expand" />
</Tooltip>
<Tooltip title="Collapse All">
<Icon type="double-right" onClick={onCollapseAll} className="TimelineCollapser--btn" />
</Tooltip>
</span>
);
}
Original file line number Diff line number Diff line change
@@ -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('<TimelineCollapser>', () => {
it('renders without exploding', () => {
const props = {
onCollapseAll: () => {},
onCollapseOne: () => {},
onExpandAll: () => {},
onExpandOne: () => {},
};
const wrapper = shallow(<TimelineCollapser {...props} />);
expect(wrapper).toBeDefined();
expect(wrapper.find('.TimelineCollapser').length).toBe(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -39,7 +44,11 @@ export default function TimelineHeaderRow(props: TimelineHeaderRowProps) {
duration,
nameColumnWidth,
numTicks,
onCollapseAll,
onCollapseOne,
onColummWidthChange,
onExpandAll,
onExpandOne,
updateViewRangeTime,
updateNextViewRangeTime,
viewRangeTime,
Expand All @@ -49,6 +58,12 @@ export default function TimelineHeaderRow(props: TimelineHeaderRowProps) {
<TimelineRow className="TimelineHeaderRow">
<TimelineRow.Cell width={nameColumnWidth}>
<h3 className="TimelineHeaderRow--title">Service &amp; Operation</h3>
<TimelineCollapser
onCollapseAll={onCollapseAll}
onExpandAll={onExpandAll}
onCollapseOne={onCollapseOne}
onExpandOne={onExpandOne}
/>
</TimelineRow.Cell>
<TimelineRow.Cell width={1 - nameColumnWidth}>
<TimelineViewingLayer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import TimelineHeaderRow from './TimelineHeaderRow';
import TimelineColumnResizer from './TimelineColumnResizer';
import TimelineViewingLayer from './TimelineViewingLayer';
import Ticks from '../Ticks';
import TimelineCollapser from './TimelineCollapser';

describe('<TimelineHeaderRow>', () => {
let wrapper;
Expand All @@ -28,7 +29,11 @@ describe('<TimelineHeaderRow>', () => {
nameColumnWidth,
duration: 1234,
numTicks: 5,
onCollapseAll: () => {},
onCollapseOne: () => {},
onColummWidthChange: () => {},
onExpandAll: () => {},
onExpandOne: () => {},
updateNextViewRangeTime: () => {},
updateViewRangeTime: () => {},
viewRangeTime: {
Expand Down Expand Up @@ -92,4 +97,16 @@ describe('<TimelineHeaderRow>', () => {
);
expect(wrapper.containsMatchingElement(elm)).toBe(true);
});

it('renders the TimelineCollapser', () => {
const elm = (
<TimelineCollapser
onCollapseAll={props.onCollapseAll}
onExpandAll={props.onExpandAll}
onCollapseOne={props.onCollapseOne}
onExpandOne={props.onExpandOne}
/>
);
expect(wrapper.containsMatchingElement(elm)).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 }),
Expand Down Expand Up @@ -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) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why prefer reduce over filter?

const childrenHiddenIDs = new Set(spans.filter(sp => sp.hasChildren));

Applies generally to these reducers.

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);
Expand Down Expand Up @@ -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,
Expand Down
Loading