mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* Adds traces overview with mock data (#22628) * Updates service overview snapshots * Adds tests for ManagedTable and ImpactBar * Refactored transaction overview to use new managed table component * Removed jsconfig file in apm * [APM] Distributed tracing - Trace details (waterfall) (#22763) * [APM] Add typescript to waterfall (#23635) * [APM] Migrate get_trace and constants to Typescript (#23634) * [APM] Add types for setup_request (#23762) * [APM] Adds trace overview queries and some refactoring (#23605) * ImpactBar component to align EuiProgress usage for impact bars * Sharing some logic between transaction and trace queries * Typescript support * Quick fix ‘banana’ * [APM] Ensure backwards compatibility for v1 and v2 (#23636) * Make interfaces versioned * Rename eventType to docType * Fixes trace links on traces overview (#24089) * [APM] use react-redux-request (#24117) * Updated yarn lockfile for new yarn version * Updated dependency issues for react-router-dom types * [APM] Display transaction info on span flyout (#24189) * [APM] Display transaction info on span flyout * Brings in real location and url param data for transaction flyout * Converts flyout to TS * Adds query param state for flyouts with ts support * Updates styles and uses EuiTabs for transaction flyout * [APM] Transaction flyout * [APM] Minor docs cleanup (#24325) * [APM] Minor docs cleanup * [APM] Fix issues with v1 spans (#24332) * [APM] Add agent marks (#24361) * [APM] Typescript migration for the transaction endpoints (#24397) * [APM] DT transaction sample header (#24294) Transaction sample header completed * Fixes link target for traces overview to include trans/trace ids as query params * Converts Transaction index file to TS * Adds trace link to sample section * Refactors the trace link and applies it to two usages * Implements transaction sample action context menu * Calculates and implements duration percentage * Re-typed how transaction groups work * Fixes transaction flyout links and context menu * Removes unnecessary ms multiplication * Removes unused commented code * Finalizes infra links * Fixes some type shenanigans
This commit is contained in:
parent
377688ab7f
commit
ab47cae457
149 changed files with 5995 additions and 2880 deletions
13
package.json
13
package.json
|
@ -78,7 +78,8 @@
|
||||||
"url": "https://github.com/elastic/kibana.git"
|
"url": "https://github.com/elastic/kibana.git"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"**/@types/node": "8.10.21"
|
"**/@types/node": "8.10.21",
|
||||||
|
"@types/react": "16.3.14"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elastic/eui": "4.5.1",
|
"@elastic/eui": "4.5.1",
|
||||||
|
@ -183,7 +184,7 @@
|
||||||
"react-input-range": "^1.3.0",
|
"react-input-range": "^1.3.0",
|
||||||
"react-markdown": "^3.1.4",
|
"react-markdown": "^3.1.4",
|
||||||
"react-redux": "^5.0.7",
|
"react-redux": "^5.0.7",
|
||||||
"react-router-dom": "4.2.2",
|
"react-router-dom": "^4.3.1",
|
||||||
"react-sizeme": "^2.3.6",
|
"react-sizeme": "^2.3.6",
|
||||||
"react-toggle": "4.0.2",
|
"react-toggle": "4.0.2",
|
||||||
"reactcss": "1.2.3",
|
"reactcss": "1.2.3",
|
||||||
|
@ -207,6 +208,7 @@
|
||||||
"topojson-client": "3.0.0",
|
"topojson-client": "3.0.0",
|
||||||
"trunc-html": "1.0.2",
|
"trunc-html": "1.0.2",
|
||||||
"trunc-text": "1.0.2",
|
"trunc-text": "1.0.2",
|
||||||
|
"ts-optchain": "^0.1.1",
|
||||||
"tslib": "^1.9.3",
|
"tslib": "^1.9.3",
|
||||||
"type-detect": "^4.0.8",
|
"type-detect": "^4.0.8",
|
||||||
"uglifyjs-webpack-plugin": "^1.2.7",
|
"uglifyjs-webpack-plugin": "^1.2.7",
|
||||||
|
@ -245,7 +247,9 @@
|
||||||
"@types/boom": "^7.2.0",
|
"@types/boom": "^7.2.0",
|
||||||
"@types/chance": "^1.0.0",
|
"@types/chance": "^1.0.0",
|
||||||
"@types/classnames": "^2.2.3",
|
"@types/classnames": "^2.2.3",
|
||||||
|
"@types/d3": "^5.0.0",
|
||||||
"@types/dedent": "^0.7.0",
|
"@types/dedent": "^0.7.0",
|
||||||
|
"@types/elasticsearch": "^5.0.26",
|
||||||
"@types/enzyme": "^3.1.12",
|
"@types/enzyme": "^3.1.12",
|
||||||
"@types/eslint": "^4.16.2",
|
"@types/eslint": "^4.16.2",
|
||||||
"@types/execa": "^0.9.0",
|
"@types/execa": "^0.9.0",
|
||||||
|
@ -261,19 +265,22 @@
|
||||||
"@types/listr": "^0.13.0",
|
"@types/listr": "^0.13.0",
|
||||||
"@types/lodash": "^3.10.1",
|
"@types/lodash": "^3.10.1",
|
||||||
"@types/minimatch": "^2.0.29",
|
"@types/minimatch": "^2.0.29",
|
||||||
|
"@types/moment-timezone": "^0.5.8",
|
||||||
"@types/mustache": "^0.8.31",
|
"@types/mustache": "^0.8.31",
|
||||||
"@types/node": "^8.10.20",
|
"@types/node": "^8.10.20",
|
||||||
"@types/prop-types": "^15.5.3",
|
"@types/prop-types": "^15.5.3",
|
||||||
"@types/puppeteer": "^1.6.2",
|
"@types/puppeteer": "^1.6.2",
|
||||||
"@types/react": "^16.3.14",
|
"@types/react": "16.3.14",
|
||||||
"@types/react-dom": "^16.0.5",
|
"@types/react-dom": "^16.0.5",
|
||||||
"@types/react-redux": "^6.0.6",
|
"@types/react-redux": "^6.0.6",
|
||||||
|
"@types/react-router-dom": "^4.3.1",
|
||||||
"@types/react-virtualized": "^9.18.7",
|
"@types/react-virtualized": "^9.18.7",
|
||||||
"@types/redux": "^3.6.31",
|
"@types/redux": "^3.6.31",
|
||||||
"@types/redux-actions": "^2.2.1",
|
"@types/redux-actions": "^2.2.1",
|
||||||
"@types/semver": "^5.5.0",
|
"@types/semver": "^5.5.0",
|
||||||
"@types/sinon": "^5.0.1",
|
"@types/sinon": "^5.0.1",
|
||||||
"@types/strip-ansi": "^3.0.0",
|
"@types/strip-ansi": "^3.0.0",
|
||||||
|
"@types/styled-components": "^3.0.1",
|
||||||
"@types/supertest": "^2.0.5",
|
"@types/supertest": "^2.0.5",
|
||||||
"@types/type-detect": "^4.0.1",
|
"@types/type-detect": "^4.0.1",
|
||||||
"@types/uuid": "^3.4.4",
|
"@types/uuid": "^3.4.4",
|
||||||
|
|
|
@ -22,7 +22,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"**/@types/node": "8.10.21"
|
"**/@types/node": "8.10.21",
|
||||||
|
"@types/react": "16.3.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kbn/dev-utils": "link:../packages/kbn-dev-utils",
|
"@kbn/dev-utils": "link:../packages/kbn-dev-utils",
|
||||||
|
@ -36,7 +37,7 @@
|
||||||
"@types/d3-shape": "^1.2.2",
|
"@types/d3-shape": "^1.2.2",
|
||||||
"@types/d3-time": "^1.0.7",
|
"@types/d3-time": "^1.0.7",
|
||||||
"@types/d3-time-format": "^2.1.0",
|
"@types/d3-time-format": "^2.1.0",
|
||||||
"@types/elasticsearch": "^5.0.22",
|
"@types/elasticsearch": "^5.0.26",
|
||||||
"@types/expect.js": "^0.3.29",
|
"@types/expect.js": "^0.3.29",
|
||||||
"@types/graphql": "^0.13.1",
|
"@types/graphql": "^0.13.1",
|
||||||
"@types/hapi": "15.0.1",
|
"@types/hapi": "15.0.1",
|
||||||
|
@ -48,11 +49,11 @@
|
||||||
"@types/mocha": "^5.2.5",
|
"@types/mocha": "^5.2.5",
|
||||||
"@types/pngjs": "^3.3.1",
|
"@types/pngjs": "^3.3.1",
|
||||||
"@types/prop-types": "^15.5.3",
|
"@types/prop-types": "^15.5.3",
|
||||||
"@types/react": "^16.3.14",
|
"@types/react": "16.3.14",
|
||||||
"@types/react-datepicker": "^1.1.5",
|
"@types/react-datepicker": "^1.1.5",
|
||||||
"@types/react-dom": "^16.0.5",
|
"@types/react-dom": "^16.0.5",
|
||||||
"@types/react-redux": "^6.0.6",
|
"@types/react-redux": "^6.0.6",
|
||||||
"@types/react-router-dom": "4.2.6",
|
"@types/react-router-dom": "^4.3.1",
|
||||||
"@types/reduce-reducers": "^0.1.3",
|
"@types/reduce-reducers": "^0.1.3",
|
||||||
"@types/sinon": "^5.0.1",
|
"@types/sinon": "^5.0.1",
|
||||||
"@types/supertest": "^2.0.5",
|
"@types/supertest": "^2.0.5",
|
||||||
|
@ -130,7 +131,6 @@
|
||||||
"@samverschueren/stream-to-observable": "^0.3.0",
|
"@samverschueren/stream-to-observable": "^0.3.0",
|
||||||
"@scant/router": "^0.1.0",
|
"@scant/router": "^0.1.0",
|
||||||
"@slack/client": "^4.2.2",
|
"@slack/client": "^4.2.2",
|
||||||
"@types/moment-timezone": "^0.5.8",
|
|
||||||
"angular-resource": "1.4.9",
|
"angular-resource": "1.4.9",
|
||||||
"angular-sanitize": "1.4.9",
|
"angular-sanitize": "1.4.9",
|
||||||
"angular-ui-ace": "0.2.3",
|
"angular-ui-ace": "0.2.3",
|
||||||
|
@ -226,7 +226,7 @@
|
||||||
"react-redux": "^5.0.7",
|
"react-redux": "^5.0.7",
|
||||||
"react-redux-request": "^1.5.6",
|
"react-redux-request": "^1.5.6",
|
||||||
"react-router-breadcrumbs-hoc": "1.1.2",
|
"react-router-breadcrumbs-hoc": "1.1.2",
|
||||||
"react-router-dom": "^4.2.2",
|
"react-router-dom": "^4.3.1",
|
||||||
"react-select": "^1.2.1",
|
"react-select": "^1.2.1",
|
||||||
"react-shortcuts": "^2.0.0",
|
"react-shortcuts": "^2.0.0",
|
||||||
"react-sticky": "^6.0.1",
|
"react-sticky": "^6.0.1",
|
||||||
|
|
440
x-pack/plugins/apm/common/constants.test.ts
Normal file
440
x-pack/plugins/apm/common/constants.test.ts
Normal file
|
@ -0,0 +1,440 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { Span } from '../typings/Span';
|
||||||
|
import { Transaction } from '../typings/Transaction';
|
||||||
|
import {
|
||||||
|
PROCESSOR_EVENT,
|
||||||
|
PROCESSOR_NAME,
|
||||||
|
REQUEST_URL_FULL,
|
||||||
|
SERVICE_AGENT_NAME,
|
||||||
|
SERVICE_LANGUAGE_NAME,
|
||||||
|
SERVICE_NAME,
|
||||||
|
SPAN_DURATION,
|
||||||
|
SPAN_HEX_ID,
|
||||||
|
SPAN_ID,
|
||||||
|
SPAN_NAME,
|
||||||
|
SPAN_SQL,
|
||||||
|
SPAN_START,
|
||||||
|
SPAN_TYPE,
|
||||||
|
TRANSACTION_DURATION,
|
||||||
|
TRANSACTION_ID,
|
||||||
|
TRANSACTION_NAME,
|
||||||
|
TRANSACTION_RESULT,
|
||||||
|
TRANSACTION_SAMPLED,
|
||||||
|
TRANSACTION_TYPE,
|
||||||
|
USER_ID
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
|
describe('Transaction v1', () => {
|
||||||
|
const transaction: Transaction = {
|
||||||
|
version: 'v1',
|
||||||
|
'@timestamp': new Date().toString(),
|
||||||
|
beat: {
|
||||||
|
hostname: 'beat hostname',
|
||||||
|
name: 'beat name',
|
||||||
|
version: 'beat version'
|
||||||
|
},
|
||||||
|
host: {
|
||||||
|
name: 'string;'
|
||||||
|
},
|
||||||
|
processor: {
|
||||||
|
name: 'transaction',
|
||||||
|
event: 'transaction'
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
system: {
|
||||||
|
architecture: 'x86',
|
||||||
|
hostname: 'some-host',
|
||||||
|
ip: '111.0.2.3',
|
||||||
|
platform: 'linux'
|
||||||
|
},
|
||||||
|
service: {
|
||||||
|
name: 'service name',
|
||||||
|
agent: {
|
||||||
|
name: 'agent name',
|
||||||
|
version: 'v1337'
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
name: 'nodejs',
|
||||||
|
version: 'v1337'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
id: '1337'
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
url: {
|
||||||
|
full: 'http://www.elastic.co'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
transaction: {
|
||||||
|
duration: {
|
||||||
|
us: 1337
|
||||||
|
},
|
||||||
|
id: 'transaction id',
|
||||||
|
name: 'transaction name',
|
||||||
|
result: 'transaction result',
|
||||||
|
sampled: true,
|
||||||
|
type: 'transaction type'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// service
|
||||||
|
it('SERVICE_NAME', () => {
|
||||||
|
expect(get(transaction, SERVICE_NAME)).toBe('service name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SERVICE_AGENT_NAME', () => {
|
||||||
|
expect(get(transaction, SERVICE_AGENT_NAME)).toBe('agent name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SERVICE_LANGUAGE_NAME', () => {
|
||||||
|
expect(get(transaction, SERVICE_LANGUAGE_NAME)).toBe('nodejs');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('REQUEST_URL_FULL', () => {
|
||||||
|
expect(get(transaction, REQUEST_URL_FULL)).toBe('http://www.elastic.co');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('USER_ID', () => {
|
||||||
|
expect(get(transaction, USER_ID)).toBe('1337');
|
||||||
|
});
|
||||||
|
|
||||||
|
// processor
|
||||||
|
it('PROCESSOR_NAME', () => {
|
||||||
|
expect(get(transaction, PROCESSOR_NAME)).toBe('transaction');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PROCESSOR_EVENT', () => {
|
||||||
|
expect(get(transaction, PROCESSOR_EVENT)).toBe('transaction');
|
||||||
|
});
|
||||||
|
|
||||||
|
// transaction
|
||||||
|
it('TRANSACTION_DURATION', () => {
|
||||||
|
expect(get(transaction, TRANSACTION_DURATION)).toBe(1337);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TRANSACTION_TYPE', () => {
|
||||||
|
expect(get(transaction, TRANSACTION_TYPE)).toBe('transaction type');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TRANSACTION_RESULT', () => {
|
||||||
|
expect(get(transaction, TRANSACTION_RESULT)).toBe('transaction result');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TRANSACTION_NAME', () => {
|
||||||
|
expect(get(transaction, TRANSACTION_NAME)).toBe('transaction name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TRANSACTION_ID', () => {
|
||||||
|
expect(get(transaction, TRANSACTION_ID)).toBe('transaction id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TRANSACTION_SAMPLED', () => {
|
||||||
|
expect(get(transaction, TRANSACTION_SAMPLED)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Transaction v2', () => {
|
||||||
|
const transaction: Transaction = {
|
||||||
|
version: 'v2',
|
||||||
|
'@timestamp': new Date().toString(),
|
||||||
|
beat: {
|
||||||
|
hostname: 'beat hostname',
|
||||||
|
name: 'beat name',
|
||||||
|
version: 'beat version'
|
||||||
|
},
|
||||||
|
host: { name: 'string;' },
|
||||||
|
processor: { name: 'transaction', event: 'transaction' },
|
||||||
|
timestamp: { us: 1337 },
|
||||||
|
trace: { id: 'trace id' },
|
||||||
|
context: {
|
||||||
|
system: {
|
||||||
|
architecture: 'x86',
|
||||||
|
hostname: 'some-host',
|
||||||
|
ip: '111.0.2.3',
|
||||||
|
platform: 'linux'
|
||||||
|
},
|
||||||
|
service: {
|
||||||
|
name: 'service name',
|
||||||
|
agent: { name: 'agent name', version: 'v1337' },
|
||||||
|
language: { name: 'nodejs', version: 'v1337' }
|
||||||
|
},
|
||||||
|
user: { id: '1337' },
|
||||||
|
request: { url: { full: 'http://www.elastic.co' } }
|
||||||
|
},
|
||||||
|
transaction: {
|
||||||
|
duration: { us: 1337 },
|
||||||
|
id: 'transaction id',
|
||||||
|
name: 'transaction name',
|
||||||
|
result: 'transaction result',
|
||||||
|
sampled: true,
|
||||||
|
type: 'transaction type'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// service
|
||||||
|
it('SERVICE_NAME', () => {
|
||||||
|
expect(get(transaction, SERVICE_NAME)).toBe('service name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SERVICE_AGENT_NAME', () => {
|
||||||
|
expect(get(transaction, SERVICE_AGENT_NAME)).toBe('agent name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SERVICE_LANGUAGE_NAME', () => {
|
||||||
|
expect(get(transaction, SERVICE_LANGUAGE_NAME)).toBe('nodejs');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('REQUEST_URL_FULL', () => {
|
||||||
|
expect(get(transaction, REQUEST_URL_FULL)).toBe('http://www.elastic.co');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('USER_ID', () => {
|
||||||
|
expect(get(transaction, USER_ID)).toBe('1337');
|
||||||
|
});
|
||||||
|
|
||||||
|
// processor
|
||||||
|
it('PROCESSOR_NAME', () => {
|
||||||
|
expect(get(transaction, PROCESSOR_NAME)).toBe('transaction');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PROCESSOR_EVENT', () => {
|
||||||
|
expect(get(transaction, PROCESSOR_EVENT)).toBe('transaction');
|
||||||
|
});
|
||||||
|
|
||||||
|
// transaction
|
||||||
|
it('TRANSACTION_DURATION', () => {
|
||||||
|
expect(get(transaction, TRANSACTION_DURATION)).toBe(1337);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TRANSACTION_TYPE', () => {
|
||||||
|
expect(get(transaction, TRANSACTION_TYPE)).toBe('transaction type');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TRANSACTION_RESULT', () => {
|
||||||
|
expect(get(transaction, TRANSACTION_RESULT)).toBe('transaction result');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TRANSACTION_NAME', () => {
|
||||||
|
expect(get(transaction, TRANSACTION_NAME)).toBe('transaction name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TRANSACTION_ID', () => {
|
||||||
|
expect(get(transaction, TRANSACTION_ID)).toBe('transaction id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TRANSACTION_SAMPLED', () => {
|
||||||
|
expect(get(transaction, TRANSACTION_SAMPLED)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Span v1', () => {
|
||||||
|
const span: Span = {
|
||||||
|
version: 'v1',
|
||||||
|
'@timestamp': new Date().toString(),
|
||||||
|
beat: {
|
||||||
|
hostname: 'beat hostname',
|
||||||
|
name: 'beat name',
|
||||||
|
version: 'beat version'
|
||||||
|
},
|
||||||
|
host: {
|
||||||
|
name: 'string;'
|
||||||
|
},
|
||||||
|
processor: {
|
||||||
|
name: 'transaction',
|
||||||
|
event: 'span'
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
db: {
|
||||||
|
statement: 'db statement'
|
||||||
|
},
|
||||||
|
service: {
|
||||||
|
name: 'service name',
|
||||||
|
agent: {
|
||||||
|
name: 'agent name',
|
||||||
|
version: 'v1337'
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
name: 'nodejs',
|
||||||
|
version: 'v1337'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
span: {
|
||||||
|
duration: {
|
||||||
|
us: 1337
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
us: 1337
|
||||||
|
},
|
||||||
|
name: 'span name',
|
||||||
|
type: 'span type',
|
||||||
|
id: 1337
|
||||||
|
},
|
||||||
|
transaction: {
|
||||||
|
id: 'transaction id'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// service
|
||||||
|
it('SERVICE_NAME', () => {
|
||||||
|
expect(get(span, SERVICE_NAME)).toBe('service name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SERVICE_AGENT_NAME', () => {
|
||||||
|
expect(get(span, SERVICE_AGENT_NAME)).toBe('agent name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SERVICE_LANGUAGE_NAME', () => {
|
||||||
|
expect(get(span, SERVICE_LANGUAGE_NAME)).toBe('nodejs');
|
||||||
|
});
|
||||||
|
|
||||||
|
// processor
|
||||||
|
it('PROCESSOR_NAME', () => {
|
||||||
|
expect(get(span, PROCESSOR_NAME)).toBe('transaction');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PROCESSOR_EVENT', () => {
|
||||||
|
expect(get(span, PROCESSOR_EVENT)).toBe('span');
|
||||||
|
});
|
||||||
|
|
||||||
|
// span
|
||||||
|
it('SPAN_START', () => {
|
||||||
|
expect(get(span, SPAN_START)).toBe(1337);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SPAN_DURATION', () => {
|
||||||
|
expect(get(span, SPAN_DURATION)).toBe(1337);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SPAN_TYPE', () => {
|
||||||
|
expect(get(span, SPAN_TYPE)).toBe('span type');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SPAN_NAME', () => {
|
||||||
|
expect(get(span, SPAN_NAME)).toBe('span name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SPAN_ID', () => {
|
||||||
|
expect(get(span, SPAN_ID)).toBe(1337);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SPAN_SQL', () => {
|
||||||
|
expect(get(span, SPAN_SQL)).toBe('db statement');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SPAN_HEX_ID', () => {
|
||||||
|
expect(get(span, SPAN_HEX_ID)).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Span v2', () => {
|
||||||
|
const span: Span = {
|
||||||
|
version: 'v2',
|
||||||
|
'@timestamp': new Date().toString(),
|
||||||
|
beat: {
|
||||||
|
hostname: 'beat hostname',
|
||||||
|
name: 'beat name',
|
||||||
|
version: 'beat version'
|
||||||
|
},
|
||||||
|
host: {
|
||||||
|
name: 'string;'
|
||||||
|
},
|
||||||
|
processor: {
|
||||||
|
name: 'transaction',
|
||||||
|
event: 'span'
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
us: 1337
|
||||||
|
},
|
||||||
|
trace: {
|
||||||
|
id: 'trace id'
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
db: {
|
||||||
|
statement: 'db statement'
|
||||||
|
},
|
||||||
|
service: {
|
||||||
|
name: 'service name',
|
||||||
|
agent: {
|
||||||
|
name: 'agent name',
|
||||||
|
version: 'v1337'
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
name: 'nodejs',
|
||||||
|
version: 'v1337'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
span: {
|
||||||
|
duration: {
|
||||||
|
us: 1337
|
||||||
|
},
|
||||||
|
name: 'span name',
|
||||||
|
type: 'span type',
|
||||||
|
id: 1337,
|
||||||
|
hex_id: 'hex id'
|
||||||
|
},
|
||||||
|
transaction: {
|
||||||
|
id: 'transaction id'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// service
|
||||||
|
it('SERVICE_NAME', () => {
|
||||||
|
expect(get(span, SERVICE_NAME)).toBe('service name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SERVICE_AGENT_NAME', () => {
|
||||||
|
expect(get(span, SERVICE_AGENT_NAME)).toBe('agent name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SERVICE_LANGUAGE_NAME', () => {
|
||||||
|
expect(get(span, SERVICE_LANGUAGE_NAME)).toBe('nodejs');
|
||||||
|
});
|
||||||
|
|
||||||
|
// processor
|
||||||
|
it('PROCESSOR_NAME', () => {
|
||||||
|
expect(get(span, PROCESSOR_NAME)).toBe('transaction');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PROCESSOR_EVENT', () => {
|
||||||
|
expect(get(span, PROCESSOR_EVENT)).toBe('span');
|
||||||
|
});
|
||||||
|
|
||||||
|
// span
|
||||||
|
it('SPAN_START', () => {
|
||||||
|
expect(get(span, SPAN_START)).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SPAN_DURATION', () => {
|
||||||
|
expect(get(span, SPAN_DURATION)).toBe(1337);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SPAN_TYPE', () => {
|
||||||
|
expect(get(span, SPAN_TYPE)).toBe('span type');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SPAN_NAME', () => {
|
||||||
|
expect(get(span, SPAN_NAME)).toBe('span name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SPAN_ID', () => {
|
||||||
|
expect(get(span, SPAN_ID)).toBe(1337);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SPAN_SQL', () => {
|
||||||
|
expect(get(span, SPAN_SQL)).toBe('db statement');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SPAN_HEX_ID', () => {
|
||||||
|
expect(get(span, SPAN_HEX_ID)).toBe('hex id');
|
||||||
|
});
|
||||||
|
});
|
|
@ -7,6 +7,8 @@
|
||||||
export const SERVICE_NAME = 'context.service.name';
|
export const SERVICE_NAME = 'context.service.name';
|
||||||
export const SERVICE_AGENT_NAME = 'context.service.agent.name';
|
export const SERVICE_AGENT_NAME = 'context.service.agent.name';
|
||||||
export const SERVICE_LANGUAGE_NAME = 'context.service.language.name';
|
export const SERVICE_LANGUAGE_NAME = 'context.service.language.name';
|
||||||
|
export const REQUEST_URL_FULL = 'context.request.url.full';
|
||||||
|
export const USER_ID = 'context.user.id';
|
||||||
|
|
||||||
export const PROCESSOR_NAME = 'processor.name';
|
export const PROCESSOR_NAME = 'processor.name';
|
||||||
export const PROCESSOR_EVENT = 'processor.event';
|
export const PROCESSOR_EVENT = 'processor.event';
|
||||||
|
@ -18,19 +20,21 @@ export const TRANSACTION_NAME = 'transaction.name';
|
||||||
export const TRANSACTION_ID = 'transaction.id';
|
export const TRANSACTION_ID = 'transaction.id';
|
||||||
export const TRANSACTION_SAMPLED = 'transaction.sampled';
|
export const TRANSACTION_SAMPLED = 'transaction.sampled';
|
||||||
|
|
||||||
|
export const TRACE_ID = 'trace.id';
|
||||||
|
|
||||||
export const SPAN_START = 'span.start.us';
|
export const SPAN_START = 'span.start.us';
|
||||||
export const SPAN_DURATION = 'span.duration.us';
|
export const SPAN_DURATION = 'span.duration.us';
|
||||||
export const SPAN_TYPE = 'span.type';
|
export const SPAN_TYPE = 'span.type';
|
||||||
export const SPAN_NAME = 'span.name';
|
export const SPAN_NAME = 'span.name';
|
||||||
export const SPAN_ID = 'span.id';
|
export const SPAN_ID = 'span.id';
|
||||||
export const SPAN_SQL = 'context.db.statement';
|
export const SPAN_SQL = 'context.db.statement';
|
||||||
|
export const SPAN_HEX_ID = 'span.hex_id';
|
||||||
|
|
||||||
|
// Parent ID for a transaction or span
|
||||||
|
export const PARENT_ID = 'parent.id';
|
||||||
|
|
||||||
export const ERROR_GROUP_ID = 'error.grouping_key';
|
export const ERROR_GROUP_ID = 'error.grouping_key';
|
||||||
export const ERROR_CULPRIT = 'error.culprit';
|
export const ERROR_CULPRIT = 'error.culprit';
|
||||||
export const ERROR_LOG_MESSAGE = 'error.log.message';
|
export const ERROR_LOG_MESSAGE = 'error.log.message';
|
||||||
export const ERROR_EXC_MESSAGE = 'error.exception.message';
|
export const ERROR_EXC_MESSAGE = 'error.exception.message';
|
||||||
export const ERROR_EXC_HANDLED = 'error.exception.handled';
|
export const ERROR_EXC_HANDLED = 'error.exception.handled';
|
||||||
|
|
||||||
export const REQUEST_URL_FULL = 'context.request.url.full';
|
|
||||||
|
|
||||||
export const USER_ID = 'context.user.id';
|
|
|
@ -9,6 +9,7 @@ import { initTransactionsApi } from './server/routes/transactions';
|
||||||
import { initServicesApi } from './server/routes/services';
|
import { initServicesApi } from './server/routes/services';
|
||||||
import { initErrorsApi } from './server/routes/errors';
|
import { initErrorsApi } from './server/routes/errors';
|
||||||
import { initStatusApi } from './server/routes/status_check';
|
import { initStatusApi } from './server/routes/status_check';
|
||||||
|
import { initTracesApi } from './server/routes/traces';
|
||||||
|
|
||||||
export function apm(kibana) {
|
export function apm(kibana) {
|
||||||
return new kibana.Plugin({
|
return new kibana.Plugin({
|
||||||
|
@ -55,6 +56,7 @@ export function apm(kibana) {
|
||||||
|
|
||||||
init(server) {
|
init(server) {
|
||||||
initTransactionsApi(server);
|
initTransactionsApi(server);
|
||||||
|
initTracesApi(server);
|
||||||
initServicesApi(server);
|
initServicesApi(server);
|
||||||
initErrorsApi(server);
|
initErrorsApi(server);
|
||||||
initStatusApi(server);
|
initStatusApi(server);
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"exclude": ["node_modules", "**/node_modules/*", "build"]
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -31,7 +31,7 @@ import {
|
||||||
EuiLink
|
EuiLink
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
|
||||||
import { ELASTIC_DOCS } from '../../../../utils/documentation';
|
import { XPACK_DOCS } from '../../../../utils/documentation/xpack';
|
||||||
|
|
||||||
import { KibanaLink } from '../../../../utils/url';
|
import { KibanaLink } from '../../../../utils/url';
|
||||||
import { createErrorGroupWatch } from './createErrorGroupWatch';
|
import { createErrorGroupWatch } from './createErrorGroupWatch';
|
||||||
|
@ -226,10 +226,7 @@ export default class WatcherFlyout extends Component {
|
||||||
This form will assist in creating a Watch that can notify you of error
|
This form will assist in creating a Watch that can notify you of error
|
||||||
occurrences from this service. To learn more about Watcher, please
|
occurrences from this service. To learn more about Watcher, please
|
||||||
read our{' '}
|
read our{' '}
|
||||||
<EuiLink
|
<EuiLink target="_blank" href={XPACK_DOCS.xpackWatcher}>
|
||||||
target="_blank"
|
|
||||||
href={_.get(ELASTIC_DOCS, 'watcher-get-started.url')}
|
|
||||||
>
|
|
||||||
documentation
|
documentation
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
.
|
.
|
||||||
|
@ -344,10 +341,7 @@ export default class WatcherFlyout extends Component {
|
||||||
helpText={
|
helpText={
|
||||||
<span>
|
<span>
|
||||||
If you have not configured email, please see the{' '}
|
If you have not configured email, please see the{' '}
|
||||||
<EuiLink
|
<EuiLink target="_blank" href={XPACK_DOCS.xpackEmails}>
|
||||||
target="_blank"
|
|
||||||
href={_.get(ELASTIC_DOCS, 'x-pack-emails.url')}
|
|
||||||
>
|
|
||||||
documentation
|
documentation
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
.
|
.
|
||||||
|
|
40
x-pack/plugins/apm/public/components/app/Main/Home.tsx
Normal file
40
x-pack/plugins/apm/public/components/app/Main/Home.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { EuiTabbedContent } from '@elastic/eui';
|
||||||
|
import React from 'react';
|
||||||
|
// @ts-ignore
|
||||||
|
import { KueryBar } from '../../shared/KueryBar';
|
||||||
|
import { SetupInstructionsLink } from '../../shared/SetupInstructionsLink';
|
||||||
|
// @ts-ignore
|
||||||
|
import { HeaderContainer } from '../../shared/UIComponents';
|
||||||
|
// @ts-ignore
|
||||||
|
import { ServiceOverview } from '../ServiceOverview';
|
||||||
|
import { TraceOverview } from '../TraceOverview';
|
||||||
|
|
||||||
|
export function Home() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<HeaderContainer>
|
||||||
|
<h1>APM</h1>
|
||||||
|
<SetupInstructionsLink />
|
||||||
|
</HeaderContainer>
|
||||||
|
<KueryBar />
|
||||||
|
<EuiTabbedContent
|
||||||
|
className="k6Tab--large"
|
||||||
|
tabs={[
|
||||||
|
{
|
||||||
|
id: 'services_overview',
|
||||||
|
name: 'Services',
|
||||||
|
content: <ServiceOverview />
|
||||||
|
},
|
||||||
|
{ id: 'traces_overview', name: 'Traces', content: <TraceOverview /> }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import { Home } from '../Home';
|
||||||
|
|
||||||
|
describe('Home component', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
expect(shallow(<Home />)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,30 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Home component should render 1`] = `
|
||||||
|
<div>
|
||||||
|
<styled.div>
|
||||||
|
<h1>
|
||||||
|
APM
|
||||||
|
</h1>
|
||||||
|
<SetupInstructionsLink />
|
||||||
|
</styled.div>
|
||||||
|
<Connect(KueryBarView) />
|
||||||
|
<EuiTabbedContent
|
||||||
|
className="k6Tab--large"
|
||||||
|
tabs={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"content": <Connect(ServiceOverview) />,
|
||||||
|
"id": "services_overview",
|
||||||
|
"name": "Services",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"content": <Connect(TraceOverview) />,
|
||||||
|
"id": "traces_overview",
|
||||||
|
"name": "Traces",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -6,25 +6,40 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import ServiceOverview from '../ServiceOverview';
|
|
||||||
import ErrorGroupDetails from '../ErrorGroupDetails';
|
import { StringMap } from '../../../../typings/common';
|
||||||
import ErrorGroupOverview from '../ErrorGroupOverview';
|
|
||||||
import TransactionDetails from '../TransactionDetails';
|
|
||||||
import TransactionOverview from '../TransactionOverview';
|
|
||||||
import { legacyDecodeURIComponent } from '../../../utils/url';
|
import { legacyDecodeURIComponent } from '../../../utils/url';
|
||||||
|
// @ts-ignore
|
||||||
|
import ErrorGroupDetails from '../ErrorGroupDetails';
|
||||||
|
// @ts-ignore
|
||||||
|
import ErrorGroupOverview from '../ErrorGroupOverview';
|
||||||
|
import { TransactionDetails } from '../TransactionDetails';
|
||||||
|
// @ts-ignore
|
||||||
|
import TransactionOverview from '../TransactionOverview';
|
||||||
|
import { Home } from './Home';
|
||||||
|
|
||||||
|
interface BreadcrumbArgs {
|
||||||
|
match: {
|
||||||
|
params: StringMap<any>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RenderArgs {
|
||||||
|
location: StringMap<any>;
|
||||||
|
}
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
{
|
{
|
||||||
exact: true,
|
exact: true,
|
||||||
path: '/',
|
path: '/',
|
||||||
component: ServiceOverview,
|
component: Home,
|
||||||
breadcrumb: 'APM'
|
breadcrumb: 'APM'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
exact: true,
|
exact: true,
|
||||||
path: '/:serviceName/errors/:groupId',
|
path: '/:serviceName/errors/:groupId',
|
||||||
component: ErrorGroupDetails,
|
component: ErrorGroupDetails,
|
||||||
breadcrumb: ({ match }) => match.params.groupId
|
breadcrumb: ({ match }: BreadcrumbArgs) => match.params.groupId
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
exact: true,
|
exact: true,
|
||||||
|
@ -44,8 +59,8 @@ export const routes = [
|
||||||
{
|
{
|
||||||
exact: true,
|
exact: true,
|
||||||
path: '/:serviceName',
|
path: '/:serviceName',
|
||||||
breadcrumb: ({ match }) => match.params.serviceName,
|
breadcrumb: ({ match }: BreadcrumbArgs) => match.params.serviceName,
|
||||||
render: ({ location }) => {
|
render: ({ location }: RenderArgs) => {
|
||||||
return (
|
return (
|
||||||
<Redirect
|
<Redirect
|
||||||
to={{
|
to={{
|
||||||
|
@ -68,14 +83,14 @@ export const routes = [
|
||||||
exact: true,
|
exact: true,
|
||||||
path: '/:serviceName/transactions/:transactionType',
|
path: '/:serviceName/transactions/:transactionType',
|
||||||
component: TransactionOverview,
|
component: TransactionOverview,
|
||||||
breadcrumb: ({ match }) =>
|
breadcrumb: ({ match }: BreadcrumbArgs) =>
|
||||||
legacyDecodeURIComponent(match.params.transactionType)
|
legacyDecodeURIComponent(match.params.transactionType)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
exact: true,
|
exact: true,
|
||||||
path: '/:serviceName/transactions/:transactionType/:transactionName',
|
path: '/:serviceName/transactions/:transactionType/:transactionName',
|
||||||
component: TransactionDetails,
|
component: TransactionDetails,
|
||||||
breadcrumb: ({ match }) =>
|
breadcrumb: ({ match }: BreadcrumbArgs) =>
|
||||||
legacyDecodeURIComponent(match.params.transactionName)
|
legacyDecodeURIComponent(match.params.transactionName)
|
||||||
}
|
}
|
||||||
];
|
];
|
|
@ -1,143 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import orderBy from 'lodash.orderby';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import numeral from '@elastic/numeral';
|
|
||||||
import { EuiBasicTable } from '@elastic/eui';
|
|
||||||
import { RelativeLink } from '../../../../utils/url';
|
|
||||||
import { fontSizes, truncate } from '../../../../style/variables';
|
|
||||||
import TooltipOverlay from '../../../shared/TooltipOverlay';
|
|
||||||
import { asMillisWithDefault } from '../../../../utils/formatters';
|
|
||||||
|
|
||||||
function formatNumber(value) {
|
|
||||||
if (value === 0) {
|
|
||||||
return '0';
|
|
||||||
}
|
|
||||||
const formatted = numeral(value).format('0.0');
|
|
||||||
return formatted <= 0.1 ? '< 0.1' : formatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: duplicated
|
|
||||||
function paginateItems({ items, pageIndex, pageSize }) {
|
|
||||||
return items.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatString(value) {
|
|
||||||
return value || 'N/A';
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppLink = styled(RelativeLink)`
|
|
||||||
font-size: ${fontSizes.large};
|
|
||||||
${truncate('100%')};
|
|
||||||
`;
|
|
||||||
|
|
||||||
class List extends Component {
|
|
||||||
state = {
|
|
||||||
page: {
|
|
||||||
index: 0,
|
|
||||||
size: 10
|
|
||||||
},
|
|
||||||
sort: {
|
|
||||||
field: 'serviceName',
|
|
||||||
direction: 'asc'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onTableChange = ({ page = {}, sort = {} }) => {
|
|
||||||
this.setState({ page, sort });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
field: 'serviceName',
|
|
||||||
name: 'Name',
|
|
||||||
width: '50%',
|
|
||||||
sortable: true,
|
|
||||||
render: serviceName => (
|
|
||||||
<TooltipOverlay content={formatString(serviceName)}>
|
|
||||||
<AppLink path={`/${serviceName}/transactions`}>
|
|
||||||
{formatString(serviceName)}
|
|
||||||
</AppLink>
|
|
||||||
</TooltipOverlay>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'agentName',
|
|
||||||
name: 'Agent',
|
|
||||||
sortable: true,
|
|
||||||
render: agentName => formatString(agentName)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'avgResponseTime',
|
|
||||||
name: 'Avg. response time',
|
|
||||||
sortable: true,
|
|
||||||
dataType: 'number',
|
|
||||||
render: value => asMillisWithDefault(value)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'transactionsPerMinute',
|
|
||||||
name: 'Trans. per minute',
|
|
||||||
sortable: true,
|
|
||||||
dataType: 'number',
|
|
||||||
render: value => `${formatNumber(value)} tpm`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'errorsPerMinute',
|
|
||||||
name: 'Errors per minute',
|
|
||||||
sortable: true,
|
|
||||||
dataType: 'number',
|
|
||||||
render: value => `${formatNumber(value)} err.`
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const sortedItems = orderBy(
|
|
||||||
this.props.items,
|
|
||||||
this.state.sort.field,
|
|
||||||
this.state.sort.direction
|
|
||||||
);
|
|
||||||
|
|
||||||
const paginatedItems = paginateItems({
|
|
||||||
items: sortedItems,
|
|
||||||
pageIndex: this.state.page.index,
|
|
||||||
pageSize: this.state.page.size
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EuiBasicTable
|
|
||||||
noItemsMessage={this.props.noItemsMessage}
|
|
||||||
items={paginatedItems}
|
|
||||||
columns={columns}
|
|
||||||
pagination={{
|
|
||||||
pageIndex: this.state.page.index,
|
|
||||||
pageSize: this.state.page.size,
|
|
||||||
totalItemCount: this.props.items.length
|
|
||||||
}}
|
|
||||||
sorting={{
|
|
||||||
sort: {
|
|
||||||
field: this.state.sort.field,
|
|
||||||
direction: this.state.sort.direction
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={this.onTableChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List.propTypes = {
|
|
||||||
noItemsMessage: PropTypes.node,
|
|
||||||
items: PropTypes.array
|
|
||||||
};
|
|
||||||
|
|
||||||
List.defaultProps = {
|
|
||||||
items: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default List;
|
|
|
@ -8,7 +8,7 @@ import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import List from '../index';
|
import { ServiceList } from '../index';
|
||||||
import props from './props.json';
|
import props from './props.json';
|
||||||
import {
|
import {
|
||||||
mountWithRouterAndStore,
|
mountWithRouterAndStore,
|
||||||
|
@ -25,7 +25,7 @@ describe('ErrorGroupOverview -> List', () => {
|
||||||
const storeState = {};
|
const storeState = {};
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<List items={[]} />
|
<ServiceList items={[]} />
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
storeState
|
storeState
|
||||||
);
|
);
|
||||||
|
@ -36,7 +36,7 @@ describe('ErrorGroupOverview -> List', () => {
|
||||||
it('should render with data', () => {
|
it('should render with data', () => {
|
||||||
const storeState = { location: {} };
|
const storeState = { location: {} };
|
||||||
const wrapper = mountWithRouterAndStore(
|
const wrapper = mountWithRouterAndStore(
|
||||||
<List items={props.items} />,
|
<ServiceList items={props.items} />,
|
||||||
storeState
|
storeState
|
||||||
);
|
);
|
||||||
|
|
|
@ -254,60 +254,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = `
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||||
>
|
/>
|
||||||
<div
|
|
||||||
className="euiPopover euiPopover--anchorUpRight euiPopover--withTitle"
|
|
||||||
id="customizablePagination"
|
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="euiPopover__anchor"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="euiButtonEmpty euiButtonEmpty--text euiButtonEmpty--xSmall euiButtonEmpty--iconRight"
|
|
||||||
onClick={[Function]}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="euiButtonEmpty__content"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
className="euiIcon euiIcon--medium euiButtonEmpty__icon"
|
|
||||||
focusable="false"
|
|
||||||
height="16"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"fill": undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
width="16"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<path
|
|
||||||
d="M13.069 5.157L8.384 9.768a.546.546 0 0 1-.768 0L2.93 5.158a.552.552 0 0 0-.771 0 .53.53 0 0 0 0 .759l4.684 4.61c.641.631 1.672.63 2.312 0l4.684-4.61a.53.53 0 0 0 0-.76.552.552 0 0 0-.771 0z"
|
|
||||||
id="arrow_down-a"
|
|
||||||
/>
|
|
||||||
</defs>
|
|
||||||
<use
|
|
||||||
fillRule="nonzero"
|
|
||||||
xlinkHref="#arrow_down-a"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span
|
|
||||||
className="euiButtonEmpty__text"
|
|
||||||
>
|
|
||||||
Rows per page: 10
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||||
>
|
>
|
||||||
|
@ -690,60 +637,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||||
>
|
/>
|
||||||
<div
|
|
||||||
className="euiPopover euiPopover--anchorUpRight euiPopover--withTitle"
|
|
||||||
id="customizablePagination"
|
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="euiPopover__anchor"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="euiButtonEmpty euiButtonEmpty--text euiButtonEmpty--xSmall euiButtonEmpty--iconRight"
|
|
||||||
onClick={[Function]}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="euiButtonEmpty__content"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
className="euiIcon euiIcon--medium euiButtonEmpty__icon"
|
|
||||||
focusable="false"
|
|
||||||
height="16"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"fill": undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
width="16"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<path
|
|
||||||
d="M13.069 5.157L8.384 9.768a.546.546 0 0 1-.768 0L2.93 5.158a.552.552 0 0 0-.771 0 .53.53 0 0 0 0 .759l4.684 4.61c.641.631 1.672.63 2.312 0l4.684-4.61a.53.53 0 0 0 0-.76.552.552 0 0 0-.771 0z"
|
|
||||||
id="arrow_down-a"
|
|
||||||
/>
|
|
||||||
</defs>
|
|
||||||
<use
|
|
||||||
fillRule="nonzero"
|
|
||||||
xlinkHref="#arrow_down-a"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span
|
|
||||||
className="euiButtonEmpty__text"
|
|
||||||
>
|
|
||||||
Rows per page: 10
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||||
>
|
>
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import numeral from '@elastic/numeral';
|
||||||
|
import { RelativeLink } from '../../../../utils/url';
|
||||||
|
import { fontSizes, truncate } from '../../../../style/variables';
|
||||||
|
import TooltipOverlay from '../../../shared/TooltipOverlay';
|
||||||
|
import { asMillisWithDefault } from '../../../../utils/formatters';
|
||||||
|
import { ManagedTable } from '../../../shared/ManagedTable';
|
||||||
|
|
||||||
|
// TODO: Consolidate these formatting helpers centrally
|
||||||
|
function formatNumber(value) {
|
||||||
|
if (value === 0) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
const formatted = numeral(value).format('0.0');
|
||||||
|
return formatted <= 0.1 ? '< 0.1' : formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatString(value) {
|
||||||
|
return value || 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppLink = styled(RelativeLink)`
|
||||||
|
font-size: ${fontSizes.large};
|
||||||
|
${truncate('100%')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SERVICE_COLUMNS = [
|
||||||
|
{
|
||||||
|
field: 'serviceName',
|
||||||
|
name: 'Name',
|
||||||
|
width: '50%',
|
||||||
|
sortable: true,
|
||||||
|
render: serviceName => (
|
||||||
|
<TooltipOverlay content={formatString(serviceName)}>
|
||||||
|
<AppLink path={`/${serviceName}/transactions`}>
|
||||||
|
{formatString(serviceName)}
|
||||||
|
</AppLink>
|
||||||
|
</TooltipOverlay>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'agentName',
|
||||||
|
name: 'Agent',
|
||||||
|
sortable: true,
|
||||||
|
render: agentName => formatString(agentName)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'avgResponseTime',
|
||||||
|
name: 'Avg. response time',
|
||||||
|
sortable: true,
|
||||||
|
dataType: 'number',
|
||||||
|
render: value => asMillisWithDefault(value)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'transactionsPerMinute',
|
||||||
|
name: 'Trans. per minute',
|
||||||
|
sortable: true,
|
||||||
|
dataType: 'number',
|
||||||
|
render: value => `${formatNumber(value)} tpm`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'errorsPerMinute',
|
||||||
|
name: 'Errors per minute',
|
||||||
|
sortable: true,
|
||||||
|
dataType: 'number',
|
||||||
|
render: value => `${formatNumber(value)} err.`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ServiceList({ items, noItemsMessage }) {
|
||||||
|
return (
|
||||||
|
<ManagedTable
|
||||||
|
columns={SERVICE_COLUMNS}
|
||||||
|
items={items}
|
||||||
|
noItemsMessage={noItemsMessage}
|
||||||
|
initialSort={{ field: 'serviceName', direction: 'asc' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceList.propTypes = {
|
||||||
|
noItemsMessage: PropTypes.node,
|
||||||
|
items: PropTypes.array
|
||||||
|
};
|
||||||
|
|
||||||
|
ServiceList.defaultProps = {
|
||||||
|
items: []
|
||||||
|
};
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import ServiceOverview from '../view';
|
import { ServiceOverview } from '../view';
|
||||||
import { STATUS } from '../../../../constants';
|
import { STATUS } from '../../../../constants';
|
||||||
import * as apmRestServices from '../../../../services/rest/apm';
|
import * as apmRestServices from '../../../../services/rest/apm';
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,9 @@
|
||||||
|
|
||||||
exports[`Service Overview -> View should render when historical data is found 1`] = `
|
exports[`Service Overview -> View should render when historical data is found 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<styled.div>
|
<EuiSpacer
|
||||||
<h1>
|
size="l"
|
||||||
Services
|
/>
|
||||||
</h1>
|
|
||||||
<SetupInstructionsLink />
|
|
||||||
</styled.div>
|
|
||||||
<Connect(KueryBarView) />
|
|
||||||
<ServiceListRequest
|
<ServiceListRequest
|
||||||
render={[Function]}
|
render={[Function]}
|
||||||
/>
|
/>
|
||||||
|
@ -20,7 +16,6 @@ Object {
|
||||||
"items": Array [],
|
"items": Array [],
|
||||||
"noItemsMessage": <EmptyMessage
|
"noItemsMessage": <EmptyMessage
|
||||||
heading="No services were found"
|
heading="No services were found"
|
||||||
hideSubheading={false}
|
|
||||||
subheading={null}
|
subheading={null}
|
||||||
/>,
|
/>,
|
||||||
}
|
}
|
||||||
|
@ -28,13 +23,9 @@ Object {
|
||||||
|
|
||||||
exports[`Service Overview -> View should render when historical data is not found 1`] = `
|
exports[`Service Overview -> View should render when historical data is not found 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<styled.div>
|
<EuiSpacer
|
||||||
<h1>
|
size="l"
|
||||||
Services
|
/>
|
||||||
</h1>
|
|
||||||
<SetupInstructionsLink />
|
|
||||||
</styled.div>
|
|
||||||
<Connect(KueryBarView) />
|
|
||||||
<ServiceListRequest
|
<ServiceListRequest
|
||||||
render={[Function]}
|
render={[Function]}
|
||||||
/>
|
/>
|
||||||
|
@ -46,7 +37,6 @@ Object {
|
||||||
"items": Array [],
|
"items": Array [],
|
||||||
"noItemsMessage": <EmptyMessage
|
"noItemsMessage": <EmptyMessage
|
||||||
heading="Looks like you don't have any services with APM installed. Let's add some!"
|
heading="Looks like you don't have any services with APM installed. Let's add some!"
|
||||||
hideSubheading={false}
|
|
||||||
subheading={
|
subheading={
|
||||||
<SetupInstructionsLink
|
<SetupInstructionsLink
|
||||||
buttonFill={true}
|
buttonFill={true}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ServiceOverview from './view';
|
import { ServiceOverview as View } from './view';
|
||||||
import { getServiceList } from '../../../store/reactReduxRequest/serviceList';
|
import { getServiceList } from '../../../store/reactReduxRequest/serviceList';
|
||||||
import { getUrlParams } from '../../../store/urlParams';
|
import { getUrlParams } from '../../../store/urlParams';
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ function mapStateToProps(state = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {};
|
const mapDispatchToProps = {};
|
||||||
export default connect(
|
export const ServiceOverview = connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(ServiceOverview);
|
)(View);
|
||||||
|
|
|
@ -8,16 +8,13 @@ import React, { Component } from 'react';
|
||||||
import { STATUS } from '../../../constants';
|
import { STATUS } from '../../../constants';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { loadAgentStatus } from '../../../services/rest/apm';
|
import { loadAgentStatus } from '../../../services/rest/apm';
|
||||||
import { KibanaLink } from '../../../utils/url';
|
import { ServiceList } from './ServiceList';
|
||||||
import { EuiButton } from '@elastic/eui';
|
import { EuiSpacer } from '@elastic/eui';
|
||||||
import List from './List';
|
|
||||||
import { HeaderContainer } from '../../shared/UIComponents';
|
|
||||||
import { KueryBar } from '../../shared/KueryBar';
|
|
||||||
|
|
||||||
import { ServiceListRequest } from '../../../store/reactReduxRequest/serviceList';
|
import { ServiceListRequest } from '../../../store/reactReduxRequest/serviceList';
|
||||||
import EmptyMessage from '../../shared/EmptyMessage';
|
import EmptyMessage from '../../shared/EmptyMessage';
|
||||||
|
import { SetupInstructionsLink } from '../../shared/SetupInstructionsLink';
|
||||||
|
|
||||||
class ServiceOverview extends Component {
|
export class ServiceOverview extends Component {
|
||||||
state = {
|
state = {
|
||||||
historicalDataFound: true
|
historicalDataFound: true
|
||||||
};
|
};
|
||||||
|
@ -59,32 +56,14 @@ class ServiceOverview extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<HeaderContainer>
|
<EuiSpacer />
|
||||||
<h1>Services</h1>
|
|
||||||
<SetupInstructionsLink />
|
|
||||||
</HeaderContainer>
|
|
||||||
|
|
||||||
<KueryBar />
|
|
||||||
|
|
||||||
<ServiceListRequest
|
<ServiceListRequest
|
||||||
urlParams={urlParams}
|
urlParams={urlParams}
|
||||||
render={({ data }) => (
|
render={({ data }) => (
|
||||||
<List items={data} noItemsMessage={noItemsMessage} />
|
<ServiceList items={data} noItemsMessage={noItemsMessage} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function SetupInstructionsLink({ buttonFill = false }) {
|
|
||||||
return (
|
|
||||||
<KibanaLink pathname={'/app/kibana'} hash={'/home/tutorial/apm'}>
|
|
||||||
<EuiButton size="s" color="primary" fill={buttonFill}>
|
|
||||||
Setup Instructions
|
|
||||||
</EuiButton>
|
|
||||||
</KibanaLink>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ServiceOverview;
|
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Transaction } from '../../../../typings/Transaction';
|
||||||
|
import { ITransactionGroup } from '../../../../typings/TransactionGroup';
|
||||||
|
import { fontSizes, truncate } from '../../../style/variables';
|
||||||
|
// @ts-ignore
|
||||||
|
import { asMillisWithDefault } from '../../../utils/formatters';
|
||||||
|
import { ImpactBar } from '../../shared/ImpactBar';
|
||||||
|
import { ITableColumn, ManagedTable } from '../../shared/ManagedTable';
|
||||||
|
// @ts-ignore
|
||||||
|
import TooltipOverlay from '../../shared/TooltipOverlay';
|
||||||
|
import { TraceLink } from '../../shared/TraceLink';
|
||||||
|
|
||||||
|
function formatString(value: string) {
|
||||||
|
return value || 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledTraceLink = styled(TraceLink)`
|
||||||
|
font-size: ${fontSizes.large};
|
||||||
|
${truncate('100%')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
items: ITransactionGroup[];
|
||||||
|
noItemsMessage: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const traceListColumns: ITableColumn[] = [
|
||||||
|
{
|
||||||
|
field: 'sample',
|
||||||
|
name: 'Name',
|
||||||
|
width: '40%',
|
||||||
|
sortable: true,
|
||||||
|
render: (transaction: Transaction) => (
|
||||||
|
<TooltipOverlay content={formatString(transaction.transaction.name)}>
|
||||||
|
<StyledTraceLink transaction={transaction}>
|
||||||
|
{formatString(transaction.transaction.name)}
|
||||||
|
</StyledTraceLink>
|
||||||
|
</TooltipOverlay>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sample',
|
||||||
|
name: 'Originating service',
|
||||||
|
sortable: true,
|
||||||
|
render: (transaction: Transaction) =>
|
||||||
|
formatString(transaction.context.service.name)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'averageResponseTime',
|
||||||
|
name: 'Avg. response time',
|
||||||
|
sortable: true,
|
||||||
|
dataType: 'number',
|
||||||
|
render: (value: number) => asMillisWithDefault(value)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'transactionsPerMinute',
|
||||||
|
name: 'Traces per minute',
|
||||||
|
sortable: true,
|
||||||
|
dataType: 'number',
|
||||||
|
render: (value: number) => `${value.toLocaleString()} tpm`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'impact',
|
||||||
|
name: 'Impact',
|
||||||
|
width: '20%',
|
||||||
|
align: 'right',
|
||||||
|
sortable: true,
|
||||||
|
render: (value: number) => <ImpactBar value={value} />
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function TraceList({ items = [], noItemsMessage, ...rest }: Props) {
|
||||||
|
return (
|
||||||
|
<ManagedTable
|
||||||
|
columns={traceListColumns}
|
||||||
|
items={items}
|
||||||
|
initialSort={{ field: 'impact', direction: 'desc' }}
|
||||||
|
noItemsMessage={noItemsMessage}
|
||||||
|
initialPageSize={25}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -5,18 +5,15 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import TransactionsDetails from './view';
|
import { IReduxState } from '../../../store/rootReducer';
|
||||||
|
// @ts-ignore
|
||||||
import { getUrlParams } from '../../../store/urlParams';
|
import { getUrlParams } from '../../../store/urlParams';
|
||||||
|
import { TraceOverview as View } from './view';
|
||||||
|
|
||||||
function mapStateToProps(state = {}) {
|
function mapStateToProps(state = {} as IReduxState) {
|
||||||
return {
|
return {
|
||||||
location: state.location,
|
|
||||||
urlParams: getUrlParams(state)
|
urlParams: getUrlParams(state)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {};
|
export const TraceOverview = connect(mapStateToProps)(View);
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(TransactionsDetails);
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EuiSpacer } from '@elastic/eui';
|
||||||
|
import React from 'react';
|
||||||
|
import { ITransactionGroup } from '../../../../typings/TransactionGroup';
|
||||||
|
// @ts-ignore
|
||||||
|
import { TraceListRequest } from '../../../store/reactReduxRequest/traceList';
|
||||||
|
import EmptyMessage from '../../shared/EmptyMessage';
|
||||||
|
import { TraceList } from './TraceList';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
urlParams: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TraceOverview(props: Props) {
|
||||||
|
const { urlParams } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<EuiSpacer />
|
||||||
|
<TraceListRequest
|
||||||
|
urlParams={urlParams}
|
||||||
|
render={({ data }: { data: ITransactionGroup[] }) => (
|
||||||
|
<TraceList
|
||||||
|
items={data}
|
||||||
|
noItemsMessage={
|
||||||
|
<EmptyMessage heading="No traces found for this query" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,7 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getFormattedBuckets } from '../view';
|
import { getFormattedBuckets } from '../index';
|
||||||
|
|
||||||
describe('Distribution', () => {
|
describe('Distribution', () => {
|
||||||
it('getFormattedBuckets', () => {
|
it('getFormattedBuckets', () => {
|
||||||
|
@ -12,32 +12,41 @@ describe('Distribution', () => {
|
||||||
{ key: 0, count: 0 },
|
{ key: 0, count: 0 },
|
||||||
{ key: 20, count: 0 },
|
{ key: 20, count: 0 },
|
||||||
{ key: 40, count: 0 },
|
{ key: 40, count: 0 },
|
||||||
{ key: 60, count: 5, transactionId: 'someTransactionId', sampled: true },
|
{
|
||||||
|
key: 60,
|
||||||
|
count: 5,
|
||||||
|
sample: {
|
||||||
|
transactionId: 'someTransactionId'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 80,
|
key: 80,
|
||||||
count: 100,
|
count: 100,
|
||||||
transactionId: 'anotherTransactionId',
|
sample: {
|
||||||
sampled: true
|
transactionId: 'anotherTransactionId'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
expect(getFormattedBuckets(buckets, 20)).toEqual([
|
expect(getFormattedBuckets(buckets, 20)).toEqual([
|
||||||
{ x: 20, x0: 0, y: 0, style: {} },
|
{ x: 20, x0: 0, y: 0, style: { cursor: 'default' } },
|
||||||
{ x: 40, x0: 20, y: 0, style: {} },
|
{ x: 40, x0: 20, y: 0, style: { cursor: 'default' } },
|
||||||
{ x: 60, x0: 40, y: 0, style: {} },
|
{ x: 60, x0: 40, y: 0, style: { cursor: 'default' } },
|
||||||
{
|
{
|
||||||
x: 80,
|
x: 80,
|
||||||
x0: 60,
|
x0: 60,
|
||||||
y: 5,
|
y: 5,
|
||||||
sampled: true,
|
sample: {
|
||||||
transactionId: 'someTransactionId',
|
transactionId: 'someTransactionId'
|
||||||
|
},
|
||||||
style: { cursor: 'pointer' }
|
style: { cursor: 'pointer' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
x: 100,
|
x: 100,
|
||||||
x0: 80,
|
x0: 80,
|
||||||
y: 100,
|
y: 100,
|
||||||
sampled: true,
|
sample: {
|
||||||
transactionId: 'anotherTransactionId',
|
transactionId: 'anotherTransactionId'
|
||||||
|
},
|
||||||
style: { cursor: 'pointer' }
|
style: { cursor: 'pointer' }
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import Distribution from './view';
|
|
||||||
import { getUrlParams } from '../../../../store/urlParams';
|
|
||||||
|
|
||||||
function mapStateToProps(state = {}) {
|
|
||||||
return {
|
|
||||||
urlParams: getUrlParams(state),
|
|
||||||
location: state.location
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {};
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(Distribution);
|
|
|
@ -4,44 +4,66 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import d3 from 'd3';
|
import d3 from 'd3';
|
||||||
import Histogram from '../../../shared/charts/Histogram';
|
import React, { Component } from 'react';
|
||||||
import { toQuery, fromQuery, history } from '../../../../utils/url';
|
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||||
import { HeaderSmall } from '../../../shared/UIComponents';
|
import { IBucket } from 'x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets';
|
||||||
import EmptyMessage from '../../../shared/EmptyMessage';
|
import { IDistributionResponse } from 'x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution';
|
||||||
|
// @ts-ignore
|
||||||
import { getTimeFormatter, timeUnit } from '../../../../utils/formatters';
|
import { getTimeFormatter, timeUnit } from '../../../../utils/formatters';
|
||||||
|
// @ts-ignore
|
||||||
|
import { fromQuery, history, toQuery } from '../../../../utils/url';
|
||||||
|
// @ts-ignore
|
||||||
|
import Histogram from '../../../shared/charts/Histogram';
|
||||||
|
import EmptyMessage from '../../../shared/EmptyMessage';
|
||||||
|
// @ts-ignore
|
||||||
|
import { HeaderSmall } from '../../../shared/UIComponents';
|
||||||
|
// @ts-ignore
|
||||||
import SamplingTooltip from './SamplingTooltip';
|
import SamplingTooltip from './SamplingTooltip';
|
||||||
|
|
||||||
export function getFormattedBuckets(buckets, bucketSize) {
|
interface IChartPoint {
|
||||||
|
sample?: IBucket['sample'];
|
||||||
|
x0: string;
|
||||||
|
x: string;
|
||||||
|
y: number;
|
||||||
|
style: {
|
||||||
|
cursor: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFormattedBuckets(buckets: IBucket[], bucketSize: number) {
|
||||||
if (!buckets) {
|
if (!buckets) {
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return buckets.map(({ sampled, count, key, transactionId }) => {
|
return buckets.map(({ sample, count, key }) => {
|
||||||
return {
|
return {
|
||||||
sampled,
|
sample,
|
||||||
transactionId,
|
|
||||||
x0: key,
|
x0: key,
|
||||||
x: key + bucketSize,
|
x: key + bucketSize,
|
||||||
y: count,
|
y: count,
|
||||||
style: count > 0 && sampled ? { cursor: 'pointer' } : {}
|
style: { cursor: count > 0 && sample ? 'pointer' : 'default' }
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class Distribution extends Component {
|
interface Props {
|
||||||
formatYShort = t => {
|
location: any;
|
||||||
|
distribution: IDistributionResponse;
|
||||||
|
urlParams: IUrlParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Distribution extends Component<Props> {
|
||||||
|
public formatYShort = (t: number) => {
|
||||||
return `${t} ${unitShort(this.props.urlParams.transactionType)}`;
|
return `${t} ${unitShort(this.props.urlParams.transactionType)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
formatYLong = t => {
|
public formatYLong = (t: number) => {
|
||||||
return `${t} ${unitLong(this.props.urlParams.transactionType, t)}`;
|
return `${t} ${unitLong(this.props.urlParams.transactionType, t)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render() {
|
||||||
const { location, distribution } = this.props;
|
const { location, distribution, urlParams } = this.props;
|
||||||
|
|
||||||
const buckets = getFormattedBuckets(
|
const buckets = getFormattedBuckets(
|
||||||
distribution.buckets,
|
distribution.buckets,
|
||||||
|
@ -58,7 +80,10 @@ class Distribution extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const bucketIndex = buckets.findIndex(
|
const bucketIndex = buckets.findIndex(
|
||||||
bucket => bucket.transactionId === this.props.urlParams.transactionId
|
bucket =>
|
||||||
|
bucket.sample != null &&
|
||||||
|
bucket.sample.transactionId === urlParams.transactionId &&
|
||||||
|
bucket.sample.traceId === urlParams.traceId
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -76,13 +101,14 @@ class Distribution extends Component {
|
||||||
buckets={buckets}
|
buckets={buckets}
|
||||||
bucketSize={distribution.bucketSize}
|
bucketSize={distribution.bucketSize}
|
||||||
bucketIndex={bucketIndex}
|
bucketIndex={bucketIndex}
|
||||||
onClick={bucket => {
|
onClick={(bucket: IChartPoint) => {
|
||||||
if (bucket.sampled && bucket.y > 0) {
|
if (bucket.sample && bucket.y > 0) {
|
||||||
history.replace({
|
history.replace({
|
||||||
...location,
|
...location,
|
||||||
search: fromQuery({
|
search: fromQuery({
|
||||||
...toQuery(location.search),
|
...toQuery(location.search),
|
||||||
transactionId: bucket.transactionId
|
transactionId: bucket.sample.transactionId,
|
||||||
|
traceId: bucket.sample.traceId
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -90,16 +116,20 @@ class Distribution extends Component {
|
||||||
formatX={timeFormatter}
|
formatX={timeFormatter}
|
||||||
formatYShort={this.formatYShort}
|
formatYShort={this.formatYShort}
|
||||||
formatYLong={this.formatYLong}
|
formatYLong={this.formatYLong}
|
||||||
verticalLineHover={bucket => bucket.y > 0 && !bucket.sampled}
|
verticalLineHover={(bucket: IChartPoint) =>
|
||||||
backgroundHover={bucket => bucket.y > 0 && bucket.sampled}
|
bucket.y > 0 && !bucket.sample
|
||||||
tooltipHeader={bucket =>
|
}
|
||||||
|
backgroundHover={(bucket: IChartPoint) =>
|
||||||
|
bucket.y > 0 && bucket.sample
|
||||||
|
}
|
||||||
|
tooltipHeader={(bucket: IChartPoint) =>
|
||||||
`${timeFormatter(bucket.x0, false)} - ${timeFormatter(
|
`${timeFormatter(bucket.x0, false)} - ${timeFormatter(
|
||||||
bucket.x,
|
bucket.x,
|
||||||
false
|
false
|
||||||
)} ${unit}`
|
)} ${unit}`
|
||||||
}
|
}
|
||||||
tooltipFooter={bucket =>
|
tooltipFooter={(bucket: IChartPoint) =>
|
||||||
!bucket.sampled && 'No sample available for this bucket'
|
!bucket.sample && 'No sample available for this bucket'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,20 +137,12 @@ class Distribution extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function unitShort(type) {
|
function unitShort(type: string | undefined) {
|
||||||
return type === 'request' ? 'req.' : 'trans.';
|
return type === 'request' ? 'req.' : 'trans.';
|
||||||
}
|
}
|
||||||
|
|
||||||
function unitLong(type, count) {
|
function unitLong(type: string | undefined, count: number) {
|
||||||
const suffix = count > 1 ? 's' : '';
|
const suffix = count > 1 ? 's' : '';
|
||||||
|
|
||||||
return type === 'request' ? `request${suffix}` : `transaction${suffix}`;
|
return type === 'request' ? `request${suffix}` : `transaction${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
Distribution.propTypes = {
|
|
||||||
urlParams: PropTypes.object.isRequired,
|
|
||||||
location: PropTypes.object.isRequired,
|
|
||||||
distribution: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Distribution;
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
EuiButton,
|
||||||
|
EuiContextMenuItem,
|
||||||
|
EuiContextMenuPanel,
|
||||||
|
EuiPopover
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
PROCESSOR_EVENT,
|
||||||
|
TRACE_ID,
|
||||||
|
TRANSACTION_ID
|
||||||
|
} from 'x-pack/plugins/apm/common/constants';
|
||||||
|
import { KibanaLink } from 'x-pack/plugins/apm/public/utils/url';
|
||||||
|
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||||
|
|
||||||
|
function getDiscoverQuery(transactionId: string, traceId?: string) {
|
||||||
|
let query = `${PROCESSOR_EVENT}:transaction AND ${TRANSACTION_ID}:${transactionId}`;
|
||||||
|
if (traceId) {
|
||||||
|
query += ` AND ${TRACE_ID}:${traceId}`;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
_a: {
|
||||||
|
interval: 'auto',
|
||||||
|
query: {
|
||||||
|
language: 'lucene',
|
||||||
|
query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInfraMetricsQuery(transaction: Transaction) {
|
||||||
|
const plus5 = new Date(transaction['@timestamp']);
|
||||||
|
const minus5 = new Date(transaction['@timestamp']);
|
||||||
|
|
||||||
|
plus5.setMinutes(plus5.getMinutes() + 5);
|
||||||
|
minus5.setMinutes(minus5.getMinutes() - 5);
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: minus5.getTime(),
|
||||||
|
to: plus5.getTime()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActionMenuButton({ onClick }: { onClick: () => void }) {
|
||||||
|
return (
|
||||||
|
<EuiButton iconType="arrowDown" iconSide="right" onClick={onClick}>
|
||||||
|
Actions
|
||||||
|
</EuiButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionMenuProps {
|
||||||
|
readonly transaction: Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionMenuState {
|
||||||
|
readonly isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ActionMenu extends React.Component<
|
||||||
|
ActionMenuProps,
|
||||||
|
ActionMenuState
|
||||||
|
> {
|
||||||
|
public state = {
|
||||||
|
isOpen: false
|
||||||
|
};
|
||||||
|
|
||||||
|
public toggle = () => {
|
||||||
|
this.setState(state => ({ isOpen: !state.isOpen }));
|
||||||
|
};
|
||||||
|
|
||||||
|
public close = () => {
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { transaction } = this.props;
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
<EuiContextMenuItem icon="discoverApp" key="discover-transaction">
|
||||||
|
<KibanaLink
|
||||||
|
pathname="/app/kibana"
|
||||||
|
hash="/discover"
|
||||||
|
query={getDiscoverQuery(
|
||||||
|
transaction.transaction.id,
|
||||||
|
transaction.version === 'v2' ? transaction.trace.id : undefined
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
View sample document
|
||||||
|
</KibanaLink>
|
||||||
|
</EuiContextMenuItem>,
|
||||||
|
<EuiContextMenuItem icon="infraApp" key="infra-host-metrics">
|
||||||
|
<KibanaLink
|
||||||
|
pathname="/app/infra"
|
||||||
|
hash={`/link-to/host-detail/${transaction.context.system.hostname}`}
|
||||||
|
query={getInfraMetricsQuery(transaction)}
|
||||||
|
>
|
||||||
|
<span>View host metrics (beta)</span>
|
||||||
|
</KibanaLink>
|
||||||
|
</EuiContextMenuItem>,
|
||||||
|
<EuiContextMenuItem icon="infraApp" key="infra-host-logs">
|
||||||
|
<KibanaLink
|
||||||
|
pathname="/app/infra"
|
||||||
|
hash={`/link-to/host-logs/${transaction.context.system.hostname}`}
|
||||||
|
query={{ time: new Date(transaction['@timestamp']).getTime() }}
|
||||||
|
>
|
||||||
|
<span>View host logs (beta)</span>
|
||||||
|
</KibanaLink>
|
||||||
|
</EuiContextMenuItem>
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiPopover
|
||||||
|
id="transactionActionMenu"
|
||||||
|
button={<ActionMenuButton onClick={this.toggle} />}
|
||||||
|
isOpen={this.state.isOpen}
|
||||||
|
closePopover={this.close}
|
||||||
|
anchorPosition="downRight"
|
||||||
|
panelPaddingSize="none"
|
||||||
|
>
|
||||||
|
<EuiContextMenuPanel items={items} title="Actions" />
|
||||||
|
</EuiPopover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,164 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { get } from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { toQuery, fromQuery, history } from '../../../../../utils/url';
|
|
||||||
import SpanDetails from './SpanDetails';
|
|
||||||
import Modal from '../../../../shared/Modal';
|
|
||||||
|
|
||||||
import {
|
|
||||||
unit,
|
|
||||||
units,
|
|
||||||
colors,
|
|
||||||
px,
|
|
||||||
fontFamilyCode,
|
|
||||||
fontSizes
|
|
||||||
} from '../../../../../style/variables';
|
|
||||||
import {
|
|
||||||
SPAN_DURATION,
|
|
||||||
SPAN_START,
|
|
||||||
SPAN_ID,
|
|
||||||
SPAN_NAME
|
|
||||||
} from '../../../../../../common/constants';
|
|
||||||
|
|
||||||
const SpanBar = styled.div`
|
|
||||||
position: relative;
|
|
||||||
height: ${unit}px;
|
|
||||||
`;
|
|
||||||
const SpanLabel = styled.div`
|
|
||||||
white-space: nowrap;
|
|
||||||
position: relative;
|
|
||||||
direction: rtl;
|
|
||||||
text-align: left;
|
|
||||||
margin: ${px(units.quarter)} 0 0;
|
|
||||||
font-family: ${fontFamilyCode};
|
|
||||||
font-size: ${fontSizes.small};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
user-select: none;
|
|
||||||
padding: ${px(units.half)} ${props => px(props.timelineMargins.right)}
|
|
||||||
${px(units.eighth)} ${props => px(props.timelineMargins.left)};
|
|
||||||
border-top: 1px solid ${colors.gray4};
|
|
||||||
background-color: ${props => (props.isSelected ? colors.gray5 : 'initial')};
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
background-color: ${colors.gray5};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function getLocationPath(location) {
|
|
||||||
return location.href.split('?')[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
class Span extends React.Component {
|
|
||||||
componentDidMount() {
|
|
||||||
this.locationPath = getLocationPath(window.location);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose = () => {
|
|
||||||
// Hack: If the modal is open, and the user clicks the back button, the url changes, the modal will be destroyed,
|
|
||||||
// and the onClose handler will fire, causing it to change the url again. We want to avoid the url changing the second time.
|
|
||||||
// Therefore we
|
|
||||||
const currentLocationPath = getLocationPath(window.location);
|
|
||||||
const didNavigate = this.locationPath !== currentLocationPath;
|
|
||||||
if (!didNavigate) {
|
|
||||||
this.resetSpanId();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
resetSpanId = () => {
|
|
||||||
const { location } = this.props;
|
|
||||||
const { spanId, ...currentQuery } = toQuery(location.search);
|
|
||||||
|
|
||||||
if (spanId === 'null') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
history.replace({
|
|
||||||
...location,
|
|
||||||
search: fromQuery({
|
|
||||||
...currentQuery,
|
|
||||||
spanId: null
|
|
||||||
})
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
timelineMargins,
|
|
||||||
totalDuration,
|
|
||||||
span,
|
|
||||||
spanTypeLabel,
|
|
||||||
color,
|
|
||||||
isSelected,
|
|
||||||
transactionId,
|
|
||||||
location
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const width = (get({ span }, SPAN_DURATION) / totalDuration) * 100;
|
|
||||||
const left = (get({ span }, SPAN_START) / totalDuration) * 100;
|
|
||||||
|
|
||||||
const spanId = get({ span }, SPAN_ID);
|
|
||||||
const spanName = get({ span }, SPAN_NAME);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container
|
|
||||||
onClick={() => {
|
|
||||||
history.replace({
|
|
||||||
...location,
|
|
||||||
search: fromQuery({
|
|
||||||
...toQuery(location.search),
|
|
||||||
spanId
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
timelineMargins={timelineMargins}
|
|
||||||
isSelected={isSelected}
|
|
||||||
>
|
|
||||||
<SpanBar
|
|
||||||
style={{
|
|
||||||
left: `${left}%`,
|
|
||||||
width: `${width}%`,
|
|
||||||
backgroundColor: color
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SpanLabel style={{ left: `${left}%`, width: `${100 - left}%` }}>
|
|
||||||
‎
|
|
||||||
{spanName}
|
|
||||||
‎
|
|
||||||
</SpanLabel>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
header="Span details"
|
|
||||||
isOpen={isSelected}
|
|
||||||
onClose={this.onClose}
|
|
||||||
close={this.onClose}
|
|
||||||
>
|
|
||||||
<SpanDetails
|
|
||||||
span={span}
|
|
||||||
spanTypeLabel={spanTypeLabel}
|
|
||||||
spanTypeColor={color}
|
|
||||||
totalDuration={totalDuration}
|
|
||||||
transactionId={transactionId}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Span.propTypes = {
|
|
||||||
location: PropTypes.object.isRequired,
|
|
||||||
totalDuration: PropTypes.number.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Span;
|
|
|
@ -1,205 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import numeral from '@elastic/numeral';
|
|
||||||
import { get } from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import Stacktrace from '../../../../../shared/Stacktrace';
|
|
||||||
import DiscoverButton from '../../../../../shared/DiscoverButton';
|
|
||||||
import { asMillis } from '../../../../../../utils/formatters';
|
|
||||||
import { Indicator } from '../../../../../shared/charts/Legend';
|
|
||||||
import {
|
|
||||||
SPAN_DURATION,
|
|
||||||
SPAN_NAME,
|
|
||||||
SERVICE_LANGUAGE_NAME
|
|
||||||
} from '../../../../../../../common/constants';
|
|
||||||
import {
|
|
||||||
unit,
|
|
||||||
units,
|
|
||||||
px,
|
|
||||||
colors,
|
|
||||||
borderRadius,
|
|
||||||
fontFamilyCode,
|
|
||||||
fontSizes,
|
|
||||||
truncate
|
|
||||||
} from '../../../../../../style/variables';
|
|
||||||
import TooltipOverlay, {
|
|
||||||
fieldNameHelper
|
|
||||||
} from '../../../../../shared/TooltipOverlay';
|
|
||||||
|
|
||||||
import SyntaxHighlighter, {
|
|
||||||
registerLanguage
|
|
||||||
} from 'react-syntax-highlighter/dist/light';
|
|
||||||
import { xcode } from 'react-syntax-highlighter/dist/styles';
|
|
||||||
|
|
||||||
import sql from 'react-syntax-highlighter/dist/languages/sql';
|
|
||||||
import { HeaderXSmall } from '../../../../../shared/UIComponents';
|
|
||||||
|
|
||||||
registerLanguage('sql', sql);
|
|
||||||
|
|
||||||
const DetailsWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-end;
|
|
||||||
border-bottom: 1px solid ${colors.gray4};
|
|
||||||
padding: ${px(unit)} 0;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DetailsElement = styled.div`
|
|
||||||
min-width: 0;
|
|
||||||
max-width: 50%;
|
|
||||||
line-height: 1.5;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DetailsHeader = styled.div`
|
|
||||||
font-size: ${fontSizes.small};
|
|
||||||
color: ${colors.gray3};
|
|
||||||
|
|
||||||
span {
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DetailsText = styled.div`
|
|
||||||
font-size: ${fontSizes.large};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SpanName = styled.div`
|
|
||||||
${truncate('100%')};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const LegendIndicator = styled(Indicator)`
|
|
||||||
display: inline-block;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StackTraceContainer = styled.div`
|
|
||||||
margin-top: ${px(unit)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DatabaseStatement = styled.div`
|
|
||||||
margin-top: ${px(unit)};
|
|
||||||
padding: ${px(units.half)} ${px(unit)};
|
|
||||||
background: ${colors.yellow};
|
|
||||||
border-radius: ${borderRadius};
|
|
||||||
border: 1px solid ${colors.gray4};
|
|
||||||
font-family: ${fontFamilyCode};
|
|
||||||
`;
|
|
||||||
|
|
||||||
function SpanDetails({ span, spanTypeLabel, spanTypeColor, totalDuration }) {
|
|
||||||
const spanDocId = get(span, 'docId');
|
|
||||||
const spanDuration = get({ span }, SPAN_DURATION);
|
|
||||||
const relativeDuration = spanDuration / totalDuration;
|
|
||||||
const spanName = get({ span }, SPAN_NAME);
|
|
||||||
const stackframes = span.stacktrace;
|
|
||||||
const codeLanguage = get(span, SERVICE_LANGUAGE_NAME);
|
|
||||||
const dbContext = get(span, 'context.db');
|
|
||||||
|
|
||||||
const discoverQuery = {
|
|
||||||
_a: {
|
|
||||||
interval: 'auto',
|
|
||||||
query: {
|
|
||||||
language: 'lucene',
|
|
||||||
query: `_id:${spanDocId}`
|
|
||||||
},
|
|
||||||
sort: { '@timestamp': 'desc' }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<DetailsWrapper>
|
|
||||||
<DetailsElement>
|
|
||||||
<DetailsHeader>
|
|
||||||
<TooltipOverlay content={fieldNameHelper('span.name')}>
|
|
||||||
<span>Name</span>
|
|
||||||
</TooltipOverlay>
|
|
||||||
</DetailsHeader>
|
|
||||||
<DetailsText>
|
|
||||||
<TooltipOverlay content={`${spanName || 'N/A'}`}>
|
|
||||||
<SpanName>{spanName || 'N/A'}</SpanName>
|
|
||||||
</TooltipOverlay>
|
|
||||||
</DetailsText>
|
|
||||||
</DetailsElement>
|
|
||||||
<DetailsElement>
|
|
||||||
<DetailsHeader>
|
|
||||||
<TooltipOverlay content={fieldNameHelper('span.type')}>
|
|
||||||
<span>Type</span>
|
|
||||||
</TooltipOverlay>
|
|
||||||
</DetailsHeader>
|
|
||||||
<DetailsText>
|
|
||||||
<LegendIndicator radius={units.minus - 1} color={spanTypeColor} />
|
|
||||||
{spanTypeLabel}
|
|
||||||
</DetailsText>
|
|
||||||
</DetailsElement>
|
|
||||||
<DetailsElement>
|
|
||||||
<DetailsHeader>
|
|
||||||
<TooltipOverlay content={fieldNameHelper('span.duration.us')}>
|
|
||||||
<span>Duration</span>
|
|
||||||
</TooltipOverlay>
|
|
||||||
</DetailsHeader>
|
|
||||||
<DetailsText>{asMillis(spanDuration)}</DetailsText>
|
|
||||||
</DetailsElement>
|
|
||||||
<DetailsElement>
|
|
||||||
<DetailsHeader>% of total time</DetailsHeader>
|
|
||||||
<DetailsText>{numeral(relativeDuration).format('0.00%')}</DetailsText>
|
|
||||||
</DetailsElement>
|
|
||||||
<DetailsElement>
|
|
||||||
<DiscoverButton query={discoverQuery}>
|
|
||||||
{`View span in Discover`}
|
|
||||||
</DiscoverButton>
|
|
||||||
</DetailsElement>
|
|
||||||
</DetailsWrapper>
|
|
||||||
|
|
||||||
<DatabaseContext dbContext={dbContext} />
|
|
||||||
|
|
||||||
<StackTraceContainer>
|
|
||||||
<Stacktrace stackframes={stackframes} codeLanguage={codeLanguage} />
|
|
||||||
</StackTraceContainer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DatabaseContext({ dbContext }) {
|
|
||||||
if (!dbContext || !dbContext.statement) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dbContext.type !== 'sql') {
|
|
||||||
return <DatabaseStatement>{dbContext.statement}</DatabaseStatement>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<HeaderXSmall>DB Statement</HeaderXSmall>
|
|
||||||
<DatabaseStatement>
|
|
||||||
<SyntaxHighlighter
|
|
||||||
language={'sql'}
|
|
||||||
style={xcode}
|
|
||||||
customStyle={{
|
|
||||||
color: null,
|
|
||||||
background: null,
|
|
||||||
padding: null,
|
|
||||||
lineHeight: px(unit * 1.5),
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
overflowX: 'scroll'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{dbContext.statement}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
</DatabaseStatement>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SpanDetails.propTypes = {
|
|
||||||
span: PropTypes.object.isRequired,
|
|
||||||
totalDuration: PropTypes.number.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SpanDetails;
|
|
|
@ -1,58 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import Legend from '../../../../shared/charts/Legend';
|
|
||||||
import {
|
|
||||||
fontSizes,
|
|
||||||
colors,
|
|
||||||
unit,
|
|
||||||
units,
|
|
||||||
px,
|
|
||||||
truncate
|
|
||||||
} from '../../../../../style/variables';
|
|
||||||
|
|
||||||
import TooltipOverlay from '../../../../shared/TooltipOverlay';
|
|
||||||
|
|
||||||
const TimelineHeaderContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: ${px(unit * 1.5)} ${px(units.plus)} 0 ${px(units.plus)};
|
|
||||||
line-height: 1.5;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Heading = styled.div`
|
|
||||||
font-size: ${fontSizes.large};
|
|
||||||
color: ${colors.gray2};
|
|
||||||
${truncate('90%')};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Legends = styled.div`
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
div {
|
|
||||||
margin-right: ${px(unit)};
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function TimelineHeader({ legends, transactionName }) {
|
|
||||||
return (
|
|
||||||
<TimelineHeaderContainer>
|
|
||||||
<TooltipOverlay content={transactionName || 'N/A'}>
|
|
||||||
<Heading>{transactionName || 'N/A'}</Heading>
|
|
||||||
</TooltipOverlay>
|
|
||||||
<Legends>
|
|
||||||
{legends.map(({ color, label }) => (
|
|
||||||
<Legend key={color} color={color} text={label} />
|
|
||||||
))}
|
|
||||||
</Legends>
|
|
||||||
</TimelineHeaderContainer>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import Spans from './view';
|
|
||||||
import { getUrlParams } from '../../../../../store/urlParams';
|
|
||||||
|
|
||||||
function mapStateToProps(state = {}) {
|
|
||||||
return {
|
|
||||||
urlParams: getUrlParams(state),
|
|
||||||
location: state.location
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {};
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(Spans);
|
|
|
@ -1,193 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { PureComponent } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { get, uniq, first, zipObject, difference, isEmpty } from 'lodash';
|
|
||||||
import Span from './Span';
|
|
||||||
import TimelineHeader from './TimelineHeader';
|
|
||||||
import { SPAN_ID } from '../../../../../../common/constants';
|
|
||||||
import { colors } from '../../../../../style/variables';
|
|
||||||
import { StickyContainer } from 'react-sticky';
|
|
||||||
import Timeline from '../../../../shared/charts/Timeline';
|
|
||||||
import EmptyMessage from '../../../../shared/EmptyMessage';
|
|
||||||
import { getFeatureDocs } from '../../../../../utils/documentation';
|
|
||||||
import { ExternalLink } from '../../../../../utils/url';
|
|
||||||
import { SpansRequest } from '../../../../../store/reactReduxRequest/spans';
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
transition: 0.1s padding ease;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DroppedSpansContainer = styled.div`
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
height: 43px;
|
|
||||||
line-height: 43px;
|
|
||||||
text-align: center;
|
|
||||||
color: ${colors.gray2};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TIMELINE_HEADER_HEIGHT = 100;
|
|
||||||
const TIMELINE_MARGINS = {
|
|
||||||
top: TIMELINE_HEADER_HEIGHT,
|
|
||||||
left: 50,
|
|
||||||
right: 50,
|
|
||||||
bottom: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
class Spans extends PureComponent {
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
agentName,
|
|
||||||
urlParams,
|
|
||||||
location,
|
|
||||||
droppedSpans,
|
|
||||||
agentMarks
|
|
||||||
} = this.props;
|
|
||||||
return (
|
|
||||||
<SpansRequest
|
|
||||||
urlParams={urlParams}
|
|
||||||
render={spans => {
|
|
||||||
if (isEmpty(spans.data.spans)) {
|
|
||||||
return (
|
|
||||||
<EmptyMessage
|
|
||||||
heading="No spans available for this transaction."
|
|
||||||
hideSubheading
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const spanTypes = uniq(
|
|
||||||
spans.data.spanTypes.map(({ type }) => getPrimaryType(type))
|
|
||||||
);
|
|
||||||
|
|
||||||
const getSpanColor = getColorByType(spanTypes);
|
|
||||||
|
|
||||||
const totalDuration = spans.data.duration;
|
|
||||||
const spanContainerHeight = 58;
|
|
||||||
const timelineHeight = spanContainerHeight * spans.data.spans.length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Container>
|
|
||||||
<StickyContainer>
|
|
||||||
<Timeline
|
|
||||||
header={
|
|
||||||
<TimelineHeader
|
|
||||||
legends={spanTypes.map(type => ({
|
|
||||||
label: getSpanLabel(type),
|
|
||||||
color: getSpanColor(type)
|
|
||||||
}))}
|
|
||||||
transactionName={urlParams.transactionName}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
agentMarks={agentMarks}
|
|
||||||
duration={totalDuration}
|
|
||||||
height={timelineHeight}
|
|
||||||
margins={TIMELINE_MARGINS}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
paddingTop: TIMELINE_MARGINS.top
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{spans.data.spans.map(span => (
|
|
||||||
<Span
|
|
||||||
location={location}
|
|
||||||
timelineMargins={TIMELINE_MARGINS}
|
|
||||||
key={get({ span }, SPAN_ID)}
|
|
||||||
color={getSpanColor(getPrimaryType(span.type))}
|
|
||||||
span={span}
|
|
||||||
spanTypeLabel={getSpanLabel(getPrimaryType(span.type))}
|
|
||||||
totalDuration={totalDuration}
|
|
||||||
isSelected={get({ span }, SPAN_ID) === urlParams.spanId}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</StickyContainer>
|
|
||||||
</Container>
|
|
||||||
|
|
||||||
{droppedSpans > 0 && (
|
|
||||||
<DroppedSpansContainer>
|
|
||||||
{droppedSpans} spans dropped due to limit of{' '}
|
|
||||||
{spans.data.spans.length}.{' '}
|
|
||||||
<DroppedSpansDocsLink agentName={agentName} />
|
|
||||||
</DroppedSpansContainer>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function DroppedSpansDocsLink({ agentName }) {
|
|
||||||
const docs = getFeatureDocs('dropped-spans', agentName);
|
|
||||||
|
|
||||||
if (!docs || !docs.url) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ExternalLink href={docs.url}>
|
|
||||||
Learn more in the documentation.
|
|
||||||
</ExternalLink>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getColorByType(types) {
|
|
||||||
const assignedColors = {
|
|
||||||
app: colors.apmBlue,
|
|
||||||
cache: colors.apmGreen,
|
|
||||||
components: colors.apmGreen,
|
|
||||||
ext: colors.apmPurple,
|
|
||||||
xhr: colors.apmPurple,
|
|
||||||
template: colors.apmRed2,
|
|
||||||
resource: colors.apmRed2,
|
|
||||||
custom: colors.apmTan,
|
|
||||||
db: colors.apmOrange,
|
|
||||||
'hard-navigation': colors.apmYellow
|
|
||||||
};
|
|
||||||
|
|
||||||
const unknownTypes = difference(types, Object.keys(assignedColors));
|
|
||||||
const unassignedColors = zipObject(unknownTypes, [
|
|
||||||
colors.apmYellow,
|
|
||||||
colors.apmRed,
|
|
||||||
colors.apmBrown,
|
|
||||||
colors.apmPink
|
|
||||||
]);
|
|
||||||
|
|
||||||
return type => assignedColors[type] || unassignedColors[type];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSpanLabel(type) {
|
|
||||||
switch (type) {
|
|
||||||
case 'db':
|
|
||||||
return 'DB';
|
|
||||||
case 'hard-navigation':
|
|
||||||
return 'Navigation timing';
|
|
||||||
default:
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPrimaryType(type) {
|
|
||||||
return first(type.split('.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
Spans.propTypes = {
|
|
||||||
agentMarks: PropTypes.array,
|
|
||||||
agentName: PropTypes.string.isRequired,
|
|
||||||
droppedSpans: PropTypes.number.isRequired,
|
|
||||||
location: PropTypes.object.isRequired,
|
|
||||||
urlParams: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Spans;
|
|
|
@ -1,57 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { get } from 'lodash';
|
|
||||||
import { StickyProperties } from '../../../shared/StickyProperties';
|
|
||||||
import {
|
|
||||||
TRANSACTION_DURATION,
|
|
||||||
TRANSACTION_RESULT,
|
|
||||||
USER_ID,
|
|
||||||
REQUEST_URL_FULL
|
|
||||||
} from '../../../../../common/constants';
|
|
||||||
import { asTime } from '../../../../utils/formatters';
|
|
||||||
|
|
||||||
export default function StickyTransactionProperties({ transaction }) {
|
|
||||||
const timestamp = get(transaction, '@timestamp');
|
|
||||||
const url = get(transaction, REQUEST_URL_FULL, 'N/A');
|
|
||||||
const duration = get(transaction, TRANSACTION_DURATION);
|
|
||||||
const stickyProperties = [
|
|
||||||
{
|
|
||||||
label: 'Timestamp',
|
|
||||||
fieldName: '@timestamp',
|
|
||||||
val: timestamp
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: REQUEST_URL_FULL,
|
|
||||||
label: 'URL',
|
|
||||||
val: url,
|
|
||||||
truncated: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Duration',
|
|
||||||
fieldName: TRANSACTION_DURATION,
|
|
||||||
val: duration ? asTime(duration) : 'N/A'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Result',
|
|
||||||
fieldName: TRANSACTION_RESULT,
|
|
||||||
val: get(transaction, TRANSACTION_RESULT, 'N/A')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'User ID',
|
|
||||||
fieldName: USER_ID,
|
|
||||||
val: get(transaction, USER_ID, 'N/A')
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return <StickyProperties stickyProperties={stickyProperties} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
StickyTransactionProperties.propTypes = {
|
|
||||||
transaction: PropTypes.object.isRequired
|
|
||||||
};
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { selectWaterfallRoot } from 'x-pack/plugins/apm/public/store/selectors/waterfall';
|
||||||
|
import {
|
||||||
|
REQUEST_URL_FULL,
|
||||||
|
TRANSACTION_DURATION,
|
||||||
|
TRANSACTION_RESULT,
|
||||||
|
USER_ID
|
||||||
|
} from '../../../../../common/constants';
|
||||||
|
import { Transaction } from '../../../../../typings/Transaction';
|
||||||
|
// @ts-ignore
|
||||||
|
import { asTime } from '../../../../utils/formatters';
|
||||||
|
// @ts-ignore
|
||||||
|
import { StickyProperties } from '../../../shared/StickyProperties';
|
||||||
|
|
||||||
|
function getDurationPercent(
|
||||||
|
transactionDuration: number,
|
||||||
|
rootDuration?: number
|
||||||
|
) {
|
||||||
|
if (rootDuration === undefined || rootDuration === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return ((transactionDuration / rootDuration) * 100).toFixed(2) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
transaction: Transaction;
|
||||||
|
root?: Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StickyTransactionPropertiesComponent({
|
||||||
|
transaction,
|
||||||
|
root
|
||||||
|
}: Props) {
|
||||||
|
const timestamp = get(transaction, '@timestamp');
|
||||||
|
const url = get(transaction, REQUEST_URL_FULL, 'N/A');
|
||||||
|
const duration = transaction.transaction.duration.us;
|
||||||
|
const rootDuration = root && root.transaction.duration.us;
|
||||||
|
const stickyProperties = [
|
||||||
|
{
|
||||||
|
label: 'Timestamp',
|
||||||
|
fieldName: '@timestamp',
|
||||||
|
val: timestamp,
|
||||||
|
truncated: true,
|
||||||
|
width: '50%'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: REQUEST_URL_FULL,
|
||||||
|
label: 'URL',
|
||||||
|
val: url,
|
||||||
|
truncated: true,
|
||||||
|
width: '50%'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Duration',
|
||||||
|
fieldName: TRANSACTION_DURATION,
|
||||||
|
val: duration ? asTime(duration) : 'N/A',
|
||||||
|
width: '25%'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '% of trace',
|
||||||
|
val: getDurationPercent(duration, rootDuration),
|
||||||
|
width: '25%'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Result',
|
||||||
|
fieldName: TRANSACTION_RESULT,
|
||||||
|
val: get(transaction, TRANSACTION_RESULT, 'N/A'),
|
||||||
|
width: '25%'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'User ID',
|
||||||
|
fieldName: USER_ID,
|
||||||
|
val: get(transaction, USER_ID, 'N/A'),
|
||||||
|
truncated: true,
|
||||||
|
width: '25%'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return <StickyProperties stickyProperties={stickyProperties} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: any, props: Partial<Props>) => ({
|
||||||
|
root: selectWaterfallRoot(state, props)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const StickyTransactionProperties = connect<{}, {}, Props>(
|
||||||
|
mapStateToProps
|
||||||
|
)(StickyTransactionPropertiesComponent);
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
EuiSpacer,
|
||||||
|
// @ts-ignore
|
||||||
|
EuiTab,
|
||||||
|
// @ts-ignore
|
||||||
|
EuiTabs
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { capitalize, first, get } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Transaction } from '../../../../../typings/Transaction';
|
||||||
|
import { IUrlParams } from '../../../../store/urlParams';
|
||||||
|
import { px, units } from '../../../../style/variables';
|
||||||
|
import { fromQuery, history, toQuery } from '../../../../utils/url';
|
||||||
|
import {
|
||||||
|
getPropertyTabNames,
|
||||||
|
PropertiesTable
|
||||||
|
} from '../../../shared/PropertiesTable';
|
||||||
|
import { WaterfallContainer } from './WaterfallContainer';
|
||||||
|
|
||||||
|
const TableContainer = styled.div`
|
||||||
|
padding: ${px(units.plus)} ${px(units.plus)} 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Ensure the selected tab exists or use the first
|
||||||
|
function getCurrentTab(tabs: string[] = [], selectedTab?: string) {
|
||||||
|
return selectedTab && tabs.includes(selectedTab) ? selectedTab : first(tabs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIMELINE_TAB = 'timeline';
|
||||||
|
|
||||||
|
function getTabs(transactionData: Transaction) {
|
||||||
|
const dynamicProps = Object.keys(transactionData.context || {});
|
||||||
|
return [TIMELINE_TAB, ...getPropertyTabNames(dynamicProps)];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransactionPropertiesTableProps {
|
||||||
|
location: any;
|
||||||
|
transaction: Transaction;
|
||||||
|
urlParams: IUrlParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TransactionPropertiesTable: React.SFC<
|
||||||
|
TransactionPropertiesTableProps
|
||||||
|
> = ({ location, transaction, urlParams }) => {
|
||||||
|
const tabs = getTabs(transaction);
|
||||||
|
const currentTab = getCurrentTab(tabs, urlParams.detailTab);
|
||||||
|
const agentName = transaction.context.service.agent.name;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<EuiTabs>
|
||||||
|
{tabs.map(key => {
|
||||||
|
return (
|
||||||
|
<EuiTab
|
||||||
|
onClick={() => {
|
||||||
|
history.replace({
|
||||||
|
...location,
|
||||||
|
search: fromQuery({
|
||||||
|
...toQuery(location.search),
|
||||||
|
detailTab: key
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
selected={currentTab === key}
|
||||||
|
key={key}
|
||||||
|
>
|
||||||
|
{capitalize(key)}
|
||||||
|
</EuiTab>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</EuiTabs>
|
||||||
|
|
||||||
|
<EuiSpacer />
|
||||||
|
|
||||||
|
{currentTab === TIMELINE_TAB && (
|
||||||
|
<WaterfallContainer
|
||||||
|
transaction={transaction}
|
||||||
|
location={location}
|
||||||
|
urlParams={urlParams}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentTab !== TIMELINE_TAB && (
|
||||||
|
<TableContainer>
|
||||||
|
<PropertiesTable
|
||||||
|
propData={get(transaction.context, currentTab)}
|
||||||
|
propKey={currentTab}
|
||||||
|
agentName={agentName}
|
||||||
|
/>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
|
||||||
|
import { capitalize, first, get } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import { Transaction } from '../../../../../typings/Transaction';
|
||||||
|
import { IUrlParams } from '../../../../store/urlParams';
|
||||||
|
// @ts-ignore
|
||||||
|
import { fromQuery, history, toQuery } from '../../../../utils/url';
|
||||||
|
import {
|
||||||
|
getPropertyTabNames,
|
||||||
|
PropertiesTable
|
||||||
|
} from '../../../shared/PropertiesTable';
|
||||||
|
// @ts-ignore
|
||||||
|
import { Tab } from '../../../shared/UIComponents';
|
||||||
|
|
||||||
|
// Ensure the selected tab exists or use the first
|
||||||
|
function getCurrentTab(tabs: string[] = [], selectedTab?: string) {
|
||||||
|
return selectedTab && tabs.includes(selectedTab) ? selectedTab : first(tabs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTabs(transactionData: Transaction) {
|
||||||
|
const dynamicProps = Object.keys(transactionData.context || {});
|
||||||
|
return getPropertyTabNames(dynamicProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
location: any;
|
||||||
|
transaction: Transaction;
|
||||||
|
urlParams: IUrlParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TransactionPropertiesTableForFlyout: React.SFC<Props> = ({
|
||||||
|
location,
|
||||||
|
transaction,
|
||||||
|
urlParams
|
||||||
|
}) => {
|
||||||
|
const tabs = getTabs(transaction);
|
||||||
|
const currentTab = getCurrentTab(tabs, urlParams.flyoutDetailTab);
|
||||||
|
const agentName = transaction.context.service.agent.name;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<EuiTabs>
|
||||||
|
{tabs.map(key => {
|
||||||
|
return (
|
||||||
|
<EuiTab
|
||||||
|
onClick={() => {
|
||||||
|
history.replace({
|
||||||
|
...location,
|
||||||
|
search: fromQuery({
|
||||||
|
...toQuery(location.search),
|
||||||
|
flyoutDetailTab: key
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
isSelected={currentTab === key}
|
||||||
|
key={key}
|
||||||
|
>
|
||||||
|
{capitalize(key)}
|
||||||
|
</EuiTab>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</EuiTabs>
|
||||||
|
<EuiSpacer />
|
||||||
|
<PropertiesTable
|
||||||
|
propData={get(transaction.context, currentTab)}
|
||||||
|
propKey={currentTab}
|
||||||
|
agentName={agentName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { px, unit } from '../../../../../style/variables';
|
||||||
|
// @ts-ignore
|
||||||
|
import Legend from '../../../../shared/charts/Legend';
|
||||||
|
|
||||||
|
const Legends = styled.div`
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
div {
|
||||||
|
margin-right: ${px(unit)};
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serviceColors: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ServiceLegends({ serviceColors }: Props) {
|
||||||
|
return (
|
||||||
|
<Legends>
|
||||||
|
{Object.entries(serviceColors).map(([label, color]) => (
|
||||||
|
<Legend key={color} color={color} text={label} />
|
||||||
|
))}
|
||||||
|
</Legends>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
SERVICE_NAME,
|
||||||
|
TRANSACTION_NAME
|
||||||
|
} from 'x-pack/plugins/apm/common/constants';
|
||||||
|
import {
|
||||||
|
KibanaLink,
|
||||||
|
legacyEncodeURIComponent
|
||||||
|
// @ts-ignore
|
||||||
|
} from 'x-pack/plugins/apm/public/utils/url';
|
||||||
|
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||||
|
// @ts-ignore
|
||||||
|
import { StickyProperties } from '../../../../../shared/StickyProperties';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
transaction: Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FlyoutTopLevelProperties({ transaction }: Props) {
|
||||||
|
const stickyProperties = [
|
||||||
|
{
|
||||||
|
label: 'Service',
|
||||||
|
fieldName: SERVICE_NAME,
|
||||||
|
val: (
|
||||||
|
<KibanaLink
|
||||||
|
pathname={'/app/apm'}
|
||||||
|
hash={`/${transaction.context.service.name}`}
|
||||||
|
>
|
||||||
|
{transaction.context.service.name}
|
||||||
|
</KibanaLink>
|
||||||
|
),
|
||||||
|
width: '50%'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Transaction',
|
||||||
|
fieldName: TRANSACTION_NAME,
|
||||||
|
val: (
|
||||||
|
<KibanaLink
|
||||||
|
pathname={'/app/apm'}
|
||||||
|
hash={`/${transaction.context.service.name}/transactions/${
|
||||||
|
transaction.transaction.type
|
||||||
|
}/${legacyEncodeURIComponent(transaction.transaction.name)}`}
|
||||||
|
>
|
||||||
|
{transaction.transaction.name}
|
||||||
|
</KibanaLink>
|
||||||
|
),
|
||||||
|
width: '50%'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return <StickyProperties stickyProperties={stickyProperties} />;
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import {
|
||||||
|
borderRadius,
|
||||||
|
colors,
|
||||||
|
fontFamilyCode,
|
||||||
|
px,
|
||||||
|
unit,
|
||||||
|
units
|
||||||
|
} from '../../../../../../../style/variables';
|
||||||
|
|
||||||
|
import SyntaxHighlighter, {
|
||||||
|
registerLanguage
|
||||||
|
// @ts-ignore
|
||||||
|
} from 'react-syntax-highlighter/dist/light';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { xcode } from 'react-syntax-highlighter/dist/styles';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import sql from 'react-syntax-highlighter/dist/languages/sql';
|
||||||
|
|
||||||
|
import { EuiTitle } from '@elastic/eui';
|
||||||
|
import { DbContext } from '../../../../../../../../typings/Span';
|
||||||
|
|
||||||
|
registerLanguage('sql', sql);
|
||||||
|
|
||||||
|
const DatabaseStatement = styled.div`
|
||||||
|
margin-top: ${px(unit)};
|
||||||
|
padding: ${px(units.half)} ${px(unit)};
|
||||||
|
background: ${colors.yellow};
|
||||||
|
border-radius: ${borderRadius};
|
||||||
|
border: 1px solid ${colors.gray4};
|
||||||
|
font-family: ${fontFamilyCode};
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
dbContext?: DbContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DatabaseContext({ dbContext }: Props) {
|
||||||
|
if (!dbContext || !dbContext.statement) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbContext.type !== 'sql') {
|
||||||
|
return <DatabaseStatement>{dbContext.statement}</DatabaseStatement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<EuiTitle size="xs">
|
||||||
|
<h3>Database statement</h3>
|
||||||
|
</EuiTitle>
|
||||||
|
<DatabaseStatement>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
language={'sql'}
|
||||||
|
style={xcode}
|
||||||
|
customStyle={{
|
||||||
|
color: null,
|
||||||
|
background: null,
|
||||||
|
padding: null,
|
||||||
|
lineHeight: px(unit * 1.5),
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
overflowX: 'scroll'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{dbContext.statement}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</DatabaseStatement>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// tslint:disable-next-line no-var-requires
|
||||||
|
const numeral = require('@elastic/numeral');
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { first } from 'lodash';
|
||||||
|
import { Span } from '../../../../../../../../typings/Span';
|
||||||
|
// @ts-ignore
|
||||||
|
import { asMillis } from '../../../../../../../utils/formatters';
|
||||||
|
// @ts-ignore
|
||||||
|
import { StickyProperties } from '../../../../../../shared/StickyProperties';
|
||||||
|
|
||||||
|
function getSpanLabel(type: string) {
|
||||||
|
switch (type) {
|
||||||
|
case 'db':
|
||||||
|
return 'DB';
|
||||||
|
case 'hard-navigation':
|
||||||
|
return 'Navigation timing';
|
||||||
|
default:
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrimaryType(type: string) {
|
||||||
|
return first(type.split('.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
span: Span;
|
||||||
|
totalDuration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StickySpanProperties({ span, totalDuration }: Props) {
|
||||||
|
const spanName = span.span.name;
|
||||||
|
const spanDuration = span.span.duration.us;
|
||||||
|
const relativeDuration = spanDuration / totalDuration;
|
||||||
|
const spanTypeLabel = getSpanLabel(getPrimaryType(span.span.type));
|
||||||
|
|
||||||
|
const stickyProperties = [
|
||||||
|
{
|
||||||
|
label: 'Name',
|
||||||
|
fieldName: 'span.name',
|
||||||
|
val: spanName || 'N/A'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'span.type',
|
||||||
|
label: 'Type',
|
||||||
|
val: spanTypeLabel
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'span.duration.us',
|
||||||
|
label: 'Duration',
|
||||||
|
val: asMillis(spanDuration)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '% of transaction',
|
||||||
|
val: numeral(relativeDuration).format('0.00%')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return <StickyProperties stickyProperties={stickyProperties} />;
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiFlyout,
|
||||||
|
EuiFlyoutBody,
|
||||||
|
EuiFlyoutHeader,
|
||||||
|
EuiHorizontalRule,
|
||||||
|
EuiTitle
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { SERVICE_LANGUAGE_NAME } from '../../../../../../../../common/constants';
|
||||||
|
import { px, unit } from '../../../../../../../style/variables';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import Stacktrace from '../../../../../../shared/Stacktrace';
|
||||||
|
|
||||||
|
import { DatabaseContext } from './DatabaseContext';
|
||||||
|
import { StickySpanProperties } from './StickySpanProperties';
|
||||||
|
|
||||||
|
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||||
|
import { Span } from '../../../../../../../../typings/Span';
|
||||||
|
// @ts-ignore
|
||||||
|
import DiscoverButton from '../../../../../../shared/DiscoverButton';
|
||||||
|
import { FlyoutTopLevelProperties } from '../FlyoutTopLevelProperties';
|
||||||
|
|
||||||
|
const StackTraceContainer = styled.div`
|
||||||
|
margin-top: ${px(unit)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
function getDiscoverQuery(span: Span) {
|
||||||
|
return {
|
||||||
|
_a: {
|
||||||
|
interval: 'auto',
|
||||||
|
query: {
|
||||||
|
language: 'lucene',
|
||||||
|
query:
|
||||||
|
span.version === 'v2'
|
||||||
|
? `span.hex_id:${span.span.hex_id}`
|
||||||
|
: `span.id:${span.span.id}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
span?: Span;
|
||||||
|
parentTransaction: Transaction;
|
||||||
|
totalDuration: number;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SpanFlyout({
|
||||||
|
span,
|
||||||
|
parentTransaction,
|
||||||
|
totalDuration,
|
||||||
|
onClose
|
||||||
|
}: Props) {
|
||||||
|
if (!span) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const stackframes = span.span.stacktrace;
|
||||||
|
const codeLanguage = get(span, SERVICE_LANGUAGE_NAME);
|
||||||
|
const dbContext = span.context.db;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiFlyout onClose={onClose} size="l">
|
||||||
|
<EuiFlyoutHeader hasBorder>
|
||||||
|
<EuiFlexGroup>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiTitle>
|
||||||
|
<h2>Span details</h2>
|
||||||
|
</EuiTitle>
|
||||||
|
</EuiFlexItem>
|
||||||
|
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<DiscoverButton query={getDiscoverQuery(span)}>
|
||||||
|
{`View span in Discover`}
|
||||||
|
</DiscoverButton>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFlyoutHeader>
|
||||||
|
<EuiFlyoutBody>
|
||||||
|
<FlyoutTopLevelProperties transaction={parentTransaction} />
|
||||||
|
<EuiHorizontalRule />
|
||||||
|
<StickySpanProperties span={span} totalDuration={totalDuration} />
|
||||||
|
<EuiHorizontalRule />
|
||||||
|
<DatabaseContext dbContext={dbContext} />
|
||||||
|
<StackTraceContainer>
|
||||||
|
<Stacktrace stackframes={stackframes} codeLanguage={codeLanguage} />
|
||||||
|
</StackTraceContainer>
|
||||||
|
</EuiFlyoutBody>
|
||||||
|
</EuiFlyout>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
EuiButton,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiFlyout,
|
||||||
|
EuiFlyoutBody,
|
||||||
|
EuiFlyoutHeader,
|
||||||
|
EuiHorizontalRule,
|
||||||
|
EuiTitle
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import React from 'react';
|
||||||
|
import { TraceLink } from 'x-pack/plugins/apm/public/components/shared/TraceLink';
|
||||||
|
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||||
|
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||||
|
import { ActionMenu } from '../../../ActionMenu';
|
||||||
|
import { StickyTransactionProperties } from '../../../StickyTransactionProperties';
|
||||||
|
import { TransactionPropertiesTableForFlyout } from '../../../TransactionPropertiesTableForFlyout';
|
||||||
|
import { FlyoutTopLevelProperties } from '../FlyoutTopLevelProperties';
|
||||||
|
import { IWaterfall } from '../waterfall_helpers/waterfall_helpers';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
transaction?: Transaction;
|
||||||
|
location: any; // TODO: import location type from react router or history types?
|
||||||
|
urlParams: IUrlParams;
|
||||||
|
waterfall: IWaterfall;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TransactionFlyout({
|
||||||
|
transaction: transactionDoc,
|
||||||
|
onClose,
|
||||||
|
location,
|
||||||
|
urlParams,
|
||||||
|
waterfall
|
||||||
|
}: Props) {
|
||||||
|
if (!transactionDoc) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiFlyout onClose={onClose} size="l">
|
||||||
|
<EuiFlyoutHeader hasBorder>
|
||||||
|
<EuiFlexGroup>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiTitle>
|
||||||
|
<h4>Transaction details</h4>
|
||||||
|
</EuiTitle>
|
||||||
|
</EuiFlexItem>
|
||||||
|
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<ActionMenu transaction={transactionDoc} />
|
||||||
|
</EuiFlexItem>
|
||||||
|
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<TraceLink transaction={transactionDoc}>
|
||||||
|
<EuiButton iconType="visLine">
|
||||||
|
View transaction group details
|
||||||
|
</EuiButton>
|
||||||
|
</TraceLink>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFlyoutHeader>
|
||||||
|
<EuiFlyoutBody>
|
||||||
|
<FlyoutTopLevelProperties transaction={transactionDoc} />
|
||||||
|
<EuiHorizontalRule />
|
||||||
|
<StickyTransactionProperties transaction={transactionDoc} />
|
||||||
|
<EuiHorizontalRule />
|
||||||
|
<TransactionPropertiesTableForFlyout
|
||||||
|
transaction={transactionDoc}
|
||||||
|
location={location}
|
||||||
|
urlParams={urlParams}
|
||||||
|
/>
|
||||||
|
</EuiFlyoutBody>
|
||||||
|
</EuiFlyout>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import {
|
||||||
|
colors,
|
||||||
|
fontFamilyCode,
|
||||||
|
fontSizes,
|
||||||
|
px,
|
||||||
|
unit,
|
||||||
|
units
|
||||||
|
} from '../../../../../../style/variables';
|
||||||
|
import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers';
|
||||||
|
|
||||||
|
const ItemBar = styled.div`
|
||||||
|
position: relative;
|
||||||
|
height: ${unit}px;
|
||||||
|
`;
|
||||||
|
const ItemLabel = styled.div`
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: left;
|
||||||
|
margin: ${px(units.quarter)} 0 0;
|
||||||
|
font-family: ${fontFamilyCode};
|
||||||
|
font-size: ${fontSizes.small};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Container = styled<
|
||||||
|
{ timelineMargins: TimelineMargins; isSelected: boolean },
|
||||||
|
'div'
|
||||||
|
>('div')`
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
user-select: none;
|
||||||
|
padding: ${px(units.half)} ${props => px(props.timelineMargins.right)}
|
||||||
|
${px(units.eighth)} ${props => px(props.timelineMargins.left)};
|
||||||
|
border-top: 1px solid ${colors.gray4};
|
||||||
|
background-color: ${props => (props.isSelected ? colors.gray5 : 'initial')};
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: ${colors.gray5};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface TimelineMargins {
|
||||||
|
right: number;
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
bottom: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
timelineMargins: TimelineMargins;
|
||||||
|
totalDuration: number;
|
||||||
|
item: IWaterfallItem;
|
||||||
|
color: string;
|
||||||
|
isSelected: boolean;
|
||||||
|
onClick: () => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WaterfallItem({
|
||||||
|
timelineMargins,
|
||||||
|
totalDuration,
|
||||||
|
item,
|
||||||
|
color,
|
||||||
|
isSelected,
|
||||||
|
onClick
|
||||||
|
}: Props) {
|
||||||
|
const width = (item.duration / totalDuration) * 100;
|
||||||
|
const left = (item.offset / totalDuration) * 100;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
onClick={onClick}
|
||||||
|
timelineMargins={timelineMargins}
|
||||||
|
isSelected={isSelected}
|
||||||
|
>
|
||||||
|
<ItemBar
|
||||||
|
style={{
|
||||||
|
left: `${left}%`,
|
||||||
|
width: `${width}%`,
|
||||||
|
minWidth: '2px',
|
||||||
|
backgroundColor: color
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ItemLabel
|
||||||
|
style={{
|
||||||
|
left: `${left}%`,
|
||||||
|
width: `${100 - left}%`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
‎
|
||||||
|
{item.name}
|
||||||
|
‎
|
||||||
|
</ItemLabel>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
// @ts-ignore
|
||||||
|
import { StickyContainer } from 'react-sticky';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { IUrlParams } from '../../../../../../store/urlParams';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { fromQuery, history, toQuery } from '../../../../../../utils/url';
|
||||||
|
// @ts-ignore
|
||||||
|
import Timeline from '../../../../../shared/charts/Timeline';
|
||||||
|
import { AgentMark } from '../get_agent_marks';
|
||||||
|
import { SpanFlyout } from './SpanFlyout';
|
||||||
|
import { TransactionFlyout } from './TransactionFlyout';
|
||||||
|
import {
|
||||||
|
IWaterfall,
|
||||||
|
IWaterfallItem
|
||||||
|
} from './waterfall_helpers/waterfall_helpers';
|
||||||
|
import { WaterfallItem } from './WaterfallItem';
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
transition: 0.1s padding ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TIMELINE_MARGINS = {
|
||||||
|
top: 40,
|
||||||
|
left: 50,
|
||||||
|
right: 50,
|
||||||
|
bottom: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
agentMarks: AgentMark[];
|
||||||
|
urlParams: IUrlParams;
|
||||||
|
waterfall: IWaterfall;
|
||||||
|
location: any;
|
||||||
|
serviceColors: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Waterfall extends Component<Props> {
|
||||||
|
public onOpenFlyout = (item: IWaterfallItem) => {
|
||||||
|
this.setQueryParams({
|
||||||
|
flyoutDetailTab: undefined,
|
||||||
|
waterfallItemId: String(item.id)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public onCloseFlyout = () => {
|
||||||
|
this.setQueryParams({
|
||||||
|
flyoutDetailTab: undefined,
|
||||||
|
waterfallItemId: undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public renderWaterfall = (item?: IWaterfallItem) => {
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { serviceColors, waterfall, urlParams }: Props = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment key={item.id}>
|
||||||
|
<WaterfallItem
|
||||||
|
timelineMargins={TIMELINE_MARGINS}
|
||||||
|
color={serviceColors[item.serviceName]}
|
||||||
|
item={item}
|
||||||
|
totalDuration={waterfall.duration}
|
||||||
|
isSelected={item.id === urlParams.waterfallItemId}
|
||||||
|
onClick={() => this.onOpenFlyout(item)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{item.children && item.children.map(this.renderWaterfall)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
public getFlyOut = () => {
|
||||||
|
const { waterfall, location, urlParams } = this.props;
|
||||||
|
|
||||||
|
const currentItem =
|
||||||
|
urlParams.waterfallItemId &&
|
||||||
|
waterfall.itemsById[urlParams.waterfallItemId];
|
||||||
|
|
||||||
|
if (!currentItem) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (currentItem.docType) {
|
||||||
|
case 'span':
|
||||||
|
return (
|
||||||
|
<SpanFlyout
|
||||||
|
totalDuration={waterfall.duration}
|
||||||
|
span={currentItem.span}
|
||||||
|
parentTransaction={currentItem.parentTransaction}
|
||||||
|
onClose={this.onCloseFlyout}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'transaction':
|
||||||
|
return (
|
||||||
|
<TransactionFlyout
|
||||||
|
transaction={currentItem.transaction}
|
||||||
|
onClose={this.onCloseFlyout}
|
||||||
|
location={location}
|
||||||
|
urlParams={urlParams}
|
||||||
|
waterfall={waterfall}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { waterfall } = this.props;
|
||||||
|
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.childrenCount;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<StickyContainer>
|
||||||
|
<Timeline
|
||||||
|
agentMarks={this.props.agentMarks}
|
||||||
|
duration={waterfall.duration}
|
||||||
|
height={waterfallHeight}
|
||||||
|
margins={TIMELINE_MARGINS}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
paddingTop: TIMELINE_MARGINS.top
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{this.renderWaterfall(waterfall.root)}
|
||||||
|
</div>
|
||||||
|
</StickyContainer>
|
||||||
|
|
||||||
|
{this.getFlyOut()}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setQueryParams(params: Partial<IUrlParams>) {
|
||||||
|
const { location } = this.props;
|
||||||
|
history.replace({
|
||||||
|
...location,
|
||||||
|
search: fromQuery({
|
||||||
|
...toQuery(location.search),
|
||||||
|
...params
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: the agent marks and note about dropped spans were removed. Need to be re-added
|
||||||
|
// agentMarks: PropTypes.array,
|
||||||
|
// agentName: PropTypes.string.isRequired,
|
||||||
|
// droppedSpans: PropTypes.number.isRequired,
|
|
@ -0,0 +1,274 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`getWaterfallRoot 1`] = `
|
||||||
|
Object {
|
||||||
|
"itemsById": Object {
|
||||||
|
"a": Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"docType": "span",
|
||||||
|
"duration": 4694,
|
||||||
|
"id": "b2",
|
||||||
|
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||||
|
"offset": 1000,
|
||||||
|
"parentId": "a",
|
||||||
|
"parentTransaction": Object {},
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"span": Object {
|
||||||
|
"transaction": Object {
|
||||||
|
"id": "a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"timestamp": 1536763736367000,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"docType": "span",
|
||||||
|
"duration": 210,
|
||||||
|
"id": "d",
|
||||||
|
"name": "SELECT",
|
||||||
|
"offset": 5000,
|
||||||
|
"parentId": "c",
|
||||||
|
"parentTransaction": Object {},
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"span": Object {
|
||||||
|
"transaction": Object {
|
||||||
|
"id": "c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"timestamp": 1536763736371000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"docType": "transaction",
|
||||||
|
"duration": 3581,
|
||||||
|
"id": "c",
|
||||||
|
"name": "APIRestController#productsRemote",
|
||||||
|
"offset": 3000,
|
||||||
|
"parentId": "b",
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"timestamp": 1536763736369000,
|
||||||
|
"transaction": Object {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"docType": "span",
|
||||||
|
"duration": 4694,
|
||||||
|
"id": "b",
|
||||||
|
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||||
|
"offset": 2000,
|
||||||
|
"parentId": "a",
|
||||||
|
"parentTransaction": Object {},
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"span": Object {
|
||||||
|
"transaction": Object {
|
||||||
|
"id": "a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"timestamp": 1536763736368000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"docType": "transaction",
|
||||||
|
"duration": 9480,
|
||||||
|
"id": "a",
|
||||||
|
"name": "APIRestController#products",
|
||||||
|
"offset": 0,
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"timestamp": 1536763736366000,
|
||||||
|
"transaction": Object {},
|
||||||
|
},
|
||||||
|
"b": Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"docType": "span",
|
||||||
|
"duration": 210,
|
||||||
|
"id": "d",
|
||||||
|
"name": "SELECT",
|
||||||
|
"offset": 5000,
|
||||||
|
"parentId": "c",
|
||||||
|
"parentTransaction": Object {},
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"span": Object {
|
||||||
|
"transaction": Object {
|
||||||
|
"id": "c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"timestamp": 1536763736371000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"docType": "transaction",
|
||||||
|
"duration": 3581,
|
||||||
|
"id": "c",
|
||||||
|
"name": "APIRestController#productsRemote",
|
||||||
|
"offset": 3000,
|
||||||
|
"parentId": "b",
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"timestamp": 1536763736369000,
|
||||||
|
"transaction": Object {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"docType": "span",
|
||||||
|
"duration": 4694,
|
||||||
|
"id": "b",
|
||||||
|
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||||
|
"offset": 2000,
|
||||||
|
"parentId": "a",
|
||||||
|
"parentTransaction": Object {},
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"span": Object {
|
||||||
|
"transaction": Object {
|
||||||
|
"id": "a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"timestamp": 1536763736368000,
|
||||||
|
},
|
||||||
|
"b2": Object {
|
||||||
|
"children": Array [],
|
||||||
|
"docType": "span",
|
||||||
|
"duration": 4694,
|
||||||
|
"id": "b2",
|
||||||
|
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||||
|
"offset": 1000,
|
||||||
|
"parentId": "a",
|
||||||
|
"parentTransaction": Object {},
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"span": Object {
|
||||||
|
"transaction": Object {
|
||||||
|
"id": "a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"timestamp": 1536763736367000,
|
||||||
|
},
|
||||||
|
"c": Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"docType": "span",
|
||||||
|
"duration": 210,
|
||||||
|
"id": "d",
|
||||||
|
"name": "SELECT",
|
||||||
|
"offset": 5000,
|
||||||
|
"parentId": "c",
|
||||||
|
"parentTransaction": Object {},
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"span": Object {
|
||||||
|
"transaction": Object {
|
||||||
|
"id": "c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"timestamp": 1536763736371000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"docType": "transaction",
|
||||||
|
"duration": 3581,
|
||||||
|
"id": "c",
|
||||||
|
"name": "APIRestController#productsRemote",
|
||||||
|
"offset": 3000,
|
||||||
|
"parentId": "b",
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"timestamp": 1536763736369000,
|
||||||
|
"transaction": Object {},
|
||||||
|
},
|
||||||
|
"d": Object {
|
||||||
|
"children": Array [],
|
||||||
|
"docType": "span",
|
||||||
|
"duration": 210,
|
||||||
|
"id": "d",
|
||||||
|
"name": "SELECT",
|
||||||
|
"offset": 5000,
|
||||||
|
"parentId": "c",
|
||||||
|
"parentTransaction": Object {},
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"span": Object {
|
||||||
|
"transaction": Object {
|
||||||
|
"id": "c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"timestamp": 1536763736371000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"docType": "span",
|
||||||
|
"duration": 4694,
|
||||||
|
"id": "b2",
|
||||||
|
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||||
|
"offset": 1000,
|
||||||
|
"parentId": "a",
|
||||||
|
"parentTransaction": Object {},
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"span": Object {
|
||||||
|
"transaction": Object {
|
||||||
|
"id": "a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"timestamp": 1536763736367000,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"docType": "span",
|
||||||
|
"duration": 210,
|
||||||
|
"id": "d",
|
||||||
|
"name": "SELECT",
|
||||||
|
"offset": 5000,
|
||||||
|
"parentId": "c",
|
||||||
|
"parentTransaction": Object {},
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"span": Object {
|
||||||
|
"transaction": Object {
|
||||||
|
"id": "c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"timestamp": 1536763736371000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"docType": "transaction",
|
||||||
|
"duration": 3581,
|
||||||
|
"id": "c",
|
||||||
|
"name": "APIRestController#productsRemote",
|
||||||
|
"offset": 3000,
|
||||||
|
"parentId": "b",
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"timestamp": 1536763736369000,
|
||||||
|
"transaction": Object {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"docType": "span",
|
||||||
|
"duration": 4694,
|
||||||
|
"id": "b",
|
||||||
|
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||||
|
"offset": 2000,
|
||||||
|
"parentId": "a",
|
||||||
|
"parentTransaction": Object {},
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"span": Object {
|
||||||
|
"transaction": Object {
|
||||||
|
"id": "a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"timestamp": 1536763736368000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"docType": "transaction",
|
||||||
|
"duration": 9480,
|
||||||
|
"id": "a",
|
||||||
|
"name": "APIRestController#products",
|
||||||
|
"offset": 0,
|
||||||
|
"serviceName": "opbeans-java",
|
||||||
|
"timestamp": 1536763736366000,
|
||||||
|
"transaction": Object {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
|
@ -0,0 +1,47 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"@timestamp": "2018-09-12T15:16:05.351Z",
|
||||||
|
"processor": {
|
||||||
|
"name": "transaction",
|
||||||
|
"event": "span"
|
||||||
|
},
|
||||||
|
"span": {
|
||||||
|
"parent_id": "e070bc3c732087f8",
|
||||||
|
"trace_id": "7d4d29bea37e48ac8ba1f962d5eb8a41",
|
||||||
|
"name": "SELECT",
|
||||||
|
"type": "db.h2.sql",
|
||||||
|
"start": {
|
||||||
|
"us": 9249
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"us": 1380
|
||||||
|
},
|
||||||
|
"hex_id": "8143a38f3367fd97"
|
||||||
|
},
|
||||||
|
"transaction": {
|
||||||
|
"id": "e070bc3c732087f8"
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"db": {
|
||||||
|
"statement": "select order0_.id as col_0_0_, order0_.created_at as col_1_0_, customer1_.full_name as col_2_0_ from orders order0_ left outer join customers customer1_ on order0_.customer_id=customer1_.id",
|
||||||
|
"type": "sql",
|
||||||
|
"user": "SA"
|
||||||
|
},
|
||||||
|
"service": {
|
||||||
|
"name": "opbeans-java",
|
||||||
|
"agent": {
|
||||||
|
"version": "0.7.0-SNAPSHOT",
|
||||||
|
"name": "java"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"beat": {
|
||||||
|
"version": "7.0.0-alpha1",
|
||||||
|
"name": "361022bff072",
|
||||||
|
"hostname": "361022bff072"
|
||||||
|
},
|
||||||
|
"host": {
|
||||||
|
"name": "361022bff072"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,93 @@
|
||||||
|
{
|
||||||
|
"@timestamp": "2018-09-12T15:16:05.341Z",
|
||||||
|
"processor": {
|
||||||
|
"name": "transaction",
|
||||||
|
"event": "transaction"
|
||||||
|
},
|
||||||
|
"transaction": {
|
||||||
|
"duration": {
|
||||||
|
"us": 9069543
|
||||||
|
},
|
||||||
|
"type": "request",
|
||||||
|
"result": "HTTP 2xx",
|
||||||
|
"trace_id": "7d4d29bea37e48ac8ba1f962d5eb8a41",
|
||||||
|
"sampled": true,
|
||||||
|
"span_count": {
|
||||||
|
"started": 1,
|
||||||
|
"dropped": {
|
||||||
|
"total": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "e070bc3c732087f8",
|
||||||
|
"name": "APIRestController#orders"
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"request": {
|
||||||
|
"url": {
|
||||||
|
"full": "http://localhost:8080/api/orders",
|
||||||
|
"hostname": "localhost",
|
||||||
|
"port": "8080",
|
||||||
|
"pathname": "/api/orders",
|
||||||
|
"protocol": "http"
|
||||||
|
},
|
||||||
|
"socket": {
|
||||||
|
"encrypted": false,
|
||||||
|
"remote_address": "0:0:0:0:0:0:0:1"
|
||||||
|
},
|
||||||
|
"http_version": "1.1",
|
||||||
|
"method": "GET",
|
||||||
|
"headers": {
|
||||||
|
"accept-encoding": "gzip, deflate",
|
||||||
|
"host": "localhost:8080",
|
||||||
|
"connection": "keep-alive",
|
||||||
|
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/70.0.3508.0 Safari/537.36",
|
||||||
|
"accept": "*/*",
|
||||||
|
"referer": "http://localhost:8080/orders"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"finished": true,
|
||||||
|
"headers_sent": true,
|
||||||
|
"status_code": 200,
|
||||||
|
"headers": {
|
||||||
|
"Transfer-Encoding": "chunked",
|
||||||
|
"Date": "Wed, 12 Sep 2018 15:16:07 GMT",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"system": {
|
||||||
|
"architecture": "x86_64",
|
||||||
|
"platform": "Srens-MacBook-Pro.local",
|
||||||
|
"ip": "172.18.0.1",
|
||||||
|
"hostname": "Mac OS X"
|
||||||
|
},
|
||||||
|
"process": {
|
||||||
|
"ppid": 10060,
|
||||||
|
"title": "/Library/Java/JavaVirtualMachines/jdk-10.0.2.jdk/Contents/Home/bin/java",
|
||||||
|
"pid": 10069
|
||||||
|
},
|
||||||
|
"service": {
|
||||||
|
"language": {
|
||||||
|
"version": "10.0.2",
|
||||||
|
"name": "Java"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"name": "Java",
|
||||||
|
"version": "10.0.2"
|
||||||
|
},
|
||||||
|
"name": "opbeans-java",
|
||||||
|
"agent": {
|
||||||
|
"name": "java",
|
||||||
|
"version": "0.7.0-SNAPSHOT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"beat": {
|
||||||
|
"version": "7.0.0-alpha1",
|
||||||
|
"name": "361022bff072",
|
||||||
|
"hostname": "361022bff072"
|
||||||
|
},
|
||||||
|
"host": {
|
||||||
|
"name": "361022bff072"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Span } from 'x-pack/plugins/apm/typings/Span';
|
||||||
|
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||||
|
import { getWaterfallRoot, IWaterfallItem } from './waterfall_helpers';
|
||||||
|
|
||||||
|
it('getWaterfallRoot', () => {
|
||||||
|
const items: IWaterfallItem[] = [
|
||||||
|
{
|
||||||
|
id: 'd',
|
||||||
|
parentId: 'c',
|
||||||
|
serviceName: 'opbeans-java',
|
||||||
|
name: 'SELECT',
|
||||||
|
duration: 210,
|
||||||
|
timestamp: 1536763736371000,
|
||||||
|
offset: 0,
|
||||||
|
docType: 'span',
|
||||||
|
parentTransaction: {} as Transaction,
|
||||||
|
span: {
|
||||||
|
transaction: {
|
||||||
|
id: 'c'
|
||||||
|
}
|
||||||
|
} as Span
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'b',
|
||||||
|
parentId: 'a',
|
||||||
|
serviceName: 'opbeans-java',
|
||||||
|
name: 'GET [0:0:0:0:0:0:0:1]',
|
||||||
|
duration: 4694,
|
||||||
|
timestamp: 1536763736368000,
|
||||||
|
offset: 0,
|
||||||
|
docType: 'span',
|
||||||
|
parentTransaction: {} as Transaction,
|
||||||
|
span: {
|
||||||
|
transaction: {
|
||||||
|
id: 'a'
|
||||||
|
}
|
||||||
|
} as Span
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'b2',
|
||||||
|
parentId: 'a',
|
||||||
|
serviceName: 'opbeans-java',
|
||||||
|
name: 'GET [0:0:0:0:0:0:0:1]',
|
||||||
|
duration: 4694,
|
||||||
|
timestamp: 1536763736367000,
|
||||||
|
offset: 0,
|
||||||
|
docType: 'span',
|
||||||
|
parentTransaction: {} as Transaction,
|
||||||
|
span: {
|
||||||
|
transaction: {
|
||||||
|
id: 'a'
|
||||||
|
}
|
||||||
|
} as Span
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'c',
|
||||||
|
parentId: 'b',
|
||||||
|
serviceName: 'opbeans-java',
|
||||||
|
name: 'APIRestController#productsRemote',
|
||||||
|
duration: 3581,
|
||||||
|
timestamp: 1536763736369000,
|
||||||
|
offset: 0,
|
||||||
|
docType: 'transaction',
|
||||||
|
transaction: {} as Transaction
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'a',
|
||||||
|
serviceName: 'opbeans-java',
|
||||||
|
name: 'APIRestController#products',
|
||||||
|
duration: 9480,
|
||||||
|
timestamp: 1536763736366000,
|
||||||
|
offset: 0,
|
||||||
|
docType: 'transaction',
|
||||||
|
transaction: {} as Transaction
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(getWaterfallRoot(items, items[4])).toMatchSnapshot();
|
||||||
|
});
|
|
@ -0,0 +1,190 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { groupBy, indexBy, sortBy } from 'lodash';
|
||||||
|
import { Span } from '../../../../../../../../typings/Span';
|
||||||
|
import { Transaction } from '../../../../../../../../typings/Transaction';
|
||||||
|
|
||||||
|
export interface IWaterfallIndex {
|
||||||
|
[key: string]: IWaterfallItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWaterfall {
|
||||||
|
duration: number;
|
||||||
|
services: string[];
|
||||||
|
childrenCount: number;
|
||||||
|
root: IWaterfallItem;
|
||||||
|
itemsById: IWaterfallIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWaterfallItemBase {
|
||||||
|
id: string | number;
|
||||||
|
parentId?: string;
|
||||||
|
serviceName: string;
|
||||||
|
name: string;
|
||||||
|
duration: number;
|
||||||
|
timestamp: number;
|
||||||
|
offset: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWaterfallItemTransaction extends IWaterfallItemBase {
|
||||||
|
transaction: Transaction;
|
||||||
|
docType: 'transaction';
|
||||||
|
children?: Array<IWaterfallItemSpan | IWaterfallItemTransaction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWaterfallItemSpan extends IWaterfallItemBase {
|
||||||
|
parentTransaction: Transaction;
|
||||||
|
span: Span;
|
||||||
|
docType: 'span';
|
||||||
|
children?: Array<IWaterfallItemSpan | IWaterfallItemTransaction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IWaterfallItem = IWaterfallItemSpan | IWaterfallItemTransaction;
|
||||||
|
|
||||||
|
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||||
|
|
||||||
|
function getTransactionItem(
|
||||||
|
transaction: Transaction
|
||||||
|
): IWaterfallItemTransaction {
|
||||||
|
if (transaction.version === 'v1') {
|
||||||
|
return {
|
||||||
|
id: transaction.transaction.id,
|
||||||
|
serviceName: transaction.context.service.name,
|
||||||
|
name: transaction.transaction.name,
|
||||||
|
duration: transaction.transaction.duration.us,
|
||||||
|
timestamp: new Date(transaction['@timestamp']).getTime() * 1000,
|
||||||
|
offset: 0,
|
||||||
|
docType: 'transaction',
|
||||||
|
transaction
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: transaction.transaction.id,
|
||||||
|
parentId: transaction.parent && transaction.parent.id,
|
||||||
|
serviceName: transaction.context.service.name,
|
||||||
|
name: transaction.transaction.name,
|
||||||
|
duration: transaction.transaction.duration.us,
|
||||||
|
timestamp: transaction.timestamp.us,
|
||||||
|
offset: 0,
|
||||||
|
docType: 'transaction',
|
||||||
|
transaction
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type PartialSpanItem = Omit<IWaterfallItemSpan, 'parentTransaction'>;
|
||||||
|
|
||||||
|
function getSpanItem(span: Span): PartialSpanItem {
|
||||||
|
if (span.version === 'v1') {
|
||||||
|
return {
|
||||||
|
id: span.span.id,
|
||||||
|
parentId: span.span.parent || span.transaction.id,
|
||||||
|
serviceName: span.context.service.name,
|
||||||
|
name: span.span.name,
|
||||||
|
duration: span.span.duration.us,
|
||||||
|
timestamp:
|
||||||
|
new Date(span['@timestamp']).getTime() * 1000 + span.span.start.us,
|
||||||
|
offset: 0,
|
||||||
|
docType: 'span',
|
||||||
|
span
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: span.span.hex_id,
|
||||||
|
parentId: span.parent && span.parent.id,
|
||||||
|
serviceName: span.context.service.name,
|
||||||
|
name: span.span.name,
|
||||||
|
duration: span.span.duration.us,
|
||||||
|
timestamp: span.timestamp.us,
|
||||||
|
offset: 0,
|
||||||
|
docType: 'span',
|
||||||
|
span
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWaterfallRoot(
|
||||||
|
items: Array<PartialSpanItem | IWaterfallItemTransaction>,
|
||||||
|
entryTransactionItem: IWaterfallItem
|
||||||
|
) {
|
||||||
|
const itemsByParentId = groupBy(
|
||||||
|
items,
|
||||||
|
item => (item.parentId ? item.parentId : 'root')
|
||||||
|
);
|
||||||
|
const itemsById: IWaterfallIndex = {};
|
||||||
|
|
||||||
|
const itemsByTransactionId = indexBy(
|
||||||
|
items.filter(item => item.docType === 'transaction'),
|
||||||
|
item => item.id
|
||||||
|
) as { [key: string]: IWaterfallItemTransaction };
|
||||||
|
|
||||||
|
function getWithChildren(
|
||||||
|
item: PartialSpanItem | IWaterfallItemTransaction
|
||||||
|
): IWaterfallItem {
|
||||||
|
const children = itemsByParentId[item.id] || [];
|
||||||
|
const nextChildren = sortBy(children, 'timestamp').map(getWithChildren);
|
||||||
|
let fullItem;
|
||||||
|
|
||||||
|
// add parent transaction to spans
|
||||||
|
if (item.docType === 'span') {
|
||||||
|
fullItem = {
|
||||||
|
parentTransaction:
|
||||||
|
itemsByTransactionId[item.span.transaction.id].transaction,
|
||||||
|
...item,
|
||||||
|
offset: item.timestamp - entryTransactionItem.timestamp,
|
||||||
|
children: nextChildren
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
fullItem = {
|
||||||
|
...item,
|
||||||
|
offset: item.timestamp - entryTransactionItem.timestamp,
|
||||||
|
children: nextChildren
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Think about storing this tree as a single, flat, indexed structure
|
||||||
|
// with "children" being an array of ids, instead of it being a real tree
|
||||||
|
itemsById[item.id] = fullItem;
|
||||||
|
|
||||||
|
return fullItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { root: getWithChildren(entryTransactionItem), itemsById };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWaterfall(
|
||||||
|
hits: Array<Span | Transaction>,
|
||||||
|
services: string[],
|
||||||
|
entryTransaction: Transaction
|
||||||
|
): IWaterfall {
|
||||||
|
const items = hits
|
||||||
|
.filter(hit => {
|
||||||
|
const docType = hit.processor.event;
|
||||||
|
return ['span', 'transaction'].includes(docType);
|
||||||
|
})
|
||||||
|
.map(hit => {
|
||||||
|
const docType = hit.processor.event;
|
||||||
|
switch (docType) {
|
||||||
|
case 'span':
|
||||||
|
return getSpanItem(hit as Span);
|
||||||
|
case 'transaction':
|
||||||
|
return getTransactionItem(hit as Transaction);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown type ${docType}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const entryTransactionItem = getTransactionItem(entryTransaction);
|
||||||
|
const { root, itemsById } = getWaterfallRoot(items, entryTransactionItem);
|
||||||
|
return {
|
||||||
|
duration: root.duration,
|
||||||
|
services,
|
||||||
|
childrenCount: hits.length,
|
||||||
|
root,
|
||||||
|
itemsById
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { zipObject } from 'lodash';
|
||||||
|
import { colors } from '../../../../../style/variables';
|
||||||
|
|
||||||
|
interface IServiceColors {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServiceColors(services: string[]): IServiceColors {
|
||||||
|
const assignedColors = [
|
||||||
|
colors.apmBlue,
|
||||||
|
colors.apmGreen,
|
||||||
|
colors.apmPurple,
|
||||||
|
colors.apmRed2,
|
||||||
|
colors.apmTan,
|
||||||
|
colors.apmOrange,
|
||||||
|
colors.apmYellow
|
||||||
|
];
|
||||||
|
|
||||||
|
return zipObject(services, assignedColors);
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||||
|
import { getAgentMarks } from './get_agent_marks';
|
||||||
|
|
||||||
|
describe('getAgentMarks', () => {
|
||||||
|
it('should sort the marks', () => {
|
||||||
|
const transaction: Transaction = {
|
||||||
|
transaction: {
|
||||||
|
marks: {
|
||||||
|
agent: {
|
||||||
|
domInteractive: 117,
|
||||||
|
timeToFirstByte: 10,
|
||||||
|
domComplete: 118
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
expect(getAgentMarks(transaction)).toEqual([
|
||||||
|
{ name: 'timeToFirstByte', us: 10000 },
|
||||||
|
{ name: 'domInteractive', us: 117000 },
|
||||||
|
{ name: 'domComplete', us: 118000 }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array if marks are missing', () => {
|
||||||
|
const transaction: Transaction = {
|
||||||
|
transaction: {}
|
||||||
|
} as any;
|
||||||
|
expect(getAgentMarks(transaction)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { sortBy } from 'lodash';
|
||||||
|
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||||
|
|
||||||
|
export interface AgentMark {
|
||||||
|
name: string;
|
||||||
|
us: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAgentMarks(transaction: Transaction): AgentMark[] {
|
||||||
|
if (!transaction.transaction.marks) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortBy(
|
||||||
|
Object.entries(transaction.transaction.marks.agent).map(([name, ms]) => ({
|
||||||
|
name,
|
||||||
|
us: ms * 1000
|
||||||
|
})),
|
||||||
|
'us'
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
// @ts-ignore
|
||||||
|
import {
|
||||||
|
SERVICE_NAME,
|
||||||
|
TRACE_ID,
|
||||||
|
TRANSACTION_ID
|
||||||
|
} from '../../../../../../common/constants';
|
||||||
|
import { Transaction } from '../../../../../../typings/Transaction';
|
||||||
|
|
||||||
|
import { RRRRender } from 'react-redux-request';
|
||||||
|
import { WaterfallV1Request } from 'x-pack/plugins/apm/public/store/reactReduxRequest/waterfallV1';
|
||||||
|
import { WaterfallV2Request } from 'x-pack/plugins/apm/public/store/reactReduxRequest/waterfallV2';
|
||||||
|
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||||
|
import { WaterfallResponse } from 'x-pack/plugins/apm/typings/waterfall';
|
||||||
|
import { getAgentMarks } from './get_agent_marks';
|
||||||
|
import { getServiceColors } from './getServiceColors';
|
||||||
|
import { ServiceLegends } from './ServiceLegends';
|
||||||
|
import { Waterfall } from './Waterfall';
|
||||||
|
import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
urlParams: IUrlParams;
|
||||||
|
transaction: Transaction;
|
||||||
|
location: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WaterfallRequestProps {
|
||||||
|
urlParams: IUrlParams;
|
||||||
|
transaction: Transaction;
|
||||||
|
render: RRRRender<WaterfallResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function WaterfallRequest({
|
||||||
|
urlParams,
|
||||||
|
transaction,
|
||||||
|
render
|
||||||
|
}: WaterfallRequestProps) {
|
||||||
|
const hasTrace = transaction.hasOwnProperty('trace');
|
||||||
|
if (hasTrace) {
|
||||||
|
return (
|
||||||
|
<WaterfallV2Request
|
||||||
|
urlParams={urlParams}
|
||||||
|
transaction={transaction}
|
||||||
|
render={render}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<WaterfallV1Request
|
||||||
|
urlParams={urlParams}
|
||||||
|
transaction={transaction}
|
||||||
|
render={render}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WaterfallContainer({
|
||||||
|
location,
|
||||||
|
urlParams,
|
||||||
|
transaction
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<WaterfallRequest
|
||||||
|
urlParams={urlParams}
|
||||||
|
transaction={transaction}
|
||||||
|
render={({ data }) => {
|
||||||
|
const agentMarks = getAgentMarks(transaction);
|
||||||
|
const waterfall = getWaterfall(data.hits, data.services, transaction);
|
||||||
|
if (!waterfall) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const serviceColors = getServiceColors(waterfall.services);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ServiceLegends serviceColors={serviceColors} />
|
||||||
|
<Waterfall
|
||||||
|
agentMarks={agentMarks}
|
||||||
|
location={location}
|
||||||
|
serviceColors={serviceColors}
|
||||||
|
urlParams={urlParams}
|
||||||
|
waterfall={waterfall}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,64 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { getAgentMarks } from '../view';
|
|
||||||
|
|
||||||
describe('TransactionDetailsView', () => {
|
|
||||||
describe('getAgentMarks', () => {
|
|
||||||
it('should be sorted', () => {
|
|
||||||
const transaction = {
|
|
||||||
transaction: {
|
|
||||||
marks: {
|
|
||||||
agent: {
|
|
||||||
domInteractive: 117,
|
|
||||||
timeToFirstByte: 10,
|
|
||||||
domComplete: 118
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
expect(getAgentMarks(transaction)).toEqual([
|
|
||||||
{ name: 'timeToFirstByte', timeLabel: 10000, timeAxis: 10000 },
|
|
||||||
{ name: 'domInteractive', timeLabel: 117000, timeAxis: 117000 },
|
|
||||||
{ name: 'domComplete', timeLabel: 118000, timeAxis: 118000 }
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ensure they are not too close', () => {
|
|
||||||
const transaction = {
|
|
||||||
transaction: {
|
|
||||||
duration: {
|
|
||||||
us: 1000 * 1000
|
|
||||||
},
|
|
||||||
marks: {
|
|
||||||
agent: {
|
|
||||||
a: 0,
|
|
||||||
b: 10,
|
|
||||||
c: 11,
|
|
||||||
d: 12,
|
|
||||||
e: 968,
|
|
||||||
f: 969,
|
|
||||||
timeToFirstByte: 970,
|
|
||||||
domInteractive: 980,
|
|
||||||
domComplete: 990
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
expect(getAgentMarks(transaction)).toEqual([
|
|
||||||
{ timeLabel: 0, name: 'a', timeAxis: 0 },
|
|
||||||
{ timeLabel: 10000, name: 'b', timeAxis: 20000 },
|
|
||||||
{ timeLabel: 11000, name: 'c', timeAxis: 40000 },
|
|
||||||
{ timeLabel: 12000, name: 'd', timeAxis: 60000 },
|
|
||||||
{ timeLabel: 968000, name: 'e', timeAxis: 910000 },
|
|
||||||
{ timeLabel: 969000, name: 'f', timeAxis: 930000 },
|
|
||||||
{ timeLabel: 970000, name: 'timeToFirstByte', timeAxis: 950000 },
|
|
||||||
{ timeLabel: 980000, name: 'domInteractive', timeAxis: 970000 },
|
|
||||||
{ timeLabel: 990000, name: 'domComplete', timeAxis: 990000 }
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import Transaction from './view';
|
|
||||||
|
|
||||||
function mapStateToProps(state = {}) {
|
|
||||||
return {
|
|
||||||
location: state.location
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {};
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(Transaction);
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
EuiButton,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiPanel,
|
||||||
|
EuiSpacer,
|
||||||
|
EuiTitle,
|
||||||
|
EuiToolTip
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import { Transaction as ITransaction } from '../../../../../typings/Transaction';
|
||||||
|
import { IUrlParams } from '../../../../store/urlParams';
|
||||||
|
import EmptyMessage from '../../../shared/EmptyMessage';
|
||||||
|
import { TraceLink } from '../../../shared/TraceLink';
|
||||||
|
import { ActionMenu } from './ActionMenu';
|
||||||
|
import { StickyTransactionProperties } from './StickyTransactionProperties';
|
||||||
|
// @ts-ignore
|
||||||
|
import { TransactionPropertiesTable } from './TransactionPropertiesTable';
|
||||||
|
|
||||||
|
function MaybeViewTraceLink({
|
||||||
|
root,
|
||||||
|
transaction
|
||||||
|
}: {
|
||||||
|
root: ITransaction;
|
||||||
|
transaction: ITransaction;
|
||||||
|
}) {
|
||||||
|
const isRoot = transaction.transaction.id === root.transaction.id;
|
||||||
|
let button;
|
||||||
|
|
||||||
|
if (isRoot || !root) {
|
||||||
|
button = (
|
||||||
|
<EuiToolTip content="Currently viewing the full trace">
|
||||||
|
<EuiButton iconType="apmApp" disabled={true}>
|
||||||
|
View full trace
|
||||||
|
</EuiButton>
|
||||||
|
</EuiToolTip>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
button = <EuiButton iconType="apmApp">View full trace</EuiButton>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<TraceLink transaction={root}>{button}</TraceLink>
|
||||||
|
</EuiFlexItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
transaction: ITransaction;
|
||||||
|
urlParams: IUrlParams;
|
||||||
|
location: Location;
|
||||||
|
waterfallRoot?: ITransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Transaction: React.SFC<Props> = ({
|
||||||
|
transaction,
|
||||||
|
urlParams,
|
||||||
|
location,
|
||||||
|
waterfallRoot
|
||||||
|
}) => {
|
||||||
|
if (isEmpty(transaction)) {
|
||||||
|
return (
|
||||||
|
<EmptyMessage
|
||||||
|
heading="No transaction sample available."
|
||||||
|
subheading="Try another time range, reset the search filter or select another bucket from the distribution histogram."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = waterfallRoot || transaction;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiPanel paddingSize="m" hasShadow={true}>
|
||||||
|
<EuiFlexGroup justifyContent="spaceBetween">
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiTitle size="s">
|
||||||
|
<span>Transaction sample</span>
|
||||||
|
</EuiTitle>
|
||||||
|
</EuiFlexItem>
|
||||||
|
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiFlexGroup justifyContent="flexEnd">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<ActionMenu transaction={transaction} />
|
||||||
|
</EuiFlexItem>
|
||||||
|
<MaybeViewTraceLink transaction={transaction} root={root} />
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
|
||||||
|
<EuiSpacer />
|
||||||
|
|
||||||
|
<StickyTransactionProperties transaction={transaction} root={root} />
|
||||||
|
|
||||||
|
<EuiSpacer />
|
||||||
|
|
||||||
|
<TransactionPropertiesTable
|
||||||
|
transaction={transaction}
|
||||||
|
location={location}
|
||||||
|
urlParams={urlParams}
|
||||||
|
/>
|
||||||
|
</EuiPanel>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,218 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import {
|
|
||||||
unit,
|
|
||||||
units,
|
|
||||||
colors,
|
|
||||||
px,
|
|
||||||
borderRadius
|
|
||||||
} from '../../../../style/variables';
|
|
||||||
import { Tab, HeaderMedium } from '../../../shared/UIComponents';
|
|
||||||
import { isEmpty, capitalize, get, sortBy, last } from 'lodash';
|
|
||||||
|
|
||||||
import StickyTransactionProperties from './StickyTransactionProperties';
|
|
||||||
import {
|
|
||||||
PropertiesTable,
|
|
||||||
getPropertyTabNames
|
|
||||||
} from '../../../shared/PropertiesTable';
|
|
||||||
import Spans from './Spans';
|
|
||||||
import DiscoverButton from '../../../shared/DiscoverButton';
|
|
||||||
import {
|
|
||||||
TRANSACTION_ID,
|
|
||||||
PROCESSOR_EVENT,
|
|
||||||
SERVICE_AGENT_NAME,
|
|
||||||
TRANSACTION_DURATION
|
|
||||||
} from '../../../../../common/constants';
|
|
||||||
import { fromQuery, toQuery, history } from '../../../../utils/url';
|
|
||||||
import EmptyMessage from '../../../shared/EmptyMessage';
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
position: relative;
|
|
||||||
border: 1px solid ${colors.gray4};
|
|
||||||
border-radius: ${borderRadius};
|
|
||||||
margin-top: ${px(units.plus)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HeaderContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: ${px(units.plus)} ${px(units.plus)} 0;
|
|
||||||
margin-bottom: ${px(unit)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabContainer = styled.div`
|
|
||||||
padding: 0 ${px(units.plus)};
|
|
||||||
border-bottom: 1px solid ${colors.gray4};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabContentContainer = styled.div`
|
|
||||||
border-radius: 0 0 ${borderRadius} ${borderRadius};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const PropertiesTableContainer = styled.div`
|
|
||||||
padding: ${px(units.plus)} ${px(units.plus)} 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DEFAULT_TAB = 'timeline';
|
|
||||||
|
|
||||||
export function getAgentMarks(transaction) {
|
|
||||||
const duration = get(transaction, TRANSACTION_DURATION);
|
|
||||||
const threshold = (duration / 100) * 2;
|
|
||||||
|
|
||||||
return sortBy(
|
|
||||||
Object.entries(get(transaction, 'transaction.marks.agent', [])),
|
|
||||||
'1'
|
|
||||||
)
|
|
||||||
.map(([name, ms]) => ({
|
|
||||||
name,
|
|
||||||
timeLabel: ms * 1000,
|
|
||||||
timeAxis: ms * 1000
|
|
||||||
}))
|
|
||||||
.reduce((acc, curItem) => {
|
|
||||||
const prevTime = get(last(acc), 'timeAxis');
|
|
||||||
const nextValidTime = prevTime + threshold;
|
|
||||||
const isTooClose = prevTime != null && nextValidTime > curItem.timeAxis;
|
|
||||||
const canFit = nextValidTime <= duration;
|
|
||||||
|
|
||||||
if (isTooClose && canFit) {
|
|
||||||
acc.push({ ...curItem, timeAxis: nextValidTime });
|
|
||||||
} else {
|
|
||||||
acc.push(curItem);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, [])
|
|
||||||
.reduceRight((acc, curItem) => {
|
|
||||||
const prevTime = get(last(acc), 'timeAxis');
|
|
||||||
const nextValidTime = prevTime - threshold;
|
|
||||||
const isTooClose = prevTime != null && nextValidTime < curItem.timeAxis;
|
|
||||||
const canFit = nextValidTime >= 0;
|
|
||||||
|
|
||||||
if (isTooClose && canFit) {
|
|
||||||
acc.push({ ...curItem, timeAxis: nextValidTime });
|
|
||||||
} else {
|
|
||||||
acc.push(curItem);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, [])
|
|
||||||
.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the selected tab exists or use the default
|
|
||||||
function getCurrentTab(tabs = [], detailTab) {
|
|
||||||
return tabs.includes(detailTab) ? detailTab : DEFAULT_TAB;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTabs(transactionData) {
|
|
||||||
const dynamicProps = Object.keys(transactionData.context || {});
|
|
||||||
return getPropertyTabNames(dynamicProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Transaction({ transaction, location, urlParams }) {
|
|
||||||
const { transactionId } = urlParams;
|
|
||||||
|
|
||||||
if (isEmpty(transaction)) {
|
|
||||||
return (
|
|
||||||
<EmptyMessage
|
|
||||||
heading="No transaction sample available."
|
|
||||||
subheading="Try another time range, reset the search filter or select another bucket from the distribution histogram."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentName = get(transaction, SERVICE_AGENT_NAME);
|
|
||||||
const tabs = getTabs(transaction);
|
|
||||||
const currentTab = getCurrentTab(tabs, urlParams.detailTab);
|
|
||||||
|
|
||||||
const discoverQuery = {
|
|
||||||
_a: {
|
|
||||||
interval: 'auto',
|
|
||||||
query: {
|
|
||||||
language: 'lucene',
|
|
||||||
query: `${PROCESSOR_EVENT}:transaction AND ${TRANSACTION_ID}:${transactionId}`
|
|
||||||
},
|
|
||||||
sort: { '@timestamp': 'desc' }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<HeaderContainer>
|
|
||||||
<HeaderMedium
|
|
||||||
css={`
|
|
||||||
margin-top: ${px(units.quarter)};
|
|
||||||
margin-bottom: 0;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
Transaction sample
|
|
||||||
</HeaderMedium>
|
|
||||||
<DiscoverButton query={discoverQuery}>
|
|
||||||
{`View transaction in Discover`}
|
|
||||||
</DiscoverButton>
|
|
||||||
</HeaderContainer>
|
|
||||||
|
|
||||||
<StickyTransactionProperties transaction={transaction} />
|
|
||||||
|
|
||||||
<TabContainer>
|
|
||||||
{[DEFAULT_TAB, ...tabs].map(key => {
|
|
||||||
return (
|
|
||||||
<Tab
|
|
||||||
onClick={() => {
|
|
||||||
history.replace({
|
|
||||||
...location,
|
|
||||||
search: fromQuery({
|
|
||||||
...toQuery(location.search),
|
|
||||||
detailTab: key
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
selected={currentTab === key}
|
|
||||||
key={key}
|
|
||||||
>
|
|
||||||
{capitalize(key)}
|
|
||||||
</Tab>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TabContainer>
|
|
||||||
|
|
||||||
<TabContentContainer>
|
|
||||||
{currentTab === DEFAULT_TAB ? (
|
|
||||||
<Spans
|
|
||||||
agentName={agentName}
|
|
||||||
agentMarks={getAgentMarks(transaction)}
|
|
||||||
droppedSpans={get(
|
|
||||||
transaction,
|
|
||||||
'transaction.spanCount.dropped.total',
|
|
||||||
0
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PropertiesTableContainer>
|
|
||||||
<PropertiesTable
|
|
||||||
propData={get(transaction.context, currentTab)}
|
|
||||||
propKey={currentTab}
|
|
||||||
agentName={agentName}
|
|
||||||
/>
|
|
||||||
</PropertiesTableContainer>
|
|
||||||
)}
|
|
||||||
</TabContentContainer>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Transaction.propTypes = {
|
|
||||||
urlParams: PropTypes.object.isRequired,
|
|
||||||
transaction: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
Transaction.defaultProps = {
|
|
||||||
transaction: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Transaction;
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { TransactionDetailsView } from 'x-pack/plugins/apm/public/components/app/TransactionDetails/view';
|
||||||
|
import { selectWaterfallRoot } from 'x-pack/plugins/apm/public/store/selectors/waterfall';
|
||||||
|
import {
|
||||||
|
getUrlParams,
|
||||||
|
IUrlParams
|
||||||
|
} from 'x-pack/plugins/apm/public/store/urlParams';
|
||||||
|
import { Transaction } from '../../../../typings/Transaction';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
location: any;
|
||||||
|
urlParams: IUrlParams;
|
||||||
|
waterfallRoot: Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: any = {}, props: Partial<Props>) {
|
||||||
|
return {
|
||||||
|
location: state.location,
|
||||||
|
urlParams: getUrlParams(state),
|
||||||
|
waterfallRoot: selectWaterfallRoot(state, props)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {};
|
||||||
|
export const TransactionDetails = connect<{}, {}, Props>(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(TransactionDetailsView);
|
|
@ -1,51 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { EuiSpacer } from '@elastic/eui';
|
|
||||||
import { HeaderLarge } from '../../shared/UIComponents';
|
|
||||||
import Transaction from './Transaction';
|
|
||||||
import Distribution from './Distribution';
|
|
||||||
import { TransactionDetailsChartsRequest } from '../../../store/reactReduxRequest/transactionDetailsCharts';
|
|
||||||
import Charts from '../../shared/charts/TransactionCharts';
|
|
||||||
import { TransactionDistributionRequest } from '../../../store/reactReduxRequest/transactionDistribution';
|
|
||||||
import { TransactionDetailsRequest } from '../../../store/reactReduxRequest/transactionDetails';
|
|
||||||
import { KueryBar } from '../../shared/KueryBar';
|
|
||||||
|
|
||||||
function TransactionDetails({ urlParams, location }) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<HeaderLarge>{urlParams.transactionName}</HeaderLarge>
|
|
||||||
|
|
||||||
<KueryBar />
|
|
||||||
|
|
||||||
<EuiSpacer size="s" />
|
|
||||||
|
|
||||||
<TransactionDetailsChartsRequest
|
|
||||||
urlParams={urlParams}
|
|
||||||
render={({ data }) => (
|
|
||||||
<Charts charts={data} urlParams={urlParams} location={location} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TransactionDistributionRequest
|
|
||||||
urlParams={urlParams}
|
|
||||||
render={({ data }) => (
|
|
||||||
<Distribution distribution={data} urlParams={urlParams} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TransactionDetailsRequest
|
|
||||||
urlParams={urlParams}
|
|
||||||
render={({ data }) => (
|
|
||||||
<Transaction transaction={data} urlParams={urlParams} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TransactionDetails;
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EuiSpacer } from '@elastic/eui';
|
||||||
|
import React from 'react';
|
||||||
|
import { RRRRenderArgs } from 'react-redux-request';
|
||||||
|
import { Transaction as ITransaction } from '../../../../typings/Transaction';
|
||||||
|
// @ts-ignore
|
||||||
|
import { TransactionDetailsRequest } from '../../../store/reactReduxRequest/transactionDetails';
|
||||||
|
// @ts-ignore
|
||||||
|
import { TransactionDetailsChartsRequest } from '../../../store/reactReduxRequest/transactionDetailsCharts';
|
||||||
|
import { TransactionDistributionRequest } from '../../../store/reactReduxRequest/transactionDistribution';
|
||||||
|
import { IUrlParams } from '../../../store/urlParams';
|
||||||
|
// @ts-ignore
|
||||||
|
import TransactionCharts from '../../shared/charts/TransactionCharts';
|
||||||
|
// @ts-ignore
|
||||||
|
import { KueryBar } from '../../shared/KueryBar';
|
||||||
|
// @ts-ignore
|
||||||
|
import { HeaderLarge } from '../../shared/UIComponents';
|
||||||
|
import { Distribution } from './Distribution';
|
||||||
|
import { Transaction } from './Transaction';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
urlParams: IUrlParams;
|
||||||
|
location: any;
|
||||||
|
waterfallRoot: ITransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TransactionDetailsView({
|
||||||
|
urlParams,
|
||||||
|
location,
|
||||||
|
waterfallRoot
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<HeaderLarge>{urlParams.transactionName}</HeaderLarge>
|
||||||
|
|
||||||
|
<KueryBar />
|
||||||
|
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
|
||||||
|
<TransactionDetailsChartsRequest
|
||||||
|
urlParams={urlParams}
|
||||||
|
render={({ data }: RRRRenderArgs<any>) => (
|
||||||
|
<TransactionCharts
|
||||||
|
charts={data}
|
||||||
|
urlParams={urlParams}
|
||||||
|
location={location}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TransactionDistributionRequest
|
||||||
|
urlParams={urlParams}
|
||||||
|
render={({ data }: RRRRenderArgs<any>) => (
|
||||||
|
<Distribution
|
||||||
|
distribution={data}
|
||||||
|
urlParams={urlParams}
|
||||||
|
location={location}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EuiSpacer size="l" />
|
||||||
|
|
||||||
|
<TransactionDetailsRequest
|
||||||
|
urlParams={urlParams}
|
||||||
|
render={(res: RRRRenderArgs<any>) => {
|
||||||
|
return (
|
||||||
|
<Transaction
|
||||||
|
location={location}
|
||||||
|
transaction={res.data}
|
||||||
|
urlParams={urlParams}
|
||||||
|
waterfallRoot={waterfallRoot}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,11 +4,8 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { EuiBasicTable } from '@elastic/eui';
|
|
||||||
import orderBy from 'lodash.orderby';
|
|
||||||
import TooltipOverlay from '../../../shared/TooltipOverlay';
|
import TooltipOverlay from '../../../shared/TooltipOverlay';
|
||||||
import { RelativeLink, legacyEncodeURIComponent } from '../../../../utils/url';
|
import { RelativeLink, legacyEncodeURIComponent } from '../../../../utils/url';
|
||||||
import {
|
import {
|
||||||
|
@ -16,9 +13,10 @@ import {
|
||||||
asDecimal,
|
asDecimal,
|
||||||
tpmUnit
|
tpmUnit
|
||||||
} from '../../../../utils/formatters';
|
} from '../../../../utils/formatters';
|
||||||
|
import { ImpactBar } from '../../../shared/ImpactBar';
|
||||||
|
|
||||||
import { fontFamilyCode, truncate } from '../../../../style/variables';
|
import { fontFamilyCode, truncate } from '../../../../style/variables';
|
||||||
import ImpactSparkline from './ImpactSparkLine';
|
import { ManagedTable } from '../../../shared/ManagedTable';
|
||||||
|
|
||||||
function tpmLabel(type) {
|
function tpmLabel(type) {
|
||||||
return type === 'request' ? 'Req. per minute' : 'Trans. per minute';
|
return type === 'request' ? 'Req. per minute' : 'Trans. per minute';
|
||||||
|
@ -28,129 +26,75 @@ function avgLabel(agentName) {
|
||||||
return agentName === 'js-base' ? 'Page load time' : 'Avg. resp. time';
|
return agentName === 'js-base' ? 'Page load time' : 'Avg. resp. time';
|
||||||
}
|
}
|
||||||
|
|
||||||
function paginateItems({ items, pageIndex, pageSize }) {
|
|
||||||
return items.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
const TransactionNameLink = styled(RelativeLink)`
|
const TransactionNameLink = styled(RelativeLink)`
|
||||||
${truncate('100%')};
|
${truncate('100%')};
|
||||||
font-family: ${fontFamilyCode};
|
font-family: ${fontFamilyCode};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
class List extends Component {
|
export default function TransactionList({
|
||||||
state = {
|
items,
|
||||||
page: {
|
agentName,
|
||||||
index: 0,
|
serviceName,
|
||||||
size: 25
|
type,
|
||||||
},
|
...rest
|
||||||
sort: {
|
}) {
|
||||||
field: 'impactRelative',
|
const columns = [
|
||||||
direction: 'desc'
|
{
|
||||||
}
|
field: 'name',
|
||||||
};
|
name: 'Name',
|
||||||
|
width: '50%',
|
||||||
|
sortable: true,
|
||||||
|
render: transactionName => {
|
||||||
|
const transactionUrl = `${serviceName}/transactions/${legacyEncodeURIComponent(
|
||||||
|
type
|
||||||
|
)}/${legacyEncodeURIComponent(transactionName)}`;
|
||||||
|
|
||||||
onTableChange = ({ page = {}, sort = {} }) => {
|
return (
|
||||||
this.setState({ page, sort });
|
<TooltipOverlay content={transactionName || 'N/A'}>
|
||||||
};
|
<TransactionNameLink path={`/${transactionUrl}`}>
|
||||||
|
{transactionName || 'N/A'}
|
||||||
render() {
|
</TransactionNameLink>
|
||||||
const { agentName, serviceName, type } = this.props;
|
</TooltipOverlay>
|
||||||
|
);
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
field: 'name',
|
|
||||||
name: 'Name',
|
|
||||||
width: '50%',
|
|
||||||
sortable: true,
|
|
||||||
render: transactionName => {
|
|
||||||
const transactionUrl = `${serviceName}/transactions/${legacyEncodeURIComponent(
|
|
||||||
type
|
|
||||||
)}/${legacyEncodeURIComponent(transactionName)}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipOverlay content={transactionName || 'N/A'}>
|
|
||||||
<TransactionNameLink path={`/${transactionUrl}`}>
|
|
||||||
{transactionName || 'N/A'}
|
|
||||||
</TransactionNameLink>
|
|
||||||
</TooltipOverlay>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'avg',
|
|
||||||
name: avgLabel(agentName),
|
|
||||||
sortable: true,
|
|
||||||
dataType: 'number',
|
|
||||||
render: value => asMillisWithDefault(value)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'p95',
|
|
||||||
name: '95th percentile',
|
|
||||||
sortable: true,
|
|
||||||
dataType: 'number',
|
|
||||||
render: value => asMillisWithDefault(value)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'tpm',
|
|
||||||
name: tpmLabel(type),
|
|
||||||
sortable: true,
|
|
||||||
dataType: 'number',
|
|
||||||
render: value => `${asDecimal(value)} ${tpmUnit(type)}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'impactRelative',
|
|
||||||
name: 'Impact',
|
|
||||||
sortable: true,
|
|
||||||
dataType: 'number',
|
|
||||||
render: value => <ImpactSparkline impact={value} />
|
|
||||||
}
|
}
|
||||||
];
|
},
|
||||||
|
{
|
||||||
|
field: 'averageResponseTime',
|
||||||
|
name: avgLabel(agentName),
|
||||||
|
sortable: true,
|
||||||
|
dataType: 'number',
|
||||||
|
render: value => asMillisWithDefault(value)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'p95',
|
||||||
|
name: '95th percentile',
|
||||||
|
sortable: true,
|
||||||
|
dataType: 'number',
|
||||||
|
render: value => asMillisWithDefault(value)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'transactionsPerMinute',
|
||||||
|
name: tpmLabel(type),
|
||||||
|
sortable: true,
|
||||||
|
dataType: 'number',
|
||||||
|
render: value => `${asDecimal(value)} ${tpmUnit(type)}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'impact',
|
||||||
|
name: 'Impact',
|
||||||
|
sortable: true,
|
||||||
|
dataType: 'number',
|
||||||
|
render: value => <ImpactBar value={value} />
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const sortedItems = orderBy(
|
return (
|
||||||
this.props.items,
|
<ManagedTable
|
||||||
this.state.sort.field,
|
columns={columns}
|
||||||
this.state.sort.direction
|
items={items}
|
||||||
);
|
initialSort={{ field: 'impact', direction: 'desc' }}
|
||||||
|
initialPageSize={25}
|
||||||
const paginatedItems = paginateItems({
|
{...rest}
|
||||||
items: sortedItems,
|
/>
|
||||||
pageIndex: this.state.page.index,
|
);
|
||||||
pageSize: this.state.page.size
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EuiBasicTable
|
|
||||||
noItemsMessage="No transactions found"
|
|
||||||
items={paginatedItems}
|
|
||||||
columns={columns}
|
|
||||||
pagination={{
|
|
||||||
pageIndex: this.state.page.index,
|
|
||||||
pageSize: this.state.page.size,
|
|
||||||
totalItemCount: this.props.items.length
|
|
||||||
}}
|
|
||||||
sorting={{
|
|
||||||
sort: {
|
|
||||||
field: this.state.sort.field,
|
|
||||||
direction: this.state.sort.direction
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={this.onTableChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List.propTypes = {
|
|
||||||
agentName: PropTypes.string,
|
|
||||||
items: PropTypes.array,
|
|
||||||
serviceName: PropTypes.string,
|
|
||||||
type: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default List;
|
|
||||||
|
|
||||||
// const renderFooterText = () => {
|
|
||||||
// return items.length === 500
|
|
||||||
// ? 'Showing first 500 results ordered by response time'
|
|
||||||
// : '';
|
|
||||||
// };
|
|
||||||
|
|
|
@ -8,9 +8,14 @@ import React from 'react';
|
||||||
import { KibanaLink } from '../../utils/url';
|
import { KibanaLink } from '../../utils/url';
|
||||||
import { EuiButton } from '@elastic/eui';
|
import { EuiButton } from '@elastic/eui';
|
||||||
|
|
||||||
function DiscoverButton({ query, children }) {
|
function DiscoverButton({ query, children, ...rest }) {
|
||||||
return (
|
return (
|
||||||
<KibanaLink pathname={'/app/kibana'} hash={'/discover'} query={query}>
|
<KibanaLink
|
||||||
|
pathname={'/app/kibana'}
|
||||||
|
hash={'/discover'}
|
||||||
|
query={query}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
<EuiButton iconType="discoverApp">
|
<EuiButton iconType="discoverApp">
|
||||||
{children || 'View in Discover'}
|
{children || 'View in Discover'}
|
||||||
</EuiButton>
|
</EuiButton>
|
||||||
|
|
|
@ -4,15 +4,14 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
function EmptyMessage({ heading, subheading, hideSubheading }) {
|
function EmptyMessage({
|
||||||
if (!subheading) {
|
heading = 'No data found.',
|
||||||
subheading = 'Try another time range or reset the search filter.';
|
subheading = 'Try another time range or reset the search filter.',
|
||||||
}
|
hideSubheading = false
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<EuiEmptyPrompt
|
<EuiEmptyPrompt
|
||||||
titleSize="s"
|
titleSize="s"
|
||||||
|
@ -22,13 +21,5 @@ function EmptyMessage({ heading, subheading, hideSubheading }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
EmptyMessage.propTypes = {
|
// tslint:disable-next-line:no-default-export
|
||||||
heading: PropTypes.string,
|
|
||||||
hideSubheading: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
EmptyMessage.defaultProps = {
|
|
||||||
hideSubheading: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmptyMessage;
|
export default EmptyMessage;
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import { ImpactBar } from '..';
|
||||||
|
|
||||||
|
describe('ImpactBar component', () => {
|
||||||
|
it('should render with default values', () => {
|
||||||
|
expect(shallow(<ImpactBar value={25} />)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render with overridden values', () => {
|
||||||
|
expect(
|
||||||
|
shallow(<ImpactBar value={2} max={5} color="danger" size="s" />)
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,21 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ImpactBar component should render with default values 1`] = `
|
||||||
|
<EuiProgress
|
||||||
|
color="primary"
|
||||||
|
max={100}
|
||||||
|
position="static"
|
||||||
|
size="l"
|
||||||
|
value={25}
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`ImpactBar component should render with overridden values 1`] = `
|
||||||
|
<EuiProgress
|
||||||
|
color="danger"
|
||||||
|
max={5}
|
||||||
|
position="static"
|
||||||
|
size="s"
|
||||||
|
value={2}
|
||||||
|
/>
|
||||||
|
`;
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EuiProgress } from '@elastic/eui';
|
||||||
|
import React from 'react';
|
||||||
|
import { StringMap } from '../../../../typings/common';
|
||||||
|
|
||||||
|
// TODO: extend from EUI's EuiProgress prop interface
|
||||||
|
export interface ImpactBarProps extends StringMap<any> {
|
||||||
|
value: number;
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImpactBar({ value, max = 100, ...rest }: ImpactBarProps) {
|
||||||
|
return (
|
||||||
|
<EuiProgress size="l" value={value} max={max} color="primary" {...rest} />
|
||||||
|
);
|
||||||
|
}
|
|
@ -5,8 +5,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import view from './view';
|
|
||||||
import { getUrlParams } from '../../../store/urlParams';
|
import { getUrlParams } from '../../../store/urlParams';
|
||||||
|
import view from './view';
|
||||||
|
|
||||||
function mapStateToProps(state = {}) {
|
function mapStateToProps(state = {}) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import { ManagedTable } from '..';
|
||||||
|
|
||||||
|
describe('ManagedTable component', () => {
|
||||||
|
let people;
|
||||||
|
let columns;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
people = [
|
||||||
|
{ name: 'Jess', age: 29 },
|
||||||
|
{ name: 'Becky', age: 43 },
|
||||||
|
{ name: 'Thomas', age: 31 }
|
||||||
|
];
|
||||||
|
columns = [
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
name: 'Name',
|
||||||
|
sortable: true,
|
||||||
|
render: name => `Name: ${name}`
|
||||||
|
},
|
||||||
|
{ field: 'age', name: 'Age', render: age => `Age: ${age}` }
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a page-full of items, with defaults', () => {
|
||||||
|
expect(
|
||||||
|
shallow(<ManagedTable columns={columns} items={people} />)
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render when specifying initial values', () => {
|
||||||
|
expect(
|
||||||
|
shallow(
|
||||||
|
<ManagedTable
|
||||||
|
columns={columns}
|
||||||
|
items={people}
|
||||||
|
initialSort={{ field: 'age', direction: 'desc' }}
|
||||||
|
initialPageIndex={1}
|
||||||
|
initialPageSize={2}
|
||||||
|
hidePerPageOptions={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,103 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ManagedTable component should render a page-full of items, with defaults 1`] = `
|
||||||
|
<EuiBasicTable
|
||||||
|
columns={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"field": "name",
|
||||||
|
"name": "Name",
|
||||||
|
"render": [Function],
|
||||||
|
"sortable": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"field": "age",
|
||||||
|
"name": "Age",
|
||||||
|
"render": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
items={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"age": 43,
|
||||||
|
"name": "Becky",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"age": 29,
|
||||||
|
"name": "Jess",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"age": 31,
|
||||||
|
"name": "Thomas",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
noItemsMessage="No items found"
|
||||||
|
onChange={[Function]}
|
||||||
|
pagination={
|
||||||
|
Object {
|
||||||
|
"hidePerPageOptions": true,
|
||||||
|
"pageIndex": 0,
|
||||||
|
"pageSize": 10,
|
||||||
|
"totalItemCount": 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responsive={true}
|
||||||
|
sorting={
|
||||||
|
Object {
|
||||||
|
"sort": Object {
|
||||||
|
"direction": "asc",
|
||||||
|
"field": "name",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`ManagedTable component should render when specifying initial values 1`] = `
|
||||||
|
<EuiBasicTable
|
||||||
|
columns={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"field": "name",
|
||||||
|
"name": "Name",
|
||||||
|
"render": [Function],
|
||||||
|
"sortable": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"field": "age",
|
||||||
|
"name": "Age",
|
||||||
|
"render": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
items={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"age": 29,
|
||||||
|
"name": "Jess",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
noItemsMessage="No items found"
|
||||||
|
onChange={[Function]}
|
||||||
|
pagination={
|
||||||
|
Object {
|
||||||
|
"hidePerPageOptions": false,
|
||||||
|
"pageIndex": 1,
|
||||||
|
"pageSize": 2,
|
||||||
|
"totalItemCount": 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responsive={true}
|
||||||
|
sorting={
|
||||||
|
Object {
|
||||||
|
"sort": Object {
|
||||||
|
"direction": "desc",
|
||||||
|
"field": "age",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
`;
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { EuiBasicTable } from '@elastic/eui';
|
||||||
|
import { get, sortByOrder } from 'lodash';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { StringMap } from '../../../../typings/common';
|
||||||
|
|
||||||
|
// TODO: this should really be imported from EUI
|
||||||
|
export interface ITableColumn {
|
||||||
|
field: string;
|
||||||
|
name: string;
|
||||||
|
dataType?: string;
|
||||||
|
align?: string;
|
||||||
|
width?: string;
|
||||||
|
sortable?: boolean;
|
||||||
|
render: (value: any, item?: any) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IManagedTableProps {
|
||||||
|
items: Array<StringMap<any>>;
|
||||||
|
columns: ITableColumn[];
|
||||||
|
initialPageIndex?: number;
|
||||||
|
initialPageSize?: number;
|
||||||
|
hidePerPageOptions?: boolean;
|
||||||
|
initialSort?: {
|
||||||
|
field: string;
|
||||||
|
direction: 'asc' | 'desc';
|
||||||
|
};
|
||||||
|
noItemsMessage?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ManagedTable extends Component<IManagedTableProps, any> {
|
||||||
|
constructor(props: IManagedTableProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const defaultSort = {
|
||||||
|
field: get(props, 'columns[0].field', ''),
|
||||||
|
direction: 'asc'
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
initialPageIndex = 0,
|
||||||
|
initialPageSize = 10,
|
||||||
|
initialSort = defaultSort
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
page: { index: initialPageIndex, size: initialPageSize },
|
||||||
|
sort: initialSort
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public onTableChange = ({ page = {}, sort = {} }) => {
|
||||||
|
this.setState({ page, sort });
|
||||||
|
};
|
||||||
|
|
||||||
|
public getCurrentItems() {
|
||||||
|
const { items } = this.props;
|
||||||
|
const { sort = {}, page = {} } = this.state;
|
||||||
|
// TODO: Use _.orderBy once we upgrade to lodash 4+
|
||||||
|
const sorted = sortByOrder(items, sort.field, sort.direction);
|
||||||
|
return sorted.slice(page.index * page.size, (page.index + 1) * page.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const {
|
||||||
|
columns,
|
||||||
|
noItemsMessage,
|
||||||
|
items,
|
||||||
|
hidePerPageOptions = true
|
||||||
|
} = this.props;
|
||||||
|
const { page, sort } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiBasicTable
|
||||||
|
noItemsMessage={noItemsMessage}
|
||||||
|
items={this.getCurrentItems()}
|
||||||
|
columns={columns}
|
||||||
|
pagination={{
|
||||||
|
hidePerPageOptions,
|
||||||
|
totalItemCount: items.length,
|
||||||
|
pageIndex: page.index,
|
||||||
|
pageSize: page.size
|
||||||
|
}}
|
||||||
|
sorting={{ sort }}
|
||||||
|
onChange={this.onTableChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,127 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import Portal from 'react-portal';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { Close } from './Icons';
|
|
||||||
import { fontSizes, units, colors } from '../../style/variables';
|
|
||||||
import { rgba } from 'polished';
|
|
||||||
|
|
||||||
const Header = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HeaderTitle = styled.div`
|
|
||||||
font-size: ${fontSizes.xlarge};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CloseButton = styled(Close)`
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: ${fontSizes.large};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ModalFixed = styled.div`
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ModalOverlay = styled(ModalFixed)`
|
|
||||||
z-index: 10;
|
|
||||||
background: ${rgba(colors.gray2, 0.8)};
|
|
||||||
height: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ModalOuterContainer = styled(ModalFixed)`
|
|
||||||
z-index: 20;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ModalInnerContainer = styled.div`
|
|
||||||
position: relative;
|
|
||||||
background: white;
|
|
||||||
min-width: 800px;
|
|
||||||
width: 80%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
padding: ${units.double}px;
|
|
||||||
border-radius: ${units.quarter}px;
|
|
||||||
margin: ${units.quadruple}px 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
class Modal extends React.Component {
|
|
||||||
shouldComponentUpdate(nextProps) {
|
|
||||||
// TODO: Make sure this doesn't cause rendering issues
|
|
||||||
return this.props.isOpen !== nextProps.isOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpen = () => {
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
this.props.onOpen();
|
|
||||||
};
|
|
||||||
|
|
||||||
onClose = () => {
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
this.props.onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
close = () => this.props.close();
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!this.props.isOpen) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Portal
|
|
||||||
onClose={this.onClose}
|
|
||||||
onOpen={this.onOpen}
|
|
||||||
closeOnEsc
|
|
||||||
isOpened={this.props.isOpen}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalOuterContainer onClick={this.close}>
|
|
||||||
<ModalInnerContainer onClick={e => e.stopPropagation()}>
|
|
||||||
<Header>
|
|
||||||
<HeaderTitle>{this.props.header}</HeaderTitle>
|
|
||||||
<CloseButton onClick={this.close} />
|
|
||||||
</Header>
|
|
||||||
{this.props.children}
|
|
||||||
</ModalInnerContainer>
|
|
||||||
</ModalOuterContainer>
|
|
||||||
</div>
|
|
||||||
</Portal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Modal.propTypes = {
|
|
||||||
children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
|
||||||
onOpen: PropTypes.func,
|
|
||||||
onClose: PropTypes.func,
|
|
||||||
close: PropTypes.func,
|
|
||||||
isOpen: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
Modal.defaultProps = {
|
|
||||||
onOpen: () => {},
|
|
||||||
onClose: () => {},
|
|
||||||
close: () => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Modal;
|
|
|
@ -7,6 +7,8 @@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { StringMap } from '../../../../typings/common';
|
||||||
import {
|
import {
|
||||||
colors,
|
colors,
|
||||||
fontFamilyCode,
|
fontFamilyCode,
|
||||||
|
@ -15,6 +17,8 @@ import {
|
||||||
units
|
units
|
||||||
} from '../../../style/variables';
|
} from '../../../style/variables';
|
||||||
|
|
||||||
|
export type KeySorter = (data: StringMap<any>, parentKey?: string) => string[];
|
||||||
|
|
||||||
const Table = styled.table`
|
const Table = styled.table`
|
||||||
font-family: ${fontFamilyCode};
|
font-family: ${fontFamilyCode};
|
||||||
font-size: ${fontSizes.small};
|
font-size: ${fontSizes.small};
|
||||||
|
|
|
@ -12,9 +12,9 @@ import {
|
||||||
sortKeysByConfig,
|
sortKeysByConfig,
|
||||||
getPropertyTabNames
|
getPropertyTabNames
|
||||||
} from '..';
|
} from '..';
|
||||||
import { getFeatureDocs } from '../../../../utils/documentation';
|
import { getAgentFeatureDocsUrl } from '../../../../utils/documentation/agents';
|
||||||
|
|
||||||
jest.mock('../../../../utils/documentation');
|
jest.mock('../../../../utils/documentation/agents');
|
||||||
jest.mock('../propertyConfig.json', () => [
|
jest.mock('../propertyConfig.json', () => [
|
||||||
{
|
{
|
||||||
key: 'testProperty',
|
key: 'testProperty',
|
||||||
|
@ -105,32 +105,13 @@ describe('getPropertyTabNames', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('AgentFeatureTipMessage component', () => {
|
describe('AgentFeatureTipMessage component', () => {
|
||||||
let mockDocs;
|
const featureName = 'user';
|
||||||
const featureName = '';
|
const agentName = 'nodejs';
|
||||||
const agentName = '';
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockDocs = {
|
|
||||||
text: 'Mock Docs Text',
|
|
||||||
url: 'mock-url'
|
|
||||||
};
|
|
||||||
getFeatureDocs.mockImplementation(() => mockDocs);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render when docs are returned', () => {
|
it('should render when docs are returned', () => {
|
||||||
expect(
|
const mockDocs = 'mock-url';
|
||||||
shallow(
|
getAgentFeatureDocsUrl.mockImplementation(() => mockDocs);
|
||||||
<AgentFeatureTipMessage
|
|
||||||
featureName={featureName}
|
|
||||||
agentName={agentName}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
).toMatchSnapshot();
|
|
||||||
expect(getFeatureDocs).toHaveBeenCalledWith(featureName, agentName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render when docs are returned, but missing a url', () => {
|
|
||||||
delete mockDocs.url;
|
|
||||||
expect(
|
expect(
|
||||||
shallow(
|
shallow(
|
||||||
<AgentFeatureTipMessage
|
<AgentFeatureTipMessage
|
||||||
|
@ -139,10 +120,11 @@ describe('AgentFeatureTipMessage component', () => {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
).toMatchSnapshot();
|
).toMatchSnapshot();
|
||||||
|
expect(getAgentFeatureDocsUrl).toHaveBeenCalledWith(featureName, agentName);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render null empty string when no docs are returned', () => {
|
it('should render null empty string when no docs are returned', () => {
|
||||||
mockDocs = null;
|
getAgentFeatureDocsUrl.mockImplementation(() => null);
|
||||||
expect(
|
expect(
|
||||||
shallow(
|
shallow(
|
||||||
<AgentFeatureTipMessage
|
<AgentFeatureTipMessage
|
||||||
|
|
|
@ -8,7 +8,7 @@ exports[`AgentFeatureTipMessage component should render when docs are returned 1
|
||||||
size="m"
|
size="m"
|
||||||
type="iInCircle"
|
type="iInCircle"
|
||||||
/>
|
/>
|
||||||
Mock Docs Text
|
You can configure your agent to add contextual information about your users.
|
||||||
|
|
||||||
<ExternalLink
|
<ExternalLink
|
||||||
href="mock-url"
|
href="mock-url"
|
||||||
|
@ -18,17 +18,6 @@ exports[`AgentFeatureTipMessage component should render when docs are returned 1
|
||||||
</styled.div>
|
</styled.div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`AgentFeatureTipMessage component should render when docs are returned, but missing a url 1`] = `
|
|
||||||
<styled.div>
|
|
||||||
<EuiIcon
|
|
||||||
size="m"
|
|
||||||
type="iInCircle"
|
|
||||||
/>
|
|
||||||
Mock Docs Text
|
|
||||||
|
|
||||||
</styled.div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`PropertiesTable component should render empty when data has no keys 1`] = `
|
exports[`PropertiesTable component should render empty when data has no keys 1`] = `
|
||||||
<styled.div>
|
<styled.div>
|
||||||
<styled.div>
|
<styled.div>
|
||||||
|
@ -68,7 +57,7 @@ exports[`PropertiesTable component should render with data 1`] = `
|
||||||
/>
|
/>
|
||||||
<AgentFeatureTipMessage
|
<AgentFeatureTipMessage
|
||||||
agentName="testAgentName"
|
agentName="testAgentName"
|
||||||
featureName="context-testPropKey"
|
featureName="testPropKey"
|
||||||
/>
|
/>
|
||||||
</styled.div>
|
</styled.div>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -8,11 +8,13 @@ import { EuiIcon } from '@elastic/eui';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { StringMap } from '../../../../typings/common';
|
||||||
import { colors, fontSize, px, unit, units } from '../../../style/variables';
|
import { colors, fontSize, px, unit, units } from '../../../style/variables';
|
||||||
import { getFeatureDocs } from '../../../utils/documentation';
|
import { getAgentFeatureDocsUrl } from '../../../utils/documentation/agents';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { ExternalLink } from '../../../utils/url';
|
import { ExternalLink } from '../../../utils/url';
|
||||||
import { NestedKeyValueTable } from './NestedKeyValueTable';
|
import { KeySorter, NestedKeyValueTable } from './NestedKeyValueTable';
|
||||||
import PROPERTY_CONFIG from './propertyConfig.json';
|
import PROPERTY_CONFIG from './propertyConfig.json';
|
||||||
|
|
||||||
const indexedPropertyConfig = _.indexBy(PROPERTY_CONFIG, 'key');
|
const indexedPropertyConfig = _.indexBy(PROPERTY_CONFIG, 'key');
|
||||||
|
@ -36,28 +38,36 @@ export function getPropertyTabNames(selected: string[]): string[] {
|
||||||
).map(({ key }: { key: string }) => key);
|
).map(({ key }: { key: string }) => key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAgentFeatureText(featureName: string) {
|
||||||
|
switch (featureName) {
|
||||||
|
case 'user':
|
||||||
|
return 'You can configure your agent to add contextual information about your users.';
|
||||||
|
case 'tags':
|
||||||
|
return 'You can configure your agent to add filterable tags on transactions.';
|
||||||
|
case 'custom':
|
||||||
|
return 'You can configure your agent to add custom contextual information on transactions.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function AgentFeatureTipMessage({
|
export function AgentFeatureTipMessage({
|
||||||
featureName,
|
featureName,
|
||||||
agentName
|
agentName
|
||||||
}: {
|
}: {
|
||||||
featureName: string;
|
featureName: string;
|
||||||
agentName: string;
|
agentName?: string;
|
||||||
}): JSX.Element | null {
|
}) {
|
||||||
const docs = getFeatureDocs(featureName, agentName);
|
const docsUrl = getAgentFeatureDocsUrl(featureName, agentName);
|
||||||
|
if (!docsUrl) {
|
||||||
if (!docs) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableInfo>
|
<TableInfo>
|
||||||
<EuiIcon type="iInCircle" />
|
<EuiIcon type="iInCircle" />
|
||||||
{docs.text}{' '}
|
{getAgentFeatureText(featureName)}{' '}
|
||||||
{docs.url && (
|
<ExternalLink href={docsUrl}>
|
||||||
<ExternalLink href={docs.url}>
|
Learn more in the documentation.
|
||||||
Learn more in the documentation.
|
</ExternalLink>
|
||||||
</ExternalLink>
|
|
||||||
)}
|
|
||||||
</TableInfo>
|
</TableInfo>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -78,7 +88,7 @@ export function PropertiesTable({
|
||||||
}: {
|
}: {
|
||||||
propData: StringMap<any>;
|
propData: StringMap<any>;
|
||||||
propKey: string;
|
propKey: string;
|
||||||
agentName: string;
|
agentName?: string;
|
||||||
}) {
|
}) {
|
||||||
if (_.isEmpty(propData)) {
|
if (_.isEmpty(propData)) {
|
||||||
return (
|
return (
|
||||||
|
@ -98,10 +108,7 @@ export function PropertiesTable({
|
||||||
keySorter={sortKeysByConfig}
|
keySorter={sortKeysByConfig}
|
||||||
depth={1}
|
depth={1}
|
||||||
/>
|
/>
|
||||||
<AgentFeatureTipMessage
|
<AgentFeatureTipMessage featureName={propKey} agentName={agentName} />
|
||||||
featureName={`context-${propKey}`}
|
|
||||||
agentName={agentName}
|
|
||||||
/>
|
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EuiButton } from '@elastic/eui';
|
||||||
|
import React from 'react';
|
||||||
|
// @ts-ignore
|
||||||
|
import { KibanaLink } from '../../utils/url';
|
||||||
|
|
||||||
|
export function SetupInstructionsLink({
|
||||||
|
buttonFill = false
|
||||||
|
}: {
|
||||||
|
buttonFill?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<KibanaLink pathname={'/app/kibana'} hash={'/home/tutorial/apm'}>
|
||||||
|
<EuiButton size="s" color="primary" fill={buttonFill}>
|
||||||
|
Setup Instructions
|
||||||
|
</EuiButton>
|
||||||
|
</KibanaLink>
|
||||||
|
);
|
||||||
|
}
|
|
@ -11,8 +11,7 @@ import CodePreview from '../../shared/CodePreview';
|
||||||
import { Ellipsis } from '../../shared/Icons';
|
import { Ellipsis } from '../../shared/Icons';
|
||||||
import { units, px } from '../../../style/variables';
|
import { units, px } from '../../../style/variables';
|
||||||
import EmptyMessage from '../../shared/EmptyMessage';
|
import EmptyMessage from '../../shared/EmptyMessage';
|
||||||
import { EuiLink } from '@elastic/eui';
|
import { EuiLink, EuiTitle } from '@elastic/eui';
|
||||||
import { HeaderXSmall } from '../UIComponents';
|
|
||||||
|
|
||||||
const LibraryFrameToggle = styled.div`
|
const LibraryFrameToggle = styled.div`
|
||||||
margin: 0 0 ${px(units.plus)} 0;
|
margin: 0 0 ${px(units.plus)} 0;
|
||||||
|
@ -75,7 +74,9 @@ class Stacktrace extends PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<HeaderXSmall>Stacktraces</HeaderXSmall>
|
<EuiTitle size="xs">
|
||||||
|
<h3>Stack traces</h3>
|
||||||
|
</EuiTitle>
|
||||||
{getCollapsedLibraryFrames(stackframes).map((item, i) => {
|
{getCollapsedLibraryFrames(stackframes).map((item, i) => {
|
||||||
if (!item.libraryFrame) {
|
if (!item.libraryFrame) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -2,46 +2,25 @@
|
||||||
|
|
||||||
exports[`StickyProperties should render 1`] = `
|
exports[`StickyProperties should render 1`] = `
|
||||||
.c0 {
|
.c0 {
|
||||||
display: -webkit-box;
|
|
||||||
display: -webkit-flex;
|
|
||||||
display: -ms-flexbox;
|
|
||||||
display: flex;
|
|
||||||
padding: 0 24px;
|
|
||||||
width: 100%;
|
|
||||||
-webkit-box-pack: start;
|
|
||||||
-webkit-justify-content: flex-start;
|
|
||||||
-ms-flex-pack: start;
|
|
||||||
justify-content: flex-start;
|
|
||||||
-webkit-flex-wrap: wrap;
|
|
||||||
-ms-flex-wrap: wrap;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c1 {
|
|
||||||
width: 33%;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c2 {
|
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999999;
|
color: #999999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c2 span {
|
.c0 span {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c4 {
|
.c2 {
|
||||||
color: #999999;
|
color: #999999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c3 {
|
.c1 {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c5 {
|
.c3 {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
@ -51,13 +30,25 @@ exports[`StickyProperties should render 1`] = `
|
||||||
}
|
}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="c0"
|
className="euiFlexGroup euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginBottom": "-1em",
|
||||||
|
"marginTop": "-1em",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="c1"
|
className="euiFlexItem"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"minWidth": 0,
|
||||||
|
"padding": "1em 1em 1em 0",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="c2"
|
className="c0"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-describedby="overlay1"
|
aria-describedby="overlay1"
|
||||||
|
@ -68,12 +59,12 @@ exports[`StickyProperties should render 1`] = `
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="c3"
|
className="c1"
|
||||||
>
|
>
|
||||||
1337 minutes ago (mocking 1536405447)
|
1337 minutes ago (mocking 1536405447)
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className="c4"
|
className="c2"
|
||||||
>
|
>
|
||||||
(
|
(
|
||||||
1st of January (mocking 1536405447)
|
1st of January (mocking 1536405447)
|
||||||
|
@ -82,10 +73,16 @@ exports[`StickyProperties should render 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="c1"
|
className="euiFlexItem"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"minWidth": 0,
|
||||||
|
"padding": "1em 1em 1em 0",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="c2"
|
className="c0"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-describedby="overlay2"
|
aria-describedby="overlay2"
|
||||||
|
@ -97,7 +94,7 @@ exports[`StickyProperties should render 1`] = `
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
aria-describedby="overlay3"
|
aria-describedby="overlay3"
|
||||||
className="c5"
|
className="c3"
|
||||||
onMouseOut={[Function]}
|
onMouseOut={[Function]}
|
||||||
onMouseOver={[Function]}
|
onMouseOver={[Function]}
|
||||||
>
|
>
|
||||||
|
@ -105,10 +102,16 @@ exports[`StickyProperties should render 1`] = `
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="c1"
|
className="euiFlexItem"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"minWidth": 0,
|
||||||
|
"padding": "1em 1em 1em 0",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="c2"
|
className="c0"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-describedby="overlay4"
|
aria-describedby="overlay4"
|
||||||
|
@ -119,16 +122,22 @@ exports[`StickyProperties should render 1`] = `
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="c3"
|
className="c1"
|
||||||
>
|
>
|
||||||
GET
|
GET
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="c1"
|
className="euiFlexItem"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"minWidth": 0,
|
||||||
|
"padding": "1em 1em 1em 0",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="c2"
|
className="c0"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-describedby="overlay5"
|
aria-describedby="overlay5"
|
||||||
|
@ -139,16 +148,22 @@ exports[`StickyProperties should render 1`] = `
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="c3"
|
className="c1"
|
||||||
>
|
>
|
||||||
true
|
true
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="c1"
|
className="euiFlexItem"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"minWidth": 0,
|
||||||
|
"padding": "1em 1em 1em 0",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="c2"
|
className="c0"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-describedby="overlay6"
|
aria-describedby="overlay6"
|
||||||
|
@ -159,7 +174,7 @@ exports[`StickyProperties should render 1`] = `
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="c3"
|
className="c1"
|
||||||
>
|
>
|
||||||
1337
|
1337
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,32 +4,23 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import TooltipOverlay from '../../shared/TooltipOverlay';
|
||||||
import {
|
import {
|
||||||
unit,
|
unit,
|
||||||
units,
|
units,
|
||||||
px,
|
px,
|
||||||
|
fontFamilyCode,
|
||||||
fontSizes,
|
fontSizes,
|
||||||
colors,
|
colors,
|
||||||
truncate
|
truncate
|
||||||
} from '../../../style/variables';
|
} from '../../../style/variables';
|
||||||
|
|
||||||
import TooltipOverlay, { fieldNameHelper } from '../../shared/TooltipOverlay';
|
const TooltipFieldName = styled.span`
|
||||||
|
font-family: ${fontFamilyCode};
|
||||||
const PropertiesContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
padding: 0 ${px(units.plus)};
|
|
||||||
width: 100%;
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Property = styled.div`
|
|
||||||
width: 33%;
|
|
||||||
margin-bottom: ${px(unit)};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const PropertyLabel = styled.div`
|
const PropertyLabel = styled.div`
|
||||||
|
@ -57,6 +48,15 @@ const PropertyValueTruncated = styled.span`
|
||||||
${truncate('100%')};
|
${truncate('100%')};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
function fieldNameHelper(name) {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
Field name: <br />
|
||||||
|
<TooltipFieldName>{name}</TooltipFieldName>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TimestampValue({ timestamp }) {
|
function TimestampValue({ timestamp }) {
|
||||||
const time = moment(timestamp);
|
const time = moment(timestamp);
|
||||||
const timeAgo = timestamp ? time.fromNow() : 'N/A';
|
const timeAgo = timestamp ? time.fromNow() : 'N/A';
|
||||||
|
@ -98,19 +98,46 @@ function getPropertyValue({ val, fieldName, truncated = false }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <PropertyValue>{String(val)}</PropertyValue>;
|
return <PropertyValue>{val}</PropertyValue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StickyProperties({ stickyProperties }) {
|
export function StickyProperties({ stickyProperties }) {
|
||||||
|
/**
|
||||||
|
* Note: the padding and margin styles here are strange because
|
||||||
|
* EUI flex groups and items have a default "gutter" applied that
|
||||||
|
* won't allow percentage widths to line up correctly, so we have
|
||||||
|
* to turn the gutter off with gutterSize: none. When we do that,
|
||||||
|
* the top/bottom spacing *also* collapses, so we have to add
|
||||||
|
* padding between each item without adding it to the outside of
|
||||||
|
* the flex group itself.
|
||||||
|
*
|
||||||
|
* Hopefully we can make EUI handle this better and remove all this.
|
||||||
|
*/
|
||||||
|
const itemStyles = {
|
||||||
|
padding: '1em 1em 1em 0'
|
||||||
|
};
|
||||||
|
const groupStyles = {
|
||||||
|
marginTop: '-1em',
|
||||||
|
marginBottom: '-1em'
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PropertiesContainer>
|
<EuiFlexGroup wrap={true} gutterSize="none" style={groupStyles}>
|
||||||
{stickyProperties &&
|
{stickyProperties &&
|
||||||
stickyProperties.map((prop, i) => (
|
stickyProperties.map(({ width = 0, ...prop }, i) => {
|
||||||
<Property key={i}>
|
return (
|
||||||
{getPropertyLabel(prop)}
|
<EuiFlexItem
|
||||||
{getPropertyValue(prop)}
|
key={i}
|
||||||
</Property>
|
style={{
|
||||||
))}
|
minWidth: width,
|
||||||
</PropertiesContainer>
|
...itemStyles
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getPropertyLabel(prop)}
|
||||||
|
{getPropertyValue(prop)}
|
||||||
|
</EuiFlexItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</EuiFlexGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,15 +5,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
|
||||||
import { fontFamilyCode } from '../../style/variables';
|
|
||||||
import { Tooltip } from 'pivotal-ui/react/tooltip';
|
import { Tooltip } from 'pivotal-ui/react/tooltip';
|
||||||
import { OverlayTrigger } from 'pivotal-ui/react/overlay-trigger';
|
import { OverlayTrigger } from 'pivotal-ui/react/overlay-trigger';
|
||||||
|
|
||||||
const TooltipFieldName = styled.span`
|
|
||||||
font-family: ${fontFamilyCode};
|
|
||||||
`;
|
|
||||||
|
|
||||||
function TooltipOverlay({ children, content, delay = 1000 }) {
|
function TooltipOverlay({ children, content, delay = 1000 }) {
|
||||||
return (
|
return (
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
|
@ -27,13 +21,4 @@ function TooltipOverlay({ children, content, delay = 1000 }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fieldNameHelper(name) {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
Field name: <br />
|
|
||||||
<TooltipFieldName>{name}</TooltipFieldName>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TooltipOverlay;
|
export default TooltipOverlay;
|
||||||
|
|
58
x-pack/plugins/apm/public/components/shared/TraceLink.tsx
Normal file
58
x-pack/plugins/apm/public/components/shared/TraceLink.tsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Transaction } from '../../../typings/Transaction';
|
||||||
|
import { KibanaLink, legacyEncodeURIComponent } from '../../utils/url';
|
||||||
|
|
||||||
|
interface TraceLinkProps {
|
||||||
|
transaction: Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the path and query used to build a trace link,
|
||||||
|
* given either a v2 Transaction or a Transaction Group
|
||||||
|
*/
|
||||||
|
export function getLinkProps(transaction: Transaction) {
|
||||||
|
const serviceName = transaction.context.service.name;
|
||||||
|
const transactionType = transaction.transaction.type;
|
||||||
|
const traceId =
|
||||||
|
transaction.version === 'v2' ? transaction.trace.id : undefined;
|
||||||
|
const transactionId = transaction.transaction.id;
|
||||||
|
const name = transaction.transaction.name;
|
||||||
|
|
||||||
|
const encodedName = legacyEncodeURIComponent(name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hash: `/${serviceName}/transactions/${transactionType}/${encodedName}`,
|
||||||
|
query: {
|
||||||
|
traceId,
|
||||||
|
transactionId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TraceLink: React.SFC<TraceLinkProps> = ({
|
||||||
|
transaction,
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
|
if (!transaction) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkProps = getLinkProps(transaction);
|
||||||
|
|
||||||
|
if (!linkProps) {
|
||||||
|
// TODO: Should this case return unlinked children, null, or something else?
|
||||||
|
return <React.Fragment>{children}</React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KibanaLink pathname="/app/apm" {...linkProps}>
|
||||||
|
{children}
|
||||||
|
</KibanaLink>
|
||||||
|
);
|
||||||
|
};
|
|
@ -5,14 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import {
|
import { unit, units, px, fontSizes, colors } from '../../style/variables';
|
||||||
unit,
|
|
||||||
units,
|
|
||||||
px,
|
|
||||||
fontSizes,
|
|
||||||
colors,
|
|
||||||
fontSize
|
|
||||||
} from '../../style/variables';
|
|
||||||
import { RelativeLink } from '../../utils/url';
|
import { RelativeLink } from '../../utils/url';
|
||||||
|
|
||||||
export const HeaderContainer = styled.div`
|
export const HeaderContainer = styled.div`
|
||||||
|
@ -47,12 +40,6 @@ export const HeaderSmall = styled.h3`
|
||||||
${props => props.css};
|
${props => props.css};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const HeaderXSmall = styled.h4`
|
|
||||||
margin: ${px(units.plus)} 0;
|
|
||||||
font-size: ${fontSize};
|
|
||||||
${props => props.css};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Tab = styled.div`
|
export const Tab = styled.div`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: ${fontSizes.large};
|
font-size: ${fontSizes.large};
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
timeUnit
|
timeUnit
|
||||||
} from '../../../../../utils/formatters';
|
} from '../../../../../utils/formatters';
|
||||||
import { toJson } from '../../../../../utils/testHelpers';
|
import { toJson } from '../../../../../utils/testHelpers';
|
||||||
import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/view';
|
import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/index';
|
||||||
|
|
||||||
describe('Histogram', () => {
|
describe('Histogram', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
@ -98,9 +98,10 @@ describe('Histogram', () => {
|
||||||
it('should update state with "hoveredBucket"', () => {
|
it('should update state with "hoveredBucket"', () => {
|
||||||
expect(wrapper.state()).toEqual({
|
expect(wrapper.state()).toEqual({
|
||||||
hoveredBucket: {
|
hoveredBucket: {
|
||||||
sampled: true,
|
sample: {
|
||||||
|
transactionId: '99c50a5b-44b4-4289-a3d1-a2815d128192'
|
||||||
|
},
|
||||||
style: { cursor: 'pointer' },
|
style: { cursor: 'pointer' },
|
||||||
transactionId: '99c50a5b-44b4-4289-a3d1-a2815d128192',
|
|
||||||
x: 869010,
|
x: 869010,
|
||||||
x0: 811076,
|
x0: 811076,
|
||||||
y: 49
|
y: 49
|
||||||
|
@ -124,9 +125,10 @@ describe('Histogram', () => {
|
||||||
|
|
||||||
it('should call onClick with bucket', () => {
|
it('should call onClick with bucket', () => {
|
||||||
expect(onClick).toHaveBeenCalledWith({
|
expect(onClick).toHaveBeenCalledWith({
|
||||||
sampled: true,
|
sample: {
|
||||||
|
transactionId: '99c50a5b-44b4-4289-a3d1-a2815d128192'
|
||||||
|
},
|
||||||
style: { cursor: 'pointer' },
|
style: { cursor: 'pointer' },
|
||||||
transactionId: '99c50a5b-44b4-4289-a3d1-a2815d128192',
|
|
||||||
x: 869010,
|
x: 869010,
|
||||||
x0: 811076,
|
x0: 811076,
|
||||||
y: 49
|
y: 49
|
||||||
|
|
|
@ -961,6 +961,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -976,6 +977,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -991,6 +993,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1006,6 +1009,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1021,6 +1025,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1084,6 +1089,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1099,6 +1105,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1130,6 +1137,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1145,6 +1153,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1160,6 +1169,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1175,6 +1185,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1190,6 +1201,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1205,6 +1217,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1220,6 +1233,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1235,6 +1249,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1250,6 +1265,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1265,6 +1281,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1280,6 +1297,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1295,6 +1313,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1310,6 +1329,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1325,6 +1345,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1340,6 +1361,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1355,6 +1377,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1370,6 +1393,7 @@ exports[`Histogram Initially should have default markup 1`] = `
|
||||||
onMouseUp={[Function]}
|
onMouseUp={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"cursor": "default",
|
||||||
"pointerEvents": "all",
|
"pointerEvents": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,20 +8,23 @@
|
||||||
{
|
{
|
||||||
"key": 579340,
|
"key": 579340,
|
||||||
"count": 8,
|
"count": 8,
|
||||||
"transactionId": "99437ee4-08d4-41f5-9b2b-93cc32ec3dfb",
|
"sample": {
|
||||||
"sampled": true
|
"transactionId": "99437ee4-08d4-41f5-9b2b-93cc32ec3dfb"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": 695208,
|
"key": 695208,
|
||||||
"count": 23,
|
"count": 23,
|
||||||
"transactionId": "d327611b-e999-4942-a94f-c60208940180",
|
"sample": {
|
||||||
"sampled": true
|
"transactionId": "d327611b-e999-4942-a94f-c60208940180"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": 811076,
|
"key": 811076,
|
||||||
"count": 49,
|
"count": 49,
|
||||||
"transactionId": "99c50a5b-44b4-4289-a3d1-a2815d128192",
|
"sample": {
|
||||||
"sampled": true
|
"transactionId": "99c50a5b-44b4-4289-a3d1-a2815d128192"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": 926944,
|
"key": 926944,
|
||||||
|
@ -36,8 +39,9 @@
|
||||||
{
|
{
|
||||||
"key": 1158680,
|
"key": 1158680,
|
||||||
"count": 13,
|
"count": 13,
|
||||||
"transactionId": "8486d3e2-7f15-48df-aa37-6ee9955adbd2",
|
"sample": {
|
||||||
"sampled": true
|
"transactionId": "8486d3e2-7f15-48df-aa37-6ee9955adbd2"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": 1274548,
|
"key": 1274548,
|
||||||
|
|
|
@ -28,7 +28,8 @@ export default function AgentMarker({ agentMark, x }) {
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: px(x - legendWidth / 2)
|
left: px(x - legendWidth / 2),
|
||||||
|
bottom: '-6px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EuiToolTip
|
<EuiToolTip
|
||||||
|
@ -37,7 +38,7 @@ export default function AgentMarker({ agentMark, x }) {
|
||||||
content={
|
content={
|
||||||
<div>
|
<div>
|
||||||
<NameContainer>{agentMark.name}</NameContainer>
|
<NameContainer>{agentMark.name}</NameContainer>
|
||||||
<TimeContainer>{asTime(agentMark.timeLabel)}</TimeContainer>
|
<TimeContainer>{asTime(agentMark.us)}</TimeContainer>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { getTimeFormatter } from '../../../../utils/formatters';
|
||||||
const getXAxisTickValues = (tickValues, xMax) =>
|
const getXAxisTickValues = (tickValues, xMax) =>
|
||||||
_.last(tickValues) * 1.05 > xMax ? tickValues.slice(0, -1) : tickValues;
|
_.last(tickValues) * 1.05 > xMax ? tickValues.slice(0, -1) : tickValues;
|
||||||
|
|
||||||
function TimelineAxis({ header, plotValues, agentMarks }) {
|
function TimelineAxis({ plotValues, agentMarks }) {
|
||||||
const { margins, tickValues, width, xDomain, xMax, xScale } = plotValues;
|
const { margins, tickValues, width, xDomain, xMax, xScale } = plotValues;
|
||||||
const tickFormat = getTimeFormatter(xMax);
|
const tickFormat = getTimeFormatter(xMax);
|
||||||
const xAxisTickValues = getXAxisTickValues(tickValues, xMax);
|
const xAxisTickValues = getXAxisTickValues(tickValues, xMax);
|
||||||
|
@ -38,13 +38,12 @@ function TimelineAxis({ header, plotValues, agentMarks }) {
|
||||||
...style
|
...style
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{header}
|
|
||||||
<XYPlot
|
<XYPlot
|
||||||
dontCheckIfEmpty
|
dontCheckIfEmpty
|
||||||
width={width}
|
width={width}
|
||||||
height={40}
|
height={margins.top}
|
||||||
margin={{
|
margin={{
|
||||||
top: 40,
|
top: margins.top,
|
||||||
left: margins.left,
|
left: margins.left,
|
||||||
right: margins.right
|
right: margins.right
|
||||||
}}
|
}}
|
||||||
|
@ -65,9 +64,9 @@ function TimelineAxis({ header, plotValues, agentMarks }) {
|
||||||
|
|
||||||
{agentMarks.map(agentMark => (
|
{agentMarks.map(agentMark => (
|
||||||
<AgentMarker
|
<AgentMarker
|
||||||
key={agentMark.timeAxis}
|
key={agentMark.name}
|
||||||
agentMark={agentMark}
|
agentMark={agentMark}
|
||||||
x={xScale(agentMark.timeAxis)}
|
x={xScale(agentMark.us)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</XYPlot>
|
</XYPlot>
|
||||||
|
|
|
@ -20,9 +20,7 @@ class VerticalLines extends PureComponent {
|
||||||
xMax
|
xMax
|
||||||
} = this.props.plotValues;
|
} = this.props.plotValues;
|
||||||
|
|
||||||
const agentMarkTimes = this.props.agentMarks.map(
|
const agentMarkTimes = this.props.agentMarks.map(({ us }) => us);
|
||||||
({ timeAxis }) => timeAxis
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -57,21 +57,18 @@ exports[`Timeline should render with data 1`] = `
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
Hello - i am a header
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className="rv-xy-plot "
|
className="rv-xy-plot "
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"height": "40px",
|
"height": "100px",
|
||||||
"width": "1000px",
|
"width": "1000px",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="rv-xy-plot__inner"
|
className="rv-xy-plot__inner"
|
||||||
height={40}
|
height={100}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onDoubleClick={[Function]}
|
onDoubleClick={[Function]}
|
||||||
onMouseDown={[Function]}
|
onMouseDown={[Function]}
|
||||||
|
@ -94,7 +91,7 @@ exports[`Timeline should render with data 1`] = `
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
className="rv-xy-plot__axis__ticks"
|
className="rv-xy-plot__axis__ticks"
|
||||||
transform="translate(0, 40)"
|
transform="translate(0, 100)"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
className="rv-xy-plot__axis__tick"
|
className="rv-xy-plot__axis__tick"
|
||||||
|
@ -519,7 +516,7 @@ exports[`Timeline should render with data 1`] = `
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<g
|
<g
|
||||||
transform="translate(950, 40)"
|
transform="translate(950, 100)"
|
||||||
>
|
>
|
||||||
<text
|
<text
|
||||||
dy="0"
|
dy="0"
|
||||||
|
@ -533,6 +530,7 @@ exports[`Timeline should render with data 1`] = `
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"bottom": "-6px",
|
||||||
"left": "484.2043232706185px",
|
"left": "484.2043232706185px",
|
||||||
"position": "absolute",
|
"position": "absolute",
|
||||||
}
|
}
|
||||||
|
@ -562,6 +560,7 @@ exports[`Timeline should render with data 1`] = `
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"bottom": "-6px",
|
||||||
"left": "528.1747555976804px",
|
"left": "528.1747555976804px",
|
||||||
"position": "absolute",
|
"position": "absolute",
|
||||||
}
|
}
|
||||||
|
@ -591,6 +590,7 @@ exports[`Timeline should render with data 1`] = `
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
"bottom": "-6px",
|
||||||
"left": "879.9382142141751px",
|
"left": "879.9382142141751px",
|
||||||
"position": "absolute",
|
"position": "absolute",
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
},
|
},
|
||||||
"animation": null,
|
"animation": null,
|
||||||
"agentMarks": [
|
"agentMarks": [
|
||||||
{ "timeLabel": 100000, "name": "timeToFirstByte", "timeAxis": 100000 },
|
{ "name": "timeToFirstByte", "us": 100000 },
|
||||||
{ "timeLabel": 110000, "name": "domInteractive", "timeAxis": 110000 },
|
{ "name": "domInteractive", "us": 110000 },
|
||||||
{ "timeLabel": 190000, "name": "domComplete", "timeAxis": 190000 }
|
{ "name": "domComplete", "us": 190000 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,35 +8,22 @@ import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { makeWidthFlexible } from 'react-vis';
|
import { makeWidthFlexible } from 'react-vis';
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { getPlotValues } from './plotUtils';
|
import { getPlotValues } from './plotUtils';
|
||||||
import TimelineAxis from './TimelineAxis';
|
import TimelineAxis from './TimelineAxis';
|
||||||
import VerticalLines from './VerticalLines';
|
import VerticalLines from './VerticalLines';
|
||||||
|
|
||||||
class Timeline extends PureComponent {
|
class Timeline extends PureComponent {
|
||||||
getPlotValues = createSelector(
|
|
||||||
state => state.duration,
|
|
||||||
state => state.height,
|
|
||||||
state => state.margins,
|
|
||||||
state => state.width,
|
|
||||||
getPlotValues
|
|
||||||
);
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { width, duration, header, agentMarks } = this.props;
|
const { width, duration, agentMarks, height, margins } = this.props;
|
||||||
if (duration == null || !width) {
|
if (duration == null || !width) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const plotValues = this.getPlotValues(this.props);
|
const plotValues = getPlotValues({ width, duration, height, margins });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<TimelineAxis
|
<TimelineAxis plotValues={plotValues} agentMarks={agentMarks} />
|
||||||
plotValues={plotValues}
|
|
||||||
agentMarks={agentMarks}
|
|
||||||
header={header}
|
|
||||||
/>
|
|
||||||
<VerticalLines plotValues={plotValues} agentMarks={agentMarks} />
|
<VerticalLines plotValues={plotValues} agentMarks={agentMarks} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import { scaleLinear } from 'd3-scale';
|
import { scaleLinear } from 'd3-scale';
|
||||||
|
|
||||||
export function getPlotValues(duration, height, margins, width) {
|
export function getPlotValues({ width, duration, height, margins }) {
|
||||||
const xMin = 0;
|
const xMin = 0;
|
||||||
const xMax = duration;
|
const xMax = duration;
|
||||||
const xScale = scaleLinear()
|
const xScale = scaleLinear()
|
||||||
|
|
|
@ -4,9 +4,22 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
import { camelizeKeys } from 'humps';
|
import { camelizeKeys } from 'humps';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { ServiceResponse } from 'x-pack/plugins/apm/server/lib/services/get_service';
|
||||||
|
import { ServiceListItemResponse } from 'x-pack/plugins/apm/server/lib/services/get_services';
|
||||||
|
import { IDistributionResponse } from 'x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution';
|
||||||
|
import { Span } from 'x-pack/plugins/apm/typings/Span';
|
||||||
|
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||||
|
import { ITransactionGroup } from 'x-pack/plugins/apm/typings/TransactionGroup';
|
||||||
|
import { WaterfallResponse } from 'x-pack/plugins/apm/typings/waterfall';
|
||||||
|
import { IUrlParams } from '../../store/urlParams';
|
||||||
|
// @ts-ignore
|
||||||
import { convertKueryToEsQuery } from '../kuery';
|
import { convertKueryToEsQuery } from '../kuery';
|
||||||
|
// @ts-ignore
|
||||||
import { callApi } from './callApi';
|
import { callApi } from './callApi';
|
||||||
|
// @ts-ignore
|
||||||
import { getAPMIndexPattern } from './savedObjects';
|
import { getAPMIndexPattern } from './savedObjects';
|
||||||
|
|
||||||
export async function loadLicense() {
|
export async function loadLicense() {
|
||||||
|
@ -27,7 +40,7 @@ export async function loadAgentStatus() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEncodedEsQuery(kuery) {
|
export async function getEncodedEsQuery(kuery?: string) {
|
||||||
if (!kuery) {
|
if (!kuery) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -42,7 +55,11 @@ export async function getEncodedEsQuery(kuery) {
|
||||||
return encodeURIComponent(JSON.stringify(esFilterQuery));
|
return encodeURIComponent(JSON.stringify(esFilterQuery));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadServiceList({ start, end, kuery }) {
|
export async function loadServiceList({
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
kuery
|
||||||
|
}: IUrlParams): Promise<ServiceListItemResponse> {
|
||||||
return callApi({
|
return callApi({
|
||||||
pathname: `/api/apm/services`,
|
pathname: `/api/apm/services`,
|
||||||
query: {
|
query: {
|
||||||
|
@ -53,7 +70,12 @@ export async function loadServiceList({ start, end, kuery }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadServiceDetails({ serviceName, start, end, kuery }) {
|
export async function loadServiceDetails({
|
||||||
|
serviceName,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
kuery
|
||||||
|
}: IUrlParams): Promise<ServiceResponse> {
|
||||||
return callApi({
|
return callApi({
|
||||||
pathname: `/api/apm/services/${serviceName}`,
|
pathname: `/api/apm/services/${serviceName}`,
|
||||||
query: {
|
query: {
|
||||||
|
@ -64,14 +86,34 @@ export async function loadServiceDetails({ serviceName, start, end, kuery }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadTraceList({
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
kuery
|
||||||
|
}: IUrlParams): Promise<ITransactionGroup[]> {
|
||||||
|
const groups: ITransactionGroup[] = await callApi({
|
||||||
|
pathname: '/api/apm/traces',
|
||||||
|
query: {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
esFilterQuery: await getEncodedEsQuery(kuery)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups.map(group => {
|
||||||
|
group.sample = addVersion(group.sample);
|
||||||
|
return group;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadTransactionList({
|
export async function loadTransactionList({
|
||||||
serviceName,
|
serviceName,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
kuery,
|
kuery,
|
||||||
transactionType
|
transactionType
|
||||||
}) {
|
}: IUrlParams): Promise<ITransactionGroup[]> {
|
||||||
return callApi({
|
const groups: ITransactionGroup[] = await callApi({
|
||||||
pathname: `/api/apm/services/${serviceName}/transactions`,
|
pathname: `/api/apm/services/${serviceName}/transactions`,
|
||||||
query: {
|
query: {
|
||||||
start,
|
start,
|
||||||
|
@ -80,6 +122,11 @@ export async function loadTransactionList({
|
||||||
transaction_type: transactionType
|
transaction_type: transactionType
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return groups.map(group => {
|
||||||
|
group.sample = addVersion(group.sample);
|
||||||
|
return group;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadTransactionDistribution({
|
export async function loadTransactionDistribution({
|
||||||
|
@ -88,7 +135,7 @@ export async function loadTransactionDistribution({
|
||||||
end,
|
end,
|
||||||
transactionName,
|
transactionName,
|
||||||
kuery
|
kuery
|
||||||
}) {
|
}: IUrlParams): Promise<IDistributionResponse> {
|
||||||
return callApi({
|
return callApi({
|
||||||
pathname: `/api/apm/services/${serviceName}/transactions/distribution`,
|
pathname: `/api/apm/services/${serviceName}/transactions/distribution`,
|
||||||
query: {
|
query: {
|
||||||
|
@ -100,14 +147,54 @@ export async function loadTransactionDistribution({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadSpans({ serviceName, start, end, transactionId }) {
|
function addVersion<T extends Span | Transaction>(item: T): T {
|
||||||
return callApi({
|
if (!isEmpty(item)) {
|
||||||
|
item.version = item.hasOwnProperty('trace') ? 'v2' : 'v1';
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSpanId(hit: Span, i: number) {
|
||||||
|
if (!hit.span.id) {
|
||||||
|
hit.span.id = i;
|
||||||
|
}
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadSpans({
|
||||||
|
serviceName,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
transactionId
|
||||||
|
}: IUrlParams): Promise<Span[]> {
|
||||||
|
const hits: Span[] = await callApi({
|
||||||
pathname: `/api/apm/services/${serviceName}/transactions/${transactionId}/spans`,
|
pathname: `/api/apm/services/${serviceName}/transactions/${transactionId}/spans`,
|
||||||
query: {
|
query: {
|
||||||
start,
|
start,
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return hits.map(addVersion).map(addSpanId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadTrace({ traceId, start, end }: IUrlParams) {
|
||||||
|
const result: WaterfallResponse = await callApi(
|
||||||
|
{
|
||||||
|
pathname: `/api/apm/traces/${traceId}`,
|
||||||
|
query: {
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
camelcase: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
result.hits = result.hits.map(addVersion);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadTransaction({
|
export async function loadTransaction({
|
||||||
|
@ -115,12 +202,14 @@ export async function loadTransaction({
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
transactionId,
|
transactionId,
|
||||||
|
traceId,
|
||||||
kuery
|
kuery
|
||||||
}) {
|
}: IUrlParams) {
|
||||||
const res = await callApi(
|
const result: Transaction = await callApi(
|
||||||
{
|
{
|
||||||
pathname: `/api/apm/services/${serviceName}/transactions/${transactionId}`,
|
pathname: `/api/apm/services/${serviceName}/transactions/${transactionId}`,
|
||||||
query: {
|
query: {
|
||||||
|
traceId,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
esFilterQuery: await getEncodedEsQuery(kuery)
|
esFilterQuery: await getEncodedEsQuery(kuery)
|
||||||
|
@ -130,11 +219,8 @@ export async function loadTransaction({
|
||||||
camelcase: false
|
camelcase: false
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const camelizedRes = camelizeKeys(res);
|
|
||||||
if (res.context) {
|
return addVersion(result);
|
||||||
camelizedRes.context = res.context;
|
|
||||||
}
|
|
||||||
return camelizedRes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadCharts({
|
export async function loadCharts({
|
||||||
|
@ -144,7 +230,7 @@ export async function loadCharts({
|
||||||
kuery,
|
kuery,
|
||||||
transactionType,
|
transactionType,
|
||||||
transactionName
|
transactionName
|
||||||
}) {
|
}: IUrlParams) {
|
||||||
return callApi({
|
return callApi({
|
||||||
pathname: `/api/apm/services/${serviceName}/transactions/charts`,
|
pathname: `/api/apm/services/${serviceName}/transactions/charts`,
|
||||||
query: {
|
query: {
|
||||||
|
@ -157,6 +243,12 @@ export async function loadCharts({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ErrorGroupListParams extends IUrlParams {
|
||||||
|
size: number;
|
||||||
|
sortField: string;
|
||||||
|
sortDirection: string;
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadErrorGroupList({
|
export async function loadErrorGroupList({
|
||||||
serviceName,
|
serviceName,
|
||||||
start,
|
start,
|
||||||
|
@ -165,7 +257,7 @@ export async function loadErrorGroupList({
|
||||||
size,
|
size,
|
||||||
sortField,
|
sortField,
|
||||||
sortDirection
|
sortDirection
|
||||||
}) {
|
}: ErrorGroupListParams) {
|
||||||
return callApi({
|
return callApi({
|
||||||
pathname: `/api/apm/services/${serviceName}/errors`,
|
pathname: `/api/apm/services/${serviceName}/errors`,
|
||||||
query: {
|
query: {
|
||||||
|
@ -185,7 +277,7 @@ export async function loadErrorGroupDetails({
|
||||||
end,
|
end,
|
||||||
kuery,
|
kuery,
|
||||||
errorGroupId
|
errorGroupId
|
||||||
}) {
|
}: IUrlParams) {
|
||||||
const res = await callApi(
|
const res = await callApi(
|
||||||
{
|
{
|
||||||
pathname: `/api/apm/services/${serviceName}/errors/${errorGroupId}`,
|
pathname: `/api/apm/services/${serviceName}/errors/${errorGroupId}`,
|
||||||
|
@ -212,7 +304,7 @@ export async function loadErrorDistribution({
|
||||||
end,
|
end,
|
||||||
kuery,
|
kuery,
|
||||||
errorGroupId
|
errorGroupId
|
||||||
}) {
|
}: IUrlParams) {
|
||||||
return callApi({
|
return callApi({
|
||||||
pathname: `/api/apm/services/${serviceName}/errors/${errorGroupId}/distribution`,
|
pathname: `/api/apm/services/${serviceName}/errors/${errorGroupId}/distribution`,
|
||||||
query: {
|
query: {
|
|
@ -4,11 +4,11 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import reducer from '../rootReducer';
|
import { rootReducer } from '../rootReducer';
|
||||||
|
|
||||||
describe('root reducer', () => {
|
describe('root reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toEqual({
|
expect(rootReducer(undefined, {})).toEqual({
|
||||||
location: { hash: '', pathname: '', search: '' },
|
location: { hash: '', pathname: '', search: '' },
|
||||||
reactReduxRequest: {},
|
reactReduxRequest: {},
|
||||||
urlParams: {}
|
urlParams: {}
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import reducer, { updateTimePicker } from '../urlParams';
|
import { urlParamsReducer, updateTimePicker } from '../urlParams';
|
||||||
import { LOCATION_UPDATE } from '../location';
|
import { LOCATION_UPDATE } from '../location';
|
||||||
|
|
||||||
describe('urlParams', () => {
|
describe('urlParams', () => {
|
||||||
it('should handle LOCATION_UPDATE for transactions section', () => {
|
it('should handle LOCATION_UPDATE for transactions section', () => {
|
||||||
const state = reducer(
|
const state = urlParamsReducer(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
type: LOCATION_UPDATE,
|
type: LOCATION_UPDATE,
|
||||||
|
@ -34,7 +34,7 @@ describe('urlParams', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle LOCATION_UPDATE for error section', () => {
|
it('should handle LOCATION_UPDATE for error section', () => {
|
||||||
const state = reducer(
|
const state = urlParamsReducer(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
type: LOCATION_UPDATE,
|
type: LOCATION_UPDATE,
|
||||||
|
@ -56,7 +56,7 @@ describe('urlParams', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle TIMEPICKER_UPDATE', () => {
|
it('should handle TIMEPICKER_UPDATE', () => {
|
||||||
const state = reducer(
|
const state = urlParamsReducer(
|
||||||
{},
|
{},
|
||||||
updateTimePicker({
|
updateTimePicker({
|
||||||
min: 'minTime',
|
min: 'minTime',
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import { createStore, applyMiddleware, compose } from 'redux';
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import throttle from '../middleware/throttle';
|
import throttle from '../middleware/throttle';
|
||||||
import rootReducer from '../rootReducer';
|
import { rootReducer } from '../rootReducer';
|
||||||
|
|
||||||
export default function configureStore(preloadedState) {
|
export default function configureStore(preloadedState) {
|
||||||
const composeEnhancers =
|
const composeEnhancers =
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import { createStore, applyMiddleware } from 'redux';
|
import { createStore, applyMiddleware } from 'redux';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import rootReducer from '../rootReducer';
|
import { rootReducer } from '../rootReducer';
|
||||||
|
|
||||||
export default function configureStore(preloadedState) {
|
export default function configureStore(preloadedState) {
|
||||||
return createStore(rootReducer, preloadedState, applyMiddleware(thunk));
|
return createStore(rootReducer, preloadedState, applyMiddleware(thunk));
|
||||||
|
|
30
x-pack/plugins/apm/public/store/mockData/mockTraceList.json
Normal file
30
x-pack/plugins/apm/public/store/mockData/mockTraceList.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "log",
|
||||||
|
"serviceName": "flask-server",
|
||||||
|
"averageResponseTime": 1329,
|
||||||
|
"tracesPerMinute": 3201,
|
||||||
|
"impact": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "products/item",
|
||||||
|
"serviceName": "client",
|
||||||
|
"averageResponseTime": 2301,
|
||||||
|
"tracesPerMinute": 5432,
|
||||||
|
"impact": 42
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "billing/payment",
|
||||||
|
"serviceName": "client",
|
||||||
|
"averageResponseTime": 789,
|
||||||
|
"tracesPerMinute": 1201,
|
||||||
|
"impact": 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user/profile",
|
||||||
|
"serviceName": "client",
|
||||||
|
"averageResponseTime": 1212,
|
||||||
|
"tracesPerMinute": 904,
|
||||||
|
"impact": 92
|
||||||
|
}
|
||||||
|
]
|
|
@ -20,7 +20,7 @@ export function getErrorDistribution(state) {
|
||||||
export function ErrorDistributionRequest({ urlParams, render }) {
|
export function ErrorDistributionRequest({ urlParams, render }) {
|
||||||
const { serviceName, start, end, errorGroupId, kuery } = urlParams;
|
const { serviceName, start, end, errorGroupId, kuery } = urlParams;
|
||||||
|
|
||||||
if (!(serviceName, start, end, errorGroupId)) {
|
if (!(serviceName && start && end && errorGroupId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue