diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.css index 4af98f1b45..bd980c3284 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.css @@ -17,6 +17,7 @@ limitations under the License. .AccordianLogs { border: 1px solid #d8d8d8; position: relative; + margin-bottom: 0.25rem; } .AccordianLogs--header { diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.css new file mode 100644 index 0000000000..fc270a0e9f --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.css @@ -0,0 +1,27 @@ +/* +Copyright (c) 2019 Uber Technologies, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.AccordianText--header { + cursor: pointer; + overflow: hidden; + padding: 0.25em 0.1em; + text-overflow: ellipsis; + white-space: nowrap; +} + +.AccordianText--header:hover { + background: #e8e8e8; +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.test.js new file mode 100644 index 0000000000..f33176dc9b --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.test.js @@ -0,0 +1,55 @@ +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { shallow } from 'enzyme'; +import AccordianText from './AccordianText'; +import TextList from './TextList'; + +const warnings = ['Duplicated tag', 'Duplicated spanId']; + +describe('', () => { + let wrapper; + + const props = { + compact: false, + data: warnings, + highContrast: false, + isOpen: false, + label: 'le-label', + onToggle: jest.fn(), + }; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('renders without exploding', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.exists()).toBe(true); + }); + + it('renders the label', () => { + const header = wrapper.find(`.AccordianText--header > strong`); + expect(header.length).toBe(1); + expect(header.text()).toBe(props.label); + }); + + it('renders the table instead of the summary when it is expanded', () => { + wrapper.setProps({ isOpen: true }); + const table = wrapper.find(TextList); + expect(table.length).toBe(1); + expect(table.prop('data')).toBe(warnings); + }); +}); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.tsx new file mode 100644 index 0000000000..7f88a9a608 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.tsx @@ -0,0 +1,69 @@ +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; +import cx from 'classnames'; +import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down'; +import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right'; +import TextList from './TextList'; +import { TNil } from '../../../../types'; + +import './AccordianText.css'; + +type AccordianTextProps = { + className?: string | TNil; + data: string[]; + highContrast?: boolean; + interactive?: boolean; + isOpen: boolean; + label: string; + onToggle?: null | (() => void); +}; + +export default function AccordianText(props: AccordianTextProps) { + const { className, data, highContrast, interactive, isOpen, label, onToggle } = props; + const isEmpty = !Array.isArray(data) || !data.length; + const iconCls = cx('u-align-icon', { 'AccordianKeyValues--emptyIcon': isEmpty }); + let arrow: React.ReactNode | null = null; + let headerProps: Object | null = null; + if (interactive) { + arrow = isOpen ? : ; + headerProps = { + 'aria-checked': isOpen, + onClick: isEmpty ? null : onToggle, + role: 'switch', + }; + } + return ( +
+
+ {arrow} {label} ({data.length}) +
+ {isOpen && } +
+ ); +} + +AccordianText.defaultProps = { + className: null, + highContrast: false, + interactive: true, + onToggle: null, +}; diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/DetailState.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/DetailState.tsx index 22835c81a0..ed822aea1d 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/DetailState.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/DetailState.tsx @@ -21,11 +21,14 @@ export default class DetailState { isTagsOpen: boolean; isProcessOpen: boolean; logs: { isOpen: boolean; openedItems: Set }; + isWarningsOpen: boolean; constructor(oldState?: DetailState) { - const { isTagsOpen, isProcessOpen, logs }: DetailState | Record = oldState || {}; + const { isTagsOpen, isProcessOpen, isWarningsOpen, logs }: DetailState | Record = + oldState || {}; this.isTagsOpen = Boolean(isTagsOpen); this.isProcessOpen = Boolean(isProcessOpen); + this.isWarningsOpen = Boolean(isWarningsOpen); this.logs = { isOpen: Boolean(logs && logs.isOpen), openedItems: logs && logs.openedItems ? new Set(logs.openedItems) : new Set(), @@ -44,6 +47,12 @@ export default class DetailState { return next; } + toggleWarnings() { + const next = new DetailState(this); + next.isWarningsOpen = !this.isWarningsOpen; + return next; + } + toggleLogs() { const next = new DetailState(this); next.logs.isOpen = !this.logs.isOpen; diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.css new file mode 100644 index 0000000000..887ebd2643 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.css @@ -0,0 +1,41 @@ +/* +Copyright (c) 2019 Uber Technologies, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.TextList { + background: #fff; + max-height: 450px; + overflow: auto; +} + +.TextList-body { + vertical-align: baseline; +} + +.TextList--List { + width: 100%; + list-style: none; + padding: 0; + margin: 0; +} + +.TextList--List:nth-child(2n) > li { + background: #f5f5f5; +} + +.TextList--List > li { + padding: 0.25rem 0.5rem; + vertical-align: top; +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.test.js new file mode 100644 index 0000000000..b8079741b8 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.test.js @@ -0,0 +1,37 @@ +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { shallow } from 'enzyme'; +import TextList from './TextList'; + +describe('', () => { + let wrapper; + + const data = [{ key: 'span.kind', value: 'client' }, { key: 'omg', value: 'mos-def' }]; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('renders without exploding', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('.TextList').length).toBe(1); + }); + + it('renders a table row for each data element', () => { + const trs = wrapper.find('li'); + expect(trs.length).toBe(data.length); + }); +}); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.tsx new file mode 100644 index 0000000000..fea861e6a5 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.tsx @@ -0,0 +1,38 @@ +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; + +import './TextList.css'; + +type TextListProps = { + data: string[]; +}; + +export default function TextList(props: TextListProps) { + const { data } = props; + return ( +
+
    + {data.map((row, i) => { + return ( + // `i` is necessary in the key because row.key can repeat + // eslint-disable-next-line react/no-array-index-key +
  • {row}
  • + ); + })} +
+
+ ); +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.css index f7c0b19e57..eefa8b698e 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.css @@ -40,3 +40,21 @@ limitations under the License. .SpanDetail--debugValue:hover { color: #333; } +.AccordianWarnings { + border: 1px solid #d8d8d8; +} +.AccordianWarnings > .AccordianText--header > strong { + color: #d36c08; +} + +.AccordianWarnings > .AccordianText--header { + background: #fff7e6; + padding: 0.25rem 0.5rem; +} + +.SpanDetail--warnIcon { + background: transparent; + color: #ffa500; + font-size: 1em; + margin-right: 0.2rem; +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js index 3af5e7907a..7a764d4551 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js @@ -20,6 +20,7 @@ import { shallow } from 'enzyme'; import AccordianKeyValues from './AccordianKeyValues'; import AccordianLogs from './AccordianLogs'; +import AccordianText from './AccordianText'; import DetailState from './DetailState'; import SpanDetail from './index'; import { formatDuration } from '../utils'; @@ -46,6 +47,7 @@ describe('', () => { logsToggle: jest.fn(), processToggle: jest.fn(), tagsToggle: jest.fn(), + warningsToggle: jest.fn(), }; span.logs = [ { @@ -58,6 +60,8 @@ describe('', () => { }, ]; + span.warnings = ['Warning 1', 'Warning 2']; + beforeEach(() => { formatDuration.mockReset(); props.tagsToggle.mockReset(); @@ -120,6 +124,15 @@ describe('', () => { expect(props.logItemToggle).toHaveBeenLastCalledWith(span.spanID, somethingUniq); }); + it('renders the warnings', () => { + const target = ( + + ); + expect(wrapper.containsMatchingElement(target)).toBe(true); + wrapper.find({ data: span.warnings }).simulate('toggle'); + expect(props.warningsToggle).toHaveBeenLastCalledWith(span.spanID); + }); + it('renders CopyIcon with deep link URL', () => { expect( wrapper diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.tsx index 3766b62d52..139ec83d1c 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.tsx @@ -17,13 +17,14 @@ import { Divider } from 'antd'; import AccordianKeyValues from './AccordianKeyValues'; import AccordianLogs from './AccordianLogs'; +import AccordianText from './AccordianText'; import DetailState from './DetailState'; import { formatDuration } from '../utils'; import CopyIcon from '../../../common/CopyIcon'; import LabeledList from '../../../common/LabeledList'; import { TNil } from '../../../../types'; -import { Log, Span, KeyValuePair, Link } from '../../../../types/trace'; +import { KeyValuePair, Link, Log, Span } from '../../../../types/trace'; import './index.css'; @@ -36,6 +37,7 @@ type SpanDetailProps = { span: Span; tagsToggle: (spanID: string) => void; traceStartTime: number; + warningsToggle: (spanID: string) => void; }; export default function SpanDetail(props: SpanDetailProps) { @@ -48,9 +50,10 @@ export default function SpanDetail(props: SpanDetailProps) { span, tagsToggle, traceStartTime, + warningsToggle, } = props; - const { isTagsOpen, isProcessOpen, logs: logsState } = detailState; - const { operationName, process, duration, relativeStartTime, spanID, logs, tags } = span; + const { isTagsOpen, isProcessOpen, logs: logsState, isWarningsOpen } = detailState; + const { operationName, process, duration, relativeStartTime, spanID, logs, tags, warnings } = span; const overviewItems = [ { key: 'svc', @@ -113,6 +116,16 @@ export default function SpanDetail(props: SpanDetailProps) { timestamp={traceStartTime} /> )} + {warnings && + warnings.length > 0 && ( + warningsToggle(spanID)} + /> + )} {spanID} void; logsToggle: (spanID: string) => void; processToggle: (spanID: string) => void; + warningsToggle: (spanID: string) => void; span: Span; tagsToggle: (spanID: string) => void; traceStartTime: number; @@ -55,6 +56,7 @@ export default class SpanDetailRow extends React.PureComponent void; detailLogItemToggle: (spanID: string, log: Log) => void; detailLogsToggle: (spanID: string) => void; + detailWarningsToggle: (spanID: string) => void; detailProcessToggle: (spanID: string) => void; detailTagsToggle: (spanID: string) => void; detailToggle: (spanID: string) => void; @@ -371,6 +372,7 @@ export class VirtualizedTraceViewImpl extends React.Component { unchecked: new DetailState(), checked: baseDetail.toggleLogs(), }, + { + msg: 'toggles warnings', + action: actions.detailWarningsToggle(id), + get: state => state.detailStates.get(id), + unchecked: new DetailState(), + checked: baseDetail.toggleWarnings(), + }, ]; beforeEach(() => { diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.tsx index b5741442f2..14e0307a20 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.tsx @@ -68,6 +68,7 @@ export const actionTypes = generateActionTypes('@jaeger-ui/trace-timeline-viewer 'DETAIL_PROCESS_TOGGLE', 'DETAIL_LOGS_TOGGLE', 'DETAIL_LOG_ITEM_TOGGLE', + 'DETAIL_WARNINGS_TOGGLE', 'EXPAND_ALL', 'EXPAND_ONE', 'FOCUS_UI_FIND_MATCHES', @@ -87,6 +88,7 @@ const fullActions = createActions({ [actionTypes.EXPAND_ALL]: () => ({}), [actionTypes.EXPAND_ONE]: (spans: Span[]) => ({ spans }), [actionTypes.DETAIL_PROCESS_TOGGLE]: (spanID: string) => ({ spanID }), + [actionTypes.DETAIL_WARNINGS_TOGGLE]: (spanID: string) => ({ spanID }), [actionTypes.DETAIL_TAGS_TOGGLE]: (spanID: string) => ({ spanID }), [actionTypes.DETAIL_TOGGLE]: (spanID: string) => ({ spanID }), [actionTypes.FOCUS_UI_FIND_MATCHES]: (trace: Trace, uiFind: string | TNil) => ({ trace, uiFind }), @@ -237,7 +239,7 @@ function detailToggle(state: TTraceTimeline, { spanID }: TSpanIdValue) { } function detailSubsectionToggle( - subSection: 'tags' | 'process' | 'logs', + subSection: 'tags' | 'process' | 'logs' | 'warnings', state: TTraceTimeline, { spanID }: TSpanIdValue ) { @@ -250,6 +252,8 @@ function detailSubsectionToggle( detailState = old.toggleTags(); } else if (subSection === 'process') { detailState = old.toggleProcess(); + } else if (subSection === 'warnings') { + detailState = old.toggleWarnings(); } else { detailState = old.toggleLogs(); } @@ -261,6 +265,7 @@ function detailSubsectionToggle( const detailTagsToggle = detailSubsectionToggle.bind(null, 'tags'); const detailProcessToggle = detailSubsectionToggle.bind(null, 'process'); const detailLogsToggle = detailSubsectionToggle.bind(null, 'logs'); +const detailWarningsToggle = detailSubsectionToggle.bind(null, 'warnings'); function detailLogItemToggle(state: TTraceTimeline, { spanID, logItem }: TSpanIdLogValue) { const old = state.detailStates.get(spanID); @@ -299,6 +304,7 @@ export default handleActions( [actionTypes.DETAIL_LOGS_TOGGLE]: guardReducer(detailLogsToggle), [actionTypes.DETAIL_LOG_ITEM_TOGGLE]: guardReducer(detailLogItemToggle), [actionTypes.DETAIL_PROCESS_TOGGLE]: guardReducer(detailProcessToggle), + [actionTypes.DETAIL_WARNINGS_TOGGLE]: guardReducer(detailWarningsToggle), [actionTypes.DETAIL_TAGS_TOGGLE]: guardReducer(detailTagsToggle), [actionTypes.DETAIL_TOGGLE]: guardReducer(detailToggle), [actionTypes.EXPAND_ALL]: guardReducer(expandAll), diff --git a/packages/jaeger-ui/src/model/transform-trace-data.tsx b/packages/jaeger-ui/src/model/transform-trace-data.tsx index 78183e5904..89c97c84f1 100644 --- a/packages/jaeger-ui/src/model/transform-trace-data.tsx +++ b/packages/jaeger-ui/src/model/transform-trace-data.tsx @@ -15,11 +15,25 @@ import _isEqual from 'lodash/isEqual'; import { getTraceSpanIdsAsTree } from '../selectors/trace'; -import { Process, Span, SpanData, Trace, TraceData } from '../types/trace'; +import { KeyValuePair, Process, Span, SpanData, Trace, TraceData } from '../types/trace'; import TreeNode from '../utils/TreeNode'; type SpanWithProcess = SpanData & { process: Process }; +function deduplicateTags(spanTags: Array) { + const warningsHash: Map = new Map(); + const tags: Array = spanTags.reduce>((uniqueTags, tag) => { + if (!uniqueTags.some(t => t.key === tag.key && t.value === tag.value)) { + uniqueTags.push(tag); + } else { + warningsHash.set(`${tag.key}:${tag.value}`, `Duplicate tag "${tag.key}:${tag.value}"`); + } + return uniqueTags; + }, []); + const warnings = Array.from(warningsHash.values()); + return { tags, warnings }; +} + /** * NOTE: Mutates `data` - Transform the HTTP response data into the form the app * generally requires. @@ -93,6 +107,10 @@ export default function transformTraceData(data: TraceData & { spans: SpanWithPr span.relativeStartTime = span.startTime - traceStartTime; span.depth = depth - 1; span.hasChildren = node.children.length > 0; + const tagsInfo = deduplicateTags(span.tags); + span.tags = tagsInfo.tags; + span.warnings = span.warnings || []; + span.warnings = span.warnings.concat(tagsInfo.warnings); span.references.forEach(ref => { const refSpan = spanMap.get(ref.spanID) as Span; if (refSpan) { diff --git a/packages/jaeger-ui/src/types/trace.tsx b/packages/jaeger-ui/src/types/trace.tsx index 8fcc5db853..8257eb5c2e 100644 --- a/packages/jaeger-ui/src/types/trace.tsx +++ b/packages/jaeger-ui/src/types/trace.tsx @@ -54,6 +54,7 @@ export type SpanData = { logs: Array; tags: Array; references: Array; + warnings: Array | null; }; export type Span = SpanData & {