From 44f04211521289fc452bfe49032a51745c7df810 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Wed, 6 Nov 2019 14:59:04 -0500 Subject: [PATCH 1/3] WIP - Implement DDGNode Vis Interactions Signed-off-by: Everett Ross --- .../Graph/DdgNodeContent/index.tsx | 76 +++++++++++++--- .../DeepDependencies/Graph/index.tsx | 37 ++++++-- .../src/components/DeepDependencies/index.tsx | 88 +++++++++++++++++-- .../GraphModel/getDerivedViewModifiers.tsx | 9 +- .../src/model/ddg/GraphModel/index.tsx | 7 +- packages/jaeger-ui/src/model/ddg/PathElem.tsx | 16 ++++ 6 files changed, 200 insertions(+), 33 deletions(-) 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 589aaf2842..de2980e416 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx @@ -31,18 +31,22 @@ import { getUrl } from '../../url'; import BreakableText from '../../../common/BreakableText'; import NewWindowIcon from '../../../common/NewWindowIcon'; import { getUrl as getSearchUrl } from '../../../SearchTracePage/url'; -import { EDdgDensity, EViewModifier, TDdgVertex, PathElem } from '../../../../model/ddg/types'; +import { ECheckedStatus, EDdgDensity, EDirection, EViewModifier, TDdgVertex, PathElem } from '../../../../model/ddg/types'; import './index.css'; type TProps = { focalNodeUrl: string | null; + focusPathsThroughVertex: (vertexKey: string) => void; + getGenerationVisibility: (vertexKey: string, direction: EDirection) => ECheckedStatus | null; getVisiblePathElems: (vertexKey: string) => PathElem[] | undefined; + hideVertex: (vertexKey: string) => void; isFocalNode: boolean; isPositioned: boolean; operation: string | null; service: string; setViewModifier: (vertexKey: string, viewModifier: EViewModifier, isEnabled: boolean) => void; + updateGenerationVisibility: (vertexKey: string, direction: EDirection) => void; vertexKey: string; }; @@ -56,14 +60,29 @@ export default class DdgNodeContent extends React.PureComponent { }; } - static getNodeRenderer( - getVisiblePathElems: (vertexKey: string) => PathElem[] | undefined, - setViewModifier: (vertexKey: string, viewModifier: EViewModifier, enable: boolean) => void, - density: EDdgDensity, - showOp: boolean, - baseUrl: string, - extraUrlArgs: { [key: string]: unknown } | undefined - ) { + static getNodeRenderer({ + baseUrl, + density, + extraUrlArgs, + focusPathsThroughVertex, + getGenerationVisibility, + getVisiblePathElems, + hideVertex, + setViewModifier, + showOp, + updateGenerationVisibility, + }: { + baseUrl: string; + density: EDdgDensity; + extraUrlArgs?: { [key: string]: unknown }; + focusPathsThroughVertex: (vertexKey: string) => void; + getGenerationVisibility: (vertexKey: string, direction: EDirection) => ECheckedStatus | null; + getVisiblePathElems: (vertexKey: string) => PathElem[] | undefined; + hideVertex: (vertexKey: string) => void; + setViewModifier: (vertexKey: string, viewModifier: EViewModifier, enable: boolean) => void; + showOp: boolean; + updateGenerationVisibility: (vertexKey: string, direction: EDirection) => void; + }) { return function renderNode(vertex: TDdgVertex, _: unknown, lv: TLayoutVertex | null) { const { isFocalNode, key, operation, service } = vertex; return ( @@ -71,18 +90,29 @@ export default class DdgNodeContent extends React.PureComponent { focalNodeUrl={ isFocalNode ? null : getUrl({ density, operation, service, showOp, ...extraUrlArgs }, baseUrl) } + focusPathsThroughVertex={focusPathsThroughVertex} + getGenerationVisibility={getGenerationVisibility} getVisiblePathElems={getVisiblePathElems} + hideVertex={hideVertex} isFocalNode={isFocalNode} isPositioned={Boolean(lv)} operation={operation} setViewModifier={setViewModifier} service={service} + updateGenerationVisibility={updateGenerationVisibility} vertexKey={key} /> ); }; } + /* + componentWillUnmount() { + const { vertexKey, setViewModifier } = this.props; + setViewModifier(vertexKey, EViewModifier.Hovered, false); + } + */ + private viewTraces = () => { const { vertexKey, getVisiblePathElems } = this.props; const elems = getVisiblePathElems(vertexKey); @@ -120,7 +150,18 @@ export default class DdgNodeContent extends React.PureComponent { }; render() { - const { focalNodeUrl, isFocalNode, isPositioned, operation, service } = this.props; + const { + focalNodeUrl, + focusPathsThroughVertex, + getGenerationVisibility, + hideVertex, + isFocalNode, + isPositioned, + operation, + service, + updateGenerationVisibility, + vertexKey, + } = this.props; const { radius, svcWidth, opWidth, svcMarginTop } = calcPositioning(service, operation); const scaleFactor = RADIUS / radius; const transform = `translate(${RADIUS - radius}px, ${RADIUS - radius}px) scale(${scaleFactor})`; @@ -162,6 +203,21 @@ export default class DdgNodeContent extends React.PureComponent { View traces + focusPathsThroughVertex(vertexKey)} role="button"> + Focus paths through this node + + hideVertex(vertexKey)} role="button"> + Hide node + + {/* TODO Move to other panels; use antd checkbox not checkedstatus text */} + updateGenerationVisibility(vertexKey, EDirection.Upstream)} role="button"> + {/* TODO Don't calculate when not visible, performance is _terrible_ as is */} + {getGenerationVisibility(vertexKey, EDirection.Upstream)} Parents + + updateGenerationVisibility(vertexKey, EDirection.Downstream)} role="button"> + {/* TODO Don't calculate when not visible, performance is _terrible_ as is */} + {getGenerationVisibility(vertexKey, EDirection.Downstream)} Children + ); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx index 2be3c11c34..2292797ae8 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx @@ -22,7 +22,14 @@ import TNonEmptyArray from '@jaegertracing/plexus/lib/types/TNonEmptyArray'; import DdgNodeContent from './DdgNodeContent'; import getNodeRenderers from './getNodeRenderers'; import getSetOnEdge from './getSetOnEdge'; -import { PathElem, TDdgVertex, EDdgDensity, EViewModifier } from '../../../model/ddg/types'; +import { + ECheckedStatus, + EDdgDensity, + EDirection, + EViewModifier, + PathElem, + TDdgVertex, +} from '../../../model/ddg/types'; import './index.css'; @@ -32,10 +39,14 @@ type TProps = { edges: TEdge[]; edgesViewModifiers: Map; extraUrlArgs?: { [key: string]: unknown }; + focusPathsThroughVertex: (vertexKey: string) => void; + getGenerationVisibility: (vertexKey: string, direction: EDirection) => ECheckedStatus | null; getVisiblePathElems: (vertexKey: string) => PathElem[] | undefined; + hideVertex: (vertexKey: string) => void; setViewModifier: (vertexKey: string, viewModifier: EViewModifier, enable: boolean) => void; showOp: boolean; uiFindMatches: Set | undefined; + updateGenerationVisibility: (vertexKey: string, direction: EDirection) => void; vertices: TDdgVertex[]; verticesViewModifiers: Map; }; @@ -82,17 +93,21 @@ export default class Graph extends PureComponent { render() { const { + baseUrl, density, edges, edgesViewModifiers, + extraUrlArgs, + focusPathsThroughVertex, + getGenerationVisibility, getVisiblePathElems, + hideVertex, setViewModifier, showOp, uiFindMatches, + updateGenerationVisibility, vertices, verticesViewModifiers, - baseUrl, - extraUrlArgs, } = this.props; const nodeRenderers = this.getNodeRenderers(uiFindMatches || this.emptyFindSet, verticesViewModifiers); @@ -140,14 +155,18 @@ export default class Graph extends PureComponent { layerType: 'html', measurable: true, measureNode: DdgNodeContent.measureNode, - renderNode: this.getNodeContentRenderer( - getVisiblePathElems, - setViewModifier, + renderNode: this.getNodeContentRenderer({ + baseUrl, density, + extraUrlArgs, + focusPathsThroughVertex, + getGenerationVisibility, + getVisiblePathElems, + hideVertex, + setViewModifier, showOp, - baseUrl, - extraUrlArgs - ), + updateGenerationVisibility, + }), }, ]} /> diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx index 6a86691192..f913c84c54 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx @@ -30,14 +30,16 @@ import { fetchedState } from '../../constants'; import getStateEntryKey from '../../model/ddg/getStateEntryKey'; import GraphModel, { makeGraph } from '../../model/ddg/GraphModel'; import { + ECheckedStatus, + EDdgDensity, EDirection, + EViewModifier, + PathElem, TDdgModelParams, TDdgSparseUrlState, TDdgVertex, - EDdgDensity, - EViewModifier, } from '../../model/ddg/types'; -import { encodeDistance } from '../../model/ddg/visibility-codec'; +import { encode, encodeDistance } from '../../model/ddg/visibility-codec'; import { ReduxState } from '../../types'; import { TDdgStateEntry } from '../../types/TDdgState'; @@ -111,6 +113,42 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { DeepDependencyGraphPageImpl.fetchModelIfStale(nextProps); } + focusPathsThroughVertex = (vertexKey: string) => { + const elems = this.getVisiblePathElems(vertexKey); + if (!elems) return; + const indices = ([] as number[]).concat(...elems.map(({ memberOf }) => memberOf.members.map(({ visibilityIdx }) => visibilityIdx))); + this.updateUrlState({ visEncoding: encode(indices) }); + } + + private getGeneration = (vertexKey: string, direction: EDirection): PathElem[] => { + const rv: PathElem[] = []; + const elems = this.getVisiblePathElems(vertexKey); + if (!elems) return rv; + elems.forEach(({ focalSideNeighbor, memberIdx, memberOf }) => { + const generationMember = memberOf.members[memberIdx + direction]; + if (generationMember && generationMember !== focalSideNeighbor) rv.push(generationMember); + }); + return rv; + } + + getGenerationVisibility = (vertexKey: string, direction: EDirection): ECheckedStatus | null => { + const { graph, urlState } = this.props; + const generation = this.getGeneration(vertexKey, direction); + if (!generation.length || !graph) return null; + + const visibleIndices = graph.getVisibleIndices(urlState.visEncoding); + let partial = false; + let full = true; + generation.forEach(elem => { + const isVis = visibleIndices.has(elem.visibilityIdx); + partial = partial || isVis; + full = full && isVis; + }); + if (full) return ECheckedStatus.Full; + if (partial) return ECheckedStatus.Partial; + return ECheckedStatus.Empty; + } + getVisiblePathElems = (key: string) => { const { graph, urlState } = this.props; if (graph) { @@ -119,6 +157,28 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { return undefined; }; + private hideElems = (elems: PathElem[]) => { + const { graph, urlState } = this.props; + const { visEncoding } = urlState; + if (!graph) return; + + const visible = graph.getVisibleIndices(visEncoding); + elems.forEach(({ externalPath }) => { + externalPath.forEach(({ visibilityIdx }) => { + visible.delete(visibilityIdx); + }); + }); + + this.updateUrlState({ visEncoding: encode(Array.from(visible)) }); + } + + hideVertex = (vertexKey: string) => { + const elems = this.getVisiblePathElems(vertexKey); + if (elems) this.hideElems(elems); + } + + setDensity = (density: EDdgDensity) => this.updateUrlState({ density }); + setDistance = (distance: number, direction: EDirection) => { const { graphState } = this.props; const { visEncoding } = this.props.urlState; @@ -137,8 +197,6 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { } }; - setDensity = (density: EDdgDensity) => this.updateUrlState({ density }); - setOperation = (operation: string) => { this.updateUrlState({ operation, visEncoding: undefined }); }; @@ -182,6 +240,22 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { toggleShowOperations = (enable: boolean) => this.updateUrlState({ showOp: enable }); + updateGenerationVisibility = (vertexKey: string, direction: EDirection) => { + const { graph, urlState } = this.props; + const { visEncoding } = urlState; + const generationElems = this.getGeneration(vertexKey, direction); + const currCheckedStatus = this.getGenerationVisibility(vertexKey, direction); + if (!graph || !generationElems || !currCheckedStatus) return; + + if (currCheckedStatus === ECheckedStatus.Full) { + this.hideElems(generationElems); + } else { + const visible = graph.getVisibleIndices(visEncoding); + generationElems.forEach(({ visibilityIdx }) => visible.add(visibilityIdx)); + this.updateUrlState({ visEncoding: encode(Array.from(visible)) }); + } + } + updateUrlState = (newValues: Partial) => { const { baseUrl, extraUrlArgs, graphState, history, uiFind, urlState } = this.props; const getUrlArg = { uiFind, ...urlState, ...newValues, ...extraUrlArgs }; @@ -227,10 +301,14 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { edges={edges} edgesViewModifiers={edgesViewModifiers} extraUrlArgs={extraUrlArgs} + focusPathsThroughVertex={this.focusPathsThroughVertex} + getGenerationVisibility={this.getGenerationVisibility} getVisiblePathElems={this.getVisiblePathElems} + hideVertex={this.hideVertex} setViewModifier={this.setViewModifier} showOp={showOp} uiFindMatches={uiFindMatches} + updateGenerationVisibility={this.updateGenerationVisibility} vertices={vertices} verticesViewModifiers={verticesViewModifiers} /> diff --git a/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.tsx b/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.tsx index fea950cddf..e8b4e2ef35 100644 --- a/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.tsx +++ b/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.tsx @@ -34,14 +34,9 @@ export default function getDerivedViewModifiers( visEncoding: string | undefined, viewModifiers: Map ) { - const vertices = new Map(); const edges = new Map(); - - const visibleIndices = new Set( - visEncoding == null - ? this.getDefaultVisiblePathElems().map(pe => pe.visibilityIdx) - : new Set(decode(visEncoding)) - ); + const vertices = new Map(); + const visibleIndices = this.getVisibleIndices(visEncoding); const pushVertexVm = (vm: number, key: string) => { // eslint-disable-next-line no-bitwise diff --git a/packages/jaeger-ui/src/model/ddg/GraphModel/index.tsx b/packages/jaeger-ui/src/model/ddg/GraphModel/index.tsx index 201b34f45e..54b5f13356 100644 --- a/packages/jaeger-ui/src/model/ddg/GraphModel/index.tsx +++ b/packages/jaeger-ui/src/model/ddg/GraphModel/index.tsx @@ -109,8 +109,7 @@ export default class GraphModel { Object.freeze(this.visIdxToPathElem); } - // Only public for bound fn getDerivedViewModifiers - public getDefaultVisiblePathElems() { + private getDefaultVisiblePathElems() { return ([] as PathElem[]).concat( this.distanceToPathElems.get(-2) || [], this.distanceToPathElems.get(-1) || [], @@ -127,6 +126,10 @@ export default class GraphModel { .filter(Boolean); } + public getVisibleIndices(visEncoding?: string): Set { + return new Set(this.getVisiblePathElems(visEncoding).map(({ visibilityIdx }) => visibilityIdx)); + } + public getVisible: (visEncoding?: string) => { edges: TEdge[]; vertices: TDdgVertex[] } = memoize(10)( (visEncoding?: string): { edges: TEdge[]; vertices: TDdgVertex[] } => { const edges: Set = new Set(); diff --git a/packages/jaeger-ui/src/model/ddg/PathElem.tsx b/packages/jaeger-ui/src/model/ddg/PathElem.tsx index 3723da3fee..395eab9bb3 100644 --- a/packages/jaeger-ui/src/model/ddg/PathElem.tsx +++ b/packages/jaeger-ui/src/model/ddg/PathElem.tsx @@ -38,6 +38,22 @@ export default class PathElem { return this.memberIdx - this.memberOf.focalIdx; } + get externalPath(): PathElem[] { + const result: PathElem[] = []; + let current: PathElem | null | undefined = this; + while (current) { + result.push(current); + current = current.externalSideNeighbor; + } + if (this.distance < 0) result.reverse(); + return result; + } + + get externalSideNeighbor(): PathElem | null | undefined { + if (!this.distance) return null; + return this.memberOf.members[this.memberIdx + Math.sign(this.distance)]; + } + get focalPath(): PathElem[] { const result: PathElem[] = []; let current: PathElem | null = this; From 44111ff2dd4268ad5d165c22daccbc61e197ce93 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Wed, 13 Nov 2019 10:51:40 -0500 Subject: [PATCH 2/3] Finish DdgNode Vis Interactions Signed-off-by: Everett Ross --- .../__snapshots__/index.test.js.snap | 806 ++++++++++++++---- .../Graph/DdgNodeContent/index.css | 25 +- .../Graph/DdgNodeContent/index.test.js | 350 +++++--- .../Graph/DdgNodeContent/index.tsx | 153 +++- .../DeepDependencies/Graph/index.tsx | 2 +- .../components/DeepDependencies/index.test.js | 268 ++++-- .../DeepDependencies/index.track.test.js | 65 ++ .../DeepDependencies/index.track.tsx | 46 +- .../src/components/DeepDependencies/index.tsx | 89 +- .../getDerivedViewModifiers.test.js | 10 +- .../GraphModel/getDerivedViewModifiers.tsx | 1 - .../src/model/ddg/GraphModel/index.test.js | 426 +++++++-- .../src/model/ddg/GraphModel/index.tsx | 113 ++- .../jaeger-ui/src/model/ddg/PathElem.test.js | 35 + .../model/ddg/sample-paths.test.resources.js | 39 + 15 files changed, 1895 insertions(+), 533 deletions(-) diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap index 6b44bc63d0..ce2b6dcc01 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap @@ -3,8 +3,8 @@ exports[` DdgNodeContent.getNodeRenderer() returns a 1`] = `
DdgNodeContent.getNodeRenderer() returns a
- `; exports[` DdgNodeContent.getNodeRenderer() returns a focal 1`] = `
DdgNodeContent.getNodeRenderer() returns a focal
- `; exports[` omits the operation if it is null 1`] = ` `; @@ -291,8 +300,8 @@ exports[` omits the operation if it is null 1`] = ` exports[` omits the operation if it is null 2`] = ` `; @@ -384,8 +474,8 @@ exports[` omits the operation if it is null 2`] = ` exports[` renders correctly when isFocalNode = true and focalNodeUrl = null 1`] = ` `; @@ -492,8 +663,8 @@ exports[` renders correctly when isFocalNode = true and focalNod exports[` renders correctly when isFocalNode = true and focalNodeUrl = null 2`] = `
renders correctly when isFocalNode = true and focalNod onClick={[Function]} role="button" > - + + + + + View traces + + + + + + + + View Parents + + + + + + + + View Children + + +
+
+`; + +exports[` renders correctly when not hovered 1`] = ` + +`; + +exports[` renders correctly when not hovered 2`] = ` +
+
+
+

+ +

+
+ +
+
`; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css index 8b3e568097..8d7f009411 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css @@ -52,16 +52,7 @@ limitations under the License. bottom: 100%; box-shadow: 0 0px 4px 1px rgba(0, 0, 0, 0.1); left: 1em; - opacity: 0; - pointer-events: none; position: absolute; - transition: opacity 0.1s; - white-space: nowrap; -} - -.DdgNodeContent:hover > .DdgNodeContent--actionsWrapper { - opacity: 1; - pointer-events: all; } .DdgNodeContent--actionsItem { @@ -74,6 +65,22 @@ limitations under the License. color: inherit; } +.DdgNodeContent--actionsItemIconWrapper { + height: 16px; + width: 16px; +} + +.DdgNodeContent--actionsItemIconWrapper > * { + height: 16px; + position: absolute; + width: 16px; +} + +.DdgNodeContent--actionsItemIconWrapper > * > .ant-checkbox { + vertical-align: unset; + top: unset; +} + .DdgNodeContent--actionsItemText { font-size: 0.9em; line-height: normal; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.test.js b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.test.js index ed8d216f43..c4860b5e49 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.test.js @@ -27,7 +27,7 @@ import DdgNodeContent from '.'; import { MAX_LENGTH, MAX_LINKED_TRACES, MIN_LENGTH, PARAM_NAME_LENGTH, RADIUS } from './constants'; import * as getSearchUrl from '../../../SearchTracePage/url'; -import { EDdgDensity, EViewModifier } from '../../../../model/ddg/types'; +import { ECheckedStatus, EDdgDensity, EDirection, EViewModifier } from '../../../../model/ddg/types'; describe('', () => { const vertexKey = 'some-key'; @@ -35,19 +35,29 @@ describe('', () => { const operation = 'some-operation'; const props = { focalNodeUrl: 'some-url', + focusPathsThroughVertex: jest.fn(), + getGenerationVisibility: jest.fn(), getVisiblePathElems: jest.fn(), + hideVertex: jest.fn(), isFocalNode: false, operation, setViewModifier: jest.fn(), service, + updateGenerationVisibility: jest.fn(), vertexKey, }; let wrapper; beforeEach(() => { + props.getGenerationVisibility.mockImplementation(direction => + direction === EDirection.Upstream ? ECheckedStatus.Full : ECheckedStatus.Partial + ); props.getVisiblePathElems.mockReset(); + props.setViewModifier.mockReset(); + props.updateGenerationVisibility.mockReset(); wrapper = shallow(); + wrapper.setState({ hovered: true }); }); it('does not explode', () => { @@ -60,22 +70,18 @@ describe('', () => { expect(wrapper).toMatchSnapshot(); }); - it('calls setViewModifier on mouse over, out', () => { - const { calls } = props.setViewModifier.mock; - wrapper.simulate('mouseover', { type: 'mouseover' }); - expect(calls.length).toBe(1); - wrapper.simulate('mouseout', { type: 'mouseout' }); - expect(calls.length).toBe(2); - expect(calls[0]).toEqual([vertexKey, EViewModifier.Hovered, true]); - expect(calls[1]).toEqual([vertexKey, EViewModifier.Hovered, false]); - }); - it('renders correctly when isFocalNode = true and focalNodeUrl = null', () => { expect(wrapper).toMatchSnapshot(); wrapper.setProps({ focalNodeUrl: null, isFocalNode: true }); expect(wrapper).toMatchSnapshot(); }); + it('renders correctly when not hovered', () => { + expect(wrapper).toMatchSnapshot(); + wrapper.setState({ hovered: false }); + expect(wrapper).toMatchSnapshot(); + }); + describe('measureNode', () => { it('returns twice the RADIUS with a buffer for svg border', () => { const diameterWithBuffer = 2 * RADIUS + 2; @@ -86,146 +92,264 @@ describe('', () => { }); }); - describe('viewTraces', () => { - const click = () => - wrapper - .find('.DdgNodeContent--actionsItem') - .at(1) - .simulate('click'); - const pad = num => `000${num}`.slice(-4); - const mockReturn = ids => - props.getVisiblePathElems.mockReturnValue(ids.map(traceIDs => ({ memberOf: { traceIDs } }))); - const calcIdxWithinLimit = arr => Math.floor(0.75 * arr.length); - const falsifyDuplicateAndMock = ids => { - const withFalsyAndDuplicate = ids.map(arr => arr.slice()); - withFalsyAndDuplicate[0].splice( - calcIdxWithinLimit(withFalsyAndDuplicate[0]), - 0, - withFalsyAndDuplicate[1][calcIdxWithinLimit(withFalsyAndDuplicate[1])], - '' - ); - withFalsyAndDuplicate[1].splice( - calcIdxWithinLimit(withFalsyAndDuplicate[1]), - 0, - withFalsyAndDuplicate[0][calcIdxWithinLimit(withFalsyAndDuplicate[0])], - '' - ); - mockReturn(withFalsyAndDuplicate); - }; - const makeIDsAndMock = (idCounts, makeID = count => `test traceID${count}`) => { - let idCount = 0; - const ids = idCounts.map(count => { - const rv = []; - for (let i = 0; i < count; i++) { - rv.push(makeID(pad(idCount++))); - } - return rv; - }); - mockReturn(ids); - return ids; - }; - let getSearchUrlSpy; - const lastIDs = () => getSearchUrlSpy.mock.calls[getSearchUrlSpy.mock.calls.length - 1][0].traceID; - let originalOpen; - + describe('hover behavior', () => { beforeAll(() => { - originalOpen = window.open; - window.open = jest.fn(); - getSearchUrlSpy = jest.spyOn(getSearchUrl, 'getUrl'); + jest.useFakeTimers(); }); - beforeEach(() => { - window.open.mockReset(); + it('calls setViewModifier on mouse enter', () => { + wrapper.simulate('mouseenter', { type: 'mouseenter' }); + + expect(props.setViewModifier).toHaveBeenCalledTimes(1); + expect(props.setViewModifier).toHaveBeenCalledWith(vertexKey, EViewModifier.Hovered, true); }); - afterAll(() => { - window.open = originalOpen; + it('calls setViewModifier on mouse leave', () => { + wrapper.simulate('mouseleave', { type: 'mouseleave' }); + + expect(props.setViewModifier).toHaveBeenCalledTimes(1); + expect(props.setViewModifier).toHaveBeenCalledWith(vertexKey, EViewModifier.Hovered, false); }); - it('no-ops if there are no elems for key', () => { - props.getVisiblePathElems.mockReturnValue(); - click(); - expect(window.open).not.toHaveBeenCalled(); + it('calls setViewModifier on unmount iff state.hovered is true', () => { + wrapper.unmount(); + + expect(props.setViewModifier).toHaveBeenCalledTimes(1); + expect(props.setViewModifier).toHaveBeenCalledWith(vertexKey, EViewModifier.Hovered, false); + + // state.hovered is initially false + const unhoveredWrapper = shallow(); + unhoveredWrapper.unmount(); + + expect(props.setViewModifier).toHaveBeenCalledTimes(1); + expect(props.setViewModifier).toHaveBeenCalledWith(vertexKey, EViewModifier.Hovered, false); }); - it('opens new tab viewing single traceID from single elem', () => { - const ids = makeIDsAndMock([1]); - click(); + it('sets state.hovered to true on mouse enter', () => { + wrapper.setState({ hovered: false }); + wrapper.simulate('mouseenter', { type: 'mouseenter' }); - expect(lastIDs().sort()).toEqual([].concat(...ids).sort()); - expect(props.getVisiblePathElems).toHaveBeenCalledTimes(1); - expect(props.getVisiblePathElems).toHaveBeenCalledWith(vertexKey); + expect(wrapper.state('hovered')).toBe(true); }); - it('opens new tab viewing multiple traceIDs from single elem', () => { - const ids = makeIDsAndMock([3]); - click(); + it('sets state.hovered to false on mouse leave, after delay', () => { + wrapper.simulate('mouseleave', { type: 'mouseleave' }); + expect(wrapper.state('hovered')).toBe(true); - expect(lastIDs().sort()).toEqual([].concat(...ids).sort()); + jest.runAllTimers(); + expect(wrapper.state('hovered')).toBe(false); }); - it('opens new tab viewing multiple traceIDs from multiple elems', () => { - const ids = makeIDsAndMock([3, 2]); - click(); + it('cancels delayed set state if mouse re-enters before timeout runs', () => { + wrapper.simulate('mouseleave', { type: 'mouseleave' }); + expect(wrapper.instance().timeout).toEqual(expect.any(Number)); + + wrapper.simulate('mouseenter', { type: 'mouseenter' }); + expect(wrapper.instance().timeout).toBeUndefined(); - expect(lastIDs().sort()).toEqual([].concat(...ids).sort()); + jest.runAllTimers(); + expect(wrapper.state('hovered')).toBe(true); }); + }); - it('ignores falsy and duplicate IDs', () => { - const ids = makeIDsAndMock([3, 3]); - falsifyDuplicateAndMock(ids); - click(); + describe('node interactions', () => { + describe('focusPaths', () => { + beforeEach(() => { + props.focusPathsThroughVertex.mockReset(); + }); - expect(lastIDs().sort()).toEqual([].concat(...ids).sort()); + it('calls this.props.focusPathsThroughVertex with this.props.vertexKey', () => { + wrapper + .find('.DdgNodeContent--actionsItem') + .at(2) + .simulate('click'); + + expect(props.focusPathsThroughVertex).toHaveBeenCalledWith(props.vertexKey); + expect(props.focusPathsThroughVertex).toHaveBeenCalledTimes(1); + }); }); - describe('MAX_LINKED_TRACES', () => { - const ids = makeIDsAndMock([MAX_LINKED_TRACES, MAX_LINKED_TRACES, 1]); - const expected = [ - ...ids[0].slice(MAX_LINKED_TRACES / 2 + 1), - ...ids[1].slice(MAX_LINKED_TRACES / 2 + 1), - ids[2][0], - ].sort(); + describe('hideVertex', () => { + beforeEach(() => { + props.hideVertex.mockReset(); + }); - it('limits link to only include MAX_LINKED_TRACES, taking equal from each pathElem', () => { + it('calls this.props.hideVertex with this.props.vertexKey', () => { + wrapper + .find('.DdgNodeContent--actionsItem') + .at(3) + .simulate('click'); + + expect(props.hideVertex).toHaveBeenCalledWith(props.vertexKey); + expect(props.hideVertex).toHaveBeenCalledTimes(1); + }); + }); + + describe('updateChildren', () => { + it('calls this.props.updateGenerationVisibility with this.props.vertexKey', () => { + wrapper + .find('.DdgNodeContent--actionsItem') + .at(5) + .simulate('click'); + + expect(props.updateGenerationVisibility).toHaveBeenCalledWith(props.vertexKey, EDirection.Downstream); + expect(props.updateGenerationVisibility).toHaveBeenCalledTimes(1); + }); + }); + + describe('updateParents', () => { + it('calls this.props.updateGenerationVisibility with this.props.vertexKey', () => { + wrapper + .find('.DdgNodeContent--actionsItem') + .at(4) + .simulate('click'); + + expect(props.updateGenerationVisibility).toHaveBeenCalledWith(props.vertexKey, EDirection.Upstream); + expect(props.updateGenerationVisibility).toHaveBeenCalledTimes(1); + }); + }); + + describe('viewTraces', () => { + const click = () => + wrapper + .find('.DdgNodeContent--actionsItem') + .at(1) + .simulate('click'); + const pad = num => `000${num}`.slice(-4); + const mockReturn = ids => + props.getVisiblePathElems.mockReturnValue(ids.map(traceIDs => ({ memberOf: { traceIDs } }))); + const calcIdxWithinLimit = arr => Math.floor(0.75 * arr.length); + const falsifyDuplicateAndMock = ids => { + const withFalsyAndDuplicate = ids.map(arr => arr.slice()); + withFalsyAndDuplicate[0].splice( + calcIdxWithinLimit(withFalsyAndDuplicate[0]), + 0, + withFalsyAndDuplicate[1][calcIdxWithinLimit(withFalsyAndDuplicate[1])], + '' + ); + withFalsyAndDuplicate[1].splice( + calcIdxWithinLimit(withFalsyAndDuplicate[1]), + 0, + withFalsyAndDuplicate[0][calcIdxWithinLimit(withFalsyAndDuplicate[0])], + '' + ); + mockReturn(withFalsyAndDuplicate); + }; + const makeIDsAndMock = (idCounts, makeID = count => `test traceID${count}`) => { + let idCount = 0; + const ids = idCounts.map(count => { + const rv = []; + for (let i = 0; i < count; i++) { + rv.push(makeID(pad(idCount++))); + } + return rv; + }); mockReturn(ids); + return ids; + }; + let getSearchUrlSpy; + const lastIDs = () => getSearchUrlSpy.mock.calls[getSearchUrlSpy.mock.calls.length - 1][0].traceID; + let originalOpen; + + beforeAll(() => { + originalOpen = window.open; + window.open = jest.fn(); + getSearchUrlSpy = jest.spyOn(getSearchUrl, 'getUrl'); + }); + + beforeEach(() => { + window.open.mockReset(); + }); + + afterAll(() => { + window.open = originalOpen; + }); + + it('no-ops if there are no elems for key', () => { + props.getVisiblePathElems.mockReturnValue(); + click(); + expect(window.open).not.toHaveBeenCalled(); + }); + + it('opens new tab viewing single traceID from single elem', () => { + const ids = makeIDsAndMock([1]); click(); - expect(lastIDs().sort()).toEqual(expected); + expect(lastIDs().sort()).toEqual([].concat(...ids).sort()); + expect(props.getVisiblePathElems).toHaveBeenCalledTimes(1); + expect(props.getVisiblePathElems).toHaveBeenCalledWith(vertexKey); }); - it('does not count falsy and duplicate IDs towards MAX_LINKED_TRACES', () => { - falsifyDuplicateAndMock(ids); + it('opens new tab viewing multiple traceIDs from single elem', () => { + const ids = makeIDsAndMock([3]); click(); - expect(lastIDs().sort()).toEqual(expected); + expect(lastIDs().sort()).toEqual([].concat(...ids).sort()); }); - }); - describe('MAX_LENGTH', () => { - const effectiveMaxLength = MAX_LENGTH - MIN_LENGTH; - const TARGET_ID_COUNT = 31; - const paddingLength = Math.floor(effectiveMaxLength / TARGET_ID_COUNT) - PARAM_NAME_LENGTH; - const idPadding = 'x'.repeat(paddingLength - pad(0).length); - const ids = makeIDsAndMock([TARGET_ID_COUNT, TARGET_ID_COUNT, 1], num => `${idPadding}${num}`); - const expected = [ - ...ids[0].slice(TARGET_ID_COUNT / 2 + 1), - ...ids[1].slice(TARGET_ID_COUNT / 2 + 1), - ids[2][0], - ].sort(); - - it('limits link to only include MAX_LENGTH, taking equal from each pathElem', () => { - mockReturn(ids); + it('opens new tab viewing multiple traceIDs from multiple elems', () => { + const ids = makeIDsAndMock([3, 2]); click(); - expect(lastIDs().sort()).toEqual(expected); + expect(lastIDs().sort()).toEqual([].concat(...ids).sort()); }); - it('does not count falsy and duplicate IDs towards MAX_LEN', () => { + it('ignores falsy and duplicate IDs', () => { + const ids = makeIDsAndMock([3, 3]); falsifyDuplicateAndMock(ids); click(); - expect(lastIDs().sort()).toEqual(expected); + expect(lastIDs().sort()).toEqual([].concat(...ids).sort()); + }); + + describe('MAX_LINKED_TRACES', () => { + const ids = makeIDsAndMock([MAX_LINKED_TRACES, MAX_LINKED_TRACES, 1]); + const expected = [ + ...ids[0].slice(MAX_LINKED_TRACES / 2 + 1), + ...ids[1].slice(MAX_LINKED_TRACES / 2 + 1), + ids[2][0], + ].sort(); + + it('limits link to only include MAX_LINKED_TRACES, taking equal from each pathElem', () => { + mockReturn(ids); + click(); + + expect(lastIDs().sort()).toEqual(expected); + }); + + it('does not count falsy and duplicate IDs towards MAX_LINKED_TRACES', () => { + falsifyDuplicateAndMock(ids); + click(); + + expect(lastIDs().sort()).toEqual(expected); + }); + }); + + describe('MAX_LENGTH', () => { + const effectiveMaxLength = MAX_LENGTH - MIN_LENGTH; + const TARGET_ID_COUNT = 31; + const paddingLength = Math.floor(effectiveMaxLength / TARGET_ID_COUNT) - PARAM_NAME_LENGTH; + const idPadding = 'x'.repeat(paddingLength - pad(0).length); + const ids = makeIDsAndMock([TARGET_ID_COUNT, TARGET_ID_COUNT, 1], num => `${idPadding}${num}`); + const expected = [ + ...ids[0].slice(TARGET_ID_COUNT / 2 + 1), + ...ids[1].slice(TARGET_ID_COUNT / 2 + 1), + ids[2][0], + ].sort(); + + it('limits link to only include MAX_LENGTH, taking equal from each pathElem', () => { + mockReturn(ids); + click(); + + expect(lastIDs().sort()).toEqual(expected); + }); + + it('does not count falsy and duplicate IDs towards MAX_LEN', () => { + falsifyDuplicateAndMock(ids); + click(); + + expect(lastIDs().sort()).toEqual(expected); + }); }); }); }); 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 de2980e416..98babcabb7 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx @@ -13,8 +13,11 @@ // limitations under the License. import * as React from 'react'; +import { Checkbox } from 'antd'; import cx from 'classnames'; import { TLayoutVertex } from '@jaegertracing/plexus/lib/types'; +import IoAndroidLocate from 'react-icons/lib/io/android-locate'; +import MdVisibilityOff from 'react-icons/lib/md/visibility-off'; import calcPositioning from './calc-positioning'; import { @@ -27,11 +30,19 @@ import { WORD_RX, } from './constants'; import { setFocusIcon } from './node-icons'; +import { trackSetFocus, trackViewTraces } from '../../index.track'; import { getUrl } from '../../url'; import BreakableText from '../../../common/BreakableText'; import NewWindowIcon from '../../../common/NewWindowIcon'; import { getUrl as getSearchUrl } from '../../../SearchTracePage/url'; -import { ECheckedStatus, EDdgDensity, EDirection, EViewModifier, TDdgVertex, PathElem } from '../../../../model/ddg/types'; +import { + ECheckedStatus, + EDdgDensity, + EDirection, + EViewModifier, + TDdgVertex, + PathElem, +} from '../../../../model/ddg/types'; import './index.css'; @@ -51,6 +62,12 @@ type TProps = { }; export default class DdgNodeContent extends React.PureComponent { + timeout?: number; + + state = { + hovered: false, + }; + static measureNode() { const diameter = 2 * (RADIUS + 1); @@ -106,14 +123,34 @@ export default class DdgNodeContent extends React.PureComponent { }; } - /* componentWillUnmount() { - const { vertexKey, setViewModifier } = this.props; - setViewModifier(vertexKey, EViewModifier.Hovered, false); + if (this.state.hovered) { + this.props.setViewModifier(this.props.vertexKey, EViewModifier.Hovered, false); + } } - */ + + private focusPaths = () => { + const { focusPathsThroughVertex, vertexKey } = this.props; + focusPathsThroughVertex(vertexKey); + }; + + private hideVertex = () => { + const { hideVertex, vertexKey } = this.props; + hideVertex(vertexKey); + }; + + private updateChildren = () => { + const { updateGenerationVisibility, vertexKey } = this.props; + updateGenerationVisibility(vertexKey, EDirection.Downstream); + }; + + private updateParents = () => { + const { updateGenerationVisibility, vertexKey } = this.props; + updateGenerationVisibility(vertexKey, EDirection.Upstream); + }; private viewTraces = () => { + trackViewTraces(); const { vertexKey, getVisiblePathElems } = this.props; const elems = getVisiblePathElems(vertexKey); if (elems) { @@ -146,27 +183,44 @@ export default class DdgNodeContent extends React.PureComponent { private onMouseUx = (event: React.MouseEvent) => { const { vertexKey, setViewModifier } = this.props; - setViewModifier(vertexKey, EViewModifier.Hovered, event.type === 'mouseover'); + const hovered = event.type === 'mouseenter'; + setViewModifier(vertexKey, EViewModifier.Hovered, hovered); + if (hovered) { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = undefined; + } else { + this.setState({ hovered }); + } + } else { + this.timeout = setTimeout(() => { + this.setState({ hovered }); + this.timeout = undefined; + }, 150); + } }; render() { + const { hovered } = this.state; const { focalNodeUrl, - focusPathsThroughVertex, getGenerationVisibility, - hideVertex, isFocalNode, isPositioned, operation, service, - updateGenerationVisibility, vertexKey, } = this.props; + const { radius, svcWidth, opWidth, svcMarginTop } = calcPositioning(service, operation); const scaleFactor = RADIUS / radius; const transform = `translate(${RADIUS - radius}px, ${RADIUS - radius}px) scale(${scaleFactor})`; + + const childrenVisibility = hovered && getGenerationVisibility(vertexKey, EDirection.Downstream); + const parentVisibility = hovered && getGenerationVisibility(vertexKey, EDirection.Upstream); + return ( -
+
{
-
- {focalNodeUrl && ( - - {setFocusIcon} - Set focus + {hovered && ( + + {!isFocalNode && ( + + + + + Focus paths through this node + + )} + {!isFocalNode && ( + + + + + Hide node + + )} + {parentVisibility && ( + + + + + View Parents + + )} + {childrenVisibility && ( + + + + + View Children + + )} +
+ )}
); } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx index 2292797ae8..8d0b2060d1 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx @@ -163,7 +163,7 @@ export default class Graph extends PureComponent { getGenerationVisibility, getVisiblePathElems, hideVertex, - setViewModifier, + setViewModifier, showOp, updateGenerationVisibility, }), diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.test.js b/packages/jaeger-ui/src/components/DeepDependencies/index.test.js index db7e52c0ef..799ce3b819 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.test.js @@ -17,6 +17,7 @@ import { shallow } from 'enzyme'; import _set from 'lodash/set'; import { DeepDependencyGraphPageImpl, mapDispatchToProps, mapStateToProps } from '.'; +import * as track from './index.track'; import * as url from './url'; import Graph from './Graph'; import Header from './Header'; @@ -27,25 +28,16 @@ import getStateEntryKey from '../../model/ddg/getStateEntryKey'; import * as GraphModel from '../../model/ddg/GraphModel'; import * as codec from '../../model/ddg/visibility-codec'; -import { EDdgDensity, EViewModifier } from '../../model/ddg/types'; +import { ECheckedStatus, EDirection, EDdgDensity, EViewModifier } from '../../model/ddg/types'; describe('DeepDependencyGraphPage', () => { describe('DeepDependencyGraphPageImpl', () => { - const props = { + const vertexKey = 'test vertex key'; + const propsWithoutGraph = { addViewModifier: jest.fn(), fetchDeepDependencyGraph: () => {}, fetchServices: jest.fn(), fetchServiceOperations: jest.fn(), - graph: { - getVisible: () => ({ - edges: [], - vertices: [], - }), - getHiddenUiFindMatches: () => new Set(), - getVertexVisiblePathElems: jest.fn(), - getVisibleUiFindMatches: () => new Set(), - getVisWithVertices: jest.fn(), - }, graphState: { model: { distanceToPathElems: new Map(), @@ -66,7 +58,24 @@ describe('DeepDependencyGraphPage', () => { visEncoding: 'testVisKey', }, }; + const props = { + ...propsWithoutGraph, + graph: { + getVisible: () => ({ + edges: [], + vertices: [], + }), + getHiddenUiFindMatches: () => new Set(), + getGenerationVisibility: jest.fn(), + getVertexVisiblePathElems: jest.fn(), + getVisibleUiFindMatches: () => new Set(), + getVisWithVertices: jest.fn(), + getVisWithoutVertex: jest.fn(), + getVisWithUpdatedGeneration: jest.fn(), + }, + }; const ddgPageImpl = new DeepDependencyGraphPageImpl(props); + const ddgWithoutGraph = new DeepDependencyGraphPageImpl(propsWithoutGraph); describe('constructor', () => { beforeEach(() => { @@ -94,19 +103,23 @@ describe('DeepDependencyGraphPage', () => { }); describe('updateUrlState', () => { + const visEncoding = 'test vis encoding'; let getUrlSpy; + let trackHideSpy; beforeAll(() => { getUrlSpy = jest.spyOn(url, 'getUrl'); + trackHideSpy = jest.spyOn(track, 'trackHide'); }); beforeEach(() => { getUrlSpy.mockReset(); props.history.push.mockReset(); + trackHideSpy.mockClear(); }); it('updates provided value', () => { - ['service', 'operation', 'start', 'end', 'visEnconding'].forEach((propName, i) => { + ['service', 'operation', 'start', 'end', 'visEncoding'].forEach((propName, i) => { const value = `new ${propName}`; const kwarg = { [propName]: value }; ddgPageImpl.updateUrlState(kwarg); @@ -164,18 +177,99 @@ describe('DeepDependencyGraphPage', () => { expect(getUrlSpy).toHaveBeenLastCalledWith(expect.objectContaining({ hash }), undefined); }); + describe('focusPathsThroughVertex', () => { + let trackFocusPathsSpy; + + beforeAll(() => { + trackFocusPathsSpy = jest.spyOn(track, 'trackFocusPaths'); + }); + + beforeEach(() => { + trackFocusPathsSpy.mockClear(); + }); + + it('no-ops if props does not have graph', () => { + ddgWithoutGraph.focusPathsThroughVertex(vertexKey); + + expect(getUrlSpy).not.toHaveBeenCalled(); + expect(trackFocusPathsSpy).not.toHaveBeenCalled(); + }); + + it('udates url state and tracks focus paths', () => { + const indices = [4, 8, 15, 16, 23, 42]; + const elems = [ + { + memberOf: { + members: indices.slice(0, indices.length / 2).map(visibilityIdx => ({ visibilityIdx })), + }, + }, + { + memberOf: { + members: indices.slice(indices.length / 2).map(visibilityIdx => ({ visibilityIdx })), + }, + }, + ]; + props.graph.getVertexVisiblePathElems.mockReturnValueOnce(elems); + ddgPageImpl.focusPathsThroughVertex(vertexKey); + + expect(getUrlSpy).toHaveBeenLastCalledWith( + Object.assign({}, props.urlState, { visEncoding: codec.encode(indices) }), + undefined + ); + expect(trackFocusPathsSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('hideVertex', () => { + it('no-ops if props does not have graph', () => { + ddgWithoutGraph.hideVertex(vertexKey); + + expect(getUrlSpy).not.toHaveBeenCalled(); + expect(trackHideSpy).not.toHaveBeenCalled(); + }); + + it('no-ops if graph.getVisWithoutVertex returns undefined', () => { + ddgPageImpl.hideVertex(vertexKey); + + expect(getUrlSpy).not.toHaveBeenCalled(); + expect(trackHideSpy).not.toHaveBeenCalled(); + }); + + it('udates url state and tracks hide', () => { + props.graph.getVisWithoutVertex.mockReturnValueOnce(visEncoding); + ddgPageImpl.hideVertex(vertexKey); + + expect(getUrlSpy).toHaveBeenLastCalledWith( + Object.assign({}, props.urlState, { visEncoding }), + undefined + ); + expect(trackHideSpy).toHaveBeenCalledTimes(1); + expect(trackHideSpy.mock.calls[0]).toHaveLength(0); + }); + }); + + describe('setDensity', () => { + it('updates url with provided density', () => { + const density = EDdgDensity.PreventPathEntanglement; + ddgPageImpl.setDensity(density); + expect(getUrlSpy).toHaveBeenLastCalledWith( + Object.assign({}, props.urlState, { density }), + undefined + ); + }); + }); + describe('setDistance', () => { - const mockNewEncoding = '1'; let encodeDistanceSpy; beforeAll(() => { - encodeDistanceSpy = jest.spyOn(codec, 'encodeDistance').mockImplementation(() => mockNewEncoding); + encodeDistanceSpy = jest.spyOn(codec, 'encodeDistance').mockImplementation(() => visEncoding); }); it('updates url with result of encodeDistance iff graph is loaded', () => { + const direction = EDirection.Upstream; const distance = -3; - const direction = -1; - const visEncoding = props.urlState.visEncoding; + const prevVisEncoding = props.urlState.visEncoding; const { graphState: e, ...graphStatelessProps } = props; const graphStateless = new DeepDependencyGraphPageImpl(graphStatelessProps); @@ -198,10 +292,10 @@ describe('DeepDependencyGraphPage', () => { ddgModel: props.graphState.model, direction, distance, - prevVisEncoding: visEncoding, + prevVisEncoding, }); expect(getUrlSpy).toHaveBeenLastCalledWith( - Object.assign({}, props.urlState, { visEncoding: mockNewEncoding }), + Object.assign({}, props.urlState, { visEncoding }), undefined ); expect(props.history.push).toHaveBeenCalledTimes(1); @@ -253,10 +347,9 @@ describe('DeepDependencyGraphPage', () => { describe('showVertices', () => { const vertices = ['vertex0', 'vertex1']; - const mockVisWithVertices = 'mockVisWithVertices'; beforeAll(() => { - props.graph.getVisWithVertices.mockReturnValue(mockVisWithVertices); + props.graph.getVisWithVertices.mockReturnValue(visEncoding); }); it('updates url with visEncoding calculated by graph', () => { @@ -266,31 +359,18 @@ describe('DeepDependencyGraphPage', () => { props.urlState.visEncoding ); expect(getUrlSpy).toHaveBeenLastCalledWith( - Object.assign({}, props.urlState, { visEncoding: mockVisWithVertices }), + Object.assign({}, props.urlState, { visEncoding }), undefined ); }); it('no-ops if not given graph', () => { - const { graph: _, ...propsWithoutGraph } = props; - const ddg = new DeepDependencyGraphPageImpl(propsWithoutGraph); const { length: callCount } = getUrlSpy.mock.calls; - ddg.showVertices(vertices); + ddgWithoutGraph.showVertices(vertices); expect(getUrlSpy.mock.calls.length).toBe(callCount); }); }); - describe('setDensity', () => { - it('updates url with provided density', () => { - const density = EDdgDensity.PreventPathEntanglement; - ddgPageImpl.setDensity(density); - expect(getUrlSpy).toHaveBeenLastCalledWith( - Object.assign({}, props.urlState, { density }), - undefined - ); - }); - }); - describe('toggleShowOperations', () => { it('updates url with provided boolean', () => { let showOp = true; @@ -308,17 +388,79 @@ describe('DeepDependencyGraphPage', () => { ); }); }); + + describe('updateGenerationVisibility', () => { + const direction = EDirection.Upstream; + let trackShowSpy; + + beforeAll(() => { + trackShowSpy = jest.spyOn(track, 'trackShow'); + }); + + beforeEach(() => { + trackShowSpy.mockClear(); + }); + + it('no-ops if props does not have graph', () => { + ddgWithoutGraph.updateGenerationVisibility(vertexKey, direction); + + expect(getUrlSpy).not.toHaveBeenCalled(); + expect(trackHideSpy).not.toHaveBeenCalled(); + expect(trackShowSpy).not.toHaveBeenCalled(); + }); + + it('no-ops if graph.getVisWithUpdatedGeneration returns undefined', () => { + ddgPageImpl.updateGenerationVisibility(vertexKey, direction); + + expect(getUrlSpy).not.toHaveBeenCalled(); + expect(trackHideSpy).not.toHaveBeenCalled(); + expect(trackShowSpy).not.toHaveBeenCalled(); + }); + + it('udates url state and tracks hide if result.status is ECheckedStatus.Empty', () => { + props.graph.getVisWithUpdatedGeneration.mockReturnValueOnce({ + visEncoding, + update: ECheckedStatus.Empty, + }); + ddgPageImpl.updateGenerationVisibility(vertexKey, direction); + + expect(getUrlSpy).toHaveBeenLastCalledWith( + Object.assign({}, props.urlState, { visEncoding }), + undefined + ); + expect(trackHideSpy).toHaveBeenCalledTimes(1); + expect(trackHideSpy).toHaveBeenCalledWith(direction); + expect(trackShowSpy).not.toHaveBeenCalled(); + }); + + it('udates url state and tracks show if result.status is ECheckedStatus.Full', () => { + props.graph.getVisWithUpdatedGeneration.mockReturnValueOnce({ + visEncoding, + update: ECheckedStatus.Full, + }); + ddgPageImpl.updateGenerationVisibility(vertexKey, direction); + + expect(getUrlSpy).toHaveBeenLastCalledWith( + Object.assign({}, props.urlState, { visEncoding }), + undefined + ); + expect(trackHideSpy).not.toHaveBeenCalled(); + expect(trackShowSpy).toHaveBeenCalledTimes(1); + expect(trackShowSpy).toHaveBeenCalledWith(direction); + }); + }); }); describe('view modifiers', () => { - const vertexKey = 'test vertex key'; const visibilityIndices = ['visId0', 'visId1', 'visId2']; const targetVM = EViewModifier.Emphasized; + let warnSpy; beforeAll(() => { props.graph.getVertexVisiblePathElems.mockReturnValue( visibilityIndices.map(visibilityIdx => ({ visibilityIdx })) ); + warnSpy = jest.spyOn(console, 'warn').mockImplementation(); }); beforeEach(() => { @@ -327,6 +469,10 @@ describe('DeepDependencyGraphPage', () => { props.removeViewModifierFromIndices.mockReset(); }); + afterAll(() => { + warnSpy.mockRestore(); + }); + it('adds given viewModifier to specified pathElems', () => { ddgPageImpl.setViewModifier(vertexKey, targetVM, true); expect(props.addViewModifier).toHaveBeenLastCalledWith({ @@ -359,12 +505,14 @@ describe('DeepDependencyGraphPage', () => { ); }); - it('throws error if given absent vertexKey', () => { + it('warns error if given absent vertexKey', () => { props.graph.getVertexVisiblePathElems.mockReturnValueOnce(undefined); const absentVertexKey = 'absentVertexKey'; - expect(() => - ddgPageImpl.setViewModifier(absentVertexKey, EViewModifier.emphasized, true) - ).toThrowError(new RegExp(`Invalid vertex key.*${absentVertexKey}`)); + ddgPageImpl.setViewModifier(absentVertexKey, EViewModifier.emphasized, true); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith( + `Invalid vertex key to set view modifier for: ${absentVertexKey}` + ); }); it('no-ops if not given dispatch fn or graph or operation or service', () => { @@ -378,8 +526,6 @@ describe('DeepDependencyGraphPage', () => { ddgWithoutRemove.setViewModifier(vertexKey, EViewModifier.emphasized, false); expect(props.graph.getVertexVisiblePathElems).not.toHaveBeenCalled(); - const { graph: _graph, ...propsWithoutGraph } = props; - const ddgWithoutGraph = new DeepDependencyGraphPageImpl(propsWithoutGraph); ddgWithoutGraph.setViewModifier(vertexKey, EViewModifier.emphasized, true); expect(props.graph.getVertexVisiblePathElems).not.toHaveBeenCalled(); @@ -403,8 +549,33 @@ describe('DeepDependencyGraphPage', () => { }); }); + describe('getGenerationVisibility', () => { + const direction = EDirection.Upstream; + const mockCheckedStatus = 'mock check status'; + + beforeAll(() => { + props.graph.getGenerationVisibility.mockReturnValue(mockCheckedStatus); + }); + + beforeEach(() => { + props.graph.getGenerationVisibility.mockClear(); + }); + + it('returns specified ECheckedStatus', () => { + expect(ddgPageImpl.getGenerationVisibility(vertexKey, direction)).toBe(mockCheckedStatus); + expect(props.graph.getGenerationVisibility).toHaveBeenLastCalledWith( + vertexKey, + direction, + props.urlState.visEncoding + ); + }); + + it('returns null if props does not have graph', () => { + expect(ddgWithoutGraph.getGenerationVisibility(vertexKey, direction)).toBe(null); + }); + }); + describe('getVisiblePathElems', () => { - const vertexKey = 'test vertex key'; const mockVisibleElems = 'mock visible elems'; beforeAll(() => { @@ -419,10 +590,8 @@ describe('DeepDependencyGraphPage', () => { ); }); - it('no-ops if not given graph', () => { - const { graph: _, ...propsWithoutGraph } = props; - const ddg = new DeepDependencyGraphPageImpl(propsWithoutGraph); - expect(() => ddg.getVisiblePathElems(vertexKey)).not.toThrowError(); + it('returns undefined if props does not have graph', () => { + expect(ddgWithoutGraph.getVisiblePathElems(vertexKey)).toBe(undefined); }); }); @@ -488,7 +657,6 @@ describe('DeepDependencyGraphPage', () => { }); it('renders indication of unknown state when done but no graph is provided', () => { - const { graph: _, ...propsWithoutGraph } = props; const wrapper = shallow(); const unknownIndication = wrapper .find('div') @@ -501,7 +669,7 @@ describe('DeepDependencyGraphPage', () => { it('calculates uiFindCount and hiddenUiFindMatches', () => { const wrapper = shallow( - + ); expect(wrapper.find(Header).prop('uiFindCount')).toBe(undefined); expect(wrapper.find(Header).prop('hiddenUiFindMatches')).toBe(undefined); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.track.test.js b/packages/jaeger-ui/src/components/DeepDependencies/index.track.test.js index c40a315ec0..cbd2ad98d3 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.track.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.track.test.js @@ -21,14 +21,27 @@ import { CATEGORY_TOGGLE_SHOW_OP, CATEGORY_UPSTREAM_HOPS_CHANGE, CATEGORY_UPSTREAM_HOPS_SELECTION, + CATEGORY_VERTEX_INTERACTIONS, ACTION_DECREASE, + ACTION_FOCUS_PATHS, ACTION_HIDE, + ACTION_HIDE_CHILDREN, + ACTION_HIDE_PARENTS, ACTION_INCREASE, + ACTION_SET_FOCUS, ACTION_SHOW, + ACTION_SHOW_CHILDREN, + ACTION_SHOW_PARENTS, + ACTION_VIEW_TRACES, trackDensityChange, + trackFocusPaths, + trackHide, trackHopChange, + trackShow, + trackSetFocus, trackShowMatches, trackToggleShowOp, + trackViewTraces, } from './index.track'; import { EDdgDensity, EDirection } from '../../model/ddg/types'; import * as trackingUtils from '../../utils/tracking'; @@ -96,6 +109,29 @@ describe('DeepDependencies tracking', () => { }); }); + describe('trackFocusPaths', () => { + it('calls trackViewTraces with the vertex category and focus paths action', () => { + trackFocusPaths(); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_VERTEX_INTERACTIONS, ACTION_FOCUS_PATHS); + }); + }); + + describe('trackHide', () => { + const testTable = [ + [ACTION_HIDE, undefined], + [ACTION_HIDE_PARENTS, EDirection.Upstream], + [ACTION_HIDE_CHILDREN, EDirection.Downstream], + ]; + + it.each(testTable)( + 'calls trackEvent with the vertex category and %p action when direction is %p', + (action, direction) => { + trackHide(direction); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_VERTEX_INTERACTIONS, action); + } + ); + }); + describe('trackHopChange', () => { const largerPosDistance = 6; const largerNegDistance = -1 * largerPosDistance; @@ -158,6 +194,28 @@ describe('DeepDependencies tracking', () => { ); }); + describe('trackSetFocus', () => { + it('calls trackEvent with the vertex category and set focus action', () => { + trackSetFocus(); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_VERTEX_INTERACTIONS, ACTION_SET_FOCUS); + }); + }); + + describe('trackShow', () => { + const testTable = [ + [ACTION_SHOW_PARENTS, EDirection.Upstream], + [ACTION_SHOW_CHILDREN, EDirection.Downstream], + ]; + + it.each(testTable)( + 'calls trackEvent with the vertex category and %p action when direction is %p', + (action, direction) => { + trackShow(direction); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_VERTEX_INTERACTIONS, action); + } + ); + }); + describe('trackShowMatches', () => { it('calls trackEvent with the match category and show action', () => { trackShowMatches(); @@ -176,4 +234,11 @@ describe('DeepDependencies tracking', () => { } ); }); + + describe('trackViewTraces', () => { + it('calls trackViewTraces with the vertex category and view traces action', () => { + trackViewTraces(); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_VERTEX_INTERACTIONS, ACTION_VIEW_TRACES); + }); + }); }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.track.tsx b/packages/jaeger-ui/src/components/DeepDependencies/index.track.tsx index c5001f8e71..7473a558d6 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.track.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.track.tsx @@ -19,19 +19,27 @@ import getTrackFilter from '../../utils/tracking/getTrackFilter'; // export for tests export const CATEGORY_DENSITY_CHANGE = 'jaeger/ux/ddg/density-change'; export const CATEGORY_DENSITY_SELECTION = 'jaeger/ux/ddg/density-selection'; -export const CATEGORY_DOWNSTREAM_HOPS_CHANGE = 'jaeger/ux/ddg/category-downstream-hops-change'; -export const CATEGORY_DOWNSTREAM_HOPS_SELECTION = 'jaeger/ux/ddg/category-downstream-hops-selection'; +export const CATEGORY_DOWNSTREAM_HOPS_CHANGE = 'jaeger/ux/ddg/downstream-hops-change'; +export const CATEGORY_DOWNSTREAM_HOPS_SELECTION = 'jaeger/ux/ddg/downstream-hops-selection'; export const CATEGORY_FILTER = 'jaeger/ux/ddg/filter'; export const CATEGORY_MATCH_INTERACTIONS = 'jaeger/ux/ddg/match-interactions'; export const CATEGORY_TOGGLE_SHOW_OP = 'jaeger/ux/ddg/toggle-show-op'; -export const CATEGORY_UPSTREAM_HOPS_CHANGE = 'jaeger/ux/ddg/category-upstream-hops-change'; -export const CATEGORY_UPSTREAM_HOPS_SELECTION = 'jaeger/ux/ddg/category-upstream-hops-selection'; +export const CATEGORY_UPSTREAM_HOPS_CHANGE = 'jaeger/ux/ddg/upstream-hops-change'; +export const CATEGORY_UPSTREAM_HOPS_SELECTION = 'jaeger/ux/ddg/upstream-hops-selection'; +export const CATEGORY_VERTEX_INTERACTIONS = 'jaeger/ux/ddg/vertex-interactions'; // export for tests export const ACTION_DECREASE = 'decrease'; +export const ACTION_FOCUS_PATHS = 'focus-paths'; export const ACTION_HIDE = 'hide'; +export const ACTION_HIDE_CHILDREN = 'hide-children'; +export const ACTION_HIDE_PARENTS = 'hide-parents'; export const ACTION_INCREASE = 'increase'; +export const ACTION_SET_FOCUS = 'set-focus'; export const ACTION_SHOW = 'show'; +export const ACTION_SHOW_CHILDREN = 'show-children'; +export const ACTION_SHOW_PARENTS = 'show-parents'; +export const ACTION_VIEW_TRACES = 'view-traces'; export function trackDensityChange( prevDensity: EDdgDensity, @@ -63,6 +71,20 @@ export function trackDensityChange( export const trackFilter = getTrackFilter(CATEGORY_FILTER); +export function trackFocusPaths() { + trackEvent(CATEGORY_VERTEX_INTERACTIONS, ACTION_FOCUS_PATHS); +} + +export function trackHide(direction?: EDirection) { + if (!direction) { + trackEvent(CATEGORY_VERTEX_INTERACTIONS, ACTION_HIDE); + } else if (direction === EDirection.Upstream) { + trackEvent(CATEGORY_VERTEX_INTERACTIONS, ACTION_HIDE_PARENTS); + } else { + trackEvent(CATEGORY_VERTEX_INTERACTIONS, ACTION_HIDE_CHILDREN); + } +} + export function trackHopChange( prevFurthestFullDistance: number, nextFurthestFullDistance: number, @@ -82,6 +104,18 @@ export function trackHopChange( trackEvent(changeCategory, changeAction); } +export function trackShow(direction: EDirection) { + if (direction === EDirection.Upstream) { + trackEvent(CATEGORY_VERTEX_INTERACTIONS, ACTION_SHOW_PARENTS); + } else { + trackEvent(CATEGORY_VERTEX_INTERACTIONS, ACTION_SHOW_CHILDREN); + } +} + +export function trackSetFocus() { + trackEvent(CATEGORY_VERTEX_INTERACTIONS, ACTION_SET_FOCUS); +} + export function trackShowMatches() { trackEvent(CATEGORY_MATCH_INTERACTIONS, ACTION_SHOW); } @@ -90,3 +124,7 @@ export function trackToggleShowOp(value: boolean) { const action = value ? ACTION_SHOW : ACTION_HIDE; trackEvent(CATEGORY_TOGGLE_SHOW_OP, action); } + +export function trackViewTraces() { + trackEvent(CATEGORY_VERTEX_INTERACTIONS, ACTION_VIEW_TRACES); +} diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx index f913c84c54..30f67282b5 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx @@ -18,6 +18,7 @@ import _get from 'lodash/get'; import { bindActionCreators, Dispatch } from 'redux'; import { connect } from 'react-redux'; +import { trackFocusPaths, trackHide, trackShow } from './index.track'; import Header from './Header'; import Graph from './Graph'; import { getUrl, getUrlState, sanitizeUrlState, ROUTE_PATH } from './url'; @@ -34,7 +35,6 @@ import { EDdgDensity, EDirection, EViewModifier, - PathElem, TDdgModelParams, TDdgSparseUrlState, TDdgVertex, @@ -116,38 +116,21 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { focusPathsThroughVertex = (vertexKey: string) => { const elems = this.getVisiblePathElems(vertexKey); if (!elems) return; - const indices = ([] as number[]).concat(...elems.map(({ memberOf }) => memberOf.members.map(({ visibilityIdx }) => visibilityIdx))); - this.updateUrlState({ visEncoding: encode(indices) }); - } - private getGeneration = (vertexKey: string, direction: EDirection): PathElem[] => { - const rv: PathElem[] = []; - const elems = this.getVisiblePathElems(vertexKey); - if (!elems) return rv; - elems.forEach(({ focalSideNeighbor, memberIdx, memberOf }) => { - const generationMember = memberOf.members[memberIdx + direction]; - if (generationMember && generationMember !== focalSideNeighbor) rv.push(generationMember); - }); - return rv; - } + trackFocusPaths(); + const indices = ([] as number[]).concat( + ...elems.map(({ memberOf }) => memberOf.members.map(({ visibilityIdx }) => visibilityIdx)) + ); + this.updateUrlState({ visEncoding: encode(indices) }); + }; getGenerationVisibility = (vertexKey: string, direction: EDirection): ECheckedStatus | null => { const { graph, urlState } = this.props; - const generation = this.getGeneration(vertexKey, direction); - if (!generation.length || !graph) return null; - - const visibleIndices = graph.getVisibleIndices(urlState.visEncoding); - let partial = false; - let full = true; - generation.forEach(elem => { - const isVis = visibleIndices.has(elem.visibilityIdx); - partial = partial || isVis; - full = full && isVis; - }); - if (full) return ECheckedStatus.Full; - if (partial) return ECheckedStatus.Partial; - return ECheckedStatus.Empty; - } + if (graph) { + return graph.getGenerationVisibility(vertexKey, direction, urlState.visEncoding); + } + return null; + }; getVisiblePathElems = (key: string) => { const { graph, urlState } = this.props; @@ -157,25 +140,17 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { return undefined; }; - private hideElems = (elems: PathElem[]) => { + hideVertex = (vertexKey: string) => { const { graph, urlState } = this.props; - const { visEncoding } = urlState; + const { visEncoding: currVisEncoding } = urlState; if (!graph) return; - const visible = graph.getVisibleIndices(visEncoding); - elems.forEach(({ externalPath }) => { - externalPath.forEach(({ visibilityIdx }) => { - visible.delete(visibilityIdx); - }); - }); - - this.updateUrlState({ visEncoding: encode(Array.from(visible)) }); - } + const visEncoding = graph.getVisWithoutVertex(vertexKey, currVisEncoding); + if (!visEncoding) return; - hideVertex = (vertexKey: string) => { - const elems = this.getVisiblePathElems(vertexKey); - if (elems) this.hideElems(elems); - } + trackHide(); + this.updateUrlState({ visEncoding }); + }; setDensity = (density: EDdgDensity) => this.updateUrlState({ density }); @@ -218,7 +193,9 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { } const pathElems = graph.getVertexVisiblePathElems(vertexKey, visEncoding); if (!pathElems) { - throw new Error(`Invalid vertex key to set view modifier for: ${vertexKey}`); + // eslint-disable-next-line no-console + console.warn(`Invalid vertex key to set view modifier for: ${vertexKey}`); + return; } const visibilityIndices = pathElems.map(pe => pe.visibilityIdx); fn({ @@ -242,19 +219,17 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { updateGenerationVisibility = (vertexKey: string, direction: EDirection) => { const { graph, urlState } = this.props; - const { visEncoding } = urlState; - const generationElems = this.getGeneration(vertexKey, direction); - const currCheckedStatus = this.getGenerationVisibility(vertexKey, direction); - if (!graph || !generationElems || !currCheckedStatus) return; + const { visEncoding: currVisEncoding } = urlState; + if (!graph) return; - if (currCheckedStatus === ECheckedStatus.Full) { - this.hideElems(generationElems); - } else { - const visible = graph.getVisibleIndices(visEncoding); - generationElems.forEach(({ visibilityIdx }) => visible.add(visibilityIdx)); - this.updateUrlState({ visEncoding: encode(Array.from(visible)) }); - } - } + const result = graph.getVisWithUpdatedGeneration(vertexKey, direction, currVisEncoding); + if (!result) return; + + const { visEncoding, update } = result; + if (update === ECheckedStatus.Empty) trackHide(direction); + else trackShow(direction); + this.updateUrlState({ visEncoding }); + }; updateUrlState = (newValues: Partial) => { const { baseUrl, extraUrlArgs, graphState, history, uiFind, urlState } = this.props; diff --git a/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.test.js b/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.test.js index b03e2f5893..94dfccdf43 100644 --- a/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.test.js +++ b/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.test.js @@ -85,10 +85,16 @@ describe('getDerivedViewModifiers', () => { describe('error cases', () => { it('errors if out of bounds visIdx has a VM', () => { + const graphWithOutOfBoundsIdx = getGraph(); const outOfBounds = graph.visIdxToPathElem.length; - const outOfBoundsEncoding = encode([...visibleIndices, outOfBounds]); + const outOfBoundsIndices = [...visibleIndices, outOfBounds]; + const outOfBoundsEncoding = encode(outOfBoundsIndices); + graphWithOutOfBoundsIdx.getVisibleIndices = () => new Set(outOfBoundsIndices); expect(() => - graph.getDerivedViewModifiers(outOfBoundsEncoding, new Map([[outOfBounds, EViewModifier.Hovered]])) + graphWithOutOfBoundsIdx.getDerivedViewModifiers( + outOfBoundsEncoding, + new Map([[outOfBounds, EViewModifier.Hovered]]) + ) ).toThrowError(`Invalid vis ids: ${outOfBounds}`); }); diff --git a/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.tsx b/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.tsx index e8b4e2ef35..6c8a0ffb9d 100644 --- a/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.tsx +++ b/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.tsx @@ -14,7 +14,6 @@ import GraphModel from './index'; import getEdgeId from './getEdgeId'; -import { decode } from '../visibility-codec'; import { EViewModifier } from '../types'; function getKeyFromVisIdx(graph: GraphModel, visIdx: number) { diff --git a/packages/jaeger-ui/src/model/ddg/GraphModel/index.test.js b/packages/jaeger-ui/src/model/ddg/GraphModel/index.test.js index 3b7f111e0c..a84a3e5a3a 100644 --- a/packages/jaeger-ui/src/model/ddg/GraphModel/index.test.js +++ b/packages/jaeger-ui/src/model/ddg/GraphModel/index.test.js @@ -16,66 +16,67 @@ import GraphModel, { makeGraph } from './index'; import { convergentPaths, doubleFocalPath, + generationPaths, focalPayloadElem, simplePath, wrap, } from '../sample-paths.test.resources'; import transformDdgData from '../transformDdgData'; -import { EDdgDensity } from '../types'; +import { ECheckedStatus, EDirection, EDdgDensity } from '../types'; import { encode } from '../visibility-codec'; describe('GraphModel', () => { const convergentModel = transformDdgData(wrap(convergentPaths), focalPayloadElem); const doubleFocalModel = transformDdgData(wrap([doubleFocalPath, simplePath]), focalPayloadElem); + const generationModel = transformDdgData(wrap(generationPaths), focalPayloadElem); const simpleModel = transformDdgData(wrap([simplePath]), focalPayloadElem); - /** - * This function takes in a Graph and validates the structure based on the expected vertices. - * - * @param {GraphModel} graph - The Graph to validate. - * @param {Object[]} expectedVertices - The vertices that the Graph should have. - * @param {number[]} expectedVertices[].visIndices - The visibility indices that should all share one - * DdgVertex. - * @param {number[]} expectedVertices[].focalSIdeNeighbors - A single visibilityIdx is sufficient to define a - * neighboring vertex. For each focalSide visibilityIdx, the expectedVertex should have an - * edge connecting the expectedVertex back to the focalSideNeighbor. - */ - function validateGraph(graph, expectedVertices) { - let expectedEdgeCount = 0; - expectedVertices.forEach(({ visIndices, focalSideNeighbors = [] }) => { - // Validate that all visIndices share the same vertex - const pathElems = visIndices.map(visIdx => graph.visIdxToPathElem[visIdx]); - const vertices = pathElems.map(elem => graph.pathElemToVertex.get(elem)); - const vertex = vertices[0]; - expect(new Set(vertices)).toEqual(new Set([vertex])); - // Validate that the common vertex is associated with all of its pathElems - expect(graph.vertexToPathElems.get(vertex)).toEqual(new Set(pathElems)); - - // Validate that there is an edge connecting the vertex with each expected focalSideNeighbor - expectedEdgeCount += focalSideNeighbors.length; - const focalSideEdges = Array.from( - new Set(pathElems.map(elem => graph.pathElemToEdge.get(elem))) - ).filter(Boolean); - const focalSideKeys = focalSideEdges.map(({ to, from }) => (to === vertex.key ? from : to)); - const expectedKeys = focalSideNeighbors.map( - idx => graph.pathElemToVertex.get(graph.visIdxToPathElem[idx]).key - ); - expect(focalSideKeys).toEqual(expectedKeys); - }); - - // Validate that there aren't any rogue vertices nor edges - expect(graph.vertices.size).toBe(expectedVertices.length); - expect(new Set(graph.pathElemToEdge.values()).size).toBe(expectedEdgeCount); - } - describe('constructor', () => { - const testGraph = new GraphModel({ - ddgModel: simpleModel, - density: EDdgDensity.PreventPathEntanglement, - showOp: true, - }); + /** + * This function takes in a Graph and validates the structure based on the expected vertices. + * + * @param {GraphModel} graph - The Graph to validate. + * @param {Object[]} expectedVertices - The vertices that the Graph should have. + * @param {number[]} expectedVertices[].visIndices - The visibility indices that should all share one + * DdgVertex. + * @param {number[]} expectedVertices[].focalSIdeNeighbors - A single visibilityIdx is sufficient to define a + * neighboring vertex. For each focalSide visibilityIdx, the expectedVertex should have an + * edge connecting the expectedVertex back to the focalSideNeighbor. + */ + function validateGraph(graph, expectedVertices) { + let expectedEdgeCount = 0; + expectedVertices.forEach(({ visIndices, focalSideNeighbors = [] }) => { + // Validate that all visIndices share the same vertex + const pathElems = visIndices.map(visIdx => graph.visIdxToPathElem[visIdx]); + const vertices = pathElems.map(elem => graph.pathElemToVertex.get(elem)); + const vertex = vertices[0]; + expect(new Set(vertices)).toEqual(new Set([vertex])); + // Validate that the common vertex is associated with all of its pathElems + expect(graph.vertexToPathElems.get(vertex)).toEqual(new Set(pathElems)); + + // Validate that there is an edge connecting the vertex with each expected focalSideNeighbor + expectedEdgeCount += focalSideNeighbors.length; + const focalSideEdges = Array.from( + new Set(pathElems.map(elem => graph.pathElemToEdge.get(elem))) + ).filter(Boolean); + const focalSideKeys = focalSideEdges.map(({ to, from }) => (to === vertex.key ? from : to)); + const expectedKeys = focalSideNeighbors.map( + idx => graph.pathElemToVertex.get(graph.visIdxToPathElem[idx]).key + ); + expect(focalSideKeys).toEqual(expectedKeys); + }); + + // Validate that there aren't any rogue vertices nor edges + expect(graph.vertices.size).toBe(expectedVertices.length); + expect(new Set(graph.pathElemToEdge.values()).size).toBe(expectedEdgeCount); + } it('creates five vertices and four edges for one-path ddg', () => { + const testGraph = new GraphModel({ + ddgModel: simpleModel, + density: EDdgDensity.PreventPathEntanglement, + showOp: true, + }); validateGraph(testGraph, [ { visIndices: [0], @@ -98,9 +99,7 @@ describe('GraphModel', () => { }, ]); }); - }); - describe('convergent paths', () => { it('adds separate vertices for equal PathElems that have different focalPaths, even those with equal focalSideNeighbors', () => { const convergentGraph = new GraphModel({ ddgModel: convergentModel, @@ -175,6 +174,260 @@ describe('GraphModel', () => { }); }); + describe('generations', () => { + const generationGraph = makeGraph(generationModel, false, EDdgDensity.MostConcise); + const oneHopIndices = [ + ...generationGraph.distanceToPathElems.get(-1), + ...generationGraph.distanceToPathElems.get(0), + ...generationGraph.distanceToPathElems.get(1), + ].map(({ visibilityIdx }) => visibilityIdx); + const external = ({ isExternal }) => isExternal; + const hasher = generationGraph.getPathElemHasher(); + const internal = ({ isExternal }) => !isExternal; + + const downstreamTargets = generationGraph.distanceToPathElems.get(2); + const upstreamTargets = generationGraph.distanceToPathElems.get(-2); + const targets = [...downstreamTargets, ...upstreamTargets]; + const twoHopIndices = [...oneHopIndices, ...targets.map(({ visibilityIdx }) => visibilityIdx)]; + const targetKey = hasher(targets[0]); + const { visibilityIdx: targetLeafIdx } = downstreamTargets.find(external); + const [hiddenDownstreamTargetNotLeaf, visibleDownstreamTargetNotLeaf] = downstreamTargets.filter( + internal + ); + + const { visibilityIdx: targetRootIdx } = upstreamTargets.find(external); + const [hiddenUpstreamTargetNotRoot, visibleUpstreamTargetNotRoot] = upstreamTargets.filter(internal); + const leafAndRootVisEncoding = encode([...oneHopIndices, targetLeafIdx, targetRootIdx]); + const partialInternalTargetVisIndices = [ + ...oneHopIndices, + targetLeafIdx, + targetRootIdx, + visibleDownstreamTargetNotLeaf.visibilityIdx, + visibleUpstreamTargetNotRoot.visibilityIdx, + ]; + const partialInternalTargetVisEncoding = encode(partialInternalTargetVisIndices); + const allVisible = encode(generationGraph.visIdxToPathElem.map((_elem, idx) => idx)); + + const subsetOfTargetExternalNeighborVisibilityIndices = [ + visibleDownstreamTargetNotLeaf.externalSideNeighbor.visibilityIdx, + visibleUpstreamTargetNotRoot.externalSideNeighbor.visibilityIdx, + ]; + + const allButSomeExternalVisible = encode([ + ...twoHopIndices, + ...subsetOfTargetExternalNeighborVisibilityIndices, + ]); + + describe('getGeneration', () => { + it('returns empty array if key does not exist', () => { + const absentKey = 'absent key'; + + expect(generationGraph.getGeneration(absentKey, EDirection.Downstream)).toEqual([]); + expect(generationGraph.getGeneration(absentKey, EDirection.Upstream)).toEqual([]); + }); + + it('returns empty array if key has no visible elems', () => { + expect( + generationGraph.getGeneration(targetKey, EDirection.Downstream, encode(oneHopIndices)) + ).toEqual([]); + expect(generationGraph.getGeneration(targetKey, EDirection.Upstream, encode(oneHopIndices))).toEqual( + [] + ); + }); + + it('returns empty array if key is leaf/root elem', () => { + expect( + generationGraph.getGeneration(targetKey, EDirection.Downstream, leafAndRootVisEncoding) + ).toEqual([]); + + expect(generationGraph.getGeneration(targetKey, EDirection.Upstream, leafAndRootVisEncoding)).toEqual( + [] + ); + }); + + it('omits focalSide elems', () => { + const downstreamResult = generationGraph.getGeneration( + targetKey, + EDirection.Downstream, + partialInternalTargetVisEncoding + ); + expect(downstreamResult).toEqual([visibleDownstreamTargetNotLeaf.externalSideNeighbor]); + expect(downstreamResult).toEqual( + expect.not.arrayContaining([hiddenDownstreamTargetNotLeaf.externalSideNeighbor]) + ); + expect(downstreamResult).toEqual( + expect.not.arrayContaining([visibleUpstreamTargetNotRoot.focalSideNeighbor]) + ); + + const upstreamResult = generationGraph.getGeneration( + targetKey, + EDirection.Upstream, + partialInternalTargetVisEncoding + ); + expect(upstreamResult).toEqual([visibleUpstreamTargetNotRoot.externalSideNeighbor]); + expect(upstreamResult).toEqual( + expect.not.arrayContaining([hiddenUpstreamTargetNotRoot.externalSideNeighbor]) + ); + expect(downstreamResult).toEqual( + expect.not.arrayContaining([visibleDownstreamTargetNotLeaf.focalSideNeighbor]) + ); + }); + }); + + describe('getGenerationVisibility', () => { + it('returns null if getGeneration returns []', () => { + expect( + generationGraph.getGenerationVisibility(targetKey, EDirection.Downstream, leafAndRootVisEncoding) + ).toEqual(null); + expect( + generationGraph.getGenerationVisibility(targetKey, EDirection.Upstream, leafAndRootVisEncoding) + ).toEqual(null); + }); + + it('returns ECheckedStatus.Empty if all neighbors are hidden', () => { + expect(generationGraph.getGenerationVisibility(targetKey, EDirection.Downstream)).toEqual( + ECheckedStatus.Empty + ); + expect( + generationGraph.getGenerationVisibility( + targetKey, + EDirection.Downstream, + partialInternalTargetVisEncoding + ) + ).toEqual(ECheckedStatus.Empty); + + expect(generationGraph.getGenerationVisibility(targetKey, EDirection.Upstream)).toEqual( + ECheckedStatus.Empty + ); + expect( + generationGraph.getGenerationVisibility( + targetKey, + EDirection.Upstream, + partialInternalTargetVisEncoding + ) + ).toEqual(ECheckedStatus.Empty); + }); + + it('returns ECheckedStatus.Full if all neighbors are visible', () => { + const partialTargetExternalEncoding = encode([ + ...partialInternalTargetVisIndices, + ...subsetOfTargetExternalNeighborVisibilityIndices, + ]); + + expect(generationGraph.getGenerationVisibility(targetKey, EDirection.Downstream, allVisible)).toEqual( + ECheckedStatus.Full + ); + expect( + generationGraph.getGenerationVisibility( + targetKey, + EDirection.Downstream, + partialTargetExternalEncoding + ) + ).toEqual(ECheckedStatus.Full); + expect(generationGraph.getGenerationVisibility(targetKey, EDirection.Upstream, allVisible)).toEqual( + ECheckedStatus.Full + ); + expect( + generationGraph.getGenerationVisibility( + targetKey, + EDirection.Upstream, + partialTargetExternalEncoding + ) + ).toEqual(ECheckedStatus.Full); + }); + + it('returns ECheckedStatus.Partial if only some neighbors are visible', () => { + expect( + generationGraph.getGenerationVisibility(targetKey, EDirection.Downstream, allButSomeExternalVisible) + ).toEqual(ECheckedStatus.Partial); + expect( + generationGraph.getGenerationVisibility(targetKey, EDirection.Upstream, allButSomeExternalVisible) + ).toEqual(ECheckedStatus.Partial); + }); + }); + + describe('getVisWithUpdatedGeneration', () => { + const downstreamFullIndices = [ + ...twoHopIndices, + ...generationGraph.distanceToPathElems.get(3).map(({ visibilityIdx }) => visibilityIdx), + ]; + const downstreamFullEncoding = encode(downstreamFullIndices); + const upstreamFullIndices = [ + ...twoHopIndices, + ...generationGraph.distanceToPathElems.get(-3).map(({ visibilityIdx }) => visibilityIdx), + ]; + const upstreamFullEncoding = encode(upstreamFullIndices); + + it('returns undefined if there is no generation to update', () => { + expect( + generationGraph.getVisWithUpdatedGeneration(targetKey, EDirection.Downstream, encode(oneHopIndices)) + ).toEqual(undefined); + expect( + generationGraph.getVisWithUpdatedGeneration(targetKey, EDirection.Upstream, encode(oneHopIndices)) + ).toEqual(undefined); + expect( + generationGraph.getVisWithUpdatedGeneration( + targetKey, + EDirection.Downstream, + leafAndRootVisEncoding + ) + ).toEqual(undefined); + expect( + generationGraph.getVisWithUpdatedGeneration(targetKey, EDirection.Upstream, leafAndRootVisEncoding) + ).toEqual(undefined); + }); + + it('emptys target generation if it is full', () => { + expect( + generationGraph.getVisWithUpdatedGeneration(targetKey, EDirection.Downstream, allVisible) + ).toEqual({ + visEncoding: upstreamFullEncoding, + update: ECheckedStatus.Empty, + }); + expect( + generationGraph.getVisWithUpdatedGeneration(targetKey, EDirection.Upstream, allVisible) + ).toEqual({ + visEncoding: downstreamFullEncoding, + update: ECheckedStatus.Empty, + }); + }); + + it('fills target generation if it is empty', () => { + expect(generationGraph.getVisWithUpdatedGeneration(targetKey, EDirection.Downstream)).toEqual({ + visEncoding: downstreamFullEncoding, + update: ECheckedStatus.Full, + }); + expect(generationGraph.getVisWithUpdatedGeneration(targetKey, EDirection.Upstream)).toEqual({ + visEncoding: upstreamFullEncoding, + update: ECheckedStatus.Full, + }); + }); + + it('fills target generation if it is partially full', () => { + expect( + generationGraph.getVisWithUpdatedGeneration( + targetKey, + EDirection.Downstream, + allButSomeExternalVisible + ) + ).toEqual({ + visEncoding: encode([...downstreamFullIndices, ...subsetOfTargetExternalNeighborVisibilityIndices]), + update: ECheckedStatus.Full, + }); + expect( + generationGraph.getVisWithUpdatedGeneration( + targetKey, + EDirection.Upstream, + allButSomeExternalVisible + ) + ).toEqual({ + visEncoding: encode([...upstreamFullIndices, ...subsetOfTargetExternalNeighborVisibilityIndices]), + update: ECheckedStatus.Full, + }); + }); + }); + }); + describe('getVisible', () => { const convergentGraph = new GraphModel({ ddgModel: convergentModel, @@ -326,32 +579,6 @@ describe('GraphModel', () => { }); }); - describe('getVisWithVertices', () => { - const overlapGraph = new GraphModel({ - ddgModel: convergentModel, - density: EDdgDensity.PreventPathEntanglement, - showOp: true, - }); - const vertices = [ - overlapGraph.pathElemToVertex.get(overlapGraph.distanceToPathElems.get(3)[0]), - overlapGraph.pathElemToVertex.get(overlapGraph.distanceToPathElems.get(-1)[0]), - ]; - - it('handles absent visEncoding', () => { - expect(overlapGraph.getVisWithVertices(vertices)).toBe(encode([0, 1, 2, 3, 4, 5, 6, 7, 8])); - }); - - it('uses provided visEncoding', () => { - expect(overlapGraph.getVisWithVertices(vertices, encode([0, 1, 3]))).toBe( - encode([0, 1, 2, 3, 4, 5, 6, 8]) - ); - }); - - it('throws error if given absent vertex', () => { - expect(() => overlapGraph.getVisWithVertices([{}])).toThrowError(); - }); - }); - describe('getVertexVisiblePathElems', () => { const overlapGraph = new GraphModel({ ddgModel: doubleFocalModel, @@ -390,6 +617,59 @@ describe('GraphModel', () => { }); }); + describe('getVisWithoutVertex', () => { + const overlapGraph = new GraphModel({ + ddgModel: convergentModel, + density: EDdgDensity.OnePerLevel, + showOp: true, + }); + const vertexKey = overlapGraph.getPathElemHasher()(overlapGraph.distanceToPathElems.get(2)[0]); + + it('handles absent visEncoding', () => { + expect(overlapGraph.getVisWithoutVertex(vertexKey)).toBe(encode([0, 1, 2, 3, 4, 5])); + }); + + it('uses provided visEncoding', () => { + expect(overlapGraph.getVisWithoutVertex(vertexKey, encode([0, 1, 2, 3, 5, 6, 7, 8, 9]))).toBe( + encode([0, 1, 2, 3, 5]) + ); + }); + + it('returns undefined if vertex is already hidden', () => { + expect(overlapGraph.getVisWithoutVertex(vertexKey, encode([0, 1, 2, 3, 5]))).toBe(undefined); + }); + + it('returns undefined if vertex has no elems', () => { + expect(overlapGraph.getVisWithoutVertex('absent vertex key')).toBe(undefined); + }); + }); + + describe('getVisWithVertices', () => { + const overlapGraph = new GraphModel({ + ddgModel: convergentModel, + density: EDdgDensity.PreventPathEntanglement, + showOp: true, + }); + const vertices = [ + overlapGraph.pathElemToVertex.get(overlapGraph.distanceToPathElems.get(3)[0]), + overlapGraph.pathElemToVertex.get(overlapGraph.distanceToPathElems.get(-1)[0]), + ]; + + it('handles absent visEncoding', () => { + expect(overlapGraph.getVisWithVertices(vertices)).toBe(encode([0, 1, 2, 3, 4, 5, 6, 7, 8])); + }); + + it('uses provided visEncoding', () => { + expect(overlapGraph.getVisWithVertices(vertices, encode([0, 1, 3]))).toBe( + encode([0, 1, 2, 3, 4, 5, 6, 8]) + ); + }); + + it('throws error if given absent vertex', () => { + expect(() => overlapGraph.getVisWithVertices([{}])).toThrowError(); + }); + }); + describe('makeGraph', () => { it('returns Graph with correct properties', () => { const graph = makeGraph(convergentModel, true, EDdgDensity.PreventPathEntanglement); diff --git a/packages/jaeger-ui/src/model/ddg/GraphModel/index.tsx b/packages/jaeger-ui/src/model/ddg/GraphModel/index.tsx index 54b5f13356..51c93a6503 100644 --- a/packages/jaeger-ui/src/model/ddg/GraphModel/index.tsx +++ b/packages/jaeger-ui/src/model/ddg/GraphModel/index.tsx @@ -21,7 +21,15 @@ import getEdgeId from './getEdgeId'; import getPathElemHasher from './getPathElemHasher'; import { decode, encode } from '../visibility-codec'; -import { PathElem, EDdgDensity, TDdgDistanceToPathElems, TDdgModel, TDdgVertex } from '../types'; +import { + PathElem, + ECheckedStatus, + EDdgDensity, + EDirection, + TDdgDistanceToPathElems, + TDdgModel, + TDdgVertex, +} from '../types'; export { default as getEdgeId } from './getEdgeId'; @@ -119,6 +127,38 @@ export default class GraphModel { ); } + private getGeneration = (vertexKey: string, direction: EDirection, visEncoding?: string): PathElem[] => { + const rv: PathElem[] = []; + const elems = this.getVertexVisiblePathElems(vertexKey, visEncoding); + if (!elems) return rv; + elems.forEach(({ focalSideNeighbor, memberIdx, memberOf }) => { + const generationMember = memberOf.members[memberIdx + direction]; + if (generationMember && generationMember !== focalSideNeighbor) rv.push(generationMember); + }); + return rv; + }; + + public getGenerationVisibility = ( + vertexKey: string, + direction: EDirection, + visEncoding?: string + ): ECheckedStatus | null => { + const generation = this.getGeneration(vertexKey, direction, visEncoding); + if (!generation.length) return null; + + const visibleIndices = this.getVisibleIndices(visEncoding); + let partial = false; + let full = true; + generation.forEach(elem => { + const isVis = visibleIndices.has(elem.visibilityIdx); + partial = partial || isVis; + full = full && isVis; + }); + if (full) return ECheckedStatus.Full; + if (partial) return ECheckedStatus.Partial; + return ECheckedStatus.Empty; + }; + private getVisiblePathElems(visEncoding?: string) { if (visEncoding == null) return this.getDefaultVisiblePathElems(); return decode(visEncoding) @@ -187,37 +227,84 @@ export default class GraphModel { } ); - public getVisWithVertices = (vertices: TDdgVertex[], visEncoding?: string) => { - const indices: Set = new Set(this.getVisiblePathElems(visEncoding).map(pe => pe.visibilityIdx)); + private getVisWithoutElems(elems: PathElem[], visEncoding?: string) { + const visible = this.getVisibleIndices(visEncoding); + elems.forEach(({ externalPath }) => { + externalPath.forEach(({ visibilityIdx }) => { + visible.delete(visibilityIdx); + }); + }); + + return encode(Array.from(visible)); + } + + public getVisWithoutVertex(vertexKey: string, visEncoding?: string): string | undefined { + const elems = this.getVertexVisiblePathElems(vertexKey, visEncoding); + if (elems && elems.length) return this.getVisWithoutElems(elems, visEncoding); + return undefined; + } + + private getVisWithElems(elems: PathElem[], visEncoding?: string) { + const visible = this.getVisibleIndices(visEncoding); + elems.forEach(({ focalPath }) => + focalPath.forEach(({ visibilityIdx }) => { + visible.add(visibilityIdx); + }) + ); + + return encode(Array.from(visible)); + } + + public getVisWithUpdatedGeneration( + vertexKey: string, + direction: EDirection, + visEncoding?: string + ): { visEncoding: string; update: ECheckedStatus } | undefined { + const generationElems = this.getGeneration(vertexKey, direction, visEncoding); + const currCheckedStatus = this.getGenerationVisibility(vertexKey, direction, visEncoding); + if (!generationElems.length || !currCheckedStatus) return undefined; + + if (currCheckedStatus === ECheckedStatus.Full) { + return { + visEncoding: this.getVisWithoutElems(generationElems, visEncoding), + update: ECheckedStatus.Empty, + }; + } + + return { + visEncoding: this.getVisWithElems(generationElems, visEncoding), + update: ECheckedStatus.Full, + }; + } + public getVisWithVertices(vertices: TDdgVertex[], visEncoding?: string) { + const elemSet: Set = new Set(); vertices.forEach(vertex => { const elems = this.vertexToPathElems.get(vertex); if (!elems) throw new Error(`${vertex} does not exist in graph`); - elems.forEach(elem => { - elem.focalPath.forEach(({ visibilityIdx }) => indices.add(visibilityIdx)); - }); + elems.forEach(elem => elemSet.add(elem)); }); - return encode(Array.from(indices)); - }; + return this.getVisWithElems(Array.from(elemSet), visEncoding); + } - public getVertexVisiblePathElems = ( + public getVertexVisiblePathElems( vertexKey: string, visEncoding: string | undefined - ): PathElem[] | undefined => { + ): PathElem[] | undefined { const vertex = this.vertices.get(vertexKey); if (vertex) { const pathElems = this.vertexToPathElems.get(vertex); if (pathElems && pathElems.size) { - const visIndices = visEncoding ? new Set(decode(visEncoding)) : undefined; + const visIndices = this.getVisibleIndices(visEncoding); return Array.from(pathElems).filter(elem => { - return visIndices ? visIndices.has(elem.visibilityIdx) : Math.abs(elem.distance) < 3; + return visIndices.has(elem.visibilityIdx); }); } } return undefined; - }; + } } export const makeGraph = memoize(10)( diff --git a/packages/jaeger-ui/src/model/ddg/PathElem.test.js b/packages/jaeger-ui/src/model/ddg/PathElem.test.js index f6ed621ee5..198916b046 100644 --- a/packages/jaeger-ui/src/model/ddg/PathElem.test.js +++ b/packages/jaeger-ui/src/model/ddg/PathElem.test.js @@ -80,6 +80,20 @@ describe('PathElem', () => { }).toThrowError(); }); + it('has externalSideNeighbor if distance is not 0 and it is not external', () => { + expect(pathElem.externalSideNeighbor).toBe(testPath.members[testMemberIdx - 1]); + }); + + it('has a null externalSideNeighbor if distance is 0', () => { + pathElem = new PathElem({ path: testPath, operation: testOperation, memberIdx: testPath.focalIdx }); + expect(pathElem.externalSideNeighbor).toBe(null); + }); + + it('has an undefined externalSideNeighbor if is external', () => { + pathElem = new PathElem({ path: testPath, operation: testOperation, memberIdx: 0 }); + expect(pathElem.externalSideNeighbor).toBe(undefined); + }); + it('has focalSideNeighbor if distance is not 0', () => { expect(pathElem.focalSideNeighbor).toBe(testPath.members[testMemberIdx + 1]); }); @@ -110,6 +124,27 @@ describe('PathElem', () => { expect(focalElem.isExternal).toBe(false); }); + describe('externalPath', () => { + const path = getPath(); + + it('returns array of itself if it is focal elem', () => { + const targetPathElem = path.members[path.focalIdx]; + expect(targetPathElem.externalPath).toEqual([targetPathElem]); + }); + + it('returns path to focal elem in correct order for upstream elem', () => { + const idx = path.focalIdx - 1; + const targetPathElem = path.members[idx]; + expect(targetPathElem.externalPath).toEqual(path.members.slice(0, idx + 1)); + }); + + it('returns path to focal elem in correct order for downstream elem', () => { + const idx = path.focalIdx + 1; + const targetPathElem = path.members[idx]; + expect(targetPathElem.externalPath).toEqual(path.members.slice(idx)); + }); + }); + describe('focalPath', () => { const path = getPath(); diff --git a/packages/jaeger-ui/src/model/ddg/sample-paths.test.resources.js b/packages/jaeger-ui/src/model/ddg/sample-paths.test.resources.js index 3e1ff249dd..54fae54526 100644 --- a/packages/jaeger-ui/src/model/ddg/sample-paths.test.resources.js +++ b/packages/jaeger-ui/src/model/ddg/sample-paths.test.resources.js @@ -89,6 +89,45 @@ export const convergentPaths = [ [firstPayloadElem, focalPayloadElem, divergentPayloadElem, afterPayloadElem, lastPayloadElem], ]; +const generationPayloadElems = { + afterFocalMid: simplePayloadElemMaker('afterFocalMid'), + afterTarget0: simplePayloadElemMaker('afterTarget0'), + afterTarget1: simplePayloadElemMaker('afterTarget1'), + beforeFocalMid: simplePayloadElemMaker('beforeFocalMid'), + beforeTarget0: simplePayloadElemMaker('beforeTarget0'), + beforeTarget1: simplePayloadElemMaker('beforeTarget1'), + target: simplePayloadElemMaker('target'), +}; + +export const generationPaths = [ + [ + generationPayloadElems.beforeTarget0, + generationPayloadElems.target, + generationPayloadElems.beforeFocalMid, + focalPayloadElem, + ], + [ + generationPayloadElems.beforeTarget1, + generationPayloadElems.target, + generationPayloadElems.beforeFocalMid, + focalPayloadElem, + ], + [focalPayloadElem, generationPayloadElems.afterFocalMid, generationPayloadElems.target], + [ + focalPayloadElem, + generationPayloadElems.afterFocalMid, + generationPayloadElems.target, + generationPayloadElems.afterTarget0, + ], + [ + focalPayloadElem, + generationPayloadElems.afterFocalMid, + generationPayloadElems.target, + generationPayloadElems.afterTarget1, + ], + [generationPayloadElems.target, generationPayloadElems.beforeFocalMid, focalPayloadElem], +]; + export const wrap = paths => ({ dependencies: paths.map(path => ({ path, attributes: [] })), }); From 6b9e8d65d501b36869b901b5a1ff6189e4613dc3 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Thu, 14 Nov 2019 14:20:58 -0500 Subject: [PATCH 3/3] Code clean up and fix node menu collisions Signed-off-by: Everett Ross --- .../Graph/DdgNodeContent/index.css | 9 ++ .../Graph/DdgNodeContent/index.test.js | 4 +- .../Graph/DdgNodeContent/index.tsx | 12 +-- .../components/DeepDependencies/index.test.js | 33 +++++-- .../src/components/DeepDependencies/index.tsx | 6 +- .../src/model/ddg/GraphModel/index.test.js | 91 ++++++++++--------- .../src/model/ddg/GraphModel/index.tsx | 12 ++- 7 files changed, 95 insertions(+), 72 deletions(-) diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css index 8d7f009411..efe949e50d 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css @@ -14,6 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ +/* + * Because .plexus-Digraph--MeasurableHtmlNode is `position: absolute`, adding a z-index to DdgNodeContent + * would have no effect on layering. + */ +.plexus-Digraph--MeasurableHtmlNode:hover { + z-index: 1; +} + .DdgNodeContent--core { background: #eee; border-radius: 100%; @@ -66,6 +74,7 @@ limitations under the License. } .DdgNodeContent--actionsItemIconWrapper { + flex: none; height: 16px; width: 16px; } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.test.js b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.test.js index c4860b5e49..f586efc864 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.test.js @@ -142,10 +142,10 @@ describe('', () => { it('cancels delayed set state if mouse re-enters before timeout runs', () => { wrapper.simulate('mouseleave', { type: 'mouseleave' }); - expect(wrapper.instance().timeout).toEqual(expect.any(Number)); + expect(wrapper.instance().hoverClearDelay).toEqual(expect.any(Number)); wrapper.simulate('mouseenter', { type: 'mouseenter' }); - expect(wrapper.instance().timeout).toBeUndefined(); + expect(wrapper.instance().hoverClearDelay).toBeUndefined(); jest.runAllTimers(); expect(wrapper.state('hovered')).toBe(true); 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 98babcabb7..4b20892165 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx @@ -62,7 +62,7 @@ type TProps = { }; export default class DdgNodeContent extends React.PureComponent { - timeout?: number; + hoverClearDelay?: number; state = { hovered: false, @@ -186,16 +186,16 @@ export default class DdgNodeContent extends React.PureComponent { const hovered = event.type === 'mouseenter'; setViewModifier(vertexKey, EViewModifier.Hovered, hovered); if (hovered) { - if (this.timeout) { - clearTimeout(this.timeout); - this.timeout = undefined; + if (this.hoverClearDelay) { + clearTimeout(this.hoverClearDelay); + this.hoverClearDelay = undefined; } else { this.setState({ hovered }); } } else { - this.timeout = setTimeout(() => { + this.hoverClearDelay = setTimeout(() => { this.setState({ hovered }); - this.timeout = undefined; + this.hoverClearDelay = undefined; }, 150); } }; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.test.js b/packages/jaeger-ui/src/components/DeepDependencies/index.test.js index 799ce3b819..e799b53d46 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.test.js @@ -76,6 +76,7 @@ describe('DeepDependencyGraphPage', () => { }; const ddgPageImpl = new DeepDependencyGraphPageImpl(props); const ddgWithoutGraph = new DeepDependencyGraphPageImpl(propsWithoutGraph); + const setIdx = visibilityIdx => ({ visibilityIdx }); describe('constructor', () => { beforeEach(() => { @@ -195,17 +196,17 @@ describe('DeepDependencyGraphPage', () => { expect(trackFocusPathsSpy).not.toHaveBeenCalled(); }); - it('udates url state and tracks focus paths', () => { + it('updates url state and tracks focus paths', () => { const indices = [4, 8, 15, 16, 23, 42]; const elems = [ { memberOf: { - members: indices.slice(0, indices.length / 2).map(visibilityIdx => ({ visibilityIdx })), + members: indices.slice(0, indices.length / 2).map(setIdx), }, }, { memberOf: { - members: indices.slice(indices.length / 2).map(visibilityIdx => ({ visibilityIdx })), + members: indices.slice(indices.length / 2).map(setIdx), }, }, ]; @@ -235,10 +236,14 @@ describe('DeepDependencyGraphPage', () => { expect(trackHideSpy).not.toHaveBeenCalled(); }); - it('udates url state and tracks hide', () => { + it('updates url state and tracks hide', () => { props.graph.getVisWithoutVertex.mockReturnValueOnce(visEncoding); ddgPageImpl.hideVertex(vertexKey); + expect(props.graph.getVisWithoutVertex).toHaveBeenLastCalledWith( + vertexKey, + props.urlState.visEncoding + ); expect(getUrlSpy).toHaveBeenLastCalledWith( Object.assign({}, props.urlState, { visEncoding }), undefined @@ -417,13 +422,18 @@ describe('DeepDependencyGraphPage', () => { expect(trackShowSpy).not.toHaveBeenCalled(); }); - it('udates url state and tracks hide if result.status is ECheckedStatus.Empty', () => { + it('updates url state and tracks hide if result.status is ECheckedStatus.Empty', () => { props.graph.getVisWithUpdatedGeneration.mockReturnValueOnce({ visEncoding, update: ECheckedStatus.Empty, }); ddgPageImpl.updateGenerationVisibility(vertexKey, direction); + expect(props.graph.getVisWithUpdatedGeneration).toHaveBeenLastCalledWith( + vertexKey, + direction, + props.urlState.visEncoding + ); expect(getUrlSpy).toHaveBeenLastCalledWith( Object.assign({}, props.urlState, { visEncoding }), undefined @@ -433,13 +443,18 @@ describe('DeepDependencyGraphPage', () => { expect(trackShowSpy).not.toHaveBeenCalled(); }); - it('udates url state and tracks show if result.status is ECheckedStatus.Full', () => { + it('updates url state and tracks show if result.status is ECheckedStatus.Full', () => { props.graph.getVisWithUpdatedGeneration.mockReturnValueOnce({ visEncoding, update: ECheckedStatus.Full, }); ddgPageImpl.updateGenerationVisibility(vertexKey, direction); + expect(props.graph.getVisWithUpdatedGeneration).toHaveBeenLastCalledWith( + vertexKey, + direction, + props.urlState.visEncoding + ); expect(getUrlSpy).toHaveBeenLastCalledWith( Object.assign({}, props.urlState, { visEncoding }), undefined @@ -457,10 +472,8 @@ describe('DeepDependencyGraphPage', () => { let warnSpy; beforeAll(() => { - props.graph.getVertexVisiblePathElems.mockReturnValue( - visibilityIndices.map(visibilityIdx => ({ visibilityIdx })) - ); - warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + props.graph.getVertexVisiblePathElems.mockReturnValue(visibilityIndices.map(setIdx)); + warnSpy = jest.spyOn(console, 'warn').mockImplementationOnce(); }); beforeEach(() => { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx index 30f67282b5..bcb8d68c81 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx @@ -142,10 +142,9 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { hideVertex = (vertexKey: string) => { const { graph, urlState } = this.props; - const { visEncoding: currVisEncoding } = urlState; if (!graph) return; - const visEncoding = graph.getVisWithoutVertex(vertexKey, currVisEncoding); + const visEncoding = graph.getVisWithoutVertex(vertexKey, urlState.visEncoding); if (!visEncoding) return; trackHide(); @@ -219,10 +218,9 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { updateGenerationVisibility = (vertexKey: string, direction: EDirection) => { const { graph, urlState } = this.props; - const { visEncoding: currVisEncoding } = urlState; if (!graph) return; - const result = graph.getVisWithUpdatedGeneration(vertexKey, direction, currVisEncoding); + const result = graph.getVisWithUpdatedGeneration(vertexKey, direction, urlState.visEncoding); if (!result) return; const { visEncoding, update } = result; diff --git a/packages/jaeger-ui/src/model/ddg/GraphModel/index.test.js b/packages/jaeger-ui/src/model/ddg/GraphModel/index.test.js index a84a3e5a3a..5d89f522ab 100644 --- a/packages/jaeger-ui/src/model/ddg/GraphModel/index.test.js +++ b/packages/jaeger-ui/src/model/ddg/GraphModel/index.test.js @@ -28,8 +28,8 @@ import { encode } from '../visibility-codec'; describe('GraphModel', () => { const convergentModel = transformDdgData(wrap(convergentPaths), focalPayloadElem); const doubleFocalModel = transformDdgData(wrap([doubleFocalPath, simplePath]), focalPayloadElem); - const generationModel = transformDdgData(wrap(generationPaths), focalPayloadElem); const simpleModel = transformDdgData(wrap([simplePath]), focalPayloadElem); + const getIdx = ({ visibilityIdx }) => visibilityIdx; describe('constructor', () => { /** @@ -175,47 +175,46 @@ describe('GraphModel', () => { }); describe('generations', () => { + const generationModel = transformDdgData(wrap(generationPaths), focalPayloadElem); const generationGraph = makeGraph(generationModel, false, EDdgDensity.MostConcise); const oneHopIndices = [ ...generationGraph.distanceToPathElems.get(-1), ...generationGraph.distanceToPathElems.get(0), ...generationGraph.distanceToPathElems.get(1), - ].map(({ visibilityIdx }) => visibilityIdx); + ].map(getIdx); + const allVisible = encode(generationGraph.visIdxToPathElem.map((_elem, idx) => idx)); const external = ({ isExternal }) => isExternal; - const hasher = generationGraph.getPathElemHasher(); const internal = ({ isExternal }) => !isExternal; const downstreamTargets = generationGraph.distanceToPathElems.get(2); + const targetKey = generationGraph.getPathElemHasher()(downstreamTargets[0]); const upstreamTargets = generationGraph.distanceToPathElems.get(-2); - const targets = [...downstreamTargets, ...upstreamTargets]; - const twoHopIndices = [...oneHopIndices, ...targets.map(({ visibilityIdx }) => visibilityIdx)]; - const targetKey = hasher(targets[0]); const { visibilityIdx: targetLeafIdx } = downstreamTargets.find(external); + const { visibilityIdx: targetRootIdx } = upstreamTargets.find(external); + const leafAndRootVisIndices = [...oneHopIndices, targetLeafIdx, targetRootIdx]; + const leafAndRootVisEncoding = encode(leafAndRootVisIndices); const [hiddenDownstreamTargetNotLeaf, visibleDownstreamTargetNotLeaf] = downstreamTargets.filter( internal ); - - const { visibilityIdx: targetRootIdx } = upstreamTargets.find(external); const [hiddenUpstreamTargetNotRoot, visibleUpstreamTargetNotRoot] = upstreamTargets.filter(internal); - const leafAndRootVisEncoding = encode([...oneHopIndices, targetLeafIdx, targetRootIdx]); - const partialInternalTargetVisIndices = [ - ...oneHopIndices, - targetLeafIdx, - targetRootIdx, + const partialTargetVisIndices = [ + ...leafAndRootVisIndices, visibleDownstreamTargetNotLeaf.visibilityIdx, visibleUpstreamTargetNotRoot.visibilityIdx, ]; - const partialInternalTargetVisEncoding = encode(partialInternalTargetVisIndices); - const allVisible = encode(generationGraph.visIdxToPathElem.map((_elem, idx) => idx)); - - const subsetOfTargetExternalNeighborVisibilityIndices = [ + const partialTargetVisEncoding = encode(partialTargetVisIndices); + const subsetOfTargetExternalNeighborsVisibilityIndices = [ visibleDownstreamTargetNotLeaf.externalSideNeighbor.visibilityIdx, visibleUpstreamTargetNotRoot.externalSideNeighbor.visibilityIdx, ]; - + const twoHopIndices = [ + ...oneHopIndices, + ...downstreamTargets.map(getIdx), + ...upstreamTargets.map(getIdx), + ]; const allButSomeExternalVisible = encode([ ...twoHopIndices, - ...subsetOfTargetExternalNeighborVisibilityIndices, + ...subsetOfTargetExternalNeighborsVisibilityIndices, ]); describe('getGeneration', () => { @@ -230,6 +229,7 @@ describe('GraphModel', () => { expect( generationGraph.getGeneration(targetKey, EDirection.Downstream, encode(oneHopIndices)) ).toEqual([]); + expect(generationGraph.getGeneration(targetKey, EDirection.Upstream, encode(oneHopIndices))).toEqual( [] ); @@ -249,7 +249,7 @@ describe('GraphModel', () => { const downstreamResult = generationGraph.getGeneration( targetKey, EDirection.Downstream, - partialInternalTargetVisEncoding + partialTargetVisEncoding ); expect(downstreamResult).toEqual([visibleDownstreamTargetNotLeaf.externalSideNeighbor]); expect(downstreamResult).toEqual( @@ -262,7 +262,7 @@ describe('GraphModel', () => { const upstreamResult = generationGraph.getGeneration( targetKey, EDirection.Upstream, - partialInternalTargetVisEncoding + partialTargetVisEncoding ); expect(upstreamResult).toEqual([visibleUpstreamTargetNotRoot.externalSideNeighbor]); expect(upstreamResult).toEqual( @@ -279,6 +279,7 @@ describe('GraphModel', () => { expect( generationGraph.getGenerationVisibility(targetKey, EDirection.Downstream, leafAndRootVisEncoding) ).toEqual(null); + expect( generationGraph.getGenerationVisibility(targetKey, EDirection.Upstream, leafAndRootVisEncoding) ).toEqual(null); @@ -289,29 +290,21 @@ describe('GraphModel', () => { ECheckedStatus.Empty ); expect( - generationGraph.getGenerationVisibility( - targetKey, - EDirection.Downstream, - partialInternalTargetVisEncoding - ) + generationGraph.getGenerationVisibility(targetKey, EDirection.Downstream, partialTargetVisEncoding) ).toEqual(ECheckedStatus.Empty); expect(generationGraph.getGenerationVisibility(targetKey, EDirection.Upstream)).toEqual( ECheckedStatus.Empty ); expect( - generationGraph.getGenerationVisibility( - targetKey, - EDirection.Upstream, - partialInternalTargetVisEncoding - ) + generationGraph.getGenerationVisibility(targetKey, EDirection.Upstream, partialTargetVisEncoding) ).toEqual(ECheckedStatus.Empty); }); it('returns ECheckedStatus.Full if all neighbors are visible', () => { - const partialTargetExternalEncoding = encode([ - ...partialInternalTargetVisIndices, - ...subsetOfTargetExternalNeighborVisibilityIndices, + const partialTargetWithRespectiveExternalVisEncoding = encode([ + ...partialTargetVisIndices, + ...subsetOfTargetExternalNeighborsVisibilityIndices, ]); expect(generationGraph.getGenerationVisibility(targetKey, EDirection.Downstream, allVisible)).toEqual( @@ -321,7 +314,7 @@ describe('GraphModel', () => { generationGraph.getGenerationVisibility( targetKey, EDirection.Downstream, - partialTargetExternalEncoding + partialTargetWithRespectiveExternalVisEncoding ) ).toEqual(ECheckedStatus.Full); expect(generationGraph.getGenerationVisibility(targetKey, EDirection.Upstream, allVisible)).toEqual( @@ -331,7 +324,7 @@ describe('GraphModel', () => { generationGraph.getGenerationVisibility( targetKey, EDirection.Upstream, - partialTargetExternalEncoding + partialTargetWithRespectiveExternalVisEncoding ) ).toEqual(ECheckedStatus.Full); }); @@ -340,6 +333,7 @@ describe('GraphModel', () => { expect( generationGraph.getGenerationVisibility(targetKey, EDirection.Downstream, allButSomeExternalVisible) ).toEqual(ECheckedStatus.Partial); + expect( generationGraph.getGenerationVisibility(targetKey, EDirection.Upstream, allButSomeExternalVisible) ).toEqual(ECheckedStatus.Partial); @@ -349,32 +343,33 @@ describe('GraphModel', () => { describe('getVisWithUpdatedGeneration', () => { const downstreamFullIndices = [ ...twoHopIndices, - ...generationGraph.distanceToPathElems.get(3).map(({ visibilityIdx }) => visibilityIdx), + ...generationGraph.distanceToPathElems.get(3).map(getIdx), ]; const downstreamFullEncoding = encode(downstreamFullIndices); const upstreamFullIndices = [ ...twoHopIndices, - ...generationGraph.distanceToPathElems.get(-3).map(({ visibilityIdx }) => visibilityIdx), + ...generationGraph.distanceToPathElems.get(-3).map(getIdx), ]; const upstreamFullEncoding = encode(upstreamFullIndices); - it('returns undefined if there is no generation to update', () => { + it('returns null if there is no generation to update', () => { expect( generationGraph.getVisWithUpdatedGeneration(targetKey, EDirection.Downstream, encode(oneHopIndices)) - ).toEqual(undefined); + ).toEqual(null); expect( generationGraph.getVisWithUpdatedGeneration(targetKey, EDirection.Upstream, encode(oneHopIndices)) - ).toEqual(undefined); + ).toEqual(null); + expect( generationGraph.getVisWithUpdatedGeneration( targetKey, EDirection.Downstream, leafAndRootVisEncoding ) - ).toEqual(undefined); + ).toEqual(null); expect( generationGraph.getVisWithUpdatedGeneration(targetKey, EDirection.Upstream, leafAndRootVisEncoding) - ).toEqual(undefined); + ).toEqual(null); }); it('emptys target generation if it is full', () => { @@ -384,6 +379,7 @@ describe('GraphModel', () => { visEncoding: upstreamFullEncoding, update: ECheckedStatus.Empty, }); + expect( generationGraph.getVisWithUpdatedGeneration(targetKey, EDirection.Upstream, allVisible) ).toEqual({ @@ -397,6 +393,7 @@ describe('GraphModel', () => { visEncoding: downstreamFullEncoding, update: ECheckedStatus.Full, }); + expect(generationGraph.getVisWithUpdatedGeneration(targetKey, EDirection.Upstream)).toEqual({ visEncoding: upstreamFullEncoding, update: ECheckedStatus.Full, @@ -411,9 +408,13 @@ describe('GraphModel', () => { allButSomeExternalVisible ) ).toEqual({ - visEncoding: encode([...downstreamFullIndices, ...subsetOfTargetExternalNeighborVisibilityIndices]), + visEncoding: encode([ + ...downstreamFullIndices, + ...subsetOfTargetExternalNeighborsVisibilityIndices, + ]), update: ECheckedStatus.Full, }); + expect( generationGraph.getVisWithUpdatedGeneration( targetKey, @@ -421,7 +422,7 @@ describe('GraphModel', () => { allButSomeExternalVisible ) ).toEqual({ - visEncoding: encode([...upstreamFullIndices, ...subsetOfTargetExternalNeighborVisibilityIndices]), + visEncoding: encode([...upstreamFullIndices, ...subsetOfTargetExternalNeighborsVisibilityIndices]), update: ECheckedStatus.Full, }); }); diff --git a/packages/jaeger-ui/src/model/ddg/GraphModel/index.tsx b/packages/jaeger-ui/src/model/ddg/GraphModel/index.tsx index 51c93a6503..4ea9c5f429 100644 --- a/packages/jaeger-ui/src/model/ddg/GraphModel/index.tsx +++ b/packages/jaeger-ui/src/model/ddg/GraphModel/index.tsx @@ -131,6 +131,7 @@ export default class GraphModel { const rv: PathElem[] = []; const elems = this.getVertexVisiblePathElems(vertexKey, visEncoding); if (!elems) return rv; + elems.forEach(({ focalSideNeighbor, memberIdx, memberOf }) => { const generationMember = memberOf.members[memberIdx + direction]; if (generationMember && generationMember !== focalSideNeighbor) rv.push(generationMember); @@ -154,6 +155,7 @@ export default class GraphModel { partial = partial || isVis; full = full && isVis; }); + if (full) return ECheckedStatus.Full; if (partial) return ECheckedStatus.Partial; return ECheckedStatus.Empty; @@ -259,10 +261,10 @@ export default class GraphModel { vertexKey: string, direction: EDirection, visEncoding?: string - ): { visEncoding: string; update: ECheckedStatus } | undefined { + ): { visEncoding: string; update: ECheckedStatus } | null { const generationElems = this.getGeneration(vertexKey, direction, visEncoding); const currCheckedStatus = this.getGenerationVisibility(vertexKey, direction, visEncoding); - if (!generationElems.length || !currCheckedStatus) return undefined; + if (!generationElems.length || !currCheckedStatus) return null; if (currCheckedStatus === ECheckedStatus.Full) { return { @@ -278,15 +280,15 @@ export default class GraphModel { } public getVisWithVertices(vertices: TDdgVertex[], visEncoding?: string) { - const elemSet: Set = new Set(); + const elemSet: PathElem[] = []; vertices.forEach(vertex => { const elems = this.vertexToPathElems.get(vertex); if (!elems) throw new Error(`${vertex} does not exist in graph`); - elems.forEach(elem => elemSet.add(elem)); + elemSet.push(...elems); }); - return this.getVisWithElems(Array.from(elemSet), visEncoding); + return this.getVisWithElems(elemSet, visEncoding); } public getVertexVisiblePathElems(