diff --git a/packages/jaeger-ui/public/index.html b/packages/jaeger-ui/public/index.html index 9a0f706b81..979216fda0 100644 --- a/packages/jaeger-ui/public/index.html +++ b/packages/jaeger-ui/public/index.html @@ -35,6 +35,26 @@ return JAEGER_VERSION; } +
diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index 0283e4179e..eb42982308 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -76,7 +76,7 @@ function getJSON(url, options = {}) { }); } -export const DEFAULT_API_ROOT = prefixUrl('/api/'); +export const DEFAULT_API_ROOT = window.VS_CODE_SETTINGS.apiBaseUrl ? `${window.VS_CODE_SETTINGS.apiBaseUrl}/api/` : prefixUrl('/api/'); export const ANALYTICS_ROOT = prefixUrl('/analytics/'); export const DEFAULT_DEPENDENCY_LOOKBACK = moment.duration(1, 'weeks').asMilliseconds(); diff --git a/packages/jaeger-ui/src/components/App/index.js b/packages/jaeger-ui/src/components/App/index.js index 32da62c2fa..9fecf249c3 100644 --- a/packages/jaeger-ui/src/components/App/index.js +++ b/packages/jaeger-ui/src/components/App/index.js @@ -54,6 +54,13 @@ export default class JaegerUIApp extends Component { } render() { + // Navigate to URL provided by VS Code + if (window.VS_CODE_SETTINGS.startPath) { + const urlToNavigate = window.VS_CODE_SETTINGS.startPath; + window.VS_CODE_SETTINGS.startPath = ""; + history.push(urlToNavigate); + } + return ( diff --git a/packages/jaeger-ui/src/components/Monitor/EmptyState/index.tsx b/packages/jaeger-ui/src/components/Monitor/EmptyState/index.tsx index 46b6e81a0e..adc6af1087 100644 --- a/packages/jaeger-ui/src/components/Monitor/EmptyState/index.tsx +++ b/packages/jaeger-ui/src/components/Monitor/EmptyState/index.tsx @@ -29,6 +29,8 @@ export default class MonitorATMEmptyState extends React.PureComponent { } render() { + const monitorImgUrl = window.VS_CODE_SETTINGS.staticPath ? new URL(monitorImg, window.VS_CODE_SETTINGS.staticPath).href : monitorImg; + return ( @@ -36,7 +38,7 @@ export default class MonitorATMEmptyState extends React.PureComponent { jaeger-monitor-tab-preview diff --git a/packages/jaeger-ui/src/components/SearchTracePage/index.js b/packages/jaeger-ui/src/components/SearchTracePage/index.js index df2c1811e5..cb34f1b90a 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/index.js +++ b/packages/jaeger-ui/src/components/SearchTracePage/index.js @@ -41,6 +41,29 @@ import JaegerLogo from '../../img/jaeger-logo.svg'; const TabPane = Tabs.TabPane; +// Sanitize query params to filter out ones provided by VS Code +const sanitizeQueryParams = (params) => { + const VS_CODE_PARAMS = [ + "id", + "origin", + "swVersion", + "extensionId", + "platform", + "vscode-resource-base-authority", + "parentOrigin" + ]; + + const filteredParams = {}; + + Object.keys(params).forEach((key) => { + if (!VS_CODE_PARAMS.includes(key)) { + filteredParams[key] = params[key] + } + }) + + return filteredParams; +}; + // export for tests export class SearchTracePageImpl extends Component { componentDidMount() { @@ -97,6 +120,7 @@ export class SearchTracePageImpl extends Component { const hasTraceResults = traceResults && traceResults.length > 0; const showErrors = errors && !loadingTraces; const showLogo = isHomepage && !hasTraceResults && !loadingTraces && !errors; + const logoUrl = window.VS_CODE_SETTINGS.staticPath ? new URL(JaegerLogo, window.VS_CODE_SETTINGS.staticPath).href : JaegerLogo; return ( {!embedded && ( @@ -147,7 +171,7 @@ export class SearchTracePageImpl extends Component { presentation )} @@ -241,6 +265,7 @@ const stateServicesXformer = memoizeOne(stateServices => { export function mapStateToProps(state) { const { embedded, router, services: stServices, traceDiff } = state; const query = getUrlState(router.location.search); + const sanitizedQuery = sanitizeQueryParams(query); const isHomepage = !Object.keys(query).length; const { query: queryOfResults, traces, maxDuration, traceError, loadingTraces } = stateTraceXformer( state.trace @@ -268,7 +293,7 @@ export function mapStateToProps(state) { errors: errors.length ? errors : null, maxTraceDuration: maxDuration, sortTracesBy: sortBy, - urlQueryParams: Object.keys(query).length > 0 ? query : null, + urlQueryParams: Object.keys(sanitizedQuery).length > 0 ? sanitizedQuery : null, }; } diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css index 0fef447ef0..d9fecb971b 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css @@ -81,7 +81,7 @@ limitations under the License. outline: none; overflow: hidden; padding-left: 4px; - padding-right: 0.25em; + padding-right: 40px; position: relative; text-overflow: ellipsis; } @@ -123,6 +123,20 @@ limitations under the License. color: #000; } +.importance-icon { + position: absolute; + height: 60%; + top: 20%; + right: 25px; +} + +.code-location-icon { + position: absolute; + height: 70%; + top: 15%; + right: 5px; +} + .span-svc-name { padding: 0 0.25rem 0 0.5rem; font-size: 1.05em; diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.tsx index 199fd0abf3..f666e9fb6d 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanBarRow.tsx @@ -27,8 +27,16 @@ import Ticks from './Ticks'; import { TNil } from '../../../types'; import { Span } from '../../../types/trace'; +import codeIcon from '../../../img/code.svg'; +import exclamationMarkIcon from '../../../img/exclamation-mark.svg'; + import './SpanBarRow.css'; +type SpanInfo = { + hasResolvedLocation: boolean, + importance?: number +} + type SpanBarRowProps = { className?: string; color: string; @@ -61,6 +69,11 @@ type SpanBarRowProps = { focusSpan: (spanID: string) => void; }; +type SpanBarRowState = { + hasResolvedLocation: boolean; + importance?: number; +} + /** * This was originally a stateless function, but changing to a PureComponent * reduced the render time of expanding a span row detail by ~50%. This is @@ -69,7 +82,16 @@ type SpanBarRowProps = { * handlers to the onClick props. E.g. for now, the PureComponent is more * performance than the stateless function. */ -export default class SpanBarRow extends React.PureComponent { +export default class SpanBarRow extends React.PureComponent { + constructor(props: SpanBarRowProps) { + super(props); + const span = window.spansWithResolvedLocation[props.span.spanID] + this.state = { + hasResolvedLocation: Boolean(span) && span.hasResolvedLocation, + importance: span && span.importance, + } + } + static defaultProps = { className: '', rpc: null, @@ -83,6 +105,36 @@ export default class SpanBarRow extends React.PureComponent { this.props.onChildrenToggled(this.props.span.spanID); }; + updateResolvedLocation = (e:{ data: { command: string, data: Record }}) => { + const message = e.data; + if (message.command === "setSpansWithResolvedLocation") { + const span = message.data[this.props.span.spanID]; + this.setState({ + hasResolvedLocation: span.hasResolvedLocation, + importance: span.importance + }); + } + } + + componentDidMount(): void { + window.addEventListener('message', this.updateResolvedLocation); + } + + componentWillUnmount(): void { + window.removeEventListener('message', this.updateResolvedLocation); + } + + getImportanceAltText(importance?: number): string { + switch (importance) { + case 1: + return "Showstopper"; + case 2: + return "Critical"; + default: + return ""; + } + } + render() { const { className, @@ -122,6 +174,9 @@ export default class SpanBarRow extends React.PureComponent { hintSide = 'right'; } + const codeIconUrl = window.VS_CODE_SETTINGS.staticPath ? new URL(codeIcon, window.VS_CODE_SETTINGS.staticPath).href : codeIcon; + const exclamationMarkIconUrl = window.VS_CODE_SETTINGS.staticPath ? new URL(exclamationMarkIcon, window.VS_CODE_SETTINGS.staticPath).href : exclamationMarkIcon; + return ( { )} {rpc ? rpc.operationName : operationName} + { + typeof this.state.importance === "number" && [1,2].includes(this.state.importance) && this.state.hasResolvedLocation && + {this.getImportanceAltText(this.state.importance)} + } + {this.state.hasResolvedLocation && } {span.references && span.references.length > 1 && ( void; }; -export default function SpanDetail(props: SpanDetailProps) { - const { - detailState, - linksGetter, - logItemToggle, - logsToggle, - processToggle, - span, - tagsToggle, - traceStartTime, - warningsToggle, - referencesToggle, - focusSpan, - } = props; - const { isTagsOpen, isProcessOpen, logs: logsState, isWarningsOpen, isReferencesOpen } = detailState; - const { - operationName, - process, - duration, - relativeStartTime, - spanID, - logs, - tags, - warnings, - references, - } = span; - const overviewItems = [ - { - key: 'svc', - label: 'Service:', - value: process.serviceName, - }, - { - key: 'duration', - label: 'Duration:', - value: formatDuration(duration), - }, - { - key: 'start', - label: 'Start Time:', - value: formatDuration(relativeStartTime), - }, - ]; - const deepLinkCopyText = `${window.location.origin}${window.location.pathname}?uiFind=${spanID}`; +type SpanInfo = { + hasResolvedLocation: boolean, + importance?: number +} - return ( -
-
-

{operationName}

- -
- +type SpanDetailState = { + hasResolvedLocation: boolean; +} + +export default class SpanDetail extends React.Component { + constructor(props: SpanDetailProps) { + super(props); + const span = window.spansWithResolvedLocation[props.span.spanID] + this.state = { + hasResolvedLocation: Boolean(span) && span.hasResolvedLocation + } + } + + updateResolvedLocation = (e:{ data: { command: string, data: Record }}) => { + const message = e.data; + if (message.command === "setSpansWithResolvedLocation") { + this.setState({ hasResolvedLocation: message.data[this.props.span.spanID].hasResolvedLocation }) + } + } + + componentDidMount(): void { + window.addEventListener('message', this.updateResolvedLocation); + } + + componentWillUnmount(): void { + window.removeEventListener('message', this.updateResolvedLocation); + } + + render() { + const { + detailState, + linksGetter, + logItemToggle, + logsToggle, + processToggle, + span, + tagsToggle, + traceStartTime, + warningsToggle, + referencesToggle, + focusSpan, + } = this.props; + const { isTagsOpen, isProcessOpen, logs: logsState, isWarningsOpen, isReferencesOpen } = detailState; + const { + operationName, + process, + duration, + relativeStartTime, + spanID, + logs, + tags, + warnings, + references, + } = span; + const overviewItems = [ + { + key: 'svc', + label: 'Service:', + value: process.serviceName, + }, + { + key: 'duration', + label: 'Duration:', + value: formatDuration(duration), + }, + { + key: 'start', + label: 'Start Time:', + value: formatDuration(relativeStartTime), + }, + ]; + const deepLinkCopyText = `${window.location.origin}${window.location.pathname}?uiFind=${spanID}`; + + const handleGoToCodeLinkClick = (e: React.MouseEvent) => { + e.preventDefault(); + const tag = span.tags.find((tag: any) => tag.key === "otel.library.name"); + if (tag && window.vscode) { + window.vscode.postMessage({ + command: "goToSpanLocation", + data: { + name: span.operationName, + instrumentationLibrary: tag && tag.value + } + }); + } + } + + return (
-
- tagsToggle(spanID)} +
+ {this.state.hasResolvedLocation ? + + {operationName} + :

{operationName}

+ } + - {process.tags && ( +
+ +
+
tagsToggle(spanID)} + /> + {process.tags && ( + processToggle(spanID)} + /> + )} +
+ {logs && logs.length > 0 && ( + processToggle(spanID)} + logs={logs} + isOpen={logsState.isOpen} + openedItems={logsState.openedItems} + onToggle={() => logsToggle(spanID)} + onItemToggle={logItem => logItemToggle(spanID, logItem)} + timestamp={traceStartTime} /> )} -
- {logs && logs.length > 0 && ( - logsToggle(spanID)} - onItemToggle={logItem => logItemToggle(spanID, logItem)} - timestamp={traceStartTime} - /> - )} - {warnings && warnings.length > 0 && ( - Warnings} - data={warnings} - isOpen={isWarningsOpen} - onToggle={() => warningsToggle(spanID)} - /> - )} - {references && - references.length > 0 && - (references.length > 1 || references[0].refType !== 'CHILD_OF') && ( - referencesToggle(spanID)} - focusSpan={focusSpan} + {warnings && warnings.length > 0 && ( + Warnings} + data={warnings} + isOpen={isWarningsOpen} + onToggle={() => warningsToggle(spanID)} /> )} - - {spanID} - - + {references && + references.length > 0 && + (references.length > 1 || references[0].refType !== 'CHILD_OF') && ( + referencesToggle(spanID)} + focusSpan={focusSpan} + /> + )} + + {spanID} + + +
-
- ); + ); + } } diff --git a/packages/jaeger-ui/src/components/TracePage/index.tsx b/packages/jaeger-ui/src/components/TracePage/index.tsx index 2101bf4be3..7e2071526d 100644 --- a/packages/jaeger-ui/src/components/TracePage/index.tsx +++ b/packages/jaeger-ui/src/components/TracePage/index.tsx @@ -187,6 +187,28 @@ export class TracePageImpl extends React.PureComponent { componentDidUpdate({ id: prevID }: TProps) { const { id, trace } = this.props; + // Get all the trace spans and send it to VS Code extension + // to verify if they have resolved location + if ( + window.vscode && + trace && + trace.data && + trace.state && + trace.state === fetchedState.DONE + ) { + window.vscode.postMessage({ + command: "getTraceSpansLocations", + data: trace.data.spans.map(span => { + const tag = span.tags.find(tag => tag.key === "otel.library.name"); + + return { + id: span.spanID, + name: span.operationName, + instrumentationLibrary: tag && tag.value + }}).filter(span => span.instrumentationLibrary) + }); + } + this._scrollManager.setTrace(trace && trace.data); this.setHeaderHeight(this._headerElm); diff --git a/packages/jaeger-ui/src/img/code.svg b/packages/jaeger-ui/src/img/code.svg new file mode 100644 index 0000000000..657d3b5bd9 --- /dev/null +++ b/packages/jaeger-ui/src/img/code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/jaeger-ui/src/img/exclamation-mark.svg b/packages/jaeger-ui/src/img/exclamation-mark.svg new file mode 100644 index 0000000000..3c258ff749 --- /dev/null +++ b/packages/jaeger-ui/src/img/exclamation-mark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/jaeger-ui/src/reducers/embedded.tsx b/packages/jaeger-ui/src/reducers/embedded.tsx index 7c1d12280d..45054eb30b 100644 --- a/packages/jaeger-ui/src/reducers/embedded.tsx +++ b/packages/jaeger-ui/src/reducers/embedded.tsx @@ -15,11 +15,18 @@ import _get from 'lodash/get'; import { EmbeddedState } from '../types/embedded'; -import { getEmbeddedState } from '../utils/embedded-url'; +import { getEmbeddedState, VERSION_0 } from '../utils/embedded-url'; export default function embeddedConfig(state: EmbeddedState | undefined) { if (state === undefined) { - const search = _get(window, 'location.search'); + let search = _get(window, 'location.search'); + + let params = new URLSearchParams(search); + if (window.VS_CODE_SETTINGS.embeddedMode && !params.get("uiEmbed")) { + params.set("uiEmbed", VERSION_0); + search = params.toString(); + } + return search ? getEmbeddedState(search) : null; } return state; diff --git a/packages/jaeger-ui/typings/custom.d.ts b/packages/jaeger-ui/typings/custom.d.ts index f8bd4fe180..ff2f66864e 100644 --- a/packages/jaeger-ui/typings/custom.d.ts +++ b/packages/jaeger-ui/typings/custom.d.ts @@ -23,6 +23,14 @@ declare interface Window { // For getting ui config getJaegerUiConfig?: () => Record; getJaegerVersion?: () => Record; + vscode?: Record; + spansWithResolvedLocation: Record; + VS_CODE_SETTINGS: { + apiBaseUrl: string; + startPath: string; + staticPath: string; + embeddedMode: boolean; + } } // For inlined envvars @@ -47,3 +55,4 @@ declare module 'json-markup'; declare module 'react-vis-force'; declare module 'tween-functions'; declare module '*.png' { export default '' as string; } +declare module "*.svg" { export default '' as string; }