Skip to content

Commit

Permalink
Merge pull request jaegertracing#480 from rubenvp8510/links-trace
Browse files Browse the repository at this point in the history
Support trace-scoped external links similar to tag links
  • Loading branch information
everett980 authored Dec 10, 2019
2 parents 1ee0cf1 + c39d6e6 commit 5f2a05d
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ import { getTraceName } from '../../../model/trace-viewer';
import { TNil } from '../../../types';
import { Trace } from '../../../types/trace';
import { formatDatetime, formatDuration } from '../../../utils/date';
import { getTraceLinks } from '../../../model/link-patterns';

import './TracePageHeader.css';
import ExternalLinks from '../../common/ExternalLinks';

type TracePageHeaderEmbedProps = {
canCollapse: boolean;
Expand Down Expand Up @@ -136,6 +138,8 @@ export function TracePageHeaderFn(props: TracePageHeaderEmbedProps & { forwarded
return null;
}

const links = getTraceLinks(trace);

const summaryItems =
!hideSummary &&
!slimView &&
Expand All @@ -159,6 +163,7 @@ export function TracePageHeaderFn(props: TracePageHeaderEmbedProps & { forwarded
<IoAndroidArrowBack />
</Link>
)}
{links && links.length > 0 && <ExternalLinks links={links} />}
{canCollapse ? (
<a
className="TracePageHeader--titleLink"
Expand Down
54 changes: 54 additions & 0 deletions packages/jaeger-ui/src/components/common/ExternalLinks.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) 2019 The Jaeger Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import React from 'react';
import { shallow } from 'enzyme';
import { Menu, Dropdown } from 'antd';

import ExternalLinks from './ExternalLinks';

describe('<ExternalLinks>', () => {
describe('render links links', () => {
it('renders dropdown with multiple links', () => {
const links = [
{ url: 'http://nowhere/', text: 'some text' },
{ url: 'http://other/', text: 'other text' },
{ url: 'http://link/', text: 'link text' },
];

const wrapper = shallow(<ExternalLinks links={links} />);
const dropdown = wrapper.find(Dropdown);
expect(dropdown.length).toBe(1);
const linkValues = shallow(dropdown.first().props().overlay);
const submenuItems = linkValues.find(Menu.Item);
expect(submenuItems.length).toBe(links.length);
submenuItems.forEach((subMenu, i) => {
const linkValue = subMenu.find('LinkValue');
expect(linkValue.props().href).toBe(links[i].url);
expect(linkValue.props().children).toBe(links[i].text);
});
});

it('renders one link', () => {
const links = [{ url: 'http://nowhere/', text: 'some text' }];
const wrapper = shallow(<ExternalLinks links={links} />);
const dropdown = wrapper.find(Dropdown);
expect(dropdown.length).toBe(0);
const linkValues = wrapper.find('LinkValue');
expect(linkValues.length).toBe(1);
expect(linkValues.prop('href')).toBe(links[0].url);
expect(linkValues.prop('title')).toBe(links[0].text);
});
});
});
66 changes: 66 additions & 0 deletions packages/jaeger-ui/src/components/common/ExternalLinks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) 2019 The Jaeger Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { Dropdown, Menu } from 'antd';
import * as React from 'react';
import { Link } from '../../types/trace';
import NewWindowIcon from './NewWindowIcon';

type ExternalLinksProps = {
links: Link[];
};

const LinkValue = (props: {
href: string;
title?: string;
children?: React.ReactNode;
className?: string;
}) => (
<a
href={props.href}
title={props.title}
target="_blank"
rel="noopener noreferrer"
className={props.className}
>
{props.children} <NewWindowIcon />
</a>
);

// export for testing
export const linkValueList = (links: Link[]) => (
<Menu>
{links.map(({ text, url }, index) => (
// `index` is necessary in the key because url can repeat
// eslint-disable-next-line react/no-array-index-key
<Menu.Item key={`${url}-${index}`}>
<LinkValue href={url}>{text}</LinkValue>
</Menu.Item>
))}
</Menu>
);

export default function ExternalLinks(props: ExternalLinksProps) {
const { links } = props;
if (links.length === 1) {
return <LinkValue href={links[0].url} title={links[0].text} className="TracePageHeader--back" />;
}
return (
<Dropdown overlay={linkValueList(links)} placement="bottomRight" trigger={['click']}>
<a className="TracePageHeader--back">
<NewWindowIcon isLarge />
</a>
</Dropdown>
);
}
35 changes: 35 additions & 0 deletions packages/jaeger-ui/src/model/link-patterns.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
processLinkPattern,
computeLinks,
createGetLinks,
computeTraceLink,
} from './link-patterns';

describe('processTemplate()', () => {
Expand Down Expand Up @@ -305,6 +306,40 @@ describe('getParameterInAncestor()', () => {
});
});

describe('computeTraceLink()', () => {
const linkPatterns = [
{
type: 'traces',
url: 'http://example.com/?myKey=#{traceID}',
text: 'first link (#{traceID})',
},
{
type: 'traces',
url: 'http://example.com/?myKey=#{traceID}&myKey=#{myKey}',
text: 'second link (#{myKey})',
},
].map(processLinkPattern);

const trace = {
processes: [],
traceID: 'trc1',
spans: [],
startTime: 1000,
endTime: 2000,
duration: 1000,
services: [],
};

it('correctly computes links', () => {
expect(computeTraceLink(linkPatterns, trace)).toEqual([
{
url: 'http://example.com/?myKey=trc1',
text: 'first link (trc1)',
},
]);
});
});

describe('computeLinks()', () => {
const linkPatterns = [
{
Expand Down
51 changes: 47 additions & 4 deletions packages/jaeger-ui/src/model/link-patterns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
// limitations under the License.

import _uniq from 'lodash/uniq';
import memoize from 'lru-memoize';
import { getConfigValue } from '../utils/config/get-config';
import { getParent } from './span';
import { TNil } from '../types';
import { Span, Link, KeyValuePair } from '../types/trace';
import { Span, Link, KeyValuePair, Trace } from '../types/trace';

const parameterRegExp = /#\{([^{}]*)\}/g;

Expand All @@ -35,6 +36,8 @@ type ProcessedLinkPattern = {
parameters: string[];
};

type TLinksRV = { url: string; text: string }[];

function getParamNames(str: string) {
const names = new Set<string>();
str.replace(parameterRegExp, (match, name) => {
Expand Down Expand Up @@ -139,6 +142,37 @@ function callTemplate(template: ProcessedTemplate, data: any) {
return template.template(data);
}

export function computeTraceLink(linkPatterns: ProcessedLinkPattern[], trace: Trace) {
const result: TLinksRV = [];
const validKeys = (Object.keys(trace) as (keyof Trace)[]).filter(
key => typeof trace[key] === 'string' || trace[key] === 'number'
);

linkPatterns
.filter(pattern => pattern.type('traces'))
.forEach(pattern => {
const parameterValues: Record<string, any> = {};
const allParameters = pattern.parameters.every(parameter => {
const key = parameter as keyof Trace;
if (validKeys.includes(key)) {
// At this point is safe to access to trace object using parameter variable because
// we validated parameter against validKeys, this implies that parameter a keyof Trace.
parameterValues[parameter] = trace[key];
return true;
}
return false;
});
if (allParameters) {
result.push({
url: callTemplate(pattern.url, parameterValues),
text: callTemplate(pattern.text, parameterValues),
});
}
});

return result;
}

export function computeLinks(
linkPatterns: ProcessedLinkPattern[],
span: Span,
Expand Down Expand Up @@ -203,7 +237,16 @@ export function createGetLinks(linkPatterns: ProcessedLinkPattern[], cache: Weak
};
}

export default createGetLinks(
(getConfigValue('linkPatterns') || []).map(processLinkPattern).filter(Boolean),
new WeakMap()
const processedLinks: ProcessedLinkPattern[] = (getConfigValue('linkPatterns') || [])
.map(processLinkPattern)
.filter(Boolean);

export const getTraceLinks: (trace: Trace | undefined) => TLinksRV = memoize(10)(
(trace: Trace | undefined) => {
const result: TLinksRV = [];
if (!trace) return result;
return computeTraceLink(processedLinks, trace);
}
);

export default createGetLinks(processedLinks, new WeakMap());
8 changes: 8 additions & 0 deletions packages/jaeger-ui/src/types/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ export type TScript = {
type: 'inline';
};

export type LinkPatternsConfig = {
type: 'process' | 'tags' | 'logs' | 'traces';
key?: string;
url: string;
text: string;
};

export type Config = {
archiveEnabled?: boolean;
dependencies?: { dagMaxServicesLen?: number; menuEnabled?: boolean };
Expand All @@ -41,4 +48,5 @@ export type Config = {
gaID: string | TNil;
trackErrors: boolean | TNil;
};
linkPatterns?: LinkPatternsConfig;
};

0 comments on commit 5f2a05d

Please sign in to comment.