This is an automated email from the ASF dual-hosted git repository.
adriancole pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-zipkin.git
The following commit(s) were added to refs/heads/master by this push:
new ad9fab1 Improve MiniTimeline (#2529)
ad9fab1 is described below
commit ad9fab1a9e33e9404d0ad57321fc801b97a9356a
Author: tacigar <[email protected]>
AuthorDate: Thu May 9 15:50:32 2019 +0900
Improve MiniTimeline (#2529)
* Add drag&drop feature
* Fix bug not rendering time markers
* Add step props to rc-clider range
* Use corsor:col-resize to MiniTimelineGraph
---
zipkin-lens/package-lock.json | 31 +++
zipkin-lens/package.json | 1 +
.../scss/components/_mini-timeline-graph.scss | 7 +
.../scss/components/_mini-timeline-label.scss | 26 +++
zipkin-lens/scss/components/_mini-timeline.scss | 36 ---
zipkin-lens/scss/custom/_rc-slider.scss | 7 +
zipkin-lens/scss/main.scss | 4 +
.../components/MiniTimeline/MiniTimelineGraph.js | 187 +++++++++++++++
.../MiniTimeline/MiniTimelineGraph.test.js | 37 +++
.../components/MiniTimeline/MiniTimelineLabel.js | 65 ++++++
.../MiniTimeline/MiniTimelineLabel.test.js | 42 ++++
.../components/MiniTimeline/MiniTimelineSlider.js | 78 +++++++
.../MiniTimeline/MiniTimelineSlider.test.js | 69 ++++++
.../MiniTimeline/MiniTimelineTimeMarkers.js | 48 ++++
.../MiniTimeline/MiniTimelineTimeMarkers.test.js | 30 +++
zipkin-lens/src/components/MiniTimeline/index.js | 257 +++------------------
.../src/components/MiniTimeline/index.test.js | 89 +++++++
zipkin-lens/src/components/MiniTimeline/util.js | 35 +++
.../src/components/MiniTimeline/util.test.js | 44 ++++
19 files changed, 832 insertions(+), 261 deletions(-)
diff --git a/zipkin-lens/package-lock.json b/zipkin-lens/package-lock.json
index ed64abd..6db1a0b 100644
--- a/zipkin-lens/package-lock.json
+++ b/zipkin-lens/package-lock.json
@@ -10715,6 +10715,27 @@
"react-lifecycles-compat": "^3.0.4"
}
},
+ "rc-slider": {
+ "version": "8.6.9",
+ "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-8.6.9.tgz",
+ "integrity":
"sha512-v5XwSARCyKGkalV7c54jwiuPNh8pGUg0i1opVD8YJVd8zQqbxepRoGmEE4xwRTxjR7Goao6/ARc7l2dGoPwZsg==",
+ "requires": {
+ "babel-runtime": "6.x",
+ "classnames": "^2.2.5",
+ "prop-types": "^15.5.4",
+ "rc-tooltip": "^3.7.0",
+ "rc-util": "^4.0.4",
+ "shallowequal": "^1.0.1",
+ "warning": "^4.0.3"
+ },
+ "dependencies": {
+ "shallowequal": {
+ "version": "1.1.0",
+ "resolved":
"https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity":
"sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+ }
+ }
+ },
"rc-time-picker": {
"version": "3.6.2",
"resolved":
"https://registry.npmjs.org/rc-time-picker/-/rc-time-picker-3.6.2.tgz",
@@ -10726,6 +10747,16 @@
"rc-trigger": "^2.2.0"
}
},
+ "rc-tooltip": {
+ "version": "3.7.3",
+ "resolved":
"https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-3.7.3.tgz",
+ "integrity":
"sha512-dE2ibukxxkrde7wH9W8ozHKUO4aQnPZ6qBHtrTH9LoO836PjDdiaWO73fgPB05VfJs9FbZdmGPVEbXCeOP99Ww==",
+ "requires": {
+ "babel-runtime": "6.x",
+ "prop-types": "^15.5.8",
+ "rc-trigger": "^2.2.2"
+ }
+ },
"rc-trigger": {
"version": "2.6.2",
"resolved":
"https://registry.npmjs.org/rc-trigger/-/rc-trigger-2.6.2.tgz",
diff --git a/zipkin-lens/package.json b/zipkin-lens/package.json
index c0cb37a..9c7d0df 100755
--- a/zipkin-lens/package.json
+++ b/zipkin-lens/package.json
@@ -68,6 +68,7 @@
"prop-types": "^15.6.2",
"query-string": "^6.1.0",
"rc-calendar": "^9.7.10",
+ "rc-slider": "^8.6.9",
"rc-time-picker": "^3.4.0",
"react": "^16.4.1",
"react-chartjs-2": "^2.7.4",
diff --git a/zipkin-lens/scss/components/_mini-timeline-graph.scss
b/zipkin-lens/scss/components/_mini-timeline-graph.scss
new file mode 100644
index 0000000..c56234b
--- /dev/null
+++ b/zipkin-lens/scss/components/_mini-timeline-graph.scss
@@ -0,0 +1,7 @@
+.mini-timeline-graph {
+ background-color: $gray-10;
+ border: 1px solid $gray-9;
+ border-radius: 2px;
+ overflow: hidden;
+ cursor: col-resize;
+}
diff --git a/zipkin-lens/scss/components/_mini-timeline-label.scss
b/zipkin-lens/scss/components/_mini-timeline-label.scss
new file mode 100644
index 0000000..5bb18be
--- /dev/null
+++ b/zipkin-lens/scss/components/_mini-timeline-label.scss
@@ -0,0 +1,26 @@
+.mini-timeline-label {
+ position: relative;
+ height: 14px;
+}
+
+.mini-timeline-label__label-wrapper {
+ position: absolute;
+}
+
+.mini-timeline-label__label {
+ color: $dark-2;
+ font-size: $font-size-xs;
+ position: absolute;
+ left: -20px;
+
+ &--first {
+ left: 2px;
+ position: absolute;
+ }
+
+ &--last {
+ left: initial;
+ right: 2px;
+ position: absolute;
+ }
+}
diff --git a/zipkin-lens/scss/components/_mini-timeline.scss
b/zipkin-lens/scss/components/_mini-timeline.scss
index 13337f0..ceb34f7 100644
--- a/zipkin-lens/scss/components/_mini-timeline.scss
+++ b/zipkin-lens/scss/components/_mini-timeline.scss
@@ -20,39 +20,3 @@
padding: 2px;
margin: 5px;
}
-
-.mini-timeline__time-marker-labels-wrapper {
- position: relative;
- height: 14px;
-}
-
-.mini-timeline__time-marker {
- position: absolute;
-}
-
-.mini-timeline__time-marker-label {
- color: $dark-2;
- font-size: $font-size-xs;
- position: absolute;
- left: -20px;
-
- &--first {
- left: 2px;
- position: absolute;
- }
-
- &--last {
- left: initial;
- right: 2px;
- position: absolute;
- }
-}
-
-.mini-timeline__graph {
- padding: 2px;
- background-color: $gray-10;
- height: 75px;
- border: 1px solid $gray-9;
- border-radius: 2px;
- cursor: pointer;
-}
diff --git a/zipkin-lens/scss/custom/_rc-slider.scss
b/zipkin-lens/scss/custom/_rc-slider.scss
new file mode 100644
index 0000000..0b7bbd8
--- /dev/null
+++ b/zipkin-lens/scss/custom/_rc-slider.scss
@@ -0,0 +1,7 @@
+.rc-slider {
+ padding: 0;
+}
+
+.rc-slider-rail {
+ background-color: $gray-5;
+}
diff --git a/zipkin-lens/scss/main.scss b/zipkin-lens/scss/main.scss
index efa2d9f..191c929 100644
--- a/zipkin-lens/scss/main.scss
+++ b/zipkin-lens/scss/main.scss
@@ -16,6 +16,7 @@
//
@import '../node_modules/rc-calendar/assets/index.css';
+@import '../node_modules/rc-slider/assets/index.css';
@import '../node_modules/rc-time-picker/assets/index.css';
@import '../node_modules/react-table/react-table.css';
@@ -26,6 +27,7 @@
@import 'base/form';
@import 'custom/rc-calendar';
+@import 'custom/rc-slider';
@import 'custom/rc-time-picker';
@import 'custom/react-modal';
@import 'custom/react-select';
@@ -47,6 +49,8 @@
@import 'components/global-search';
@import 'components/loading-overlay';
@import 'components/mini-timeline';
+@import 'components/mini-timeline-graph';
+@import 'components/mini-timeline-label';
@import 'components/search-condition';
@import 'components/service-name-badge';
@import 'components/sidebar';
diff --git a/zipkin-lens/src/components/MiniTimeline/MiniTimelineGraph.js
b/zipkin-lens/src/components/MiniTimeline/MiniTimelineGraph.js
new file mode 100644
index 0000000..6e68b5f
--- /dev/null
+++ b/zipkin-lens/src/components/MiniTimeline/MiniTimelineGraph.js
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 PropTypes from 'prop-types';
+import React from 'react';
+
+import MiniTimelineTimeMarkers from './MiniTimelineTimeMarkers';
+import { getGraphHeight, getGraphLineHeight } from './util';
+import { getServiceNameColor } from '../../util/color';
+import { detailedSpansPropTypes } from '../../prop-types';
+
+const leftMouseButton = 0;
+
+const propTypes = {
+ spans: detailedSpansPropTypes.isRequired,
+ startTs: PropTypes.number.isRequired,
+ endTs: PropTypes.number.isRequired,
+ duration: PropTypes.number.isRequired,
+ onStartAndEndTsChange: PropTypes.func.isRequired,
+ numTimeMarkers: PropTypes.number.isRequired,
+};
+
+class MiniTimelineGraph extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isDragging: false,
+ dragStartX: null,
+ dragCurrentX: null,
+ };
+ this.element = undefined;
+ this.handleMouseDown = this.handleMouseDown.bind(this);
+ this.handleMouseMove = this.handleMouseMove.bind(this);
+ this.handleMouseUp = this.handleMouseUp.bind(this);
+ }
+
+ getPositionX(clientX) {
+ const { left, width } = this.element.getBoundingClientRect();
+ return (clientX - left) / width;
+ }
+
+ handleMouseDown(event) {
+ if (event.button !== leftMouseButton) {
+ return;
+ }
+ const currentX = this.getPositionX(event.clientX);
+ this.setState({
+ isDragging: true,
+ dragStartX: currentX,
+ dragCurrentX: currentX,
+ });
+ window.addEventListener('mousemove', this.handleMouseMove);
+ window.addEventListener('mouseup', this.handleMouseUp);
+ }
+
+ handleMouseMove(event) {
+ this.setState({ dragCurrentX: this.getPositionX(event.clientX) });
+ }
+
+ handleMouseUp(event) {
+ const { duration, onStartAndEndTsChange } = this.props;
+ const { dragStartX } = this.state;
+ this.setState({ isDragging: false });
+
+ let startTs;
+ let endTs;
+ const currentX = this.getPositionX(event.clientX);
+ if (currentX > dragStartX) {
+ startTs = Math.max(dragStartX * duration, 0);
+ endTs = Math.min(currentX * duration, duration);
+ } else {
+ startTs = Math.max(currentX * duration, 0);
+ endTs = Math.min(dragStartX * duration, duration);
+ }
+ onStartAndEndTsChange(startTs, endTs);
+
+ window.removeEventListener('mousemove', this.handleMouseMove);
+ window.removeEventListener('mouseup', this.handleMouseUp);
+ }
+
+ render() {
+ const {
+ spans, startTs, endTs, duration, numTimeMarkers,
+ } = this.props;
+ const { isDragging, dragStartX, dragCurrentX } = this.state;
+ const graphHeight = getGraphHeight(spans.length);
+ const graphLineHeight = getGraphLineHeight(spans.length);
+ return (
+ <div
+ className="mini-timeline-graph"
+ style={{ height: graphHeight }}
+ ref={(element) => { this.element = element; }}
+ role="presentation"
+ onMouseDown={this.handleMouseDown}
+ >
+ <svg version="1.1" width="100%" height={graphHeight}
xmlns="http://www.w3.org/2000/svg">
+ <MiniTimelineTimeMarkers
+ height={graphHeight}
+ numTimeMarkers={numTimeMarkers}
+ />
+ {
+ spans.map((span, i) => (
+ <rect
+ key={span.spanId}
+ width={`${span.width}%`}
+ height={graphLineHeight}
+ x={`${span.left}%`}
+ y={i * graphLineHeight}
+ fill={getServiceNameColor(span.serviceName)}
+ />
+ ))
+ }
+ {
+ isDragging
+ ? (
+ <g stroke="#999" strokeWidth="1">
+ <line
+ x1={`${dragStartX * 100}%`}
+ x2={`${dragStartX * 100}%`}
+ y1={0}
+ y2={graphHeight}
+ />
+ <line
+ x1={`${dragStartX * 100}%`}
+ x2={`${dragCurrentX * 100}%`}
+ y1={graphHeight / 2}
+ y2={graphHeight / 2}
+
+ />
+ <line
+ x1={`${dragCurrentX * 100}%`}
+ x2={`${dragCurrentX * 100}%`}
+ y1={0}
+ y2={graphHeight}
+ />
+ </g>
+ )
+ : null
+ }
+ {
+ startTs
+ ? (
+ <rect
+ width={`${startTs / duration * 100}%`}
+ height={graphHeight}
+ x="0"
+ y="0"
+ fill="rgba(50, 50, 50, 0.2)"
+ />
+ )
+ : null
+ }
+ {
+ endTs
+ ? (
+ <rect
+ width={`${(duration - endTs) / duration * 100}%`}
+ height={graphHeight}
+ x={`${endTs / duration * 100}%`}
+ y="0"
+ fill="rgba(50, 50, 50, 0.2)"
+ />
+ )
+ : null
+ }
+ </svg>
+ </div>
+ );
+ }
+}
+
+MiniTimelineGraph.propTypes = propTypes;
+
+export default MiniTimelineGraph;
diff --git a/zipkin-lens/src/components/MiniTimeline/MiniTimelineGraph.test.js
b/zipkin-lens/src/components/MiniTimeline/MiniTimelineGraph.test.js
new file mode 100644
index 0000000..390f55f
--- /dev/null
+++ b/zipkin-lens/src/components/MiniTimeline/MiniTimelineGraph.test.js
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 MiniTimelineGraph from './MiniTimelineGraph';
+
+// TODO: need more tests.
+describe('<MiniTimelineGraph />', () => {
+ it('should be rendered', () => {
+ const wrapper = shallow(
+ <MiniTimelineGraph
+ spans={[]}
+ startTs={0}
+ endTs={10}
+ duration={10}
+ onStartAndEndTsChange={jest.fn()}
+ numTimeMarkers={5}
+ />,
+ );
+ expect(wrapper.find('.mini-timeline-graph').length).toBe(1);
+ });
+});
diff --git a/zipkin-lens/src/components/MiniTimeline/MiniTimelineLabel.js
b/zipkin-lens/src/components/MiniTimeline/MiniTimelineLabel.js
new file mode 100644
index 0000000..8ec3244
--- /dev/null
+++ b/zipkin-lens/src/components/MiniTimeline/MiniTimelineLabel.js
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 PropTypes from 'prop-types';
+import React from 'react';
+
+import { formatDuration } from '../../util/timestamp';
+
+const propTypes = {
+ numTimeMarkers: PropTypes.number.isRequired,
+ duration: PropTypes.number.isRequired,
+};
+
+const MiniTimelineLabel = ({ numTimeMarkers, duration }) => {
+ const timeMarkers = [];
+ for (let i = 0; i < numTimeMarkers; i += 1) {
+ const label = formatDuration((i / (numTimeMarkers - 1)) * duration);
+ const portion = i / (numTimeMarkers - 1);
+
+ let modifier = '';
+ if (portion === 0) {
+ modifier = '--first';
+ } else if (portion >= 1) {
+ modifier = '--last';
+ }
+
+ timeMarkers.push(
+ <div
+ key={portion}
+ className="mini-timeline-label__label-wrapper"
+ style={{ left: `${portion * 100}%` }}
+ data-test="label-wrapper"
+ >
+ <span
+ className={`mini-timeline-label__label
mini-timeline-label__label${modifier}`}
+ data-test="label"
+ >
+ {label}
+ </span>
+ </div>,
+ );
+ }
+ return (
+ <div className="mini-timeline-label">
+ {timeMarkers}
+ </div>
+ );
+};
+
+MiniTimelineLabel.propTypes = propTypes;
+
+export default MiniTimelineLabel;
diff --git a/zipkin-lens/src/components/MiniTimeline/MiniTimelineLabel.test.js
b/zipkin-lens/src/components/MiniTimeline/MiniTimelineLabel.test.js
new file mode 100644
index 0000000..0e5f97c
--- /dev/null
+++ b/zipkin-lens/src/components/MiniTimeline/MiniTimelineLabel.test.js
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 MiniTimelineLabel from './MiniTimelineLabel';
+
+describe('<MiniTimelineLabel />', () => {
+ it('should set proper positions', () => {
+ const wrapper = shallow(<MiniTimelineLabel numTimeMarkers={5}
duration={300} />);
+ const labelWrappers = wrapper.find('[data-test="label-wrapper"]');
+ expect(labelWrappers.at(0).prop('style')).toEqual({ left: '0%' });
+ expect(labelWrappers.at(1).prop('style')).toEqual({ left: '25%' });
+ expect(labelWrappers.at(2).prop('style')).toEqual({ left: '50%' });
+ expect(labelWrappers.at(3).prop('style')).toEqual({ left: '75%' });
+ expect(labelWrappers.at(4).prop('style')).toEqual({ left: '100%' });
+ });
+
+ it('should set proper modifiers', () => {
+ const wrapper = shallow(<MiniTimelineLabel numTimeMarkers={5}
duration={300} />);
+ const labelWrappers = wrapper.find('[data-test="label"]');
+ expect(labelWrappers.at(0).hasClass('mini-timeline-label__label--first'));
+ expect(labelWrappers.at(1).hasClass('mini-timeline-label__label--first'));
+ expect(labelWrappers.at(2).hasClass('mini-timeline-label__label--first'));
+ expect(labelWrappers.at(3).hasClass('mini-timeline-label__label--first'));
+ expect(labelWrappers.at(4).hasClass('mini-timeline-label__label--last'));
+ });
+});
diff --git a/zipkin-lens/src/components/MiniTimeline/MiniTimelineSlider.js
b/zipkin-lens/src/components/MiniTimeline/MiniTimelineSlider.js
new file mode 100644
index 0000000..8542c5b
--- /dev/null
+++ b/zipkin-lens/src/components/MiniTimeline/MiniTimelineSlider.js
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 PropTypes from 'prop-types';
+import React from 'react';
+import Slider from 'rc-slider';
+
+const { Range } = Slider;
+
+const propTypes = {
+ duration: PropTypes.number.isRequired,
+ startTs: PropTypes.number.isRequired,
+ endTs: PropTypes.number.isRequired,
+ onStartAndEndTsChange: PropTypes.func.isRequired,
+};
+
+class MiniTimelineSlider extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { isDragging: false };
+ this.handleBeforeRangeChange = this.handleBeforeRangeChange.bind(this);
+ this.handleAfterRangeChange = this.handleAfterRangeChange.bind(this);
+ }
+
+ handleBeforeRangeChange() {
+ this.setState({ isDragging: true });
+ }
+
+ handleAfterRangeChange(value) {
+ const { duration, onStartAndEndTsChange } = this.props;
+ onStartAndEndTsChange(
+ value[0] * duration / 100,
+ value[1] * duration / 100,
+ );
+ this.setState({ isDragging: false });
+ }
+
+ render() {
+ const { duration, startTs, endTs } = this.props;
+ const { isDragging } = this.state;
+
+ const props = {
+ allowCase: false,
+ defaultValue: [0, 100],
+ step: 0.01,
+ onBeforeChange: this.handleBeforeRangeChange,
+ onAfterChange: this.handleAfterRangeChange,
+ };
+ if (!isDragging) {
+ props.value = [
+ startTs / duration * 100,
+ endTs / duration * 100,
+ ];
+ }
+ return (
+ <div className="mini-timeline-slider">
+ <Range {...props} />
+ </div>
+ );
+ }
+}
+
+MiniTimelineSlider.propTypes = propTypes;
+
+export default MiniTimelineSlider;
diff --git a/zipkin-lens/src/components/MiniTimeline/MiniTimelineSlider.test.js
b/zipkin-lens/src/components/MiniTimeline/MiniTimelineSlider.test.js
new file mode 100644
index 0000000..d4921ac
--- /dev/null
+++ b/zipkin-lens/src/components/MiniTimeline/MiniTimelineSlider.test.js
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 Slider from 'rc-slider';
+
+import MiniTimelineSlider from './MiniTimelineSlider';
+
+const { Range } = Slider;
+
+describe('<MiniTimelineSlider />', () => {
+ it('should change isDragging state before changing the range', () => {
+ const wrapper = shallow(
+ <MiniTimelineSlider
+ duration={10}
+ startTs={0}
+ endTs={10}
+ onStartAndEndTsChange={() => {}}
+ />,
+ );
+ wrapper.find(Range).prop('onBeforeChange')();
+ expect(wrapper.state('isDragging')).toEqual(true);
+ });
+
+ it('should change isDragging state after changing the range', () => {
+ const wrapper = shallow(
+ <MiniTimelineSlider
+ duration={10}
+ startTs={0}
+ endTs={10}
+ onStartAndEndTsChange={() => {}}
+ />,
+ );
+ wrapper.find(Range).prop('onBeforeChange')(); // isDragging === true
+ wrapper.find(Range).prop('onAfterChange')([2, 6]);
+ expect(wrapper.state('isDragging')).toEqual(false);
+ });
+
+ it('should call onStartAndEndTsChange after range change', () => {
+ const onStartAndEndTsChange = jest.fn();
+ const wrapper = shallow(
+ <MiniTimelineSlider
+ duration={10}
+ startTs={0}
+ endTs={10}
+ onStartAndEndTsChange={onStartAndEndTsChange}
+ />,
+ );
+ wrapper.find(Range).prop('onAfterChange')([2, 6]);
+ expect(onStartAndEndTsChange).toHaveBeenCalledWith(
+ 2 / 10,
+ 6 / 10,
+ );
+ });
+});
diff --git a/zipkin-lens/src/components/MiniTimeline/MiniTimelineTimeMarkers.js
b/zipkin-lens/src/components/MiniTimeline/MiniTimelineTimeMarkers.js
new file mode 100644
index 0000000..b2656d7
--- /dev/null
+++ b/zipkin-lens/src/components/MiniTimeline/MiniTimelineTimeMarkers.js
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 PropTypes from 'prop-types';
+import React from 'react';
+
+const propTypes = {
+ height: PropTypes.number.isRequired,
+ numTimeMarkers: PropTypes.number.isRequired,
+};
+
+const MiniTimelineTimeMarkers = ({ height, numTimeMarkers }) => {
+ const timeMarkers = [];
+ for (let i = 1; i < numTimeMarkers - 1; i += 1) {
+ const portion = 100 / (numTimeMarkers - 1) * i;
+ timeMarkers.push(
+ <line
+ key={portion}
+ x1={`${portion}%`}
+ x2={`${portion}%`}
+ y1="0"
+ y2={height}
+ />,
+ );
+ }
+ return (
+ <g stroke="#888" strokeWidth="1">
+ {timeMarkers}
+ </g>
+ );
+};
+
+MiniTimelineTimeMarkers.propTypes = propTypes;
+
+export default MiniTimelineTimeMarkers;
diff --git
a/zipkin-lens/src/components/MiniTimeline/MiniTimelineTimeMarkers.test.js
b/zipkin-lens/src/components/MiniTimeline/MiniTimelineTimeMarkers.test.js
new file mode 100644
index 0000000..78b9b84
--- /dev/null
+++ b/zipkin-lens/src/components/MiniTimeline/MiniTimelineTimeMarkers.test.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 MiniTimelineTimeMarkers from './MiniTimelineTimeMarkers';
+
+describe('<MiniTimelineTimeMarkers />', () => {
+ it('should set proper positions', () => {
+ const wrapper = shallow(<MiniTimelineTimeMarkers height={75}
numTimeMarkers={5} />);
+ const timeMarkers = wrapper.find('line');
+ expect(timeMarkers.at(0).prop('x1')).toEqual('25%');
+ expect(timeMarkers.at(1).prop('x1')).toEqual('50%');
+ expect(timeMarkers.at(2).prop('x1')).toEqual('75%');
+ });
+});
diff --git a/zipkin-lens/src/components/MiniTimeline/index.js
b/zipkin-lens/src/components/MiniTimeline/index.js
index ba4510d..d85435b 100644
--- a/zipkin-lens/src/components/MiniTimeline/index.js
+++ b/zipkin-lens/src/components/MiniTimeline/index.js
@@ -17,10 +17,13 @@
import PropTypes from 'prop-types';
import React from 'react';
-import { formatDuration } from '../../util/timestamp';
-import { getServiceNameColor } from '../../util/color';
+import MiniTimelineGraph from './MiniTimelineGraph';
+import MiniTimelineLabel from './MiniTimelineLabel';
+import MiniTimelineSlider from './MiniTimelineSlider';
import { detailedTraceSummaryPropTypes } from '../../prop-types';
+const defaultNumTimeMarkers = 5;
+
const propTypes = {
startTs: PropTypes.number.isRequired,
endTs: PropTypes.number.isRequired,
@@ -28,234 +31,38 @@ const propTypes = {
onStartAndEndTsChange: PropTypes.func.isRequired,
};
-const graphHeight = 75;
-const numTimeMarkers = 5;
-const leftMouseButton = 0;
-
-const renderTimeMarkers = () => {
- const timeMarkers = [];
- for (let i = 1; i < numTimeMarkers - 1; i += 1) {
- const portion = 100 / (numTimeMarkers - 1) * i;
- timeMarkers.push(
- <line
- key={portion}
- x1={`${portion}%`}
- x2={`${portion}%`}
- y1="0"
- y2={graphHeight}
- />,
- );
+const MiniTimeline = ({
+ startTs, endTs, traceSummary, onStartAndEndTsChange,
+}) => {
+ const { spans, duration } = traceSummary;
+ if (spans.length <= 1) {
+ return null;
}
+
return (
- <g stroke="#888" strokeWidth="1">
- {timeMarkers}
- </g>
+ <div className="mini-timeline">
+ <MiniTimelineLabel
+ numTimeMarkers={defaultNumTimeMarkers}
+ duration={duration}
+ />
+ <MiniTimelineGraph
+ spans={spans}
+ duration={duration}
+ startTs={startTs}
+ endTs={endTs}
+ onStartAndEndTsChange={onStartAndEndTsChange}
+ numTimeMarkers={defaultNumTimeMarkers}
+ />
+ <MiniTimelineSlider
+ duration={duration}
+ startTs={startTs}
+ endTs={endTs}
+ onStartAndEndTsChange={onStartAndEndTsChange}
+ />
+ </div>
);
};
-class MiniTimeline extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- isDragging: false,
- dragStartX: null,
- dragCurrentX: null,
- };
- this._graphElement = undefined;
-
- this.setGraphElement = this.setGraphElement.bind(this);
- this.handleMouseDown = this.handleMouseDown.bind(this);
- this.handleMouseMove = this.handleMouseMove.bind(this);
- this.handleMouseUp = this.handleMouseUp.bind(this);
- this.handleDoubleClick = this.handleDoubleClick.bind(this);
- }
-
- setGraphElement(element) {
- this._graphElement = element;
- }
-
- getPosition(clientX) {
- const { left, width } = this._graphElement.getBoundingClientRect();
- return (clientX - left) / width;
- }
-
- handleMouseDown(event) {
- if (event.button !== leftMouseButton) {
- return;
- }
- const currentX = this.getPosition(event.clientX);
- this.setState({
- isDragging: true,
- dragStartX: currentX,
- dragCurrentX: currentX,
- });
- window.addEventListener('mousemove', this.handleMouseMove);
- window.addEventListener('mouseup', this.handleMouseUp);
- }
-
- handleMouseMove(event) {
- this.setState({
- dragCurrentX: this.getPosition(event.clientX),
- });
- }
-
- handleMouseUp(event) {
- const { traceSummary, onStartAndEndTsChange } = this.props;
- const { dragStartX } = this.state;
- this.setState({ isDragging: false });
-
- let startTs;
- let endTs;
- const currentX = this.getPosition(event.clientX);
- if (currentX > dragStartX) {
- startTs = Math.max(dragStartX * traceSummary.duration, 0);
- endTs = Math.min(currentX * traceSummary.duration,
traceSummary.duration);
- } else {
- startTs = Math.max(currentX * traceSummary.duration, 0);
- endTs = Math.min(dragStartX * traceSummary.duration,
traceSummary.duration);
- }
- onStartAndEndTsChange(startTs, endTs);
-
- window.removeEventListener('mousemove', this.handleMouseMove);
- window.removeEventListener('mouseup', this.handleMouseUp);
- }
-
- handleDoubleClick() {
- const { traceSummary, onStartAndEndTsChange } = this.props;
- onStartAndEndTsChange(0, traceSummary.duration);
- }
-
- renderTimeMarkerLabels() {
- const { traceSummary } = this.props;
-
- const timeMarkers = [];
- for (let i = 0; i < numTimeMarkers; i += 1) {
- const label = formatDuration((i / (numTimeMarkers - 1)) *
(traceSummary.duration));
-
- const portion = i / (numTimeMarkers - 1);
-
- let modifier = '';
- if (portion === 0) {
- modifier = '--first';
- } else if (portion >= 1) {
- modifier = '--last';
- }
-
- timeMarkers.push(
- <div
- key={portion}
- className="mini-timeline__time-marker"
- style={{
- left: `${portion * 100}%`,
- }}
- >
- <span className={
- `mini-timeline__time-marker-label
mini-timeline__time-marker-label${modifier}`}
- >
- {label}
- </span>
- </div>,
- );
- }
- return (
- <div>
- {timeMarkers}
- </div>
- );
- }
-
- render() {
- const { traceSummary, startTs, endTs } = this.props;
- const { isDragging, dragStartX, dragCurrentX } = this.state;
- const { spans } = traceSummary;
- const lineHeight = graphHeight / spans.length;
-
- return (
- <div className="mini-timeline">
- <div className="mini-timeline__time-marker-labels-wrapper">
- {this.renderTimeMarkerLabels()}
- </div>
- <div
- className="mini-timeline__graph"
- ref={this.setGraphElement}
- role="presentation"
- onMouseDown={this.handleMouseDown}
- onDoubleClick={this.handleDoubleClick}
- >
- <svg version="1.1" width="100%" height={graphHeight}
xmlns="http://www.w3.org/2000/svg">
- {renderTimeMarkers()}
- {
- spans.map((span, i) => (
- <rect
- key={span.spanId}
- width={`${span.width}%`}
- height={lineHeight}
- x={`${span.left}%`}
- y={i * lineHeight}
- fill={getServiceNameColor(span.serviceName)}
- />
- ))
- }
- {
- isDragging
- ? (
- <g stroke="#999" strokeWidth="1">
- <line
- x1={`${dragStartX * 100}%`}
- x2={`${dragStartX * 100}%`}
- y1={0}
- y2={graphHeight}
- />
- <line
- x1={`${dragStartX * 100}%`}
- x2={`${dragCurrentX * 100}%`}
- y1={graphHeight / 2}
- y2={graphHeight / 2}
-
- />
- <line
- x1={`${dragCurrentX * 100}%`}
- x2={`${dragCurrentX * 100}%`}
- y1={0}
- y2={graphHeight}
- />
- </g>
- )
- : null
- }
- {
- startTs
- ? (
- <rect
- width={`${startTs / traceSummary.duration * 100}%`}
- height={graphHeight}
- x="0"
- y="0"
- fill="rgba(50, 50, 50, 0.2)"
- />
- )
- : null
- }
- {
- endTs
- ? (
- <rect
- width={`${(traceSummary.duration - endTs) /
traceSummary.duration * 100}%`}
- height={graphHeight}
- x={`${endTs / traceSummary.duration * 100}%`}
- y="0"
- fill="rgba(50, 50, 50, 0.2)"
- />
- )
- : null
- }
- </svg>
- </div>
- </div>
- );
- }
-}
-
MiniTimeline.propTypes = propTypes;
export default MiniTimeline;
diff --git a/zipkin-lens/src/components/MiniTimeline/index.test.js
b/zipkin-lens/src/components/MiniTimeline/index.test.js
new file mode 100644
index 0000000..1139bdf
--- /dev/null
+++ b/zipkin-lens/src/components/MiniTimeline/index.test.js
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 MiniTimeline from './index';
+
+describe('<MiniTimeline />', () => {
+ const commonProps = {
+ startTs: 0,
+ endTs: 10,
+ onStartAndEndTsChange: () => {},
+ };
+
+ const dummySpan = {
+ spanId: '1',
+ spanName: 'span',
+ parentId: '2',
+ childIds: [],
+ serviceName: 'service',
+ serviceNames: [],
+ timestamp: 0,
+ duration: 10,
+ durationStr: '10μs',
+ tags: [],
+ annotations: [],
+ errorType: 'none',
+ depth: 1,
+ width: 1,
+ left: 1,
+ };
+
+ it('should return null if the number of spans is less than 2', () => {
+ let props = {
+ ...commonProps,
+ traceSummary: {
+ traceId: '12345',
+ spans: [],
+ serviceNameAndSpanCounts: [],
+ duration: 10,
+ durationStr: '10μs',
+ },
+ };
+ let wrapper = shallow(<MiniTimeline {...props} />);
+ expect(wrapper.type()).toEqual(null);
+
+ props = {
+ ...commonProps,
+ traceSummary: {
+ traceId: '12345',
+ spans: [dummySpan],
+ serviceNameAndSpanCounts: [],
+ duration: 10,
+ durationStr: '10μs',
+ },
+ };
+ wrapper = shallow(<MiniTimeline {...props} />);
+ expect(wrapper.type()).toEqual(null);
+ });
+
+ it('should return mini timeline otherwise', () => {
+ const props = {
+ ...commonProps,
+ traceSummary: {
+ traceId: '12345',
+ spans: [dummySpan, dummySpan],
+ serviceNameAndSpanCounts: [],
+ duration: 10,
+ durationStr: '10μs',
+ },
+ };
+ const wrapper = shallow(<MiniTimeline {...props} />);
+ expect(wrapper.find('.mini-timeline').length).toBe(1);
+ });
+});
diff --git a/zipkin-lens/src/components/MiniTimeline/util.js
b/zipkin-lens/src/components/MiniTimeline/util.js
new file mode 100644
index 0000000..847c851
--- /dev/null
+++ b/zipkin-lens/src/components/MiniTimeline/util.js
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+export const getGraphHeight = (numSpans) => {
+ if (numSpans <= 1) {
+ return 0;
+ }
+ if (numSpans <= 14) {
+ return numSpans * 5;
+ }
+ return 75;
+};
+
+export const getGraphLineHeight = (numSpans) => {
+ if (numSpans <= 1) {
+ return 0;
+ }
+ if (numSpans <= 14) {
+ return 5;
+ }
+ return Math.max(75 / numSpans, 1);
+};
diff --git a/zipkin-lens/src/components/MiniTimeline/util.test.js
b/zipkin-lens/src/components/MiniTimeline/util.test.js
new file mode 100644
index 0000000..bd1423e
--- /dev/null
+++ b/zipkin-lens/src/components/MiniTimeline/util.test.js
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 { getGraphHeight, getGraphLineHeight } from './util';
+
+describe('getGraphHeight', () => {
+ it('should return proper value', () => {
+ expect(getGraphHeight(-1)).toEqual(0);
+ expect(getGraphHeight(0)).toEqual(0);
+ expect(getGraphHeight(1)).toEqual(0);
+ expect(getGraphHeight(2)).toEqual(2 * 5);
+ expect(getGraphHeight(14)).toEqual(14 * 5);
+ expect(getGraphHeight(15)).toEqual(75);
+ expect(getGraphHeight(16)).toEqual(75);
+ expect(getGraphHeight(100)).toEqual(75);
+ });
+});
+
+describe('getGraphLineHeight', () => {
+ it('should return proper value', () => {
+ expect(getGraphLineHeight(-1)).toEqual(0);
+ expect(getGraphLineHeight(0)).toEqual(0);
+ expect(getGraphLineHeight(1)).toEqual(0);
+ expect(getGraphLineHeight(2)).toEqual(5);
+ expect(getGraphLineHeight(14)).toEqual(5);
+ expect(getGraphLineHeight(15)).toEqual(5);
+ expect(getGraphLineHeight(16)).toEqual(75 / 16);
+ expect(getGraphLineHeight(20)).toEqual(75 / 20);
+ expect(getGraphLineHeight(100)).toEqual(1);
+ });
+});