From 008bee8f27a67427a8f98b8866d6275c9a8c8652 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Wed, 8 Apr 2020 17:16:22 +0200 Subject: [PATCH] Tracing: Adds header and minimap (#23315) * Add integration with Jeager Add Jaeger datasource and modify derived fields in loki to allow for opening a trace in Jager in separate split. Modifies build so that this branch docker images are pushed to docker hub Add a traceui dir with docker-compose and provision files for demoing.:wq * Enable docker logger plugin to send logs to loki * Add placeholder zipkin datasource * Fixed rebase issues, added enhanceDataFrame to non-legacy code path * Trace selector for jaeger query field * Fix logs default mode for Loki * Fix loading jaeger query field services on split * Updated grafana image in traceui/compose file * Fix prettier error * Hide behind feature flag, clean up unused code. * Fix tests * Fix tests * Cleanup code and review feedback * Remove traceui directory * Remove circle build changes * Fix feature toggles object * Fix merge issues * Add trace ui in Explore * WIP * WIP * WIP * Make jaeger datasource return trace data instead of link * Allow js in jest tests * Return data from Jaeger datasource * Take yarn.lock from master * Fix missing component * Update yarn lock * Fix some ts and lint errors * Fix merge * Fix type errors * Make tests pass again * Add tests * Fix es5 compatibility * Add header with minimap * Fix sizing issue due to column resizer handle * Fix issues with sizing, search functionality, duplicate react, tests * Refactor TraceView component, fix tests * Fix type errors * Add tests for hooks Co-authored-by: David Kaltschmidt --- conf/provisioning/datasources/loki_test.yaml | 17 + .../grafana-ui/src/components/Input/Input.tsx | 4 +- packages/jaeger-ui-components/package.json | 1 - .../SpanGraph/CanvasSpanGraph.test.js | 32 + .../SpanGraph/CanvasSpanGraph.tsx | 73 ++ .../SpanGraph/GraphTicks.test.js | 44 ++ .../TracePageHeader/SpanGraph/GraphTicks.tsx | 47 ++ .../SpanGraph/Scrubber.test.js | 62 ++ .../TracePageHeader/SpanGraph/Scrubber.tsx | 108 +++ .../SpanGraph/TickLabels.test.js | 59 ++ .../TracePageHeader/SpanGraph/TickLabels.tsx | 60 ++ .../SpanGraph/ViewingLayer.test.js | 328 ++++++++ .../SpanGraph/ViewingLayer.tsx | 408 ++++++++++ .../TracePageHeader/SpanGraph/index.test.js | 76 ++ .../src/TracePageHeader/SpanGraph/index.tsx | 102 +++ .../SpanGraph/render-into-canvas.test.js | 199 +++++ .../SpanGraph/render-into-canvas.tsx | 62 ++ .../TracePageHeader/TracePageHeader.test.js | 80 ++ .../src/TracePageHeader/TracePageHeader.tsx | 293 +++++++ .../TracePageSearchBar.markers.tsx | 15 + .../TracePageSearchBar.test.js | 92 +++ .../TracePageHeader/TracePageSearchBar.tsx | 144 ++++ .../src/TracePageHeader/index.tsx | 15 + .../TimelineColumnResizer.tsx | 6 +- .../TimelineHeaderRow/TimelineHeaderRow.tsx | 11 +- .../src/TraceTimelineViewer/index.tsx | 19 +- .../src/common/BreakableText.tsx | 64 ++ .../src/common/ExternalLinks.tsx | 59 ++ .../src/common/LoadingIndicator.tsx | 78 ++ .../src/common/TraceName.tsx | 65 ++ .../src/common/UiFindInput.test.js | 65 ++ .../src/common/UiFindInput.tsx | 66 ++ .../__snapshots__/UiFindInput.test.js.snap | 10 + packages/jaeger-ui-components/src/index.ts | 2 + .../src/model/link-patterns.tsx | 12 +- .../jaeger-ui-components/src/model/span.tsx | 1 - .../src/model/trace-viewer.ts | 20 + .../jaeger-ui-components/src/types/trace.tsx | 2 +- .../src/uberUtilityStyles.ts | 12 + .../src/uiElementsContext.tsx | 40 +- .../src/utils/filter-spans.test.js | 184 +++++ .../src/utils/filter-spans.tsx | 67 ++ public/app/features/explore/Explore.test.tsx | 2 +- public/app/features/explore/Explore.tsx | 26 +- .../app/features/explore/TraceView.test.tsx | 182 ----- public/app/features/explore/TraceView.tsx | 247 ------ .../explore/TraceView/TraceView.test.tsx | 216 ++++++ .../features/explore/TraceView/TraceView.tsx | 119 +++ .../features/explore/TraceView/uiElements.tsx | 45 ++ .../TraceView/useChildrenState.test.ts | 65 ++ .../explore/TraceView/useChildrenState.ts | 94 +++ .../explore/TraceView/useDetailState.test.ts | 56 ++ .../explore/TraceView/useDetailState.ts | 70 ++ .../TraceView/useHoverIndentGuide.test.ts | 16 + .../explore/TraceView/useHoverIndentGuide.ts | 30 + .../explore/TraceView/useSearch.test.ts | 44 ++ .../features/explore/TraceView/useSearch.ts | 15 + .../explore/TraceView/useViewRange.test.ts | 25 + .../explore/TraceView/useViewRange.ts | 35 + public/app/features/explore/Wrapper.tsx | 2 +- yarn.lock | 734 ++++++++++++++++-- 61 files changed, 4603 insertions(+), 524 deletions(-) create mode 100644 conf/provisioning/datasources/loki_test.yaml create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.test.js create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.test.js create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.tsx create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.test.js create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.tsx create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/TickLabels.test.js create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/TickLabels.tsx create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.js create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.js create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.tsx create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.test.js create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.tsx create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.test.js create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.tsx create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.markers.tsx create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx create mode 100644 packages/jaeger-ui-components/src/TracePageHeader/index.tsx create mode 100644 packages/jaeger-ui-components/src/common/BreakableText.tsx create mode 100644 packages/jaeger-ui-components/src/common/ExternalLinks.tsx create mode 100644 packages/jaeger-ui-components/src/common/LoadingIndicator.tsx create mode 100644 packages/jaeger-ui-components/src/common/TraceName.tsx create mode 100644 packages/jaeger-ui-components/src/common/UiFindInput.test.js create mode 100644 packages/jaeger-ui-components/src/common/UiFindInput.tsx create mode 100644 packages/jaeger-ui-components/src/common/__snapshots__/UiFindInput.test.js.snap create mode 100644 packages/jaeger-ui-components/src/model/trace-viewer.ts create mode 100644 packages/jaeger-ui-components/src/utils/filter-spans.test.js create mode 100644 packages/jaeger-ui-components/src/utils/filter-spans.tsx delete mode 100644 public/app/features/explore/TraceView.test.tsx delete mode 100644 public/app/features/explore/TraceView.tsx create mode 100644 public/app/features/explore/TraceView/TraceView.test.tsx create mode 100644 public/app/features/explore/TraceView/TraceView.tsx create mode 100644 public/app/features/explore/TraceView/uiElements.tsx create mode 100644 public/app/features/explore/TraceView/useChildrenState.test.ts create mode 100644 public/app/features/explore/TraceView/useChildrenState.ts create mode 100644 public/app/features/explore/TraceView/useDetailState.test.ts create mode 100644 public/app/features/explore/TraceView/useDetailState.ts create mode 100644 public/app/features/explore/TraceView/useHoverIndentGuide.test.ts create mode 100644 public/app/features/explore/TraceView/useHoverIndentGuide.ts create mode 100644 public/app/features/explore/TraceView/useSearch.test.ts create mode 100644 public/app/features/explore/TraceView/useSearch.ts create mode 100644 public/app/features/explore/TraceView/useViewRange.test.ts create mode 100644 public/app/features/explore/TraceView/useViewRange.ts diff --git a/conf/provisioning/datasources/loki_test.yaml b/conf/provisioning/datasources/loki_test.yaml new file mode 100644 index 00000000000..70f43d0ee92 --- /dev/null +++ b/conf/provisioning/datasources/loki_test.yaml @@ -0,0 +1,17 @@ + +apiVersion: 1 + +datasources: + - name: loki-derived-test + type: loki + access: proxy + url: http://localhost:3100 + editable: false + jsonData: + derivedFields: + - name: "traceID" + matcherRegex: "traceID=(\\w+)" + url: "$${__value.raw}" + datasourceName: "Jaeger" + + diff --git a/packages/grafana-ui/src/components/Input/Input.tsx b/packages/grafana-ui/src/components/Input/Input.tsx index ae674f3328f..1386ee33fca 100644 --- a/packages/grafana-ui/src/components/Input/Input.tsx +++ b/packages/grafana-ui/src/components/Input/Input.tsx @@ -11,9 +11,9 @@ export interface Props extends Omit, 'prefix' | 'siz /** Show an invalid state around the input */ invalid?: boolean; /** Show an icon as a prefix in the input */ - prefix?: JSX.Element | string | null; + prefix?: ReactNode; /** Show an icon as a suffix in the input */ - suffix?: JSX.Element | string | null; + suffix?: ReactNode; /** Show a loading indicator as a suffix in the input */ loading?: boolean; /** Add a component as an addon before the input */ diff --git a/packages/jaeger-ui-components/package.json b/packages/jaeger-ui-components/package.json index 213600919aa..8d20a90e383 100644 --- a/packages/jaeger-ui-components/package.json +++ b/packages/jaeger-ui-components/package.json @@ -34,7 +34,6 @@ "lru-memoize": "^1.1.0", "memoize-one": "^5.0.0", "moment": "^2.18.1", - "react": "^16.3.2", "react-icons": "2.2.7", "recompose": "^0.25.0", "tween-functions": "^1.2.0" diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.test.js b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.test.js new file mode 100644 index 00000000000..a3dbab15165 --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.test.js @@ -0,0 +1,32 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { shallow } from 'enzyme'; + +import CanvasSpanGraph from './CanvasSpanGraph'; + +describe('', () => { + it('renders without exploding', () => { + const items = [{ valueWidth: 1, valueOffset: 1, serviceName: 'service-name-0' }]; + const wrapper = shallow(); + expect(wrapper).toBeDefined(); + wrapper.instance()._setCanvasRef({ + getContext: () => ({ + fillRect: () => {}, + }), + }); + wrapper.setProps({ items }); + }); +}); diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx new file mode 100644 index 00000000000..4ce62c6988c --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx @@ -0,0 +1,73 @@ +// 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 * as React from 'react'; +import { css } from 'emotion'; + +import renderIntoCanvas from './render-into-canvas'; +import colorGenerator from '../../utils/color-generator'; +import { TNil } from '../../types'; + +import { createStyle } from '../../Theme'; + +const getStyles = createStyle(() => { + return { + CanvasSpanGraph: css` + label: CanvasSpanGraph; + background: #fafafa; + height: 60px; + position: absolute; + width: 100%; + `, + }; +}); + +type CanvasSpanGraphProps = { + items: Array<{ valueWidth: number; valueOffset: number; serviceName: string }>; + valueWidth: number; +}; + +const getColor = (hex: string) => colorGenerator.getRgbColorByKey(hex); + +export default class CanvasSpanGraph extends React.PureComponent { + _canvasElm: HTMLCanvasElement | TNil; + + constructor(props: CanvasSpanGraphProps) { + super(props); + this._canvasElm = undefined; + } + + componentDidMount() { + this._draw(); + } + + componentDidUpdate() { + this._draw(); + } + + _setCanvasRef = (elm: HTMLCanvasElement | TNil) => { + this._canvasElm = elm; + }; + + _draw() { + if (this._canvasElm) { + const { valueWidth: totalValueWidth, items } = this.props; + renderIntoCanvas(this._canvasElm, items, totalValueWidth, getColor); + } + } + + render() { + return ; + } +} diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.test.js b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.test.js new file mode 100644 index 00000000000..9f1352dfcb2 --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.test.js @@ -0,0 +1,44 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { shallow } from 'enzyme'; + +import GraphTicks from './GraphTicks'; + +describe('', () => { + const defaultProps = { + items: [ + { valueWidth: 100, valueOffset: 25, serviceName: 'a' }, + { valueWidth: 100, valueOffset: 50, serviceName: 'b' }, + ], + valueWidth: 200, + numTicks: 4, + }; + + let ticksG; + + beforeEach(() => { + const wrapper = shallow(); + ticksG = wrapper.find('[data-test="ticks"]'); + }); + + it('creates a for ticks', () => { + expect(ticksG.length).toBe(1); + }); + + it('creates a line for each ticks excluding the first and last', () => { + expect(ticksG.find('line').length).toBe(defaultProps.numTicks - 1); + }); +}); diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.tsx b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.tsx new file mode 100644 index 00000000000..c6502943df9 --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/GraphTicks.tsx @@ -0,0 +1,47 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { css } from 'emotion'; +import { createStyle } from '../../Theme'; + +const getStyles = createStyle(() => { + return { + GraphTick: css` + label: GraphTick; + stroke: #aaa; + stroke-width: 1px; + `, + }; +}); + +type GraphTicksProps = { + numTicks: number; +}; + +export default function GraphTicks(props: GraphTicksProps) { + const { numTicks } = props; + const ticks = []; + // i starts at 1, limit is `i < numTicks` so the first and last ticks aren't drawn + for (let i = 1; i < numTicks; i++) { + const x = `${(i / numTicks) * 100}%`; + ticks.push(); + } + + return ( + + ); +} diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.test.js b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.test.js new file mode 100644 index 00000000000..afe6525f464 --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.test.js @@ -0,0 +1,62 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; + +import Scrubber, { getStyles } from './Scrubber'; + +describe('', () => { + const defaultProps = { + onMouseDown: sinon.spy(), + position: 0, + }; + + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('contains the proper svg components', () => { + const styles = getStyles(); + expect( + wrapper.matchesElement( + + + + + + + + ) + ).toBeTruthy(); + }); + + it('calculates the correct x% for a timestamp', () => { + wrapper = shallow(); + const line = wrapper.find('line').first(); + const rect = wrapper.find('rect').first(); + expect(line.prop('x1')).toBe('50%'); + expect(line.prop('x2')).toBe('50%'); + expect(rect.prop('x')).toBe('50%'); + }); + + it('supports onMouseDown', () => { + const event = {}; + wrapper.find(`.${getStyles().ScrubberHandles}`).prop('onMouseDown')(event); + expect(defaultProps.onMouseDown.calledWith(event)).toBeTruthy(); + }); +}); diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.tsx b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.tsx new file mode 100644 index 00000000000..61364e37dda --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/Scrubber.tsx @@ -0,0 +1,108 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import cx from 'classnames'; + +import { createStyle } from '../../Theme'; +import { css } from 'emotion'; + +export const getStyles = createStyle(() => { + const ScrubberHandleExpansion = css` + label: ScrubberHandleExpansion; + cursor: col-resize; + fill-opacity: 0; + fill: #44f; + `; + const ScrubberHandle = css` + label: ScrubberHandle; + cursor: col-resize; + fill: #555; + `; + const ScrubberLine = css` + label: ScrubberLine; + pointer-events: none; + stroke: #555; + `; + return { + ScrubberDragging: css` + label: ScrubberDragging; + & .${ScrubberHandleExpansion} { + fill-opacity: 1; + } + & .${ScrubberHandle} { + fill: #44f; + } + & > .${ScrubberLine} { + stroke: #44f; + } + `, + ScrubberHandles: css` + label: ScrubberHandles; + &:hover > .${ScrubberHandleExpansion} { + fill-opacity: 1; + } + &:hover > .${ScrubberHandle} { + fill: #44f; + } + &:hover + .${ScrubberLine} { + stroke: #44f; + } + `, + ScrubberHandleExpansion, + ScrubberHandle, + ScrubberLine, + }; +}); + +type ScrubberProps = { + isDragging: boolean; + position: number; + onMouseDown: (evt: React.MouseEvent) => void; + onMouseEnter: (evt: React.MouseEvent) => void; + onMouseLeave: (evt: React.MouseEvent) => void; +}; + +export default function Scrubber({ isDragging, onMouseDown, onMouseEnter, onMouseLeave, position }: ScrubberProps) { + const xPercent = `${position * 100}%`; + const styles = getStyles(); + const className = cx({ [styles.ScrubberDragging]: isDragging }); + return ( + + + {/* handleExpansion is only visible when `isDragging` is true */} + + + + + + ); +} diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/TickLabels.test.js b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/TickLabels.test.js new file mode 100644 index 00000000000..619a0ee8b9d --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/TickLabels.test.js @@ -0,0 +1,59 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { shallow } from 'enzyme'; + +import TickLabels from './TickLabels'; + +describe('', () => { + const defaultProps = { + numTicks: 4, + duration: 5000, + }; + + let wrapper; + let ticks; + + beforeEach(() => { + wrapper = shallow(); + ticks = wrapper.find('[data-test="tick"]'); + }); + + it('renders the right number of ticks', () => { + expect(ticks.length).toBe(defaultProps.numTicks + 1); + }); + + it('places the first tick on the left', () => { + const firstTick = ticks.first(); + expect(firstTick.prop('style')).toEqual({ left: '0%' }); + }); + + it('places the last tick on the right', () => { + const lastTick = ticks.last(); + expect(lastTick.prop('style')).toEqual({ right: '0%' }); + }); + + it('places middle ticks at proper intervals', () => { + const positions = ['25%', '50%', '75%']; + positions.forEach((pos, i) => { + const tick = ticks.at(i + 1); + expect(tick.prop('style')).toEqual({ left: pos }); + }); + }); + + it("doesn't explode if no trace is present", () => { + expect(() => shallow()).not.toThrow(); + }); +}); diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/TickLabels.tsx b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/TickLabels.tsx new file mode 100644 index 00000000000..8fd779ab95f --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/TickLabels.tsx @@ -0,0 +1,60 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; + +import { formatDuration } from '../../utils/date'; + +import { createStyle } from '../../Theme'; +import { css } from 'emotion'; + +const getStyles = createStyle(() => { + return { + TickLabels: css` + label: TickLabels; + height: 1rem; + position: relative; + `, + TickLabelsLabel: css` + label: TickLabelsLabel; + color: #717171; + font-size: 0.7rem; + position: absolute; + user-select: none; + `, + }; +}); + +type TickLabelsProps = { + numTicks: number; + duration: number; +}; + +export default function TickLabels(props: TickLabelsProps) { + const { numTicks, duration } = props; + const styles = getStyles(); + + const ticks = []; + for (let i = 0; i < numTicks + 1; i++) { + const portion = i / numTicks; + const style = portion === 1 ? { right: '0%' } : { left: `${portion * 100}%` }; + ticks.push( +
+ {formatDuration(duration * portion)} +
+ ); + } + + return
{ticks}
; +} diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.js b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.js new file mode 100644 index 00000000000..c76f3766bc2 --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.js @@ -0,0 +1,328 @@ +// 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 { shallow } from 'enzyme'; +import React from 'react'; + +import GraphTicks from './GraphTicks'; +import Scrubber from './Scrubber'; +import ViewingLayer, { dragTypes, getStyles } from './ViewingLayer'; +import { EUpdateTypes } from '../../utils/DraggableManager'; +import { polyfill as polyfillAnimationFrame } from '../../utils/test/requestAnimationFrame'; + +function getViewRange(viewStart, viewEnd) { + return { + time: { + current: [viewStart, viewEnd], + }, + }; +} + +describe('', () => { + polyfillAnimationFrame(window); + + let props; + let wrapper; + + beforeEach(() => { + props = { + height: 60, + numTicks: 5, + updateNextViewRangeTime: jest.fn(), + updateViewRangeTime: jest.fn(), + viewRange: getViewRange(0, 1), + }; + wrapper = shallow(); + }); + + describe('_getDraggingBounds()', () => { + beforeEach(() => { + props = { ...props, viewRange: getViewRange(0.1, 0.9) }; + wrapper = shallow(); + wrapper.instance()._setRoot({ + getBoundingClientRect() { + return { left: 10, width: 100 }; + }, + }); + }); + + it('throws if _root is not set', () => { + const instance = wrapper.instance(); + instance._root = null; + expect(() => instance._getDraggingBounds(dragTypes.REFRAME)).toThrow(); + }); + + it('returns the correct bounds for reframe', () => { + const bounds = wrapper.instance()._getDraggingBounds(dragTypes.REFRAME); + expect(bounds).toEqual({ + clientXLeft: 10, + width: 100, + maxValue: 1, + minValue: 0, + }); + }); + + it('returns the correct bounds for shiftStart', () => { + const bounds = wrapper.instance()._getDraggingBounds(dragTypes.SHIFT_START); + expect(bounds).toEqual({ + clientXLeft: 10, + width: 100, + maxValue: 0.9, + minValue: 0, + }); + }); + + it('returns the correct bounds for shiftEnd', () => { + const bounds = wrapper.instance()._getDraggingBounds(dragTypes.SHIFT_END); + expect(bounds).toEqual({ + clientXLeft: 10, + width: 100, + maxValue: 1, + minValue: 0.1, + }); + }); + }); + + describe('DraggableManager callbacks', () => { + describe('reframe', () => { + it('handles mousemove', () => { + const value = 0.5; + wrapper.instance()._handleReframeMouseMove({ value }); + const calls = props.updateNextViewRangeTime.mock.calls; + expect(calls).toEqual([[{ cursor: value }]]); + }); + + it('handles mouseleave', () => { + wrapper.instance()._handleReframeMouseLeave(); + const calls = props.updateNextViewRangeTime.mock.calls; + expect(calls).toEqual([[{ cursor: null }]]); + }); + + describe('drag update', () => { + it('handles sans anchor', () => { + const value = 0.5; + wrapper.instance()._handleReframeDragUpdate({ value }); + const calls = props.updateNextViewRangeTime.mock.calls; + expect(calls).toEqual([[{ reframe: { anchor: value, shift: value } }]]); + }); + + it('handles the existing anchor', () => { + const value = 0.5; + const anchor = 0.1; + const time = { ...props.viewRange.time, reframe: { anchor } }; + props = { ...props, viewRange: { time } }; + wrapper = shallow(); + wrapper.instance()._handleReframeDragUpdate({ value }); + const calls = props.updateNextViewRangeTime.mock.calls; + expect(calls).toEqual([[{ reframe: { anchor, shift: value } }]]); + }); + }); + + describe('drag end', () => { + let manager; + + beforeEach(() => { + manager = { resetBounds: jest.fn() }; + }); + + it('handles sans anchor', () => { + const value = 0.5; + wrapper.instance()._handleReframeDragEnd({ manager, value }); + expect(manager.resetBounds.mock.calls).toEqual([[]]); + const calls = props.updateViewRangeTime.mock.calls; + expect(calls).toEqual([[value, value, 'minimap']]); + }); + + it('handles dragged left (anchor is greater)', () => { + const value = 0.5; + const anchor = 0.6; + const time = { ...props.viewRange.time, reframe: { anchor } }; + props = { ...props, viewRange: { time } }; + wrapper = shallow(); + wrapper.instance()._handleReframeDragEnd({ manager, value }); + + expect(manager.resetBounds.mock.calls).toEqual([[]]); + const calls = props.updateViewRangeTime.mock.calls; + expect(calls).toEqual([[value, anchor, 'minimap']]); + }); + + it('handles dragged right (anchor is less)', () => { + const value = 0.5; + const anchor = 0.4; + const time = { ...props.viewRange.time, reframe: { anchor } }; + props = { ...props, viewRange: { time } }; + wrapper = shallow(); + wrapper.instance()._handleReframeDragEnd({ manager, value }); + + expect(manager.resetBounds.mock.calls).toEqual([[]]); + const calls = props.updateViewRangeTime.mock.calls; + expect(calls).toEqual([[anchor, value, 'minimap']]); + }); + }); + }); + + describe('scrubber', () => { + it('prevents the cursor from being drawn on scrubber mouseover', () => { + wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseEnter }); + expect(wrapper.state('preventCursorLine')).toBe(true); + }); + + it('prevents the cursor from being drawn on scrubber mouseleave', () => { + wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseLeave }); + expect(wrapper.state('preventCursorLine')).toBe(false); + }); + + describe('drag start and update', () => { + it('stops propagation on drag start', () => { + const stopPropagation = jest.fn(); + const update = { + event: { stopPropagation }, + type: EUpdateTypes.DragStart, + }; + wrapper.instance()._handleScrubberDragUpdate(update); + expect(stopPropagation.mock.calls).toEqual([[]]); + }); + + it('updates the viewRange for shiftStart and shiftEnd', () => { + const instance = wrapper.instance(); + const value = 0.5; + const cases = [ + { + dragUpdate: { + value, + tag: dragTypes.SHIFT_START, + type: EUpdateTypes.DragMove, + }, + viewRangeUpdate: { shiftStart: value }, + }, + { + dragUpdate: { + value, + tag: dragTypes.SHIFT_END, + type: EUpdateTypes.DragMove, + }, + viewRangeUpdate: { shiftEnd: value }, + }, + ]; + cases.forEach(_case => { + instance._handleScrubberDragUpdate(_case.dragUpdate); + expect(props.updateNextViewRangeTime).lastCalledWith(_case.viewRangeUpdate); + }); + }); + }); + + it('updates the view on drag end', () => { + const instance = wrapper.instance(); + const [viewStart, viewEnd] = props.viewRange.time.current; + const value = 0.5; + const cases = [ + { + dragUpdate: { + value, + manager: { resetBounds: jest.fn() }, + tag: dragTypes.SHIFT_START, + }, + viewRangeUpdate: [value, viewEnd], + }, + { + dragUpdate: { + value, + manager: { resetBounds: jest.fn() }, + tag: dragTypes.SHIFT_END, + }, + viewRangeUpdate: [viewStart, value], + }, + ]; + cases.forEach(_case => { + const { manager } = _case.dragUpdate; + wrapper.setState({ preventCursorLine: true }); + expect(wrapper.state('preventCursorLine')).toBe(true); + instance._handleScrubberDragEnd(_case.dragUpdate); + expect(wrapper.state('preventCursorLine')).toBe(false); + expect(manager.resetBounds.mock.calls).toEqual([[]]); + expect(props.updateViewRangeTime).lastCalledWith(..._case.viewRangeUpdate, 'minimap'); + }); + }); + }); + + describe('.ViewingLayer--resetZoom', () => { + it('should not render .ViewingLayer--resetZoom if props.viewRange.time.current = [0,1]', () => { + expect(wrapper.find(`.${getStyles().ViewingLayerResetZoom}`).length).toBe(0); + wrapper.setProps({ viewRange: { time: { current: [0, 1] } } }); + expect(wrapper.find(`.${getStyles().ViewingLayerResetZoom}`).length).toBe(0); + }); + + it('should render ViewingLayer--resetZoom if props.viewRange.time.current[0] !== 0', () => { + // If the test fails on the following expect statement, this may be a false negative + expect(wrapper.find(`.${getStyles().ViewingLayerResetZoom}`).length).toBe(0); + wrapper.setProps({ viewRange: { time: { current: [0.1, 1] } } }); + expect(wrapper.find(`.${getStyles().ViewingLayerResetZoom}`).length).toBe(1); + }); + + it('should render ViewingLayer--resetZoom if props.viewRange.time.current[1] !== 1', () => { + // If the test fails on the following expect statement, this may be a false negative + expect(wrapper.find(`.${getStyles().ViewingLayerResetZoom}`).length).toBe(0); + wrapper.setProps({ viewRange: { time: { current: [0, 0.9] } } }); + expect(wrapper.find(`.${getStyles().ViewingLayerResetZoom}`).length).toBe(1); + }); + + it('should call props.updateViewRangeTime when clicked', () => { + wrapper.setProps({ viewRange: { time: { current: [0.1, 0.9] } } }); + const resetZoomButton = wrapper.find(`.${getStyles().ViewingLayerResetZoom}`); + // If the test fails on the following expect statement, this may be a false negative caused + // by a regression to rendering. + expect(resetZoomButton.length).toBe(1); + + resetZoomButton.simulate('click'); + expect(props.updateViewRangeTime).lastCalledWith(0, 1); + }); + }); + }); + + it('renders a ', () => { + expect(wrapper.find(GraphTicks).length).toBe(1); + }); + + it('renders a filtering box if leftBound exists', () => { + const _props = { ...props, viewRange: getViewRange(0.2, 1) }; + wrapper = shallow(); + + const leftBox = wrapper.find(`.${getStyles().ViewingLayerInactive}`); + expect(leftBox.length).toBe(1); + const width = Number(leftBox.prop('width').slice(0, -1)); + const x = leftBox.prop('x'); + expect(Math.round(width)).toBe(20); + expect(x).toBe(0); + }); + + it('renders a filtering box if rightBound exists', () => { + const _props = { ...props, viewRange: getViewRange(0, 0.8) }; + wrapper = shallow(); + + const rightBox = wrapper.find(`.${getStyles().ViewingLayerInactive}`); + expect(rightBox.length).toBe(1); + const width = Number(rightBox.prop('width').slice(0, -1)); + const x = Number(rightBox.prop('x').slice(0, -1)); + expect(Math.round(width)).toBe(20); + expect(x).toBe(80); + }); + + it('renders handles for the timeRangeFilter', () => { + const [viewStart, viewEnd] = props.viewRange.time.current; + let scrubber = ; + expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy(); + scrubber = ; + expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy(); + }); +}); diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx new file mode 100644 index 00000000000..b450bda63af --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx @@ -0,0 +1,408 @@ +// 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 cx from 'classnames'; +import * as React from 'react'; +import { css } from 'emotion'; + +import GraphTicks from './GraphTicks'; +import Scrubber from './Scrubber'; +import { TUpdateViewRangeTimeFunction, UIButton, ViewRange, ViewRangeTimeUpdate } from '../..'; +import { TNil } from '../..'; +import DraggableManager, { DraggableBounds, DraggingUpdate, EUpdateTypes } from '../../utils/DraggableManager'; + +import { createStyle } from '../../Theme'; + +export const getStyles = createStyle(() => { + // Need this cause emotion will merge emotion generated classes into single className if used with cx from emotion + // package and the selector won't work + const ViewingLayerResetZoomHoverClassName = 'JaegerUiComponents__ViewingLayerResetZoomHoverClassName'; + const ViewingLayerResetZoom = css` + label: ViewingLayerResetZoom; + display: none; + position: absolute; + right: 1%; + top: 10%; + z-index: 1; + `; + return { + ViewingLayer: css` + label: ViewingLayer; + cursor: vertical-text; + position: relative; + z-index: 1; + &:hover > .${ViewingLayerResetZoomHoverClassName} { + display: unset; + } + `, + ViewingLayerGraph: css` + label: ViewingLayerGraph; + border: 1px solid #999; + /* need !important here to overcome something from semantic UI */ + overflow: visible !important; + position: relative; + transform-origin: 0 0; + width: 100%; + `, + ViewingLayerInactive: css` + label: ViewingLayerInactive; + fill: rgba(214, 214, 214, 0.5); + `, + ViewingLayerCursorGuide: css` + label: ViewingLayerCursorGuide; + stroke: #f44; + stroke-width: 1; + `, + ViewingLayerDraggedShift: css` + label: ViewingLayerDraggedShift; + fill-opacity: 0.2; + `, + ViewingLayerDrag: css` + label: ViewingLayerDrag; + fill: #44f; + `, + ViewingLayerFullOverlay: css` + label: ViewingLayerFullOverlay; + bottom: 0; + cursor: col-resize; + left: 0; + position: fixed; + right: 0; + top: 0; + user-select: none; + `, + ViewingLayerResetZoom, + ViewingLayerResetZoomHoverClassName, + }; +}); + +type ViewingLayerProps = { + height: number; + numTicks: number; + updateViewRangeTime: TUpdateViewRangeTimeFunction; + updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void; + viewRange: ViewRange; +}; + +type ViewingLayerState = { + /** + * Cursor line should not be drawn when the mouse is over the scrubber handle. + */ + preventCursorLine: boolean; +}; + +/** + * Designate the tags for the different dragging managers. Exported for tests. + */ +export const dragTypes = { + /** + * Tag for dragging the right scrubber, e.g. end of the current view range. + */ + SHIFT_END: 'SHIFT_END', + /** + * Tag for dragging the left scrubber, e.g. start of the current view range. + */ + SHIFT_START: 'SHIFT_START', + /** + * Tag for dragging a new view range. + */ + REFRAME: 'REFRAME', +}; + +/** + * Returns the layout information for drawing the view-range differential, e.g. + * show what will change when the mouse is released. Basically, this is the + * difference from the start of the drag to the current position. + * + * @returns {{ x: string, width: string, leadginX: string }} + */ +function getNextViewLayout(start: number, position: number) { + const [left, right] = start < position ? [start, position] : [position, start]; + return { + x: `${left * 100}%`, + width: `${(right - left) * 100}%`, + leadingX: `${position * 100}%`, + }; +} + +/** + * `ViewingLayer` is rendered on top of the Canvas rendering of the minimap and + * handles showing the current view range and handles mouse UX for modifying it. + */ +export default class ViewingLayer extends React.PureComponent { + state: ViewingLayerState; + + _root: Element | TNil; + + /** + * `_draggerReframe` handles clicking and dragging on the `ViewingLayer` to + * redefined the view range. + */ + _draggerReframe: DraggableManager; + + /** + * `_draggerStart` handles dragging the left scrubber to adjust the start of + * the view range. + */ + _draggerStart: DraggableManager; + + /** + * `_draggerEnd` handles dragging the right scrubber to adjust the end of + * the view range. + */ + _draggerEnd: DraggableManager; + + constructor(props: ViewingLayerProps) { + super(props); + + this._draggerReframe = new DraggableManager({ + getBounds: this._getDraggingBounds, + onDragEnd: this._handleReframeDragEnd, + onDragMove: this._handleReframeDragUpdate, + onDragStart: this._handleReframeDragUpdate, + onMouseMove: this._handleReframeMouseMove, + onMouseLeave: this._handleReframeMouseLeave, + tag: dragTypes.REFRAME, + }); + + this._draggerStart = new DraggableManager({ + getBounds: this._getDraggingBounds, + onDragEnd: this._handleScrubberDragEnd, + onDragMove: this._handleScrubberDragUpdate, + onDragStart: this._handleScrubberDragUpdate, + onMouseEnter: this._handleScrubberEnterLeave, + onMouseLeave: this._handleScrubberEnterLeave, + tag: dragTypes.SHIFT_START, + }); + + this._draggerEnd = new DraggableManager({ + getBounds: this._getDraggingBounds, + onDragEnd: this._handleScrubberDragEnd, + onDragMove: this._handleScrubberDragUpdate, + onDragStart: this._handleScrubberDragUpdate, + onMouseEnter: this._handleScrubberEnterLeave, + onMouseLeave: this._handleScrubberEnterLeave, + tag: dragTypes.SHIFT_END, + }); + + this._root = undefined; + this.state = { + preventCursorLine: false, + }; + } + + componentWillUnmount() { + this._draggerReframe.dispose(); + this._draggerEnd.dispose(); + this._draggerStart.dispose(); + } + + _setRoot = (elm: SVGElement | TNil) => { + this._root = elm; + }; + + _getDraggingBounds = (tag: string | TNil): DraggableBounds => { + if (!this._root) { + throw new Error('invalid state'); + } + const { left: clientXLeft, width } = this._root.getBoundingClientRect(); + const [viewStart, viewEnd] = this.props.viewRange.time.current; + let maxValue = 1; + let minValue = 0; + if (tag === dragTypes.SHIFT_START) { + maxValue = viewEnd; + } else if (tag === dragTypes.SHIFT_END) { + minValue = viewStart; + } + return { clientXLeft, maxValue, minValue, width }; + }; + + _handleReframeMouseMove = ({ value }: DraggingUpdate) => { + this.props.updateNextViewRangeTime({ cursor: value }); + }; + + _handleReframeMouseLeave = () => { + this.props.updateNextViewRangeTime({ cursor: null }); + }; + + _handleReframeDragUpdate = ({ value }: DraggingUpdate) => { + const shift = value; + const { time } = this.props.viewRange; + const anchor = time.reframe ? time.reframe.anchor : shift; + const update = { reframe: { anchor, shift } }; + this.props.updateNextViewRangeTime(update); + }; + + _handleReframeDragEnd = ({ manager, value }: DraggingUpdate) => { + const { time } = this.props.viewRange; + const anchor = time.reframe ? time.reframe.anchor : value; + const [start, end] = value < anchor ? [value, anchor] : [anchor, value]; + manager.resetBounds(); + this.props.updateViewRangeTime(start, end, 'minimap'); + }; + + _handleScrubberEnterLeave = ({ type }: DraggingUpdate) => { + const preventCursorLine = type === EUpdateTypes.MouseEnter; + this.setState({ preventCursorLine }); + }; + + _handleScrubberDragUpdate = ({ event, tag, type, value }: DraggingUpdate) => { + if (type === EUpdateTypes.DragStart) { + event.stopPropagation(); + } + if (tag === dragTypes.SHIFT_START) { + this.props.updateNextViewRangeTime({ shiftStart: value }); + } else if (tag === dragTypes.SHIFT_END) { + this.props.updateNextViewRangeTime({ shiftEnd: value }); + } + }; + + _handleScrubberDragEnd = ({ manager, tag, value }: DraggingUpdate) => { + const [viewStart, viewEnd] = this.props.viewRange.time.current; + let update: [number, number]; + if (tag === dragTypes.SHIFT_START) { + update = [value, viewEnd]; + } else if (tag === dragTypes.SHIFT_END) { + update = [viewStart, value]; + } else { + // to satisfy flow + throw new Error('bad state'); + } + manager.resetBounds(); + this.setState({ preventCursorLine: false }); + this.props.updateViewRangeTime(update[0], update[1], 'minimap'); + }; + + /** + * Resets the zoom to fully zoomed out. + */ + _resetTimeZoomClickHandler = () => { + this.props.updateViewRangeTime(0, 1); + }; + + /** + * Renders the difference between where the drag started and the current + * position, e.g. the red or blue highlight. + * + * @returns React.Node[] + */ + _getMarkers(from: number, to: number) { + const styles = getStyles(); + const layout = getNextViewLayout(from, to); + return [ + , + , + ]; + } + + render() { + const { height, viewRange, numTicks } = this.props; + const { preventCursorLine } = this.state; + const { current, cursor, shiftStart, shiftEnd, reframe } = viewRange.time; + const haveNextTimeRange = shiftStart != null || shiftEnd != null || reframe != null; + const [viewStart, viewEnd] = current; + let leftInactive = 0; + if (viewStart) { + leftInactive = viewStart * 100; + } + let rightInactive = 100; + if (viewEnd) { + rightInactive = 100 - viewEnd * 100; + } + let cursorPosition: string | undefined; + if (!haveNextTimeRange && cursor != null && !preventCursorLine) { + cursorPosition = `${cursor * 100}%`; + } + const styles = getStyles(); + + return ( +
+ {(viewStart !== 0 || viewEnd !== 1) && ( + + Reset Selection + + )} + + {leftInactive > 0 && ( + + )} + {rightInactive > 0 && ( + + )} + + {cursorPosition && ( + + )} + {shiftStart != null && this._getMarkers(viewStart, shiftStart)} + {shiftEnd != null && this._getMarkers(viewEnd, shiftEnd)} + + + {reframe != null && this._getMarkers(reframe.anchor, reframe.shift)} + + {/* fullOverlay updates the mouse cursor blocks mouse events */} + {haveNextTimeRange &&
} +
+ ); + } +} diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.js b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.js new file mode 100644 index 00000000000..8fee2747036 --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.test.js @@ -0,0 +1,76 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { shallow } from 'enzyme'; + +import CanvasSpanGraph from './CanvasSpanGraph'; +import SpanGraph from './index'; +import TickLabels from './TickLabels'; +import ViewingLayer from './ViewingLayer'; +import traceGenerator from '../../demo/trace-generators'; +import transformTraceData from '../../model/transform-trace-data'; +import { polyfill as polyfillAnimationFrame } from '../../utils/test/requestAnimationFrame'; + +describe('', () => { + polyfillAnimationFrame(window); + + const trace = transformTraceData(traceGenerator.trace({})); + const props = { + trace, + updateViewRangeTime: () => {}, + viewRange: { + time: { + current: [0, 1], + }, + }, + }; + + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('renders a ', () => { + expect(wrapper.find(CanvasSpanGraph).length).toBe(1); + }); + + it('renders a ', () => { + expect(wrapper.find(TickLabels).length).toBe(1); + }); + + it('returns a
if a trace is not provided', () => { + wrapper = shallow(); + expect(wrapper.matchesElement(
)).toBeTruthy(); + }); + + it('passes the number of ticks to render to components', () => { + const tickHeader = wrapper.find(TickLabels); + const viewingLayer = wrapper.find(ViewingLayer); + expect(tickHeader.prop('numTicks')).toBeGreaterThan(1); + expect(viewingLayer.prop('numTicks')).toBeGreaterThan(1); + expect(tickHeader.prop('numTicks')).toBe(viewingLayer.prop('numTicks')); + }); + + it('passes items to CanvasSpanGraph', () => { + const canvasGraph = wrapper.find(CanvasSpanGraph).first(); + const items = trace.spans.map(span => ({ + valueOffset: span.relativeStartTime, + valueWidth: span.duration, + serviceName: span.process.serviceName, + })); + expect(canvasGraph.prop('items')).toEqual(items); + }); +}); diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.tsx b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.tsx new file mode 100644 index 00000000000..93e3037a3b9 --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/index.tsx @@ -0,0 +1,102 @@ +// 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 * as React from 'react'; +import cx from 'classnames'; + +import CanvasSpanGraph from './CanvasSpanGraph'; +import TickLabels from './TickLabels'; +import ViewingLayer from './ViewingLayer'; +import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from '../..'; +import { Span, Trace } from '../..'; +import { ubPb2, ubPx2, ubRelative } from '../../uberUtilityStyles'; + +const DEFAULT_HEIGHT = 60; +const TIMELINE_TICK_INTERVAL = 4; + +type SpanGraphProps = { + height?: number; + trace: Trace; + viewRange: ViewRange; + updateViewRangeTime: TUpdateViewRangeTimeFunction; + updateNextViewRangeTime: (nextUpdate: ViewRangeTimeUpdate) => void; +}; + +/** + * Store `items` in state so they are not regenerated every render. Otherwise, + * the canvas graph will re-render itself every time. + */ +type SpanGraphState = { + items: Array<{ + valueOffset: number; + valueWidth: number; + serviceName: string; + }>; +}; + +function getItem(span: Span) { + return { + valueOffset: span.relativeStartTime, + valueWidth: span.duration, + serviceName: span.process.serviceName, + }; +} + +export default class SpanGraph extends React.PureComponent { + state: SpanGraphState; + + static defaultProps = { + height: DEFAULT_HEIGHT, + }; + + constructor(props: SpanGraphProps) { + super(props); + const { trace } = props; + this.state = { + items: trace ? trace.spans.map(getItem) : [], + }; + } + + componentWillReceiveProps(nextProps: SpanGraphProps) { + const { trace } = nextProps; + if (this.props.trace !== trace) { + this.setState({ + items: trace ? trace.spans.map(getItem) : [], + }); + } + } + + render() { + const { height, trace, viewRange, updateNextViewRangeTime, updateViewRangeTime } = this.props; + if (!trace) { + return
; + } + const { items } = this.state; + return ( +
+ +
+ + +
+
+ ); + } +} diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.test.js b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.test.js new file mode 100644 index 00000000000..bc56e6ef3cd --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.test.js @@ -0,0 +1,199 @@ +// 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 _range from 'lodash/range'; + +import renderIntoCanvas, { + BG_COLOR, + ITEM_ALPHA, + MIN_ITEM_HEIGHT, + MAX_TOTAL_HEIGHT, + MIN_ITEM_WIDTH, + MIN_TOTAL_HEIGHT, + MAX_ITEM_HEIGHT, +} from './render-into-canvas'; + +const getCanvasWidth = () => window.innerWidth * 2; +const getBgFillRect = items => ({ + fillStyle: BG_COLOR, + height: + !items || items.length < MIN_TOTAL_HEIGHT ? MIN_TOTAL_HEIGHT : Math.min(MAX_TOTAL_HEIGHT, items.length), + width: getCanvasWidth(), + x: 0, + y: 0, +}); + +describe('renderIntoCanvas()', () => { + const basicItem = { valueWidth: 100, valueOffset: 50, serviceName: 'some-name' }; + + class CanvasContext { + constructor() { + this.fillStyle = undefined; + this.fillRectAccumulator = []; + } + + fillRect(x, y, width, height) { + const fillStyle = this.fillStyle; + this.fillRectAccumulator.push({ + fillStyle, + height, + width, + x, + y, + }); + } + } + + class Canvas { + constructor() { + this.contexts = []; + this.height = NaN; + this.width = NaN; + this.getContext = jest.fn(this._getContext.bind(this)); + } + + _getContext() { + const ctx = new CanvasContext(); + this.contexts.push(ctx); + return ctx; + } + } + + function getColorFactory() { + let i = 0; + const inputOutput = []; + function getFakeColor(str) { + const rv = [i, i, i]; + i++; + inputOutput.push({ + input: str, + output: rv.slice(), + }); + return rv; + } + getFakeColor.inputOutput = inputOutput; + return getFakeColor; + } + + it('sets the width', () => { + const canvas = new Canvas(); + expect(canvas.width !== canvas.width).toBe(true); + renderIntoCanvas(canvas, [basicItem], 150, getColorFactory()); + expect(canvas.width).toBe(getCanvasWidth()); + }); + + describe('when there are limited number of items', () => { + it('sets the height', () => { + const canvas = new Canvas(); + expect(canvas.height !== canvas.height).toBe(true); + renderIntoCanvas(canvas, [basicItem], 150, getColorFactory()); + expect(canvas.height).toBe(MIN_TOTAL_HEIGHT); + }); + + it('draws the background', () => { + const expectedDrawing = [getBgFillRect()]; + const canvas = new Canvas(); + const items = []; + const totalValueWidth = 4000; + const getFillColor = getColorFactory(); + renderIntoCanvas(canvas, items, totalValueWidth, getFillColor); + expect(canvas.getContext.mock.calls).toEqual([['2d', { alpha: false }]]); + expect(canvas.contexts.length).toBe(1); + expect(canvas.contexts[0].fillRectAccumulator).toEqual(expectedDrawing); + }); + + it('draws the map', () => { + const totalValueWidth = 4000; + const items = [ + { valueWidth: 50, valueOffset: 50, serviceName: 'service-name-0' }, + { valueWidth: 100, valueOffset: 100, serviceName: 'service-name-1' }, + { valueWidth: 150, valueOffset: 150, serviceName: 'service-name-2' }, + ]; + const expectedColors = [ + { input: items[0].serviceName, output: [0, 0, 0] }, + { input: items[1].serviceName, output: [1, 1, 1] }, + { input: items[2].serviceName, output: [2, 2, 2] }, + ]; + const cHeight = + items.length < MIN_TOTAL_HEIGHT ? MIN_TOTAL_HEIGHT : Math.min(items.length, MAX_TOTAL_HEIGHT); + + const expectedDrawings = [ + getBgFillRect(), + ...items.map((item, i) => { + const { valueWidth, valueOffset } = item; + const color = expectedColors[i].output; + const fillStyle = `rgba(${color.concat(ITEM_ALPHA).join()})`; + const height = Math.min(MAX_ITEM_HEIGHT, Math.max(MIN_ITEM_HEIGHT, cHeight / items.length)); + const width = (valueWidth / totalValueWidth) * getCanvasWidth(); + const x = (valueOffset / totalValueWidth) * getCanvasWidth(); + const y = (cHeight / items.length) * i; + return { fillStyle, height, width, x, y }; + }), + ]; + const canvas = new Canvas(); + const getFillColor = getColorFactory(); + renderIntoCanvas(canvas, items, totalValueWidth, getFillColor); + expect(getFillColor.inputOutput).toEqual(expectedColors); + expect(canvas.getContext.mock.calls).toEqual([['2d', { alpha: false }]]); + expect(canvas.contexts.length).toBe(1); + expect(canvas.contexts[0].fillRectAccumulator).toEqual(expectedDrawings); + }); + }); + + describe('when there are many items', () => { + it('sets the height when there are many items', () => { + const canvas = new Canvas(); + const items = []; + for (let i = 0; i < MIN_TOTAL_HEIGHT + 1; i++) { + items.push(basicItem); + } + expect(canvas.height !== canvas.height).toBe(true); + renderIntoCanvas(canvas, items, 150, getColorFactory()); + expect(canvas.height).toBe(items.length); + }); + + it('draws the map', () => { + const totalValueWidth = 4000; + const items = _range(MIN_TOTAL_HEIGHT * 10).map(i => ({ + valueWidth: i, + valueOffset: i, + serviceName: `service-name-${i}`, + })); + const expectedColors = items.map((item, i) => ({ + input: item.serviceName, + output: [i, i, i], + })); + const expectedDrawings = [ + getBgFillRect(items), + ...items.map((item, i) => { + const { valueWidth, valueOffset } = item; + const color = expectedColors[i].output; + const fillStyle = `rgba(${color.concat(ITEM_ALPHA).join()})`; + const height = MIN_ITEM_HEIGHT; + const width = Math.max(MIN_ITEM_WIDTH, (valueWidth / totalValueWidth) * getCanvasWidth()); + const x = (valueOffset / totalValueWidth) * getCanvasWidth(); + const y = (MAX_TOTAL_HEIGHT / items.length) * i; + return { fillStyle, height, width, x, y }; + }), + ]; + const canvas = new Canvas(); + const getFillColor = getColorFactory(); + renderIntoCanvas(canvas, items, totalValueWidth, getFillColor); + expect(getFillColor.inputOutput).toEqual(expectedColors); + expect(canvas.getContext.mock.calls).toEqual([['2d', { alpha: false }]]); + expect(canvas.contexts.length).toBe(1); + expect(canvas.contexts[0].fillRectAccumulator).toEqual(expectedDrawings); + }); + }); +}); diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.tsx b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.tsx new file mode 100644 index 00000000000..1ade5b33cdd --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/render-into-canvas.tsx @@ -0,0 +1,62 @@ +// 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 { TNil } from '../..'; + +// exported for tests +export const BG_COLOR = '#fff'; +export const ITEM_ALPHA = 0.8; +export const MIN_ITEM_HEIGHT = 2; +export const MAX_TOTAL_HEIGHT = 200; +export const MIN_ITEM_WIDTH = 10; +export const MIN_TOTAL_HEIGHT = 60; +export const MAX_ITEM_HEIGHT = 6; + +export default function renderIntoCanvas( + canvas: HTMLCanvasElement, + items: Array<{ valueWidth: number; valueOffset: number; serviceName: string }>, + totalValueWidth: number, + getFillColor: (serviceName: string) => [number, number, number] +) { + const fillCache: Map = new Map(); + const cHeight = items.length < MIN_TOTAL_HEIGHT ? MIN_TOTAL_HEIGHT : Math.min(items.length, MAX_TOTAL_HEIGHT); + const cWidth = window.innerWidth * 2; + // eslint-disable-next-line no-param-reassign + canvas.width = cWidth; + // eslint-disable-next-line no-param-reassign + canvas.height = cHeight; + const itemHeight = Math.min(MAX_ITEM_HEIGHT, Math.max(MIN_ITEM_HEIGHT, cHeight / items.length)); + const itemYChange = cHeight / items.length; + + const ctx = canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D; + ctx.fillStyle = BG_COLOR; + ctx.fillRect(0, 0, cWidth, cHeight); + for (let i = 0; i < items.length; i++) { + const { valueWidth, valueOffset, serviceName } = items[i]; + const x = (valueOffset / totalValueWidth) * cWidth; + let width = (valueWidth / totalValueWidth) * cWidth; + if (width < MIN_ITEM_WIDTH) { + width = MIN_ITEM_WIDTH; + } + let fillStyle = fillCache.get(serviceName); + if (!fillStyle) { + fillStyle = `rgba(${getFillColor(serviceName) + .concat(ITEM_ALPHA) + .join()})`; + fillCache.set(serviceName, fillStyle); + } + ctx.fillStyle = fillStyle; + ctx.fillRect(x, i * itemYChange, width, itemHeight); + } +} diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.test.js b/packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.test.js new file mode 100644 index 00000000000..d4ba4132d82 --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.test.js @@ -0,0 +1,80 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { shallow, mount } from 'enzyme'; + +import SpanGraph from './SpanGraph'; +import TracePageHeader, { HEADER_ITEMS } from './TracePageHeader'; +import LabeledList from '../common/LabeledList'; +import traceGenerator from '../demo/trace-generators'; +import { getTraceName } from '../model/trace-viewer'; +import transformTraceData from '../model/transform-trace-data'; + +describe('', () => { + const trace = transformTraceData(traceGenerator.trace({})); + const defaultProps = { + trace, + showArchiveButton: false, + showShortcutsHelp: false, + showStandaloneLink: false, + showViewOptions: false, + textFilter: '', + updateTextFilter: () => {}, + }; + + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('renders a
', () => { + expect(wrapper.find('header').length).toBe(1); + }); + + it('renders an empty
if a trace is not present', () => { + wrapper = mount(); + expect(wrapper.children().length).toBe(0); + }); + + it('renders the trace title', () => { + expect(wrapper.find({ traceName: getTraceName(trace.spans) })).toBeTruthy(); + }); + + it('renders the header items', () => { + wrapper.find('.horizontal .item').forEach((item, i) => { + expect(item.contains(HEADER_ITEMS[i].title)).toBeTruthy(); + expect(item.contains(HEADER_ITEMS[i].renderer(defaultProps.trace))).toBeTruthy(); + }); + }); + + it('renders a ', () => { + expect(wrapper.find(SpanGraph).length).toBe(1); + }); + + describe('observes the visibility toggles for various UX elements', () => { + it('hides the minimap when hideMap === true', () => { + expect(wrapper.find(SpanGraph).length).toBe(1); + wrapper.setProps({ hideMap: true }); + expect(wrapper.find(SpanGraph).length).toBe(0); + }); + + it('hides the summary when hideSummary === true', () => { + expect(wrapper.find(LabeledList).length).toBe(1); + wrapper.setProps({ hideSummary: true }); + expect(wrapper.find(LabeledList).length).toBe(0); + }); + }); +}); diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.tsx b/packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.tsx new file mode 100644 index 00000000000..a3f5d2e6bbd --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.tsx @@ -0,0 +1,293 @@ +// 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 * as React from 'react'; +import _get from 'lodash/get'; +import _maxBy from 'lodash/maxBy'; +import _values from 'lodash/values'; +import MdKeyboardArrowRight from 'react-icons/lib/md/keyboard-arrow-right'; +import { css } from 'emotion'; +import cx from 'classnames'; + +import SpanGraph from './SpanGraph'; +import TracePageSearchBar from './TracePageSearchBar'; +import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from '..'; +import LabeledList from '../common/LabeledList'; +import TraceName from '../common/TraceName'; +import { getTraceName } from '../model/trace-viewer'; +import { TNil } from '../types'; +import { Trace } from '..'; +import { formatDatetime, formatDuration } from '../utils/date'; +import { getTraceLinks } from '../model/link-patterns'; + +import ExternalLinks from '../common/ExternalLinks'; +import { createStyle } from '../Theme'; +import { uTxMuted } from '../uberUtilityStyles'; + +const getStyles = createStyle(() => { + const TracePageHeaderOverviewItemValueDetail = css` + label: TracePageHeaderOverviewItemValueDetail; + color: #aaa; + `; + return { + TracePageHeader: css` + label: TracePageHeader; + & > :first-child { + border-bottom: 1px solid #e8e8e8; + } + & > :nth-child(2) { + background-color: #eee; + border-bottom: 1px solid #e4e4e4; + } + & > :last-child { + background-color: #f8f8f8; + border-bottom: 1px solid #ccc; + } + `, + TracePageHeaderTitleRow: css` + label: TracePageHeaderTitleRow; + align-items: center; + background-color: #fff; + display: flex; + `, + TracePageHeaderBack: css` + label: TracePageHeaderBack; + align-items: center; + align-self: stretch; + background-color: #fafafa; + border-bottom: 1px solid #ddd; + border-right: 1px solid #ddd; + color: inherit; + display: flex; + font-size: 1.4rem; + padding: 0 1rem; + margin-bottom: -1px; + &:hover { + background-color: #f0f0f0; + border-color: #ccc; + } + `, + TracePageHeaderTitleLink: css` + label: TracePageHeaderTitleLink; + align-items: center; + color: rgba(0, 0, 0, 0.85); + display: flex; + flex: 1; + + &:hover * { + text-decoration: underline; + } + &:hover > *, + &:hover small { + text-decoration: none; + } + `, + TracePageHeaderDetailToggle: css` + label: TracePageHeaderDetailToggle; + font-size: 2.5rem; + transition: transform 0.07s ease-out; + `, + TracePageHeaderDetailToggleExpanded: css` + label: TracePageHeaderDetailToggleExpanded; + transform: rotate(90deg); + `, + TracePageHeaderTitle: css` + label: TracePageHeaderTitle; + color: inherit; + flex: 1; + font-size: 1.7em; + line-height: 1em; + margin: 0 0 0 0.5em; + padding: 0.5em 0; + `, + TracePageHeaderTitleCollapsible: css` + label: TracePageHeaderTitleCollapsible; + margin-left: 0; + `, + TracePageHeaderOverviewItems: css` + label: TracePageHeaderOverviewItems; + border-bottom: 1px solid #e4e4e4; + padding: 0.25rem 0.5rem; + `, + TracePageHeaderOverviewItemValueDetail, + TracePageHeaderOverviewItemValue: css` + label: TracePageHeaderOverviewItemValue; + &:hover > .${TracePageHeaderOverviewItemValueDetail} { + color: unset; + } + `, + TracePageHeaderArchiveIcon: css` + label: TracePageHeaderArchiveIcon; + font-size: 1.78em; + margin-right: 0.15em; + `, + }; +}); + +type TracePageHeaderEmbedProps = { + canCollapse: boolean; + clearSearch: () => void; + focusUiFindMatches: () => void; + hideMap: boolean; + hideSummary: boolean; + nextResult: () => void; + onSlimViewClicked: () => void; + onTraceGraphViewClicked: () => void; + prevResult: () => void; + resultCount: number; + slimView: boolean; + textFilter: string | TNil; + trace: Trace; + traceGraphView: boolean; + updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void; + updateViewRangeTime: TUpdateViewRangeTimeFunction; + viewRange: ViewRange; + searchValue: string; + onSearchValueChange: (value: string) => void; + hideSearchButtons?: boolean; +}; + +export const HEADER_ITEMS = [ + { + key: 'timestamp', + label: 'Trace Start', + renderer: (trace: Trace) => { + const styles = getStyles(); + const dateStr = formatDatetime(trace.startTime); + const match = dateStr.match(/^(.+)(:\d\d\.\d+)$/); + return match ? ( + + {match[1]} + {match[2]} + + ) : ( + dateStr + ); + }, + }, + { + key: 'duration', + label: 'Duration', + renderer: (trace: Trace) => formatDuration(trace.duration), + }, + { + key: 'service-count', + label: 'Services', + renderer: (trace: Trace) => new Set(_values(trace.processes).map(p => p.serviceName)).size, + }, + { + key: 'depth', + label: 'Depth', + renderer: (trace: Trace) => _get(_maxBy(trace.spans, 'depth'), 'depth', 0) + 1, + }, + { + key: 'span-count', + label: 'Total Spans', + renderer: (trace: Trace) => trace.spans.length, + }, +]; + +export default function TracePageHeader(props: TracePageHeaderEmbedProps) { + const { + canCollapse, + clearSearch, + focusUiFindMatches, + hideMap, + hideSummary, + nextResult, + onSlimViewClicked, + prevResult, + resultCount, + slimView, + textFilter, + trace, + traceGraphView, + updateNextViewRangeTime, + updateViewRangeTime, + viewRange, + searchValue, + onSearchValueChange, + hideSearchButtons, + } = props; + + if (!trace) { + return null; + } + + const links = getTraceLinks(trace); + + const summaryItems = + !hideSummary && + !slimView && + HEADER_ITEMS.map(item => { + const { renderer, ...rest } = item; + return { ...rest, value: renderer(trace) }; + }); + + const styles = getStyles(); + + const title = ( +

+ {' '} + {trace.traceID.slice(0, 7)} +

+ ); + + return ( +
+
+ {links && links.length > 0 && } + {canCollapse ? ( + + + {title} + + ) : ( + title + )} + +
+ {summaryItems && } + {!hideMap && !slimView && ( + + )} +
+ ); +} diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.markers.tsx b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.markers.tsx new file mode 100644 index 00000000000..908f49bd026 --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.markers.tsx @@ -0,0 +1,15 @@ +// 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. + +export const IN_TRACE_SEARCH = 'in-trace-search'; diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js new file mode 100644 index 00000000000..92f80028e98 --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js @@ -0,0 +1,92 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { shallow } from 'enzyme'; + +import * as markers from './TracePageSearchBar.markers'; +import TracePageSearchBar, { getStyles } from './TracePageSearchBar'; +import UiFindInput from '../common/UiFindInput'; + +const defaultProps = { + forwardedRef: React.createRef(), + navigable: true, + nextResult: () => {}, + prevResult: () => {}, + resultCount: 0, + textFilter: 'something', +}; + +describe('', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + 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, + name: 'search', + }) + ); + expect(suffix.hasClass(getStyles().TracePageSearchBarCount)).toBe(true); + expect(suffix.text()).toBe(String(defaultProps.resultCount)); + }); + + it('renders buttons', () => { + const buttons = wrapper.find('UIButton'); + expect(buttons.length).toBe(4); + buttons.forEach(button => { + expect(button.hasClass(getStyles().TracePageSearchBarBtn)).toBe(true); + expect(button.hasClass(getStyles().TracePageSearchBarBtnDisabled)).toBe(false); + expect(button.prop('disabled')).toBe(false); + }); + expect(wrapper.find('UIButton[icon="up"]').prop('onClick')).toBe(defaultProps.prevResult); + expect(wrapper.find('UIButton[icon="down"]').prop('onClick')).toBe(defaultProps.nextResult); + expect(wrapper.find('UIButton[icon="close"]').prop('onClick')).toBe(defaultProps.clearSearch); + }); + + it('hides navigation buttons when not navigable', () => { + wrapper.setProps({ navigable: false }); + const button = wrapper.find('UIButton'); + expect(button.length).toBe(1); + expect(button.prop('icon')).toBe('close'); + }); + }); + + 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('UIButton'); + expect(buttons.length).toBe(4); + buttons.forEach(button => { + expect(button.hasClass(getStyles().TracePageSearchBarBtn)).toBe(true); + expect(button.hasClass(getStyles().TracePageSearchBarBtnDisabled)).toBe(true); + expect(button.prop('disabled')).toBe(true); + }); + }); + }); +}); diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx new file mode 100644 index 00000000000..22d77ef7c67 --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx @@ -0,0 +1,144 @@ +// 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 * as React from 'react'; +import cx from 'classnames'; +import IoAndroidLocate from 'react-icons/lib/io/android-locate'; +import { css } from 'emotion'; + +import * as markers from './TracePageSearchBar.markers'; +import UiFindInput from '../common/UiFindInput'; +import { TNil } from '../types'; + +import { UIButton, UIInputGroup } from '../uiElementsContext'; +import { createStyle } from '../Theme'; +import { ubFlexAuto, ubJustifyEnd } from '../uberUtilityStyles'; + +export const getStyles = createStyle(() => { + return { + TracePageSearchBar: css` + label: TracePageSearchBar; + `, + TracePageSearchBarBar: css` + label: TracePageSearchBarBar; + max-width: 20rem; + transition: max-width 0.5s; + &:focus-within { + max-width: 100%; + } + `, + TracePageSearchBarCount: css` + label: TracePageSearchBarCount; + opacity: 0.6; + `, + TracePageSearchBarBtn: css` + label: TracePageSearchBarBtn; + border-left: none; + transition: 0.2s; + `, + TracePageSearchBarBtnDisabled: css` + label: TracePageSearchBarBtnDisabled; + opacity: 0.5; + `, + TracePageSearchBarLocateBtn: css` + label: TracePageSearchBarLocateBtn; + padding: 1px 8px 4px; + `, + }; +}); + +type TracePageSearchBarProps = { + textFilter: string | TNil; + prevResult: () => void; + nextResult: () => void; + clearSearch: () => void; + focusUiFindMatches: () => void; + resultCount: number; + navigable: boolean; + searchValue: string; + onSearchValueChange: (value: string) => void; + hideSearchButtons?: boolean; +}; + +export default function TracePageSearchBar(props: TracePageSearchBarProps) { + const { + clearSearch, + focusUiFindMatches, + navigable, + nextResult, + prevResult, + resultCount, + textFilter, + onSearchValueChange, + searchValue, + hideSearchButtons, + } = props; + const styles = getStyles(); + + const count = textFilter ? {resultCount} : null; + + const btnClass = cx(styles.TracePageSearchBarBtn, { [styles.TracePageSearchBarBtnDisabled]: !textFilter }); + const uiFindInputInputProps = { + 'data-test': markers.IN_TRACE_SEARCH, + className: cx(styles.TracePageSearchBarBar, ubFlexAuto), + name: 'search', + suffix: count, + }; + + return ( +
+ {/* style inline because compact overwrites the display */} + + + {!hideSearchButtons && ( + <> + {navigable && ( + <> + + + + + + + )} + + + )} + +
+ ); +} diff --git a/packages/jaeger-ui-components/src/TracePageHeader/index.tsx b/packages/jaeger-ui-components/src/TracePageHeader/index.tsx new file mode 100644 index 00000000000..8d42d318acc --- /dev/null +++ b/packages/jaeger-ui-components/src/TracePageHeader/index.tsx @@ -0,0 +1,15 @@ +// 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. + +export { default } from './TracePageHeader'; diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx index f536c8a8d3d..2d431542b76 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx @@ -101,6 +101,7 @@ type TimelineColumnResizerProps = { max: number; onChange: (newSize: number) => void; position: number; + columnResizeHandleHeight: number; }; type TimelineColumnResizerState = { @@ -164,8 +165,8 @@ export default class TimelineColumnResizer extends React.PureComponent< render() { let left; - let draggerStyle; - const { position } = this.props; + let draggerStyle: React.CSSProperties; + const { position, columnResizeHandleHeight } = this.props; const { dragPosition } = this.state; left = `${position * 100}%`; const gripStyle = { left }; @@ -188,6 +189,7 @@ export default class TimelineColumnResizer extends React.PureComponent< } else { draggerStyle = gripStyle; } + draggerStyle.height = columnResizeHandleHeight; const isDragging = isDraggingLeft || isDraggingRight; return ( diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.tsx index eac3bf7a5cb..add9e46d395 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.tsx @@ -34,6 +34,7 @@ const getStyles = createStyle(() => { line-height: 38px; width: 100%; z-index: 4; + position: relative; `, title: css` flex: 1; @@ -57,6 +58,7 @@ type TimelineHeaderRowProps = { updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void; updateViewRangeTime: TUpdateViewRangeTimeFunction; viewRangeTime: ViewRangeTime; + columnResizeHandleHeight: number; }; export default function TimelineHeaderRow(props: TimelineHeaderRowProps) { @@ -72,6 +74,7 @@ export default function TimelineHeaderRow(props: TimelineHeaderRowProps) { updateViewRangeTime, updateNextViewRangeTime, viewRangeTime, + columnResizeHandleHeight, } = props; const [viewStart, viewEnd] = viewRangeTime.current; const styles = getStyles(); @@ -95,7 +98,13 @@ export default function TimelineHeaderRow(props: TimelineHeaderRowProps) { /> - + ); } diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx index 26d04ba1dee..5aef6220193 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx @@ -33,6 +33,7 @@ type TExtractUiFindFromStateReturn = { const getStyles = createStyle(() => { return { TraceTimelineViewer: css` + label: TraceTimelineViewer; border-bottom: 1px solid #bbb; & .json-markup { @@ -98,6 +99,11 @@ type TProps = TExtractUiFindFromStateReturn & { linksGetter: (span: Span, items: KeyValuePair[], itemIndex: number) => Link[]; }; +type State = { + // Will be set to real height of the component so it can be passed down to size some other elements. + height: number; +}; + const NUM_TICKS = 5; /** @@ -106,7 +112,12 @@ const NUM_TICKS = 5; * re-render the ListView every time the cursor is moved on the trace minimap * or `TimelineHeaderRow`. */ -export default class TraceTimelineViewer extends React.PureComponent { +export default class TraceTimelineViewer extends React.PureComponent { + constructor(props: TProps) { + super(props); + this.state = { height: 0 }; + } + componentDidMount() { mergeShortcuts({ collapseAll: this.collapseAll, @@ -147,7 +158,10 @@ export default class TraceTimelineViewer extends React.PureComponent { return ( -
+
ref && this.setState({ height: ref.getBoundingClientRect().height })} + > { viewRangeTime={viewRange.time} updateNextViewRangeTime={updateNextViewRangeTime} updateViewRangeTime={updateViewRangeTime} + columnResizeHandleHeight={this.state.height} /> { + return { + BreakableText: css` + label: BreakableText; + display: inline-block; + white-space: pre; + `, + }; +}); + +const WORD_RX = /\W*\w+\W*/g; + +type Props = { + text: string; + className?: string; + wordRegexp?: RegExp; +}; + +// TODO typescript doesn't understand text or null as react nodes +// https://github.com/Microsoft/TypeScript/issues/21699 +export default function BreakableText( + props: Props +): any /* React.ReactNode /* React.ReactElement | React.ReactElement[] \*\/ */ { + const { className, text, wordRegexp = WORD_RX } = props; + if (!text) { + return typeof text === 'string' ? text : null; + } + const spans = []; + wordRegexp.exec(''); + // if the given text has no words, set the first match to the entire text + let match: RegExpExecArray | string[] | null = wordRegexp.exec(text) || [text]; + while (match) { + spans.push( + + {match[0]} + + ); + match = wordRegexp.exec(text); + } + return spans; +} + +BreakableText.defaultProps = { + wordRegexp: WORD_RX, +}; diff --git a/packages/jaeger-ui-components/src/common/ExternalLinks.tsx b/packages/jaeger-ui-components/src/common/ExternalLinks.tsx new file mode 100644 index 00000000000..5c8cbea4a20 --- /dev/null +++ b/packages/jaeger-ui-components/src/common/ExternalLinks.tsx @@ -0,0 +1,59 @@ +// 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 * as React from 'react'; +import { UIDropdown, UIMenu, UIMenuItem } from '..'; +import NewWindowIcon from './NewWindowIcon'; + +type Link = { + text: string; + url: string; +}; + +type ExternalLinksProps = { + links: Link[]; + className?: string; +}; + +const LinkValue = (props: { href: string; title?: string; children?: React.ReactNode; className?: string }) => ( + + {props.children} + +); + +// export for testing +export const linkValueList = (links: Link[]) => ( + + {links.map(({ text, url }, index) => ( + // `index` is necessary in the key because url can repeat + + {text} + + ))} + +); + +export default function ExternalLinks(props: ExternalLinksProps) { + const { links } = props; + if (links.length === 1) { + return ; + } + return ( + + + + + + ); +} diff --git a/packages/jaeger-ui-components/src/common/LoadingIndicator.tsx b/packages/jaeger-ui-components/src/common/LoadingIndicator.tsx new file mode 100644 index 00000000000..147fae32b90 --- /dev/null +++ b/packages/jaeger-ui-components/src/common/LoadingIndicator.tsx @@ -0,0 +1,78 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import cx from 'classnames'; +import { css, keyframes } from 'emotion'; + +import { createStyle } from '../Theme'; +import { UIIcon } from '../uiElementsContext'; + +const getStyles = createStyle(() => { + const LoadingIndicatorColorAnim = keyframes` + /* + rgb(0, 128, 128) == teal + rgba(0, 128, 128, 0.3) == #bedfdf + */ + from { + color: #bedfdf; + } + to { + color: teal; + } + `; + return { + LoadingIndicator: css` + label: LoadingIndicator; + animation: ${LoadingIndicatorColorAnim} 1s infinite alternate; + font-size: 36px; + /* outline / stroke the loading indicator */ + text-shadow: -0.5px 0 rgba(0, 128, 128, 0.6), 0 0.5px rgba(0, 128, 128, 0.6), 0.5px 0 rgba(0, 128, 128, 0.6), + 0 -0.5px rgba(0, 128, 128, 0.6); + `, + LoadingIndicatorCentered: css` + label: LoadingIndicatorCentered; + display: block; + margin-left: auto; + margin-right: auto; + `, + LoadingIndicatorSmall: css` + label: LoadingIndicatorSmall; + font-size: 0.7em; + `, + }; +}); + +type LoadingIndicatorProps = { + centered?: boolean; + className?: string; + small?: boolean; +}; + +export default function LoadingIndicator(props: LoadingIndicatorProps) { + const { centered, className, small, ...rest } = props; + const styles = getStyles(); + const cls = cx(styles.LoadingIndicator, { + [styles.LoadingIndicatorCentered]: centered, + [styles.LoadingIndicatorSmall]: small, + className, + }); + return ; +} + +LoadingIndicator.defaultProps = { + centered: false, + className: undefined, + small: false, +}; diff --git a/packages/jaeger-ui-components/src/common/TraceName.tsx b/packages/jaeger-ui-components/src/common/TraceName.tsx new file mode 100644 index 00000000000..ba8918a15c2 --- /dev/null +++ b/packages/jaeger-ui-components/src/common/TraceName.tsx @@ -0,0 +1,65 @@ +// 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 * as React from 'react'; +import { css } from 'emotion'; + +import BreakableText from './BreakableText'; +import LoadingIndicator from './LoadingIndicator'; +import { fetchedState, FALLBACK_TRACE_NAME } from '../constants'; + +import { FetchedState, TNil } from '../types'; +import { ApiError } from '../types/api-error'; +import { createStyle } from '../Theme'; + +const getStyles = createStyle(() => { + return { + TraceNameError: css` + color: #c00; + `, + }; +}); + +type Props = { + className?: string; + error?: ApiError | TNil; + state?: FetchedState | TNil; + traceName?: string | TNil; +}; + +export default function TraceName(props: Props) { + const { className, error, state, traceName } = props; + const isErred = state === fetchedState.ERROR; + let title: string | React.ReactNode = traceName || FALLBACK_TRACE_NAME; + const styles = getStyles(); + let errorCssClass = ''; + if (isErred) { + errorCssClass = styles.TraceNameError; + let titleStr = ''; + if (error) { + titleStr = typeof error === 'string' ? error : error.message || String(error); + } + if (!titleStr) { + titleStr = 'Error: Unknown error'; + } + title = titleStr; + title = ; + } else if (state === fetchedState.LOADING) { + title = ; + } else { + const text = String(traceName || FALLBACK_TRACE_NAME); + title = ; + } + return {title}; +} diff --git a/packages/jaeger-ui-components/src/common/UiFindInput.test.js b/packages/jaeger-ui-components/src/common/UiFindInput.test.js new file mode 100644 index 00000000000..d3973774e47 --- /dev/null +++ b/packages/jaeger-ui-components/src/common/UiFindInput.test.js @@ -0,0 +1,65 @@ +// 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 debounceMock from 'lodash/debounce'; + +import UiFindInput from './UiFindInput'; +import {UIInput} from "../uiElementsContext"; + +jest.mock('lodash/debounce'); + +describe('UiFindInput', () => { + const flushMock = jest.fn(); + + const uiFind = 'uiFind'; + const ownInputValue = 'ownInputValue'; + const props = { + uiFind: undefined, + 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 `undefined`', () => { + wrapper.setProps({value: uiFind}); + expect(wrapper.find(UIInput).prop('value')).toBe(uiFind); + }); + }) +}); diff --git a/packages/jaeger-ui-components/src/common/UiFindInput.tsx b/packages/jaeger-ui-components/src/common/UiFindInput.tsx new file mode 100644 index 00000000000..f37c588bf7e --- /dev/null +++ b/packages/jaeger-ui-components/src/common/UiFindInput.tsx @@ -0,0 +1,66 @@ +// 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 { TNil } from '../types/index'; +import { UIIcon, UIInput } from '../uiElementsContext'; + +type Props = { + allowClear?: boolean; + inputProps: Record; + location: Location; + match: any; + trackFindFunction?: (str: string | TNil) => void; + value: string | undefined; + onChange: (value: string) => void; +}; + +export default class UiFindInput extends React.PureComponent { + static defaultProps: Partial = { + inputProps: {}, + trackFindFunction: undefined, + value: undefined, + }; + + clearUiFind = () => { + this.props.onChange(''); + }; + + componentWillUnmount(): void { + console.log('unomuet'); + } + + render() { + const { allowClear, inputProps, value } = this.props; + + const suffix = ( + <> + {allowClear && value && value.length && } + {inputProps.suffix} + + ); + + return ( + this.props.onChange(e.target.value)} + suffix={suffix} + value={value} + /> + ); + } +} diff --git a/packages/jaeger-ui-components/src/common/__snapshots__/UiFindInput.test.js.snap b/packages/jaeger-ui-components/src/common/__snapshots__/UiFindInput.test.js.snap new file mode 100644 index 00000000000..207f93ee1de --- /dev/null +++ b/packages/jaeger-ui-components/src/common/__snapshots__/UiFindInput.test.js.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UiFindInput rendering renders as expected 1`] = ` +} +/> +`; diff --git a/packages/jaeger-ui-components/src/index.ts b/packages/jaeger-ui-components/src/index.ts index 4f9997402ec..dba87017fa9 100644 --- a/packages/jaeger-ui-components/src/index.ts +++ b/packages/jaeger-ui-components/src/index.ts @@ -1,10 +1,12 @@ export { default as TraceTimelineViewer } from './TraceTimelineViewer'; +export { default as TracePageHeader } from './TracePageHeader'; export { default as UIElementsContext } from './uiElementsContext'; export * from './uiElementsContext'; export * from './types'; export * from './TraceTimelineViewer/types'; export { default as DetailState } from './TraceTimelineViewer/SpanDetail/DetailState'; export { default as transformTraceData } from './model/transform-trace-data'; +export { default as filterSpans } from './utils/filter-spans'; import { onlyUpdateForKeys } from 'recompose'; diff --git a/packages/jaeger-ui-components/src/model/link-patterns.tsx b/packages/jaeger-ui-components/src/model/link-patterns.tsx index 50f55f5fba6..f07d329c565 100644 --- a/packages/jaeger-ui-components/src/model/link-patterns.tsx +++ b/packages/jaeger-ui-components/src/model/link-patterns.tsx @@ -17,7 +17,7 @@ import memoize from 'lru-memoize'; import { getConfigValue } from '../utils/config/get-config'; import { getParent } from './span'; import { TNil } from '../types'; -import { Span, Link, KeyValuePair, Trace } from '../types/trace'; +import { Span, Link, KeyValuePair, Trace } from '..'; const parameterRegExp = /#\{([^{}]*)\}/g; @@ -36,7 +36,7 @@ type ProcessedLinkPattern = { parameters: string[]; }; -type TLinksRV = { url: string; text: string }[]; +type TLinksRV = Array<{ url: string; text: string }>; function getParamNames(str: string) { const names = new Set(); @@ -143,7 +143,7 @@ function callTemplate(template: ProcessedTemplate, data: any) { export function computeTraceLink(linkPatterns: ProcessedLinkPattern[], trace: Trace) { const result: TLinksRV = []; - const validKeys = (Object.keys(trace) as (keyof Trace)[]).filter( + const validKeys = (Object.keys(trace) as Array).filter( key => typeof trace[key] === 'string' || trace[key] === 'number' ); @@ -188,7 +188,7 @@ export function computeLinks( if (spanTags) { type = 'tags'; } - const result: { url: string; text: string }[] = []; + const result: Array<{ url: string; text: string }> = []; linkPatterns.forEach(pattern => { if (pattern.type(type) && pattern.key(item.key) && pattern.value(item.value)) { const parameterValues: Record = {}; @@ -242,7 +242,9 @@ const processedLinks: ProcessedLinkPattern[] = (getConfigValue('linkPatterns') | export const getTraceLinks: (trace: Trace | undefined) => TLinksRV = memoize(10)((trace: Trace | undefined) => { const result: TLinksRV = []; - if (!trace) return result; + if (!trace) { + return result; + } return computeTraceLink(processedLinks, trace); }); diff --git a/packages/jaeger-ui-components/src/model/span.tsx b/packages/jaeger-ui-components/src/model/span.tsx index 462c7886af9..16ee980a43d 100644 --- a/packages/jaeger-ui-components/src/model/span.tsx +++ b/packages/jaeger-ui-components/src/model/span.tsx @@ -19,7 +19,6 @@ import { Span } from '../types/trace'; * @param {Span} span The span whose parent is to be returned. * @return {Span|null} The parent span if there is one, null otherwise. */ -// eslint-disable-next-line import/prefer-default-export export function getParent(span: Span) { const parentRef = span.references ? span.references.find(ref => ref.refType === 'CHILD_OF') : null; return parentRef ? parentRef.span : null; diff --git a/packages/jaeger-ui-components/src/model/trace-viewer.ts b/packages/jaeger-ui-components/src/model/trace-viewer.ts new file mode 100644 index 00000000000..cc881b5ad0a --- /dev/null +++ b/packages/jaeger-ui-components/src/model/trace-viewer.ts @@ -0,0 +1,20 @@ +// 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 { Span } from '../types'; + +export function getTraceName(spans: Span[]): string { + const span = spans.filter(sp => !sp.references || !sp.references.length)[0]; + return span ? `${span.process.serviceName}: ${span.operationName}` : ''; +} diff --git a/packages/jaeger-ui-components/src/types/trace.tsx b/packages/jaeger-ui-components/src/types/trace.tsx index 83f650edff3..7f94304da9d 100644 --- a/packages/jaeger-ui-components/src/types/trace.tsx +++ b/packages/jaeger-ui-components/src/types/trace.tsx @@ -19,7 +19,7 @@ // TODO: Everett Tech Debt: Fix KeyValuePair types export type KeyValuePair = { key: string; - type: string; + type?: string; value: any; }; diff --git a/packages/jaeger-ui-components/src/uberUtilityStyles.ts b/packages/jaeger-ui-components/src/uberUtilityStyles.ts index cdc675f0fec..b7894a12272 100644 --- a/packages/jaeger-ui-components/src/uberUtilityStyles.ts +++ b/packages/jaeger-ui-components/src/uberUtilityStyles.ts @@ -22,6 +22,10 @@ export const ubPx2 = css` padding-right: 0.5rem; `; +export const ubPb2 = css` + padding-bottom: 0.5rem; +`; + export const ubFlex = css` display: flex; `; @@ -55,3 +59,11 @@ export const uTxEllipsis = css` export const uWidth100 = css` width: 100%; `; + +export const uTxMuted = css` + color: #aaa; +`; + +export const ubJustifyEnd = css` + justify-content: flex-end; +`; diff --git a/packages/jaeger-ui-components/src/uiElementsContext.tsx b/packages/jaeger-ui-components/src/uiElementsContext.tsx index d8dd549d398..2d61ad35d56 100644 --- a/packages/jaeger-ui-components/src/uiElementsContext.tsx +++ b/packages/jaeger-ui-components/src/uiElementsContext.tsx @@ -135,6 +135,7 @@ export type ButtonProps = { htmlType?: ButtonHTMLType; icon?: string; onClick?: React.MouseEventHandler; + disabled?: boolean; }; export const UIButton = function UIButton(props: ButtonProps) { @@ -162,7 +163,42 @@ export const UIDivider = function UIDivider(props: DividerProps) { ); }; -type Elements = { +export type InputProps = { + autosize?: boolean | null; + placeholder?: string; + onChange: (value: React.ChangeEvent) => void; + suffix: React.ReactNode; + value?: string; +}; + +export const UIInput: React.FC = function UIInput(props: InputProps) { + return ( + + {(elements: Elements) => { + return ; + }} + + ); +}; + +export type InputGroupProps = { + className?: string; + compact?: boolean; + style?: React.CSSProperties; + children?: React.ReactNode; +}; + +export const UIInputGroup = function UIInputGroup(props: InputGroupProps) { + return ( + + {(elements: Elements) => { + return ; + }} + + ); +}; + +export type Elements = { Popover: React.ComponentType; Tooltip: React.ComponentType; Icon: React.ComponentType; @@ -171,6 +207,8 @@ type Elements = { MenuItem: React.ComponentType; Button: React.ComponentType; Divider: React.ComponentType; + Input: React.ComponentType; + InputGroup: React.ComponentType; }; /** diff --git a/packages/jaeger-ui-components/src/utils/filter-spans.test.js b/packages/jaeger-ui-components/src/utils/filter-spans.test.js new file mode 100644 index 00000000000..b8d61cc973b --- /dev/null +++ b/packages/jaeger-ui-components/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-components/src/utils/filter-spans.tsx b/packages/jaeger-ui-components/src/utils/filter-spans.tsx new file mode 100644 index 00000000000..9891efe460e --- /dev/null +++ b/packages/jaeger-ui-components/src/utils/filter-spans.tsx @@ -0,0 +1,67 @@ +// 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 { KeyValuePair, Span } from '../types/trace'; +import { TNil } from '../types'; + +export default function filterSpans(textFilter: string, spans: Span[] | TNil) { + if (!spans) { + return null; + } + + // if a span field includes at least one filter in includeFilters, the span is a match + const includeFilters: string[] = []; + + // values with keys that include text in any one of the excludeKeys will be ignored + const excludeKeys: string[] = []; + + // 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: string[], text: string) => + filters.some(filter => text.toLowerCase().includes(filter)); + + const isTextInKeyValues = (kvs: KeyValuePair[]) => + 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/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index 8717d30e3fd..b6d1e7ce14d 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -19,7 +19,7 @@ import { toggleGraph } from './state/actions'; import { Provider } from 'react-redux'; import { configureStore } from 'app/store/configureStore'; import { SecondaryActions } from './SecondaryActions'; -import { TraceView } from './TraceView'; +import { TraceView } from './TraceView/TraceView'; const dummyProps: ExploreProps = { changeSize: jest.fn(), diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index e047025dd1a..664a5eafe3d 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -59,7 +59,7 @@ import { getTimeZone } from '../profile/state/selectors'; import { ErrorContainer } from './ErrorContainer'; import { scanStopAction } from './state/actionTypes'; import { ExploreGraphPanel } from './ExploreGraphPanel'; -import { TraceView } from './TraceView'; +import { TraceView } from './TraceView/TraceView'; import { SecondaryActions } from './SecondaryActions'; const getStyles = stylesFactory(() => { @@ -73,18 +73,6 @@ const getStyles = stylesFactory(() => { button: css` margin: 1em 4px 0 0; `, - // Utility class for iframe parents so that we can show iframe content with reasonable height instead of squished - // or some random explicit height. - fullHeight: css` - label: fullHeight; - height: 100%; - `, - iframe: css` - label: iframe; - border: none; - width: 100%; - height: 100%; - `, }; }); @@ -330,22 +318,14 @@ export class Explore extends React.PureComponent { onClickRichHistoryButton={this.toggleShowRichHistory} /> - + {({ width }) => { if (width === 0) { return null; } return ( -
+
{showStartPage && StartPage && (
diff --git a/public/app/features/explore/TraceView.test.tsx b/public/app/features/explore/TraceView.test.tsx deleted file mode 100644 index e25e0561ae1..00000000000 --- a/public/app/features/explore/TraceView.test.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { TraceView } from './TraceView'; -import { SpanData, TraceData, TraceTimelineViewer } from '@jaegertracing/jaeger-ui-components'; - -describe('TraceView', () => { - it('renders TraceTimelineViewer', () => { - const wrapper = shallow(); - expect(wrapper.find(TraceTimelineViewer)).toHaveLength(1); - }); - - it('toggles detailState', () => { - const wrapper = shallow(); - let viewer = wrapper.find(TraceTimelineViewer); - expect(viewer.props().traceTimeline.detailStates.size).toBe(0); - - viewer.props().detailToggle('1'); - viewer = wrapper.find(TraceTimelineViewer); - expect(viewer.props().traceTimeline.detailStates.size).toBe(1); - expect(viewer.props().traceTimeline.detailStates.get('1')).not.toBeUndefined(); - - viewer.props().detailToggle('1'); - viewer = wrapper.find(TraceTimelineViewer); - expect(viewer.props().traceTimeline.detailStates.size).toBe(0); - }); - - it('toggles children visibility', () => { - const wrapper = shallow(); - let viewer = wrapper.find(TraceTimelineViewer); - expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0); - - viewer.props().childrenToggle('1'); - viewer = wrapper.find(TraceTimelineViewer); - expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(1); - expect(viewer.props().traceTimeline.childrenHiddenIDs.has('1')).toBeTruthy(); - - viewer.props().childrenToggle('1'); - viewer = wrapper.find(TraceTimelineViewer); - expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0); - }); - - it('toggles adds and removes hover indent guides', () => { - const wrapper = shallow(); - let viewer = wrapper.find(TraceTimelineViewer); - expect(viewer.props().traceTimeline.hoverIndentGuideIds.size).toBe(0); - - viewer.props().addHoverIndentGuideId('1'); - viewer = wrapper.find(TraceTimelineViewer); - expect(viewer.props().traceTimeline.hoverIndentGuideIds.size).toBe(1); - expect(viewer.props().traceTimeline.hoverIndentGuideIds.has('1')).toBeTruthy(); - - viewer.props().removeHoverIndentGuideId('1'); - viewer = wrapper.find(TraceTimelineViewer); - expect(viewer.props().traceTimeline.hoverIndentGuideIds.size).toBe(0); - }); - - it('toggles collapses and expands one level of spans', () => { - const wrapper = shallow(); - let viewer = wrapper.find(TraceTimelineViewer); - expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0); - const spans = viewer.props().trace.spans; - - viewer.props().collapseOne(spans); - viewer = wrapper.find(TraceTimelineViewer); - expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(1); - expect(viewer.props().traceTimeline.childrenHiddenIDs.has('3fb050342773d333')).toBeTruthy(); - - viewer.props().expandOne(spans); - viewer = wrapper.find(TraceTimelineViewer); - expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0); - }); - - it('toggles collapses and expands all levels', () => { - const wrapper = shallow(); - let viewer = wrapper.find(TraceTimelineViewer); - expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0); - const spans = viewer.props().trace.spans; - - viewer.props().collapseAll(spans); - viewer = wrapper.find(TraceTimelineViewer); - expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(2); - expect(viewer.props().traceTimeline.childrenHiddenIDs.has('3fb050342773d333')).toBeTruthy(); - expect(viewer.props().traceTimeline.childrenHiddenIDs.has('1ed38015486087ca')).toBeTruthy(); - - viewer.props().expandAll(); - viewer = wrapper.find(TraceTimelineViewer); - expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0); - }); -}); - -const response: TraceData & { spans: SpanData[] } = { - traceID: '1ed38015486087ca', - spans: [ - { - traceID: '1ed38015486087ca', - spanID: '1ed38015486087ca', - flags: 1, - operationName: 'HTTP POST - api_prom_push', - references: [] as any, - startTime: 1585244579835187, - duration: 1098, - tags: [ - { key: 'sampler.type', type: 'string', value: 'const' }, - { key: 'sampler.param', type: 'bool', value: true }, - { key: 'span.kind', type: 'string', value: 'server' }, - { key: 'http.method', type: 'string', value: 'POST' }, - { key: 'http.url', type: 'string', value: '/api/prom/push' }, - { key: 'component', type: 'string', value: 'net/http' }, - { key: 'http.status_code', type: 'int64', value: 204 }, - { key: 'internal.span.format', type: 'string', value: 'proto' }, - ], - logs: [ - { - timestamp: 1585244579835229, - fields: [{ key: 'event', type: 'string', value: 'util.ParseProtoRequest[start reading]' }], - }, - { - timestamp: 1585244579835241, - fields: [ - { key: 'event', type: 'string', value: 'util.ParseProtoRequest[decompress]' }, - { key: 'size', type: 'int64', value: 315 }, - ], - }, - { - timestamp: 1585244579835245, - fields: [ - { key: 'event', type: 'string', value: 'util.ParseProtoRequest[unmarshal]' }, - { key: 'size', type: 'int64', value: 446 }, - ], - }, - ], - processID: 'p1', - warnings: null as any, - }, - { - traceID: '1ed38015486087ca', - spanID: '3fb050342773d333', - flags: 1, - operationName: '/logproto.Pusher/Push', - references: [{ refType: 'CHILD_OF', traceID: '1ed38015486087ca', spanID: '1ed38015486087ca' }], - startTime: 1585244579835341, - duration: 921, - tags: [ - { key: 'span.kind', type: 'string', value: 'client' }, - { key: 'component', type: 'string', value: 'gRPC' }, - { key: 'internal.span.format', type: 'string', value: 'proto' }, - ], - logs: [], - processID: 'p1', - warnings: null, - }, - { - traceID: '1ed38015486087ca', - spanID: '35118c298fc91f68', - flags: 1, - operationName: '/logproto.Pusher/Push', - references: [{ refType: 'CHILD_OF', traceID: '1ed38015486087ca', spanID: '3fb050342773d333' }], - startTime: 1585244579836040, - duration: 36, - tags: [ - { key: 'span.kind', type: 'string', value: 'server' }, - { key: 'component', type: 'string', value: 'gRPC' }, - { key: 'internal.span.format', type: 'string', value: 'proto' }, - ], - logs: [] as any, - processID: 'p1', - warnings: null as any, - }, - ], - processes: { - p1: { - serviceName: 'loki-all', - tags: [ - { key: 'client-uuid', type: 'string', value: '2a59d08899ef6a8a' }, - { key: 'hostname', type: 'string', value: '0080b530fae3' }, - { key: 'ip', type: 'string', value: '172.18.0.6' }, - { key: 'jaeger.version', type: 'string', value: 'Go-2.20.1' }, - ], - }, - }, - warnings: null as any, -}; diff --git a/public/app/features/explore/TraceView.tsx b/public/app/features/explore/TraceView.tsx deleted file mode 100644 index 4728740e02d..00000000000 --- a/public/app/features/explore/TraceView.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import { - DetailState, - KeyValuePair, - Link, - Log, - Span, - // SpanData, - // SpanReference, - Trace, - TraceTimelineViewer, - TTraceTimeline, - UIElementsContext, - ViewRangeTimeUpdate, - transformTraceData, - SpanData, - TraceData, -} from '@jaegertracing/jaeger-ui-components'; -import React, { useState } from 'react'; - -type Props = { - trace: TraceData & { spans: SpanData[] }; -}; - -export function TraceView(props: Props) { - /** - * Track whether details are open per span. - */ - const [detailStates, setDetailStates] = useState(new Map()); - - /** - * Track whether span is collapsed, meaning its children spans are hidden. - */ - const [childrenHiddenIDs, setChildrenHiddenIDs] = useState(new Set()); - - /** - * For some reason this is used internally to handle hover state of indent guide. As indent guides are separate - * components per each row/span and you need to highlight all in multiple rows to make the effect of single line - * they need this kind of common imperative state changes. - * - * Ideally would be changed to trace view internal state. - */ - const [hoverIndentGuideIds, setHoverIndentGuideIds] = useState(new Set()); - - /** - * Keeps state of resizable name column - */ - const [spanNameColumnWidth, setSpanNameColumnWidth] = useState(0.25); - - function toggleDetail(spanID: string) { - const newDetailStates = new Map(detailStates); - if (newDetailStates.has(spanID)) { - newDetailStates.delete(spanID); - } else { - newDetailStates.set(spanID, new DetailState()); - } - setDetailStates(newDetailStates); - } - - function expandOne(spans: Span[]) { - if (childrenHiddenIDs.size === 0) { - return; - } - let prevExpandedDepth = -1; - let expandNextHiddenSpan = true; - const newChildrenHiddenIDs = spans.reduce((res, s) => { - if (s.depth <= prevExpandedDepth) { - expandNextHiddenSpan = true; - } - if (expandNextHiddenSpan && res.has(s.spanID)) { - res.delete(s.spanID); - expandNextHiddenSpan = false; - prevExpandedDepth = s.depth; - } - return res; - }, new Set(childrenHiddenIDs)); - setChildrenHiddenIDs(newChildrenHiddenIDs); - } - - function collapseOne(spans: Span[]) { - if (shouldDisableCollapse(spans, childrenHiddenIDs)) { - return; - } - let nearestCollapsedAncestor: Span | undefined; - const newChildrenHiddenIDs = spans.reduce((res, curSpan) => { - if (nearestCollapsedAncestor && curSpan.depth <= nearestCollapsedAncestor.depth) { - res.add(nearestCollapsedAncestor.spanID); - if (curSpan.hasChildren) { - nearestCollapsedAncestor = curSpan; - } - } else if (curSpan.hasChildren && !res.has(curSpan.spanID)) { - nearestCollapsedAncestor = curSpan; - } - return res; - }, new Set(childrenHiddenIDs)); - // The last one - if (nearestCollapsedAncestor) { - newChildrenHiddenIDs.add(nearestCollapsedAncestor.spanID); - } - setChildrenHiddenIDs(newChildrenHiddenIDs); - } - - function expandAll() { - setChildrenHiddenIDs(new Set()); - } - - function collapseAll(spans: Span[]) { - if (shouldDisableCollapse(spans, childrenHiddenIDs)) { - return; - } - const newChildrenHiddenIDs = spans.reduce((res, s) => { - if (s.hasChildren) { - res.add(s.spanID); - } - return res; - }, new Set()); - - setChildrenHiddenIDs(newChildrenHiddenIDs); - } - - function childrenToggle(spanID: string) { - const newChildrenHiddenIDs = new Set(childrenHiddenIDs); - if (childrenHiddenIDs.has(spanID)) { - newChildrenHiddenIDs.delete(spanID); - } else { - newChildrenHiddenIDs.add(spanID); - } - setChildrenHiddenIDs(newChildrenHiddenIDs); - } - - function detailLogItemToggle(spanID: string, log: Log) { - const old = detailStates.get(spanID); - if (!old) { - return; - } - const detailState = old.toggleLogItem(log); - const newDetailStates = new Map(detailStates); - newDetailStates.set(spanID, detailState); - return setDetailStates(newDetailStates); - } - - function addHoverIndentGuideId(spanID: string) { - setHoverIndentGuideIds(prevState => { - const newHoverIndentGuideIds = new Set(prevState); - newHoverIndentGuideIds.add(spanID); - return newHoverIndentGuideIds; - }); - } - - function removeHoverIndentGuideId(spanID: string) { - setHoverIndentGuideIds(prevState => { - const newHoverIndentGuideIds = new Set(prevState); - newHoverIndentGuideIds.delete(spanID); - return newHoverIndentGuideIds; - }); - } - - const traceProp = transformTraceData(props.trace); - - return ( - null as any) as any, - Tooltip: (() => null as any) as any, - Icon: (() => null as any) as any, - Dropdown: (() => null as any) as any, - Menu: (() => null as any) as any, - MenuItem: (() => null as any) as any, - Button: (() => null as any) as any, - Divider: (() => null as any) as any, - }} - > - {}} - scrollToFirstVisibleSpan={() => {}} - findMatchesIDs={null} - trace={traceProp} - traceTimeline={ - { - childrenHiddenIDs, - detailStates, - hoverIndentGuideIds, - shouldScrollToFirstUiFindMatch: false, - spanNameColumnWidth, - traceID: '50b96206cf81dd64', - } as TTraceTimeline - } - updateNextViewRangeTime={(update: ViewRangeTimeUpdate) => {}} - updateViewRangeTime={() => {}} - viewRange={{ time: { current: [0, 1], cursor: null } }} - focusSpan={() => {}} - createLinkToExternalSpan={() => ''} - setSpanNameColumnWidth={setSpanNameColumnWidth} - collapseAll={collapseAll} - collapseOne={collapseOne} - expandAll={expandAll} - expandOne={expandOne} - childrenToggle={childrenToggle} - clearShouldScrollToFirstUiFindMatch={() => {}} - detailLogItemToggle={detailLogItemToggle} - detailLogsToggle={makeDetailSubsectionToggle('logs', detailStates, setDetailStates)} - detailWarningsToggle={makeDetailSubsectionToggle('warnings', detailStates, setDetailStates)} - detailReferencesToggle={makeDetailSubsectionToggle('references', detailStates, setDetailStates)} - detailProcessToggle={makeDetailSubsectionToggle('process', detailStates, setDetailStates)} - detailTagsToggle={makeDetailSubsectionToggle('tags', detailStates, setDetailStates)} - detailToggle={toggleDetail} - setTrace={(trace: Trace | null, uiFind: string | null) => {}} - addHoverIndentGuideId={addHoverIndentGuideId} - removeHoverIndentGuideId={removeHoverIndentGuideId} - linksGetter={(span: Span, items: KeyValuePair[], itemIndex: number) => [] as Link[]} - uiFind={undefined} - /> - - ); -} - -function shouldDisableCollapse(allSpans: Span[], hiddenSpansIds: Set) { - const allParentSpans = allSpans.filter(s => s.hasChildren); - return allParentSpans.length === hiddenSpansIds.size; -} - -function makeDetailSubsectionToggle( - subSection: 'tags' | 'process' | 'logs' | 'warnings' | 'references', - detailStates: Map, - setDetailStates: (detailStates: Map) => void -) { - return (spanID: string) => { - const old = detailStates.get(spanID); - if (!old) { - return; - } - let detailState; - if (subSection === 'tags') { - detailState = old.toggleTags(); - } else if (subSection === 'process') { - detailState = old.toggleProcess(); - } else if (subSection === 'warnings') { - detailState = old.toggleWarnings(); - } else if (subSection === 'references') { - detailState = old.toggleReferences(); - } else { - detailState = old.toggleLogs(); - } - const newDetailStates = new Map(detailStates); - newDetailStates.set(spanID, detailState); - setDetailStates(newDetailStates); - }; -} diff --git a/public/app/features/explore/TraceView/TraceView.test.tsx b/public/app/features/explore/TraceView/TraceView.test.tsx new file mode 100644 index 00000000000..f9c47853ec6 --- /dev/null +++ b/public/app/features/explore/TraceView/TraceView.test.tsx @@ -0,0 +1,216 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { TraceView } from './TraceView'; +import { SpanData, TraceData, TracePageHeader, TraceTimelineViewer } from '@jaegertracing/jaeger-ui-components'; + +function renderTraceView() { + const wrapper = shallow(); + return { + timeline: wrapper.find(TraceTimelineViewer), + header: wrapper.find(TracePageHeader), + wrapper, + }; +} + +describe('TraceView', () => { + it('renders TraceTimelineViewer', () => { + const { timeline, header } = renderTraceView(); + expect(timeline).toHaveLength(1); + expect(header).toHaveLength(1); + }); + + it('toggles detailState', () => { + let { timeline, wrapper } = renderTraceView(); + expect(timeline.props().traceTimeline.detailStates.size).toBe(0); + + timeline.props().detailToggle('1'); + timeline = wrapper.find(TraceTimelineViewer); + expect(timeline.props().traceTimeline.detailStates.size).toBe(1); + expect(timeline.props().traceTimeline.detailStates.get('1')).not.toBeUndefined(); + + timeline.props().detailToggle('1'); + timeline = wrapper.find(TraceTimelineViewer); + expect(timeline.props().traceTimeline.detailStates.size).toBe(0); + }); + + it('toggles children visibility', () => { + let { timeline, wrapper } = renderTraceView(); + expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0); + + timeline.props().childrenToggle('1'); + timeline = wrapper.find(TraceTimelineViewer); + expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(1); + expect(timeline.props().traceTimeline.childrenHiddenIDs.has('1')).toBeTruthy(); + + timeline.props().childrenToggle('1'); + timeline = wrapper.find(TraceTimelineViewer); + expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0); + }); + + it('toggles adds and removes hover indent guides', () => { + let { timeline, wrapper } = renderTraceView(); + expect(timeline.props().traceTimeline.hoverIndentGuideIds.size).toBe(0); + + timeline.props().addHoverIndentGuideId('1'); + timeline = wrapper.find(TraceTimelineViewer); + expect(timeline.props().traceTimeline.hoverIndentGuideIds.size).toBe(1); + expect(timeline.props().traceTimeline.hoverIndentGuideIds.has('1')).toBeTruthy(); + + timeline.props().removeHoverIndentGuideId('1'); + timeline = wrapper.find(TraceTimelineViewer); + expect(timeline.props().traceTimeline.hoverIndentGuideIds.size).toBe(0); + }); + + it('toggles collapses and expands one level of spans', () => { + let { timeline, wrapper } = renderTraceView(); + expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0); + const spans = timeline.props().trace.spans; + + timeline.props().collapseOne(spans); + timeline = wrapper.find(TraceTimelineViewer); + expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(1); + expect(timeline.props().traceTimeline.childrenHiddenIDs.has('3fb050342773d333')).toBeTruthy(); + + timeline.props().expandOne(spans); + timeline = wrapper.find(TraceTimelineViewer); + expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0); + }); + + it('toggles collapses and expands all levels', () => { + let { timeline, wrapper } = renderTraceView(); + expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0); + const spans = timeline.props().trace.spans; + + timeline.props().collapseAll(spans); + timeline = wrapper.find(TraceTimelineViewer); + expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(2); + expect(timeline.props().traceTimeline.childrenHiddenIDs.has('3fb050342773d333')).toBeTruthy(); + expect(timeline.props().traceTimeline.childrenHiddenIDs.has('1ed38015486087ca')).toBeTruthy(); + + timeline.props().expandAll(); + timeline = wrapper.find(TraceTimelineViewer); + expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0); + }); + + it('searches for spans', () => { + let { wrapper, header } = renderTraceView(); + header.props().onSearchValueChange('HTTP POST - api_prom_push'); + + const timeline = wrapper.find(TraceTimelineViewer); + expect(timeline.props().findMatchesIDs.has('1ed38015486087ca')).toBeTruthy(); + }); + + it('change viewRange', () => { + let { header, timeline, wrapper } = renderTraceView(); + const defaultRange = { time: { current: [0, 1] } }; + expect(timeline.props().viewRange).toEqual(defaultRange); + expect(header.props().viewRange).toEqual(defaultRange); + header.props().updateViewRangeTime(0.2, 0.8); + + let newRange = { time: { current: [0.2, 0.8] } }; + timeline = wrapper.find(TraceTimelineViewer); + header = wrapper.find(TracePageHeader); + expect(timeline.props().viewRange).toEqual(newRange); + expect(header.props().viewRange).toEqual(newRange); + + newRange = { time: { current: [0.3, 0.7] } }; + timeline.props().updateViewRangeTime(0.3, 0.7); + timeline = wrapper.find(TraceTimelineViewer); + header = wrapper.find(TracePageHeader); + expect(timeline.props().viewRange).toEqual(newRange); + expect(header.props().viewRange).toEqual(newRange); + }); +}); + +const response: TraceData & { spans: SpanData[] } = { + traceID: '1ed38015486087ca', + spans: [ + { + traceID: '1ed38015486087ca', + spanID: '1ed38015486087ca', + flags: 1, + operationName: 'HTTP POST - api_prom_push', + references: [] as any, + startTime: 1585244579835187, + duration: 1098, + tags: [ + { key: 'sampler.type', type: 'string', value: 'const' }, + { key: 'sampler.param', type: 'bool', value: true }, + { key: 'span.kind', type: 'string', value: 'server' }, + { key: 'http.method', type: 'string', value: 'POST' }, + { key: 'http.url', type: 'string', value: '/api/prom/push' }, + { key: 'component', type: 'string', value: 'net/http' }, + { key: 'http.status_code', type: 'int64', value: 204 }, + { key: 'internal.span.format', type: 'string', value: 'proto' }, + ], + logs: [ + { + timestamp: 1585244579835229, + fields: [{ key: 'event', type: 'string', value: 'util.ParseProtoRequest[start reading]' }], + }, + { + timestamp: 1585244579835241, + fields: [ + { key: 'event', type: 'string', value: 'util.ParseProtoRequest[decompress]' }, + { key: 'size', type: 'int64', value: 315 }, + ], + }, + { + timestamp: 1585244579835245, + fields: [ + { key: 'event', type: 'string', value: 'util.ParseProtoRequest[unmarshal]' }, + { key: 'size', type: 'int64', value: 446 }, + ], + }, + ], + processID: 'p1', + warnings: null as any, + }, + { + traceID: '1ed38015486087ca', + spanID: '3fb050342773d333', + flags: 1, + operationName: '/logproto.Pusher/Push', + references: [{ refType: 'CHILD_OF', traceID: '1ed38015486087ca', spanID: '1ed38015486087ca' }], + startTime: 1585244579835341, + duration: 921, + tags: [ + { key: 'span.kind', type: 'string', value: 'client' }, + { key: 'component', type: 'string', value: 'gRPC' }, + { key: 'internal.span.format', type: 'string', value: 'proto' }, + ], + logs: [], + processID: 'p1', + warnings: null, + }, + { + traceID: '1ed38015486087ca', + spanID: '35118c298fc91f68', + flags: 1, + operationName: '/logproto.Pusher/Push', + references: [{ refType: 'CHILD_OF', traceID: '1ed38015486087ca', spanID: '3fb050342773d333' }], + startTime: 1585244579836040, + duration: 36, + tags: [ + { key: 'span.kind', type: 'string', value: 'server' }, + { key: 'component', type: 'string', value: 'gRPC' }, + { key: 'internal.span.format', type: 'string', value: 'proto' }, + ], + logs: [] as any, + processID: 'p1', + warnings: null as any, + }, + ], + processes: { + p1: { + serviceName: 'loki-all', + tags: [ + { key: 'client-uuid', type: 'string', value: '2a59d08899ef6a8a' }, + { key: 'hostname', type: 'string', value: '0080b530fae3' }, + { key: 'ip', type: 'string', value: '172.18.0.6' }, + { key: 'jaeger.version', type: 'string', value: 'Go-2.20.1' }, + ], + }, + }, + warnings: null as any, +}; diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx new file mode 100644 index 00000000000..90d4007642a --- /dev/null +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -0,0 +1,119 @@ +import React, { useState } from 'react'; +import { + KeyValuePair, + Link, + Span, + Trace, + TraceTimelineViewer, + TTraceTimeline, + UIElementsContext, + transformTraceData, + SpanData, + TraceData, + TracePageHeader, +} from '@jaegertracing/jaeger-ui-components'; +import { UIElements } from './uiElements'; +import { useViewRange } from './useViewRange'; +import { useSearch } from './useSearch'; +import { useChildrenState } from './useChildrenState'; +import { useDetailState } from './useDetailState'; +import { useHoverIndentGuide } from './useHoverIndentGuide'; + +type Props = { + trace: TraceData & { spans: SpanData[] }; +}; + +export function TraceView(props: Props) { + const { expandOne, collapseOne, childrenToggle, collapseAll, childrenHiddenIDs, expandAll } = useChildrenState(); + const { + detailStates, + toggleDetail, + detailLogItemToggle, + detailLogsToggle, + detailProcessToggle, + detailReferencesToggle, + detailTagsToggle, + detailWarningsToggle, + } = useDetailState(); + const { removeHoverIndentGuideId, addHoverIndentGuideId, hoverIndentGuideIds } = useHoverIndentGuide(); + const { viewRange, updateViewRangeTime, updateNextViewRangeTime } = useViewRange(); + + /** + * Keeps state of resizable name column width + */ + const [spanNameColumnWidth, setSpanNameColumnWidth] = useState(0.25); + /** + * State of the top minimap, slim means it is collapsed. + */ + const [slim, setSlim] = useState(false); + + const traceProp = transformTraceData(props.trace); + const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans); + + return ( + + {}} + focusUiFindMatches={() => {}} + hideMap={false} + hideSummary={false} + nextResult={() => {}} + onSlimViewClicked={() => setSlim(!slim)} + onTraceGraphViewClicked={() => {}} + prevResult={() => {}} + resultCount={0} + slimView={slim} + textFilter={null} + trace={traceProp} + traceGraphView={false} + updateNextViewRangeTime={updateNextViewRangeTime} + updateViewRangeTime={updateViewRangeTime} + viewRange={viewRange} + searchValue={search} + onSearchValueChange={setSearch} + hideSearchButtons={true} + /> + {}} + scrollToFirstVisibleSpan={() => {}} + findMatchesIDs={spanFindMatches} + trace={traceProp} + traceTimeline={ + { + childrenHiddenIDs, + detailStates, + hoverIndentGuideIds, + shouldScrollToFirstUiFindMatch: false, + spanNameColumnWidth, + traceID: '50b96206cf81dd64', + } as TTraceTimeline + } + updateNextViewRangeTime={updateNextViewRangeTime} + updateViewRangeTime={updateViewRangeTime} + viewRange={viewRange} + focusSpan={() => {}} + createLinkToExternalSpan={() => ''} + setSpanNameColumnWidth={setSpanNameColumnWidth} + collapseAll={collapseAll} + collapseOne={collapseOne} + expandAll={expandAll} + expandOne={expandOne} + childrenToggle={childrenToggle} + clearShouldScrollToFirstUiFindMatch={() => {}} + detailLogItemToggle={detailLogItemToggle} + detailLogsToggle={detailLogsToggle} + detailWarningsToggle={detailWarningsToggle} + detailReferencesToggle={detailReferencesToggle} + detailProcessToggle={detailProcessToggle} + detailTagsToggle={detailTagsToggle} + detailToggle={toggleDetail} + setTrace={(trace: Trace | null, uiFind: string | null) => {}} + addHoverIndentGuideId={addHoverIndentGuideId} + removeHoverIndentGuideId={removeHoverIndentGuideId} + linksGetter={(span: Span, items: KeyValuePair[], itemIndex: number) => [] as Link[]} + uiFind={search} + /> + + ); +} diff --git a/public/app/features/explore/TraceView/uiElements.tsx b/public/app/features/explore/TraceView/uiElements.tsx new file mode 100644 index 00000000000..3a41b4f0347 --- /dev/null +++ b/public/app/features/explore/TraceView/uiElements.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { ButtonProps, Elements } from '@jaegertracing/jaeger-ui-components'; +import { Button, Input } from '@grafana/ui'; + +/** + * Right now Jaeger components need some UI elements to be injected. This is to get rid of AntD UI library that was + * used by default. + */ + +// This needs to be static to prevent remounting on every render. +export const UIElements: Elements = { + Popover: (() => null as any) as any, + Tooltip: (() => null as any) as any, + Icon: (() => null as any) as any, + Dropdown: (() => null as any) as any, + Menu: (() => null as any) as any, + MenuItem: (() => null as any) as any, + Button: ({ onClick, children, className }: ButtonProps) => ( + + ), + Divider, + Input: props => , + InputGroup: ({ children, className, style }) => ( + + {children} + + ), +}; + +function Divider({ className }: { className?: string }) { + return ( +
+ ); +} diff --git a/public/app/features/explore/TraceView/useChildrenState.test.ts b/public/app/features/explore/TraceView/useChildrenState.test.ts new file mode 100644 index 00000000000..a71dadd44ca --- /dev/null +++ b/public/app/features/explore/TraceView/useChildrenState.test.ts @@ -0,0 +1,65 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useChildrenState } from './useChildrenState'; +import { Span } from '@jaegertracing/jaeger-ui-components'; + +describe('useChildrenState', () => { + describe('childrenToggle', () => { + it('toggles children state', async () => { + const { result } = renderHook(() => useChildrenState()); + expect(result.current.childrenHiddenIDs.size).toBe(0); + act(() => result.current.childrenToggle('testId')); + + expect(result.current.childrenHiddenIDs.size).toBe(1); + expect(result.current.childrenHiddenIDs.has('testId')).toBe(true); + + act(() => result.current.childrenToggle('testId')); + + expect(result.current.childrenHiddenIDs.size).toBe(0); + }); + }); + + describe('expandAll', () => { + it('expands all', async () => { + const { result } = renderHook(() => useChildrenState()); + act(() => result.current.childrenToggle('testId1')); + act(() => result.current.childrenToggle('testId2')); + + expect(result.current.childrenHiddenIDs.size).toBe(2); + + act(() => result.current.expandAll()); + + expect(result.current.childrenHiddenIDs.size).toBe(0); + }); + }); + + describe('collapseAll', () => { + it('hides spans that have children', async () => { + const { result } = renderHook(() => useChildrenState()); + act(() => + result.current.collapseAll([ + { spanID: 'span1', hasChildren: true } as Span, + { spanID: 'span2', hasChildren: false } as Span, + ]) + ); + + expect(result.current.childrenHiddenIDs.size).toBe(1); + expect(result.current.childrenHiddenIDs.has('span1')).toBe(true); + }); + + it('does nothing if already collapsed', async () => { + const { result } = renderHook(() => useChildrenState()); + act(() => result.current.childrenToggle('span1')); + act(() => + result.current.collapseAll([ + { spanID: 'span1', hasChildren: true } as Span, + { spanID: 'span2', hasChildren: false } as Span, + ]) + ); + + expect(result.current.childrenHiddenIDs.size).toBe(1); + expect(result.current.childrenHiddenIDs.has('span1')).toBe(true); + }); + }); + + // Other function are not yet used. +}); diff --git a/public/app/features/explore/TraceView/useChildrenState.ts b/public/app/features/explore/TraceView/useChildrenState.ts new file mode 100644 index 00000000000..1315682b6f7 --- /dev/null +++ b/public/app/features/explore/TraceView/useChildrenState.ts @@ -0,0 +1,94 @@ +import { useState } from 'react'; +import { Span } from '@jaegertracing/jaeger-ui-components'; + +/** + * Children state means whether spans are collapsed or not. Also provides some functions to manipulate that state. + */ +export function useChildrenState() { + const [childrenHiddenIDs, setChildrenHiddenIDs] = useState(new Set()); + + function expandOne(spans: Span[]) { + if (childrenHiddenIDs.size === 0) { + return; + } + let prevExpandedDepth = -1; + let expandNextHiddenSpan = true; + const newChildrenHiddenIDs = spans.reduce((res, s) => { + if (s.depth <= prevExpandedDepth) { + expandNextHiddenSpan = true; + } + if (expandNextHiddenSpan && res.has(s.spanID)) { + res.delete(s.spanID); + expandNextHiddenSpan = false; + prevExpandedDepth = s.depth; + } + return res; + }, new Set(childrenHiddenIDs)); + setChildrenHiddenIDs(newChildrenHiddenIDs); + } + + function collapseOne(spans: Span[]) { + if (shouldDisableCollapse(spans, childrenHiddenIDs)) { + return; + } + let nearestCollapsedAncestor: Span | undefined; + const newChildrenHiddenIDs = spans.reduce((res, curSpan) => { + if (nearestCollapsedAncestor && curSpan.depth <= nearestCollapsedAncestor.depth) { + res.add(nearestCollapsedAncestor.spanID); + if (curSpan.hasChildren) { + nearestCollapsedAncestor = curSpan; + } + } else if (curSpan.hasChildren && !res.has(curSpan.spanID)) { + nearestCollapsedAncestor = curSpan; + } + return res; + }, new Set(childrenHiddenIDs)); + // The last one + if (nearestCollapsedAncestor) { + newChildrenHiddenIDs.add(nearestCollapsedAncestor.spanID); + } + setChildrenHiddenIDs(newChildrenHiddenIDs); + } + + function expandAll() { + setChildrenHiddenIDs(new Set()); + } + + function collapseAll(spans: Span[]) { + if (shouldDisableCollapse(spans, childrenHiddenIDs)) { + return; + } + const newChildrenHiddenIDs = spans.reduce((res, s) => { + if (s.hasChildren) { + res.add(s.spanID); + } + return res; + }, new Set()); + + setChildrenHiddenIDs(newChildrenHiddenIDs); + } + + function childrenToggle(spanID: string) { + const newChildrenHiddenIDs = new Set(childrenHiddenIDs); + if (childrenHiddenIDs.has(spanID)) { + newChildrenHiddenIDs.delete(spanID); + } else { + newChildrenHiddenIDs.add(spanID); + } + setChildrenHiddenIDs(newChildrenHiddenIDs); + } + + return { + childrenHiddenIDs, + expandOne, + collapseOne, + expandAll, + collapseAll, + childrenToggle, + }; +} + +function shouldDisableCollapse(allSpans: Span[], hiddenSpansIds: Set) { + const allParentSpans = allSpans.filter(s => s.hasChildren); + return allParentSpans.length === hiddenSpansIds.size; +} diff --git a/public/app/features/explore/TraceView/useDetailState.test.ts b/public/app/features/explore/TraceView/useDetailState.test.ts new file mode 100644 index 00000000000..f3076f031dc --- /dev/null +++ b/public/app/features/explore/TraceView/useDetailState.test.ts @@ -0,0 +1,56 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { Log } from '@jaegertracing/jaeger-ui-components'; +import { useDetailState } from './useDetailState'; + +describe('useDetailState', () => { + it('toggles detail', async () => { + const { result } = renderHook(() => useDetailState()); + expect(result.current.detailStates.size).toBe(0); + + act(() => result.current.toggleDetail('span1')); + expect(result.current.detailStates.size).toBe(1); + expect(result.current.detailStates.has('span1')).toBe(true); + + act(() => result.current.toggleDetail('span1')); + expect(result.current.detailStates.size).toBe(0); + }); + + it('toggles logs and logs items', async () => { + const { result } = renderHook(() => useDetailState()); + act(() => result.current.toggleDetail('span1')); + act(() => result.current.detailLogsToggle('span1')); + expect(result.current.detailStates.get('span1').logs.isOpen).toBe(true); + + const log = { timestamp: 1 } as Log; + act(() => result.current.detailLogItemToggle('span1', log)); + expect(result.current.detailStates.get('span1').logs.openedItems.has(log)).toBe(true); + }); + + it('toggles warnings', async () => { + const { result } = renderHook(() => useDetailState()); + act(() => result.current.toggleDetail('span1')); + act(() => result.current.detailWarningsToggle('span1')); + expect(result.current.detailStates.get('span1').isWarningsOpen).toBe(true); + }); + + it('toggles references', async () => { + const { result } = renderHook(() => useDetailState()); + act(() => result.current.toggleDetail('span1')); + act(() => result.current.detailReferencesToggle('span1')); + expect(result.current.detailStates.get('span1').isReferencesOpen).toBe(true); + }); + + it('toggles processes', async () => { + const { result } = renderHook(() => useDetailState()); + act(() => result.current.toggleDetail('span1')); + act(() => result.current.detailProcessToggle('span1')); + expect(result.current.detailStates.get('span1').isProcessOpen).toBe(true); + }); + + it('toggles tags', async () => { + const { result } = renderHook(() => useDetailState()); + act(() => result.current.toggleDetail('span1')); + act(() => result.current.detailTagsToggle('span1')); + expect(result.current.detailStates.get('span1').isTagsOpen).toBe(true); + }); +}); diff --git a/public/app/features/explore/TraceView/useDetailState.ts b/public/app/features/explore/TraceView/useDetailState.ts new file mode 100644 index 00000000000..cf725a1f370 --- /dev/null +++ b/public/app/features/explore/TraceView/useDetailState.ts @@ -0,0 +1,70 @@ +import { useState } from 'react'; +import { DetailState, Log } from '@jaegertracing/jaeger-ui-components'; + +/** + * Keeps state of the span detail. This means whether span details are open but also state of each detail subitem + * like logs or tags. + */ +export function useDetailState() { + const [detailStates, setDetailStates] = useState(new Map()); + + function toggleDetail(spanID: string) { + const newDetailStates = new Map(detailStates); + if (newDetailStates.has(spanID)) { + newDetailStates.delete(spanID); + } else { + newDetailStates.set(spanID, new DetailState()); + } + setDetailStates(newDetailStates); + } + + function detailLogItemToggle(spanID: string, log: Log) { + const old = detailStates.get(spanID); + if (!old) { + return; + } + const detailState = old.toggleLogItem(log); + const newDetailStates = new Map(detailStates); + newDetailStates.set(spanID, detailState); + return setDetailStates(newDetailStates); + } + + return { + detailStates, + toggleDetail, + detailLogItemToggle, + detailLogsToggle: makeDetailSubsectionToggle('logs', detailStates, setDetailStates), + detailWarningsToggle: makeDetailSubsectionToggle('warnings', detailStates, setDetailStates), + detailReferencesToggle: makeDetailSubsectionToggle('references', detailStates, setDetailStates), + detailProcessToggle: makeDetailSubsectionToggle('process', detailStates, setDetailStates), + detailTagsToggle: makeDetailSubsectionToggle('tags', detailStates, setDetailStates), + }; +} + +function makeDetailSubsectionToggle( + subSection: 'tags' | 'process' | 'logs' | 'warnings' | 'references', + detailStates: Map, + setDetailStates: (detailStates: Map) => void +) { + return (spanID: string) => { + const old = detailStates.get(spanID); + if (!old) { + return; + } + let detailState; + if (subSection === 'tags') { + detailState = old.toggleTags(); + } else if (subSection === 'process') { + detailState = old.toggleProcess(); + } else if (subSection === 'warnings') { + detailState = old.toggleWarnings(); + } else if (subSection === 'references') { + detailState = old.toggleReferences(); + } else { + detailState = old.toggleLogs(); + } + const newDetailStates = new Map(detailStates); + newDetailStates.set(spanID, detailState); + setDetailStates(newDetailStates); + }; +} diff --git a/public/app/features/explore/TraceView/useHoverIndentGuide.test.ts b/public/app/features/explore/TraceView/useHoverIndentGuide.test.ts new file mode 100644 index 00000000000..94a280e2182 --- /dev/null +++ b/public/app/features/explore/TraceView/useHoverIndentGuide.test.ts @@ -0,0 +1,16 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useHoverIndentGuide } from './useHoverIndentGuide'; + +describe('useHoverIndentGuide', () => { + it('adds and removes indent guide ids', async () => { + const { result } = renderHook(() => useHoverIndentGuide()); + expect(result.current.hoverIndentGuideIds.size).toBe(0); + + act(() => result.current.addHoverIndentGuideId('span1')); + expect(result.current.hoverIndentGuideIds.size).toBe(1); + expect(result.current.hoverIndentGuideIds.has('span1')).toBe(true); + + act(() => result.current.removeHoverIndentGuideId('span1')); + expect(result.current.hoverIndentGuideIds.size).toBe(0); + }); +}); diff --git a/public/app/features/explore/TraceView/useHoverIndentGuide.ts b/public/app/features/explore/TraceView/useHoverIndentGuide.ts new file mode 100644 index 00000000000..d628b779927 --- /dev/null +++ b/public/app/features/explore/TraceView/useHoverIndentGuide.ts @@ -0,0 +1,30 @@ +import { useState } from 'react'; + +/** + * This is used internally to handle hover state of indent guide. As indent guides are separate + * components per each row/span and you need to highlight all in multiple rows to make the effect of single line + * they need this kind of common imperative state changes. + * + * Ideally would be changed to trace view internal state. + */ +export function useHoverIndentGuide() { + const [hoverIndentGuideIds, setHoverIndentGuideIds] = useState(new Set()); + + function addHoverIndentGuideId(spanID: string) { + setHoverIndentGuideIds(prevState => { + const newHoverIndentGuideIds = new Set(prevState); + newHoverIndentGuideIds.add(spanID); + return newHoverIndentGuideIds; + }); + } + + function removeHoverIndentGuideId(spanID: string) { + setHoverIndentGuideIds(prevState => { + const newHoverIndentGuideIds = new Set(prevState); + newHoverIndentGuideIds.delete(spanID); + return newHoverIndentGuideIds; + }); + } + + return { hoverIndentGuideIds, addHoverIndentGuideId, removeHoverIndentGuideId }; +} diff --git a/public/app/features/explore/TraceView/useSearch.test.ts b/public/app/features/explore/TraceView/useSearch.test.ts new file mode 100644 index 00000000000..4bfca13e27b --- /dev/null +++ b/public/app/features/explore/TraceView/useSearch.test.ts @@ -0,0 +1,44 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useSearch } from './useSearch'; +import { Span } from '@jaegertracing/jaeger-ui-components'; + +describe('useSearch', () => { + it('returns matching span IDs', async () => { + const { result } = renderHook(() => + useSearch([ + { + spanID: 'span1', + operationName: 'operation1', + process: { + serviceName: 'service1', + tags: [], + }, + tags: [], + logs: [], + } as Span, + + { + spanID: 'span2', + operationName: 'operation2', + process: { + serviceName: 'service2', + tags: [], + }, + tags: [], + logs: [], + } as Span, + ]) + ); + + act(() => result.current.setSearch('service1')); + expect(result.current.spanFindMatches.size).toBe(1); + expect(result.current.spanFindMatches.has('span1')).toBe(true); + }); + + it('works without spans', async () => { + const { result } = renderHook(() => useSearch()); + + act(() => result.current.setSearch('service1')); + expect(result.current.spanFindMatches).toBe(undefined); + }); +}); diff --git a/public/app/features/explore/TraceView/useSearch.ts b/public/app/features/explore/TraceView/useSearch.ts new file mode 100644 index 00000000000..eb15b3ff811 --- /dev/null +++ b/public/app/features/explore/TraceView/useSearch.ts @@ -0,0 +1,15 @@ +import { useState } from 'react'; +import { Span, filterSpans } from '@jaegertracing/jaeger-ui-components'; + +/** + * Controls the state of search input that highlights spans if they match the search string. + * @param spans + */ +export function useSearch(spans?: Span[]) { + const [search, setSearch] = useState(''); + let spanFindMatches: Set | undefined; + if (search && spans) { + spanFindMatches = filterSpans(search, spans); + } + return { search, setSearch, spanFindMatches }; +} diff --git a/public/app/features/explore/TraceView/useViewRange.test.ts b/public/app/features/explore/TraceView/useViewRange.test.ts new file mode 100644 index 00000000000..0e4a55abe93 --- /dev/null +++ b/public/app/features/explore/TraceView/useViewRange.test.ts @@ -0,0 +1,25 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useViewRange } from './useViewRange'; + +describe('useViewRange', () => { + it('defaults to full range', async () => { + const { result } = renderHook(() => useViewRange()); + expect(result.current.viewRange).toEqual({ time: { current: [0, 1] } }); + }); + + describe('updateNextViewRangeTime', () => { + it('updates time', async () => { + const { result } = renderHook(() => useViewRange()); + act(() => result.current.updateNextViewRangeTime({ cursor: 0.5 })); + expect(result.current.viewRange).toEqual({ time: { current: [0, 1], cursor: 0.5 } }); + }); + }); + + describe('updateViewRangeTime', () => { + it('updates time', async () => { + const { result } = renderHook(() => useViewRange()); + act(() => result.current.updateViewRangeTime(0.1, 0.2)); + expect(result.current.viewRange).toEqual({ time: { current: [0.1, 0.2] } }); + }); + }); +}); diff --git a/public/app/features/explore/TraceView/useViewRange.ts b/public/app/features/explore/TraceView/useViewRange.ts new file mode 100644 index 00000000000..84254fd11af --- /dev/null +++ b/public/app/features/explore/TraceView/useViewRange.ts @@ -0,0 +1,35 @@ +import { useState } from 'react'; +import { ViewRangeTimeUpdate, ViewRange } from '@jaegertracing/jaeger-ui-components'; + +/** + * Controls state of the zoom function that can be used through minimap in header or on the timeline. ViewRange contains + * state not only for current range that is showing but range that is currently being selected by the user. + */ +export function useViewRange() { + const [viewRange, setViewRange] = useState({ + time: { + current: [0, 1], + }, + }); + + function updateNextViewRangeTime(update: ViewRangeTimeUpdate) { + setViewRange( + (prevRange): ViewRange => { + const time = { ...prevRange.time, ...update }; + return { ...prevRange, time }; + } + ); + } + + function updateViewRangeTime(start: number, end: number) { + const current: [number, number] = [start, end]; + const time = { current }; + setViewRange( + (prevRange): ViewRange => { + return { ...prevRange, time }; + } + ); + } + + return { viewRange, updateViewRangeTime, updateNextViewRangeTime }; +} diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index e2285cb13d3..9a3679d4509 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -25,7 +25,7 @@ export class Wrapper extends Component { return (
-
+
diff --git a/yarn.lock b/yarn.lock index f883859d8fd..3713cadf089 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3196,6 +3196,15 @@ js-yaml "~3.13.1" resolve "1.8.1" +"@grafana/data@6.7.2", "@grafana/data@^6.6.0-pre": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@grafana/data/-/data-6.7.2.tgz#f3f303654923cac17da6b3664d31227fc088fe54" + integrity sha512-W/oygRtN8FrFalaIaCndUqSWN0lRQQA05/q4sulxy7wMY6o+JXJwtmFA83tDeJ4l4SmVPISK5XewA1um4U77Pw== + dependencies: + apache-arrow "0.15.1" + lodash "4.17.15" + rxjs "6.5.4" + "@grafana/eslint-config@^1.0.0-rc1": version "1.0.0-rc1" resolved "https://registry.yarnpkg.com/@grafana/eslint-config/-/eslint-config-1.0.0-rc1.tgz#3b0a1abddfea900a57abc9526ad31abb1da2d42c" @@ -3223,11 +3232,140 @@ tiny-invariant "^1.0.1" tiny-warning "^0.0.3" +"@grafana/toolkit@^6.6.0-pre": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@grafana/toolkit/-/toolkit-6.7.2.tgz#be201ae5fb0420319c342ed95ec300c19a7ed68f" + integrity sha512-Y0k696zW32fjP6dUz9gE6p0T3+EvHXcMxjKeuNfuy7yyHty0pbvSAelCy8gFFq9UmYoWCsfUuer3srTDO0KXcw== + dependencies: + "@babel/core" "7.9.0" + "@babel/preset-env" "7.9.0" + "@grafana/data" "6.7.2" + "@grafana/eslint-config" "^1.0.0-rc1" + "@grafana/tsconfig" "^1.0.0-rc1" + "@grafana/ui" "6.7.2" + "@types/command-exists" "^1.2.0" + "@types/execa" "^0.9.0" + "@types/expect-puppeteer" "3.3.1" + "@types/fs-extra" "^8.1.0" + "@types/inquirer" "^6.0.3" + "@types/jest" "24.0.13" + "@types/jest-cli" "^23.6.0" + "@types/node" "^12.0.4" + "@types/prettier" "^1.16.4" + "@types/puppeteer-core" "1.9.0" + "@types/react-dev-utils" "^9.0.1" + "@types/rimraf" "^2.0.3" + "@types/semver" "^6.0.0" + "@types/tmp" "^0.1.0" + "@types/webpack" "4.4.34" + "@typescript-eslint/eslint-plugin" "2.19.2" + "@typescript-eslint/parser" "2.19.2" + axios "0.19.0" + babel-jest "24.8.0" + babel-loader "8.1.0" + babel-plugin-angularjs-annotate "0.10.0" + chalk "^2.4.2" + command-exists "^1.2.8" + commander "^2.20.0" + concurrently "4.1.0" + copy-webpack-plugin "5.0.3" + css-loader "^3.0.0" + eslint "6.7.2" + eslint-config-prettier "6.7.0" + eslint-plugin-jsdoc "18.4.1" + eslint-plugin-prettier "3.1.1" + execa "^1.0.0" + expect-puppeteer "4.1.1" + file-loader "^4.0.0" + fs-extra "^8.1.0" + globby "^10.0.1" + html-loader "0.5.5" + html-webpack-plugin "^3.2.0" + inquirer "^6.3.1" + jest "24.8.0" + jest-canvas-mock "2.1.2" + jest-cli "^24.8.0" + jest-coverage-badges "^1.1.2" + jest-junit "^6.4.0" + less "^3.10.3" + less-loader "^5.0.0" + lodash "4.17.15" + md5-file "^4.0.0" + mini-css-extract-plugin "^0.7.0" + node-sass "^4.12.0" + optimize-css-assets-webpack-plugin "^5.0.3" + ora "^3.4.0" + pixelmatch "^5.0.2" + pngjs "^3.4.0" + postcss-flexbugs-fixes "4.1.0" + postcss-loader "3.0.0" + postcss-preset-env "6.6.0" + prettier "^1.19.1" + puppeteer-core "1.18.1" + react-dev-utils "^9.0.1" + replace-in-file "^4.1.0" + replace-in-file-webpack-plugin "^1.0.6" + rimraf "^3.0.0" + sass-loader "7.1.0" + semver "^6.1.1" + simple-git "^1.112.0" + style-loader "^0.23.1" + terser-webpack-plugin "^1.3.0" + ts-jest "24.1.0" + ts-loader "6.2.1" + ts-node "8.5.0" + tslib "1.10.0" + typescript "3.7.2" + url-loader "^2.0.1" + webpack "4.35.0" + "@grafana/tsconfig@^1.0.0-rc1": version "1.0.0-rc1" resolved "https://registry.yarnpkg.com/@grafana/tsconfig/-/tsconfig-1.0.0-rc1.tgz#d07ea16755a50cae21000113f30546b61647a200" integrity sha512-nucKPGyzlSKYSiJk5RA8GzMdVWhdYNdF+Hh65AXxjD9PlY69JKr5wANj8bVdQboag6dgg0BFKqgKPyY+YtV4Iw== +"@grafana/ui@6.7.2", "@grafana/ui@^6.6.0-pre": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-6.7.2.tgz#cc27f17220bfecb76731d268dc72535be2891e4a" + integrity sha512-YXunXjBi2SgF9QhSa3koNHW2eS83tByiR9+e6trJONrzwpMtxfSytBOzt4IeZb3dH9dofP0I6P20K4VX//HnKQ== + dependencies: + "@emotion/core" "^10.0.27" + "@grafana/data" "6.7.2" + "@grafana/slate-react" "0.22.9-grafana" + "@grafana/tsconfig" "^1.0.0-rc1" + "@torkelo/react-select" "3.0.8" + "@types/react-color" "2.17.0" + "@types/react-select" "3.0.8" + "@types/react-table" "7.0.2" + "@types/slate" "0.47.1" + "@types/slate-react" "0.22.5" + bizcharts "^3.5.5" + classnames "2.2.6" + d3 "5.15.0" + emotion "10.0.27" + immutable "3.8.2" + jquery "3.4.1" + lodash "4.17.15" + moment "2.24.0" + papaparse "4.6.3" + rc-cascader "0.17.5" + rc-drawer "3.0.2" + rc-slider "8.7.1" + rc-time-picker "^3.7.2" + react "16.12.0" + react-calendar "2.19.2" + react-color "2.17.0" + react-custom-scrollbars "4.2.1" + react-dom "16.12.0" + react-highlight-words "0.11.0" + react-hook-form "4.5.3" + react-popper "1.3.3" + react-storybook-addon-props-combinations "1.1.0" + react-table "7.0.0-rc.15" + react-transition-group "2.6.1" + slate "0.47.8" + tinycolor2 "1.4.1" + "@icons/material@^0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" @@ -5801,6 +5939,13 @@ dependencies: "@types/react" "*" +"@types/react-color@2.17.0": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-2.17.0.tgz#7f3c958bb43ebeedc7e04309576a235d5233ce9d" + integrity sha512-NQCLW437DXzaV/XvtoH3cBW75f0KQ9ZtFvvXnn7QEudLTR5zGxLdsEhPffrateSizsG2CTml4X+2/2TyEisotQ== + dependencies: + "@types/react" "*" + "@types/react-color@3.0.1", "@types/react-color@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.1.tgz#5433e2f503ea0e0831cbc6fd0c20f8157d93add0" @@ -5815,7 +5960,7 @@ dependencies: "@types/react" "*" -"@types/react-dev-utils@^9.0.4": +"@types/react-dev-utils@^9.0.1", "@types/react-dev-utils@^9.0.4": version "9.0.4" resolved "https://registry.yarnpkg.com/@types/react-dev-utils/-/react-dev-utils-9.0.4.tgz#3e4bee79b7536777cef219427ab1d38adc24f3f2" integrity sha512-8cv9rAeSP1EmyRQAbZ/i6uYtai1VoKHGSBwDyCLM82wCkqoh3WPjJgI1pfi2kiLc0C5hNU7DLo7/c4hylfHLWg== @@ -5903,6 +6048,13 @@ dependencies: "@types/react" "*" +"@types/react-table@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.0.2.tgz#184de5ad5a7c5aced08b49812002a4d2e8918cc0" + integrity sha512-sxvjV0JCk/ijCzENejXth99cFMnmucATaC31gz1bMk8iQwUDE2VYaw2QQTcDrzBxzastBQGdcLpcFIN61RvgIA== + dependencies: + "@types/react" "*" + "@types/react-test-renderer@*": version "16.9.1" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.1.tgz#9d432c46c515ebe50c45fa92c6fb5acdc22e39c4" @@ -6196,6 +6348,17 @@ "@types/webpack-sources" "*" source-map "^0.6.0" +"@types/webpack@4.4.34": + version "4.4.34" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.34.tgz#e5f88b9a795da11683b4ec4a07d1c2b023b19810" + integrity sha512-GnEBgjHsfO1M7DIQ0dAupSofcmDItE3Zsu3reK8SQpl/6N0rtUQxUmQzVFAS5ou/FGjsYKjXAWfItLZ0kNFTfQ== + dependencies: + "@types/anymatch" "*" + "@types/node" "*" + "@types/tapable" "*" + "@types/uglify-js" "*" + source-map "^0.6.0" + "@types/webpack@4.41.7": version "4.41.7" resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.7.tgz#22be27dbd4362b01c3954ca9b021dbc9328d9511" @@ -6250,6 +6413,17 @@ regexpp "^3.0.0" tsutils "^3.17.1" +"@typescript-eslint/eslint-plugin@2.19.2": + version "2.19.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.19.2.tgz#e279aaae5d5c1f2547b4cff99204e1250bc7a058" + integrity sha512-HX2qOq2GOV04HNrmKnTpSIpHjfl7iwdXe3u/Nvt+/cpmdvzYvY0NHSiTkYN257jHnq4OM/yo+OsFgati+7LqJA== + dependencies: + "@typescript-eslint/experimental-utils" "2.19.2" + eslint-utils "^1.4.3" + functional-red-black-tree "^1.0.1" + regexpp "^3.0.0" + tsutils "^3.17.1" + "@typescript-eslint/eslint-plugin@2.24.0": version "2.24.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.24.0.tgz#a86cf618c965a462cddf3601f594544b134d6d68" @@ -6270,6 +6444,15 @@ "@typescript-eslint/typescript-estree" "2.19.0" eslint-scope "^5.0.0" +"@typescript-eslint/experimental-utils@2.19.2": + version "2.19.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.19.2.tgz#4611d44cf0f0cb460c26aa7676fc0a787281e233" + integrity sha512-B88QuwT1wMJR750YvTJBNjMZwmiPpbmKYLm1yI7PCc3x0NariqPwqaPsoJRwU9DmUi0cd9dkhz1IqEnwfD+P1A== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.19.2" + eslint-scope "^5.0.0" + "@typescript-eslint/experimental-utils@2.24.0": version "2.24.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.24.0.tgz#a5cb2ed89fedf8b59638dc83484eb0c8c35e1143" @@ -6289,6 +6472,16 @@ "@typescript-eslint/typescript-estree" "2.19.0" eslint-visitor-keys "^1.1.0" +"@typescript-eslint/parser@2.19.2": + version "2.19.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.19.2.tgz#21f42c0694846367e7d6a907feb08ab2f89c0879" + integrity sha512-8uwnYGKqX9wWHGPGdLB9sk9+12sjcdqEEYKGgbS8A0IvYX59h01o8os5qXUHMq2na8vpDRaV0suTLM7S8wraTA== + dependencies: + "@types/eslint-visitor-keys" "^1.0.0" + "@typescript-eslint/experimental-utils" "2.19.2" + "@typescript-eslint/typescript-estree" "2.19.2" + eslint-visitor-keys "^1.1.0" + "@typescript-eslint/parser@2.24.0": version "2.24.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.24.0.tgz#2cf0eae6e6dd44d162486ad949c126b887f11eb8" @@ -6312,6 +6505,19 @@ semver "^6.3.0" tsutils "^3.17.1" +"@typescript-eslint/typescript-estree@2.19.2": + version "2.19.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.19.2.tgz#67485b00172f400474d243c6c0be27581a579350" + integrity sha512-Xu/qa0MDk6upQWqE4Qy2X16Xg8Vi32tQS2PR0AvnT/ZYS4YGDvtn2MStOh5y8Zy2mg4NuL06KUHlvCh95j9C6Q== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^6.3.0" + tsutils "^3.17.1" + "@typescript-eslint/typescript-estree@2.24.0": version "2.24.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.24.0.tgz#38bbc8bb479790d2f324797ffbcdb346d897c62a" @@ -6538,6 +6744,11 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" +acorn-dynamic-import@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" + integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== + acorn-es7-plugin@^1.0.12: version "1.1.7" resolved "https://registry.yarnpkg.com/acorn-es7-plugin/-/acorn-es7-plugin-1.1.7.tgz#f2ee1f3228a90eead1245f9ab1922eb2e71d336b" @@ -6600,6 +6811,11 @@ acorn@^6.0.1, acorn@^6.0.7, acorn@^6.2.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== +acorn@^6.0.5: + version "6.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" + integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== + acorn@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" @@ -6916,6 +7132,22 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +apache-arrow@0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/apache-arrow/-/apache-arrow-0.15.1.tgz#136c03e18c3fa2617b41999608e7e685b0966147" + integrity sha512-3H+sC789nWn8JDnMwfd2j19NJ4gMcdtbpp2Haa22wBoDGUbbA5FgD2OqfE9Mr4yPlJZFWVJDw7C1hgJo2UolxA== + dependencies: + "@types/flatbuffers" "^1.9.1" + "@types/node" "^12.0.4" + "@types/text-encoding-utf-8" "^1.0.1" + command-line-args "5.0.2" + command-line-usage "5.0.5" + flatbuffers "1.11.0" + json-bignum "^0.0.3" + pad-left "^2.1.0" + text-encoding-utf-8 "^1.0.2" + tslib "^1.9.3" + apache-arrow@0.16.0: version "0.16.0" resolved "https://registry.yarnpkg.com/apache-arrow/-/apache-arrow-0.16.0.tgz#7ee7a6397d1a2d6349aed90c6ce5b92362e79881" @@ -7310,6 +7542,19 @@ autoprefixer@9.7.4, autoprefixer@^9.6.1, autoprefixer@^9.7.2: postcss "^7.0.26" postcss-value-parser "^4.0.2" +autoprefixer@^9.4.9: + version "9.7.6" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.6.tgz#63ac5bbc0ce7934e6997207d5bb00d68fa8293a4" + integrity sha512-F7cYpbN7uVVhACZTeeIeealwdGM6wMtfWARVLTy5xmKtgVdBNJvbDRoCK3YO1orcs7gv/KwYlb3iXwu9Ug9BkQ== + dependencies: + browserslist "^4.11.1" + caniuse-lite "^1.0.30001039" + chalk "^2.4.2" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^7.0.27" + postcss-value-parser "^4.0.3" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -7848,7 +8093,7 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== -bizcharts@^3.5.8: +bizcharts@^3.5.5, bizcharts@^3.5.8: version "3.5.8" resolved "https://registry.yarnpkg.com/bizcharts/-/bizcharts-3.5.8.tgz#50abcb4960891aada6ca35318af791dd68d85825" integrity sha512-s/Nt66HLQXD8oyN8yE26inh5ZGkoIr1eFE+/2TBln6lpyATm51LrqCXJPOTgOSyEp3dSNVZ7rOFCKFMMVcdOwA== @@ -8117,6 +8362,16 @@ browserslist@^4.0.0, browserslist@^4.6.0, browserslist@^4.7.0: electron-to-chromium "^1.3.284" node-releases "^1.1.36" +browserslist@^4.11.1, browserslist@^4.4.2: + version "4.11.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.11.1.tgz#92f855ee88d6e050e7e7311d987992014f1a1f1b" + integrity sha512-DCTr3kDrKEYNw6Jb9HFxVLQNaue8z+0ZfRBRjmCunKDEXEBajKDj2Y+Uelg+Pi29OnvaSGwjOsnRyNEkXzHg5g== + dependencies: + caniuse-lite "^1.0.30001038" + electron-to-chromium "^1.3.390" + node-releases "^1.1.53" + pkg-up "^2.0.0" + browserslist@^4.6.4, browserslist@^4.9.1: version "4.11.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.11.0.tgz#aef4357b10a8abda00f97aac7cd587b2082ba1ad" @@ -8254,6 +8509,26 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +cacache@^11.3.2: + version "11.3.3" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.3.tgz#8bd29df8c6a718a6ebd2d010da4d7972ae3bbadc" + integrity sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA== + dependencies: + bluebird "^3.5.5" + chownr "^1.1.1" + figgy-pudding "^3.5.1" + glob "^7.1.4" + graceful-fs "^4.1.15" + lru-cache "^5.1.1" + mississippi "^3.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.3" + ssri "^6.0.1" + unique-filename "^1.1.1" + y18n "^4.0.0" + cacache@^12.0.0, cacache@^12.0.2, cacache@^12.0.3: version "12.0.3" resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.3.tgz#be99abba4e1bf5df461cd5a2c1071fc432573390" @@ -8452,6 +8727,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30000999: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000999.tgz#427253a69ad7bea4aa8d8345687b8eec51ca0e43" integrity sha512-1CUyKyecPeksKwXZvYw0tEoaMCo/RwBlXmEtN5vVnabvO0KPd9RQLcaAuR9/1F+KDMv6esmOFWlsXuzDk+8rxg== +caniuse-lite@^1.0.30000939, caniuse-lite@^1.0.30001038, caniuse-lite@^1.0.30001039: + version "1.0.30001039" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001039.tgz#b3814a1c38ffeb23567f8323500c09526a577bbe" + integrity sha512-SezbWCTT34eyFoWHgx8UWso7YtvtM7oosmFoXbCkdC6qJzRfBTeTgE9REtKtiuKXuMwWTZEvdnFNGAyVMorv8Q== + caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001035: version "1.0.30001036" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001036.tgz#930ea5272010d8bf190d859159d757c0b398caf0" @@ -8672,7 +8952,7 @@ chownr@^1.1.1, chownr@^1.1.2: resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw== -chrome-trace-event@^1.0.2: +chrome-trace-event@^1.0.0, chrome-trace-event@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== @@ -8786,6 +9066,11 @@ cli-spinners@^0.1.2: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" integrity sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw= +cli-spinners@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.3.0.tgz#0632239a4b5aa4c958610142c34bb7a651fc8df5" + integrity sha512-Xs2Hf2nzrvJMFKimOR7YR0QwZ8fc0u98kdtwN1eNAZzNQgH3vK2pXzff6GJtKh7S5hoJ87ECiAiZFS2fb5Ii2w== + cli-spinners@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77" @@ -9110,7 +9395,7 @@ commander@~2.19.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== -comment-parser@^0.7.2: +comment-parser@^0.7.0, comment-parser@^0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-0.7.2.tgz#baf6d99b42038678b81096f15b630d18142f4b8a" integrity sha512-4Rjb1FnxtOcv9qsfuaNuVsmmVn4ooVoBHzYfyKteiXwIU84PClyGA5jASoFMwPV93+FPh9spwueXauxFJZkGAg== @@ -9440,6 +9725,24 @@ copy-to-clipboard@^3.1.0, copy-to-clipboard@^3.2.0: dependencies: toggle-selection "^1.0.6" +copy-webpack-plugin@5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-5.0.3.tgz#2179e3c8fd69f13afe74da338896f1f01a875b5c" + integrity sha512-PlZRs9CUMnAVylZq+vg2Juew662jWtwOXOqH4lbQD9ZFhRG9R7tVStOgHt21CBGVq7k5yIJaz8TXDLSjV+Lj8Q== + dependencies: + cacache "^11.3.2" + find-cache-dir "^2.1.0" + glob-parent "^3.1.0" + globby "^7.1.1" + is-glob "^4.0.1" + loader-utils "^1.2.3" + minimatch "^3.0.4" + normalize-path "^3.0.0" + p-limit "^2.2.0" + schema-utils "^1.0.0" + serialize-javascript "^1.7.0" + webpack-log "^2.0.0" + copy-webpack-plugin@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-5.1.1.tgz#5481a03dea1123d88a988c6ff8b78247214f0b88" @@ -9837,7 +10140,7 @@ css-what@2.1, css-what@^2.1.2: resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== -cssdb@^4.4.0: +cssdb@^4.3.0, cssdb@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0" integrity sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ== @@ -10740,7 +11043,7 @@ dom-css@^2.0.0: prefix-style "2.0.1" to-camel-case "1.0.0" -dom-helpers@^3.4.0: +dom-helpers@^3.3.1, dom-helpers@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== @@ -10944,6 +11247,11 @@ electron-to-chromium@^1.3.380: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.381.tgz#952678ff91a5f36175a3832358a6dd2de3bf62b7" integrity sha512-JQBpVUr83l+QOqPQpj2SbOve1bBE4ACpmwcMNqWlZmfib7jccxJ02qFNichDpZ5LS4Zsqc985NIPKegBIZjK8Q== +electron-to-chromium@^1.3.390: + version "1.3.398" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.398.tgz#4c01e29091bf39e578ac3f66c1f157d92fa5725d" + integrity sha512-BJjxuWLKFbM5axH3vES7HKMQgAknq9PZHBkMK/rEXUQG9i1Iw5R+6hGkm6GtsQSANjSUrh/a6m32nzCNDNo/+w== + elegant-spinner@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" @@ -11405,6 +11713,13 @@ eslint-config-prettier@6.10.0: dependencies: get-stdin "^6.0.0" +eslint-config-prettier@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.7.0.tgz#9a876952e12df2b284adbd3440994bf1f39dfbb9" + integrity sha512-FamQVKM3jjUVwhG4hEMnbtsq7xOIDm+SY5iBPfR8gKsJoAB2IQnNF+bk1+8Fy44Nq7PPJaLvkRxILYdJWoguKQ== + dependencies: + get-stdin "^6.0.0" + eslint-loader@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-3.0.3.tgz#e018e3d2722381d982b1201adb56819c73b480ca" @@ -11416,6 +11731,20 @@ eslint-loader@^3.0.3: object-hash "^2.0.1" schema-utils "^2.6.1" +eslint-plugin-jsdoc@18.4.1: + version "18.4.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-18.4.1.tgz#f59089394ca64cd1d932812c2c84e03b3f868524" + integrity sha512-JyGUby6EPpyjfGBpTy3VRfG8yfJOjFc+l6GbusHVRludgrHsJpR105bm9IBv2s0mI6AxC8/r+Ec7kBubmDugTg== + dependencies: + comment-parser "^0.7.0" + debug "^4.1.1" + jsdoctypeparser "^6.0.0" + lodash "^4.17.15" + object.entries-ponyfill "^1.0.1" + regextras "^0.6.1" + semver "^6.3.0" + spdx-expression-parse "^3.0.0" + eslint-plugin-jsdoc@22.1.0: version "22.1.0" resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-22.1.0.tgz#dadfa62653fc0d87f900d810307f5ed07ef6ecd5" @@ -11429,6 +11758,13 @@ eslint-plugin-jsdoc@22.1.0: semver "^6.3.0" spdx-expression-parse "^3.0.0" +eslint-plugin-prettier@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.1.tgz#507b8562410d02a03f0ddc949c616f877852f2ba" + integrity sha512-A+TZuHZ0KU0cnn56/9mfR7/KjUJ9QNVXUhwvRFSR7PGPe0zQR6PTkmyqg1AtUUEOzTqeRsUwyKFh0oVZKVCrtA== + dependencies: + prettier-linter-helpers "^1.0.0" + eslint-plugin-prettier@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz#432e5a667666ab84ce72f945c72f77d996a5c9ba" @@ -11452,7 +11788,7 @@ eslint-plugin-react@7.18.3: resolve "^1.14.2" string.prototype.matchall "^4.0.2" -eslint-scope@^4.0.3: +eslint-scope@^4.0.0, eslint-scope@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== @@ -11480,6 +11816,49 @@ eslint-visitor-keys@^1.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== +eslint@6.7.2: + version "6.7.2" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.7.2.tgz#c17707ca4ad7b2d8af986a33feba71e18a9fecd1" + integrity sha512-qMlSWJaCSxDFr8fBPvJM9kJwbazrhNcBU3+DszDW1OlEwKBBRWsJc7NJFelvwQpanHCR14cOLD41x8Eqvo3Nng== + dependencies: + "@babel/code-frame" "^7.0.0" + ajv "^6.10.0" + chalk "^2.1.0" + cross-spawn "^6.0.5" + debug "^4.0.1" + doctrine "^3.0.0" + eslint-scope "^5.0.0" + eslint-utils "^1.4.3" + eslint-visitor-keys "^1.1.0" + espree "^6.1.2" + esquery "^1.0.1" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + inquirer "^7.0.0" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.14" + minimatch "^3.0.4" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.3" + progress "^2.0.0" + regexpp "^2.0.1" + semver "^6.1.2" + strip-ansi "^5.2.0" + strip-json-comments "^3.0.1" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + eslint@6.8.0: version "6.8.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" @@ -12465,6 +12844,19 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +fork-ts-checker-webpack-plugin@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-1.0.0.tgz#0f9ff0219f9b6f1a1b10fa25d7cc5015e60c997a" + integrity sha512-Kc7LI0OlnWB0FRbDQO+nnDCfZr+LhFSJIP8kZppDXvuXI/opeMg3IrlMedBX/EGgOUK0ma5Hafgkdp3DuxgYdg== + dependencies: + babel-code-frame "^6.22.0" + chalk "^2.4.1" + chokidar "^2.0.4" + micromatch "^3.1.10" + minimatch "^3.0.4" + semver "^5.6.0" + tapable "^1.0.0" + fork-ts-checker-webpack-plugin@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-1.5.0.tgz#ce1d77190b44d81a761b10b6284a373795e41f0c" @@ -15533,7 +15925,7 @@ jscodeshift@^0.7.0: temp "^0.8.1" write-file-atomic "^2.3.0" -jsdoctypeparser@^6.1.0: +jsdoctypeparser@^6.0.0, jsdoctypeparser@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-6.1.0.tgz#acfb936c26300d98f1405cb03e20b06748e512a8" integrity sha512-UCQBZ3xCUBv/PLfwKAJhp6jmGOSLFNKzrotXGNgbKhWvz27wPsCsVeP7gIcHPElQw2agBmynAitXqhxR58XAmA== @@ -15885,7 +16277,7 @@ less-loader@^5.0.0: loader-utils "^1.1.0" pify "^4.0.1" -less@^3.11.1: +less@^3.10.3, less@^3.11.1: version "3.11.1" resolved "https://registry.yarnpkg.com/less/-/less-3.11.1.tgz#c6bf08e39e02404fe6b307a3dfffafdc55bd36e2" integrity sha512-tlWX341RECuTOvoDIvtFqXsKj072hm3+9ymRBe76/mD6O5ZZecnlAOVDlWAleF2+aohFrxNidXhv2773f6kY7g== @@ -16107,7 +16499,7 @@ loader-fs-cache@^1.0.2: find-cache-dir "^0.1.1" mkdirp "0.5.1" -loader-runner@^2.4.0: +loader-runner@^2.3.0, loader-runner@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== @@ -16312,7 +16704,7 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.15, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.1.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: +lodash@4.17.15, lodash@>4.17.4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.1.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -16712,7 +17104,7 @@ memoizerific@^1.11.3: dependencies: map-or-similar "^1.5.0" -memory-fs@^0.4.0, memory-fs@^0.4.1: +memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= @@ -16803,7 +17195,7 @@ microevent.ts@~0.1.1: resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g== -micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: +micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -17077,6 +17469,13 @@ mkdirp@^0.5.3: dependencies: minimist "^1.2.5" +mkdirp@~0.5.0: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + mocha@7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.0.1.tgz#276186d35a4852f6249808c6dd4a1376cbf6c6ce" @@ -17456,7 +17855,7 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= -node-libs-browser@^2.2.1: +node-libs-browser@^2.0.0, node-libs-browser@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== @@ -17554,7 +17953,12 @@ node-releases@^1.1.52: dependencies: semver "^6.3.0" -node-sass@4.13.1, node-sass@^4.13.1: +node-releases@^1.1.53: + version "1.1.53" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.53.tgz#2d821bfa499ed7c5dffc5e2f28c88e78a08ee3f4" + integrity sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ== + +node-sass@4.13.1, node-sass@^4.12.0, node-sass@^4.13.1: version "4.13.1" resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.13.1.tgz#9db5689696bb2eec2c32b98bfea4c7a2e992d0a3" integrity sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw== @@ -17840,6 +18244,11 @@ object.defaults@^1.1.0: for-own "^1.0.0" isobject "^3.0.0" +object.entries-ponyfill@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object.entries-ponyfill/-/object.entries-ponyfill-1.0.1.tgz#29abdf77cbfbd26566dd1aa24e9d88f65433d256" + integrity sha1-Kavfd8v70mVm3RqiTp2I9lQz0lY= + object.entries@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519" @@ -18065,6 +18474,18 @@ ora@^0.2.3: cli-spinners "^0.1.2" object-assign "^4.0.1" +ora@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318" + integrity sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg== + dependencies: + chalk "^2.4.2" + cli-cursor "^2.1.0" + cli-spinners "^2.0.0" + log-symbols "^2.2.0" + strip-ansi "^5.2.0" + wcwidth "^1.0.1" + ora@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/ora/-/ora-4.0.3.tgz#752a1b7b4be4825546a7a3d59256fa523b6b6d05" @@ -18652,7 +19073,7 @@ pirates@^4.0.0, pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" -pixelmatch@5.1.0, pixelmatch@^5.1.0: +pixelmatch@5.1.0, pixelmatch@^5.0.2, pixelmatch@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.1.0.tgz#b640f0e5a03a09f235a4b818ef3b9b98d9d0b911" integrity sha512-HqtgvuWN12tBzKJf7jYsc38Ha28Q2NYpmBL9WostEGgDHJqbTLkjydZXL1ZHM02ZnB+Dkwlxo87HBY38kMiD6A== @@ -18680,7 +19101,7 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -pkg-up@2.0.0: +pkg-up@2.0.0, pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" integrity sha1-yBmscoBZpGHKscOImivjxJoATX8= @@ -18808,7 +19229,7 @@ postcss-color-gray@^5.0.0: postcss "^7.0.5" postcss-values-parser "^2.0.0" -postcss-color-hex-alpha@^5.0.3: +postcss-color-hex-alpha@^5.0.2, postcss-color-hex-alpha@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz#a8d9ca4c39d497c9661e374b9c51899ef0f87388" integrity sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw== @@ -18852,14 +19273,14 @@ postcss-convert-values@^4.0.1: postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-custom-media@^7.0.8: +postcss-custom-media@^7.0.7, postcss-custom-media@^7.0.8: version "7.0.8" resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz#fffd13ffeffad73621be5f387076a28b00294e0c" integrity sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg== dependencies: postcss "^7.0.14" -postcss-custom-properties@^8.0.11: +postcss-custom-properties@^8.0.11, postcss-custom-properties@^8.0.9: version "8.0.11" resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz#2d61772d6e92f22f5e0d52602df8fae46fa30d97" integrity sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA== @@ -18927,6 +19348,13 @@ postcss-env-function@^2.0.2: postcss "^7.0.2" postcss-values-parser "^2.0.0" +postcss-flexbugs-fixes@4.1.0, postcss-flexbugs-fixes@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.1.0.tgz#e094a9df1783e2200b7b19f875dcad3b3aff8b20" + integrity sha512-jr1LHxQvStNNAHlgco6PzY308zvLklh7SJVYuWUwyUQncofaAlD2l+P/gxKHOdqWKe7xJSkVLFF/2Tp+JqMSZA== + dependencies: + postcss "^7.0.0" + postcss-flexbugs-fixes@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.2.0.tgz#662b3dcb6354638b9213a55eed8913bcdc8d004a" @@ -18934,13 +19362,6 @@ postcss-flexbugs-fixes@4.2.0: dependencies: postcss "^7.0.26" -postcss-flexbugs-fixes@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.1.0.tgz#e094a9df1783e2200b7b19f875dcad3b3aff8b20" - integrity sha512-jr1LHxQvStNNAHlgco6PzY308zvLklh7SJVYuWUwyUQncofaAlD2l+P/gxKHOdqWKe7xJSkVLFF/2Tp+JqMSZA== - dependencies: - postcss "^7.0.0" - postcss-focus-visible@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz#477d107113ade6024b14128317ade2bd1e17046e" @@ -19248,6 +19669,49 @@ postcss-place@^4.0.1: postcss "^7.0.2" postcss-values-parser "^2.0.0" +postcss-preset-env@6.6.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-6.6.0.tgz#642e7d962e2bdc2e355db117c1eb63952690ed5b" + integrity sha512-I3zAiycfqXpPIFD6HXhLfWXIewAWO8emOKz+QSsxaUZb9Dp8HbF5kUf+4Wy/AxR33o+LRoO8blEWCHth0ZsCLA== + dependencies: + autoprefixer "^9.4.9" + browserslist "^4.4.2" + caniuse-lite "^1.0.30000939" + css-blank-pseudo "^0.1.4" + css-has-pseudo "^0.10.0" + css-prefers-color-scheme "^3.1.1" + cssdb "^4.3.0" + postcss "^7.0.14" + postcss-attribute-case-insensitive "^4.0.1" + postcss-color-functional-notation "^2.0.1" + postcss-color-gray "^5.0.0" + postcss-color-hex-alpha "^5.0.2" + postcss-color-mod-function "^3.0.3" + postcss-color-rebeccapurple "^4.0.1" + postcss-custom-media "^7.0.7" + postcss-custom-properties "^8.0.9" + postcss-custom-selectors "^5.1.2" + postcss-dir-pseudo-class "^5.0.0" + postcss-double-position-gradients "^1.0.0" + postcss-env-function "^2.0.2" + postcss-focus-visible "^4.0.0" + postcss-focus-within "^3.0.0" + postcss-font-variant "^4.0.0" + postcss-gap-properties "^2.0.0" + postcss-image-set-function "^3.0.1" + postcss-initial "^3.0.0" + postcss-lab-function "^2.0.1" + postcss-logical "^3.0.0" + postcss-media-minmax "^4.0.0" + postcss-nesting "^7.0.0" + postcss-overflow-shorthand "^2.0.0" + postcss-page-break "^2.0.0" + postcss-place "^4.0.1" + postcss-pseudo-class-any-link "^6.0.0" + postcss-replace-overflow-wrap "^3.0.0" + postcss-selector-matches "^4.0.0" + postcss-selector-not "^4.0.0" + postcss-preset-env@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-6.7.0.tgz#c34ddacf8f902383b35ad1e030f178f4cdf118a5" @@ -19408,6 +19872,11 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz#482282c09a42706d1fc9a069b73f44ec08391dc9" integrity sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ== +postcss-value-parser@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d" + integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg== + postcss-values-parser@^2.0.0, postcss-values-parser@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz#da8b472d901da1e205b47bdc98637b9e9e550e5f" @@ -19444,6 +19913,15 @@ postcss@^7.0.23, postcss@^7.0.26: source-map "^0.6.1" supports-color "^6.1.0" +postcss@^7.0.27: + version "7.0.27" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9" + integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + power-assert-context-formatter@^1.0.7: version "1.2.0" resolved "https://registry.yarnpkg.com/power-assert-context-formatter/-/power-assert-context-formatter-1.2.0.tgz#8fbe72692288ec5a7203cdf215c8b838a6061d2a" @@ -20208,6 +20686,19 @@ rc-animate@^2.10.2: rc-util "^4.15.3" react-lifecycles-compat "^3.0.4" +rc-cascader@0.17.5: + version "0.17.5" + resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-0.17.5.tgz#4fde91d23b7608c420263c38eee9c0687f80f7dc" + integrity sha512-WYMVcxU0+Lj+xLr4YYH0+yXODumvNXDcVEs5i7L1mtpWwYkubPV/zbQpn+jGKFCIW/hOhjkU4J1db8/P/UKE7A== + dependencies: + array-tree-filter "^2.1.0" + prop-types "^15.5.8" + rc-trigger "^2.2.0" + rc-util "^4.0.4" + react-lifecycles-compat "^3.0.4" + shallow-equal "^1.0.0" + warning "^4.0.1" + rc-cascader@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-1.0.1.tgz#770de1e1fa7bd559aabd4d59e525819b8bc809b7" @@ -20218,6 +20709,16 @@ rc-cascader@1.0.1: rc-util "^4.0.4" warning "^4.0.1" +rc-drawer@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-3.0.2.tgz#1c42b2b7790040344f8f05f1d132b1ef0e97b783" + integrity sha512-oPScGXB/8/ov9gEFLxPH8RBv/9jLTZboZtyF/GgrrnCAvbFwUxXdELH6n6XIowmuDKKvTGIMgZdnao0T46Yv3A== + dependencies: + babel-runtime "^6.26.0" + classnames "^2.2.6" + rc-util "^4.11.2" + react-lifecycles-compat "^3.0.4" + rc-drawer@3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-3.1.3.tgz#cbcb04d4c07f0b66f2ece11d847f4a1bd80ea0b7" @@ -20227,6 +20728,20 @@ rc-drawer@3.1.3: rc-util "^4.16.1" react-lifecycles-compat "^3.0.4" +rc-slider@8.7.1: + version "8.7.1" + resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-8.7.1.tgz#9ed07362dc93489a38e654b21b8122ad70fd3c42" + integrity sha512-WMT5mRFUEcrLWwTxsyS8jYmlaMsTVCZIGENLikHsNv+tE8ThU2lCoPfi/xFNUfJFNFSBFP3MwPez9ZsJmNp13g== + dependencies: + babel-runtime "6.x" + classnames "^2.2.5" + prop-types "^15.5.4" + rc-tooltip "^3.7.0" + rc-util "^4.0.4" + react-lifecycles-compat "^3.0.4" + shallowequal "^1.1.0" + warning "^4.0.3" + rc-slider@9.2.3: version "9.2.3" resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-9.2.3.tgz#b80f03ada71ef3ec35dfcb67d2e0f87d84ce2781" @@ -20239,7 +20754,7 @@ rc-slider@9.2.3: shallowequal "^1.1.0" warning "^4.0.3" -rc-time-picker@^3.7.3: +rc-time-picker@^3.7.2, rc-time-picker@^3.7.3: version "3.7.3" resolved "https://registry.yarnpkg.com/rc-time-picker/-/rc-time-picker-3.7.3.tgz#65a8de904093250ae9c82b02a4905e0f995e23e2" integrity sha512-Lv1Mvzp9fRXhXEnRLO4nW6GLNxUkfAZ3RsiIBsWjGjXXvMNjdr4BX/ayElHAFK0DoJqOhm7c5tjmIYpEOwcUXg== @@ -20251,6 +20766,15 @@ rc-time-picker@^3.7.3: rc-trigger "^2.2.0" react-lifecycles-compat "^3.0.4" +rc-tooltip@^3.7.0: + version "3.7.3" + resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-3.7.3.tgz#280aec6afcaa44e8dff0480fbaff9e87fc00aecc" + integrity sha512-dE2ibukxxkrde7wH9W8ozHKUO4aQnPZ6qBHtrTH9LoO836PjDdiaWO73fgPB05VfJs9FbZdmGPVEbXCeOP99Ww== + dependencies: + babel-runtime "6.x" + prop-types "^15.5.8" + rc-trigger "^2.2.2" + rc-tooltip@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-4.0.3.tgz#728b760863643ec2e85827a2e7fb28d961b3b759" @@ -20258,7 +20782,7 @@ rc-tooltip@^4.0.0: dependencies: rc-trigger "^4.0.0" -rc-trigger@^2.2.0: +rc-trigger@^2.2.0, rc-trigger@^2.2.2: version "2.6.5" resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-2.6.5.tgz#140a857cf28bd0fa01b9aecb1e26a50a700e9885" integrity sha512-m6Cts9hLeZWsTvWnuMm7oElhf+03GOjOLfTuU0QmdB9ZrW7jR2IpI5rpNM7i9MvAAlMAmTx5Zr7g3uu/aMvZAw== @@ -20294,6 +20818,17 @@ rc-util@^4.0.4, rc-util@^4.4.0, rc-util@^4.8.0: react-lifecycles-compat "^3.0.4" shallowequal "^0.2.2" +rc-util@^4.11.2: + version "4.20.3" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.20.3.tgz#c4d4ee6171cf685dc75572752a764310325888d3" + integrity sha512-NBBc9Ad5yGAVTp4jV+pD7tXQGqHxGM2onPSZFyVoJ5fuvRF+ZgzSjZ6RXLPE0pVVISRJ07h+APgLJPBcAeZQlg== + dependencies: + add-dom-event-listener "^1.1.0" + prop-types "^15.5.10" + react-is "^16.12.0" + react-lifecycles-compat "^3.0.4" + shallowequal "^1.1.0" + rc-util@^4.12.0, rc-util@^4.15.3, rc-util@^4.16.1, rc-util@^4.20.0: version "4.20.1" resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.20.1.tgz#a5976eabfc3198ed9b8e79ffb8c53c231db36e77" @@ -20362,6 +20897,18 @@ react-clientside-effect@^1.2.2: dependencies: "@babel/runtime" "^7.0.0" +react-color@2.17.0: + version "2.17.0" + resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.17.0.tgz#e14b8a11f4e89163f65a34c8b43faf93f7f02aaa" + integrity sha512-kJfE5tSaFe6GzalXOHksVjqwCPAsTl+nzS9/BWfP7j3EXbQ4IiLAF9sZGNzk3uq7HfofGYgjmcUgh0JP7xAQ0w== + dependencies: + "@icons/material" "^0.2.4" + lodash ">4.17.4" + material-colors "^1.2.1" + prop-types "^15.5.10" + reactcss "^1.2.0" + tinycolor2 "^1.4.1" + react-color@2.18.0: version "2.18.0" resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.18.0.tgz#34956f0bac394f6c3bc01692fd695644cc775ffd" @@ -20425,7 +20972,7 @@ react-dev-utils@^10.2.1: strip-ansi "6.0.0" text-table "0.2.0" -react-dev-utils@^9.0.0: +react-dev-utils@^9.0.0, react-dev-utils@^9.0.1: version "9.1.0" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-9.1.0.tgz#3ad2bb8848a32319d760d0a84c56c14bdaae5e81" integrity sha512-X2KYF/lIGyGwP/F/oXgGDF24nxDA2KC4b7AFto+eqzc/t838gpSGiaU8trTqHXOohuLxxc5qi1eDzsl9ucPDpg== @@ -20585,6 +21132,14 @@ react-helmet-async@^1.0.2: react-fast-compare "^2.0.4" shallowequal "^1.1.0" +react-highlight-words@0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.11.0.tgz#4f3c2039a8fd275f3ab795e59946b0324d8e6bee" + integrity sha512-b+fgdQXNjX6RwHfiBYn6qH2D2mJEDNLuxdsqRseIiQffoCAoj7naMQ5EktUkmo9Bh1mXq/aMpJbdx7Lf2PytcQ== + dependencies: + highlight-words-core "^1.2.0" + prop-types "^15.5.8" + react-highlight-words@0.16.0: version "0.16.0" resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.16.0.tgz#4b4b9824e3d2b98789d3e3b3aedb5e961ae1b7cf" @@ -20594,6 +21149,11 @@ react-highlight-words@0.16.0: memoize-one "^4.0.0" prop-types "^15.5.8" +react-hook-form@4.5.3: + version "4.5.3" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-4.5.3.tgz#3f9abac7bd78eedf0624d02aa9e1f8487d729e18" + integrity sha512-oQB6s3zzXbFwM8xaWEkZJZR+5KD2LwUUYTexQbpdUuFzrfs41Qg0UE3kzfzxG8shvVlzADdkYKLMXqOLWQSS/Q== + react-hook-form@5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-5.1.3.tgz#24610e11878c6bd143569ce203320f7367893e75" @@ -20814,6 +21374,11 @@ react-table@7.0.0: resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0.tgz#3297e454cbffe916626b184f5394d7e7e098fa36" integrity sha512-/RKUYLuqrupUs0qHdjdQLmgwdQ9mgXPnpshqv2T+OQUGhTu0XuLXVc6GOIywemXNf6qjL3dj81O6zALLK74Emw== +react-table@7.0.0-rc.15: + version "7.0.0-rc.15" + resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-rc.15.tgz#bb855e4e2abbb4aaf0ed2334404a41f3ada8e13a" + integrity sha512-ofMOlgrioHhhvHjvjsQkxvfQzU98cqwy6BjPGNwhLN1vhgXeWi0mUGreaCPvRenEbTiXsQbMl4k3Xmx3Mut8Rw== + react-test-renderer@16.12.0: version "16.12.0" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.12.0.tgz#11417ffda579306d4e841a794d32140f3da1b43f" @@ -20852,6 +21417,16 @@ react-textarea-autosize@^7.1.0: "@babel/runtime" "^7.1.2" prop-types "^15.6.0" +react-transition-group@2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.6.1.tgz#abf4a95e2f13fb9ba83a970a896fedbc5c4856a2" + integrity sha512-9DHwCy0aOYEe35frlEN68N9ut/THDQBLnVoQuKTvzF4/s3tk7lqkefCqxK2Nv96fOh6JXk6tQtliygk6tl3bQA== + dependencies: + dom-helpers "^3.3.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-lifecycles-compat "^3.0.4" + react-transition-group@4.3.0, react-transition-group@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.3.0.tgz#fea832e386cf8796c58b61874a3319704f5ce683" @@ -20913,15 +21488,6 @@ react@16.12.0: object-assign "^4.1.1" prop-types "^15.6.2" -react@^16.3.2: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" - integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - react@^16.8.3: version "16.10.2" resolved "https://registry.yarnpkg.com/react/-/react-16.10.2.tgz#a5ede5cdd5c536f745173c8da47bda64797a4cf0" @@ -21380,6 +21946,11 @@ regexpu-core@^4.7.0: unicode-match-property-ecmascript "^1.0.4" unicode-match-property-value-ecmascript "^1.2.0" +regextras@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/regextras/-/regextras-0.6.1.tgz#9689641bbb338e0ff7001a5c507c6a2008df7b36" + integrity sha512-EzIHww9xV2Kpqx+corS/I7OBmf2rZ0pKKJPsw5Dc+l6Zq1TslDmtRIP9maVn3UH+72MIXmn8zzDgP07ihQogUA== + regextras@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/regextras/-/regextras-0.7.0.tgz#2298bef8cfb92b1b7e3b9b12aa8f69547b7d71e4" @@ -22302,7 +22873,7 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -22457,6 +23028,11 @@ shallow-clone@^3.0.0: dependencies: kind-of "^6.0.2" +shallow-equal@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da" + integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA== + shallow-equal@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.0.tgz#fd828d2029ff4e19569db7e19e535e94e2d1f5cc" @@ -22549,7 +23125,7 @@ simple-get@^3.0.3: once "^1.3.1" simple-concat "^1.0.0" -simple-git@^1.132.0: +simple-git@^1.112.0, simple-git@^1.132.0: version "1.132.0" resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-1.132.0.tgz#53ac4c5ec9e74e37c2fd461e23309f22fcdf09b1" integrity sha512-xauHm1YqCTom1sC9eOjfq3/9RKiUA9iPnxBbrY2DdL8l4ADMu0jjM5l5lphQP5YWNqAL2aXC/OeuQ76vHtW5fg== @@ -23625,7 +24201,7 @@ table@^5.2.3: slice-ansi "^2.1.0" string-width "^3.0.0" -tapable@^1.0.0, tapable@^1.1.3: +tapable@^1.0.0, tapable@^1.1.0, tapable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== @@ -23770,31 +24346,31 @@ terser-webpack-plugin@2.3.5, terser-webpack-plugin@^2.3.4: terser "^4.4.3" webpack-sources "^1.4.3" -terser-webpack-plugin@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4" - integrity sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg== +terser-webpack-plugin@^1.1.0, terser-webpack-plugin@^1.3.0, terser-webpack-plugin@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c" + integrity sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA== dependencies: cacache "^12.0.2" find-cache-dir "^2.1.0" is-wsl "^1.1.0" schema-utils "^1.0.0" - serialize-javascript "^1.7.0" + serialize-javascript "^2.1.2" source-map "^0.6.1" terser "^4.1.2" webpack-sources "^1.4.0" worker-farm "^1.7.0" -terser-webpack-plugin@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c" - integrity sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA== +terser-webpack-plugin@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4" + integrity sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg== dependencies: cacache "^12.0.2" find-cache-dir "^2.1.0" is-wsl "^1.1.0" schema-utils "^1.0.0" - serialize-javascript "^2.1.2" + serialize-javascript "^1.7.0" source-map "^0.6.1" terser "^4.1.2" webpack-sources "^1.4.0" @@ -24228,6 +24804,17 @@ ts-map@^1.0.3: resolved "https://registry.yarnpkg.com/ts-map/-/ts-map-1.0.3.tgz#1c4d218dec813d2103b7e04e4bcf348e1471c1ff" integrity sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w== +ts-node@8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.5.0.tgz#bc7d5a39133d222bf25b1693651e4d893785f884" + integrity sha512-fbG32iZEupNV2E2Fd2m2yt1TdAwR3GTCrJQBHDevIiEBNy1A8kqnyl1fv7jmRmmbtcapFab2glZXHJvfD1ed0Q== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "^3.0.0" + ts-node@8.8.1: version "8.8.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.8.1.tgz#7c4d3e9ed33aa703b64b28d7f9d194768be5064d" @@ -25015,6 +25602,15 @@ warning@^4.0.1, warning@^4.0.2, warning@^4.0.3: dependencies: loose-envify "^1.0.0" +watchpack@^1.5.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.1.tgz#280da0a8718592174010c078c7585a74cd8cd0e2" + integrity sha512-+IF9hfUFOrYOOaKyfaI7h7dquUIOgyEMoQMLA7OP5FxegKA2+XdXThAZ9TU2kucfhDH7rfMHs1oPYziVGWRnZA== + dependencies: + chokidar "^2.1.8" + graceful-fs "^4.1.2" + neo-async "^2.5.0" + watchpack@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" @@ -25178,7 +25774,7 @@ webpack-merge@4.2.2: dependencies: lodash "^4.17.15" -webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: +webpack-sources@^1.1.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== @@ -25193,6 +25789,36 @@ webpack-virtual-modules@^0.2.0: dependencies: debug "^3.0.0" +webpack@4.35.0: + version "4.35.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.35.0.tgz#ad3f0f8190876328806ccb7a36f3ce6e764b8378" + integrity sha512-M5hL3qpVvtr8d4YaJANbAQBc4uT01G33eDpl/psRTBCfjxFTihdhin1NtAKB1ruDwzeVdcsHHV3NX+QsAgOosw== + dependencies: + "@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" + ajv-keywords "^3.1.0" + chrome-trace-event "^1.0.0" + enhanced-resolve "^4.1.0" + eslint-scope "^4.0.0" + json-parse-better-errors "^1.0.2" + loader-runner "^2.3.0" + loader-utils "^1.1.0" + memory-fs "~0.4.1" + micromatch "^3.1.8" + mkdirp "~0.5.0" + neo-async "^2.5.0" + node-libs-browser "^2.0.0" + schema-utils "^1.0.0" + tapable "^1.1.0" + terser-webpack-plugin "^1.1.0" + watchpack "^1.5.0" + webpack-sources "^1.3.0" + webpack@4.41.5: version "4.41.5" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.5.tgz#3210f1886bce5310e62bb97204d18c263341b77c" @@ -25816,7 +26442,7 @@ yauzl@2.4.1: dependencies: fd-slicer "~1.0.1" -yn@3.1.1: +yn@3.1.1, yn@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==