From 01da88ffd3d9181a2cb299cd358b30bfaa7817a0 Mon Sep 17 00:00:00 2001 From: Everett Date: Tue, 19 May 2020 15:33:08 -0400 Subject: [PATCH] Test quality metrics and DetailsCard (#577) * Test existing files + CircularProgressbar * Test QualityMetrics/!index, move & rename Resizer * Add key to metric details, test QualityMetrics * Move ExamplesLink&DetailsCard, split&test DC * Clean up types files * Clean up types files * Clean up tests Signed-off-by: Everett Ross Signed-off-by: vvvprabhakar --- packages/jaeger-ui/src/api/jaeger.js | 6 +- packages/jaeger-ui/src/api/jaeger.test.js | 20 + .../App/__snapshots__/index.test.js.snap | 88 ++++ .../src/components/App/index.test.js | 7 +- .../SidePanel/DetailsCard/index.tsx | 235 ----------- .../SidePanel/DetailsPanel.test.js | 4 +- .../SidePanel/DetailsPanel.tsx | 20 +- .../__snapshots__/DetailsPanel.test.js.snap | 16 +- .../QualityMetrics/BannerText.test.js | 44 ++ .../QualityMetrics/CountCard.test.js | 36 ++ .../components/QualityMetrics/CountCard.tsx | 4 +- .../components/QualityMetrics/Header.test.js | 96 +++++ .../QualityMetrics/MetricCard.test.js | 104 +++++ .../components/QualityMetrics/MetricCard.tsx | 3 +- .../QualityMetrics/ScoreCard.test.js | 70 ++++ .../__snapshots__/BannerText.test.js.snap | 23 ++ .../__snapshots__/CountCard.test.js.snap | 46 +++ .../__snapshots__/Header.test.js.snap | 75 ++++ .../__snapshots__/MetricCard.test.js.snap | 376 ++++++++++++++++++ .../__snapshots__/ScoreCard.test.js.snap | 98 +++++ .../__snapshots__/index.test.js.snap | 138 +++++++ .../components/QualityMetrics/index.test.js | 299 ++++++++++++++ .../src/components/QualityMetrics/index.tsx | 11 +- .../src/components/QualityMetrics/types.tsx | 8 +- .../src/components/QualityMetrics/url.test.js | 97 +++++ .../src/components/QualityMetrics/url.tsx | 2 +- .../components/SearchTracePage/url.test.js | 63 ++- .../TimelineHeaderRow.test.js | 6 +- .../TimelineHeaderRow/TimelineHeaderRow.tsx | 9 +- .../common/CircularProgressbar.test.js | 41 ++ .../common/DetailsCard/DetailList.test.js | 34 ++ .../common/DetailsCard/DetailList.tsx | 31 ++ .../common/DetailsCard/DetailTable.test.js | 280 +++++++++++++ .../common/DetailsCard/DetailTable.tsx | 147 +++++++ .../__snapshots__/DetailList.test.js.snap | 29 ++ .../__snapshots__/DetailTable.test.js.snap | 207 ++++++++++ .../__snapshots__/index.test.js.snap | 242 +++++++++++ .../DetailsCard/index.css | 0 .../common/DetailsCard/index.test.js | 74 ++++ .../components/common/DetailsCard/index.tsx | 97 +++++ .../components/common/DetailsCard/types.tsx | 34 ++ .../components/common/ExamplesLink.test.js | 69 ++++ .../ExamplesLink.tsx | 9 +- .../VerticalResizer.css} | 40 +- .../VerticalResizer.test.js} | 76 +++- .../VerticalResizer.tsx} | 25 +- .../CircularProgressbar.test.js.snap | 79 ++++ .../__snapshots__/ExamplesLink.test.js.snap | 37 ++ .../__snapshots__/index.test.js.snap | 23 ++ .../path-agnostic-decorations/index.test.js | 108 +++++ .../model/path-agnostic-decorations/types.tsx | 26 -- 51 files changed, 3354 insertions(+), 358 deletions(-) create mode 100644 packages/jaeger-ui/src/components/App/__snapshots__/index.test.js.snap delete mode 100644 packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/BannerText.test.js create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/CountCard.test.js create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/Header.test.js create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/MetricCard.test.js create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.test.js create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/BannerText.test.js.snap create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/CountCard.test.js.snap create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/Header.test.js.snap create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/MetricCard.test.js.snap create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/ScoreCard.test.js.snap create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/index.test.js.snap create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/index.test.js create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/url.test.js create mode 100644 packages/jaeger-ui/src/components/common/CircularProgressbar.test.js create mode 100644 packages/jaeger-ui/src/components/common/DetailsCard/DetailList.test.js create mode 100644 packages/jaeger-ui/src/components/common/DetailsCard/DetailList.tsx create mode 100644 packages/jaeger-ui/src/components/common/DetailsCard/DetailTable.test.js create mode 100644 packages/jaeger-ui/src/components/common/DetailsCard/DetailTable.tsx create mode 100644 packages/jaeger-ui/src/components/common/DetailsCard/__snapshots__/DetailList.test.js.snap create mode 100644 packages/jaeger-ui/src/components/common/DetailsCard/__snapshots__/DetailTable.test.js.snap create mode 100644 packages/jaeger-ui/src/components/common/DetailsCard/__snapshots__/index.test.js.snap rename packages/jaeger-ui/src/components/{DeepDependencies/SidePanel => common}/DetailsCard/index.css (100%) create mode 100644 packages/jaeger-ui/src/components/common/DetailsCard/index.test.js create mode 100644 packages/jaeger-ui/src/components/common/DetailsCard/index.tsx create mode 100644 packages/jaeger-ui/src/components/common/DetailsCard/types.tsx create mode 100644 packages/jaeger-ui/src/components/common/ExamplesLink.test.js rename packages/jaeger-ui/src/components/{QualityMetrics => common}/ExamplesLink.tsx (92%) rename packages/jaeger-ui/src/components/{TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.css => common/VerticalResizer.css} (55%) rename packages/jaeger-ui/src/components/{TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.test.js => common/VerticalResizer.test.js} (50%) rename packages/jaeger-ui/src/components/{TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx => common/VerticalResizer.tsx} (84%) create mode 100644 packages/jaeger-ui/src/components/common/__snapshots__/CircularProgressbar.test.js.snap create mode 100644 packages/jaeger-ui/src/components/common/__snapshots__/ExamplesLink.test.js.snap create mode 100644 packages/jaeger-ui/src/model/path-agnostic-decorations/__snapshots__/index.test.js.snap create mode 100644 packages/jaeger-ui/src/model/path-agnostic-decorations/index.test.js diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index c18bae3a96..7db5b041bd 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -80,9 +80,6 @@ const JaegerAPI = { archiveTrace(id) { return getJSON(`${this.apiRoot}archive/${id}`, { method: 'POST' }); }, - fetchQualityMetrics(service, lookback) { - return getJSON(`/qualitymetrics-v2`, { query: { service, lookback } }); - }, fetchDecoration(url) { return getJSON(url); }, @@ -92,6 +89,9 @@ const JaegerAPI = { fetchDependencies(endTs = new Date().getTime(), lookback = DEFAULT_DEPENDENCY_LOOKBACK) { return getJSON(`${this.apiRoot}dependencies`, { query: { endTs, lookback } }); }, + fetchQualityMetrics(service, lookback) { + return getJSON(`/qualitymetrics-v2`, { query: { service, lookback } }); + }, fetchServiceOperations(serviceName) { return getJSON(`${this.apiRoot}services/${encodeURIComponent(serviceName)}/operations`); }, diff --git a/packages/jaeger-ui/src/api/jaeger.test.js b/packages/jaeger-ui/src/api/jaeger.test.js index fc7a57b6f4..045bdd9856 100644 --- a/packages/jaeger-ui/src/api/jaeger.test.js +++ b/packages/jaeger-ui/src/api/jaeger.test.js @@ -48,6 +48,14 @@ describe('archiveTrace', () => { }); }); +describe('fetchDecoration', () => { + it('GETs the specified url', () => { + const url = 'foo.bar.baz'; + JaegerAPI.fetchDecoration(url); + expect(fetchMock).toHaveBeenLastCalledWith(url, defaultOptions); + }); +}); + describe('fetchDeepDependencyGraph', () => { it('GETs the specified query', () => { const query = { service: 'serviceName', start: 400, end: 800 }; @@ -81,6 +89,18 @@ describe('fetchDependencies', () => { }); }); +describe('fetchQualityMetrics', () => { + it('GETs the specified service and lookback', () => { + const lookback = '3h'; + const service = 'test-service'; + JaegerAPI.fetchQualityMetrics(service, lookback); + expect(fetchMock).toHaveBeenLastCalledWith( + `/qualitymetrics-v2?${queryString.stringify({ service, lookback })}`, + defaultOptions + ); + }); +}); + describe('fetchServiceServerOps', () => { it('GETs the specified query', () => { const service = 'serviceName'; diff --git a/packages/jaeger-ui/src/components/App/__snapshots__/index.test.js.snap b/packages/jaeger-ui/src/components/App/__snapshots__/index.test.js.snap new file mode 100644 index 0000000000..601feb1701 --- /dev/null +++ b/packages/jaeger-ui/src/components/App/__snapshots__/index.test.js.snap @@ -0,0 +1,88 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`JaegerUIApp does not explode 1`] = ` + + + + + + + + + + + + + + + + + + +`; diff --git a/packages/jaeger-ui/src/components/App/index.test.js b/packages/jaeger-ui/src/components/App/index.test.js index bfdea6cb9b..fd5bfae426 100644 --- a/packages/jaeger-ui/src/components/App/index.test.js +++ b/packages/jaeger-ui/src/components/App/index.test.js @@ -17,6 +17,9 @@ import { shallow } from 'enzyme'; import JaegerUIApp from './index'; -it('JaegerUIApp does not explode', () => { - shallow(); +describe('JaegerUIApp', () => { + it('does not explode', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx deleted file mode 100644 index 10543f1c97..0000000000 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright (c) 2020 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 { List, Table } from 'antd'; -import cx from 'classnames'; -import _isEmpty from 'lodash/isEmpty'; -import MdKeyboardArrowDown from 'react-icons/lib/md/keyboard-arrow-down'; - -import ExamplesLink from '../../../QualityMetrics/ExamplesLink'; - -import { - TExample, - TPadColumnDef, - TPadColumnDefs, - TPadDetails, - TPadRow, - TStyledValue, -} from '../../../../model/path-agnostic-decorations/types'; - -import './index.css'; - -const { Column } = Table; -const { Item } = List; - -type TProps = { - className?: string; - collapsible?: boolean; - columnDefs?: TPadColumnDefs; - description?: string; - details: TPadDetails; - header: string; -}; - -type TState = { - collapsed: boolean; -}; - -function isList(arr: string[] | TPadRow[]): arr is string[] { - return typeof arr[0] === 'string'; -} - -export default class DetailsCard extends React.PureComponent { - state: TState; - - static renderList(details: string[]) { - return ( - ( - - {s} - - )} - /> - ); - } - - static renderColumn(def: TPadColumnDef | string) { - let dataIndex: string; - let key: string; - let sortable: boolean = true; - let style: React.CSSProperties | undefined; - let title: string; - if (typeof def === 'string') { - // eslint-disable-next-line no-multi-assign - key = title = dataIndex = def; - } else { - // eslint-disable-next-line no-multi-assign - key = title = dataIndex = def.key; - if (def.label) title = def.label; - if (def.styling) style = def.styling; - if (def.preventSort) sortable = false; - } - - const props = { - dataIndex, - key, - title, - onCell: (row: TPadRow) => { - const cellData = row[dataIndex]; - if (!cellData || typeof cellData !== 'object' || Array.isArray(cellData)) return null; - const { styling } = cellData; - if (_isEmpty(styling)) return null; - return { - style: styling, - }; - }, - onHeaderCell: () => ({ - style, - }), - render: (cellData: undefined | string | TStyledValue) => { - if (!cellData || typeof cellData !== 'object') return cellData; - if (Array.isArray(cellData)) return ; - if (!cellData.linkTo) return cellData.value; - return ( - - {cellData.value} - - ); - }, - sorter: - sortable && - ((a: TPadRow, b: TPadRow) => { - const aData = a[dataIndex]; - let aValue; - if (Array.isArray(aData)) aValue = aData.length; - else if (typeof aData === 'object' && typeof aData.value === 'string') aValue = aData.value; - else aValue = aData; - - const bData = b[dataIndex]; - let bValue; - if (Array.isArray(bData)) bValue = bData.length; - else if (typeof bData === 'object' && typeof bData.value === 'string') bValue = bData.value; - else bValue = bData; - - if (aValue < bValue) return -1; - return bValue < aValue ? 1 : 0; - }), - }; - - return ; - } - - constructor(props: TProps) { - super(props); - - this.state = { collapsed: Boolean(props.collapsible) }; - } - - renderTable(details: TPadRow[]) { - const { columnDefs: _columnDefs } = this.props; - const columnDefs: TPadColumnDefs = _columnDefs ? _columnDefs.slice() : []; - const knownColumns = new Set( - columnDefs.map(keyOrObj => { - if (typeof keyOrObj === 'string') return keyOrObj; - return keyOrObj.key; - }) - ); - details.forEach(row => { - Object.keys(row).forEach((col: string) => { - if (!knownColumns.has(col)) { - knownColumns.add(col); - columnDefs.push(col); - } - }); - }); - - return ( - - JSON.stringify(row, function replacer( - key: string, - value: TPadRow | string | number | TStyledValue | TExample[] - ) { - function isRow(v: typeof value): v is TPadRow { - return v === row; - } - if (isRow(value)) return value; - if (Array.isArray(value)) return JSON.stringify(value); - if (typeof value === 'object') { - if (typeof value.value === 'string') return value.value; - return value.value.key || 'Unknown'; - } - return value; - }) - } - > - {columnDefs.map(DetailsCard.renderColumn)} -
- ); - } - - renderDetails() { - const { details } = this.props; - - if (Array.isArray(details)) { - if (details.length === 0) return null; - - if (isList(details)) return DetailsCard.renderList(details); - return this.renderTable(details); - } - - return {details}; - } - - toggleCollapse = () => { - this.setState((prevState: TState) => ({ - collapsed: !prevState.collapsed, - })); - }; - - render() { - const { collapsed } = this.state; - const { className, collapsible, description, header } = this.props; - - return ( -
-
- {collapsible && ( - - )} -
- {header} - {description &&

{description}

} -
-
-
- {this.renderDetails()} -
-
- ); - } -} diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js index a349d4fb59..3b3626e36c 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js @@ -18,7 +18,7 @@ import _set from 'lodash/set'; import stringSupplant from '../../../utils/stringSupplant'; import JaegerAPI from '../../../api/jaeger'; -import ColumnResizer from '../../TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer'; +import VerticalResizer from '../../common/VerticalResizer'; import { UnconnectedDetailsPanel as DetailsPanel } from './DetailsPanel'; describe('', () => { @@ -296,7 +296,7 @@ describe('', () => { const wrapper = shallow(); expect(wrapper.state('width')).not.toBe(width); - wrapper.find(ColumnResizer).prop('onChange')(width); + wrapper.find(VerticalResizer).prop('onChange')(width); expect(wrapper.state('width')).toBe(width); }); }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx index 18451fa3b3..68e84c5ac3 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx @@ -18,18 +18,16 @@ import _get from 'lodash/get'; import { connect } from 'react-redux'; import BreakableText from '../../common/BreakableText'; +import DetailsCard from '../../common/DetailsCard'; import LoadingIndicator from '../../common/LoadingIndicator'; import NewWindowIcon from '../../common/NewWindowIcon'; +import VerticalResizer from '../../common/VerticalResizer'; import JaegerAPI from '../../../api/jaeger'; -import ColumnResizer from '../../TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer'; import extractDecorationFromState, { TDecorationFromState } from '../../../model/path-agnostic-decorations'; -import { - TPathAgnosticDecorationSchema, - TPadColumnDefs, - TPadDetails, -} from '../../../model/path-agnostic-decorations/types'; import stringSupplant from '../../../utils/stringSupplant'; -import DetailsCard from './DetailsCard'; + +import { TPathAgnosticDecorationSchema } from '../../../model/path-agnostic-decorations/types'; +import { TColumnDefs, TDetails } from '../../common/DetailsCard/types'; import './DetailsPanel.css'; @@ -40,8 +38,8 @@ type TProps = TDecorationFromState & { }; type TState = { - columnDefs?: TPadColumnDefs; - details?: TPadDetails; + columnDefs?: TColumnDefs; + details?: TDetails; detailsErred?: boolean; detailsLoading?: boolean; width?: number; @@ -111,7 +109,7 @@ export class UnconnectedDetailsPanel extends React.PureComponent details = `\`${getDetailPath}\` not found in response`; detailsErred = true; } - const columnDefs: TPadColumnDefs = getDefPath ? _get(res, getDefPath, []) : []; + const columnDefs: TColumnDefs = getDefPath ? _get(res, getDefPath, []) : []; this.setState({ columnDefs, details, detailsErred, detailsLoading: false }); }) @@ -172,7 +170,7 @@ export class UnconnectedDetailsPanel extends React.PureComponent header="Details" /> )} - + ); } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/DetailsPanel.test.js.snap b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/DetailsPanel.test.js.snap index 64143fcf26..f14a7e6833 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/DetailsPanel.test.js.snap +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/DetailsPanel.test.js.snap @@ -30,7 +30,7 @@ exports[` render renders 1`] = ` > test decorationValue - render renders detailLink 1`] = ` small={false} /> - render renders details 1`] = ` details="details string" header="Details" /> - render renders details error 1`] = ` details="details error" header="Details" /> - render renders omitted array of operations 1`] = ` > test decorationValue - render renders while loading 1`] = ` small={false} /> - render renders with operation 1`] = ` > test decorationValue - render renders with progressbar 1`] = ` stand-in progressbar - { + it('renders null when props.bannerText is falsy', () => { + expect(shallow().type()).toBe(null); + }); + + it('renders header when props.bannerText is a string', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('renders styled header when props.bannerText is a styled value', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); + }); +}); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/CountCard.test.js b/packages/jaeger-ui/src/components/QualityMetrics/CountCard.test.js new file mode 100644 index 0000000000..d7e8438e31 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/CountCard.test.js @@ -0,0 +1,36 @@ +// Copyright (c) 2020 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 CountCard from './CountCard'; + +describe('CountCard', () => { + const count = 108; + const title = 'Test Title'; + + it('renders null when props.count or props.title is absent', () => { + expect(shallow().type()).toBe(null); + expect(shallow().type()).toBe(null); + }); + + it('renders as expected when given count and title', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('renders as expected when given count, title, and examples', () => { + expect(shallow()).toMatchSnapshot(); + }); +}); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx b/packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx index 9ba6033a8d..8345b20f81 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx @@ -14,9 +14,7 @@ import * as React from 'react'; -import ExamplesLink from './ExamplesLink'; - -import { TExample } from '../../model/path-agnostic-decorations/types'; +import ExamplesLink, { TExample } from '../common/ExamplesLink'; import './CountCard.css'; diff --git a/packages/jaeger-ui/src/components/QualityMetrics/Header.test.js b/packages/jaeger-ui/src/components/QualityMetrics/Header.test.js new file mode 100644 index 0000000000..9421da27d2 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/Header.test.js @@ -0,0 +1,96 @@ +// Copyright (c) 2020 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 { InputNumber } from 'antd'; +import debounceMock from 'lodash/debounce'; + +import Header from './Header'; + +jest.mock('lodash/debounce'); + +describe('Header', () => { + const lookback = 4; + const minProps = { + lookback, + setLookback: jest.fn(), + setService: jest.fn(), + }; + const service = 'test service'; + const props = { + ...minProps, + service, + services: ['foo', 'bar', 'baz'], + }; + let wrapper; + let callDebouncedFn; + let setLookbackSpy; + + beforeAll(() => { + debounceMock.mockImplementation(fn => { + setLookbackSpy = jest.fn((...args) => { + callDebouncedFn = () => fn(...args); + }); + return setLookbackSpy; + }); + }); + + beforeEach(() => { + props.setLookback.mockReset(); + setLookbackSpy = undefined; + wrapper = shallow(
); + }); + + describe('rendering', () => { + it('renders as expected with minimum props', () => { + wrapper = shallow(
); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders as expected with full props', () => { + expect(wrapper).toMatchSnapshot(); + }); + + it('renders props.lookback when state.ownInputValue is `undefined`', () => { + expect(wrapper.find(InputNumber).prop('value')).toBe(lookback); + }); + + it('renders state.ownInputValue when it is not `undefined` regardless of props.lookback', () => { + const ownInputValue = 27; + wrapper.setState({ ownInputValue }); + expect(wrapper.find(InputNumber).prop('value')).toBe(ownInputValue); + }); + }); + + describe('setting lookback', () => { + it('no-ops for string values', () => { + wrapper.find(InputNumber).prop('onChange')('foo'); + expect(wrapper.state('ownInputValue')).toBe(undefined); + }); + + it('updates state with numeric value, then clears state and calls props.setLookback after debounce', () => { + const value = 42; + wrapper.find(InputNumber).prop('onChange')(value); + + expect(wrapper.state('ownInputValue')).toBe(value); + expect(setLookbackSpy).toHaveBeenCalledWith(42); + expect(props.setLookback).not.toHaveBeenCalled(); + + callDebouncedFn(); + expect(wrapper.state('ownInputValue')).toBe(undefined); + expect(props.setLookback).toHaveBeenCalledWith(42); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.test.js b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.test.js new file mode 100644 index 0000000000..5476f51e20 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.test.js @@ -0,0 +1,104 @@ +// Copyright (c) 2020 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 MetricCard from './MetricCard'; + +describe('MetricCard', () => { + const metric = { + name: 'Metric Name', + description: 'Metric Description', + metricDocumentationLink: 'metric.documentation.link', + passCount: 108, + passExamples: ['foo'], + failureCount: 255, + failureExamples: ['bar'], + exemptionCount: 42, + exemptionExamples: ['baz'], + }; + const details = [ + { + columns: ['col0', 'col1'], + description: 'Details[0] Description', + }, + { + columns: ['col2', 'col3'], + description: 'Details[1] Description', + rows: [], + }, + { + columns: ['col4', 'col5'], + description: 'Details[2] Description', + rows: [ + { + col4: 'value for fourth column', + col5: 'value for fifth column', + }, + ], + }, + { + columns: ['col6', 'col7'], + description: 'Details[3] Description', + header: 'Details[3] Header', + rows: [ + { + col6: 'value for sixth column', + col7: 'value for seventh column', + }, + ], + }, + ]; + + it('renders as expected without details', () => { + expect(shallow()).toMatchSnapshot(); + expect( + shallow( + + ) + ).toMatchSnapshot(); + }); + + it('renders as expected with details', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); + }); + + it('renders as expected when passCount is zero', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); + }); +}); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx index 82dad7c4d4..6ac7d7bd3a 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx @@ -17,7 +17,7 @@ import { Tooltip } from 'antd'; import CircularProgressbar from '../common/CircularProgressbar'; import NewWindowIcon from '../common/NewWindowIcon'; -import DetailsCard from '../DeepDependencies/SidePanel/DetailsCard'; +import DetailsCard from '../common/DetailsCard'; import CountCard from './CountCard'; import { TQualityMetrics } from './types'; @@ -80,6 +80,7 @@ export default class MetricCard extends React.PureComponent { detail => Boolean(detail.rows && detail.rows.length) && ( { + const link = 'test.link'; + const label = 'Test Score'; + const value = 42; + const max = 108; + + it('renders as expected when score is below max', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); + }); + + it('renders as expected when score is max', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); + }); + + it('renders as expected when score is zero', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); + }); +}); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/BannerText.test.js.snap b/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/BannerText.test.js.snap new file mode 100644 index 0000000000..8e84cf4911 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/BannerText.test.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BannerText renders header when props.bannerText is a string 1`] = ` +
+ foo text +
+`; + +exports[`BannerText renders styled header when props.bannerText is a styled value 1`] = ` +
+ foo text +
+`; diff --git a/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/CountCard.test.js.snap b/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/CountCard.test.js.snap new file mode 100644 index 0000000000..0853a27fa1 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/CountCard.test.js.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CountCard renders as expected when given count and title 1`] = ` +
+ + Test Title + + + 108 + + +
+`; + +exports[`CountCard renders as expected when given count, title, and examples 1`] = ` +
+ + Test Title + + + 108 + + +
+`; diff --git a/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/Header.test.js.snap b/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/Header.test.js.snap new file mode 100644 index 0000000000..0c375f82ea --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/Header.test.js.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header rendering renders as expected with full props 1`] = ` +
+ + + + + (in hours) + +
+`; + +exports[`Header rendering renders as expected with minimum props 1`] = ` +
+ + + + + (in hours) + +
+`; diff --git a/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/MetricCard.test.js.snap b/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/MetricCard.test.js.snap new file mode 100644 index 0000000000..0e2ec16626 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/MetricCard.test.js.snap @@ -0,0 +1,376 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MetricCard renders as expected when passCount is zero 1`] = ` +
+
+ +
+
+ + Metric Name + + + + + + + +

+ Metric Description +

+
+ + + +
+
+
+`; + +exports[`MetricCard renders as expected with details 1`] = ` +
+
+ +
+
+ + Metric Name + + + + + + + +

+ Metric Description +

+
+ + + +
+ + +
+
+`; + +exports[`MetricCard renders as expected without details 1`] = ` +
+
+ +
+
+ + Metric Name + + + + + + + +

+ Metric Description +

+
+ + + +
+
+
+`; + +exports[`MetricCard renders as expected without details 2`] = ` +
+
+ +
+
+ + Metric Name + + + + + + + +

+ Metric Description +

+
+ + + +
+
+
+`; diff --git a/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/ScoreCard.test.js.snap b/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/ScoreCard.test.js.snap new file mode 100644 index 0000000000..448f6364b9 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/ScoreCard.test.js.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ScoreCard renders as expected when score is below max 1`] = ` +
+ + Test Score + +
+ +
+ + How to improve + + +
+`; + +exports[`ScoreCard renders as expected when score is max 1`] = ` +
+ + Test Score + +
+ +
+ + Great! What does this mean + + +
+`; + +exports[`ScoreCard renders as expected when score is zero 1`] = ` +
+ + Test Score + +
+ +
+ + How to improve + + +
+`; diff --git a/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/index.test.js.snap b/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/index.test.js.snap new file mode 100644 index 0000000000..54a3975b79 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/__snapshots__/index.test.js.snap @@ -0,0 +1,138 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`QualityMetrics UnconnectedQualityMetrics render renders when errored 1`] = ` +
+
+
+ Error message +
+
+`; + +exports[`QualityMetrics UnconnectedQualityMetrics render renders when loading 1`] = ` +
+
+ +
+`; + +exports[`QualityMetrics UnconnectedQualityMetrics render renders with metrics 1`] = ` +
+
+ +
+
+ + +
+
+ + +
+
+
+`; + +exports[`QualityMetrics UnconnectedQualityMetrics render renders without loading, error, or metrics 1`] = ` +
+
+
+`; diff --git a/packages/jaeger-ui/src/components/QualityMetrics/index.test.js b/packages/jaeger-ui/src/components/QualityMetrics/index.test.js new file mode 100644 index 0000000000..102f9e1911 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/index.test.js @@ -0,0 +1,299 @@ +// Copyright (c) 2020 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 JaegerAPI from '../../api/jaeger'; +import Header from './Header'; +import * as getUrl from './url'; +import { UnconnectedQualityMetrics, mapDispatchToProps, mapStateToProps } from '.'; + +describe('QualityMetrics', () => { + describe('UnconnectedQualityMetrics', () => { + const props = { + fetchServices: jest.fn(), + history: { + push: jest.fn(), + }, + lookback: 48, + service: 'test-service', + services: ['foo', 'bar', 'baz'], + }; + const { service: _s, ...propsWithoutService } = props; + let fetchQualityMetricsSpy; + let promise; + let res; + let rej; + + beforeAll(() => { + fetchQualityMetricsSpy = jest.spyOn(JaegerAPI, 'fetchQualityMetrics').mockImplementation(() => { + promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + return promise; + }); + }); + + beforeEach(() => { + props.history.push.mockClear(); + props.fetchServices.mockClear(); + fetchQualityMetricsSpy.mockClear(); + }); + + describe('constructor', () => { + it('fetches services if none are provided', () => { + const { services: _ses, ...propsWithoutServices } = props; + // eslint-disable-next-line no-new + new UnconnectedQualityMetrics(propsWithoutServices); + expect(props.fetchServices).toHaveBeenCalledTimes(1); + }); + + it('no-ops if services are provided', () => { + // eslint-disable-next-line no-new + new UnconnectedQualityMetrics(props); + expect(props.fetchServices).not.toHaveBeenCalled(); + }); + }); + + describe('componentDidMount', () => { + it('fetches quality metrics', () => { + shallow(); + expect(fetchQualityMetricsSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('componentDidUpdate', () => { + const expectedState = { + qualityMetrics: undefined, + error: undefined, + loading: true, + }; + const initialState = { + qualityMetrics: { + scores: [], + metrics: [], + }, + error: {}, + loading: false, + }; + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + wrapper.setState(initialState); + }); + + it('clears state and fetches quality metrics if service changed', () => { + expect(fetchQualityMetricsSpy).toHaveBeenCalledTimes(1); + wrapper.setProps({ service: `not-${props.service}` }); + expect(fetchQualityMetricsSpy).toHaveBeenCalledTimes(2); + expect(wrapper.state()).toEqual(expectedState); + }); + + it('clears state and fetches quality metrics if lookback changed', () => { + expect(fetchQualityMetricsSpy).toHaveBeenCalledTimes(1); + wrapper.setProps({ lookback: `not-${props.lookback}` }); + expect(fetchQualityMetricsSpy).toHaveBeenCalledTimes(2); + expect(wrapper.state()).toEqual(expectedState); + }); + + it('no-ops if neither service or lookback changed', () => { + expect(fetchQualityMetricsSpy).toHaveBeenCalledTimes(1); + wrapper.setProps({ services: [] }); + expect(fetchQualityMetricsSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('fetches quality metrics', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('no-ops on falsy service', () => { + expect(wrapper.state('loading')).toBe(undefined); + }); + + it('fetches quality metrics and updates state on success', async () => { + wrapper.setProps({ service: props.service }); + expect(wrapper.state('loading')).toBe(true); + + const qualityMetrics = {}; + res(qualityMetrics); + await promise; + + expect(wrapper.state('loading')).toBe(false); + expect(wrapper.state('qualityMetrics')).toBe(qualityMetrics); + }); + + it('fetches quality metrics and updates state on error', async () => { + wrapper.setProps({ service: props.service }); + expect(wrapper.state('loading')).toBe(true); + + const error = {}; + rej(error); + await promise.catch(() => {}); + + expect(wrapper.state('loading')).toBe(false); + expect(wrapper.state('error')).toBe(error); + }); + }); + + describe('url updates', () => { + const testLookback = props.lookback * 4; + const testUrl = 'test.url'; + let getUrlSpy; + let latestUrl; + let setLookback; + let wrapper; + + beforeAll(() => { + getUrlSpy = jest.spyOn(getUrl, 'getUrl').mockImplementation(() => { + latestUrl = `${testUrl}.${getUrlSpy.mock.calls.length}`; + return latestUrl; + }); + wrapper = shallow(); + setLookback = wrapper.find(Header).prop('setLookback'); + }); + + it('sets service', () => { + const testService = `new ${props.service}`; + wrapper.find(Header).prop('setService')(testService); + expect(getUrlSpy).toHaveBeenLastCalledWith({ + lookback: props.lookback, + service: testService, + }); + expect(props.history.push).toHaveBeenLastCalledWith(latestUrl); + }); + + it('sets lookback', () => { + setLookback(testLookback); + expect(getUrlSpy).toHaveBeenLastCalledWith({ + lookback: testLookback, + service: props.service, + }); + expect(props.history.push).toHaveBeenLastCalledWith(latestUrl); + }); + + it('sets lookback without service', () => { + wrapper.setProps({ service: undefined }); + setLookback(testLookback); + expect(getUrlSpy).toHaveBeenLastCalledWith({ + lookback: testLookback, + service: '', + }); + expect(props.history.push).toHaveBeenLastCalledWith(latestUrl); + }); + + it('ignores falsy lookback', () => { + setLookback(null); + expect(props.history.push).not.toHaveBeenCalled(); + }); + + it('ignores string lookback', () => { + setLookback(props.service); + expect(props.history.push).not.toHaveBeenCalled(); + }); + + it('ignores less than one lookback', () => { + setLookback(-1); + expect(props.history.push).not.toHaveBeenCalled(); + }); + + it('ignores fractional lookback', () => { + setLookback(2.5); + expect(props.history.push).not.toHaveBeenCalled(); + }); + }); + + describe('render', () => { + it('renders without loading, error, or metrics', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('renders when loading', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('renders when errored', async () => { + const wrapper = shallow(); + const message = 'Error message'; + rej({ message }); + await promise.catch(() => {}); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders with metrics', async () => { + const wrapper = shallow(); + const metrics = { + bannerText: 'test banner text', + traceQualityDocumentationLink: 'trace.quality.documentation/link', + scores: [ + { + key: 'score0', + }, + { + key: 'score1', + }, + ], + metrics: [ + { + name: 'metric 0', + }, + { + name: 'metric 1', + }, + ], + }; + res(metrics); + await promise; + expect(wrapper).toMatchSnapshot(); + }); + }); + }); + + describe('mapDispatchToProps()', () => { + it('creates the actions correctly', () => { + expect(mapDispatchToProps(() => {})).toEqual({ + fetchServices: expect.any(Function), + }); + }); + }); + + describe('mapStateToProps()', () => { + let getUrlStateSpy; + const urlState = { + lookback: 108, + service: 'test-service', + }; + + beforeAll(() => { + getUrlStateSpy = jest.spyOn(getUrl, 'getUrlState').mockImplementation(search => search && urlState); + }); + + it('gets services from redux state', () => { + const services = [urlState.service, 'foo', 'bar']; + expect(mapStateToProps({ services: { services } }, { location: {} })).toEqual({ services }); + }); + + it('gets current service and lookback from url', () => { + const search = 'test search'; + expect(mapStateToProps({ services: {} }, { location: { search } })).toEqual(urlState); + expect(getUrlStateSpy).toHaveBeenLastCalledWith(search); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/index.tsx b/packages/jaeger-ui/src/components/QualityMetrics/index.tsx index 46e7bd7af3..a13dbec670 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/index.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/index.tsx @@ -20,9 +20,9 @@ import { bindActionCreators, Dispatch } from 'redux'; import * as jaegerApiActions from '../../actions/jaeger-api'; import JaegerAPI from '../../api/jaeger'; import LoadingIndicator from '../common/LoadingIndicator'; -import DetailsCard from '../DeepDependencies/SidePanel/DetailsCard'; +import DetailsCard from '../common/DetailsCard'; +import ExamplesLink from '../common/ExamplesLink'; import BannerText from './BannerText'; -import ExamplesLink from './ExamplesLink'; import Header from './Header'; import MetricCard from './MetricCard'; import ScoreCard from './ScoreCard'; @@ -90,7 +90,7 @@ export class UnconnectedQualityMetrics extends React.PureComponent { this.setState({ qualityMetrics, loading: false }); }) @@ -195,8 +195,9 @@ export class UnconnectedQualityMetrics extends React.PureComponent { + const lookback = 42; + const service = 'test-service'; + + describe('matches', () => { + const path = 'path argument'; + let matchPathSpy; + + beforeAll(() => { + matchPathSpy = jest.spyOn(reactRouterDom, 'matchPath'); + }); + + it('calls matchPath with expected arguments', () => { + matches(path); + expect(matchPathSpy).toHaveBeenLastCalledWith(path, { + path: ROUTE_PATH, + strict: true, + exact: true, + }); + }); + + it("returns truthiness of matchPath's return value", () => { + matchPathSpy.mockReturnValueOnce(null); + expect(matches(path)).toBe(false); + matchPathSpy.mockReturnValueOnce({}); + expect(matches(path)).toBe(true); + }); + }); + + describe('getUrl', () => { + it('handles an absent param arg', () => { + expect(getUrl()).toBe(ROUTE_PATH); + }); + + it('handles param arg', () => { + const arg = { + lookback, + service, + }; + expect(getUrl(arg)).toBe(`${ROUTE_PATH}?lookback=${arg.lookback}&service=${arg.service}`); + }); + }); + + describe('getUrlState', () => { + const defaultState = { + lookback: 48, + }; + + it('defaults lookback to 48h', () => { + expect(getUrlState('')).toEqual(defaultState); + }); + + it('parses lookback from url', () => { + expect(getUrlState(`?lookback=${lookback}`)).toEqual({ + lookback, + }); + }); + + it('parses first lookback in url', () => { + expect(getUrlState(`?lookback=${lookback}&lookback="second unused lookback value"`)).toEqual({ + lookback, + }); + }); + + it('gets service from url', () => { + expect(getUrlState(`?service=${service}`)).toEqual({ + ...defaultState, + service, + }); + }); + + it('uses first service in url', () => { + expect(getUrlState(`?service=${service}&service="second unused service value"`)).toEqual({ + ...defaultState, + service, + }); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/url.tsx b/packages/jaeger-ui/src/components/QualityMetrics/url.tsx index 71031db32c..e4608b0823 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/url.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/url.tsx @@ -43,7 +43,7 @@ export const getUrlState = memoizeOne(function getUrlState(search: string): TRet const lookbackStr = Array.isArray(lookbackFromUrl) ? lookbackFromUrl[0] : lookbackFromUrl; const lookback = lookbackStr && Number.parseInt(lookbackStr, 10); const rv: TReturnValue = { - lookback: 1, + lookback: 48, }; if (service) rv.service = service; if (lookback) rv.lookback = lookback; diff --git a/packages/jaeger-ui/src/components/SearchTracePage/url.test.js b/packages/jaeger-ui/src/components/SearchTracePage/url.test.js index af417be216..d1143b16c7 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/url.test.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/url.test.js @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { getUrl, getUrlState, isSameQuery } from './url'; +import * as reactRouterDom from 'react-router-dom'; + +import { MAX_LENGTH } from '../DeepDependencies/Graph/DdgNodeContent/constants'; +import { ROUTE_PATH, getUrl, getUrlState, isSameQuery, matches } from './url'; describe('SearchTracePage/url', () => { const span0 = 'span-0'; @@ -22,6 +25,31 @@ describe('SearchTracePage/url', () => { const trace1 = 'trace-1'; const trace2 = 'trace-2'; + describe('matches', () => { + const path = 'path argument'; + let matchPathSpy; + + beforeAll(() => { + matchPathSpy = jest.spyOn(reactRouterDom, 'matchPath'); + }); + + it('calls matchPath with expected arguments', () => { + matches(path); + expect(matchPathSpy).toHaveBeenLastCalledWith(path, { + path: ROUTE_PATH, + strict: true, + exact: true, + }); + }); + + it("returns truthiness of matchPath's return value", () => { + matchPathSpy.mockReturnValueOnce(null); + expect(matches(path)).toBe(false); + matchPathSpy.mockReturnValueOnce({}); + expect(matches(path)).toBe(true); + }); + }); + describe('getUrl', () => { it('handles no args given', () => { expect(getUrl()).toBe('/search'); @@ -96,6 +124,39 @@ describe('SearchTracePage/url', () => { }) ).toBe(`/search?span=${span0}%20${span1}%40${trace0}&span=${span2}%40${trace1}&traceID=${trace2}`); }); + + describe('too long urls', () => { + const oneID = getUrl({ + traceID: trace0, + }); + const lengthBeforeArgs = oneID.indexOf('?'); + const lengthOfOneArg = oneID.length - lengthBeforeArgs; + const maxLengthOfArgs = MAX_LENGTH - lengthBeforeArgs; + + it('limits url length', () => { + const numberOfArgs = Math.ceil(maxLengthOfArgs / lengthOfOneArg); + + expect( + getUrl({ + traceID: new Array(numberOfArgs).fill(trace0), + }).length + ).toBeLessThan(MAX_LENGTH); + }); + + it('does not over shorten', () => { + const numberOfArgs = Math.floor(maxLengthOfArgs / lengthOfOneArg); + const remainder = maxLengthOfArgs % lengthOfOneArg; + const ids = new Array(numberOfArgs).fill(trace0); + ids[ids.length - 1] = `${ids[ids.length - 1]}${'x'.repeat(remainder)}`; + ids.push(trace0); + + expect( + getUrl({ + traceID: ids, + }).length + ).toBe(MAX_LENGTH); + }); + }); }); describe('getUrlState', () => { diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.test.js index e116990b4c..780f35acd3 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.test.js @@ -15,8 +15,8 @@ import React from 'react'; import { shallow } from 'enzyme'; +import VerticalResizer from '../../../common/VerticalResizer'; import TimelineHeaderRow from './TimelineHeaderRow'; -import TimelineColumnResizer from './TimelineColumnResizer'; import TimelineViewingLayer from './TimelineViewingLayer'; import Ticks from '../Ticks'; import TimelineCollapser from './TimelineCollapser'; @@ -86,9 +86,9 @@ describe('', () => { expect(wrapper.containsMatchingElement(elm)).toBe(true); }); - it('renders the TimelineColumnResizer', () => { + it('renders the VerticalResizer', () => { const elm = ( - - + ); } diff --git a/packages/jaeger-ui/src/components/common/CircularProgressbar.test.js b/packages/jaeger-ui/src/components/common/CircularProgressbar.test.js new file mode 100644 index 0000000000..a814826515 --- /dev/null +++ b/packages/jaeger-ui/src/components/common/CircularProgressbar.test.js @@ -0,0 +1,41 @@ +// Copyright (c) 2020 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 CircularProgressbar from './CircularProgressbar'; + +describe('CircularProgressbar', () => { + const minProps = { + maxValue: 108, + value: 42, + }; + + const fullProps = { + ...minProps, + backgroundHue: 0, + decorationHue: 120, + strokeWidth: 8, + text: 'test text', + }; + + it('renders as expected with all props', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('handles minimal props', () => { + expect(shallow()).toMatchSnapshot(); + }); +}); diff --git a/packages/jaeger-ui/src/components/common/DetailsCard/DetailList.test.js b/packages/jaeger-ui/src/components/common/DetailsCard/DetailList.test.js new file mode 100644 index 0000000000..0b2208326c --- /dev/null +++ b/packages/jaeger-ui/src/components/common/DetailsCard/DetailList.test.js @@ -0,0 +1,34 @@ +// Copyright (c) 2020 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 { List } from 'antd'; + +import DetailList from './DetailList'; + +describe('DetailList', () => { + const details = ['foo', 'bar', 'baz']; + + it('renders list as expected', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('renders item as expected', () => { + const renderItem = shallow() + .find(List) + .prop('renderItem'); + expect(shallow(
{renderItem(details[0])}
)).toMatchSnapshot(); + }); +}); diff --git a/packages/jaeger-ui/src/components/common/DetailsCard/DetailList.tsx b/packages/jaeger-ui/src/components/common/DetailsCard/DetailList.tsx new file mode 100644 index 0000000000..73588149c8 --- /dev/null +++ b/packages/jaeger-ui/src/components/common/DetailsCard/DetailList.tsx @@ -0,0 +1,31 @@ +// Copyright (c) 2020 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 { List } from 'antd'; + +const { Item } = List; + +export default function DetailList({ details }: { details: string[] }) { + return ( + ( + + {s} + + )} + /> + ); +} diff --git a/packages/jaeger-ui/src/components/common/DetailsCard/DetailTable.test.js b/packages/jaeger-ui/src/components/common/DetailsCard/DetailTable.test.js new file mode 100644 index 0000000000..efb6f08925 --- /dev/null +++ b/packages/jaeger-ui/src/components/common/DetailsCard/DetailTable.test.js @@ -0,0 +1,280 @@ +// Copyright (c) 2020 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 ExamplesLink from '../ExamplesLink'; +import DetailTable, { _onCell, _makeColumns, _renderCell, _rowKey, _sort } from './DetailTable'; + +describe('DetailTable', () => { + describe('render', () => { + const col0 = { + styling: { + background: 'red', + color: 'white', + }, + key: 'col0', + }; + const col1 = 'col1'; + const row0 = { + [col0.key]: 'val0', + [col1]: 'val1', + }; + const row1 = { + [col0.key]: 'val2', + [col1]: 'val3', + }; + + it('renders given rows and columns', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('infers all columns', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('infers missing columns', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('does not duplicate columns', () => { + expect(shallow()).toMatchSnapshot(); + }); + }); + + describe('_rowKey', () => { + const column = 'col'; + const examples = [{ spanIDs: ['id0', 'id1'], traceID: 'traceID' }]; + + it('handles undefined', () => { + const row = { [column]: undefined }; + expect(_rowKey(row)).toBe(JSON.stringify(row)); + }); + + it('handles array', () => { + const row = { [column]: examples }; + expect(_rowKey(row)).toBe(JSON.stringify({ [column]: JSON.stringify(examples) })); + }); + + it('handles object with string value', () => { + const valueObject = { value: 'test-value' }; + const row = { [column]: valueObject }; + expect(_rowKey(row)).toBe(JSON.stringify({ [column]: JSON.stringify(valueObject) })); + }); + + it('handles object with React.Element value with key', () => { + const key = 'test-key'; + const elem = ; + const row = { [column]: { value: elem } }; + expect(_rowKey(row)).toBe(JSON.stringify({ [column]: key })); + }); + + it('handles object with React.Element value without key', () => { + const elem = ; + const row = { [column]: { value: elem } }; + expect(_rowKey(row)).toBe(JSON.stringify({ [column]: 'Unknown' })); + }); + }); + + describe('_makeColumns', () => { + const stringColumn = 'stringCol'; + + describe('static props', () => { + const makeColumn = def => _makeColumns({ defs: [def] })[0]; + + it('renders string column', () => { + expect(makeColumn(stringColumn)).toEqual({ + dataIndex: stringColumn, + key: stringColumn, + title: stringColumn, + onCell: expect.any(Function), + onHeaderCell: expect.any(Function), + render: expect.any(Function), + sorter: expect.any(Function), + }); + }); + + it('renders object column', () => { + expect(makeColumn({ key: stringColumn })).toEqual({ + dataIndex: stringColumn, + key: stringColumn, + title: stringColumn, + onCell: expect.any(Function), + onHeaderCell: expect.any(Function), + render: expect.any(Function), + sorter: expect.any(Function), + }); + }); + + it('renders object column with label', () => { + const label = `label, not-${stringColumn}`; + expect( + makeColumn({ + key: stringColumn, + label, + }) + ).toEqual({ + dataIndex: stringColumn, + key: stringColumn, + title: label, + onCell: expect.any(Function), + onHeaderCell: expect.any(Function), + render: expect.any(Function), + sorter: expect.any(Function), + }); + }); + + it('renders object column with styling', () => { + const styling = { + background: 'red', + color: 'white', + }; + expect( + makeColumn({ + key: stringColumn, + styling, + }).onHeaderCell().style + ).toBe(styling); + }); + + it('renders object column without sort', () => { + expect( + makeColumn({ + key: stringColumn, + preventSort: true, + }) + ).toEqual({ + dataIndex: stringColumn, + key: stringColumn, + title: stringColumn, + onCell: expect.any(Function), + onHeaderCell: expect.any(Function), + render: expect.any(Function), + sorter: false, + }); + }); + }); + + describe('function props', () => { + const makeTestFn = fn => (...vals) => + fn(stringColumn)( + ...vals.map(v => ({ + [stringColumn]: v, + })) + ); + + describe('_onCell', () => { + const onCell = makeTestFn(_onCell); + + it('returns null for undefined', () => { + expect(onCell(undefined)).toBe(null); + }); + + it('returns null for string', () => { + expect(onCell('test-string')).toBe(null); + }); + + it('returns null for array', () => { + expect(onCell([])).toBe(null); + }); + + it('returns null for unstyled object', () => { + expect(onCell({})).toBe(null); + expect(onCell({ styling: {} })).toBe(null); + }); + + it('returns styling for styled object', () => { + const styling = { + background: 'red', + color: 'white', + }; + expect(onCell({ styling }).style).toBe(styling); + }); + }); + + describe('_renderCell', () => { + it('renders a string', () => { + expect(_renderCell('a')).toBe('a'); + }); + + it('handles undefined', () => { + expect(_renderCell()).toBe(undefined); + }); + + it("renders an object's value", () => { + const value = 'test-value'; + expect(_renderCell({ value })).toBe(value); + }); + + it('renders ', () => { + const examples = []; + const exampleLink = _renderCell(examples); + expect(exampleLink.type).toBe(ExamplesLink); + expect(exampleLink.props.examples).toBe(examples); + }); + + it('renders a regular link', () => { + expect(_renderCell({ linkTo: 'test.link', value: 'test-value' })).toMatchSnapshot(); + }); + }); + + describe('_sort ', () => { + const sort = makeTestFn(_sort); + + it('sorts strings', () => { + expect(sort('a', 'b')).toBe(-1); + expect(sort('a', 'a')).toBe(0); + expect(sort('b', 'a')).toBe(1); + }); + + it('sorts arrays by length', () => { + expect(sort(new Array(2), new Array(3))).toBe(-1); + expect(sort(new Array(2), new Array(2))).toBe(0); + expect(sort(new Array(3), new Array(2))).toBe(1); + }); + + it('sorts objects with string values', () => { + expect(sort({ value: 'a' }, { value: 'b' })).toBe(-1); + expect(sort({ value: 'a' }, { value: 'a' })).toBe(0); + expect(sort({ value: 'b' }, { value: 'a' })).toBe(1); + }); + + it('handles objects without string values', () => { + expect(() => sort({}, {})).not.toThrow(); + }); + + describe('mixed types', () => { + it('sorts a string and an object', () => { + expect(sort('a', { value: 'b' })).toBe(-1); + expect(sort('a', { value: 'a' })).toBe(0); + expect(sort('b', { value: 'a' })).toBe(1); + }); + + it('sorts an object and an array', () => { + expect(sort({ value: 'a' }, new Array(3))).toBe(0); + expect(sort({ value: 'a' }, new Array(2))).toBe(0); + expect(sort({ value: 'b' }, new Array(2))).toBe(0); + }); + + it('sorts an array and a string', () => { + expect(sort(new Array(2), 'a')).toBe(0); + expect(sort(new Array(2), 'a')).toBe(0); + expect(sort(new Array(3), 'b')).toBe(0); + }); + }); + }); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/common/DetailsCard/DetailTable.tsx b/packages/jaeger-ui/src/components/common/DetailsCard/DetailTable.tsx new file mode 100644 index 0000000000..a53b74f941 --- /dev/null +++ b/packages/jaeger-ui/src/components/common/DetailsCard/DetailTable.tsx @@ -0,0 +1,147 @@ +// Copyright (c) 2020 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 { Table } from 'antd'; +import _isEmpty from 'lodash/isEmpty'; + +import ExamplesLink, { TExample } from '../ExamplesLink'; + +import { TColumnDef, TColumnDefs, TRow, TStyledValue } from './types'; + +// exported for tests +export const _onCell = (dataIndex: string) => (row: TRow) => { + const cellData = row[dataIndex]; + if (!cellData || typeof cellData !== 'object' || Array.isArray(cellData)) return null; + const { styling } = cellData; + if (_isEmpty(styling)) return null; + return { + style: styling, + }; +}; + +// exported for tests +export const _renderCell = (cellData: undefined | string | TStyledValue) => { + if (!cellData || typeof cellData !== 'object') return cellData; + if (Array.isArray(cellData)) return ; + if (!cellData.linkTo) return cellData.value; + return ( + + {cellData.value} + + ); +}; + +// exported for tests +export const _sort = (dataIndex: string) => (a: TRow, b: TRow) => { + const aData = a[dataIndex]; + let aValue; + if (Array.isArray(aData)) aValue = aData.length; + else if (typeof aData === 'object' && typeof aData.value === 'string') aValue = aData.value; + else aValue = aData; + + const bData = b[dataIndex]; + let bValue; + if (Array.isArray(bData)) bValue = bData.length; + else if (typeof bData === 'object' && typeof bData.value === 'string') bValue = bData.value; + else bValue = bData; + + if (aValue < bValue) return -1; + return bValue < aValue ? 1 : 0; +}; + +// exported for tests +export const _makeColumns = ({ defs }: { defs: TColumnDefs }) => + defs.map((def: TColumnDef | string) => { + let dataIndex: string; + let key: string; + let sortable: boolean = true; + let style: React.CSSProperties | undefined; + let title: string; + if (typeof def === 'string') { + // eslint-disable-next-line no-multi-assign + key = title = dataIndex = def; + } else { + // eslint-disable-next-line no-multi-assign + key = title = dataIndex = def.key; + if (def.label) title = def.label; + if (def.styling) style = def.styling; + if (def.preventSort) sortable = false; + } + + return { + dataIndex, + key, + title, + onCell: _onCell(dataIndex), + onHeaderCell: () => ({ + style, + }), + render: _renderCell, + sorter: sortable && _sort(dataIndex), + }; + }); + +// exported for tests +export const _rowKey = (row: TRow) => + JSON.stringify(row, function replacer( + key: string, + value: TRow | undefined | string | number | TStyledValue | TExample[] + ) { + function isRow(v: typeof value): v is TRow { + return v === row; + } + if (isRow(value)) return value; + if (Array.isArray(value)) return JSON.stringify(value); + if (typeof value === 'object') { + if (typeof value.value === 'string') return JSON.stringify(value); + return value.value.key || 'Unknown'; + } + return value; + }); + +export default function DetailTable({ + columnDefs: _columnDefs, + details, +}: { + columnDefs?: TColumnDefs; + details: TRow[]; +}) { + const columnDefs: TColumnDefs = _columnDefs ? _columnDefs.slice() : []; + const knownColumns = new Set( + columnDefs.map(keyOrObj => { + if (typeof keyOrObj === 'string') return keyOrObj; + return keyOrObj.key; + }) + ); + details.forEach(row => { + Object.keys(row).forEach((col: string) => { + if (!knownColumns.has(col)) { + knownColumns.add(col); + columnDefs.push(col); + } + }); + }); + + return ( + + ); +} diff --git a/packages/jaeger-ui/src/components/common/DetailsCard/__snapshots__/DetailList.test.js.snap b/packages/jaeger-ui/src/components/common/DetailsCard/__snapshots__/DetailList.test.js.snap new file mode 100644 index 0000000000..dff11202a5 --- /dev/null +++ b/packages/jaeger-ui/src/components/common/DetailsCard/__snapshots__/DetailList.test.js.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DetailList renders item as expected 1`] = ` +
+ + + foo + + +
+`; + +exports[`DetailList renders list as expected 1`] = ` + +`; diff --git a/packages/jaeger-ui/src/components/common/DetailsCard/__snapshots__/DetailTable.test.js.snap b/packages/jaeger-ui/src/components/common/DetailsCard/__snapshots__/DetailTable.test.js.snap new file mode 100644 index 0000000000..112ecb673e --- /dev/null +++ b/packages/jaeger-ui/src/components/common/DetailsCard/__snapshots__/DetailTable.test.js.snap @@ -0,0 +1,207 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DetailTable _makeColumns function props _renderCell renders a regular link 1`] = ` + + test-value + +`; + +exports[`DetailTable render does not duplicate columns 1`] = ` +
+`; + +exports[`DetailTable render infers all columns 1`] = ` +
+`; + +exports[`DetailTable render infers missing columns 1`] = ` +
+`; + +exports[`DetailTable render renders given rows and columns 1`] = ` +
+`; diff --git a/packages/jaeger-ui/src/components/common/DetailsCard/__snapshots__/index.test.js.snap b/packages/jaeger-ui/src/components/common/DetailsCard/__snapshots__/index.test.js.snap new file mode 100644 index 0000000000..ab0c060df9 --- /dev/null +++ b/packages/jaeger-ui/src/components/common/DetailsCard/__snapshots__/index.test.js.snap @@ -0,0 +1,242 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DetailsCard handles empty details array 1`] = ` +
+
+
+ + Details Card Header + +
+
+
+
+`; + +exports[`DetailsCard renders as collapsible 1`] = ` +
+
+ +
+ + Details Card Header + +
+
+
+ +
+
+`; + +exports[`DetailsCard renders list details 1`] = ` +
+
+
+ + Details Card Header + +
+
+
+ +
+
+`; + +exports[`DetailsCard renders string details 1`] = ` +
+
+
+ + Details Card Header + +
+
+
+ + test details + +
+
+`; + +exports[`DetailsCard renders table details 1`] = ` +
+
+
+ + Details Card Header + +
+
+
+ +
+
+`; + +exports[`DetailsCard renders table details with column defs 1`] = ` +
+
+
+ + Details Card Header + +
+
+
+ +
+
+`; + +exports[`DetailsCard renders with className 1`] = ` +
+
+
+ + Details Card Header + +
+
+
+ +
+
+`; + +exports[`DetailsCard renders with description 1`] = ` +
+
+
+ + Details Card Header + +

+ test description +

+
+
+
+ +
+
+`; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css b/packages/jaeger-ui/src/components/common/DetailsCard/index.css similarity index 100% rename from packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css rename to packages/jaeger-ui/src/components/common/DetailsCard/index.css diff --git a/packages/jaeger-ui/src/components/common/DetailsCard/index.test.js b/packages/jaeger-ui/src/components/common/DetailsCard/index.test.js new file mode 100644 index 0000000000..2ae1f27de4 --- /dev/null +++ b/packages/jaeger-ui/src/components/common/DetailsCard/index.test.js @@ -0,0 +1,74 @@ +// Copyright (c) 2020 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 DetailsCard from '.'; + +describe('DetailsCard', () => { + const header = 'Details Card Header'; + + it('renders string details', () => { + const details = 'test details'; + expect(shallow()).toMatchSnapshot(); + }); + + it('handles empty details array', () => { + const details = []; + expect(shallow()).toMatchSnapshot(); + }); + + it('renders list details', () => { + const details = ['foo', 'bar', 'baz']; + expect(shallow()).toMatchSnapshot(); + }); + + it('renders table details', () => { + const details = [{ value: 'foo' }]; + expect(shallow()).toMatchSnapshot(); + }); + + it('renders table details with column defs', () => { + const columnDefs = ['col']; + const details = [{ [columnDefs[0]]: 'foo' }]; + expect( + shallow() + ).toMatchSnapshot(); + }); + + it('renders with description', () => { + const description = 'test description'; + expect(shallow()).toMatchSnapshot(); + }); + + it('renders with className', () => { + const className = 'test className'; + expect(shallow()).toMatchSnapshot(); + }); + + it('renders as collapsible', () => { + expect(shallow().state('collapsed')).toBe(false); + + const wrapper = shallow(); + expect(wrapper.state('collapsed')).toBe(true); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('button').simulate('click'); + expect(wrapper.state('collapsed')).toBe(false); + + wrapper.find('button').simulate('click'); + expect(wrapper.state('collapsed')).toBe(true); + }); +}); diff --git a/packages/jaeger-ui/src/components/common/DetailsCard/index.tsx b/packages/jaeger-ui/src/components/common/DetailsCard/index.tsx new file mode 100644 index 0000000000..74da3cfc45 --- /dev/null +++ b/packages/jaeger-ui/src/components/common/DetailsCard/index.tsx @@ -0,0 +1,97 @@ +// Copyright (c) 2020 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 MdKeyboardArrowDown from 'react-icons/lib/md/keyboard-arrow-down'; + +import { TColumnDefs, TDetails, TRow } from './types'; +import DetailTable from './DetailTable'; +import DetailList from './DetailList'; + +import './index.css'; + +type TProps = { + className?: string; + collapsible?: boolean; + columnDefs?: TColumnDefs; + description?: string; + details: TDetails; + header: string; +}; + +type TState = { + collapsed: boolean; +}; + +function isList(arr: string[] | TRow[]): arr is string[] { + return typeof arr[0] === 'string'; +} + +export default class DetailsCard extends React.PureComponent { + state: TState; + + constructor(props: TProps) { + super(props); + + this.state = { collapsed: Boolean(props.collapsible) }; + } + + renderDetails() { + const { columnDefs, details } = this.props; + + if (Array.isArray(details)) { + if (details.length === 0) return null; + + if (isList(details)) return ; + return ; + } + + return {details}; + } + + toggleCollapse = () => { + this.setState((prevState: TState) => ({ + collapsed: !prevState.collapsed, + })); + }; + + render() { + const { collapsed } = this.state; + const { className, collapsible, description, header } = this.props; + + return ( +
+
+ {collapsible && ( + + )} +
+ {header} + {description &&

{description}

} +
+
+
+ {this.renderDetails()} +
+
+ ); + } +} diff --git a/packages/jaeger-ui/src/components/common/DetailsCard/types.tsx b/packages/jaeger-ui/src/components/common/DetailsCard/types.tsx new file mode 100644 index 0000000000..1fc6c533c9 --- /dev/null +++ b/packages/jaeger-ui/src/components/common/DetailsCard/types.tsx @@ -0,0 +1,34 @@ +// Copyright (c) 2020 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 { TExample } from '../ExamplesLink'; + +export type TStyledValue = { + linkTo?: string; + styling?: React.CSSProperties; + value: string | React.ReactElement; +}; + +export type TColumnDef = { + key: string; + label?: string; + preventSort?: boolean; + styling?: React.CSSProperties; +}; + +export type TColumnDefs = (string | TColumnDef)[]; + +export type TRow = Record; + +export type TDetails = string | string[] | TRow[]; diff --git a/packages/jaeger-ui/src/components/common/ExamplesLink.test.js b/packages/jaeger-ui/src/components/common/ExamplesLink.test.js new file mode 100644 index 0000000000..d6a0c642c2 --- /dev/null +++ b/packages/jaeger-ui/src/components/common/ExamplesLink.test.js @@ -0,0 +1,69 @@ +// Copyright (c) 2020 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 ExamplesLink from './ExamplesLink'; + +describe('ExamplesLink', () => { + const traceLinks = [ + { + traceID: 'foo', + }, + { + traceID: 'bar', + }, + { + traceID: 'baz', + }, + ]; + const spanLinks = traceLinks.map(({ traceID }, i) => ({ + traceID: `${traceID}${i}`, + spanIDs: new Array(i + 1).fill('spanID').map((str, j) => `${str}${i}${j}`), + })); + + it('renders null when props.examples is absent', () => { + expect(shallow().type()).toBe(null); + }); + + it('renders null when props.examples is empty', () => { + expect(shallow().type()).toBe(null); + }); + + it('renders as expected when given trace links', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('renders as expected when given span links', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('renders as expected when given both span and trace links', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('renders label text iff props.includeText is true', () => { + expect( + shallow() + .find('a') + .props().children[0] + ).toBe(undefined); + expect( + shallow() + .find('a') + .props().children[0] + ).toBe('Examples '); + }); +}); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/ExamplesLink.tsx b/packages/jaeger-ui/src/components/common/ExamplesLink.tsx similarity index 92% rename from packages/jaeger-ui/src/components/QualityMetrics/ExamplesLink.tsx rename to packages/jaeger-ui/src/components/common/ExamplesLink.tsx index 6d601fea2f..6a1167f399 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/ExamplesLink.tsx +++ b/packages/jaeger-ui/src/components/common/ExamplesLink.tsx @@ -14,12 +14,15 @@ import * as React from 'react'; -import NewWindowIcon from '../common/NewWindowIcon'; import { getUrl } from '../SearchTracePage/url'; +import NewWindowIcon from './NewWindowIcon'; -import { TExample } from '../../model/path-agnostic-decorations/types'; +export type TExample = { + spanIDs?: string[]; + traceID: string; +}; -export type TProps = { +type TProps = { examples?: TExample[]; includeText?: boolean; }; diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.css b/packages/jaeger-ui/src/components/common/VerticalResizer.css similarity index 55% rename from packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.css rename to packages/jaeger-ui/src/components/common/VerticalResizer.css index 6e00ed0cc7..b5a7352bb6 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.css +++ b/packages/jaeger-ui/src/components/common/VerticalResizer.css @@ -14,24 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -.TimelineColumnResizer { +.VerticalResizer { left: 0; position: absolute; right: 0; top: 0; } -.TimelineColumnResizer.is-flipped { +.VerticalResizer.is-flipped { transform: scaleX(-1); } -.TimelineColumnResizer--wrapper { +.VerticalResizer--wrapper { bottom: 0; position: absolute; top: 0; } -.TimelineColumnResizer--dragger { +.VerticalResizer--dragger { border-left: 2px solid transparent; cursor: col-resize; height: calc(100vh - var(--nav-height)); @@ -41,27 +41,27 @@ limitations under the License. width: 1px; } -.TimelineColumnResizer--dragger:hover { +.VerticalResizer--dragger:hover { border-left: 2px solid rgba(0, 0, 0, 0.3); } -.TimelineColumnResizer.isDraggingLeft > .TimelineColumnResizer--dragger, -.TimelineColumnResizer.isDraggingRight > .TimelineColumnResizer--dragger { +.VerticalResizer.isDraggingLeft > .VerticalResizer--dragger, +.VerticalResizer.isDraggingRight > .VerticalResizer--dragger { background: rgba(136, 0, 136, 0.05); width: unset; } -.TimelineColumnResizer.isDraggingLeft > .TimelineColumnResizer--dragger { +.VerticalResizer.isDraggingLeft > .VerticalResizer--dragger { border-left: 2px solid #808; border-right: 1px solid #999; } -.TimelineColumnResizer.isDraggingRight > .TimelineColumnResizer--dragger { +.VerticalResizer.isDraggingRight > .VerticalResizer--dragger { border-left: 1px solid #999; border-right: 2px solid #808; } -.TimelineColumnResizer--dragger::before { +.VerticalResizer--dragger::before { position: absolute; top: 0; bottom: 0; @@ -70,20 +70,20 @@ limitations under the License. content: ' '; } -.TimelineColumnResizer.isDraggingLeft > .TimelineColumnResizer--dragger::before, -.TimelineColumnResizer.isDraggingRight > .TimelineColumnResizer--dragger::before { +.VerticalResizer.isDraggingLeft > .VerticalResizer--dragger::before, +.VerticalResizer.isDraggingRight > .VerticalResizer--dragger::before { left: -2000px; right: -2000px; } -.TimelineColumnResizer--gripIcon { +.VerticalResizer--gripIcon { position: absolute; top: 0; bottom: 0; } -.TimelineColumnResizer--gripIcon::before, -.TimelineColumnResizer--gripIcon::after { +.VerticalResizer--gripIcon::before, +.VerticalResizer--gripIcon::after { border-right: 1px solid #ccc; content: ' '; height: 9px; @@ -92,13 +92,13 @@ limitations under the License. top: 25px; } -.TimelineColumnResizer--gripIcon::after { +.VerticalResizer--gripIcon::after { right: 5px; } -.TimelineColumnResizer.isDraggingLeft > .TimelineColumnResizer--gripIcon::before, -.TimelineColumnResizer.isDraggingRight > .TimelineColumnResizer--gripIcon::before, -.TimelineColumnResizer.isDraggingLeft > .TimelineColumnResizer--gripIcon::after, -.TimelineColumnResizer.isDraggingRight > .TimelineColumnResizer--gripIcon::after { +.VerticalResizer.isDraggingLeft > .VerticalResizer--gripIcon::before, +.VerticalResizer.isDraggingRight > .VerticalResizer--gripIcon::before, +.VerticalResizer.isDraggingLeft > .VerticalResizer--gripIcon::after, +.VerticalResizer.isDraggingRight > .VerticalResizer--gripIcon::after { border-right: 1px solid rgba(136, 0, 136, 0.5); } diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.test.js b/packages/jaeger-ui/src/components/common/VerticalResizer.test.js similarity index 50% rename from packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.test.js rename to packages/jaeger-ui/src/components/common/VerticalResizer.test.js index 439b387632..7be1a417a9 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.test.js +++ b/packages/jaeger-ui/src/components/common/VerticalResizer.test.js @@ -15,9 +15,9 @@ import React from 'react'; import { mount } from 'enzyme'; -import TimelineColumnResizer from './TimelineColumnResizer'; +import VerticalResizer from './VerticalResizer'; -describe('', () => { +describe('', () => { let wrapper; let instance; @@ -30,19 +30,19 @@ describe('', () => { beforeEach(() => { props.onChange.mockReset(); - wrapper = mount(); + wrapper = mount(); instance = wrapper.instance(); }); it('renders without exploding', () => { expect(wrapper).toBeDefined(); - expect(wrapper.find('.TimelineColumnResizer').length).toBe(1); - expect(wrapper.find('.TimelineColumnResizer--gripIcon').length).toBe(1); - expect(wrapper.find('.TimelineColumnResizer--dragger').length).toBe(1); + expect(wrapper.find('.VerticalResizer').length).toBe(1); + expect(wrapper.find('.VerticalResizer--gripIcon').length).toBe(1); + expect(wrapper.find('.VerticalResizer--dragger').length).toBe(1); }); it('sets the root elm', () => { - const rootWrapper = wrapper.find('.TimelineColumnResizer'); + const rootWrapper = wrapper.find('.VerticalResizer'); expect(rootWrapper.getDOMNode()).toBe(instance._rootElm); }); @@ -50,7 +50,7 @@ describe('', () => { it('handles mouse down on the dragger', () => { const dragger = wrapper.find({ onMouseDown: instance._dragManager.handleMouseDown }); expect(dragger.length).toBe(1); - expect(dragger.is('.TimelineColumnResizer--dragger')).toBe(true); + expect(dragger.is('.VerticalResizer--dragger')).toBe(true); }); it('returns the draggable bounds via _getDraggingBounds()', () => { @@ -65,6 +65,24 @@ describe('', () => { }); }); + it('returns the flipped draggable bounds via _getDraggingBounds()', () => { + const left = 10; + const width = 100; + wrapper.setProps({ rightSide: true }); + instance._rootElm.getBoundingClientRect = () => ({ left, width }); + expect(instance._getDraggingBounds()).toEqual({ + width, + clientXLeft: left, + maxValue: 1 - props.min, + minValue: 1 - props.max, + }); + }); + + it('throws if dragged before rendered', () => { + wrapper.instance()._rootElm = null; + expect(instance._getDraggingBounds).toThrow('invalid state'); + }); + it('handles drag start', () => { const value = Math.random(); expect(wrapper.state('dragPosition')).toBe(null); @@ -72,6 +90,21 @@ describe('', () => { expect(wrapper.state('dragPosition')).toBe(value); }); + it('handles drag update', () => { + const value = props.position * 1.1; + expect(wrapper.state('dragPosition')).toBe(null); + wrapper.instance()._handleDragUpdate({ value }); + expect(wrapper.state('dragPosition')).toBe(value); + }); + + it('handles flipped drag update', () => { + const value = props.position * 1.1; + wrapper.setProps({ rightSide: true }); + expect(wrapper.state('dragPosition')).toBe(null); + wrapper.instance()._handleDragUpdate({ value }); + expect(wrapper.state('dragPosition')).toBe(1 - value); + }); + it('handles drag end', () => { const manager = { resetBounds: jest.fn() }; const value = Math.random(); @@ -81,11 +114,28 @@ describe('', () => { expect(wrapper.state('dragPosition')).toBe(null); expect(props.onChange.mock.calls).toEqual([[value]]); }); + + it('handles flipped drag end', () => { + const manager = { resetBounds: jest.fn() }; + const value = Math.random(); + wrapper.setProps({ rightSide: true }); + wrapper.setState({ dragPosition: 2 * value }); + instance._handleDragEnd({ manager, value }); + expect(manager.resetBounds.mock.calls).toEqual([[]]); + expect(wrapper.state('dragPosition')).toBe(null); + expect(props.onChange.mock.calls).toEqual([[1 - value]]); + }); + + it('cleans up DraggableManager on unmount', () => { + const disposeSpy = jest.spyOn(wrapper.instance()._dragManager, 'dispose'); + wrapper.unmount(); + expect(disposeSpy).toHaveBeenCalledTimes(1); + }); }); it('does not render a dragging indicator when not dragging', () => { expect(wrapper.find('.isDraggingLeft').length + wrapper.find('.isDraggingRight').length).toBe(0); - expect(wrapper.find('.TimelineColumnResizer--dragger').prop('style').right).toBe(undefined); + expect(wrapper.find('.VerticalResizer--dragger').prop('style').right).toBe(undefined); }); it('renders a dragging indicator when dragging', () => { @@ -94,6 +144,12 @@ describe('', () => { instance.forceUpdate(); wrapper.update(); expect(wrapper.find('.isDraggingLeft').length + wrapper.find('.isDraggingRight').length).toBe(1); - expect(wrapper.find('.TimelineColumnResizer--dragger').prop('style').right).toBeDefined(); + expect(wrapper.find('.VerticalResizer--dragger').prop('style').right).toBeDefined(); + }); + + it('renders is-flipped classname when positioned on rightSide', () => { + expect(wrapper.find('.is-flipped').length).toBe(0); + wrapper.setProps({ rightSide: true }); + expect(wrapper.find('.is-flipped').length).toBe(1); }); }); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx b/packages/jaeger-ui/src/components/common/VerticalResizer.tsx similarity index 84% rename from packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx rename to packages/jaeger-ui/src/components/common/VerticalResizer.tsx index 246e3a8600..9adb883793 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx +++ b/packages/jaeger-ui/src/components/common/VerticalResizer.tsx @@ -15,12 +15,12 @@ import * as React from 'react'; import cx from 'classnames'; -import { TNil } from '../../../../types'; -import DraggableManager, { DraggableBounds, DraggingUpdate } from '../../../../utils/DraggableManager'; +import { TNil } from '../../types'; +import DraggableManager, { DraggableBounds, DraggingUpdate } from '../../utils/DraggableManager'; -import './TimelineColumnResizer.css'; +import './VerticalResizer.css'; -type TimelineColumnResizerProps = { +type VerticalResizerProps = { max: number; min: number; onChange: (newSize: number) => void; @@ -28,20 +28,17 @@ type TimelineColumnResizerProps = { rightSide?: boolean; }; -type TimelineColumnResizerState = { +type VerticalResizerState = { dragPosition: number | TNil; }; -export default class TimelineColumnResizer extends React.PureComponent< - TimelineColumnResizerProps, - TimelineColumnResizerState -> { - state: TimelineColumnResizerState; +export default class VerticalResizer extends React.PureComponent { + state: VerticalResizerState; _dragManager: DraggableManager; _rootElm: Element | TNil; - constructor(props: TimelineColumnResizerProps) { + constructor(props: VerticalResizerProps) { super(props); this._dragManager = new DraggableManager({ getBounds: this._getDraggingBounds, @@ -119,13 +116,13 @@ export default class TimelineColumnResizer extends React.PureComponent< } return (
-
+
diff --git a/packages/jaeger-ui/src/components/common/__snapshots__/CircularProgressbar.test.js.snap b/packages/jaeger-ui/src/components/common/__snapshots__/CircularProgressbar.test.js.snap new file mode 100644 index 0000000000..2cf6efcc3d --- /dev/null +++ b/packages/jaeger-ui/src/components/common/__snapshots__/CircularProgressbar.test.js.snap @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CircularProgressbar handles minimal props 1`] = ` + +`; + +exports[`CircularProgressbar renders as expected with all props 1`] = ` + +`; diff --git a/packages/jaeger-ui/src/components/common/__snapshots__/ExamplesLink.test.js.snap b/packages/jaeger-ui/src/components/common/__snapshots__/ExamplesLink.test.js.snap new file mode 100644 index 0000000000..20762163b9 --- /dev/null +++ b/packages/jaeger-ui/src/components/common/__snapshots__/ExamplesLink.test.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ExamplesLink renders as expected when given both span and trace links 1`] = ` + + + +`; + +exports[`ExamplesLink renders as expected when given span links 1`] = ` + + + +`; + +exports[`ExamplesLink renders as expected when given trace links 1`] = ` + + + +`; diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/__snapshots__/index.test.js.snap b/packages/jaeger-ui/src/model/path-agnostic-decorations/__snapshots__/index.test.js.snap new file mode 100644 index 0000000000..bb7d98a6a8 --- /dev/null +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/__snapshots__/index.test.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`extractDecorationFromState prefers operation specific decoration over service decoration 1`] = ` + +`; + +exports[`extractDecorationFromState returns service decoration 1`] = ` + +`; diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/index.test.js b/packages/jaeger-ui/src/model/path-agnostic-decorations/index.test.js new file mode 100644 index 0000000000..e737fe607d --- /dev/null +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/index.test.js @@ -0,0 +1,108 @@ +// Copyright (c) 2020 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 _set from 'lodash/set'; +import queryString from 'query-string'; + +import extractDecorationFromState from '.'; + +describe('extractDecorationFromState', () => { + const decorationID = 'test decoration id'; + const service = 'test service'; + const operation = 'test operation'; + const decorationValue = 42; + const decorationMax = 108; + + function makeState({ decoration = decorationID, opValue, opMax, withoutOpValue, withoutOpMax }) { + const state = {}; + const deco = Array.isArray(decoration) ? decoration[0] : decoration; + + _set(state, 'router.location.search', decoration ? queryString.stringify({ decoration }) : ''); + if (opValue !== undefined) + _set(state, `pathAgnosticDecorations.${deco}.withOp.${service}.${operation}`, opValue); + if (opMax !== undefined) _set(state, `pathAgnosticDecorations.${deco}.withOpMax`, opMax); + if (withoutOpValue !== undefined) + _set(state, `pathAgnosticDecorations.${deco}.withoutOp.${service}`, withoutOpValue); + if (withoutOpMax !== undefined) _set(state, `pathAgnosticDecorations.${deco}.withoutOpMax`, withoutOpMax); + + return state; + } + + function extractWrapper(stateArgs, svpOp = { service, operation }) { + return extractDecorationFromState(makeState(stateArgs), svpOp); + } + + it('returns an empty object if url lacks a decorationID', () => { + expect(extractWrapper({ decoration: null })).toEqual({}); + }); + + it('prefers operation specific decoration over service decoration', () => { + const otherValue = 'other value'; + const otherMax = 'other max'; + const res = extractWrapper({ + opValue: decorationValue, + opMax: decorationMax, + withoutOpValue: otherValue, + withoutOpMax: otherMax, + }); + expect(res).toEqual( + expect.objectContaining({ + decorationID, + decorationValue, + }) + ); + expect(res.decorationProgressbar).toMatchSnapshot(); + }); + + it('returns service decoration', () => { + const res = extractWrapper({ + withoutOpValue: decorationValue, + withoutOpMax: decorationMax, + }); + expect(res).toEqual( + expect.objectContaining({ + decorationID, + decorationValue, + }) + ); + expect(res.decorationProgressbar).toMatchSnapshot(); + }); + + it('omits CircularProgressbar if value is a string', () => { + const withoutOpValue = 'without op string value'; + const res = extractWrapper({ + withoutOpValue, + withoutOpMax: decorationMax, + }); + expect(res).toEqual({ + decorationID, + decorationValue: withoutOpValue, + decorationProgressbar: undefined, + }); + }); + + it('uses first decoration if multiple exist in url', () => { + const withoutOpValue = 'without op string value'; + const res = extractWrapper({ + decoration: [decorationID, `not-${decorationID}`], + withoutOpValue, + withoutOpMax: decorationMax, + }); + expect(res).toEqual({ + decorationID, + decorationValue: withoutOpValue, + decorationProgressbar: undefined, + }); + }); +}); diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx index 6b1e20ef8f..96cc12d726 100644 --- a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx @@ -12,13 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React from 'react'; - -export type TExample = { - spanIDs?: string[]; - traceID: string; -}; - export type TPathAgnosticDecorationSchema = { acronym: string; id: string; @@ -36,25 +29,6 @@ export type TPathAgnosticDecorationSchema = { opDetailColumnDefPath?: string; }; -export type TStyledValue = { - linkTo?: string; - styling?: React.CSSProperties; - value: string | React.ReactElement; -}; - -export type TPadColumnDef = { - key: string; - label?: string; - preventSort?: boolean; - styling?: React.CSSProperties; -}; - -export type TPadColumnDefs = (string | TPadColumnDef)[]; - -export type TPadRow = Record; - -export type TPadDetails = string | string[] | TPadRow[]; - export type TPadEntry = number | string; export type TNewData = Record<