update ui, add themes,languages,mobx etc

pull/43/head
sunface 5 years ago
parent 1d1fc6d06e
commit 2bfb0741e0

@ -1 +1 @@
10 13

@ -17,7 +17,6 @@
}, },
"devDependencies": { "devDependencies": {
"@types/match-sorter": "^2.3.0", "@types/match-sorter": "^2.3.0",
"@types/react-window": "^1.8.0",
"@typescript-eslint/eslint-plugin": "1.12.0", "@typescript-eslint/eslint-plugin": "1.12.0",
"@typescript-eslint/parser": "1.12.0", "@typescript-eslint/parser": "1.12.0",
"@typescript-eslint/typescript-estree": "1.12.0", "@typescript-eslint/typescript-estree": "1.12.0",
@ -34,7 +33,6 @@
"eslint-plugin-jsx-a11y": "6.2.1", "eslint-plugin-jsx-a11y": "6.2.1",
"eslint-plugin-react": "7.12.4", "eslint-plugin-react": "7.12.4",
"http-proxy-middleware": "^0.19.1", "http-proxy-middleware": "^0.19.1",
"husky": "1.3.1",
"jsdom": "13.2.0", "jsdom": "13.2.0",
"npm-run-all": "4.1.5", "npm-run-all": "4.1.5",
"prettier": "1.18.2", "prettier": "1.18.2",
@ -45,82 +43,63 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.1.0", "@ant-design/icons": "^4.1.0",
"@types/classnames": "^2.2.7",
"@types/deep-freeze": "^0.1.1", "@types/deep-freeze": "^0.1.1",
"@types/history": "^4.7.2",
"@types/js-cookie": "^2.2.6", "@types/js-cookie": "^2.2.6",
"@types/lodash": "^4.14.123", "@types/lodash": "^4.14.123",
"@types/memoize-one": "4.1.1", "@types/memoize-one": "4.1.1",
"@types/moment": "^2.13.0", "@types/moment": "^2.13.0",
"@types/object-hash": "^1.3.0",
"@types/react-copy-to-clipboard": "^4.2.6", "@types/react-copy-to-clipboard": "^4.2.6",
"@types/react-grid-layout": "^0.17.1",
"@types/react-icons": "2.2.7",
"@types/react-redux": "^5.0.6",
"@types/react-router-dom": "^5.1.4", "@types/react-router-dom": "^5.1.4",
"@types/react-virtualized-select": "^3.0.7",
"@types/recompose": "^0.30.5",
"@types/redux-actions": "2.2.1",
"antd": "^4.3.0", "antd": "^4.3.0",
"antd-theme-generator": "^1.2.3", "antd-theme-generator": "^1.2.3",
"antd-theme-webpack-plugin": "^1.3.4", "antd-theme-webpack-plugin": "^1.3.4",
"apm-plexus": "^0.2.0",
"axios": "^0.19.2", "axios": "^0.19.2",
"babel-plugin-import": "1.13.0", "babel-plugin-import": "1.13.0",
"classnames": "^2.2.5",
"combokeys": "^3.0.0", "combokeys": "^3.0.0",
"copy-to-clipboard": "^3.1.0", "copy-to-clipboard": "^3.1.0",
"customize-cra": "^0.9.1", "customize-cra": "^0.9.1",
"deep-freeze": "^0.0.1", "deep-freeze": "^0.0.1",
"drange": "^2.0.0",
"fuzzy": "^0.1.3",
"global": "^4.3.2", "global": "^4.3.2",
"history": "^4.6.3",
"is-promise": "^2.1.0",
"isomorphic-fetch": "^2.2.1",
"js-cookie": "^2.2.1", "js-cookie": "^2.2.1",
"json-markup": "^1.1.0",
"less": "^3.11.1", "less": "^3.11.1",
"less-loader": "^5.0.0", "less-loader": "^5.0.0",
"less-vars-to-js": "^1.2.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"lru-memoize": "^1.1.0",
"match-sorter": "^3.1.1",
"memoize-one": "^5.0.0", "memoize-one": "^5.0.0",
"mobx": "^5.15.4", "mobx": "^5.15.4",
"mobx-react": "^6.2.2", "mobx-react": "^6.2.2",
"moment": "^2.18.1", "moment": "^2.18.1",
"prop-types": "^15.5.10",
"query-string": "^6.3.0", "query-string": "^6.3.0",
"raven-js": "^3.22.1",
"react": "^16.3.2", "react": "^16.3.2",
"react-app-rewired": "^2.1.5", "react-app-rewired": "^2.1.5",
"react-circular-progressbar": "^2.0.3",
"react-color": "^2.14.1",
"react-dimensions": "^1.3.0",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-ga": "^2.4.1",
"react-grid-layout": "^0.18.3",
"react-helmet": "^5.1.3",
"react-icons": "2.2.7",
"react-intl": "^3.9.3", "react-intl": "^3.9.3",
"react-redux": "^5.0.6",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-router-redux": "5.0.0-alpha.6",
"react-scripts": "3.4.1", "react-scripts": "3.4.1",
"react-virtualized-select": "^3.1.0",
"react-vis": "^1.7.2",
"react-vis-force": "^0.3.1",
"react-window": "^1.8.3",
"recompose": "^0.25.0",
"redux": "^3.7.2",
"redux-actions": "^2.2.1",
"redux-form": "^7.0.3",
"redux-promise-middleware": "^4.3.0",
"reselect": "^3.0.1",
"store": "^2.0.12",
"ts-key-enum": "^2.0.0",
"tween-functions": "^1.2.0",
"typescript": "3.5.3", "typescript": "3.5.3",
"u-basscss": "2.0.0" "u-basscss": "2.0.0"
}, },

@ -26,7 +26,7 @@ import { Provider } from 'mobx-react'
// eslint-disable-next-line import/order, import/no-unresolved // eslint-disable-next-line import/order, import/no-unresolved
import UIApp from './pages/App'; import UIApp from './pages/App';
import './styles/main.less' import './styles/main.less'
import 'react-grid-layout/css/styles.css'
// these need to go after the App import // these need to go after the App import
/* eslint-disable import/first */ /* eslint-disable import/first */

@ -13,10 +13,7 @@
// limitations under the License. // limitations under the License.
import React from 'react'; import React from 'react';
// import { Link } from 'react-router-dom';
import ErrorMessage from '../Trace/components/common/ErrorMessage';
// import prefixUrl from '../../utils/prefix-url';
type NotFoundProps = { type NotFoundProps = {
error: any; error: any;
@ -26,8 +23,6 @@ export default function NotFound({ error }: NotFoundProps) {
return ( return (
<section className="ub-m3"> <section className="ub-m3">
<h1>Error</h1> <h1>Error</h1>
{error && <ErrorMessage error={error} />}
{/* <Link to={prefixUrl('/')}>Back home</Link> */}
</section> </section>
); );
} }

@ -13,50 +13,27 @@
// limitations under the License. // limitations under the License.
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Provider } from 'react-redux'; import { Route, Switch, BrowserRouter as Router } from 'react-router-dom';
import { Route, Switch,BrowserRouter as Router } from 'react-router-dom';
import { ConnectedRouter } from 'react-router-redux';
import NotFound from './NotFound'; import NotFound from './NotFound';
// eslint-disable-next-line import/order, import/no-unresolved
import TracePage from '../Trace/components/TracePage';
// eslint-disable-next-line import/order, import/no-unresolved
import Login from '../Login' import Login from '../Login'
import { ROUTE_PATH as tracePath } from '../Trace/components/TracePage/url';
import configureStore from '../Trace/utils/configure-store';
import processScripts from '../Trace/utils/config/process-scripts';
import './index.css'; import './index.css';
import Layouts from '../../layouts/index.jsx' import Layouts from '../../layouts/index.jsx'
const history = require("history").createBrowserHistory()
export default class UIApp extends Component { export default class UIApp extends Component {
constructor(props) {
super(props);
this.store = configureStore(history);
processScripts();
}
render() { render() {
return ( return (
<Provider store={this.store}> <Router>
<ConnectedRouter history={history}> <Switch>
<Router> <Route path="/ui" component={Layouts} />
<Switch> <Route path="/login" exact component={Login} />
<Route path={tracePath} component={TracePage} /> <Route component={NotFound} />
<Route path="/ui" component={Layouts} /> </Switch>
<Route path="/login" exact component={Login} /> </Router>
<Route component={NotFound} />
</Switch>
</Router>
</ConnectedRouter>
</Provider>
); );
} }
} }

@ -3,7 +3,6 @@ import './index.less';
import { Card } from 'antd'; import { Card } from 'antd';
import { ISystem } from '../../store/system' import { ISystem } from '../../store/system'
import { inject, observer } from 'mobx-react' import { inject, observer } from 'mobx-react'
import GridLayout from 'react-grid-layout';
const { Meta } = Card; const { Meta } = Card;
@ -13,44 +12,9 @@ function Index(props: {system:ISystem}) {
},[system.startDate,system.endDate]) },[system.startDate,system.endDate])
const layout = [
{i: 'a', x: 0, y: 0, w: 3, h: 3},
{i: 'b', x: 3, y: 0, w: 3, h: 3},
{i: 'c', x: 6, y: 0, w: 3, h: 3}
];
return (
<div className="test">
<GridLayout className="layout" layout={layout} cols={12} width={1200}>
<div key="a">
<Card
hoverable
style={{ width: '100%',height:'100%' }}
// cover={<img alt="example" src="https://os.alipayobjects.com/rmsportal/QBnOOoLaAfKPirc.png" />}
>
<Meta title="Europe Street beat" description="www.instagram.com" />
</Card>
</div>
<div key="b">
<Card
hoverable
style={{ width: '100%',height:'100%' }}
// cover={<img alt="example" src="https://os.alipayobjects.com/rmsportal/QBnOOoLaAfKPirc.png" />}
>
<Meta title="Europe Street beat" description="www.instagram.com" />
</Card>
</div>
<div key="c">
<Card
hoverable
style={{ width: '100%',height:'100%' }}
// cover={<img alt="example" src="https://os.alipayobjects.com/rmsportal/QBnOOoLaAfKPirc.png" />}
>
<Meta title="Europe Street beat" description="www.instagram.com" />
</Card>
</div>
</GridLayout>
</div> return (
<div />
); );
} }

@ -1,30 +0,0 @@
// Copyright (c) 2019 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as fileReaderActions from './file-reader-api';
import readJsonFile from '../utils/readJsonFile';
jest.mock('../utils/readJsonFile');
describe('actions/file-reader-api', () => {
beforeEach(() => {
readJsonFile.mockReset();
});
it('loadJsonTraces calls readJsonFile', () => {
const arg = 'example-arg';
fileReaderActions.loadJsonTraces(arg);
expect(readJsonFile.mock.calls).toEqual([[arg]]);
});
});

@ -1,24 +0,0 @@
// Copyright (c) 2019 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { createAction } from 'redux-actions';
import readJsonFile from '../utils/readJsonFile';
// eslint-disable-next-line import/prefer-default-export
export const loadJsonTraces = createAction(
'@FILE_READER_API/LOAD_JSON',
fileList => readJsonFile(fileList),
fileList => ({ fileList })
);

@ -1,64 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { createAction } from 'redux-actions';
import JaegerAPI from '../api/jaeger';
export const fetchTrace = createAction(
'@JAEGER_API/FETCH_TRACE',
id => JaegerAPI.fetchTrace(id),
id => ({ id })
);
export const fetchMultipleTraces = createAction(
'@JAEGER_API/FETCH_MULTIPLE_TRACES',
ids => JaegerAPI.searchTraces({ traceID: ids }),
ids => ({ ids })
);
export const archiveTrace = createAction(
'@JAEGER_API/ARCHIVE_TRACE',
id => JaegerAPI.archiveTrace(id),
id => ({ id })
);
export const searchTraces = createAction(
'@JAEGER_API/SEARCH_TRACES',
query => JaegerAPI.searchTraces(query),
query => ({ query })
);
export const fetchServices = createAction('@JAEGER_API/FETCH_SERVICES', () => JaegerAPI.fetchServices());
export const fetchServiceOperations = createAction(
'@JAEGER_API/FETCH_SERVICE_OPERATIONS',
serviceName => JaegerAPI.fetchServiceOperations(serviceName),
serviceName => ({ serviceName })
);
export const fetchServiceServerOps = createAction(
'@JAEGER_API/FETCH_SERVICE_SERVER_OP',
serviceName => JaegerAPI.fetchServiceServerOps(serviceName),
serviceName => ({ serviceName })
);
export const fetchDeepDependencyGraph = createAction(
'@JAEGER_API/FETCH_DEEP_DEPENDENCY_GRAPH',
query => JaegerAPI.fetchDeepDependencyGraph(query),
query => ({ query })
);
export const fetchDependencies = createAction('@JAEGER_API/FETCH_DEPENDENCIES', () =>
JaegerAPI.fetchDependencies()
);

@ -1,152 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/* eslint-disable import/first */
jest.mock('node-fetch', () => () =>
Promise.resolve({
status: 200,
data: () => Promise.resolve({ data: null }),
json: () => Promise.resolve({ data: null }),
})
);
import sinon from 'sinon';
import isPromise from 'is-promise';
import * as jaegerApiActions from './jaeger-api';
import JaegerAPI from '../api/jaeger';
describe('actions/jaeger-api', () => {
const query = { param: 'value' };
const id = 'my-trace-id';
const ids = [id, id];
let mock;
beforeEach(() => {
mock = sinon.mock(JaegerAPI);
});
afterEach(() => {
mock.restore();
});
it('@JAEGER_API/FETCH_TRACE should fetch the trace by id', () => {
mock.expects('fetchTrace').withExactArgs(id);
jaegerApiActions.fetchTrace(id);
expect(() => mock.verify()).not.toThrow();
});
it('@JAEGER_API/FETCH_TRACE should return the promise', () => {
const { payload } = jaegerApiActions.fetchTrace(id);
expect(isPromise(payload)).toBeTruthy();
});
it('@JAEGER_API/FETCH_TRACE should attach the id as meta', () => {
const { meta } = jaegerApiActions.fetchTrace(id);
expect(meta.id).toBe(id);
});
it('@JAEGER_API/FETCH_MULTIPLE_TRACES should fetch traces by ids', () => {
mock.expects('searchTraces').withExactArgs(sinon.match.has('traceID', ids));
jaegerApiActions.fetchMultipleTraces(ids);
expect(() => mock.verify()).not.toThrow();
});
it('@JAEGER_API/FETCH_MULTIPLE_TRACES should return the promise', () => {
const { payload } = jaegerApiActions.fetchMultipleTraces(ids);
expect(isPromise(payload)).toBeTruthy();
});
it('@JAEGER_API/FETCH_MULTIPLE_TRACES should attach the ids as meta', () => {
const { meta } = jaegerApiActions.fetchMultipleTraces(ids);
expect(meta.ids).toBe(ids);
});
it('@JAEGER_API/ARCHIVE_TRACE should archive the trace by id', () => {
mock.expects('archiveTrace').withExactArgs(id);
jaegerApiActions.archiveTrace(id);
expect(() => mock.verify()).not.toThrow();
});
it('@JAEGER_API/ARCHIVE_TRACE should return the promise', () => {
const { payload } = jaegerApiActions.archiveTrace(id);
expect(isPromise(payload)).toBeTruthy();
});
it('@JAEGER_API/ARCHIVE_TRACE should attach the id as meta', () => {
const { meta } = jaegerApiActions.archiveTrace(id);
expect(meta.id).toBe(id);
});
it('@JAEGER_API/SEARCH_TRACES should fetch the trace by id', () => {
mock.expects('searchTraces').withExactArgs(query);
jaegerApiActions.searchTraces(query);
expect(() => mock.verify()).not.toThrow();
});
it('@JAEGER_API/SEARCH_TRACES should return the promise', () => {
const { payload } = jaegerApiActions.searchTraces(query);
expect(isPromise(payload)).toBeTruthy();
});
it('@JAEGER_API/SEARCH_TRACES should attach the query as meta', () => {
const { meta } = jaegerApiActions.searchTraces(query);
expect(meta.query).toEqual(query);
});
it('@JAEGER_API/FETCH_SERVICES should return a promise', () => {
const { payload } = jaegerApiActions.fetchServices();
expect(isPromise(payload)).toBeTruthy();
});
it('@JAEGER_API/FETCH_SERVICE_OPERATIONS should call the JaegerAPI', () => {
const called = mock
.expects('fetchServiceOperations')
.once()
.withExactArgs('service');
jaegerApiActions.fetchServiceOperations('service');
expect(called.verify()).toBeTruthy();
});
it('@JAEGER_API/FETCH_SERVICE_SERVER_OP should call the JaegerAPI', () => {
const called = mock
.expects('fetchServiceServerOps')
.once()
.withExactArgs('service');
jaegerApiActions.fetchServiceServerOps('service');
expect(called.verify()).toBeTruthy();
});
it('@JAEGER_API/FETCH_DEEP_DEPENDENCY_GRAPH should fetch the graph by params', () => {
mock.expects('fetchDeepDependencyGraph').withExactArgs(query);
jaegerApiActions.fetchDeepDependencyGraph(query);
expect(() => mock.verify()).not.toThrow();
});
it('@JAEGER_API/FETCH_DEEP_DEPENDENCY_GRAPH should return the promise', () => {
const { payload } = jaegerApiActions.fetchDeepDependencyGraph(query);
expect(isPromise(payload)).toBeTruthy();
});
it('@JAEGER_API/FETCH_DEEP_DEPENDENCY_GRAPH should attach the query as meta', () => {
const { meta } = jaegerApiActions.fetchDeepDependencyGraph(query);
expect(meta.query).toEqual(query);
});
it('@JAEGER_API/FETCH_DEPENDENCIES should call the JaegerAPI', () => {
const called = mock.expects('fetchDependencies').once();
jaegerApiActions.fetchDependencies();
expect(called.verify()).toBeTruthy();
});
});

@ -1,111 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import fetch from 'isomorphic-fetch';
import moment from 'moment';
import queryString from 'query-string';
import prefixUrl from '../utils/prefix-url';
// export for tests
export function getMessageFromError(errData, status) {
if (errData.code != null && errData.msg != null) {
if (errData.code === status) {
return errData.msg;
}
return `${errData.code} - ${errData.msg}`;
}
try {
return JSON.stringify(errData);
} catch (_) {
return String(errData);
}
}
function getJSON(url, options = {}) {
const { query = null, ...init } = options;
init.credentials = 'same-origin';
const queryStr = query ? `?${queryString.stringify(query)}` : '';
return fetch(`${url}${queryStr}`, init).then(response => {
if (response.status < 400) {
return response.json();
}
return response.text().then(bodyText => {
let data;
let bodyTextFmt;
let errorMessage;
try {
data = JSON.parse(bodyText);
bodyTextFmt = JSON.stringify(data, null, 2);
} catch (_) {
data = null;
bodyTextFmt = null;
}
if (data && Array.isArray(data.errors) && data.errors.length) {
errorMessage = data.errors.map(err => getMessageFromError(err, response.status)).join('; ');
} else {
errorMessage = bodyText || `${response.status} - ${response.statusText}`;
}
if (typeof errorMessage === 'string') {
errorMessage = errorMessage.trim();
}
const error = new Error(`HTTP Error: ${errorMessage}`);
error.httpStatus = response.status;
error.httpStatusText = response.statusText;
error.httpBody = bodyTextFmt || bodyText;
error.httpUrl = url;
error.httpQuery = typeof query === 'string' ? query : queryString.stringify(query);
throw error;
});
});
}
export const DEFAULT_API_ROOT = prefixUrl('/api/');
export const ANALYTICS_ROOT = prefixUrl('/analytics/');
export const DEFAULT_DEPENDENCY_LOOKBACK = moment.duration(1, 'weeks').asMilliseconds();
const JaegerAPI = {
apiRoot: DEFAULT_API_ROOT,
archiveTrace(id) {
return getJSON(`${this.apiRoot}archive/${id}`, { method: 'POST' });
},
fetchQualityMetrics(service, lookback) {
return getJSON(`/qualitymetrics-v2`, { query: { service, lookback } });
},
fetchDecoration(url) {
return getJSON(url);
},
fetchDeepDependencyGraph(query) {
return getJSON(`${ANALYTICS_ROOT}v1/dependencies`, { query });
},
fetchDependencies(endTs = new Date().getTime(), lookback = DEFAULT_DEPENDENCY_LOOKBACK) {
return getJSON(`${this.apiRoot}dependencies`, { query: { endTs, lookback } });
},
fetchServiceOperations(serviceName) {
return getJSON(`${this.apiRoot}services/${encodeURIComponent(serviceName)}/operations`);
},
fetchServiceServerOps(service) {
return getJSON(`${this.apiRoot}operations`, {
query: {
service,
spanKind: 'server',
},
});
},
fetchTrace(id) {
return getJSON(`${this.apiRoot}traces/${id}`);
},
};
export default JaegerAPI;

@ -1,183 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/* eslint-disable import/first */
jest.mock('isomorphic-fetch', () =>
jest.fn(() =>
Promise.resolve({
status: 200,
data: () => Promise.resolve({ data: null }),
json: () => Promise.resolve({ data: null }),
})
)
);
import fetchMock from 'isomorphic-fetch';
import queryString from 'query-string';
import traceGenerator from '../demo/trace-generators';
import JaegerAPI, {
getMessageFromError,
DEFAULT_API_ROOT,
DEFAULT_DEPENDENCY_LOOKBACK,
ANALYTICS_ROOT,
} from './jaeger';
const defaultOptions = {
credentials: 'same-origin',
};
describe('archiveTrace', () => {
it('POSTs the specified id', () => {
JaegerAPI.archiveTrace('trace-id');
expect(fetchMock).toHaveBeenLastCalledWith(`${DEFAULT_API_ROOT}archive/trace-id`, {
...defaultOptions,
method: 'POST',
});
});
});
describe('fetchDeepDependencyGraph', () => {
it('GETs the specified query', () => {
const query = { service: 'serviceName', start: 400, end: 800 };
JaegerAPI.fetchDeepDependencyGraph(query);
expect(fetchMock).toHaveBeenLastCalledWith(
`${ANALYTICS_ROOT}v1/dependencies?${queryString.stringify(query)}`,
defaultOptions
);
});
});
describe('fetchDependencies', () => {
it('GETs the specified query', () => {
const endTs = 'end time stamp';
const lookback = 'test lookback';
JaegerAPI.fetchDependencies(endTs, lookback);
expect(fetchMock).toHaveBeenLastCalledWith(
`${DEFAULT_API_ROOT}dependencies?${queryString.stringify({ endTs, lookback })}`,
defaultOptions
);
});
it('has default query values', () => {
JaegerAPI.fetchDependencies();
expect(fetchMock).toHaveBeenLastCalledWith(
expect.stringMatching(
new RegExp(`${DEFAULT_API_ROOT}dependencies\\?endTs=\\d+&lookback=${DEFAULT_DEPENDENCY_LOOKBACK}`)
),
defaultOptions
);
});
});
describe('fetchServiceServerOps', () => {
it('GETs the specified query', () => {
const service = 'serviceName';
const query = { service, spanKind: 'server' };
JaegerAPI.fetchServiceServerOps(service);
expect(fetchMock).toHaveBeenLastCalledWith(
`${DEFAULT_API_ROOT}operations?${queryString.stringify(query)}`,
defaultOptions
);
});
});
describe('fetchTrace', () => {
const generatedTraces = traceGenerator.traces({ numberOfTraces: 5 });
it('fetchTrace() should fetch with the id', () => {
JaegerAPI.fetchTrace('trace-id');
expect(fetchMock).toHaveBeenLastCalledWith(`${DEFAULT_API_ROOT}traces/trace-id`, defaultOptions);
});
it('fetchTrace() should resolve the whole response', async () => {
fetchMock.mockReturnValue(
Promise.resolve({
status: 200,
json: () => Promise.resolve({ data: generatedTraces }),
})
);
const resp = await JaegerAPI.fetchTrace('trace-id');
expect(resp.data).toBe(generatedTraces);
});
it('fetchTrace() throws an error on a >= 400 status code', done => {
const status = 400;
const statusText = 'some-status';
const msg = 'some-message';
const errorData = { errors: [{ msg, code: status }] };
fetchMock.mockReturnValue(
Promise.resolve({
status,
statusText,
text: () => Promise.resolve(JSON.stringify(errorData)),
})
);
JaegerAPI.fetchTrace('trace-id').catch(err => {
expect(err.message).toMatch(msg);
expect(err.httpStatus).toBe(status);
expect(err.httpStatusText).toBe(statusText);
done();
});
});
it('fetchTrace() throws an useful error derived from a text payload', done => {
const status = 400;
const statusText = 'some-status';
const errorData = 'this is some error message';
fetchMock.mockReturnValue(
Promise.resolve({
status,
statusText,
text: () => Promise.resolve(errorData),
})
);
JaegerAPI.fetchTrace('trace-id').catch(err => {
expect(err.message).toMatch(errorData);
expect(err.httpStatus).toBe(status);
expect(err.httpStatusText).toBe(statusText);
done();
});
});
});
describe('getMessageFromError()', () => {
describe('{ code, msg } error data', () => {
const data = { code: 1, msg: 'some-message' };
it('ignores code if it is the same as `status` arg', () => {
expect(getMessageFromError(data, 1)).toBe(data.msg);
});
it('returns`$code - $msg` when code is novel', () => {
const rv = getMessageFromError(data, -1);
expect(rv).toBe(`${data.code} - ${data.msg}`);
});
});
describe('other data formats', () => {
it('stringifies the value, when possible', () => {
const data = ['abc'];
expect(getMessageFromError(data)).toBe(JSON.stringify(data));
});
it('returns the string, otherwise', () => {
const data = {};
data.data = data;
expect(getMessageFromError(data)).toBe(String(data));
});
});
});

@ -1,82 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { createActions, handleActions, ActionFunctionAny } from 'redux-actions';
import { archiveTrace } from '../../../actions/jaeger-api';
import { ApiError } from '../../../types/api-error';
import { TracesArchive } from '../../../types/archive';
import generateActionTypes from '../../../utils/generate-action-types';
type ArchiveAction = {
meta: {
id: string;
};
payload?: ApiError | string;
};
const initialState: TracesArchive = {};
const actionTypes = generateActionTypes('@jaeger-ui/archive-trace', ['ACKNOWLEDGE']);
const fullActions = createActions({
[actionTypes.ACKNOWLEDGE]: traceID => traceID,
});
export const actions: { [actionType: string]: ActionFunctionAny<any> } = {
...(fullActions as any).jaegerUi.archiveTrace,
archiveTrace,
};
function acknowledge(state: TracesArchive, { payload }: ArchiveAction) {
const traceID = typeof payload === 'string' ? payload : null;
if (!traceID) {
// make flow happy
throw new Error('Invalid state, missing traceID for archive acknowledge');
}
const traceArchive = state[traceID];
if (traceArchive && traceArchive.isLoading) {
// acknowledgement during loading is invalid (should not happen)
return state;
}
const next = { ...traceArchive, isAcknowledged: true };
return { ...state, [traceID]: next };
}
function archiveStarted(state: TracesArchive, { meta }: ArchiveAction) {
return { ...state, [meta.id]: { isLoading: true } };
}
function archiveDone(state: TracesArchive, { meta }: ArchiveAction) {
return { ...state, [meta.id]: { isArchived: true, isAcknowledged: false } };
}
function archiveErred(state: TracesArchive, { meta, payload }: ArchiveAction) {
if (!payload) {
// make flow happy
throw new Error('Invalid state, missing API error details');
}
const traceArchive = { error: payload, isArchived: false, isError: true, isAcknowledged: false };
return { ...state, [meta.id]: traceArchive };
}
export default handleActions(
{
[actionTypes.ACKNOWLEDGE]: acknowledge,
[`${archiveTrace}_PENDING`]: archiveStarted,
[`${archiveTrace}_FULFILLED`]: archiveDone,
[`${archiveTrace}_REJECTED`]: archiveErred,
},
initialState
);

@ -1,34 +0,0 @@
/*
Copyright (c) 2017 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.ArchiveNotifier--errorNotification {
position: absolute;
right: 0;
width: 650px;
}
.ArchiveNotifier--errorIcon,
.ArchiveNotifier--doneIcon {
font-size: 30px;
}
.ArchiveNotifier--errorIcon {
color: #c00;
}
.ArchiveNotifier--doneIcon {
color: teal;
}

@ -1,117 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as React from 'react';
import { notification } from 'antd';
import { LoadingOutlined,ClockCircleOutlined} from '@ant-design/icons';
import ErrorMessage from '../../common/ErrorMessage';
import './index.css';
const ENotifiedState = {
Progress : 'ENotifiedState.Progress',
Outcome : 'ENotifiedState.Outcome',
}
function getNextNotifiedState(props) {
const { archivedState } = props;
if (!archivedState) {
return null;
}
if (archivedState.isLoading) {
return ENotifiedState.Progress;
}
return archivedState.isAcknowledged ? null : ENotifiedState.Outcome;
}
function updateNotification(oldState,nextState, props) {
if (oldState === nextState) {
return;
}
if (oldState) {
notification.close(oldState);
}
if (nextState === ENotifiedState.Progress) {
notification.info({
key: ENotifiedState.Progress,
description: null,
duration: 0,
icon: <LoadingOutlined />,
message: 'Archiving trace...',
});
return;
}
const { acknowledge, archivedState } = props;
if (nextState === ENotifiedState.Outcome) {
if (archivedState && archivedState.error) {
const error = typeof archivedState.error === 'string' ? archivedState.error : archivedState.error;
notification.warn({
key: ENotifiedState.Outcome,
className: 'ArchiveNotifier--errorNotification',
message: <ErrorMessage.Message error={error} wrap />,
description: <ErrorMessage.Details error={error} wrap />,
duration: null,
icon: <ClockCircleOutlined className="ArchiveNotifier--errorIcon" />,
onClose: acknowledge,
});
} else if (archivedState && archivedState.isArchived) {
notification.success({
key: ENotifiedState.Outcome,
description: null,
duration: null,
icon: <ClockCircleOutlined className="ArchiveNotifier--doneIcon" />,
message: 'This trace has been archived.',
onClose: acknowledge,
});
} else {
throw new Error('Unexpected condition');
}
}
}
function processProps(notifiedState, props) {
const nxNotifiedState = getNextNotifiedState(props);
updateNotification(notifiedState, nxNotifiedState, props);
return nxNotifiedState;
}
export default class ArchiveNotifier extends React.PureComponent {
constructor(props) {
super(props);
const notifiedState = processProps(null, props);
this.state = { notifiedState };
}
componentWillReceiveProps(nextProps) {
const notifiedState = processProps(this.state.notifiedState, nextProps);
if (this.state.notifiedState !== notifiedState) {
this.setState({ notifiedState });
}
}
componentWillUnmount() {
const { notifiedState } = this.state;
if (notifiedState) {
notification.close(notifiedState);
}
}
render() {
return null;
}
}

@ -1,286 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/* eslint-disable import/first */
jest.mock('./scroll-page');
import { scrollBy, scrollTo } from './scroll-page';
import ScrollManager from './ScrollManager';
const SPAN_HEIGHT = 2;
function getTrace() {
const spans = [];
const trace = {
spans,
duration: 2000,
startTime: 1000,
};
for (let i = 0; i < 10; i++) {
spans.push({ duration: 1, startTime: 1000, spanID: i + 1 });
}
return trace;
}
function getAccessors() {
return {
getViewRange: jest.fn(() => [0, 1]),
getSearchedSpanIDs: jest.fn(),
getCollapsedChildren: jest.fn(),
getViewHeight: jest.fn(() => SPAN_HEIGHT * 2),
getBottomRowIndexVisible: jest.fn(),
getTopRowIndexVisible: jest.fn(),
getRowPosition: jest.fn(),
mapRowIndexToSpanIndex: jest.fn(n => n),
mapSpanIndexToRowIndex: jest.fn(n => n),
};
}
describe('ScrollManager', () => {
let trace;
let accessors;
let manager;
beforeEach(() => {
scrollBy.mockReset();
scrollTo.mockReset();
trace = getTrace();
accessors = getAccessors();
manager = new ScrollManager(trace, { scrollBy, scrollTo });
manager.setAccessors(accessors);
});
it('saves the accessors', () => {
const n = Math.random();
manager.setAccessors(n);
expect(manager._accessors).toBe(n);
});
describe('_scrollPast()', () => {
it('throws if accessors is not set', () => {
manager.setAccessors(null);
expect(manager._scrollPast).toThrow();
});
it('is a noop if an invalid rowPosition is returned by the accessors', () => {
// eslint-disable-next-line no-console
const oldWarn = console.warn;
// eslint-disable-next-line no-console
console.warn = () => {};
manager._scrollPast(null, null);
expect(accessors.getRowPosition.mock.calls.length).toBe(1);
expect(accessors.getViewHeight.mock.calls.length).toBe(0);
expect(scrollTo.mock.calls.length).toBe(0);
// eslint-disable-next-line no-console
console.warn = oldWarn;
});
it('scrolls up with direction is `-1`', () => {
const y = 10;
const expectTo = y - 0.5 * accessors.getViewHeight();
accessors.getRowPosition.mockReturnValue({ y, height: SPAN_HEIGHT });
manager._scrollPast(NaN, -1);
expect(scrollTo.mock.calls).toEqual([[expectTo]]);
});
it('scrolls down with direction `1`', () => {
const y = 10;
const vh = accessors.getViewHeight();
const expectTo = y + SPAN_HEIGHT - 0.5 * vh;
accessors.getRowPosition.mockReturnValue({ y, height: SPAN_HEIGHT });
manager._scrollPast(NaN, 1);
expect(scrollTo.mock.calls).toEqual([[expectTo]]);
});
});
describe('_scrollToVisibleSpan()', () => {
function getRefs(spanID) {
return [{ refType: 'CHILD_OF', spanID }];
}
let scrollPastMock;
beforeEach(() => {
scrollPastMock = jest.fn();
manager._scrollPast = scrollPastMock;
});
it('throws if accessors is not set', () => {
manager.setAccessors(null);
expect(manager._scrollToVisibleSpan).toThrow();
});
it('exits if the trace is not set', () => {
manager.setTrace(null);
manager._scrollToVisibleSpan();
expect(scrollPastMock.mock.calls.length).toBe(0);
});
it('does nothing if already at the boundary', () => {
accessors.getTopRowIndexVisible.mockReturnValue(0);
accessors.getBottomRowIndexVisible.mockReturnValue(trace.spans.length - 1);
manager._scrollToVisibleSpan(-1);
expect(scrollPastMock.mock.calls.length).toBe(0);
manager._scrollToVisibleSpan(1);
expect(scrollPastMock.mock.calls.length).toBe(0);
});
it('centers the current top or bottom span', () => {
accessors.getTopRowIndexVisible.mockReturnValue(5);
accessors.getBottomRowIndexVisible.mockReturnValue(5);
manager._scrollToVisibleSpan(-1);
expect(scrollPastMock).lastCalledWith(5, -1);
manager._scrollToVisibleSpan(1);
expect(scrollPastMock).lastCalledWith(5, 1);
});
it('skips spans that are out of view', () => {
trace.spans[4].startTime = trace.startTime + trace.duration * 0.5;
accessors.getViewRange = () => [0.4, 0.6];
accessors.getTopRowIndexVisible.mockReturnValue(trace.spans.length - 1);
accessors.getBottomRowIndexVisible.mockReturnValue(0);
manager._scrollToVisibleSpan(1);
expect(scrollPastMock).lastCalledWith(4, 1);
manager._scrollToVisibleSpan(-1);
expect(scrollPastMock).lastCalledWith(4, -1);
});
it('skips spans that do not match the text search', () => {
accessors.getTopRowIndexVisible.mockReturnValue(trace.spans.length - 1);
accessors.getBottomRowIndexVisible.mockReturnValue(0);
accessors.getSearchedSpanIDs = () => new Set([trace.spans[4].spanID]);
manager._scrollToVisibleSpan(1);
expect(scrollPastMock).lastCalledWith(4, 1);
manager._scrollToVisibleSpan(-1);
expect(scrollPastMock).lastCalledWith(4, -1);
});
it('scrolls to boundary when scrolling away from closest spanID in findMatches', () => {
const closetFindMatchesSpanID = 4;
accessors.getTopRowIndexVisible.mockReturnValue(closetFindMatchesSpanID - 1);
accessors.getBottomRowIndexVisible.mockReturnValue(closetFindMatchesSpanID + 1);
accessors.getSearchedSpanIDs = () => new Set([trace.spans[closetFindMatchesSpanID].spanID]);
manager._scrollToVisibleSpan(1);
expect(scrollPastMock).lastCalledWith(trace.spans.length - 1, 1);
manager._scrollToVisibleSpan(-1);
expect(scrollPastMock).lastCalledWith(0, -1);
});
it('scrolls to last visible row when boundary is hidden', () => {
const parentOfLastRowWithHiddenChildrenIndex = trace.spans.length - 2;
accessors.getBottomRowIndexVisible.mockReturnValue(0);
accessors.getCollapsedChildren = () =>
new Set([trace.spans[parentOfLastRowWithHiddenChildrenIndex].spanID]);
accessors.getSearchedSpanIDs = () => new Set([trace.spans[0].spanID]);
trace.spans[trace.spans.length - 1].references = getRefs(
trace.spans[parentOfLastRowWithHiddenChildrenIndex].spanID
);
manager._scrollToVisibleSpan(1);
expect(scrollPastMock).lastCalledWith(parentOfLastRowWithHiddenChildrenIndex, 1);
});
describe('scrollToNextVisibleSpan() and scrollToPrevVisibleSpan()', () => {
beforeEach(() => {
// change spans so 0 and 4 are top-level and their children are collapsed
const spans = trace.spans;
let parentID;
for (let i = 0; i < spans.length; i++) {
switch (i) {
case 0:
case 4:
parentID = spans[i].spanID;
break;
default:
spans[i].references = getRefs(parentID);
}
}
// set which spans are "in-view" and which have collapsed children
accessors.getTopRowIndexVisible.mockReturnValue(trace.spans.length - 1);
accessors.getBottomRowIndexVisible.mockReturnValue(0);
accessors.getCollapsedChildren.mockReturnValue(new Set([spans[0].spanID, spans[4].spanID]));
});
it('skips spans that are hidden because their parent is collapsed', () => {
manager.scrollToNextVisibleSpan();
expect(scrollPastMock).lastCalledWith(4, 1);
manager.scrollToPrevVisibleSpan();
expect(scrollPastMock).lastCalledWith(4, -1);
});
it('ignores references with unknown types', () => {
// modify spans[2] so that it has an unknown refType
const spans = trace.spans;
spans[2].references = [{ refType: 'OTHER' }];
manager.scrollToNextVisibleSpan();
expect(scrollPastMock).lastCalledWith(2, 1);
manager.scrollToPrevVisibleSpan();
expect(scrollPastMock).lastCalledWith(4, -1);
});
it('handles more than one level of ancestry', () => {
// modify spans[2] so that it has an unknown refType
const spans = trace.spans;
spans[2].references = getRefs(spans[1].spanID);
manager.scrollToNextVisibleSpan();
expect(scrollPastMock).lastCalledWith(4, 1);
manager.scrollToPrevVisibleSpan();
expect(scrollPastMock).lastCalledWith(4, -1);
});
});
describe('scrollToFirstVisibleSpan', () => {
beforeEach(() => {
jest.spyOn(manager, '_scrollToVisibleSpan').mockImplementationOnce();
});
it('calls _scrollToVisibleSpan searching downwards from first span', () => {
manager.scrollToFirstVisibleSpan();
expect(manager._scrollToVisibleSpan).toHaveBeenCalledWith(1, 0);
});
});
});
describe('scrollPageDown() and scrollPageUp()', () => {
it('scrolls by +/~ viewHeight when invoked', () => {
manager.scrollPageDown();
expect(scrollBy).lastCalledWith(0.95 * accessors.getViewHeight(), true);
manager.scrollPageUp();
expect(scrollBy).lastCalledWith(-0.95 * accessors.getViewHeight(), true);
});
it('is a no-op if _accessors or _scroller is not defined', () => {
manager._accessors = null;
manager.scrollPageDown();
manager.scrollPageUp();
expect(scrollBy.mock.calls.length).toBe(0);
manager._accessors = accessors;
manager._scroller = null;
manager.scrollPageDown();
manager.scrollPageUp();
expect(scrollBy.mock.calls.length).toBe(0);
});
});
describe('destroy()', () => {
it('disposes', () => {
expect(manager._trace).toBeDefined();
expect(manager._accessors).toBeDefined();
expect(manager._scroller).toBeDefined();
manager.destroy();
expect(manager._trace).not.toBeDefined();
expect(manager._accessors).not.toBeDefined();
expect(manager._scroller).not.toBeDefined();
});
});
});

@ -1,274 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { TNil } from '../../types';
import { Span, SpanReference, Trace } from '../../types/trace';
/**
* `Accessors` is necessary because `ScrollManager` needs to be created by
* `TracePage` so it can be passed into the keyboard shortcut manager. But,
* `ScrollManager` needs to know about the state of `ListView` and `Positions`,
* which are very low-level. And, storing their state info in redux or
* `TracePage#state` would be inefficient because the state info only rarely
* needs to be accessed (when a keyboard shortcut is triggered). `Accessors`
* allows that state info to be accessed in a loosely coupled fashion on an
* as-needed basis.
*/
export type Accessors = {
getViewRange: () => [number, number];
getSearchedSpanIDs: () => Set<string> | TNil;
getCollapsedChildren: () => Set<string> | TNil;
getViewHeight: () => number;
getBottomRowIndexVisible: () => number;
getTopRowIndexVisible: () => number;
getRowPosition: (rowIndex: number) => { height: number; y: number };
mapRowIndexToSpanIndex: (rowIndex: number) => number;
mapSpanIndexToRowIndex: (spanIndex: number) => number;
};
interface IScroller {
scrollTo: (rowIndex: number) => void;
// TODO arg names throughout
scrollBy: (rowIndex: number, opt?: boolean) => void;
}
/**
* Returns `{ isHidden: true, ... }` if one of the parents of `span` is
* collapsed, e.g. has children hidden.
*
* @param {Span} span The Span to check for.
* @param {Set<string>} childrenAreHidden The set of Spans known to have hidden
* children, either because it is
* collapsed or has a collapsed parent.
* @param {Map<string, Span | TNil} spansMap Mapping from spanID to Span.
* @returns {{ isHidden: boolean, parentIds: Set<string> }}
*/
function isSpanHidden(span: Span, childrenAreHidden: Set<string>, spansMap: Map<string, Span | TNil>) {
const parentIDs = new Set<string>();
let { references }: { references: SpanReference[] | TNil } = span;
let parentID: undefined | string;
const checkRef = (ref: SpanReference) => {
if (ref.refType === 'CHILD_OF' || ref.refType === 'FOLLOWS_FROM') {
parentID = ref.spanID;
parentIDs.add(parentID);
return childrenAreHidden.has(parentID);
}
return false;
};
while (Array.isArray(references) && references.length) {
const isHidden = references.some(checkRef);
if (isHidden) {
return { isHidden, parentIDs };
}
if (!parentID) {
break;
}
const parent = spansMap.get(parentID);
parentID = undefined;
references = parent && parent.references;
}
return { parentIDs, isHidden: false };
}
/**
* ScrollManager is intended for scrolling the TracePage. Has two modes, paging
* and scrolling to the previous or next visible span.
*/
export default class ScrollManager {
_trace: Trace | TNil;
_scroller: IScroller;
_accessors: Accessors | TNil;
constructor(trace: Trace | TNil, scroller: IScroller) {
this._trace = trace;
this._scroller = scroller;
this._accessors = undefined;
}
_scrollPast(rowIndex: number, direction: 1 | -1) {
const xrs = this._accessors;
/* istanbul ignore next */
if (!xrs) {
throw new Error('Accessors not set');
}
const isUp = direction < 0;
const position = xrs.getRowPosition(rowIndex);
if (!position) {
// eslint-disable-next-line no-console
console.warn('Invalid row index');
return;
}
let { y } = position;
const vh = xrs.getViewHeight();
if (!isUp) {
y += position.height;
// scrollTop is based on the top of the window
y -= vh;
}
y += direction * 0.5 * vh;
this._scroller.scrollTo(y);
}
_scrollToVisibleSpan(direction: 1 | -1, startRow?: number) {
const xrs = this._accessors;
/* istanbul ignore next */
if (!xrs) {
throw new Error('Accessors not set');
}
if (!this._trace) {
return;
}
const { duration, spans, startTime: traceStartTime } = this._trace;
const isUp = direction < 0;
let boundaryRow: number;
if (startRow != null) {
boundaryRow = startRow;
} else if (isUp) {
boundaryRow = xrs.getTopRowIndexVisible();
} else {
boundaryRow = xrs.getBottomRowIndexVisible();
}
const spanIndex = xrs.mapRowIndexToSpanIndex(boundaryRow);
if ((spanIndex === 0 && isUp) || (spanIndex === spans.length - 1 && !isUp)) {
return;
}
// fullViewSpanIndex is one row inside the view window unless already at the top or bottom
let fullViewSpanIndex = spanIndex;
if (spanIndex !== 0 && spanIndex !== spans.length - 1) {
fullViewSpanIndex -= direction;
}
const [viewStart, viewEnd] = xrs.getViewRange();
const checkVisibility = viewStart !== 0 || viewEnd !== 1;
// use NaN as fallback to make flow happy
const startTime = checkVisibility ? traceStartTime + duration * viewStart : NaN;
const endTime = checkVisibility ? traceStartTime + duration * viewEnd : NaN;
const findMatches = xrs.getSearchedSpanIDs();
const _collapsed = xrs.getCollapsedChildren();
const childrenAreHidden = _collapsed ? new Set(_collapsed) : null;
// use empty Map as fallback to make flow happy
const spansMap: Map<string, Span> = childrenAreHidden
? new Map(spans.map(s => [s.spanID, s] as [string, Span]))
: new Map();
const boundary = direction < 0 ? -1 : spans.length;
let nextSpanIndex: number | undefined;
for (let i = fullViewSpanIndex + direction; i !== boundary; i += direction) {
const span = spans[i];
const { duration: spanDuration, spanID, startTime: spanStartTime } = span;
const spanEndTime = spanStartTime + spanDuration;
if (checkVisibility && (spanStartTime > endTime || spanEndTime < startTime)) {
// span is not visible within the view range
continue;
}
if (findMatches && !findMatches.has(spanID)) {
// skip to search matches (when searching)
continue;
}
if (childrenAreHidden) {
// make sure the span is not collapsed
const { isHidden, parentIDs } = isSpanHidden(span, childrenAreHidden, spansMap);
if (isHidden) {
parentIDs.forEach(id => childrenAreHidden.add(id));
continue;
}
}
nextSpanIndex = i;
break;
}
if (!nextSpanIndex || nextSpanIndex === boundary) {
// might as well scroll to the top or bottom
nextSpanIndex = boundary - direction;
// If there are hidden children, scroll to the last visible span
if (childrenAreHidden) {
let isFallbackHidden: boolean;
do {
const { isHidden, parentIDs } = isSpanHidden(spans[nextSpanIndex], childrenAreHidden, spansMap);
if (isHidden) {
parentIDs.forEach(id => childrenAreHidden.add(id));
nextSpanIndex--;
}
isFallbackHidden = isHidden;
} while (isFallbackHidden);
}
}
const nextRow = xrs.mapSpanIndexToRowIndex(nextSpanIndex);
this._scrollPast(nextRow, direction);
}
/**
* Sometimes the ScrollManager is created before the trace is loaded. This
* setter allows the trace to be set asynchronously.
*/
setTrace(trace: Trace | TNil) {
this._trace = trace;
}
/**
* `setAccessors` is bound in the ctor, so it can be passed as a prop to
* children components.
*/
setAccessors = (accessors: Accessors) => {
this._accessors = accessors;
};
/**
* Scrolls around one page down (0.95x). It is bounds in the ctor, so it can
* be used as a keyboard shortcut handler.
*/
scrollPageDown = () => {
if (!this._scroller || !this._accessors) {
return;
}
this._scroller.scrollBy(0.95 * this._accessors.getViewHeight(), true);
};
/**
* Scrolls around one page up (0.95x). It is bounds in the ctor, so it can
* be used as a keyboard shortcut handler.
*/
scrollPageUp = () => {
if (!this._scroller || !this._accessors) {
return;
}
this._scroller.scrollBy(-0.95 * this._accessors.getViewHeight(), true);
};
/**
* Scrolls to the next visible span, ignoring spans that do not match the
* text filter, if there is one. It is bounds in the ctor, so it can
* be used as a keyboard shortcut handler.
*/
scrollToNextVisibleSpan = () => {
this._scrollToVisibleSpan(1);
};
/**
* Scrolls to the previous visible span, ignoring spans that do not match the
* text filter, if there is one. It is bounds in the ctor, so it can
* be used as a keyboard shortcut handler.
*/
scrollToPrevVisibleSpan = () => {
this._scrollToVisibleSpan(-1);
};
scrollToFirstVisibleSpan = () => {
this._scrollToVisibleSpan(1, 0);
};
destroy() {
this._trace = undefined;
this._scroller = undefined as any;
this._accessors = undefined;
}
}

@ -1,83 +0,0 @@
/*
Copyright (c) 2018 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.
*/
.OpNode {
width: 100%;
background: #fff;
cursor: pointer;
white-space: nowrap;
border-collapse: separate;
}
.OpNode--popoverContent {
border: 1px solid #bbb;
}
.OpNode--vectorBorder {
box-sizing: content-box;
stroke: rgba(0, 0, 0, 0.4);
stroke-width: 2;
}
.OpNode td,
.OpNode th {
border: none;
}
.OpNode.is-ui-find-match {
outline: inherit;
outline-color: #fff3d7;
}
.OpNode--popover .OpNode.is-ui-find-match {
outline: #fff3d7 solid 3px;
}
.OpNode--legendNode {
background: #096dd9;
}
.OpNode--mode-time {
background: #eee;
}
.OpNode--metricCell {
text-align: right;
padding: 0.3rem 0.5rem;
background: rgba(255, 255, 255, 0.3);
}
.OpNode--labelCell {
padding: 0.3rem 0.5rem 0.3rem 0.75rem;
}
.OpNode--popover .OpNode--copyIcon,
.OpNode:not(:hover) .OpNode--copyIcon {
color: transparent;
pointer-events: none;
}
.OpNode--service {
display: flex;
justify-content: space-between;
}
/* Tweak the popover aesthetics - unfortunate but necessary */
.OpNode--popover .ant-popover-inner-content {
padding: 0;
position: relative;
}

@ -1,107 +0,0 @@
// Copyright (c) 2018 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 OpNode, { getNodeRenderer, MODE_SERVICE, MODE_TIME, MODE_SELFTIME } from './OpNode';
import CopyIcon from '../../common/CopyIcon';
describe('<OpNode>', () => {
let wrapper;
let mode;
let props;
beforeEach(() => {
mode = MODE_SERVICE;
props = {
count: 5,
errors: 0,
isUiFindMatch: false,
operation: 'op1',
percent: 7.89,
percentSelfTime: 90,
selfTime: 180000,
service: 'service1',
time: 200000,
};
wrapper = shallow(<OpNode {...props} mode={mode} />);
});
it('does not explode', () => {
expect(wrapper).toBeDefined();
expect(wrapper.find('.OpNode').length).toBe(1);
expect(wrapper.find('.OpNode--mode-service').length).toBe(1);
});
it('renders OpNode', () => {
expect(wrapper.find('.OpNode--count').text()).toBe('5 / 0');
expect(wrapper.find('.OpNode--time').text()).toBe('200 ms (7.89 %)');
expect(wrapper.find('.OpNode--avg').text()).toBe('40 ms');
expect(wrapper.find('.OpNode--selfTime').text()).toBe('180 ms (90 %)');
expect(wrapper.find('.OpNode--op').text()).toBe('op1');
expect(
wrapper
.find('.OpNode--service')
.find('strong')
.text()
).toBe('service1');
});
it('switches mode', () => {
mode = MODE_SERVICE;
wrapper = shallow(<OpNode {...props} mode={mode} />);
expect(wrapper.find('.OpNode--mode-service').length).toBe(1);
expect(wrapper.find('.OpNode--mode-time').length).toBe(0);
expect(wrapper.find('.OpNode--mode-selftime').length).toBe(0);
mode = MODE_TIME;
wrapper = shallow(<OpNode {...props} mode={mode} />);
expect(wrapper.find('.OpNode--mode-service').length).toBe(0);
expect(wrapper.find('.OpNode--mode-time').length).toBe(1);
expect(wrapper.find('.OpNode--mode-selftime').length).toBe(0);
mode = MODE_SELFTIME;
wrapper = shallow(<OpNode {...props} mode={mode} />);
expect(wrapper.find('.OpNode--mode-service').length).toBe(0);
expect(wrapper.find('.OpNode--mode-time').length).toBe(0);
expect(wrapper.find('.OpNode--mode-selftime').length).toBe(1);
});
it('renders a copy icon', () => {
const copyIcon = wrapper.find(CopyIcon);
expect(copyIcon.length).toBe(1);
expect(copyIcon.prop('copyText')).toBe(`${props.service} ${props.operation}`);
expect(copyIcon.prop('tooltipTitle')).toBe('Copy label');
});
describe('getNodeRenderer()', () => {
const key = 'key test value';
const vertex = {
data: {
service: 'service1',
operation: 'op1',
data: {},
},
key,
};
it('creates OpNode', () => {
const drawNode = getNodeRenderer(MODE_SERVICE);
const opNode = drawNode(vertex);
expect(opNode.type === 'OpNode');
expect(opNode.props.mode).toBe(MODE_SERVICE);
});
});
});

@ -1,153 +0,0 @@
// Copyright (c) 2018 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 * as React from 'react';
import { Popover } from 'antd';
import { TLayoutVertex } from 'apm-plexus/lib/types';
import { TSumSpan } from './types';
import CopyIcon from '../../common/CopyIcon';
import { TDenseSpanMembers } from '../../../model/trace-dag/types';
import TDagPlexusVertex from '../../../model/trace-dag/types/TDagPlexusVertex';
import colorGenerator from '../../../utils/color-generator';
import './OpNode.css';
import EmphasizedNode from '../../common/EmphasizedNode';
type Props = {
count: number;
errors: number;
time: number;
percent: number;
selfTime: number;
percentSelfTime: number;
operation: string;
service: string;
mode: string;
};
export const MODE_SERVICE = 'service';
export const MODE_TIME = 'time';
export const MODE_SELFTIME = 'selftime';
export const HELP_TABLE = (
<table className="OpNode OpNode--legendNode">
<tbody>
<tr>
<td className="OpNode--metricCell">Count / Error</td>
<td className="OpNode--labelCell">
<strong>Service</strong>
</td>
<td className="OpNode--metricCell">Avg</td>
</tr>
<tr>
<td className="OpNode--metricCell">Duration</td>
<td className="OpNode--labelCell">Operation</td>
<td className="OpNode--metricCell">Self time</td>
</tr>
</tbody>
</table>
);
export function round2(percent: number) {
return Math.round(percent * 100) / 100;
}
export default class OpNode extends React.PureComponent<Props> {
render() {
const { count, errors, time, percent, selfTime, percentSelfTime, operation, service, mode } = this.props;
// Spans over 20 % time are full red - we have probably to reconsider better approach
let backgroundColor;
if (mode === MODE_TIME) {
const percentBoosted = Math.min(percent / 20, 1);
backgroundColor = [255, 0, 0, percentBoosted].join();
} else if (mode === MODE_SELFTIME) {
backgroundColor = [255, 0, 0, percentSelfTime / 100].join();
} else {
backgroundColor = colorGenerator
.getRgbColorByKey(service)
.concat(0.8)
.join();
}
const table = (
<table className={`OpNode OpNode--mode-${mode}`} cellSpacing="0">
<tbody
className="OpNode--body"
style={{
background: `rgba(${backgroundColor})`,
}}
>
<tr>
<td className="OpNode--metricCell OpNode--count">
{count} / {errors}
</td>
<td className="OpNode--labelCell OpNode--service">
<strong>{service}</strong>
<CopyIcon
className="OpNode--copyIcon"
copyText={`${service} ${operation}`}
tooltipTitle="Copy label"
/>
</td>
<td className="OpNode--metricCell OpNode--avg">{round2(time / 1000 / count)} ms</td>
</tr>
<tr>
<td className="OpNode--metricCell OpNode--time">
{time / 1000} ms ({round2(percent)} %)
</td>
<td className="OpNode--labelCell OpNode--op">{operation}</td>
<td className="OpNode--metricCell OpNode--selfTime">
{selfTime / 1000} ms ({round2(percentSelfTime)} %)
</td>
</tr>
</tbody>
</table>
);
const popoverContent = <div className="OpNode--popoverContent">{table}</div>;
return (
<Popover overlayClassName="OpNode--popover" mouseEnterDelay={0.25} content={popoverContent}>
{table}
</Popover>
);
}
}
export function getNodeRenderer(mode: string) {
return function drawNode(vertex: TDagPlexusVertex<TSumSpan & TDenseSpanMembers>) {
return <OpNode {...vertex.data} mode={mode} />;
};
}
export function getNodeFindEmphasisRenderer(uiFindVertexKeys: Set<string> | null | undefined) {
return function renderFindEmphasis(lv: TLayoutVertex<TDagPlexusVertex<TSumSpan & TDenseSpanMembers>>) {
if (!uiFindVertexKeys || !uiFindVertexKeys.has(lv.vertex.key)) {
return null;
}
return <EmphasizedNode height={lv.height} width={lv.width} />;
};
}
export function renderNodeVectorBorder(lv: TLayoutVertex<TDagPlexusVertex<TSumSpan>>) {
return (
<rect
className="OpNode--vectorBorder"
vectorEffect="non-scaling-stroke"
width={lv.width}
height={lv.height}
/>
);
}

@ -1,115 +0,0 @@
/*
Copyright (c) 2018 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.
*/
.TraceGraph--experimental {
background-color: #a00;
color: #fff;
position: absolute;
padding: 1px 15px;
}
.TraceGraph--graphWrapper {
background: #f0f0f0;
bottom: 0;
cursor: move;
left: 0;
overflow: auto;
position: absolute;
right: 0;
top: 0;
transition: background 0.5s ease;
display: flex;
}
.TraceGraph--graphWrapper.is-uiFind-mode {
background: #ddd;
}
.TraceGraph--sidebar-container {
background: #f4f4f4;
display: flex;
z-index: 1;
}
.TraceGraph--menu {
border-left: 1px solid #e4e4e4;
border-right: 1px solid #e4e4e4;
cursor: pointer;
padding: 0.8rem;
}
.TraceGraph--menu > li {
list-style-type: none;
text-align: center;
padding-bottom: 0.3rem;
}
.TraceGraph--sidebar {
cursor: default;
box-shadow: -1px 0 rgba(0, 0, 0, 0.2);
}
.TraceGraph--help-content > div {
margin-top: 1rem;
}
.TraceGraph--dag {
stroke-width: 0.8;
}
/* DAG minimap */
.TraceGraph--miniMap {
align-items: flex-end;
bottom: 1rem;
display: flex;
left: 1rem;
position: absolute;
z-index: 1;
}
.TraceGraph--miniMap > .plexus-MiniMap--item {
border: 1px solid #777;
background: #999;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
margin-right: 1rem;
position: relative;
}
.TraceGraph--miniMap > .plexus-MiniMap--map {
/* dynamic widht, height */
box-sizing: content-box;
cursor: not-allowed;
}
.TraceGraph--miniMap .plexus-MiniMap--mapActive {
/* dynamic: width, height, transform */
background: #ccc;
position: absolute;
}
.TraceGraph--miniMap > .plexus-MiniMap--button {
background: #ccc;
color: #888;
cursor: pointer;
font-size: 1.6em;
line-height: 0;
padding: 0.1rem;
}
.TraceGraph--miniMap > .plexus-MiniMap--button:hover {
background: #ddd;
}

@ -1,252 +0,0 @@
/* eslint react/prop-types: 0 */
// Copyright (c) 2018 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 * as React from 'react';
import { Card, Button, Tooltip } from 'antd';
import { QuestionCircleOutlined,CloseOutlined} from '@ant-design/icons';
import cx from 'classnames';
import { Digraph, LayoutManager } from 'apm-plexus';
import cacheAs from 'apm-plexus/lib/cacheAs';
import {
getNodeRenderer,
getNodeFindEmphasisRenderer,
renderNodeVectorBorder,
MODE_SERVICE,
MODE_TIME,
MODE_SELFTIME,
HELP_TABLE,
} from './OpNode';
import './TraceGraph.css';
const { classNameIsSmall, scaleOpacity, scaleStrokeOpacity } = Digraph.propsFactories;
export function setOnEdgePath(e) {
return e.followsFrom ? { strokeDasharray: 4 } : {};
}
const HELP_CONTENT = (
<div className="TraceGraph--help-content">
{HELP_TABLE}
<div>
<table>
<tbody>
<tr>
<td>
<Button htmlType="button" shape="circle" size="small">
S
</Button>
</td>
<td>Service</td>
<td>Colored by service</td>
</tr>
<tr>
<td>
<Button htmlType="button" shape="circle" size="small">
T
</Button>
</td>
<td>Time</td>
<td>Colored by total time</td>
</tr>
<tr>
<td>
<Button htmlType="button" shape="circle" size="small">
ST
</Button>
</td>
<td>Selftime</td>
<td>Colored by self time</td>
</tr>
</tbody>
</table>
</div>
<div>
<svg width="100%" height="40">
<line x1="0" y1="10" x2="90" y2="10" style={{ stroke: '#000', strokeWidth: 2 }} />
<text alignmentBaseline="middle" x="100" y="10">
ChildOf
</text>
<line
x1="0"
y1="30"
x2="90"
y2="30"
style={{ stroke: '#000', strokeWidth: 2, strokeDasharray: '4' }}
/>
<text alignmentBaseline="middle" x="100" y="30">
FollowsFrom
</text>
</svg>
</div>
</div>
);
export default class TraceGraph extends React.PureComponent {
static defaultProps = {
ev: null,
};
constructor(props) {
super(props);
this.state = {
showHelp: false,
mode: MODE_SERVICE,
};
this.layoutManager = new LayoutManager({ useDotEdges: true, splines: 'polyline' });
}
componentWillUnmount() {
this.layoutManager.stopAndRelease();
}
toggleNodeMode(newMode) {
this.setState({ mode: newMode });
}
showHelp = () => {
this.setState({ showHelp: true });
};
closeSidebar = () => {
this.setState({ showHelp: false });
};
render() {
const { ev, headerHeight, uiFind, uiFindVertexKeys } = this.props;
const { showHelp, mode } = this.state;
if (!ev) {
return <h1 className="u-mt-vast u-tx-muted ub-tx-center">No trace found</h1>;
}
const wrapperClassName = cx('TraceGraph--graphWrapper', { 'is-uiFind-mode': uiFind });
return (
<div className={wrapperClassName} style={{ paddingTop: headerHeight + 47 }}>
<Digraph
minimap
zoom
className="TraceGraph--dag"
minimapClassName="u-miniMap"
layoutManager={this.layoutManager}
measurableNodesKey="nodes"
layers={[
{
key: 'node-find-emphasis',
layerType: 'svg',
renderNode: getNodeFindEmphasisRenderer(uiFindVertexKeys),
},
{
key: 'edges',
edges: true,
layerType: 'svg',
defs: [{ localId: 'arrow' }],
markerEndId: 'arrow',
setOnContainer: [scaleOpacity, scaleStrokeOpacity],
setOnEdge: setOnEdgePath,
},
{
key: 'nodes-borders',
layerType: 'svg',
setOnContainer: scaleStrokeOpacity,
renderNode: renderNodeVectorBorder,
},
{
key: 'nodes',
layerType: 'html',
measurable: true,
renderNode: cacheAs(`trace-graph/nodes/render/${mode}`, getNodeRenderer(mode)),
},
]}
setOnGraph={classNameIsSmall}
edges={ev.edges}
vertices={ev.vertices}
/>
<a
className="TraceGraph--experimental"
href="https://github.com/jaegertracing/jaeger-ui/issues/293"
target="_blank"
rel="noopener noreferrer"
>
Experimental
</a>
<div className="TraceGraph--sidebar-container">
<ul className="TraceGraph--menu">
<li>
<QuestionCircleOutlined onClick={this.showHelp} />
</li>
<li>
<Tooltip placement="left" title="Service">
<Button
className="TraceGraph--btn-service"
htmlType="button"
shape="circle"
size="small"
onClick={() => this.toggleNodeMode(MODE_SERVICE)}
>
S
</Button>
</Tooltip>
</li>
<li>
<Tooltip placement="left" title="Time">
<Button
className="TraceGraph--btn-time"
htmlType="button"
shape="circle"
size="small"
onClick={() => this.toggleNodeMode(MODE_TIME)}
>
T
</Button>
</Tooltip>
</li>
<li>
<Tooltip placement="left" title="Selftime">
<Button
className="TraceGraph--btn-selftime"
htmlType="button"
shape="circle"
size="small"
onClick={() => this.toggleNodeMode(MODE_SELFTIME)}
>
ST
</Button>
</Tooltip>
</li>
</ul>
{showHelp && (
<Card
title="Help"
bordered={false}
extra={
<a onClick={this.closeSidebar} role="button">
<CloseOutlined />
</a>
}
>
{HELP_CONTENT}
</Card>
)}
</div>
</div>
);
}
}

@ -1,51 +0,0 @@
// 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 transformTraceData from '../../../model/transform-trace-data';
import calculateTraceDagEV from './calculateTraceDagEV';
const testTrace = require('./testTrace.json');
const transformedTrace = transformTraceData(testTrace);
function assertData(nodes, service, operation, count, errors, time, percent, selfTime) {
const d = nodes.find(({ data: n }) => n.service === service && n.operation === operation).data;
expect(d).toBeDefined();
expect(d.count).toBe(count);
expect(d.errors).toBe(errors);
expect(d.time).toBe(time * 1000);
expect(d.percent).toBeCloseTo(percent, 2);
expect(d.selfTime).toBe(selfTime * 1000);
}
describe('calculateTraceDagEV', () => {
it('calculates TraceGraph', () => {
const traceDag = calculateTraceDagEV(transformedTrace);
const { vertices: nodes } = traceDag;
expect(nodes.length).toBe(9);
assertData(nodes, 'service1', 'op1', 1, 0, 390, 39, 224);
// accumulate data (count,times)
assertData(nodes, 'service1', 'op2', 2, 1, 70, 7, 70);
// self-time is substracted from child
assertData(nodes, 'service1', 'op3', 1, 0, 66, 6.6, 46);
assertData(nodes, 'service2', 'op1', 1, 0, 20, 2, 2);
assertData(nodes, 'service2', 'op2', 1, 0, 18, 1.8, 18);
// follows_from relation will not influence self-time
assertData(nodes, 'service1', 'op4', 1, 0, 20, 2, 20);
assertData(nodes, 'service2', 'op3', 1, 0, 200, 20, 200);
// fork-join self-times are calculated correctly (self-time drange)
assertData(nodes, 'service1', 'op6', 1, 0, 10, 1, 1);
assertData(nodes, 'service1', 'op7', 2, 0, 17, 1.7, 17);
});
});

@ -1,113 +0,0 @@
// 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 DRange from 'drange';
import { TEdge } from 'apm-plexus/lib/types';
import convPlexus from '../../../model/trace-dag/convPlexus';
import TraceDag from '../../../model/trace-dag/TraceDag';
import TDagNode from '../../../model/trace-dag/types/TDagNode';
import { TDenseSpanMembers } from '../../../model/trace-dag/types';
import { Trace, Span, KeyValuePair } from '../../../types/trace';
import { TSumSpan, TEv } from './types';
let parentChildOfMap: Record<string, Span[]>;
export function isError(tags: Array<KeyValuePair>) {
if (tags) {
const errorTag = tags.find(t => t.key === 'error');
if (errorTag) {
return errorTag.value;
}
}
return false;
}
function mapFollowsFrom(
edges: TEdge[],
nodes: TDagNode<TSumSpan & TDenseSpanMembers>[]
): TEdge<{ followsFrom: boolean }>[] {
return edges.map(e => {
let hasChildOf = true;
if (typeof e.to === 'number') {
const node = nodes[e.to];
hasChildOf = node.members.some(
m => m.span.references && m.span.references.some(r => r.refType === 'CHILD_OF')
);
}
return { ...e, followsFrom: !hasChildOf };
});
}
function getChildOfSpans(parentID: string, trace: Trace): Span[] {
if (!parentChildOfMap) {
parentChildOfMap = {};
trace.spans.forEach(s => {
if (s.references) {
// Filter for CHILD_OF we don't want to calculate FOLLOWS_FROM (prod-cons)
const parentIDs = s.references.filter(r => r.refType === 'CHILD_OF').map(r => r.spanID);
parentIDs.forEach((pID: string) => {
parentChildOfMap[pID] = parentChildOfMap[pID] || [];
parentChildOfMap[pID].push(s);
});
}
});
}
return parentChildOfMap[parentID] || [];
}
function getChildOfDrange(parentID: string, trace: Trace) {
const childrenDrange = new DRange();
getChildOfSpans(parentID, trace).forEach(s => {
// -1 otherwise it will take for each child a micro (incluse,exclusive)
childrenDrange.add(s.startTime, s.startTime + (s.duration <= 0 ? 0 : s.duration - 1));
});
return childrenDrange;
}
export function calculateTraceDag(trace: Trace): TraceDag<TSumSpan & TDenseSpanMembers> {
const baseDag = TraceDag.newFromTrace(trace);
const dag = new TraceDag<TSumSpan & TDenseSpanMembers>();
baseDag.nodesMap.forEach(node => {
const ntime = node.members.reduce((p, m) => p + m.span.duration, 0);
const numErrors = node.members.reduce((p, m) => (p + isError(m.span.tags) ? 1 : 0), 0);
const childDurationsDRange = node.members.reduce((p, m) => {
// Using DRange to handle overlapping spans (fork-join)
const cdr = new DRange(m.span.startTime, m.span.startTime + m.span.duration).intersect(
getChildOfDrange(m.span.spanID, trace)
);
return p + cdr.length;
}, 0);
const stime = ntime - childDurationsDRange;
dag.addNode(node.id, node.parentID, {
...node,
count: node.members.length,
errors: numErrors,
time: ntime,
percent: (100 / trace.duration) * ntime,
selfTime: stime,
percentSelfTime: (100 / ntime) * stime,
});
});
return dag;
}
export default function calculateTraceDagEV(trace: Trace): TEv {
const traceDag = calculateTraceDag(trace);
const nodes = [...traceDag.nodesMap.values()];
const ev = convPlexus(traceDag.nodesMap);
const edges = mapFollowsFrom(ev.edges, nodes);
return { ...ev, edges };
}

@ -1,284 +0,0 @@
{
"traceID": "trace-123",
"spans": [
{
"traceID": "trace-123",
"spanID": "span-1",
"flags": 1,
"operationName": "op1",
"startTime": 1542666452979000,
"duration": 390000,
"references": [],
"tags": [
{
"key": "span.kind",
"type": "string",
"value": "server"
}
],
"logs": [],
"processID": "p1",
"warnings": null
},
{
"traceID": "trace-123",
"spanID": "span-2",
"flags": 1,
"operationName": "op2",
"startTime": 1542666453104000,
"duration": 33000,
"references": [
{
"refType": "CHILD_OF",
"traceID": "trace-123",
"spanID": "span-1"
}
],
"tags": [
{
"key": "span.kind",
"type": "string",
"value": "client"
},
{
"key": "error",
"type": "bool",
"value": "true"
}
],
"logs": [],
"processID": "p1",
"warnings": null
},
{
"traceID": "trace-123",
"spanID": "span-2_1",
"flags": 1,
"operationName": "op2",
"startTime": 1542666453229000,
"duration": 37000,
"references": [
{
"refType": "CHILD_OF",
"traceID": "trace-123",
"spanID": "span-1"
}
],
"tags": [
{
"key": "span.kind",
"type": "string",
"value": "client"
}
],
"logs": [],
"processID": "p1",
"warnings": null
},
{
"traceID": "trace-123",
"spanID": "span-3",
"flags": 1,
"operationName": "op3",
"startTime": 1542666453159000,
"duration": 66000,
"references": [
{
"refType": "CHILD_OF",
"traceID": "trace-123",
"spanID": "span-1"
}
],
"tags": [
{
"key": "span.kind",
"type": "string",
"value": "client"
}
],
"logs": [],
"processID": "p1",
"warnings": null
},
{
"traceID": "trace-123",
"spanID": "span-4",
"flags": 1,
"operationName": "op1",
"startTime": 1542666453179000,
"duration": 20000,
"references": [
{
"refType": "CHILD_OF",
"traceID": "trace-123",
"spanID": "span-3"
}
],
"tags": [
{
"key": "span.kind",
"type": "string",
"value": "server"
}
],
"logs": [],
"processID": "p2",
"warnings": null
},
{
"traceID": "trace-123",
"spanID": "span-5",
"flags": 1,
"operationName": "op2",
"startTime": 1542666453180000,
"duration": 18000,
"references": [
{
"refType": "CHILD_OF",
"traceID": "trace-123",
"spanID": "span-4"
}
],
"tags": [
{
"key": "db.type",
"type": "string",
"value": "sql"
}
],
"logs": [],
"processID": "p2",
"warnings": null
},
{
"traceID": "trace-123",
"spanID": "span-6",
"flags": 1,
"operationName": "op4",
"startTime": 1542666453279000,
"duration": 20000,
"references": [
{
"refType": "CHILD_OF",
"traceID": "trace-123",
"spanID": "span-1"
}
],
"tags": [
{
"key": "span.kind",
"type": "string",
"value": "producer"
}
],
"logs": [],
"processID": "p1",
"warnings": null
},
{
"traceID": "trace-123",
"spanID": "span-7",
"flags": 1,
"operationName": "op3",
"startTime": 1542666453779000,
"duration": 200000,
"references": [
{
"refType": "FOLLOWS_FROM",
"traceID": "trace-123",
"spanID": "span-6"
}
],
"tags": [
{
"key": "span.kind",
"type": "string",
"value": "consumer"
}
],
"logs": [],
"processID": "p2",
"warnings": null
},
{
"traceID": "trace-123",
"spanID": "span-12",
"flags": 1,
"operationName": "op6",
"startTime": 1542666453309000,
"duration": 10000,
"references": [
{
"refType": "CHILD_OF",
"traceID": "trace-123",
"spanID": "span-1"
}
],
"tags": [],
"logs": [],
"processID": "p1",
"warnings": null
},
{
"traceID": "trace-123",
"spanID": "span-13",
"flags": 1,
"operationName": "op7",
"startTime": 1542666453310000,
"duration": 9000,
"references": [
{
"refType": "CHILD_OF",
"traceID": "trace-123",
"spanID": "span-12"
}
],
"tags": [],
"logs": [],
"processID": "p1",
"warnings": null
},
{
"traceID": "trace-123",
"spanID": "span-14",
"flags": 1,
"operationName": "op7",
"startTime": 1542666453311000,
"duration": 8000,
"references": [
{
"refType": "CHILD_OF",
"traceID": "trace-123",
"spanID": "span-12"
}
],
"tags": [],
"logs": [],
"processID": "p1",
"warnings": null
}
],
"processes": {
"p1": {
"serviceName": "service1",
"tags": [
{
"key": "hostname",
"type": "string",
"value": "foobar.org"
}
]
},
"p2": {
"serviceName": "service2",
"tags": [
{
"key": "hostname",
"type": "string",
"value": "foobar.org"
}
]
}
},
"warnings": null
}

@ -1,32 +0,0 @@
// Copyright (c) 2019-2020 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 { TEdge } from 'apm-plexus/lib/types';
import { TDenseSpanMembers } from '../../../model/trace-dag/types';
import TDagPlexusVertex from '../../../model/trace-dag/types/TDagPlexusVertex';
export type TSumSpan = {
count: number;
errors: number;
percent: number;
percentSelfTime: number;
selfTime: number;
time: number;
};
export type TEv = {
edges: TEdge<{ followsFrom: boolean }>[];
vertices: TDagPlexusVertex<TSumSpan & TDenseSpanMembers>[];
};

@ -1,106 +0,0 @@
// Copyright (c) 2019 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as React from 'react';
import { shallow } from 'enzyme';
import { Button, Dropdown } from 'antd';
import { Link } from 'react-router-dom';
import AltViewOptions from './AltViewOptions';
import * as track from './TracePageHeader.track';
describe('AltViewOptions', () => {
let trackGanttView;
let trackGraphView;
let trackJsonView;
let trackRawJsonView;
let wrapper;
const getLink = text => {
const menu = shallow(wrapper.find(Dropdown).prop('overlay'));
const links = menu.find(Link);
for (let i = 0; i < links.length; i++) {
const link = links.at(i);
if (link.children().text() === text) return link;
}
const link = menu.find('a');
if (link.children().text() === text) return link;
throw new Error(`Could not find "${text}"`);
};
const props = {
traceGraphView: true,
traceID: 'test trace ID',
onTraceGraphViewClicked: jest.fn(),
};
beforeAll(() => {
trackGanttView = jest.spyOn(track, 'trackGanttView');
trackGraphView = jest.spyOn(track, 'trackGraphView');
trackJsonView = jest.spyOn(track, 'trackJsonView');
trackRawJsonView = jest.spyOn(track, 'trackRawJsonView');
});
beforeEach(() => {
jest.clearAllMocks();
wrapper = shallow(<AltViewOptions {...props} />);
});
it('renders correctly', () => {
expect(wrapper).toMatchSnapshot();
});
it('tracks viewing JSONs', () => {
expect(trackJsonView).not.toHaveBeenCalled();
getLink('Trace JSON').simulate('click');
expect(trackJsonView).toHaveBeenCalledTimes(1);
expect(trackRawJsonView).not.toHaveBeenCalled();
getLink('Trace JSON (unadjusted)').simulate('click');
expect(trackRawJsonView).toHaveBeenCalledTimes(1);
expect(trackJsonView).toHaveBeenCalledTimes(1);
expect(trackGanttView).not.toHaveBeenCalled();
expect(trackGraphView).not.toHaveBeenCalled();
});
it('toggles and tracks toggle', () => {
expect(trackGanttView).not.toHaveBeenCalled();
expect(props.onTraceGraphViewClicked).not.toHaveBeenCalled();
getLink('Trace Timeline').simulate('click');
expect(trackGanttView).toHaveBeenCalledTimes(1);
expect(props.onTraceGraphViewClicked).toHaveBeenCalledTimes(1);
wrapper.setProps({ traceGraphView: false });
expect(trackGraphView).not.toHaveBeenCalled();
getLink('Trace Graph').simulate('click');
expect(trackGraphView).toHaveBeenCalledTimes(1);
expect(props.onTraceGraphViewClicked).toHaveBeenCalledTimes(2);
wrapper.setProps({ traceGraphView: true });
expect(trackGanttView).toHaveBeenCalledTimes(1);
wrapper.find(Button).simulate('click');
expect(trackGanttView).toHaveBeenCalledTimes(2);
expect(props.onTraceGraphViewClicked).toHaveBeenCalledTimes(3);
wrapper.setProps({ traceGraphView: false });
expect(trackGraphView).toHaveBeenCalledTimes(1);
wrapper.find(Button).simulate('click');
expect(trackGraphView).toHaveBeenCalledTimes(2);
expect(props.onTraceGraphViewClicked).toHaveBeenCalledTimes(4);
expect(trackGanttView).toHaveBeenCalledTimes(2);
expect(trackJsonView).not.toHaveBeenCalled();
expect(trackRawJsonView).not.toHaveBeenCalled();
});
});

@ -1,72 +0,0 @@
// Copyright (c) 2018 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as React from 'react';
import { Button, Dropdown, Menu } from 'antd';
import { DownOutlined } from '@ant-design/icons';
import { Link } from 'react-router-dom';
import { trackGanttView, trackGraphView, trackJsonView, trackRawJsonView } from './TracePageHeader.track';
import prefixUrl from '../../../utils/prefix-url';
type Props = {
onTraceGraphViewClicked: () => void;
traceGraphView: boolean;
traceID: string;
};
export default function AltViewOptions(props: Props) {
const { onTraceGraphViewClicked, traceGraphView, traceID } = props;
const handleToggleView = () => {
if (traceGraphView) trackGanttView();
else trackGraphView();
onTraceGraphViewClicked();
};
const menu = (
<Menu>
<Menu.Item>
<a onClick={handleToggleView} role="button">
{traceGraphView ? 'Trace Timeline' : 'Trace Graph'}
</a>
</Menu.Item>
<Menu.Item>
<Link
to={prefixUrl(`/api/traces/${traceID}?prettyPrint=true`)}
rel="noopener noreferrer"
target="_blank"
onClick={trackJsonView}
>
Trace JSON
</Link>
</Menu.Item>
<Menu.Item>
<Link
to={prefixUrl(`/api/traces/${traceID}?raw=true&prettyPrint=true`)}
rel="noopener noreferrer"
target="_blank"
onClick={trackRawJsonView}
>
Trace JSON (unadjusted)
</Link>
</Menu.Item>
</Menu>
);
return (
<Dropdown overlay={menu}>
<Button className="ub-mr2" htmlType="button" onClick={handleToggleView}>
Alternate Views <DownOutlined />
</Button>
</Dropdown>
);
}

@ -1,40 +0,0 @@
/*
Copyright (c) 2017 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.KeyboardShortcutsHelp--cta {
font-size: 1.2rem;
margin: 0 -7px;
}
.KeyboardShortcutsHelp--table {
background-color: #fafafa;
max-height: 500px;
overflow: auto;
}
.KeyboardShortcutsHelp--oddRow {
background-color: #f6f6f6;
}
.KeyboardShortcutsHelp--table kbd {
background: #f0f0f0;
border-bottom: 1px solid #bbb;
border-radius: 3px;
border: 1px solid #d4d4d4;
color: #000;
font-family: monospace;
padding: 0.25em 0.3em;
}

@ -1,115 +0,0 @@
/* eslint react/prop-types: 0 */
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as React from 'react';
import { Button, Modal, Table } from 'antd';
import keyboardMappings from '../keyboard-mappings';
import track from './KeyboardShortcutsHelp.track';
import './KeyboardShortcutsHelp.css';
const { Column } = Table;
const SYMBOL_CONV = {
up: '↑',
right: '→',
down: '↓',
left: '←',
shift: '⇧',
};
const ODD_ROW_CLASS = 'KeyboardShortcutsHelp--oddRow';
function convertKeys(keyConfig) {
const config = Array.isArray(keyConfig) ? keyConfig : [keyConfig];
return config.map(str => str.split('+').map(part => SYMBOL_CONV[part] || part.toUpperCase()));
}
const padLeft = text => <span className="ub-pl4">{text}</span>;
const padRight = text => <span className="ub-pr4">{text}</span>;
const getRowClass = (_, index) => (index % 2 > 0 ? ODD_ROW_CLASS : '');
let kbdTable= null;
function getHelpModal() {
if (kbdTable) {
return kbdTable;
}
const data = [];
Object.keys(keyboardMappings).forEach(handle => {
const { binding, label } = keyboardMappings[handle];
const keyConfigs = convertKeys(binding);
data.push(
...keyConfigs.map(config => ({
key: String(config),
kbds: <kbd>{config.join(' ')}</kbd>,
description: label,
}))
);
});
kbdTable = (
<Table
className="KeyboardShortcutsHelp--table u-simple-scrollbars"
dataSource={data}
size="middle"
pagination={false}
showHeader={false}
rowClassName={getRowClass}
>
<Column title="Description" dataIndex="description" key="description" render={padLeft} />
<Column title="Key(s)" dataIndex="kbds" key="kbds" align="right" render={padRight} />
</Table>
);
return kbdTable;
}
export default class KeyboardShortcutsHelp extends React.PureComponent {
state = {
visible: false,
};
onCtaClicked = () => {
track();
this.setState({ visible: true });
};
onCloserClicked = () => this.setState({ visible: false });
render() {
const { className } = this.props;
return (
<React.Fragment>
<Button className={className} htmlType="button" onClick={this.onCtaClicked}>
<span className="KeyboardShortcutsHelp--cta"></span>
</Button>
<Modal
align={undefined}
title="Keyboard Shortcuts"
visible={this.state.visible}
onOk={this.onCloserClicked}
onCancel={this.onCloserClicked}
cancelButtonProps={{ style: { display: 'none' } }}
bodyStyle={{ padding: 0 }}
>
{getHelpModal()}
</Modal>
</React.Fragment>
);
}
}

@ -1,57 +0,0 @@
// Copyright (c) 2019 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { Button, Modal } from 'antd';
import { shallow } from 'enzyme';
import KeyboardShortcutsHelp from './KeyboardShortcutsHelp';
import * as track from './KeyboardShortcutsHelp.track';
describe('KeyboardShortcutsHelp', () => {
const testClassName = 'test--ClassName';
const wrapper = shallow(<KeyboardShortcutsHelp className={testClassName} />);
let trackSpy;
beforeAll(() => {
trackSpy = jest.spyOn(track, 'default');
});
beforeEach(() => {
trackSpy.mockReset();
});
it('renders as expected', () => {
expect(wrapper.find(Button).hasClass(testClassName)).toBe(true);
expect(wrapper).toMatchSnapshot();
});
it('opens modal and tracks its opening', () => {
expect(wrapper.setState({ visible: false }));
wrapper.find(Button).simulate('click', {});
expect(wrapper.state('visible')).toBe(true);
expect(trackSpy).toHaveBeenCalled();
});
it('closes modal', () => {
wrapper.setState({ visible: true });
wrapper.find(Modal).prop('onOk')();
expect(wrapper.state('visible')).toBe(false);
wrapper.setState({ visible: true });
wrapper.find(Modal).prop('onCancel')();
expect(wrapper.state('visible')).toBe(false);
});
});

@ -1,20 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { OPEN } from '../../../utils/tracking/common';
import { trackEvent } from '../../../utils/tracking';
const CATEGORY = 'jaeger/ux/trace/kbd-modal';
export default trackEvent.bind(null, CATEGORY, OPEN);

@ -1,22 +0,0 @@
/*
Copyright (c) 2017 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.CanvasSpanGraph {
background: #fafafa;
height: 60px;
position: absolute;
width: 100%;
}

@ -1,32 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { shallow } from 'enzyme';
import CanvasSpanGraph from './CanvasSpanGraph';
describe('<CanvasSpanGraph>', () => {
it('renders without exploding', () => {
const items = [{ valueWidth: 1, valueOffset: 1, serviceName: 'service-name-0' }];
const wrapper = shallow(<CanvasSpanGraph items={[]} valueWidth={4000} />);
expect(wrapper).toBeDefined();
wrapper.instance()._setCanvasRef({
getContext: () => ({
fillRect: () => {},
}),
});
wrapper.setProps({ items });
});
});

@ -1,60 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as React from 'react';
import renderIntoCanvas from './render-into-canvas';
import colorGenerator from '../../../../utils/color-generator';
import { TNil } from '../../../../types';
import './CanvasSpanGraph.css';
type CanvasSpanGraphProps = {
items: { valueWidth: number; valueOffset: number; serviceName: string }[];
valueWidth: number;
};
const getColor = (hex: string) => colorGenerator.getRgbColorByKey(hex);
export default class CanvasSpanGraph extends React.PureComponent<CanvasSpanGraphProps> {
_canvasElm: HTMLCanvasElement | TNil;
constructor(props: CanvasSpanGraphProps) {
super(props);
this._canvasElm = undefined;
}
componentDidMount() {
this._draw();
}
componentDidUpdate() {
this._draw();
}
_setCanvasRef = (elm: HTMLCanvasElement | TNil) => {
this._canvasElm = elm;
};
_draw() {
if (this._canvasElm) {
const { valueWidth: totalValueWidth, items } = this.props;
renderIntoCanvas(this._canvasElm, items, totalValueWidth, getColor);
}
}
render() {
return <canvas className="CanvasSpanGraph" ref={this._setCanvasRef} />;
}
}

@ -1,20 +0,0 @@
/*
Copyright (c) 2017 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.GraphTick {
stroke: #aaa;
stroke-width: 1px;
}

@ -1,44 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { shallow } from 'enzyme';
import GraphTicks from './GraphTicks';
describe('<GraphTicks>', () => {
const defaultProps = {
items: [
{ valueWidth: 100, valueOffset: 25, serviceName: 'a' },
{ valueWidth: 100, valueOffset: 50, serviceName: 'b' },
],
valueWidth: 200,
numTicks: 4,
};
let ticksG;
beforeEach(() => {
const wrapper = shallow(<GraphTicks {...defaultProps} />);
ticksG = wrapper.find('[data-test="ticks"]');
});
it('creates a <g> for ticks', () => {
expect(ticksG.length).toBe(1);
});
it('creates a line for each ticks excluding the first and last', () => {
expect(ticksG.find('line').length).toBe(defaultProps.numTicks - 1);
});
});

@ -1,37 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import './GraphTicks.css';
type GraphTicksProps = {
numTicks: number;
};
export default function GraphTicks(props: GraphTicksProps) {
const { numTicks } = props;
const ticks = [];
// i starts at 1, limit is `i < numTicks` so the first and last ticks aren't drawn
for (let i = 1; i < numTicks; i++) {
const x = `${(i / numTicks) * 100}%`;
ticks.push(<line className="GraphTick" x1={x} y1="0%" x2={x} y2="100%" key={i / numTicks} />);
}
return (
<g data-test="ticks" aria-hidden="true">
{ticks}
</g>
);
}

@ -1,46 +0,0 @@
/*
Copyright (c) 2017 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.Scrubber--handleExpansion {
cursor: col-resize;
fill-opacity: 0;
fill: #44f;
}
.Scrubber.isDragging .Scrubber--handleExpansion,
.Scrubber--handles:hover > .Scrubber--handleExpansion {
fill-opacity: 1;
}
.Scrubber--handle {
cursor: col-resize;
fill: #555;
}
.Scrubber.isDragging .Scrubber--handle,
.Scrubber--handles:hover > .Scrubber--handle {
fill: #44f;
}
.Scrubber--line {
pointer-events: none;
stroke: #555;
}
.Scrubber.isDragging > .Scrubber--line,
.Scrubber--handles:hover + .Scrubber--line {
stroke: #44f;
}

@ -1,61 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import Scrubber from './Scrubber';
describe('<Scrubber>', () => {
const defaultProps = {
onMouseDown: sinon.spy(),
position: 0,
};
let wrapper;
beforeEach(() => {
wrapper = shallow(<Scrubber {...defaultProps} />);
});
it('contains the proper svg components', () => {
expect(
wrapper.matchesElement(
<g>
<g className="Scrubber--handles">
<rect className="Scrubber--handleExpansion" />
<rect className="Scrubber--handle" />
</g>
<line className="Scrubber--line" />
</g>
)
).toBeTruthy();
});
it('calculates the correct x% for a timestamp', () => {
wrapper = shallow(<Scrubber {...defaultProps} position={0.5} />);
const line = wrapper.find('line').first();
const rect = wrapper.find('rect').first();
expect(line.prop('x1')).toBe('50%');
expect(line.prop('x2')).toBe('50%');
expect(rect.prop('x')).toBe('50%');
});
it('supports onMouseDown', () => {
const event = {};
wrapper.find('.Scrubber--handles').prop('onMouseDown')(event);
expect(defaultProps.onMouseDown.calledWith(event)).toBeTruthy();
});
});

@ -1,64 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import cx from 'classnames';
import './Scrubber.css';
type ScrubberProps = {
isDragging: boolean;
position: number;
onMouseDown: (evt: React.MouseEvent<any>) => void;
onMouseEnter: (evt: React.MouseEvent<any>) => void;
onMouseLeave: (evt: React.MouseEvent<any>) => void;
};
export default function Scrubber({
isDragging,
onMouseDown,
onMouseEnter,
onMouseLeave,
position,
}: ScrubberProps) {
const xPercent = `${position * 100}%`;
const className = cx('Scrubber', { isDragging });
return (
<g className={className}>
<g
className="Scrubber--handles"
onMouseDown={onMouseDown}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{/* handleExpansion is only visible when `isDragging` is true */}
<rect
x={xPercent}
className="Scrubber--handleExpansion"
style={{ transform: `translate(-4.5px)` }}
width="9"
height="20"
/>
<rect
x={xPercent}
className="Scrubber--handle"
style={{ transform: `translate(-1.5px)` }}
width="3"
height="20"
/>
</g>
<line className="Scrubber--line" y2="100%" x1={xPercent} x2={xPercent} />
</g>
);
}

@ -1,27 +0,0 @@
/*
Copyright (c) 2017 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.TickLabels {
height: 1rem;
position: relative;
}
.TickLabels--label {
color: #717171;
font-size: 0.7rem;
position: absolute;
user-select: none;
}

@ -1,59 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { shallow } from 'enzyme';
import TickLabels from './TickLabels';
describe('<TickLabels>', () => {
const defaultProps = {
numTicks: 4,
duration: 5000,
};
let wrapper;
let ticks;
beforeEach(() => {
wrapper = shallow(<TickLabels {...defaultProps} />);
ticks = wrapper.find('[data-test="tick"]');
});
it('renders the right number of ticks', () => {
expect(ticks.length).toBe(defaultProps.numTicks + 1);
});
it('places the first tick on the left', () => {
const firstTick = ticks.first();
expect(firstTick.prop('style')).toEqual({ left: '0%' });
});
it('places the last tick on the right', () => {
const lastTick = ticks.last();
expect(lastTick.prop('style')).toEqual({ right: '0%' });
});
it('places middle ticks at proper intervals', () => {
const positions = ['25%', '50%', '75%'];
positions.forEach((pos, i) => {
const tick = ticks.at(i + 1);
expect(tick.prop('style')).toEqual({ left: pos });
});
});
it("doesn't explode if no trace is present", () => {
expect(() => shallow(<TickLabels {...defaultProps} trace={null} />)).not.toThrow();
});
});

@ -1,41 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { formatDuration } from '../../../../utils/date';
import './TickLabels.css';
type TickLabelsProps = {
numTicks: number;
duration: number;
};
export default function TickLabels(props: TickLabelsProps) {
const { numTicks, duration } = props;
const ticks = [];
for (let i = 0; i < numTicks + 1; i++) {
const portion = i / numTicks;
const style = portion === 1 ? { right: '0%' } : { left: `${portion * 100}%` };
ticks.push(
<div key={portion} className="TickLabels--label" style={style} data-test="tick">
{formatDuration(duration * portion)}
</div>
);
}
return <div className="TickLabels">{ticks}</div>;
}

@ -1,75 +0,0 @@
/*
Copyright (c) 2017 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.ViewingLayer {
cursor: vertical-text;
position: relative;
z-index: 1;
}
.ViewingLayer--graph {
border: 1px solid #999;
/* need !important here to overcome something from semantic UI */
overflow: visible !important;
position: relative;
transform-origin: 0 0;
width: 100%;
}
.ViewingLayer--inactive {
fill: rgba(214, 214, 214, 0.5);
}
.ViewingLayer--cursorGuide {
stroke: #f44;
stroke-width: 1;
}
.ViewingLayer--draggedShift {
fill-opacity: 0.2;
}
.ViewingLayer--draggedShift.isShiftDrag,
.ViewingLayer--draggedEdge.isShiftDrag {
fill: #44f;
}
.ViewingLayer--draggedShift.isReframeDrag,
.ViewingLayer--draggedEdge.isReframeDrag {
fill: #f44;
}
.ViewingLayer--fullOverlay {
bottom: 0;
cursor: col-resize;
left: 0;
position: fixed;
right: 0;
top: 0;
user-select: none;
}
.ViewingLayer--resetZoom {
display: none;
position: absolute;
right: 1%;
top: 10%;
z-index: 1;
}
.ViewingLayer:hover > .ViewingLayer--resetZoom {
display: unset;
}

@ -1,328 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { shallow } from 'enzyme';
import React from 'react';
import GraphTicks from './GraphTicks';
import Scrubber from './Scrubber';
import ViewingLayer, { dragTypes } from './ViewingLayer';
import { EUpdateTypes } from '../../../../utils/DraggableManager';
import { polyfill as polyfillAnimationFrame } from '../../../../utils/test/requestAnimationFrame';
function getViewRange(viewStart, viewEnd) {
return {
time: {
current: [viewStart, viewEnd],
},
};
}
describe('<SpanGraph>', () => {
polyfillAnimationFrame(window);
let props;
let wrapper;
beforeEach(() => {
props = {
height: 60,
numTicks: 5,
updateNextViewRangeTime: jest.fn(),
updateViewRangeTime: jest.fn(),
viewRange: getViewRange(0, 1),
};
wrapper = shallow(<ViewingLayer {...props} />);
});
describe('_getDraggingBounds()', () => {
beforeEach(() => {
props = { ...props, viewRange: getViewRange(0.1, 0.9) };
wrapper = shallow(<ViewingLayer {...props} />);
wrapper.instance()._setRoot({
getBoundingClientRect() {
return { left: 10, width: 100 };
},
});
});
it('throws if _root is not set', () => {
const instance = wrapper.instance();
instance._root = null;
expect(() => instance._getDraggingBounds(dragTypes.REFRAME)).toThrow();
});
it('returns the correct bounds for reframe', () => {
const bounds = wrapper.instance()._getDraggingBounds(dragTypes.REFRAME);
expect(bounds).toEqual({
clientXLeft: 10,
width: 100,
maxValue: 1,
minValue: 0,
});
});
it('returns the correct bounds for shiftStart', () => {
const bounds = wrapper.instance()._getDraggingBounds(dragTypes.SHIFT_START);
expect(bounds).toEqual({
clientXLeft: 10,
width: 100,
maxValue: 0.9,
minValue: 0,
});
});
it('returns the correct bounds for shiftEnd', () => {
const bounds = wrapper.instance()._getDraggingBounds(dragTypes.SHIFT_END);
expect(bounds).toEqual({
clientXLeft: 10,
width: 100,
maxValue: 1,
minValue: 0.1,
});
});
});
describe('DraggableManager callbacks', () => {
describe('reframe', () => {
it('handles mousemove', () => {
const value = 0.5;
wrapper.instance()._handleReframeMouseMove({ value });
const calls = props.updateNextViewRangeTime.mock.calls;
expect(calls).toEqual([[{ cursor: value }]]);
});
it('handles mouseleave', () => {
wrapper.instance()._handleReframeMouseLeave();
const calls = props.updateNextViewRangeTime.mock.calls;
expect(calls).toEqual([[{ cursor: null }]]);
});
describe('drag update', () => {
it('handles sans anchor', () => {
const value = 0.5;
wrapper.instance()._handleReframeDragUpdate({ value });
const calls = props.updateNextViewRangeTime.mock.calls;
expect(calls).toEqual([[{ reframe: { anchor: value, shift: value } }]]);
});
it('handles the existing anchor', () => {
const value = 0.5;
const anchor = 0.1;
const time = { ...props.viewRange.time, reframe: { anchor } };
props = { ...props, viewRange: { time } };
wrapper = shallow(<ViewingLayer {...props} />);
wrapper.instance()._handleReframeDragUpdate({ value });
const calls = props.updateNextViewRangeTime.mock.calls;
expect(calls).toEqual([[{ reframe: { anchor, shift: value } }]]);
});
});
describe('drag end', () => {
let manager;
beforeEach(() => {
manager = { resetBounds: jest.fn() };
});
it('handles sans anchor', () => {
const value = 0.5;
wrapper.instance()._handleReframeDragEnd({ manager, value });
expect(manager.resetBounds.mock.calls).toEqual([[]]);
const calls = props.updateViewRangeTime.mock.calls;
expect(calls).toEqual([[value, value, 'minimap']]);
});
it('handles dragged left (anchor is greater)', () => {
const value = 0.5;
const anchor = 0.6;
const time = { ...props.viewRange.time, reframe: { anchor } };
props = { ...props, viewRange: { time } };
wrapper = shallow(<ViewingLayer {...props} />);
wrapper.instance()._handleReframeDragEnd({ manager, value });
expect(manager.resetBounds.mock.calls).toEqual([[]]);
const calls = props.updateViewRangeTime.mock.calls;
expect(calls).toEqual([[value, anchor, 'minimap']]);
});
it('handles dragged right (anchor is less)', () => {
const value = 0.5;
const anchor = 0.4;
const time = { ...props.viewRange.time, reframe: { anchor } };
props = { ...props, viewRange: { time } };
wrapper = shallow(<ViewingLayer {...props} />);
wrapper.instance()._handleReframeDragEnd({ manager, value });
expect(manager.resetBounds.mock.calls).toEqual([[]]);
const calls = props.updateViewRangeTime.mock.calls;
expect(calls).toEqual([[anchor, value, 'minimap']]);
});
});
});
describe('scrubber', () => {
it('prevents the cursor from being drawn on scrubber mouseover', () => {
wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseEnter });
expect(wrapper.state('preventCursorLine')).toBe(true);
});
it('prevents the cursor from being drawn on scrubber mouseleave', () => {
wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseLeave });
expect(wrapper.state('preventCursorLine')).toBe(false);
});
describe('drag start and update', () => {
it('stops propagation on drag start', () => {
const stopPropagation = jest.fn();
const update = {
event: { stopPropagation },
type: EUpdateTypes.DragStart,
};
wrapper.instance()._handleScrubberDragUpdate(update);
expect(stopPropagation.mock.calls).toEqual([[]]);
});
it('updates the viewRange for shiftStart and shiftEnd', () => {
const instance = wrapper.instance();
const value = 0.5;
const cases = [
{
dragUpdate: {
value,
tag: dragTypes.SHIFT_START,
type: EUpdateTypes.DragMove,
},
viewRangeUpdate: { shiftStart: value },
},
{
dragUpdate: {
value,
tag: dragTypes.SHIFT_END,
type: EUpdateTypes.DragMove,
},
viewRangeUpdate: { shiftEnd: value },
},
];
cases.forEach(_case => {
instance._handleScrubberDragUpdate(_case.dragUpdate);
expect(props.updateNextViewRangeTime).lastCalledWith(_case.viewRangeUpdate);
});
});
});
it('updates the view on drag end', () => {
const instance = wrapper.instance();
const [viewStart, viewEnd] = props.viewRange.time.current;
const value = 0.5;
const cases = [
{
dragUpdate: {
value,
manager: { resetBounds: jest.fn() },
tag: dragTypes.SHIFT_START,
},
viewRangeUpdate: [value, viewEnd],
},
{
dragUpdate: {
value,
manager: { resetBounds: jest.fn() },
tag: dragTypes.SHIFT_END,
},
viewRangeUpdate: [viewStart, value],
},
];
cases.forEach(_case => {
const { manager } = _case.dragUpdate;
wrapper.setState({ preventCursorLine: true });
expect(wrapper.state('preventCursorLine')).toBe(true);
instance._handleScrubberDragEnd(_case.dragUpdate);
expect(wrapper.state('preventCursorLine')).toBe(false);
expect(manager.resetBounds.mock.calls).toEqual([[]]);
expect(props.updateViewRangeTime).lastCalledWith(..._case.viewRangeUpdate, 'minimap');
});
});
});
describe('.ViewingLayer--resetZoom', () => {
it('should not render .ViewingLayer--resetZoom if props.viewRange.time.current = [0,1]', () => {
expect(wrapper.find('.ViewingLayer--resetZoom').length).toBe(0);
wrapper.setProps({ viewRange: { time: { current: [0, 1] } } });
expect(wrapper.find('.ViewingLayer--resetZoom').length).toBe(0);
});
it('should render ViewingLayer--resetZoom if props.viewRange.time.current[0] !== 0', () => {
// If the test fails on the following expect statement, this may be a false negative
expect(wrapper.find('.ViewingLayer--resetZoom').length).toBe(0);
wrapper.setProps({ viewRange: { time: { current: [0.1, 1] } } });
expect(wrapper.find('.ViewingLayer--resetZoom').length).toBe(1);
});
it('should render ViewingLayer--resetZoom if props.viewRange.time.current[1] !== 1', () => {
// If the test fails on the following expect statement, this may be a false negative
expect(wrapper.find('.ViewingLayer--resetZoom').length).toBe(0);
wrapper.setProps({ viewRange: { time: { current: [0, 0.9] } } });
expect(wrapper.find('.ViewingLayer--resetZoom').length).toBe(1);
});
it('should call props.updateViewRangeTime when clicked', () => {
wrapper.setProps({ viewRange: { time: { current: [0.1, 0.9] } } });
const resetZoomButton = wrapper.find('.ViewingLayer--resetZoom');
// If the test fails on the following expect statement, this may be a false negative caused
// by a regression to rendering.
expect(resetZoomButton.length).toBe(1);
resetZoomButton.simulate('click');
expect(props.updateViewRangeTime).lastCalledWith(0, 1);
});
});
});
it('renders a <GraphTicks />', () => {
expect(wrapper.find(GraphTicks).length).toBe(1);
});
it('renders a filtering box if leftBound exists', () => {
const _props = { ...props, viewRange: getViewRange(0.2, 1) };
wrapper = shallow(<ViewingLayer {..._props} />);
const leftBox = wrapper.find('.ViewingLayer--inactive');
expect(leftBox.length).toBe(1);
const width = Number(leftBox.prop('width').slice(0, -1));
const x = leftBox.prop('x');
expect(Math.round(width)).toBe(20);
expect(x).toBe(0);
});
it('renders a filtering box if rightBound exists', () => {
const _props = { ...props, viewRange: getViewRange(0, 0.8) };
wrapper = shallow(<ViewingLayer {..._props} />);
const rightBox = wrapper.find('.ViewingLayer--inactive');
expect(rightBox.length).toBe(1);
const width = Number(rightBox.prop('width').slice(0, -1));
const x = Number(rightBox.prop('x').slice(0, -1));
expect(Math.round(width)).toBe(20);
expect(x).toBe(80);
});
it('renders handles for the timeRangeFilter', () => {
const [viewStart, viewEnd] = props.viewRange.time.current;
let scrubber = <Scrubber position={viewStart} />;
expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy();
scrubber = <Scrubber position={viewEnd} />;
expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy();
});
});

@ -1,351 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Button } from 'antd';
import cx from 'classnames';
import * as React from 'react';
import GraphTicks from './GraphTicks';
import Scrubber from './Scrubber';
import { TUpdateViewRangeTimeFunction, IViewRange, ViewRangeTimeUpdate } from '../../types';
import { TNil } from '../../../../types';
import DraggableManager, {
DraggableBounds,
DraggingUpdate,
EUpdateTypes,
} from '../../../../utils/DraggableManager';
import './ViewingLayer.css';
type ViewingLayerProps = {
height: number;
numTicks: number;
updateViewRangeTime: TUpdateViewRangeTimeFunction;
updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void;
viewRange: IViewRange;
};
type ViewingLayerState = {
/**
* Cursor line should not be drawn when the mouse is over the scrubber handle.
*/
preventCursorLine: boolean;
};
/**
* Designate the tags for the different dragging managers. Exported for tests.
*/
export const dragTypes = {
/**
* Tag for dragging the right scrubber, e.g. end of the current view range.
*/
SHIFT_END: 'SHIFT_END',
/**
* Tag for dragging the left scrubber, e.g. start of the current view range.
*/
SHIFT_START: 'SHIFT_START',
/**
* Tag for dragging a new view range.
*/
REFRAME: 'REFRAME',
};
/**
* Returns the layout information for drawing the view-range differential, e.g.
* show what will change when the mouse is released. Basically, this is the
* difference from the start of the drag to the current position.
*
* @returns {{ x: string, width: string, leadginX: string }}
*/
function getNextViewLayout(start: number, position: number) {
const [left, right] = start < position ? [start, position] : [position, start];
return {
x: `${left * 100}%`,
width: `${(right - left) * 100}%`,
leadingX: `${position * 100}%`,
};
}
/**
* `ViewingLayer` is rendered on top of the Canvas rendering of the minimap and
* handles showing the current view range and handles mouse UX for modifying it.
*/
export default class ViewingLayer extends React.PureComponent<ViewingLayerProps, ViewingLayerState> {
state: ViewingLayerState;
_root: Element | TNil;
/**
* `_draggerReframe` handles clicking and dragging on the `ViewingLayer` to
* redefined the view range.
*/
_draggerReframe: DraggableManager;
/**
* `_draggerStart` handles dragging the left scrubber to adjust the start of
* the view range.
*/
_draggerStart: DraggableManager;
/**
* `_draggerEnd` handles dragging the right scrubber to adjust the end of
* the view range.
*/
_draggerEnd: DraggableManager;
constructor(props: ViewingLayerProps) {
super(props);
this._draggerReframe = new DraggableManager({
getBounds: this._getDraggingBounds,
onDragEnd: this._handleReframeDragEnd,
onDragMove: this._handleReframeDragUpdate,
onDragStart: this._handleReframeDragUpdate,
onMouseMove: this._handleReframeMouseMove,
onMouseLeave: this._handleReframeMouseLeave,
tag: dragTypes.REFRAME,
});
this._draggerStart = new DraggableManager({
getBounds: this._getDraggingBounds,
onDragEnd: this._handleScrubberDragEnd,
onDragMove: this._handleScrubberDragUpdate,
onDragStart: this._handleScrubberDragUpdate,
onMouseEnter: this._handleScrubberEnterLeave,
onMouseLeave: this._handleScrubberEnterLeave,
tag: dragTypes.SHIFT_START,
});
this._draggerEnd = new DraggableManager({
getBounds: this._getDraggingBounds,
onDragEnd: this._handleScrubberDragEnd,
onDragMove: this._handleScrubberDragUpdate,
onDragStart: this._handleScrubberDragUpdate,
onMouseEnter: this._handleScrubberEnterLeave,
onMouseLeave: this._handleScrubberEnterLeave,
tag: dragTypes.SHIFT_END,
});
this._root = undefined;
this.state = {
preventCursorLine: false,
};
}
componentWillUnmount() {
this._draggerReframe.dispose();
this._draggerEnd.dispose();
this._draggerStart.dispose();
}
_setRoot = (elm: SVGElement | TNil) => {
this._root = elm;
};
_getDraggingBounds = (tag: string | TNil): DraggableBounds => {
if (!this._root) {
throw new Error('invalid state');
}
const { left: clientXLeft, width } = this._root.getBoundingClientRect();
const [viewStart, viewEnd] = this.props.viewRange.time.current;
let maxValue = 1;
let minValue = 0;
if (tag === dragTypes.SHIFT_START) {
maxValue = viewEnd;
} else if (tag === dragTypes.SHIFT_END) {
minValue = viewStart;
}
return { clientXLeft, maxValue, minValue, width };
};
_handleReframeMouseMove = ({ value }: DraggingUpdate) => {
this.props.updateNextViewRangeTime({ cursor: value });
};
_handleReframeMouseLeave = () => {
this.props.updateNextViewRangeTime({ cursor: null });
};
_handleReframeDragUpdate = ({ value }: DraggingUpdate) => {
const shift = value;
const { time } = this.props.viewRange;
const anchor = time.reframe ? time.reframe.anchor : shift;
const update = { reframe: { anchor, shift } };
this.props.updateNextViewRangeTime(update);
};
_handleReframeDragEnd = ({ manager, value }: DraggingUpdate) => {
const { time } = this.props.viewRange;
const anchor = time.reframe ? time.reframe.anchor : value;
const [start, end] = value < anchor ? [value, anchor] : [anchor, value];
manager.resetBounds();
this.props.updateViewRangeTime(start, end, 'minimap');
};
_handleScrubberEnterLeave = ({ type }: DraggingUpdate) => {
const preventCursorLine = type === EUpdateTypes.MouseEnter;
this.setState({ preventCursorLine });
};
_handleScrubberDragUpdate = ({ event, tag, type, value }: DraggingUpdate) => {
if (type === EUpdateTypes.DragStart) {
event.stopPropagation();
}
if (tag === dragTypes.SHIFT_START) {
this.props.updateNextViewRangeTime({ shiftStart: value });
} else if (tag === dragTypes.SHIFT_END) {
this.props.updateNextViewRangeTime({ shiftEnd: value });
}
};
_handleScrubberDragEnd = ({ manager, tag, value }: DraggingUpdate) => {
const [viewStart, viewEnd] = this.props.viewRange.time.current;
let update: [number, number];
if (tag === dragTypes.SHIFT_START) {
update = [value, viewEnd];
} else if (tag === dragTypes.SHIFT_END) {
update = [viewStart, value];
} else {
// to satisfy flow
throw new Error('bad state');
}
manager.resetBounds();
this.setState({ preventCursorLine: false });
this.props.updateViewRangeTime(update[0], update[1], 'minimap');
};
/**
* Resets the zoom to fully zoomed out.
*/
_resetTimeZoomClickHandler = () => {
this.props.updateViewRangeTime(0, 1);
};
/**
* Renders the difference between where the drag started and the current
* position, e.g. the red or blue highlight.
*
* @returns React.Node[]
*/
_getMarkers(from: number, to: number, isShift: boolean) {
const layout = getNextViewLayout(from, to);
const cls = cx({
isShiftDrag: isShift,
isReframeDrag: !isShift,
});
return [
<rect
key="fill"
className={`ViewingLayer--draggedShift ${cls}`}
x={layout.x}
y="0"
width={layout.width}
height={this.props.height - 2}
/>,
<rect
key="edge"
className={`ViewingLayer--draggedEdge ${cls}`}
x={layout.leadingX}
y="0"
width="1"
height={this.props.height - 2}
/>,
];
}
render() {
const { height, viewRange, numTicks } = this.props;
const { preventCursorLine } = this.state;
const { current, cursor, shiftStart, shiftEnd, reframe } = viewRange.time;
const haveNextTimeRange = shiftStart != null || shiftEnd != null || reframe != null;
const [viewStart, viewEnd] = current;
let leftInactive = 0;
if (viewStart) {
leftInactive = viewStart * 100;
}
let rightInactive = 100;
if (viewEnd) {
rightInactive = 100 - viewEnd * 100;
}
let cursorPosition: string | undefined;
if (!haveNextTimeRange && cursor != null && !preventCursorLine) {
cursorPosition = `${cursor * 100}%`;
}
return (
<div aria-hidden className="ViewingLayer" style={{ height }}>
{(viewStart !== 0 || viewEnd !== 1) && (
<Button
onClick={this._resetTimeZoomClickHandler}
className="ViewingLayer--resetZoom"
htmlType="button"
>
Reset Selection
</Button>
)}
<svg
height={height}
className="ViewingLayer--graph"
ref={this._setRoot}
onMouseDown={this._draggerReframe.handleMouseDown}
onMouseLeave={this._draggerReframe.handleMouseLeave}
onMouseMove={this._draggerReframe.handleMouseMove}
>
{leftInactive > 0 && (
<rect x={0} y={0} height="100%" width={`${leftInactive}%`} className="ViewingLayer--inactive" />
)}
{rightInactive > 0 && (
<rect
x={`${100 - rightInactive}%`}
y={0}
height="100%"
width={`${rightInactive}%`}
className="ViewingLayer--inactive"
/>
)}
<GraphTicks numTicks={numTicks} />
{cursorPosition && (
<line
className="ViewingLayer--cursorGuide"
x1={cursorPosition}
y1="0"
x2={cursorPosition}
y2={height - 2}
strokeWidth="1"
/>
)}
{shiftStart != null && this._getMarkers(viewStart, shiftStart, true)}
{shiftEnd != null && this._getMarkers(viewEnd, shiftEnd, true)}
<Scrubber
isDragging={shiftStart != null}
onMouseDown={this._draggerStart.handleMouseDown}
onMouseEnter={this._draggerStart.handleMouseEnter}
onMouseLeave={this._draggerStart.handleMouseLeave}
position={viewStart || 0}
/>
<Scrubber
isDragging={shiftEnd != null}
position={viewEnd || 1}
onMouseDown={this._draggerEnd.handleMouseDown}
onMouseEnter={this._draggerEnd.handleMouseEnter}
onMouseLeave={this._draggerEnd.handleMouseLeave}
/>
{reframe != null && this._getMarkers(reframe.anchor, reframe.shift, false)}
</svg>
{/* fullOverlay updates the mouse cursor blocks mouse events */}
{haveNextTimeRange && <div className="ViewingLayer--fullOverlay" />}
</div>
);
}
}

@ -1,76 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { shallow } from 'enzyme';
import CanvasSpanGraph from './CanvasSpanGraph';
import SpanGraph from './index';
import TickLabels from './TickLabels';
import ViewingLayer from './ViewingLayer';
import traceGenerator from '../../../../demo/trace-generators';
import transformTraceData from '../../../../model/transform-trace-data';
import { polyfill as polyfillAnimationFrame } from '../../../../utils/test/requestAnimationFrame';
describe('<SpanGraph>', () => {
polyfillAnimationFrame(window);
const trace = transformTraceData(traceGenerator.trace({}));
const props = {
trace,
updateViewRangeTime: () => {},
viewRange: {
time: {
current: [0, 1],
},
},
};
let wrapper;
beforeEach(() => {
wrapper = shallow(<SpanGraph {...props} />);
});
it('renders a <CanvasSpanGraph />', () => {
expect(wrapper.find(CanvasSpanGraph).length).toBe(1);
});
it('renders a <TickLabels />', () => {
expect(wrapper.find(TickLabels).length).toBe(1);
});
it('returns a <div> if a trace is not provided', () => {
wrapper = shallow(<SpanGraph {...props} trace={null} />);
expect(wrapper.matchesElement(<div />)).toBeTruthy();
});
it('passes the number of ticks to render to components', () => {
const tickHeader = wrapper.find(TickLabels);
const viewingLayer = wrapper.find(ViewingLayer);
expect(tickHeader.prop('numTicks')).toBeGreaterThan(1);
expect(viewingLayer.prop('numTicks')).toBeGreaterThan(1);
expect(tickHeader.prop('numTicks')).toBe(viewingLayer.prop('numTicks'));
});
it('passes items to CanvasSpanGraph', () => {
const canvasGraph = wrapper.find(CanvasSpanGraph).first();
const items = trace.spans.map(span => ({
valueOffset: span.relativeStartTime,
valueWidth: span.duration,
serviceName: span.process.serviceName,
}));
expect(canvasGraph.prop('items')).toEqual(items);
});
});

@ -1,100 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as React from 'react';
import CanvasSpanGraph from './CanvasSpanGraph';
import TickLabels from './TickLabels';
import ViewingLayer from './ViewingLayer';
import { TUpdateViewRangeTimeFunction, IViewRange, ViewRangeTimeUpdate } from '../../types';
import { Span, Trace } from '../../../../types/trace';
const DEFAULT_HEIGHT = 60;
const TIMELINE_TICK_INTERVAL = 4;
type SpanGraphProps = {
height?: number;
trace: Trace;
viewRange: IViewRange;
updateViewRangeTime: TUpdateViewRangeTimeFunction;
updateNextViewRangeTime: (nextUpdate: ViewRangeTimeUpdate) => void;
};
/**
* Store `items` in state so they are not regenerated every render. Otherwise,
* the canvas graph will re-render itself every time.
*/
type SpanGraphState = {
items: {
valueOffset: number;
valueWidth: number;
serviceName: string;
}[];
};
function getItem(span: Span) {
return {
valueOffset: span.relativeStartTime,
valueWidth: span.duration,
serviceName: span.process.serviceName,
};
}
export default class SpanGraph extends React.PureComponent<SpanGraphProps, SpanGraphState> {
state: SpanGraphState;
static defaultProps = {
height: DEFAULT_HEIGHT,
};
constructor(props: SpanGraphProps) {
super(props);
const { trace } = props;
this.state = {
items: trace ? trace.spans.map(getItem) : [],
};
}
componentWillReceiveProps(nextProps: SpanGraphProps) {
const { trace } = nextProps;
if (this.props.trace !== trace) {
this.setState({
items: trace ? trace.spans.map(getItem) : [],
});
}
}
render() {
const { height, trace, viewRange, updateNextViewRangeTime, updateViewRangeTime } = this.props;
if (!trace) {
return <div />;
}
const { items } = this.state;
return (
<div className="ub-pb2 ub-px2">
<TickLabels numTicks={TIMELINE_TICK_INTERVAL} duration={trace.duration} />
<div className="ub-relative">
<CanvasSpanGraph valueWidth={trace.duration} items={items} />
<ViewingLayer
viewRange={viewRange}
numTicks={TIMELINE_TICK_INTERVAL}
height={height || DEFAULT_HEIGHT}
updateViewRangeTime={updateViewRangeTime}
updateNextViewRangeTime={updateNextViewRangeTime}
/>
</div>
</div>
);
}
}

@ -1,199 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import _range from 'lodash/range';
import renderIntoCanvas, {
BG_COLOR,
ITEM_ALPHA,
MIN_ITEM_HEIGHT,
MAX_TOTAL_HEIGHT,
MIN_ITEM_WIDTH,
MIN_TOTAL_HEIGHT,
MAX_ITEM_HEIGHT,
} from './render-into-canvas';
const getCanvasWidth = () => window.innerWidth * 2;
const getBgFillRect = items => ({
fillStyle: BG_COLOR,
height:
!items || items.length < MIN_TOTAL_HEIGHT ? MIN_TOTAL_HEIGHT : Math.min(MAX_TOTAL_HEIGHT, items.length),
width: getCanvasWidth(),
x: 0,
y: 0,
});
describe('renderIntoCanvas()', () => {
const basicItem = { valueWidth: 100, valueOffset: 50, serviceName: 'some-name' };
class CanvasContext {
constructor() {
this.fillStyle = undefined;
this.fillRectAccumulator = [];
}
fillRect(x, y, width, height) {
const fillStyle = this.fillStyle;
this.fillRectAccumulator.push({
fillStyle,
height,
width,
x,
y,
});
}
}
class Canvas {
constructor() {
this.contexts = [];
this.height = NaN;
this.width = NaN;
this.getContext = jest.fn(this._getContext.bind(this));
}
_getContext() {
const ctx = new CanvasContext();
this.contexts.push(ctx);
return ctx;
}
}
function getColorFactory() {
let i = 0;
const inputOutput = [];
function getFakeColor(str) {
const rv = [i, i, i];
i++;
inputOutput.push({
input: str,
output: rv.slice(),
});
return rv;
}
getFakeColor.inputOutput = inputOutput;
return getFakeColor;
}
it('sets the width', () => {
const canvas = new Canvas();
expect(canvas.width !== canvas.width).toBe(true);
renderIntoCanvas(canvas, [basicItem], 150, getColorFactory());
expect(canvas.width).toBe(getCanvasWidth());
});
describe('when there are limited number of items', () => {
it('sets the height', () => {
const canvas = new Canvas();
expect(canvas.height !== canvas.height).toBe(true);
renderIntoCanvas(canvas, [basicItem], 150, getColorFactory());
expect(canvas.height).toBe(MIN_TOTAL_HEIGHT);
});
it('draws the background', () => {
const expectedDrawing = [getBgFillRect()];
const canvas = new Canvas();
const items = [];
const totalValueWidth = 4000;
const getFillColor = getColorFactory();
renderIntoCanvas(canvas, items, totalValueWidth, getFillColor);
expect(canvas.getContext.mock.calls).toEqual([['2d', { alpha: false }]]);
expect(canvas.contexts.length).toBe(1);
expect(canvas.contexts[0].fillRectAccumulator).toEqual(expectedDrawing);
});
it('draws the map', () => {
const totalValueWidth = 4000;
const items = [
{ valueWidth: 50, valueOffset: 50, serviceName: 'service-name-0' },
{ valueWidth: 100, valueOffset: 100, serviceName: 'service-name-1' },
{ valueWidth: 150, valueOffset: 150, serviceName: 'service-name-2' },
];
const expectedColors = [
{ input: items[0].serviceName, output: [0, 0, 0] },
{ input: items[1].serviceName, output: [1, 1, 1] },
{ input: items[2].serviceName, output: [2, 2, 2] },
];
const cHeight =
items.length < MIN_TOTAL_HEIGHT ? MIN_TOTAL_HEIGHT : Math.min(items.length, MAX_TOTAL_HEIGHT);
const expectedDrawings = [
getBgFillRect(),
...items.map((item, i) => {
const { valueWidth, valueOffset } = item;
const color = expectedColors[i].output;
const fillStyle = `rgba(${color.concat(ITEM_ALPHA).join()})`;
const height = Math.min(MAX_ITEM_HEIGHT, Math.max(MIN_ITEM_HEIGHT, cHeight / items.length));
const width = (valueWidth / totalValueWidth) * getCanvasWidth();
const x = (valueOffset / totalValueWidth) * getCanvasWidth();
const y = (cHeight / items.length) * i;
return { fillStyle, height, width, x, y };
}),
];
const canvas = new Canvas();
const getFillColor = getColorFactory();
renderIntoCanvas(canvas, items, totalValueWidth, getFillColor);
expect(getFillColor.inputOutput).toEqual(expectedColors);
expect(canvas.getContext.mock.calls).toEqual([['2d', { alpha: false }]]);
expect(canvas.contexts.length).toBe(1);
expect(canvas.contexts[0].fillRectAccumulator).toEqual(expectedDrawings);
});
});
describe('when there are many items', () => {
it('sets the height when there are many items', () => {
const canvas = new Canvas();
const items = [];
for (let i = 0; i < MIN_TOTAL_HEIGHT + 1; i++) {
items.push(basicItem);
}
expect(canvas.height !== canvas.height).toBe(true);
renderIntoCanvas(canvas, items, 150, getColorFactory());
expect(canvas.height).toBe(items.length);
});
it('draws the map', () => {
const totalValueWidth = 4000;
const items = _range(MIN_TOTAL_HEIGHT * 10).map(i => ({
valueWidth: i,
valueOffset: i,
serviceName: `service-name-${i}`,
}));
const expectedColors = items.map((item, i) => ({
input: item.serviceName,
output: [i, i, i],
}));
const expectedDrawings = [
getBgFillRect(items),
...items.map((item, i) => {
const { valueWidth, valueOffset } = item;
const color = expectedColors[i].output;
const fillStyle = `rgba(${color.concat(ITEM_ALPHA).join()})`;
const height = MIN_ITEM_HEIGHT;
const width = Math.max(MIN_ITEM_WIDTH, (valueWidth / totalValueWidth) * getCanvasWidth());
const x = (valueOffset / totalValueWidth) * getCanvasWidth();
const y = (MAX_TOTAL_HEIGHT / items.length) * i;
return { fillStyle, height, width, x, y };
}),
];
const canvas = new Canvas();
const getFillColor = getColorFactory();
renderIntoCanvas(canvas, items, totalValueWidth, getFillColor);
expect(getFillColor.inputOutput).toEqual(expectedColors);
expect(canvas.getContext.mock.calls).toEqual([['2d', { alpha: false }]]);
expect(canvas.contexts.length).toBe(1);
expect(canvas.contexts[0].fillRectAccumulator).toEqual(expectedDrawings);
});
});
});

@ -1,63 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { TNil } from '../../../../types';
// exported for tests
export const BG_COLOR = '#fff';
export const ITEM_ALPHA = 0.8;
export const MIN_ITEM_HEIGHT = 2;
export const MAX_TOTAL_HEIGHT = 200;
export const MIN_ITEM_WIDTH = 10;
export const MIN_TOTAL_HEIGHT = 60;
export const MAX_ITEM_HEIGHT = 6;
export default function renderIntoCanvas(
canvas: HTMLCanvasElement,
items: { valueWidth: number; valueOffset: number; serviceName: string }[],
totalValueWidth: number,
getFillColor: (serviceName: string) => [number, number, number]
) {
const fillCache: Map<string, string | TNil> = new Map();
const cHeight =
items.length < MIN_TOTAL_HEIGHT ? MIN_TOTAL_HEIGHT : Math.min(items.length, MAX_TOTAL_HEIGHT);
const cWidth = window.innerWidth * 2;
// eslint-disable-next-line no-param-reassign
canvas.width = cWidth;
// eslint-disable-next-line no-param-reassign
canvas.height = cHeight;
const itemHeight = Math.min(MAX_ITEM_HEIGHT, Math.max(MIN_ITEM_HEIGHT, cHeight / items.length));
const itemYChange = cHeight / items.length;
const ctx = canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D;
ctx.fillStyle = BG_COLOR;
ctx.fillRect(0, 0, cWidth, cHeight);
for (let i = 0; i < items.length; i++) {
const { valueWidth, valueOffset, serviceName } = items[i];
const x = (valueOffset / totalValueWidth) * cWidth;
let width = (valueWidth / totalValueWidth) * cWidth;
if (width < MIN_ITEM_WIDTH) {
width = MIN_ITEM_WIDTH;
}
let fillStyle = fillCache.get(serviceName);
if (!fillStyle) {
fillStyle = `rgba(${getFillColor(serviceName)
.concat(ITEM_ALPHA)
.join()})`;
fillCache.set(serviceName, fillStyle);
}
ctx.fillStyle = fillStyle;
ctx.fillRect(x, i * itemYChange, width, itemHeight);
}
}

@ -1,110 +0,0 @@
/*
Copyright (c) 2017 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.TracePageHeader > :first-child {
border-bottom: 1px solid #e8e8e8;
}
.TracePageHeader > :nth-child(2) {
background-color: #eee;
border-bottom: 1px solid #e4e4e4;
}
.TracePageHeader > :last-child {
background-color: #f8f8f8;
border-bottom: 1px solid #ccc;
}
/* boost the specificity for cases with only one row -- bg should be white */
.TracePageHeader > .TracePageHeader--titleRow {
align-items: center;
background-color: #fff;
display: flex;
}
.TracePageHeader--back {
align-items: center;
align-self: stretch;
background-color: #fafafa;
border-bottom: 1px solid #ddd;
border-right: 1px solid #ddd;
color: inherit;
display: flex;
font-size: 1.4rem;
padding: 0 1rem;
margin-bottom: -1px;
}
.TracePageHeader--back:hover {
background-color: #f0f0f0;
border-color: #ccc;
}
.TracePageHeader--titleLink {
align-items: center;
color: var(--tx-color-title);
display: flex;
flex: 1;
}
.TracePageHeader--titleLink:hover * {
text-decoration: underline;
}
.TracePageHeader--titleLink:hover > *,
.TracePageHeader--titleLink:hover small {
text-decoration: none;
}
.TracePageHeader--detailToggle {
font-size: 2.5rem;
transition: transform 0.07s ease-out;
}
.TracePageHeader--detailToggle.is-expanded {
transform: rotate(90deg);
}
.TracePageHeader--title {
color: inherit;
flex: 1;
font-size: 1.7em;
line-height: 1em;
margin: 0 0 0 0.5em;
padding: 0.5em 0;
}
.TracePageHeader--title.is-collapsible {
margin-left: 0;
}
.TracePageHeader--overviewItems {
border-bottom: 1px solid #e4e4e4;
padding: 0.25rem 0.5rem;
}
.TracePageHeader--overviewItem--valueDetail {
color: #aaa;
}
.TracePageHeader--overviewItem--value:hover > .TracePageHeader--overviewItem--valueDetail {
color: unset;
}
.TracePageHeader--archiveIcon {
font-size: 1.78em;
margin-right: 0.15em;
}

@ -1,135 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { shallow, mount } from 'enzyme';
import { Link } from 'react-router-dom';
import AltViewOptions from './AltViewOptions';
import KeyboardShortcutsHelp from './KeyboardShortcutsHelp';
import SpanGraph from './SpanGraph';
import { TracePageHeaderFn as TracePageHeader, HEADER_ITEMS } from './TracePageHeader';
import LabeledList from '../../common/LabeledList';
import traceGenerator from '../../../demo/trace-generators';
import { getTraceName } from '../../../model/trace-viewer';
import transformTraceData from '../../../model/transform-trace-data';
describe('<TracePageHeader>', () => {
const trace = transformTraceData(traceGenerator.trace({}));
const defaultProps = {
trace,
showArchiveButton: false,
showShortcutsHelp: false,
showStandaloneLink: false,
showViewOptions: false,
textFilter: '',
updateTextFilter: () => {},
};
let wrapper;
beforeEach(() => {
wrapper = shallow(<TracePageHeader {...defaultProps} />);
});
it('renders a <header />', () => {
expect(wrapper.find('header').length).toBe(1);
});
it('renders an empty <div> if a trace is not present', () => {
wrapper = mount(<TracePageHeader {...defaultProps} trace={null} />);
expect(wrapper.children().length).toBe(0);
});
it('renders the trace title', () => {
expect(wrapper.find({ traceName: getTraceName(trace.spans) })).toBeTruthy();
});
it('renders the header items', () => {
wrapper.find('.horizontal .item').forEach((item, i) => {
expect(item.contains(HEADER_ITEMS[i].title)).toBeTruthy();
expect(item.contains(HEADER_ITEMS[i].renderer(defaultProps.trace))).toBeTruthy();
});
});
it('renders a <SpanGraph>', () => {
expect(wrapper.find(SpanGraph).length).toBe(1);
});
describe('observes the visibility toggles for various UX elements', () => {
it('hides the minimap when hideMap === true', () => {
expect(wrapper.find(SpanGraph).length).toBe(1);
wrapper.setProps({ hideMap: true });
expect(wrapper.find(SpanGraph).length).toBe(0);
});
it('hides the summary when hideSummary === true', () => {
expect(wrapper.find(LabeledList).length).toBe(1);
wrapper.setProps({ hideSummary: true });
expect(wrapper.find(LabeledList).length).toBe(0);
});
it('toggles the archive button', () => {
const onArchiveClicked = () => {};
const props = {
onArchiveClicked,
showArchiveButton: true,
};
wrapper.setProps(props);
expect(wrapper.find({ onClick: onArchiveClicked }).length).toBe(1);
props.showArchiveButton = false;
wrapper.setProps(props);
expect(wrapper.find({ onClick: onArchiveClicked }).length).toBe(0);
});
it('toggles <KeyboardShortcutsHelp />', () => {
const props = { showShortcutsHelp: true };
wrapper.setProps(props);
expect(wrapper.find(KeyboardShortcutsHelp).length).toBe(1);
props.showShortcutsHelp = false;
wrapper.setProps(props);
expect(wrapper.find(KeyboardShortcutsHelp).length).toBe(0);
});
it('toggles <AltViewOptions />', () => {
const props = { showViewOptions: true };
wrapper.setProps(props);
expect(wrapper.find(AltViewOptions).length).toBe(1);
props.showViewOptions = false;
wrapper.setProps(props);
expect(wrapper.find(AltViewOptions).length).toBe(0);
});
it('renders the link to search', () => {
expect(wrapper.find(Link).length).toBe(0);
const toSearch = 'some-link';
wrapper.setProps({ toSearch });
expect(wrapper.find({ to: toSearch }).length).toBe(1);
});
it('toggles the standalone link', () => {
const linkToStandalone = 'some-link';
const props = {
linkToStandalone,
showStandaloneLink: true,
};
wrapper.setProps(props);
expect(wrapper.find({ to: linkToStandalone }).length).toBe(1);
props.showStandaloneLink = false;
wrapper.setProps(props);
expect(wrapper.find({ to: linkToStandalone }).length).toBe(0);
});
});
});

@ -1,81 +0,0 @@
// Copyright (c) 2019 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/* eslint-disable import/first */
jest.mock('../../../utils/tracking');
import * as track from './TracePageHeader.track'; /* {
CATEGORY_ALT_VIEW,
CATEGORY_SLIM_HEADER,
ACTION_GANTT,
ACTION_GRAPH,
ACTION_JSON,
ACTION_RAW_JSON,
} from './index.track'; */
import { trackEvent } from '../../../utils/tracking';
import { OPEN, CLOSE } from '../../../utils/tracking/common';
describe('TracePageHeader.track', () => {
beforeEach(trackEvent.mockClear);
const cases = [
{
action: track.ACTION_GANTT,
category: track.CATEGORY_ALT_VIEW,
msg: 'tracks a GA event for viewing gantt chart',
fn: 'trackGanttView',
},
{
action: track.ACTION_GRAPH,
category: track.CATEGORY_ALT_VIEW,
msg: 'tracks a GA event for viewing trace graph',
fn: 'trackGraphView',
},
{
action: track.ACTION_JSON,
category: track.CATEGORY_ALT_VIEW,
msg: 'tracks a GA event for viewing trace JSON',
fn: 'trackJsonView',
},
{
action: track.ACTION_RAW_JSON,
category: track.CATEGORY_ALT_VIEW,
msg: 'tracks a GA event for viewing trace JSON (raw)',
fn: 'trackRawJsonView',
},
{
action: OPEN,
arg: false,
category: track.CATEGORY_SLIM_HEADER,
msg: 'tracks a GA event for opening slim header',
fn: 'trackSlimHeaderToggle',
},
{
action: CLOSE,
arg: true,
category: track.CATEGORY_SLIM_HEADER,
msg: 'tracks a GA event for closing slim header',
fn: 'trackSlimHeaderToggle',
},
];
cases.forEach(({ action, arg, msg, fn, category }) => {
it(msg, () => {
track[fn](arg);
expect(trackEvent.mock.calls.length).toBe(1);
expect(trackEvent.mock.calls[0]).toEqual([category, action]);
});
});
});

@ -1,35 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { getToggleValue } from '../../../utils/tracking/common';
import { trackEvent } from '../../../utils/tracking';
// export for tests
export const CATEGORY_ALT_VIEW = 'jaeger/ux/trace/alt-view';
export const CATEGORY_SLIM_HEADER = 'jaeger/ux/trace/slim-header';
// export for tests
export const ACTION_GANTT = 'gantt';
export const ACTION_GRAPH = 'graph';
export const ACTION_JSON = 'json';
export const ACTION_RAW_JSON = 'rawJson';
// use a closure instead of bind to prevent forwarding any arguments to trackEvent()
export const trackGanttView = () => trackEvent(CATEGORY_ALT_VIEW, ACTION_GANTT);
export const trackGraphView = () => trackEvent(CATEGORY_ALT_VIEW, ACTION_GRAPH);
export const trackJsonView = () => trackEvent(CATEGORY_ALT_VIEW, ACTION_JSON);
export const trackRawJsonView = () => trackEvent(CATEGORY_ALT_VIEW, ACTION_RAW_JSON);
export const trackSlimHeaderToggle = (isOpen: boolean) =>
trackEvent(CATEGORY_SLIM_HEADER, getToggleValue(isOpen));

@ -1,232 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as React from 'react';
import { Button, Input } from 'antd';
import _get from 'lodash/get';
import _maxBy from 'lodash/maxBy';
import _values from 'lodash/values';
import IoAndroidArrowBack from 'react-icons/lib/io/android-arrow-back';
import IoIosFilingOutline from 'react-icons/lib/io/ios-filing-outline';
import MdKeyboardArrowRight from 'react-icons/lib/md/keyboard-arrow-right';
import { Link } from 'react-router-dom';
import AltViewOptions from './AltViewOptions';
import KeyboardShortcutsHelp from './KeyboardShortcutsHelp';
import SpanGraph from './SpanGraph';
import TracePageSearchBar from './TracePageSearchBar';
import { TUpdateViewRangeTimeFunction, IViewRange, ViewRangeTimeUpdate } from '../types';
import LabeledList from '../../common/LabeledList';
import NewWindowIcon from '../../common/NewWindowIcon';
import TraceName from '../../common/TraceName';
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;
clearSearch: () => void;
focusUiFindMatches: () => void;
hideMap: boolean;
hideSummary: boolean;
linkToStandalone: string;
nextResult: () => void;
onArchiveClicked: () => void;
onSlimViewClicked: () => void;
onTraceGraphViewClicked: () => void;
prevResult: () => void;
resultCount: number;
showArchiveButton: boolean;
showShortcutsHelp: boolean;
showStandaloneLink: boolean;
showViewOptions: boolean;
slimView: boolean;
textFilter: string | TNil;
toSearch: string | null;
trace: Trace;
traceGraphView: boolean;
updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void;
updateViewRangeTime: TUpdateViewRangeTimeFunction;
viewRange: IViewRange;
};
export const HEADER_ITEMS = [
{
key: 'timestamp',
label: 'Trace Start',
renderer: (trace: Trace) => {
const dateStr = formatDatetime(trace.startTime);
const match = dateStr.match(/^(.+)(:\d\d\.\d+)$/);
return match ? (
<span className="TracePageHeader--overviewItem--value">
{match[1]}
<span className="TracePageHeader--overviewItem--valueDetail">{match[2]}</span>
</span>
) : (
dateStr
);
},
},
{
key: 'duration',
label: 'Duration',
renderer: (trace: Trace) => formatDuration(trace.duration),
},
{
key: 'service-count',
label: 'Services',
renderer: (trace: Trace) => new Set(_values(trace.processes).map(p => p.serviceName)).size,
},
{
key: 'depth',
label: 'Depth',
renderer: (trace: Trace) => _get(_maxBy(trace.spans, 'depth'), 'depth', 0) + 1,
},
{
key: 'span-count',
label: 'Total Spans',
renderer: (trace: Trace) => trace.spans.length,
},
];
export function TracePageHeaderFn(props: TracePageHeaderEmbedProps & { forwardedRef: React.Ref<Input> }) {
const {
canCollapse,
clearSearch,
focusUiFindMatches,
forwardedRef,
hideMap,
hideSummary,
linkToStandalone,
nextResult,
onArchiveClicked,
onSlimViewClicked,
onTraceGraphViewClicked,
prevResult,
resultCount,
showArchiveButton,
showShortcutsHelp,
showStandaloneLink,
showViewOptions,
slimView,
textFilter,
toSearch,
trace,
traceGraphView,
updateNextViewRangeTime,
updateViewRangeTime,
viewRange,
} = props;
if (!trace) {
return null;
}
const links = getTraceLinks(trace);
const summaryItems =
!hideSummary &&
!slimView &&
HEADER_ITEMS.map(item => {
const { renderer, ...rest } = item;
return { ...rest, value: renderer(trace) };
});
const title = (
<h1 className={`TracePageHeader--title ${canCollapse ? 'is-collapsible' : ''}`}>
<TraceName traceName={getTraceName(trace.spans)} />{' '}
<small className="u-tx-muted">{trace.traceID.slice(0, 7)}</small>
</h1>
);
return (
<header className="TracePageHeader">
<div className="TracePageHeader--titleRow">
{toSearch && (
<Link className="TracePageHeader--back" to={toSearch}>
<IoAndroidArrowBack />
</Link>
)}
{links && links.length > 0 && <ExternalLinks links={links} />}
{canCollapse ? (
<a
className="TracePageHeader--titleLink"
onClick={onSlimViewClicked}
role="switch"
aria-checked={!slimView}
>
<MdKeyboardArrowRight
className={`TracePageHeader--detailToggle ${!slimView ? 'is-expanded' : ''}`}
/>
{title}
</a>
) : (
title
)}
<TracePageSearchBar
clearSearch={clearSearch}
focusUiFindMatches={focusUiFindMatches}
nextResult={nextResult}
prevResult={prevResult}
ref={forwardedRef}
resultCount={resultCount}
textFilter={textFilter}
navigable={!traceGraphView}
/>
{showShortcutsHelp && <KeyboardShortcutsHelp className="ub-m2" />}
{showViewOptions && (
<AltViewOptions
onTraceGraphViewClicked={onTraceGraphViewClicked}
traceGraphView={traceGraphView}
traceID={trace.traceID}
/>
)}
{showArchiveButton && (
<Button className="ub-mr2 ub-flex ub-items-center" htmlType="button" onClick={onArchiveClicked}>
<IoIosFilingOutline className="TracePageHeader--archiveIcon" />
Archive Trace
</Button>
)}
{showStandaloneLink && (
<Link
className="u-tx-inherit ub-nowrap ub-mx2"
to={linkToStandalone}
target="_blank"
rel="noopener noreferrer"
>
<NewWindowIcon isLarge />
</Link>
)}
</div>
{summaryItems && <LabeledList className="TracePageHeader--overviewItems" items={summaryItems} />}
{!hideMap && !slimView && (
<SpanGraph
trace={trace}
viewRange={viewRange}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
/>
)}
</header>
);
}
export default React.forwardRef((props: TracePageHeaderEmbedProps, ref: React.Ref<Input>) => (
<TracePageHeaderFn {...props} forwardedRef={ref} />
));

@ -1,45 +0,0 @@
/*
Copyright (c) 2018 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.TracePageSearchBar {
width: 40%;
}
.TracePageSearchBar--bar {
max-width: 20rem;
transition: max-width 0.5s;
}
.TracePageSearchBar--bar:focus-within {
max-width: 100%;
}
.TracePageSearchBar--count {
opacity: 0.6;
}
.TracePageSearchBar--btn {
border-left: none;
transition: 0.2s;
}
.TracePageSearchBar--btn.is-disabled {
opacity: 0.5;
}
.TracePageSearchBar--locateBtn {
padding: 1px 8px 4px;
}

@ -1,16 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// eslint-disable-next-line import/prefer-default-export
export const IN_TRACE_SEARCH = 'in-trace-search';

@ -1,105 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { shallow } from 'enzyme';
import * as markers from './TracePageSearchBar.markers';
import DefaultTracePageSearchBar, { TracePageSearchBarFn as TracePageSearchBar } from './TracePageSearchBar';
import { trackFilter } from '../index.track';
import UiFindInput from '../../common/UiFindInput';
const defaultProps = {
forwardedRef: React.createRef(),
navigable: true,
nextResult: () => {},
prevResult: () => {},
resultCount: 0,
textFilter: 'something',
};
describe('<TracePageSearchBar>', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<TracePageSearchBar {...defaultProps} />);
});
describe('truthy textFilter', () => {
it('renders UiFindInput with correct props', () => {
const renderedUiFindInput = wrapper.find(UiFindInput);
const suffix = shallow(renderedUiFindInput.prop('inputProps').suffix);
expect(renderedUiFindInput.prop('inputProps')).toEqual(
expect.objectContaining({
'data-test': markers.IN_TRACE_SEARCH,
className: 'TracePageSearchBar--bar ub-flex-auto',
name: 'search',
})
);
expect(suffix.hasClass('TracePageSearchBar--count')).toBe(true);
expect(suffix.text()).toBe(String(defaultProps.resultCount));
expect(renderedUiFindInput.prop('forwardedRef')).toBe(defaultProps.forwardedRef);
expect(renderedUiFindInput.prop('trackFindFunction')).toBe(trackFilter);
});
it('renders buttons', () => {
const buttons = wrapper.find('Button');
expect(buttons.length).toBe(4);
buttons.forEach(button => {
expect(button.hasClass('TracePageSearchBar--btn')).toBe(true);
expect(button.hasClass('is-disabled')).toBe(false);
expect(button.prop('disabled')).toBe(false);
});
expect(wrapper.find('Button[icon="up"]').prop('onClick')).toBe(defaultProps.prevResult);
expect(wrapper.find('Button[icon="down"]').prop('onClick')).toBe(defaultProps.nextResult);
expect(wrapper.find('Button[icon="close"]').prop('onClick')).toBe(defaultProps.clearSearch);
});
it('hides navigation buttons when not navigable', () => {
wrapper.setProps({ navigable: false });
const button = wrapper.find('Button');
expect(button.length).toBe(1);
expect(button.prop('icon')).toBe('close');
});
});
describe('falsy textFilter', () => {
beforeEach(() => {
wrapper.setProps({ textFilter: '' });
});
it('renders UiFindInput with correct props', () => {
expect(wrapper.find(UiFindInput).prop('inputProps').suffix).toBe(null);
});
it('renders buttons', () => {
const buttons = wrapper.find('Button');
expect(buttons.length).toBe(4);
buttons.forEach(button => {
expect(button.hasClass('TracePageSearchBar--btn')).toBe(true);
expect(button.hasClass('is-disabled')).toBe(true);
expect(button.prop('disabled')).toBe(true);
});
});
});
});
describe('<DefaultTracePageSearchBar>', () => {
const { forwardedRef: ref, ...propsWithoutRef } = defaultProps;
it('forwardsRef correctly', () => {
const wrapper = shallow(<DefaultTracePageSearchBar {...propsWithoutRef} ref={ref} />);
expect(wrapper.find(TracePageSearchBar).props()).toEqual(defaultProps);
});
});

@ -1,108 +0,0 @@
// Copyright (c) 2018 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as React from 'react';
import { Button, Input } from 'antd';
import cx from 'classnames';
import IoAndroidLocate from 'react-icons/lib/io/android-locate';
import * as markers from './TracePageSearchBar.markers';
import { trackFilter } from '../index.track';
import UiFindInput from '../../common/UiFindInput';
import { TNil } from '../../../types';
import './TracePageSearchBar.css';
type TracePageSearchBarProps = {
textFilter: string | TNil;
prevResult: () => void;
nextResult: () => void;
clearSearch: () => void;
focusUiFindMatches: () => void;
resultCount: number;
navigable: boolean;
};
export function TracePageSearchBarFn(props: TracePageSearchBarProps & { forwardedRef: React.Ref<Input> }) {
const {
clearSearch,
focusUiFindMatches,
forwardedRef,
navigable,
nextResult,
prevResult,
resultCount,
textFilter,
} = props;
const count = textFilter ? <span className="TracePageSearchBar--count">{resultCount}</span> : null;
const btnClass = cx('TracePageSearchBar--btn', { 'is-disabled': !textFilter });
const uiFindInputInputProps = {
'data-test': markers.IN_TRACE_SEARCH,
className: 'TracePageSearchBar--bar ub-flex-auto',
name: 'search',
suffix: count,
};
return (
<div className="TracePageSearchBar">
{/* style inline because compact overwrites the display */}
<Input.Group className="ub-justify-end" compact style={{ display: 'flex' }}>
<UiFindInput
inputProps={uiFindInputInputProps}
forwardedRef={forwardedRef}
trackFindFunction={trackFilter}
/>
{navigable && (
<>
<Button
className={cx(btnClass, 'TracePageSearchBar--locateBtn')}
disabled={!textFilter}
htmlType="button"
onClick={focusUiFindMatches}
>
<IoAndroidLocate />
</Button>
<Button
className={btnClass}
disabled={!textFilter}
htmlType="button"
icon="up"
onClick={prevResult}
/>
<Button
className={btnClass}
disabled={!textFilter}
htmlType="button"
icon="down"
onClick={nextResult}
/>
</>
)}
<Button
className={btnClass}
disabled={!textFilter}
htmlType="button"
icon="close"
onClick={clearSearch}
/>
</Input.Group>
</div>
);
}
export default React.forwardRef((props: TracePageSearchBarProps, ref: React.Ref<Input>) => (
<TracePageSearchBarFn {...props} forwardedRef={ref} />
));

@ -1,64 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AltViewOptions renders correctly 1`] = `
<Dropdown
mouseEnterDelay={0.15}
mouseLeaveDelay={0.1}
overlay={
<Menu
className=""
focusable={false}
prefixCls="ant-menu"
theme="light"
>
<MenuItem>
<a
onClick={[Function]}
role="button"
>
Trace Timeline
</a>
</MenuItem>
<MenuItem>
<Link
onClick={[MockFunction]}
rel="noopener noreferrer"
replace={false}
target="_blank"
to="/api/traces/test trace ID?prettyPrint=true"
>
Trace JSON
</Link>
</MenuItem>
<MenuItem>
<Link
onClick={[MockFunction]}
rel="noopener noreferrer"
replace={false}
target="_blank"
to="/api/traces/test trace ID?raw=true&prettyPrint=true"
>
Trace JSON (unadjusted)
</Link>
</MenuItem>
</Menu>
}
placement="bottomLeft"
prefixCls="ant-dropdown"
>
<Button
block={false}
className="ub-mr2"
ghost={false}
htmlType="button"
loading={false}
onClick={[Function]}
prefixCls="ant-btn"
>
Alternate Views
<Icon
type="down"
/>
</Button>
</Dropdown>
`;

@ -1,234 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`KeyboardShortcutsHelp renders as expected 1`] = `
<Fragment>
<Button
block={false}
className="test--ClassName"
ghost={false}
htmlType="button"
loading={false}
onClick={[Function]}
prefixCls="ant-btn"
>
<span
className="KeyboardShortcutsHelp--cta"
>
</span>
</Button>
<Modal
bodyStyle={
Object {
"padding": 0,
}
}
cancelButtonDisabled={false}
cancelButtonProps={
Object {
"style": Object {
"display": "none",
},
}
}
confirmLoading={false}
maskTransitionName="fade"
okButtonDisabled={false}
okType="primary"
onCancel={[Function]}
onOk={[Function]}
prefixCls="ant-modal"
title="Keyboard Shortcuts"
transitionName="zoom"
visible={false}
width={520}
>
<Table
bordered={false}
className="KeyboardShortcutsHelp--table u-simple-scrollbars"
dataSource={
Array [
Object {
"description": "Scroll down",
"kbds": <kbd>
S
</kbd>,
"key": "S",
},
Object {
"description": "Scroll up",
"kbds": <kbd>
W
</kbd>,
"key": "W",
},
Object {
"description": "Scroll to the next visible span",
"kbds": <kbd>
F
</kbd>,
"key": "F",
},
Object {
"description": "Scroll to the previous visible span",
"kbds": <kbd>
B
</kbd>,
"key": "B",
},
Object {
"description": "Pan left",
"kbds": <kbd>
A
</kbd>,
"key": "A",
},
Object {
"description": "Pan left",
"kbds": <kbd>
</kbd>,
"key": "←",
},
Object {
"description": "Pan left — Large",
"kbds": <kbd>
⇧ A
</kbd>,
"key": "⇧,A",
},
Object {
"description": "Pan left — Large",
"kbds": <kbd>
⇧ ←
</kbd>,
"key": "⇧,←",
},
Object {
"description": "Pan right",
"kbds": <kbd>
D
</kbd>,
"key": "D",
},
Object {
"description": "Pan right",
"kbds": <kbd>
</kbd>,
"key": "→",
},
Object {
"description": "Pan right — Large",
"kbds": <kbd>
⇧ D
</kbd>,
"key": "⇧,D",
},
Object {
"description": "Pan right — Large",
"kbds": <kbd>
⇧ →
</kbd>,
"key": "⇧,→",
},
Object {
"description": "Zoom in",
"kbds": <kbd>
</kbd>,
"key": "↑",
},
Object {
"description": "Zoom in — Large",
"kbds": <kbd>
⇧ ↑
</kbd>,
"key": "⇧,↑",
},
Object {
"description": "Zoom out",
"kbds": <kbd>
</kbd>,
"key": "↓",
},
Object {
"description": "Zoom out — Large",
"kbds": <kbd>
⇧ ↓
</kbd>,
"key": "⇧,↓",
},
Object {
"description": "Collapse All",
"kbds": <kbd>
]
</kbd>,
"key": "]",
},
Object {
"description": "Expand All",
"kbds": <kbd>
[
</kbd>,
"key": "[",
},
Object {
"description": "Collapse One Level",
"kbds": <kbd>
P
</kbd>,
"key": "P",
},
Object {
"description": "Expand One Level",
"kbds": <kbd>
O
</kbd>,
"key": "O",
},
Object {
"description": "Search Spans",
"kbds": <kbd>
CTRL B
</kbd>,
"key": "CTRL,B",
},
Object {
"description": "Clear Search",
"kbds": <kbd>
ESCAPE
</kbd>,
"key": "ESCAPE",
},
]
}
indentSize={20}
loading={false}
locale={Object {}}
pagination={false}
prefixCls="ant-table"
rowClassName={[Function]}
rowKey="key"
showHeader={false}
size="middle"
useFixedHeader={false}
>
<Column
dataIndex="description"
key="description"
render={[Function]}
title="Description"
/>
<Column
align="right"
dataIndex="kbds"
key="kbds"
render={[Function]}
title="Key(s)"
/>
</Table>
</Modal>
</Fragment>
`;

@ -1,15 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export { default } from './TracePageHeader';

@ -1,244 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Positions from './Positions';
describe('Positions', () => {
const bufferLen = 1;
const getHeight = i => i * 2 + 2;
let ps;
beforeEach(() => {
ps = new Positions(bufferLen);
ps.profileData(10);
});
describe('constructor()', () => {
it('intializes member variables correctly', () => {
ps = new Positions(1);
expect(ps.ys).toEqual([]);
expect(ps.heights).toEqual([]);
expect(ps.bufferLen).toBe(1);
expect(ps.dataLen).toBe(-1);
expect(ps.lastI).toBe(-1);
});
});
describe('profileData(...)', () => {
it('manages increases in data length correctly', () => {
expect(ps.dataLen).toBe(10);
expect(ps.ys.length).toBe(10);
expect(ps.heights.length).toBe(10);
expect(ps.lastI).toBe(-1);
});
it('manages decreases in data length correctly', () => {
ps.lastI = 9;
ps.profileData(5);
expect(ps.dataLen).toBe(5);
expect(ps.ys.length).toBe(5);
expect(ps.heights.length).toBe(5);
expect(ps.lastI).toBe(4);
});
it('does nothing when data length is unchanged', () => {
expect(ps.dataLen).toBe(10);
expect(ps.ys.length).toBe(10);
expect(ps.heights.length).toBe(10);
expect(ps.lastI).toBe(-1);
ps.profileData(10);
expect(ps.dataLen).toBe(10);
expect(ps.ys.length).toBe(10);
expect(ps.heights.length).toBe(10);
expect(ps.lastI).toBe(-1);
});
});
describe('calcHeights()', () => {
it('updates lastI correctly', () => {
ps.calcHeights(1, getHeight);
expect(ps.lastI).toBe(bufferLen + 1);
});
it('saves the heights and y-values up to `lastI <= max + bufferLen`', () => {
const ys = [0, 2, 6, 12];
ys.length = 10;
const heights = [2, 4, 6];
heights.length = 10;
ps.calcHeights(1, getHeight);
expect(ps.ys).toEqual(ys);
expect(ps.heights).toEqual(heights);
});
it('does nothing when `max + buffer <= lastI`', () => {
ps.calcHeights(2, getHeight);
const ys = ps.ys.slice();
const heights = ps.heights.slice();
ps.calcHeights(1, getHeight);
expect(ps.ys).toEqual(ys);
expect(ps.heights).toEqual(heights);
});
describe('recalculates values up to `max + bufferLen` when `max + buffer <= lastI` and `forcedLastI = 0` is passed', () => {
beforeEach(() => {
// the initial state for the test
ps.calcHeights(2, getHeight);
});
it('test-case has a valid initial state', () => {
const initialYs = [0, 2, 6, 12, 20];
initialYs.length = 10;
const initialHeights = [2, 4, 6, 8];
initialHeights.length = 10;
expect(ps.ys).toEqual(initialYs);
expect(ps.heights).toEqual(initialHeights);
expect(ps.lastI).toBe(3);
});
it('recalcualtes the y-values correctly', () => {
// recalc a sub-set of the calcualted values using a different getHeight
ps.calcHeights(1, () => 2, 0);
const ys = [0, 2, 4, 6, 20];
ys.length = 10;
expect(ps.ys).toEqual(ys);
});
it('recalcualtes the heights correctly', () => {
// recalc a sub-set of the calcualted values using a different getHeight
ps.calcHeights(1, () => 2, 0);
const heights = [2, 2, 2, 8];
heights.length = 10;
expect(ps.heights).toEqual(heights);
});
it('saves lastI correctly', () => {
// recalc a sub-set of the calcualted values
ps.calcHeights(1, getHeight, 0);
expect(ps.lastI).toBe(2);
});
});
it('limits caclulations to the known data length', () => {
ps.calcHeights(999, getHeight);
expect(ps.lastI).toBe(ps.dataLen - 1);
});
});
describe('calcYs()', () => {
it('scans forward until `yValue` is met or exceeded', () => {
ps.calcYs(11, getHeight);
const ys = [0, 2, 6, 12, 20];
ys.length = 10;
const heights = [2, 4, 6, 8];
heights.length = 10;
expect(ps.ys).toEqual(ys);
expect(ps.heights).toEqual(heights);
});
it('exits early if the known y-values exceed `yValue`', () => {
ps.calcYs(11, getHeight);
const spy = jest.spyOn(ps, 'calcHeights');
ps.calcYs(10, getHeight);
expect(spy).not.toHaveBeenCalled();
});
it('exits when exceeds the data length even if yValue is unmet', () => {
ps.calcYs(999, getHeight);
expect(ps.ys[ps.ys.length - 1]).toBeLessThan(999);
});
});
describe('findFloorIndex()', () => {
beforeEach(() => {
ps.calcYs(11, getHeight);
// Note: ps.ys = [0, 2, 6, 12, 20, undefined x 5];
});
it('scans y-values for index that equals or preceeds `yValue`', () => {
let i = ps.findFloorIndex(3, getHeight);
expect(i).toBe(1);
i = ps.findFloorIndex(21, getHeight);
expect(i).toBe(4);
ps.calcYs(999, getHeight);
i = ps.findFloorIndex(11, getHeight);
expect(i).toBe(2);
i = ps.findFloorIndex(12, getHeight);
expect(i).toBe(3);
i = ps.findFloorIndex(20, getHeight);
expect(i).toBe(4);
});
it('is robust against non-positive y-values', () => {
let i = ps.findFloorIndex(0, getHeight);
expect(i).toBe(0);
i = ps.findFloorIndex(-10, getHeight);
expect(i).toBe(0);
});
it('scans no further than dataLen even if `yValue` is unmet', () => {
const i = ps.findFloorIndex(999, getHeight);
expect(i).toBe(ps.lastI);
});
});
describe('getEstimatedHeight()', () => {
const simpleGetHeight = () => 2;
beforeEach(() => {
ps.calcYs(5, simpleGetHeight);
// Note: ps.ys = [0, 2, 4, 6, 8, undefined x 5];
});
it('returns the estimated max height, surpassing known values', () => {
const estHeight = ps.getEstimatedHeight();
expect(estHeight).toBeGreaterThan(ps.heights[ps.lastI]);
});
it('returns the known max height, if all heights have been calculated', () => {
ps.calcYs(999, simpleGetHeight);
const totalHeight = ps.getEstimatedHeight();
expect(totalHeight).toBeGreaterThan(ps.heights[ps.heights.length - 1]);
});
});
describe('confirmHeight()', () => {
const simpleGetHeight = () => 2;
beforeEach(() => {
ps.calcYs(5, simpleGetHeight);
// Note: ps.ys = [0, 2, 4, 6, 8, undefined x 5];
});
it('calculates heights up to and including `_i` if necessary', () => {
const startNumHeights = ps.heights.filter(Boolean).length;
const calcHeightsSpy = jest.spyOn(ps, 'calcHeights');
ps.confirmHeight(7, simpleGetHeight);
const endNumHeights = ps.heights.filter(Boolean).length;
expect(startNumHeights).toBeLessThan(endNumHeights);
expect(calcHeightsSpy).toHaveBeenCalled();
});
it('invokes `heightGetter` at `_i` to compare result with known height', () => {
const getHeightSpy = jest.fn(simpleGetHeight);
ps.confirmHeight(ps.lastI - 1, getHeightSpy);
expect(getHeightSpy).toHaveBeenCalled();
});
it('cascades difference in observed height vs known height to known y-values', () => {
const getLargerHeight = () => simpleGetHeight() + 2;
const knownYs = ps.ys.slice();
const expectedYValues = knownYs.map(value => (value ? value + 2 : value));
ps.confirmHeight(0, getLargerHeight);
expect(ps.ys).toEqual(expectedYValues);
});
});
});

@ -1,197 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
type THeightGetter = (index: number) => number;
/**
* Keeps track of the height and y-position for anything sequenctial where
* y-positions follow one-after-another and can be derived from the height of
* the prior entries. The height is known from an accessor function parameter
* to the methods that require new knowledge the heights.
*
* @export
* @class Positions
*/
export default class Positions {
/**
* Indicates how far past the explicitly required height or y-values should
* checked.
*/
bufferLen: number;
dataLen: number;
heights: number[];
/**
* `lastI` keeps track of which values have already been visited. In many
* scenarios, values do not need to be revisited. But, revisiting is required
* when heights have changed, so `lastI` can be forced.
*/
lastI: number;
ys: number[];
constructor(bufferLen: number) {
this.ys = [];
this.heights = [];
this.bufferLen = bufferLen;
this.dataLen = -1;
this.lastI = -1;
}
/**
* Used to make sure the length of y-values and heights is consistent with
* the context; in particular `lastI` needs to remain valid.
*/
profileData(dataLength: number) {
if (dataLength !== this.dataLen) {
this.dataLen = dataLength;
this.ys.length = dataLength;
this.heights.length = dataLength;
if (this.lastI >= dataLength) {
this.lastI = dataLength - 1;
}
}
}
/**
* Calculate and save the heights and y-values, based on `heightGetter`, from
* `lastI` until the`max` index; the starting point (`lastI`) can be forced
* via the `forcedLastI` parameter.
* @param {number=} forcedLastI
*/
calcHeights(max: number, heightGetter: THeightGetter, forcedLastI?: number) {
if (forcedLastI != null) {
this.lastI = forcedLastI;
}
let _max = max + this.bufferLen;
if (_max <= this.lastI) {
return;
}
if (_max >= this.heights.length) {
_max = this.heights.length - 1;
}
let i = this.lastI;
if (this.lastI === -1) {
i = 0;
this.ys[0] = 0;
}
while (i <= _max) {
// eslint-disable-next-line no-multi-assign
const h = (this.heights[i] = heightGetter(i));
this.ys[i + 1] = this.ys[i] + h;
i++;
}
this.lastI = _max;
}
/**
* Verify the height and y-values from `lastI` up to `yValue`.
*/
calcYs(yValue: number, heightGetter: THeightGetter) {
while ((this.ys[this.lastI] == null || yValue > this.ys[this.lastI]) && this.lastI < this.dataLen - 1) {
this.calcHeights(this.lastI, heightGetter);
}
}
/**
* Get the latest height for index `_i`. If it's in new terretory
* (_i > lastI), find the heights (and y-values) leading up to it. If it's in
* known territory (_i <= lastI) and the height is different than what is
* known, recalculate subsequent y values, but don't confirm the heights of
* those items, just update based on the difference.
*/
confirmHeight(_i: number, heightGetter: THeightGetter) {
let i = _i;
if (i > this.lastI) {
this.calcHeights(i, heightGetter);
return;
}
const h = heightGetter(i);
if (h === this.heights[i]) {
return;
}
const chg = h - this.heights[i];
this.heights[i] = h;
// shift the y positions by `chg` for all known y positions
while (++i <= this.lastI) {
this.ys[i] += chg;
}
if (this.ys[this.lastI + 1] != null) {
this.ys[this.lastI + 1] += chg;
}
}
/**
* Given a target y-value (`yValue`), find the closest index (in the `.ys`
* array) that is prior to the y-value; e.g. map from y-value to index in
* `.ys`.
*/
findFloorIndex(yValue: number, heightGetter: THeightGetter): number {
this.calcYs(yValue, heightGetter);
let imin = 0;
let imax = this.lastI;
if (this.ys.length < 2 || yValue < this.ys[1]) {
return 0;
}
if (yValue > this.ys[imax]) {
return imax;
}
let i;
while (imin < imax) {
// eslint-disable-next-line no-bitwise
i = (imin + 0.5 * (imax - imin)) | 0;
if (yValue > this.ys[i]) {
if (yValue <= this.ys[i + 1]) {
return i;
}
imin = i;
} else if (yValue < this.ys[i]) {
if (yValue >= this.ys[i - 1]) {
return i - 1;
}
imax = i;
} else {
return i;
}
}
throw new Error(`unable to find floor index for y=${yValue}`);
}
/**
* Get the `y` and `height` for a given row.
*
* @returns {{ height: number, y: number }}
*/
getRowPosition(index: number, heightGetter: THeightGetter) {
this.confirmHeight(index, heightGetter);
return {
height: this.heights[index],
y: this.ys[index],
};
}
/**
* Get the estimated height of the whole shebang by extrapolating based on
* the average known height.
*/
getEstimatedHeight(): number {
const known = this.ys[this.lastI] + this.heights[this.lastI];
if (this.lastI >= this.dataLen - 1) {
// eslint-disable-next-line no-bitwise
return known | 0;
}
// eslint-disable-next-line no-bitwise
return ((known / (this.lastI + 1)) * this.heights.length) | 0;
}
}

@ -1,101 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ListView> shallow tests matches a snapshot 1`] = `
<div
onScroll={[Function]}
style={
Object {
"height": "100%",
"overflowY": "auto",
"position": "relative",
}
}
>
<div
style={
Object {
"height": 1640,
"position": "relative",
}
}
>
<div
className="SomeClassName"
style={
Object {
"margin": 0,
"padding": 0,
"position": "absolute",
"top": 0,
}
}
>
<Item
data-item-key="0"
key="0"
style={
Object {
"height": 2,
"position": "absolute",
"top": 0,
}
}
>
0
</Item>
<Item
data-item-key="1"
key="1"
style={
Object {
"height": 4,
"position": "absolute",
"top": 2,
}
}
>
1
</Item>
<Item
data-item-key="2"
key="2"
style={
Object {
"height": 6,
"position": "absolute",
"top": 6,
}
}
>
2
</Item>
<Item
data-item-key="3"
key="3"
style={
Object {
"height": 8,
"position": "absolute",
"top": 12,
}
}
>
3
</Item>
<Item
data-item-key="4"
key="4"
style={
Object {
"height": 10,
"position": "absolute",
"top": 20,
}
}
>
4
</Item>
</div>
</div>
</div>
`;

@ -1,478 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as React from 'react';
import Positions from './Positions';
import { TNil } from '../../../../types';
type TWrapperProps = {
style: React.CSSProperties;
ref: (elm: HTMLDivElement) => void;
onScroll?: () => void;
};
/**
* @typedef
*/
type TListViewProps = {
/**
* Number of elements in the list.
*/
dataLength: number;
/**
* Convert item index (number) to the key (string). ListView uses both indexes
* and keys to handle the addtion of new rows.
*/
getIndexFromKey: (key: string) => number;
/**
* Convert item key (string) to the index (number). ListView uses both indexes
* and keys to handle the addtion of new rows.
*/
getKeyFromIndex: (index: number) => string;
/**
* Number of items to draw and add to the DOM, initially.
*/
initialDraw?: number;
/**
* The parent provides fallback height measurements when there is not a
* rendered element to measure.
*/
itemHeightGetter: (index: number, key: string) => number;
/**
* Function that renders an item; rendered items are added directly to the
* DOM, they are not wrapped in list item wrapper HTMLElement.
*/
// itemRenderer(itemKey, style, i, attrs)
itemRenderer: (
itemKey: string,
style: Record<string, string | number>,
index: number,
attributes: Record<string, string>
) => React.ReactNode;
/**
* `className` for the HTMLElement that holds the items.
*/
itemsWrapperClassName?: string;
/**
* When adding new items to the DOM, this is the number of items to add above
* and below the current view. E.g. if list is 100 items and is srcolled
* halfway down (so items [46, 55] are in view), then when a new range of
* items is rendered, it will render items `46 - viewBuffer` to
* `55 + viewBuffer`.
*/
viewBuffer: number;
/**
* The minimum number of items offscreen in either direction; e.g. at least
* `viewBuffer` number of items must be off screen above and below the
* current view, or more items will be rendered.
*/
viewBufferMin: number;
/**
* When `true`, expect `_wrapperElm` to have `overflow: visible` and to,
* essentially, be tall to the point the entire page will will end up
* scrolling as a result of the ListView. Similar to react-virtualized
* window scroller.
*
* - Ref: https://bvaughn.github.io/react-virtualized/#/components/WindowScroller
* - Ref:https://github.com/bvaughn/react-virtualized/blob/497e2a1942529560681d65a9ef9f5e9c9c9a49ba/docs/WindowScroller.md
*/
windowScroller?: boolean;
};
const DEFAULT_INITIAL_DRAW = 300;
/**
* Virtualized list view component, for the most part, only renders the window
* of items that are in-view with some buffer before and after. Listens for
* scroll events and updates which items are rendered. See react-virtualized
* for a suite of components with similar, but generalized, functinality.
* https://github.com/bvaughn/react-virtualized
*
* Note: Presently, ListView cannot be a PureComponent. This is because ListView
* is sensitive to the underlying state that drives the list items, but it
* doesn't actually receive that state. So, a render may still be required even
* if ListView's props are unchanged.
*
* @export
* @class ListView
*/
export default class ListView extends React.Component<TListViewProps> {
/**
* Keeps track of the height and y-value of items, by item index, in the
* ListView.
*/
_yPositions: Positions;
/**
* Keep track of the known / measured heights of the rendered items; populated
* with values through observation and keyed on the item key, not the item
* index.
*/
_knownHeights: Map<string, number>;
/**
* The start index of the items currently drawn.
*/
_startIndexDrawn: number;
/**
* The end index of the items currently drawn.
*/
_endIndexDrawn: number;
/**
* The start index of the items currently in view.
*/
_startIndex: number;
/**
* The end index of the items currently in view.
*/
_endIndex: number;
/**
* Height of the visual window, e.g. height of the scroller element.
*/
_viewHeight: number;
/**
* `scrollTop` of the current scroll position.
*/
_scrollTop: number;
/**
* Used to keep track of whether or not a re-calculation of what should be
* drawn / viewable has been scheduled.
*/
_isScrolledOrResized: boolean;
/**
* If `windowScroller` is true, this notes how far down the page the scroller
* is located. (Note: repositioning and below-the-fold views are untested)
*/
_htmlTopOffset: number;
_windowScrollListenerAdded: boolean;
_htmlElm: HTMLElement;
/**
* HTMLElement holding the scroller.
*/
_wrapperElm: HTMLElement | TNil;
/**
* HTMLElement holding the rendered items.
*/
_itemHolderElm: HTMLElement | TNil;
static defaultProps = {
initialDraw: DEFAULT_INITIAL_DRAW,
itemsWrapperClassName: '',
windowScroller: false,
};
constructor(props: TListViewProps) {
super(props);
this._yPositions = new Positions(200);
// _knownHeights is (item-key -> observed height) of list items
this._knownHeights = new Map();
this._startIndexDrawn = 2 ** 20;
this._endIndexDrawn = -(2 ** 20);
this._startIndex = 0;
this._endIndex = 0;
this._viewHeight = -1;
this._scrollTop = -1;
this._isScrolledOrResized = false;
this._htmlTopOffset = -1;
this._windowScrollListenerAdded = false;
// _htmlElm is only relevant if props.windowScroller is true
this._htmlElm = document.documentElement as any;
this._wrapperElm = undefined;
this._itemHolderElm = undefined;
}
componentDidMount() {
if (this.props.windowScroller) {
if (this._wrapperElm) {
const { top } = this._wrapperElm.getBoundingClientRect();
this._htmlTopOffset = top + this._htmlElm.scrollTop;
}
window.addEventListener('scroll', this._onScroll);
this._windowScrollListenerAdded = true;
}
}
componentDidUpdate() {
if (this._itemHolderElm) {
this._scanItemHeights();
}
}
componentWillUnmount() {
if (this._windowScrollListenerAdded) {
window.removeEventListener('scroll', this._onScroll);
}
}
getViewHeight = () => this._viewHeight;
/**
* Get the index of the item at the bottom of the current view.
*/
getBottomVisibleIndex = (): number => {
const bottomY = this._scrollTop + this._viewHeight;
return this._yPositions.findFloorIndex(bottomY, this._getHeight);
};
/**
* Get the index of the item at the top of the current view.
*/
getTopVisibleIndex = (): number => this._yPositions.findFloorIndex(this._scrollTop, this._getHeight);
getRowPosition = (index: number): { height: number; y: number } =>
this._yPositions.getRowPosition(index, this._getHeight);
/**
* Scroll event listener that schedules a remeasuring of which items should be
* rendered.
*/
_onScroll = () => {
if (!this._isScrolledOrResized) {
this._isScrolledOrResized = true;
window.requestAnimationFrame(this._positionList);
}
};
/**
* Returns true is the view height (scroll window) or scroll position have
* changed.
*/
_isViewChanged() {
if (!this._wrapperElm) {
return false;
}
const useRoot = this.props.windowScroller;
const clientHeight = useRoot ? this._htmlElm.clientHeight : this._wrapperElm.clientHeight;
const scrollTop = useRoot ? this._htmlElm.scrollTop : this._wrapperElm.scrollTop;
return clientHeight !== this._viewHeight || scrollTop !== this._scrollTop;
}
/**
* Recalculate _startIndex and _endIndex, e.g. which items are in view.
*/
_calcViewIndexes() {
const useRoot = this.props.windowScroller;
// funky if statement is to satisfy flow
if (!useRoot) {
/* istanbul ignore next */
if (!this._wrapperElm) {
this._viewHeight = -1;
this._startIndex = 0;
this._endIndex = 0;
return;
}
this._viewHeight = this._wrapperElm.clientHeight;
this._scrollTop = this._wrapperElm.scrollTop;
} else {
this._viewHeight = window.innerHeight - this._htmlTopOffset;
this._scrollTop = window.scrollY;
}
const yStart = this._scrollTop;
const yEnd = this._scrollTop + this._viewHeight;
this._startIndex = this._yPositions.findFloorIndex(yStart, this._getHeight);
this._endIndex = this._yPositions.findFloorIndex(yEnd, this._getHeight);
}
/**
* Checked to see if the currently rendered items are sufficient, if not,
* force an update to trigger more items to be rendered.
*/
_positionList = () => {
this._isScrolledOrResized = false;
if (!this._wrapperElm) {
return;
}
this._calcViewIndexes();
// indexes drawn should be padded by at least props.viewBufferMin
const maxStart =
this.props.viewBufferMin > this._startIndex ? 0 : this._startIndex - this.props.viewBufferMin;
const minEnd =
this.props.viewBufferMin < this.props.dataLength - this._endIndex
? this._endIndex + this.props.viewBufferMin
: this.props.dataLength - 1;
if (maxStart < this._startIndexDrawn || minEnd > this._endIndexDrawn) {
this.forceUpdate();
}
};
_initWrapper = (elm: HTMLElement | TNil) => {
this._wrapperElm = elm;
if (!this.props.windowScroller && elm) {
this._viewHeight = elm.clientHeight;
}
};
_initItemHolder = (elm: HTMLElement | TNil) => {
this._itemHolderElm = elm;
this._scanItemHeights();
};
/**
* Go through all items that are rendered and save their height based on their
* item-key (which is on a data-* attribute). If any new or adjusted heights
* are found, re-measure the current known y-positions (via .yPositions).
*/
_scanItemHeights = () => {
const getIndexFromKey = this.props.getIndexFromKey;
if (!this._itemHolderElm) {
return;
}
// note the keys for the first and last altered heights, the `yPositions`
// needs to be updated
let lowDirtyKey = null;
let highDirtyKey = null;
let isDirty = false;
// iterating childNodes is faster than children
// https://jsperf.com/large-htmlcollection-vs-large-nodelist
const nodes = this._itemHolderElm.childNodes;
const max = nodes.length;
for (let i = 0; i < max; i++) {
const node: HTMLElement = nodes[i] as any;
// use `.getAttribute(...)` instead of `.dataset` for jest / JSDOM
const itemKey = node.getAttribute('data-item-key');
if (!itemKey) {
// eslint-disable-next-line no-console
console.warn('itemKey not found');
continue;
}
// measure the first child, if it's available, otherwise the node itself
// (likely not transferable to other contexts, and instead is specific to
// how we have the items rendered)
const measureSrc: Element = node.firstElementChild || node;
const observed = measureSrc.clientHeight;
const known = this._knownHeights.get(itemKey);
if (observed !== known) {
this._knownHeights.set(itemKey, observed);
if (!isDirty) {
isDirty = true;
// eslint-disable-next-line no-multi-assign
lowDirtyKey = highDirtyKey = itemKey;
} else {
highDirtyKey = itemKey;
}
}
}
if (lowDirtyKey != null && highDirtyKey != null) {
// update yPositions, then redraw
const imin = getIndexFromKey(lowDirtyKey);
const imax = highDirtyKey === lowDirtyKey ? imin : getIndexFromKey(highDirtyKey);
this._yPositions.calcHeights(imax, this._getHeight, imin);
this.forceUpdate();
}
};
/**
* Get the height of the element at index `i`; first check the known heigths,
* fallbck to `.props.itemHeightGetter(...)`.
*/
_getHeight = (i: number) => {
const key = this.props.getKeyFromIndex(i);
const known = this._knownHeights.get(key);
// known !== known iff known is NaN
// eslint-disable-next-line no-self-compare
if (known != null && known === known) {
return known;
}
return this.props.itemHeightGetter(i, key);
};
render() {
const {
dataLength,
getKeyFromIndex,
initialDraw = DEFAULT_INITIAL_DRAW,
itemRenderer,
viewBuffer,
viewBufferMin,
} = this.props;
const heightGetter = this._getHeight;
const items = [];
let start;
let end;
this._yPositions.profileData(dataLength);
if (!this._wrapperElm) {
start = 0;
end = (initialDraw < dataLength ? initialDraw : dataLength) - 1;
} else {
if (this._isViewChanged()) {
this._calcViewIndexes();
}
const maxStart = viewBufferMin > this._startIndex ? 0 : this._startIndex - viewBufferMin;
const minEnd =
viewBufferMin < dataLength - this._endIndex ? this._endIndex + viewBufferMin : dataLength - 1;
if (maxStart < this._startIndexDrawn || minEnd > this._endIndexDrawn) {
start = viewBuffer > this._startIndex ? 0 : this._startIndex - viewBuffer;
end = this._endIndex + viewBuffer;
if (end >= dataLength) {
end = dataLength - 1;
}
} else {
start = this._startIndexDrawn;
end = this._endIndexDrawn > dataLength - 1 ? dataLength - 1 : this._endIndexDrawn;
}
}
this._yPositions.calcHeights(end, heightGetter, start || -1);
this._startIndexDrawn = start;
this._endIndexDrawn = end;
items.length = end - start + 1;
for (let i = start; i <= end; i++) {
const { y: top, height } = this._yPositions.getRowPosition(i, heightGetter);
const style = {
height,
top,
position: 'absolute',
};
const itemKey = getKeyFromIndex(i);
const attrs = { 'data-item-key': itemKey };
items.push(itemRenderer(itemKey, style, i, attrs));
}
const wrapperProps: TWrapperProps = {
style: { position: 'relative' },
ref: this._initWrapper,
};
if (!this.props.windowScroller) {
wrapperProps.onScroll = this._onScroll;
wrapperProps.style.height = '100%';
wrapperProps.style.overflowY = 'auto';
}
const scrollerStyle = {
position: 'relative' as 'relative',
height: this._yPositions.getEstimatedHeight(),
};
return (
<div {...wrapperProps}>
<div style={scrollerStyle}>
<div
style={{
position: 'absolute',
top: 0,
margin: 0,
padding: 0,
}}
className={this.props.itemsWrapperClassName}
ref={this._initItemHolder}
>
{items}
</div>
</div>
</div>
);
}
}

@ -1,36 +0,0 @@
/*
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.
*/
.ReferencesButton-MultiParent {
padding: 0 5px;
color: #000;
}
.ReferencesButton-MultiParent ~ .ReferencesButton-MultiParent {
margin-left: 5px;
}
a.ReferencesButton--TraceRefLink {
display: flex;
justify-content: space-between;
}
a.ReferencesButton--TraceRefLink > .NewWindowIcon {
margin: 0.2em 0 0;
}
.ReferencesButton-tooltip {
max-width: none;
}

@ -1,83 +0,0 @@
// 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, Tooltip } from 'antd';
import ReferencesButton from './ReferencesButton';
import transformTraceData from '../../../model/transform-trace-data';
import traceGenerator from '../../../demo/trace-generators';
import ReferenceLink from '../url/ReferenceLink';
describe(ReferencesButton, () => {
const trace = transformTraceData(traceGenerator.trace({ numberOfSpans: 10 }));
const oneReference = trace.spans[1].references;
const moreReferences = oneReference.slice();
const externalSpanID = 'extSpan';
moreReferences.push(
{
refType: 'CHILD_OF',
traceID: trace.traceID,
spanID: trace.spans[2].spanID,
span: trace.spans[2],
},
{
refType: 'CHILD_OF',
traceID: 'otherTrace',
spanID: externalSpanID,
}
);
const baseProps = {
focusSpan: () => {},
};
it('renders single reference', () => {
const props = { ...baseProps, references: oneReference };
const wrapper = shallow(<ReferencesButton {...props} />);
const dropdown = wrapper.find(Dropdown);
const refLink = wrapper.find(ReferenceLink);
const tooltip = wrapper.find(Tooltip);
expect(dropdown.length).toBe(0);
expect(refLink.length).toBe(1);
expect(refLink.prop('reference')).toBe(oneReference[0]);
expect(refLink.first().props().className).toBe('ReferencesButton-MultiParent');
expect(tooltip.length).toBe(1);
expect(tooltip.prop('title')).toBe(props.tooltipText);
});
it('renders multiple references', () => {
const props = { ...baseProps, references: moreReferences };
const wrapper = shallow(<ReferencesButton {...props} />);
const dropdown = wrapper.find(Dropdown);
expect(dropdown.length).toBe(1);
const menuInstance = shallow(dropdown.first().props().overlay);
const submenuItems = menuInstance.find(Menu.Item);
expect(submenuItems.length).toBe(3);
submenuItems.forEach((submenuItem, i) => {
expect(submenuItem.find(ReferenceLink).prop('reference')).toBe(moreReferences[i]);
});
expect(
submenuItems
.at(2)
.find(ReferenceLink)
.childAt(0)
.text()
).toBe(`(another trace) - ${moreReferences[2].spanID}`);
});
});

@ -1,83 +0,0 @@
// 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 { Dropdown, Menu, Tooltip } from 'antd';
import { TooltipPlacement } from 'antd/lib/tooltip';
import NewWindowIcon from '../../common/NewWindowIcon';
import { SpanReference } from '../../../types/trace';
import './ReferencesButton.css';
import ReferenceLink from '../url/ReferenceLink';
type TReferencesButtonProps = {
references: SpanReference[];
children: React.ReactNode;
tooltipText: string;
focusSpan: (spanID: string) => void;
};
export default class ReferencesButton extends React.PureComponent<TReferencesButtonProps> {
referencesList = (references: SpanReference[]) => (
<Menu>
{references.map(ref => {
const { span, spanID } = ref;
return (
<Menu.Item key={`${spanID}`}>
<ReferenceLink
reference={ref}
focusSpan={this.props.focusSpan}
className="ReferencesButton--TraceRefLink"
>
{span
? `${span.process.serviceName}:${span.operationName} - ${ref.spanID}`
: `(another trace) - ${ref.spanID}`}
{!span && <NewWindowIcon />}
</ReferenceLink>
</Menu.Item>
);
})}
</Menu>
);
render() {
const { references, children, tooltipText, focusSpan } = this.props;
const tooltipProps = {
arrowPointAtCenter: true,
mouseLeaveDelay: 0.5,
placement: 'bottom' as TooltipPlacement,
title: tooltipText,
overlayClassName: 'ReferencesButton--tooltip',
};
if (references.length > 1) {
return (
<Tooltip {...tooltipProps}>
<Dropdown overlay={this.referencesList(references)} placement="bottomRight" trigger={['click']}>
<a className="ReferencesButton-MultiParent">{children}</a>
</Dropdown>
</Tooltip>
);
}
const ref = references[0];
return (
<Tooltip {...tooltipProps}>
<ReferenceLink reference={ref} focusSpan={focusSpan} className="ReferencesButton-MultiParent">
{children}
</ReferenceLink>
</Tooltip>
);
}
}

@ -1,104 +0,0 @@
/*
Copyright (c) 2017 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.SpanBar--wrapper {
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
overflow: hidden;
z-index: 0;
}
.span-row.is-expanded .SpanBar--wrapper,
.span-row:hover .SpanBar--wrapper {
opacity: 1;
}
.SpanBar--bar {
border-radius: 3px;
min-width: 2px;
position: absolute;
height: 36%;
top: 32%;
}
.SpanBar--rpc {
position: absolute;
top: 35%;
bottom: 35%;
z-index: 1;
}
.SpanBar--label {
color: #aaa;
font-size: 12px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1em;
white-space: nowrap;
padding: 0 0.5em;
position: absolute;
}
.SpanBar--label.is-right {
left: 100%;
}
.SpanBar--label.is-left {
right: 100%;
}
.span-row.is-expanded .SpanBar--label,
.span-row:hover .SpanBar--label {
color: #000;
}
.SpanBar--logMarker {
background-color: rgba(0, 0, 0, 0.5);
cursor: pointer;
height: 60%;
min-width: 1px;
position: absolute;
top: 20%;
}
.SpanBar--logMarker:hover {
background-color: #000;
}
.SpanBar--logMarker::before,
.SpanBar--logMarker::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
right: 0;
border: 1px solid transparent;
}
.SpanBar--logMarker::after {
left: 0;
}
.SpanBar--logHint {
pointer-events: none;
}
/* Tweak the popover aesthetics - unfortunate but necessary */
.SpanBar--logHint .ant-popover-inner-content {
padding: 0.25rem;
}

@ -1,86 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { mount } from 'enzyme';
import { Popover } from 'antd';
import SpanBar from './SpanBar';
describe('<SpanBar>', () => {
const shortLabel = 'omg-so-awesome';
const longLabel = 'omg-awesome-long-label';
const props = {
longLabel,
shortLabel,
color: '#fff',
hintSide: 'right',
viewEnd: 1,
viewStart: 0,
getViewedBounds: s => {
// Log entries
if (s === 10) {
return { start: 0.1, end: 0.1 };
}
if (s === 20) {
return { start: 0.2, end: 0.2 };
}
return { error: 'error' };
},
rpc: {
viewStart: 0.25,
viewEnd: 0.75,
color: '#000',
},
tracestartTime: 0,
span: {
logs: [
{
timestamp: 10,
fields: [{ key: 'message', value: 'oh the log message' }, { key: 'something', value: 'else' }],
},
{
timestamp: 10,
fields: [
{ key: 'message', value: 'oh the second log message' },
{ key: 'something', value: 'different' },
],
},
{
timestamp: 20,
fields: [{ key: 'message', value: 'oh the next log message' }, { key: 'more', value: 'stuff' }],
},
],
},
};
it('renders without exploding', () => {
const wrapper = mount(<SpanBar {...props} />);
expect(wrapper).toBeDefined();
const { onMouseOver, onMouseOut } = wrapper.find('.SpanBar--wrapper').props();
const labelElm = wrapper.find('.SpanBar--label');
expect(labelElm.text()).toBe(shortLabel);
onMouseOver();
expect(labelElm.text()).toBe(longLabel);
onMouseOut();
expect(labelElm.text()).toBe(shortLabel);
});
it('log markers count', () => {
// 3 log entries, two grouped together with the same timestamp
const wrapper = mount(<SpanBar {...props} />);
expect(wrapper.find(Popover).length).toEqual(2);
});
});

@ -1,154 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { Popover } from 'antd';
import _groupBy from 'lodash/groupBy';
import { onlyUpdateForKeys, compose, withState, withProps } from 'recompose';
import AccordianLogs from './SpanDetail/AccordianLogs';
import { ViewedBoundsFunctionType } from './utils';
import { TNil } from '../../../types';
import { Span } from '../../../types/trace';
import './SpanBar.css';
type TCommonProps = {
color: string;
hintSide: string;
// onClick: (evt: React.MouseEvent<any>) => void;
onClick?: (evt: React.MouseEvent<any>) => void;
viewEnd: number;
viewStart: number;
getViewedBounds: ViewedBoundsFunctionType;
rpc:
| {
viewStart: number;
viewEnd: number;
color: string;
}
| TNil;
traceStartTime: number;
span: Span;
};
type TInnerProps = {
label: string;
setLongLabel: () => void;
setShortLabel: () => void;
} & TCommonProps;
type TOuterProps = {
longLabel: string;
shortLabel: string;
} & TCommonProps;
function toPercent(value: number) {
return `${(value * 100).toFixed(1)}%`;
}
function SpanBar(props: TInnerProps) {
const {
viewEnd,
viewStart,
getViewedBounds,
color,
label,
hintSide,
onClick,
setLongLabel,
setShortLabel,
rpc,
traceStartTime,
span,
} = props;
// group logs based on timestamps
const logGroups = _groupBy(span.logs, log => {
const posPercent = getViewedBounds(log.timestamp, log.timestamp).start;
// round to the nearest 0.2%
return toPercent(Math.round(posPercent * 500) / 500);
});
return (
<div
className="SpanBar--wrapper"
onClick={onClick}
onMouseOut={setShortLabel}
onMouseOver={setLongLabel}
aria-hidden
>
<div
aria-label={label}
className="SpanBar--bar"
style={{
background: color,
left: toPercent(viewStart),
width: toPercent(viewEnd - viewStart),
}}
>
<div className={`SpanBar--label is-${hintSide}`}>{label}</div>
</div>
<div>
{Object.keys(logGroups).map(positionKey => (
<Popover
key={positionKey}
arrowPointAtCenter
overlayClassName="SpanBar--logHint"
placement="topLeft"
content={
<AccordianLogs
interactive={false}
isOpen
logs={logGroups[positionKey]}
timestamp={traceStartTime}
/>
}
>
<div className="SpanBar--logMarker" style={{ left: positionKey }} />
</Popover>
))}
</div>
{rpc && (
<div
className="SpanBar--rpc"
style={{
background: rpc.color,
left: toPercent(rpc.viewStart),
width: toPercent(rpc.viewEnd - rpc.viewStart),
}}
/>
)}
</div>
);
}
export default compose<TInnerProps, TOuterProps>(
withState('label', 'setLabel', (props: { shortLabel: string }) => props.shortLabel),
withProps(
({
setLabel,
shortLabel,
longLabel,
}: {
setLabel: (label: string) => void;
shortLabel: string;
longLabel: string;
}) => ({
setLongLabel: () => setLabel(longLabel),
setShortLabel: () => setLabel(shortLabel),
})
),
onlyUpdateForKeys(['label', 'rpc', 'viewStart', 'viewEnd'])
)(SpanBar);

@ -1,196 +0,0 @@
/*
Copyright (c) 2017 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.span-row.is-matching-filter {
background-color: #fffce4;
}
.span-name-column {
position: relative;
white-space: nowrap;
z-index: 1;
}
.span-name-column:hover {
z-index: 1;
}
.span-row.clipping-left .span-name-column::before {
content: ' ';
height: 100%;
position: absolute;
width: 6px;
background-image: linear-gradient(to right, rgba(25, 25, 25, 0.25), rgba(32, 32, 32, 0));
left: 100%;
z-index: -1;
}
.span-name-wrapper {
background: #f8f8f8;
line-height: 27px;
overflow: hidden;
display: flex;
}
.span-name-wrapper.is-matching-filter {
background-color: #fffce4;
}
.span-name-wrapper:hover {
border-right: 1px solid #bbb;
float: left;
min-width: calc(100% + 1px);
overflow: visible;
}
.span-row:hover .span-name-wrapper {
background: #f8f8f8;
background: linear-gradient(90deg, #fafafa, #f8f8f8 75%, #eee);
}
.span-row.is-matching-filter:hover .span-name-wrapper {
background: linear-gradient(90deg, #fff5e1, #fff5e1 75%, #ffe6c9);
}
.span-row.is-expanded .span-name-wrapper {
background: #f0f0f0;
box-shadow: 0 1px 0 #ddd;
}
.span-row.is-expanded .span-name-wrapper.is-matching-filter {
background: #fff3d7;
}
.span-name {
color: #000;
cursor: pointer;
flex: 1 1 auto;
outline: none;
overflow: hidden;
padding-left: 4px;
padding-right: 0.25em;
position: relative;
text-overflow: ellipsis;
}
.span-name::before {
content: ' ';
position: absolute;
top: 4px;
bottom: 4px;
left: 0;
border-left: 4px solid;
border-left-color: inherit;
}
.span-name.is-detail-expanded::before {
bottom: 0;
}
/* This is so the hit area of the span-name extends the rest of the width of the span-name column */
.span-name::after {
background: transparent;
bottom: 0;
content: ' ';
left: 0;
position: absolute;
top: 0;
width: 1000px;
}
.span-name:focus {
text-decoration: none;
}
.endpoint-name {
color: #808080;
}
.span-name:hover > .endpoint-name {
color: #000;
}
.span-svc-name {
padding: 0 0.25rem 0 0.5rem;
font-size: 1.05em;
}
.span-svc-name.is-children-collapsed {
font-weight: bold;
font-style: italic;
}
.span-view {
position: relative;
}
.span-row:hover .span-view {
background-color: #f5f5f5;
outline: 1px solid #ddd;
}
.span-row.is-matching-filter:hover .span-view {
background-color: #fff3d7;
outline: 1px solid #ddd;
}
.span-row.is-expanded .span-view {
background: #f8f8f8;
outline: 1px solid #ddd;
}
.span-row.is-expanded.is-matching-filter .span-view {
background: #fff3d7;
outline: 1px solid #ddd;
}
.span-row.is-expanded:hover .span-view {
background: #eee;
}
.span-row.is-expanded.is-matching-filter:hover .span-view {
background: #ffeccf;
}
.span-row.clipping-right .span-view::before {
content: ' ';
height: 100%;
position: absolute;
width: 6px;
background-image: linear-gradient(to left, rgba(25, 25, 25, 0.25), rgba(32, 32, 32, 0));
right: 0%;
z-index: 1;
}
.SpanBarRow--errorIcon {
background: #db2828;
border-radius: 6.5px;
color: #fff;
font-size: 0.85em;
margin-right: 0.25rem;
padding: 1px;
}
.SpanBarRow--rpcColorMarker {
border-radius: 6.5px;
display: inline-block;
font-size: 0.85em;
height: 1em;
margin-right: 0.25rem;
padding: 1px;
width: 1em;
vertical-align: middle;
}

@ -1,165 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { mount, shallow } from 'enzyme';
import SpanBarRow from './SpanBarRow';
import SpanTreeOffset from './SpanTreeOffset';
import ReferencesButton from './ReferencesButton';
jest.mock('./SpanTreeOffset');
describe('<SpanBarRow>', () => {
const spanID = 'some-id';
const props = {
className: 'a-class-name',
color: 'color-a',
columnDivision: '0.5',
isChildrenExpanded: true,
isDetailExpanded: false,
isFilteredOut: false,
onDetailToggled: jest.fn(),
onChildrenToggled: jest.fn(),
operationName: 'op-name',
numTicks: 5,
rpc: {
viewStart: 0.25,
viewEnd: 0.75,
color: 'color-b',
operationName: 'rpc-op-name',
serviceName: 'rpc-service-name',
},
showErrorIcon: false,
getViewedBounds: () => ({ start: 0, end: 1 }),
span: {
duration: 'test-duration',
hasChildren: true,
process: {
serviceName: 'service-name',
},
spanID,
logs: [],
},
};
let wrapper;
beforeEach(() => {
props.onDetailToggled.mockReset();
props.onChildrenToggled.mockReset();
wrapper = mount(<SpanBarRow {...props} />);
});
it('renders without exploding', () => {
expect(wrapper).toBeDefined();
});
it('escalates detail toggling', () => {
const { onDetailToggled } = props;
expect(onDetailToggled.mock.calls.length).toBe(0);
wrapper.find('div.span-view').prop('onClick')();
expect(onDetailToggled.mock.calls).toEqual([[spanID]]);
});
it('escalates children toggling', () => {
const { onChildrenToggled } = props;
expect(onChildrenToggled.mock.calls.length).toBe(0);
wrapper.find(SpanTreeOffset).prop('onClick')();
expect(onChildrenToggled.mock.calls).toEqual([[spanID]]);
});
it('render references button', () => {
const span = Object.assign(
{
references: [
{
refType: 'CHILD_OF',
traceID: 'trace1',
spanID: 'span0',
span: {
spanID: 'span0',
},
},
{
refType: 'CHILD_OF',
traceID: 'otherTrace',
spanID: 'span1',
span: {
spanID: 'span1',
},
},
],
},
props.span
);
const spanRow = shallow(<SpanBarRow {...props} span={span} />);
const refButton = spanRow.find(ReferencesButton);
expect(refButton.length).toEqual(1);
expect(refButton.at(0).props().tooltipText).toEqual('Contains multiple references');
});
it('render referenced to by single span', () => {
const span = Object.assign(
{
subsidiarilyReferencedBy: [
{
refType: 'CHILD_OF',
traceID: 'trace1',
spanID: 'span0',
span: {
spanID: 'span0',
},
},
],
},
props.span
);
const spanRow = shallow(<SpanBarRow {...props} span={span} />);
const refButton = spanRow.find(ReferencesButton);
expect(refButton.length).toEqual(1);
expect(refButton.at(0).props().tooltipText).toEqual('This span is referenced by another span');
});
it('render referenced to by multiple span', () => {
const span = Object.assign(
{
subsidiarilyReferencedBy: [
{
refType: 'CHILD_OF',
traceID: 'trace1',
spanID: 'span0',
span: {
spanID: 'span0',
},
},
{
refType: 'CHILD_OF',
traceID: 'trace1',
spanID: 'span1',
span: {
spanID: 'span1',
},
},
],
},
props.span
);
const spanRow = shallow(<SpanBarRow {...props} span={span} />);
const refButton = spanRow.find(ReferencesButton);
expect(refButton.length).toEqual(1);
expect(refButton.at(0).props().tooltipText).toEqual('This span is referenced by multiple other spans');
});
});

@ -1,202 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as React from 'react';
import IoAlert from 'react-icons/lib/io/alert';
import IoArrowRightA from 'react-icons/lib/io/arrow-right-a';
import IoNetwork from 'react-icons/lib/io/network';
import MdFileUpload from 'react-icons/lib/md/file-upload';
import ReferencesButton from './ReferencesButton';
import TimelineRow from './TimelineRow';
import { formatDuration, ViewedBoundsFunctionType } from './utils';
import SpanTreeOffset from './SpanTreeOffset';
import SpanBar from './SpanBar';
import Ticks from './Ticks';
import { TNil } from '../../../types';
import { Span } from '../../../types/trace';
import './SpanBarRow.css';
type SpanBarRowProps = {
className?: string;
color: string;
columnDivision: number;
isChildrenExpanded: boolean;
isDetailExpanded: boolean;
isMatchingFilter: boolean;
onDetailToggled: (spanID: string) => void;
onChildrenToggled: (spanID: string) => void;
numTicks: number;
rpc?:
| {
viewStart: number;
viewEnd: number;
color: string;
operationName: string;
serviceName: string;
}
| TNil;
showErrorIcon: boolean;
getViewedBounds: ViewedBoundsFunctionType;
traceStartTime: number;
span: Span;
focusSpan: (spanID: string) => void;
};
/**
* 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
* even true in the case where the stateless function has the same prop types as
* this class and arrow functions are created in the stateless function as
* 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<SpanBarRowProps> {
static defaultProps = {
className: '',
rpc: null,
};
_detailToggle = () => {
this.props.onDetailToggled(this.props.span.spanID);
};
_childrenToggle = () => {
this.props.onChildrenToggled(this.props.span.spanID);
};
render() {
const {
className,
color,
columnDivision,
isChildrenExpanded,
isDetailExpanded,
isMatchingFilter,
numTicks,
rpc,
showErrorIcon,
getViewedBounds,
traceStartTime,
span,
focusSpan,
} = this.props;
const {
duration,
hasChildren: isParent,
operationName,
process: { serviceName },
} = span;
const label = formatDuration(duration);
const viewBounds = getViewedBounds(span.startTime, span.startTime + span.duration);
const viewStart = viewBounds.start;
const viewEnd = viewBounds.end;
const labelDetail = `${serviceName}::${operationName}`;
let longLabel;
let hintSide;
if (viewStart > 1 - viewEnd) {
longLabel = `${labelDetail} | ${label}`;
hintSide = 'left';
} else {
longLabel = `${label} | ${labelDetail}`;
hintSide = 'right';
}
return (
<TimelineRow
className={`
span-row
${className || ''}
${isDetailExpanded ? 'is-expanded' : ''}
${isMatchingFilter ? 'is-matching-filter' : ''}
`}
>
<TimelineRow.Cell className="span-name-column" width={columnDivision}>
<div className={`span-name-wrapper ${isMatchingFilter ? 'is-matching-filter' : ''}`}>
<SpanTreeOffset
childrenVisible={isChildrenExpanded}
span={span}
onClick={isParent ? this._childrenToggle : undefined}
/>
<a
className={`span-name ${isDetailExpanded ? 'is-detail-expanded' : ''}`}
aria-checked={isDetailExpanded}
onClick={this._detailToggle}
role="switch"
style={{ borderColor: color }}
tabIndex={0}
>
<span
className={`span-svc-name ${isParent && !isChildrenExpanded ? 'is-children-collapsed' : ''}`}
>
{showErrorIcon && <IoAlert className="SpanBarRow--errorIcon" />}
{serviceName}{' '}
{rpc && (
<span>
<IoArrowRightA />{' '}
<i className="SpanBarRow--rpcColorMarker" style={{ background: rpc.color }} />
{rpc.serviceName}
</span>
)}
</span>
<small className="endpoint-name">{rpc ? rpc.operationName : operationName}</small>
</a>
{span.references && span.references.length > 1 && (
<ReferencesButton
references={span.references}
tooltipText="Contains multiple references"
focusSpan={focusSpan}
>
<IoNetwork />
</ReferencesButton>
)}
{span.subsidiarilyReferencedBy && span.subsidiarilyReferencedBy.length > 0 && (
<ReferencesButton
references={span.subsidiarilyReferencedBy}
tooltipText={`This span is referenced by ${
span.subsidiarilyReferencedBy.length === 1 ? 'another span' : 'multiple other spans'
}`}
focusSpan={focusSpan}
>
<MdFileUpload />
</ReferencesButton>
)}
</div>
</TimelineRow.Cell>
<TimelineRow.Cell
className="span-view"
style={{ cursor: 'pointer' }}
width={1 - columnDivision}
onClick={this._detailToggle}
>
<Ticks numTicks={numTicks} />
<SpanBar
rpc={rpc}
viewStart={viewStart}
viewEnd={viewEnd}
getViewedBounds={getViewedBounds}
color={color}
shortLabel={label}
longLabel={longLabel}
hintSide={hintSide}
traceStartTime={traceStartTime}
span={span}
/>
</TimelineRow.Cell>
</TimelineRow>
);
}
}

@ -1,67 +0,0 @@
/*
Copyright (c) 2017 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.AccordianKeyValues--header {
cursor: pointer;
overflow: hidden;
padding: 0.25em 0.1em;
text-overflow: ellipsis;
white-space: nowrap;
}
.AccordianKeyValues--header:hover {
background: #e8e8e8;
}
.AccordianKeyValues--header.is-empty {
background: none;
cursor: initial;
}
.AccordianKeyValues--header.is-high-contrast:not(.is-empty):hover {
background: #ddd;
}
.AccordianKeyValues--emptyIcon {
color: #aaa;
}
.AccordianKeyValues--summary {
display: inline;
list-style: none;
padding: 0;
}
.AccordianKeyValues--summaryItem {
display: inline;
margin-left: 0.7em;
padding-right: 0.5rem;
border-right: 1px solid #ddd;
}
.AccordianKeyValues--summaryItem:last-child {
padding-right: 0;
border-right: none;
}
.AccordianKeyValues--summaryLabel {
color: #777;
}
.AccordianKeyValues--summaryDelim {
color: #bbb;
padding: 0 0.2em;
}

@ -1,16 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// eslint-disable-next-line import/prefer-default-export
export const LABEL = 'label';

@ -1,94 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { shallow } from 'enzyme';
import AccordianKeyValues, { KeyValuesSummary } from './AccordianKeyValues';
import * as markers from './AccordianKeyValues.markers';
import KeyValuesTable from './KeyValuesTable';
const tags = [{ key: 'span.kind', value: 'client' }, { key: 'omg', value: 'mos-def' }];
describe('<KeyValuesSummary>', () => {
let wrapper;
const props = { data: tags };
beforeEach(() => {
wrapper = shallow(<KeyValuesSummary {...props} />);
});
it('renders without exploding', () => {
expect(wrapper).toBeDefined();
});
it('returns `null` when props.data is empty', () => {
wrapper.setProps({ data: null });
expect(wrapper.isEmptyRender()).toBe(true);
});
it('generates a list from `data`', () => {
expect(wrapper.find('li').length).toBe(tags.length);
});
it('renders the data as text', () => {
const texts = wrapper.find('li').map(node => node.text());
const expectedTexts = tags.map(tag => `${tag.key}=${tag.value}`);
expect(texts).toEqual(expectedTexts);
});
});
describe('<AccordianKeyValues>', () => {
let wrapper;
const props = {
compact: false,
data: tags,
highContrast: false,
isOpen: false,
label: 'le-label',
onToggle: jest.fn(),
};
beforeEach(() => {
wrapper = shallow(<AccordianKeyValues {...props} />);
});
it('renders without exploding', () => {
expect(wrapper).toBeDefined();
expect(wrapper.exists()).toBe(true);
});
it('renders the label', () => {
const header = wrapper.find(`[data-test="${markers.LABEL}"]`);
expect(header.length).toBe(1);
expect(header.text()).toBe(`${props.label}:`);
});
it('renders the summary instead of the table when it is not expanded', () => {
const summary = wrapper.find('.AccordianKeyValues--header').find(KeyValuesSummary);
expect(summary.length).toBe(1);
expect(summary.prop('data')).toBe(tags);
expect(wrapper.find(KeyValuesTable).length).toBe(0);
});
it('renders the table instead of the summarywhen it is expanded', () => {
wrapper.setProps({ isOpen: true });
expect(wrapper.find(KeyValuesSummary).length).toBe(0);
const table = wrapper.find(KeyValuesTable);
expect(table.length).toBe(1);
expect(table.prop('data')).toBe(tags);
});
});

@ -1,104 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as React from 'react';
import cx from 'classnames';
import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down';
import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right';
import * as markers from './AccordianKeyValues.markers';
import KeyValuesTable from './KeyValuesTable';
import { TNil } from '../../../../types';
import { KeyValuePair, Link } from '../../../../types/trace';
import './AccordianKeyValues.css';
type AccordianKeyValuesProps = {
className?: string | TNil;
data: KeyValuePair[];
highContrast?: boolean;
interactive?: boolean;
isOpen: boolean;
label: string;
linksGetter: ((pairs: KeyValuePair[], index: number) => Link[]) | TNil;
onToggle?: null | (() => void);
};
// export for tests
export function KeyValuesSummary(props: { data?: KeyValuePair[] }) {
const { data } = props;
if (!Array.isArray(data) || !data.length) {
return null;
}
return (
<ul className="AccordianKeyValues--summary">
{data.map((item, i) => (
// `i` is necessary in the key because item.key can repeat
// eslint-disable-next-line react/no-array-index-key
<li className="AccordianKeyValues--summaryItem" key={`${item.key}-${i}`}>
<span className="AccordianKeyValues--summaryLabel">{item.key}</span>
<span className="AccordianKeyValues--summaryDelim">=</span>
{String(item.value)}
</li>
))}
</ul>
);
}
KeyValuesSummary.defaultProps = {
data: null,
};
export default function AccordianKeyValues(props: AccordianKeyValuesProps) {
const { className, data, highContrast, interactive, isOpen, label, linksGetter, onToggle } = props;
const isEmpty = !Array.isArray(data) || !data.length;
const iconCls = cx('u-align-icon', { 'AccordianKeyValues--emptyIcon': isEmpty });
let arrow: React.ReactNode | null = null;
let headerProps: Object | null = null;
if (interactive) {
arrow = isOpen ? <IoIosArrowDown className={iconCls} /> : <IoIosArrowRight className={iconCls} />;
headerProps = {
'aria-checked': isOpen,
onClick: isEmpty ? null : onToggle,
role: 'switch',
};
}
return (
<div className={cx(className, 'u-tx-ellipsis')}>
<div
className={cx('AccordianKeyValues--header', {
'is-empty': isEmpty,
'is-high-contrast': highContrast,
})}
{...headerProps}
>
{arrow}
<strong data-test={markers.LABEL}>
{label}
{isOpen || ':'}
</strong>
{!isOpen && <KeyValuesSummary data={data} />}
</div>
{isOpen && <KeyValuesTable data={data} linksGetter={linksGetter} />}
</div>
);
}
AccordianKeyValues.defaultProps = {
className: null,
highContrast: false,
interactive: true,
onToggle: null,
};

@ -1,42 +0,0 @@
/*
Copyright (c) 2017 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.AccordianLogs {
border: 1px solid #d8d8d8;
position: relative;
margin-bottom: 0.25rem;
}
.AccordianLogs--header {
background: #e4e4e4;
color: inherit;
display: block;
padding: 0.25rem 0.5rem;
}
.AccordianLogs--header:hover {
background: #dadada;
}
.AccordianLogs--content {
background: #f0f0f0;
border-top: 1px solid #d8d8d8;
padding: 0.5rem 0.5rem 0.25rem 0.5rem;
}
.AccordianLogs--footer {
color: #999;
}

@ -1,82 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { shallow } from 'enzyme';
import AccordianKeyValues from './AccordianKeyValues';
import AccordianLogs from './AccordianLogs';
describe('<AccordianLogs>', () => {
let wrapper;
const logs = [
{
timestamp: 10,
fields: [{ key: 'message', value: 'oh the log message' }, { key: 'something', value: 'else' }],
},
{
timestamp: 20,
fields: [{ key: 'message', value: 'oh the next log message' }, { key: 'more', value: 'stuff' }],
},
];
const props = {
logs,
isOpen: false,
onItemToggle: jest.fn(),
onToggle: () => {},
openedItems: new Set([logs[1]]),
timestamp: 5,
};
beforeEach(() => {
props.onItemToggle.mockReset();
wrapper = shallow(<AccordianLogs {...props} />);
});
it('renders without exploding', () => {
expect(wrapper).toBeDefined();
});
it('shows the number of log entries', () => {
const regex = new RegExp(`Logs \\(${logs.length}\\)`);
expect(wrapper.find('a').text()).toMatch(regex);
});
it('hides log entries when not expanded', () => {
expect(wrapper.find(AccordianKeyValues).exists()).toBe(false);
});
it('shows log entries when expanded', () => {
expect(wrapper.find(AccordianKeyValues).exists()).toBe(false);
wrapper.setProps({ isOpen: true });
const logViews = wrapper.find(AccordianKeyValues);
expect(logViews.length).toBe(logs.length);
logViews.forEach((node, i) => {
const log = logs[i];
expect(node.prop('data')).toBe(log.fields);
node.simulate('toggle');
expect(props.onItemToggle).toHaveBeenLastCalledWith(log);
});
});
it('propagates isOpen to log items correctly', () => {
wrapper.setProps({ isOpen: true });
const logViews = wrapper.find(AccordianKeyValues);
logViews.forEach((node, i) => {
expect(node.prop('isOpen')).toBe(props.openedItems.has(logs[i]));
});
});
});

@ -1,95 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as React from 'react';
import cx from 'classnames';
import _sortBy from 'lodash/sortBy';
import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down';
import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right';
import AccordianKeyValues from './AccordianKeyValues';
import { formatDuration } from '../utils';
import { TNil } from '../../../../types';
import { Log, KeyValuePair, Link } from '../../../../types/trace';
import './AccordianLogs.css';
type AccordianLogsProps = {
interactive?: boolean;
isOpen: boolean;
linksGetter: ((pairs: KeyValuePair[], index: number) => Link[]) | TNil;
logs: Log[];
onItemToggle?: (log: Log) => void;
onToggle?: () => void;
openedItems?: Set<Log>;
timestamp: number;
};
export default function AccordianLogs(props: AccordianLogsProps) {
const { interactive, isOpen, linksGetter, logs, openedItems, onItemToggle, onToggle, timestamp } = props;
let arrow: React.ReactNode | null = null;
let HeaderComponent: 'span' | 'a' = 'span';
let headerProps: Object | null = null;
if (interactive) {
arrow = isOpen ? (
<IoIosArrowDown className="u-align-icon" />
) : (
<IoIosArrowRight className="u-align-icon" />
);
HeaderComponent = 'a';
headerProps = {
'aria-checked': isOpen,
onClick: onToggle,
role: 'switch',
};
}
return (
<div className="AccordianLogs">
<HeaderComponent className={cx('AccordianLogs--header', { 'is-open': isOpen })} {...headerProps}>
{arrow} <strong>Logs</strong> ({logs.length})
</HeaderComponent>
{isOpen && (
<div className="AccordianLogs--content">
{_sortBy(logs, 'timestamp').map((log, i) => (
<AccordianKeyValues
// `i` is necessary in the key because timestamps can repeat
// eslint-disable-next-line react/no-array-index-key
key={`${log.timestamp}-${i}`}
className={i < logs.length - 1 ? 'ub-mb1' : null}
data={log.fields || []}
highContrast
interactive={interactive}
isOpen={openedItems ? openedItems.has(log) : false}
label={`${formatDuration(log.timestamp - timestamp)}`}
linksGetter={linksGetter}
onToggle={interactive && onItemToggle ? () => onItemToggle(log) : null}
/>
))}
<small className="AccordianLogs--footer">
Log timestamps are relative to the start time of the full trace.
</small>
</div>
)}
</div>
);
}
AccordianLogs.defaultProps = {
interactive: true,
linksGetter: undefined,
onItemToggle: undefined,
onToggle: undefined,
openedItems: undefined,
};

@ -1,60 +0,0 @@
/*
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.
*/
.ReferencesList--List {
width: 100%;
list-style: none;
padding: 0;
margin: 0;
background: #fff;
}
.ReferencesList {
background: #fff;
border: 1px solid #ddd;
margin-bottom: 0.7em;
max-height: 450px;
overflow: auto;
}
.ReferencesList--itemContent {
padding: 0.25rem 0.5rem;
display: flex;
width: 100%;
justify-content: space-between;
}
.ReferencesList--itemContent > a {
width: 100%;
display: inline-block;
}
.ReferencesList--Item:nth-child(2n) {
background: #f5f5f5;
}
.SpanReference--debugInfo {
letter-spacing: 0.25px;
margin: 0.5em 0 0;
}
.SpanReference--debugLabel::before {
color: #bbb;
content: attr(data-label);
}
.SpanReference--debugLabel {
margin: 0 5px 0 5px;
}

@ -1,111 +0,0 @@
// 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 AccordianReferences, { References } from './AccordianReferences';
import ReferenceLink from '../../url/ReferenceLink';
const traceID = 'trace1';
const references = [
{
refType: 'CHILD_OF',
span: {
spanID: 'span1',
traceID,
operationName: 'op1',
process: {
serviceName: 'service1',
},
},
spanID: 'span1',
traceID,
},
{
refType: 'CHILD_OF',
span: {
spanID: 'span3',
traceID,
operationName: 'op2',
process: {
serviceName: 'service2',
},
},
spanID: 'span3',
traceID,
},
{
refType: 'CHILD_OF',
spanID: 'span5',
traceID: 'trace2',
},
];
describe('<AccordianReferences>', () => {
let wrapper;
const props = {
compact: false,
data: references,
highContrast: false,
isOpen: false,
onToggle: jest.fn(),
focusSpan: jest.fn(),
};
beforeEach(() => {
wrapper = shallow(<AccordianReferences {...props} />);
});
it('renders without exploding', () => {
expect(wrapper).toBeDefined();
expect(wrapper.exists()).toBe(true);
});
it('renders the content when it is expanded', () => {
wrapper.setProps({ isOpen: true });
const content = wrapper.find(References);
expect(content.length).toBe(1);
expect(content.prop('data')).toBe(references);
});
});
describe('<References>', () => {
let wrapper;
const props = {
data: references,
focusSpan: jest.fn(),
};
beforeEach(() => {
wrapper = shallow(<References {...props} />);
});
it('render references list', () => {
const refLinks = wrapper.find(ReferenceLink);
expect(refLinks.length).toBe(references.length);
refLinks.forEach((refLink, i) => {
const span = references[i].span;
const serviceName = refLink.find('span.span-svc-name').text();
if (span && span.traceID === traceID) {
const endpointName = refLink.find('small.endpoint-name').text();
expect(serviceName).toBe(span.process.serviceName);
expect(endpointName).toBe(span.operationName);
} else {
expect(serviceName).toBe('< span in another trace >');
}
});
});
});

@ -1,116 +0,0 @@
// 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 * as React from 'react';
import cx from 'classnames';
import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down';
import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right';
import './AccordianReferences.css';
import { SpanReference } from '../../../../types/trace';
import ReferenceLink from '../../url/ReferenceLink';
type AccordianReferencesProps = {
data: SpanReference[];
highContrast?: boolean;
interactive?: boolean;
isOpen: boolean;
onToggle?: null | (() => void);
focusSpan: (uiFind: string) => void;
};
type ReferenceItemProps = {
data: SpanReference[];
focusSpan: (uiFind: string) => void;
};
// export for test
export function References(props: ReferenceItemProps) {
const { data, focusSpan } = props;
return (
<div className="ReferencesList u-simple-scrollbars">
<ul className="ReferencesList--List">
{data.map(reference => {
return (
<li className="ReferencesList--Item" key={`${reference.spanID}`}>
<ReferenceLink reference={reference} focusSpan={focusSpan}>
<span className="ReferencesList--itemContent">
{reference.span ? (
<span>
<span className="span-svc-name">{reference.span.process.serviceName}</span>
<small className="endpoint-name">{reference.span.operationName}</small>
</span>
) : (
<span className="span-svc-name">&lt; span in another trace &gt;</span>
)}
<small className="SpanReference--debugInfo">
<span className="SpanReference--debugLabel" data-label="Reference Type:">
{reference.refType}
</span>
<span className="SpanReference--debugLabel" data-label="SpanID:">
{reference.spanID}
</span>
</small>
</span>
</ReferenceLink>
</li>
);
})}
</ul>
</div>
);
}
export default class AccordianReferences extends React.PureComponent<AccordianReferencesProps> {
static defaultProps = {
highContrast: false,
interactive: true,
onToggle: null,
};
render() {
const { data, highContrast, interactive, isOpen, onToggle, focusSpan } = this.props;
const isEmpty = !Array.isArray(data) || !data.length;
const iconCls = cx('u-align-icon', { 'AccordianKReferences--emptyIcon': isEmpty });
let arrow: React.ReactNode | null = null;
let headerProps: Object | null = null;
if (interactive) {
arrow = isOpen ? <IoIosArrowDown className={iconCls} /> : <IoIosArrowRight className={iconCls} />;
headerProps = {
'aria-checked': isOpen,
onClick: isEmpty ? null : onToggle,
role: 'switch',
};
}
return (
<div className="AccordianReferences">
<div
className={cx('AccordianReferences--header', 'AccordianReferences--header', {
'is-empty': isEmpty,
'is-high-contrast': highContrast,
'is-open': isOpen,
})}
{...headerProps}
>
{arrow}
<strong>
<span className="AccordianReferences--label">References</span>
</strong>{' '}
({data.length})
</div>
{isOpen && <References data={data} focusSpan={focusSpan} />}
</div>
);
}
}

@ -1,27 +0,0 @@
/*
Copyright (c) 2019 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.AccordianText--header {
cursor: pointer;
overflow: hidden;
padding: 0.25em 0.1em;
text-overflow: ellipsis;
white-space: nowrap;
}
.AccordianText--header:hover {
background: #e8e8e8;
}

@ -1,55 +0,0 @@
// Copyright (c) 2019 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { shallow } from 'enzyme';
import AccordianText from './AccordianText';
import TextList from './TextList';
const warnings = ['Duplicated tag', 'Duplicated spanId'];
describe('<AccordianText>', () => {
let wrapper;
const props = {
compact: false,
data: warnings,
highContrast: false,
isOpen: false,
label: 'le-label',
onToggle: jest.fn(),
};
beforeEach(() => {
wrapper = shallow(<AccordianText {...props} />);
});
it('renders without exploding', () => {
expect(wrapper).toBeDefined();
expect(wrapper.exists()).toBe(true);
});
it('renders the label', () => {
const header = wrapper.find(`.AccordianText--header > strong`);
expect(header.length).toBe(1);
expect(header.text()).toBe(props.label);
});
it('renders the content when it is expanded', () => {
wrapper.setProps({ isOpen: true });
const content = wrapper.find(TextList);
expect(content.length).toBe(1);
expect(content.prop('data')).toBe(warnings);
});
});

@ -1,71 +0,0 @@
// Copyright (c) 2019 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as React from 'react';
import cx from 'classnames';
import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down';
import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right';
import TextList from './TextList';
import { TNil } from '../../../../types';
import './AccordianText.css';
type AccordianTextProps = {
className?: string | TNil;
data: string[];
headerClassName?: string | TNil;
highContrast?: boolean;
interactive?: boolean;
isOpen: boolean;
label: React.ReactNode;
onToggle?: null | (() => void);
};
export default function AccordianText(props: AccordianTextProps) {
const { className, data, headerClassName, highContrast, interactive, isOpen, label, onToggle } = props;
const isEmpty = !Array.isArray(data) || !data.length;
const iconCls = cx('u-align-icon', { 'AccordianKeyValues--emptyIcon': isEmpty });
let arrow: React.ReactNode | null = null;
let headerProps: Object | null = null;
if (interactive) {
arrow = isOpen ? <IoIosArrowDown className={iconCls} /> : <IoIosArrowRight className={iconCls} />;
headerProps = {
'aria-checked': isOpen,
onClick: isEmpty ? null : onToggle,
role: 'switch',
};
}
return (
<div className={className || ''}>
<div
className={cx('AccordianText--header', headerClassName, {
'is-empty': isEmpty,
'is-high-contrast': highContrast,
'is-open': isOpen,
})}
{...headerProps}
>
{arrow} <strong>{label}</strong> ({data.length})
</div>
{isOpen && <TextList data={data} />}
</div>
);
}
AccordianText.defaultProps = {
className: null,
highContrast: false,
interactive: true,
onToggle: null,
};

@ -1,84 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Log } from '../../../../types/trace';
/**
* Which items of a {@link SpanDetail} component are expanded.
*/
export default class DetailState {
isTagsOpen: boolean;
isProcessOpen: boolean;
logs: { isOpen: boolean; openedItems: Set<Log> };
isWarningsOpen: boolean;
isReferencesOpen: boolean;
constructor(oldState?: DetailState) {
const {
isTagsOpen,
isProcessOpen,
isReferencesOpen,
isWarningsOpen,
logs,
}: DetailState | Record<string, undefined> = oldState || {};
this.isTagsOpen = Boolean(isTagsOpen);
this.isProcessOpen = Boolean(isProcessOpen);
this.isReferencesOpen = Boolean(isReferencesOpen);
this.isWarningsOpen = Boolean(isWarningsOpen);
this.logs = {
isOpen: Boolean(logs && logs.isOpen),
openedItems: logs && logs.openedItems ? new Set(logs.openedItems) : new Set(),
};
}
toggleTags() {
const next = new DetailState(this);
next.isTagsOpen = !this.isTagsOpen;
return next;
}
toggleProcess() {
const next = new DetailState(this);
next.isProcessOpen = !this.isProcessOpen;
return next;
}
toggleReferences() {
const next = new DetailState(this);
next.isReferencesOpen = !this.isReferencesOpen;
return next;
}
toggleWarnings() {
const next = new DetailState(this);
next.isWarningsOpen = !this.isWarningsOpen;
return next;
}
toggleLogs() {
const next = new DetailState(this);
next.logs.isOpen = !this.logs.isOpen;
return next;
}
toggleLogItem(logItem: Log) {
const next = new DetailState(this);
if (next.logs.openedItems.has(logItem)) {
next.logs.openedItems.delete(logItem);
} else {
next.logs.openedItems.add(logItem);
}
return next;
}
}

@ -1,59 +0,0 @@
/*
Copyright (c) 2017 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.KeyValueTable {
background: #fff;
border: 1px solid #ddd;
margin-bottom: 0.7em;
max-height: 450px;
overflow: auto;
}
.KeyValueTable--body {
vertical-align: baseline;
}
.KeyValueTable--row > td {
padding: 0.25rem 0.5rem;
}
.KeyValueTable--row:nth-child(2n) > td {
background: #f5f5f5;
}
.KeyValueTable--keyColumn {
color: #888;
white-space: pre;
width: 125px;
}
.KeyValueTable--copyColumn {
text-align: right;
}
.KeyValueTable--row:not(:hover) > .KeyValueTable--copyColumn > .KeyValueTable--copyIcon {
display: none;
}
.KeyValueTable--row > td {
padding: 0.25rem 0.5rem;
vertical-align: top;
}
.KeyValueTable--linkIcon {
vertical-align: middle;
font-weight: bold;
}

@ -1,118 +0,0 @@
/* eslint react/prop-types: 0 */
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as React from 'react';
import jsonMarkup from 'json-markup';
import { Dropdown, Menu } from 'antd';
import { ExportOutlined, ProfileOutlined } from '@ant-design/icons';
import CopyIcon from '../../../common/CopyIcon';
import './KeyValuesTable.css';
const jsonObjectOrArrayStartRegex = /^(\[|\{)/;
function parseIfComplexJson(value) {
// if the value is a string representing actual json object or array, then use json-markup
if (typeof value === 'string' && jsonObjectOrArrayStartRegex.test(value)) {
// otherwise just return as is
try {
return JSON.parse(value);
// eslint-disable-next-line no-empty
} catch (_) {}
}
return value;
}
export const LinkValue = props => (
<a href={props.href} title={props.title} target="_blank" rel="noopener noreferrer">
{props.children} <ExportOutlined className="KeyValueTable--linkIcon" />
</a>
);
LinkValue.defaultProps = {
title: '',
};
const linkValueList = links => (
<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 KeyValuesTable(props) {
const { data, linksGetter } = props;
return (
<div className="KeyValueTable u-simple-scrollbars">
<table className="u-width-100">
<tbody className="KeyValueTable--body">
{data.map((row, i) => {
const markup = {
__html: jsonMarkup(parseIfComplexJson(row.value)),
};
// eslint-disable-next-line react/no-danger
const jsonTable = <div className="ub-inline-block" dangerouslySetInnerHTML={markup} />;
const links = linksGetter ? linksGetter(data, i) : null;
let valueMarkup;
if (links && links.length === 1) {
valueMarkup = (
<div>
<LinkValue href={links[0].url} title={links[0].text}>
{jsonTable}
</LinkValue>
</div>
);
} else if (links && links.length > 1) {
valueMarkup = (
<div>
<Dropdown overlay={linkValueList(links)} placement="bottomRight" trigger={['click']}>
<a>
{jsonTable} <ProfileOutlined className="KeyValueTable--linkIcon" />
</a>
</Dropdown>
</div>
);
} else {
valueMarkup = jsonTable;
}
return (
// `i` is necessary in the key because row.key can repeat
// eslint-disable-next-line react/no-array-index-key
<tr className="KeyValueTable--row" key={`${row.key}-${i}`}>
<td className="KeyValueTable--keyColumn">{row.key}</td>
<td>{valueMarkup}</td>
<td className="KeyValueTable--copyColumn">
<CopyIcon
className="KeyValueTable--copyIcon"
copyText={JSON.stringify(row, null, 2)}
tooltipTitle="Copy JSON"
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}

@ -1,145 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { shallow } from 'enzyme';
import { Dropdown, Icon } from 'antd';
import CopyIcon from '../../../common/CopyIcon';
import KeyValuesTable, { LinkValue } from './KeyValuesTable';
describe('LinkValue', () => {
const title = 'titleValue';
const href = 'hrefValue';
const childrenText = 'childrenTextValue';
const wrapper = shallow(
<LinkValue href={href} title={title}>
{childrenText}
</LinkValue>
);
it('renders as expected', () => {
expect(wrapper.find('a').prop('href')).toBe(href);
expect(wrapper.find('a').prop('title')).toBe(title);
expect(wrapper.find('a').text()).toMatch(/childrenText/);
});
it('renders correct Icon', () => {
expect(wrapper.find(Icon).hasClass('KeyValueTable--linkIcon')).toBe(true);
expect(wrapper.find(Icon).prop('type')).toBe('export');
});
});
describe('<KeyValuesTable>', () => {
let wrapper;
const data = [
{ key: 'span.kind', value: 'client' },
{ key: 'omg', value: 'mos-def' },
{ key: 'numericString', value: '12345678901234567890' },
{ key: 'jsonkey', value: JSON.stringify({ hello: 'world' }) },
];
beforeEach(() => {
wrapper = shallow(<KeyValuesTable data={data} />);
});
it('renders without exploding', () => {
expect(wrapper).toBeDefined();
expect(wrapper.find('.KeyValueTable').length).toBe(1);
});
it('renders a table row for each data element', () => {
const trs = wrapper.find('tr');
expect(trs.length).toBe(data.length);
trs.forEach((tr, i) => {
expect(tr.find('.KeyValueTable--keyColumn').text()).toMatch(data[i].key);
});
});
it('renders a single link correctly', () => {
wrapper.setProps({
linksGetter: (array, i) =>
array[i].key === 'span.kind'
? [
{
url: `http://example.com/?kind=${encodeURIComponent(array[i].value)}`,
text: `More info about ${array[i].value}`,
},
]
: [],
});
const anchor = wrapper.find(LinkValue);
expect(anchor).toHaveLength(1);
expect(anchor.prop('href')).toBe('http://example.com/?kind=client');
expect(anchor.prop('title')).toBe('More info about client');
expect(
anchor
.closest('tr')
.find('td')
.first()
.text()
).toBe('span.kind');
});
it('renders multiple links correctly', () => {
wrapper.setProps({
linksGetter: (array, i) =>
array[i].key === 'span.kind'
? [
{ url: `http://example.com/1?kind=${encodeURIComponent(array[i].value)}`, text: 'Example 1' },
{ url: `http://example.com/2?kind=${encodeURIComponent(array[i].value)}`, text: 'Example 2' },
]
: [],
});
const dropdown = wrapper.find(Dropdown);
const menu = shallow(dropdown.prop('overlay'));
const anchors = menu.find(LinkValue);
expect(anchors).toHaveLength(2);
const firstAnchor = anchors.first();
expect(firstAnchor.prop('href')).toBe('http://example.com/1?kind=client');
expect(firstAnchor.children().text()).toBe('Example 1');
const secondAnchor = anchors.last();
expect(secondAnchor.prop('href')).toBe('http://example.com/2?kind=client');
expect(secondAnchor.children().text()).toBe('Example 2');
expect(
dropdown
.closest('tr')
.find('td')
.first()
.text()
).toBe('span.kind');
});
it('renders a <CopyIcon /> with correct copyText for each data element', () => {
const copyIcons = wrapper.find(CopyIcon);
expect(copyIcons.length).toBe(data.length);
copyIcons.forEach((copyIcon, i) => {
expect(copyIcon.prop('copyText')).toBe(JSON.stringify(data[i], null, 2));
expect(copyIcon.prop('tooltipTitle')).toBe('Copy JSON');
});
});
it('renders a span value containing numeric string correctly', () => {
const el = wrapper.find('.ub-inline-block');
expect(el.length).toBe(data.length);
el.forEach((valueDiv, i) => {
if (data[i].key !== 'jsonkey') {
expect(valueDiv.html()).toMatch(`"${data[i].value}"`);
}
});
});
});

@ -1,36 +0,0 @@
/*
Copyright (c) 2019 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.TextList {
max-height: 450px;
overflow: auto;
}
.TextList--List {
width: 100%;
list-style: none;
padding: 0;
margin: 0;
}
.TextList--List > li:nth-child(2n) {
background: #f5f5f5;
}
.TextList--List > li {
padding: 0.25rem 0.5rem;
vertical-align: top;
}

@ -1,37 +0,0 @@
// Copyright (c) 2019 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { shallow } from 'enzyme';
import TextList from './TextList';
describe('<TextList>', () => {
let wrapper;
const data = [{ key: 'span.kind', value: 'client' }, { key: 'omg', value: 'mos-def' }];
beforeEach(() => {
wrapper = shallow(<TextList data={data} />);
});
it('renders without exploding', () => {
expect(wrapper).toBeDefined();
expect(wrapper.find('.TextList').length).toBe(1);
});
it('renders a table row for each data element', () => {
const trs = wrapper.find('li');
expect(trs.length).toBe(data.length);
});
});

@ -1,38 +0,0 @@
// Copyright (c) 2019 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as React from 'react';
import './TextList.css';
type TextListProps = {
data: string[];
};
export default function TextList(props: TextListProps) {
const { data } = props;
return (
<div className="TextList u-simple-scrollbars">
<ul className="TextList--List ">
{data.map((row, i) => {
return (
// `i` is necessary in the key because row.key can repeat
// eslint-disable-next-line react/no-array-index-key
<li key={`${i}`}>{row}</li>
);
})}
</ul>
</div>
);
}

@ -1,63 +0,0 @@
/*
Copyright (c) 2017 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.SpanDetail--divider {
background: #ddd;
}
.SpanDetail--debugInfo {
display: block;
letter-spacing: 0.25px;
margin: 0.5em 0 -0.75em;
text-align: right;
}
.SpanDetail--debugLabel::before {
color: #bbb;
content: attr(data-label);
}
.SpanDetail--debugValue {
background-color: inherit;
border: none;
color: #888;
cursor: pointer;
}
.SpanDetail--debugValue:hover {
color: #333;
}
.AccordianWarnings {
background: #fafafa;
border: 1px solid #e4e4e4;
margin-bottom: 0.25rem;
}
.AccordianWarnings--header {
background: #fff7e6;
padding: 0.25rem 0.5rem;
}
.AccordianWarnings--header:hover {
background: #ffe7ba;
}
.AccordianWarnings--header.is-open {
border-bottom: 1px solid #e8e8e8;
}
.AccordianWarnings--label {
color: #d36c08;
}

@ -1,192 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/* eslint-disable import/first */
jest.mock('../utils');
import React from 'react';
import { shallow } from 'enzyme';
import AccordianKeyValues from './AccordianKeyValues';
import AccordianLogs from './AccordianLogs';
import DetailState from './DetailState';
import SpanDetail from './index';
import { formatDuration } from '../utils';
import CopyIcon from '../../../common/CopyIcon';
import LabeledList from '../../../common/LabeledList';
import traceGenerator from '../../../../demo/trace-generators';
import transformTraceData from '../../../../model/transform-trace-data';
describe('<SpanDetail>', () => {
let wrapper;
// use `transformTraceData` on a fake trace to get a fully processed span
const span = transformTraceData(traceGenerator.trace({ numberOfSpans: 1 })).spans[0];
const detailState = new DetailState()
.toggleLogs()
.toggleProcess()
.toggleReferences()
.toggleTags();
const traceStartTime = 5;
const props = {
detailState,
span,
traceStartTime,
logItemToggle: jest.fn(),
logsToggle: jest.fn(),
processToggle: jest.fn(),
tagsToggle: jest.fn(),
warningsToggle: jest.fn(),
referencesToggle: jest.fn(),
};
span.logs = [
{
timestamp: 10,
fields: [{ key: 'message', value: 'oh the log message' }, { key: 'something', value: 'else' }],
},
{
timestamp: 20,
fields: [{ key: 'message', value: 'oh the next log message' }, { key: 'more', value: 'stuff' }],
},
];
span.warnings = ['Warning 1', 'Warning 2'];
span.references = [
{
refType: 'CHILD_OF',
span: {
spanID: 'span2',
traceID: 'trace1',
operationName: 'op1',
process: {
serviceName: 'service1',
},
},
spanID: 'span1',
traceID: 'trace1',
},
{
refType: 'CHILD_OF',
span: {
spanID: 'span3',
traceID: 'trace1',
operationName: 'op2',
process: {
serviceName: 'service2',
},
},
spanID: 'span4',
traceID: 'trace1',
},
{
refType: 'CHILD_OF',
span: {
spanID: 'span6',
traceID: 'trace2',
operationName: 'op2',
process: {
serviceName: 'service2',
},
},
spanID: 'span5',
traceID: 'trace2',
},
];
beforeEach(() => {
formatDuration.mockReset();
props.tagsToggle.mockReset();
props.processToggle.mockReset();
props.logsToggle.mockReset();
props.logItemToggle.mockReset();
wrapper = shallow(<SpanDetail {...props} />);
});
it('renders without exploding', () => {
expect(wrapper).toBeDefined();
});
it('shows the operation name', () => {
expect(wrapper.find('h2').text()).toBe(span.operationName);
});
it('lists the service name, duration and start time', () => {
const words = ['Duration:', 'Service:', 'Start Time:'];
const overview = wrapper.find(LabeledList);
expect(
overview
.prop('items')
.map(item => item.label)
.sort()
).toEqual(words);
});
it('renders the span tags', () => {
const target = <AccordianKeyValues data={span.tags} label="Tags" isOpen={detailState.isTagsOpen} />;
expect(wrapper.containsMatchingElement(target)).toBe(true);
wrapper.find({ data: span.tags }).simulate('toggle');
expect(props.tagsToggle).toHaveBeenLastCalledWith(span.spanID);
});
it('renders the process tags', () => {
const target = (
<AccordianKeyValues data={span.process.tags} label="Process" isOpen={detailState.isProcessOpen} />
);
expect(wrapper.containsMatchingElement(target)).toBe(true);
wrapper.find({ data: span.process.tags }).simulate('toggle');
expect(props.processToggle).toHaveBeenLastCalledWith(span.spanID);
});
it('renders the logs', () => {
const somethingUniq = {};
const target = (
<AccordianLogs
logs={span.logs}
isOpen={detailState.logs.isOpen}
openedItems={detailState.logs.openedItems}
timestamp={traceStartTime}
/>
);
expect(wrapper.containsMatchingElement(target)).toBe(true);
const accordianLogs = wrapper.find(AccordianLogs);
accordianLogs.simulate('toggle');
accordianLogs.simulate('itemToggle', somethingUniq);
expect(props.logsToggle).toHaveBeenLastCalledWith(span.spanID);
expect(props.logItemToggle).toHaveBeenLastCalledWith(span.spanID, somethingUniq);
});
it('renders the warnings', () => {
const warningElm = wrapper.find({ data: span.warnings });
expect(warningElm.length).toBe(1);
warningElm.simulate('toggle');
expect(props.warningsToggle).toHaveBeenLastCalledWith(span.spanID);
});
it('renders the references', () => {
const refElem = wrapper.find({ data: span.references });
expect(refElem.length).toBe(1);
refElem.simulate('toggle');
expect(props.referencesToggle).toHaveBeenLastCalledWith(span.spanID);
});
it('renders CopyIcon with deep link URL', () => {
expect(
wrapper
.find(CopyIcon)
.prop('copyText')
.includes(`?uiFind=${props.span.spanID}`)
).toBe(true);
});
});

@ -1,163 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { Divider } from 'antd';
import AccordianKeyValues from './AccordianKeyValues';
import AccordianLogs from './AccordianLogs';
import AccordianText from './AccordianText';
import DetailState from './DetailState';
import { formatDuration } from '../utils';
import CopyIcon from '../../../common/CopyIcon';
import LabeledList from '../../../common/LabeledList';
import { TNil } from '../../../../types';
import { KeyValuePair, Link, Log, Span } from '../../../../types/trace';
import './index.css';
import AccordianReferences from './AccordianReferences';
type SpanDetailProps = {
detailState: DetailState;
linksGetter: ((links: KeyValuePair[], index: number) => Link[]) | TNil;
logItemToggle: (spanID: string, log: Log) => void;
logsToggle: (spanID: string) => void;
processToggle: (spanID: string) => void;
span: Span;
tagsToggle: (spanID: string) => void;
traceStartTime: number;
warningsToggle: (spanID: string) => void;
referencesToggle: (spanID: string) => void;
focusSpan: (uiFind: string) => 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}`;
return (
<div>
<div className="ub-flex ub-items-center">
<h2 className="ub-flex-auto ub-m0">{operationName}</h2>
<LabeledList
className="ub-tx-right-align"
dividerClassName="SpanDetail--divider"
items={overviewItems}
/>
</div>
<Divider className="SpanDetail--divider ub-my1" />
<div>
<div>
<AccordianKeyValues
data={tags}
label="Tags"
linksGetter={linksGetter}
isOpen={isTagsOpen}
onToggle={() => tagsToggle(spanID)}
/>
{process.tags && (
<AccordianKeyValues
className="ub-mb1"
data={process.tags}
label="Process"
linksGetter={linksGetter}
isOpen={isProcessOpen}
onToggle={() => processToggle(spanID)}
/>
)}
</div>
{logs && logs.length > 0 && (
<AccordianLogs
linksGetter={linksGetter}
logs={logs}
isOpen={logsState.isOpen}
openedItems={logsState.openedItems}
onToggle={() => logsToggle(spanID)}
onItemToggle={logItem => logItemToggle(spanID, logItem)}
timestamp={traceStartTime}
/>
)}
{warnings && warnings.length > 0 && (
<AccordianText
className="AccordianWarnings"
headerClassName="AccordianWarnings--header"
label={<span className="AccordianWarnings--label">Warnings</span>}
data={warnings}
isOpen={isWarningsOpen}
onToggle={() => warningsToggle(spanID)}
/>
)}
{references && references.length > 1 && (
<AccordianReferences
data={references}
isOpen={isReferencesOpen}
onToggle={() => referencesToggle(spanID)}
focusSpan={focusSpan}
/>
)}
<small className="SpanDetail--debugInfo">
<span className="SpanDetail--debugLabel" data-label="SpanID:" /> {spanID}
<CopyIcon
copyText={deepLinkCopyText}
icon="link"
placement="topRight"
tooltipTitle="Copy deep link to this span"
/>
</small>
</div>
</div>
);
}

@ -1,56 +0,0 @@
/*
Copyright (c) 2017 Uber Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.detail-row-expanded-accent {
cursor: pointer;
height: 100%;
overflow: hidden;
position: absolute;
width: 100%;
}
.detail-row-expanded-accent::before {
border-left: 4px solid;
pointer-events: none;
width: 1000px;
}
.detail-row-expanded-accent::after {
border-right: 1000px solid;
border-color: inherit;
cursor: pointer;
opacity: 0.2;
}
/* border-color inherit must come AFTER other border declarations for accent */
.detail-row-expanded-accent::before,
.detail-row-expanded-accent::after {
border-color: inherit;
content: ' ';
position: absolute;
height: 100%;
}
.detail-row-expanded-accent:hover::after {
opacity: 0.35;
}
.detail-info-wrapper {
background: #f5f5f5;
border: 1px solid #d3d3d3;
border-top: 3px solid;
padding: 0.75rem;
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save