diff --git a/.flowconfig b/.flowconfig index 2d7b73caf6..6ebdd73701 100644 --- a/.flowconfig +++ b/.flowconfig @@ -13,6 +13,7 @@ ./flow-typed/npm [options] +suppress_comment= \\(.\\|\n\\)*\\$FlowIgnore [version] 0.71.0 diff --git a/packages/jaeger-ui/package.json b/packages/jaeger-ui/package.json index cee86b92d5..b6e80674b7 100644 --- a/packages/jaeger-ui/package.json +++ b/packages/jaeger-ui/package.json @@ -19,8 +19,8 @@ "babel-plugin-import": "1.11.0", "bluebird": "^3.5.0", "customize-cra": "0.2.9", - "enzyme": "^3.2.0", - "enzyme-adapter-react-16": "^1.1.0", + "enzyme": "^3.8.0", + "enzyme-adapter-react-16": "^1.2.0", "enzyme-to-json": "^3.3.0", "http-proxy-middleware": "^0.19.1", "less": "3.9.0", diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiff.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiff.js index 15fa072b2d..569877873c 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiff.js +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiff.js @@ -48,22 +48,22 @@ type State = { graphTopOffset: number, }; -function syncStates(urlSt, reduxSt, forceState) { - const { a: urlA, b: urlB } = urlSt; - const { a: reduxA, b: reduxB } = reduxSt; +function syncStates(urlValues, reduxValues, forceState) { + const { a: urlA, b: urlB } = urlValues; + const { a: reduxA, b: reduxB } = reduxValues; if (urlA !== reduxA || urlB !== reduxB) { - forceState(urlSt); + forceState(urlValues); return; } - const urlCohort = new Set(urlSt.cohort || []); - const reduxCohort = new Set(reduxSt.cohort || []); + const urlCohort = new Set(urlValues.cohort); + const reduxCohort = new Set(reduxValues.cohort || []); if (urlCohort.size !== reduxCohort.size) { - forceState(urlSt); + forceState(urlValues); return; } const needSync = Array.from(urlCohort).some(id => !reduxCohort.has(id)); if (needSync) { - forceState(urlSt); + forceState(urlValues); } } diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiff.test.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiff.test.js new file mode 100644 index 0000000000..2f216c6797 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiff.test.js @@ -0,0 +1,372 @@ +// Copyright (c) 2019 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import queryString from 'query-string'; +import * as redux from 'redux'; + +import { mapStateToProps, mapDispatchToProps, TraceDiffImpl } from './TraceDiff'; +import TraceDiffHeader from './TraceDiffHeader'; +import { actions as diffActions } from './duck'; +import * as TraceDiffUrl from './url'; +import * as jaegerApiActions from '../../actions/jaeger-api'; +import { fetchedState, TOP_NAV_HEIGHT } from '../../constants'; + +describe('TraceDiff', () => { + const defaultA = 'trace-id-a'; + const defaultB = 'trace-id-b'; + const defaultCohortIds = ['trace-id-cohort-0', 'trace-id-cohort-1', 'trace-id-cohort-2']; + const defaultCohort = [defaultA, defaultB, ...defaultCohortIds]; + const fetchMultipleTracesMock = jest.fn(); + const forceStateMock = jest.fn(); + const historyPushMock = jest.fn(); + const defaultProps = { + a: defaultA, + b: defaultB, + cohort: defaultCohort, + fetchMultipleTraces: fetchMultipleTracesMock, + forceState: forceStateMock, + history: { + push: historyPushMock, + }, + tracesData: new Map(defaultCohort.map(id => [id, { id, state: fetchedState.DONE }])), + traceDiffState: { + a: defaultA, + b: defaultB, + cohort: defaultCohort, + }, + }; + const newAValue = 'newAValue'; + const newBValue = 'newBValue'; + const nonDefaultCohortId = 'non-default-cohort-id'; + const getUrlSpyMockReturnValue = 'getUrlSpyMockReturnValue'; + let getUrlSpy; + let wrapper; + + beforeAll(() => { + getUrlSpy = jest.spyOn(TraceDiffUrl, 'getUrl').mockReturnValue(getUrlSpyMockReturnValue); + }); + + beforeEach(() => { + fetchMultipleTracesMock.mockClear(); + forceStateMock.mockClear(); + getUrlSpy.mockClear(); + historyPushMock.mockClear(); + wrapper = shallow(); + }); + + describe('syncStates', () => { + it('forces state if a is inconsistent between url and reduxState', () => { + wrapper.setProps({ a: newAValue }); + expect(forceStateMock).toHaveBeenLastCalledWith({ + a: newAValue, + b: defaultProps.b, + cohort: defaultProps.cohort, + }); + }); + + it('forces state if b is inconsistent between url and reduxState', () => { + wrapper.setProps({ b: newBValue }); + expect(forceStateMock).toHaveBeenLastCalledWith({ + a: defaultProps.a, + b: newBValue, + cohort: defaultProps.cohort, + }); + }); + + it('forces state if cohort size has changed', () => { + const newCohort = [...defaultProps.cohort, nonDefaultCohortId]; + wrapper.setProps({ cohort: newCohort }); + expect(forceStateMock).toHaveBeenLastCalledWith({ + a: defaultProps.a, + b: defaultProps.b, + cohort: newCohort, + }); + + wrapper.setProps({ + cohort: defaultProps.cohort, + traceDiffState: { ...defaultProps.traceDiffState, cohort: null }, + }); + expect(forceStateMock).toHaveBeenLastCalledWith({ + a: defaultProps.a, + b: defaultProps.b, + cohort: defaultProps.cohort, + }); + }); + + it('forces state if cohort entry has changed', () => { + const newCohort = [...defaultProps.cohort.slice(1), nonDefaultCohortId]; + wrapper.setProps({ cohort: newCohort }); + expect(forceStateMock).toHaveBeenLastCalledWith({ + a: defaultProps.a, + b: defaultProps.b, + cohort: newCohort, + }); + }); + + it('does not force state if cohorts have same values in differing orders', () => { + wrapper.setProps({ + traceDiffState: { + ...defaultProps.traceDiffState, + cohort: defaultProps.traceDiffState.cohort.slice().reverse(), + }, + }); + expect(forceStateMock).not.toHaveBeenCalled(); + }); + }); + + it('requests traces lacking a state', () => { + const newId0 = 'new-id-0'; + const newId1 = 'new-id-1'; + expect(fetchMultipleTracesMock).toHaveBeenCalledTimes(0); + wrapper.setProps({ cohort: [...defaultProps.cohort, newId0, newId1] }); + expect(fetchMultipleTracesMock).toHaveBeenCalledWith([newId0, newId1]); + expect(fetchMultipleTracesMock).toHaveBeenCalledTimes(1); + }); + + it('does not request traces if all traces have a state', () => { + const newId0 = 'new-id-0'; + const newId1 = 'new-id-1'; + expect(fetchMultipleTracesMock).toHaveBeenCalledTimes(0); + const cohort = [...defaultProps.cohort, newId0, newId1]; + const tracesData = new Map(defaultProps.tracesData); + tracesData.set(newId0, { id: newId0, state: fetchedState.ERROR }); + tracesData.set(newId1, { id: newId0, state: fetchedState.LOADING }); + wrapper.setProps({ cohort, tracesData }); + expect(fetchMultipleTracesMock).not.toHaveBeenCalled(); + }); + + it('updates url when TraceDiffHeader sets a or b', () => { + wrapper.find(TraceDiffHeader).prop('diffSetA')(newAValue); + expect(getUrlSpy).toHaveBeenLastCalledWith({ + a: newAValue.toLowerCase(), + b: defaultProps.b, + cohort: defaultProps.cohort, + }); + + wrapper.find(TraceDiffHeader).prop('diffSetB')(newBValue); + expect(getUrlSpy).toHaveBeenLastCalledWith({ + a: defaultProps.a, + b: newBValue.toLowerCase(), + cohort: defaultProps.cohort, + }); + + wrapper.find(TraceDiffHeader).prop('diffSetA')(''); + expect(getUrlSpy).toHaveBeenLastCalledWith({ + a: defaultProps.a, + b: defaultProps.b, + cohort: defaultProps.cohort, + }); + + wrapper.find(TraceDiffHeader).prop('diffSetB')(''); + expect(getUrlSpy).toHaveBeenLastCalledWith({ + a: defaultProps.a, + b: defaultProps.b, + cohort: defaultProps.cohort, + }); + + expect(historyPushMock).toHaveBeenCalledTimes(4); + }); + + describe('render', () => { + it('renders as expected', () => { + expect(wrapper).toMatchSnapshot(); + }); + + it('handles a and b not in props.tracesData', () => { + const tracesData = new Map(defaultProps.tracesData); + tracesData.delete(defaultA); + tracesData.delete(defaultB); + wrapper.setProps({ tracesData }); + expect(wrapper.find(TraceDiffHeader).props()).toEqual( + expect.objectContaining({ + a: { id: defaultA }, + b: { id: defaultB }, + }) + ); + }); + + it('handles absent a and b', () => { + wrapper.setProps({ a: null, b: null }); + expect(wrapper.find(TraceDiffHeader).props()).toEqual(expect.objectContaining({ a: null, b: null })); + }); + }); + + describe('TraceDiff--graphWrapper top offset', () => { + const arbitraryHeight = TOP_NAV_HEIGHT * 2; + + it('initializes as TOP_NAV_HEIGHT', () => { + expect(wrapper.state().graphTopOffset).toBe(TOP_NAV_HEIGHT); + }); + + it('defaults to TOP_NAV_HEIGHT', () => { + wrapper.setState({ graphTopOffset: arbitraryHeight }); + wrapper.instance().headerWrapperRef(null); + expect(wrapper.state().graphTopOffset).toBe(TOP_NAV_HEIGHT); + }); + + it('adjusts TraceDiff--graphWrapper top offset based on TraceDiffHeader height', () => { + wrapper.instance().headerWrapperRef({ clientHeight: arbitraryHeight }); + expect(wrapper.state().graphTopOffset).toBe(TOP_NAV_HEIGHT + arbitraryHeight); + }); + }); + + describe('mapStateToProps', () => { + const getOwnProps = ({ a = defaultA, b = defaultB } = {}) => ({ + match: { + params: { + a, + b, + }, + }, + }); + const makeTestReduxState = ({ cohortIds = defaultCohortIds } = {}) => ({ + router: { + location: { + search: queryString.stringify({ cohort: cohortIds }), + }, + }, + trace: { + traces: cohortIds.reduce((traces, id) => ({ ...traces, [id]: { id, state: fetchedState.DONE } }), {}), + }, + traceDiff: { + a: 'trace-diff-a', + b: 'trace-diff-b', + }, + }); + + it('gets a and b from ownProps', () => { + expect(mapStateToProps(makeTestReduxState(), getOwnProps())).toEqual( + expect.objectContaining({ + a: defaultA, + b: defaultB, + }) + ); + }); + + it('defaults cohort to empty array if a, b, and cohort are not available', () => { + expect( + mapStateToProps(makeTestReduxState({ cohortIds: [] }), getOwnProps({ a: null, b: null })).cohort + ).toEqual([]); + }); + + it('gets cohort from ownProps and state.router.location.search', () => { + expect(mapStateToProps(makeTestReduxState(), getOwnProps()).cohort).toEqual([ + defaultA, + defaultB, + ...defaultCohortIds, + ]); + }); + + it('filters falsy values from cohort', () => { + expect(mapStateToProps(makeTestReduxState(), getOwnProps({ a: null })).cohort).toEqual([ + defaultB, + ...defaultCohortIds, + ]); + + expect(mapStateToProps(makeTestReduxState(), getOwnProps({ b: null })).cohort).toEqual([ + defaultA, + ...defaultCohortIds, + ]); + + expect( + mapStateToProps( + makeTestReduxState({ cohortIds: [...defaultCohortIds, '', nonDefaultCohortId] }), + getOwnProps() + ).cohort + ).toEqual([defaultA, defaultB, ...defaultCohortIds, nonDefaultCohortId]); + }); + + it('filters redundant values from cohort', () => { + expect( + mapStateToProps( + makeTestReduxState({ cohortIds: [...defaultCohortIds, nonDefaultCohortId] }), + getOwnProps({ a: nonDefaultCohortId }) + ).cohort + ).toEqual([nonDefaultCohortId, defaultB, ...defaultCohortIds]); + + expect( + mapStateToProps( + makeTestReduxState({ cohortIds: [...defaultCohortIds, nonDefaultCohortId] }), + getOwnProps({ b: nonDefaultCohortId }) + ).cohort + ).toEqual([defaultA, nonDefaultCohortId, ...defaultCohortIds]); + + expect( + mapStateToProps( + makeTestReduxState({ cohortIds: [...defaultCohortIds, nonDefaultCohortId, nonDefaultCohortId] }), + getOwnProps() + ).cohort + ).toEqual([defaultA, defaultB, ...defaultCohortIds, nonDefaultCohortId]); + }); + + // This test may false negative if previous tests are failing + it('builds tracesData Map from cohort and state.trace.traces', () => { + const { tracesData, cohort: { length: expectedSize } } = mapStateToProps( + makeTestReduxState(), + getOwnProps() + ); + defaultCohortIds.forEach(id => { + expect(tracesData.get(id)).toEqual({ + id, + state: fetchedState.DONE, + }); + }); + expect(tracesData.get(defaultA)).toEqual({ + id: defaultA, + state: null, + }); + expect(tracesData.get(defaultB)).toEqual({ + id: defaultB, + state: null, + }); + expect(tracesData.size).toBe(expectedSize); + }); + + it('includes state.traceDiff as traceDiffState', () => { + const testReduxState = makeTestReduxState(); + const { traceDiffState } = mapStateToProps(testReduxState, getOwnProps()); + expect(traceDiffState).toBe(testReduxState.traceDiff); + }); + }); + + describe('mapDispatchToProps', () => { + let bindActionCreatorsSpy; + + beforeAll(() => { + bindActionCreatorsSpy = jest.spyOn(redux, 'bindActionCreators').mockImplementation(actions => { + if (actions === jaegerApiActions) { + return { fetchMultipleTraces: fetchMultipleTracesMock }; + } + if (actions === diffActions) { + return { forceState: forceStateMock }; + } + return {}; + }); + }); + + afterAll(() => { + bindActionCreatorsSpy.mockRestore(); + }); + + it('correctly binds actions to dispatch', () => { + const dispatchMock = () => {}; + const result = mapDispatchToProps(dispatchMock); + expect(result.fetchMultipleTraces).toBe(fetchMultipleTracesMock); + expect(result.forceState).toBe(forceStateMock); + expect(bindActionCreatorsSpy.mock.calls[0][1]).toBe(dispatchMock); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.css b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.css index 9d2353eb4b..113e3cb62d 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.css +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.css @@ -40,13 +40,33 @@ limitations under the License. } .TraceDiffGraph--dag { + transition: background 0.5s ease; stroke-width: 1.2; } +.TraceDiffGraph--dag.is-uiFind-mode { + background: #ddd; +} + .TraceDiffGraph--dag.is-small { stroke-width: 0.7; } +/* Find within diff */ +.TraceDiffGraph--uiFind { + bottom: 20px; + position: absolute; + right: 20px; + width: 300px; + z-index: 1; +} + +.TraceDiffGraph--uiFind > .ant-input { + border: 1px solid #aaa; + border-radius: 0; + box-shadow: 0 0 2px 2px #ccc; +} + /* DAG minimap */ .TraceDiffGraph--miniMap { diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.js index 2f182644f1..b8401b7a8b 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.js +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.js @@ -16,13 +16,16 @@ import * as React from 'react'; import { DirectedGraph, LayoutManager } from '@jaegertracing/plexus'; +import cx from 'classnames'; +import { connect } from 'react-redux'; -import drawNode from './drawNode'; +import drawNodeGenerator from './drawNode'; +import { getUiFindVertexKeys, getEdgesAndVertices } from './traceDiffGraphUtils'; import ErrorMessage from '../../common/ErrorMessage'; import LoadingIndicator from '../../common/LoadingIndicator'; +import UiFindInput, { extractUiFindFromState } from '../../common/UiFindInput'; import { fetchedState } from '../../../constants'; -import convPlexus from '../../../model/trace-dag/convPlexus'; -import TraceDag from '../../../model/trace-dag/TraceDag'; +import { setOnEdgesContainer, setOnNodesContainer, setOnNode } from '../../../utils/plexus/set-on-graph'; import type { FetchedTrace } from '../../../types'; @@ -31,25 +34,19 @@ import './TraceDiffGraph.css'; type Props = { a: ?FetchedTrace, b: ?FetchedTrace, + uiFind?: string, }; const { classNameIsSmall } = DirectedGraph.propsFactories; -function setOnEdgesContainer(state: Object) { - const { zoomTransform } = state; - if (!zoomTransform) { - return null; - } - const { k } = zoomTransform; - const opacity = 0.1 + k * 0.9; - return { style: { opacity } }; -} - -export default class TraceDiffGraph extends React.PureComponent { +export class UnconnectedTraceDiffGraph extends React.PureComponent { props: Props; - layoutManager: LayoutManager; + static defaultProps = { + uiFind: '', + }; + constructor(props: Props) { super(props); this.layoutManager = new LayoutManager({ useDotEdges: true, splines: 'polyline' }); @@ -60,7 +57,12 @@ export default class TraceDiffGraph extends React.PureComponent { } render() { - const { a, b } = this.props; + const { + a, + b, + // Flow requires `= ''` because it does not interpret defaultProps + uiFind = '', + } = this.props; if (!a || !b) { return

At least two Traces are needed

; } @@ -92,10 +94,9 @@ export default class TraceDiffGraph extends React.PureComponent { if (!aData || !bData) { return
; } - const aTraceDag = TraceDag.newFromTrace(aData); - const bTraceDag = TraceDag.newFromTrace(bData); - const diffDag = TraceDag.diff(aTraceDag, bTraceDag); - const { edges, vertices } = convPlexus(diffDag.nodesMap); + const { edges, vertices } = getEdgesAndVertices(aData, bData); + const keys = getUiFindVertexKeys(uiFind, vertices); + const dagClassName = cx('TraceDiffGraph--dag', { 'is-uiFind-mode': uiFind }); return (
@@ -103,16 +104,26 @@ export default class TraceDiffGraph extends React.PureComponent { minimap zoom arrowScaleDampener={0} - className="TraceDiffGraph--dag" + className={dagClassName} minimapClassName="TraceDiffGraph--miniMap" layoutManager={this.layoutManager} - getNodeLabel={drawNode} + getNodeLabel={drawNodeGenerator(keys)} setOnRoot={classNameIsSmall} setOnEdgesContainer={setOnEdgesContainer} + setOnNodesContainer={setOnNodesContainer} + setOnNode={setOnNode} edges={edges} vertices={vertices} /> +
); } } + +export default connect(extractUiFindFromState)(UnconnectedTraceDiffGraph); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.test.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.test.js new file mode 100644 index 0000000000..28bf70d558 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.test.js @@ -0,0 +1,155 @@ +// Copyright (c) 2019 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 * as React from 'react'; +import { shallow } from 'enzyme'; +// import _mapValues from 'lodash/mapValues'; + +import { UnconnectedTraceDiffGraph as TraceDiffGraph } from './TraceDiffGraph'; +import ErrorMessage from '../../common/ErrorMessage'; +import LoadingIndicator from '../../common/LoadingIndicator'; +import { fetchedState } from '../../../constants'; + +describe('TraceDiffGraph', () => { + const props = { + a: { + data: { + spans: [], + traceID: 'trace-id-a', + }, + error: null, + id: 'trace-id-a', + state: fetchedState.DONE, + }, + b: { + data: { + spans: [], + traceID: 'trace-id-b', + }, + error: null, + id: 'trace-id-b', + state: fetchedState.DONE, + }, + }; + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('renders warning when a or b are not provided', () => { + expect(wrapper.find('h1').length).toBe(0); + + wrapper.setProps({ a: undefined }); + expect(wrapper.find('h1').length).toBe(1); + expect(wrapper.find('h1').text()).toBe('At least two Traces are needed'); + + wrapper.setProps({ b: undefined }); + expect(wrapper.find('h1').length).toBe(1); + expect(wrapper.find('h1').text()).toBe('At least two Traces are needed'); + + wrapper.setProps({ a: props.a }); + expect(wrapper.find('h1').length).toBe(1); + expect(wrapper.find('h1').text()).toBe('At least two Traces are needed'); + }); + + it('renders warning when a or b have errored', () => { + expect(wrapper.find(ErrorMessage).length).toBe(0); + + const errorA = 'some error text for trace a'; + wrapper.setProps({ + a: { + ...props.a, + error: errorA, + }, + }); + + expect(wrapper.find(ErrorMessage).length).toBe(1); + expect(wrapper.find(ErrorMessage).props()).toEqual( + expect.objectContaining({ + error: errorA, + }) + ); + const errorB = 'some error text for trace a'; + wrapper.setProps({ + b: { + ...props.b, + error: errorB, + }, + }); + + expect(wrapper.find(ErrorMessage).length).toBe(2); + expect( + wrapper + .find(ErrorMessage) + .at(1) + .props() + ).toEqual( + expect.objectContaining({ + error: errorB, + }) + ); + wrapper.setProps({ + a: props.a, + }); + expect(wrapper.find(ErrorMessage).length).toBe(1); + expect(wrapper.find(ErrorMessage).props()).toEqual( + expect.objectContaining({ + error: errorB, + }) + ); + }); + + it('renders a loading indicator when a or b are loading', () => { + expect(wrapper.find(LoadingIndicator).length).toBe(0); + + wrapper.setProps({ + a: { + state: fetchedState.LOADING, + }, + }); + expect(wrapper.find(LoadingIndicator).length).toBe(1); + + wrapper.setProps({ + b: { + state: fetchedState.LOADING, + }, + }); + expect(wrapper.find(LoadingIndicator).length).toBe(1); + + wrapper.setProps({ a: props.a }); + expect(wrapper.find(LoadingIndicator).length).toBe(1); + }); + + it('renders an empty div when a or b lack data', () => { + expect(wrapper.children().length).not.toBe(0); + + const { data: unusedAData, ...aWithoutData } = props.a; + wrapper.setProps({ a: aWithoutData }); + expect(wrapper.children().length).toBe(0); + + const { data: unusedBData, ...bWithoutData } = props.b; + wrapper.setProps({ b: bWithoutData }); + expect(wrapper.children().length).toBe(0); + + wrapper.setProps({ a: props.a }); + expect(wrapper.children().length).toBe(0); + }); + + it('cleans up layoutManager before unmounting', () => { + const layoutManager = jest.spyOn(wrapper.instance().layoutManager, 'stopAndRelease'); + wrapper.unmount(); + expect(layoutManager).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/__snapshots__/drawNode.test.js.snap b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/__snapshots__/drawNode.test.js.snap new file mode 100644 index 0000000000..7b8d1ccb15 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/__snapshots__/drawNode.test.js.snap @@ -0,0 +1,677 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`drawNode diffNode renders as expected when props.a and props.b are the same 1`] = ` + + + + + 100 + + + + serviceName + + + + + + + operationName + + + + + } + mouseEnterDelay={0.25} + mouseLeaveDelay={0.1} + overlayClassName="DiffNode--popover is-same" + overlayStyle={Object {}} + placement="top" + prefixCls="ant-popover" + transitionName="zoom-big" + trigger="hover" +> + + + + + + + + + + +
+ 100 + + + serviceName + + +
+ operationName +
+
+`; + +exports[`drawNode diffNode renders as expected when props.a is 0 1`] = ` + + + + + + + + + 100 + + + + serviceName + + + + + + + + + + + 100 + + % + + + + operationName + + + + + } + mouseEnterDelay={0.25} + mouseLeaveDelay={0.1} + overlayClassName="DiffNode--popover is-changed is-added" + overlayStyle={Object {}} + placement="top" + prefixCls="ant-popover" + transitionName="zoom-big" + trigger="hover" +> + + + + + + + + + + + +
+ + + + + 100 + + + serviceName + + +
+ + + + + 100 + + % + + + operationName +
+
+`; + +exports[`drawNode diffNode renders as expected when props.a is less than props.b 1`] = ` + + + + + + + + + 50 + + + + serviceName + + + + + + + + + + + 50 + + % + + + + operationName + + + + + } + mouseEnterDelay={0.25} + mouseLeaveDelay={0.1} + overlayClassName="DiffNode--popover is-changed is-more" + overlayStyle={Object {}} + placement="top" + prefixCls="ant-popover" + transitionName="zoom-big" + trigger="hover" +> + + + + + + + + + + + +
+ + + + + 50 + + + serviceName + + +
+ + + + + 50 + + % + + + operationName +
+
+`; + +exports[`drawNode diffNode renders as expected when props.a is more than props.b 1`] = ` + + + + + + - + + 100 + + + + serviceName + + + + + + + + - + + 50 + + % + + + + operationName + + + + + } + mouseEnterDelay={0.25} + mouseLeaveDelay={0.1} + overlayClassName="DiffNode--popover is-changed is-less" + overlayStyle={Object {}} + placement="top" + prefixCls="ant-popover" + transitionName="zoom-big" + trigger="hover" +> + + + + + + + + + + + +
+ + - + + 100 + + + serviceName + + +
+ + - + + 50 + + % + + + operationName +
+
+`; + +exports[`drawNode diffNode renders as expected when props.b is 0 1`] = ` + + + + + + - + + 100 + + + + serviceName + + + + + + + + - + + 100 + + % + + + + operationName + + + + + } + mouseEnterDelay={0.25} + mouseLeaveDelay={0.1} + overlayClassName="DiffNode--popover is-changed is-removed" + overlayStyle={Object {}} + placement="top" + prefixCls="ant-popover" + transitionName="zoom-big" + trigger="hover" +> + + + + + + + + + + + +
+ + - + + 100 + + + serviceName + + +
+ + - + + 100 + + % + + + operationName +
+
+`; + +exports[`drawNode diffNode renders as expected when props.isUiFindMatch is true 1`] = ` + + + + + 100 + + + + serviceName + + + + + + + operationName + + + + + } + mouseEnterDelay={0.25} + mouseLeaveDelay={0.1} + overlayClassName="DiffNode--popover is-same is-ui-find-match" + overlayStyle={Object {}} + placement="top" + prefixCls="ant-popover" + transitionName="zoom-big" + trigger="hover" +> + + + + + + + + + + +
+ 100 + + + serviceName + + +
+ operationName +
+
+`; diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.css b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.css index e89c92416f..f6e703c07d 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.css +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.css @@ -17,13 +17,14 @@ limitations under the License. .DiffNode { background: #bbb; border: 1px solid #777; - box-shadow: 0 0px 3px rgba(0, 0, 0, 0.2); + box-shadow: 0 0px 3px 1px rgba(0, 0, 0, 0.2); cursor: pointer; white-space: nowrap; } -.TraceDiffGraph--dag.is-small .DiffNode > tbody { - opacity: 0; +.DiffNode.is-ui-find-match { + outline: inherit; + outline-color: #fff3d7; } .DiffNode.is-changed { @@ -52,6 +53,14 @@ limitations under the License. color: #fff; } +.TraceDiffGraph--dag.is-small .DiffNode--body { + opacity: 0; +} + +.DiffNode--popover .DiffNode.is-ui-find-match { + outline: #fff3d7 solid 3px; +} + .DiffNode--metricCell { padding: 0.3rem 0.5rem; background: rgba(255, 255, 255, 0.3); @@ -78,7 +87,7 @@ limitations under the License. .DiffNode--popover .DiffNode--copyIcon, .DiffNode:not(:hover) .DiffNode--copyIcon { - display: none; + color: transparent; } /* Tweak the popover aesthetics - unfortunate but necessary */ diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.js index 1b7bc97b4b..fc33671812 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.js +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.js @@ -27,6 +27,7 @@ import './drawNode.css'; type Props = { a: number, b: number, + isUiFindMatch: boolean, operation: string, service: string, }; @@ -34,11 +35,9 @@ type Props = { const abs = Math.abs; const max = Math.max; -class DiffNode extends React.PureComponent { - props: Props; - +export class DiffNode extends React.PureComponent { render() { - const { a, b, operation, service } = this.props; + const { a, b, isUiFindMatch, operation, service } = this.props; const isSame = a === b; const className = cx({ 'is-same': isSame, @@ -47,11 +46,12 @@ class DiffNode extends React.PureComponent { 'is-added': a === 0, 'is-less': a > b && b > 0, 'is-removed': b === 0, + 'is-ui-find-match': isUiFindMatch, }); const chgSign = a < b ? '+' : '-'; const table = ( - +
{isSame ? null : {chgSign}} @@ -88,7 +88,21 @@ class DiffNode extends React.PureComponent { } } -export default function drawNode(vertex: PVertex) { - const { data, operation, service } = vertex.data; - return ; +function drawNode(vertex: PVertex, keys: Set) { + const { data, members, operation, service } = vertex.data; + return ( + + ); +} + +export default function drawNodeGenerator(keys: Set) { + return function drawVertex(vertex: PVertex) { + return drawNode(vertex, keys); + }; } diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.test.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.test.js new file mode 100644 index 0000000000..a4796ee53f --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.test.js @@ -0,0 +1,111 @@ +// Copyright (c) 2019 The Jaeger Authors. +// +// 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 drawNodeGenerator, { DiffNode } from './drawNode'; + +describe('drawNode', () => { + const members = [ + { + span: { + spanID: 'members-span-id-0', + }, + }, + { + span: { + spanID: 'members-span-id-1', + }, + }, + ]; + const operation = 'operationName'; + const service = 'serviceName'; + describe('diffNode', () => { + const defaultCount = 100; + const props = { + a: defaultCount, + b: defaultCount, + members, + operation, + service, + }; + + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('renders as expected when props.a and props.b are the same', () => { + expect(wrapper).toMatchSnapshot(); + }); + + it('renders as expected when props.a is less than props.b', () => { + wrapper.setProps({ a: defaultCount / 2 }); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders as expected when props.a is more than props.b', () => { + wrapper.setProps({ a: defaultCount * 2 }); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders as expected when props.a is 0', () => { + wrapper.setProps({ a: 0 }); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders as expected when props.b is 0', () => { + wrapper.setProps({ b: 0 }); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders as expected when props.isUiFindMatch is true', () => { + wrapper.setProps({ isUiFindMatch: true }); + expect(wrapper).toMatchSnapshot(); + }); + }); + + describe('drawNode function', () => { + const dataKey = 'data-key'; + const dataValue = 'data-value'; + const key = 'vertex key'; + const vertex = { + data: { + data: { + [dataKey]: dataValue, + }, + members, + operation, + service, + }, + key, + }; + + it('extracts values from vertex.data', () => { + const drawNodeResult = drawNodeGenerator(new Set())(vertex); + expect(drawNodeResult.props[dataKey]).toBe(dataValue); + expect(drawNodeResult.props.isUiFindMatch).toBe(false); + expect(drawNodeResult.props.members).toBe(members); + expect(drawNodeResult.props.operation).toBe(operation); + expect(drawNodeResult.props.service).toBe(service); + }); + + it('passes isUiFindMatch as true if key is in set', () => { + const drawNodeResult = drawNodeGenerator(new Set([key]))(vertex); + expect(drawNodeResult.props.isUiFindMatch).toBe(true); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/traceDiffGraphUtils.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/traceDiffGraphUtils.js new file mode 100644 index 0000000000..be1a1702a6 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/traceDiffGraphUtils.js @@ -0,0 +1,64 @@ +// Copyright (c) 2019 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 _get from 'lodash/get'; +import _map from 'lodash/map'; + +import convPlexus from '../../../model/trace-dag/convPlexus'; +import TraceDag from '../../../model/trace-dag/TraceDag'; +import filterSpans from '../../../utils/filter-spans'; + +import type { PVertex } from '../../../model/trace-dag/types'; +import type { Trace } from '../../../types/trace'; + +export type TVertexKeys = Set; + +let lastUiFind: string; +let lastVertices: PVertex[]; +let uiFindVertexKeys: ?TVertexKeys; + +export function getUiFindVertexKeys(uiFind: string, vertices: PVertex[]): TVertexKeys { + if (!uiFind) return new Set(); + if (uiFind === lastUiFind && vertices === lastVertices && uiFindVertexKeys) { + return uiFindVertexKeys; + } + const newVertexKeys: Set = new Set(); + vertices.forEach(({ key, data: { members } }) => { + if (_get(filterSpans(uiFind, _map(members, 'span')), 'size')) { + newVertexKeys.add(key); + } + }); + lastUiFind = uiFind; + lastVertices = vertices; + uiFindVertexKeys = newVertexKeys; + return newVertexKeys; +} + +let lastAData: ?Trace; +let lastBData: ?Trace; +// TODO: use convPlexus type (everett JAG-343) +let edgesAndVertices: ?Object; + +export function getEdgesAndVertices(aData: Trace, bData: Trace) { + if (aData === lastAData && bData === lastBData && edgesAndVertices) { + return edgesAndVertices; + } + lastAData = aData; + lastBData = bData; + const aTraceDag = TraceDag.newFromTrace(aData); + const bTraceDag = TraceDag.newFromTrace(bData); + const diffDag = TraceDag.diff(aTraceDag, bTraceDag); + edgesAndVertices = convPlexus(diffDag.nodesMap); + return edgesAndVertices; +} diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.js index e3a44153ad..d4fd088fe1 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.js +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.js @@ -43,7 +43,7 @@ const defaultRowSelection = { type: 'radio', }; -const NEED_MORE_TRACES_MESSAGE = ( +export const NEED_MORE_TRACES_MESSAGE = (

Enter a Trace ID or perform a search and select from the results.

diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.test.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.test.js new file mode 100644 index 0000000000..0a7d03b9e9 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.test.js @@ -0,0 +1,229 @@ +// Copyright (c) 2019 The Jaeger Authors. +// +// 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 { Table, Tag } from 'antd'; + +import CohortTable, { NEED_MORE_TRACES_MESSAGE } from './CohortTable'; +import TraceTimelineLink from './TraceTimelineLink'; +import RelativeDate from '../../common/RelativeDate'; +import TraceName from '../../common/TraceName'; +import { fetchedState } from '../../../constants'; +import * as dateUtils from '../../../utils/date'; + +const { Column } = Table; + +describe('CohortTable', () => { + const cohort = [ + { + data: { + traceName: 'trace name 0', + }, + error: 'api error', + id: 'trace-id-0', + state: fetchedState.ERROR, + }, + { + id: 'trace-id-1', + }, + { + id: 'trace-id-2', + }, + ]; + const selectTrace = jest.fn(); + const props = { + cohort, + current: cohort[0].id, + selection: { + [cohort[0].id]: { + label: 'selected index 0', + }, + }, + selectTrace, + }; + + let formatDurationSpy; + let wrapper; + + /** + * Creates a new wrapper with default props and specified props. It is necessary to create a new wrapper + * when props change because enzyme does not support wrapper.setProps for classes that render an array of + * elements. + * + * @param {Object} [specifiedProps={}] - Props to set that are different from props defined above. + * @returns {Object} - New wrapper. + */ + function updateWrapper(specifiedProps = {}) { + wrapper = shallow(); + } + + function getRowRenderer(dataIndex, fromData = true) { + return wrapper + .find(Column) + .find(`[dataIndex="${fromData ? 'data.' : ''}${dataIndex}"]`) + .prop('render'); + } + + beforeAll(() => { + formatDurationSpy = jest.spyOn(dateUtils, 'formatDuration'); + }); + + beforeEach(() => { + selectTrace.mockReset(); + formatDurationSpy.mockReset(); + updateWrapper(); + }); + + it('renders as expected', () => { + expect(wrapper).toMatchSnapshot(); + }); + + describe('row selection', () => { + let rowSelection; + + function updateRowSelection() { + rowSelection = wrapper.find(Table).prop('rowSelection'); + } + + beforeEach(() => { + updateRowSelection(); + }); + + it('defaults selectedRowKeys to empty array', () => { + updateWrapper({ current: undefined }); + updateRowSelection(); + expect(rowSelection.selectedRowKeys).toEqual([]); + }); + + it('calls props.selectTrace on row selection', () => { + rowSelection.onChange([cohort[1].id, cohort[2].id]); + expect(selectTrace).toHaveBeenCalledWith(cohort[1].id); + }); + + it('calculates checkbox props for selected and current record with error', () => { + expect(rowSelection.getCheckboxProps(cohort[0])).toEqual({ disabled: true }); + }); + + it('calculates checkbox props for selected and current record without error', () => { + expect(rowSelection.getCheckboxProps({ ...cohort[0], state: fetchedState.DONE })).toEqual({}); + }); + + it('calculates checkbox props for selected but not current record without error', () => { + updateWrapper({ + selection: { + ...props.selecetion, + [cohort[1].id]: { + label: 'selected index 1', + }, + }, + }); + updateRowSelection(); + expect(rowSelection.getCheckboxProps(cohort[1])).toEqual({ disabled: true }); + }); + + it('calculates checkbox props for not selected record', () => { + expect(rowSelection.getCheckboxProps(cohort[1])).toEqual({}); + }); + }); + + it('renders shortened id', () => { + const idRenderer = getRowRenderer('id', false); + const traceID = 'trace-id-longer-than-eight-characters'; + const renderedId = shallow(idRenderer(traceID)); + expect(renderedId.text()).toBe(traceID.slice(0, 7)); + }); + + it('renders TraceName fragment when given complete data', () => { + const traceNameColumnRenderer = getRowRenderer('traceName'); + const testTrace = cohort[0]; + const { id, error, state, data: { traceName } } = testTrace; + const renderedTraceNameColumn = shallow( + // traceNameRenderer returns a React Fragment, wrapper div helps enzyme +
{traceNameColumnRenderer('unused argument', testTrace)}
+ ); + + const tag = renderedTraceNameColumn.find(Tag); + expect(tag.length).toBe(1); + expect(tag.html().includes(props.selection[id].label)).toBe(true); + + const renderedTraceName = renderedTraceNameColumn.find(TraceName); + expect(renderedTraceName.length).toBe(1); + expect(renderedTraceName.props()).toEqual( + expect.objectContaining({ + error, + state, + traceName, + }) + ); + }); + + it('renders TraceName fragment when given minimal data', () => { + const traceNameColumnRenderer = getRowRenderer('traceName'); + const testTrace = cohort[1]; + const renderedTraceNameColumn = shallow( + // traceNameRenderer returns a React Fragment, wrapper div helps enzyme +
{traceNameColumnRenderer('unused argument', testTrace)}
+ ); + + expect(renderedTraceNameColumn.find(Tag).length).toBe(0); + expect(renderedTraceNameColumn.find(TraceName).length).toBe(1); + }); + + it('renders date iff record state is fetchedState.DONE', () => { + const dateRenderer = getRowRenderer('startTime'); + const date = 1548689901403; + + expect(dateRenderer(date, { state: fetchedState.ERROR })).toBe(false); + const renderedDate = dateRenderer(date, { state: fetchedState.DONE }); + expect(renderedDate.type).toBe(RelativeDate); + expect(renderedDate.props).toEqual({ + fullMonthName: true, + includeTime: true, + value: date / 1000, + }); + }); + + it('renders duration iff record state is fetchedState.DONE', () => { + const durationRenderer = getRowRenderer('duration'); + const duration = 150; + const formatDurationSpyMockReturnValue = 'formatDurationSpyMockReturnValue'; + formatDurationSpy.mockReturnValue(formatDurationSpyMockReturnValue); + + expect(durationRenderer(duration, { state: fetchedState.ERROR })).toBe(false); + expect(formatDurationSpy).toHaveBeenCalledTimes(0); + + expect(durationRenderer(duration, { state: fetchedState.DONE })).toBe(formatDurationSpyMockReturnValue); + expect(formatDurationSpy).toHaveBeenCalledTimes(1); + expect(formatDurationSpy).toHaveBeenCalledWith(duration); + }); + + it('renders link', () => { + const linkRenderer = getRowRenderer('traceID'); + const traceID = 'trace-id'; + const renderedLink = linkRenderer(traceID); + expect(renderedLink.type).toBe(TraceTimelineLink); + expect(renderedLink.props).toEqual({ + traceID, + }); + }); + + it('renders NEED_MORE_TRACES_MESSAGE if cohort is too small', () => { + expect(wrapper.contains(NEED_MORE_TRACES_MESSAGE)).toBe(false); + updateWrapper({ cohort: cohort.slice(0, 1) }); + expect(wrapper.contains(NEED_MORE_TRACES_MESSAGE)).toBe(true); + updateWrapper({ cohort: [] }); + expect(wrapper.contains(NEED_MORE_TRACES_MESSAGE)).toBe(true); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceDiffHeader.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceDiffHeader.js index 59bce3ee40..81e515fa94 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceDiffHeader.js +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceDiffHeader.js @@ -76,10 +76,9 @@ export default class TraceDiffHeader extends React.PureComponent { const { tableVisible } = this.state; const { data: aData = {}, id: aId, state: aState, error: aError } = a || {}; const { data: bData = {}, id: bId, state: bState, error: bError } = b || {}; - const selection = { - [aId || '_']: { label: 'A' }, - [bId || '__']: { label: 'B' }, - }; + const selection = {}; + if (aId) selection[aId] = { label: 'A' }; + if (bId) selection[bId] = { label: 'B' }; const cohortTableA = ( ); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceDiffHeader.test.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceDiffHeader.test.js new file mode 100644 index 0000000000..ff17c75611 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceDiffHeader.test.js @@ -0,0 +1,189 @@ +// Copyright (c) 2019 The Jaeger Authors. +// +// 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 { Popover } from 'antd'; + +import TraceDiffHeader from './TraceDiffHeader'; +import { fetchedState } from '../../../constants'; + +describe('TraceDiffHeader', () => { + const cohort = [ + { + data: { + duration: 0, + // purposefully missing spans + startTime: 0, + traceName: 'cohort-trace-name-0', + }, + error: 'error 0', + id: 'cohort-id-0', + state: fetchedState.ERROR, + }, + { + data: { + duration: 100, + spans: [ + { + spanID: 'trace-1-span-0', + }, + ], + startTime: 100, + traceName: 'cohort-trace-name-1', + }, + error: 'error 1', + id: 'cohort-id-1', + state: fetchedState.DONE, + }, + { + data: { + duration: 200, + spans: [ + { + spanID: 'trace-2-span-1', + }, + { + spanID: 'trace-2-span-2', + }, + ], + startTime: 200, + traceName: 'cohort-trace-name-2', + }, + error: 'error 2', + id: 'cohort-id-2', + state: fetchedState.DONE, + }, + { + data: { + duration: 300, + spans: [ + { + spanID: 'trace-3-span-1', + }, + { + spanID: 'trace-3-span-2', + }, + { + spanID: 'trace-3-span-3', + }, + ], + startTime: 300, + traceName: 'cohort-trace-name-3', + }, + error: 'error 3', + id: 'cohort-id-3', + state: fetchedState.DONE, + }, + ]; + const diffSetA = jest.fn(); + const diffSetB = jest.fn(); + const props = { + a: cohort[1], + b: cohort[2], + cohort, + diffSetA, + diffSetB, + }; + + let wrapper; + + function getPopoverProp(popoverIndex, propName) { + return wrapper + .find(Popover) + .at(popoverIndex) + .prop(propName); + } + + beforeEach(() => { + diffSetA.mockReset(); + diffSetB.mockReset(); + wrapper = shallow(); + }); + + it('renders as expected', () => { + expect(wrapper).toMatchSnapshot(); + }); + + it('handles trace without spans', () => { + wrapper.setProps({ a: cohort[0] }); + }); + + it('handles absent a', () => { + wrapper.setProps({ a: null }); + expect(wrapper).toMatchSnapshot(); + }); + + it('handles absent b', () => { + wrapper.setProps({ b: null }); + expect(wrapper).toMatchSnapshot(); + }); + + it('handles absent a & b', () => { + wrapper.setProps({ a: null, b: null }); + expect(wrapper).toMatchSnapshot(); + }); + + it('manages visibility correctly', () => { + expect(wrapper.state().tableVisible).toBe(null); + const popovers = wrapper.find(Popover); + expect(popovers.length).toBe(2); + popovers.forEach(popover => expect(popover.prop('visible')).toBe(false)); + + getPopoverProp(0, 'onVisibleChange')(true); + expect(getPopoverProp(0, 'visible')).toBe(true); + expect(getPopoverProp(1, 'visible')).toBe(false); + + getPopoverProp(1, 'onVisibleChange')(true); + expect(getPopoverProp(0, 'visible')).toBe(false); + expect(getPopoverProp(1, 'visible')).toBe(true); + + // repeat onVisibleChange call to test that visibility remains correct + getPopoverProp(1, 'onVisibleChange')(true); + expect(getPopoverProp(0, 'visible')).toBe(false); + expect(getPopoverProp(1, 'visible')).toBe(true); + + getPopoverProp(1, 'onVisibleChange')(false); + wrapper.find(Popover).forEach(popover => expect(popover.prop('visible')).toBe(false)); + }); + + describe('bound functions to set a & b and passes them to Popover JSX props correctly', () => { + const shouldCall = { + a: diffSetA, + b: diffSetB, + }; + const shouldNotCall = { + a: diffSetB, + b: diffSetA, + }; + + ['a', 'b'].forEach(aOrB => { + ['title', 'content'].forEach(popoverSection => { + it(`sets trace ${aOrB} from popover ${popoverSection}`, () => { + const selectTraceArgument = `aOrB: ${aOrB}, popoverSection: ${popoverSection}`; + wrapper.setState({ tableVisible: aOrB }); + wrapper + .find(Popover) + .at(Number(aOrB === 'b')) + .prop(popoverSection) + .props.selectTrace(selectTraceArgument); + + expect(shouldCall[aOrB]).toHaveBeenCalledWith(selectTraceArgument); + expect(shouldNotCall[aOrB]).not.toHaveBeenCalled(); + expect(wrapper.state().tableVisible).toBe(null); + }); + }); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceIdInput.test.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceIdInput.test.js new file mode 100644 index 0000000000..7c143b2063 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceIdInput.test.js @@ -0,0 +1,32 @@ +// Copyright (c) 2019 The Jaeger Authors. +// +// 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 { Input } from 'antd'; + +import TraceIdInput from './TraceIdInput'; + +describe('TraceIdInput', () => { + const props = { + selectTrace: jest.fn(), + }; + const { Search } = Input; + + it('renders as expected', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find(Search).prop('onSearch')).toBe(props.selectTrace); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/CohortTable.test.js.snap b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/CohortTable.test.js.snap new file mode 100644 index 0000000000..93bbf93060 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/CohortTable.test.js.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CohortTable renders as expected 1`] = ` +Array [ + + + + + + + +
, + "", +] +`; diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/TraceDiffHeader.test.js.snap b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/TraceDiffHeader.test.js.snap new file mode 100644 index 0000000000..b45466970c --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/TraceDiffHeader.test.js.snap @@ -0,0 +1,991 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TraceDiffHeader handles absent a & b 1`] = ` +
+
+

+ A +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+

+ VS +

+
+
+

+ B +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+`; + +exports[`TraceDiffHeader handles absent a 1`] = ` +
+
+

+ A +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+

+ VS +

+
+
+

+ B +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+`; + +exports[`TraceDiffHeader handles absent b 1`] = ` +
+
+

+ A +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+

+ VS +

+
+
+

+ B +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+`; + +exports[`TraceDiffHeader renders as expected 1`] = ` +
+
+

+ A +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+

+ VS +

+
+
+

+ B +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+`; diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/TraceIdInput.test.js.snap b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/TraceIdInput.test.js.snap new file mode 100644 index 0000000000..bf8948aa6e --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/TraceIdInput.test.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TraceIdInput renders as expected 1`] = ` + +`; diff --git a/packages/jaeger-ui/src/components/TraceDiff/__snapshots__/TraceDiff.test.js.snap b/packages/jaeger-ui/src/components/TraceDiff/__snapshots__/TraceDiff.test.js.snap new file mode 100644 index 0000000000..e24f00eb34 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/__snapshots__/TraceDiff.test.js.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TraceDiff render renders as expected 1`] = ` + +
+ +
+
+ +
+
+`; diff --git a/packages/jaeger-ui/src/components/TraceDiff/duck.js b/packages/jaeger-ui/src/components/TraceDiff/duck.js index 22cdbc7732..f82a353d0b 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/duck.js +++ b/packages/jaeger-ui/src/components/TraceDiff/duck.js @@ -52,21 +52,21 @@ export const actions = fullActions.jaegerUi.traceDiff; function cohortAddTrace(state, { payload }) { const { traceID } = payload; - const cohort = state.cohort.slice(); - if (cohort.indexOf(traceID) >= 0) { + if (state.cohort.indexOf(traceID) >= 0) { return state; } + const cohort = state.cohort.slice(); cohort.push(traceID); return { ...state, cohort }; } function cohortRemoveTrace(state, { payload }) { const { traceID } = payload; - const cohort = state.cohort.slice(); - const i = cohort.indexOf(traceID); + const i = state.cohort.indexOf(traceID); if (i < 0) { return state; } + const cohort = state.cohort.slice(); cohort.splice(i, 1); const a = state.a === traceID ? null : state.a; const b = state.b === traceID ? null : state.b; diff --git a/packages/jaeger-ui/src/components/TraceDiff/duck.test.js b/packages/jaeger-ui/src/components/TraceDiff/duck.test.js new file mode 100644 index 0000000000..0d948d6821 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/duck.test.js @@ -0,0 +1,135 @@ +// 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 { createStore } from 'redux'; + +import reducer, { actions, newInitialState } from './duck'; + +describe('TraceDiff/duck', () => { + const initialCohort = ['trace-id-0', 'trace-id-1', 'trace-id-2']; + const newTraceId = 'new-trace-id'; + let store; + + beforeEach(() => { + store = createStore(reducer, { + a: initialCohort[0], + b: initialCohort[1], + cohort: initialCohort, + }); + }); + + describe('newInitialState', () => { + it('creates an empty set', () => { + expect(newInitialState()).toEqual({ + a: null, + b: null, + cohort: [], + }); + }); + }); + + describe('cohortAddTrace', () => { + it('adds trace that does not already exist in state', () => { + const oldCohort = store.getState().cohort; + expect(oldCohort.includes(newTraceId)).toBe(false); + + store.dispatch(actions.cohortAddTrace(newTraceId)); + const newCohort = store.getState().cohort; + expect(newCohort).not.toBe(oldCohort); + expect(newCohort.includes(newTraceId)).toBe(true); + expect(newCohort).toEqual(expect.arrayContaining(oldCohort)); + }); + + it('returns original state if traceID already exists in state', () => { + const state = store.getState(); + store.dispatch(actions.cohortAddTrace(initialCohort[0])); + expect(store.getState()).toBe(state); + }); + }); + + describe('cohortRemoveTrace', () => { + it('removes trace that exists in state.cohort', () => { + const oldCohort = store.getState().cohort; + store.dispatch(actions.cohortRemoveTrace(initialCohort[2])); + const newCohort = store.getState().cohort; + expect(newCohort).not.toBe(oldCohort); + expect(newCohort.includes(initialCohort[2])).toBe(false); + expect(newCohort).toEqual(oldCohort.slice(0, 2)); + }); + + it('removes state.a', () => { + const oldState = store.getState(); + const oldCohort = oldState.cohort; + store.dispatch(actions.cohortRemoveTrace(oldState.a)); + const newState = store.getState(); + const newCohort = newState.cohort; + expect(newState.a).toBe(null); + expect(newCohort).not.toBe(oldCohort); + expect(newCohort.includes(oldState.a)).toBe(false); + expect(newCohort).toEqual(oldCohort.slice(1)); + }); + + it('removes state.b', () => { + const oldState = store.getState(); + const oldCohort = oldState.cohort; + store.dispatch(actions.cohortRemoveTrace(oldState.b)); + const newState = store.getState(); + const newCohort = newState.cohort; + expect(newState.b).toBe(null); + expect(newCohort).not.toBe(oldCohort); + expect(newCohort.includes(oldState.b)).toBe(false); + expect(newCohort).toEqual(oldCohort.filter(entry => entry !== oldState.b)); + }); + + it('returns original state if traceID already exists in state', () => { + const state = store.getState(); + store.dispatch(actions.cohortRemoveTrace(newTraceId)); + expect(store.getState()).toBe(state); + }); + }); + + describe('diffSetA', () => { + it('set a to provided traceId', () => { + const oldState = store.getState(); + store.dispatch(actions.diffSetA(newTraceId)); + const newState = store.getState(); + expect(newState).not.toBe(oldState); + expect(newState).toEqual({ + ...oldState, + a: newTraceId, + }); + }); + }); + + describe('diffSetB', () => { + it('set b to provided traceId', () => { + const oldState = store.getState(); + store.dispatch(actions.diffSetB(newTraceId)); + const newState = store.getState(); + expect(newState).not.toBe(oldState); + expect(newState).toEqual({ + ...oldState, + b: newTraceId, + }); + }); + }); + + describe('forceState', () => { + it('returns given state', () => { + const newState = newInitialState(); + store.dispatch(actions.forceState(newState)); + expect(store.getState()).toBe(newState); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/getValidState.test.js b/packages/jaeger-ui/src/components/TraceDiff/getValidState.test.js new file mode 100644 index 0000000000..52c1dc5e02 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/getValidState.test.js @@ -0,0 +1,53 @@ +// Copyright (c) 2019 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 getValidState from './getValidState'; + +describe('getValidState', () => { + const a = 'a string'; + const b = 'b string'; + const cohort = ['first string', 'second string', 'third string']; + + it('handles absent argument', () => { + expect(getValidState()).toEqual({ + a: undefined, + b: undefined, + cohort: [], + }); + }); + + it('uses cohort kwarg when a and b are missing', () => { + expect(getValidState({ cohort })).toEqual({ + a: cohort[0], + b: cohort[1], + cohort, + }); + }); + + it('uses a and b when provided', () => { + expect(getValidState({ a, b, cohort })).toEqual({ + a, + b, + cohort: [a, b, ...cohort], + }); + }); + + it('uses b as a and cohort[0] for b when only b is provided', () => { + expect(getValidState({ b, cohort })).toEqual({ + a: b, + b: cohort[0], + cohort: [b, ...cohort], + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/url.test.js b/packages/jaeger-ui/src/components/TraceDiff/url.test.js new file mode 100644 index 0000000000..a898d85b2c --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/url.test.js @@ -0,0 +1,64 @@ +// Copyright (c) 2019 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 * as reactRouterDom from 'react-router-dom'; + +import { ROUTE_PATH, matches, getUrl } from './url'; + +describe('TraceDiff/url', () => { + describe('matches', () => { + const path = 'path argument'; + let matchPathSpy; + + beforeAll(() => { + matchPathSpy = jest.spyOn(reactRouterDom, 'matchPath'); + }); + + it('calls matchPath with expected arguments', () => { + matches(path); + expect(matchPathSpy).toHaveBeenLastCalledWith(path, { + path: ROUTE_PATH, + strict: true, + exact: true, + }); + }); + + it("returns truthiness of matchPath's return value", () => { + matchPathSpy.mockReturnValueOnce(null); + expect(matches(path)).toBe(false); + matchPathSpy.mockReturnValueOnce({}); + expect(matches(path)).toBe(true); + }); + }); + + describe('getUrl', () => { + it('handles an empty state', () => { + expect(getUrl()).toBe('/trace/...'); + }); + + it('handles a single traceId', () => { + const cohort = ['first']; + expect(getUrl({ cohort })).toBe(`/trace/${cohort[0]}...?cohort=${cohort[0]}`); + }); + + it('handles multiple traceIds', () => { + const cohort = ['first', 'second', 'third']; + const result = getUrl({ cohort }); + expect(result).toMatch(`${cohort[0]}...${cohort[1]}`); + cohort.forEach(cohortEntry => { + expect(result).toMatch(`cohort=${cohortEntry}`); + }); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/TracePage/ScrollManager.js b/packages/jaeger-ui/src/components/TracePage/ScrollManager.js index a1175437a3..bd28bc1830 100644 --- a/packages/jaeger-ui/src/components/TracePage/ScrollManager.js +++ b/packages/jaeger-ui/src/components/TracePage/ScrollManager.js @@ -179,6 +179,19 @@ export default class ScrollManager { 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) { + childrenAreHidden.add(...parentIDs); + nextSpanIndex--; + } + isFallbackHidden = isHidden; + } while (isFallbackHidden); + } } const nextRow = xrs.mapSpanIndexToRowIndex(nextSpanIndex); this._scrollPast(nextRow, direction); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.css b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.css index f34560e4dd..fd8cc52e74 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.css @@ -20,7 +20,6 @@ limitations under the License. cursor: pointer; white-space: nowrap; border-collapse: separate; - border-radius: 2px; } .OpNode td, @@ -28,6 +27,15 @@ limitations under the License. border: none; } +.OpNode.is-ui-find-match { + outline: inherit; + outline-color: #fff3d7; +} + +.OpNode--popover .OpNode.is-ui-find-match { + outline: #fff3d7 solid 3px; +} + .OpMode--mode-service { background: #bbb; } @@ -48,7 +56,7 @@ limitations under the License. .OpNode--popover .OpNode--copyIcon, .OpNode:not(:hover) .OpNode--copyIcon { - display: none; + color: transparent; } .OpNode--service { diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.js b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.js index 7d757964dc..d75a696980 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.js @@ -16,6 +16,7 @@ import * as React from 'react'; import { Popover } from 'antd'; +import cx from 'classnames'; import CopyIcon from '../../common/CopyIcon'; import colorGenerator from '../../../utils/color-generator'; @@ -34,6 +35,7 @@ type Props = { operation: string, service: string, mode: string, + isUiFindMatch: boolean, }; export const MODE_SERVICE = 'service'; @@ -64,10 +66,19 @@ export function round2(percent: number) { } export default class OpNode extends React.PureComponent { - props: Props; - render() { - const { count, errors, time, percent, selfTime, percentSelfTime, operation, service, mode } = this.props; + const { + count, + errors, + time, + percent, + selfTime, + percentSelfTime, + operation, + service, + mode, + isUiFindMatch, + } = this.props; // Spans over 20 % time are full red - we have probably to reconsider better approach let backgroundColor; @@ -83,9 +94,14 @@ export default class OpNode extends React.PureComponent { .join(); } + const className = cx('OpNode', `OpNode--mode-${mode}`, { + 'is-ui-find-match': isUiFindMatch, + }); + const table = ( - +
{ } } -export function getNodeDrawer(mode: string) { +export function getNodeDrawer(mode: string, uiFindVertexKeys: Set) { return function drawNode(vertex: PVertex) { const { data, operation, service } = vertex.data; - return ; + return ( + + ); }; } diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.test.js b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.test.js index 701e92959b..f9b2d4fddf 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/OpNode.test.js @@ -28,23 +28,24 @@ describe('', () => { props = { count: 5, errors: 0, - time: 200000, + isUiFindMatch: false, + operation: 'op1', percent: 7.89, - selfTime: 180000, percentSelfTime: 90, - operation: 'op1', + selfTime: 180000, service: 'service1', + time: 200000, }; wrapper = shallow(); }); - it('it does not explode', () => { + it('does not explode', () => { expect(wrapper).toBeDefined(); expect(wrapper.find('.OpNode').length).toBe(1); expect(wrapper.find('.OpNode--mode-service').length).toBe(1); }); - it('it renders OpNode', () => { + it('renders OpNode', () => { expect(wrapper.find('.OpNode--count').text()).toBe('5 / 0'); expect(wrapper.find('.OpNode--time').text()).toBe('200 ms (7.89 %)'); expect(wrapper.find('.OpNode--avg').text()).toBe('40 ms'); @@ -58,7 +59,7 @@ describe('', () => { ).toBe('service1'); }); - it('it switches mode', () => { + it('switches mode', () => { mode = MODE_SERVICE; wrapper = shallow(); expect(wrapper.find('.OpNode--mode-service').length).toBe(1); @@ -78,6 +79,11 @@ describe('', () => { expect(wrapper.find('.OpNode--mode-selftime').length).toBe(1); }); + it('updates class when it matches search', () => { + wrapper.setProps({ isUiFindMatch: true }); + expect(wrapper.find('.is-ui-find-match').length).toBe(1); + }); + it('renders a copy icon', () => { const copyIcon = wrapper.find(CopyIcon); expect(copyIcon.length).toBe(1); @@ -86,17 +92,30 @@ describe('', () => { }); describe('getNodeDrawer()', () => { - it('it creates OpNode', () => { - const vertex = { - data: { - service: 'service1', - operation: 'op1', - data: {}, - }, - }; - const drawNode = getNodeDrawer(MODE_SERVICE); + const key = 'key test value'; + const vertex = { + data: { + service: 'service1', + operation: 'op1', + data: {}, + }, + key, + }; + + it('creates OpNode', () => { + const drawNode = getNodeDrawer(MODE_SERVICE, new Set()); + const opNode = drawNode(vertex); + expect(opNode.type === 'OpNode'); + expect(opNode.props.isUiFindMatch).toBe(false); + expect(opNode.props.mode).toBe(MODE_SERVICE); + }); + + it('creates OpNode that matches uiFind', () => { + const drawNode = getNodeDrawer(MODE_TIME, new Set([key])); const opNode = drawNode(vertex); expect(opNode.type === 'OpNode'); + expect(opNode.props.isUiFindMatch).toBe(true); + expect(opNode.props.mode).toBe(MODE_TIME); }); }); }); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.css b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.css index 3a11034d1f..746110a79c 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.css @@ -29,9 +29,14 @@ limitations under the License. position: absolute; right: 0; top: 0; + transition: background 0.5s ease; display: flex; } +.TraceGraph--graphWrapper.is-uiFind-mode { + background: #ddd; +} + .TraceGraph--sidebar-container { display: flex; z-index: 1; @@ -39,8 +44,7 @@ limitations under the License. .TraceGraph--menu { cursor: pointer; - padding-right: 1rem; - padding-top: 1rem; + padding: 0.8rem; } .TraceGraph--menu > li { diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.js b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.js index c1bc20fc4e..f76f5866ef 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.js @@ -16,29 +16,19 @@ import * as React from 'react'; import { Card, Icon, Button, Tooltip } from 'antd'; +import cx from 'classnames'; import { DirectedGraph, LayoutManager } from '@jaegertracing/plexus'; -import DRange from 'drange'; import { getNodeDrawer, MODE_SERVICE, MODE_TIME, MODE_SELFTIME, HELP_TABLE } from './OpNode'; -import convPlexus from '../../../model/trace-dag/convPlexus'; -import TraceDag from '../../../model/trace-dag/TraceDag'; - -import type { Trace, Span, KeyValuePair } from '../../../types/trace'; +import { setOnEdgesContainer, setOnNodesContainer, setOnNode } from '../../../utils/plexus/set-on-graph'; import './TraceGraph.css'; -type SumSpan = { - count: number, - errors: number, - time: number, - percent: number, - selfTime: number, - percentSelfTime: number, -}; - type Props = { headerHeight: number, - trace: Trace, + ev?: ?Object, + uiFind: string, + uiFindVertexKeys: Set, }; type State = { showHelp: boolean, @@ -51,39 +41,6 @@ export function setOnEdgePath(e: any) { return e.followsFrom ? { strokeDasharray: 4 } : {}; } -function extendFollowsFrom(edges: any, nodes: any) { - return edges.map(e => { - let hasChildOf = true; - if (typeof e.to === 'number') { - const n = nodes[e.to]; - hasChildOf = n.members.some( - m => m.span.references && m.span.references.some(r => r.refType === 'CHILD_OF') - ); - } - return { ...e, followsFrom: !hasChildOf }; - }); -} - -export function isError(tags: Array) { - if (tags) { - const errorTag = tags.find(t => t.key === 'error'); - if (errorTag) { - return errorTag.value; - } - } - return false; -} - -function setOnEdgesContainer(state: Object) { - const { zoomTransform } = state; - if (!zoomTransform) { - return null; - } - const { k } = zoomTransform; - const opacity = 0.1 + k * 0.9; - return { style: { opacity } }; -} - const HELP_CONTENT = (
{HELP_TABLE} @@ -145,11 +102,14 @@ export default class TraceGraph extends React.PureComponent { props: Props; state: State; - parentChildOfMap: { [string]: Span[] }; cache: any; layoutManager: LayoutManager; + static defaultProps = { + ev: null, + }; + constructor(props: Props) { super(props); this.state = { @@ -163,68 +123,6 @@ export default class TraceGraph extends React.PureComponent { this.layoutManager.stopAndRelease(); } - calculateTraceDag(): TraceDag { - const traceDag: TraceDag = new TraceDag(); - traceDag._initFromTrace(this.props.trace, { - count: 0, - errors: 0, - time: 0, - percent: 0, - selfTime: 0, - percentSelfTime: 0, - }); - - traceDag.nodesMap.forEach(n => { - const ntime = n.members.reduce((p, m) => p + m.span.duration, 0); - const numErrors = n.members.reduce((p, m) => (p + isError(m.span.tags) ? 1 : 0), 0); - const childDurationsDRange = n.members.reduce((p, m) => { - // Using DRange to handle overlapping spans (fork-join) - const cdr = new DRange(m.span.startTime, m.span.startTime + m.span.duration).intersect( - this.getChildOfDrange(m.span.spanID) - ); - return p + cdr.length; - }, 0); - const stime = ntime - childDurationsDRange; - const nd = { - count: n.members.length, - errors: numErrors, - time: ntime, - percent: 100 / this.props.trace.duration * ntime, - selfTime: stime, - percentSelfTime: 100 / ntime * stime, - }; - // eslint-disable-next-line no-param-reassign - n.data = nd; - }); - return traceDag; - } - - getChildOfDrange(parentID: string): number { - const childrenDrange = new DRange(); - this.getChildOfSpans(parentID).forEach(s => { - // -1 otherwise it will take for each child a micro (incluse,exclusive) - childrenDrange.add(s.startTime, s.startTime + (s.duration <= 0 ? 0 : s.duration - 1)); - }); - return childrenDrange; - } - - getChildOfSpans(parentID: string): Span[] { - if (!this.parentChildOfMap) { - this.parentChildOfMap = {}; - this.props.trace.spans.forEach(s => { - if (s.references) { - // Filter for CHILD_OF we don't want to calculate FOLLOWS_FROM (prod-cons) - const parentIDs = s.references.filter(r => r.refType === 'CHILD_OF').map(r => r.spanID); - parentIDs.forEach((pID: string) => { - this.parentChildOfMap[pID] = this.parentChildOfMap[pID] || []; - this.parentChildOfMap[pID].push(s); - }); - } - }); - } - return this.parentChildOfMap[parentID] || []; - } - toggleNodeMode(newMode: string) { this.setState({ mode: newMode }); } @@ -238,24 +136,16 @@ export default class TraceGraph extends React.PureComponent { }; render() { - const { headerHeight, trace } = this.props; + const { ev, headerHeight, uiFind, uiFindVertexKeys } = this.props; const { showHelp, mode } = this.state; - if (!trace) { + if (!ev) { return

No trace found

; } - // Caching edges/vertices so that DirectedGraph is not redrawn - let ev = this.cache; - if (!ev) { - const traceDag = this.calculateTraceDag(); - const nodes = [...traceDag.nodesMap.values()]; - ev = convPlexus(traceDag.nodesMap); - ev.edges = extendFollowsFrom(ev.edges, nodes); - this.cache = ev; - } + const wrapperClassName = cx('TraceGraph--graphWrapper', { 'is-uiFind-mode': uiFind }); return ( -
+
{ className="TraceGraph--dag" minimapClassName="TraceGraph--miniMap" layoutManager={this.layoutManager} - getNodeLabel={getNodeDrawer(mode)} + getNodeLabel={getNodeDrawer(mode, uiFindVertexKeys)} setOnRoot={classNameIsSmall} setOnEdgePath={setOnEdgePath} setOnEdgesContainer={setOnEdgesContainer} + setOnNodesContainer={setOnNodesContainer} + setOnNode={setOnNode} edges={ev.edges} vertices={ev.vertices} /> diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.test.js b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.test.js index ad58f6f272..e92f86ab5f 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/TraceGraph.test.js @@ -16,21 +16,14 @@ import React from 'react'; import { shallow } from 'enzyme'; import transformTraceData from '../../../model/transform-trace-data'; - +import calculateTraceDagEV from './calculateTraceDagEV'; import TraceGraph, { setOnEdgePath } from './TraceGraph'; import { MODE_SERVICE, MODE_TIME, MODE_SELFTIME } from './OpNode'; const testTrace = require('./testTrace.json'); -function assertData(nodes, service, operation, count, errors, time, percent, selfTime) { - const d = nodes.find(n => n.service === service && n.operation === operation).data; - expect(d).toBeDefined(); - expect(d.count).toBe(count); - expect(d.errors).toBe(errors); - expect(d.time).toBe(time * 1000); - expect(d.percent).toBeCloseTo(percent, 2); - expect(d.selfTime).toBe(selfTime * 1000); -} +const transformedTrace = transformTraceData(testTrace); +const ev = calculateTraceDagEV(transformedTrace); describe('', () => { let wrapper; @@ -38,44 +31,25 @@ describe('', () => { beforeEach(() => { const props = { headerHeight: 60, - trace: transformTraceData(testTrace), + ev, }; wrapper = shallow(); }); - it('it does not explode', () => { + it('does not explode', () => { expect(wrapper).toBeDefined(); expect(wrapper.find('.TraceGraph--menu').length).toBe(1); expect(wrapper.find('Button').length).toBe(3); }); - it('it calculates TraceGraph', () => { - const traceDag = wrapper.instance().calculateTraceDag(); - expect(traceDag.nodesMap.size).toBe(9); - const nodes = [...traceDag.nodesMap.values()]; - assertData(nodes, 'service1', 'op1', 1, 0, 390, 39, 224); - // accumulate data (count,times) - assertData(nodes, 'service1', 'op2', 2, 1, 70, 7, 70); - // self-time is substracted from child - assertData(nodes, 'service1', 'op3', 1, 0, 66, 6.6, 46); - assertData(nodes, 'service2', 'op1', 1, 0, 20, 2, 2); - assertData(nodes, 'service2', 'op2', 1, 0, 18, 1.8, 18); - // follows_from relation will not influence self-time - assertData(nodes, 'service1', 'op4', 1, 0, 20, 2, 20); - assertData(nodes, 'service2', 'op3', 1, 0, 200, 20, 200); - // fork-join self-times are calculated correctly (self-time drange) - assertData(nodes, 'service1', 'op6', 1, 0, 10, 1, 1); - assertData(nodes, 'service1', 'op7', 2, 0, 17, 1.7, 17); - }); - - it('it may show no traces', () => { + it('may show no traces', () => { const props = {}; wrapper = shallow(); expect(wrapper).toBeDefined(); expect(wrapper.find('h1').text()).toBe('No trace found'); }); - it('it toggle nodeMode to time', () => { + it('toggles nodeMode to time', () => { const mode = MODE_SERVICE; wrapper.setState({ mode }); wrapper.instance().toggleNodeMode(MODE_TIME); @@ -83,7 +57,7 @@ describe('', () => { expect(modeState).toEqual(MODE_TIME); }); - it('it validates button nodeMode change click', () => { + it('validates button nodeMode change click', () => { const toggleNodeMode = jest.spyOn(wrapper.instance(), 'toggleNodeMode'); const btnService = wrapper.find('.TraceGraph--btn-service'); expect(btnService.length).toBe(1); @@ -99,21 +73,21 @@ describe('', () => { expect(toggleNodeMode).toHaveBeenCalledWith(MODE_SELFTIME); }); - it('it shows help', () => { + it('shows help', () => { const showHelp = false; wrapper.setState({ showHelp }); wrapper.instance().showHelp(); expect(wrapper.state('showHelp')).toBe(true); }); - it('it hides help', () => { + it('hides help', () => { const showHelp = true; wrapper.setState({ showHelp }); wrapper.instance().closeSidebar(); expect(wrapper.state('showHelp')).toBe(false); }); - it('it uses stroke-dash edges for followsFrom', () => { + it('uses stroke-dash edges for followsFrom', () => { const edge = { from: 0, to: 1, followsFrom: true }; expect(setOnEdgePath(edge)).toEqual({ strokeDasharray: 4 }); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/calculateTraceDagEV.js b/packages/jaeger-ui/src/components/TracePage/TraceGraph/calculateTraceDagEV.js new file mode 100644 index 0000000000..6b35ad12df --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/calculateTraceDagEV.js @@ -0,0 +1,125 @@ +// @flow + +// Copyright (c) 2019 The Jaeger Authors. +// +// 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 DRange from 'drange'; + +import convPlexus from '../../../model/trace-dag/convPlexus'; +import TraceDag from '../../../model/trace-dag/TraceDag'; +import type { Trace, Span, KeyValuePair } from '../../../types/trace'; + +type SumSpan = { + count: number, + errors: number, + time: number, + percent: number, + selfTime: number, + percentSelfTime: number, +}; + +let parentChildOfMap: { [string]: Span[] }; + +export function isError(tags: Array) { + if (tags) { + const errorTag = tags.find(t => t.key === 'error'); + if (errorTag) { + return errorTag.value; + } + } + return false; +} + +function extendFollowsFrom(edges: any, nodes: any) { + return edges.map(e => { + let hasChildOf = true; + if (typeof e.to === 'number') { + const n = nodes[e.to]; + hasChildOf = n.members.some( + m => m.span.references && m.span.references.some(r => r.refType === 'CHILD_OF') + ); + } + return { ...e, followsFrom: !hasChildOf }; + }); +} + +function getChildOfSpans(parentID: string, trace: Trace): Span[] { + if (!parentChildOfMap) { + parentChildOfMap = {}; + trace.spans.forEach(s => { + if (s.references) { + // Filter for CHILD_OF we don't want to calculate FOLLOWS_FROM (prod-cons) + const parentIDs = s.references.filter(r => r.refType === 'CHILD_OF').map(r => r.spanID); + parentIDs.forEach((pID: string) => { + parentChildOfMap[pID] = parentChildOfMap[pID] || []; + parentChildOfMap[pID].push(s); + }); + } + }); + } + return parentChildOfMap[parentID] || []; +} + +function getChildOfDrange(parentID: string, trace: Trace): number { + const childrenDrange = new DRange(); + getChildOfSpans(parentID, trace).forEach(s => { + // -1 otherwise it will take for each child a micro (incluse,exclusive) + childrenDrange.add(s.startTime, s.startTime + (s.duration <= 0 ? 0 : s.duration - 1)); + }); + return childrenDrange; +} + +export function calculateTraceDag(trace: Trace): TraceDag { + const traceDag: TraceDag = new TraceDag(); + traceDag._initFromTrace(trace, { + count: 0, + errors: 0, + time: 0, + percent: 0, + selfTime: 0, + percentSelfTime: 0, + }); + + traceDag.nodesMap.forEach(n => { + const ntime = n.members.reduce((p, m) => p + m.span.duration, 0); + const numErrors = n.members.reduce((p, m) => (p + isError(m.span.tags) ? 1 : 0), 0); + const childDurationsDRange = n.members.reduce((p, m) => { + // Using DRange to handle overlapping spans (fork-join) + const cdr = new DRange(m.span.startTime, m.span.startTime + m.span.duration).intersect( + getChildOfDrange(m.span.spanID, trace) + ); + return p + cdr.length; + }, 0); + const stime = ntime - childDurationsDRange; + const nd = { + count: n.members.length, + errors: numErrors, + time: ntime, + percent: 100 / trace.duration * ntime, + selfTime: stime, + percentSelfTime: 100 / ntime * stime, + }; + // eslint-disable-next-line no-param-reassign + n.data = nd; + }); + return traceDag; +} + +export default function calculateTraceDagEV(trace: Trace) { + const traceDag = calculateTraceDag(trace); + const nodes = [...traceDag.nodesMap.values()]; + const ev = convPlexus(traceDag.nodesMap); + ev.edges = extendFollowsFrom(ev.edges, nodes); + return ev; +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceGraph/calculateTraceDagEV.test.js b/packages/jaeger-ui/src/components/TracePage/TraceGraph/calculateTraceDagEV.test.js new file mode 100644 index 0000000000..e00c89a956 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceGraph/calculateTraceDagEV.test.js @@ -0,0 +1,51 @@ +// Copyright (c) 2019 The Jaeger Authors. +// +// 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 transformTraceData from '../../../model/transform-trace-data'; +import calculateTraceDagEV from './calculateTraceDagEV'; + +const testTrace = require('./testTrace.json'); + +const transformedTrace = transformTraceData(testTrace); + +function assertData(nodes, service, operation, count, errors, time, percent, selfTime) { + const d = nodes.find(({ data: n }) => n.service === service && n.operation === operation).data.data; + expect(d).toBeDefined(); + expect(d.count).toBe(count); + expect(d.errors).toBe(errors); + expect(d.time).toBe(time * 1000); + expect(d.percent).toBeCloseTo(percent, 2); + expect(d.selfTime).toBe(selfTime * 1000); +} + +describe('calculateTraceDagEV', () => { + it('calculates TraceGraph', () => { + const traceDag = calculateTraceDagEV(transformedTrace); + const { vertices: nodes } = traceDag; + expect(nodes.length).toBe(9); + assertData(nodes, 'service1', 'op1', 1, 0, 390, 39, 224); + // accumulate data (count,times) + assertData(nodes, 'service1', 'op2', 2, 1, 70, 7, 70); + // self-time is substracted from child + assertData(nodes, 'service1', 'op3', 1, 0, 66, 6.6, 46); + assertData(nodes, 'service2', 'op1', 1, 0, 20, 2, 2); + assertData(nodes, 'service2', 'op2', 1, 0, 18, 1.8, 18); + // follows_from relation will not influence self-time + assertData(nodes, 'service1', 'op4', 1, 0, 20, 2, 20); + assertData(nodes, 'service2', 'op3', 1, 0, 200, 20, 200); + // fork-join self-times are calculated correctly (self-time drange) + assertData(nodes, 'service1', 'op6', 1, 0, 10, 1, 1); + assertData(nodes, 'service1', 'op7', 2, 0, 17, 1.7, 17); + }); +}); diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.js index 97482c822a..27261aed0c 100644 --- a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.js +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageHeader.js @@ -167,13 +167,14 @@ export function TracePageHeaderFn(props: TracePageHeaderEmbedProps) { )} {showShortcutsHelp && } {showViewOptions && ( diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageSearchBar.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageSearchBar.js index 742d0bf862..91500ed8f3 100644 --- a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageSearchBar.js +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageSearchBar.js @@ -16,59 +16,60 @@ import * as React from 'react'; import { Button, Input } from 'antd'; +import cx from 'classnames'; import * as markers from './TracePageSearchBar.markers'; +import { trackFilter } from '../index.track'; +import UiFindInput from '../../common/UiFindInput'; import './TracePageSearchBar.css'; type TracePageSearchBarProps = { - updateTextFilter: string => void, textFilter: string, prevResult: () => void, nextResult: () => void, clearSearch: () => void, resultCount: number, forwardedRef: { current: Input | null }, + navigable: boolean, }; export function TracePageSearchBarFn(props: TracePageSearchBarProps) { - const { - prevResult, - nextResult, - clearSearch, - resultCount, - updateTextFilter, - textFilter, - forwardedRef, - } = props; + const { clearSearch, forwardedRef, navigable, nextResult, prevResult, resultCount, textFilter } = props; const count = textFilter ? {resultCount} : null; - const updateFilter = event => updateTextFilter(event.target.value); - const onKeyDown = e => { - if (e.keyCode === 27) clearSearch(); + const navigationBtnDisabled = !navigable || !textFilter; + const navigationBtnClass = cx('TracePageSearchBar--btn', { 'is-disabled': navigationBtnDisabled }); + const btnClass = cx('TracePageSearchBar--btn', { 'is-disabled': !textFilter }); + const uiFindInputInputProps = { + 'data-test': markers.IN_TRACE_SEARCH, + className: 'TracePageSearchBar--bar ub-flex-auto', + name: 'search', + suffix: count, }; - const btnClass = `TracePageSearchBar--btn${textFilter ? '' : ' is-disabled'}`; - return (
{/* style inline because compact overwrites the display */} - +
diff --git a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageSearchBar.test.js b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageSearchBar.test.js index c4beaa08c3..bb3f249b96 100644 --- a/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageSearchBar.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TracePageHeader/TracePageSearchBar.test.js @@ -17,14 +17,17 @@ import { shallow } from 'enzyme'; import * as markers from './TracePageSearchBar.markers'; import { TracePageSearchBarFn as TracePageSearchBar } from './TracePageSearchBar'; +import { trackFilter } from '../index.track'; +import UiFindInput from '../../common/UiFindInput'; describe('', () => { const defaultProps = { - updateTextFilter: () => {}, - textFilter: 'something', - prevResult: () => {}, + forwardedRef: React.createRef(), + navigable: true, nextResult: () => {}, + prevResult: () => {}, resultCount: 0, + textFilter: 'something', }; let wrapper; @@ -33,23 +36,65 @@ describe('', () => { wrapper = shallow(); }); - it('calls updateTextFilter() function for onChange of the input', () => { - const updateTextFilter = jest.fn(); - const props = { ...defaultProps, updateTextFilter }; - wrapper = shallow(); - const event = { target: { value: 'my new value' } }; - wrapper - .find(`[data-test="${markers.IN_TRACE_SEARCH}"]`) - .first() - .simulate('change', event); - expect(updateTextFilter.mock.calls.length).toBe(1); - }); + describe('truthy textFilter', () => { + it('renders UiFindInput with correct props', () => { + const renderedUiFindInput = wrapper.find(UiFindInput); + const suffix = shallow(renderedUiFindInput.prop('inputProps').suffix); + expect(renderedUiFindInput.prop('inputProps')).toEqual( + expect.objectContaining({ + 'data-test': markers.IN_TRACE_SEARCH, + className: 'TracePageSearchBar--bar ub-flex-auto', + name: 'search', + }) + ); + expect(suffix.hasClass('TracePageSearchBar--count')).toBe(true); + expect(suffix.text()).toBe(String(defaultProps.resultCount)); + expect(renderedUiFindInput.prop('forwardedRef')).toBe(defaultProps.forwardedRef); + expect(renderedUiFindInput.prop('trackFindFunction')).toBe(trackFilter); + }); + + it('renders buttons', () => { + const buttons = wrapper.find('Button'); + expect(buttons.length).toBe(3); + buttons.forEach(button => { + expect(button.hasClass('TracePageSearchBar--btn')).toBe(true); + expect(button.hasClass('is-disabled')).toBe(false); + expect(button.prop('disabled')).toBe(false); + }); + expect(wrapper.find('Button[icon="up"]').prop('onClick')).toBe(defaultProps.prevResult); + expect(wrapper.find('Button[icon="down"]').prop('onClick')).toBe(defaultProps.nextResult); + expect(wrapper.find('Button[icon="close"]').prop('onClick')).toBe(defaultProps.clearSearch); + }); - it('renders the search bar', () => { - expect(wrapper.find('Input').length).toBe(1); + it('disables navigation buttons when not navigable', () => { + wrapper.setProps({ navigable: false }); + const buttons = wrapper.find('Button'); + expect(buttons.length).toBe(3); + buttons.forEach((button, i) => { + expect(button.hasClass('TracePageSearchBar--btn')).toBe(true); + expect(button.hasClass('is-disabled')).toBe(i !== 2); + expect(button.prop('disabled')).toBe(i !== 2); + }); + }); }); - it('renders the buttons', () => { - expect(wrapper.find('Button').length).toBe(3); + describe('falsy textFilter', () => { + beforeEach(() => { + wrapper.setProps({ textFilter: '' }); + }); + + it('renders UiFindInput with correct props', () => { + expect(wrapper.find(UiFindInput).prop('inputProps').suffix).toBe(null); + }); + + it('renders buttons', () => { + const buttons = wrapper.find('Button'); + expect(buttons.length).toBe(3); + buttons.forEach(button => { + expect(button.hasClass('TracePageSearchBar--btn')).toBe(true); + expect(button.hasClass('is-disabled')).toBe(true); + expect(button.prop('disabled')).toBe(true); + }); + }); }); }); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.css index a2c84df85d..f7c0b19e57 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.css @@ -31,7 +31,10 @@ limitations under the License. } .SpanDetail--debugValue { + background-color: inherit; + border: none; color: #888; + cursor: pointer; } .SpanDetail--debugValue:hover { diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.js index fc6948c52a..7ccc0c4538 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.js @@ -15,18 +15,20 @@ // limitations under the License. import React from 'react'; -import { Divider } from 'antd'; +import { Divider, Tooltip } from 'antd'; import AccordianKeyValues from './AccordianKeyValues'; import AccordianLogs from './AccordianLogs'; import DetailState from './DetailState'; import { formatDuration } from '../utils'; import LabeledList from '../../../common/LabeledList'; + import type { Log, Span, KeyValuePair, Link } from '../../../../types/trace'; import './index.css'; type SpanDetailProps = { + addToUiFind: string => void, detailState: DetailState, linksGetter: ?(KeyValuePair[], number) => Link[], logItemToggle: (string, Log) => void, @@ -39,6 +41,7 @@ type SpanDetailProps = { export default function SpanDetail(props: SpanDetailProps) { const { + addToUiFind, detailState, linksGetter, logItemToggle, @@ -67,6 +70,7 @@ export default function SpanDetail(props: SpanDetailProps) { value: formatDuration(relativeStartTime), }, ]; + return (
@@ -112,8 +116,12 @@ export default function SpanDetail(props: SpanDetailProps) { )} - {' '} - {spanID} + + {' '} + +
diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js index 774bbaf211..9740c8b20d 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js @@ -38,6 +38,7 @@ describe('', () => { .toggleTags(); const traceStartTime = 5; const props = { + addToUiFind: jest.fn(), detailState, span, traceStartTime, @@ -63,6 +64,7 @@ describe('', () => { props.processToggle.mockReset(); props.logsToggle.mockReset(); props.logItemToggle.mockReset(); + props.addToUiFind.mockReset(); wrapper = shallow(); }); @@ -118,4 +120,10 @@ describe('', () => { expect(props.logsToggle).toHaveBeenLastCalledWith(span.spanID); expect(props.logItemToggle).toHaveBeenLastCalledWith(span.spanID, somethingUniq); }); + + it('calls addToUiFind when the spanID is clicked', () => { + const spanIDButton = wrapper.find('button'); + spanIDButton.simulate('click'); + expect(props.addToUiFind).toHaveBeenCalledWith(props.span.spanID); + }); }); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.js index 3ac9f115d0..f00e4a87cb 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetailRow.js @@ -25,6 +25,7 @@ import type { Log, Span, KeyValuePair, Link } from '../../../types/trace'; import './SpanDetailRow.css'; type SpanDetailRowProps = { + addToUiFind: string => void, color: string, columnDivision: number, detailState: DetailState, @@ -52,6 +53,7 @@ export default class SpanDetailRow extends React.PureComponent
{ const ownSpanID = 'ownSpanID'; const parentSpanID = 'parentSpanID'; const rootSpanID = 'rootSpanID'; - const spanWithTwoAncestors = { - hasChildren: false, - references: [ - { - refType: 'CHILD_OF', - span: { - spanID: parentSpanID, - references: [ - { - refType: 'CHILD_OF', - span: { - spanID: rootSpanID, - }, - }, - ], - }, - }, - ], - spanID: ownSpanID, - }; - const specialRootID = 'root'; let props; let wrapper; beforeEach(() => { + // Mock implementation instead of Mock return value so that each call returns a new array (like normal) + spanAncestorIdsSpy.mockImplementation(() => [parentSpanID, rootSpanID]); props = { addHoverIndentGuideId: jest.fn(), hoverIndentGuideIds: new Set(), removeHoverIndentGuideId: jest.fn(), - span: spanWithTwoAncestors, + span: { + hasChildren: false, + spanID: ownSpanID, + }, }; wrapper = shallow(); }); describe('.SpanTreeOffset--indentGuide', () => { it('renders only one .SpanTreeOffset--indentGuide for entire trace if span has no ancestors', () => { - props.span = { - spanID: 'parentlessSpanID', - references: [ - { - refType: 'NOT_CHILD_OF', - span: { - spanID: 'notAParentSpanID', - references: [], - }, - }, - ], - }; + spanAncestorIdsSpy.mockReturnValue([]); wrapper = shallow(); const indentGuides = wrapper.find('.SpanTreeOffset--indentGuide'); expect(indentGuides.length).toBe(1); expect(indentGuides.prop('data-ancestor-id')).toBe(specialRootID); }); - it('renders one .SpanTreeOffset--indentGuide per ancestor span, plus one for entire trace, including FOLLOWS_FROM', () => { - props.span = { - hasChildren: false, - references: [ - { - refType: 'FOLLOWS_FROM', - span: { - spanID: parentSpanID, - references: [ - { - refType: 'CHILD_OF', - span: { - spanID: rootSpanID, - }, - }, - ], - }, - }, - ], - spanID: ownSpanID, - }; - wrapper = shallow(); - const indentGuides = wrapper.find('.SpanTreeOffset--indentGuide'); - expect(indentGuides.length).toBe(3); - expect(indentGuides.at(0).prop('data-ancestor-id')).toBe(specialRootID); - expect(indentGuides.at(1).prop('data-ancestor-id')).toBe(rootSpanID); - expect(indentGuides.at(2).prop('data-ancestor-id')).toBe(parentSpanID); - }); - it('renders one .SpanTreeOffset--indentGuide per ancestor span, plus one for entire trace', () => { const indentGuides = wrapper.find('.SpanTreeOffset--indentGuide'); expect(indentGuides.length).toBe(3); @@ -165,6 +112,12 @@ describe('SpanTreeOffset', () => { expect(wrapper.find(IoIosArrowDown).length).toBe(0); }); + it('does not render icon if props.span.hasChildren is true and showChildrenIcon is false', () => { + wrapper.setProps({ showChildrenIcon: false }); + expect(wrapper.find(IoChevronRight).length).toBe(0); + expect(wrapper.find(IoIosArrowDown).length).toBe(0); + }); + it('renders IoChevronRight if props.span.hasChildren is true and props.childrenVisible is false', () => { expect(wrapper.find(IoChevronRight).length).toBe(1); expect(wrapper.find(IoIosArrowDown).length).toBe(0); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js index b7b2bd74bf..236497e520 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js @@ -18,6 +18,9 @@ import * as React from 'react'; import cx from 'classnames'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; +import { withRouter } from 'react-router-dom'; + +import type { Location, RouterHistory } from 'react-router-dom'; import { actions } from './duck'; import ListView from './ListView'; @@ -25,8 +28,10 @@ import SpanBarRow from './SpanBarRow'; import DetailState from './SpanDetail/DetailState'; import SpanDetailRow from './SpanDetailRow'; import { createViewedBoundsFunc, findServerChildSpan, isErrorSpan, spanContainsErredSpan } from './utils'; +import { extractUiFindFromState } from '../../common/UiFindInput'; import getLinks from '../../../model/link-patterns'; import colorGenerator from '../../../utils/color-generator'; +import updateUiFind from '../../../utils/update-ui-find'; import type { ViewedBoundsFunctionType } from './utils'; import type { Accessors } from '../ScrollManager'; @@ -53,11 +58,14 @@ type VirtualizedTraceViewProps = { detailTagsToggle: string => void, detailToggle: string => void, findMatchesIDs: Set, + history: RouterHistory, + location: Location, registerAccessors: Accessors => void, setSpanNameColumnWidth: number => void, - setTrace: (?string) => void, + setTrace: (?Trace, ?string) => void, spanNameColumnWidth: number, trace: Trace, + uiFind: ?string, }; // export for tests @@ -133,7 +141,7 @@ export class VirtualizedTraceViewImpl extends React.PureComponent { + const { uiFind, history, location } = this.props; + if (!uiFind || !uiFind.includes(addition)) { + updateUiFind({ + history, + location, + uiFind: cx(uiFind, addition), + }); + } + }; + getAccessors() { const lv = this.listView; if (!lv) { @@ -346,6 +364,7 @@ export class VirtualizedTraceViewImpl extends React.PureComponent ', () => { const trace = transformTraceData(traceGenerator.trace({ numberOfSpans: 10 })); const props = { - trace, childrenHiddenIDs: new Set(), childrenToggle: jest.fn(), currentViewRangeTime: [0.25, 0.75], @@ -45,6 +44,8 @@ describe('', () => { setSpanNameColumnWidth: jest.fn(), setTrace: jest.fn(), spanNameColumnWidth: 0.5, + trace, + uiFind: 'uiFind', }; function expandRow(rowIndex) { @@ -101,12 +102,12 @@ describe('', () => { }); it('sets the trace for global state.traceTimeline', () => { - expect(props.setTrace.mock.calls).toEqual([[trace.traceID]]); + expect(props.setTrace.mock.calls).toEqual([[trace, props.uiFind]]); props.setTrace.mockReset(); const traceID = 'some-other-id'; const _trace = { ...trace, traceID }; wrapper.setProps({ trace: _trace }); - expect(props.setTrace.mock.calls).toEqual([[traceID]]); + expect(props.setTrace.mock.calls).toEqual([[_trace, props.uiFind]]); }); describe('props.registerAccessors', () => { diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.js index 12b7d28e01..d9b1b4c0c1 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.js @@ -15,7 +15,9 @@ import { createActions, handleActions } from 'redux-actions'; import DetailState from './SpanDetail/DetailState'; +import filterSpans from '../../../utils/filter-spans'; import generateActionTypes from '../../../utils/generate-action-types'; +import spanAncestorIds from '../../../utils/span-ancestor-ids'; // DetailState { // isTagsOpen: bool, @@ -33,13 +35,13 @@ import generateActionTypes from '../../../utils/generate-action-types'; // detailStates: Map // } -export function newInitialState({ spanNameColumnWidth = null, traceID = null } = {}) { +export function newInitialState() { return { childrenHiddenIDs: new Set(), detailStates: new Map(), hoverIndentGuideIds: new Set(), - spanNameColumnWidth: spanNameColumnWidth || 0.25, - traceID, + spanNameColumnWidth: 0.25, + traceID: null, }; } @@ -61,7 +63,7 @@ export const actionTypes = generateActionTypes('@jaeger-ui/trace-timeline-viewer ]); const fullActions = createActions({ - [actionTypes.SET_TRACE]: traceID => ({ traceID }), + [actionTypes.SET_TRACE]: (trace, uiFind) => ({ trace, uiFind }), [actionTypes.SET_SPAN_NAME_COLUMN_WIDTH]: width => ({ width }), [actionTypes.CHILDREN_TOGGLE]: spanID => ({ spanID }), [actionTypes.EXPAND_ALL]: () => ({}), @@ -80,13 +82,35 @@ const fullActions = createActions({ export const actions = fullActions.jaegerUi.traceTimelineViewer; function setTrace(state, { payload }) { - const { traceID } = payload; + const { uiFind, trace } = payload; + const { traceID, spans } = trace; if (traceID === state.traceID) { return state; } // preserve spanNameColumnWidth when resetting state const { spanNameColumnWidth } = state; - return newInitialState({ spanNameColumnWidth, traceID }); + const newStateValues = { spanNameColumnWidth, traceID }; + + // If there is a filter, collapse all rows except the path(s) to match(es) and show details for match(es) + if (uiFind) { + const spansMap = new Map(); + + newStateValues.childrenHiddenIDs = new Set(); + newStateValues.detailStates = new Map(); + + spans.forEach(span => { + spansMap.set(span.spanID, span); + newStateValues.childrenHiddenIDs.add(span.spanID); + }); + + filterSpans(uiFind, spans).forEach(spanID => { + const span = spansMap.get(spanID); + newStateValues.detailStates.set(spanID, new DetailState()); + spanAncestorIds(span).forEach(ancestorID => newStateValues.childrenHiddenIDs.delete(ancestorID)); + }); + } + + return Object.assign(newInitialState(), newStateValues); } function setColumnWidth(state, { payload }) { 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 97bbc70fd3..db3cedc347 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.test.js @@ -13,19 +13,25 @@ // limitations under the License. import { createStore } from 'redux'; +import _reduce from 'lodash/reduce'; 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'; +import filterSpansSpy from '../../../utils/filter-spans'; +import spanAncestorIdsSpy from '../../../utils/span-ancestor-ids'; + +jest.mock('../../../utils/filter-spans'); +jest.mock('../../../utils/span-ancestor-ids'); describe('TraceTimelineViewer/duck', () => { - const trace = transformTraceData(traceGenerator.trace({ numberOfSpans: 10 })); + const trace = transformTraceData(traceGenerator.trace({ numberOfSpans: 30 })); let store; beforeEach(() => { - store = createStore(reducer, newInitialState(trace)); + store = createStore(reducer, newInitialState()); }); it('the initial state has no details, collapsed children or text search', () => { @@ -45,38 +51,102 @@ describe('TraceTimelineViewer/duck', () => { expect(width).toBe(n); }); - it('retains all state when setting to the same traceID', () => { - const state = store.getState(); - const action = actions.setTrace(trace.traceID); - store.dispatch(action); - expect(store.getState()).toBe(state); - }); + describe('setTrace', () => { + describe('without uiFind', () => { + it('retains all state when setting to the same traceID', () => { + const action = actions.setTrace(trace); + store.dispatch(action); + const state = store.getState(); + store.dispatch(action); + expect(store.getState()).toBe(state); + }); - it('retains only the spanNameColumnWidth when changing traceIDs', () => { - let action; - const width = 0.5; - const id = 'some-id'; + it('retains only the spanNameColumnWidth when changing traceIDs', () => { + let action; + const width = 0.5; + const id = 'some-id'; - action = actions.childrenToggle(id); - store.dispatch(action); - action = actions.detailToggle(id); - store.dispatch(action); - action = actions.setSpanNameColumnWidth(width); - store.dispatch(action); + action = actions.childrenToggle(id); + store.dispatch(action); + action = actions.detailToggle(id); + store.dispatch(action); + action = actions.setSpanNameColumnWidth(width); + store.dispatch(action); - let state = store.getState(); - expect(state.traceID).toBe(trace.traceID); - expect(state.childrenHiddenIDs).not.toEqual(new Set()); - expect(state.detailStates).not.toEqual(new Map()); - expect(state.spanNameColumnWidth).toBe(width); + let state = store.getState(); + expect(state.traceID).toBe(null); + expect(state.childrenHiddenIDs).not.toEqual(new Set()); + expect(state.detailStates).not.toEqual(new Map()); + expect(state.spanNameColumnWidth).toBe(width); - action = actions.setTrace(id); - store.dispatch(action); - state = store.getState(); - expect(state.traceID).toBe(id); - expect(state.childrenHiddenIDs).toEqual(new Set()); - expect(state.detailStates).toEqual(new Map()); - expect(state.spanNameColumnWidth).toBe(width); + action = actions.setTrace(trace); + store.dispatch(action); + state = store.getState(); + expect(state.traceID).toBe(trace.traceID); + expect(state.childrenHiddenIDs).toEqual(new Set()); + expect(state.detailStates).toEqual(new Map()); + expect(state.spanNameColumnWidth).toBe(width); + }); + }); + + describe('with uiFind', () => { + const uiFind = 'uiFind'; + const uiFindMatchesArray = [trace.spans[5].spanID, trace.spans[10].spanID, trace.spans[15].spanID]; + const uiFindMatches = new Set(uiFindMatchesArray); + const uiFindAncestorIdsMockSchema = { + [uiFindMatchesArray[0]]: [trace.spans[0].spanID, trace.spans[3].spanID, trace.spans[4].spanID], + [uiFindMatchesArray[1]]: [ + trace.spans[0].spanID, + trace.spans[3].spanID, + trace.spans[4].spanID, + trace.spans[5].spanID, + trace.spans[8].spanID, + trace.spans[9].spanID, + ], + [uiFindMatchesArray[2]]: [ + trace.spans[0].spanID, + trace.spans[3].spanID, + trace.spans[13].spanID, + trace.spans[14].spanID, + ], + }; + const uiFindAncestorIdsSet = new Set( + _reduce(uiFindAncestorIdsMockSchema, (allAncestors, spanAncestors) => + allAncestors.concat(spanAncestors) + ) + ); + let state; + + beforeAll(() => { + filterSpansSpy.mockReturnValue(uiFindMatches); + spanAncestorIdsSpy.mockImplementation(({ spanID }) => uiFindAncestorIdsMockSchema[spanID]); + const action = actions.setTrace(trace, uiFind); + store = createStore(reducer, newInitialState()); + store.dispatch(action); + state = store.getState(); + }); + + it('adds a detailState for each span matching the uiFind filter', () => { + // Sanity check + expect(trace.spans).toHaveLength(30); + expect(uiFindMatches.size).toBe(3); + + expect(filterSpansSpy).toHaveBeenCalledWith(uiFind, trace.spans); + trace.spans.forEach(({ spanID }) => { + expect(state.detailStates.has(spanID)).toBe(uiFindMatches.has(spanID)); + }); + }); + + it('hides the children of all spanIDs that are not ancestors of a span matching the uiFind filter', () => { + // Sanity check + expect(trace.spans).toHaveLength(30); + expect(uiFindAncestorIdsSet.size).toBe(8); + + trace.spans.forEach(({ spanID }) => { + expect(state.childrenHiddenIDs.has(spanID)).toBe(!uiFindAncestorIdsSet.has(spanID)); + }); + }); + }); }); describe('toggles children and details', () => { diff --git a/packages/jaeger-ui/src/components/TracePage/index.js b/packages/jaeger-ui/src/components/TracePage/index.js index b7620c210f..30446bc87d 100644 --- a/packages/jaeger-ui/src/components/TracePage/index.js +++ b/packages/jaeger-ui/src/components/TracePage/index.js @@ -17,7 +17,9 @@ import * as React from 'react'; import { Input } from 'antd'; import _clamp from 'lodash/clamp'; +import _get from 'lodash/get'; import _mapValues from 'lodash/mapValues'; +import _memoize from 'lodash/memoize'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -25,10 +27,11 @@ import type { Location, Match, RouterHistory } from 'react-router-dom'; import ArchiveNotifier from './ArchiveNotifier'; import { actions as archiveActions } from './ArchiveNotifier/duck'; -import { trackFilter, trackRange } from './index.track'; +import { trackRange } from './index.track'; import { merge as mergeShortcuts, reset as resetShortcuts } from './keyboard-shortcuts'; import { cancel as cancelScroll, scrollBy, scrollTo } from './scroll-page'; import ScrollManager from './ScrollManager'; +import calculateTraceDagEV from './TraceGraph/calculateTraceDagEV'; import TraceGraph from './TraceGraph/TraceGraph'; import { trackSlimHeaderToggle } from './TracePageHeader/TracePageHeader.track'; import TracePageHeader from './TracePageHeader'; @@ -36,15 +39,18 @@ import TraceTimelineViewer from './TraceTimelineViewer'; import { getLocation, getUrl } from './url'; import ErrorMessage from '../common/ErrorMessage'; import LoadingIndicator from '../common/LoadingIndicator'; +import { extractUiFindFromState } from '../common/UiFindInput'; import * as jaegerApiActions from '../../actions/jaeger-api'; +import { getUiFindVertexKeys } from '../TraceDiff/TraceDiffGraph/traceDiffGraphUtils'; import { fetchedState } from '../../constants'; +import filterSpans from '../../utils/filter-spans'; +import updateUiFind from '../../utils/update-ui-find'; import type { CombokeysHandler, ShortcutCallbacks } from './keyboard-shortcuts'; import type { ViewRange, ViewRangeTimeUpdate } from './types'; import type { FetchedTrace, ReduxState } from '../../types'; import type { TraceArchive } from '../../types/archive'; import type { EmbeddedState } from '../../types/embedded'; -import type { KeyValuePair, Span } from '../../types/trace'; import './index.css'; @@ -60,14 +66,13 @@ type TracePageProps = { location: Location, searchUrl: null | string, trace: ?FetchedTrace, + uiFind: ?string, }; type TracePageState = { - findMatchesIDs: ?Set, headerHeight: ?number, slimView: boolean, traceGraphView: boolean, - textFilter: string, viewRange: ViewRange, }; @@ -105,17 +110,18 @@ export class TracePageImpl extends React.PureComponent + `${textFilter} ${_get(this.props.trace, 'traceID')} ${_get(this.props.trace, 'data.spans.length')}` + ); this._scrollManager = new ScrollManager(trace && trace.data, { scrollBy, scrollTo, @@ -217,69 +230,14 @@ export class TracePageImpl extends React.PureComponent { - const spans = this.props.trace && this.props.trace.data && this.props.trace.data.spans; - if (!spans) return null; - - // if a span field includes at least one filter in includeFilters, the span is a match - const includeFilters = []; - - // values with keys that include text in any one of the excludeKeys will be ignored - const excludeKeys = []; - - // split textFilter by whitespace, remove empty strings, and extract includeFilters and excludeKeys - textFilter - .split(' ') - .map(s => s.trim()) - .filter(s => s) - .forEach(w => { - if (w[0] === '-') { - excludeKeys.push(w.substr(1).toLowerCase()); - } else { - includeFilters.push(w.toLowerCase()); - } - }); - - const isTextInFilters = (filters: Array, text: string) => - filters.some(filter => text.toLowerCase().includes(filter)); - - const isTextInKeyValues = (kvs: Array) => - kvs - ? kvs.some(kv => { - // ignore checking key and value for a match if key is in excludeKeys - if (isTextInFilters(excludeKeys, kv.key)) return false; - // match if key or value matches an item in includeFilters - return ( - isTextInFilters(includeFilters, kv.key) || isTextInFilters(includeFilters, kv.value.toString()) - ); - }) - : false; - - const isSpanAMatch = (span: Span) => - isTextInFilters(includeFilters, span.operationName) || - isTextInFilters(includeFilters, span.process.serviceName) || - isTextInKeyValues(span.tags) || - span.logs.some(log => isTextInKeyValues(log.fields)) || - isTextInKeyValues(span.process.tags); - - // declare as const because need to disambiguate the type - const rv: Set = new Set(spans.filter(isSpanAMatch).map((span: Span) => span.spanID)); - return rv; - }; - - updateTextFilter = (textFilter: string) => { - let findMatchesIDs; - if (textFilter.trim()) { - findMatchesIDs = this.filterSpans(textFilter); - } else { - findMatchesIDs = null; - } - trackFilter(textFilter); - this.setState({ textFilter, findMatchesIDs }); - }; - clearSearch = () => { - this.updateTextFilter(''); + const { history, location } = this.props; + // flow does not allow omitting optional kwargs when using an object literal. + const arg = { + history, + location, + }; + updateUiFind(arg); if (this._searchBar.current) this._searchBar.current.blur(); }; @@ -307,6 +265,9 @@ export class TracePageImpl extends React.PureComponent { const { traceGraphView } = this.state; + if (this.props.trace && this.props.trace.data) { + this.traceDagEV = calculateTraceDagEV(this.props.trace.data); + } this.setState({ traceGraphView: !traceGraphView }); }; @@ -333,8 +294,8 @@ export class TracePageImpl extends React.PureComponent; } @@ -343,10 +304,14 @@ export class TracePageImpl extends React.PureComponent; } + // $FlowIgnore because flow believes Set cannot be assigned to Set + const findMatches: Set = traceGraphView + ? getUiFindVertexKeys(uiFind || '', _get(this.traceDagEV, 'vertices', [])) + : this._filterSpans(uiFind || '', _get(trace, 'data.spans')); const isEmbedded = Boolean(embedded); const headerProps = { slimView, - textFilter, + textFilter: uiFind, traceGraphView, viewRange, canCollapse: !embedded || !embedded.timeline.hideSummary || !embedded.timeline.hideMinimap, @@ -360,7 +325,7 @@ export class TracePageImpl extends React.PureComponent - + ) : (
{ let adjRange; @@ -75,19 +82,78 @@ describe('', () => { const trace = transformTraceData(traceGenerator.trace({})); const defaultProps = { - trace: { data: trace, state: fetchedState.DONE }, + acknowledgeArchive: () => {}, fetchTrace() {}, id: trace.traceID, + history: { + replace: () => {}, + }, + location: { + search: null, + }, + trace: { data: trace, state: fetchedState.DONE }, }; + const notDefaultPropsId = `not ${defaultProps.id}`; let wrapper; + beforeAll(() => { + filterSpansSpy.mockReturnValue(new Set()); + }); + beforeEach(() => { wrapper = shallow(); + filterSpansSpy.mockClear(); + updateUiFindSpy.mockClear(); }); - it.skip('renders a ', () => { - expect(wrapper.find(TracePageHeader).get(0)).toBeTruthy(); + describe('clearSearch', () => { + it('calls updateUiFind with expected kwargs when clearing search', () => { + expect(updateUiFindSpy).not.toHaveBeenCalled(); + wrapper.setProps({ id: notDefaultPropsId }); + expect(updateUiFindSpy).toHaveBeenCalledWith({ + history: defaultProps.history, + location: defaultProps.location, + }); + }); + + it('blurs _searchBar.current when _searchBar.current exists', () => { + const blur = jest.fn(); + wrapper.instance()._searchBar.current = { + blur, + }; + wrapper.setProps({ id: notDefaultPropsId }); + expect(blur).toHaveBeenCalledTimes(1); + }); + + it('handles null _searchBar.current', () => { + expect(wrapper.instance()._searchBar.current).toBe(null); + wrapper.setProps({ id: notDefaultPropsId }); + }); + }); + + it('uses props.uiFind, props.trace.traceID, and props.trace.spans.length to create filterSpans memo cache key', () => { + expect(filterSpansSpy).toHaveBeenCalledTimes(0); + + const uiFind = 'uiFind'; + wrapper.setProps({ uiFind }); + // changing props.id is used to trigger renders without invalidating memo cache key + wrapper.setProps({ id: notDefaultPropsId }); + expect(filterSpansSpy).toHaveBeenCalledTimes(1); + expect(filterSpansSpy).toHaveBeenLastCalledWith(uiFind, defaultProps.trace.data.spans); + + const newTrace = { ...defaultProps.trace, traceID: `not-${defaultProps.trace.traceID}` }; + wrapper.setProps({ trace: newTrace }); + wrapper.setProps({ id: defaultProps.id }); + expect(filterSpansSpy).toHaveBeenCalledTimes(2); + expect(filterSpansSpy).toHaveBeenLastCalledWith(uiFind, newTrace.data.spans); + + // Mutating props is not advised, but emulates behavior done somewhere else + newTrace.data.spans.splice(0, newTrace.data.spans.length / 2); + wrapper.setProps({ id: notDefaultPropsId }); + wrapper.setProps({ id: defaultProps.id }); + expect(filterSpansSpy).toHaveBeenCalledTimes(3); + expect(filterSpansSpy).toHaveBeenLastCalledWith(uiFind, newTrace.data.spans); }); it('renders a a loading indicator when not provided a fetched trace', () => { @@ -107,6 +173,37 @@ describe('', () => { expect(loading.length).toBe(1); }); + it('forces lowercase id', () => { + const replaceMock = jest.fn(); + const props = { + ...defaultProps, + id: trace.traceID.toUpperCase(), + history: { + replace: replaceMock, + }, + }; + shallow(); + expect(replaceMock).toHaveBeenCalledWith( + expect.objectContaining({ + pathname: expect.stringContaining(trace.traceID), + }) + ); + }); + + it('focuses on search bar when there is a search bar and focusOnSearchBar is called', () => { + const focus = jest.fn(); + wrapper.instance()._searchBar.current = { + focus, + }; + wrapper.instance().focusOnSearchBar(); + expect(focus).toHaveBeenCalledTimes(1); + }); + + it('handles absent search bar when there is not a search bar and focusOnSearchBar is called', () => { + expect(wrapper.instance()._searchBar.current).toBe(null); + wrapper.instance().focusOnSearchBar(); + }); + it('fetches the trace if necessary', () => { const fetchTrace = sinon.spy(); wrapper = mount(); @@ -114,13 +211,13 @@ describe('', () => { expect(fetchTrace.calledWith(trace.traceID)).toBe(true); }); - it.skip("doesn't fetch the trace if already present", () => { + it("doesn't fetch the trace if already present", () => { const fetchTrace = sinon.spy(); wrapper = mount(); expect(fetchTrace.called).toBeFalsy(); }); - it.skip('resets the view range when the trace changes', () => { + it('resets the view range when the trace changes', () => { const altTrace = { ...trace, traceID: 'some-other-id' }; // mount because `.componentDidUpdate()` wrapper = mount(); @@ -148,6 +245,130 @@ describe('', () => { expect(cancelScroll.mock.calls).toEqual([[]]); }); + describe('TracePageHeader props', () => { + describe('canCollapse', () => { + it('is true if !embedded', () => { + expect(wrapper.find(TracePageHeader).prop('canCollapse')).toBe(true); + }); + + it('is true if either of embedded.timeline.hideSummary and embedded.timeline.hideMinimap are false', () => { + [true, false].forEach(hideSummary => { + [true, false].forEach(hideMinimap => { + const embedded = { + timeline: { + hideSummary, + hideMinimap, + }, + }; + wrapper.setProps({ embedded }); + expect(wrapper.find(TracePageHeader).prop('canCollapse')).toBe(!hideSummary || !hideMinimap); + }); + }); + }); + }); + + describe('calculates hideMap correctly', () => { + it('is true if on traceGraphView', () => { + wrapper.instance().traceDagEV = { vertices: [], nodes: [] }; + wrapper.setState({ traceGraphView: true }); + expect(wrapper.find(TracePageHeader).prop('hideMap')).toBe(true); + }); + + it('is true if embedded indicates it should be', () => { + wrapper.setProps({ + embedded: { + timeline: { + hideMinimap: false, + }, + }, + }); + expect(wrapper.find(TracePageHeader).prop('hideMap')).toBe(false); + wrapper.setProps({ + embedded: { + timeline: { + hideMinimap: true, + }, + }, + }); + expect(wrapper.find(TracePageHeader).prop('hideMap')).toBe(true); + }); + }); + + describe('calculates hideSummary correctly', () => { + it('is false if embedded is not provided', () => { + expect(wrapper.find(TracePageHeader).prop('hideSummary')).toBe(false); + }); + + it('is true if embedded indicates it should be', () => { + wrapper.setProps({ + embedded: { + timeline: { + hideSummary: false, + }, + }, + }); + expect(wrapper.find(TracePageHeader).prop('hideSummary')).toBe(false); + wrapper.setProps({ + embedded: { + timeline: { + hideSummary: true, + }, + }, + }); + expect(wrapper.find(TracePageHeader).prop('hideSummary')).toBe(true); + }); + }); + + describe('showArchiveButton', () => { + it('is true when not embedded and archive is enabled', () => { + [{ timeline: {} }, undefined].forEach(embedded => { + [true, false].forEach(archiveEnabled => { + wrapper.setProps({ embedded, archiveEnabled }); + expect(wrapper.find(TracePageHeader).prop('showArchiveButton')).toBe(!embedded && archiveEnabled); + }); + }); + }); + }); + + describe('resultCount', () => { + it('is the size of findMatchesIDs when available', () => { + expect(wrapper.find(TracePageHeader).prop('resultCount')).toBe(0); + + const size = 20; + filterSpansSpy.mockReturnValueOnce({ size }); + wrapper.setProps({ uiFind: 'new ui find to bust memo' }); + expect(wrapper.find(TracePageHeader).prop('resultCount')).toBe(size); + }); + + it('defaults to 0', () => { + filterSpansSpy.mockReturnValueOnce(null); + wrapper.setProps({ uiFind: 'new ui find to bust memo' }); + expect(wrapper.find(TracePageHeader).prop('resultCount')).toBe(0); + }); + }); + + describe('isEmbedded derived props', () => { + it('toggles derived props when embedded is provided', () => { + expect(wrapper.find(TracePageHeader).props()).toEqual( + expect.objectContaining({ + showShortcutsHelp: true, + showStandaloneLink: false, + showViewOptions: true, + }) + ); + + wrapper.setProps({ embedded: { timeline: {} } }); + expect(wrapper.find(TracePageHeader).props()).toEqual( + expect.objectContaining({ + showShortcutsHelp: false, + showStandaloneLink: true, + showViewOptions: false, + }) + ); + }); + }); + }); + describe('_adjustViewRange()', () => { let instance; let time; @@ -211,6 +432,28 @@ describe('', () => { }); }); + describe('Archive', () => { + it('renders ArchiveNotifier if props.archiveEnabled is true', () => { + expect(wrapper.find(ArchiveNotifier).length).toBe(0); + wrapper.setProps({ archiveEnabled: true }); + expect(wrapper.find(ArchiveNotifier).length).toBe(1); + }); + + it('calls props.acknowledgeArchive when ArchiveNotifier acknowledges', () => { + const acknowledgeArchive = jest.fn(); + wrapper.setProps({ acknowledgeArchive, archiveEnabled: true }); + wrapper.find(ArchiveNotifier).prop('acknowledge')(); + expect(acknowledgeArchive).toHaveBeenCalledWith(defaultProps.id); + }); + + it("calls props.archiveTrace when TracePageHeader's archive button is clicked", () => { + const archiveTrace = jest.fn(); + wrapper.setProps({ archiveTrace }); + wrapper.find(TracePageHeader).prop('onArchiveClicked')(); + expect(archiveTrace).toHaveBeenCalledWith(defaultProps.id); + }); + }); + describe('manages various UI state', () => { let header; let spanGraph; @@ -230,7 +473,7 @@ describe('', () => { refreshWrappers(); }); - it.skip('propagates headerHeight changes', () => { + it('propagates headerHeight changes', () => { const h = 100; const { setHeaderHeight } = wrapper.instance(); // use the method directly because it is a `ref` prop @@ -247,17 +490,7 @@ describe('', () => { expect(sections.length).toBe(0); }); - it.skip('propagates textFilter changes', () => { - const s = 'abc'; - const { updateTextFilter } = header.props(); - expect(header.prop('textFilter')).toBe(''); - updateTextFilter(s); - wrapper.update(); - refreshWrappers(); - expect(header.prop('textFilter')).toBe(s); - }); - - it.skip('propagates slimView changes', () => { + it('propagates slimView changes', () => { const { onSlimViewClicked } = header.props(); expect(header.prop('slimView')).toBe(false); expect(spanGraph.type()).toBeDefined(); @@ -268,7 +501,24 @@ describe('', () => { expect(spanGraph.length).toBe(0); }); - it.skip('propagates viewRange changes', () => { + it('propagates textFilter changes', () => { + const s = 'abc'; + expect(header.prop('textFilter')).toBeUndefined(); + wrapper.setProps({ uiFind: s }); + refreshWrappers(); + expect(header.prop('textFilter')).toBe(s); + }); + + it('propagates traceGraphView changes', () => { + const { onTraceGraphViewClicked } = header.props(); + expect(header.prop('traceGraphView')).toBe(false); + onTraceGraphViewClicked(); + wrapper.update(); + refreshWrappers(); + expect(header.prop('traceGraphView')).toBe(true); + }); + + it('propagates viewRange changes', () => { const viewRange = { time: { current: [0, 1] }, }; @@ -280,7 +530,7 @@ describe('', () => { updateNextViewRangeTime({ cursor }); wrapper.update(); refreshWrappers(); - viewRange.time.cursor = cursor; + viewRange.cursor = cursor; expect(spanGraph.prop('viewRange')).toEqual(viewRange); expect(timeline.prop('viewRange')).toEqual(viewRange); updateViewRangeTime(...current); @@ -309,7 +559,7 @@ describe('', () => { refreshWrappers(); }); - it.skip('tracks setting the header to slim-view', () => { + it('tracks setting the header to slim-view', () => { const { onSlimViewClicked } = header.props(); trackSlimHeaderToggle.mockReset(); onSlimViewClicked(true); @@ -317,15 +567,7 @@ describe('', () => { expect(trackSlimHeaderToggle.mock.calls).toEqual([[true], [false]]); }); - it.skip('tracks setting or clearing the filter', () => { - const { updateTextFilter } = header.props(); - track.trackFilter.mockClear(); - updateTextFilter('abc'); - updateTextFilter(''); - expect(track.trackFilter.mock.calls).toEqual([['abc'], ['']]); - }); - - it.skip('tracks changes to the viewRange', () => { + it('tracks changes to the viewRange', () => { const src = 'some-source'; const { updateViewRangeTime } = spanGraph.props(); track.trackRange.mockClear(); @@ -387,6 +629,23 @@ describe('mapStateToProps()', () => { }); }); + it('handles falsy ownProps.match.params.id', () => { + const props = mapStateToProps(state, { + match: { + params: { + id: '', + }, + }, + }); + expect(props).toEqual( + expect.objectContaining({ + archiveTraceState: null, + id: '', + trace: null, + }) + ); + }); + it('propagates fromSearch correctly', () => { const fakeUrl = 'fake-url'; state.router.location.state = { fromSearch: fakeUrl }; diff --git a/packages/jaeger-ui/src/components/common/UiFindInput.js b/packages/jaeger-ui/src/components/common/UiFindInput.js new file mode 100644 index 0000000000..41a027ec37 --- /dev/null +++ b/packages/jaeger-ui/src/components/common/UiFindInput.js @@ -0,0 +1,98 @@ +// @flow + +// Copyright (c) 2019 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 * as React from 'react'; +import { Input } from 'antd'; +import _debounce from 'lodash/debounce'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; +import queryString from 'query-string'; + +import type { Location, RouterHistory } from 'react-router-dom'; + +import updateUiFind from '../../utils/update-ui-find'; + +import type { ReduxState } from '../../types/index'; + +type PropsType = { + forwardedRef?: { current: Input | null }, + inputProps?: Object, + history: RouterHistory, + location: Location, + trackFindFunction?: (?string) => void, + uiFind?: string, +}; + +type StateType = { + ownInputValue: ?string, +}; + +export class UnconnectedUiFindInput extends React.PureComponent { + static defaultProps = { + forwardedRef: null, + inputProps: {}, + trackFindFunction: null, + uiFind: null, + }; + + state = { + ownInputValue: null, + }; + + handleInputBlur = () => { + this.updateUiFindQueryParam.flush(); + this.setState({ ownInputValue: null }); + }; + + handleInputChange = (evt: SyntheticInputEvent) => { + const { value } = evt.target; + this.updateUiFindQueryParam(value); + this.setState({ ownInputValue: value }); + }; + + updateUiFindQueryParam = _debounce((uiFind: ?string) => { + const { history, location, trackFindFunction } = this.props; + updateUiFind({ + location, + history, + trackFindFunction, + uiFind, + }); + }, 250); + + render() { + const inputValue = + typeof this.state.ownInputValue === 'string' ? this.state.ownInputValue : this.props.uiFind; + + return ( + + ); + } +} + +export function extractUiFindFromState(state: ReduxState): { uiFind?: string } { + const { uiFind } = queryString.parse(state.router.location.search); + return { uiFind }; +} + +export default withRouter(connect(extractUiFindFromState)(UnconnectedUiFindInput)); diff --git a/packages/jaeger-ui/src/components/common/UiFindInput.test.js b/packages/jaeger-ui/src/components/common/UiFindInput.test.js new file mode 100644 index 0000000000..e5f85b10de --- /dev/null +++ b/packages/jaeger-ui/src/components/common/UiFindInput.test.js @@ -0,0 +1,142 @@ +// Copyright (c) 2019 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import { Input } from 'antd'; +import debounceMock from 'lodash/debounce'; +import queryString from 'query-string'; + +import { UnconnectedUiFindInput, extractUiFindFromState } from './UiFindInput'; +import updateUiFindSpy from '../../utils/update-ui-find'; + +jest.mock('lodash/debounce'); + +jest.mock('../../utils/update-ui-find'); + +describe('UiFind', () => { + const flushMock = jest.fn(); + const queryStringParseSpy = jest.spyOn(queryString, 'parse'); + + const uiFind = 'uiFind'; + const ownInputValue = 'ownInputValue'; + const props = { + uiFind: null, + history: { + replace: () => {}, + }, + location: { + search: null, + }, + }; + let wrapper; + + beforeAll(() => { + debounceMock.mockImplementation(fn => { + function debounceFunction(...args) { + fn(...args); + } + debounceFunction.flush = flushMock; + return debounceFunction; + }); + }); + + beforeEach(() => { + flushMock.mockReset(); + wrapper = shallow(); + }); + + describe('rendering', () => { + it('renders as expected', () => { + expect(wrapper).toMatchSnapshot(); + }); + + it('renders props.uiFind when state.ownInputValue is `null`', () => { + wrapper.setProps({ uiFind }); + expect(wrapper.find(Input).prop('value')).toBe(uiFind); + }); + + it('renders state.ownInputValue when it is not `null` regardless of props.uiFind', () => { + wrapper.setProps({ uiFind }); + wrapper.setState({ ownInputValue }); + expect(wrapper.find(Input).prop('value')).toBe(ownInputValue); + }); + }); + + describe('typing in input', () => { + const newValue = 'newValue'; + + it('updates state', () => { + wrapper.find(Input).simulate('change', { target: { value: newValue } }); + expect(wrapper.state('ownInputValue')).toBe(newValue); + }); + + it('calls updateUiFind with correct kwargs', () => { + wrapper.find(Input).simulate('change', { target: { value: newValue } }); + expect(updateUiFindSpy).toHaveBeenLastCalledWith({ + history: props.history, + location: props.location, + trackFindFunction: null, + uiFind: newValue, + }); + }); + + it('calls updateUiFind with correct kwargs with tracking enabled', () => { + const trackFindFunction = function trackFindFunction() {}; + wrapper.setProps({ trackFindFunction }); + wrapper.find(Input).simulate('change', { target: { value: newValue } }); + expect(updateUiFindSpy).toHaveBeenLastCalledWith({ + history: props.history, + location: props.location, + trackFindFunction, + uiFind: newValue, + }); + }); + }); + + describe('blurring input', () => { + it('clears state.ownInputValue', () => { + wrapper.setState({ ownInputValue }); + expect(wrapper.state('ownInputValue')).toBe(ownInputValue); + wrapper.find(Input).simulate('blur'); + expect(wrapper.state('ownInputValue')).toBe(null); + }); + + it('triggers pending queryParameter updates', () => { + wrapper.find(Input).simulate('blur'); + expect(flushMock).toHaveBeenCalledTimes(1); + }); + }); + + describe('extractUiFindFromState', () => { + beforeAll(() => { + queryStringParseSpy.mockReturnValue({ uiFind }); + }); + + it('returns uiFind from parsed state.router.location.search', () => { + const reduxStateValue = 'state.router.location.search'; + const result = extractUiFindFromState({ + router: { + location: { + search: reduxStateValue, + }, + }, + }); + expect(queryStringParseSpy).toHaveBeenCalledWith(reduxStateValue); + expect(result).toEqual({ + uiFind, + }); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/common/__snapshots__/UiFindInput.test.js.snap b/packages/jaeger-ui/src/components/common/__snapshots__/UiFindInput.test.js.snap new file mode 100644 index 0000000000..6b54793a94 --- /dev/null +++ b/packages/jaeger-ui/src/components/common/__snapshots__/UiFindInput.test.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UiFind rendering renders as expected 1`] = ` + +`; diff --git a/packages/jaeger-ui/src/model/trace-dag/TraceDag.js b/packages/jaeger-ui/src/model/trace-dag/TraceDag.js index 42c2bd47aa..c5bd817fc0 100644 --- a/packages/jaeger-ui/src/model/trace-dag/TraceDag.js +++ b/packages/jaeger-ui/src/model/trace-dag/TraceDag.js @@ -43,6 +43,7 @@ export default class TraceDag { }); const { data } = node; data[key] = src.count; + node.members.push(...src.members); node.count = data.b - data.a; if (!node.parentID) { dt.rootIDs.add(node.id); diff --git a/packages/jaeger-ui/src/utils/filter-spans.js b/packages/jaeger-ui/src/utils/filter-spans.js new file mode 100644 index 0000000000..e858670e2c --- /dev/null +++ b/packages/jaeger-ui/src/utils/filter-spans.js @@ -0,0 +1,65 @@ +// @flow +// Copyright (c) 2019 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 type { KeyValuePair, Span } from '../types/trace'; + +export default function filterSpans(textFilter: string, spans: ?(Span[])) { + if (!spans) return null; + + // if a span field includes at least one filter in includeFilters, the span is a match + const includeFilters = []; + + // values with keys that include text in any one of the excludeKeys will be ignored + const excludeKeys = []; + + // split textFilter by whitespace, remove empty strings, and extract includeFilters and excludeKeys + textFilter + .split(/\s+/) + .filter(Boolean) + .forEach(w => { + if (w[0] === '-') { + excludeKeys.push(w.substr(1).toLowerCase()); + } else { + includeFilters.push(w.toLowerCase()); + } + }); + + const isTextInFilters = (filters: Array, text: string) => + filters.some(filter => text.toLowerCase().includes(filter)); + + const isTextInKeyValues = (kvs: Array) => + kvs + ? kvs.some(kv => { + // ignore checking key and value for a match if key is in excludeKeys + if (isTextInFilters(excludeKeys, kv.key)) return false; + // match if key or value matches an item in includeFilters + return ( + isTextInFilters(includeFilters, kv.key) || isTextInFilters(includeFilters, kv.value.toString()) + ); + }) + : false; + + const isSpanAMatch = (span: Span) => + isTextInFilters(includeFilters, span.operationName) || + isTextInFilters(includeFilters, span.process.serviceName) || + isTextInKeyValues(span.tags) || + span.logs.some(log => isTextInKeyValues(log.fields)) || + isTextInKeyValues(span.process.tags) || + includeFilters.some(filter => filter === span.spanID); + + // declare as const because need to disambiguate the type + const rv: Set = new Set(spans.filter(isSpanAMatch).map((span: Span) => span.spanID)); + return rv; +} diff --git a/packages/jaeger-ui/src/utils/filter-spans.test.js b/packages/jaeger-ui/src/utils/filter-spans.test.js new file mode 100644 index 0000000000..b8d61cc973 --- /dev/null +++ b/packages/jaeger-ui/src/utils/filter-spans.test.js @@ -0,0 +1,184 @@ +// Copyright (c) 2019 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 filterSpans from './filter-spans'; + +describe('filterSpans', () => { + // span0 contains strings that end in 0 or 1 + const spanID0 = 'span-id-0'; + const span0 = { + spanID: spanID0, + operationName: 'operationName0', + process: { + serviceName: 'serviceName0', + tags: [ + { + key: 'processTagKey0', + value: 'processTagValue0', + }, + { + key: 'processTagKey1', + value: 'processTagValue1', + }, + ], + }, + tags: [ + { + key: 'tagKey0', + value: 'tagValue0', + }, + { + key: 'tagKey1', + value: 'tagValue1', + }, + ], + logs: [ + { + fields: [ + { + key: 'logFieldKey0', + value: 'logFieldValue0', + }, + { + key: 'logFieldKey1', + value: 'logFieldValue1', + }, + ], + }, + ], + }; + // span2 contains strings that end in 1 or 2, for overlap with span0 + // KVs in span2 have different numbers for key and value to facilitate excludesKey testing + const spanID2 = 'span-id-2'; + const span2 = { + spanID: spanID2, + operationName: 'operationName2', + process: { + serviceName: 'serviceName2', + tags: [ + { + key: 'processTagKey2', + value: 'processTagValue1', + }, + { + key: 'processTagKey1', + value: 'processTagValue2', + }, + ], + }, + tags: [ + { + key: 'tagKey2', + value: 'tagValue1', + }, + { + key: 'tagKey1', + value: 'tagValue2', + }, + ], + logs: [ + { + fields: [ + { + key: 'logFieldKey2', + value: 'logFieldValue1', + }, + { + key: 'logFieldKey1', + value: 'logFieldValue2', + }, + ], + }, + ], + }; + const spans = [span0, span2]; + + it('should return `null` if spans is falsy', () => { + expect(filterSpans('operationName', null)).toBe(null); + }); + + it('should return spans whose spanID exactly match a filter', () => { + expect(filterSpans('spanID', spans)).toEqual(new Set([])); + expect(filterSpans(spanID0, spans)).toEqual(new Set([spanID0])); + expect(filterSpans(spanID2, spans)).toEqual(new Set([spanID2])); + }); + + it('should return spans whose operationName match a filter', () => { + expect(filterSpans('operationName', spans)).toEqual(new Set([spanID0, spanID2])); + expect(filterSpans('operationName0', spans)).toEqual(new Set([spanID0])); + expect(filterSpans('operationName2', spans)).toEqual(new Set([spanID2])); + }); + + it('should return spans whose serviceName match a filter', () => { + expect(filterSpans('serviceName', spans)).toEqual(new Set([spanID0, spanID2])); + expect(filterSpans('serviceName0', spans)).toEqual(new Set([spanID0])); + expect(filterSpans('serviceName2', spans)).toEqual(new Set([spanID2])); + }); + + it("should return spans whose tags' kv.key match a filter", () => { + expect(filterSpans('tagKey1', spans)).toEqual(new Set([spanID0, spanID2])); + expect(filterSpans('tagKey0', spans)).toEqual(new Set([spanID0])); + expect(filterSpans('tagKey2', spans)).toEqual(new Set([spanID2])); + }); + + it("should return spans whose tags' kv.value match a filter", () => { + expect(filterSpans('tagValue1', spans)).toEqual(new Set([spanID0, spanID2])); + expect(filterSpans('tagValue0', spans)).toEqual(new Set([spanID0])); + expect(filterSpans('tagValue2', spans)).toEqual(new Set([spanID2])); + }); + + it("should exclude span whose tags' kv.value or kv.key match a filter if the key matches an excludeKey", () => { + expect(filterSpans('tagValue1 -tagKey2', spans)).toEqual(new Set([spanID0])); + expect(filterSpans('tagValue1 -tagKey1', spans)).toEqual(new Set([spanID2])); + }); + + it('should return spans whose logs have a field whose kv.key match a filter', () => { + expect(filterSpans('logFieldKey1', spans)).toEqual(new Set([spanID0, spanID2])); + expect(filterSpans('logFieldKey0', spans)).toEqual(new Set([spanID0])); + expect(filterSpans('logFieldKey2', spans)).toEqual(new Set([spanID2])); + }); + + it('should return spans whose logs have a field whose kv.value match a filter', () => { + expect(filterSpans('logFieldValue1', spans)).toEqual(new Set([spanID0, spanID2])); + expect(filterSpans('logFieldValue0', spans)).toEqual(new Set([spanID0])); + expect(filterSpans('logFieldValue2', spans)).toEqual(new Set([spanID2])); + }); + + it('should exclude span whose logs have a field whose kv.value or kv.key match a filter if the key matches an excludeKey', () => { + expect(filterSpans('logFieldValue1 -logFieldKey2', spans)).toEqual(new Set([spanID0])); + expect(filterSpans('logFieldValue1 -logFieldKey1', spans)).toEqual(new Set([spanID2])); + }); + + it("should return spans whose process.tags' kv.key match a filter", () => { + expect(filterSpans('processTagKey1', spans)).toEqual(new Set([spanID0, spanID2])); + expect(filterSpans('processTagKey0', spans)).toEqual(new Set([spanID0])); + expect(filterSpans('processTagKey2', spans)).toEqual(new Set([spanID2])); + }); + + it("should return spans whose process.processTags' kv.value match a filter", () => { + expect(filterSpans('processTagValue1', spans)).toEqual(new Set([spanID0, spanID2])); + expect(filterSpans('processTagValue0', spans)).toEqual(new Set([spanID0])); + expect(filterSpans('processTagValue2', spans)).toEqual(new Set([spanID2])); + }); + + it("should exclude span whose process.processTags' kv.value or kv.key match a filter if the key matches an excludeKey", () => { + expect(filterSpans('processTagValue1 -processTagKey2', spans)).toEqual(new Set([spanID0])); + expect(filterSpans('processTagValue1 -processTagKey1', spans)).toEqual(new Set([spanID2])); + }); + + // This test may false positive if other tests are failing + it('should return an empty set if no spans match the filter', () => { + expect(filterSpans('-processTagKey1', spans)).toEqual(new Set()); + }); +}); diff --git a/packages/jaeger-ui/src/utils/plexus/set-on-graph.js b/packages/jaeger-ui/src/utils/plexus/set-on-graph.js new file mode 100644 index 0000000000..5f04315a01 --- /dev/null +++ b/packages/jaeger-ui/src/utils/plexus/set-on-graph.js @@ -0,0 +1,48 @@ +// @flow + +// Copyright (c) 2019 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 _get from 'lodash/get'; + +const BASE_MATCH_SIZE = 8; +const SCALABLE_MATCH_SIZE = 4; + +export function setOnEdgesContainer(state: Object) { + const { zoomTransform } = state; + if (!zoomTransform) { + return null; + } + const { k } = zoomTransform; + const opacity = 0.1 + k * 0.9; + return { style: { opacity, zIndex: 1, position: 'absolute', pointerEvents: 'none' } }; +} + +export function setOnNodesContainer(state: Object) { + const { zoomTransform } = state; + const matchSize = BASE_MATCH_SIZE + SCALABLE_MATCH_SIZE / _get(zoomTransform, 'k', 1); + return { + style: { + outline: `transparent solid ${matchSize}px`, + }, + }; +} + +export function setOnNode() { + return { + style: { + outline: 'inherit', + }, + }; +} diff --git a/packages/jaeger-ui/src/utils/plexus/set-on-graph.test.js b/packages/jaeger-ui/src/utils/plexus/set-on-graph.test.js new file mode 100644 index 0000000000..4c2c43aea2 --- /dev/null +++ b/packages/jaeger-ui/src/utils/plexus/set-on-graph.test.js @@ -0,0 +1,72 @@ +// Copyright (c) 2019 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 { setOnNodesContainer, setOnEdgesContainer, setOnNode } from './set-on-graph'; + +describe('Set on graph utils', () => { + describe('setOnNodesContainer', () => { + function getComputedWidth(k) { + const { style } = setOnNodesContainer({ zoomTransform: k != undefined ? { k } : undefined }); // eslint-disable-line eqeqeq + return Number.parseInt(style.outline.replace(/[^.\d]/g, ''), 10); + } + + const SIZE_IDENTITY = 12; + + it('defaults style object with outline width off of 2 if zoomTransform.k is not provided', () => { + expect(getComputedWidth(null)).toBe(SIZE_IDENTITY); + expect(getComputedWidth(undefined)).toBe(SIZE_IDENTITY); + }); + + it('calculates style object with outline width at default size if zoomTransform.k is 1', () => { + expect(getComputedWidth(1)).toBe(SIZE_IDENTITY); + }); + + it('calculates style object with outline width one third larger if zoomTransform.k is .5', () => { + expect(getComputedWidth(0.5)).toBe(4 / 3 * SIZE_IDENTITY); + }); + + it('calculates style object with outline width two thirds larger if zoomTransform.k is .33', () => { + expect(getComputedWidth(0.33)).toBe(5 / 3 * SIZE_IDENTITY); + }); + + it('calculates style object with outline width twice as large if zoomTransform.k is .25', () => { + expect(getComputedWidth(0.25)).toBe(2 * SIZE_IDENTITY); + }); + }); + + describe('setOnEdgesContainer', () => { + it('returns null if zoomTransform kwarg is falsy', () => { + expect(setOnEdgesContainer({ zoomTransform: null })).toBe(null); + expect(setOnEdgesContainer({ zoomTransform: undefined })).toBe(null); + }); + + it('calculates style object with opacity off of zoomTransform.k', () => { + expect(setOnEdgesContainer({ zoomTransform: { k: 0.0 } }).style.opacity).toBe(0.1); + expect(setOnEdgesContainer({ zoomTransform: { k: 0.3 } }).style.opacity).toBe(0.37); + expect(setOnEdgesContainer({ zoomTransform: { k: 0.5 } }).style.opacity).toBe(0.55); + expect(setOnEdgesContainer({ zoomTransform: { k: 0.7 } }).style.opacity).toBe(0.73); + expect(setOnEdgesContainer({ zoomTransform: { k: 1.0 } }).style.opacity).toBe(1); + }); + }); + + describe('setOnNode', () => { + it("inherits container's outline", () => { + expect(setOnNode()).toEqual({ + style: { + outline: 'inherit', + }, + }); + }); + }); +}); diff --git a/packages/jaeger-ui/src/utils/span-ancestor-ids.js b/packages/jaeger-ui/src/utils/span-ancestor-ids.js new file mode 100644 index 0000000000..e2553177a4 --- /dev/null +++ b/packages/jaeger-ui/src/utils/span-ancestor-ids.js @@ -0,0 +1,41 @@ +// @flow + +// Copyright (c) 2019 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 _find from 'lodash/find'; +import _get from 'lodash/get'; + +import type { Span } from '../types/trace'; + +function getFirstAncestor(span: Span): ?Span { + return _get( + _find( + span.references, + ({ span: ref, refType }) => ref && ref.spanID && (refType === 'CHILD_OF' || refType === 'FOLLOWS_FROM') + ), + 'span' + ); +} + +export default function spanAncestorIds(span: ?Span): string[] { + if (!span) return []; + const ancestorIDs: Set = new Set(); + let ref = getFirstAncestor(span); + while (ref) { + ancestorIDs.add(ref.spanID); + ref = getFirstAncestor(ref); + } + return Array.from(ancestorIDs); +} diff --git a/packages/jaeger-ui/src/utils/span-ancestor-ids.test.js b/packages/jaeger-ui/src/utils/span-ancestor-ids.test.js new file mode 100644 index 0000000000..0714d141b0 --- /dev/null +++ b/packages/jaeger-ui/src/utils/span-ancestor-ids.test.js @@ -0,0 +1,96 @@ +// Copyright (c) 2018 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 spanAncestorIdsSpy from './span-ancestor-ids'; + +describe('spanAncestorIdsSpy', () => { + const ownSpanID = 'ownSpanID'; + const firstParentSpanID = 'firstParentSpanID'; + const firstParentFirstGrandparentSpanID = 'firstParentFirstGrandparentSpanID'; + const firstParentSecondGrandparentSpanID = 'firstParentSecondGrandparentSpanID'; + const secondParentSpanID = 'secondParentSpanID'; + const rootSpanID = 'rootSpanID'; + const span = { + references: [ + { + span: { + spanID: firstParentSpanID, + references: [ + { + span: { + spanID: firstParentFirstGrandparentSpanID, + references: [ + { + span: { + spanID: rootSpanID, + }, + }, + ], + }, + refType: 'not an ancestor ref type', + }, + { + span: { + spanID: firstParentSecondGrandparentSpanID, + references: [ + { + span: { + spanID: rootSpanID, + }, + refType: 'FOLLOWS_FROM', + }, + ], + }, + refType: 'CHILD_OF', + }, + ], + }, + refType: 'CHILD_OF', + }, + { + span: { + spanID: secondParentSpanID, + }, + refType: 'CHILD_OF', + }, + ], + spanID: ownSpanID, + }; + const expectedAncestorIds = [firstParentSpanID, firstParentSecondGrandparentSpanID, rootSpanID]; + + it('returns an empty array if given falsy span', () => { + expect(spanAncestorIdsSpy(null)).toEqual([]); + }); + + it('returns an empty array if span has no references', () => { + const spanWithoutReferences = { + spanID: 'parentlessSpanID', + references: [], + }; + + expect(spanAncestorIdsSpy(spanWithoutReferences)).toEqual([]); + }); + + it('returns all unique spanIDs from first valid CHILD_OF or FOLLOWS_FROM reference up to the root span', () => { + expect(spanAncestorIdsSpy(span)).toEqual(expectedAncestorIds); + }); + + it('ignores references without a span', () => { + const spanWithSomeEmptyReferences = { + ...span, + references: [{ refType: 'CHILD_OF' }, { refType: 'FOLLOWS_FROM', span: {} }, ...span.references], + }; + expect(spanAncestorIdsSpy(spanWithSomeEmptyReferences)).toEqual(expectedAncestorIds); + }); +}); diff --git a/packages/jaeger-ui/src/utils/update-ui-find.js b/packages/jaeger-ui/src/utils/update-ui-find.js new file mode 100644 index 0000000000..4d5738248e --- /dev/null +++ b/packages/jaeger-ui/src/utils/update-ui-find.js @@ -0,0 +1,43 @@ +// @flow + +// Copyright (c) 2019 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 queryString from 'query-string'; + +import type { Location, RouterHistory } from 'react-router-dom'; + +export default function updateUiFind({ + history, + location, + trackFindFunction, + uiFind, +}: { + history: RouterHistory, + location: Location, + trackFindFunction?: (?string) => void, + uiFind?: ?string, +}) { + const { uiFind: omittedOldValue, ...queryParams } = queryString.parse(location.search); + if (trackFindFunction) { + trackFindFunction(uiFind); + } + if (uiFind) { + queryParams.uiFind = uiFind; + } + history.replace({ + ...location, + search: `?${queryString.stringify(queryParams)}`, + }); +} diff --git a/packages/jaeger-ui/src/utils/update-ui-find.test.js b/packages/jaeger-ui/src/utils/update-ui-find.test.js new file mode 100644 index 0000000000..05e016ca5d --- /dev/null +++ b/packages/jaeger-ui/src/utils/update-ui-find.test.js @@ -0,0 +1,114 @@ +// Copyright (c) 2019 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 queryString from 'query-string'; + +import updateUiFind from './update-ui-find'; + +describe('updateUiFind', () => { + const existingUiFind = 'existingUiFind'; + const newUiFind = 'newUiFind'; + const unrelatedQueryParamName = 'unrelatedQueryParamName'; + const unrelatedQueryParamValue = 'unrelatedQueryParamValue'; + + const replaceMock = jest.fn(); + const queryStringParseSpy = jest.spyOn(queryString, 'parse').mockReturnValue({ + uiFind: existingUiFind, + [unrelatedQueryParamName]: unrelatedQueryParamValue, + }); + const queryStringStringifySpyMockReturnValue = 'queryStringStringifySpyMockReturnValue'; + const queryStringStringifySpy = jest + .spyOn(queryString, 'stringify') + .mockReturnValue(queryStringStringifySpyMockReturnValue); + + const history = { + replace: replaceMock, + }; + const location = { + pathname: '/trace/traceID', + search: 'location.search', + }; + const expectedReplaceMockArgument = { + ...location, + search: `?${queryStringStringifySpyMockReturnValue}`, + }; + const trackFindFunction = jest.fn(); + + beforeEach(() => { + replaceMock.mockReset(); + trackFindFunction.mockClear(); + queryStringParseSpy.mockClear(); + queryStringStringifySpy.mockClear(); + }); + + it('adds truthy graphSearch to existing params', () => { + updateUiFind({ + history, + location, + uiFind: newUiFind, + }); + expect(queryStringParseSpy).toHaveBeenCalledWith(location.search); + expect(queryStringStringifySpy).toHaveBeenCalledWith({ + uiFind: newUiFind, + [unrelatedQueryParamName]: unrelatedQueryParamValue, + }); + expect(replaceMock).toHaveBeenCalledWith(expectedReplaceMockArgument); + }); + + it('omits falsy graphSearch from query params', () => { + updateUiFind({ + history, + location, + uiFind: '', + }); + expect(queryStringParseSpy).toHaveBeenCalledWith(location.search); + expect(queryStringStringifySpy).toHaveBeenCalledWith({ + [unrelatedQueryParamName]: unrelatedQueryParamValue, + }); + expect(replaceMock).toHaveBeenCalledWith(expectedReplaceMockArgument); + }); + + it('omits absent graphSearch from query params', () => { + updateUiFind({ + history, + location, + }); + expect(queryStringParseSpy).toHaveBeenCalledWith(location.search); + expect(queryStringStringifySpy).toHaveBeenCalledWith({ + [unrelatedQueryParamName]: unrelatedQueryParamValue, + }); + expect(replaceMock).toHaveBeenCalledWith(expectedReplaceMockArgument); + }); + + describe('trackFindFunction provided', () => { + it('tracks undefined when uiFind value is omitted', () => { + updateUiFind({ + history, + location, + trackFindFunction, + }); + expect(trackFindFunction).toHaveBeenCalledWith(undefined); + }); + + it('tracks given value', () => { + updateUiFind({ + history, + location, + trackFindFunction, + uiFind: newUiFind, + }); + expect(trackFindFunction).toHaveBeenCalledWith(newUiFind); + }); + }); +}); diff --git a/packages/plexus/src/LayoutManager/Coordinator.tsx b/packages/plexus/src/LayoutManager/Coordinator.tsx index 73175f5f0f..3379b1616b 100644 --- a/packages/plexus/src/LayoutManager/Coordinator.tsx +++ b/packages/plexus/src/LayoutManager/Coordinator.tsx @@ -61,7 +61,7 @@ function killWorker(worker: LayoutWorker) { // to make flow happy const noop = () => {}; w.onmessage = noop; - // $FlowFixMe - https://github.com/facebook/flow/issues/6191 + // $FlowIgnore - https://github.com/facebook/flow/issues/6191 w.onmessageerror = noop; w.onerror = noop; w.terminate(); diff --git a/yarn.lock b/yarn.lock index e23971500b..2f87c6e2ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -47,16 +47,16 @@ source-map "^0.5.0" "@babel/core@^7.0.0-beta.49", "@babel/core@^7.0.1", "@babel/core@^7.1.6": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.3.tgz#d090d157b7c5060d05a05acaebc048bd2b037947" + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.4.tgz#921a5a13746c21e32445bf0798680e9d11a6530b" dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.3.3" + "@babel/generator" "^7.3.4" "@babel/helpers" "^7.2.0" - "@babel/parser" "^7.3.3" + "@babel/parser" "^7.3.4" "@babel/template" "^7.2.2" - "@babel/traverse" "^7.2.2" - "@babel/types" "^7.3.3" + "@babel/traverse" "^7.3.4" + "@babel/types" "^7.3.4" convert-source-map "^1.1.0" debug "^4.1.0" json5 "^2.1.0" @@ -65,11 +65,11 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.1.6", "@babel/generator@^7.2.2", "@babel/generator@^7.3.3": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.3.tgz#185962ade59a52e00ca2bdfcfd1d58e528d4e39e" +"@babel/generator@^7.1.6", "@babel/generator@^7.2.2", "@babel/generator@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.4.tgz#9aa48c1989257877a9d971296e5b73bfe72e446e" dependencies: - "@babel/types" "^7.3.3" + "@babel/types" "^7.3.4" jsesc "^2.5.1" lodash "^4.17.11" source-map "^0.5.0" @@ -103,15 +103,16 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-create-class-features-plugin@^7.3.0": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.2.tgz#ba1685603eb1c9f2f51c9106d5180135c163fe73" +"@babel/helper-create-class-features-plugin@^7.3.0", "@babel/helper-create-class-features-plugin@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.4.tgz#092711a7a3ad8ea34de3e541644c2ce6af1f6f0c" dependencies: "@babel/helper-function-name" "^7.1.0" "@babel/helper-member-expression-to-functions" "^7.0.0" "@babel/helper-optimise-call-expression" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.2.3" + "@babel/helper-replace-supers" "^7.3.4" + "@babel/helper-split-export-declaration" "^7.0.0" "@babel/helper-define-map@^7.1.0": version "7.1.0" @@ -197,14 +198,14 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.2.3.tgz#19970020cf22677d62b3a689561dbd9644d8c5e5" +"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.3.4.tgz#a795208e9b911a6eeb08e5891faacf06e7013e13" dependencies: "@babel/helper-member-expression-to-functions" "^7.0.0" "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/traverse" "^7.2.3" - "@babel/types" "^7.0.0" + "@babel/traverse" "^7.3.4" + "@babel/types" "^7.3.4" "@babel/helper-simple-access@^7.1.0": version "7.1.0" @@ -244,9 +245,9 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.6", "@babel/parser@^7.2.2", "@babel/parser@^7.2.3", "@babel/parser@^7.3.3": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.3.tgz#092d450db02bdb6ccb1ca8ffd47d8774a91aef87" +"@babel/parser@^7.0.0", "@babel/parser@^7.1.6", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c" "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" @@ -264,10 +265,10 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-class-properties@^7.1.0": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.3.tgz#e69ee114a834a671293ace001708cc1682ed63f9" + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.4.tgz#410f5173b3dc45939f9ab30ca26684d72901405e" dependencies: - "@babel/helper-create-class-features-plugin" "^7.3.0" + "@babel/helper-create-class-features-plugin" "^7.3.4" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-decorators@7.3.0": @@ -285,13 +286,20 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-json-strings" "^7.2.0" -"@babel/plugin-proposal-object-rest-spread@7.3.2", "@babel/plugin-proposal-object-rest-spread@^7.3.1": +"@babel/plugin-proposal-object-rest-spread@7.3.2": version "7.3.2" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.2.tgz#6d1859882d4d778578e41f82cc5d7bf3d5daf6c1" dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" +"@babel/plugin-proposal-object-rest-spread@^7.3.1", "@babel/plugin-proposal-object-rest-spread@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.4.tgz#47f73cf7f2a721aad5c0261205405c642e424654" + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-object-rest-spread" "^7.2.0" + "@babel/plugin-proposal-optional-catch-binding@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz#135d81edb68a081e55e56ec48541ece8065c38f5" @@ -367,9 +375,9 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-async-to-generator@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.2.0.tgz#68b8a438663e88519e65b776f8938f3445b1a2ff" +"@babel/plugin-transform-async-to-generator@^7.2.0", "@babel/plugin-transform-async-to-generator@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.3.4.tgz#4e45408d3c3da231c0e7b823f407a53a7eb3048c" dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -381,12 +389,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-block-scoping@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.2.0.tgz#f17c49d91eedbcdf5dd50597d16f5f2f770132d4" +"@babel/plugin-transform-block-scoping@^7.2.0", "@babel/plugin-transform-block-scoping@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.3.4.tgz#5c22c339de234076eee96c8783b2fed61202c5c4" dependencies: "@babel/helper-plugin-utils" "^7.0.0" - lodash "^4.17.10" + lodash "^4.17.11" "@babel/plugin-transform-classes@7.2.2": version "7.2.2" @@ -401,16 +409,16 @@ "@babel/helper-split-export-declaration" "^7.0.0" globals "^11.1.0" -"@babel/plugin-transform-classes@^7.2.0": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.3.3.tgz#a0532d6889c534d095e8f604e9257f91386c4b51" +"@babel/plugin-transform-classes@^7.2.0", "@babel/plugin-transform-classes@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.3.4.tgz#dc173cb999c6c5297e0b5f2277fdaaec3739d0cc" dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" "@babel/helper-define-map" "^7.1.0" "@babel/helper-function-name" "^7.1.0" "@babel/helper-optimise-call-expression" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.1.0" + "@babel/helper-replace-supers" "^7.3.4" "@babel/helper-split-export-declaration" "^7.0.0" globals "^11.1.0" @@ -488,9 +496,9 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-simple-access" "^7.1.0" -"@babel/plugin-transform-modules-systemjs@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.2.0.tgz#912bfe9e5ff982924c81d0937c92d24994bb9068" +"@babel/plugin-transform-modules-systemjs@^7.2.0", "@babel/plugin-transform-modules-systemjs@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.3.4.tgz#813b34cd9acb6ba70a84939f3680be0eb2e58861" dependencies: "@babel/helper-hoist-variables" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -564,11 +572,11 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-jsx" "^7.2.0" -"@babel/plugin-transform-regenerator@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0.tgz#5b41686b4ed40bef874d7ed6a84bdd849c13e0c1" +"@babel/plugin-transform-regenerator@^7.0.0", "@babel/plugin-transform-regenerator@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.3.4.tgz#1601655c362f5b38eead6a52631f5106b29fa46a" dependencies: - regenerator-transform "^0.13.3" + regenerator-transform "^0.13.4" "@babel/plugin-transform-runtime@7.2.0": version "7.2.0" @@ -626,7 +634,7 @@ "@babel/helper-regex" "^7.0.0" regexpu-core "^4.1.3" -"@babel/preset-env@7.3.1", "@babel/preset-env@^7.0.0", "@babel/preset-env@^7.1.6": +"@babel/preset-env@7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.1.tgz#389e8ca6b17ae67aaf9a2111665030be923515db" dependencies: @@ -674,6 +682,54 @@ js-levenshtein "^1.1.3" semver "^5.3.0" +"@babel/preset-env@^7.0.0", "@babel/preset-env@^7.1.6": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.4.tgz#887cf38b6d23c82f19b5135298bdb160062e33e1" + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-async-generator-functions" "^7.2.0" + "@babel/plugin-proposal-json-strings" "^7.2.0" + "@babel/plugin-proposal-object-rest-spread" "^7.3.4" + "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.2.0" + "@babel/plugin-syntax-async-generators" "^7.2.0" + "@babel/plugin-syntax-json-strings" "^7.2.0" + "@babel/plugin-syntax-object-rest-spread" "^7.2.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" + "@babel/plugin-transform-arrow-functions" "^7.2.0" + "@babel/plugin-transform-async-to-generator" "^7.3.4" + "@babel/plugin-transform-block-scoped-functions" "^7.2.0" + "@babel/plugin-transform-block-scoping" "^7.3.4" + "@babel/plugin-transform-classes" "^7.3.4" + "@babel/plugin-transform-computed-properties" "^7.2.0" + "@babel/plugin-transform-destructuring" "^7.2.0" + "@babel/plugin-transform-dotall-regex" "^7.2.0" + "@babel/plugin-transform-duplicate-keys" "^7.2.0" + "@babel/plugin-transform-exponentiation-operator" "^7.2.0" + "@babel/plugin-transform-for-of" "^7.2.0" + "@babel/plugin-transform-function-name" "^7.2.0" + "@babel/plugin-transform-literals" "^7.2.0" + "@babel/plugin-transform-modules-amd" "^7.2.0" + "@babel/plugin-transform-modules-commonjs" "^7.2.0" + "@babel/plugin-transform-modules-systemjs" "^7.3.4" + "@babel/plugin-transform-modules-umd" "^7.2.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.3.0" + "@babel/plugin-transform-new-target" "^7.0.0" + "@babel/plugin-transform-object-super" "^7.2.0" + "@babel/plugin-transform-parameters" "^7.2.0" + "@babel/plugin-transform-regenerator" "^7.3.4" + "@babel/plugin-transform-shorthand-properties" "^7.2.0" + "@babel/plugin-transform-spread" "^7.2.0" + "@babel/plugin-transform-sticky-regex" "^7.2.0" + "@babel/plugin-transform-template-literals" "^7.2.0" + "@babel/plugin-transform-typeof-symbol" "^7.2.0" + "@babel/plugin-transform-unicode-regex" "^7.2.0" + browserslist "^4.3.4" + invariant "^2.2.2" + js-levenshtein "^1.1.3" + semver "^5.3.0" + "@babel/preset-react@7.0.0", "@babel/preset-react@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.0.0.tgz#e86b4b3d99433c7b3e9e91747e2653958bc6b3c0" @@ -691,12 +747,18 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-typescript" "^7.1.0" -"@babel/runtime@7.3.1", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2": +"@babel/runtime@7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.1.tgz#574b03e8e8a9898eaf4a872a92ea20b7846f6f2a" dependencies: regenerator-runtime "^0.12.0" +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" + dependencies: + regenerator-runtime "^0.12.0" + "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" @@ -705,23 +767,23 @@ "@babel/parser" "^7.2.2" "@babel/types" "^7.2.2" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.1.6", "@babel/traverse@^7.2.2", "@babel/traverse@^7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.2.3.tgz#7ff50cefa9c7c0bd2d81231fdac122f3957748d8" +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.1.6", "@babel/traverse@^7.2.2", "@babel/traverse@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06" dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.2.2" + "@babel/generator" "^7.3.4" "@babel/helper-function-name" "^7.1.0" "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/parser" "^7.2.3" - "@babel/types" "^7.2.2" + "@babel/parser" "^7.3.4" + "@babel/types" "^7.3.4" debug "^4.1.0" globals "^11.1.0" - lodash "^4.17.10" + lodash "^4.17.11" -"@babel/types@^7.0.0", "@babel/types@^7.1.6", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.3": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.3.tgz#6c44d1cdac2a7625b624216657d5bc6c107ab436" +"@babel/types@^7.0.0", "@babel/types@^7.1.6", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.4.tgz#bf482eaeaffb367a28abbf9357a94963235d90ed" dependencies: esutils "^2.0.2" lodash "^4.17.11" @@ -1425,24 +1487,26 @@ url-template "^2.0.8" "@octokit/plugin-enterprise-rest@^2.1.0": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-2.1.1.tgz#ee7b245aada06d3ffdd409205ad1b891107fee0b" + version "2.2.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-2.2.0.tgz#7ee72a187e8a034d6fc21b8174bef40e34c22f02" -"@octokit/request@2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-2.3.0.tgz#da2672308bcf0b9376ef66f51bddbe5eb87cc00a" +"@octokit/request@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-2.4.1.tgz#98c4d6870e4abe3ccdd2b9799034b4ae3f441c30" dependencies: "@octokit/endpoint" "^3.1.1" + deprecation "^1.0.1" is-plain-object "^2.0.4" node-fetch "^2.3.0" + once "^1.4.0" universal-user-agent "^2.0.1" "@octokit/rest@^16.15.0": - version "16.16.0" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.16.0.tgz#b686407d34c756c3463f8a7b1e42aa035a504306" + version "16.17.0" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.17.0.tgz#3a8c0ff5290e25a48b11f6957aa90791c672c91e" dependencies: - "@octokit/request" "2.3.0" - before-after-hook "^1.2.0" + "@octokit/request" "2.4.1" + before-after-hook "^1.4.0" btoa-lite "^1.0.0" lodash.get "^4.4.2" lodash.set "^4.3.2" @@ -1451,21 +1515,22 @@ universal-user-agent "^2.0.0" url-template "^2.0.8" -"@sinonjs/commons@^1.0.2": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.3.0.tgz#50a2754016b6f30a994ceda6d9a0a8c36adda849" +"@sinonjs/commons@^1", "@sinonjs/commons@^1.0.2": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.4.0.tgz#7b3ec2d96af481d7a0321252e7b1c94724ec5a78" dependencies: type-detect "4.0.8" "@sinonjs/formatio@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.1.0.tgz#6ac9d1eb1821984d84c4996726e45d1646d8cce5" + version "3.2.1" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.1.tgz#52310f2f9bcbc67bdac18c94ad4901b95fde267e" dependencies: - "@sinonjs/samsam" "^2 || ^3" + "@sinonjs/commons" "^1" + "@sinonjs/samsam" "^3.1.0" -"@sinonjs/samsam@^2 || ^3": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.1.1.tgz#8e2eceb2353f6626e2867352e3def951d3366240" +"@sinonjs/samsam@^3.1.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.0.tgz#9557ea89cd39dbc94ffbd093c8085281cac87416" dependencies: "@sinonjs/commons" "^1.0.2" array-from "^2.1.1" @@ -1520,12 +1585,12 @@ "@types/d3-selection" "*" "@types/node@*": - version "11.9.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-11.9.5.tgz#011eece9d3f839a806b63973e228f85967b79ed3" + version "11.11.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.0.tgz#070e9ce7c90e727aca0e0c14e470f9a93ffe9390" "@types/prop-types@*": - version "15.5.9" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.9.tgz#f2d14df87b0739041bc53a7d75e3d77d726a3ec0" + version "15.7.0" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.0.tgz#4c48fed958d6dcf9487195a0ef6456d5f6e0163a" "@types/q@^1.5.1": version "1.5.1" @@ -1538,8 +1603,8 @@ "@types/react" "*" "@types/react@*", "@types/react@^16.7.20": - version "16.8.4" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.4.tgz#134307f5266e866d5e7c25e47f31f9abd5b2ea34" + version "16.8.7" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.7.tgz#7b1c0223dd5494f9b4501ad2a69aa6acb350a29b" dependencies: "@types/prop-types" "*" csstype "^2.2.0" @@ -1581,37 +1646,37 @@ "@webassemblyjs/wast-parser" "1.7.6" mamacro "^0.0.3" -"@webassemblyjs/ast@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.3.tgz#63a741bd715a6b6783f2ea5c6ab707516aa215eb" +"@webassemblyjs/ast@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" dependencies: - "@webassemblyjs/helper-module-context" "1.8.3" - "@webassemblyjs/helper-wasm-bytecode" "1.8.3" - "@webassemblyjs/wast-parser" "1.8.3" + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/wast-parser" "1.8.5" "@webassemblyjs/floating-point-hex-parser@1.7.6": version "1.7.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.6.tgz#7cb37d51a05c3fe09b464ae7e711d1ab3837801f" -"@webassemblyjs/floating-point-hex-parser@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.3.tgz#f198a2d203b3c50846a064f5addd6a133ef9bc0e" +"@webassemblyjs/floating-point-hex-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz#1ba926a2923613edce496fd5b02e8ce8a5f49721" "@webassemblyjs/helper-api-error@1.7.6": version "1.7.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.6.tgz#99b7e30e66f550a2638299a109dda84a622070ef" -"@webassemblyjs/helper-api-error@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.3.tgz#3b708f6926accd64dcbaa7ba5b63db5660ff4f66" +"@webassemblyjs/helper-api-error@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz#c49dad22f645227c5edb610bdb9697f1aab721f7" "@webassemblyjs/helper-buffer@1.7.6": version "1.7.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.6.tgz#ba0648be12bbe560c25c997e175c2018df39ca3e" -"@webassemblyjs/helper-buffer@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.3.tgz#f3150a23ffaba68621e1f094c8a14bebfd53dd48" +"@webassemblyjs/helper-buffer@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz#fea93e429863dd5e4338555f42292385a653f204" "@webassemblyjs/helper-code-frame@1.7.6": version "1.7.6" @@ -1619,19 +1684,19 @@ dependencies: "@webassemblyjs/wast-printer" "1.7.6" -"@webassemblyjs/helper-code-frame@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.3.tgz#f43ac605789b519d95784ef350fd2968aebdd3ef" +"@webassemblyjs/helper-code-frame@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz#9a740ff48e3faa3022b1dff54423df9aa293c25e" dependencies: - "@webassemblyjs/wast-printer" "1.8.3" + "@webassemblyjs/wast-printer" "1.8.5" "@webassemblyjs/helper-fsm@1.7.6": version "1.7.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.6.tgz#ae1741c6f6121213c7a0b587fb964fac492d3e49" -"@webassemblyjs/helper-fsm@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.3.tgz#46aaa03f41082a916850ebcb97e9fc198ef36a9c" +"@webassemblyjs/helper-fsm@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz#ba0b7d3b3f7e4733da6059c9332275d860702452" "@webassemblyjs/helper-module-context@1.7.6": version "1.7.6" @@ -1639,20 +1704,20 @@ dependencies: mamacro "^0.0.3" -"@webassemblyjs/helper-module-context@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.3.tgz#150da405d90c8ea81ae0b0e1965b7b64e585634f" +"@webassemblyjs/helper-module-context@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz#def4b9927b0101dc8cbbd8d1edb5b7b9c82eb245" dependencies: - "@webassemblyjs/ast" "1.8.3" + "@webassemblyjs/ast" "1.8.5" mamacro "^0.0.3" "@webassemblyjs/helper-wasm-bytecode@1.7.6": version "1.7.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.6.tgz#98e515eaee611aa6834eb5f6a7f8f5b29fefb6f1" -"@webassemblyjs/helper-wasm-bytecode@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.3.tgz#12f55bbafbbc7ddf9d8059a072cb7b0c17987901" +"@webassemblyjs/helper-wasm-bytecode@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz#537a750eddf5c1e932f3744206551c91c1b93e61" "@webassemblyjs/helper-wasm-section@1.7.6": version "1.7.6" @@ -1663,14 +1728,14 @@ "@webassemblyjs/helper-wasm-bytecode" "1.7.6" "@webassemblyjs/wasm-gen" "1.7.6" -"@webassemblyjs/helper-wasm-section@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.3.tgz#9e79456d9719e116f4f8998ee62ab54ba69a6cf3" +"@webassemblyjs/helper-wasm-section@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz#74ca6a6bcbe19e50a3b6b462847e69503e6bfcbf" dependencies: - "@webassemblyjs/ast" "1.8.3" - "@webassemblyjs/helper-buffer" "1.8.3" - "@webassemblyjs/helper-wasm-bytecode" "1.8.3" - "@webassemblyjs/wasm-gen" "1.8.3" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" "@webassemblyjs/ieee754@1.7.6": version "1.7.6" @@ -1678,9 +1743,9 @@ dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/ieee754@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.8.3.tgz#0a89355b1f6c9d08d0605c2acbc2a6fe3141f5b4" +"@webassemblyjs/ieee754@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz#712329dbef240f36bf57bd2f7b8fb9bf4154421e" dependencies: "@xtuc/ieee754" "^1.2.0" @@ -1690,9 +1755,9 @@ dependencies: "@xtuc/long" "4.2.1" -"@webassemblyjs/leb128@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.8.3.tgz#b7fd9d7c039e34e375c4473bd4dc89ce8228b920" +"@webassemblyjs/leb128@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.8.5.tgz#044edeb34ea679f3e04cd4fd9824d5e35767ae10" dependencies: "@xtuc/long" "4.2.2" @@ -1700,9 +1765,9 @@ version "1.7.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.7.6.tgz#eb62c66f906af2be70de0302e29055d25188797d" -"@webassemblyjs/utf8@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.8.3.tgz#75712db52cfdda868731569ddfe11046f1f1e7a2" +"@webassemblyjs/utf8@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.8.5.tgz#a8bf3b5d8ffe986c7c1e373ccbdc2a0915f0cedc" "@webassemblyjs/wasm-edit@1.7.6": version "1.7.6" @@ -1717,18 +1782,18 @@ "@webassemblyjs/wasm-parser" "1.7.6" "@webassemblyjs/wast-printer" "1.7.6" -"@webassemblyjs/wasm-edit@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.3.tgz#23c3c6206b096f9f6aa49623a5310a102ef0fb87" +"@webassemblyjs/wasm-edit@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz#962da12aa5acc1c131c81c4232991c82ce56e01a" dependencies: - "@webassemblyjs/ast" "1.8.3" - "@webassemblyjs/helper-buffer" "1.8.3" - "@webassemblyjs/helper-wasm-bytecode" "1.8.3" - "@webassemblyjs/helper-wasm-section" "1.8.3" - "@webassemblyjs/wasm-gen" "1.8.3" - "@webassemblyjs/wasm-opt" "1.8.3" - "@webassemblyjs/wasm-parser" "1.8.3" - "@webassemblyjs/wast-printer" "1.8.3" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/helper-wasm-section" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + "@webassemblyjs/wasm-opt" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + "@webassemblyjs/wast-printer" "1.8.5" "@webassemblyjs/wasm-gen@1.7.6": version "1.7.6" @@ -1740,15 +1805,15 @@ "@webassemblyjs/leb128" "1.7.6" "@webassemblyjs/utf8" "1.7.6" -"@webassemblyjs/wasm-gen@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.3.tgz#1a433b8ab97e074e6ac2e25fcbc8cb6125400813" +"@webassemblyjs/wasm-gen@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz#54840766c2c1002eb64ed1abe720aded714f98bc" dependencies: - "@webassemblyjs/ast" "1.8.3" - "@webassemblyjs/helper-wasm-bytecode" "1.8.3" - "@webassemblyjs/ieee754" "1.8.3" - "@webassemblyjs/leb128" "1.8.3" - "@webassemblyjs/utf8" "1.8.3" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/ieee754" "1.8.5" + "@webassemblyjs/leb128" "1.8.5" + "@webassemblyjs/utf8" "1.8.5" "@webassemblyjs/wasm-opt@1.7.6": version "1.7.6" @@ -1759,14 +1824,14 @@ "@webassemblyjs/wasm-gen" "1.7.6" "@webassemblyjs/wasm-parser" "1.7.6" -"@webassemblyjs/wasm-opt@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.3.tgz#54754bcf88f88e92b909416a91125301cc81419c" +"@webassemblyjs/wasm-opt@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz#b24d9f6ba50394af1349f510afa8ffcb8a63d264" dependencies: - "@webassemblyjs/ast" "1.8.3" - "@webassemblyjs/helper-buffer" "1.8.3" - "@webassemblyjs/wasm-gen" "1.8.3" - "@webassemblyjs/wasm-parser" "1.8.3" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" "@webassemblyjs/wasm-parser@1.7.6": version "1.7.6" @@ -1779,16 +1844,16 @@ "@webassemblyjs/leb128" "1.7.6" "@webassemblyjs/utf8" "1.7.6" -"@webassemblyjs/wasm-parser@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.3.tgz#d12ed19d1b8e8667a7bee040d2245aaaf215340b" +"@webassemblyjs/wasm-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz#21576f0ec88b91427357b8536383668ef7c66b8d" dependencies: - "@webassemblyjs/ast" "1.8.3" - "@webassemblyjs/helper-api-error" "1.8.3" - "@webassemblyjs/helper-wasm-bytecode" "1.8.3" - "@webassemblyjs/ieee754" "1.8.3" - "@webassemblyjs/leb128" "1.8.3" - "@webassemblyjs/utf8" "1.8.3" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-api-error" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/ieee754" "1.8.5" + "@webassemblyjs/leb128" "1.8.5" + "@webassemblyjs/utf8" "1.8.5" "@webassemblyjs/wast-parser@1.7.6": version "1.7.6" @@ -1802,15 +1867,15 @@ "@xtuc/long" "4.2.1" mamacro "^0.0.3" -"@webassemblyjs/wast-parser@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.8.3.tgz#44aa123e145503e995045dc3e5e2770069da117b" +"@webassemblyjs/wast-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz#e10eecd542d0e7bd394f6827c49f3df6d4eefb8c" dependencies: - "@webassemblyjs/ast" "1.8.3" - "@webassemblyjs/floating-point-hex-parser" "1.8.3" - "@webassemblyjs/helper-api-error" "1.8.3" - "@webassemblyjs/helper-code-frame" "1.8.3" - "@webassemblyjs/helper-fsm" "1.8.3" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/floating-point-hex-parser" "1.8.5" + "@webassemblyjs/helper-api-error" "1.8.5" + "@webassemblyjs/helper-code-frame" "1.8.5" + "@webassemblyjs/helper-fsm" "1.8.5" "@xtuc/long" "4.2.2" "@webassemblyjs/wast-printer@1.7.6": @@ -1821,12 +1886,12 @@ "@webassemblyjs/wast-parser" "1.7.6" "@xtuc/long" "4.2.1" -"@webassemblyjs/wast-printer@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.8.3.tgz#b1177780b266b1305f2eeba87c4d6aa732352060" +"@webassemblyjs/wast-printer@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz#114bbc481fd10ca0e23b3560fa812748b0bae5bc" dependencies: - "@webassemblyjs/ast" "1.8.3" - "@webassemblyjs/wast-parser" "1.8.3" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/wast-parser" "1.8.5" "@xtuc/long" "4.2.2" "@xtuc/ieee754@^1.2.0": @@ -1893,8 +1958,8 @@ acorn@^5.0.0, acorn@^5.5.3, acorn@^5.6.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" acorn@^6.0.1, acorn@^6.0.2, acorn@^6.0.4, acorn@^6.0.5, acorn@^6.0.7: - version "6.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.0.tgz#b0a3be31752c97a0f7013c5f4903b71a05db6818" + version "6.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" add-dom-event-listener@^1.1.0: version "1.1.0" @@ -1906,7 +1971,7 @@ address@1.0.3, address@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" -agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0: +agent-base@4, agent-base@^4.1.0, agent-base@~4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" dependencies: @@ -1927,8 +1992,8 @@ ajv-keywords@^3.0.0, ajv-keywords@^3.1.0: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.0.tgz#4b831e7b531415a7cc518cd404e73f6193c6349d" ajv@^6.0.1, ajv@^6.1.0, ajv@^6.5.3, ajv@^6.5.5, ajv@^6.9.1: - version "6.9.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.2.tgz#4927adb83e7f48e5a32b45729744c71ec39c9c7b" + version "6.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" dependencies: fast-deep-equal "^2.0.1" fast-json-stable-stringify "^2.0.0" @@ -1940,8 +2005,8 @@ alphanum-sort@^1.0.0: resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" ansi-colors@^3.0.0: - version "3.2.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: version "3.2.0" @@ -1959,9 +2024,9 @@ ansi-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" -ansi-regex@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9" +ansi-regex@^4.0.0, ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" ansi-styles@^2.2.1: version "2.2.1" @@ -2251,11 +2316,11 @@ atob@^2.1.1: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" autoprefixer@^9.3.1: - version "9.4.8" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.4.8.tgz#575dcdfd984228c7bccbc08c5fe53f0ea6915593" + version "9.4.10" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.4.10.tgz#e1be61fc728bacac8f4252ed242711ec0dcc6a7b" dependencies: - browserslist "^4.4.1" - caniuse-lite "^1.0.30000938" + browserslist "^4.4.2" + caniuse-lite "^1.0.30000940" normalize-range "^0.1.2" num2fraction "^1.2.2" postcss "^7.0.14" @@ -2458,8 +2523,8 @@ babel-preset-jest@^23.2.0: babel-plugin-syntax-object-rest-spread "^6.13.0" babel-preset-react-app@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-7.0.1.tgz#8dd7fef73fba124a6e140d245185ca657a943313" + version "7.0.2" + resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-7.0.2.tgz#d01ae973edc93b9f1015cb0236dd55889a584308" dependencies: "@babel/core" "7.2.2" "@babel/plugin-proposal-class-properties" "7.3.0" @@ -2571,9 +2636,9 @@ beeper@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809" -before-after-hook@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.3.2.tgz#7bfbf844ad670aa7a96b5a4e4e15bd74b08ed66b" +before-after-hook@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.4.0.tgz#2b6bf23dca4f32e628fd2747c10a37c74a4b484d" bfj@6.1.1: version "6.1.1" @@ -2745,7 +2810,7 @@ browserslist@4.4.1: electron-to-chromium "^1.3.103" node-releases "^1.1.3" -browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.4.1: +browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.4.2.tgz#6ea8a74d6464bb0bd549105f659b41197d8f0ba2" dependencies: @@ -2938,8 +3003,8 @@ camelcase@^4.1.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" camelcase@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" + version "5.2.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.2.0.tgz#e7522abda5ed94cc0489e1b8466610e88404cf45" caniuse-api@^3.0.0: version "3.0.0" @@ -2950,9 +3015,9 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000905, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000938, caniuse-lite@^1.0.30000939: - version "1.0.30000939" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000939.tgz#b9ab7ac9e861bf78840b80c5dfbc471a5cd7e679" +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000905, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000939, caniuse-lite@^1.0.30000940: + version "1.0.30000943" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000943.tgz#00b25bd5808edc2ed1cfb53533a6a6ff6ca014ee" capture-exit@^1.2.0: version "1.2.0" @@ -3153,7 +3218,7 @@ co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" -coa@~2.0.1: +coa@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" dependencies: @@ -3215,10 +3280,6 @@ colors@^1.2.1: version "1.3.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" -colors@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" - colorspace@1.1.x: version "1.1.1" resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.1.tgz#9ac2491e1bc6f8fb690e2176814f8d091636d972" @@ -3322,9 +3383,9 @@ config-chain@^1.1.11: ini "^1.3.4" proto-list "~1.2.1" -confusing-browser-globals@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.5.tgz#0171050cfdd4261e278978078bc00c4d88e135f4" +confusing-browser-globals@^1.0.5, confusing-browser-globals@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.6.tgz#5918188e8244492cdd46d6be1cab60edef3063ce" connect-history-api-fallback@^1.3.0: version "1.6.0" @@ -3631,7 +3692,7 @@ css-loader@^1.0.1: postcss-value-parser "^3.3.0" source-list-map "^2.0.0" -css-select-base-adapter@~0.1.0: +css-select-base-adapter@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" @@ -3688,8 +3749,8 @@ css-what@2.1, css-what@^2.1.2: resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" cssdb@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.3.0.tgz#2e1229900616f80c66ff2d568ea2b4f92db1c78c" + version "4.4.0" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0" cssesc@^0.1.0: version "0.1.0" @@ -3765,7 +3826,7 @@ cssnano@^4.1.0: is-resolvable "^1.0.0" postcss "^7.0.0" -csso@^3.5.0: +csso@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b" dependencies: @@ -3782,8 +3843,8 @@ cssstyle@^1.0.0, cssstyle@^1.1.1: cssom "0.3.x" csstype@^2.2.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.2.tgz#3043d5e065454579afc7478a18de41909c8a2f01" + version "2.6.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.3.tgz#b701e5968245bf9b08d54ac83d00b624e622a9fa" currently-unhandled@^0.4.1: version "0.4.1" @@ -3808,8 +3869,8 @@ cytoscape-dagre@^2.0.0: dagre "^0.8.2" cytoscape@^3.2.1: - version "3.4.2" - resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.4.2.tgz#f35f0d4a8ccb5901e801046230edccf1c3a39806" + version "3.5.0" + resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.5.0.tgz#b6cc5a9c66b7f29f8d7f6dfbb0c0e31a8af748a1" dependencies: heap "^0.2.6" lodash.debounce "^4.0.8" @@ -4092,8 +4153,8 @@ default-gateway@^2.6.0: ip-regex "^2.1.0" default-gateway@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.0.1.tgz#3a7d071ca610a2831190341bd0666382b9dbc340" + version "4.2.0" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" dependencies: execa "^1.0.0" ip-regex "^2.1.0" @@ -4158,6 +4219,10 @@ depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" +deprecation@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-1.0.1.tgz#2df79b79005752180816b7b6e079cbd80490d711" + des.js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" @@ -4281,8 +4346,8 @@ doctrine@^3.0.0: esutils "^2.0.2" dom-align@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.8.0.tgz#c0e89b5b674c6e836cd248c52c2992135f093654" + version "1.8.2" + resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.8.2.tgz#fdcd36bce25ba8d34fe3582efd57ac767df490bd" dom-closest@^0.2.0: version "0.2.0" @@ -4420,8 +4485,8 @@ ejs@^2.6.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" electron-to-chromium@^1.3.103, electron-to-chromium@^1.3.113: - version "1.3.113" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz#b1ccf619df7295aea17bc6951dc689632629e4a9" + version "1.3.114" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.114.tgz#1862887589db93f832057c81878c56c404960aa6" element-resize-event@^2.0.4: version "2.0.9" @@ -4493,12 +4558,11 @@ env-variable@0.0.x: version "0.0.5" resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.5.tgz#913dd830bef11e96a039c038d4130604eba37f88" -enzyme-adapter-react-16@^1.1.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.9.1.tgz#6d49a3a31c3a0fccf527610f31b837e0f307128a" +enzyme-adapter-react-16@^1.2.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.10.0.tgz#12e5b6f4be84f9a2ef374acc2555f829f351fc6e" dependencies: enzyme-adapter-utils "^1.10.0" - function.prototype.name "^1.1.0" object.assign "^4.1.0" object.values "^1.1.0" prop-types "^15.6.2" @@ -4506,13 +4570,13 @@ enzyme-adapter-react-16@^1.1.0: react-test-renderer "^16.0.0-0" enzyme-adapter-utils@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.10.0.tgz#5836169f68b9e8733cb5b69cad5da2a49e34f550" + version "1.10.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.10.1.tgz#58264efa19a7befdbf964fb7981a108a5452ac96" dependencies: function.prototype.name "^1.1.0" object.assign "^4.1.0" object.fromentries "^2.0.0" - prop-types "^15.6.2" + prop-types "^15.7.2" semver "^5.6.0" enzyme-to-json@^3.3.0: @@ -4545,7 +4609,7 @@ enzyme@3.8.0: rst-selector-parser "^2.2.3" string.prototype.trim "^1.1.2" -enzyme@^3.2.0: +enzyme@^3.8.0: version "3.9.0" resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.9.0.tgz#2b491f06ca966eb56b6510068c7894a7e0be3909" dependencies: @@ -4668,12 +4732,18 @@ eslint-config-prettier@4.0.0: dependencies: get-stdin "^6.0.0" -eslint-config-react-app@3.0.7, eslint-config-react-app@^3.0.6: +eslint-config-react-app@3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-3.0.7.tgz#d58c9216ff285e2b4de0eb8403c28b0600e45b3e" dependencies: confusing-browser-globals "^1.0.5" +eslint-config-react-app@^3.0.6: + version "3.0.8" + resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-3.0.8.tgz#6f606828ba30bafee7d744c41cd07a3fea8f3035" + dependencies: + confusing-browser-globals "^1.0.6" + eslint-import-resolver-node@^0.3.1, eslint-import-resolver-node@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" @@ -4804,8 +4874,8 @@ eslint-scope@3.7.1: estraverse "^4.1.1" eslint-scope@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" + version "4.0.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.2.tgz#5f10cd6cabb1965bf479fa65745673439e21cb0e" dependencies: esrecurse "^4.1.0" estraverse "^4.1.1" @@ -4818,7 +4888,7 @@ eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" -eslint@5.14.1, eslint@^5.14.1: +eslint@5.14.1: version "5.14.1" resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.14.1.tgz#490a28906be313685c55ccd43a39e8d22efc04ba" dependencies: @@ -6424,7 +6494,7 @@ internal-ip@^3.0.1: default-gateway "^2.6.0" ipaddr.js "^1.5.2" -internal-ip@^4.0.0: +internal-ip@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.2.0.tgz#46e81b638d84c338e5c67e42b1a17db67d0814fa" dependencies: @@ -7197,8 +7267,8 @@ js-yaml@0.3.x: resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-0.3.7.tgz#d739d8ee86461e54b354d6a7d7d1f2ad9a167f62" js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0: - version "3.12.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600" + version "3.12.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.2.tgz#ef1d067c5a9d9cb65bd72f285b5d8105c77f14fc" dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -7207,7 +7277,7 @@ jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" -jsdom@13.2.0, jsdom@>=11.0.0: +jsdom@13.2.0: version "13.2.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-13.2.0.tgz#b1a0dbdadc255435262be8ea3723d2dba0d7eb3a" dependencies: @@ -7238,6 +7308,37 @@ jsdom@13.2.0, jsdom@>=11.0.0: ws "^6.1.2" xml-name-validator "^3.0.0" +jsdom@>=11.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-14.0.0.tgz#c7f1441ebcc57902d08d5fb2f6ba2baf746da7c6" + dependencies: + abab "^2.0.0" + acorn "^6.0.4" + acorn-globals "^4.3.0" + array-equal "^1.0.0" + cssom "^0.3.4" + cssstyle "^1.1.1" + data-urls "^1.1.0" + domexception "^1.0.1" + escodegen "^1.11.0" + html-encoding-sniffer "^1.0.2" + nwsapi "^2.0.9" + parse5 "5.1.0" + pn "^1.1.0" + request "^2.88.0" + request-promise-native "^1.0.5" + saxes "^3.1.5" + symbol-tree "^3.2.2" + tough-cookie "^2.5.0" + w3c-hr-time "^1.0.1" + w3c-xmlserializer "^1.0.1" + webidl-conversions "^4.0.2" + whatwg-encoding "^1.0.5" + whatwg-mimetype "^2.3.0" + whatwg-url "^7.0.0" + ws "^6.1.2" + xml-name-validator "^3.0.0" + jsdom@^11.5.1: version "11.12.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" @@ -8389,8 +8490,8 @@ node-pre-gyp@^0.10.0: tar "^4" node-releases@^1.1.3, node-releases@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.8.tgz#32a63fff63c5e51b7e0f540ac95947d220fc6862" + version "1.1.10" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.10.tgz#5dbeb6bc7f4e9c85b899e2e7adcc0635c9b2adf7" dependencies: semver "^5.3.0" @@ -8516,8 +8617,8 @@ number-is-nan@^1.0.0: resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" nwsapi@^2.0.7, nwsapi@^2.0.9: - version "2.1.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.0.tgz#781065940aed90d9bb01ca5d0ce0fcf81c32712f" + version "2.1.1" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.1.tgz#08d6d75e69fd791bdea31507ffafe8c843b67e9c" oauth-sign@~0.9.0: version "0.9.0" @@ -8757,8 +8858,8 @@ p-limit@^1.1.0: p-try "^1.0.0" p-limit@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.1.0.tgz#1d5a0d20fb12707c758a655f6bbc4386b5930d68" + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" dependencies: p-try "^2.0.0" @@ -8839,8 +8940,8 @@ pacote@^9.4.1: which "^1.3.1" pako@~1.0.5: - version "1.0.8" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.8.tgz#6844890aab9c635af868ad5fecc62e8acbba3ea4" + version "1.0.10" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" parallel-transform@^1.1.0: version "1.1.0" @@ -9743,7 +9844,7 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@15.x, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: +prop-types@15.x, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" dependencies: @@ -10017,8 +10118,8 @@ rc-dropdown@~2.2.0: react-lifecycles-compat "^3.0.2" rc-editor-core@~0.8.3: - version "0.8.8" - resolved "https://registry.yarnpkg.com/rc-editor-core/-/rc-editor-core-0.8.8.tgz#331034cb8d50df218839fb399cdfb2a913e71630" + version "0.8.9" + resolved "https://registry.yarnpkg.com/rc-editor-core/-/rc-editor-core-0.8.9.tgz#f611952c8eed965e3e348d84ae7be885daeb221c" dependencies: babel-runtime "^6.26.0" classnames "^2.2.5" @@ -10042,8 +10143,8 @@ rc-editor-mention@^1.0.2: rc-editor-core "~0.8.3" rc-form@^2.1.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/rc-form/-/rc-form-2.4.2.tgz#b32a84b80c8201087bbb7e1b54fd75527bce2230" + version "2.4.3" + resolved "https://registry.yarnpkg.com/rc-form/-/rc-form-2.4.3.tgz#742ed935ed029c6cd9a98ee6837cb37ab3b51e5d" dependencies: async-validator "~1.8.5" babel-runtime "6.x" @@ -10304,8 +10405,8 @@ react-addons-test-utils@15.6.2: resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz#c12b6efdc2247c10da7b8770d185080a7b047156" react-app-polyfill@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-0.2.1.tgz#96c701a40b9671c8547f70bdbb4a47f4d5767790" + version "0.2.2" + resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-0.2.2.tgz#a903b61a8bfd9c5e5f16fc63bebe44d6922a44fb" dependencies: core-js "2.6.4" object-assign "4.1.1" @@ -10329,8 +10430,8 @@ react-copy-to-clipboard@^5.0.1: prop-types "^15.5.8" react-dev-utils@^7.0.0: - version "7.0.3" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-7.0.3.tgz#f1316cfffd792fd41b0c28ad5db86c1d74484d6f" + version "7.0.5" + resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-7.0.5.tgz#cb95375d01ae71ca27b3c7616006ef7a77d14e8e" dependencies: "@babel/code-frame" "7.0.0" address "1.0.3" @@ -10350,7 +10451,7 @@ react-dev-utils@^7.0.0: loader-utils "1.2.3" opn "5.4.0" pkg-up "2.0.0" - react-error-overlay "^5.1.3" + react-error-overlay "^5.1.4" recursive-readdir "2.2.2" shell-quote "1.6.1" sockjs-client "1.3.0" @@ -10364,17 +10465,17 @@ react-dimensions@^1.3.0: element-resize-event "^2.0.4" react-dom@^16.3.2, react-dom@^16.7.0: - version "16.8.3" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.3.tgz#ae236029e66210783ac81999d3015dfc475b9c32" + version "16.8.4" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.4.tgz#1061a8e01a2b3b0c8160037441c3bf00a0e3bc48" dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.13.3" + scheduler "^0.13.4" -react-error-overlay@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.3.tgz#16fcbde75ed4dc6161dc6dc959b48e92c6ffa9ad" +react-error-overlay@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.4.tgz#88dfb88857c18ceb3b9f95076f850d7121776991" react-ga@^2.4.1: version "2.5.7" @@ -10405,9 +10506,9 @@ react-input-autosize@^2.1.2: dependencies: prop-types "^15.5.8" -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.3: - version "16.8.3" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.3.tgz#4ad8b029c2a718fc0cfc746c8d4e1b7221e5387d" +react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4: + version "16.8.4" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" react-lazy-load@^3.0.12: version "3.0.13" @@ -10572,13 +10673,13 @@ react-test-renderer@^15.6.1: object-assign "^4.1.0" react-test-renderer@^16.0.0-0: - version "16.8.3" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.3.tgz#230006af264cc46aeef94392e04747c21839e05e" + version "16.8.4" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.4.tgz#abee4c2c3bf967a8892a7b37f77370c5570d5329" dependencies: object-assign "^4.1.1" prop-types "^15.6.2" - react-is "^16.8.3" - scheduler "^0.13.3" + react-is "^16.8.4" + scheduler "^0.13.4" react-virtualized-select@^3.1.0: version "3.1.3" @@ -10633,13 +10734,13 @@ react-vis@^1.7.2: react-motion "^0.5.2" react@^16.3.2, react@^16.7.0: - version "16.8.3" - resolved "https://registry.yarnpkg.com/react/-/react-16.8.3.tgz#c6f988a2ce895375de216edcfaedd6b9a76451d9" + version "16.8.4" + resolved "https://registry.yarnpkg.com/react/-/react-16.8.4.tgz#fdf7bd9ae53f03a9c4cd1a371432c206be1c4768" dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.13.3" + scheduler "^0.13.4" read-cmd-shim@^1.0.1: version "1.0.1" @@ -10740,8 +10841,8 @@ read@1, read@~1.0.1: util-deprecate "~1.0.1" readable-stream@^3.0.6, readable-stream@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.1.1.tgz#ed6bbc6c5ba58b090039ff18ce670515795aeb06" + version "3.2.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d" dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -10813,8 +10914,8 @@ reduce-reducers@^0.4.3: resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.4.3.tgz#8e052618801cd8fc2714b4915adaa8937eb6d66c" redux-actions@^2.2.1: - version "2.6.4" - resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-2.6.4.tgz#e1d9d7d987d274071b0134b707365d3e25ba3b26" + version "2.6.5" + resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-2.6.5.tgz#bdca548768ee99832a63910c276def85e821a27e" dependencies: invariant "^2.2.4" just-curry-it "^3.1.0" @@ -10861,9 +10962,9 @@ redux@^3.0.2, redux@^3.7.2: loose-envify "^1.1.0" symbol-observable "^1.0.3" -regenerate-unicode-properties@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c" +regenerate-unicode-properties@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.2.tgz#7b38faa296252376d363558cfbda90c9ce709662" dependencies: regenerate "^1.4.0" @@ -10879,7 +10980,7 @@ regenerator-runtime@^0.12.0: version "0.12.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" -regenerator-transform@^0.13.3: +regenerator-transform@^0.13.4: version "0.13.4" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb" dependencies: @@ -10915,15 +11016,15 @@ regexpu-core@^1.0.0: regjsparser "^0.1.4" regexpu-core@^4.1.3, regexpu-core@^4.2.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.4.0.tgz#8d43e0d1266883969720345e70c275ee0aec0d32" + version "4.5.4" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.4.tgz#080d9d02289aa87fe1667a4f5136bc98a6aebaae" dependencies: regenerate "^1.4.0" - regenerate-unicode-properties "^7.0.0" + regenerate-unicode-properties "^8.0.2" regjsgen "^0.5.0" regjsparser "^0.6.0" unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.0.2" + unicode-match-property-value-ecmascript "^1.1.0" regjsgen@^0.2.0: version "0.2.0" @@ -11229,15 +11330,14 @@ sax@^1.2.4, sax@~1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" saxes@^3.1.5: - version "3.1.7" - resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.7.tgz#d322d8295b410521a183c659a456a5c1a51864f4" + version "3.1.9" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.9.tgz#c1c197cd54956d88c09f960254b999e192d7058b" dependencies: - eslint "^5.14.1" xmlchars "^1.3.1" -scheduler@^0.13.3: - version "0.13.3" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.3.tgz#bed3c5850f62ea9c716a4d781f9daeb9b2a58896" +scheduler@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.4.tgz#8fef05e7a3580c76c0364d2df5e550e4c9140298" dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -11534,15 +11634,15 @@ sockjs@0.3.19: uuid "^3.0.1" socks-proxy-agent@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.1.tgz#5936bf8b707a993079c6f37db2091821bffa6473" + version "4.0.2" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386" dependencies: - agent-base "~4.2.0" - socks "~2.2.0" + agent-base "~4.2.1" + socks "~2.3.2" -socks@~2.2.0: - version "2.2.3" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.2.3.tgz#7399ce11e19b2a997153c983a9ccb6306721f2dc" +socks@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.2.tgz#ade388e9e6d87fdb11649c15746c578922a5883e" dependencies: ip "^1.1.5" smart-buffer "4.0.2" @@ -11586,7 +11686,7 @@ source-map-support@^0.4.15: dependencies: source-map "^0.5.6" -source-map-support@^0.5.6, source-map-support@~0.5.9: +source-map-support@^0.5.6, source-map-support@~0.5.10: version "0.5.10" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c" dependencies: @@ -11732,7 +11832,7 @@ ssri@^6.0.0, ssri@^6.0.1: dependencies: figgy-pudding "^3.5.1" -stable@~0.1.6: +stable@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" @@ -11826,12 +11926,12 @@ string-width@^1.0.1: strip-ansi "^4.0.0" string-width@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.0.0.tgz#5a1690a57cc78211fffd9bf24bbe24d090604eb1" + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" dependencies: emoji-regex "^7.0.1" is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.0.0" + strip-ansi "^5.1.0" string.prototype.trim@^1.1.2: version "1.1.2" @@ -11861,7 +11961,7 @@ stringify-object@^3.2.2: is-obj "^1.0.1" is-regexp "^1.0.0" -strip-ansi@5.0.0, strip-ansi@^5.0.0: +strip-ansi@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f" dependencies: @@ -11879,6 +11979,12 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^5.0.0, strip-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.1.0.tgz#55aaa54e33b4c0649a7338a43437b1887d153ec4" + dependencies: + ansi-regex "^4.1.0" + strip-bom@3.0.0, strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -11967,21 +12073,21 @@ supports-color@^6.1.0: has-flag "^3.0.0" svgo@^1.0.0, svgo@^1.0.5: - version "1.1.1" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.1.1.tgz#12384b03335bcecd85cfa5f4e3375fed671cb985" + version "1.2.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.2.0.tgz#305a8fc0f4f9710828c65039bb93d5793225ffc3" dependencies: - coa "~2.0.1" - colors "~1.1.2" + chalk "^2.4.1" + coa "^2.0.2" css-select "^2.0.0" - css-select-base-adapter "~0.1.0" + css-select-base-adapter "^0.1.1" css-tree "1.0.0-alpha.28" css-url-regex "^1.1.0" - csso "^3.5.0" + csso "^3.5.1" js-yaml "^3.12.0" mkdirp "~0.5.1" - object.values "^1.0.4" + object.values "^1.1.0" sax "~1.2.4" - stable "~0.1.6" + stable "^0.1.8" unquote "~1.1.1" util.promisify "~1.0.0" @@ -12072,8 +12178,8 @@ terser-webpack-plugin@1.1.0: worker-farm "^1.5.2" terser-webpack-plugin@^1.1.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.2.tgz#9bff3a891ad614855a7dde0d707f7db5a927e3d9" + version "1.2.3" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.3.tgz#3f98bc902fac3e5d0de730869f50668561262ec8" dependencies: cacache "^11.0.2" find-cache-dir "^2.0.0" @@ -12085,12 +12191,12 @@ terser-webpack-plugin@^1.1.0: worker-farm "^1.5.2" terser@^3.16.1, terser@^3.8.1: - version "3.16.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-3.16.1.tgz#5b0dd4fa1ffd0b0b43c2493b2c364fd179160493" + version "3.17.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2" dependencies: - commander "~2.17.1" + commander "^2.19.0" source-map "~0.6.1" - source-map-support "~0.5.9" + source-map-support "~0.5.10" test-exclude@^4.2.1: version "4.2.3" @@ -12270,8 +12376,8 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" tsutils@^3.7.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.8.0.tgz#7a3dbadc88e465596440622b65c04edc8e187ae5" + version "3.9.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.9.0.tgz#fb8b6943f654a0857f17b538236d7f811c5bc5ce" dependencies: tslib "^1.8.1" @@ -12376,13 +12482,13 @@ unicode-match-property-ecmascript@^1.0.4: unicode-canonical-property-names-ecmascript "^1.0.4" unicode-property-aliases-ecmascript "^1.0.4" -unicode-match-property-value-ecmascript@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz#9f1dc76926d6ccf452310564fd834ace059663d4" +unicode-match-property-value-ecmascript@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277" unicode-property-aliases-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0" + version "1.0.5" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57" union-value@^1.0.0: version "1.0.0" @@ -12439,8 +12545,8 @@ unset-value@^1.0.0: isobject "^3.0.0" upath@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" + version "1.1.2" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" upper-case@^1.1.1: version "1.1.3" @@ -12646,8 +12752,8 @@ webidl-conversions@^4.0.2: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" webpack-chain@^5.0.1: - version "5.2.0" - resolved "https://registry.yarnpkg.com/webpack-chain/-/webpack-chain-5.2.0.tgz#752c01e42752412610b27e3dbd6e0781146a465a" + version "5.2.1" + resolved "https://registry.yarnpkg.com/webpack-chain/-/webpack-chain-5.2.1.tgz#0e8f5e5ddba35d263ac357cf5ae7ec84138d57c5" dependencies: deepmerge "^1.5.2" javascript-stringify "^1.6.0" @@ -12678,8 +12784,8 @@ webpack-dev-middleware@3.4.0: webpack-log "^2.0.0" webpack-dev-middleware@^3.5.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.6.0.tgz#71f1b04e52ff8d442757af2be3a658237d53a3e5" + version "3.6.1" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.6.1.tgz#91f2531218a633a99189f7de36045a331a4b9cd4" dependencies: memory-fs "^0.4.1" mime "^2.3.1" @@ -12720,8 +12826,8 @@ webpack-dev-server@3.1.9: yargs "12.0.2" webpack-dev-server@^3.1.14: - version "3.2.0" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.2.0.tgz#cf22c8819e0d41736ba1922dde985274716f1214" + version "3.2.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.2.1.tgz#1b45ce3ecfc55b6ebe5e36dab2777c02bc508c4e" dependencies: ansi-html "0.0.7" bonjour "^3.5.0" @@ -12734,7 +12840,7 @@ webpack-dev-server@^3.1.14: html-entities "^1.2.0" http-proxy-middleware "^0.19.1" import-local "^2.0.0" - internal-ip "^4.0.0" + internal-ip "^4.2.0" ip "^1.1.5" killable "^1.0.0" loglevel "^1.4.1" @@ -12810,13 +12916,13 @@ webpack@4.19.1: webpack-sources "^1.2.0" webpack@^4.28.4: - version "4.29.5" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.29.5.tgz#52b60a7b0838427c3a894cd801a11dc0836bc79f" + version "4.29.6" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.29.6.tgz#66bf0ec8beee4d469f8b598d3988ff9d8d90e955" dependencies: - "@webassemblyjs/ast" "1.8.3" - "@webassemblyjs/helper-module-context" "1.8.3" - "@webassemblyjs/wasm-edit" "1.8.3" - "@webassemblyjs/wasm-parser" "1.8.3" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/wasm-edit" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" acorn "^6.0.5" acorn-dynamic-import "^4.0.0" ajv "^6.1.0" @@ -13115,8 +13221,8 @@ ws@^5.2.0: async-limiter "~1.0.0" ws@^6.1.2: - version "6.1.4" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" + version "6.2.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.0.tgz#13806d9913b2a5f3cbb9ba47b563c002cbc7c526" dependencies: async-limiter "~1.0.0"