mirror of https://github.com/sunface/rust-course
parent
1d1fc6d06e
commit
2bfb0741e0
@ -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">< span in another trace ></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…
Reference in new issue