mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[APM] Trace sample performance improvements (#183802)
Fixes #178985 ## Summary This PR changes the frontend logic to render the trace waterfall component. Instead of recursively rendering transactions/spans and their child transactions/spans, which causes high memory usage depending on the amount of data/how complex the trace to be rendered is, it now uses tree data structure and BFS/DFS algorithms. Besides that, the trace sample component can render a very long list. To avoid rendering too many elements in the DOM, this PR changes it to use a virtual list ### Memory consumption 15-minutes worth of data | before | after | |-------|-------| |<img width="590" alt="image" src="45746f12
-3119-4641-9d68-a725a1fff6ac">|<img width="590" alt="image" src="64e7e5f2
-8dda-40eb-8abc-f1974aeb7072">| 30-minutes worth of data | before | after | |-------|-------| |<img width="590" alt="image" src="a0b32774
-4bb9-4d8c-a088-b4baea0c204a">|<img width="590" alt="image" src="b09188e3
-2fa9-4d38-b344-f3dd3656bde8">| 1-hour worth of data | before | after | |-------|-------| |<img width="590" alt="image" src="c33f61ff
-e7f8-4f1c-ac49-28bb4c819303">|<img width="590" alt="image" src="ad5299cd
-7a72-43e1-aa4a-407c99acb107">| ### Extra Sticky header fix632485ee
-80c5-486d-aaa2-c34047b9c11e ### How to test The best way to test is to connect to an oblt cluster - Navigate to APM > Dependencies - Go into `cartService` - Click on `Operations` tab and click on `POST /nodejs/addToCart` operation. - Select different date ranges and services ### For reviewers There is a problem with positioning the trace elements in the grid when rendering data for large date ranges https://github.com/elastic/kibana/issues/178985#issuecomment-2137480777. This won't be addressed in this PR --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
15424370e1
commit
620359f893
24 changed files with 1601 additions and 512 deletions
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { ApmFields } from './apm_fields';
|
||||
import { Serializable } from '../serializable';
|
||||
import { generateLongId, generateShortId } from '../utils/generate_id';
|
||||
import { generateLongIdWithSeed, generateShortId, generateLongId } from '../utils/generate_id';
|
||||
|
||||
export class ApmError extends Serializable<ApmFields> {
|
||||
constructor(fields: ApmFields) {
|
||||
|
@ -21,10 +21,13 @@ export class ApmError extends Serializable<ApmFields> {
|
|||
}
|
||||
|
||||
serialize() {
|
||||
const errorMessage =
|
||||
this.fields['error.grouping_name'] || this.fields['error.exception']?.[0]?.message;
|
||||
|
||||
const [data] = super.serialize();
|
||||
data['error.grouping_key'] = generateLongId(
|
||||
this.fields['error.grouping_name'] || this.fields['error.exception']?.[0]?.message
|
||||
);
|
||||
data['error.grouping_key'] = errorMessage
|
||||
? generateLongIdWithSeed(errorMessage)
|
||||
: generateLongId();
|
||||
return [data];
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import { Span } from './span';
|
|||
import { Transaction } from './transaction';
|
||||
import { Event } from './event';
|
||||
import { ApmApplicationMetricFields, ApmFields, GeoLocation, SpanParams } from './apm_fields';
|
||||
import { generateLongId } from '../utils/generate_id';
|
||||
import { generateLongIdWithSeed, generateLongId } from '../utils/generate_id';
|
||||
import { Metricset } from './metricset';
|
||||
import { ApmError } from './apm_error';
|
||||
|
||||
|
@ -259,7 +259,7 @@ export class MobileDevice extends Entity<ApmFields> {
|
|||
return new ApmError({
|
||||
...this.fields,
|
||||
'error.type': 'crash',
|
||||
'error.id': generateLongId(message),
|
||||
'error.id': generateLongIdWithSeed(message),
|
||||
'error.exception': [{ message, ...{ type: 'crash' } }],
|
||||
'error.grouping_name': groupingName || message,
|
||||
});
|
||||
|
|
|
@ -7,16 +7,33 @@
|
|||
*/
|
||||
|
||||
let seq = 0;
|
||||
const pid = String(process.pid);
|
||||
|
||||
function generateId(seed?: string, length: number = 32) {
|
||||
const str = seed ?? String(seq++);
|
||||
return str.padStart(length, '0');
|
||||
const LONG_ID_LENGTH = 32;
|
||||
const SHORT_ID_LENGTH = 16;
|
||||
|
||||
function generateId(length: number = LONG_ID_LENGTH) {
|
||||
const id = String(seq++);
|
||||
const generatedId = pid + id.padStart(length - pid.length, '0');
|
||||
if (generatedId.length > length) {
|
||||
throw new Error(`generated id is longer than ${length} characters: ${generatedId.length}`);
|
||||
}
|
||||
|
||||
return generatedId;
|
||||
}
|
||||
|
||||
export function generateShortId(seed?: string) {
|
||||
return generateId(seed, 16);
|
||||
function generateIdWithSeed(seed: string, length: number = LONG_ID_LENGTH) {
|
||||
return seed?.padStart(length, '0');
|
||||
}
|
||||
|
||||
export function generateLongId(seed?: string) {
|
||||
return generateId(seed, 32);
|
||||
export function generateShortId() {
|
||||
return generateId(SHORT_ID_LENGTH);
|
||||
}
|
||||
|
||||
export function generateLongId() {
|
||||
return generateId(LONG_ID_LENGTH);
|
||||
}
|
||||
|
||||
export function generateLongIdWithSeed(seed: string) {
|
||||
return generateIdWithSeed(seed, LONG_ID_LENGTH);
|
||||
}
|
||||
|
|
|
@ -48,7 +48,24 @@ describe('simple trace', () => {
|
|||
|
||||
// TODO this is not entirely factual, since id's are generated of a global sequence number
|
||||
it('generates the same data every time', () => {
|
||||
expect(events).toMatchSnapshot();
|
||||
expect(events).toMatchSnapshot(
|
||||
events.map((event) => {
|
||||
const matchers: Record<string, any> = {};
|
||||
if (event['transaction.id']) {
|
||||
matchers['transaction.id'] = expect.any(String);
|
||||
}
|
||||
if (event['trace.id']) {
|
||||
matchers['trace.id'] = expect.any(String);
|
||||
}
|
||||
if (event['span.id']) {
|
||||
matchers['span.id'] = expect.any(String);
|
||||
}
|
||||
if (event['parent.id']) {
|
||||
matchers['parent.id'] = expect.any(String);
|
||||
}
|
||||
return matchers;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('generates 15 transaction events', () => {
|
||||
|
@ -83,9 +100,9 @@ describe('simple trace', () => {
|
|||
'service.name': 'opbeans-java',
|
||||
'service.node.name': 'instance-1',
|
||||
'timestamp.us': 1609459200000000,
|
||||
'trace.id': '00000000000000000000000000000241',
|
||||
'trace.id': expect.stringContaining('00000000000000000000000241'),
|
||||
'transaction.duration.us': 1000000,
|
||||
'transaction.id': '0000000000000240',
|
||||
'transaction.id': expect.stringContaining('0000000240'),
|
||||
'transaction.name': 'GET /api/product/list',
|
||||
'transaction.type': 'request',
|
||||
'transaction.sampled': true,
|
||||
|
@ -95,26 +112,28 @@ describe('simple trace', () => {
|
|||
it('outputs span events', () => {
|
||||
const [, span] = events;
|
||||
|
||||
expect(span).toEqual({
|
||||
'@timestamp': 1609459200050,
|
||||
'agent.name': 'java',
|
||||
'container.id': 'instance-1',
|
||||
'event.outcome': 'success',
|
||||
'host.name': 'instance-1',
|
||||
'parent.id': '0000000000000300',
|
||||
'processor.event': 'span',
|
||||
'processor.name': 'transaction',
|
||||
'service.environment': 'production',
|
||||
'service.name': 'opbeans-java',
|
||||
'service.node.name': 'instance-1',
|
||||
'span.duration.us': 900000,
|
||||
'span.id': '0000000000000302',
|
||||
'span.name': 'GET apm-*/_search',
|
||||
'span.subtype': 'elasticsearch',
|
||||
'span.type': 'db',
|
||||
'timestamp.us': 1609459200050000,
|
||||
'trace.id': '00000000000000000000000000000301',
|
||||
'transaction.id': '0000000000000300',
|
||||
});
|
||||
expect(span).toEqual(
|
||||
expect.objectContaining({
|
||||
'@timestamp': 1609459200050,
|
||||
'agent.name': 'java',
|
||||
'container.id': 'instance-1',
|
||||
'event.outcome': 'success',
|
||||
'host.name': 'instance-1',
|
||||
'parent.id': expect.stringContaining('0000000300'),
|
||||
'processor.event': 'span',
|
||||
'processor.name': 'transaction',
|
||||
'service.environment': 'production',
|
||||
'service.name': 'opbeans-java',
|
||||
'service.node.name': 'instance-1',
|
||||
'span.duration.us': 900000,
|
||||
'span.id': expect.stringContaining('0000000302'),
|
||||
'span.name': 'GET apm-*/_search',
|
||||
'span.subtype': 'elasticsearch',
|
||||
'span.type': 'db',
|
||||
'timestamp.us': 1609459200050000,
|
||||
'trace.id': expect.stringContaining('00000000000000000000000301'),
|
||||
'transaction.id': expect.stringContaining('0000000300'),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,9 +14,9 @@ Array [
|
|||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"timestamp.us": 1609459200000000,
|
||||
"trace.id": "00000000000000000000000000000001",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.duration.us": 1000000,
|
||||
"transaction.id": "0000000000000000",
|
||||
"transaction.id": Any<String>,
|
||||
"transaction.name": "GET /api/product/list",
|
||||
"transaction.sampled": true,
|
||||
"transaction.type": "request",
|
||||
|
@ -27,20 +27,20 @@ Array [
|
|||
"container.id": "instance-1",
|
||||
"event.outcome": "success",
|
||||
"host.name": "instance-1",
|
||||
"parent.id": "0000000000000000",
|
||||
"parent.id": Any<String>,
|
||||
"processor.event": "span",
|
||||
"processor.name": "transaction",
|
||||
"service.environment": "production",
|
||||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"span.duration.us": 900000,
|
||||
"span.id": "0000000000000002",
|
||||
"span.id": Any<String>,
|
||||
"span.name": "GET apm-*/_search",
|
||||
"span.subtype": "elasticsearch",
|
||||
"span.type": "db",
|
||||
"timestamp.us": 1609459200050000,
|
||||
"trace.id": "00000000000000000000000000000001",
|
||||
"transaction.id": "0000000000000000",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.id": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"@timestamp": 1609459200000,
|
||||
|
@ -95,9 +95,9 @@ Array [
|
|||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"timestamp.us": 1609459260000000,
|
||||
"trace.id": "00000000000000000000000000000005",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.duration.us": 1000000,
|
||||
"transaction.id": "0000000000000004",
|
||||
"transaction.id": Any<String>,
|
||||
"transaction.name": "GET /api/product/list",
|
||||
"transaction.sampled": true,
|
||||
"transaction.type": "request",
|
||||
|
@ -108,20 +108,20 @@ Array [
|
|||
"container.id": "instance-1",
|
||||
"event.outcome": "success",
|
||||
"host.name": "instance-1",
|
||||
"parent.id": "0000000000000004",
|
||||
"parent.id": Any<String>,
|
||||
"processor.event": "span",
|
||||
"processor.name": "transaction",
|
||||
"service.environment": "production",
|
||||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"span.duration.us": 900000,
|
||||
"span.id": "0000000000000006",
|
||||
"span.id": Any<String>,
|
||||
"span.name": "GET apm-*/_search",
|
||||
"span.subtype": "elasticsearch",
|
||||
"span.type": "db",
|
||||
"timestamp.us": 1609459260050000,
|
||||
"trace.id": "00000000000000000000000000000005",
|
||||
"transaction.id": "0000000000000004",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.id": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"@timestamp": 1609459260000,
|
||||
|
@ -176,9 +176,9 @@ Array [
|
|||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"timestamp.us": 1609459320000000,
|
||||
"trace.id": "00000000000000000000000000000009",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.duration.us": 1000000,
|
||||
"transaction.id": "0000000000000008",
|
||||
"transaction.id": Any<String>,
|
||||
"transaction.name": "GET /api/product/list",
|
||||
"transaction.sampled": true,
|
||||
"transaction.type": "request",
|
||||
|
@ -189,20 +189,20 @@ Array [
|
|||
"container.id": "instance-1",
|
||||
"event.outcome": "success",
|
||||
"host.name": "instance-1",
|
||||
"parent.id": "0000000000000008",
|
||||
"parent.id": Any<String>,
|
||||
"processor.event": "span",
|
||||
"processor.name": "transaction",
|
||||
"service.environment": "production",
|
||||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"span.duration.us": 900000,
|
||||
"span.id": "0000000000000010",
|
||||
"span.id": Any<String>,
|
||||
"span.name": "GET apm-*/_search",
|
||||
"span.subtype": "elasticsearch",
|
||||
"span.type": "db",
|
||||
"timestamp.us": 1609459320050000,
|
||||
"trace.id": "00000000000000000000000000000009",
|
||||
"transaction.id": "0000000000000008",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.id": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"@timestamp": 1609459320000,
|
||||
|
@ -257,9 +257,9 @@ Array [
|
|||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"timestamp.us": 1609459380000000,
|
||||
"trace.id": "00000000000000000000000000000013",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.duration.us": 1000000,
|
||||
"transaction.id": "0000000000000012",
|
||||
"transaction.id": Any<String>,
|
||||
"transaction.name": "GET /api/product/list",
|
||||
"transaction.sampled": true,
|
||||
"transaction.type": "request",
|
||||
|
@ -270,20 +270,20 @@ Array [
|
|||
"container.id": "instance-1",
|
||||
"event.outcome": "success",
|
||||
"host.name": "instance-1",
|
||||
"parent.id": "0000000000000012",
|
||||
"parent.id": Any<String>,
|
||||
"processor.event": "span",
|
||||
"processor.name": "transaction",
|
||||
"service.environment": "production",
|
||||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"span.duration.us": 900000,
|
||||
"span.id": "0000000000000014",
|
||||
"span.id": Any<String>,
|
||||
"span.name": "GET apm-*/_search",
|
||||
"span.subtype": "elasticsearch",
|
||||
"span.type": "db",
|
||||
"timestamp.us": 1609459380050000,
|
||||
"trace.id": "00000000000000000000000000000013",
|
||||
"transaction.id": "0000000000000012",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.id": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"@timestamp": 1609459380000,
|
||||
|
@ -338,9 +338,9 @@ Array [
|
|||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"timestamp.us": 1609459440000000,
|
||||
"trace.id": "00000000000000000000000000000017",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.duration.us": 1000000,
|
||||
"transaction.id": "0000000000000016",
|
||||
"transaction.id": Any<String>,
|
||||
"transaction.name": "GET /api/product/list",
|
||||
"transaction.sampled": true,
|
||||
"transaction.type": "request",
|
||||
|
@ -351,20 +351,20 @@ Array [
|
|||
"container.id": "instance-1",
|
||||
"event.outcome": "success",
|
||||
"host.name": "instance-1",
|
||||
"parent.id": "0000000000000016",
|
||||
"parent.id": Any<String>,
|
||||
"processor.event": "span",
|
||||
"processor.name": "transaction",
|
||||
"service.environment": "production",
|
||||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"span.duration.us": 900000,
|
||||
"span.id": "0000000000000018",
|
||||
"span.id": Any<String>,
|
||||
"span.name": "GET apm-*/_search",
|
||||
"span.subtype": "elasticsearch",
|
||||
"span.type": "db",
|
||||
"timestamp.us": 1609459440050000,
|
||||
"trace.id": "00000000000000000000000000000017",
|
||||
"transaction.id": "0000000000000016",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.id": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"@timestamp": 1609459440000,
|
||||
|
@ -419,9 +419,9 @@ Array [
|
|||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"timestamp.us": 1609459500000000,
|
||||
"trace.id": "00000000000000000000000000000021",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.duration.us": 1000000,
|
||||
"transaction.id": "0000000000000020",
|
||||
"transaction.id": Any<String>,
|
||||
"transaction.name": "GET /api/product/list",
|
||||
"transaction.sampled": true,
|
||||
"transaction.type": "request",
|
||||
|
@ -432,20 +432,20 @@ Array [
|
|||
"container.id": "instance-1",
|
||||
"event.outcome": "success",
|
||||
"host.name": "instance-1",
|
||||
"parent.id": "0000000000000020",
|
||||
"parent.id": Any<String>,
|
||||
"processor.event": "span",
|
||||
"processor.name": "transaction",
|
||||
"service.environment": "production",
|
||||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"span.duration.us": 900000,
|
||||
"span.id": "0000000000000022",
|
||||
"span.id": Any<String>,
|
||||
"span.name": "GET apm-*/_search",
|
||||
"span.subtype": "elasticsearch",
|
||||
"span.type": "db",
|
||||
"timestamp.us": 1609459500050000,
|
||||
"trace.id": "00000000000000000000000000000021",
|
||||
"transaction.id": "0000000000000020",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.id": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"@timestamp": 1609459500000,
|
||||
|
@ -500,9 +500,9 @@ Array [
|
|||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"timestamp.us": 1609459560000000,
|
||||
"trace.id": "00000000000000000000000000000025",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.duration.us": 1000000,
|
||||
"transaction.id": "0000000000000024",
|
||||
"transaction.id": Any<String>,
|
||||
"transaction.name": "GET /api/product/list",
|
||||
"transaction.sampled": true,
|
||||
"transaction.type": "request",
|
||||
|
@ -513,20 +513,20 @@ Array [
|
|||
"container.id": "instance-1",
|
||||
"event.outcome": "success",
|
||||
"host.name": "instance-1",
|
||||
"parent.id": "0000000000000024",
|
||||
"parent.id": Any<String>,
|
||||
"processor.event": "span",
|
||||
"processor.name": "transaction",
|
||||
"service.environment": "production",
|
||||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"span.duration.us": 900000,
|
||||
"span.id": "0000000000000026",
|
||||
"span.id": Any<String>,
|
||||
"span.name": "GET apm-*/_search",
|
||||
"span.subtype": "elasticsearch",
|
||||
"span.type": "db",
|
||||
"timestamp.us": 1609459560050000,
|
||||
"trace.id": "00000000000000000000000000000025",
|
||||
"transaction.id": "0000000000000024",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.id": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"@timestamp": 1609459560000,
|
||||
|
@ -581,9 +581,9 @@ Array [
|
|||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"timestamp.us": 1609459620000000,
|
||||
"trace.id": "00000000000000000000000000000029",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.duration.us": 1000000,
|
||||
"transaction.id": "0000000000000028",
|
||||
"transaction.id": Any<String>,
|
||||
"transaction.name": "GET /api/product/list",
|
||||
"transaction.sampled": true,
|
||||
"transaction.type": "request",
|
||||
|
@ -594,20 +594,20 @@ Array [
|
|||
"container.id": "instance-1",
|
||||
"event.outcome": "success",
|
||||
"host.name": "instance-1",
|
||||
"parent.id": "0000000000000028",
|
||||
"parent.id": Any<String>,
|
||||
"processor.event": "span",
|
||||
"processor.name": "transaction",
|
||||
"service.environment": "production",
|
||||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"span.duration.us": 900000,
|
||||
"span.id": "0000000000000030",
|
||||
"span.id": Any<String>,
|
||||
"span.name": "GET apm-*/_search",
|
||||
"span.subtype": "elasticsearch",
|
||||
"span.type": "db",
|
||||
"timestamp.us": 1609459620050000,
|
||||
"trace.id": "00000000000000000000000000000029",
|
||||
"transaction.id": "0000000000000028",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.id": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"@timestamp": 1609459620000,
|
||||
|
@ -662,9 +662,9 @@ Array [
|
|||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"timestamp.us": 1609459680000000,
|
||||
"trace.id": "00000000000000000000000000000033",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.duration.us": 1000000,
|
||||
"transaction.id": "0000000000000032",
|
||||
"transaction.id": Any<String>,
|
||||
"transaction.name": "GET /api/product/list",
|
||||
"transaction.sampled": true,
|
||||
"transaction.type": "request",
|
||||
|
@ -675,20 +675,20 @@ Array [
|
|||
"container.id": "instance-1",
|
||||
"event.outcome": "success",
|
||||
"host.name": "instance-1",
|
||||
"parent.id": "0000000000000032",
|
||||
"parent.id": Any<String>,
|
||||
"processor.event": "span",
|
||||
"processor.name": "transaction",
|
||||
"service.environment": "production",
|
||||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"span.duration.us": 900000,
|
||||
"span.id": "0000000000000034",
|
||||
"span.id": Any<String>,
|
||||
"span.name": "GET apm-*/_search",
|
||||
"span.subtype": "elasticsearch",
|
||||
"span.type": "db",
|
||||
"timestamp.us": 1609459680050000,
|
||||
"trace.id": "00000000000000000000000000000033",
|
||||
"transaction.id": "0000000000000032",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.id": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"@timestamp": 1609459680000,
|
||||
|
@ -743,9 +743,9 @@ Array [
|
|||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"timestamp.us": 1609459740000000,
|
||||
"trace.id": "00000000000000000000000000000037",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.duration.us": 1000000,
|
||||
"transaction.id": "0000000000000036",
|
||||
"transaction.id": Any<String>,
|
||||
"transaction.name": "GET /api/product/list",
|
||||
"transaction.sampled": true,
|
||||
"transaction.type": "request",
|
||||
|
@ -756,20 +756,20 @@ Array [
|
|||
"container.id": "instance-1",
|
||||
"event.outcome": "success",
|
||||
"host.name": "instance-1",
|
||||
"parent.id": "0000000000000036",
|
||||
"parent.id": Any<String>,
|
||||
"processor.event": "span",
|
||||
"processor.name": "transaction",
|
||||
"service.environment": "production",
|
||||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"span.duration.us": 900000,
|
||||
"span.id": "0000000000000038",
|
||||
"span.id": Any<String>,
|
||||
"span.name": "GET apm-*/_search",
|
||||
"span.subtype": "elasticsearch",
|
||||
"span.type": "db",
|
||||
"timestamp.us": 1609459740050000,
|
||||
"trace.id": "00000000000000000000000000000037",
|
||||
"transaction.id": "0000000000000036",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.id": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"@timestamp": 1609459740000,
|
||||
|
@ -824,9 +824,9 @@ Array [
|
|||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"timestamp.us": 1609459800000000,
|
||||
"trace.id": "00000000000000000000000000000041",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.duration.us": 1000000,
|
||||
"transaction.id": "0000000000000040",
|
||||
"transaction.id": Any<String>,
|
||||
"transaction.name": "GET /api/product/list",
|
||||
"transaction.sampled": true,
|
||||
"transaction.type": "request",
|
||||
|
@ -837,20 +837,20 @@ Array [
|
|||
"container.id": "instance-1",
|
||||
"event.outcome": "success",
|
||||
"host.name": "instance-1",
|
||||
"parent.id": "0000000000000040",
|
||||
"parent.id": Any<String>,
|
||||
"processor.event": "span",
|
||||
"processor.name": "transaction",
|
||||
"service.environment": "production",
|
||||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"span.duration.us": 900000,
|
||||
"span.id": "0000000000000042",
|
||||
"span.id": Any<String>,
|
||||
"span.name": "GET apm-*/_search",
|
||||
"span.subtype": "elasticsearch",
|
||||
"span.type": "db",
|
||||
"timestamp.us": 1609459800050000,
|
||||
"trace.id": "00000000000000000000000000000041",
|
||||
"transaction.id": "0000000000000040",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.id": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"@timestamp": 1609459800000,
|
||||
|
@ -905,9 +905,9 @@ Array [
|
|||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"timestamp.us": 1609459860000000,
|
||||
"trace.id": "00000000000000000000000000000045",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.duration.us": 1000000,
|
||||
"transaction.id": "0000000000000044",
|
||||
"transaction.id": Any<String>,
|
||||
"transaction.name": "GET /api/product/list",
|
||||
"transaction.sampled": true,
|
||||
"transaction.type": "request",
|
||||
|
@ -918,20 +918,20 @@ Array [
|
|||
"container.id": "instance-1",
|
||||
"event.outcome": "success",
|
||||
"host.name": "instance-1",
|
||||
"parent.id": "0000000000000044",
|
||||
"parent.id": Any<String>,
|
||||
"processor.event": "span",
|
||||
"processor.name": "transaction",
|
||||
"service.environment": "production",
|
||||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"span.duration.us": 900000,
|
||||
"span.id": "0000000000000046",
|
||||
"span.id": Any<String>,
|
||||
"span.name": "GET apm-*/_search",
|
||||
"span.subtype": "elasticsearch",
|
||||
"span.type": "db",
|
||||
"timestamp.us": 1609459860050000,
|
||||
"trace.id": "00000000000000000000000000000045",
|
||||
"transaction.id": "0000000000000044",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.id": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"@timestamp": 1609459860000,
|
||||
|
@ -986,9 +986,9 @@ Array [
|
|||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"timestamp.us": 1609459920000000,
|
||||
"trace.id": "00000000000000000000000000000049",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.duration.us": 1000000,
|
||||
"transaction.id": "0000000000000048",
|
||||
"transaction.id": Any<String>,
|
||||
"transaction.name": "GET /api/product/list",
|
||||
"transaction.sampled": true,
|
||||
"transaction.type": "request",
|
||||
|
@ -999,20 +999,20 @@ Array [
|
|||
"container.id": "instance-1",
|
||||
"event.outcome": "success",
|
||||
"host.name": "instance-1",
|
||||
"parent.id": "0000000000000048",
|
||||
"parent.id": Any<String>,
|
||||
"processor.event": "span",
|
||||
"processor.name": "transaction",
|
||||
"service.environment": "production",
|
||||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"span.duration.us": 900000,
|
||||
"span.id": "0000000000000050",
|
||||
"span.id": Any<String>,
|
||||
"span.name": "GET apm-*/_search",
|
||||
"span.subtype": "elasticsearch",
|
||||
"span.type": "db",
|
||||
"timestamp.us": 1609459920050000,
|
||||
"trace.id": "00000000000000000000000000000049",
|
||||
"transaction.id": "0000000000000048",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.id": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"@timestamp": 1609459920000,
|
||||
|
@ -1067,9 +1067,9 @@ Array [
|
|||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"timestamp.us": 1609459980000000,
|
||||
"trace.id": "00000000000000000000000000000053",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.duration.us": 1000000,
|
||||
"transaction.id": "0000000000000052",
|
||||
"transaction.id": Any<String>,
|
||||
"transaction.name": "GET /api/product/list",
|
||||
"transaction.sampled": true,
|
||||
"transaction.type": "request",
|
||||
|
@ -1080,20 +1080,20 @@ Array [
|
|||
"container.id": "instance-1",
|
||||
"event.outcome": "success",
|
||||
"host.name": "instance-1",
|
||||
"parent.id": "0000000000000052",
|
||||
"parent.id": Any<String>,
|
||||
"processor.event": "span",
|
||||
"processor.name": "transaction",
|
||||
"service.environment": "production",
|
||||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"span.duration.us": 900000,
|
||||
"span.id": "0000000000000054",
|
||||
"span.id": Any<String>,
|
||||
"span.name": "GET apm-*/_search",
|
||||
"span.subtype": "elasticsearch",
|
||||
"span.type": "db",
|
||||
"timestamp.us": 1609459980050000,
|
||||
"trace.id": "00000000000000000000000000000053",
|
||||
"transaction.id": "0000000000000052",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.id": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"@timestamp": 1609459980000,
|
||||
|
@ -1148,9 +1148,9 @@ Array [
|
|||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"timestamp.us": 1609460040000000,
|
||||
"trace.id": "00000000000000000000000000000057",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.duration.us": 1000000,
|
||||
"transaction.id": "0000000000000056",
|
||||
"transaction.id": Any<String>,
|
||||
"transaction.name": "GET /api/product/list",
|
||||
"transaction.sampled": true,
|
||||
"transaction.type": "request",
|
||||
|
@ -1161,20 +1161,20 @@ Array [
|
|||
"container.id": "instance-1",
|
||||
"event.outcome": "success",
|
||||
"host.name": "instance-1",
|
||||
"parent.id": "0000000000000056",
|
||||
"parent.id": Any<String>,
|
||||
"processor.event": "span",
|
||||
"processor.name": "transaction",
|
||||
"service.environment": "production",
|
||||
"service.name": "opbeans-java",
|
||||
"service.node.name": "instance-1",
|
||||
"span.duration.us": 900000,
|
||||
"span.id": "0000000000000058",
|
||||
"span.id": Any<String>,
|
||||
"span.name": "GET apm-*/_search",
|
||||
"span.subtype": "elasticsearch",
|
||||
"span.type": "db",
|
||||
"timestamp.us": 1609460040050000,
|
||||
"trace.id": "00000000000000000000000000000057",
|
||||
"transaction.id": "0000000000000056",
|
||||
"trace.id": Any<String>,
|
||||
"transaction.id": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"@timestamp": 1609460040000,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { getErrorGroupingKey } from '@kbn/apm-synthtrace-client/src/lib/apm/instance';
|
||||
import { generateLongId } from '@kbn/apm-synthtrace-client/src/lib/utils/generate_id';
|
||||
import { generateLongIdWithSeed } from '@kbn/apm-synthtrace-client/src/lib/utils/generate_id';
|
||||
|
||||
import url from 'url';
|
||||
import { synthtrace } from '../../../synthtrace';
|
||||
|
@ -71,7 +71,7 @@ describe('Error details', () => {
|
|||
});
|
||||
|
||||
describe('when error has data', () => {
|
||||
const errorGroupingKey = generateLongId('Error 1');
|
||||
const errorGroupingKey = generateLongIdWithSeed('Error 1');
|
||||
const errorGroupingKeyShort = errorGroupingKey.slice(0, 5);
|
||||
const errorDetailsPageHref = url.format({
|
||||
pathname: `/app/apm/services/opbeans-java/errors/${errorGroupingKey}`,
|
||||
|
|
|
@ -42,7 +42,9 @@ describe('Large Trace in waterfall', () => {
|
|||
});
|
||||
|
||||
it('renders waterfall items', () => {
|
||||
cy.getByTestSubj('waterfallItem').should('have.length.greaterThan', 200);
|
||||
// it renders a virtual list, so the number of items rendered is not the same as the number of items in the trace
|
||||
cy.getByTestSubj('waterfallItem').should('have.length.at.least', 39);
|
||||
cy.getByTestSubj('waterfall').should('have.css', 'height').and('eq', '10011px');
|
||||
});
|
||||
|
||||
it('shows warning about trace size', () => {
|
||||
|
@ -70,7 +72,9 @@ describe('Large Trace in waterfall', () => {
|
|||
});
|
||||
|
||||
it('renders waterfall items', () => {
|
||||
cy.getByTestSubj('waterfallItem').should('have.length.greaterThan', 400);
|
||||
// it renders a virtual list, so the number of items rendered is not the same as the number of items in the trace
|
||||
cy.getByTestSubj('waterfallItem').should('have.length.at.least', 39);
|
||||
cy.getByTestSubj('waterfall').should('have.css', 'height').and('eq', '10011px');
|
||||
});
|
||||
|
||||
it('does not show the warning about trace size', () => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { omit, orderBy } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
|
@ -20,6 +20,7 @@ import { ResettingHeightRetainer } from '../../shared/height_retainer/resetting_
|
|||
import { push, replace } from '../../shared/links/url_helpers';
|
||||
import { useWaterfallFetcher } from '../transaction_details/use_waterfall_fetcher';
|
||||
import { WaterfallWithSummary } from '../transaction_details/waterfall_with_summary';
|
||||
import { TransactionTab } from '../transaction_details/waterfall_with_summary/transaction_tabs';
|
||||
import { DependencyOperationDistributionChart } from './dependency_operation_distribution_chart';
|
||||
import { DetailViewHeader } from './detail_view_header';
|
||||
import { maybeRedirectToAvailableSpanSample } from './maybe_redirect_to_available_span_sample';
|
||||
|
@ -115,9 +116,37 @@ export function DependencyOperationDetailView() {
|
|||
const isWaterfallLoading =
|
||||
spanFetch.status === FETCH_STATUS.NOT_INITIATED ||
|
||||
(spanFetch.status === FETCH_STATUS.LOADING && samples.length === 0) ||
|
||||
waterfallFetch.status === FETCH_STATUS.LOADING ||
|
||||
!waterfallFetch.waterfall.entryWaterfallTransaction;
|
||||
(waterfallFetch.status === FETCH_STATUS.LOADING &&
|
||||
!waterfallFetch.waterfall.entryWaterfallTransaction);
|
||||
|
||||
const onSampleClick = useCallback(
|
||||
(sample: any) => {
|
||||
push(history, { query: { spanId: sample.spanId } });
|
||||
},
|
||||
[history]
|
||||
);
|
||||
|
||||
const onTabClick = useCallback(
|
||||
(nextDetailTab: TransactionTab) => {
|
||||
push(history, {
|
||||
query: {
|
||||
detailTab: nextDetailTab,
|
||||
},
|
||||
});
|
||||
},
|
||||
[history]
|
||||
);
|
||||
|
||||
const onShowCriticalPathChange = useCallback(
|
||||
(nextShowCriticalPath: boolean) => {
|
||||
push(history, {
|
||||
query: {
|
||||
showCriticalPath: nextShowCriticalPath ? 'true' : 'false',
|
||||
},
|
||||
});
|
||||
},
|
||||
[history]
|
||||
);
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
|
@ -147,31 +176,18 @@ export function DependencyOperationDetailView() {
|
|||
<ResettingHeightRetainer reset={!isWaterfallLoading}>
|
||||
<WaterfallWithSummary
|
||||
environment={environment}
|
||||
waterfallFetchResult={waterfallFetch}
|
||||
waterfallFetchResult={waterfallFetch.waterfall}
|
||||
waterfallFetchStatus={waterfallFetch.status}
|
||||
traceSamples={samples}
|
||||
traceSamplesFetchStatus={spanFetch.status}
|
||||
onSampleClick={(sample) => {
|
||||
push(history, { query: { spanId: sample.spanId } });
|
||||
}}
|
||||
onTabClick={(tab) => {
|
||||
push(history, {
|
||||
query: {
|
||||
detailTab: tab,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onSampleClick={onSampleClick}
|
||||
onTabClick={onTabClick}
|
||||
serviceName={waterfallFetch.waterfall.entryWaterfallTransaction?.doc.service.name}
|
||||
waterfallItemId={waterfallItemId}
|
||||
detailTab={detailTab}
|
||||
selectedSample={selectedSample || null}
|
||||
showCriticalPath={showCriticalPath}
|
||||
onShowCriticalPathChange={(nextShowCriticalPath) => {
|
||||
push(history, {
|
||||
query: {
|
||||
showCriticalPath: nextShowCriticalPath ? 'true' : 'false',
|
||||
},
|
||||
});
|
||||
}}
|
||||
onShowCriticalPathChange={onShowCriticalPathChange}
|
||||
/>
|
||||
</ResettingHeightRetainer>
|
||||
</EuiPanel>
|
||||
|
|
|
@ -4,14 +4,17 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useEffect } from 'react';
|
||||
import { FETCH_STATUS } from '@kbn/observability-shared-plugin/public';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { useTraceExplorerSamples } from '../../../hooks/use_trace_explorer_samples';
|
||||
import { ResettingHeightRetainer } from '../../shared/height_retainer/resetting_height_container';
|
||||
import { push, replace } from '../../shared/links/url_helpers';
|
||||
import { useWaterfallFetcher } from '../transaction_details/use_waterfall_fetcher';
|
||||
import { WaterfallWithSummary } from '../transaction_details/waterfall_with_summary';
|
||||
import { TransactionTab } from '../transaction_details/waterfall_with_summary/transaction_tabs';
|
||||
|
||||
export function TraceExplorerWaterfall() {
|
||||
const history = useHistory();
|
||||
|
@ -52,39 +55,61 @@ export function TraceExplorerWaterfall() {
|
|||
end,
|
||||
});
|
||||
|
||||
const onSampleClick = useCallback(
|
||||
(sample: any) => {
|
||||
push(history, {
|
||||
query: {
|
||||
traceId: sample.traceId,
|
||||
transactionId: sample.transactionId,
|
||||
waterfallItemId: '',
|
||||
},
|
||||
});
|
||||
},
|
||||
[history]
|
||||
);
|
||||
|
||||
const onTabClick = useCallback(
|
||||
(nextDetailTab: TransactionTab) => {
|
||||
push(history, {
|
||||
query: {
|
||||
detailTab: nextDetailTab,
|
||||
},
|
||||
});
|
||||
},
|
||||
[history]
|
||||
);
|
||||
|
||||
const onShowCriticalPathChange = useCallback(
|
||||
(nextShowCriticalPath: boolean) => {
|
||||
push(history, {
|
||||
query: {
|
||||
showCriticalPath: nextShowCriticalPath ? 'true' : 'false',
|
||||
},
|
||||
});
|
||||
},
|
||||
[history]
|
||||
);
|
||||
|
||||
const isWaterfallLoading =
|
||||
waterfallFetchResult.status === FETCH_STATUS.LOADING &&
|
||||
!waterfallFetchResult.waterfall.entryWaterfallTransaction;
|
||||
|
||||
return (
|
||||
<WaterfallWithSummary
|
||||
waterfallFetchResult={waterfallFetchResult}
|
||||
traceSamples={traceSamplesFetchResult.data.traceSamples}
|
||||
traceSamplesFetchStatus={traceSamplesFetchResult.status}
|
||||
environment={environment}
|
||||
onSampleClick={(sample) => {
|
||||
push(history, {
|
||||
query: {
|
||||
traceId: sample.traceId,
|
||||
transactionId: sample.transactionId,
|
||||
waterfallItemId: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
onTabClick={(nextDetailTab) => {
|
||||
push(history, {
|
||||
query: {
|
||||
detailTab: nextDetailTab,
|
||||
},
|
||||
});
|
||||
}}
|
||||
detailTab={detailTab}
|
||||
waterfallItemId={waterfallItemId}
|
||||
serviceName={waterfallFetchResult.waterfall.entryWaterfallTransaction?.doc.service.name}
|
||||
showCriticalPath={showCriticalPath}
|
||||
onShowCriticalPathChange={(nextShowCriticalPath) => {
|
||||
push(history, {
|
||||
query: {
|
||||
showCriticalPath: nextShowCriticalPath ? 'true' : 'false',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ResettingHeightRetainer reset={!isWaterfallLoading}>
|
||||
<WaterfallWithSummary
|
||||
waterfallFetchResult={waterfallFetchResult.waterfall}
|
||||
waterfallFetchStatus={waterfallFetchResult.status}
|
||||
traceSamples={traceSamplesFetchResult.data.traceSamples}
|
||||
traceSamplesFetchStatus={traceSamplesFetchResult.status}
|
||||
environment={environment}
|
||||
onSampleClick={onSampleClick}
|
||||
onTabClick={onTabClick}
|
||||
detailTab={detailTab}
|
||||
waterfallItemId={waterfallItemId}
|
||||
serviceName={waterfallFetchResult.waterfall.entryWaterfallTransaction?.doc.service.name}
|
||||
showCriticalPath={showCriticalPath}
|
||||
onShowCriticalPathChange={onShowCriticalPathChange}
|
||||
/>
|
||||
</ResettingHeightRetainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -92,6 +92,20 @@ export function TransactionDistribution({
|
|||
[history]
|
||||
);
|
||||
|
||||
const onSampleClick = useCallback(
|
||||
(sample: { transactionId: string; traceId: string }) => {
|
||||
history.push({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...toQuery(history.location.search),
|
||||
transactionId: sample.transactionId,
|
||||
traceId: sample.traceId,
|
||||
}),
|
||||
});
|
||||
},
|
||||
[history]
|
||||
);
|
||||
|
||||
return (
|
||||
<ResettingHeightRetainer reset={!traceId}>
|
||||
<div data-test-subj="apmTransactionDistributionTabContent">
|
||||
|
@ -111,21 +125,13 @@ export function TransactionDistribution({
|
|||
<EuiSpacer size="s" />
|
||||
<WaterfallWithSummary
|
||||
environment={environment}
|
||||
onSampleClick={(sample) => {
|
||||
history.push({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...toQuery(history.location.search),
|
||||
transactionId: sample.transactionId,
|
||||
traceId: sample.traceId,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
onSampleClick={onSampleClick}
|
||||
onTabClick={onTabClick}
|
||||
serviceName={serviceName}
|
||||
waterfallItemId={waterfallItemId}
|
||||
detailTab={detailTab as TransactionTab | undefined}
|
||||
waterfallFetchResult={waterfallFetchResult}
|
||||
waterfallFetchResult={waterfallFetchResult.waterfall}
|
||||
waterfallFetchStatus={waterfallFetchResult.status}
|
||||
traceSamplesFetchStatus={traceSamplesFetchResult.status}
|
||||
traceSamples={traceSamplesFetchResult.data?.traceSamples}
|
||||
showCriticalPath={showCriticalPath}
|
||||
|
|
|
@ -25,9 +25,10 @@ import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
|
|||
import { WaterfallFetchResult } from '../use_waterfall_fetcher';
|
||||
|
||||
interface Props<TSample extends {}> {
|
||||
waterfallFetchResult: WaterfallFetchResult;
|
||||
waterfallFetchResult: WaterfallFetchResult['waterfall'];
|
||||
traceSamples?: TSample[];
|
||||
traceSamplesFetchStatus: FETCH_STATUS;
|
||||
waterfallFetchStatus: FETCH_STATUS;
|
||||
environment: Environment;
|
||||
onSampleClick: (sample: TSample) => void;
|
||||
onTabClick: (tab: TransactionTab) => void;
|
||||
|
@ -41,6 +42,7 @@ interface Props<TSample extends {}> {
|
|||
|
||||
export function WaterfallWithSummary<TSample extends {}>({
|
||||
waterfallFetchResult,
|
||||
waterfallFetchStatus,
|
||||
traceSamples,
|
||||
traceSamplesFetchStatus,
|
||||
environment,
|
||||
|
@ -58,12 +60,12 @@ export function WaterfallWithSummary<TSample extends {}>({
|
|||
const isControlled = selectedSample !== undefined;
|
||||
|
||||
const isLoading =
|
||||
waterfallFetchResult.status === FETCH_STATUS.LOADING ||
|
||||
waterfallFetchStatus === FETCH_STATUS.LOADING ||
|
||||
traceSamplesFetchStatus === FETCH_STATUS.LOADING;
|
||||
// When traceId is not present, call to waterfallFetchResult will not be initiated
|
||||
const isSucceded =
|
||||
(waterfallFetchResult.status === FETCH_STATUS.SUCCESS ||
|
||||
waterfallFetchResult.status === FETCH_STATUS.NOT_INITIATED) &&
|
||||
(waterfallFetchStatus === FETCH_STATUS.SUCCESS ||
|
||||
waterfallFetchStatus === FETCH_STATUS.NOT_INITIATED) &&
|
||||
traceSamplesFetchStatus === FETCH_STATUS.SUCCESS;
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -86,7 +88,7 @@ export function WaterfallWithSummary<TSample extends {}>({
|
|||
: 0
|
||||
: sampleActivePage;
|
||||
|
||||
const { entryTransaction } = waterfallFetchResult.waterfall;
|
||||
const { entryTransaction } = waterfallFetchResult;
|
||||
|
||||
if (!entryTransaction && traceSamples?.length === 0 && isSucceded) {
|
||||
return (
|
||||
|
@ -136,7 +138,7 @@ export function WaterfallWithSummary<TSample extends {}>({
|
|||
<MaybeViewTraceLink
|
||||
isLoading={isLoading}
|
||||
transaction={entryTransaction}
|
||||
waterfall={waterfallFetchResult.waterfall}
|
||||
waterfall={waterfallFetchResult}
|
||||
environment={environment}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -153,8 +155,8 @@ export function WaterfallWithSummary<TSample extends {}>({
|
|||
) : (
|
||||
<EuiFlexItem grow={false}>
|
||||
<TransactionSummary
|
||||
errorCount={waterfallFetchResult.waterfall.totalErrorsCount}
|
||||
totalDuration={waterfallFetchResult.waterfall.rootWaterfallTransaction?.duration}
|
||||
errorCount={waterfallFetchResult.totalErrorsCount}
|
||||
totalDuration={waterfallFetchResult.rootWaterfallTransaction?.duration}
|
||||
transaction={entryTransaction}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -167,7 +169,7 @@ export function WaterfallWithSummary<TSample extends {}>({
|
|||
serviceName={serviceName}
|
||||
waterfallItemId={waterfallItemId}
|
||||
onTabClick={onTabClick}
|
||||
waterfall={waterfallFetchResult.waterfall}
|
||||
waterfall={waterfallFetchResult}
|
||||
isLoading={isLoading}
|
||||
showCriticalPath={showCriticalPath}
|
||||
onShowCriticalPathChange={onShowCriticalPathChange}
|
||||
|
|
|
@ -15,20 +15,24 @@ import {
|
|||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { groupBy } from 'lodash';
|
||||
import { transparentize } from 'polished';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { WindowScroller, AutoSizer } from 'react-virtualized';
|
||||
import { areEqual, ListChildComponentProps, VariableSizeList as List } from 'react-window';
|
||||
import { asBigNumber } from '../../../../../../../common/utils/formatters';
|
||||
import { getCriticalPath } from '../../../../../../../common/critical_path/get_critical_path';
|
||||
import { useTheme } from '../../../../../../hooks/use_theme';
|
||||
import { Margins } from '../../../../../shared/charts/timeline';
|
||||
import { IWaterfall, IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers';
|
||||
import {
|
||||
IWaterfallNodeFlatten,
|
||||
IWaterfall,
|
||||
IWaterfallSpanOrTransaction,
|
||||
} from './waterfall_helpers/waterfall_helpers';
|
||||
import { WaterfallItem } from './waterfall_item';
|
||||
import { WaterfallContextProvider } from './context/waterfall_context';
|
||||
import { useWaterfallContext } from './context/use_waterfall';
|
||||
|
||||
interface AccordionWaterfallProps {
|
||||
isOpen: boolean;
|
||||
item: IWaterfallSpanOrTransaction;
|
||||
level: number;
|
||||
duration: IWaterfall['duration'];
|
||||
waterfallItemId?: string;
|
||||
waterfall: IWaterfall;
|
||||
|
@ -38,24 +42,27 @@ interface AccordionWaterfallProps {
|
|||
maxLevelOpen: number;
|
||||
}
|
||||
|
||||
const ACCORDION_HEIGHT = '48px';
|
||||
type WaterfallProps = Omit<
|
||||
AccordionWaterfallProps,
|
||||
'item' | 'maxLevelOpen' | 'showCriticalPath' | 'waterfall' | 'isOpen'
|
||||
>;
|
||||
|
||||
interface WaterfallNodeProps extends WaterfallProps {
|
||||
node: IWaterfallNodeFlatten;
|
||||
}
|
||||
|
||||
const ACCORDION_HEIGHT = 48;
|
||||
|
||||
const StyledAccordion = euiStyled(EuiAccordion).withConfig({
|
||||
shouldForwardProp: (prop) => !['childrenCount', 'marginLeftLevel', 'hasError'].includes(prop),
|
||||
shouldForwardProp: (prop) => !['marginLeftLevel', 'hasError'].includes(prop),
|
||||
})<
|
||||
EuiAccordionProps & {
|
||||
childrenCount: number;
|
||||
marginLeftLevel: number;
|
||||
hasError: boolean;
|
||||
}
|
||||
>`
|
||||
.waterfall_accordion {
|
||||
border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
|
||||
}
|
||||
|
||||
.euiAccordion__childWrapper {
|
||||
transition: none;
|
||||
}
|
||||
border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
|
||||
|
||||
${(props) => {
|
||||
const borderLeft = props.hasError
|
||||
|
@ -63,7 +70,7 @@ const StyledAccordion = euiStyled(EuiAccordion).withConfig({
|
|||
: `1px solid ${props.theme.eui.euiColorLightShade};`;
|
||||
return `.button_${props.id} {
|
||||
width: 100%;
|
||||
height: ${ACCORDION_HEIGHT};
|
||||
height: ${ACCORDION_HEIGHT}px;
|
||||
margin-left: ${props.marginLeftLevel}px;
|
||||
border-left: ${borderLeft}
|
||||
&:hover {
|
||||
|
@ -78,111 +85,166 @@ const StyledAccordion = euiStyled(EuiAccordion).withConfig({
|
|||
}
|
||||
`;
|
||||
|
||||
export function AccordionWaterfall(props: AccordionWaterfallProps) {
|
||||
const {
|
||||
item,
|
||||
level,
|
||||
duration,
|
||||
waterfall,
|
||||
waterfallItemId,
|
||||
timelineMargins,
|
||||
onClickWaterfallItem,
|
||||
showCriticalPath,
|
||||
maxLevelOpen,
|
||||
} = props;
|
||||
export function AccordionWaterfall({
|
||||
maxLevelOpen,
|
||||
showCriticalPath,
|
||||
waterfall,
|
||||
isOpen,
|
||||
...props
|
||||
}: AccordionWaterfallProps) {
|
||||
return (
|
||||
<WaterfallContextProvider
|
||||
maxLevelOpen={maxLevelOpen}
|
||||
showCriticalPath={showCriticalPath}
|
||||
waterfall={waterfall}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<Waterfall {...props} />
|
||||
</WaterfallContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Waterfall(props: WaterfallProps) {
|
||||
const listRef = useRef<List>(null);
|
||||
const rowSizeMapRef = useRef(new Map<number, number>());
|
||||
const { traceList } = useWaterfallContext();
|
||||
|
||||
const onRowLoad = (index: number, size: number) => {
|
||||
rowSizeMapRef.current.set(index, size);
|
||||
};
|
||||
|
||||
const getRowSize = (index: number) => {
|
||||
// adds 1px for the border top
|
||||
return rowSizeMapRef.current.get(index) || ACCORDION_HEIGHT + 1;
|
||||
};
|
||||
|
||||
const onScroll = ({ scrollTop }: { scrollTop: number }) => {
|
||||
listRef.current?.scrollTo(scrollTop);
|
||||
};
|
||||
|
||||
return (
|
||||
<WindowScroller onScroll={onScroll}>
|
||||
{({ registerChild }) => (
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
<div data-test-subj="waterfall" ref={registerChild}>
|
||||
<List
|
||||
ref={listRef}
|
||||
style={{ height: '100%' }}
|
||||
itemCount={traceList.length}
|
||||
itemSize={getRowSize}
|
||||
height={window.innerHeight}
|
||||
width={width}
|
||||
itemData={{ ...props, traceList, onLoad: onRowLoad }}
|
||||
>
|
||||
{VirtualRow}
|
||||
</List>
|
||||
</div>
|
||||
)}
|
||||
</AutoSizer>
|
||||
)}
|
||||
</WindowScroller>
|
||||
);
|
||||
}
|
||||
|
||||
const VirtualRow = React.memo(
|
||||
({
|
||||
index,
|
||||
style,
|
||||
data,
|
||||
}: ListChildComponentProps<
|
||||
Omit<WaterfallNodeProps, 'node'> & {
|
||||
traceList: IWaterfallNodeFlatten[];
|
||||
onLoad: (index: number, size: number) => void;
|
||||
}
|
||||
>) => {
|
||||
const { onLoad, traceList, ...props } = data;
|
||||
|
||||
const ref = React.useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
onLoad(index, ref.current?.getBoundingClientRect().height ?? ACCORDION_HEIGHT);
|
||||
}, [index, onLoad]);
|
||||
|
||||
return (
|
||||
<div style={style} ref={ref}>
|
||||
<WaterfallNode {...props} node={traceList[index]} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
areEqual
|
||||
);
|
||||
|
||||
const WaterfallNode = React.memo((props: WaterfallNodeProps) => {
|
||||
const theme = useTheme();
|
||||
const { duration, waterfallItemId, onClickWaterfallItem, timelineMargins, node } = props;
|
||||
const { criticalPathSegmentsById, getErrorCount, updateTreeNode, showCriticalPath } =
|
||||
useWaterfallContext();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(props.isOpen);
|
||||
const displayedColor = showCriticalPath ? transparentize(0.5, node.item.color) : node.item.color;
|
||||
const marginLeftLevel = 8 * node.level;
|
||||
const hasToggle = !!node.childrenToLoad;
|
||||
const errorCount = getErrorCount(node.item.id);
|
||||
|
||||
let children = waterfall.childrenByParentId[item.id] || [];
|
||||
const segments = criticalPathSegmentsById[node.item.id]
|
||||
?.filter((segment) => segment.self)
|
||||
.map((segment) => ({
|
||||
id: segment.item.id,
|
||||
color: theme.eui.euiColorAccent,
|
||||
left: (segment.offset - node.item.offset - node.item.skew) / node.item.duration,
|
||||
width: segment.duration / node.item.duration,
|
||||
}));
|
||||
|
||||
const criticalPath = showCriticalPath ? getCriticalPath(waterfall) : undefined;
|
||||
const toggleAccordion = () => {
|
||||
updateTreeNode({ ...node, expanded: !node.expanded });
|
||||
};
|
||||
|
||||
const criticalPathSegmentsById = groupBy(criticalPath?.segments, (segment) => segment.item.id);
|
||||
|
||||
let displayedColor = item.color;
|
||||
|
||||
if (showCriticalPath) {
|
||||
children = children.filter((child) => criticalPathSegmentsById[child.id]?.length);
|
||||
displayedColor = transparentize(0.5, item.color);
|
||||
}
|
||||
|
||||
const errorCount = waterfall.getErrorCount(item.id);
|
||||
|
||||
// To indent the items creating the parent/child tree
|
||||
const marginLeftLevel = 8 * level;
|
||||
|
||||
function toggleAccordion() {
|
||||
setIsOpen((isCurrentOpen) => !isCurrentOpen);
|
||||
}
|
||||
|
||||
const hasToggle = !!children.length;
|
||||
const onWaterfallItemClick = (flyoutDetailTab: string) => {
|
||||
onClickWaterfallItem(node.item, flyoutDetailTab);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledAccordion
|
||||
data-test-subj="waterfallItem"
|
||||
className="waterfall_accordion"
|
||||
style={{ position: 'relative' }}
|
||||
buttonClassName={`button_${item.id}`}
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
hasError={item.doc.event?.outcome === 'failure'}
|
||||
buttonClassName={`button_${node.item.id}`}
|
||||
id={node.item.id}
|
||||
hasError={node.item.doc.event?.outcome === 'failure'}
|
||||
marginLeftLevel={marginLeftLevel}
|
||||
childrenCount={children.length}
|
||||
buttonContentClassName="accordion__buttonContent"
|
||||
buttonContent={
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexGroup gutterSize="none" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ToggleAccordionButton
|
||||
show={hasToggle}
|
||||
isOpen={isOpen}
|
||||
childrenCount={children.length}
|
||||
isOpen={node.expanded}
|
||||
childrenCount={node.childrenToLoad}
|
||||
onClick={toggleAccordion}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<WaterfallItem
|
||||
key={item.id}
|
||||
key={node.item.id}
|
||||
timelineMargins={timelineMargins}
|
||||
color={displayedColor}
|
||||
item={item}
|
||||
item={node.item}
|
||||
hasToggle={hasToggle}
|
||||
totalDuration={duration}
|
||||
isSelected={item.id === waterfallItemId}
|
||||
isSelected={node.item.id === waterfallItemId}
|
||||
errorCount={errorCount}
|
||||
marginLeftLevel={marginLeftLevel}
|
||||
onClick={(flyoutDetailTab: string) => {
|
||||
onClickWaterfallItem(item, flyoutDetailTab);
|
||||
}}
|
||||
segments={criticalPathSegmentsById[item.id]
|
||||
?.filter((segment) => segment.self)
|
||||
.map((segment) => ({
|
||||
color: theme.eui.euiColorAccent,
|
||||
left: (segment.offset - item.offset - item.skew) / item.duration,
|
||||
width: segment.duration / item.duration,
|
||||
}))}
|
||||
onClick={onWaterfallItemClick}
|
||||
segments={segments}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
arrowDisplay="none"
|
||||
initialIsOpen={true}
|
||||
forceState={isOpen ? 'open' : 'closed'}
|
||||
initialIsOpen
|
||||
forceState={node.expanded ? 'open' : 'closed'}
|
||||
onToggle={toggleAccordion}
|
||||
>
|
||||
{isOpen &&
|
||||
children.map((child) => (
|
||||
<AccordionWaterfall
|
||||
{...props}
|
||||
key={child.id}
|
||||
isOpen={maxLevelOpen > level}
|
||||
level={level + 1}
|
||||
item={child}
|
||||
/>
|
||||
))}
|
||||
</StyledAccordion>
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function ToggleAccordionButton({
|
||||
show,
|
||||
|
@ -206,7 +268,7 @@ function ToggleAccordionButton({
|
|||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center" justifyContent="center">
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center" justifyContent="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
|
||||
<div
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
import { WaterfallContext } from './waterfall_context';
|
||||
|
||||
export function useWaterfallContext() {
|
||||
return useContext(WaterfallContext);
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Dictionary, groupBy } from 'lodash';
|
||||
import React, { PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { CriticalPathSegment } from '../../../../../../../../common/critical_path/types';
|
||||
import { getCriticalPath } from '../../../../../../../../common/critical_path/get_critical_path';
|
||||
import {
|
||||
buildTraceTree,
|
||||
convertTreeToList,
|
||||
IWaterfall,
|
||||
IWaterfallNode,
|
||||
IWaterfallNodeFlatten,
|
||||
updateTraceTreeNode,
|
||||
} from '../waterfall_helpers/waterfall_helpers';
|
||||
|
||||
export interface WaterfallContextProps {
|
||||
waterfall: IWaterfall;
|
||||
showCriticalPath: boolean;
|
||||
maxLevelOpen: number;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export const WaterfallContext = React.createContext<
|
||||
{
|
||||
criticalPathSegmentsById: Dictionary<CriticalPathSegment[]>;
|
||||
showCriticalPath: boolean;
|
||||
traceList: IWaterfallNodeFlatten[];
|
||||
getErrorCount: (waterfallItemId: string) => number;
|
||||
updateTreeNode: (newTree: IWaterfallNodeFlatten) => void;
|
||||
} & Pick<WaterfallContextProps, 'showCriticalPath'>
|
||||
>({
|
||||
criticalPathSegmentsById: {} as Dictionary<CriticalPathSegment[]>,
|
||||
showCriticalPath: false,
|
||||
traceList: [],
|
||||
getErrorCount: () => 0,
|
||||
updateTreeNode: () => undefined,
|
||||
});
|
||||
|
||||
export function WaterfallContextProvider({
|
||||
showCriticalPath,
|
||||
waterfall,
|
||||
maxLevelOpen,
|
||||
children,
|
||||
isOpen,
|
||||
}: PropsWithChildren<WaterfallContextProps>) {
|
||||
const [tree, setTree] = useState<IWaterfallNode | null>(null);
|
||||
const criticalPathSegmentsById = useMemo(() => {
|
||||
if (!showCriticalPath) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const criticalPath = getCriticalPath(waterfall);
|
||||
return groupBy(criticalPath.segments, (segment) => segment.item.id);
|
||||
}, [showCriticalPath, waterfall]);
|
||||
|
||||
const traceList = useMemo(() => {
|
||||
return convertTreeToList(tree);
|
||||
}, [tree]);
|
||||
|
||||
const getErrorCount = useCallback(
|
||||
(waterfallItemId) => waterfall.getErrorCount(waterfallItemId),
|
||||
[waterfall]
|
||||
);
|
||||
|
||||
const updateTreeNode = useCallback(
|
||||
(updatedNode: IWaterfallNodeFlatten) => {
|
||||
if (!tree) return;
|
||||
|
||||
const newTree = updateTraceTreeNode({
|
||||
root: tree,
|
||||
updatedNode,
|
||||
waterfall,
|
||||
path: {
|
||||
criticalPathSegmentsById,
|
||||
showCriticalPath,
|
||||
},
|
||||
});
|
||||
|
||||
if (newTree) {
|
||||
setTree(newTree);
|
||||
}
|
||||
},
|
||||
[criticalPathSegmentsById, showCriticalPath, tree, waterfall]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const root = buildTraceTree({
|
||||
waterfall,
|
||||
maxLevelOpen,
|
||||
isOpen,
|
||||
path: {
|
||||
criticalPathSegmentsById,
|
||||
showCriticalPath,
|
||||
},
|
||||
});
|
||||
|
||||
setTree(root);
|
||||
}, [criticalPathSegmentsById, isOpen, maxLevelOpen, showCriticalPath, waterfall]);
|
||||
|
||||
return (
|
||||
<WaterfallContext.Provider
|
||||
value={{
|
||||
showCriticalPath,
|
||||
criticalPathSegmentsById,
|
||||
getErrorCount,
|
||||
traceList,
|
||||
updateTreeNode,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</WaterfallContext.Provider>
|
||||
);
|
||||
}
|
|
@ -11,7 +11,12 @@ import { History } from 'history';
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { Timeline } from '../../../../../shared/charts/timeline';
|
||||
import { css } from '@emotion/react';
|
||||
import { useTheme } from '../../../../../../hooks/use_theme';
|
||||
import {
|
||||
VerticalLinesContainer,
|
||||
TimelineAxisContainer,
|
||||
} from '../../../../../shared/charts/timeline';
|
||||
import { fromQuery, toQuery } from '../../../../../shared/links/url_helpers';
|
||||
import { getAgentMarks } from '../marks/get_agent_marks';
|
||||
import { getErrorMarks } from '../marks/get_error_marks';
|
||||
|
@ -22,7 +27,6 @@ import { IWaterfall, IWaterfallItem } from './waterfall_helpers/waterfall_helper
|
|||
const Container = euiStyled.div`
|
||||
transition: 0.1s padding ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const toggleFlyout = ({
|
||||
|
@ -59,35 +63,35 @@ function getWaterfallMaxLevel(waterfall: IWaterfall) {
|
|||
if (!entryId) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let maxLevel = 1;
|
||||
function countLevels(id: string, currentLevel: number) {
|
||||
const visited = new Set<string>();
|
||||
const queue: Array<{ id: string; level: number }> = [{ id: entryId, level: 1 }];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { id, level } = queue.shift()!;
|
||||
const children = waterfall.childrenByParentId[id] || [];
|
||||
if (children.length) {
|
||||
children.forEach((child) => {
|
||||
// Skip processing when a child node has the same ID as its parent
|
||||
// to prevent infinite loop
|
||||
if (child.id !== id) {
|
||||
countLevels(child.id, currentLevel + 1);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (maxLevel < currentLevel) {
|
||||
maxLevel = currentLevel;
|
||||
|
||||
maxLevel = Math.max(maxLevel, level);
|
||||
visited.add(id);
|
||||
|
||||
children.forEach((child) => {
|
||||
if (child.id !== id && !visited.has(child.id)) {
|
||||
queue.push({ id: child.id, level: level + 1 });
|
||||
visited.add(child.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
countLevels(entryId, 1);
|
||||
return maxLevel;
|
||||
}
|
||||
// level starts with 0
|
||||
const maxLevelOpen = 2;
|
||||
|
||||
const MAX_DEPTH_OPEN_LIMIT = 2;
|
||||
|
||||
export function Waterfall({ waterfall, waterfallItemId, showCriticalPath }: Props) {
|
||||
const history = useHistory();
|
||||
const theme = useTheme();
|
||||
const [isAccordionOpen, setIsAccordionOpen] = useState(true);
|
||||
const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found
|
||||
const waterfallHeight = itemContainerHeight * waterfall.items.length;
|
||||
|
||||
const { duration } = waterfall;
|
||||
|
||||
|
@ -124,47 +128,59 @@ export function Waterfall({ waterfall, waterfallItemId, showCriticalPath }: Prop
|
|||
})}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmWaterfallButton"
|
||||
style={{ zIndex: 3, position: 'absolute' }}
|
||||
iconType={isAccordionOpen ? 'fold' : 'unfold'}
|
||||
onClick={() => {
|
||||
setIsAccordionOpen((isOpen) => !isOpen);
|
||||
}}
|
||||
/>
|
||||
<Timeline
|
||||
marks={[...agentMarks, ...errorMarks]}
|
||||
xMax={duration}
|
||||
height={waterfallHeight}
|
||||
margins={timelineMargins}
|
||||
/>
|
||||
</div>
|
||||
<WaterfallItemsContainer>
|
||||
{!waterfall.entryWaterfallTransaction ? null : (
|
||||
<AccordionWaterfall
|
||||
// used to recreate the entire tree when `isAccordionOpen` changes, collapsing or expanding all elements.
|
||||
key={`accordion_state_${isAccordionOpen}`}
|
||||
isOpen={isAccordionOpen}
|
||||
item={waterfall.entryWaterfallTransaction}
|
||||
level={0}
|
||||
waterfallItemId={waterfallItemId}
|
||||
duration={duration}
|
||||
waterfall={waterfall}
|
||||
timelineMargins={timelineMargins}
|
||||
onClickWaterfallItem={(item: IWaterfallItem, flyoutDetailTab: string) =>
|
||||
toggleFlyout({ history, item, flyoutDetailTab })
|
||||
}
|
||||
showCriticalPath={showCriticalPath}
|
||||
maxLevelOpen={
|
||||
waterfall.traceDocsTotal > 500 ? maxLevelOpen : waterfall.traceDocsTotal
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</WaterfallItemsContainer>
|
||||
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: var(--euiFixedHeadersOffset, 0);
|
||||
z-index: ${theme.eui.euiZLevel2};
|
||||
background-color: ${theme.eui.euiColorEmptyShade};
|
||||
border-bottom: 1px solid ${theme.eui.euiColorMediumShade};
|
||||
`}
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="apmWaterfallButton"
|
||||
css={css`
|
||||
position: absolute;
|
||||
z-index: ${theme.eui.euiZLevel2};
|
||||
`}
|
||||
iconType={isAccordionOpen ? 'fold' : 'unfold'}
|
||||
onClick={() => {
|
||||
setIsAccordionOpen((isOpen) => !isOpen);
|
||||
}}
|
||||
/>
|
||||
<TimelineAxisContainer
|
||||
marks={[...agentMarks, ...errorMarks]}
|
||||
xMax={duration}
|
||||
margins={timelineMargins}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VerticalLinesContainer
|
||||
marks={[...agentMarks, ...errorMarks]}
|
||||
xMax={duration}
|
||||
margins={timelineMargins}
|
||||
/>
|
||||
<WaterfallItemsContainer>
|
||||
{!waterfall.entryWaterfallTransaction ? null : (
|
||||
<AccordionWaterfall
|
||||
isOpen={isAccordionOpen}
|
||||
waterfallItemId={waterfallItemId}
|
||||
duration={duration}
|
||||
waterfall={waterfall}
|
||||
timelineMargins={timelineMargins}
|
||||
onClickWaterfallItem={(item: IWaterfallItem, flyoutDetailTab: string) =>
|
||||
toggleFlyout({ history, item, flyoutDetailTab })
|
||||
}
|
||||
showCriticalPath={showCriticalPath}
|
||||
maxLevelOpen={
|
||||
waterfall.traceDocsTotal > 500 ? MAX_DEPTH_OPEN_LIMIT : waterfall.traceDocsTotal
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</WaterfallItemsContainer>
|
||||
|
||||
<WaterfallFlyout
|
||||
waterfallItemId={waterfallItemId}
|
||||
waterfall={waterfall}
|
||||
|
|
|
@ -17,6 +17,11 @@ import {
|
|||
IWaterfallError,
|
||||
IWaterfallSpanOrTransaction,
|
||||
getOrphanTraceItemsCount,
|
||||
buildTraceTree,
|
||||
convertTreeToList,
|
||||
updateTraceTreeNode,
|
||||
IWaterfallNode,
|
||||
IWaterfallNodeFlatten,
|
||||
} from './waterfall_helpers';
|
||||
import { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error';
|
||||
import {
|
||||
|
@ -25,113 +30,113 @@ import {
|
|||
} from '../../../../../../../../common/waterfall/typings';
|
||||
|
||||
describe('waterfall_helpers', () => {
|
||||
describe('getWaterfall', () => {
|
||||
const hits = [
|
||||
{
|
||||
processor: { event: 'transaction' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-node' },
|
||||
transaction: {
|
||||
duration: { us: 49660 },
|
||||
name: 'GET /api',
|
||||
id: 'myTransactionId1',
|
||||
},
|
||||
timestamp: { us: 1549324795784006 },
|
||||
} as Transaction,
|
||||
{
|
||||
parent: { id: 'mySpanIdA' },
|
||||
processor: { event: 'span' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-ruby' },
|
||||
transaction: { id: 'myTransactionId2' },
|
||||
timestamp: { us: 1549324795825633 },
|
||||
span: {
|
||||
duration: { us: 481 },
|
||||
name: 'SELECT FROM products',
|
||||
id: 'mySpanIdB',
|
||||
},
|
||||
} as Span,
|
||||
{
|
||||
parent: { id: 'myTransactionId2' },
|
||||
processor: { event: 'span' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-ruby' },
|
||||
transaction: { id: 'myTransactionId2' },
|
||||
span: {
|
||||
duration: { us: 6161 },
|
||||
name: 'Api::ProductsController#index',
|
||||
id: 'mySpanIdA',
|
||||
},
|
||||
timestamp: { us: 1549324795824504 },
|
||||
} as Span,
|
||||
{
|
||||
parent: { id: 'mySpanIdA' },
|
||||
processor: { event: 'span' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-ruby' },
|
||||
transaction: { id: 'myTransactionId2' },
|
||||
span: {
|
||||
duration: { us: 532 },
|
||||
name: 'SELECT FROM product',
|
||||
id: 'mySpanIdC',
|
||||
},
|
||||
timestamp: { us: 1549324795827905 },
|
||||
} as Span,
|
||||
{
|
||||
parent: { id: 'myTransactionId1' },
|
||||
processor: { event: 'span' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-node' },
|
||||
transaction: { id: 'myTransactionId1' },
|
||||
span: {
|
||||
duration: { us: 47557 },
|
||||
name: 'GET opbeans-ruby:3000/api/products',
|
||||
id: 'mySpanIdD',
|
||||
},
|
||||
timestamp: { us: 1549324795785760 },
|
||||
} as Span,
|
||||
{
|
||||
parent: { id: 'mySpanIdD' },
|
||||
processor: { event: 'transaction' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-ruby' },
|
||||
transaction: {
|
||||
duration: { us: 8634 },
|
||||
name: 'Api::ProductsController#index',
|
||||
id: 'myTransactionId2',
|
||||
marks: {
|
||||
agent: {
|
||||
domInteractive: 382,
|
||||
domComplete: 383,
|
||||
timeToFirstByte: 14,
|
||||
},
|
||||
const hits = [
|
||||
{
|
||||
processor: { event: 'transaction' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-node' },
|
||||
transaction: {
|
||||
duration: { us: 49660 },
|
||||
name: 'GET /api',
|
||||
id: 'myTransactionId1',
|
||||
},
|
||||
timestamp: { us: 1549324795784006 },
|
||||
} as Transaction,
|
||||
{
|
||||
parent: { id: 'mySpanIdA' },
|
||||
processor: { event: 'span' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-ruby' },
|
||||
transaction: { id: 'myTransactionId2' },
|
||||
timestamp: { us: 1549324795825633 },
|
||||
span: {
|
||||
duration: { us: 481 },
|
||||
name: 'SELECT FROM products',
|
||||
id: 'mySpanIdB',
|
||||
},
|
||||
} as Span,
|
||||
{
|
||||
parent: { id: 'myTransactionId2' },
|
||||
processor: { event: 'span' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-ruby' },
|
||||
transaction: { id: 'myTransactionId2' },
|
||||
span: {
|
||||
duration: { us: 6161 },
|
||||
name: 'Api::ProductsController#index',
|
||||
id: 'mySpanIdA',
|
||||
},
|
||||
timestamp: { us: 1549324795824504 },
|
||||
} as Span,
|
||||
{
|
||||
parent: { id: 'mySpanIdA' },
|
||||
processor: { event: 'span' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-ruby' },
|
||||
transaction: { id: 'myTransactionId2' },
|
||||
span: {
|
||||
duration: { us: 532 },
|
||||
name: 'SELECT FROM product',
|
||||
id: 'mySpanIdC',
|
||||
},
|
||||
timestamp: { us: 1549324795827905 },
|
||||
} as Span,
|
||||
{
|
||||
parent: { id: 'myTransactionId1' },
|
||||
processor: { event: 'span' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-node' },
|
||||
transaction: { id: 'myTransactionId1' },
|
||||
span: {
|
||||
duration: { us: 47557 },
|
||||
name: 'GET opbeans-ruby:3000/api/products',
|
||||
id: 'mySpanIdD',
|
||||
},
|
||||
timestamp: { us: 1549324795785760 },
|
||||
} as Span,
|
||||
{
|
||||
parent: { id: 'mySpanIdD' },
|
||||
processor: { event: 'transaction' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-ruby' },
|
||||
transaction: {
|
||||
duration: { us: 8634 },
|
||||
name: 'Api::ProductsController#index',
|
||||
id: 'myTransactionId2',
|
||||
marks: {
|
||||
agent: {
|
||||
domInteractive: 382,
|
||||
domComplete: 383,
|
||||
timeToFirstByte: 14,
|
||||
},
|
||||
},
|
||||
timestamp: { us: 1549324795823304 },
|
||||
} as unknown as Transaction,
|
||||
];
|
||||
const errorDocs = [
|
||||
{
|
||||
processor: { event: 'error' },
|
||||
parent: { id: 'myTransactionId1' },
|
||||
timestamp: { us: 1549324795810000 },
|
||||
trace: { id: 'myTraceId' },
|
||||
transaction: { id: 'myTransactionId1' },
|
||||
error: {
|
||||
id: 'error1',
|
||||
grouping_key: 'errorGroupingKey1',
|
||||
log: {
|
||||
message: 'error message',
|
||||
},
|
||||
},
|
||||
timestamp: { us: 1549324795823304 },
|
||||
} as unknown as Transaction,
|
||||
];
|
||||
const errorDocs = [
|
||||
{
|
||||
processor: { event: 'error' },
|
||||
parent: { id: 'myTransactionId1' },
|
||||
timestamp: { us: 1549324795810000 },
|
||||
trace: { id: 'myTraceId' },
|
||||
transaction: { id: 'myTransactionId1' },
|
||||
error: {
|
||||
id: 'error1',
|
||||
grouping_key: 'errorGroupingKey1',
|
||||
log: {
|
||||
message: 'error message',
|
||||
},
|
||||
service: { name: 'opbeans-ruby' },
|
||||
agent: {
|
||||
name: 'ruby',
|
||||
version: '2',
|
||||
},
|
||||
} as unknown as APMError,
|
||||
];
|
||||
},
|
||||
service: { name: 'opbeans-ruby' },
|
||||
agent: {
|
||||
name: 'ruby',
|
||||
version: '2',
|
||||
},
|
||||
} as unknown as APMError,
|
||||
];
|
||||
|
||||
describe('getWaterfall', () => {
|
||||
it('should return full waterfall', () => {
|
||||
const apiResp = {
|
||||
traceItems: {
|
||||
|
@ -750,4 +755,229 @@ describe('waterfall_helpers', () => {
|
|||
expect(getOrphanTraceItemsCount(traceItems)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#trace tree', () => {
|
||||
const waterfall = getWaterfall({
|
||||
traceItems: {
|
||||
traceDocs: hits,
|
||||
errorDocs,
|
||||
exceedsMax: false,
|
||||
spanLinksCountById: {},
|
||||
traceDocsTotal: hits.length,
|
||||
maxTraceItems: 5000,
|
||||
},
|
||||
entryTransaction: {
|
||||
processor: { event: 'transaction' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-node' },
|
||||
transaction: {
|
||||
duration: { us: 49660 },
|
||||
name: 'GET /api',
|
||||
id: 'myTransactionId1',
|
||||
},
|
||||
timestamp: { us: 1549324795784006 },
|
||||
} as Transaction,
|
||||
});
|
||||
|
||||
const tree: IWaterfallNode = {
|
||||
id: 'myTransactionId1',
|
||||
item: {
|
||||
docType: 'transaction',
|
||||
doc: {
|
||||
agent: { name: 'nodejs' },
|
||||
processor: { event: 'transaction' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-node' },
|
||||
transaction: {
|
||||
duration: { us: 49660 },
|
||||
name: 'GET /api',
|
||||
id: 'myTransactionId1',
|
||||
type: 'request',
|
||||
},
|
||||
timestamp: { us: 1549324795784006 },
|
||||
},
|
||||
id: 'myTransactionId1',
|
||||
duration: 49660,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
legendValues: { serviceName: 'opbeans-node', spanType: '' },
|
||||
color: '',
|
||||
spanLinksCount: { linkedParents: 0, linkedChildren: 0 },
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: '0-mySpanIdD-0',
|
||||
item: {
|
||||
docType: 'span',
|
||||
doc: {
|
||||
agent: { name: 'nodejs' },
|
||||
parent: { id: 'myTransactionId1' },
|
||||
processor: { event: 'span' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-node' },
|
||||
transaction: { id: 'myTransactionId1' },
|
||||
span: {
|
||||
duration: { us: 47557 },
|
||||
name: 'GET opbeans-ruby:3000/api/products',
|
||||
id: 'mySpanIdD',
|
||||
type: 'request',
|
||||
},
|
||||
timestamp: { us: 1549324795785760 },
|
||||
},
|
||||
id: 'mySpanIdD',
|
||||
parentId: 'myTransactionId1',
|
||||
duration: 47557,
|
||||
offset: 1754,
|
||||
skew: 0,
|
||||
legendValues: { serviceName: 'opbeans-node', spanType: '' },
|
||||
color: '',
|
||||
spanLinksCount: { linkedParents: 0, linkedChildren: 0 },
|
||||
},
|
||||
children: [],
|
||||
childrenToLoad: 1,
|
||||
level: 1,
|
||||
expanded: false,
|
||||
hasInitializedChildren: false,
|
||||
},
|
||||
],
|
||||
level: 0,
|
||||
childrenToLoad: 1,
|
||||
expanded: true,
|
||||
hasInitializedChildren: true,
|
||||
};
|
||||
|
||||
describe('buildTraceTree', () => {
|
||||
it('should build the trace tree correctly', () => {
|
||||
const result = buildTraceTree({
|
||||
waterfall,
|
||||
path: {
|
||||
criticalPathSegmentsById: {},
|
||||
showCriticalPath: false,
|
||||
},
|
||||
maxLevelOpen: 1,
|
||||
isOpen: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
item: expect.objectContaining({ id: 'myTransactionId1' }),
|
||||
level: 0,
|
||||
expanded: true,
|
||||
hasInitializedChildren: true,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result?.children[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
item: expect.objectContaining({ id: 'mySpanIdD' }),
|
||||
level: 1,
|
||||
expanded: false,
|
||||
childrenToLoad: 1,
|
||||
children: [],
|
||||
hasInitializedChildren: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertTreeToList', () => {
|
||||
it('should convert the trace tree to a list correctly', () => {
|
||||
const result = convertTreeToList(tree);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
item: expect.objectContaining({ id: 'myTransactionId1' }),
|
||||
level: 0,
|
||||
expanded: true,
|
||||
hasInitializedChildren: true,
|
||||
childrenToLoad: 1,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
item: expect.objectContaining({ id: 'mySpanIdD' }),
|
||||
level: 1,
|
||||
expanded: false,
|
||||
hasInitializedChildren: false,
|
||||
childrenToLoad: 1,
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTraceTreeNode', () => {
|
||||
it('should update the "mySpanIdD" node setting "expanded" to true', () => {
|
||||
const updatedNode: IWaterfallNodeFlatten = {
|
||||
id: '0-mySpanIdD-0',
|
||||
item: {
|
||||
docType: 'span',
|
||||
doc: {
|
||||
agent: { name: 'nodejs' },
|
||||
parent: { id: 'myTransactionId1' },
|
||||
processor: { event: 'span' },
|
||||
trace: { id: 'myTraceId' },
|
||||
service: { name: 'opbeans-node' },
|
||||
transaction: { id: 'myTransactionId1' },
|
||||
span: {
|
||||
duration: { us: 47557 },
|
||||
name: 'GET opbeans-ruby:3000/api/products',
|
||||
id: 'mySpanIdD',
|
||||
type: 'request',
|
||||
},
|
||||
timestamp: { us: 1549324795785760 },
|
||||
},
|
||||
id: 'mySpanIdD',
|
||||
parentId: 'myTransactionId1',
|
||||
duration: 47557,
|
||||
offset: 1754,
|
||||
skew: 0,
|
||||
legendValues: { serviceName: 'opbeans-node', spanType: '' },
|
||||
color: '',
|
||||
spanLinksCount: { linkedParents: 0, linkedChildren: 0 },
|
||||
},
|
||||
childrenToLoad: 1,
|
||||
level: 1,
|
||||
expanded: true,
|
||||
hasInitializedChildren: false,
|
||||
};
|
||||
|
||||
const result = updateTraceTreeNode({
|
||||
root: tree,
|
||||
updatedNode,
|
||||
waterfall,
|
||||
path: {
|
||||
criticalPathSegmentsById: {},
|
||||
showCriticalPath: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
item: expect.objectContaining({ id: 'myTransactionId1' }),
|
||||
level: 0,
|
||||
expanded: true,
|
||||
hasInitializedChildren: true,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result?.children[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
item: expect.objectContaining({ id: 'mySpanIdD' }),
|
||||
level: 1,
|
||||
expanded: true,
|
||||
hasInitializedChildren: true,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result?.children[0].children[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
item: expect.objectContaining({ id: 'myTransactionId2' }),
|
||||
level: 2,
|
||||
expanded: false,
|
||||
hasInitializedChildren: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
*/
|
||||
|
||||
import { euiPaletteColorBlind } from '@elastic/eui';
|
||||
import { first, flatten, groupBy, isEmpty, sortBy, uniq } from 'lodash';
|
||||
import { Dictionary, first, flatten, groupBy, isEmpty, sortBy, uniq } from 'lodash';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import { CriticalPathSegment } from '../../../../../../../../common/critical_path/types';
|
||||
import type { APIReturnType } from '../../../../../../../services/rest/create_call_apm_api';
|
||||
import type { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction';
|
||||
import {
|
||||
|
@ -93,6 +94,23 @@ export interface IWaterfallLegend {
|
|||
color: string;
|
||||
}
|
||||
|
||||
export interface IWaterfallNode {
|
||||
id: string;
|
||||
item: IWaterfallItem;
|
||||
// children that are loaded
|
||||
children: IWaterfallNode[];
|
||||
// total number of children that needs to be loaded
|
||||
childrenToLoad: number;
|
||||
// collapsed or expanded state
|
||||
expanded: boolean;
|
||||
// level in the tree
|
||||
level: number;
|
||||
// flag to indicate if children are loaded
|
||||
hasInitializedChildren: boolean;
|
||||
}
|
||||
|
||||
export type IWaterfallNodeFlatten = Omit<IWaterfallNode, 'children'>;
|
||||
|
||||
function getLegendValues(transactionOrSpan: WaterfallTransaction | WaterfallSpan) {
|
||||
return {
|
||||
[WaterfallLegendType.ServiceName]: transactionOrSpan.service.name,
|
||||
|
@ -462,3 +480,186 @@ export function getWaterfall(apiResponse: TraceAPIResponse): IWaterfall {
|
|||
orphanTraceItemsCount,
|
||||
};
|
||||
}
|
||||
|
||||
function getChildren({
|
||||
path,
|
||||
waterfall,
|
||||
waterfallItemId,
|
||||
}: {
|
||||
waterfallItemId: string;
|
||||
waterfall: IWaterfall;
|
||||
path: {
|
||||
criticalPathSegmentsById: Dictionary<CriticalPathSegment[]>;
|
||||
showCriticalPath: boolean;
|
||||
};
|
||||
}) {
|
||||
const children = waterfall.childrenByParentId[waterfallItemId] ?? [];
|
||||
return path.showCriticalPath
|
||||
? children.filter((child) => path.criticalPathSegmentsById[child.id]?.length)
|
||||
: children;
|
||||
}
|
||||
|
||||
function buildTree({
|
||||
root,
|
||||
waterfall,
|
||||
maxLevelOpen,
|
||||
path,
|
||||
}: {
|
||||
root: IWaterfallNode;
|
||||
waterfall: IWaterfall;
|
||||
maxLevelOpen: number;
|
||||
path: {
|
||||
criticalPathSegmentsById: Dictionary<CriticalPathSegment[]>;
|
||||
showCriticalPath: boolean;
|
||||
};
|
||||
}) {
|
||||
const tree = { ...root };
|
||||
const queue: IWaterfallNode[] = [tree];
|
||||
|
||||
for (let queueIndex = 0; queueIndex < queue.length; queueIndex++) {
|
||||
const node = queue[queueIndex];
|
||||
|
||||
const children = getChildren({ path, waterfall, waterfallItemId: node.item.id });
|
||||
|
||||
// Set childrenToLoad for all nodes enqueued.
|
||||
// this allows lazy loading of child nodes
|
||||
node.childrenToLoad = children.length;
|
||||
|
||||
if (maxLevelOpen > node.level) {
|
||||
children.forEach((child, index) => {
|
||||
const level = node.level + 1;
|
||||
|
||||
const currentNode: IWaterfallNode = {
|
||||
id: `${level}-${child.id}-${index}`,
|
||||
item: child,
|
||||
children: [],
|
||||
level,
|
||||
expanded: level < maxLevelOpen,
|
||||
childrenToLoad: 0,
|
||||
hasInitializedChildren: false,
|
||||
};
|
||||
|
||||
node.children.push(currentNode);
|
||||
queue.push(currentNode);
|
||||
});
|
||||
|
||||
node.hasInitializedChildren = true;
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
export function buildTraceTree({
|
||||
waterfall,
|
||||
maxLevelOpen,
|
||||
isOpen,
|
||||
path,
|
||||
}: {
|
||||
waterfall: IWaterfall;
|
||||
maxLevelOpen: number;
|
||||
isOpen: boolean;
|
||||
path: {
|
||||
criticalPathSegmentsById: Dictionary<CriticalPathSegment[]>;
|
||||
showCriticalPath: boolean;
|
||||
};
|
||||
}): IWaterfallNode | null {
|
||||
const entry = waterfall.entryWaterfallTransaction;
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const root: IWaterfallNode = {
|
||||
id: entry.id,
|
||||
item: entry,
|
||||
children: [],
|
||||
level: 0,
|
||||
expanded: isOpen,
|
||||
childrenToLoad: 0,
|
||||
hasInitializedChildren: false,
|
||||
};
|
||||
|
||||
return buildTree({ root, maxLevelOpen, waterfall, path });
|
||||
}
|
||||
|
||||
export const convertTreeToList = (root: IWaterfallNode | null): IWaterfallNodeFlatten[] => {
|
||||
if (!root) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: IWaterfallNodeFlatten[] = [];
|
||||
const stack: IWaterfallNode[] = [root];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
|
||||
const { children, ...nodeWithoutChildren } = node;
|
||||
result.push(nodeWithoutChildren);
|
||||
|
||||
if (node.expanded) {
|
||||
for (let i = node.children.length - 1; i >= 0; i--) {
|
||||
stack.push(node.children[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const updateTraceTreeNode = ({
|
||||
root,
|
||||
updatedNode,
|
||||
waterfall,
|
||||
path,
|
||||
}: {
|
||||
root: IWaterfallNode;
|
||||
updatedNode: IWaterfallNodeFlatten;
|
||||
waterfall: IWaterfall;
|
||||
path: {
|
||||
criticalPathSegmentsById: Dictionary<CriticalPathSegment[]>;
|
||||
showCriticalPath: boolean;
|
||||
};
|
||||
}) => {
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tree = { ...root };
|
||||
const stack: Array<{ parent: IWaterfallNode | null; index: number; node: IWaterfallNode }> = [
|
||||
{ parent: null, index: 0, node: root },
|
||||
];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const { parent, index, node } = stack.pop()!;
|
||||
|
||||
if (node.id === updatedNode.id) {
|
||||
Object.assign(node, updatedNode);
|
||||
|
||||
if (updatedNode.expanded && !updatedNode.hasInitializedChildren) {
|
||||
Object.assign(
|
||||
node,
|
||||
buildTree({
|
||||
root: node,
|
||||
waterfall,
|
||||
maxLevelOpen: node.level + 1, // Only one level above the current node will be loaded
|
||||
path,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
parent.children[index] = node;
|
||||
} else {
|
||||
Object.assign(tree, node);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
for (let i = node.children.length - 1; i >= 0; i--) {
|
||||
stack.push({ parent: node, index: i, node: node.children[i] });
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { EuiBadge, EuiIcon, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { ReactNode, useRef, useState, useEffect } from 'react';
|
||||
import React, { ReactNode, useRef, useEffect, useState } from 'react';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { useTheme } from '../../../../../../hooks/use_theme';
|
||||
import { isMobileAgentName, isRumAgentName } from '../../../../../../../common/agent_name';
|
||||
|
@ -115,6 +115,7 @@ interface IWaterfallItemProps {
|
|||
errorCount: number;
|
||||
marginLeftLevel: number;
|
||||
segments?: Array<{
|
||||
id: string;
|
||||
left: number;
|
||||
width: number;
|
||||
color: string;
|
||||
|
@ -267,6 +268,7 @@ export function WaterfallItem({
|
|||
<CriticalPathItemBar>
|
||||
{segments?.map((segment) => (
|
||||
<CriticalPathItemSegment
|
||||
key={segment.id}
|
||||
color={segment.color}
|
||||
left={segment.left}
|
||||
width={segment.width}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Timeline should render with data 1`] = `
|
||||
exports[`Timeline TimelineAxisContainer should render with data 1`] = `
|
||||
.c0 {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
|
@ -8,5 +13,350 @@ exports[`Timeline should render with data 1`] = `
|
|||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"height": 100,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
height={100}
|
||||
style={
|
||||
Object {
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
width={0}
|
||||
>
|
||||
<g
|
||||
transform="translate(0 80)"
|
||||
>
|
||||
<text
|
||||
fill="#69707d"
|
||||
fontSize={11}
|
||||
textAnchor="middle"
|
||||
x={50}
|
||||
y={0}
|
||||
>
|
||||
0 μs
|
||||
</text>
|
||||
<text
|
||||
fill="#69707d"
|
||||
fontSize={11}
|
||||
textAnchor="middle"
|
||||
x={30}
|
||||
y={0}
|
||||
>
|
||||
200 μs
|
||||
</text>
|
||||
<text
|
||||
fill="#69707d"
|
||||
fontSize={11}
|
||||
textAnchor="middle"
|
||||
x={10}
|
||||
y={0}
|
||||
>
|
||||
400 μs
|
||||
</text>
|
||||
<text
|
||||
fill="#69707d"
|
||||
fontSize={11}
|
||||
textAnchor="middle"
|
||||
x={-10}
|
||||
y={0}
|
||||
>
|
||||
600 μs
|
||||
</text>
|
||||
<text
|
||||
fill="#69707d"
|
||||
fontSize={11}
|
||||
textAnchor="middle"
|
||||
x={-30}
|
||||
y={0}
|
||||
>
|
||||
800 μs
|
||||
</text>
|
||||
<text
|
||||
fill="#343741"
|
||||
textAnchor="middle"
|
||||
x={-50}
|
||||
y={0}
|
||||
>
|
||||
1,000 μs
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
<div
|
||||
className="c0"
|
||||
style={
|
||||
Object {
|
||||
"left": -9955.5,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #69707d;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
margin-right: 0;
|
||||
background: #98a2b3;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
<span
|
||||
className="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
onKeyDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
>
|
||||
<div
|
||||
className="c0"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<span
|
||||
className="c1"
|
||||
color="#98a2b3"
|
||||
shape="circle"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="c0"
|
||||
style={
|
||||
Object {
|
||||
"left": -10955.5,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #69707d;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
margin-right: 0;
|
||||
background: #98a2b3;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
<span
|
||||
className="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
onKeyDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
>
|
||||
<div
|
||||
className="c0"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<span
|
||||
className="c1"
|
||||
color="#98a2b3"
|
||||
shape="circle"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="c0"
|
||||
style={
|
||||
Object {
|
||||
"left": -18955.5,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #69707d;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
margin-right: 0;
|
||||
background: #98a2b3;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
<span
|
||||
className="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
|
||||
onKeyDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
>
|
||||
<div
|
||||
className="c0"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<span
|
||||
className="c1"
|
||||
color="#98a2b3"
|
||||
shape="circle"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Timeline VerticalLinesContainer should render with data 1`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
height="100%"
|
||||
style={
|
||||
Object {
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
width={0}
|
||||
>
|
||||
<g
|
||||
transform="translate(0 100)"
|
||||
>
|
||||
<line
|
||||
stroke="#f5f7fa"
|
||||
x1={50}
|
||||
x2={50}
|
||||
y1={0}
|
||||
y2="100%"
|
||||
/>
|
||||
<line
|
||||
stroke="#f5f7fa"
|
||||
x1={30}
|
||||
x2={30}
|
||||
y1={0}
|
||||
y2="100%"
|
||||
/>
|
||||
<line
|
||||
stroke="#f5f7fa"
|
||||
x1={10}
|
||||
x2={10}
|
||||
y1={0}
|
||||
y2="100%"
|
||||
/>
|
||||
<line
|
||||
stroke="#f5f7fa"
|
||||
x1={-10}
|
||||
x2={-10}
|
||||
y1={0}
|
||||
y2="100%"
|
||||
/>
|
||||
<line
|
||||
stroke="#f5f7fa"
|
||||
x1={-30}
|
||||
x2={-30}
|
||||
y1={0}
|
||||
y2="100%"
|
||||
/>
|
||||
<line
|
||||
stroke="#f5f7fa"
|
||||
x1={-50}
|
||||
x2={-50}
|
||||
y1={0}
|
||||
y2="100%"
|
||||
/>
|
||||
<line
|
||||
stroke="#98a2b3"
|
||||
x1={-9950}
|
||||
x2={-9950}
|
||||
y1={0}
|
||||
y2="100%"
|
||||
/>
|
||||
<line
|
||||
stroke="#98a2b3"
|
||||
x1={-10950}
|
||||
x2={-10950}
|
||||
y1={0}
|
||||
y2="100%"
|
||||
/>
|
||||
<line
|
||||
stroke="#98a2b3"
|
||||
x1={-18950}
|
||||
x2={-18950}
|
||||
y1={0}
|
||||
y2="100%"
|
||||
/>
|
||||
<line
|
||||
stroke="#98a2b3"
|
||||
x1={-50}
|
||||
x2={-50}
|
||||
y1={0}
|
||||
y2="100%"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -22,39 +22,59 @@ export interface Margins {
|
|||
left: number;
|
||||
}
|
||||
|
||||
interface TimelineProps {
|
||||
export interface TimelineProps {
|
||||
marks?: Mark[];
|
||||
xMin?: number;
|
||||
xMax?: number;
|
||||
height: number;
|
||||
margins: Margins;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
function TimeLineContainer({ width, xMin, xMax, height, marks, margins }: TimelineProps) {
|
||||
if (xMax == null || !width) {
|
||||
export function TimelineAxisContainer({ xMax, xMin, margins, marks }: TimelineProps) {
|
||||
const [width, setWidth] = useState(0);
|
||||
if (xMax === undefined) {
|
||||
return null;
|
||||
}
|
||||
const plotValues = getPlotValues({ width, xMin, xMax, height, margins });
|
||||
const topTraceDuration = xMax - (xMin ?? 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TimelineAxis plotValues={plotValues} marks={marks} topTraceDuration={topTraceDuration} />
|
||||
<VerticalLines plotValues={plotValues} marks={marks} topTraceDuration={topTraceDuration} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Timeline(props: TimelineProps) {
|
||||
const [width, setWidth] = useState(0);
|
||||
return (
|
||||
<EuiResizeObserver onResize={(size) => setWidth(size.width)}>
|
||||
{(resizeRef) => (
|
||||
<div style={{ width: '100%', height: '100%' }} ref={resizeRef}>
|
||||
<TimeLineContainer {...props} width={width} />
|
||||
</div>
|
||||
)}
|
||||
{(resizeRef) => {
|
||||
const plotValues = getPlotValues({ width, xMin, xMax, margins });
|
||||
const topTraceDuration = xMax - (xMin ?? 0);
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%' }} ref={resizeRef}>
|
||||
<TimelineAxis
|
||||
plotValues={plotValues}
|
||||
marks={marks}
|
||||
topTraceDuration={topTraceDuration}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</EuiResizeObserver>
|
||||
);
|
||||
}
|
||||
|
||||
export function VerticalLinesContainer({ xMax, xMin, margins, marks }: TimelineProps) {
|
||||
const [width, setWidth] = useState(0);
|
||||
if (xMax == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiResizeObserver onResize={(size) => setWidth(size.width)}>
|
||||
{(resizeRef) => {
|
||||
const plotValues = getPlotValues({ width, xMin, xMax, margins });
|
||||
const topTraceDuration = xMax - (xMin ?? 0);
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%' }} ref={resizeRef}>
|
||||
<VerticalLines
|
||||
plotValues={plotValues}
|
||||
marks={marks}
|
||||
topTraceDuration={topTraceDuration}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</EuiResizeObserver>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,13 +14,11 @@ export function getPlotValues({
|
|||
width,
|
||||
xMin = 0,
|
||||
xMax,
|
||||
height,
|
||||
margins,
|
||||
}: {
|
||||
width: number;
|
||||
xMin?: number;
|
||||
xMax: number;
|
||||
height: number;
|
||||
margins: Margins;
|
||||
}) {
|
||||
const xScale = scaleLinear()
|
||||
|
@ -28,7 +26,6 @@ export function getPlotValues({
|
|||
.range([margins.left, width - margins.right]);
|
||||
|
||||
return {
|
||||
height,
|
||||
margins,
|
||||
tickValues: xScale.ticks(7),
|
||||
width,
|
||||
|
|
|
@ -12,10 +12,10 @@ import {
|
|||
mockMoment,
|
||||
toJson,
|
||||
} from '../../../../utils/test_helpers';
|
||||
import { Timeline } from '.';
|
||||
import { TimelineAxisContainer, TimelineProps, VerticalLinesContainer } from '.';
|
||||
import { AgentMark } from '../../../app/transaction_details/waterfall_with_summary/waterfall_container/marks/get_agent_marks';
|
||||
|
||||
describe('Timeline', () => {
|
||||
describe.each([[TimelineAxisContainer], [VerticalLinesContainer]])(`Timeline`, (Component) => {
|
||||
let consoleMock: jest.SpyInstance;
|
||||
|
||||
beforeAll(() => {
|
||||
|
@ -27,19 +27,15 @@ describe('Timeline', () => {
|
|||
consoleMock.mockRestore();
|
||||
});
|
||||
|
||||
it('should render with data', () => {
|
||||
const props = {
|
||||
traceRootDuration: 200000,
|
||||
width: 1000,
|
||||
duration: 200000,
|
||||
height: 116,
|
||||
it(`${Component.name} should render with data`, () => {
|
||||
const props: TimelineProps = {
|
||||
xMax: 1000,
|
||||
margins: {
|
||||
top: 100,
|
||||
left: 50,
|
||||
right: 50,
|
||||
bottom: 0,
|
||||
},
|
||||
animation: null,
|
||||
marks: [
|
||||
{
|
||||
id: 'timeToFirstByte',
|
||||
|
@ -62,17 +58,14 @@ describe('Timeline', () => {
|
|||
] as AgentMark[],
|
||||
};
|
||||
|
||||
const wrapper = mountWithTheme(<Timeline {...props} />);
|
||||
const wrapper = mountWithTheme(<Component {...props} />);
|
||||
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should not crash if traceRootDuration is 0', () => {
|
||||
const props = {
|
||||
traceRootDuration: 0,
|
||||
width: 1000,
|
||||
it(`${Component.name} should not crash if traceRootDuration is 0`, () => {
|
||||
const props: TimelineProps = {
|
||||
xMax: 0,
|
||||
height: 116,
|
||||
margins: {
|
||||
top: 100,
|
||||
left: 50,
|
||||
|
@ -81,7 +74,7 @@ describe('Timeline', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const mountTimeline = () => mountWithTheme(<Timeline {...props} />);
|
||||
const mountTimeline = () => mountWithTheme(<Component {...props} />);
|
||||
|
||||
expect(mountTimeline).not.toThrow();
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { inRange } from 'lodash';
|
||||
import React, { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { getDurationFormatter } from '../../../../../common/utils/formatters';
|
||||
import { useTheme } from '../../../../hooks/use_theme';
|
||||
import { Mark } from '.';
|
||||
|
@ -30,7 +30,6 @@ const getXAxisTickValues = (tickValues: number[], topTraceDuration?: number) =>
|
|||
};
|
||||
|
||||
interface TimelineAxisProps {
|
||||
header?: ReactNode;
|
||||
plotValues: PlotValues;
|
||||
marks?: Mark[];
|
||||
topTraceDuration: number;
|
||||
|
@ -54,11 +53,7 @@ export function TimelineAxis({ plotValues, marks = [], topTraceDuration }: Timel
|
|||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
borderBottom: `1px solid ${theme.eui.euiColorMediumShade}`,
|
||||
height: margins.top,
|
||||
zIndex: 2,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -17,7 +17,7 @@ interface VerticalLinesProps {
|
|||
}
|
||||
|
||||
export function VerticalLines({ topTraceDuration, plotValues, marks = [] }: VerticalLinesProps) {
|
||||
const { width, height, margins, tickValues, xScale } = plotValues;
|
||||
const { width, margins, tickValues, xScale } = plotValues;
|
||||
|
||||
const markTimes = marks.filter((mark) => mark.verticalLine).map(({ offset }) => offset);
|
||||
|
||||
|
@ -38,7 +38,7 @@ export function VerticalLines({ topTraceDuration, plotValues, marks = [] }: Vert
|
|||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height + margins.top}
|
||||
height="100%"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
|
@ -52,7 +52,7 @@ export function VerticalLines({ topTraceDuration, plotValues, marks = [] }: Vert
|
|||
x1={position}
|
||||
x2={position}
|
||||
y1={0}
|
||||
y2={height}
|
||||
y2="100%"
|
||||
stroke={theme.eui.euiColorLightestShade}
|
||||
/>
|
||||
))}
|
||||
|
@ -62,7 +62,7 @@ export function VerticalLines({ topTraceDuration, plotValues, marks = [] }: Vert
|
|||
x1={position}
|
||||
x2={position}
|
||||
y1={0}
|
||||
y2={height}
|
||||
y2="100%"
|
||||
stroke={theme.eui.euiColorMediumShade}
|
||||
/>
|
||||
))}
|
||||
|
@ -72,7 +72,7 @@ export function VerticalLines({ topTraceDuration, plotValues, marks = [] }: Vert
|
|||
x1={topTraceDurationPosition}
|
||||
x2={topTraceDurationPosition}
|
||||
y1={0}
|
||||
y2={height}
|
||||
y2="100%"
|
||||
stroke={theme.eui.euiColorMediumShade}
|
||||
/>
|
||||
)}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue