diff --git a/packages/jaeger-ui/src/components/App/Page.css b/packages/jaeger-ui/src/components/App/Page.css index d0a0896f19..620189931c 100644 --- a/packages/jaeger-ui/src/components/App/Page.css +++ b/packages/jaeger-ui/src/components/App/Page.css @@ -23,6 +23,8 @@ limitations under the License. } .Page--content { + display: flex; + flex-direction: column; left: 0; min-height: calc(100% - 46px); position: absolute; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx index 07618b15ba..18901e13a8 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx @@ -60,13 +60,17 @@ export default class DdgNodeContent extends React.PureComponent { getVisiblePathElems: (vertexKey: string) => PathElem[] | undefined, setViewModifier: (vertexKey: string, viewModifier: EViewModifier, enable: boolean) => void, density: EDdgDensity, - showOp: boolean + showOp: boolean, + baseUrl: string, + extraUrlArgs: { [key: string]: unknown } | undefined ) { return function renderNode(vertex: TDdgVertex, _: unknown, lv: TLayoutVertex | null) { const { isFocalNode, key, operation, service } = vertex; return ( ; + extraUrlArgs?: { [key: string]: unknown }; getVisiblePathElems: (vertexKey: string) => PathElem[] | undefined; setViewModifier: (vertexKey: string, viewModifier: EViewModifier, enable: boolean) => void; showOp: boolean; @@ -85,6 +87,8 @@ export default class Graph extends PureComponent { uiFindMatches, vertices, verticesViewModifiers, + baseUrl, + extraUrlArgs, } = this.props; const nodeRenderers = this.getNodeRenderers(uiFindMatches || this.emptyFindSet, verticesViewModifiers); @@ -132,7 +136,14 @@ export default class Graph extends PureComponent { layerType: 'html', measurable: true, measureNode: DdgNodeContent.measureNode, - renderNode: this.getNodeContentRenderer(getVisiblePathElems, setViewModifier, density, showOp), + renderNode: this.getNodeContentRenderer( + getVisiblePathElems, + setViewModifier, + density, + showOp, + baseUrl, + extraUrlArgs + ), }, ]} /> diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Header/index.css b/packages/jaeger-ui/src/components/DeepDependencies/Header/index.css index f9d53274f0..8f1a4aa1e4 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Header/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/Header/index.css @@ -28,19 +28,19 @@ limitations under the License. .DdgHeader--controlHeader { align-items: center; background-color: #f6f6f6; - border-top: 1px solid #eee; display: flex; justify-content: space-between; padding-left: 1.25rem; } .DdgHeader--paramsHeader { - flex-wrap: wrap; - font-size: unset; align-items: center; background: #fafafa; + border-bottom: 1px solid #eee; display: flex; flex: 1; + flex-wrap: wrap; + font-size: unset; margin: 0; padding: 0.75rem 1.25rem 0.75rem 1.25rem; position: relative; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Header/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Header/index.tsx index cbb1ec0abe..b0e2a20980 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Header/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Header/index.tsx @@ -37,6 +37,7 @@ type TProps = { setOperation: (operation: string) => void; setService: (service: string) => void; showOperations: boolean; + showParameters?: boolean; showVertices: (vertices: TDdgVertex[]) => void; toggleShowOperations: (enable: boolean) => void; uiFindCount: number | undefined; @@ -45,6 +46,10 @@ type TProps = { export default class Header extends React.PureComponent { private _uiFindInput: React.RefObject = React.createRef(); + static defaultProps = { + showParameters: true, + }; + focusUiFindInput = () => { if (this._uiFindInput.current) { this._uiFindInput.current.focus(); @@ -103,30 +108,33 @@ export default class Header extends React.PureComponent { showOperations, toggleShowOperations, visEncoding, + showParameters, } = this.props; return (
-
- - {service && ( + {showParameters && ( +
- )} -
+ {service && ( + + )} +
+ )}
{ const value = `new ${propName}`; const kwarg = { [propName]: value }; ddgPageImpl.updateUrlState(kwarg); - expect(getUrlSpy).toHaveBeenLastCalledWith(Object.assign({}, props.urlState, kwarg)); + expect(getUrlSpy).toHaveBeenLastCalledWith(Object.assign({}, props.urlState, kwarg), undefined); expect(props.history.push).toHaveBeenCalledTimes(i + 1); }); }); @@ -115,7 +115,7 @@ describe('DeepDependencyGraphPage', () => { start: 'new start', }; ddgPageImpl.updateUrlState(kwarg); - expect(getUrlSpy).toHaveBeenLastCalledWith(Object.assign({}, props.urlState, kwarg)); + expect(getUrlSpy).toHaveBeenLastCalledWith(Object.assign({}, props.urlState, kwarg), undefined); expect(props.history.push).toHaveBeenCalledTimes(1); }); @@ -130,14 +130,17 @@ describe('DeepDependencyGraphPage', () => { }; const ddgPageWithFewerProps = new DeepDependencyGraphPageImpl(otherProps); ddgPageWithFewerProps.updateUrlState(kwarg); - expect(getUrlSpy).toHaveBeenLastCalledWith(Object.assign({}, otherUrlState, kwarg)); + expect(getUrlSpy).toHaveBeenLastCalledWith(Object.assign({}, otherUrlState, kwarg), undefined); expect(getUrlSpy).not.toHaveBeenLastCalledWith(expect.objectContaining({ start: expect.anything() })); expect(props.history.push).toHaveBeenCalledTimes(1); }); it('includes props.graphState.model.hash iff it is truthy', () => { ddgPageImpl.updateUrlState({}); - expect(getUrlSpy).toHaveBeenLastCalledWith(expect.not.objectContaining({ hash: expect.anything() })); + expect(getUrlSpy).toHaveBeenLastCalledWith( + expect.not.objectContaining({ hash: expect.anything() }), + undefined + ); const hash = 'testHash'; const propsWithHash = { @@ -152,7 +155,7 @@ describe('DeepDependencyGraphPage', () => { }; const ddgPageWithHash = new DeepDependencyGraphPageImpl(propsWithHash); ddgPageWithHash.updateUrlState({}); - expect(getUrlSpy).toHaveBeenLastCalledWith(expect.objectContaining({ hash })); + expect(getUrlSpy).toHaveBeenLastCalledWith(expect.objectContaining({ hash }), undefined); }); describe('setDistance', () => { @@ -192,7 +195,8 @@ describe('DeepDependencyGraphPage', () => { prevVisEncoding: visEncoding, }); expect(getUrlSpy).toHaveBeenLastCalledWith( - Object.assign({}, props.urlState, { visEncoding: mockNewEncoding }) + Object.assign({}, props.urlState, { visEncoding: mockNewEncoding }), + undefined ); expect(props.history.push).toHaveBeenCalledTimes(1); }); @@ -203,7 +207,8 @@ describe('DeepDependencyGraphPage', () => { const operation = 'newOperation'; ddgPageImpl.setOperation(operation); expect(getUrlSpy).toHaveBeenLastCalledWith( - Object.assign({}, props.urlState, { operation, visEncoding: undefined }) + Object.assign({}, props.urlState, { operation, visEncoding: undefined }), + undefined ); expect(props.history.push).toHaveBeenCalledTimes(1); }); @@ -219,7 +224,8 @@ describe('DeepDependencyGraphPage', () => { it('updates service and clears operation and visEncoding', () => { ddgPageImpl.setService(service); expect(getUrlSpy).toHaveBeenLastCalledWith( - Object.assign({}, props.urlState, { operation: undefined, service, visEncoding: undefined }) + Object.assign({}, props.urlState, { operation: undefined, service, visEncoding: undefined }), + undefined ); expect(props.history.push).toHaveBeenCalledTimes(1); }); @@ -382,20 +388,19 @@ describe('DeepDependencyGraphPage', () => { } ); let getUrlStateSpy; - let sanitizeUrlStateSpy; let makeGraphSpy; + let sanitizeUrlStateSpy; beforeAll(() => { getUrlStateSpy = jest.spyOn(url, 'getUrlState'); sanitizeUrlStateSpy = jest.spyOn(url, 'sanitizeUrlState'); - makeGraphSpy = jest.spyOn(GraphModel, 'makeGraph'); + makeGraphSpy = jest.spyOn(GraphModel, 'makeGraph').mockReturnValue(mockGraph); }); beforeEach(() => { - getUrlStateSpy.mockReset(); + getUrlStateSpy.mockClear(); getUrlStateSpy.mockReturnValue(expected.urlState); - makeGraphSpy.mockReset(); - makeGraphSpy.mockReturnValue(mockGraph); + makeGraphSpy.mockClear(); }); it('uses gets relevant params from location.search', () => { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx index ae64951b87..50734cce7d 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx @@ -20,7 +20,7 @@ import { connect } from 'react-redux'; import Header from './Header'; import Graph from './Graph'; -import { getUrl, getUrlState, sanitizeUrlState } from './url'; +import { getUrl, getUrlState, sanitizeUrlState, ROUTE_PATH } from './url'; import ErrorMessage from '../common/ErrorMessage'; import LoadingIndicator from '../common/LoadingIndicator'; import { extractUiFindFromState, TExtractUiFindFromStateReturn } from '../common/UiFindInput'; @@ -43,38 +43,46 @@ import { TDdgStateEntry } from '../../types/TDdgState'; import './index.css'; -type TDispatchProps = { - addViewModifier: (kwarg: TDdgModelParams & { viewModifier: number; visibilityIndices: number[] }) => void; - fetchDeepDependencyGraph: (query: TDdgModelParams) => void; - fetchServices: () => void; - fetchServiceOperations: (service: string) => void; - removeViewModifierFromIndices: ( +export type TDispatchProps = { + addViewModifier?: (kwarg: TDdgModelParams & { viewModifier: number; visibilityIndices: number[] }) => void; + fetchDeepDependencyGraph?: (query: TDdgModelParams) => void; + fetchServices?: () => void; + fetchServiceOperations?: (service: string) => void; + removeViewModifierFromIndices?: ( kwarg: TDdgModelParams & { viewModifier: number; visibilityIndices: number[] } ) => void; }; -type TReduxProps = TExtractUiFindFromStateReturn & { +export type TReduxProps = TExtractUiFindFromStateReturn & { graph: GraphModel | undefined; graphState?: TDdgStateEntry; - operationsForService: Record; + operationsForService?: Record; services?: string[] | null; urlState: TDdgSparseUrlState; }; -type TOwnProps = { +export type TOwnProps = { + baseUrl: string; + extraUrlArgs?: { [key: string]: unknown }; history: RouterHistory; location: Location; + showSvcOpsHeader: boolean; }; -type TProps = TDispatchProps & TReduxProps & TOwnProps; +export type TProps = TDispatchProps & TReduxProps & TOwnProps; // export for tests export class DeepDependencyGraphPageImpl extends React.PureComponent { + static defaultProps = { + showSvcOpsHeader: true, + baseUrl: ROUTE_PATH, + }; + static fetchModelIfStale(props: TProps) { const { fetchDeepDependencyGraph, graphState = null, urlState } = props; const { service, operation } = urlState; // backend temporarily requires service and operation - if (!graphState && service && operation) { + if (!graphState && service && operation && fetchDeepDependencyGraph) { fetchDeepDependencyGraph({ service, operation, start: 0, end: 0 }); } } @@ -86,10 +94,15 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { const { fetchServices, fetchServiceOperations, operationsForService, services, urlState } = props; const { service } = urlState; - if (!services) { + if (!services && fetchServices) { fetchServices(); } - if (service && !Reflect.has(operationsForService, service)) { + if ( + service && + operationsForService && + !Reflect.has(operationsForService, service) && + fetchServiceOperations + ) { fetchServiceOperations(service); } } @@ -133,7 +146,7 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { setService = (service: string) => { const { fetchServiceOperations, operationsForService } = this.props; - if (!Reflect.has(operationsForService, service)) { + if (operationsForService && !Reflect.has(operationsForService, service) && fetchServiceOperations) { fetchServiceOperations(service); } this.updateUrlState({ operation: undefined, service, visEncoding: undefined }); @@ -151,14 +164,16 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { } const visibilityIndices = pathElems.map(pe => pe.visibilityIdx); const fn = enable ? addViewModifier : removeViewModifierFromIndices; - fn({ - operation, - service, - viewModifier, - visibilityIndices, - end: 0, - start: 0, - }); + if (fn) { + fn({ + operation, + service, + viewModifier, + visibilityIndices, + end: 0, + start: 0, + }); + } }; showVertices = (vertices: TDdgVertex[]) => { @@ -171,15 +186,25 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { toggleShowOperations = (enable: boolean) => this.updateUrlState({ showOp: enable }); updateUrlState = (newValues: Partial) => { - const { graphState, history, uiFind, urlState } = this.props; - const getUrlArg = { uiFind, ...urlState, ...newValues }; + const { baseUrl, extraUrlArgs, graphState, history, uiFind, urlState } = this.props; + const getUrlArg = { uiFind, ...urlState, ...newValues, ...extraUrlArgs }; const hash = _get(graphState, 'model.hash'); if (hash) getUrlArg.hash = hash; - history.push(getUrl(getUrlArg)); + history.push(getUrl(getUrlArg, baseUrl)); }; render() { - const { graph, graphState, operationsForService, services, uiFind, urlState } = this.props; + const { + baseUrl, + extraUrlArgs, + graph, + graphState, + operationsForService, + services, + uiFind, + urlState, + showSvcOpsHeader, + } = this.props; const { density, operation, service, showOp, visEncoding } = urlState; const distanceToPathElems = graphState && graphState.state === fetchedState.DONE ? graphState.model.distanceToPathElems : undefined; @@ -200,9 +225,11 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { content = ( { distanceToPathElems={distanceToPathElems} hiddenUiFindMatches={hiddenUiFindMatches} operation={operation} - operations={operationsForService[service || '']} + operations={operationsForService && operationsForService[service || '']} service={service} services={services} setDensity={this.setDensity} @@ -240,6 +267,7 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { setOperation={this.setOperation} setService={this.setService} showOperations={showOp} + showParameters={showSvcOpsHeader} showVertices={this.showVertices} toggleShowOperations={this.toggleShowOperations} uiFindCount={uiFind ? uiFindMatches && uiFindMatches.size : undefined} diff --git a/packages/jaeger-ui/src/components/DeepDependencies/traces.test.js b/packages/jaeger-ui/src/components/DeepDependencies/traces.test.js new file mode 100644 index 0000000000..20333e571c --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/traces.test.js @@ -0,0 +1,141 @@ +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; +import { shallow } from 'enzyme'; +import queryString from 'query-string'; + +import { DeepDependencyGraphPageImpl } from '.'; +import { TracesDdgImpl, mapStateToProps } from './traces'; +import * as url from './url'; +import { ROUTE_PATH } from '../SearchTracePage/url'; +import * as GraphModel from '../../model/ddg/GraphModel'; +import * as transformDdgData from '../../model/ddg/transformDdgData'; +import * as transformTracesToPaths from '../../model/ddg/transformTracesToPaths'; + +describe('TracesDdg', () => { + it('renders DeepDependencyGraphPageImpl with specific props', () => { + const passProps = { + propName0: 'propValue0', + propName1: 'propValue1', + }; + const extraUrlArgs = ['end', 'start', 'limit', 'lookback', 'maxDuration', 'minDuration', 'view'].reduce( + (curr, key) => ({ + ...curr, + [key]: `test ${key}`, + }), + {} + ); + const search = queryString.stringify({ ...extraUrlArgs, extraParam: 'extraParam' }); + + const wrapper = shallow(); + const ddgPage = wrapper.find(DeepDependencyGraphPageImpl); + expect(ddgPage.props()).toEqual( + expect.objectContaining({ + ...passProps, + baseUrl: ROUTE_PATH, + extraUrlArgs, + showSvcOpsHeader: false, + }) + ); + }); + + describe('mapStateToProps()', () => { + const hash = 'test hash'; + const mockModel = { hash }; + const mockGraph = { model: mockModel }; + const mockPayload = 'test payload'; + const urlState = { + service: 'testService', + operation: 'testOperation', + visEncoding: 'testVisEncoding', + }; + const ownProps = { + location: { + search: queryString.stringify(urlState), + }, + }; + const state = { + router: { location: ownProps.location }, + trace: { + traces: { + testTraceID: 'test trace data', + }, + }, + }; + + let getUrlStateSpy; + let makeGraphSpy; + let sanitizeUrlStateSpy; + let spies; + let transformDdgDataSpy; + let transformTracesToPathsSpy; + + beforeAll(() => { + getUrlStateSpy = jest.spyOn(url, 'getUrlState'); + makeGraphSpy = jest.spyOn(GraphModel, 'makeGraph').mockReturnValue(mockGraph); + sanitizeUrlStateSpy = jest.spyOn(url, 'sanitizeUrlState').mockImplementation(u => u); + transformDdgDataSpy = jest.spyOn(transformDdgData, 'default').mockReturnValue(mockModel); + transformTracesToPathsSpy = jest.spyOn(transformTracesToPaths, 'default').mockReturnValue(mockPayload); + spies = [ + getUrlStateSpy, + makeGraphSpy, + sanitizeUrlStateSpy, + transformDdgDataSpy, + transformTracesToPathsSpy, + ]; + }); + + beforeEach(() => { + spies.forEach(spy => spy.mockClear()); + getUrlStateSpy.mockReturnValue(urlState); + }); + + it('gets props from url', () => { + expect(mapStateToProps(state, ownProps)).toEqual(expect.objectContaining({ urlState })); + }); + + it('calculates graphState and graph iff service is provided', () => { + expect(mapStateToProps(state, ownProps)).toEqual( + expect.objectContaining({ + graph: mockGraph, + graphState: expect.objectContaining({ model: mockModel }), + }) + ); + + const { service: _, ...urlStateWithoutService } = urlState; + getUrlStateSpy.mockReturnValue(urlStateWithoutService); + expect(mapStateToProps(state, ownProps)).toEqual( + expect.objectContaining({ + graph: undefined, + graphState: undefined, + }) + ); + }); + + it('feeds memoized functions same arguments for same url and state data', () => { + mapStateToProps(state, ownProps); + mapStateToProps(state, ownProps); + spies.forEach(spy => { + const [call0, call1] = spy.mock.calls; + call0.forEach((arg, i) => expect(call1[i]).toBe(arg)); + }); + }); + + it('sanitizes url', () => { + mapStateToProps(state, ownProps); + expect(sanitizeUrlStateSpy).toHaveBeenLastCalledWith(urlState, hash); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/traces.tsx b/packages/jaeger-ui/src/components/DeepDependencies/traces.tsx new file mode 100644 index 0000000000..e7cf44b0d0 --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/traces.tsx @@ -0,0 +1,78 @@ +// 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 _get from 'lodash/get'; +import memoizeOne from 'memoize-one'; +import queryString from 'query-string'; +import { connect } from 'react-redux'; + +import { DeepDependencyGraphPageImpl, TOwnProps, TProps, TReduxProps } from '.'; +import { getUrlState, sanitizeUrlState } from './url'; +import { ROUTE_PATH } from '../SearchTracePage/url'; +import GraphModel, { makeGraph } from '../../model/ddg/GraphModel'; +import { fetchedState } from '../../constants'; +import { extractUiFindFromState } from '../common/UiFindInput'; +import transformDdgData from '../../model/ddg/transformDdgData'; +import transformTracesToPaths from '../../model/ddg/transformTracesToPaths'; + +import { TDdgStateEntry } from '../../types/TDdgState'; +import { ReduxState } from '../../types'; + +// Required for proper memoization of subsequent function calls +const svcOp = memoizeOne((service, operation) => ({ service, operation })); + +// export for tests +export function mapStateToProps(state: ReduxState, ownProps: TOwnProps): TReduxProps { + const urlState = getUrlState(ownProps.location.search); + const { density, operation, service, showOp } = urlState; + let graphState: TDdgStateEntry | undefined; + let graph: GraphModel | undefined; + if (service) { + const payload = transformTracesToPaths(state.trace.traces, service, operation); + graphState = { + model: transformDdgData(payload, svcOp(service, operation)), + state: fetchedState.DONE, + viewModifiers: new Map(), + }; + graph = makeGraph(graphState.model, showOp, density); + } + + return { + graph, + graphState, + urlState: sanitizeUrlState(urlState, _get(graphState, 'model.hash')), + ...extractUiFindFromState(state), + }; +} + +// export for tests +export class TracesDdgImpl extends React.PureComponent { + render(): React.ReactNode { + const { location } = this.props; + const urlArgs = queryString.parse(location.search); + const { end, start, limit, lookback, maxDuration, minDuration, view } = urlArgs; + const extraArgs = { end, start, limit, lookback, maxDuration, minDuration, view }; + return ( + + ); + } +} + +export default connect(mapStateToProps)(TracesDdgImpl); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/url.test.js b/packages/jaeger-ui/src/components/DeepDependencies/url.test.js index 6b8946cfa9..26be8e5219 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/url.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/url.test.js @@ -52,6 +52,11 @@ describe('DeepDependencyGraph/url', () => { expect(getUrl({})).toBe('/deep-dependencies'); }); + it('uses given baseUrl', () => { + const baseUrl = 'test base url'; + expect(getUrl({}, baseUrl)).toBe(baseUrl); + }); + it('includes provided args', () => { const paramA = 'aParam'; const paramB = 'bParam'; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/url.tsx b/packages/jaeger-ui/src/components/DeepDependencies/url.tsx index 9f37dda2b8..e31564d6c6 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/url.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/url.tsx @@ -28,7 +28,7 @@ export function matches(path: string) { return Boolean(matchPath(path, ROUTE_MATCHER)); } -export function getUrl(args?: { [key: string]: unknown; showOp?: boolean }) { +export function getUrl(args?: { [key: string]: unknown; showOp?: boolean }, baseUrl: string = ROUTE_PATH) { if (args && !_isEmpty(args)) { const stringifyArgs = Reflect.has(args, 'showOp') ? { @@ -36,9 +36,9 @@ export function getUrl(args?: { [key: string]: unknown; showOp?: boolean }) { showOp: args.showOp ? 1 : 0, } : args; - return `${ROUTE_PATH}?${queryString.stringify(stringifyArgs)}`; + return `${baseUrl}?${queryString.stringify(stringifyArgs)}`; } - return ROUTE_PATH; + return baseUrl; } function firstParam(arg: string | string[]): string { diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/AltViewOptions.test.js b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/AltViewOptions.test.js new file mode 100644 index 0000000000..74da688e73 --- /dev/null +++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/AltViewOptions.test.js @@ -0,0 +1,40 @@ +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { Button } from 'antd'; + +import AltViewOptions from './AltViewOptions'; + +describe('AltViewOptions', () => { + const props = { + traceResultsView: true, + onTraceGraphViewClicked: jest.fn(), + }; + let wrapper; + + beforeEach(() => { + props.onTraceGraphViewClicked.mockClear(); + wrapper = shallow(); + }); + + it('renders correct label', () => { + const getLabel = () => wrapper.find(Button).prop('children'); + expect(getLabel()).toBe('Deep Dependency Graph'); + + wrapper.setProps({ traceResultsView: false }); + expect(getLabel()).toBe('Trace Results'); + }); +}); diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/AltViewOptions.tsx b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/AltViewOptions.tsx new file mode 100644 index 0000000000..d99d9b1248 --- /dev/null +++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/AltViewOptions.tsx @@ -0,0 +1,30 @@ +// 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 { Button } from 'antd'; + +type Props = { + onTraceGraphViewClicked: () => void; + traceResultsView: boolean; +}; + +export default function AltViewOptions(props: Props) { + const { onTraceGraphViewClicked, traceResultsView } = props; + return ( + + ); +} diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.css b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.css index ed61fc1911..3b715672c5 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.css +++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.css @@ -14,6 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +.SearchResults { + display: flex; + flex-direction: column; + flex-grow: 1; +} + .SearchResults--header { border: 1px solid #e6e6e6; color: var(--tx-color-title); @@ -23,11 +29,14 @@ limitations under the License. .SearchResults--headerOverview { align-items: center; background-color: #f5f5f5; - border-top: 1px solid #e6e6e6; display: flex; padding: 1rem; } +.SearchResults--headerScatterPlot { + border-bottom: 1px solid #e6e6e6; +} + .SearchResults--resultLink { color: inherit; } @@ -35,3 +44,9 @@ limitations under the License. .SearchResults--resultLink:hover { color: inherit; } + +.SearchResults--ddg-container { + border: #e6e6e6 1px solid; + flex-grow: 1; + position: relative; +} diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.js b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.js index 82c6d343cd..912092a583 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.js @@ -16,9 +16,12 @@ import * as React from 'react'; import { Select } from 'antd'; -import { Link } from 'react-router-dom'; -import { Field, reduxForm, formValueSelector } from 'redux-form'; +import { History as RouterHistory, Location } from 'history'; +import { Link, withRouter } from 'react-router-dom'; +import { Field, formValueSelector, reduxForm } from 'redux-form'; +import queryString from 'query-string'; +import AltViewOptions from './AltViewOptions'; import DiffSelection from './DiffSelection'; import * as markers from './index.markers'; import ResultItem from './ResultItem'; @@ -26,6 +29,7 @@ import ScatterPlot from './ScatterPlot'; import { getUrl } from '../url'; import LoadingIndicator from '../../common/LoadingIndicator'; import NewWindowIcon from '../../common/NewWindowIcon'; +import SearchResultsDDG from '../../DeepDependencies/traces'; import { getLocation } from '../../TracePage/url'; import * as orderBy from '../../../model/order-by'; import { getPercentageOfDuration } from '../../../utils/date'; @@ -44,7 +48,9 @@ type SearchResultsProps = { disableComparisons: boolean, goToTrace: string => void, hideGraph: boolean, + history: RouterHistory, loading: boolean, + location: Location, maxTraceDuration: number, queryOfResults?: SearchQuery, showStandaloneLink: boolean, @@ -81,7 +87,7 @@ const SelectSort = reduxForm({ export const sortFormSelector = formValueSelector('traceResultsSort'); -export default class SearchResults extends React.PureComponent { +export class UnconnectedSearchResults extends React.PureComponent { props: SearchResultsProps; static defaultProps = { skipMessage: false, queryOfResults: undefined }; @@ -95,19 +101,31 @@ export default class SearchResults extends React.PureComponent { + const { location, history } = this.props; + const urlState = queryString.parse(location.search); + const view = urlState.view && urlState.view === 'ddg' ? 'traces' : 'ddg'; + history.push(getUrl({ ...urlState, view })); + }; + render() { const { diffCohort, disableComparisons, goToTrace, hideGraph, + history, loading, + location, maxTraceDuration, queryOfResults, showStandaloneLink, skipMessage, traces, } = this.props; + + const traceResultsView = queryString.parse(location.search).view !== 'ddg'; + const diffSelection = !disableComparisons && ( ); @@ -134,45 +152,52 @@ export default class SearchResults extends React.PureComponent datum.id)); const searchUrl = queryOfResults ? getUrl(stripEmbeddedState(queryOfResults)) : getUrl(); return ( -
-
-
- {!hideGraph && ( -
- ({ - x: t.startTime, - y: t.duration, - traceID: t.traceID, - size: t.spans.length, - name: t.traceName, - }))} - onValueClick={t => { - goToTrace(t.traceID); - }} - /> -
- )} -
-

- {traces.length} Trace{traces.length > 1 && 's'} -

- - {showStandaloneLink && ( - - - - )} +
+
+ {!hideGraph && traceResultsView && ( +
+ ({ + x: t.startTime, + y: t.duration, + traceID: t.traceID, + size: t.spans.length, + name: t.traceName, + }))} + onValueClick={t => { + goToTrace(t.traceID); + }} + />
+ )} +
+

+ {traces.length} Trace{traces.length > 1 && 's'} +

+ {traceResultsView && } + + {showStandaloneLink && ( + + + + )}
-
- {diffSelection} + {!traceResultsView && ( +
+ +
+ )} + {traceResultsView && diffSelection} + {traceResultsView && (
    {traces.map(trace => (
  • @@ -187,8 +212,10 @@ export default class SearchResults extends React.PureComponent ))}
-
+ )}
); } } + +export default withRouter(UnconnectedSearchResults); diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.test.js b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.test.js index 0cc7bfa82c..67d6a868d4 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.test.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/index.test.js @@ -15,12 +15,15 @@ import React from 'react'; import { shallow } from 'enzyme'; -import SearchResults from '.'; +import { UnconnectedSearchResults as SearchResults } from '.'; import * as markers from './index.markers'; +import AltViewOptions from './AltViewOptions'; +import DiffSelection from './DiffSelection'; import ResultItem from './ResultItem'; import ScatterPlot from './ScatterPlot'; -import DiffSelection from './DiffSelection'; +import { getUrl } from '../url'; import LoadingIndicator from '../../common/LoadingIndicator'; +import SearchResultsDDG from '../../DeepDependencies/traces'; describe('', () => { let wrapper; @@ -32,6 +35,7 @@ describe('', () => { props = { diffCohort: [], goToTrace: () => {}, + location: {}, loading: false, maxTraceDuration: 1, queryOfResults: {}, @@ -68,5 +72,45 @@ describe('', () => { it('shows a result entry for each trace', () => { expect(wrapper.find(ResultItem).length).toBe(traces.length); }); + + describe('ddg', () => { + const searchParam = 'view'; + const viewDdg = 'ddg'; + const viewTraces = 'traces'; + const search = `${searchParam}=${viewDdg}`; + + it('updates url to view ddg and back and back again', () => { + const otherParam = 'param'; + const otherValue = 'value'; + const otherSearch = `?${otherParam}=${otherValue}`; + const push = jest.fn(); + wrapper.setProps({ history: { push }, location: { search: otherSearch } }); + + const toggle = wrapper.find(AltViewOptions).prop('onTraceGraphViewClicked'); + toggle(); + expect(push).toHaveBeenLastCalledWith(getUrl({ [otherParam]: otherValue, [searchParam]: viewDdg })); + + wrapper.setProps({ location: { search: `${otherSearch}&${search}` } }); + toggle(); + expect(push).toHaveBeenLastCalledWith( + getUrl({ [otherParam]: otherValue, [searchParam]: viewTraces }) + ); + + wrapper.setProps({ location: { search: `${otherSearch}&${searchParam}=${viewTraces}` } }); + toggle(); + expect(push).toHaveBeenLastCalledWith(getUrl({ [otherParam]: otherValue, [searchParam]: viewDdg })); + }); + + it('shows ddg instead of scatterplot and results', () => { + expect(wrapper.find(SearchResultsDDG).length).toBe(0); + expect(wrapper.find(ResultItem).length).not.toBe(0); + expect(wrapper.find(ScatterPlot).length).not.toBe(0); + + wrapper.setProps({ location: { search: `?${search}` } }); + expect(wrapper.find(SearchResultsDDG).length).toBe(1); + expect(wrapper.find(ResultItem).length).toBe(0); + expect(wrapper.find(ScatterPlot).length).toBe(0); + }); + }); }); }); diff --git a/packages/jaeger-ui/src/components/SearchTracePage/index.css b/packages/jaeger-ui/src/components/SearchTracePage/index.css index 313943627e..16a0d2ddfa 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/index.css +++ b/packages/jaeger-ui/src/components/SearchTracePage/index.css @@ -14,7 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ +.SearchTracePage--row { + display: flex; + flex-grow: 1; +} + .SearchTracePage--column { + display: flex; + flex-direction: column; + flex-grow: 1; padding: 1rem 0.5rem; } diff --git a/packages/jaeger-ui/src/components/SearchTracePage/index.js b/packages/jaeger-ui/src/components/SearchTracePage/index.js index cd4426736a..993636f0e4 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/index.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/index.js @@ -95,58 +95,56 @@ export class SearchTracePageImpl extends Component { const showErrors = errors && !loadingTraces; const showLogo = isHomepage && !hasTraceResults && !loadingTraces && !errors; return ( -
- - {!embedded && ( - -
- - - {!loadingServices && services ? : } - - - - - -
- - )} - - {showErrors && ( -
-

There was an error querying for traces:

- {errors.map(err => ( - - ))} -
- )} - {!showErrors && ( - - )} - {showLogo && ( - presentation - )} + + {!embedded && ( + +
+ + + {!loadingServices && services ? : } + + + + + +
-
-
+ )} + + {showErrors && ( +
+

There was an error querying for traces:

+ {errors.map(err => ( + + ))} +
+ )} + {!showErrors && ( + + )} + {showLogo && ( + presentation + )} + + ); } } diff --git a/packages/jaeger-ui/src/components/SearchTracePage/index.test.js b/packages/jaeger-ui/src/components/SearchTracePage/index.test.js index 65ca7277bd..95035816fa 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/index.test.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/index.test.js @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { MemoryRouter } from 'react-router-dom'; + jest.mock('redux-form', () => { function reduxForm() { return component => component; @@ -23,7 +25,6 @@ jest.mock('redux-form', () => { return { Field, formValueSelector, reduxForm }; }); -jest.mock('react-router-dom'); jest.mock('store'); /* eslint-disable import/first */ @@ -76,7 +77,11 @@ describe('', () => { props.fetchServiceOperations.mockClear(); const oldFn = store.get; store.get = jest.fn(() => ({ service: 'svc-b' })); - wrapper = mount(); + wrapper = mount( + + + + ); expect(props.fetchServices.mock.calls.length).toBe(1); expect(props.fetchServiceOperations.mock.calls.length).toBe(1); store.get = oldFn; @@ -87,8 +92,16 @@ describe('', () => { const query = 'some-query'; const historyPush = jest.fn(); const historyMock = { push: historyPush }; - wrapper = mount(); - wrapper.instance().goToTrace(traceID); + wrapper = mount( + + + + ); + wrapper + .find(SearchTracePage) + .first() + .instance() + .goToTrace(traceID); expect(historyPush.mock.calls.length).toBe(1); expect(historyPush.mock.calls[0][0]).toEqual({ pathname: `/trace/${traceID}`, @@ -155,9 +168,14 @@ describe('mapStateToProps()', () => { services: stateServices, }; - const { maxTraceDuration, traceResults, diffCohort, numberOfTraceResults, ...rest } = mapStateToProps( - state - ); + const { + maxTraceDuration, + traceResults, + diffCohort, + numberOfTraceResults, + location, + ...rest + } = mapStateToProps(state); expect(traceResults).toHaveLength(stateTrace.search.results.length); expect(traceResults[0].traceID).toBe(trace.traceID); expect(maxTraceDuration).toBe(trace.duration); diff --git a/packages/jaeger-ui/src/model/ddg/transformDdgData.tsx b/packages/jaeger-ui/src/model/ddg/transformDdgData.tsx index bd6bff9af3..4680fe36c3 100644 --- a/packages/jaeger-ui/src/model/ddg/transformDdgData.tsx +++ b/packages/jaeger-ui/src/model/ddg/transformDdgData.tsx @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import memoizeOne from 'memoize-one'; import objectHash from 'object-hash'; import { @@ -36,7 +37,7 @@ function group(arg: { key: string; value: any }[]): Record { return result; } -export default function transformDdgData( +function transformDdgData( { dependencies }: TDdgPayload, { service: focalService, operation: focalOperation }: { service: string; operation?: string } ): TDdgModel { @@ -152,3 +153,5 @@ export default function transformDdgData( visIdxToPathElem, }; } + +export default memoizeOne(transformDdgData); diff --git a/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.test.js b/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.test.js new file mode 100644 index 0000000000..05970215e7 --- /dev/null +++ b/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.test.js @@ -0,0 +1,153 @@ +// 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 transformTracesToPaths from './transformTracesToPaths'; + +describe('transform traces to ddg paths', () => { + const makeSpan = (spanName, childOf) => ({ + hasChildren: true, + operationName: `${spanName} operation`, + processID: `${spanName} processID`, + references: childOf + ? [ + { + refType: 'CHILD_OF', + span: childOf, + spanID: childOf.spanID, + }, + ] + : [], + spanID: `${spanName} spanID`, + }); + const makeTrace = (spans, traceID) => ({ + data: { + processes: spans.reduce( + (result, span) => ({ + ...result, + [span.processID]: { + serviceName: `${span.spanID.split(' ')[0]} service`, + }, + }), + {} + ), + spans, + traceID, + }, + }); + + const linearTraceID = 'linearTraceID'; + const missTraceID = 'missTraceID'; + const shortTraceID = 'shortTraceID'; + const rootSpan = makeSpan('root'); + const childSpan = makeSpan('child', rootSpan); + const grandchildSpan = makeSpan('grandchild', childSpan); + + it('transforms single short trace result payload', () => { + const traces = { + [shortTraceID]: makeTrace([rootSpan, { ...childSpan, hasChildren: false }], shortTraceID), + }; + + const { dependencies: result } = transformTracesToPaths(traces, 'child service'); + expect(result.length).toBe(1); + expect(result[0].path.length).toBe(2); + }); + + it('transforms multiple traces result payload', () => { + const traces = { + [shortTraceID]: makeTrace([rootSpan, { ...childSpan, hasChildren: false }], shortTraceID), + [linearTraceID]: makeTrace( + [rootSpan, childSpan, { ...grandchildSpan, hasChildren: false }], + linearTraceID + ), + }; + + const { dependencies: result } = transformTracesToPaths(traces, 'child service'); + expect(result.length).toBe(2); + expect(result[0].path.length).toBe(2); + expect(result[1].path.length).toBe(3); + }); + + it('ignores paths without focalService', () => { + const branchingTraceID = 'branchingTraceID'; + const uncleSpan = makeSpan('uncle', rootSpan); + uncleSpan.hasChildren = false; + const traces = { + [missTraceID]: makeTrace([rootSpan, childSpan, uncleSpan], missTraceID), + [branchingTraceID]: makeTrace( + [rootSpan, childSpan, uncleSpan, { ...grandchildSpan, hasChildren: false }], + branchingTraceID + ), + }; + + const { dependencies: result } = transformTracesToPaths(traces, 'child service'); + expect(result.length).toBe(1); + expect(result[0].path.length).toBe(3); + }); + + it('matches service and operation names', () => { + const childSpanWithDiffOp = { + ...childSpan, + hasChildren: false, + operationName: 'diff operation', + }; + const traces = { + [missTraceID]: makeTrace([rootSpan, childSpanWithDiffOp], missTraceID), + [linearTraceID]: makeTrace( + [rootSpan, childSpan, { ...grandchildSpan, hasChildren: false }], + linearTraceID + ), + }; + + const { dependencies: result } = transformTracesToPaths(traces, 'child service'); + expect(result.length).toBe(2); + expect(result[0].path.length).toBe(2); + expect(result[1].path.length).toBe(3); + + const { dependencies: resultWithOp } = transformTracesToPaths(traces, 'child service', 'child operation'); + expect(resultWithOp.length).toBe(1); + expect(resultWithOp[0].path.length).toBe(3); + }); + + it('transforms multiple paths from single trace', () => { + const traces = { + [linearTraceID]: makeTrace( + [rootSpan, { ...childSpan, hasChildren: false }, { ...grandchildSpan, hasChildren: false }], + linearTraceID + ), + }; + + const { dependencies: result } = transformTracesToPaths(traces, 'child service'); + expect(result.length).toBe(2); + expect(result[0].path.length).toBe(2); + expect(result[1].path.length).toBe(3); + }); + + it('errors if span has ancestor id not in trace data', () => { + const traces = { + [linearTraceID]: makeTrace([rootSpan, { ...grandchildSpan, hasChildren: false }], linearTraceID), + }; + + expect(() => transformTracesToPaths(traces, 'child service')).toThrowError(/Ancestor spanID.*not found/); + }); + + it('skips trace without data', () => { + const traces = { + [shortTraceID]: makeTrace([rootSpan, { ...childSpan, hasChildren: false }], shortTraceID), + noData: {}, + }; + + const { dependencies: result } = transformTracesToPaths(traces, 'child service'); + expect(result.length).toBe(1); + }); +}); diff --git a/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.tsx b/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.tsx new file mode 100644 index 0000000000..d5e5d4b514 --- /dev/null +++ b/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.tsx @@ -0,0 +1,75 @@ +// 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 memoizeOne from 'memoize-one'; + +import spanAncestorIds from '../../utils/span-ancestor-ids'; + +import { TDdgPayloadEntry, TDdgPayloadPath, TDdgPayload } from './types'; +import { FetchedTrace } from '../../types'; +import { Span, Trace } from '../../types/trace'; + +function convertSpan(span: Span, trace: Trace): TDdgPayloadEntry { + const serviceName = trace.processes[span.processID].serviceName; + const operationName = span.operationName; + return { service: serviceName, operation: operationName }; +} + +function transformTracesToPaths( + traces: Record, + focalService: string, + focalOperation: string | undefined +): TDdgPayload { + const dependencies: TDdgPayloadPath[] = []; + Object.values(traces).forEach(({ data }) => { + if (data) { + const spanMap: Map = new Map(); + const { traceID } = data; + data.spans + .filter(span => { + spanMap.set(span.spanID, span); + return !span.hasChildren; + }) + .forEach(leaf => { + const path = spanAncestorIds(leaf).map(id => { + const span = spanMap.get(id); + if (!span) throw new Error(`Ancestor spanID ${id} not found in trace ${traceID}`); + + return convertSpan(span, data); + }); + path.push(convertSpan(leaf, data)); + + if ( + path.some( + ({ service, operation }) => + service === focalService && (!focalOperation || operation === focalOperation) + ) + ) { + dependencies.push({ + path, + attributes: [ + { + key: 'exemplar_trace_id', + value: traceID, + }, + ], + }); + } + }); + } + }); + return { dependencies }; +} + +export default memoizeOne(transformTracesToPaths); diff --git a/packages/jaeger-ui/src/model/ddg/types.tsx b/packages/jaeger-ui/src/model/ddg/types.tsx index 0cb08932ff..b9887cf399 100644 --- a/packages/jaeger-ui/src/model/ddg/types.tsx +++ b/packages/jaeger-ui/src/model/ddg/types.tsx @@ -50,17 +50,19 @@ export type TDdgPayloadEntry = { service: string; }; -export type TDdgPayload = { - dependencies: { - path: TDdgPayloadEntry[]; - // TODO: Everett Tech Debt: Fix KeyValuePair types - attributes: { - key: 'exemplar_trace_id'; // eslint-disable-line camelcase - value: string; - }[]; +export type TDdgPayloadPath = { + path: TDdgPayloadEntry[]; + // TODO: Everett Tech Debt: Fix KeyValuePair types + attributes: { + key: 'exemplar_trace_id'; // eslint-disable-line camelcase + value: string; }[]; }; +export type TDdgPayload = { + dependencies: TDdgPayloadPath[]; +}; + export type TDdgService = { name: string; operations: Map;