diff --git a/package.json b/package.json index ca2d2fe7b436c..43ebb899e57d9 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,8 @@ "url": "https://github.com/elastic/kibana.git" }, "resolutions": { - "**/@types/node": "8.10.21" + "**/@types/node": "8.10.21", + "@types/react": "16.3.14" }, "dependencies": { "@elastic/eui": "4.5.1", @@ -183,7 +184,7 @@ "react-input-range": "^1.3.0", "react-markdown": "^3.1.4", "react-redux": "^5.0.7", - "react-router-dom": "4.2.2", + "react-router-dom": "^4.3.1", "react-sizeme": "^2.3.6", "react-toggle": "4.0.2", "reactcss": "1.2.3", @@ -207,6 +208,7 @@ "topojson-client": "3.0.0", "trunc-html": "1.0.2", "trunc-text": "1.0.2", + "ts-optchain": "^0.1.1", "tslib": "^1.9.3", "type-detect": "^4.0.8", "uglifyjs-webpack-plugin": "^1.2.7", @@ -245,7 +247,9 @@ "@types/boom": "^7.2.0", "@types/chance": "^1.0.0", "@types/classnames": "^2.2.3", + "@types/d3": "^5.0.0", "@types/dedent": "^0.7.0", + "@types/elasticsearch": "^5.0.26", "@types/enzyme": "^3.1.12", "@types/eslint": "^4.16.2", "@types/execa": "^0.9.0", @@ -261,19 +265,22 @@ "@types/listr": "^0.13.0", "@types/lodash": "^3.10.1", "@types/minimatch": "^2.0.29", + "@types/moment-timezone": "^0.5.8", "@types/mustache": "^0.8.31", "@types/node": "^8.10.20", "@types/prop-types": "^15.5.3", "@types/puppeteer": "^1.6.2", - "@types/react": "^16.3.14", + "@types/react": "16.3.14", "@types/react-dom": "^16.0.5", "@types/react-redux": "^6.0.6", + "@types/react-router-dom": "^4.3.1", "@types/react-virtualized": "^9.18.7", "@types/redux": "^3.6.31", "@types/redux-actions": "^2.2.1", "@types/semver": "^5.5.0", "@types/sinon": "^5.0.1", "@types/strip-ansi": "^3.0.0", + "@types/styled-components": "^3.0.1", "@types/supertest": "^2.0.5", "@types/type-detect": "^4.0.1", "@types/uuid": "^3.4.4", diff --git a/x-pack/package.json b/x-pack/package.json index 29e5d03359640..fcc9c50f84691 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -22,7 +22,8 @@ } }, "resolutions": { - "**/@types/node": "8.10.21" + "**/@types/node": "8.10.21", + "@types/react": "16.3.14" }, "devDependencies": { "@kbn/dev-utils": "link:../packages/kbn-dev-utils", @@ -36,7 +37,7 @@ "@types/d3-shape": "^1.2.2", "@types/d3-time": "^1.0.7", "@types/d3-time-format": "^2.1.0", - "@types/elasticsearch": "^5.0.22", + "@types/elasticsearch": "^5.0.26", "@types/expect.js": "^0.3.29", "@types/graphql": "^0.13.1", "@types/hapi": "15.0.1", @@ -48,11 +49,11 @@ "@types/mocha": "^5.2.5", "@types/pngjs": "^3.3.1", "@types/prop-types": "^15.5.3", - "@types/react": "^16.3.14", + "@types/react": "16.3.14", "@types/react-datepicker": "^1.1.5", "@types/react-dom": "^16.0.5", "@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/sinon": "^5.0.1", "@types/supertest": "^2.0.5", @@ -130,7 +131,6 @@ "@samverschueren/stream-to-observable": "^0.3.0", "@scant/router": "^0.1.0", "@slack/client": "^4.2.2", - "@types/moment-timezone": "^0.5.8", "angular-resource": "1.4.9", "angular-sanitize": "1.4.9", "angular-ui-ace": "0.2.3", @@ -226,7 +226,7 @@ "react-redux": "^5.0.7", "react-redux-request": "^1.5.6", "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-shortcuts": "^2.0.0", "react-sticky": "^6.0.1", diff --git a/x-pack/plugins/apm/common/constants.test.ts b/x-pack/plugins/apm/common/constants.test.ts new file mode 100644 index 0000000000000..e8cfc6441d814 --- /dev/null +++ b/x-pack/plugins/apm/common/constants.test.ts @@ -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'); + }); +}); diff --git a/x-pack/plugins/apm/common/constants.js b/x-pack/plugins/apm/common/constants.ts similarity index 90% rename from x-pack/plugins/apm/common/constants.js rename to x-pack/plugins/apm/common/constants.ts index e0627338bbc2f..fde95ed165469 100644 --- a/x-pack/plugins/apm/common/constants.js +++ b/x-pack/plugins/apm/common/constants.ts @@ -7,6 +7,8 @@ export const SERVICE_NAME = 'context.service.name'; export const SERVICE_AGENT_NAME = 'context.service.agent.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_EVENT = 'processor.event'; @@ -18,19 +20,21 @@ export const TRANSACTION_NAME = 'transaction.name'; export const TRANSACTION_ID = 'transaction.id'; export const TRANSACTION_SAMPLED = 'transaction.sampled'; +export const TRACE_ID = 'trace.id'; + export const SPAN_START = 'span.start.us'; export const SPAN_DURATION = 'span.duration.us'; export const SPAN_TYPE = 'span.type'; export const SPAN_NAME = 'span.name'; export const SPAN_ID = 'span.id'; 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_CULPRIT = 'error.culprit'; export const ERROR_LOG_MESSAGE = 'error.log.message'; export const ERROR_EXC_MESSAGE = 'error.exception.message'; export const ERROR_EXC_HANDLED = 'error.exception.handled'; - -export const REQUEST_URL_FULL = 'context.request.url.full'; - -export const USER_ID = 'context.user.id'; diff --git a/x-pack/plugins/apm/index.js b/x-pack/plugins/apm/index.js index 6b18d589813c3..d3b7f01511e3d 100644 --- a/x-pack/plugins/apm/index.js +++ b/x-pack/plugins/apm/index.js @@ -9,6 +9,7 @@ import { initTransactionsApi } from './server/routes/transactions'; import { initServicesApi } from './server/routes/services'; import { initErrorsApi } from './server/routes/errors'; import { initStatusApi } from './server/routes/status_check'; +import { initTracesApi } from './server/routes/traces'; export function apm(kibana) { return new kibana.Plugin({ @@ -55,6 +56,7 @@ export function apm(kibana) { init(server) { initTransactionsApi(server); + initTracesApi(server); initServicesApi(server); initErrorsApi(server); initStatusApi(server); diff --git a/x-pack/plugins/apm/jsconfig.json b/x-pack/plugins/apm/jsconfig.json deleted file mode 100644 index bdf4e8b91f067..0000000000000 --- a/x-pack/plugins/apm/jsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "exclude": ["node_modules", "**/node_modules/*", "build"] -} diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__test__/__snapshots__/DetailView.test.js.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__test__/__snapshots__/DetailView.test.js.snap index 01d0b34a0dd9d..c73f56e2ccbf6 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__test__/__snapshots__/DetailView.test.js.snap +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__test__/__snapshots__/DetailView.test.js.snap @@ -4,46 +4,25 @@ exports[`DetailView should render empty state 1`] = `""`; exports[`DetailView should render with data 1`] = ` .c3 { - 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; -} - -.c4 { - width: 33%; - margin-bottom: 16px; -} - -.c5 { margin-bottom: 8px; font-size: 12px; color: #999999; } -.c5 span { +.c3 span { cursor: help; } -.c7 { +.c5 { color: #999999; } -.c6 { +.c4 { display: inline-block; line-height: 16px; } -.c8 { +.c6 { display: inline-block; line-height: 16px; max-width: 100%; @@ -58,12 +37,7 @@ exports[`DetailView should render with data 1`] = ` margin: 0; } -.c13 { - margin: 24px 0; - font-size: 14px; -} - -.c10 { +.c8 { display: inline-block; font-size: 16px; padding: 16px 20px; @@ -76,7 +50,7 @@ exports[`DetailView should render with data 1`] = ` border-bottom: 2px solid #006E8A; } -.c11 { +.c9 { display: inline-block; font-size: 16px; padding: 16px 20px; @@ -88,12 +62,12 @@ exports[`DetailView should render with data 1`] = ` user-select: none; } -.c18 { +.c15 { position: relative; border-radius: 0 0 5px 5px; } -.c19 { +.c16 { position: absolute; width: 100%; height: 18px; @@ -102,7 +76,7 @@ exports[`DetailView should render with data 1`] = ` background-color: #FCF2E6; } -.c20 { +.c17 { position: absolute; top: 0; left: 0; @@ -110,7 +84,7 @@ exports[`DetailView should render with data 1`] = ` background: #f5f5f5; } -.c21 { +.c18 { position: relative; min-width: 42px; padding-left: 8px; @@ -121,11 +95,11 @@ exports[`DetailView should render with data 1`] = ` border-right: 1px solid #d9d9d9; } -.c21:last-of-type { +.c18:last-of-type { border-radius: 0 0 0 5px; } -.c22 { +.c19 { position: relative; min-width: 42px; padding-left: 8px; @@ -137,22 +111,22 @@ exports[`DetailView should render with data 1`] = ` background-color: #FCF2E6; } -.c22:last-of-type { +.c19:last-of-type { border-radius: 0 0 0 5px; } -.c23 { +.c20 { overflow: auto; margin: 0 0 0 42px; padding: 0; background-color: #ffffff; } -.c23:last-of-type { +.c20:last-of-type { border-radius: 0 0 5px 0; } -.c24 { +.c21 { margin: 0; color: inherit; background: inherit; @@ -163,7 +137,7 @@ exports[`DetailView should render with data 1`] = ` line-height: 18px; } -.c25 { +.c22 { position: relative; padding: 0; margin: 0; @@ -171,18 +145,18 @@ exports[`DetailView should render with data 1`] = ` z-index: 2; } -.c15 { +.c12 { color: #999999; padding: 8px; border-bottom: 1px solid #d9d9d9; border-radius: 5px 5px 0 0; } -.c17 { +.c14 { font-weight: bold; } -.c14 { +.c11 { margin: 0 0 24px 0; position: relative; font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; @@ -191,11 +165,11 @@ exports[`DetailView should render with data 1`] = ` background: #f5f5f5; } -.c14 .c16 { +.c11 .c13 { color: #000000; } -.c26 { +.c23 { margin: 0 0 24px 0; -webkit-user-select: none; -moz-user-select: none; @@ -227,12 +201,12 @@ exports[`DetailView should render with data 1`] = ` margin-bottom: 16px; } -.c9 { +.c7 { padding: 0 24px; border-bottom: 1px solid #d9d9d9; } -.c12 { +.c10 { padding: 24px 24px 0; } @@ -249,7 +223,7 @@ exports[`DetailView should render with data 1`] = `
1337 minutes ago (mocking 1515508740) ( 1st of January (mocking 1515508740) @@ -321,10 +307,16 @@ exports[`DetailView should render with data 1`] = `
@@ -344,10 +336,16 @@ exports[`DetailView should render with data 1`] = `
GET
N/A
N/A
Exception stacktrace
Request
Response
System
Service
Process
User
Tags
@@ -472,146 +482,146 @@ exports[`DetailView should render with data 1`] = `
-

- Stacktraces -

+ Stack traces +
server/coffee.js in <anonymous> at line 9
2 .
3 .
4 .
5 .
6 .
7 .
8 .
9 .
10 .
11 .
12 .
13 .
14 .
15 .
16 .
               
                 
 
               
             
               
                 
             
               
                 
             
               
                 
 
               
             
               
                 
             
               
                 
 
               
             
               
                 app.get(
                 
             
               
                   
                 
             
               
                     res.send(
                 
             
               
                   } 
                 
             
               
                     res.send(
                 
             
               
                   }
               
             
               
                 })
               
             
               
                 
 
               
             
               
                 app.get(
                 
       
server.js in <anonymous> at line 27
20 .
21 .
22 .
23 .
24 .
25 .
26 .
27 .
28 .
29 .
30 .
31 .
32 .
33 .
34 .
               
                 app.use(
                 
             
               
                 app.use(express.static(
                 
             
               
                 app.use(
                 
             
               
                   apm.setTag(
                 
             
               
                   apm.setTag(
                 
             
               
                   apm.setTag(
                 
             
               
                   apm.setTag(
                 
             
               
                   next()
               
             
               
                 })
               
             
               
                 
 
               
             
               
                 app.use(
                 
             
               
                 app.use(
                 
             
               
                 app.get(
                 
             
               
                   res.sendFile(path.resolve(__dirname, 
                 
             
               
                 })
               
@@ -1768,7 +1778,7 @@ exports[`DetailView should render with data 1`] = `
       
-
-
-
+ />
@@ -690,60 +637,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` >
-
-
- -
-
-
+ />
diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/List/__test__/props.json b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceOverview/List/__test__/props.json rename to x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js new file mode 100644 index 0000000000000..f218cd8fe7460 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js @@ -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 => ( + + + {formatString(serviceName)} + + + ) + }, + { + 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 ( + + ); +} + +ServiceList.propTypes = { + noItemsMessage: PropTypes.node, + items: PropTypes.array +}; + +ServiceList.defaultProps = { + items: [] +}; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.js index 120196541c04b..8ef92b829be9c 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.js @@ -6,7 +6,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import ServiceOverview from '../view'; +import { ServiceOverview } from '../view'; import { STATUS } from '../../../../constants'; import * as apmRestServices from '../../../../services/rest/apm'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.js.snap index 19bf212986f01..8f437ed35630e 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.js.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.js.snap @@ -2,13 +2,9 @@ exports[`Service Overview -> View should render when historical data is found 1`] = `
- -

- Services -

- -
- + @@ -20,7 +16,6 @@ Object { "items": Array [], "noItemsMessage": , } @@ -28,13 +23,9 @@ Object { exports[`Service Overview -> View should render when historical data is not found 1`] = `
- -

- Services -

- -
- + @@ -46,7 +37,6 @@ Object { "items": Array [], "noItemsMessage": - -

Services

- -
- - - + ( - + )} />
); } } - -function SetupInstructionsLink({ buttonFill = false }) { - return ( - - - Setup Instructions - - - ); -} - -export default ServiceOverview; diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx new file mode 100644 index 0000000000000..39047e48c68c5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx @@ -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) => ( + + + {formatString(transaction.transaction.name)} + + + ) + }, + { + 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) => + } +]; + +export function TraceList({ items = [], noItemsMessage, ...rest }: Props) { + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.js b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx similarity index 62% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/index.js rename to x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx index e6855e5a068dc..09239e7ec6c14 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.js +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -5,18 +5,15 @@ */ import { connect } from 'react-redux'; -import TransactionsDetails from './view'; +import { IReduxState } from '../../../store/rootReducer'; +// @ts-ignore import { getUrlParams } from '../../../store/urlParams'; +import { TraceOverview as View } from './view'; -function mapStateToProps(state = {}) { +function mapStateToProps(state = {} as IReduxState) { return { - location: state.location, urlParams: getUrlParams(state) }; } -const mapDispatchToProps = {}; -export default connect( - mapStateToProps, - mapDispatchToProps -)(TransactionsDetails); +export const TraceOverview = connect(mapStateToProps)(View); diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/view.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/view.tsx new file mode 100644 index 0000000000000..e54636e0dcd4e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/view.tsx @@ -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 ( +
+ + ( + + } + /> + )} + /> +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__jest__/distribution.test.js b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__jest__/distribution.test.js index 634059e6acd5a..75338c669d0b3 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__jest__/distribution.test.js +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__jest__/distribution.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getFormattedBuckets } from '../view'; +import { getFormattedBuckets } from '../index'; describe('Distribution', () => { it('getFormattedBuckets', () => { @@ -12,32 +12,41 @@ describe('Distribution', () => { { key: 0, count: 0 }, { key: 20, count: 0 }, { key: 40, count: 0 }, - { key: 60, count: 5, transactionId: 'someTransactionId', sampled: true }, + { + key: 60, + count: 5, + sample: { + transactionId: 'someTransactionId' + } + }, { key: 80, count: 100, - transactionId: 'anotherTransactionId', - sampled: true + sample: { + transactionId: 'anotherTransactionId' + } } ]; expect(getFormattedBuckets(buckets, 20)).toEqual([ - { x: 20, x0: 0, y: 0, style: {} }, - { x: 40, x0: 20, y: 0, style: {} }, - { x: 60, x0: 40, y: 0, style: {} }, + { x: 20, x0: 0, y: 0, style: { cursor: 'default' } }, + { x: 40, x0: 20, y: 0, style: { cursor: 'default' } }, + { x: 60, x0: 40, y: 0, style: { cursor: 'default' } }, { x: 80, x0: 60, y: 5, - sampled: true, - transactionId: 'someTransactionId', + sample: { + transactionId: 'someTransactionId' + }, style: { cursor: 'pointer' } }, { x: 100, x0: 80, y: 100, - sampled: true, - transactionId: 'anotherTransactionId', + sample: { + transactionId: 'anotherTransactionId' + }, style: { cursor: 'pointer' } } ]); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.js b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.js deleted file mode 100644 index 562e8c6d2479d..0000000000000 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.js +++ /dev/null @@ -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); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/view.js b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx similarity index 54% rename from x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/view.js rename to x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index 3e01ddf7cfedb..8c4dd4c262d26 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/view.js +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -4,44 +4,66 @@ * 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 React, { Component } from 'react'; +import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams'; +import { IBucket } from 'x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets'; +import { IDistributionResponse } from 'x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution'; +// @ts-ignore +import { getTimeFormatter, timeUnit } from '../../../../utils/formatters'; +// @ts-ignore +import { fromQuery, history, toQuery } from '../../../../utils/url'; +// @ts-ignore import Histogram from '../../../shared/charts/Histogram'; -import { toQuery, fromQuery, history } from '../../../../utils/url'; -import { HeaderSmall } from '../../../shared/UIComponents'; import EmptyMessage from '../../../shared/EmptyMessage'; -import { getTimeFormatter, timeUnit } from '../../../../utils/formatters'; +// @ts-ignore +import { HeaderSmall } from '../../../shared/UIComponents'; +// @ts-ignore 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) { - return null; + return []; } - return buckets.map(({ sampled, count, key, transactionId }) => { + return buckets.map(({ sample, count, key }) => { return { - sampled, - transactionId, + sample, x0: key, x: key + bucketSize, y: count, - style: count > 0 && sampled ? { cursor: 'pointer' } : {} + style: { cursor: count > 0 && sample ? 'pointer' : 'default' } }; }); } -class Distribution extends Component { - formatYShort = t => { +interface Props { + location: any; + distribution: IDistributionResponse; + urlParams: IUrlParams; +} + +export class Distribution extends Component { + public formatYShort = (t: number) => { return `${t} ${unitShort(this.props.urlParams.transactionType)}`; }; - formatYLong = t => { + public formatYLong = (t: number) => { return `${t} ${unitLong(this.props.urlParams.transactionType, t)}`; }; - render() { - const { location, distribution } = this.props; + public render() { + const { location, distribution, urlParams } = this.props; const buckets = getFormattedBuckets( distribution.buckets, @@ -58,7 +80,10 @@ class Distribution extends Component { } 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 ( @@ -76,13 +101,14 @@ class Distribution extends Component { buckets={buckets} bucketSize={distribution.bucketSize} bucketIndex={bucketIndex} - onClick={bucket => { - if (bucket.sampled && bucket.y > 0) { + onClick={(bucket: IChartPoint) => { + if (bucket.sample && bucket.y > 0) { history.replace({ ...location, search: fromQuery({ ...toQuery(location.search), - transactionId: bucket.transactionId + transactionId: bucket.sample.transactionId, + traceId: bucket.sample.traceId }) }); } @@ -90,16 +116,20 @@ class Distribution extends Component { formatX={timeFormatter} formatYShort={this.formatYShort} formatYLong={this.formatYLong} - verticalLineHover={bucket => bucket.y > 0 && !bucket.sampled} - backgroundHover={bucket => bucket.y > 0 && bucket.sampled} - tooltipHeader={bucket => + verticalLineHover={(bucket: IChartPoint) => + bucket.y > 0 && !bucket.sample + } + backgroundHover={(bucket: IChartPoint) => + bucket.y > 0 && bucket.sample + } + tooltipHeader={(bucket: IChartPoint) => `${timeFormatter(bucket.x0, false)} - ${timeFormatter( bucket.x, false )} ${unit}` } - tooltipFooter={bucket => - !bucket.sampled && 'No sample available for this bucket' + tooltipFooter={(bucket: IChartPoint) => + !bucket.sample && 'No sample available for this bucket' } />
@@ -107,20 +137,12 @@ class Distribution extends Component { } } -function unitShort(type) { +function unitShort(type: string | undefined) { return type === 'request' ? 'req.' : 'trans.'; } -function unitLong(type, count) { +function unitLong(type: string | undefined, count: number) { const suffix = count > 1 ? 's' : ''; return type === 'request' ? `request${suffix}` : `transaction${suffix}`; } - -Distribution.propTypes = { - urlParams: PropTypes.object.isRequired, - location: PropTypes.object.isRequired, - distribution: PropTypes.object -}; - -export default Distribution; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/ActionMenu.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/ActionMenu.tsx new file mode 100644 index 0000000000000..fc553a60e024f --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/ActionMenu.tsx @@ -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 ( + + Actions + + ); +} + +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 = [ + + + View sample document + + , + + + View host metrics (beta) + + , + + + View host logs (beta) + + + ]; + + return ( + } + isOpen={this.state.isOpen} + closePopover={this.close} + anchorPosition="downRight" + panelPaddingSize="none" + > + + + ); + } +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/Spans/Span.js b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/Spans/Span.js deleted file mode 100644 index 1266e6eea0c46..0000000000000 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/Spans/Span.js +++ /dev/null @@ -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 ( - { - history.replace({ - ...location, - search: fromQuery({ - ...toQuery(location.search), - spanId - }) - }); - }} - timelineMargins={timelineMargins} - isSelected={isSelected} - > - - - ‎ - {spanName} - ‎ - - - - - - - ); - } -} - -Span.propTypes = { - location: PropTypes.object.isRequired, - totalDuration: PropTypes.number.isRequired -}; - -export default Span; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/Spans/SpanDetails/index.js b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/Spans/SpanDetails/index.js deleted file mode 100644 index 3ef67277a68c7..0000000000000 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/Spans/SpanDetails/index.js +++ /dev/null @@ -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 ( -
- - - - - Name - - - - - {spanName || 'N/A'} - - - - - - - Type - - - - - {spanTypeLabel} - - - - - - Duration - - - {asMillis(spanDuration)} - - - % of total time - {numeral(relativeDuration).format('0.00%')} - - - - {`View span in Discover`} - - - - - - - - - -
- ); -} - -function DatabaseContext({ dbContext }) { - if (!dbContext || !dbContext.statement) { - return null; - } - - if (dbContext.type !== 'sql') { - return {dbContext.statement}; - } - - return ( -
- DB Statement - - - {dbContext.statement} - - -
- ); -} - -SpanDetails.propTypes = { - span: PropTypes.object.isRequired, - totalDuration: PropTypes.number.isRequired -}; - -export default SpanDetails; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/Spans/TimelineHeader.js b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/Spans/TimelineHeader.js deleted file mode 100644 index 4abea517a7a8f..0000000000000 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/Spans/TimelineHeader.js +++ /dev/null @@ -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 ( - - - {transactionName || 'N/A'} - - - {legends.map(({ color, label }) => ( - - ))} - - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/Spans/index.js b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/Spans/index.js deleted file mode 100644 index 4ecae0f2067cb..0000000000000 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/Spans/index.js +++ /dev/null @@ -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); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/Spans/view.js b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/Spans/view.js deleted file mode 100644 index 9d7d283043101..0000000000000 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/Spans/view.js +++ /dev/null @@ -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 ( - { - if (isEmpty(spans.data.spans)) { - return ( - - ); - } - - 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 ( -
- - - ({ - label: getSpanLabel(type), - color: getSpanColor(type) - }))} - transactionName={urlParams.transactionName} - /> - } - agentMarks={agentMarks} - duration={totalDuration} - height={timelineHeight} - margins={TIMELINE_MARGINS} - /> -
- {spans.data.spans.map(span => ( - - ))} -
-
-
- - {droppedSpans > 0 && ( - - {droppedSpans} spans dropped due to limit of{' '} - {spans.data.spans.length}.{' '} - - - )} -
- ); - }} - /> - ); - } -} - -function DroppedSpansDocsLink({ agentName }) { - const docs = getFeatureDocs('dropped-spans', agentName); - - if (!docs || !docs.url) { - return null; - } - - return ( - - Learn more in the documentation. - - ); -} - -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; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.js b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.js deleted file mode 100644 index 55cedaedec4f8..0000000000000 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.js +++ /dev/null @@ -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 ; -} - -StickyTransactionProperties.propTypes = { - transaction: PropTypes.object.isRequired -}; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.tsx new file mode 100644 index 0000000000000..c4c95f9a5f08c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.tsx @@ -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 ; +} + +const mapStateToProps = (state: any, props: Partial) => ({ + root: selectWaterfallRoot(state, props) +}); + +export const StickyTransactionProperties = connect<{}, {}, Props>( + mapStateToProps +)(StickyTransactionPropertiesComponent); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/TransactionPropertiesTable.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/TransactionPropertiesTable.tsx new file mode 100644 index 0000000000000..dff9b29797821 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/TransactionPropertiesTable.tsx @@ -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 ( +
+ + {tabs.map(key => { + return ( + { + history.replace({ + ...location, + search: fromQuery({ + ...toQuery(location.search), + detailTab: key + }) + }); + }} + selected={currentTab === key} + key={key} + > + {capitalize(key)} + + ); + })} + + + + + {currentTab === TIMELINE_TAB && ( + + )} + + {currentTab !== TIMELINE_TAB && ( + + + + )} +
+ ); +}; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/TransactionPropertiesTableForFlyout.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/TransactionPropertiesTableForFlyout.tsx new file mode 100644 index 0000000000000..25632040ae325 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/TransactionPropertiesTableForFlyout.tsx @@ -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 = ({ + location, + transaction, + urlParams +}) => { + const tabs = getTabs(transaction); + const currentTab = getCurrentTab(tabs, urlParams.flyoutDetailTab); + const agentName = transaction.context.service.agent.name; + + return ( +
+ + {tabs.map(key => { + return ( + { + history.replace({ + ...location, + search: fromQuery({ + ...toQuery(location.search), + flyoutDetailTab: key + }) + }); + }} + isSelected={currentTab === key} + key={key} + > + {capitalize(key)} + + ); + })} + + + +
+ ); +}; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/ServiceLegends.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/ServiceLegends.tsx new file mode 100644 index 0000000000000..0ebadbe099e0b --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/ServiceLegends.tsx @@ -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 ( + + {Object.entries(serviceColors).map(([label, color]) => ( + + ))} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx new file mode 100644 index 0000000000000..533aa85278dd3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx @@ -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: ( + + {transaction.context.service.name} + + ), + width: '50%' + }, + { + label: 'Transaction', + fieldName: TRANSACTION_NAME, + val: ( + + {transaction.transaction.name} + + ), + width: '50%' + } + ]; + + return ; +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx new file mode 100644 index 0000000000000..93266464ecdf5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx @@ -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 {dbContext.statement}; + } + + return ( + + +

Database statement

+
+ + + {dbContext.statement} + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx new file mode 100644 index 0000000000000..a746dec184f1c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx @@ -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 ; +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/index.tsx new file mode 100644 index 0000000000000..8d277a243e3f3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/index.tsx @@ -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 ( + + + + + +

Span details

+
+
+ + + + {`View span in Discover`} + + +
+
+ + + + + + + + + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx new file mode 100644 index 0000000000000..70cad1e83d252 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx @@ -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 ( + + + + + +

Transaction details

+
+
+ + + + + + + + + View transaction group details + + + +
+
+ + + + + + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/WaterfallItem.tsx new file mode 100644 index 0000000000000..be831598aca9a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -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 ( + + + + ‎ + {item.name} + ‎ + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/index.tsx new file mode 100644 index 0000000000000..66c991acf11ff --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/index.tsx @@ -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 { + 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 ( + + this.onOpenFlyout(item)} + /> + + {item.children && item.children.map(this.renderWaterfall)} + + ); + }; + + 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 ( + + ); + case 'transaction': + return ( + + ); + 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 ( + + + +
+ {this.renderWaterfall(waterfall.root)} +
+
+ + {this.getFlyOut()} +
+ ); + } + + private setQueryParams(params: Partial) { + 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, diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap new file mode 100644 index 0000000000000..2373e1aab64a5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap @@ -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 {}, + }, +} +`; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json new file mode 100644 index 0000000000000..e12fe757ef1ee --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json @@ -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" + } + } +] diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json new file mode 100644 index 0000000000000..401b8473ec3fc --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json @@ -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" + } +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts new file mode 100644 index 0000000000000..92305031c5af6 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts @@ -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(); +}); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts new file mode 100644 index 0000000000000..3b036da8202ec --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -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; +} + +interface IWaterfallItemSpan extends IWaterfallItemBase { + parentTransaction: Transaction; + span: Span; + docType: 'span'; + children?: Array; +} + +export type IWaterfallItem = IWaterfallItemSpan | IWaterfallItemTransaction; + +type Omit = Pick>; + +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; + +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, + 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, + 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 + }; +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/getServiceColors.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/getServiceColors.ts new file mode 100644 index 0000000000000..77d179d4b64eb --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/getServiceColors.ts @@ -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); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.test.ts new file mode 100644 index 0000000000000..295682df068ec --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.test.ts @@ -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([]); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.ts new file mode 100644 index 0000000000000..0aeb1d48b9456 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.ts @@ -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' + ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/index.tsx new file mode 100644 index 0000000000000..86892077ddc1c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/index.tsx @@ -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; +} + +function WaterfallRequest({ + urlParams, + transaction, + render +}: WaterfallRequestProps) { + const hasTrace = transaction.hasOwnProperty('trace'); + if (hasTrace) { + return ( + + ); + } else { + return ( + + ); + } +} + +export function WaterfallContainer({ + location, + urlParams, + transaction +}: Props) { + return ( + { + const agentMarks = getAgentMarks(transaction); + const waterfall = getWaterfall(data.hits, data.services, transaction); + if (!waterfall) { + return null; + } + const serviceColors = getServiceColors(waterfall.services); + + return ( +
+ + +
+ ); + }} + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/__jest__/view.test.js b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/__jest__/view.test.js deleted file mode 100644 index f26588b861615..0000000000000 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/__jest__/view.test.js +++ /dev/null @@ -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 } - ]); - }); - }); -}); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/index.js b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/index.js deleted file mode 100644 index 977acf2025cbf..0000000000000 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/index.js +++ /dev/null @@ -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); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx new file mode 100644 index 0000000000000..c329548d51f0d --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx @@ -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 = ( + + + View full trace + + + ); + } else { + button = View full trace; + } + + return ( + + {button} + + ); +} + +interface Props { + transaction: ITransaction; + urlParams: IUrlParams; + location: Location; + waterfallRoot?: ITransaction; +} + +export const Transaction: React.SFC = ({ + transaction, + urlParams, + location, + waterfallRoot +}) => { + if (isEmpty(transaction)) { + return ( + + ); + } + + const root = waterfallRoot || transaction; + + return ( + + + + + Transaction sample + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/view.js b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/view.js deleted file mode 100644 index 0844cc054cad8..0000000000000 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/view.js +++ /dev/null @@ -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 ( - - ); - } - - 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 ( - - - - Transaction sample - - - {`View transaction in Discover`} - - - - - - - {[DEFAULT_TAB, ...tabs].map(key => { - return ( - { - history.replace({ - ...location, - search: fromQuery({ - ...toQuery(location.search), - detailTab: key - }) - }); - }} - selected={currentTab === key} - key={key} - > - {capitalize(key)} - - ); - })} - - - - {currentTab === DEFAULT_TAB ? ( - - ) : ( - - - - )} - - - ); -} - -Transaction.propTypes = { - urlParams: PropTypes.object.isRequired, - transaction: PropTypes.object -}; - -Transaction.defaultProps = { - transaction: {} -}; - -export default Transaction; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.ts new file mode 100644 index 0000000000000..af24413928283 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.ts @@ -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) { + return { + location: state.location, + urlParams: getUrlParams(state), + waterfallRoot: selectWaterfallRoot(state, props) + }; +} + +const mapDispatchToProps = {}; +export const TransactionDetails = connect<{}, {}, Props>( + mapStateToProps, + mapDispatchToProps +)(TransactionDetailsView); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/view.js b/x-pack/plugins/apm/public/components/app/TransactionDetails/view.js deleted file mode 100644 index d1c48d31ba5cf..0000000000000 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/view.js +++ /dev/null @@ -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 ( -
- {urlParams.transactionName} - - - - - - ( - - )} - /> - - ( - - )} - /> - - ( - - )} - /> -
- ); -} - -export default TransactionDetails; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/view.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/view.tsx new file mode 100644 index 0000000000000..6ddc0c85c2161 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/view.tsx @@ -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 ( +
+ {urlParams.transactionName} + + + + + + ) => ( + + )} + /> + + ) => ( + + )} + /> + + + + ) => { + return ( + + ); + }} + /> +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.js b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.js index 97b257275ad63..52d8d6f4b3433 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.js +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.js @@ -4,11 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React from 'react'; import styled from 'styled-components'; -import { EuiBasicTable } from '@elastic/eui'; -import orderBy from 'lodash.orderby'; import TooltipOverlay from '../../../shared/TooltipOverlay'; import { RelativeLink, legacyEncodeURIComponent } from '../../../../utils/url'; import { @@ -16,9 +13,10 @@ import { asDecimal, tpmUnit } from '../../../../utils/formatters'; +import { ImpactBar } from '../../../shared/ImpactBar'; import { fontFamilyCode, truncate } from '../../../../style/variables'; -import ImpactSparkline from './ImpactSparkLine'; +import { ManagedTable } from '../../../shared/ManagedTable'; function tpmLabel(type) { 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'; } -function paginateItems({ items, pageIndex, pageSize }) { - return items.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize); -} - const TransactionNameLink = styled(RelativeLink)` ${truncate('100%')}; font-family: ${fontFamilyCode}; `; -class List extends Component { - state = { - page: { - index: 0, - size: 25 +export default function TransactionList({ + items, + agentName, + serviceName, + type, + ...rest +}) { + const columns = [ + { + field: 'name', + name: 'Name', + width: '50%', + sortable: true, + render: transactionName => { + const transactionUrl = `${serviceName}/transactions/${legacyEncodeURIComponent( + type + )}/${legacyEncodeURIComponent(transactionName)}`; + + return ( + + + {transactionName || 'N/A'} + + + ); + } + }, + { + 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) }, - sort: { - field: 'impactRelative', - direction: 'desc' + { + 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 => } - }; - - onTableChange = ({ page = {}, sort = {} }) => { - this.setState({ page, sort }); - }; - - render() { - const { agentName, serviceName, type } = this.props; - - const columns = [ - { - field: 'name', - name: 'Name', - width: '50%', - sortable: true, - render: transactionName => { - const transactionUrl = `${serviceName}/transactions/${legacyEncodeURIComponent( - type - )}/${legacyEncodeURIComponent(transactionName)}`; - - return ( - - - {transactionName || 'N/A'} - - - ); - } - }, - { - 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 => - } - ]; - - 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 ( - - ); - } + ]; + + return ( + + ); } - -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' -// : ''; -// }; diff --git a/x-pack/plugins/apm/public/components/shared/DiscoverButton.js b/x-pack/plugins/apm/public/components/shared/DiscoverButton.js index af241b297f97f..153a1a0282eba 100644 --- a/x-pack/plugins/apm/public/components/shared/DiscoverButton.js +++ b/x-pack/plugins/apm/public/components/shared/DiscoverButton.js @@ -8,9 +8,14 @@ import React from 'react'; import { KibanaLink } from '../../utils/url'; import { EuiButton } from '@elastic/eui'; -function DiscoverButton({ query, children }) { +function DiscoverButton({ query, children, ...rest }) { return ( - + {children || 'View in Discover'} diff --git a/x-pack/plugins/apm/public/components/shared/EmptyMessage.js b/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx similarity index 59% rename from x-pack/plugins/apm/public/components/shared/EmptyMessage.js rename to x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx index 45d1b453370cc..274d063555d14 100644 --- a/x-pack/plugins/apm/public/components/shared/EmptyMessage.js +++ b/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx @@ -4,15 +4,14 @@ * 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 React from 'react'; -function EmptyMessage({ heading, subheading, hideSubheading }) { - if (!subheading) { - subheading = 'Try another time range or reset the search filter.'; - } - +function EmptyMessage({ + heading = 'No data found.', + subheading = 'Try another time range or reset the search filter.', + hideSubheading = false +}) { return ( { + it('should render with default values', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('should render with overridden values', () => { + expect( + shallow() + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap b/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap new file mode 100644 index 0000000000000..67378b5634040 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ImpactBar component should render with default values 1`] = ` + +`; + +exports[`ImpactBar component should render with overridden values 1`] = ` + +`; diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx new file mode 100644 index 0000000000000..53f21c4e247c0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx @@ -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 { + value: number; + max?: number; +} + +export function ImpactBar({ value, max = 100, ...rest }: ImpactBarProps) { + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.js b/x-pack/plugins/apm/public/components/shared/KueryBar/index.js index e1a8baf74db7b..f5a621dbb2faa 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.js +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.js @@ -5,8 +5,8 @@ */ import { connect } from 'react-redux'; -import view from './view'; import { getUrlParams } from '../../../store/urlParams'; +import view from './view'; function mapStateToProps(state = {}) { return { diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js b/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js new file mode 100644 index 0000000000000..a775602cce600 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js @@ -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() + ).toMatchSnapshot(); + }); + + it('should render when specifying initial values', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap b/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap new file mode 100644 index 0000000000000..59679bfe11641 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ManagedTable component should render a page-full of items, with defaults 1`] = ` + +`; + +exports[`ManagedTable component should render when specifying initial values 1`] = ` + +`; diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx new file mode 100644 index 0000000000000..c963a0b458554 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx @@ -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>; + columns: ITableColumn[]; + initialPageIndex?: number; + initialPageSize?: number; + hidePerPageOptions?: boolean; + initialSort?: { + field: string; + direction: 'asc' | 'desc'; + }; + noItemsMessage?: any; +} + +export class ManagedTable extends Component { + 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 ( + + ); + } +} diff --git a/x-pack/plugins/apm/public/components/shared/Modal.js b/x-pack/plugins/apm/public/components/shared/Modal.js deleted file mode 100644 index 07e8d9ada9961..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/Modal.js +++ /dev/null @@ -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 ( - -
- - - e.stopPropagation()}> -
- {this.props.header} - -
- {this.props.children} -
-
-
-
- ); - } -} - -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; diff --git a/x-pack/plugins/apm/public/components/shared/PropertiesTable/NestedKeyValueTable.tsx b/x-pack/plugins/apm/public/components/shared/PropertiesTable/NestedKeyValueTable.tsx index 37671e7975f91..b23549a200a26 100644 --- a/x-pack/plugins/apm/public/components/shared/PropertiesTable/NestedKeyValueTable.tsx +++ b/x-pack/plugins/apm/public/components/shared/PropertiesTable/NestedKeyValueTable.tsx @@ -7,6 +7,8 @@ import _ from 'lodash'; import React from 'react'; import styled from 'styled-components'; + +import { StringMap } from '../../../../typings/common'; import { colors, fontFamilyCode, @@ -15,6 +17,8 @@ import { units } from '../../../style/variables'; +export type KeySorter = (data: StringMap, parentKey?: string) => string[]; + const Table = styled.table` font-family: ${fontFamilyCode}; font-size: ${fontSizes.small}; diff --git a/x-pack/plugins/apm/public/components/shared/PropertiesTable/__test__/PropertiesTable.test.js b/x-pack/plugins/apm/public/components/shared/PropertiesTable/__test__/PropertiesTable.test.js index 7cee0b14c57af..0a90bedc95d32 100644 --- a/x-pack/plugins/apm/public/components/shared/PropertiesTable/__test__/PropertiesTable.test.js +++ b/x-pack/plugins/apm/public/components/shared/PropertiesTable/__test__/PropertiesTable.test.js @@ -12,9 +12,9 @@ import { sortKeysByConfig, getPropertyTabNames } 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', () => [ { key: 'testProperty', @@ -105,32 +105,13 @@ describe('getPropertyTabNames', () => { }); describe('AgentFeatureTipMessage component', () => { - let mockDocs; - const featureName = ''; - const agentName = ''; - - beforeEach(() => { - mockDocs = { - text: 'Mock Docs Text', - url: 'mock-url' - }; - getFeatureDocs.mockImplementation(() => mockDocs); - }); + const featureName = 'user'; + const agentName = 'nodejs'; it('should render when docs are returned', () => { - expect( - shallow( - - ) - ).toMatchSnapshot(); - expect(getFeatureDocs).toHaveBeenCalledWith(featureName, agentName); - }); + const mockDocs = 'mock-url'; + getAgentFeatureDocsUrl.mockImplementation(() => mockDocs); - it('should render when docs are returned, but missing a url', () => { - delete mockDocs.url; expect( shallow( { /> ) ).toMatchSnapshot(); + expect(getAgentFeatureDocsUrl).toHaveBeenCalledWith(featureName, agentName); }); it('should render null empty string when no docs are returned', () => { - mockDocs = null; + getAgentFeatureDocsUrl.mockImplementation(() => null); expect( shallow( - Mock Docs Text + You can configure your agent to add contextual information about your users. `; -exports[`AgentFeatureTipMessage component should render when docs are returned, but missing a url 1`] = ` - - - Mock Docs Text - - -`; - exports[`PropertiesTable component should render empty when data has no keys 1`] = ` @@ -68,7 +57,7 @@ exports[`PropertiesTable component should render with data 1`] = ` /> `; diff --git a/x-pack/plugins/apm/public/components/shared/PropertiesTable/index.tsx b/x-pack/plugins/apm/public/components/shared/PropertiesTable/index.tsx index 979a3cab39097..dfbdae8a7df5c 100644 --- a/x-pack/plugins/apm/public/components/shared/PropertiesTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/PropertiesTable/index.tsx @@ -8,11 +8,13 @@ import { EuiIcon } from '@elastic/eui'; import _ from 'lodash'; import React from 'react'; import styled from 'styled-components'; + +import { StringMap } from '../../../../typings/common'; import { colors, fontSize, px, unit, units } from '../../../style/variables'; -import { getFeatureDocs } from '../../../utils/documentation'; +import { getAgentFeatureDocsUrl } from '../../../utils/documentation/agents'; // @ts-ignore import { ExternalLink } from '../../../utils/url'; -import { NestedKeyValueTable } from './NestedKeyValueTable'; +import { KeySorter, NestedKeyValueTable } from './NestedKeyValueTable'; import PROPERTY_CONFIG from './propertyConfig.json'; const indexedPropertyConfig = _.indexBy(PROPERTY_CONFIG, 'key'); @@ -36,28 +38,36 @@ export function getPropertyTabNames(selected: string[]): string[] { ).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({ featureName, agentName }: { featureName: string; - agentName: string; -}): JSX.Element | null { - const docs = getFeatureDocs(featureName, agentName); - - if (!docs) { + agentName?: string; +}) { + const docsUrl = getAgentFeatureDocsUrl(featureName, agentName); + if (!docsUrl) { return null; } return ( - {docs.text}{' '} - {docs.url && ( - - Learn more in the documentation. - - )} + {getAgentFeatureText(featureName)}{' '} + + Learn more in the documentation. + ); } @@ -78,7 +88,7 @@ export function PropertiesTable({ }: { propData: StringMap; propKey: string; - agentName: string; + agentName?: string; }) { if (_.isEmpty(propData)) { return ( @@ -98,10 +108,7 @@ export function PropertiesTable({ keySorter={sortKeysByConfig} depth={1} /> - + ); } diff --git a/x-pack/plugins/apm/public/components/shared/SetupInstructionsLink.tsx b/x-pack/plugins/apm/public/components/shared/SetupInstructionsLink.tsx new file mode 100644 index 0000000000000..e57754364fdc8 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/SetupInstructionsLink.tsx @@ -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 ( + + + Setup Instructions + + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js index 2e93f5c17c0e3..0bec4149914d4 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js @@ -11,8 +11,7 @@ import CodePreview from '../../shared/CodePreview'; import { Ellipsis } from '../../shared/Icons'; import { units, px } from '../../../style/variables'; import EmptyMessage from '../../shared/EmptyMessage'; -import { EuiLink } from '@elastic/eui'; -import { HeaderXSmall } from '../UIComponents'; +import { EuiLink, EuiTitle } from '@elastic/eui'; const LibraryFrameToggle = styled.div` margin: 0 0 ${px(units.plus)} 0; @@ -75,7 +74,9 @@ class Stacktrace extends PureComponent { return (
- Stacktraces + +

Stack traces

+
{getCollapsedLibraryFrames(stackframes).map((item, i) => { if (!item.libraryFrame) { return ( diff --git a/x-pack/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap b/x-pack/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap index e71121fbe578e..daefa91bbd80f 100644 --- a/x-pack/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap @@ -2,46 +2,25 @@ exports[`StickyProperties should render 1`] = ` .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; font-size: 12px; color: #999999; } -.c2 span { +.c0 span { cursor: help; } -.c4 { +.c2 { color: #999999; } -.c3 { +.c1 { display: inline-block; line-height: 16px; } -.c5 { +.c3 { display: inline-block; line-height: 16px; max-width: 100%; @@ -51,13 +30,25 @@ exports[`StickyProperties should render 1`] = ` }
1337 minutes ago (mocking 1536405447) ( 1st of January (mocking 1536405447) @@ -82,10 +73,16 @@ exports[`StickyProperties should render 1`] = `
@@ -105,10 +102,16 @@ exports[`StickyProperties should render 1`] = `
GET
true
1337
diff --git a/x-pack/plugins/apm/public/components/shared/StickyProperties/index.js b/x-pack/plugins/apm/public/components/shared/StickyProperties/index.js index 3c690e3031c6a..5a51ba338ad5a 100644 --- a/x-pack/plugins/apm/public/components/shared/StickyProperties/index.js +++ b/x-pack/plugins/apm/public/components/shared/StickyProperties/index.js @@ -4,32 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; import moment from 'moment'; - +import TooltipOverlay from '../../shared/TooltipOverlay'; import { unit, units, px, + fontFamilyCode, fontSizes, colors, truncate } from '../../../style/variables'; -import TooltipOverlay, { fieldNameHelper } from '../../shared/TooltipOverlay'; - -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 TooltipFieldName = styled.span` + font-family: ${fontFamilyCode}; `; const PropertyLabel = styled.div` @@ -57,6 +48,15 @@ const PropertyValueTruncated = styled.span` ${truncate('100%')}; `; +function fieldNameHelper(name) { + return ( + + Field name:
+ {name} +
+ ); +} + function TimestampValue({ timestamp }) { const time = moment(timestamp); const timeAgo = timestamp ? time.fromNow() : 'N/A'; @@ -98,19 +98,46 @@ function getPropertyValue({ val, fieldName, truncated = false }) { ); } - return {String(val)}; + return {val}; } 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 ( - + {stickyProperties && - stickyProperties.map((prop, i) => ( - - {getPropertyLabel(prop)} - {getPropertyValue(prop)} - - ))} - + stickyProperties.map(({ width = 0, ...prop }, i) => { + return ( + + {getPropertyLabel(prop)} + {getPropertyValue(prop)} + + ); + })} + ); } diff --git a/x-pack/plugins/apm/public/components/shared/TooltipOverlay.js b/x-pack/plugins/apm/public/components/shared/TooltipOverlay.js index 2addcc925ea91..c49fc01a4c124 100644 --- a/x-pack/plugins/apm/public/components/shared/TooltipOverlay.js +++ b/x-pack/plugins/apm/public/components/shared/TooltipOverlay.js @@ -5,15 +5,9 @@ */ import React from 'react'; -import styled from 'styled-components'; -import { fontFamilyCode } from '../../style/variables'; import { Tooltip } from 'pivotal-ui/react/tooltip'; import { OverlayTrigger } from 'pivotal-ui/react/overlay-trigger'; -const TooltipFieldName = styled.span` - font-family: ${fontFamilyCode}; -`; - function TooltipOverlay({ children, content, delay = 1000 }) { return ( - Field name:
- {name} - - ); -} - export default TooltipOverlay; diff --git a/x-pack/plugins/apm/public/components/shared/TraceLink.tsx b/x-pack/plugins/apm/public/components/shared/TraceLink.tsx new file mode 100644 index 0000000000000..684f82d27646d --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/TraceLink.tsx @@ -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 = ({ + transaction, + children +}) => { + if (!transaction) { + return null; + } + + const linkProps = getLinkProps(transaction); + + if (!linkProps) { + // TODO: Should this case return unlinked children, null, or something else? + return {children}; + } + + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/apm/public/components/shared/UIComponents.js b/x-pack/plugins/apm/public/components/shared/UIComponents.js index 97030a3fa6d79..e286b68c7e495 100644 --- a/x-pack/plugins/apm/public/components/shared/UIComponents.js +++ b/x-pack/plugins/apm/public/components/shared/UIComponents.js @@ -5,14 +5,7 @@ */ import styled from 'styled-components'; -import { - unit, - units, - px, - fontSizes, - colors, - fontSize -} from '../../style/variables'; +import { unit, units, px, fontSizes, colors } from '../../style/variables'; import { RelativeLink } from '../../utils/url'; export const HeaderContainer = styled.div` @@ -47,12 +40,6 @@ export const HeaderSmall = styled.h3` ${props => props.css}; `; -export const HeaderXSmall = styled.h4` - margin: ${px(units.plus)} 0; - font-size: ${fontSize}; - ${props => props.css}; -`; - export const Tab = styled.div` display: inline-block; font-size: ${fontSizes.large}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js index 24e1711608cfb..9b1c462ed201e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js @@ -16,7 +16,7 @@ import { timeUnit } from '../../../../../utils/formatters'; import { toJson } from '../../../../../utils/testHelpers'; -import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/view'; +import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/index'; describe('Histogram', () => { let wrapper; @@ -98,9 +98,10 @@ describe('Histogram', () => { it('should update state with "hoveredBucket"', () => { expect(wrapper.state()).toEqual({ hoveredBucket: { - sampled: true, + sample: { + transactionId: '99c50a5b-44b4-4289-a3d1-a2815d128192' + }, style: { cursor: 'pointer' }, - transactionId: '99c50a5b-44b4-4289-a3d1-a2815d128192', x: 869010, x0: 811076, y: 49 @@ -124,9 +125,10 @@ describe('Histogram', () => { it('should call onClick with bucket', () => { expect(onClick).toHaveBeenCalledWith({ - sampled: true, + sample: { + transactionId: '99c50a5b-44b4-4289-a3d1-a2815d128192' + }, style: { cursor: 'pointer' }, - transactionId: '99c50a5b-44b4-4289-a3d1-a2815d128192', x: 869010, x0: 811076, y: 49 diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap index 2dfbab058941d..2dfc2dfe57967 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap @@ -961,6 +961,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -976,6 +977,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -991,6 +993,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1006,6 +1009,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1021,6 +1025,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1084,6 +1089,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1099,6 +1105,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1130,6 +1137,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1145,6 +1153,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1160,6 +1169,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1175,6 +1185,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1190,6 +1201,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1205,6 +1217,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1220,6 +1233,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1235,6 +1249,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1250,6 +1265,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1265,6 +1281,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1280,6 +1297,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1295,6 +1313,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1310,6 +1329,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1325,6 +1345,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1340,6 +1361,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1355,6 +1377,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } @@ -1370,6 +1393,7 @@ exports[`Histogram Initially should have default markup 1`] = ` onMouseUp={[Function]} style={ Object { + "cursor": "default", "pointerEvents": "all", } } diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json index a642de12a28ff..f48213f72a983 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json @@ -8,20 +8,23 @@ { "key": 579340, "count": 8, - "transactionId": "99437ee4-08d4-41f5-9b2b-93cc32ec3dfb", - "sampled": true + "sample": { + "transactionId": "99437ee4-08d4-41f5-9b2b-93cc32ec3dfb" + } }, { "key": 695208, "count": 23, - "transactionId": "d327611b-e999-4942-a94f-c60208940180", - "sampled": true + "sample": { + "transactionId": "d327611b-e999-4942-a94f-c60208940180" + } }, { "key": 811076, "count": 49, - "transactionId": "99c50a5b-44b4-4289-a3d1-a2815d128192", - "sampled": true + "sample": { + "transactionId": "99c50a5b-44b4-4289-a3d1-a2815d128192" + } }, { "key": 926944, @@ -36,8 +39,9 @@ { "key": 1158680, "count": 13, - "transactionId": "8486d3e2-7f15-48df-aa37-6ee9955adbd2", - "sampled": true + "sample": { + "transactionId": "8486d3e2-7f15-48df-aa37-6ee9955adbd2" + } }, { "key": 1274548, diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js b/x-pack/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js index a7d2c57151a61..6468562012a93 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js @@ -28,7 +28,8 @@ export default function AgentMarker({ agentMark, x }) {
{agentMark.name} - {asTime(agentMark.timeLabel)} + {asTime(agentMark.us)}
} > diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js index 32c9db3fe762a..0dc155c0bc082 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js @@ -18,7 +18,7 @@ import { getTimeFormatter } from '../../../../utils/formatters'; const getXAxisTickValues = (tickValues, xMax) => _.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 tickFormat = getTimeFormatter(xMax); const xAxisTickValues = getXAxisTickValues(tickValues, xMax); @@ -38,13 +38,12 @@ function TimelineAxis({ header, plotValues, agentMarks }) { ...style }} > - {header} ( ))} diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.js b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.js index 317f01290f403..ae1297aa1a191 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.js @@ -20,9 +20,7 @@ class VerticalLines extends PureComponent { xMax } = this.props.plotValues; - const agentMarkTimes = this.props.agentMarks.map( - ({ timeAxis }) => timeAxis - ); + const agentMarkTimes = this.props.agentMarks.map(({ us }) => us); return (
-
- Hello - i am a header -
state.duration, - state => state.height, - state => state.margins, - state => state.width, - getPlotValues - ); - render() { - const { width, duration, header, agentMarks } = this.props; + const { width, duration, agentMarks, height, margins } = this.props; if (duration == null || !width) { return null; } - const plotValues = this.getPlotValues(this.props); + const plotValues = getPlotValues({ width, duration, height, margins }); return (
- +
); diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/plotUtils.js b/x-pack/plugins/apm/public/components/shared/charts/Timeline/plotUtils.js index 383a5a26b8ac0..e3004edd7b3d9 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/plotUtils.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/plotUtils.js @@ -6,7 +6,7 @@ import { scaleLinear } from 'd3-scale'; -export function getPlotValues(duration, height, margins, width) { +export function getPlotValues({ width, duration, height, margins }) { const xMin = 0; const xMax = duration; const xScale = scaleLinear() diff --git a/x-pack/plugins/apm/public/services/rest/apm.js b/x-pack/plugins/apm/public/services/rest/apm.ts similarity index 59% rename from x-pack/plugins/apm/public/services/rest/apm.js rename to x-pack/plugins/apm/public/services/rest/apm.ts index 3fd48afba9a3e..93980cbd5218d 100644 --- a/x-pack/plugins/apm/public/services/rest/apm.js +++ b/x-pack/plugins/apm/public/services/rest/apm.ts @@ -4,9 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +// @ts-ignore 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'; +// @ts-ignore import { callApi } from './callApi'; +// @ts-ignore import { getAPMIndexPattern } from './savedObjects'; 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) { return; } @@ -42,7 +55,11 @@ export async function getEncodedEsQuery(kuery) { return encodeURIComponent(JSON.stringify(esFilterQuery)); } -export async function loadServiceList({ start, end, kuery }) { +export async function loadServiceList({ + start, + end, + kuery +}: IUrlParams): Promise { return callApi({ pathname: `/api/apm/services`, 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 { return callApi({ pathname: `/api/apm/services/${serviceName}`, query: { @@ -64,14 +86,34 @@ export async function loadServiceDetails({ serviceName, start, end, kuery }) { }); } +export async function loadTraceList({ + start, + end, + kuery +}: IUrlParams): Promise { + 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({ serviceName, start, end, kuery, transactionType -}) { - return callApi({ +}: IUrlParams): Promise { + const groups: ITransactionGroup[] = await callApi({ pathname: `/api/apm/services/${serviceName}/transactions`, query: { start, @@ -80,6 +122,11 @@ export async function loadTransactionList({ transaction_type: transactionType } }); + + return groups.map(group => { + group.sample = addVersion(group.sample); + return group; + }); } export async function loadTransactionDistribution({ @@ -88,7 +135,7 @@ export async function loadTransactionDistribution({ end, transactionName, kuery -}) { +}: IUrlParams): Promise { return callApi({ pathname: `/api/apm/services/${serviceName}/transactions/distribution`, query: { @@ -100,14 +147,54 @@ export async function loadTransactionDistribution({ }); } -export async function loadSpans({ serviceName, start, end, transactionId }) { - return callApi({ +function addVersion(item: T): T { + 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 { + const hits: Span[] = await callApi({ pathname: `/api/apm/services/${serviceName}/transactions/${transactionId}/spans`, query: { start, 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({ @@ -115,12 +202,14 @@ export async function loadTransaction({ start, end, transactionId, + traceId, kuery -}) { - const res = await callApi( +}: IUrlParams) { + const result: Transaction = await callApi( { pathname: `/api/apm/services/${serviceName}/transactions/${transactionId}`, query: { + traceId, start, end, esFilterQuery: await getEncodedEsQuery(kuery) @@ -130,11 +219,8 @@ export async function loadTransaction({ camelcase: false } ); - const camelizedRes = camelizeKeys(res); - if (res.context) { - camelizedRes.context = res.context; - } - return camelizedRes; + + return addVersion(result); } export async function loadCharts({ @@ -144,7 +230,7 @@ export async function loadCharts({ kuery, transactionType, transactionName -}) { +}: IUrlParams) { return callApi({ pathname: `/api/apm/services/${serviceName}/transactions/charts`, query: { @@ -157,6 +243,12 @@ export async function loadCharts({ }); } +interface ErrorGroupListParams extends IUrlParams { + size: number; + sortField: string; + sortDirection: string; +} + export async function loadErrorGroupList({ serviceName, start, @@ -165,7 +257,7 @@ export async function loadErrorGroupList({ size, sortField, sortDirection -}) { +}: ErrorGroupListParams) { return callApi({ pathname: `/api/apm/services/${serviceName}/errors`, query: { @@ -185,7 +277,7 @@ export async function loadErrorGroupDetails({ end, kuery, errorGroupId -}) { +}: IUrlParams) { const res = await callApi( { pathname: `/api/apm/services/${serviceName}/errors/${errorGroupId}`, @@ -212,7 +304,7 @@ export async function loadErrorDistribution({ end, kuery, errorGroupId -}) { +}: IUrlParams) { return callApi({ pathname: `/api/apm/services/${serviceName}/errors/${errorGroupId}/distribution`, query: { diff --git a/x-pack/plugins/apm/public/store/__jest__/rootReducer.test.js b/x-pack/plugins/apm/public/store/__jest__/rootReducer.test.js index 46e96d5303b22..2db1f1f23eb26 100644 --- a/x-pack/plugins/apm/public/store/__jest__/rootReducer.test.js +++ b/x-pack/plugins/apm/public/store/__jest__/rootReducer.test.js @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import reducer from '../rootReducer'; +import { rootReducer } from '../rootReducer'; describe('root reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual({ + expect(rootReducer(undefined, {})).toEqual({ location: { hash: '', pathname: '', search: '' }, reactReduxRequest: {}, urlParams: {} diff --git a/x-pack/plugins/apm/public/store/__jest__/urlParams.test.js b/x-pack/plugins/apm/public/store/__jest__/urlParams.test.js index bd849be83c0df..c55e8e66724c4 100644 --- a/x-pack/plugins/apm/public/store/__jest__/urlParams.test.js +++ b/x-pack/plugins/apm/public/store/__jest__/urlParams.test.js @@ -4,12 +4,12 @@ * 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'; describe('urlParams', () => { it('should handle LOCATION_UPDATE for transactions section', () => { - const state = reducer( + const state = urlParamsReducer( {}, { type: LOCATION_UPDATE, @@ -34,7 +34,7 @@ describe('urlParams', () => { }); it('should handle LOCATION_UPDATE for error section', () => { - const state = reducer( + const state = urlParamsReducer( {}, { type: LOCATION_UPDATE, @@ -56,7 +56,7 @@ describe('urlParams', () => { }); it('should handle TIMEPICKER_UPDATE', () => { - const state = reducer( + const state = urlParamsReducer( {}, updateTimePicker({ min: 'minTime', diff --git a/x-pack/plugins/apm/public/store/config/configureStore.dev.js b/x-pack/plugins/apm/public/store/config/configureStore.dev.js index e2f37391ee317..8187e87507858 100644 --- a/x-pack/plugins/apm/public/store/config/configureStore.dev.js +++ b/x-pack/plugins/apm/public/store/config/configureStore.dev.js @@ -7,7 +7,7 @@ import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; import throttle from '../middleware/throttle'; -import rootReducer from '../rootReducer'; +import { rootReducer } from '../rootReducer'; export default function configureStore(preloadedState) { const composeEnhancers = diff --git a/x-pack/plugins/apm/public/store/config/configureStore.prod.js b/x-pack/plugins/apm/public/store/config/configureStore.prod.js index d52bc2ba45f53..ee3034156b927 100644 --- a/x-pack/plugins/apm/public/store/config/configureStore.prod.js +++ b/x-pack/plugins/apm/public/store/config/configureStore.prod.js @@ -6,7 +6,7 @@ import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; -import rootReducer from '../rootReducer'; +import { rootReducer } from '../rootReducer'; export default function configureStore(preloadedState) { return createStore(rootReducer, preloadedState, applyMiddleware(thunk)); diff --git a/x-pack/plugins/apm/public/store/mockData/mockTraceList.json b/x-pack/plugins/apm/public/store/mockData/mockTraceList.json new file mode 100644 index 0000000000000..4e97a030a2621 --- /dev/null +++ b/x-pack/plugins/apm/public/store/mockData/mockTraceList.json @@ -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 + } +] diff --git a/x-pack/plugins/apm/public/store/reactReduxRequest/errorDistribution.js b/x-pack/plugins/apm/public/store/reactReduxRequest/errorDistribution.js index b01bee5f37d78..a800b31f9b3b5 100644 --- a/x-pack/plugins/apm/public/store/reactReduxRequest/errorDistribution.js +++ b/x-pack/plugins/apm/public/store/reactReduxRequest/errorDistribution.js @@ -20,7 +20,7 @@ export function getErrorDistribution(state) { export function ErrorDistributionRequest({ urlParams, render }) { const { serviceName, start, end, errorGroupId, kuery } = urlParams; - if (!(serviceName, start, end, errorGroupId)) { + if (!(serviceName && start && end && errorGroupId)) { return null; } diff --git a/x-pack/plugins/apm/public/store/reactReduxRequest/spans.js b/x-pack/plugins/apm/public/store/reactReduxRequest/spans.js deleted file mode 100644 index eb35e32a311e0..0000000000000 --- a/x-pack/plugins/apm/public/store/reactReduxRequest/spans.js +++ /dev/null @@ -1,36 +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 { createInitialDataSelector } from './helpers'; -import { Request } from 'react-redux-request'; -import { loadSpans } from '../../services/rest/apm'; - -const ID = 'spans'; -const INITIAL_DATA = {}; -const withInitialData = createInitialDataSelector(INITIAL_DATA); - -export function getSpans(state) { - return withInitialData(state.reactReduxRequest[ID]); -} - -export function SpansRequest({ urlParams, render }) { - const { serviceName, start, end, transactionId, kuery } = urlParams; - - if (!(serviceName && start && end && transactionId)) { - return null; - } - - return ( - - ); -} diff --git a/x-pack/plugins/apm/public/store/reactReduxRequest/traceList.js b/x-pack/plugins/apm/public/store/reactReduxRequest/traceList.js new file mode 100644 index 0000000000000..804e404189861 --- /dev/null +++ b/x-pack/plugins/apm/public/store/reactReduxRequest/traceList.js @@ -0,0 +1,48 @@ +/* + * 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 { Request } from 'react-redux-request'; +import { createSelector } from 'reselect'; +import { loadTraceList } from '../../services/rest/apm'; +import { createInitialDataSelector } from './helpers'; + +const ID = 'traceList'; +const INITIAL_DATA = []; +const withInitialData = createInitialDataSelector(INITIAL_DATA); + +const selectRRR = (state = {}) => state.reactReduxRequest; + +export const selectTraceList = createSelector( + [selectRRR], + reactReduxRequest => { + return withInitialData(reactReduxRequest[ID]); + } +); + +export function TraceListRequest({ urlParams = {}, render }) { + const { start, end, kuery } = urlParams; + + if (!start || !end) { + return null; + } + + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/store/reactReduxRequest/transactionDetails.js b/x-pack/plugins/apm/public/store/reactReduxRequest/transactionDetails.js index bfc2c0fee99e0..887bb2a16dcbe 100644 --- a/x-pack/plugins/apm/public/store/reactReduxRequest/transactionDetails.js +++ b/x-pack/plugins/apm/public/store/reactReduxRequest/transactionDetails.js @@ -18,7 +18,7 @@ export function getTransactionDetails(state) { } export function TransactionDetailsRequest({ urlParams, render }) { - const { serviceName, start, end, transactionId, kuery } = urlParams; + const { serviceName, start, end, transactionId, traceId, kuery } = urlParams; if (!(serviceName && start && end && transactionId)) { return null; @@ -29,7 +29,7 @@ export function TransactionDetailsRequest({ urlParams, render }) { id={ID} fn={loadTransaction} selector={getTransactionDetails} - args={[{ serviceName, start, end, transactionId, kuery }]} + args={[{ serviceName, start, end, transactionId, traceId, kuery }]} render={render} /> ); diff --git a/x-pack/plugins/apm/public/store/reactReduxRequest/transactionDistribution.js b/x-pack/plugins/apm/public/store/reactReduxRequest/transactionDistribution.tsx similarity index 57% rename from x-pack/plugins/apm/public/store/reactReduxRequest/transactionDistribution.js rename to x-pack/plugins/apm/public/store/reactReduxRequest/transactionDistribution.tsx index ffc2d27614695..26f2d46add30c 100644 --- a/x-pack/plugins/apm/public/store/reactReduxRequest/transactionDistribution.js +++ b/x-pack/plugins/apm/public/store/reactReduxRequest/transactionDistribution.tsx @@ -5,24 +5,44 @@ */ import React from 'react'; -import { createInitialDataSelector } from './helpers'; -import { Request } from 'react-redux-request'; +import { Request, RRRRenderArgs } from 'react-redux-request'; +import { IDistributionResponse } from '../../../server/lib/transactions/distribution/get_distribution'; import { loadTransactionDistribution } from '../../services/rest/apm'; +import { IReduxState } from '../rootReducer'; +import { IUrlParams } from '../urlParams'; +// @ts-ignore +import { createInitialDataSelector } from './helpers'; const ID = 'transactionDistribution'; const INITIAL_DATA = { buckets: [], totalHits: 0 }; const withInitialData = createInitialDataSelector(INITIAL_DATA); -export function getTransactionDistribution(state) { +interface RrrResponse { + data: T; +} + +export function getTransactionDistribution( + state: IReduxState +): RrrResponse { return withInitialData(state.reactReduxRequest[ID]); } -export function getDefaultTransactionId(state) { +export function getDefaultDistributionSample(state: IReduxState) { const distribution = getTransactionDistribution(state); - return distribution.data.defaultTransactionId; + const { defaultSample = {} } = distribution.data; + return { + traceId: defaultSample.traceId, + transactionId: defaultSample.transactionId + }; } -export function TransactionDistributionRequest({ urlParams, render }) { +export function TransactionDistributionRequest({ + urlParams, + render +}: { + urlParams: IUrlParams; + render: (args: RRRRenderArgs) => any; +}) { const { serviceName, start, end, transactionName, kuery } = urlParams; if (!(serviceName && start && end && transactionName)) { diff --git a/x-pack/plugins/apm/public/store/reactReduxRequest/waterfallV1.tsx b/x-pack/plugins/apm/public/store/reactReduxRequest/waterfallV1.tsx new file mode 100644 index 0000000000000..00b6709fc5017 --- /dev/null +++ b/x-pack/plugins/apm/public/store/reactReduxRequest/waterfallV1.tsx @@ -0,0 +1,54 @@ +/* + * 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 { Request, RRRRender } from 'react-redux-request'; +import { + SERVICE_NAME, + TRANSACTION_ID +} from 'x-pack/plugins/apm/common/constants'; +import { Span } from 'x-pack/plugins/apm/typings/Span'; +import { Transaction } from 'x-pack/plugins/apm/typings/Transaction'; +import { WaterfallResponse } from 'x-pack/plugins/apm/typings/waterfall'; +import { loadSpans } from '../../services/rest/apm'; +import { IUrlParams } from '../urlParams'; +// @ts-ignore +import { createInitialDataSelector } from './helpers'; + +export const ID = 'waterfallV1'; + +interface Props { + urlParams: IUrlParams; + transaction: Transaction; + render: RRRRender; +} + +export function WaterfallV1Request({ urlParams, transaction, render }: Props) { + const { start, end } = urlParams; + const transactionId: string = get(transaction, TRANSACTION_ID); + const serviceName: string = get(transaction, SERVICE_NAME); + + if (!(serviceName && transactionId && start && end)) { + return null; + } + + return ( + + id={ID} + fn={loadSpans} + args={[{ serviceName, start, end, transactionId }]} + render={({ status, data = [], args }) => { + const res = { + hits: [transaction, ...data], + services: [serviceName] + }; + + return render({ status, data: res, args }); + }} + /> + ); +} diff --git a/x-pack/plugins/apm/public/store/reactReduxRequest/waterfallV2.tsx b/x-pack/plugins/apm/public/store/reactReduxRequest/waterfallV2.tsx new file mode 100644 index 0000000000000..c5983f7574090 --- /dev/null +++ b/x-pack/plugins/apm/public/store/reactReduxRequest/waterfallV2.tsx @@ -0,0 +1,45 @@ +/* + * 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 { Request, RRRRender } from 'react-redux-request'; +import { TRACE_ID } from 'x-pack/plugins/apm/common/constants'; +import { Transaction } from 'x-pack/plugins/apm/typings/Transaction'; +import { WaterfallResponse } from 'x-pack/plugins/apm/typings/waterfall'; +import { loadTrace } from '../../services/rest/apm'; +import { IUrlParams } from '../urlParams'; +// @ts-ignore +import { createInitialDataSelector } from './helpers'; + +export const ID = 'waterfallV2'; + +interface Props { + urlParams: IUrlParams; + transaction: Transaction; + render: RRRRender; +} + +const defaultData = { hits: [], services: [] }; +export function WaterfallV2Request({ urlParams, transaction, render }: Props) { + const { start, end } = urlParams; + const traceId: string = get(transaction, TRACE_ID); + + if (!(traceId && start && end)) { + return null; + } + + return ( + + id={ID} + fn={loadTrace} + args={[{ traceId, start, end }]} + render={({ args, data = defaultData, status }) => + render({ args, data, status }) + } + /> + ); +} diff --git a/x-pack/plugins/apm/public/store/rootReducer.js b/x-pack/plugins/apm/public/store/rootReducer.ts similarity index 57% rename from x-pack/plugins/apm/public/store/rootReducer.js rename to x-pack/plugins/apm/public/store/rootReducer.ts index 8c2b6635ec56b..3efab71b998b9 100644 --- a/x-pack/plugins/apm/public/store/rootReducer.js +++ b/x-pack/plugins/apm/public/store/rootReducer.ts @@ -4,15 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import { reducer } from 'react-redux-request'; import { combineReducers } from 'redux'; +import { StringMap } from '../../typings/common'; +// @ts-ignore import location from './location'; -import urlParams from './urlParams'; -import { reducer } from 'react-redux-request'; +import { IUrlParams, urlParamsReducer } from './urlParams'; -const rootReducer = combineReducers({ +export interface IReduxState { + location: any; + urlParams: IUrlParams; + reactReduxRequest: StringMap; +} + +export const rootReducer = combineReducers({ location, - urlParams, + urlParams: urlParamsReducer, reactReduxRequest: reducer }); - -export default rootReducer; diff --git a/x-pack/plugins/apm/public/store/selectors/waterfall.ts b/x-pack/plugins/apm/public/store/selectors/waterfall.ts new file mode 100644 index 0000000000000..85c64d1f97f29 --- /dev/null +++ b/x-pack/plugins/apm/public/store/selectors/waterfall.ts @@ -0,0 +1,43 @@ +/* + * 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 { RRRRenderArgs } from 'react-redux-request'; +import { createSelector, ParametricSelector } from 'reselect'; +import { TransactionV2 } from '../../../typings/Transaction'; +import { WaterfallResponse } from '../../../typings/waterfall'; +import { ID as v1ID } from '../reactReduxRequest/waterfallV1'; +import { ID as v2ID } from '../reactReduxRequest/waterfallV2'; + +interface ReduxState { + reactReduxRequest: { + [v1ID]?: RRRRenderArgs; + [v2ID]?: RRRRenderArgs; + }; +} + +export const selectWaterfall: ParametricSelector< + ReduxState, + any, + WaterfallResponse | null +> = state => { + const waterfall = + state.reactReduxRequest[v1ID] || state.reactReduxRequest[v2ID]; + + return waterfall && waterfall.data ? waterfall.data : null; +}; + +export const selectWaterfallRoot = createSelector( + [selectWaterfall], + waterfall => { + if (!waterfall) { + return; + } + + return waterfall.hits.find( + hit => hit.version === 'v2' && !hit.parent + ) as TransactionV2; + } +); diff --git a/x-pack/plugins/apm/public/store/urlParams.js b/x-pack/plugins/apm/public/store/urlParams.ts similarity index 57% rename from x-pack/plugins/apm/public/store/urlParams.js rename to x-pack/plugins/apm/public/store/urlParams.ts index 23a8313f6b90f..cedcbfe337fee 100644 --- a/x-pack/plugins/apm/public/store/urlParams.js +++ b/x-pack/plugins/apm/public/store/urlParams.ts @@ -5,11 +5,16 @@ */ import _ from 'lodash'; +import { AnyAction } from 'redux'; import { createSelector } from 'reselect'; +// @ts-ignore +import { legacyDecodeURIComponent, toQuery } from '../utils/url'; +// @ts-ignore import { LOCATION_UPDATE } from './location'; -import { toQuery, legacyDecodeURIComponent } from '../utils/url'; -import { getDefaultTransactionId } from './reactReduxRequest/transactionDistribution'; +// @ts-ignore import { getDefaultTransactionType } from './reactReduxRequest/serviceDetails'; +import { getDefaultDistributionSample } from './reactReduxRequest/transactionDistribution'; +import { IReduxState } from './rootReducer'; // ACTION TYPES export const TIMEPICKER_UPDATE = 'TIMEPICKER_UPDATE'; @@ -22,7 +27,7 @@ export const TIMEPICKER_UPDATE = 'TIMEPICKER_UPDATE'; // serviceName: opbeans-backend (path param) // transactionType: Brewing%20Bot (path param) // transactionId: 1321 (query param) -function urlParams(state = {}, action) { +export function urlParamsReducer(state = {}, action: AnyAction) { switch (action.type) { case LOCATION_UPDATE: { const { @@ -34,8 +39,11 @@ function urlParams(state = {}, action) { } = getPathParams(action.location.pathname); const { + traceId, transactionId, detailTab, + flyoutDetailTab, + waterfallItemId, spanId, page, sortDirection, @@ -43,17 +51,20 @@ function urlParams(state = {}, action) { kuery } = toQuery(action.location.search); - return { + return removeUndefinedProps({ ...state, // query params sortDirection, sortField, page: toNumber(page) || 0, - transactionId, - detailTab, + transactionId: toString(transactionId), + traceId: toString(traceId), + waterfallItemId: toString(waterfallItemId), + detailTab: toString(detailTab), + flyoutDetailTab: toString(flyoutDetailTab), spanId: toNumber(spanId), - kuery: legacyDecodeURIComponent(kuery), + kuery: legacyDecodeURIComponent(kuery as string | undefined), // path params processorEvent, @@ -61,7 +72,7 @@ function urlParams(state = {}, action) { transactionType: legacyDecodeURIComponent(transactionType), transactionName: legacyDecodeURIComponent(transactionName), errorGroupId - }; + }); } case TIMEPICKER_UPDATE: @@ -72,17 +83,33 @@ function urlParams(state = {}, action) { } } -function toNumber(value) { - if (value != null) { +function toNumber(value?: string | string[]) { + if (value !== undefined && !Array.isArray(value)) { return parseInt(value, 10); } } -function getPathAsArray(pathname) { +function toString(str?: string | string[]) { + if ( + str === '' || + str === 'null' || + str === 'undefined' || + Array.isArray(str) + ) { + return; + } + return str; +} + +function getPathAsArray(pathname: string) { return _.compact(pathname.split('/')); } -function getPathParams(pathname) { +function removeUndefinedProps(obj: T): Partial { + return _.pick(obj, value => value !== undefined); +} + +function getPathParams(pathname: string) { const paths = getPathAsArray(pathname); const pageName = paths[1]; @@ -106,21 +133,40 @@ function getPathParams(pathname) { } // ACTION CREATORS -export function updateTimePicker(time) { +export function updateTimePicker(time: string) { return { type: TIMEPICKER_UPDATE, time }; } // Selectors export const getUrlParams = createSelector( - state => state.urlParams, + (state: IReduxState) => state.urlParams, getDefaultTransactionType, - getDefaultTransactionId, - (urlParams, transactionType, transactionId) => { - return _.defaults({}, urlParams, { + getDefaultDistributionSample, + ( + urlParams, + transactionType: string, + { traceId, transactionId } + ): IUrlParams => { + return { transactionType, - transactionId - }); + transactionId, + traceId, + ...urlParams + }; } ); -export default urlParams; +export interface IUrlParams { + end?: string; + errorGroupId?: string; + flyoutDetailTab?: string; + detailTab?: string; + kuery?: string; + serviceName?: string; + start?: string; + traceId?: string; + transactionId?: string; + transactionName?: string; + transactionType?: string; + waterfallItemId?: string; +} diff --git a/x-pack/plugins/apm/public/style/global_overrides.css b/x-pack/plugins/apm/public/style/global_overrides.css index 7b032a40914de..a4d51186ab759 100644 --- a/x-pack/plugins/apm/public/style/global_overrides.css +++ b/x-pack/plugins/apm/public/style/global_overrides.css @@ -31,4 +31,11 @@ Hide default dashed gridlines in EUI chart component for all APM graphs .rv-xy-plot__grid-lines__line { stroke-opacity: 1; stroke-dasharray: 1; -} \ No newline at end of file +} + +/* +Override tab size since K6 theme makes "s" and "m" tabs both 14px +*/ +.k6Tab--large .euiTab { + font-size: 16px; +} diff --git a/x-pack/plugins/apm/public/utils/__test__/__snapshots__/url.test.js.snap b/x-pack/plugins/apm/public/utils/__test__/__snapshots__/url.test.js.snap index 6430ae0cc5804..c2f0bcbe6ff86 100644 --- a/x-pack/plugins/apm/public/utils/__test__/__snapshots__/url.test.js.snap +++ b/x-pack/plugins/apm/public/utils/__test__/__snapshots__/url.test.js.snap @@ -3,7 +3,7 @@ exports[`KibanaLinkComponent should render correct markup 1`] = ` Go to Discover diff --git a/x-pack/plugins/apm/public/utils/__test__/url.test.js b/x-pack/plugins/apm/public/utils/__test__/url.test.js index 6980d2c3b1620..b3617da276379 100644 --- a/x-pack/plugins/apm/public/utils/__test__/url.test.js +++ b/x-pack/plugins/apm/public/utils/__test__/url.test.js @@ -211,7 +211,7 @@ describe('KibanaLinkComponent', () => { it('should have correct url', () => { expect(wrapper.find('a').prop('href')).toBe( - "myBasePath/app/kibana#/discover?_g=&_a=(interval:auto,query:(language:lucene,query:'context.service.name:myServiceName AND error.grouping_key:myGroupId'),sort:('@timestamp':desc))" + "myBasePath/app/kibana#/discover?_a=(interval:auto,query:(language:lucene,query:'context.service.name:myServiceName AND error.grouping_key:myGroupId'),sort:('@timestamp':desc))&_g=" ); }); diff --git a/x-pack/plugins/apm/public/utils/documentation.ts b/x-pack/plugins/apm/public/utils/documentation.ts deleted file mode 100644 index 0ad3ce722d819..0000000000000 --- a/x-pack/plugins/apm/public/utils/documentation.ts +++ /dev/null @@ -1,181 +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 { get } from 'lodash'; -// @ts-ignore -import { metadata } from 'ui/metadata'; - -const STACK_VERSION = metadata.branch; -const DOCS_ROOT = 'https://www.elastic.co/guide/en/apm'; - -// -// General APM -// -export const APM_DOCS = { - 'get-started': { - url: `${DOCS_ROOT}/get-started/${STACK_VERSION}/index.html` - } -}; - -// -// APM Server docs -// -export const APM_SERVER_DOCS = { - download: { - url: 'https://www.elastic.co/downloads/apm/apm-server' - }, - configuring: { - url: `${DOCS_ROOT}/server/${STACK_VERSION}/configuring.html` - }, - 'running-on-docker': { - url: `${DOCS_ROOT}/server/${STACK_VERSION}/running-on-docker.html#running-on-docker` - }, - frontend: { - url: `${DOCS_ROOT}/server/${STACK_VERSION}/frontend.html` - } -}; - -// -// APM Agents docs -// -const featureContextUserText = - 'You can configure your agent to add contextual information about your users.'; -const featureContextTagsText = - 'You can configure your agent to add filterable tags on transactions.'; -const featureContextCustomText = - 'You can configure your agent to add custom contextual information on transactions.'; - -export const APM_AGENT_DOCS = { - home: { - nodejs: { - url: `${DOCS_ROOT}/agent/nodejs/1.x/index.html` - }, - python: { - url: `${DOCS_ROOT}/agent/python/2.x/index.html` - }, - ruby: { - url: `${DOCS_ROOT}/agent/ruby/1.x/index.html` - }, - javascript: { - url: `${DOCS_ROOT}/agent/js-base/0.x/index.html` - } - }, - 'get-started': { - python: { - url: `${DOCS_ROOT}/agent/python/2.x/getting-started.html` - }, - javascript: { - url: `${DOCS_ROOT}/agent/js-base/0.x/getting-started.html` - } - }, - 'nodejs-only': { - 'babel-es-modules': { - url: `${DOCS_ROOT}/agent/nodejs/1.x/advanced-setup.html#es-modules` - } - }, - 'python-only': { - django: { url: `${DOCS_ROOT}/agent/python/2.x/django-support.html` }, - flask: { url: `${DOCS_ROOT}/agent/python/2.x/flask-support.html` } - }, - 'context-user': { - nodejs: { - text: featureContextUserText, - url: `${DOCS_ROOT}/agent/nodejs/1.x/agent-api.html#apm-set-user-context` - }, - python: { - text: featureContextUserText, - url: `${DOCS_ROOT}/agent/python/2.x/api.html#api-set-user-context` - }, - ruby: { - text: featureContextUserText, - url: `${DOCS_ROOT}/agent/ruby/1.x/advanced.html#_providing_info_about_the_user` - }, - javascript: { - text: featureContextUserText, - url: `${DOCS_ROOT}/agent/js-base/0.x/api.html#apm-set-user-context` - } - }, - 'context-tags': { - nodejs: { - text: featureContextTagsText, - url: `${DOCS_ROOT}/agent/nodejs/1.x/agent-api.html#apm-set-tag` - }, - python: { - text: featureContextTagsText, - url: `${DOCS_ROOT}/agent/python/2.x/api.html#api-tag` - }, - ruby: { - text: featureContextTagsText, - url: `${DOCS_ROOT}/agent/ruby/1.x/advanced.html#_adding_tags` - }, - javascript: { - text: `${DOCS_ROOT}/agent/js-base/0.x/api.html#apm-set-tags` - } - }, - 'context-custom': { - nodejs: { - text: featureContextCustomText, - url: `${DOCS_ROOT}/agent/nodejs/1.x/agent-api.html#apm-set-custom-context` - }, - python: { - text: featureContextCustomText, - url: `${DOCS_ROOT}/agent/python/2.x/api.html#api-set-custom-context` - }, - ruby: { - text: featureContextCustomText, - url: `${DOCS_ROOT}/agent/ruby/1.x/advanced.html#_adding_custom_context` - }, - javascript: { - text: featureContextCustomText, - url: `${DOCS_ROOT}/agent/js-base/0.x/api.html#apm-set-custom-context` - } - }, - 'dropped-spans': { - nodejs: { - url: `${DOCS_ROOT}/agent/nodejs/1.x/agent-api.html#transaction-max-spans` - }, - python: { - url: `${DOCS_ROOT}/agent/python/2.x/configuration.html#config-transaction-max-spans` - } - } -}; - -// -// Elastic docs -// -export const ELASTIC_DOCS = { - 'x-pack-emails': { - url: `https://www.elastic.co/guide/en/x-pack/${STACK_VERSION}/actions-email.html#configuring-email` - }, - 'watcher-get-started': { - url: `https://www.elastic.co/guide/en/x-pack/${STACK_VERSION}/watcher-getting-started.html` - } -}; - -// -// Helper methods -// -function translateAgentName(agentName: string): string { - switch (agentName) { - case 'js-react': - case 'js-base': - return 'javascript'; - - default: - return agentName; - } -} - -export function getFeatureDocs( - featureName: string, - agentName: string -): { - url: string; - text?: string; -} { - const translatedAgentName = translateAgentName(agentName); - return get(APM_AGENT_DOCS, `${featureName}.${translatedAgentName}`); -} diff --git a/x-pack/plugins/apm/public/utils/documentation/agents.ts b/x-pack/plugins/apm/public/utils/documentation/agents.ts new file mode 100644 index 0000000000000..70ba4c8644cbf --- /dev/null +++ b/x-pack/plugins/apm/public/utils/documentation/agents.ts @@ -0,0 +1,50 @@ +/* + * 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. + */ + +const AGENT_URL_ROOT = 'https://www.elastic.co/guide/en/apm/agent'; + +// TODO: currently unused but should be added to timeline view +export const APM_AGENT_DROPPED_SPANS_DOCS = { + nodejs: `${AGENT_URL_ROOT}/nodejs/1.x/agent-api.html#transaction-max-spans`, + python: `${AGENT_URL_ROOT}/python/2.x/configuration.html#config-transaction-max-spans` +}; + +const APM_AGENT_FEATURE_DOCS: { + [featureName: string]: { + [agentName: string]: string; + }; +} = { + user: { + nodejs: `${AGENT_URL_ROOT}/nodejs/1.x/agent-api.html#apm-set-user-context`, + python: `${AGENT_URL_ROOT}/python/2.x/api.html#api-set-user-context`, + ruby: `${AGENT_URL_ROOT}/ruby/1.x/advanced.html#_providing_info_about_the_user`, + 'js-react': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-user-context`, + 'js-base': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-user-context` + }, + tags: { + nodejs: `${AGENT_URL_ROOT}/nodejs/1.x/agent-api.html#apm-set-tag`, + python: `${AGENT_URL_ROOT}/python/2.x/api.html#api-tag`, + ruby: `${AGENT_URL_ROOT}/ruby/1.x/advanced.html#_adding_tags`, + 'js-react': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-tags`, + 'js-base': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-tags` + }, + custom: { + nodejs: `${AGENT_URL_ROOT}/nodejs/1.x/agent-api.html#apm-set-custom-context`, + python: `${AGENT_URL_ROOT}/python/2.x/api.html#api-set-custom-context`, + ruby: `${AGENT_URL_ROOT}/ruby/1.x/advanced.html#_adding_custom_context`, + 'js-react': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-custom-context`, + 'js-base': `${AGENT_URL_ROOT}/js-base/0.x/api.html#apm-set-custom-context` + } +}; + +export function getAgentFeatureDocsUrl( + featureName: string, + agentName?: string +) { + if (APM_AGENT_FEATURE_DOCS[featureName] && agentName) { + return APM_AGENT_FEATURE_DOCS[featureName][agentName]; + } +} diff --git a/x-pack/plugins/apm/public/utils/documentation/xpack.ts b/x-pack/plugins/apm/public/utils/documentation/xpack.ts new file mode 100644 index 0000000000000..11741fb3ea803 --- /dev/null +++ b/x-pack/plugins/apm/public/utils/documentation/xpack.ts @@ -0,0 +1,16 @@ +/* + * 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 { metadata } from 'ui/metadata'; +const STACK_VERSION = metadata.branch; + +const XPACK_URL_ROOT = `https://www.elastic.co/guide/en/x-pack/${STACK_VERSION}`; + +export const XPACK_DOCS = { + xpackEmails: `${XPACK_URL_ROOT}/actions-email.html#configuring-email`, + xpackWatcher: `${XPACK_URL_ROOT}/watcher-getting-started.html` +}; diff --git a/x-pack/plugins/apm/public/utils/url.js b/x-pack/plugins/apm/public/utils/url.tsx similarity index 56% rename from x-pack/plugins/apm/public/utils/url.js rename to x-pack/plugins/apm/public/utils/url.tsx index 8b1ab93bffe60..f95c1298f92a0 100644 --- a/x-pack/plugins/apm/public/utils/url.js +++ b/x-pack/plugins/apm/public/utils/url.tsx @@ -4,33 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; +import createHistory from 'history/createHashHistory'; +import _ from 'lodash'; +import qs from 'querystring'; import React from 'react'; -import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import qs from 'querystring'; -import url from 'url'; import { Link } from 'react-router-dom'; -import _ from 'lodash'; import rison from 'rison-node'; -import { EuiLink } from '@elastic/eui'; -import createHistory from 'history/createHashHistory'; import chrome from 'ui/chrome'; +import url from 'url'; +import { StringMap } from '../../typings/common'; + +interface ViewMlJobArgs { + serviceName: string; + transactionType: string; + location: any; + children: any; +} export function ViewMLJob({ serviceName, transactionType, location, children = 'View Job' -}) { +}: ViewMlJobArgs) { const { _g, _a } = decodeKibanaSearchParams(location.search); - const pathname = '/app/ml'; const hash = '/timeseriesexplorer'; + const jobId = `${serviceName}-${transactionType}-high_mean_response_time`; const query = { _g: { - ..._g, + ...(_g as object), ml: { - jobIds: [`${serviceName}-${transactionType}-high_mean_response_time`] + jobIds: [jobId] } }, _a @@ -46,17 +53,17 @@ export function ViewMLJob({ ); } -export function toQuery(search) { - return qs.parse(search.slice(1)); +export function toQuery(search?: string) { + return search ? qs.parse(search.slice(1)) : {}; } -export function fromQuery(query) { +export function fromQuery(query: StringMap) { const encodedQuery = encodeQuery(query, ['_g', '_a']); return stringifyWithoutEncoding(encodedQuery); } -export function encodeQuery(query, exclude = []) { - return _.mapValues(query, (value, key) => { +export function encodeQuery(query: StringMap, exclude: string[] = []) { + return _.mapValues(query, (value: any, key: string) => { if (exclude.includes(key)) { return encodeURI(value); } @@ -64,34 +71,54 @@ export function encodeQuery(query, exclude = []) { }); } -function stringifyWithoutEncoding(query) { - return qs.stringify(query, null, null, { - encodeURIComponent: v => v +function stringifyWithoutEncoding(query: StringMap) { + return qs.stringify(query, undefined, undefined, { + encodeURIComponent: (v: string) => v }); } -export function decodeKibanaSearchParams(search) { +function decodeAsObject(value: string) { + const decoded = rison.decode(value); + return _.isPlainObject(decoded) ? decoded : {}; +} + +export function decodeKibanaSearchParams(search: string) { const query = toQuery(search); return { - _g: query._g ? rison.decode(query._g) : null, - _a: query._a ? rison.decode(query._a) : null + _g: + query._g && typeof query._g === 'string' + ? decodeAsObject(query._g) + : null, + _a: + query._a && typeof query._a === 'string' ? decodeAsObject(query._a) : null }; } -export function encodeKibanaSearchParams(query) { +export function encodeKibanaSearchParams(query: StringMap) { return stringifyWithoutEncoding({ _g: rison.encode(query._g), _a: rison.encode(query._a) }); } +export interface RelativeLinkComponentArgs { + location: { + search?: string; + pathname?: string; + }; + path: string; + query?: StringMap; + disabled: boolean; + to: StringMap; + className: string; +} export function RelativeLinkComponent({ location, path, query, disabled, ...props -}) { +}: RelativeLinkComponentArgs) { if (disabled) { return ; } @@ -118,15 +145,36 @@ export function RelativeLinkComponent({ ); } +// TODO: +// Both KibanaLink and RelativeLink does similar things, are too magic, and have different APIs. +// The initial idea with KibanaLink was to automatically preserve the timestamp (_g) when making links. RelativeLink went a bit overboard and preserves all query args +// The two components have different APIs: `path` vs `pathname` and one uses EuiLink the other react-router's Link (which behaves differently) +// Suggestion: Deprecate RelativeLink, and clean up KibanaLink + +export interface KibanaLinkArgs { + location: { + search?: string; + pathname?: string; + }; + pathname: string; + hash?: string; + query?: StringMap; + disabled?: boolean; + to?: StringMap; + className?: string; +} + export function KibanaLinkComponent({ location, pathname, hash, query = {}, ...props -}) { +}: KibanaLinkArgs) { + // Preserve current _g and _a const currentQuery = toQuery(location.search); const nextQuery = { + ...query, _g: query._g ? rison.encode(query._g) : currentQuery._g, _a: query._a ? rison.encode(query._a) : '' }; @@ -141,7 +189,7 @@ export function KibanaLinkComponent({ } const withLocation = connect( - ({ location }) => ({ location }), + ({ location }: { location: any }) => ({ location }), {} ); export const RelativeLink = withLocation(RelativeLinkComponent); @@ -150,22 +198,18 @@ export const KibanaLink = withLocation(KibanaLinkComponent); // This is downright horrible 😭 💔 // Angular decodes encoded url tokens like "%2F" to "/" which causes the route to change. // It was supposedly fixed in https://github.com/angular/angular.js/commit/1b779028fdd339febaa1fff5f3bd4cfcda46cc09 but still seeing the issue -export function legacyEncodeURIComponent(url) { - return url && encodeURIComponent(url).replace(/%/g, '~'); +export function legacyEncodeURIComponent(rawUrl?: string) { + return rawUrl && encodeURIComponent(rawUrl).replace(/%/g, '~'); } -export function legacyDecodeURIComponent(url) { - return url && decodeURIComponent(url.replace(/~/g, '%')); +export function legacyDecodeURIComponent(encodedUrl?: string) { + return encodedUrl && decodeURIComponent(encodedUrl.replace(/~/g, '%')); } -export function ExternalLink(props) { +export function ExternalLink(props: EuiLinkAnchorProps) { return ; } -ExternalLink.propTypes = { - href: PropTypes.string.isRequired -}; - // Make history singleton available across APM project. // This is not great. Other options are to use context or withRouter helper // React Context API is unstable and will change soon-ish (probably 16.3) diff --git a/x-pack/plugins/apm/server/lib/helpers/input_validation.js b/x-pack/plugins/apm/server/lib/helpers/input_validation.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/helpers/input_validation.js rename to x-pack/plugins/apm/server/lib/helpers/input_validation.ts diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.js b/x-pack/plugins/apm/server/lib/helpers/setup_request.js deleted file mode 100644 index 0754816660769..0000000000000 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.js +++ /dev/null @@ -1,38 +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. - */ - -/* eslint-disable no-console */ -import moment from 'moment'; - -function decodeEsQuery(esQuery) { - return esQuery ? JSON.parse(decodeURIComponent(esQuery)) : null; -} - -export function setupRequest(req, reply) { - const cluster = req.server.plugins.elasticsearch.getCluster('data'); - - const setup = { - start: moment.utc(req.query.start).valueOf(), - end: moment.utc(req.query.end).valueOf(), - esFilterQuery: decodeEsQuery(req.query.esFilterQuery), - client: (type, params) => { - if (req.query._debug) { - console.log(`DEBUG ES QUERY:`); - console.log( - `${req.method.toUpperCase()} ${req.url.pathname} ${JSON.stringify( - req.query - )}` - ); - console.log(`GET ${params.index}/_search`); - console.log(JSON.stringify(params.body, null, 4)); - } - return cluster.callWithRequest(req, type, params); - }, - config: req.server.config() - }; - - reply(setup); -} diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts new file mode 100644 index 0000000000000..b5bc84634dd3d --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -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 no-console */ +import { SearchParams, SearchResponse } from 'elasticsearch'; +import { Request, Server } from 'hapi'; +import moment from 'moment'; + +function decodeEsQuery(esQuery?: string): object { + return esQuery ? JSON.parse(decodeURIComponent(esQuery)) : null; +} + +interface KibanaServer extends Server { + config: () => KibanaConfig; +} + +interface KibanaRequest extends Request { + server: KibanaServer; +} + +interface KibanaConfig { + get: (key: string) => any; +} + +type Client = (type: string, params: SearchParams) => SearchResponse; + +export interface Setup { + start: number; + end: number; + esFilterQuery: any; + client: Client; + config: KibanaConfig; +} + +export function setupRequest( + req: KibanaRequest, + reply: (setup: Setup) => void +) { + const cluster = req.server.plugins.elasticsearch.getCluster('data'); + + function client(type: string, params: SearchParams): SearchResponse { + if (req.query._debug) { + console.log(`DEBUG ES QUERY:`); + console.log( + `${req.method.toUpperCase()} ${req.url.pathname} ${JSON.stringify( + req.query + )}` + ); + console.log(`GET ${params.index}/_search`); + console.log(JSON.stringify(params.body, null, 4)); + } + return cluster.callWithRequest(req, type, params); + } + + const setup = { + start: moment.utc(req.query.start).valueOf(), + end: moment.utc(req.query.end).valueOf(), + esFilterQuery: decodeEsQuery(req.query.esFilterQuery), + client, + config: req.server.config() + }; + + reply(setup); +} diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_group_query.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_group_query.ts new file mode 100644 index 0000000000000..db7b65c646870 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_group_query.ts @@ -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. + */ + +import moment from 'moment'; +import { + TRANSACTION_DURATION, + TRANSACTION_NAME +} from '../../../common/constants'; +import { Transaction } from '../../../typings/Transaction'; +import { ITransactionGroup } from '../../../typings/TransactionGroup'; + +export interface ITransactionGroupBucket { + key: string; + doc_count: number; + avg: { + value: number; + }; + p95: { + values: { + '95.0': number; + }; + }; + sample: { + hits: { + hits: Array<{ + _source: Transaction; + }>; + }; + }; +} + +export const TRANSACTION_GROUP_AGGREGATES = { + transactions: { + terms: { + field: `${TRANSACTION_NAME}.keyword`, + order: { avg: 'desc' }, + size: 100 + }, + aggs: { + sample: { + top_hits: { + size: 1, + sort: [{ '@timestamp': { order: 'desc' } }] + } + }, + avg: { avg: { field: TRANSACTION_DURATION } }, + p95: { percentiles: { field: TRANSACTION_DURATION, percents: [95] } } + } + } +}; + +function calculateRelativeImpacts(results: ITransactionGroup[]) { + const values = results.map(({ impact }) => impact); + const max = Math.max(...values); + const min = Math.min(...values); + + return results.map(bucket => ({ + ...bucket, + impact: ((bucket.impact - min) / (max - min)) * 100 + })); +} + +export function prepareTransactionGroups({ + buckets, + start, + end +}: { + buckets: ITransactionGroupBucket[]; + start: number; + end: number; +}) { + const duration = moment.duration(end - start); + const minutes = duration.asMinutes(); + + const results = buckets.map((bucket: ITransactionGroupBucket) => { + const averageResponseTime = bucket.avg.value; + const transactionsPerMinute = bucket.doc_count / minutes; + const impact = Math.round(averageResponseTime * transactionsPerMinute); + const sample = bucket.sample.hits.hits[0]._source; + + return { + name: bucket.key, + sample, + p95: bucket.p95.values['95.0'], + averageResponseTime, + transactionsPerMinute, + impact + }; + }); + + return calculateRelativeImpacts(results); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service.js b/x-pack/plugins/apm/server/lib/services/get_service.ts similarity index 64% rename from x-pack/plugins/apm/server/lib/services/get_service.js rename to x-pack/plugins/apm/server/lib/services/get_service.ts index 416dfaf3746c4..2998f3487ecc6 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service.js +++ b/x-pack/plugins/apm/server/lib/services/get_service.ts @@ -4,14 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; +import { oc } from 'ts-optchain'; +import { TermsAggsBucket } from 'x-pack/plugins/apm/typings/elasticsearch'; import { + SERVICE_AGENT_NAME, SERVICE_NAME, - TRANSACTION_TYPE, - SERVICE_AGENT_NAME + TRANSACTION_TYPE } from '../../../common/constants'; +import { Setup } from '../helpers/setup_request'; -export async function getService({ serviceName, setup }) { +export interface ServiceResponse { + service_name: string; + types: string[]; + agent_name?: string; +} + +export async function getService( + serviceName: string, + setup: Setup +): Promise { const { start, end, esFilterQuery, client, config } = setup; const params = { @@ -52,11 +63,21 @@ export async function getService({ serviceName, setup }) { params.body.query.bool.filter.push(esFilterQuery); } + interface Aggs { + types: { + buckets: TermsAggsBucket[]; + }; + agents: { + buckets: TermsAggsBucket[]; + }; + } + const resp = await client('search', params); + const aggs: Aggs = resp.aggregations; return { service_name: serviceName, - types: resp.aggregations.types.buckets.map(bucket => bucket.key), - agent_name: get(resp, 'aggregations.agents.buckets[0].key') + types: aggs.types.buckets.map(bucket => bucket.key), + agent_name: oc(aggs).agents.buckets[0].key() }; } diff --git a/x-pack/plugins/apm/server/lib/services/get_services.js b/x-pack/plugins/apm/server/lib/services/get_services.ts similarity index 67% rename from x-pack/plugins/apm/server/lib/services/get_services.js rename to x-pack/plugins/apm/server/lib/services/get_services.ts index 37da74123ffdd..eeb39c256e8a3 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services.js +++ b/x-pack/plugins/apm/server/lib/services/get_services.ts @@ -4,15 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import { oc } from 'ts-optchain'; +import { TermsAggsBucket } from 'x-pack/plugins/apm/typings/elasticsearch'; import { - SERVICE_NAME, - TRANSACTION_DURATION, + PROCESSOR_EVENT, SERVICE_AGENT_NAME, - PROCESSOR_EVENT + SERVICE_NAME, + TRANSACTION_DURATION } from '../../../common/constants'; -import { get } from 'lodash'; +import { Setup } from '../helpers/setup_request'; + +export interface ServiceListItemResponse { + service_name: string; + agent_name: string | undefined; + transactions_per_minute: number; + errors_per_minute: number; + avg_response_time: number; +} -export async function getServices({ setup }) { +export async function getServices( + setup: Setup +): Promise { const { start, end, esFilterQuery, client, config } = setup; const params = { @@ -71,26 +83,43 @@ export async function getServices({ setup }) { params.body.query.bool.filter.push(esFilterQuery); } + interface ServiceBucket extends TermsAggsBucket { + avg: { + value: number; + }; + agents: { + buckets: TermsAggsBucket[]; + }; + events: { + buckets: TermsAggsBucket[]; + }; + } + + interface Aggs extends TermsAggsBucket { + services: { + buckets: ServiceBucket[]; + }; + } + const resp = await client('search', params); + const aggs: Aggs = resp.aggregations; + const serviceBuckets = oc(aggs).services.buckets([]); - const buckets = get(resp.aggregations, 'services.buckets', []); - return buckets.map(bucket => { + return serviceBuckets.map(bucket => { const eventTypes = bucket.events.buckets; - const transactions = eventTypes.find(e => e.key === 'transaction'); - const totalTransactions = get(transactions, 'doc_count', 0); + const totalTransactions = oc(transactions).doc_count(0); const errors = eventTypes.find(e => e.key === 'error'); - const totalErrors = get(errors, 'doc_count', 0); + const totalErrors = oc(errors).doc_count(0); const deltaAsMinutes = (end - start) / 1000 / 60; - const transactionsPerMinute = totalTransactions / deltaAsMinutes; const errorsPerMinute = totalErrors / deltaAsMinutes; return { service_name: bucket.key, - agent_name: get(bucket, 'agents.buckets[0].key', null), + agent_name: oc(bucket).agents.buckets[0].key(), transactions_per_minute: transactionsPerMinute, errors_per_minute: errorsPerMinute, avg_response_time: bucket.avg.value diff --git a/x-pack/plugins/apm/server/lib/traces/get_top_traces.ts b/x-pack/plugins/apm/server/lib/traces/get_top_traces.ts new file mode 100644 index 0000000000000..990879690d9b7 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/traces/get_top_traces.ts @@ -0,0 +1,77 @@ +/* + * 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 { SearchResponse } from 'elasticsearch'; +import { get } from 'lodash'; +import { + PARENT_ID, + PROCESSOR_EVENT, + TRACE_ID +} from '../../../common/constants'; +import { Transaction } from '../../../typings/Transaction'; +import { ITransactionGroup } from '../../../typings/TransactionGroup'; +import { Setup } from '../helpers/setup_request'; +import { + ITransactionGroupBucket, + prepareTransactionGroups, + TRANSACTION_GROUP_AGGREGATES +} from '../helpers/transaction_group_query'; + +export async function getTopTraces(setup: Setup): Promise { + const { start, end, esFilterQuery, client, config } = setup; + + const params = { + index: config.get('apm_oss.transactionIndices'), + body: { + size: 0, + query: { + bool: { + must: { + // this criterion safeguards against data that lacks a transaction + // parent ID but still is not a "trace" by way of not having a + // trace ID (e.g. old data before parent ID was implemented, etc) + exists: { + field: TRACE_ID + } + }, + must_not: { + // no parent ID alongside a trace ID means this transaction is a + // "root" transaction, i.e. a trace + exists: { + field: PARENT_ID + } + }, + filter: [ + { + range: { + '@timestamp': { + gte: start, + lte: end, + format: 'epoch_millis' + } + } + }, + { term: { [PROCESSOR_EVENT]: 'transaction' } } + ] + } + }, + aggs: TRANSACTION_GROUP_AGGREGATES + } + }; + + if (esFilterQuery) { + params.body.query.bool.filter.push(esFilterQuery); + } + + const response: SearchResponse = await client('search', params); + const buckets: ITransactionGroupBucket[] = get( + response.aggregations, + 'transactions.buckets', + [] + ); + + return prepareTransactionGroups({ buckets, start, end }); +} diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace.ts b/x-pack/plugins/apm/server/lib/traces/get_trace.ts new file mode 100644 index 0000000000000..eb43e5a76185f --- /dev/null +++ b/x-pack/plugins/apm/server/lib/traces/get_trace.ts @@ -0,0 +1,63 @@ +/* + * 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 { SearchParams, SearchResponse } from 'elasticsearch'; +import { WaterfallResponse } from 'x-pack/plugins/apm/typings/waterfall'; +import { SERVICE_NAME, TRACE_ID } from '../../../common/constants'; +import { TermsAggsBucket } from '../../../typings/elasticsearch'; +import { Span } from '../../../typings/Span'; +import { Transaction } from '../../../typings/Transaction'; +import { Setup } from '../helpers/setup_request'; + +export async function getTrace( + traceId: string, + setup: Setup +): Promise { + const { start, end, client, config } = setup; + + const params: SearchParams = { + index: config.get('apm_oss.transactionIndices'), + body: { + size: 1000, + query: { + bool: { + filter: [ + { term: { [TRACE_ID]: traceId } }, + { + range: { + '@timestamp': { + gte: start, + lte: end, + format: 'epoch_millis' + } + } + } + ] + } + }, + aggs: { + services: { + terms: { + field: SERVICE_NAME, + size: 500 + } + } + } + } + }; + + const resp: SearchResponse = await client( + 'search', + params + ); + + return { + services: (resp.aggregations.services.buckets as TermsAggsBucket[]).map( + bucket => bucket.key + ), + hits: resp.hits.hits.map(hit => hit._source) + }; +} diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.js b/x-pack/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.ts similarity index 72% rename from x-pack/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.js rename to x-pack/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.ts index 1102211f0cc1b..38025110730bf 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.js +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.ts @@ -4,20 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SearchParams } from 'elasticsearch'; import { SERVICE_NAME, - TRANSACTION_NAME, - TRANSACTION_DURATION + TRANSACTION_DURATION, + TRANSACTION_NAME } from '../../../../common/constants'; +import { Setup } from '../../helpers/setup_request'; -export async function calculateBucketSize({ - serviceName, - transactionName, - setup -}) { +export async function calculateBucketSize( + serviceName: string, + transactionName: string, + setup: Setup +) { const { start, end, esFilterQuery, client, config } = setup; - const params = { + const params: SearchParams = { index: config.get('apm_oss.transactionIndices'), body: { size: 0, @@ -53,9 +55,9 @@ export async function calculateBucketSize({ } const resp = await client('search', params); - const minBucketSize = config.get('xpack.apm.minimumBucketSize'); - const bucketTargetCount = config.get('xpack.apm.bucketTargetCount'); - const { max } = resp.aggregations.stats; + const minBucketSize: number = config.get('xpack.apm.minimumBucketSize'); + const bucketTargetCount: number = config.get('xpack.apm.bucketTargetCount'); + const max: number = resp.aggregations.stats.max; const bucketSize = Math.floor(max / bucketTargetCount); return bucketSize > minBucketSize ? bucketSize : minBucketSize; diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets.js b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets.ts similarity index 50% rename from x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets.js rename to x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets.ts index d5c0d4835a7bf..b806685df7dcb 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets.js +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets.ts @@ -4,26 +4,54 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; +import { SearchParams, SearchResponse } from 'elasticsearch'; +import { oc } from 'ts-optchain'; import { SERVICE_NAME, + TRACE_ID, TRANSACTION_DURATION, TRANSACTION_ID, TRANSACTION_NAME, TRANSACTION_SAMPLED } from '../../../../common/constants'; +import { TermsAggsBucket } from '../../../../typings/elasticsearch'; +import { Transaction } from '../../../../typings/Transaction'; +import { Setup } from '../../helpers/setup_request'; -export async function getBuckets({ - serviceName, - transactionName, - bucketSize = 100, - setup -}) { - const { start, end, esFilterQuery, client, config } = setup; +export interface IBucket { + key: string; + count: number; + sample?: IBucketSample; +} + +interface IBucketSample { + traceId?: string; + transactionId?: string; +} - const bucketTargetCount = config.get('xpack.apm.bucketTargetCount'); +interface IBucketsResponse { + totalHits: number; + buckets: IBucket[]; +} - const params = { +interface ESBucket extends TermsAggsBucket { + sample: SearchResponse<{ + transaction: Pick; + trace: { + id: string; + }; + }>; +} + +export async function getBuckets( + serviceName: string, + transactionName: string, + bucketSize: number, + setup: Setup +): Promise { + const { start, end, esFilterQuery, client, config } = setup; + const bucketTargetCount: number = config.get('xpack.apm.bucketTargetCount'); + const params: SearchParams = { index: config.get('apm_oss.transactionIndices'), body: { size: 0, @@ -57,9 +85,9 @@ export async function getBuckets({ } }, aggs: { - transaction: { + sample: { top_hits: { - _source: [TRANSACTION_ID, TRANSACTION_SAMPLED], + _source: [TRANSACTION_ID, TRANSACTION_SAMPLED, TRACE_ID], size: 1 } } @@ -74,19 +102,25 @@ export async function getBuckets({ } const resp = await client('search', params); + const buckets = (resp.aggregations.distribution.buckets as ESBucket[]).map( + bucket => { + const sampleSource = oc(bucket).sample.hits.hits[0]._source(); + const isSampled = oc(sampleSource).transaction.sampled(false); + const sample = { + traceId: oc(sampleSource).trace.id(), + transactionId: oc(sampleSource).transaction.id() + }; - const buckets = resp.aggregations.distribution.buckets.map(bucket => { - const transaction = get(bucket.transaction.hits.hits[0], '_source'); - return { - key: bucket.key, - count: bucket.doc_count, - transaction_id: get(transaction, TRANSACTION_ID), - sampled: get(transaction, TRANSACTION_SAMPLED) - }; - }); + return { + key: bucket.key, + count: bucket.doc_count, + sample: isSampled ? sample : undefined + }; + } + ); return { - total_hits: resp.hits.total, + totalHits: resp.hits.total, buckets }; } diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution.js b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution.js deleted file mode 100644 index c6902eab3a454..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution.js +++ /dev/null @@ -1,43 +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 { get, isEmpty } from 'lodash'; -import { getBuckets } from './get_buckets'; -import { calculateBucketSize } from './calculate_bucket_size'; - -function getDefaultTransactionId(buckets) { - const filledBuckets = buckets.filter( - bucket => bucket.count && bucket.sampled - ); - - if (isEmpty(filledBuckets)) { - return; - } - - const middleIndex = Math.floor(filledBuckets.length / 2); - return get(filledBuckets, `[${middleIndex}].transaction_id`); -} - -export async function getDistribution({ serviceName, transactionName, setup }) { - const bucketSize = await calculateBucketSize({ - serviceName, - transactionName, - setup - }); - const { buckets, total_hits: totalHits } = await getBuckets({ - serviceName, - transactionName, - setup, - bucketSize - }); - - return { - total_hits: totalHits, - buckets, - bucket_size: bucketSize, - default_transaction_id: getDefaultTransactionId(buckets) - }; -} diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution.ts new file mode 100644 index 0000000000000..c3ba7d53c23e3 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution.ts @@ -0,0 +1,55 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { Setup } from '../../helpers/setup_request'; +import { calculateBucketSize } from './calculate_bucket_size'; +import { getBuckets, IBucket } from './get_buckets'; + +export interface IDistributionResponse { + totalHits: number; + buckets: IBucket[]; + bucketSize: number; + defaultSample?: IBucket['sample']; +} + +function getDefaultSample(buckets: IBucket[]) { + const samples = buckets + .filter(bucket => bucket.count > 0 && bucket.sample) + .map(bucket => bucket.sample); + + if (isEmpty(samples)) { + return; + } + + const middleIndex = Math.floor(samples.length / 2); + return samples[middleIndex]; +} + +export async function getDistribution( + serviceName: string, + transactionName: string, + setup: Setup +): Promise { + const bucketSize = await calculateBucketSize( + serviceName, + transactionName, + setup + ); + const { buckets, totalHits } = await getBuckets( + serviceName, + transactionName, + bucketSize, + setup + ); + + return { + totalHits, + buckets, + bucketSize, + defaultSample: getDefaultSample(buckets) + }; +} diff --git a/x-pack/plugins/apm/server/lib/transactions/get_top_transactions.js b/x-pack/plugins/apm/server/lib/transactions/get_top_transactions.js deleted file mode 100644 index e9e607f277f1a..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/get_top_transactions.js +++ /dev/null @@ -1,101 +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 moment from 'moment'; -import { - SERVICE_NAME, - TRANSACTION_TYPE, - TRANSACTION_NAME, - TRANSACTION_ID, - TRANSACTION_DURATION -} from '../../../common/constants'; -import { get, sortBy } from 'lodash'; - -export async function getTopTransactions({ - transactionType, - serviceName, - setup -}) { - const { start, end, esFilterQuery, client, config } = setup; - - const duration = moment.duration(end - start); - const minutes = duration.asMinutes(); - - const params = { - index: config.get('apm_oss.transactionIndices'), - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - { - range: { - '@timestamp': { - gte: start, - lte: end, - format: 'epoch_millis' - } - } - } - ] - } - }, - aggs: { - transactions: { - terms: { - field: `${TRANSACTION_NAME}.keyword`, - order: { avg: 'desc' }, - size: 100 - }, - aggs: { - sample: { - top_hits: { - _source: [TRANSACTION_ID], - size: 1, - sort: [{ '@timestamp': { order: 'desc' } }] - } - }, - avg: { - avg: { field: TRANSACTION_DURATION } - }, - p95: { - percentiles: { - field: TRANSACTION_DURATION, - percents: [95] - } - } - } - } - } - } - }; - - if (esFilterQuery) { - params.body.query.bool.filter.push(esFilterQuery); - } - - const resp = await client('search', params); - const buckets = get(resp, 'aggregations.transactions.buckets', []); - const results = buckets.map(bucket => { - const avg = bucket.avg.value; - const tpm = bucket.doc_count / minutes; - const impact = Math.round(avg * tpm); - return { - name: bucket.key, - id: get(bucket, `sample.hits.hits[0]._source.${TRANSACTION_ID}`), - p95: bucket.p95.values['95.0'], - avg, - tpm, - impact, - transaction_type: transactionType - }; - }); - - // Sort results by impact - needs to be desc, hence the reverse() - return sortBy(results, o => o.impact).reverse(); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/get_top_transactions.ts b/x-pack/plugins/apm/server/lib/transactions/get_top_transactions.ts new file mode 100644 index 0000000000000..059d3710b969a --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/get_top_transactions.ts @@ -0,0 +1,63 @@ +/* + * 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 { SearchParams, SearchResponse } from 'elasticsearch'; +import { get } from 'lodash'; +import { + PROCESSOR_EVENT, + SERVICE_NAME, + TRANSACTION_TYPE +} from '../../../common/constants'; +import { Transaction } from '../../../typings/Transaction'; +import { ITransactionGroup } from '../../../typings/TransactionGroup'; +import { Setup } from '../helpers/setup_request'; +import { + prepareTransactionGroups, + TRANSACTION_GROUP_AGGREGATES +} from '../helpers/transaction_group_query'; + +export async function getTopTransactions({ + setup, + transactionType, + serviceName +}: { + setup: Setup; + transactionType: string; + serviceName: string; +}): Promise { + const { start, end, esFilterQuery, client, config } = setup; + + const params: SearchParams = { + index: config.get('apm_oss.transactionIndices'), + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { + range: { + '@timestamp': { gte: start, lte: end, format: 'epoch_millis' } + } + } + ] + } + }, + aggs: TRANSACTION_GROUP_AGGREGATES + } + }; + + if (esFilterQuery) { + params.body.query.bool.filter.push(esFilterQuery); + } + + const response: SearchResponse = await client('search', params); + const buckets = get(response, 'aggregations.transactions.buckets', []); + + return prepareTransactionGroups({ buckets, start, end }); +} diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction.js b/x-pack/plugins/apm/server/lib/transactions/get_transaction.js deleted file mode 100644 index 3319814a4ec90..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction.js +++ /dev/null @@ -1,45 +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 { TRANSACTION_ID, PROCESSOR_EVENT } from '../../../common/constants'; -import { get } from 'lodash'; - -async function getTransaction({ transactionId, setup }) { - const { start, end, esFilterQuery, client, config } = setup; - - const params = { - index: config.get('apm_oss.transactionIndices'), - body: { - size: 1, - query: { - bool: { - filter: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, - { term: { [TRANSACTION_ID]: transactionId } }, - { - range: { - '@timestamp': { - gte: start, - lte: end, - format: 'epoch_millis' - } - } - } - ] - } - } - } - }; - - if (esFilterQuery) { - params.body.query.bool.filter.push(esFilterQuery); - } - - const resp = await client('search', params); - return get(resp, 'hits.hits[0]._source', {}); -} - -export default getTransaction; diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction.ts new file mode 100644 index 0000000000000..d9932a1013716 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction.ts @@ -0,0 +1,72 @@ +/* + * 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 { SearchParams, SearchResponse } from 'elasticsearch'; +import { oc } from 'ts-optchain'; +import { Transaction } from 'x-pack/plugins/apm/typings/Transaction'; +import { + PROCESSOR_EVENT, + TRACE_ID, + TRANSACTION_ID +} from '../../../common/constants'; +import { Setup } from '../helpers/setup_request'; + +interface HttpError extends Error { + statusCode?: number; +} + +export async function getTransaction( + transactionId: string, + traceId: string | undefined, + setup: Setup +) { + const { start, end, esFilterQuery, client, config } = setup; + + const params: SearchParams = { + index: config.get('apm_oss.transactionIndices'), + body: { + size: 1, + query: { + bool: { + filter: [ + { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [TRANSACTION_ID]: transactionId } }, + { + range: { + '@timestamp': { + gte: start, + lte: end, + format: 'epoch_millis' + } + } + } + ] + } + } + } + }; + + if (esFilterQuery) { + params.body.query.bool.filter.push(esFilterQuery); + } + + if (traceId) { + params.body.query.bool.filter.push({ term: { [TRACE_ID]: traceId } }); + } + + const resp: SearchResponse = await client('search', params); + const result = oc(resp).hits.hits[0]._source(); + + if (result === undefined) { + const notFoundError = new Error( + `No results found for transaction ID ${transactionId} and trace ID ${traceId}` + ) as HttpError; + notFoundError.statusCode = 404; + throw notFoundError; + } + + return result; +} diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction_duration.js b/x-pack/plugins/apm/server/lib/transactions/get_transaction_duration.js deleted file mode 100644 index fb9620fe9659a..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction_duration.js +++ /dev/null @@ -1,48 +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 { get } from 'lodash'; -import { - TRANSACTION_ID, - TRANSACTION_DURATION, - PROCESSOR_EVENT -} from '../../../common/constants'; - -export async function getTransactionDuration({ transactionId, setup }) { - const { start, end, esFilterQuery, client, config } = setup; - - const params = { - index: config.get('apm_oss.transactionIndices'), - body: { - size: 1, - _source: TRANSACTION_DURATION, - query: { - bool: { - filter: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, - { term: { [TRANSACTION_ID]: transactionId } }, - { - range: { - '@timestamp': { - gte: start, - lte: end, - format: 'epoch_millis' - } - } - } - ] - } - } - } - }; - - if (esFilterQuery) { - params.body.query.bool.filter.push(esFilterQuery); - } - - const resp = await client('search', params); - return get(resp, `hits.hits[0]._source.${TRANSACTION_DURATION}`); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/spans/get_spans.js b/x-pack/plugins/apm/server/lib/transactions/spans/get_spans.ts similarity index 70% rename from x-pack/plugins/apm/server/lib/transactions/spans/get_spans.js rename to x-pack/plugins/apm/server/lib/transactions/spans/get_spans.ts index b7f3a9f3d96df..dc587dfd2f49b 100644 --- a/x-pack/plugins/apm/server/lib/transactions/spans/get_spans.js +++ b/x-pack/plugins/apm/server/lib/transactions/spans/get_spans.ts @@ -4,14 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SearchResponse } from 'elasticsearch'; +import { Span } from 'x-pack/plugins/apm/typings/Span'; import { - TRANSACTION_ID, + PROCESSOR_EVENT, SPAN_START, SPAN_TYPE, - PROCESSOR_EVENT + TRANSACTION_ID } from '../../../../common/constants'; +import { Setup } from '../../helpers/setup_request'; -async function getSpans({ transactionId, setup }) { +export async function getSpans(transactionId: string, setup: Setup) { const { start, end, client, config } = setup; const params = { @@ -47,19 +50,6 @@ async function getSpans({ transactionId, setup }) { } }; - const resp = await client('search', params); - return { - span_types: resp.aggregations.types.buckets.map(bucket => ({ - type: bucket.key, - count: bucket.doc_count - })), - spans: resp.hits.hits.map((doc, i) => ({ - doc_id: doc._id, - id: i, - ...doc._source.span, - context: doc._source.context - })) - }; + const resp: SearchResponse = await client('search', params); + return resp.hits.hits.map(hit => hit._source); } - -export default getSpans; diff --git a/x-pack/plugins/apm/server/routes/services.js b/x-pack/plugins/apm/server/routes/services.ts similarity index 76% rename from x-pack/plugins/apm/server/routes/services.js rename to x-pack/plugins/apm/server/routes/services.ts index 90a3d923800ba..fab8b7809ee7e 100644 --- a/x-pack/plugins/apm/server/routes/services.js +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -5,19 +5,22 @@ */ import Boom from 'boom'; -import { getServices } from '../lib/services/get_services'; -import { getService } from '../lib/services/get_service'; -import { setupRequest } from '../lib/helpers/setup_request'; +import { IReply, Request, Server } from 'hapi'; import { withDefaultValidators } from '../lib/helpers/input_validation'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { getService } from '../lib/services/get_service'; +import { getServices } from '../lib/services/get_services'; const ROOT = '/api/apm/services'; const pre = [{ method: setupRequest, assign: 'setup' }]; -const defaultErrorHandler = reply => err => { +const defaultErrorHandler = (reply: IReply) => (err: Error) => { + // tslint:disable-next-line console.error(err.stack); + // @ts-ignore reply(Boom.wrap(err, 400)); }; -export function initServicesApi(server) { +export function initServicesApi(server: Server) { server.route({ method: 'GET', path: ROOT, @@ -27,9 +30,9 @@ export function initServicesApi(server) { query: withDefaultValidators() } }, - handler: (req, reply) => { + handler: (req: Request, reply: IReply) => { const { setup } = req.pre; - return getServices({ setup }) + return getServices(setup) .then(reply) .catch(defaultErrorHandler(reply)); } @@ -44,10 +47,10 @@ export function initServicesApi(server) { query: withDefaultValidators() } }, - handler: (req, reply) => { + handler: (req: Request, reply: IReply) => { const { setup } = req.pre; const { serviceName } = req.params; - return getService({ serviceName, setup }) + return getService(serviceName, setup) .then(reply) .catch(defaultErrorHandler(reply)); } diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts new file mode 100644 index 0000000000000..737f1d48cbf9a --- /dev/null +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -0,0 +1,61 @@ +/* + * 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 Boom from 'boom'; +import { IReply, Request, Server } from 'hapi'; +import { withDefaultValidators } from '../lib/helpers/input_validation'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { getTopTraces } from '../lib/traces/get_top_traces'; +import { getTrace } from '../lib/traces/get_trace'; + +const pre = [{ method: setupRequest, assign: 'setup' }]; +const ROOT = '/api/apm/traces'; +const defaultErrorHandler = (reply: IReply) => (err: Error) => { + // tslint:disable-next-line + console.error(err.stack); + // @ts-ignore + reply(Boom.wrap(err, 400)); +}; + +export function initTracesApi(server: Server) { + // Get trace list + server.route({ + method: 'GET', + path: ROOT, + config: { + pre, + validate: { + query: withDefaultValidators() + } + }, + handler: (req: Request, reply: IReply) => { + const { setup } = req.pre; + + return getTopTraces(setup) + .then(reply) + .catch(defaultErrorHandler(reply)); + } + }); + + // Get individual trace + server.route({ + method: 'GET', + path: `${ROOT}/{traceId}`, + config: { + pre, + validate: { + query: withDefaultValidators() + } + }, + handler: (req: Request, reply: IReply) => { + const { traceId } = req.params; + const { setup } = req.pre; + return getTrace(traceId, setup) + .then(reply) + .catch(defaultErrorHandler(reply)); + } + }); +} diff --git a/x-pack/plugins/apm/server/routes/transactions.js b/x-pack/plugins/apm/server/routes/transactions.ts similarity index 75% rename from x-pack/plugins/apm/server/routes/transactions.js rename to x-pack/plugins/apm/server/routes/transactions.ts index 1bad37fece9c2..b525ee8f1499b 100644 --- a/x-pack/plugins/apm/server/routes/transactions.js +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -4,26 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; import Boom from 'boom'; - +import { IReply, Request, Server } from 'hapi'; +import Joi from 'joi'; +import { withDefaultValidators } from '../lib/helpers/input_validation'; +import { setupRequest } from '../lib/helpers/setup_request'; +// @ts-ignore import { getTimeseriesData } from '../lib/transactions/charts/get_timeseries_data'; -import getSpans from '../lib/transactions/spans/get_spans'; import { getDistribution } from '../lib/transactions/distribution/get_distribution'; -import { getTransactionDuration } from '../lib/transactions/get_transaction_duration'; import { getTopTransactions } from '../lib/transactions/get_top_transactions'; -import getTransaction from '../lib/transactions/get_transaction'; -import { setupRequest } from '../lib/helpers/setup_request'; -import { withDefaultValidators } from '../lib/helpers/input_validation'; +import { getTransaction } from '../lib/transactions/get_transaction'; +import { getSpans } from '../lib/transactions/spans/get_spans'; const pre = [{ method: setupRequest, assign: 'setup' }]; const ROOT = '/api/apm/services/{serviceName}/transactions'; -const defaultErrorHandler = reply => err => { +const defaultErrorHandler = (reply: IReply) => (err: Error) => { + // tslint:disable-next-line console.error(err.stack); - reply(Boom.wrap(err, 400)); + // @ts-ignore + reply(Boom.wrap(err, err.statusCode || 400)); }; -export function initTransactionsApi(server) { +export function initTransactionsApi(server: Server) { server.route({ method: 'GET', path: ROOT, @@ -36,7 +38,7 @@ export function initTransactionsApi(server) { }) } }, - handler: (req, reply) => { + handler: (req: Request, reply: IReply) => { const { serviceName } = req.params; const { transaction_type: transactionType } = req.query; const { setup } = req.pre; @@ -57,14 +59,17 @@ export function initTransactionsApi(server) { config: { pre, validate: { - query: withDefaultValidators() + query: withDefaultValidators({ + traceId: Joi.string().allow('') + }) } }, - handler: (req, reply) => { + handler: (req: Request, reply: IReply) => { const { transactionId } = req.params; + const { traceId } = req.query; const { setup } = req.pre; - return getTransaction({ transactionId, setup }) - .then(res => reply(res)) + return getTransaction(transactionId, traceId, setup) + .then(reply) .catch(defaultErrorHandler(reply)); } }); @@ -78,14 +83,11 @@ export function initTransactionsApi(server) { query: withDefaultValidators() } }, - handler: (req, reply) => { + handler: (req: Request, reply: IReply) => { const { transactionId } = req.params; const { setup } = req.pre; - return Promise.all([ - getSpans({ transactionId, setup }), - getTransactionDuration({ transactionId, setup }) - ]) - .then(([spans, duration]) => reply({ ...spans, duration })) + return getSpans(transactionId, setup) + .then(reply) .catch(defaultErrorHandler(reply)); } }); @@ -103,7 +105,7 @@ export function initTransactionsApi(server) { }) } }, - handler: (req, reply) => { + handler: (req: Request, reply: IReply) => { const { setup } = req.pre; const { serviceName } = req.params; const transactionType = req.query.transaction_type; @@ -131,15 +133,11 @@ export function initTransactionsApi(server) { }) } }, - handler: (req, reply) => { + handler: (req: Request, reply: IReply) => { const { setup } = req.pre; const { serviceName } = req.params; const { transaction_name: transactionName } = req.query; - return getDistribution({ - serviceName, - transactionName, - setup - }) + return getDistribution(serviceName, transactionName, setup) .then(reply) .catch(defaultErrorHandler(reply)); } diff --git a/x-pack/plugins/apm/typings/APMDoc.ts b/x-pack/plugins/apm/typings/APMDoc.ts new file mode 100644 index 0000000000000..dc8fffad9bc30 --- /dev/null +++ b/x-pack/plugins/apm/typings/APMDoc.ts @@ -0,0 +1,81 @@ +/* + * 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. + */ + +export interface APMDocV1 { + '@timestamp': string; + beat: { + hostname: string; + name: string; + version: string; + }; + host: { + name: string; + }; +} + +export interface APMDocV2 extends APMDocV1 { + timestamp: { + us: number; + }; + parent?: { + id: string; // parent ID is not available on the root transaction + }; + trace: { + id: string; + }; +} + +export interface ContextService { + name: string; + agent: { + name: string; + version: string; + }; + framework?: { + name: string; + version: string; + }; + runtime?: { + name: string; + version: string; + }; + language?: { + name: string; + version?: string; + }; +} + +export interface Stackframe { + filename: string; + line: { + number: number; + column?: number; + context?: string; + }; + abs_path?: string; + colno?: number; + context_line?: string; + function?: string; + library_frame?: boolean; + exclude_from_grouping?: boolean; + module?: string; + context?: { + post?: string[]; + pre?: string[]; + }; + sourcemap?: { + updated?: boolean; + error?: string; + }; + vars?: any; + orig?: { + filename?: string; + abs_path?: string; + function?: string; + lineno?: number; + colno?: number; + }; +} diff --git a/x-pack/plugins/apm/typings/Error.ts b/x-pack/plugins/apm/typings/Error.ts new file mode 100644 index 0000000000000..a4bacfeff62ce --- /dev/null +++ b/x-pack/plugins/apm/typings/Error.ts @@ -0,0 +1,46 @@ +/* + * 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 { APMDocV1, ContextService, Stackframe } from './APMDoc'; + +export interface Error extends APMDocV1 { + processor: { + name: 'error'; + event: 'error'; + }; + context: { + process?: { + pid: number; + }; + service: ContextService; + }; + transaction?: { + id: string; // transaction ID is not required in v1 + }; + error: { + id?: string; // ID is not required in v1 + timestamp: string; + culprit: string; + grouping_key: string; + // either exception or log are given + exception?: { + message?: string; // either message or type are given + type?: string; + code?: string; + module?: string; + attributes?: any; + handled?: boolean; + stacktrace?: Stackframe[]; + }; + log?: { + message: string; + param_message?: string; + logger_name?: string; + level?: string; + stacktrace?: Stackframe[]; + }; + }; +} diff --git a/x-pack/plugins/apm/typings/Span.ts b/x-pack/plugins/apm/typings/Span.ts new file mode 100644 index 0000000000000..b14a328a85988 --- /dev/null +++ b/x-pack/plugins/apm/typings/Span.ts @@ -0,0 +1,68 @@ +/* + * 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 { APMDocV1, APMDocV2, ContextService, Stackframe } from './APMDoc'; + +export interface DbContext { + instance?: string; + statement?: string; + type?: string; + user?: string; +} + +interface Processor { + name: 'transaction'; + event: 'span'; +} + +interface Context { + db?: DbContext; + service: ContextService; + [key: string]: any; +} + +export interface SpanV1 extends APMDocV1 { + version: 'v1'; + processor: Processor; + context: Context; + span: { + duration: { + us: number; + }; + start: { + us: number; // only v1 + }; + name: string; + type: string; + id: number; // we are manually adding span.id + parent?: string; // only v1 + stacktrace?: Stackframe[]; + }; + transaction: { + id: string; + }; +} + +export interface SpanV2 extends APMDocV2 { + version: 'v2'; + processor: Processor; + context: Context; + span: { + duration: { + us: number; + }; + name: string; + type: string; + id: number; // id will be derived from hex encoded 64 bit hex_id string in v2 + hex_id: string; // only v2 + stacktrace?: Stackframe[]; + }; + transaction: { + id: string; + }; +} + +export type Span = SpanV1 | SpanV2; diff --git a/x-pack/plugins/apm/typings/Transaction.ts b/x-pack/plugins/apm/typings/Transaction.ts new file mode 100644 index 0000000000000..e7d33d7403ba2 --- /dev/null +++ b/x-pack/plugins/apm/typings/Transaction.ts @@ -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 { APMDocV1, APMDocV2, ContextService } from './APMDoc'; + +interface Processor { + name: 'transaction'; + event: 'transaction'; +} + +interface ContextSystem { + architecture: string; + hostname: string; + ip: string; + platform: string; +} + +interface Context { + process?: { + pid: number; + }; + service: ContextService; + system: ContextSystem; + request: { + url: { + full: string; + }; + }; + user?: { + id: string; + }; + [key: string]: any; +} + +interface Marks { + agent: { + [name: string]: number; + }; +} + +export interface TransactionV1 extends APMDocV1 { + version: 'v1'; + processor: Processor; + context: Context; + transaction: { + duration: { + us: number; + }; + id: string; + marks?: Marks; + name: string; // name could be missing in ES but the UI will always only aggregate on transactions with a name + result?: string; + sampled: boolean; + span_count?: { + dropped?: { + total?: number; + }; + }; + type: string; + }; +} + +export interface TransactionV2 extends APMDocV2 { + version: 'v2'; + processor: Processor; + context: Context; + transaction: { + duration: { + us: number; + }; + id: string; + marks?: Marks; + name: string; // name could be missing in ES but the UI will always only aggregate on transactions with a name + result?: string; + sampled: boolean; + + span_count?: { + started?: number; // only v2 + dropped?: { + total?: number; + }; + }; + type: string; + }; +} + +export type Transaction = TransactionV1 | TransactionV2; diff --git a/x-pack/plugins/apm/typings/TransactionGroup.ts b/x-pack/plugins/apm/typings/TransactionGroup.ts new file mode 100644 index 0000000000000..3967d4d94d5cf --- /dev/null +++ b/x-pack/plugins/apm/typings/TransactionGroup.ts @@ -0,0 +1,16 @@ +/* + * 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 './Transaction'; + +export interface ITransactionGroup { + name: string; + sample: Transaction; + p95: number; + averageResponseTime: number; + transactionsPerMinute: number; + impact: number; +} diff --git a/x-pack/plugins/apm/public/components/shared/PropertiesTable/types.d.ts b/x-pack/plugins/apm/typings/common.ts similarity index 76% rename from x-pack/plugins/apm/public/components/shared/PropertiesTable/types.d.ts rename to x-pack/plugins/apm/typings/common.ts index 057011fc8ab98..c69188724f8ed 100644 --- a/x-pack/plugins/apm/public/components/shared/PropertiesTable/types.d.ts +++ b/x-pack/plugins/apm/typings/common.ts @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -type KeySorter = (data: StringMap, parentKey?: string) => string[]; +export interface StringMap { + [key: string]: T; +} diff --git a/x-pack/plugins/apm/public/components/shared/HOCUtils.js b/x-pack/plugins/apm/typings/elasticsearch.ts similarity index 65% rename from x-pack/plugins/apm/public/components/shared/HOCUtils.js rename to x-pack/plugins/apm/typings/elasticsearch.ts index 042d5086cf445..f3a536e8993ff 100644 --- a/x-pack/plugins/apm/public/components/shared/HOCUtils.js +++ b/x-pack/plugins/apm/typings/elasticsearch.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export function getDisplayName(WrappedComponent) { - return WrappedComponent.displayName || WrappedComponent.name || 'Component'; +export interface TermsAggsBucket { + key: string; + doc_count: number; } diff --git a/x-pack/plugins/apm/types.d.ts b/x-pack/plugins/apm/typings/global_types.d.ts similarity index 92% rename from x-pack/plugins/apm/types.d.ts rename to x-pack/plugins/apm/typings/global_types.d.ts index e538124ba47eb..ba1e9e998727f 100644 --- a/x-pack/plugins/apm/types.d.ts +++ b/x-pack/plugins/apm/typings/global_types.d.ts @@ -11,7 +11,3 @@ declare module '*.json' { const json: any; export default json; } - -interface StringMap { - [key: string]: T; -} diff --git a/x-pack/plugins/apm/typings/react-redux-request.d.ts b/x-pack/plugins/apm/typings/react-redux-request.d.ts new file mode 100644 index 0000000000000..abee82e2f6754 --- /dev/null +++ b/x-pack/plugins/apm/typings/react-redux-request.d.ts @@ -0,0 +1,35 @@ +/* + * 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. + */ + +// Everything in here should be moved to http://github.com/sqren/react-redux-request + +declare module 'react-redux-request' { + import React from 'react'; + + export interface RRRRenderArgs { + status: 'SUCCESS' | 'LOADING' | 'FAILURE'; + data: T; + args: P; + } + + export type RRRRender = ( + args: RRRRenderArgs + ) => JSX.Element | null; + + export interface RequestProps { + id: string; + fn: (args: any) => Promise; + selector?: (state: any) => any; + args?: any[]; + render?: RRRRender; + } + + export function reducer(state: any): any; + + export class Request extends React.Component< + RequestProps + > {} +} diff --git a/x-pack/plugins/apm/typings/waterfall.ts b/x-pack/plugins/apm/typings/waterfall.ts new file mode 100644 index 0000000000000..ad191907698de --- /dev/null +++ b/x-pack/plugins/apm/typings/waterfall.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Span } from './Span'; +import { Transaction } from './Transaction'; + +export interface WaterfallResponse { + services: string[]; + hits: Array; +} diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 7f043436e6180..2d5f38c688105 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -226,10 +226,10 @@ resolved "https://registry.yarnpkg.com/@types/delay/-/delay-2.0.1.tgz#61bcf318a74b61e79d1658fbf054f984c90ef901" integrity sha512-D1/YuYOcdOIdaQnaiUJ77VcilVvESkynw79CtGqpjkXyv4OUezEVZtdXnSOwXL8Zcelu66QbyC8QQcVQ/ZPdig== -"@types/elasticsearch@^5.0.22": - version "5.0.24" - resolved "https://registry.yarnpkg.com/@types/elasticsearch/-/elasticsearch-5.0.24.tgz#b09082d2ba3d8ae1627ea771bd2fbd2851e4a035" - integrity sha512-QRpGleGwKv70hEcdklBh3HiLZ3OHPp40nRiVfhLk9wlQ4+V//SX+n90uIHN/mfKz828bjSSAxSG/kDUEp4Yp8Q== +"@types/elasticsearch@^5.0.26": + version "5.0.28" + resolved "https://registry.yarnpkg.com/@types/elasticsearch/-/elasticsearch-5.0.28.tgz#0e4cdf7d9c9a3fe901c0da4fb9ad824c6d3b4091" + integrity sha512-hM9Rs1trCkthBz1z9UwAJKQ4/ZaNnKbKB2Utf4iuY91OssLMUHzXZP8mBrkUif4kp/bNOPt6w92L9YmL2nSMuA== "@types/events@*": version "1.2.0" @@ -277,7 +277,12 @@ dependencies: "@types/node" "*" -"@types/history@*", "@types/history@^4.6.2": +"@types/history@*": + version "4.7.2" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.2.tgz#0e670ea254d559241b6eeb3894f8754991e73220" + integrity sha512-ui3WwXmjTaY73fOQ3/m3nnajU/Orhi6cEu5rzX+BrAAJxa3eITXZ5ch9suPqtM03OWhAHhPSyBGCN4UKoxO20Q== + +"@types/history@^4.6.2": version "4.6.2" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0" integrity sha512-eVAb52MJ4lfPLiO9VvTgv8KaZDEIqCwhv+lXOMLlt4C1YHTShgmMULEg0RrCbnqfYd6QKfHsMp0MiX0vWISpSw== @@ -321,13 +326,6 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.5.tgz#8a4accfc403c124a0bafe8a9fc61a05ec1032073" integrity sha512-lAVp+Kj54ui/vLUFxsJTMtWvZraZxum3w3Nwkble2dNuV5VnPA+Mi2oGX9XYJAaIvZi3tn3cbjS/qcJXRb6Bww== -"@types/moment-timezone@^0.5.8": - version "0.5.8" - resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.8.tgz#92aba9bc238cabf69a27a1a4f52e0ebb8f10f896" - integrity sha512-FpC+fLd/Hmxxcl4cxeb5HTyCmEvl3b4TeX8w9J+0frdzH+UCEkexKe4WZ3DTALwLj2/hyujn8tp3zl1YdgLrxQ== - dependencies: - moment ">=2.14.0" - "@types/node@*", "@types/node@8.10.21", "@types/node@^9.4.6", "@types/node@^9.4.7": version "8.10.21" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.21.tgz#12b3f2359b27aa05a45d886c8ba1eb8d1a77e285" @@ -392,24 +390,24 @@ "@types/react" "*" redux "^4.0.0" -"@types/react-router-dom@4.2.6": - version "4.2.6" - resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.2.6.tgz#9f7eb3c0e6661a9607d878ff8675cc4ea95cd276" - integrity sha512-K7SdbkF8xgecp2WCeXw51IMySYvQ1EuVPKfjU1fymyTSX9bZk5Qx8T5cipwtAY8Zhb/4GIjhYKm0ZGVEbCKEzQ== +"@types/react-router-dom@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.1.tgz#71fe2918f8f60474a891520def40a63997dafe04" + integrity sha512-GbztJAScOmQ/7RsQfO4cd55RuH1W4g6V1gDW3j4riLlt+8yxYLqqsiMzmyuXBLzdFmDtX/uU2Bpcm0cmudv44A== dependencies: "@types/history" "*" "@types/react" "*" "@types/react-router" "*" "@types/react-router@*": - version "4.0.27" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.27.tgz#553f54df7c4b09d6046b0201ce9b91c46b2940e3" - integrity sha512-EqGMptbgv4IkwJdU/ozonsFiL1iESUXk57rA6myayd/bIgYP4/pD0cZJUpOWCSvYT7QLDBuDkrwyEgCqfMZfNg== + version "4.0.32" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.32.tgz#501529e3d7aa7d5c738d339367e1a7dd5338b2a7" + integrity sha512-VLQSifCIKCTpfMFrJN/nO5a45LduB6qSMkO9ASbcGdCHiDwJnrLNzk91Q895yG0qWY7RqT2jR16giBRpRG1HQw== dependencies: "@types/history" "*" "@types/react" "*" -"@types/react@*", "@types/react@^16.3.14": +"@types/react@*", "@types/react@16.3.14": version "16.3.14" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.3.14.tgz#f90ac6834de172e13ecca430dcb6814744225d36" integrity sha512-wNUGm49fPl7eE2fnYdF0v5vSOrUMdKMQD/4NwtQRnb6mnPwtkhabmuFz37eq90+hhyfz0pWd38jkZHOcaZ6LGw== @@ -9004,7 +9002,7 @@ react-router-breadcrumbs-hoc@1.1.2: resolved "https://registry.yarnpkg.com/react-router-breadcrumbs-hoc/-/react-router-breadcrumbs-hoc-1.1.2.tgz#4fafb620e7c6b876d98f7151f4c85ae5c3157dc0" integrity sha1-T6+2IOfGuHbZj3FR9Mha5cMVfcA= -react-router-dom@^4.2.2: +react-router-dom@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6" integrity sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA== diff --git a/yarn.lock b/yarn.lock index 789f9866fa849..1c067416c1dc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -420,6 +420,222 @@ resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.0.tgz#4b7daf2c51696cfc70b942c11690528229d1a1ce" integrity sha512-EIjmpvnHj+T4nMcKwHwxZKUfDmphIKJc2qnEMhSoOvr1lYEQpuRKRz8orWr//krYIIArS/KGGLfL2YGVUYXmIA== +"@types/d3-array@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.3.tgz#dd141e3ba311485fffbf0792a1b01a7f2ec12dc1" + integrity sha512-yTO4ws1jnWC7iSKK8j7sUAGKIcJ628ioiGTdyXmPd36cNnuY9fDcjgEW2r19yXWuQFLu61/JhHVZ8RYYTEzFSg== + +"@types/d3-axis@*": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-1.0.11.tgz#efd975f9fec14c2afd03828f3acec0ef97d37c3b" + integrity sha512-cuigApCyCwYJxaQPghj+BqaxzbdRdT/lpZBMtF7EuEIJ61NMQ8yvGnqFvHCIgJEmUu2Wb2wiZqy9kiHi3Ddftg== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-brush@*": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-1.0.9.tgz#c71070845946eeee4cf330e04123a3997e6476bf" + integrity sha512-mAx8IVc0luUHfk51pl0UN1vzybnAzLMUsvIwLt3fbsqqPkSXr+Pu1AxOPPeyNc27LhHJnfH/LCV7Jlv+Yzqu1A== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-chord@*": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-1.0.8.tgz#08c0fbb10281be0a5b3fdf48c9c081af02f79fb6" + integrity sha512-F0ftYOo7FenAIxsRjXLt8vbij0NLDuVcL+xaGY7R9jUmF2Mrpj1T5XukBI9Cad+Ei7YSxEWREIO+CYcaKCl2qQ== + +"@types/d3-collection@*": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-collection/-/d3-collection-1.0.7.tgz#829e1db477d6bbbcdc038cbc489f22798752d707" + integrity sha512-vR3BT0GwHc5y93Jv6bxn3zoxP/vGu+GdXu/r1ApjbP9dLk9I2g6NiV7iP/QMQSuFZd0It0n/qWrfXHxCWwHIkg== + +"@types/d3-color@*": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.2.1.tgz#26141c3c554e320edd40726b793570a3ae57397e" + integrity sha512-xwb1tqvYNWllbHuhMFhiXk63Imf+QNq/dJdmbXmr2wQVnwGenCuj3/0IWJ9hdIFQIqzvhT7T37cvx93jtAsDbQ== + +"@types/d3-contour@*": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-1.2.2.tgz#b32256b55aed9e2113f88a8ea23846e357fa386c" + integrity sha512-2BIp8c80HWJP/K6t7hov6CX6G/9LWPaf1IkRXmAY3xRDr293u6OxQDSsJNc8IHl3SDWfrUw9mZhBIavS5UOGKg== + dependencies: + "@types/d3-array" "*" + "@types/geojson" "*" + +"@types/d3-dispatch@*": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-1.0.6.tgz#19b173f669cd2ab7dd3d862e8037aae1a98c7508" + integrity sha512-xyWJQMr832vqhu6fD/YqX+MSFBWnkxasNhcStvlhqygXxj0cKqPft0wuGoH5TIq5ADXgP83qeNVa4R7bEYN3uA== + +"@types/d3-drag@*": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-1.2.1.tgz#6394bcf2f6414140b3b0d521259cadc6fa1da926" + integrity sha512-J9liJ4NNeV0oN40MzPiqwWjqNi3YHCRtHNfNMZ1d3uL9yh1+vDuo346LBEr8yyBm30WHvrHssAkExVZrGCswtA== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-dsv@*": + version "1.0.33" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-1.0.33.tgz#18de1867927f7ec898671aef82f730f16d4c7fcb" + integrity sha512-jx5YvaVC3Wfh6LobaiWTeU1NkvL2wPmmpmajk618bD+xVz98yNWzmZMvmlPHGK0HXbMeHmW/6oVX48V9AH1bRQ== + +"@types/d3-ease@*": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-1.0.7.tgz#93a301868be9e15061f3d44343b1ab3f8acb6f09" + integrity sha1-k6MBhovp4VBh89RDQ7GrP4rLbwk= + +"@types/d3-fetch@*": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-1.1.2.tgz#a59921477e25850ca6b3353e03d5d29e5a0e8e03" + integrity sha512-w6ANZv/mUh+6IV3drT22zgPWMRobzuGXhzOZC8JPD+ygce0/Vx6vTci3m3dizkocnQQCOwNbrWWWPYqpWiKzRQ== + dependencies: + "@types/d3-dsv" "*" + +"@types/d3-force@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-1.1.1.tgz#185c18b77932df63457894bd36d0d6e9692546c0" + integrity sha512-ePkELuaFWY4yOuf+Bvx5Xd+ihFiYG4bdnW0BlvigovIm8Sob2t76e9RGO6lybQbv6AlW9Icn9HuZ9fmdzEoJyg== + +"@types/d3-format@*": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.3.0.tgz#c5e115fac8e6861ce656fe9861892b22f6b0cfcb" + integrity sha512-ZiY4j3iJvAdOwzwW24WjlZbUNvqOsnPAMfPBmdXqxj3uKJbrzBlRrdGl5uC89pZpFs9Dc92E81KcwG2uEgkIZA== + +"@types/d3-geo@*": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-1.10.3.tgz#3c01b2baa480e1108301096328dc2837e7ff4d8a" + integrity sha512-hfdaxM2L0wA9mDZrrSf2o+DyhEpnJYCiAN+lHFtpfZOVCQrYBA5g33sGRpUbAvjSMyO5jkHbftMWPEhuCMChSg== + dependencies: + "@types/geojson" "*" + +"@types/d3-hierarchy@*": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.4.tgz#b04dfcb1f2074da789ada10fe4942d13f0bce421" + integrity sha512-+d2VLfLPgW66VB7k56T8tC4LobfS6Rrhm+1pmYPMmlCpO5rccJLuwux7YXl/eGVst3Bhb5PJTN5/oaJERpNw8g== + +"@types/d3-interpolate@*": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-1.3.0.tgz#65b9627900bfdd82474875d9b23d574a4388af7c" + integrity sha512-Ng4ds7kPSvP/c3W3J5PPUQlgewif1tGBqCeh5lgY+UG82Y7H9zQ8c2gILsEFDLg7wRGOwnuKZ940Q/LSN14w9w== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.7.tgz#a0736fceed688a695f48265a82ff7a3369414b81" + integrity sha512-U8dFRG+8WhkLJr2sxZ9Cw/5WeRgBnNqMxGdA1+Z0+ZG6tK0s75OQ4OXnxeyfKuh6E4wQPY8OAKr1+iNDx01BEQ== + +"@types/d3-polygon@*": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-1.0.6.tgz#db25c630a2afb9191fe51ba61dd37baee9dd44c7" + integrity sha512-E6Kyodn9JThgLq20nxSbEce9ow5/ePgm9PX2EO6W1INIL4DayM7cFaiG10DStuamjYAd0X4rntW2q+GRjiIktw== + +"@types/d3-quadtree@*": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-1.0.6.tgz#45da9e603688ba90eedd3d40f6e504764e06e493" + integrity sha512-sphVuDdiSIaxLt9kQgebJW98pTktQ/xuN7Ysd8X68Rnjeg/q8+c36/ShlqU52qoKg9nob/JEHH1uQMdxURZidQ== + +"@types/d3-random@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-1.1.1.tgz#38647ce2ff4ce7d0d56974334c1c4092513c8b9f" + integrity sha512-jUPeBq1XKK9/5XasTvy5QAUwFeMsjma2yt/nP02yC2Tijovx7i/W5776U/HZugxc5SSmtpx4Z3g9KFVon0QrjQ== + +"@types/d3-scale-chromatic@*": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-1.3.0.tgz#b8b58a7a262a583fc1c95ce851d5a75811875034" + integrity sha512-JqQH5uu1kmdQEa6XSu7NYzQM71lL1YreBPS5o8SnmEDcBRKL6ooykXa8iFPPOEUiTah25ydi+cTrbsogBSMNSQ== + +"@types/d3-scale@*": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-2.0.2.tgz#61145948aa1a52ab31384766cd013308699112b3" + integrity sha512-pnmZsEVwTyX+68bjG9r3XXUBASUF6z3Ir2nlrv81mWCH9yqeRscR98myMNP5OwDd9urUnvjNabJul5B9K0+F2w== + dependencies: + "@types/d3-time" "*" + +"@types/d3-selection@*": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.3.2.tgz#dd5661a560ba9ce3aba823c424b8d4a1bc7e833f" + integrity sha512-K23sDOi7yMussv7aiqk097IWWbjFYbJpcDppQAcaf6DfmHxAsjr+6N4HJGokETLDuV7y/qJeeIJINPnkWJM5Hg== + +"@types/d3-shape@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.2.4.tgz#e65585f2254d83ae42c47af2e730dd9b97952996" + integrity sha512-X4Xq2mpChPIMDMAXwLfxHKLbqv+sowkJ94bENeSMqqhQJ5v4oXuoyLo0vnIkydVbuQ52ZwPplk219K0m2HJODg== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time-format@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-2.1.0.tgz#011e0fb7937be34a9a8f580ae1e2f2f1336a8a22" + integrity sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA== + +"@types/d3-time@*": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.0.9.tgz#c2cf05a3cd51f810b8d8a9bbca0c74030d4e535e" + integrity sha512-m+D4NbQdDlTVaO7QgXAnatR3IDxQYDMBtRhgSCi5rs9R1LPq1y7/2aqa1FJ2IWjFm1mOV63swDxonnCDlHgHMA== + +"@types/d3-timer@*": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-1.0.8.tgz#a3441d9605367059e14ad8c3494132143cbc8d58" + integrity sha512-AKUgQ/nljUFcUO2P3gK24weVI5XwUTdJvjoh8gJ0yxT4aJ+d7t2Or3TB+k9dEYl14BAjoj32D0ky+YzQSVszfg== + +"@types/d3-transition@*": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-1.1.3.tgz#efcf4941dae22135d595514ba488f4f370d396b0" + integrity sha512-1EukXNuVu/z2G1GZpZagzFJnie9C5zze17ox/vhTgGXNy46rYAm4UkhLLlUeeZ1ndq88k95SOeC8898RpKMLOQ== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-voronoi@*": + version "1.1.8" + resolved "https://registry.yarnpkg.com/@types/d3-voronoi/-/d3-voronoi-1.1.8.tgz#a039cb8368bce4efc1a70aebe744d210851cf1a7" + integrity sha512-zqNhW7QsYQGlfOdrwPNPG3Wk64zUa4epKRurkJ/dVc6oeXrB+iTDt8sRZ0KZKOOXvvfa1dcdB0e45TZeLBiodQ== + +"@types/d3-zoom@*": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-1.7.2.tgz#ee67f063199c179949d83b6b1e6166207de5f06e" + integrity sha512-/ORNUzQ0g7h2f34L/hD1o+IytOjpNLwEf403yKmYAA+z3LC8eCH6xCKaCc0weuCWwiaZ2UqBW41Y6ciqjd+ndQ== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + +"@types/d3@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-5.0.0.tgz#fec49f2aea0f0784f829eff38132926e92676d57" + integrity sha512-BVfPw7ha+UgsG24v6ymerMY4+pJgQ/6p+hJA4loCeaaqV9snGS/G6ReVaQEn8Himn67dWn/Je9WhRbnDO7MzLw== + dependencies: + "@types/d3-array" "*" + "@types/d3-axis" "*" + "@types/d3-brush" "*" + "@types/d3-chord" "*" + "@types/d3-collection" "*" + "@types/d3-color" "*" + "@types/d3-contour" "*" + "@types/d3-dispatch" "*" + "@types/d3-drag" "*" + "@types/d3-dsv" "*" + "@types/d3-ease" "*" + "@types/d3-fetch" "*" + "@types/d3-force" "*" + "@types/d3-format" "*" + "@types/d3-geo" "*" + "@types/d3-hierarchy" "*" + "@types/d3-interpolate" "*" + "@types/d3-path" "*" + "@types/d3-polygon" "*" + "@types/d3-quadtree" "*" + "@types/d3-random" "*" + "@types/d3-scale" "*" + "@types/d3-scale-chromatic" "*" + "@types/d3-selection" "*" + "@types/d3-shape" "*" + "@types/d3-time" "*" + "@types/d3-time-format" "*" + "@types/d3-timer" "*" + "@types/d3-transition" "*" + "@types/d3-voronoi" "*" + "@types/d3-zoom" "*" + "@types/dedent@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@types/dedent/-/dedent-0.7.0.tgz#155f339ca404e6dd90b9ce46a3f78fd69ca9b050" @@ -430,6 +646,11 @@ resolved "https://registry.yarnpkg.com/@types/delay/-/delay-2.0.1.tgz#61bcf318a74b61e79d1658fbf054f984c90ef901" integrity sha512-D1/YuYOcdOIdaQnaiUJ77VcilVvESkynw79CtGqpjkXyv4OUezEVZtdXnSOwXL8Zcelu66QbyC8QQcVQ/ZPdig== +"@types/elasticsearch@^5.0.26": + version "5.0.26" + resolved "https://registry.yarnpkg.com/@types/elasticsearch/-/elasticsearch-5.0.26.tgz#f05d27974b0f14f48295904fa228c830f20de0ea" + integrity sha512-SyNqeVTxWmegueOAYoTD9RahSIwBAAB6Lcuh4ZsYCidrtvP+cIuIMRLXFhmirB7sLkkWqQNWtt/GofEz96gi3Q== + "@types/enzyme@^3.1.12": version "3.1.12" resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.12.tgz#293bb07c1ef5932d37add3879e72e0f5bc614f3c" @@ -475,6 +696,11 @@ dependencies: "@types/node" "*" +"@types/geojson@*": + version "7946.0.4" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.4.tgz#4e049756383c3f055dd8f3d24e63fb543e98eb07" + integrity sha512-MHmwBtCb7OCv1DSivz2UNJXPGU/1btAWRKlqJ2saEhVJkpkvqHMMaOpKg0v4sAbDWSQekHGvPVMM8nQ+Jen03Q== + "@types/getopts@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/getopts/-/getopts-2.0.0.tgz#8a603370cb367d3192bd8012ad39ab2320b5b476" @@ -520,6 +746,11 @@ resolved "https://registry.yarnpkg.com/@types/has-ansi/-/has-ansi-3.0.0.tgz#636403dc4e0b2649421c4158e5c404416f3f0330" integrity sha512-H3vFOwfLlFEC0MOOrcSkus8PCnMCzz4N0EqUbdJZCdDhBTfkAu86aRYA+MTxjKW6jCpUvxcn4715US8g+28BMA== +"@types/history@*": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.0.tgz#2fac51050c68f7d6f96c5aafc631132522f4aa3f" + integrity sha512-1A/RUAX4VtmGzNTGLSfmiPxQ3XwUSe/1YN4lW9GRa+j307oFK6MPjhlvw6jEHDodUBIvSvrA7/iHDchr5LS+0Q== + "@types/iron@*": version "5.0.1" resolved "https://registry.yarnpkg.com/@types/iron/-/iron-5.0.1.tgz#5420bbda8623c48ee51b9a78ebad05d7305b4b24" @@ -683,6 +914,23 @@ "@types/react" "*" redux "^4.0.0" +"@types/react-router-dom@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.1.tgz#71fe2918f8f60474a891520def40a63997dafe04" + integrity sha512-GbztJAScOmQ/7RsQfO4cd55RuH1W4g6V1gDW3j4riLlt+8yxYLqqsiMzmyuXBLzdFmDtX/uU2Bpcm0cmudv44A== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "4.0.31" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.31.tgz#416bac49d746800810886c7b8582a622ed9604fc" + integrity sha512-57Tqu1EDMgDzHhmIEjjQZHrc/N7/+GGv6CtH1wRTLmMIy3UMxX69vQoeEz0AmK0/zkf5ecfEW1ZX8DLVQ6Gl7Q== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-virtualized@^9.18.7": version "9.18.7" resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.18.7.tgz#8703d8904236819facff90b8b320f29233160c90" @@ -691,7 +939,7 @@ "@types/prop-types" "*" "@types/react" "*" -"@types/react@*", "@types/react@^16.3.14": +"@types/react@*", "@types/react@16.3.14": version "16.3.14" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.3.14.tgz#f90ac6834de172e13ecca430dcb6814744225d36" integrity sha512-wNUGm49fPl7eE2fnYdF0v5vSOrUMdKMQD/4NwtQRnb6mnPwtkhabmuFz37eq90+hhyfz0pWd38jkZHOcaZ6LGw== @@ -735,6 +983,14 @@ resolved "https://registry.yarnpkg.com/@types/strip-ansi/-/strip-ansi-3.0.0.tgz#9b63d453a6b54aa849182207711a08be8eea48ae" integrity sha1-m2PUU6a1SqhJGCIHcRoIvo7qSK4= +"@types/styled-components@^3.0.1": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-3.0.2.tgz#274133bfafaca17f28707b667858bce197ae3b84" + integrity sha512-nG9swaAqmSrUDXyjpE0NxabjVYAGlmtqWXlCpRWRIZBMbTkdcyQULC+ElvTfghTc+1ANJjn6DCyUQirF5a2OOg== + dependencies: + "@types/node" "*" + "@types/react" "*" + "@types/superagent@*": version "3.8.2" resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-3.8.2.tgz#ffdda92843f8966fb4c5f482755ee641ffc53aa7" @@ -13392,19 +13648,7 @@ react-router-breadcrumbs-hoc@1.1.2: resolved "https://registry.yarnpkg.com/react-router-breadcrumbs-hoc/-/react-router-breadcrumbs-hoc-1.1.2.tgz#4fafb620e7c6b876d98f7151f4c85ae5c3157dc0" integrity sha1-T6+2IOfGuHbZj3FR9Mha5cMVfcA= -react-router-dom@4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d" - integrity sha512-cHMFC1ZoLDfEaMFoKTjN7fry/oczMgRt5BKfMAkTu5zEuJvUiPp1J8d0eXSVTnBh6pxlbdqDhozunOOLtmKfPA== - dependencies: - history "^4.7.2" - invariant "^2.2.2" - loose-envify "^1.3.1" - prop-types "^15.5.4" - react-router "^4.2.0" - warning "^3.0.0" - -react-router-dom@^4.2.2: +react-router-dom@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6" integrity sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA== @@ -13416,7 +13660,7 @@ react-router-dom@^4.2.2: react-router "^4.3.1" warning "^4.0.1" -react-router@^4.2.0, react-router@^4.3.1: +react-router@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e" integrity sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg== @@ -16039,6 +16283,11 @@ ts-node@^7.0.1: source-map-support "^0.5.6" yn "^2.0.0" +ts-optchain@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ts-optchain/-/ts-optchain-0.1.1.tgz#9d45e2c3fc6201c2f9be82edad4c76fefb2a36d9" + integrity sha512-WWNaATI+rtNm6C3550HbLzvu32zIg0I0gBkvJZpOQnyeGIoGxg2RZOt96U7bi90ZlQ7bFiI5PCRaXScv4+4Y4A== + tslib@^1.7.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"