mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[APM] Distributed Tracing (#24062)
* Adds traces overview with mock data (#22628) * Updates service overview snapshots * Adds tests for ManagedTable and ImpactBar * Refactored transaction overview to use new managed table component * Removed jsconfig file in apm * [APM] Distributed tracing - Trace details (waterfall) (#22763) * [APM] Add typescript to waterfall (#23635) * [APM] Migrate get_trace and constants to Typescript (#23634) * [APM] Add types for setup_request (#23762) * [APM] Adds trace overview queries and some refactoring (#23605) * ImpactBar component to align EuiProgress usage for impact bars * Sharing some logic between transaction and trace queries * Typescript support * Quick fix ‘banana’ * [APM] Ensure backwards compatibility for v1 and v2 (#23636) * Make interfaces versioned * Rename eventType to docType * Fixes trace links on traces overview (#24089) * [APM] use react-redux-request (#24117) * Updated yarn lockfile for new yarn version * Updated dependency issues for react-router-dom types * [APM] Display transaction info on span flyout (#24189) * [APM] Display transaction info on span flyout * Brings in real location and url param data for transaction flyout * Converts flyout to TS * Adds query param state for flyouts with ts support * Updates styles and uses EuiTabs for transaction flyout * [APM] Transaction flyout * [APM] Minor docs cleanup (#24325) * [APM] Minor docs cleanup * [APM] Fix issues with v1 spans (#24332) * [APM] Add agent marks (#24361) * [APM] Typescript migration for the transaction endpoints (#24397) * [APM] DT transaction sample header (#24294) Transaction sample header completed * Fixes link target for traces overview to include trans/trace ids as query params * Converts Transaction index file to TS * Adds trace link to sample section * Refactors the trace link and applies it to two usages * Implements transaction sample action context menu * Calculates and implements duration percentage * Re-typed how transaction groups work * Fixes transaction flyout links and context menu * Removes unnecessary ms multiplication * Removes unused commented code * Finalizes infra links * Fixes some type shenanigans
This commit is contained in:
parent
1bcdeab4dd
commit
3151da280b
149 changed files with 5995 additions and 2880 deletions
13
package.json
13
package.json
|
@ -62,7 +62,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",
|
||||
|
@ -166,7 +167,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",
|
||||
|
@ -189,6 +190,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",
|
||||
|
@ -227,7 +229,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",
|
||||
|
@ -243,19 +247,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",
|
||||
|
|
|
@ -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",
|
||||
|
|
440
x-pack/plugins/apm/common/constants.test.ts
Normal file
440
x-pack/plugins/apm/common/constants.test.ts
Normal file
|
@ -0,0 +1,440 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { Span } from '../typings/Span';
|
||||
import { Transaction } from '../typings/Transaction';
|
||||
import {
|
||||
PROCESSOR_EVENT,
|
||||
PROCESSOR_NAME,
|
||||
REQUEST_URL_FULL,
|
||||
SERVICE_AGENT_NAME,
|
||||
SERVICE_LANGUAGE_NAME,
|
||||
SERVICE_NAME,
|
||||
SPAN_DURATION,
|
||||
SPAN_HEX_ID,
|
||||
SPAN_ID,
|
||||
SPAN_NAME,
|
||||
SPAN_SQL,
|
||||
SPAN_START,
|
||||
SPAN_TYPE,
|
||||
TRANSACTION_DURATION,
|
||||
TRANSACTION_ID,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_RESULT,
|
||||
TRANSACTION_SAMPLED,
|
||||
TRANSACTION_TYPE,
|
||||
USER_ID
|
||||
} from './constants';
|
||||
|
||||
describe('Transaction v1', () => {
|
||||
const transaction: Transaction = {
|
||||
version: 'v1',
|
||||
'@timestamp': new Date().toString(),
|
||||
beat: {
|
||||
hostname: 'beat hostname',
|
||||
name: 'beat name',
|
||||
version: 'beat version'
|
||||
},
|
||||
host: {
|
||||
name: 'string;'
|
||||
},
|
||||
processor: {
|
||||
name: 'transaction',
|
||||
event: 'transaction'
|
||||
},
|
||||
context: {
|
||||
system: {
|
||||
architecture: 'x86',
|
||||
hostname: 'some-host',
|
||||
ip: '111.0.2.3',
|
||||
platform: 'linux'
|
||||
},
|
||||
service: {
|
||||
name: 'service name',
|
||||
agent: {
|
||||
name: 'agent name',
|
||||
version: 'v1337'
|
||||
},
|
||||
language: {
|
||||
name: 'nodejs',
|
||||
version: 'v1337'
|
||||
}
|
||||
},
|
||||
user: {
|
||||
id: '1337'
|
||||
},
|
||||
request: {
|
||||
url: {
|
||||
full: 'http://www.elastic.co'
|
||||
}
|
||||
}
|
||||
},
|
||||
transaction: {
|
||||
duration: {
|
||||
us: 1337
|
||||
},
|
||||
id: 'transaction id',
|
||||
name: 'transaction name',
|
||||
result: 'transaction result',
|
||||
sampled: true,
|
||||
type: 'transaction type'
|
||||
}
|
||||
};
|
||||
|
||||
// service
|
||||
it('SERVICE_NAME', () => {
|
||||
expect(get(transaction, SERVICE_NAME)).toBe('service name');
|
||||
});
|
||||
|
||||
it('SERVICE_AGENT_NAME', () => {
|
||||
expect(get(transaction, SERVICE_AGENT_NAME)).toBe('agent name');
|
||||
});
|
||||
|
||||
it('SERVICE_LANGUAGE_NAME', () => {
|
||||
expect(get(transaction, SERVICE_LANGUAGE_NAME)).toBe('nodejs');
|
||||
});
|
||||
|
||||
it('REQUEST_URL_FULL', () => {
|
||||
expect(get(transaction, REQUEST_URL_FULL)).toBe('http://www.elastic.co');
|
||||
});
|
||||
|
||||
it('USER_ID', () => {
|
||||
expect(get(transaction, USER_ID)).toBe('1337');
|
||||
});
|
||||
|
||||
// processor
|
||||
it('PROCESSOR_NAME', () => {
|
||||
expect(get(transaction, PROCESSOR_NAME)).toBe('transaction');
|
||||
});
|
||||
|
||||
it('PROCESSOR_EVENT', () => {
|
||||
expect(get(transaction, PROCESSOR_EVENT)).toBe('transaction');
|
||||
});
|
||||
|
||||
// transaction
|
||||
it('TRANSACTION_DURATION', () => {
|
||||
expect(get(transaction, TRANSACTION_DURATION)).toBe(1337);
|
||||
});
|
||||
|
||||
it('TRANSACTION_TYPE', () => {
|
||||
expect(get(transaction, TRANSACTION_TYPE)).toBe('transaction type');
|
||||
});
|
||||
|
||||
it('TRANSACTION_RESULT', () => {
|
||||
expect(get(transaction, TRANSACTION_RESULT)).toBe('transaction result');
|
||||
});
|
||||
|
||||
it('TRANSACTION_NAME', () => {
|
||||
expect(get(transaction, TRANSACTION_NAME)).toBe('transaction name');
|
||||
});
|
||||
|
||||
it('TRANSACTION_ID', () => {
|
||||
expect(get(transaction, TRANSACTION_ID)).toBe('transaction id');
|
||||
});
|
||||
|
||||
it('TRANSACTION_SAMPLED', () => {
|
||||
expect(get(transaction, TRANSACTION_SAMPLED)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Transaction v2', () => {
|
||||
const transaction: Transaction = {
|
||||
version: 'v2',
|
||||
'@timestamp': new Date().toString(),
|
||||
beat: {
|
||||
hostname: 'beat hostname',
|
||||
name: 'beat name',
|
||||
version: 'beat version'
|
||||
},
|
||||
host: { name: 'string;' },
|
||||
processor: { name: 'transaction', event: 'transaction' },
|
||||
timestamp: { us: 1337 },
|
||||
trace: { id: 'trace id' },
|
||||
context: {
|
||||
system: {
|
||||
architecture: 'x86',
|
||||
hostname: 'some-host',
|
||||
ip: '111.0.2.3',
|
||||
platform: 'linux'
|
||||
},
|
||||
service: {
|
||||
name: 'service name',
|
||||
agent: { name: 'agent name', version: 'v1337' },
|
||||
language: { name: 'nodejs', version: 'v1337' }
|
||||
},
|
||||
user: { id: '1337' },
|
||||
request: { url: { full: 'http://www.elastic.co' } }
|
||||
},
|
||||
transaction: {
|
||||
duration: { us: 1337 },
|
||||
id: 'transaction id',
|
||||
name: 'transaction name',
|
||||
result: 'transaction result',
|
||||
sampled: true,
|
||||
type: 'transaction type'
|
||||
}
|
||||
};
|
||||
|
||||
// service
|
||||
it('SERVICE_NAME', () => {
|
||||
expect(get(transaction, SERVICE_NAME)).toBe('service name');
|
||||
});
|
||||
|
||||
it('SERVICE_AGENT_NAME', () => {
|
||||
expect(get(transaction, SERVICE_AGENT_NAME)).toBe('agent name');
|
||||
});
|
||||
|
||||
it('SERVICE_LANGUAGE_NAME', () => {
|
||||
expect(get(transaction, SERVICE_LANGUAGE_NAME)).toBe('nodejs');
|
||||
});
|
||||
|
||||
it('REQUEST_URL_FULL', () => {
|
||||
expect(get(transaction, REQUEST_URL_FULL)).toBe('http://www.elastic.co');
|
||||
});
|
||||
|
||||
it('USER_ID', () => {
|
||||
expect(get(transaction, USER_ID)).toBe('1337');
|
||||
});
|
||||
|
||||
// processor
|
||||
it('PROCESSOR_NAME', () => {
|
||||
expect(get(transaction, PROCESSOR_NAME)).toBe('transaction');
|
||||
});
|
||||
|
||||
it('PROCESSOR_EVENT', () => {
|
||||
expect(get(transaction, PROCESSOR_EVENT)).toBe('transaction');
|
||||
});
|
||||
|
||||
// transaction
|
||||
it('TRANSACTION_DURATION', () => {
|
||||
expect(get(transaction, TRANSACTION_DURATION)).toBe(1337);
|
||||
});
|
||||
|
||||
it('TRANSACTION_TYPE', () => {
|
||||
expect(get(transaction, TRANSACTION_TYPE)).toBe('transaction type');
|
||||
});
|
||||
|
||||
it('TRANSACTION_RESULT', () => {
|
||||
expect(get(transaction, TRANSACTION_RESULT)).toBe('transaction result');
|
||||
});
|
||||
|
||||
it('TRANSACTION_NAME', () => {
|
||||
expect(get(transaction, TRANSACTION_NAME)).toBe('transaction name');
|
||||
});
|
||||
|
||||
it('TRANSACTION_ID', () => {
|
||||
expect(get(transaction, TRANSACTION_ID)).toBe('transaction id');
|
||||
});
|
||||
|
||||
it('TRANSACTION_SAMPLED', () => {
|
||||
expect(get(transaction, TRANSACTION_SAMPLED)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Span v1', () => {
|
||||
const span: Span = {
|
||||
version: 'v1',
|
||||
'@timestamp': new Date().toString(),
|
||||
beat: {
|
||||
hostname: 'beat hostname',
|
||||
name: 'beat name',
|
||||
version: 'beat version'
|
||||
},
|
||||
host: {
|
||||
name: 'string;'
|
||||
},
|
||||
processor: {
|
||||
name: 'transaction',
|
||||
event: 'span'
|
||||
},
|
||||
context: {
|
||||
db: {
|
||||
statement: 'db statement'
|
||||
},
|
||||
service: {
|
||||
name: 'service name',
|
||||
agent: {
|
||||
name: 'agent name',
|
||||
version: 'v1337'
|
||||
},
|
||||
language: {
|
||||
name: 'nodejs',
|
||||
version: 'v1337'
|
||||
}
|
||||
}
|
||||
},
|
||||
span: {
|
||||
duration: {
|
||||
us: 1337
|
||||
},
|
||||
start: {
|
||||
us: 1337
|
||||
},
|
||||
name: 'span name',
|
||||
type: 'span type',
|
||||
id: 1337
|
||||
},
|
||||
transaction: {
|
||||
id: 'transaction id'
|
||||
}
|
||||
};
|
||||
|
||||
// service
|
||||
it('SERVICE_NAME', () => {
|
||||
expect(get(span, SERVICE_NAME)).toBe('service name');
|
||||
});
|
||||
|
||||
it('SERVICE_AGENT_NAME', () => {
|
||||
expect(get(span, SERVICE_AGENT_NAME)).toBe('agent name');
|
||||
});
|
||||
|
||||
it('SERVICE_LANGUAGE_NAME', () => {
|
||||
expect(get(span, SERVICE_LANGUAGE_NAME)).toBe('nodejs');
|
||||
});
|
||||
|
||||
// processor
|
||||
it('PROCESSOR_NAME', () => {
|
||||
expect(get(span, PROCESSOR_NAME)).toBe('transaction');
|
||||
});
|
||||
|
||||
it('PROCESSOR_EVENT', () => {
|
||||
expect(get(span, PROCESSOR_EVENT)).toBe('span');
|
||||
});
|
||||
|
||||
// span
|
||||
it('SPAN_START', () => {
|
||||
expect(get(span, SPAN_START)).toBe(1337);
|
||||
});
|
||||
|
||||
it('SPAN_DURATION', () => {
|
||||
expect(get(span, SPAN_DURATION)).toBe(1337);
|
||||
});
|
||||
|
||||
it('SPAN_TYPE', () => {
|
||||
expect(get(span, SPAN_TYPE)).toBe('span type');
|
||||
});
|
||||
|
||||
it('SPAN_NAME', () => {
|
||||
expect(get(span, SPAN_NAME)).toBe('span name');
|
||||
});
|
||||
|
||||
it('SPAN_ID', () => {
|
||||
expect(get(span, SPAN_ID)).toBe(1337);
|
||||
});
|
||||
|
||||
it('SPAN_SQL', () => {
|
||||
expect(get(span, SPAN_SQL)).toBe('db statement');
|
||||
});
|
||||
|
||||
it('SPAN_HEX_ID', () => {
|
||||
expect(get(span, SPAN_HEX_ID)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Span v2', () => {
|
||||
const span: Span = {
|
||||
version: 'v2',
|
||||
'@timestamp': new Date().toString(),
|
||||
beat: {
|
||||
hostname: 'beat hostname',
|
||||
name: 'beat name',
|
||||
version: 'beat version'
|
||||
},
|
||||
host: {
|
||||
name: 'string;'
|
||||
},
|
||||
processor: {
|
||||
name: 'transaction',
|
||||
event: 'span'
|
||||
},
|
||||
timestamp: {
|
||||
us: 1337
|
||||
},
|
||||
trace: {
|
||||
id: 'trace id'
|
||||
},
|
||||
context: {
|
||||
db: {
|
||||
statement: 'db statement'
|
||||
},
|
||||
service: {
|
||||
name: 'service name',
|
||||
agent: {
|
||||
name: 'agent name',
|
||||
version: 'v1337'
|
||||
},
|
||||
language: {
|
||||
name: 'nodejs',
|
||||
version: 'v1337'
|
||||
}
|
||||
}
|
||||
},
|
||||
span: {
|
||||
duration: {
|
||||
us: 1337
|
||||
},
|
||||
name: 'span name',
|
||||
type: 'span type',
|
||||
id: 1337,
|
||||
hex_id: 'hex id'
|
||||
},
|
||||
transaction: {
|
||||
id: 'transaction id'
|
||||
}
|
||||
};
|
||||
|
||||
// service
|
||||
it('SERVICE_NAME', () => {
|
||||
expect(get(span, SERVICE_NAME)).toBe('service name');
|
||||
});
|
||||
|
||||
it('SERVICE_AGENT_NAME', () => {
|
||||
expect(get(span, SERVICE_AGENT_NAME)).toBe('agent name');
|
||||
});
|
||||
|
||||
it('SERVICE_LANGUAGE_NAME', () => {
|
||||
expect(get(span, SERVICE_LANGUAGE_NAME)).toBe('nodejs');
|
||||
});
|
||||
|
||||
// processor
|
||||
it('PROCESSOR_NAME', () => {
|
||||
expect(get(span, PROCESSOR_NAME)).toBe('transaction');
|
||||
});
|
||||
|
||||
it('PROCESSOR_EVENT', () => {
|
||||
expect(get(span, PROCESSOR_EVENT)).toBe('span');
|
||||
});
|
||||
|
||||
// span
|
||||
it('SPAN_START', () => {
|
||||
expect(get(span, SPAN_START)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('SPAN_DURATION', () => {
|
||||
expect(get(span, SPAN_DURATION)).toBe(1337);
|
||||
});
|
||||
|
||||
it('SPAN_TYPE', () => {
|
||||
expect(get(span, SPAN_TYPE)).toBe('span type');
|
||||
});
|
||||
|
||||
it('SPAN_NAME', () => {
|
||||
expect(get(span, SPAN_NAME)).toBe('span name');
|
||||
});
|
||||
|
||||
it('SPAN_ID', () => {
|
||||
expect(get(span, SPAN_ID)).toBe(1337);
|
||||
});
|
||||
|
||||
it('SPAN_SQL', () => {
|
||||
expect(get(span, SPAN_SQL)).toBe('db statement');
|
||||
});
|
||||
|
||||
it('SPAN_HEX_ID', () => {
|
||||
expect(get(span, SPAN_HEX_ID)).toBe('hex id');
|
||||
});
|
||||
});
|
|
@ -7,6 +7,8 @@
|
|||
export const SERVICE_NAME = 'context.service.name';
|
||||
export const SERVICE_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';
|
|
@ -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);
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"exclude": ["node_modules", "**/node_modules/*", "build"]
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -31,7 +31,7 @@ import {
|
|||
EuiLink
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { ELASTIC_DOCS } from '../../../../utils/documentation';
|
||||
import { XPACK_DOCS } from '../../../../utils/documentation/xpack';
|
||||
|
||||
import { KibanaLink } from '../../../../utils/url';
|
||||
import { createErrorGroupWatch } from './createErrorGroupWatch';
|
||||
|
@ -226,10 +226,7 @@ export default class WatcherFlyout extends Component {
|
|||
This form will assist in creating a Watch that can notify you of error
|
||||
occurrences from this service. To learn more about Watcher, please
|
||||
read our{' '}
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
href={_.get(ELASTIC_DOCS, 'watcher-get-started.url')}
|
||||
>
|
||||
<EuiLink target="_blank" href={XPACK_DOCS.xpackWatcher}>
|
||||
documentation
|
||||
</EuiLink>
|
||||
.
|
||||
|
@ -344,10 +341,7 @@ export default class WatcherFlyout extends Component {
|
|||
helpText={
|
||||
<span>
|
||||
If you have not configured email, please see the{' '}
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
href={_.get(ELASTIC_DOCS, 'x-pack-emails.url')}
|
||||
>
|
||||
<EuiLink target="_blank" href={XPACK_DOCS.xpackEmails}>
|
||||
documentation
|
||||
</EuiLink>
|
||||
.
|
||||
|
|
40
x-pack/plugins/apm/public/components/app/Main/Home.tsx
Normal file
40
x-pack/plugins/apm/public/components/app/Main/Home.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import { EuiTabbedContent } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
// @ts-ignore
|
||||
import { KueryBar } from '../../shared/KueryBar';
|
||||
import { SetupInstructionsLink } from '../../shared/SetupInstructionsLink';
|
||||
// @ts-ignore
|
||||
import { HeaderContainer } from '../../shared/UIComponents';
|
||||
// @ts-ignore
|
||||
import { ServiceOverview } from '../ServiceOverview';
|
||||
import { TraceOverview } from '../TraceOverview';
|
||||
|
||||
export function Home() {
|
||||
return (
|
||||
<div>
|
||||
<HeaderContainer>
|
||||
<h1>APM</h1>
|
||||
<SetupInstructionsLink />
|
||||
</HeaderContainer>
|
||||
<KueryBar />
|
||||
<EuiTabbedContent
|
||||
className="k6Tab--large"
|
||||
tabs={[
|
||||
{
|
||||
id: 'services_overview',
|
||||
name: 'Services',
|
||||
content: <ServiceOverview />
|
||||
},
|
||||
{ id: 'traces_overview', name: 'Traces', content: <TraceOverview /> }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { Home } from '../Home';
|
||||
|
||||
describe('Home component', () => {
|
||||
it('should render', () => {
|
||||
expect(shallow(<Home />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Home component should render 1`] = `
|
||||
<div>
|
||||
<styled.div>
|
||||
<h1>
|
||||
APM
|
||||
</h1>
|
||||
<SetupInstructionsLink />
|
||||
</styled.div>
|
||||
<Connect(KueryBarView) />
|
||||
<EuiTabbedContent
|
||||
className="k6Tab--large"
|
||||
tabs={
|
||||
Array [
|
||||
Object {
|
||||
"content": <Connect(ServiceOverview) />,
|
||||
"id": "services_overview",
|
||||
"name": "Services",
|
||||
},
|
||||
Object {
|
||||
"content": <Connect(TraceOverview) />,
|
||||
"id": "traces_overview",
|
||||
"name": "Traces",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -6,25 +6,40 @@
|
|||
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import ServiceOverview from '../ServiceOverview';
|
||||
import ErrorGroupDetails from '../ErrorGroupDetails';
|
||||
import ErrorGroupOverview from '../ErrorGroupOverview';
|
||||
import TransactionDetails from '../TransactionDetails';
|
||||
import TransactionOverview from '../TransactionOverview';
|
||||
|
||||
import { StringMap } from '../../../../typings/common';
|
||||
import { legacyDecodeURIComponent } from '../../../utils/url';
|
||||
// @ts-ignore
|
||||
import ErrorGroupDetails from '../ErrorGroupDetails';
|
||||
// @ts-ignore
|
||||
import ErrorGroupOverview from '../ErrorGroupOverview';
|
||||
import { TransactionDetails } from '../TransactionDetails';
|
||||
// @ts-ignore
|
||||
import TransactionOverview from '../TransactionOverview';
|
||||
import { Home } from './Home';
|
||||
|
||||
interface BreadcrumbArgs {
|
||||
match: {
|
||||
params: StringMap<any>;
|
||||
};
|
||||
}
|
||||
|
||||
interface RenderArgs {
|
||||
location: StringMap<any>;
|
||||
}
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
exact: true,
|
||||
path: '/',
|
||||
component: ServiceOverview,
|
||||
component: Home,
|
||||
breadcrumb: 'APM'
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/:serviceName/errors/:groupId',
|
||||
component: ErrorGroupDetails,
|
||||
breadcrumb: ({ match }) => match.params.groupId
|
||||
breadcrumb: ({ match }: BreadcrumbArgs) => match.params.groupId
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
|
@ -44,8 +59,8 @@ export const routes = [
|
|||
{
|
||||
exact: true,
|
||||
path: '/:serviceName',
|
||||
breadcrumb: ({ match }) => match.params.serviceName,
|
||||
render: ({ location }) => {
|
||||
breadcrumb: ({ match }: BreadcrumbArgs) => match.params.serviceName,
|
||||
render: ({ location }: RenderArgs) => {
|
||||
return (
|
||||
<Redirect
|
||||
to={{
|
||||
|
@ -68,14 +83,14 @@ export const routes = [
|
|||
exact: true,
|
||||
path: '/:serviceName/transactions/:transactionType',
|
||||
component: TransactionOverview,
|
||||
breadcrumb: ({ match }) =>
|
||||
breadcrumb: ({ match }: BreadcrumbArgs) =>
|
||||
legacyDecodeURIComponent(match.params.transactionType)
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/:serviceName/transactions/:transactionType/:transactionName',
|
||||
component: TransactionDetails,
|
||||
breadcrumb: ({ match }) =>
|
||||
breadcrumb: ({ match }: BreadcrumbArgs) =>
|
||||
legacyDecodeURIComponent(match.params.transactionName)
|
||||
}
|
||||
];
|
|
@ -1,143 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import orderBy from 'lodash.orderby';
|
||||
import styled from 'styled-components';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { EuiBasicTable } from '@elastic/eui';
|
||||
import { RelativeLink } from '../../../../utils/url';
|
||||
import { fontSizes, truncate } from '../../../../style/variables';
|
||||
import TooltipOverlay from '../../../shared/TooltipOverlay';
|
||||
import { asMillisWithDefault } from '../../../../utils/formatters';
|
||||
|
||||
function formatNumber(value) {
|
||||
if (value === 0) {
|
||||
return '0';
|
||||
}
|
||||
const formatted = numeral(value).format('0.0');
|
||||
return formatted <= 0.1 ? '< 0.1' : formatted;
|
||||
}
|
||||
|
||||
// TODO: duplicated
|
||||
function paginateItems({ items, pageIndex, pageSize }) {
|
||||
return items.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
|
||||
}
|
||||
|
||||
function formatString(value) {
|
||||
return value || 'N/A';
|
||||
}
|
||||
|
||||
const AppLink = styled(RelativeLink)`
|
||||
font-size: ${fontSizes.large};
|
||||
${truncate('100%')};
|
||||
`;
|
||||
|
||||
class List extends Component {
|
||||
state = {
|
||||
page: {
|
||||
index: 0,
|
||||
size: 10
|
||||
},
|
||||
sort: {
|
||||
field: 'serviceName',
|
||||
direction: 'asc'
|
||||
}
|
||||
};
|
||||
|
||||
onTableChange = ({ page = {}, sort = {} }) => {
|
||||
this.setState({ page, sort });
|
||||
};
|
||||
|
||||
render() {
|
||||
const columns = [
|
||||
{
|
||||
field: 'serviceName',
|
||||
name: 'Name',
|
||||
width: '50%',
|
||||
sortable: true,
|
||||
render: serviceName => (
|
||||
<TooltipOverlay content={formatString(serviceName)}>
|
||||
<AppLink path={`/${serviceName}/transactions`}>
|
||||
{formatString(serviceName)}
|
||||
</AppLink>
|
||||
</TooltipOverlay>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'agentName',
|
||||
name: 'Agent',
|
||||
sortable: true,
|
||||
render: agentName => formatString(agentName)
|
||||
},
|
||||
{
|
||||
field: 'avgResponseTime',
|
||||
name: 'Avg. response time',
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => asMillisWithDefault(value)
|
||||
},
|
||||
{
|
||||
field: 'transactionsPerMinute',
|
||||
name: 'Trans. per minute',
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => `${formatNumber(value)} tpm`
|
||||
},
|
||||
{
|
||||
field: 'errorsPerMinute',
|
||||
name: 'Errors per minute',
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => `${formatNumber(value)} err.`
|
||||
}
|
||||
];
|
||||
|
||||
const sortedItems = orderBy(
|
||||
this.props.items,
|
||||
this.state.sort.field,
|
||||
this.state.sort.direction
|
||||
);
|
||||
|
||||
const paginatedItems = paginateItems({
|
||||
items: sortedItems,
|
||||
pageIndex: this.state.page.index,
|
||||
pageSize: this.state.page.size
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
noItemsMessage={this.props.noItemsMessage}
|
||||
items={paginatedItems}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
pageIndex: this.state.page.index,
|
||||
pageSize: this.state.page.size,
|
||||
totalItemCount: this.props.items.length
|
||||
}}
|
||||
sorting={{
|
||||
sort: {
|
||||
field: this.state.sort.field,
|
||||
direction: this.state.sort.direction
|
||||
}
|
||||
}}
|
||||
onChange={this.onTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List.propTypes = {
|
||||
noItemsMessage: PropTypes.node,
|
||||
items: PropTypes.array
|
||||
};
|
||||
|
||||
List.defaultProps = {
|
||||
items: []
|
||||
};
|
||||
|
||||
export default List;
|
|
@ -8,7 +8,7 @@ import React from 'react';
|
|||
import { mount } from 'enzyme';
|
||||
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import List from '../index';
|
||||
import { ServiceList } from '../index';
|
||||
import props from './props.json';
|
||||
import {
|
||||
mountWithRouterAndStore,
|
||||
|
@ -25,7 +25,7 @@ describe('ErrorGroupOverview -> List', () => {
|
|||
const storeState = {};
|
||||
const wrapper = mount(
|
||||
<MemoryRouter>
|
||||
<List items={[]} />
|
||||
<ServiceList items={[]} />
|
||||
</MemoryRouter>,
|
||||
storeState
|
||||
);
|
||||
|
@ -36,7 +36,7 @@ describe('ErrorGroupOverview -> List', () => {
|
|||
it('should render with data', () => {
|
||||
const storeState = { location: {} };
|
||||
const wrapper = mountWithRouterAndStore(
|
||||
<List items={props.items} />,
|
||||
<ServiceList items={props.items} />,
|
||||
storeState
|
||||
);
|
||||
|
|
@ -254,60 +254,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = `
|
|||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div
|
||||
className="euiPopover euiPopover--anchorUpRight euiPopover--withTitle"
|
||||
id="customizablePagination"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<div
|
||||
className="euiPopover__anchor"
|
||||
>
|
||||
<button
|
||||
className="euiButtonEmpty euiButtonEmpty--text euiButtonEmpty--xSmall euiButtonEmpty--iconRight"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="euiButtonEmpty__content"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="euiIcon euiIcon--medium euiButtonEmpty__icon"
|
||||
focusable="false"
|
||||
height="16"
|
||||
style={
|
||||
Object {
|
||||
"fill": undefined,
|
||||
}
|
||||
}
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<defs>
|
||||
<path
|
||||
d="M13.069 5.157L8.384 9.768a.546.546 0 0 1-.768 0L2.93 5.158a.552.552 0 0 0-.771 0 .53.53 0 0 0 0 .759l4.684 4.61c.641.631 1.672.63 2.312 0l4.684-4.61a.53.53 0 0 0 0-.76.552.552 0 0 0-.771 0z"
|
||||
id="arrow_down-a"
|
||||
/>
|
||||
</defs>
|
||||
<use
|
||||
fillRule="nonzero"
|
||||
xlinkHref="#arrow_down-a"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="euiButtonEmpty__text"
|
||||
>
|
||||
Rows per page: 10
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -690,60 +637,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
|
|||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div
|
||||
className="euiPopover euiPopover--anchorUpRight euiPopover--withTitle"
|
||||
id="customizablePagination"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<div
|
||||
className="euiPopover__anchor"
|
||||
>
|
||||
<button
|
||||
className="euiButtonEmpty euiButtonEmpty--text euiButtonEmpty--xSmall euiButtonEmpty--iconRight"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="euiButtonEmpty__content"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="euiIcon euiIcon--medium euiButtonEmpty__icon"
|
||||
focusable="false"
|
||||
height="16"
|
||||
style={
|
||||
Object {
|
||||
"fill": undefined,
|
||||
}
|
||||
}
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<defs>
|
||||
<path
|
||||
d="M13.069 5.157L8.384 9.768a.546.546 0 0 1-.768 0L2.93 5.158a.552.552 0 0 0-.771 0 .53.53 0 0 0 0 .759l4.684 4.61c.641.631 1.672.63 2.312 0l4.684-4.61a.53.53 0 0 0 0-.76.552.552 0 0 0-.771 0z"
|
||||
id="arrow_down-a"
|
||||
/>
|
||||
</defs>
|
||||
<use
|
||||
fillRule="nonzero"
|
||||
xlinkHref="#arrow_down-a"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="euiButtonEmpty__text"
|
||||
>
|
||||
Rows per page: 10
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { RelativeLink } from '../../../../utils/url';
|
||||
import { fontSizes, truncate } from '../../../../style/variables';
|
||||
import TooltipOverlay from '../../../shared/TooltipOverlay';
|
||||
import { asMillisWithDefault } from '../../../../utils/formatters';
|
||||
import { ManagedTable } from '../../../shared/ManagedTable';
|
||||
|
||||
// TODO: Consolidate these formatting helpers centrally
|
||||
function formatNumber(value) {
|
||||
if (value === 0) {
|
||||
return '0';
|
||||
}
|
||||
const formatted = numeral(value).format('0.0');
|
||||
return formatted <= 0.1 ? '< 0.1' : formatted;
|
||||
}
|
||||
|
||||
function formatString(value) {
|
||||
return value || 'N/A';
|
||||
}
|
||||
|
||||
const AppLink = styled(RelativeLink)`
|
||||
font-size: ${fontSizes.large};
|
||||
${truncate('100%')};
|
||||
`;
|
||||
|
||||
const SERVICE_COLUMNS = [
|
||||
{
|
||||
field: 'serviceName',
|
||||
name: 'Name',
|
||||
width: '50%',
|
||||
sortable: true,
|
||||
render: serviceName => (
|
||||
<TooltipOverlay content={formatString(serviceName)}>
|
||||
<AppLink path={`/${serviceName}/transactions`}>
|
||||
{formatString(serviceName)}
|
||||
</AppLink>
|
||||
</TooltipOverlay>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'agentName',
|
||||
name: 'Agent',
|
||||
sortable: true,
|
||||
render: agentName => formatString(agentName)
|
||||
},
|
||||
{
|
||||
field: 'avgResponseTime',
|
||||
name: 'Avg. response time',
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => asMillisWithDefault(value)
|
||||
},
|
||||
{
|
||||
field: 'transactionsPerMinute',
|
||||
name: 'Trans. per minute',
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => `${formatNumber(value)} tpm`
|
||||
},
|
||||
{
|
||||
field: 'errorsPerMinute',
|
||||
name: 'Errors per minute',
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => `${formatNumber(value)} err.`
|
||||
}
|
||||
];
|
||||
|
||||
export function ServiceList({ items, noItemsMessage }) {
|
||||
return (
|
||||
<ManagedTable
|
||||
columns={SERVICE_COLUMNS}
|
||||
items={items}
|
||||
noItemsMessage={noItemsMessage}
|
||||
initialSort={{ field: 'serviceName', direction: 'asc' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ServiceList.propTypes = {
|
||||
noItemsMessage: PropTypes.node,
|
||||
items: PropTypes.array
|
||||
};
|
||||
|
||||
ServiceList.defaultProps = {
|
||||
items: []
|
||||
};
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import ServiceOverview from '../view';
|
||||
import { ServiceOverview } from '../view';
|
||||
import { STATUS } from '../../../../constants';
|
||||
import * as apmRestServices from '../../../../services/rest/apm';
|
||||
|
||||
|
|
|
@ -2,13 +2,9 @@
|
|||
|
||||
exports[`Service Overview -> View should render when historical data is found 1`] = `
|
||||
<div>
|
||||
<styled.div>
|
||||
<h1>
|
||||
Services
|
||||
</h1>
|
||||
<SetupInstructionsLink />
|
||||
</styled.div>
|
||||
<Connect(KueryBarView) />
|
||||
<EuiSpacer
|
||||
size="l"
|
||||
/>
|
||||
<ServiceListRequest
|
||||
render={[Function]}
|
||||
/>
|
||||
|
@ -20,7 +16,6 @@ Object {
|
|||
"items": Array [],
|
||||
"noItemsMessage": <EmptyMessage
|
||||
heading="No services were found"
|
||||
hideSubheading={false}
|
||||
subheading={null}
|
||||
/>,
|
||||
}
|
||||
|
@ -28,13 +23,9 @@ Object {
|
|||
|
||||
exports[`Service Overview -> View should render when historical data is not found 1`] = `
|
||||
<div>
|
||||
<styled.div>
|
||||
<h1>
|
||||
Services
|
||||
</h1>
|
||||
<SetupInstructionsLink />
|
||||
</styled.div>
|
||||
<Connect(KueryBarView) />
|
||||
<EuiSpacer
|
||||
size="l"
|
||||
/>
|
||||
<ServiceListRequest
|
||||
render={[Function]}
|
||||
/>
|
||||
|
@ -46,7 +37,6 @@ Object {
|
|||
"items": Array [],
|
||||
"noItemsMessage": <EmptyMessage
|
||||
heading="Looks like you don't have any services with APM installed. Let's add some!"
|
||||
hideSubheading={false}
|
||||
subheading={
|
||||
<SetupInstructionsLink
|
||||
buttonFill={true}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import ServiceOverview from './view';
|
||||
import { ServiceOverview as View } from './view';
|
||||
import { getServiceList } from '../../../store/reactReduxRequest/serviceList';
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
|
||||
|
@ -17,7 +17,7 @@ function mapStateToProps(state = {}) {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {};
|
||||
export default connect(
|
||||
export const ServiceOverview = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ServiceOverview);
|
||||
)(View);
|
||||
|
|
|
@ -8,16 +8,13 @@ import React, { Component } from 'react';
|
|||
import { STATUS } from '../../../constants';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { loadAgentStatus } from '../../../services/rest/apm';
|
||||
import { KibanaLink } from '../../../utils/url';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import List from './List';
|
||||
import { HeaderContainer } from '../../shared/UIComponents';
|
||||
import { KueryBar } from '../../shared/KueryBar';
|
||||
|
||||
import { ServiceList } from './ServiceList';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { ServiceListRequest } from '../../../store/reactReduxRequest/serviceList';
|
||||
import EmptyMessage from '../../shared/EmptyMessage';
|
||||
import { SetupInstructionsLink } from '../../shared/SetupInstructionsLink';
|
||||
|
||||
class ServiceOverview extends Component {
|
||||
export class ServiceOverview extends Component {
|
||||
state = {
|
||||
historicalDataFound: true
|
||||
};
|
||||
|
@ -59,32 +56,14 @@ class ServiceOverview extends Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<HeaderContainer>
|
||||
<h1>Services</h1>
|
||||
<SetupInstructionsLink />
|
||||
</HeaderContainer>
|
||||
|
||||
<KueryBar />
|
||||
|
||||
<EuiSpacer />
|
||||
<ServiceListRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }) => (
|
||||
<List items={data} noItemsMessage={noItemsMessage} />
|
||||
<ServiceList items={data} noItemsMessage={noItemsMessage} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function SetupInstructionsLink({ buttonFill = false }) {
|
||||
return (
|
||||
<KibanaLink pathname={'/app/kibana'} hash={'/home/tutorial/apm'}>
|
||||
<EuiButton size="s" color="primary" fill={buttonFill}>
|
||||
Setup Instructions
|
||||
</EuiButton>
|
||||
</KibanaLink>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServiceOverview;
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Transaction } from '../../../../typings/Transaction';
|
||||
import { ITransactionGroup } from '../../../../typings/TransactionGroup';
|
||||
import { fontSizes, truncate } from '../../../style/variables';
|
||||
// @ts-ignore
|
||||
import { asMillisWithDefault } from '../../../utils/formatters';
|
||||
import { ImpactBar } from '../../shared/ImpactBar';
|
||||
import { ITableColumn, ManagedTable } from '../../shared/ManagedTable';
|
||||
// @ts-ignore
|
||||
import TooltipOverlay from '../../shared/TooltipOverlay';
|
||||
import { TraceLink } from '../../shared/TraceLink';
|
||||
|
||||
function formatString(value: string) {
|
||||
return value || 'N/A';
|
||||
}
|
||||
|
||||
const StyledTraceLink = styled(TraceLink)`
|
||||
font-size: ${fontSizes.large};
|
||||
${truncate('100%')};
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
items: ITransactionGroup[];
|
||||
noItemsMessage: any;
|
||||
}
|
||||
|
||||
const traceListColumns: ITableColumn[] = [
|
||||
{
|
||||
field: 'sample',
|
||||
name: 'Name',
|
||||
width: '40%',
|
||||
sortable: true,
|
||||
render: (transaction: Transaction) => (
|
||||
<TooltipOverlay content={formatString(transaction.transaction.name)}>
|
||||
<StyledTraceLink transaction={transaction}>
|
||||
{formatString(transaction.transaction.name)}
|
||||
</StyledTraceLink>
|
||||
</TooltipOverlay>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'sample',
|
||||
name: 'Originating service',
|
||||
sortable: true,
|
||||
render: (transaction: Transaction) =>
|
||||
formatString(transaction.context.service.name)
|
||||
},
|
||||
{
|
||||
field: 'averageResponseTime',
|
||||
name: 'Avg. response time',
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: (value: number) => asMillisWithDefault(value)
|
||||
},
|
||||
{
|
||||
field: 'transactionsPerMinute',
|
||||
name: 'Traces per minute',
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: (value: number) => `${value.toLocaleString()} tpm`
|
||||
},
|
||||
{
|
||||
field: 'impact',
|
||||
name: 'Impact',
|
||||
width: '20%',
|
||||
align: 'right',
|
||||
sortable: true,
|
||||
render: (value: number) => <ImpactBar value={value} />
|
||||
}
|
||||
];
|
||||
|
||||
export function TraceList({ items = [], noItemsMessage, ...rest }: Props) {
|
||||
return (
|
||||
<ManagedTable
|
||||
columns={traceListColumns}
|
||||
items={items}
|
||||
initialSort={{ field: 'impact', direction: 'desc' }}
|
||||
noItemsMessage={noItemsMessage}
|
||||
initialPageSize={25}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -5,18 +5,15 @@
|
|||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import 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);
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { ITransactionGroup } from '../../../../typings/TransactionGroup';
|
||||
// @ts-ignore
|
||||
import { TraceListRequest } from '../../../store/reactReduxRequest/traceList';
|
||||
import EmptyMessage from '../../shared/EmptyMessage';
|
||||
import { TraceList } from './TraceList';
|
||||
|
||||
interface Props {
|
||||
urlParams: object;
|
||||
}
|
||||
|
||||
export function TraceOverview(props: Props) {
|
||||
const { urlParams } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiSpacer />
|
||||
<TraceListRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }: { data: ITransactionGroup[] }) => (
|
||||
<TraceList
|
||||
items={data}
|
||||
noItemsMessage={
|
||||
<EmptyMessage heading="No traces found for this query" />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
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' }
|
||||
}
|
||||
]);
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import Distribution from './view';
|
||||
import { getUrlParams } from '../../../../store/urlParams';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
urlParams: getUrlParams(state),
|
||||
location: state.location
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {};
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Distribution);
|
|
@ -4,44 +4,66 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import d3 from 'd3';
|
||||
import Histogram from '../../../shared/charts/Histogram';
|
||||
import { toQuery, fromQuery, history } from '../../../../utils/url';
|
||||
import { HeaderSmall } from '../../../shared/UIComponents';
|
||||
import EmptyMessage from '../../../shared/EmptyMessage';
|
||||
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 EmptyMessage from '../../../shared/EmptyMessage';
|
||||
// @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<Props> {
|
||||
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'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
@ -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;
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiPopover
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import {
|
||||
PROCESSOR_EVENT,
|
||||
TRACE_ID,
|
||||
TRANSACTION_ID
|
||||
} from 'x-pack/plugins/apm/common/constants';
|
||||
import { KibanaLink } from 'x-pack/plugins/apm/public/utils/url';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||
|
||||
function getDiscoverQuery(transactionId: string, traceId?: string) {
|
||||
let query = `${PROCESSOR_EVENT}:transaction AND ${TRANSACTION_ID}:${transactionId}`;
|
||||
if (traceId) {
|
||||
query += ` AND ${TRACE_ID}:${traceId}`;
|
||||
}
|
||||
return {
|
||||
_a: {
|
||||
interval: 'auto',
|
||||
query: {
|
||||
language: 'lucene',
|
||||
query
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getInfraMetricsQuery(transaction: Transaction) {
|
||||
const plus5 = new Date(transaction['@timestamp']);
|
||||
const minus5 = new Date(transaction['@timestamp']);
|
||||
|
||||
plus5.setMinutes(plus5.getMinutes() + 5);
|
||||
minus5.setMinutes(minus5.getMinutes() - 5);
|
||||
|
||||
return {
|
||||
from: minus5.getTime(),
|
||||
to: plus5.getTime()
|
||||
};
|
||||
}
|
||||
|
||||
function ActionMenuButton({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<EuiButton iconType="arrowDown" iconSide="right" onClick={onClick}>
|
||||
Actions
|
||||
</EuiButton>
|
||||
);
|
||||
}
|
||||
|
||||
interface ActionMenuProps {
|
||||
readonly transaction: Transaction;
|
||||
}
|
||||
|
||||
interface ActionMenuState {
|
||||
readonly isOpen: boolean;
|
||||
}
|
||||
|
||||
export class ActionMenu extends React.Component<
|
||||
ActionMenuProps,
|
||||
ActionMenuState
|
||||
> {
|
||||
public state = {
|
||||
isOpen: false
|
||||
};
|
||||
|
||||
public toggle = () => {
|
||||
this.setState(state => ({ isOpen: !state.isOpen }));
|
||||
};
|
||||
|
||||
public close = () => {
|
||||
this.setState({ isOpen: false });
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { transaction } = this.props;
|
||||
|
||||
const items = [
|
||||
<EuiContextMenuItem icon="discoverApp" key="discover-transaction">
|
||||
<KibanaLink
|
||||
pathname="/app/kibana"
|
||||
hash="/discover"
|
||||
query={getDiscoverQuery(
|
||||
transaction.transaction.id,
|
||||
transaction.version === 'v2' ? transaction.trace.id : undefined
|
||||
)}
|
||||
>
|
||||
View sample document
|
||||
</KibanaLink>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem icon="infraApp" key="infra-host-metrics">
|
||||
<KibanaLink
|
||||
pathname="/app/infra"
|
||||
hash={`/link-to/host-detail/${transaction.context.system.hostname}`}
|
||||
query={getInfraMetricsQuery(transaction)}
|
||||
>
|
||||
<span>View host metrics (beta)</span>
|
||||
</KibanaLink>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem icon="infraApp" key="infra-host-logs">
|
||||
<KibanaLink
|
||||
pathname="/app/infra"
|
||||
hash={`/link-to/host-logs/${transaction.context.system.hostname}`}
|
||||
query={{ time: new Date(transaction['@timestamp']).getTime() }}
|
||||
>
|
||||
<span>View host logs (beta)</span>
|
||||
</KibanaLink>
|
||||
</EuiContextMenuItem>
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="transactionActionMenu"
|
||||
button={<ActionMenuButton onClick={this.toggle} />}
|
||||
isOpen={this.state.isOpen}
|
||||
closePopover={this.close}
|
||||
anchorPosition="downRight"
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenuPanel items={items} title="Actions" />
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { get } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { toQuery, fromQuery, history } from '../../../../../utils/url';
|
||||
import SpanDetails from './SpanDetails';
|
||||
import Modal from '../../../../shared/Modal';
|
||||
|
||||
import {
|
||||
unit,
|
||||
units,
|
||||
colors,
|
||||
px,
|
||||
fontFamilyCode,
|
||||
fontSizes
|
||||
} from '../../../../../style/variables';
|
||||
import {
|
||||
SPAN_DURATION,
|
||||
SPAN_START,
|
||||
SPAN_ID,
|
||||
SPAN_NAME
|
||||
} from '../../../../../../common/constants';
|
||||
|
||||
const SpanBar = styled.div`
|
||||
position: relative;
|
||||
height: ${unit}px;
|
||||
`;
|
||||
const SpanLabel = styled.div`
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
margin: ${px(units.quarter)} 0 0;
|
||||
font-family: ${fontFamilyCode};
|
||||
font-size: ${fontSizes.small};
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
display: block;
|
||||
user-select: none;
|
||||
padding: ${px(units.half)} ${props => px(props.timelineMargins.right)}
|
||||
${px(units.eighth)} ${props => px(props.timelineMargins.left)};
|
||||
border-top: 1px solid ${colors.gray4};
|
||||
background-color: ${props => (props.isSelected ? colors.gray5 : 'initial')};
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: ${colors.gray5};
|
||||
}
|
||||
`;
|
||||
|
||||
function getLocationPath(location) {
|
||||
return location.href.split('?')[0];
|
||||
}
|
||||
|
||||
class Span extends React.Component {
|
||||
componentDidMount() {
|
||||
this.locationPath = getLocationPath(window.location);
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
// Hack: If the modal is open, and the user clicks the back button, the url changes, the modal will be destroyed,
|
||||
// and the onClose handler will fire, causing it to change the url again. We want to avoid the url changing the second time.
|
||||
// Therefore we
|
||||
const currentLocationPath = getLocationPath(window.location);
|
||||
const didNavigate = this.locationPath !== currentLocationPath;
|
||||
if (!didNavigate) {
|
||||
this.resetSpanId();
|
||||
}
|
||||
};
|
||||
|
||||
resetSpanId = () => {
|
||||
const { location } = this.props;
|
||||
const { spanId, ...currentQuery } = toQuery(location.search);
|
||||
|
||||
if (spanId === 'null') {
|
||||
return;
|
||||
}
|
||||
|
||||
history.replace({
|
||||
...location,
|
||||
search: fromQuery({
|
||||
...currentQuery,
|
||||
spanId: null
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
timelineMargins,
|
||||
totalDuration,
|
||||
span,
|
||||
spanTypeLabel,
|
||||
color,
|
||||
isSelected,
|
||||
transactionId,
|
||||
location
|
||||
} = this.props;
|
||||
|
||||
const width = (get({ span }, SPAN_DURATION) / totalDuration) * 100;
|
||||
const left = (get({ span }, SPAN_START) / totalDuration) * 100;
|
||||
|
||||
const spanId = get({ span }, SPAN_ID);
|
||||
const spanName = get({ span }, SPAN_NAME);
|
||||
|
||||
return (
|
||||
<Container
|
||||
onClick={() => {
|
||||
history.replace({
|
||||
...location,
|
||||
search: fromQuery({
|
||||
...toQuery(location.search),
|
||||
spanId
|
||||
})
|
||||
});
|
||||
}}
|
||||
timelineMargins={timelineMargins}
|
||||
isSelected={isSelected}
|
||||
>
|
||||
<SpanBar
|
||||
style={{
|
||||
left: `${left}%`,
|
||||
width: `${width}%`,
|
||||
backgroundColor: color
|
||||
}}
|
||||
/>
|
||||
<SpanLabel style={{ left: `${left}%`, width: `${100 - left}%` }}>
|
||||
‎
|
||||
{spanName}
|
||||
‎
|
||||
</SpanLabel>
|
||||
|
||||
<Modal
|
||||
header="Span details"
|
||||
isOpen={isSelected}
|
||||
onClose={this.onClose}
|
||||
close={this.onClose}
|
||||
>
|
||||
<SpanDetails
|
||||
span={span}
|
||||
spanTypeLabel={spanTypeLabel}
|
||||
spanTypeColor={color}
|
||||
totalDuration={totalDuration}
|
||||
transactionId={transactionId}
|
||||
/>
|
||||
</Modal>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Span.propTypes = {
|
||||
location: PropTypes.object.isRequired,
|
||||
totalDuration: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default Span;
|
|
@ -1,205 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { get } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import Stacktrace from '../../../../../shared/Stacktrace';
|
||||
import DiscoverButton from '../../../../../shared/DiscoverButton';
|
||||
import { asMillis } from '../../../../../../utils/formatters';
|
||||
import { Indicator } from '../../../../../shared/charts/Legend';
|
||||
import {
|
||||
SPAN_DURATION,
|
||||
SPAN_NAME,
|
||||
SERVICE_LANGUAGE_NAME
|
||||
} from '../../../../../../../common/constants';
|
||||
import {
|
||||
unit,
|
||||
units,
|
||||
px,
|
||||
colors,
|
||||
borderRadius,
|
||||
fontFamilyCode,
|
||||
fontSizes,
|
||||
truncate
|
||||
} from '../../../../../../style/variables';
|
||||
import TooltipOverlay, {
|
||||
fieldNameHelper
|
||||
} from '../../../../../shared/TooltipOverlay';
|
||||
|
||||
import SyntaxHighlighter, {
|
||||
registerLanguage
|
||||
} from 'react-syntax-highlighter/dist/light';
|
||||
import { xcode } from 'react-syntax-highlighter/dist/styles';
|
||||
|
||||
import sql from 'react-syntax-highlighter/dist/languages/sql';
|
||||
import { HeaderXSmall } from '../../../../../shared/UIComponents';
|
||||
|
||||
registerLanguage('sql', sql);
|
||||
|
||||
const DetailsWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
border-bottom: 1px solid ${colors.gray4};
|
||||
padding: ${px(unit)} 0;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const DetailsElement = styled.div`
|
||||
min-width: 0;
|
||||
max-width: 50%;
|
||||
line-height: 1.5;
|
||||
`;
|
||||
|
||||
const DetailsHeader = styled.div`
|
||||
font-size: ${fontSizes.small};
|
||||
color: ${colors.gray3};
|
||||
|
||||
span {
|
||||
cursor: help;
|
||||
}
|
||||
`;
|
||||
|
||||
const DetailsText = styled.div`
|
||||
font-size: ${fontSizes.large};
|
||||
`;
|
||||
|
||||
const SpanName = styled.div`
|
||||
${truncate('100%')};
|
||||
`;
|
||||
|
||||
const LegendIndicator = styled(Indicator)`
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
const StackTraceContainer = styled.div`
|
||||
margin-top: ${px(unit)};
|
||||
`;
|
||||
|
||||
const DatabaseStatement = styled.div`
|
||||
margin-top: ${px(unit)};
|
||||
padding: ${px(units.half)} ${px(unit)};
|
||||
background: ${colors.yellow};
|
||||
border-radius: ${borderRadius};
|
||||
border: 1px solid ${colors.gray4};
|
||||
font-family: ${fontFamilyCode};
|
||||
`;
|
||||
|
||||
function SpanDetails({ span, spanTypeLabel, spanTypeColor, totalDuration }) {
|
||||
const spanDocId = get(span, 'docId');
|
||||
const spanDuration = get({ span }, SPAN_DURATION);
|
||||
const relativeDuration = spanDuration / totalDuration;
|
||||
const spanName = get({ span }, SPAN_NAME);
|
||||
const stackframes = span.stacktrace;
|
||||
const codeLanguage = get(span, SERVICE_LANGUAGE_NAME);
|
||||
const dbContext = get(span, 'context.db');
|
||||
|
||||
const discoverQuery = {
|
||||
_a: {
|
||||
interval: 'auto',
|
||||
query: {
|
||||
language: 'lucene',
|
||||
query: `_id:${spanDocId}`
|
||||
},
|
||||
sort: { '@timestamp': 'desc' }
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DetailsWrapper>
|
||||
<DetailsElement>
|
||||
<DetailsHeader>
|
||||
<TooltipOverlay content={fieldNameHelper('span.name')}>
|
||||
<span>Name</span>
|
||||
</TooltipOverlay>
|
||||
</DetailsHeader>
|
||||
<DetailsText>
|
||||
<TooltipOverlay content={`${spanName || 'N/A'}`}>
|
||||
<SpanName>{spanName || 'N/A'}</SpanName>
|
||||
</TooltipOverlay>
|
||||
</DetailsText>
|
||||
</DetailsElement>
|
||||
<DetailsElement>
|
||||
<DetailsHeader>
|
||||
<TooltipOverlay content={fieldNameHelper('span.type')}>
|
||||
<span>Type</span>
|
||||
</TooltipOverlay>
|
||||
</DetailsHeader>
|
||||
<DetailsText>
|
||||
<LegendIndicator radius={units.minus - 1} color={spanTypeColor} />
|
||||
{spanTypeLabel}
|
||||
</DetailsText>
|
||||
</DetailsElement>
|
||||
<DetailsElement>
|
||||
<DetailsHeader>
|
||||
<TooltipOverlay content={fieldNameHelper('span.duration.us')}>
|
||||
<span>Duration</span>
|
||||
</TooltipOverlay>
|
||||
</DetailsHeader>
|
||||
<DetailsText>{asMillis(spanDuration)}</DetailsText>
|
||||
</DetailsElement>
|
||||
<DetailsElement>
|
||||
<DetailsHeader>% of total time</DetailsHeader>
|
||||
<DetailsText>{numeral(relativeDuration).format('0.00%')}</DetailsText>
|
||||
</DetailsElement>
|
||||
<DetailsElement>
|
||||
<DiscoverButton query={discoverQuery}>
|
||||
{`View span in Discover`}
|
||||
</DiscoverButton>
|
||||
</DetailsElement>
|
||||
</DetailsWrapper>
|
||||
|
||||
<DatabaseContext dbContext={dbContext} />
|
||||
|
||||
<StackTraceContainer>
|
||||
<Stacktrace stackframes={stackframes} codeLanguage={codeLanguage} />
|
||||
</StackTraceContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DatabaseContext({ dbContext }) {
|
||||
if (!dbContext || !dbContext.statement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dbContext.type !== 'sql') {
|
||||
return <DatabaseStatement>{dbContext.statement}</DatabaseStatement>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeaderXSmall>DB Statement</HeaderXSmall>
|
||||
<DatabaseStatement>
|
||||
<SyntaxHighlighter
|
||||
language={'sql'}
|
||||
style={xcode}
|
||||
customStyle={{
|
||||
color: null,
|
||||
background: null,
|
||||
padding: null,
|
||||
lineHeight: px(unit * 1.5),
|
||||
whiteSpace: 'pre-wrap',
|
||||
overflowX: 'scroll'
|
||||
}}
|
||||
>
|
||||
{dbContext.statement}
|
||||
</SyntaxHighlighter>
|
||||
</DatabaseStatement>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SpanDetails.propTypes = {
|
||||
span: PropTypes.object.isRequired,
|
||||
totalDuration: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default SpanDetails;
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Legend from '../../../../shared/charts/Legend';
|
||||
import {
|
||||
fontSizes,
|
||||
colors,
|
||||
unit,
|
||||
units,
|
||||
px,
|
||||
truncate
|
||||
} from '../../../../../style/variables';
|
||||
|
||||
import TooltipOverlay from '../../../../shared/TooltipOverlay';
|
||||
|
||||
const TimelineHeaderContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: ${px(unit * 1.5)} ${px(units.plus)} 0 ${px(units.plus)};
|
||||
line-height: 1.5;
|
||||
`;
|
||||
|
||||
const Heading = styled.div`
|
||||
font-size: ${fontSizes.large};
|
||||
color: ${colors.gray2};
|
||||
${truncate('90%')};
|
||||
`;
|
||||
|
||||
const Legends = styled.div`
|
||||
display: flex;
|
||||
|
||||
div {
|
||||
margin-right: ${px(unit)};
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default function TimelineHeader({ legends, transactionName }) {
|
||||
return (
|
||||
<TimelineHeaderContainer>
|
||||
<TooltipOverlay content={transactionName || 'N/A'}>
|
||||
<Heading>{transactionName || 'N/A'}</Heading>
|
||||
</TooltipOverlay>
|
||||
<Legends>
|
||||
{legends.map(({ color, label }) => (
|
||||
<Legend key={color} color={color} text={label} />
|
||||
))}
|
||||
</Legends>
|
||||
</TimelineHeaderContainer>
|
||||
);
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import Spans from './view';
|
||||
import { getUrlParams } from '../../../../../store/urlParams';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
urlParams: getUrlParams(state),
|
||||
location: state.location
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {};
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Spans);
|
|
@ -1,193 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { get, uniq, first, zipObject, difference, isEmpty } from 'lodash';
|
||||
import Span from './Span';
|
||||
import TimelineHeader from './TimelineHeader';
|
||||
import { SPAN_ID } from '../../../../../../common/constants';
|
||||
import { colors } from '../../../../../style/variables';
|
||||
import { StickyContainer } from 'react-sticky';
|
||||
import Timeline from '../../../../shared/charts/Timeline';
|
||||
import EmptyMessage from '../../../../shared/EmptyMessage';
|
||||
import { getFeatureDocs } from '../../../../../utils/documentation';
|
||||
import { ExternalLink } from '../../../../../utils/url';
|
||||
import { SpansRequest } from '../../../../../store/reactReduxRequest/spans';
|
||||
|
||||
const Container = styled.div`
|
||||
transition: 0.1s padding ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const DroppedSpansContainer = styled.div`
|
||||
border-top: 1px solid #ddd;
|
||||
height: 43px;
|
||||
line-height: 43px;
|
||||
text-align: center;
|
||||
color: ${colors.gray2};
|
||||
`;
|
||||
|
||||
const TIMELINE_HEADER_HEIGHT = 100;
|
||||
const TIMELINE_MARGINS = {
|
||||
top: TIMELINE_HEADER_HEIGHT,
|
||||
left: 50,
|
||||
right: 50,
|
||||
bottom: 0
|
||||
};
|
||||
|
||||
class Spans extends PureComponent {
|
||||
render() {
|
||||
const {
|
||||
agentName,
|
||||
urlParams,
|
||||
location,
|
||||
droppedSpans,
|
||||
agentMarks
|
||||
} = this.props;
|
||||
return (
|
||||
<SpansRequest
|
||||
urlParams={urlParams}
|
||||
render={spans => {
|
||||
if (isEmpty(spans.data.spans)) {
|
||||
return (
|
||||
<EmptyMessage
|
||||
heading="No spans available for this transaction."
|
||||
hideSubheading
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const spanTypes = uniq(
|
||||
spans.data.spanTypes.map(({ type }) => getPrimaryType(type))
|
||||
);
|
||||
|
||||
const getSpanColor = getColorByType(spanTypes);
|
||||
|
||||
const totalDuration = spans.data.duration;
|
||||
const spanContainerHeight = 58;
|
||||
const timelineHeight = spanContainerHeight * spans.data.spans.length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Container>
|
||||
<StickyContainer>
|
||||
<Timeline
|
||||
header={
|
||||
<TimelineHeader
|
||||
legends={spanTypes.map(type => ({
|
||||
label: getSpanLabel(type),
|
||||
color: getSpanColor(type)
|
||||
}))}
|
||||
transactionName={urlParams.transactionName}
|
||||
/>
|
||||
}
|
||||
agentMarks={agentMarks}
|
||||
duration={totalDuration}
|
||||
height={timelineHeight}
|
||||
margins={TIMELINE_MARGINS}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
paddingTop: TIMELINE_MARGINS.top
|
||||
}}
|
||||
>
|
||||
{spans.data.spans.map(span => (
|
||||
<Span
|
||||
location={location}
|
||||
timelineMargins={TIMELINE_MARGINS}
|
||||
key={get({ span }, SPAN_ID)}
|
||||
color={getSpanColor(getPrimaryType(span.type))}
|
||||
span={span}
|
||||
spanTypeLabel={getSpanLabel(getPrimaryType(span.type))}
|
||||
totalDuration={totalDuration}
|
||||
isSelected={get({ span }, SPAN_ID) === urlParams.spanId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</StickyContainer>
|
||||
</Container>
|
||||
|
||||
{droppedSpans > 0 && (
|
||||
<DroppedSpansContainer>
|
||||
{droppedSpans} spans dropped due to limit of{' '}
|
||||
{spans.data.spans.length}.{' '}
|
||||
<DroppedSpansDocsLink agentName={agentName} />
|
||||
</DroppedSpansContainer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function DroppedSpansDocsLink({ agentName }) {
|
||||
const docs = getFeatureDocs('dropped-spans', agentName);
|
||||
|
||||
if (!docs || !docs.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ExternalLink href={docs.url}>
|
||||
Learn more in the documentation.
|
||||
</ExternalLink>
|
||||
);
|
||||
}
|
||||
|
||||
function getColorByType(types) {
|
||||
const assignedColors = {
|
||||
app: colors.apmBlue,
|
||||
cache: colors.apmGreen,
|
||||
components: colors.apmGreen,
|
||||
ext: colors.apmPurple,
|
||||
xhr: colors.apmPurple,
|
||||
template: colors.apmRed2,
|
||||
resource: colors.apmRed2,
|
||||
custom: colors.apmTan,
|
||||
db: colors.apmOrange,
|
||||
'hard-navigation': colors.apmYellow
|
||||
};
|
||||
|
||||
const unknownTypes = difference(types, Object.keys(assignedColors));
|
||||
const unassignedColors = zipObject(unknownTypes, [
|
||||
colors.apmYellow,
|
||||
colors.apmRed,
|
||||
colors.apmBrown,
|
||||
colors.apmPink
|
||||
]);
|
||||
|
||||
return type => assignedColors[type] || unassignedColors[type];
|
||||
}
|
||||
|
||||
function getSpanLabel(type) {
|
||||
switch (type) {
|
||||
case 'db':
|
||||
return 'DB';
|
||||
case 'hard-navigation':
|
||||
return 'Navigation timing';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
function getPrimaryType(type) {
|
||||
return first(type.split('.'));
|
||||
}
|
||||
|
||||
Spans.propTypes = {
|
||||
agentMarks: PropTypes.array,
|
||||
agentName: PropTypes.string.isRequired,
|
||||
droppedSpans: PropTypes.number.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
urlParams: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default Spans;
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { get } from 'lodash';
|
||||
import { StickyProperties } from '../../../shared/StickyProperties';
|
||||
import {
|
||||
TRANSACTION_DURATION,
|
||||
TRANSACTION_RESULT,
|
||||
USER_ID,
|
||||
REQUEST_URL_FULL
|
||||
} from '../../../../../common/constants';
|
||||
import { asTime } from '../../../../utils/formatters';
|
||||
|
||||
export default function StickyTransactionProperties({ transaction }) {
|
||||
const timestamp = get(transaction, '@timestamp');
|
||||
const url = get(transaction, REQUEST_URL_FULL, 'N/A');
|
||||
const duration = get(transaction, TRANSACTION_DURATION);
|
||||
const stickyProperties = [
|
||||
{
|
||||
label: 'Timestamp',
|
||||
fieldName: '@timestamp',
|
||||
val: timestamp
|
||||
},
|
||||
{
|
||||
fieldName: REQUEST_URL_FULL,
|
||||
label: 'URL',
|
||||
val: url,
|
||||
truncated: true
|
||||
},
|
||||
{
|
||||
label: 'Duration',
|
||||
fieldName: TRANSACTION_DURATION,
|
||||
val: duration ? asTime(duration) : 'N/A'
|
||||
},
|
||||
{
|
||||
label: 'Result',
|
||||
fieldName: TRANSACTION_RESULT,
|
||||
val: get(transaction, TRANSACTION_RESULT, 'N/A')
|
||||
},
|
||||
{
|
||||
label: 'User ID',
|
||||
fieldName: USER_ID,
|
||||
val: get(transaction, USER_ID, 'N/A')
|
||||
}
|
||||
];
|
||||
|
||||
return <StickyProperties stickyProperties={stickyProperties} />;
|
||||
}
|
||||
|
||||
StickyTransactionProperties.propTypes = {
|
||||
transaction: PropTypes.object.isRequired
|
||||
};
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectWaterfallRoot } from 'x-pack/plugins/apm/public/store/selectors/waterfall';
|
||||
import {
|
||||
REQUEST_URL_FULL,
|
||||
TRANSACTION_DURATION,
|
||||
TRANSACTION_RESULT,
|
||||
USER_ID
|
||||
} from '../../../../../common/constants';
|
||||
import { Transaction } from '../../../../../typings/Transaction';
|
||||
// @ts-ignore
|
||||
import { asTime } from '../../../../utils/formatters';
|
||||
// @ts-ignore
|
||||
import { StickyProperties } from '../../../shared/StickyProperties';
|
||||
|
||||
function getDurationPercent(
|
||||
transactionDuration: number,
|
||||
rootDuration?: number
|
||||
) {
|
||||
if (rootDuration === undefined || rootDuration === 0) {
|
||||
return '';
|
||||
}
|
||||
return ((transactionDuration / rootDuration) * 100).toFixed(2) + '%';
|
||||
}
|
||||
|
||||
interface Props {
|
||||
transaction: Transaction;
|
||||
root?: Transaction;
|
||||
}
|
||||
|
||||
export function StickyTransactionPropertiesComponent({
|
||||
transaction,
|
||||
root
|
||||
}: Props) {
|
||||
const timestamp = get(transaction, '@timestamp');
|
||||
const url = get(transaction, REQUEST_URL_FULL, 'N/A');
|
||||
const duration = transaction.transaction.duration.us;
|
||||
const rootDuration = root && root.transaction.duration.us;
|
||||
const stickyProperties = [
|
||||
{
|
||||
label: 'Timestamp',
|
||||
fieldName: '@timestamp',
|
||||
val: timestamp,
|
||||
truncated: true,
|
||||
width: '50%'
|
||||
},
|
||||
{
|
||||
fieldName: REQUEST_URL_FULL,
|
||||
label: 'URL',
|
||||
val: url,
|
||||
truncated: true,
|
||||
width: '50%'
|
||||
},
|
||||
{
|
||||
label: 'Duration',
|
||||
fieldName: TRANSACTION_DURATION,
|
||||
val: duration ? asTime(duration) : 'N/A',
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
label: '% of trace',
|
||||
val: getDurationPercent(duration, rootDuration),
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
label: 'Result',
|
||||
fieldName: TRANSACTION_RESULT,
|
||||
val: get(transaction, TRANSACTION_RESULT, 'N/A'),
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
label: 'User ID',
|
||||
fieldName: USER_ID,
|
||||
val: get(transaction, USER_ID, 'N/A'),
|
||||
truncated: true,
|
||||
width: '25%'
|
||||
}
|
||||
];
|
||||
|
||||
return <StickyProperties stickyProperties={stickyProperties} />;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: any, props: Partial<Props>) => ({
|
||||
root: selectWaterfallRoot(state, props)
|
||||
});
|
||||
|
||||
export const StickyTransactionProperties = connect<{}, {}, Props>(
|
||||
mapStateToProps
|
||||
)(StickyTransactionPropertiesComponent);
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiSpacer,
|
||||
// @ts-ignore
|
||||
EuiTab,
|
||||
// @ts-ignore
|
||||
EuiTabs
|
||||
} from '@elastic/eui';
|
||||
import { capitalize, first, get } from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Transaction } from '../../../../../typings/Transaction';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { px, units } from '../../../../style/variables';
|
||||
import { fromQuery, history, toQuery } from '../../../../utils/url';
|
||||
import {
|
||||
getPropertyTabNames,
|
||||
PropertiesTable
|
||||
} from '../../../shared/PropertiesTable';
|
||||
import { WaterfallContainer } from './WaterfallContainer';
|
||||
|
||||
const TableContainer = styled.div`
|
||||
padding: ${px(units.plus)} ${px(units.plus)} 0;
|
||||
`;
|
||||
|
||||
// Ensure the selected tab exists or use the first
|
||||
function getCurrentTab(tabs: string[] = [], selectedTab?: string) {
|
||||
return selectedTab && tabs.includes(selectedTab) ? selectedTab : first(tabs);
|
||||
}
|
||||
|
||||
const TIMELINE_TAB = 'timeline';
|
||||
|
||||
function getTabs(transactionData: Transaction) {
|
||||
const dynamicProps = Object.keys(transactionData.context || {});
|
||||
return [TIMELINE_TAB, ...getPropertyTabNames(dynamicProps)];
|
||||
}
|
||||
|
||||
interface TransactionPropertiesTableProps {
|
||||
location: any;
|
||||
transaction: Transaction;
|
||||
urlParams: IUrlParams;
|
||||
}
|
||||
|
||||
export const TransactionPropertiesTable: React.SFC<
|
||||
TransactionPropertiesTableProps
|
||||
> = ({ location, transaction, urlParams }) => {
|
||||
const tabs = getTabs(transaction);
|
||||
const currentTab = getCurrentTab(tabs, urlParams.detailTab);
|
||||
const agentName = transaction.context.service.agent.name;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiTabs>
|
||||
{tabs.map(key => {
|
||||
return (
|
||||
<EuiTab
|
||||
onClick={() => {
|
||||
history.replace({
|
||||
...location,
|
||||
search: fromQuery({
|
||||
...toQuery(location.search),
|
||||
detailTab: key
|
||||
})
|
||||
});
|
||||
}}
|
||||
selected={currentTab === key}
|
||||
key={key}
|
||||
>
|
||||
{capitalize(key)}
|
||||
</EuiTab>
|
||||
);
|
||||
})}
|
||||
</EuiTabs>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
{currentTab === TIMELINE_TAB && (
|
||||
<WaterfallContainer
|
||||
transaction={transaction}
|
||||
location={location}
|
||||
urlParams={urlParams}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentTab !== TIMELINE_TAB && (
|
||||
<TableContainer>
|
||||
<PropertiesTable
|
||||
propData={get(transaction.context, currentTab)}
|
||||
propKey={currentTab}
|
||||
agentName={agentName}
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
|
||||
import { capitalize, first, get } from 'lodash';
|
||||
import React from 'react';
|
||||
import { Transaction } from '../../../../../typings/Transaction';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
// @ts-ignore
|
||||
import { fromQuery, history, toQuery } from '../../../../utils/url';
|
||||
import {
|
||||
getPropertyTabNames,
|
||||
PropertiesTable
|
||||
} from '../../../shared/PropertiesTable';
|
||||
// @ts-ignore
|
||||
import { Tab } from '../../../shared/UIComponents';
|
||||
|
||||
// Ensure the selected tab exists or use the first
|
||||
function getCurrentTab(tabs: string[] = [], selectedTab?: string) {
|
||||
return selectedTab && tabs.includes(selectedTab) ? selectedTab : first(tabs);
|
||||
}
|
||||
|
||||
function getTabs(transactionData: Transaction) {
|
||||
const dynamicProps = Object.keys(transactionData.context || {});
|
||||
return getPropertyTabNames(dynamicProps);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
location: any;
|
||||
transaction: Transaction;
|
||||
urlParams: IUrlParams;
|
||||
}
|
||||
|
||||
export const TransactionPropertiesTableForFlyout: React.SFC<Props> = ({
|
||||
location,
|
||||
transaction,
|
||||
urlParams
|
||||
}) => {
|
||||
const tabs = getTabs(transaction);
|
||||
const currentTab = getCurrentTab(tabs, urlParams.flyoutDetailTab);
|
||||
const agentName = transaction.context.service.agent.name;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiTabs>
|
||||
{tabs.map(key => {
|
||||
return (
|
||||
<EuiTab
|
||||
onClick={() => {
|
||||
history.replace({
|
||||
...location,
|
||||
search: fromQuery({
|
||||
...toQuery(location.search),
|
||||
flyoutDetailTab: key
|
||||
})
|
||||
});
|
||||
}}
|
||||
isSelected={currentTab === key}
|
||||
key={key}
|
||||
>
|
||||
{capitalize(key)}
|
||||
</EuiTab>
|
||||
);
|
||||
})}
|
||||
</EuiTabs>
|
||||
<EuiSpacer />
|
||||
<PropertiesTable
|
||||
propData={get(transaction.context, currentTab)}
|
||||
propKey={currentTab}
|
||||
agentName={agentName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { px, unit } from '../../../../../style/variables';
|
||||
// @ts-ignore
|
||||
import Legend from '../../../../shared/charts/Legend';
|
||||
|
||||
const Legends = styled.div`
|
||||
display: flex;
|
||||
|
||||
div {
|
||||
margin-right: ${px(unit)};
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
serviceColors: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function ServiceLegends({ serviceColors }: Props) {
|
||||
return (
|
||||
<Legends>
|
||||
{Object.entries(serviceColors).map(([label, color]) => (
|
||||
<Legend key={color} color={color} text={label} />
|
||||
))}
|
||||
</Legends>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_NAME
|
||||
} from 'x-pack/plugins/apm/common/constants';
|
||||
import {
|
||||
KibanaLink,
|
||||
legacyEncodeURIComponent
|
||||
// @ts-ignore
|
||||
} from 'x-pack/plugins/apm/public/utils/url';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||
// @ts-ignore
|
||||
import { StickyProperties } from '../../../../../shared/StickyProperties';
|
||||
|
||||
interface Props {
|
||||
transaction: Transaction;
|
||||
}
|
||||
|
||||
export function FlyoutTopLevelProperties({ transaction }: Props) {
|
||||
const stickyProperties = [
|
||||
{
|
||||
label: 'Service',
|
||||
fieldName: SERVICE_NAME,
|
||||
val: (
|
||||
<KibanaLink
|
||||
pathname={'/app/apm'}
|
||||
hash={`/${transaction.context.service.name}`}
|
||||
>
|
||||
{transaction.context.service.name}
|
||||
</KibanaLink>
|
||||
),
|
||||
width: '50%'
|
||||
},
|
||||
{
|
||||
label: 'Transaction',
|
||||
fieldName: TRANSACTION_NAME,
|
||||
val: (
|
||||
<KibanaLink
|
||||
pathname={'/app/apm'}
|
||||
hash={`/${transaction.context.service.name}/transactions/${
|
||||
transaction.transaction.type
|
||||
}/${legacyEncodeURIComponent(transaction.transaction.name)}`}
|
||||
>
|
||||
{transaction.transaction.name}
|
||||
</KibanaLink>
|
||||
),
|
||||
width: '50%'
|
||||
}
|
||||
];
|
||||
|
||||
return <StickyProperties stickyProperties={stickyProperties} />;
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
borderRadius,
|
||||
colors,
|
||||
fontFamilyCode,
|
||||
px,
|
||||
unit,
|
||||
units
|
||||
} from '../../../../../../../style/variables';
|
||||
|
||||
import SyntaxHighlighter, {
|
||||
registerLanguage
|
||||
// @ts-ignore
|
||||
} from 'react-syntax-highlighter/dist/light';
|
||||
|
||||
// @ts-ignore
|
||||
import { xcode } from 'react-syntax-highlighter/dist/styles';
|
||||
|
||||
// @ts-ignore
|
||||
import sql from 'react-syntax-highlighter/dist/languages/sql';
|
||||
|
||||
import { EuiTitle } from '@elastic/eui';
|
||||
import { DbContext } from '../../../../../../../../typings/Span';
|
||||
|
||||
registerLanguage('sql', sql);
|
||||
|
||||
const DatabaseStatement = styled.div`
|
||||
margin-top: ${px(unit)};
|
||||
padding: ${px(units.half)} ${px(unit)};
|
||||
background: ${colors.yellow};
|
||||
border-radius: ${borderRadius};
|
||||
border: 1px solid ${colors.gray4};
|
||||
font-family: ${fontFamilyCode};
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
dbContext?: DbContext;
|
||||
}
|
||||
|
||||
export function DatabaseContext({ dbContext }: Props) {
|
||||
if (!dbContext || !dbContext.statement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dbContext.type !== 'sql') {
|
||||
return <DatabaseStatement>{dbContext.statement}</DatabaseStatement>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="xs">
|
||||
<h3>Database statement</h3>
|
||||
</EuiTitle>
|
||||
<DatabaseStatement>
|
||||
<SyntaxHighlighter
|
||||
language={'sql'}
|
||||
style={xcode}
|
||||
customStyle={{
|
||||
color: null,
|
||||
background: null,
|
||||
padding: null,
|
||||
lineHeight: px(unit * 1.5),
|
||||
whiteSpace: 'pre-wrap',
|
||||
overflowX: 'scroll'
|
||||
}}
|
||||
>
|
||||
{dbContext.statement}
|
||||
</SyntaxHighlighter>
|
||||
</DatabaseStatement>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// tslint:disable-next-line no-var-requires
|
||||
const numeral = require('@elastic/numeral');
|
||||
import React from 'react';
|
||||
|
||||
import { first } from 'lodash';
|
||||
import { Span } from '../../../../../../../../typings/Span';
|
||||
// @ts-ignore
|
||||
import { asMillis } from '../../../../../../../utils/formatters';
|
||||
// @ts-ignore
|
||||
import { StickyProperties } from '../../../../../../shared/StickyProperties';
|
||||
|
||||
function getSpanLabel(type: string) {
|
||||
switch (type) {
|
||||
case 'db':
|
||||
return 'DB';
|
||||
case 'hard-navigation':
|
||||
return 'Navigation timing';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
function getPrimaryType(type: string) {
|
||||
return first(type.split('.'));
|
||||
}
|
||||
|
||||
interface Props {
|
||||
span: Span;
|
||||
totalDuration: number;
|
||||
}
|
||||
|
||||
export function StickySpanProperties({ span, totalDuration }: Props) {
|
||||
const spanName = span.span.name;
|
||||
const spanDuration = span.span.duration.us;
|
||||
const relativeDuration = spanDuration / totalDuration;
|
||||
const spanTypeLabel = getSpanLabel(getPrimaryType(span.span.type));
|
||||
|
||||
const stickyProperties = [
|
||||
{
|
||||
label: 'Name',
|
||||
fieldName: 'span.name',
|
||||
val: spanName || 'N/A'
|
||||
},
|
||||
{
|
||||
fieldName: 'span.type',
|
||||
label: 'Type',
|
||||
val: spanTypeLabel
|
||||
},
|
||||
{
|
||||
fieldName: 'span.duration.us',
|
||||
label: 'Duration',
|
||||
val: asMillis(spanDuration)
|
||||
},
|
||||
{
|
||||
label: '% of transaction',
|
||||
val: numeral(relativeDuration).format('0.00%')
|
||||
}
|
||||
];
|
||||
|
||||
return <StickyProperties stickyProperties={stickyProperties} />;
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiHorizontalRule,
|
||||
EuiTitle
|
||||
} from '@elastic/eui';
|
||||
import { get } from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
// @ts-ignore
|
||||
import { SERVICE_LANGUAGE_NAME } from '../../../../../../../../common/constants';
|
||||
import { px, unit } from '../../../../../../../style/variables';
|
||||
|
||||
// @ts-ignore
|
||||
import Stacktrace from '../../../../../../shared/Stacktrace';
|
||||
|
||||
import { DatabaseContext } from './DatabaseContext';
|
||||
import { StickySpanProperties } from './StickySpanProperties';
|
||||
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||
import { Span } from '../../../../../../../../typings/Span';
|
||||
// @ts-ignore
|
||||
import DiscoverButton from '../../../../../../shared/DiscoverButton';
|
||||
import { FlyoutTopLevelProperties } from '../FlyoutTopLevelProperties';
|
||||
|
||||
const StackTraceContainer = styled.div`
|
||||
margin-top: ${px(unit)};
|
||||
`;
|
||||
|
||||
function getDiscoverQuery(span: Span) {
|
||||
return {
|
||||
_a: {
|
||||
interval: 'auto',
|
||||
query: {
|
||||
language: 'lucene',
|
||||
query:
|
||||
span.version === 'v2'
|
||||
? `span.hex_id:${span.span.hex_id}`
|
||||
: `span.id:${span.span.id}`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
span?: Span;
|
||||
parentTransaction: Transaction;
|
||||
totalDuration: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function SpanFlyout({
|
||||
span,
|
||||
parentTransaction,
|
||||
totalDuration,
|
||||
onClose
|
||||
}: Props) {
|
||||
if (!span) {
|
||||
return null;
|
||||
}
|
||||
const stackframes = span.span.stacktrace;
|
||||
const codeLanguage = get(span, SERVICE_LANGUAGE_NAME);
|
||||
const dbContext = span.context.db;
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onClose} size="l">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h2>Span details</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<DiscoverButton query={getDiscoverQuery(span)}>
|
||||
{`View span in Discover`}
|
||||
</DiscoverButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<FlyoutTopLevelProperties transaction={parentTransaction} />
|
||||
<EuiHorizontalRule />
|
||||
<StickySpanProperties span={span} totalDuration={totalDuration} />
|
||||
<EuiHorizontalRule />
|
||||
<DatabaseContext dbContext={dbContext} />
|
||||
<StackTraceContainer>
|
||||
<Stacktrace stackframes={stackframes} codeLanguage={codeLanguage} />
|
||||
</StackTraceContainer>
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiHorizontalRule,
|
||||
EuiTitle
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { TraceLink } from 'x-pack/plugins/apm/public/components/shared/TraceLink';
|
||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||
import { ActionMenu } from '../../../ActionMenu';
|
||||
import { StickyTransactionProperties } from '../../../StickyTransactionProperties';
|
||||
import { TransactionPropertiesTableForFlyout } from '../../../TransactionPropertiesTableForFlyout';
|
||||
import { FlyoutTopLevelProperties } from '../FlyoutTopLevelProperties';
|
||||
import { IWaterfall } from '../waterfall_helpers/waterfall_helpers';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
transaction?: Transaction;
|
||||
location: any; // TODO: import location type from react router or history types?
|
||||
urlParams: IUrlParams;
|
||||
waterfall: IWaterfall;
|
||||
}
|
||||
|
||||
export function TransactionFlyout({
|
||||
transaction: transactionDoc,
|
||||
onClose,
|
||||
location,
|
||||
urlParams,
|
||||
waterfall
|
||||
}: Props) {
|
||||
if (!transactionDoc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onClose} size="l">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h4>Transaction details</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<ActionMenu transaction={transactionDoc} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<TraceLink transaction={transactionDoc}>
|
||||
<EuiButton iconType="visLine">
|
||||
View transaction group details
|
||||
</EuiButton>
|
||||
</TraceLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<FlyoutTopLevelProperties transaction={transactionDoc} />
|
||||
<EuiHorizontalRule />
|
||||
<StickyTransactionProperties transaction={transactionDoc} />
|
||||
<EuiHorizontalRule />
|
||||
<TransactionPropertiesTableForFlyout
|
||||
transaction={transactionDoc}
|
||||
location={location}
|
||||
urlParams={urlParams}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
colors,
|
||||
fontFamilyCode,
|
||||
fontSizes,
|
||||
px,
|
||||
unit,
|
||||
units
|
||||
} from '../../../../../../style/variables';
|
||||
import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers';
|
||||
|
||||
const ItemBar = styled.div`
|
||||
position: relative;
|
||||
height: ${unit}px;
|
||||
`;
|
||||
const ItemLabel = styled.div`
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
margin: ${px(units.quarter)} 0 0;
|
||||
font-family: ${fontFamilyCode};
|
||||
font-size: ${fontSizes.small};
|
||||
`;
|
||||
|
||||
const Container = styled<
|
||||
{ timelineMargins: TimelineMargins; isSelected: boolean },
|
||||
'div'
|
||||
>('div')`
|
||||
position: relative;
|
||||
display: block;
|
||||
user-select: none;
|
||||
padding: ${px(units.half)} ${props => px(props.timelineMargins.right)}
|
||||
${px(units.eighth)} ${props => px(props.timelineMargins.left)};
|
||||
border-top: 1px solid ${colors.gray4};
|
||||
background-color: ${props => (props.isSelected ? colors.gray5 : 'initial')};
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: ${colors.gray5};
|
||||
}
|
||||
`;
|
||||
|
||||
interface TimelineMargins {
|
||||
right: number;
|
||||
left: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
timelineMargins: TimelineMargins;
|
||||
totalDuration: number;
|
||||
item: IWaterfallItem;
|
||||
color: string;
|
||||
isSelected: boolean;
|
||||
onClick: () => any;
|
||||
}
|
||||
|
||||
export function WaterfallItem({
|
||||
timelineMargins,
|
||||
totalDuration,
|
||||
item,
|
||||
color,
|
||||
isSelected,
|
||||
onClick
|
||||
}: Props) {
|
||||
const width = (item.duration / totalDuration) * 100;
|
||||
const left = (item.offset / totalDuration) * 100;
|
||||
|
||||
return (
|
||||
<Container
|
||||
onClick={onClick}
|
||||
timelineMargins={timelineMargins}
|
||||
isSelected={isSelected}
|
||||
>
|
||||
<ItemBar
|
||||
style={{
|
||||
left: `${left}%`,
|
||||
width: `${width}%`,
|
||||
minWidth: '2px',
|
||||
backgroundColor: color
|
||||
}}
|
||||
/>
|
||||
<ItemLabel
|
||||
style={{
|
||||
left: `${left}%`,
|
||||
width: `${100 - left}%`
|
||||
}}
|
||||
>
|
||||
‎
|
||||
{item.name}
|
||||
‎
|
||||
</ItemLabel>
|
||||
</Container>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
// @ts-ignore
|
||||
import { StickyContainer } from 'react-sticky';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { IUrlParams } from '../../../../../../store/urlParams';
|
||||
|
||||
// @ts-ignore
|
||||
import { fromQuery, history, toQuery } from '../../../../../../utils/url';
|
||||
// @ts-ignore
|
||||
import Timeline from '../../../../../shared/charts/Timeline';
|
||||
import { AgentMark } from '../get_agent_marks';
|
||||
import { SpanFlyout } from './SpanFlyout';
|
||||
import { TransactionFlyout } from './TransactionFlyout';
|
||||
import {
|
||||
IWaterfall,
|
||||
IWaterfallItem
|
||||
} from './waterfall_helpers/waterfall_helpers';
|
||||
import { WaterfallItem } from './WaterfallItem';
|
||||
|
||||
const Container = styled.div`
|
||||
transition: 0.1s padding ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const TIMELINE_MARGINS = {
|
||||
top: 40,
|
||||
left: 50,
|
||||
right: 50,
|
||||
bottom: 0
|
||||
};
|
||||
|
||||
interface Props {
|
||||
agentMarks: AgentMark[];
|
||||
urlParams: IUrlParams;
|
||||
waterfall: IWaterfall;
|
||||
location: any;
|
||||
serviceColors: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class Waterfall extends Component<Props> {
|
||||
public onOpenFlyout = (item: IWaterfallItem) => {
|
||||
this.setQueryParams({
|
||||
flyoutDetailTab: undefined,
|
||||
waterfallItemId: String(item.id)
|
||||
});
|
||||
};
|
||||
|
||||
public onCloseFlyout = () => {
|
||||
this.setQueryParams({
|
||||
flyoutDetailTab: undefined,
|
||||
waterfallItemId: undefined
|
||||
});
|
||||
};
|
||||
|
||||
public renderWaterfall = (item?: IWaterfallItem) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { serviceColors, waterfall, urlParams }: Props = this.props;
|
||||
|
||||
return (
|
||||
<Fragment key={item.id}>
|
||||
<WaterfallItem
|
||||
timelineMargins={TIMELINE_MARGINS}
|
||||
color={serviceColors[item.serviceName]}
|
||||
item={item}
|
||||
totalDuration={waterfall.duration}
|
||||
isSelected={item.id === urlParams.waterfallItemId}
|
||||
onClick={() => this.onOpenFlyout(item)}
|
||||
/>
|
||||
|
||||
{item.children && item.children.map(this.renderWaterfall)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
public getFlyOut = () => {
|
||||
const { waterfall, location, urlParams } = this.props;
|
||||
|
||||
const currentItem =
|
||||
urlParams.waterfallItemId &&
|
||||
waterfall.itemsById[urlParams.waterfallItemId];
|
||||
|
||||
if (!currentItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (currentItem.docType) {
|
||||
case 'span':
|
||||
return (
|
||||
<SpanFlyout
|
||||
totalDuration={waterfall.duration}
|
||||
span={currentItem.span}
|
||||
parentTransaction={currentItem.parentTransaction}
|
||||
onClose={this.onCloseFlyout}
|
||||
/>
|
||||
);
|
||||
case 'transaction':
|
||||
return (
|
||||
<TransactionFlyout
|
||||
transaction={currentItem.transaction}
|
||||
onClose={this.onCloseFlyout}
|
||||
location={location}
|
||||
urlParams={urlParams}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { waterfall } = this.props;
|
||||
const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found
|
||||
const waterfallHeight = itemContainerHeight * waterfall.childrenCount;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<StickyContainer>
|
||||
<Timeline
|
||||
agentMarks={this.props.agentMarks}
|
||||
duration={waterfall.duration}
|
||||
height={waterfallHeight}
|
||||
margins={TIMELINE_MARGINS}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
paddingTop: TIMELINE_MARGINS.top
|
||||
}}
|
||||
>
|
||||
{this.renderWaterfall(waterfall.root)}
|
||||
</div>
|
||||
</StickyContainer>
|
||||
|
||||
{this.getFlyOut()}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
private setQueryParams(params: Partial<IUrlParams>) {
|
||||
const { location } = this.props;
|
||||
history.replace({
|
||||
...location,
|
||||
search: fromQuery({
|
||||
...toQuery(location.search),
|
||||
...params
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: the agent marks and note about dropped spans were removed. Need to be re-added
|
||||
// agentMarks: PropTypes.array,
|
||||
// agentName: PropTypes.string.isRequired,
|
||||
// droppedSpans: PropTypes.number.isRequired,
|
|
@ -0,0 +1,274 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getWaterfallRoot 1`] = `
|
||||
Object {
|
||||
"itemsById": Object {
|
||||
"a": Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 4694,
|
||||
"id": "b2",
|
||||
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||
"offset": 1000,
|
||||
"parentId": "a",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "a",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736367000,
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 210,
|
||||
"id": "d",
|
||||
"name": "SELECT",
|
||||
"offset": 5000,
|
||||
"parentId": "c",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "c",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736371000,
|
||||
},
|
||||
],
|
||||
"docType": "transaction",
|
||||
"duration": 3581,
|
||||
"id": "c",
|
||||
"name": "APIRestController#productsRemote",
|
||||
"offset": 3000,
|
||||
"parentId": "b",
|
||||
"serviceName": "opbeans-java",
|
||||
"timestamp": 1536763736369000,
|
||||
"transaction": Object {},
|
||||
},
|
||||
],
|
||||
"docType": "span",
|
||||
"duration": 4694,
|
||||
"id": "b",
|
||||
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||
"offset": 2000,
|
||||
"parentId": "a",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "a",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736368000,
|
||||
},
|
||||
],
|
||||
"docType": "transaction",
|
||||
"duration": 9480,
|
||||
"id": "a",
|
||||
"name": "APIRestController#products",
|
||||
"offset": 0,
|
||||
"serviceName": "opbeans-java",
|
||||
"timestamp": 1536763736366000,
|
||||
"transaction": Object {},
|
||||
},
|
||||
"b": Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 210,
|
||||
"id": "d",
|
||||
"name": "SELECT",
|
||||
"offset": 5000,
|
||||
"parentId": "c",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "c",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736371000,
|
||||
},
|
||||
],
|
||||
"docType": "transaction",
|
||||
"duration": 3581,
|
||||
"id": "c",
|
||||
"name": "APIRestController#productsRemote",
|
||||
"offset": 3000,
|
||||
"parentId": "b",
|
||||
"serviceName": "opbeans-java",
|
||||
"timestamp": 1536763736369000,
|
||||
"transaction": Object {},
|
||||
},
|
||||
],
|
||||
"docType": "span",
|
||||
"duration": 4694,
|
||||
"id": "b",
|
||||
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||
"offset": 2000,
|
||||
"parentId": "a",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "a",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736368000,
|
||||
},
|
||||
"b2": Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 4694,
|
||||
"id": "b2",
|
||||
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||
"offset": 1000,
|
||||
"parentId": "a",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "a",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736367000,
|
||||
},
|
||||
"c": Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 210,
|
||||
"id": "d",
|
||||
"name": "SELECT",
|
||||
"offset": 5000,
|
||||
"parentId": "c",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "c",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736371000,
|
||||
},
|
||||
],
|
||||
"docType": "transaction",
|
||||
"duration": 3581,
|
||||
"id": "c",
|
||||
"name": "APIRestController#productsRemote",
|
||||
"offset": 3000,
|
||||
"parentId": "b",
|
||||
"serviceName": "opbeans-java",
|
||||
"timestamp": 1536763736369000,
|
||||
"transaction": Object {},
|
||||
},
|
||||
"d": Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 210,
|
||||
"id": "d",
|
||||
"name": "SELECT",
|
||||
"offset": 5000,
|
||||
"parentId": "c",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "c",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736371000,
|
||||
},
|
||||
},
|
||||
"root": Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 4694,
|
||||
"id": "b2",
|
||||
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||
"offset": 1000,
|
||||
"parentId": "a",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "a",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736367000,
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 210,
|
||||
"id": "d",
|
||||
"name": "SELECT",
|
||||
"offset": 5000,
|
||||
"parentId": "c",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "c",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736371000,
|
||||
},
|
||||
],
|
||||
"docType": "transaction",
|
||||
"duration": 3581,
|
||||
"id": "c",
|
||||
"name": "APIRestController#productsRemote",
|
||||
"offset": 3000,
|
||||
"parentId": "b",
|
||||
"serviceName": "opbeans-java",
|
||||
"timestamp": 1536763736369000,
|
||||
"transaction": Object {},
|
||||
},
|
||||
],
|
||||
"docType": "span",
|
||||
"duration": 4694,
|
||||
"id": "b",
|
||||
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||
"offset": 2000,
|
||||
"parentId": "a",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "a",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736368000,
|
||||
},
|
||||
],
|
||||
"docType": "transaction",
|
||||
"duration": 9480,
|
||||
"id": "a",
|
||||
"name": "APIRestController#products",
|
||||
"offset": 0,
|
||||
"serviceName": "opbeans-java",
|
||||
"timestamp": 1536763736366000,
|
||||
"transaction": Object {},
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,47 @@
|
|||
[
|
||||
{
|
||||
"@timestamp": "2018-09-12T15:16:05.351Z",
|
||||
"processor": {
|
||||
"name": "transaction",
|
||||
"event": "span"
|
||||
},
|
||||
"span": {
|
||||
"parent_id": "e070bc3c732087f8",
|
||||
"trace_id": "7d4d29bea37e48ac8ba1f962d5eb8a41",
|
||||
"name": "SELECT",
|
||||
"type": "db.h2.sql",
|
||||
"start": {
|
||||
"us": 9249
|
||||
},
|
||||
"duration": {
|
||||
"us": 1380
|
||||
},
|
||||
"hex_id": "8143a38f3367fd97"
|
||||
},
|
||||
"transaction": {
|
||||
"id": "e070bc3c732087f8"
|
||||
},
|
||||
"context": {
|
||||
"db": {
|
||||
"statement": "select order0_.id as col_0_0_, order0_.created_at as col_1_0_, customer1_.full_name as col_2_0_ from orders order0_ left outer join customers customer1_ on order0_.customer_id=customer1_.id",
|
||||
"type": "sql",
|
||||
"user": "SA"
|
||||
},
|
||||
"service": {
|
||||
"name": "opbeans-java",
|
||||
"agent": {
|
||||
"version": "0.7.0-SNAPSHOT",
|
||||
"name": "java"
|
||||
}
|
||||
}
|
||||
},
|
||||
"beat": {
|
||||
"version": "7.0.0-alpha1",
|
||||
"name": "361022bff072",
|
||||
"hostname": "361022bff072"
|
||||
},
|
||||
"host": {
|
||||
"name": "361022bff072"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"@timestamp": "2018-09-12T15:16:05.341Z",
|
||||
"processor": {
|
||||
"name": "transaction",
|
||||
"event": "transaction"
|
||||
},
|
||||
"transaction": {
|
||||
"duration": {
|
||||
"us": 9069543
|
||||
},
|
||||
"type": "request",
|
||||
"result": "HTTP 2xx",
|
||||
"trace_id": "7d4d29bea37e48ac8ba1f962d5eb8a41",
|
||||
"sampled": true,
|
||||
"span_count": {
|
||||
"started": 1,
|
||||
"dropped": {
|
||||
"total": 0
|
||||
}
|
||||
},
|
||||
"id": "e070bc3c732087f8",
|
||||
"name": "APIRestController#orders"
|
||||
},
|
||||
"context": {
|
||||
"request": {
|
||||
"url": {
|
||||
"full": "http://localhost:8080/api/orders",
|
||||
"hostname": "localhost",
|
||||
"port": "8080",
|
||||
"pathname": "/api/orders",
|
||||
"protocol": "http"
|
||||
},
|
||||
"socket": {
|
||||
"encrypted": false,
|
||||
"remote_address": "0:0:0:0:0:0:0:1"
|
||||
},
|
||||
"http_version": "1.1",
|
||||
"method": "GET",
|
||||
"headers": {
|
||||
"accept-encoding": "gzip, deflate",
|
||||
"host": "localhost:8080",
|
||||
"connection": "keep-alive",
|
||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/70.0.3508.0 Safari/537.36",
|
||||
"accept": "*/*",
|
||||
"referer": "http://localhost:8080/orders"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"finished": true,
|
||||
"headers_sent": true,
|
||||
"status_code": 200,
|
||||
"headers": {
|
||||
"Transfer-Encoding": "chunked",
|
||||
"Date": "Wed, 12 Sep 2018 15:16:07 GMT",
|
||||
"Content-Type": "application/json;charset=UTF-8"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"architecture": "x86_64",
|
||||
"platform": "Srens-MacBook-Pro.local",
|
||||
"ip": "172.18.0.1",
|
||||
"hostname": "Mac OS X"
|
||||
},
|
||||
"process": {
|
||||
"ppid": 10060,
|
||||
"title": "/Library/Java/JavaVirtualMachines/jdk-10.0.2.jdk/Contents/Home/bin/java",
|
||||
"pid": 10069
|
||||
},
|
||||
"service": {
|
||||
"language": {
|
||||
"version": "10.0.2",
|
||||
"name": "Java"
|
||||
},
|
||||
"runtime": {
|
||||
"name": "Java",
|
||||
"version": "10.0.2"
|
||||
},
|
||||
"name": "opbeans-java",
|
||||
"agent": {
|
||||
"name": "java",
|
||||
"version": "0.7.0-SNAPSHOT"
|
||||
}
|
||||
}
|
||||
},
|
||||
"beat": {
|
||||
"version": "7.0.0-alpha1",
|
||||
"name": "361022bff072",
|
||||
"hostname": "361022bff072"
|
||||
},
|
||||
"host": {
|
||||
"name": "361022bff072"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Span } from 'x-pack/plugins/apm/typings/Span';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||
import { getWaterfallRoot, IWaterfallItem } from './waterfall_helpers';
|
||||
|
||||
it('getWaterfallRoot', () => {
|
||||
const items: IWaterfallItem[] = [
|
||||
{
|
||||
id: 'd',
|
||||
parentId: 'c',
|
||||
serviceName: 'opbeans-java',
|
||||
name: 'SELECT',
|
||||
duration: 210,
|
||||
timestamp: 1536763736371000,
|
||||
offset: 0,
|
||||
docType: 'span',
|
||||
parentTransaction: {} as Transaction,
|
||||
span: {
|
||||
transaction: {
|
||||
id: 'c'
|
||||
}
|
||||
} as Span
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
parentId: 'a',
|
||||
serviceName: 'opbeans-java',
|
||||
name: 'GET [0:0:0:0:0:0:0:1]',
|
||||
duration: 4694,
|
||||
timestamp: 1536763736368000,
|
||||
offset: 0,
|
||||
docType: 'span',
|
||||
parentTransaction: {} as Transaction,
|
||||
span: {
|
||||
transaction: {
|
||||
id: 'a'
|
||||
}
|
||||
} as Span
|
||||
},
|
||||
{
|
||||
id: 'b2',
|
||||
parentId: 'a',
|
||||
serviceName: 'opbeans-java',
|
||||
name: 'GET [0:0:0:0:0:0:0:1]',
|
||||
duration: 4694,
|
||||
timestamp: 1536763736367000,
|
||||
offset: 0,
|
||||
docType: 'span',
|
||||
parentTransaction: {} as Transaction,
|
||||
span: {
|
||||
transaction: {
|
||||
id: 'a'
|
||||
}
|
||||
} as Span
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
parentId: 'b',
|
||||
serviceName: 'opbeans-java',
|
||||
name: 'APIRestController#productsRemote',
|
||||
duration: 3581,
|
||||
timestamp: 1536763736369000,
|
||||
offset: 0,
|
||||
docType: 'transaction',
|
||||
transaction: {} as Transaction
|
||||
},
|
||||
{
|
||||
id: 'a',
|
||||
serviceName: 'opbeans-java',
|
||||
name: 'APIRestController#products',
|
||||
duration: 9480,
|
||||
timestamp: 1536763736366000,
|
||||
offset: 0,
|
||||
docType: 'transaction',
|
||||
transaction: {} as Transaction
|
||||
}
|
||||
];
|
||||
|
||||
expect(getWaterfallRoot(items, items[4])).toMatchSnapshot();
|
||||
});
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { groupBy, indexBy, sortBy } from 'lodash';
|
||||
import { Span } from '../../../../../../../../typings/Span';
|
||||
import { Transaction } from '../../../../../../../../typings/Transaction';
|
||||
|
||||
export interface IWaterfallIndex {
|
||||
[key: string]: IWaterfallItem;
|
||||
}
|
||||
|
||||
export interface IWaterfall {
|
||||
duration: number;
|
||||
services: string[];
|
||||
childrenCount: number;
|
||||
root: IWaterfallItem;
|
||||
itemsById: IWaterfallIndex;
|
||||
}
|
||||
|
||||
interface IWaterfallItemBase {
|
||||
id: string | number;
|
||||
parentId?: string;
|
||||
serviceName: string;
|
||||
name: string;
|
||||
duration: number;
|
||||
timestamp: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
interface IWaterfallItemTransaction extends IWaterfallItemBase {
|
||||
transaction: Transaction;
|
||||
docType: 'transaction';
|
||||
children?: Array<IWaterfallItemSpan | IWaterfallItemTransaction>;
|
||||
}
|
||||
|
||||
interface IWaterfallItemSpan extends IWaterfallItemBase {
|
||||
parentTransaction: Transaction;
|
||||
span: Span;
|
||||
docType: 'span';
|
||||
children?: Array<IWaterfallItemSpan | IWaterfallItemTransaction>;
|
||||
}
|
||||
|
||||
export type IWaterfallItem = IWaterfallItemSpan | IWaterfallItemTransaction;
|
||||
|
||||
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
function getTransactionItem(
|
||||
transaction: Transaction
|
||||
): IWaterfallItemTransaction {
|
||||
if (transaction.version === 'v1') {
|
||||
return {
|
||||
id: transaction.transaction.id,
|
||||
serviceName: transaction.context.service.name,
|
||||
name: transaction.transaction.name,
|
||||
duration: transaction.transaction.duration.us,
|
||||
timestamp: new Date(transaction['@timestamp']).getTime() * 1000,
|
||||
offset: 0,
|
||||
docType: 'transaction',
|
||||
transaction
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: transaction.transaction.id,
|
||||
parentId: transaction.parent && transaction.parent.id,
|
||||
serviceName: transaction.context.service.name,
|
||||
name: transaction.transaction.name,
|
||||
duration: transaction.transaction.duration.us,
|
||||
timestamp: transaction.timestamp.us,
|
||||
offset: 0,
|
||||
docType: 'transaction',
|
||||
transaction
|
||||
};
|
||||
}
|
||||
|
||||
type PartialSpanItem = Omit<IWaterfallItemSpan, 'parentTransaction'>;
|
||||
|
||||
function getSpanItem(span: Span): PartialSpanItem {
|
||||
if (span.version === 'v1') {
|
||||
return {
|
||||
id: span.span.id,
|
||||
parentId: span.span.parent || span.transaction.id,
|
||||
serviceName: span.context.service.name,
|
||||
name: span.span.name,
|
||||
duration: span.span.duration.us,
|
||||
timestamp:
|
||||
new Date(span['@timestamp']).getTime() * 1000 + span.span.start.us,
|
||||
offset: 0,
|
||||
docType: 'span',
|
||||
span
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: span.span.hex_id,
|
||||
parentId: span.parent && span.parent.id,
|
||||
serviceName: span.context.service.name,
|
||||
name: span.span.name,
|
||||
duration: span.span.duration.us,
|
||||
timestamp: span.timestamp.us,
|
||||
offset: 0,
|
||||
docType: 'span',
|
||||
span
|
||||
};
|
||||
}
|
||||
|
||||
export function getWaterfallRoot(
|
||||
items: Array<PartialSpanItem | IWaterfallItemTransaction>,
|
||||
entryTransactionItem: IWaterfallItem
|
||||
) {
|
||||
const itemsByParentId = groupBy(
|
||||
items,
|
||||
item => (item.parentId ? item.parentId : 'root')
|
||||
);
|
||||
const itemsById: IWaterfallIndex = {};
|
||||
|
||||
const itemsByTransactionId = indexBy(
|
||||
items.filter(item => item.docType === 'transaction'),
|
||||
item => item.id
|
||||
) as { [key: string]: IWaterfallItemTransaction };
|
||||
|
||||
function getWithChildren(
|
||||
item: PartialSpanItem | IWaterfallItemTransaction
|
||||
): IWaterfallItem {
|
||||
const children = itemsByParentId[item.id] || [];
|
||||
const nextChildren = sortBy(children, 'timestamp').map(getWithChildren);
|
||||
let fullItem;
|
||||
|
||||
// add parent transaction to spans
|
||||
if (item.docType === 'span') {
|
||||
fullItem = {
|
||||
parentTransaction:
|
||||
itemsByTransactionId[item.span.transaction.id].transaction,
|
||||
...item,
|
||||
offset: item.timestamp - entryTransactionItem.timestamp,
|
||||
children: nextChildren
|
||||
};
|
||||
} else {
|
||||
fullItem = {
|
||||
...item,
|
||||
offset: item.timestamp - entryTransactionItem.timestamp,
|
||||
children: nextChildren
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Think about storing this tree as a single, flat, indexed structure
|
||||
// with "children" being an array of ids, instead of it being a real tree
|
||||
itemsById[item.id] = fullItem;
|
||||
|
||||
return fullItem;
|
||||
}
|
||||
|
||||
return { root: getWithChildren(entryTransactionItem), itemsById };
|
||||
}
|
||||
|
||||
export function getWaterfall(
|
||||
hits: Array<Span | Transaction>,
|
||||
services: string[],
|
||||
entryTransaction: Transaction
|
||||
): IWaterfall {
|
||||
const items = hits
|
||||
.filter(hit => {
|
||||
const docType = hit.processor.event;
|
||||
return ['span', 'transaction'].includes(docType);
|
||||
})
|
||||
.map(hit => {
|
||||
const docType = hit.processor.event;
|
||||
switch (docType) {
|
||||
case 'span':
|
||||
return getSpanItem(hit as Span);
|
||||
case 'transaction':
|
||||
return getTransactionItem(hit as Transaction);
|
||||
default:
|
||||
throw new Error(`Unknown type ${docType}`);
|
||||
}
|
||||
});
|
||||
|
||||
const entryTransactionItem = getTransactionItem(entryTransaction);
|
||||
const { root, itemsById } = getWaterfallRoot(items, entryTransactionItem);
|
||||
return {
|
||||
duration: root.duration,
|
||||
services,
|
||||
childrenCount: hits.length,
|
||||
root,
|
||||
itemsById
|
||||
};
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { zipObject } from 'lodash';
|
||||
import { colors } from '../../../../../style/variables';
|
||||
|
||||
interface IServiceColors {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export function getServiceColors(services: string[]): IServiceColors {
|
||||
const assignedColors = [
|
||||
colors.apmBlue,
|
||||
colors.apmGreen,
|
||||
colors.apmPurple,
|
||||
colors.apmRed2,
|
||||
colors.apmTan,
|
||||
colors.apmOrange,
|
||||
colors.apmYellow
|
||||
];
|
||||
|
||||
return zipObject(services, assignedColors);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||
import { getAgentMarks } from './get_agent_marks';
|
||||
|
||||
describe('getAgentMarks', () => {
|
||||
it('should sort the marks', () => {
|
||||
const transaction: Transaction = {
|
||||
transaction: {
|
||||
marks: {
|
||||
agent: {
|
||||
domInteractive: 117,
|
||||
timeToFirstByte: 10,
|
||||
domComplete: 118
|
||||
}
|
||||
}
|
||||
}
|
||||
} as any;
|
||||
expect(getAgentMarks(transaction)).toEqual([
|
||||
{ name: 'timeToFirstByte', us: 10000 },
|
||||
{ name: 'domInteractive', us: 117000 },
|
||||
{ name: 'domComplete', us: 118000 }
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return empty array if marks are missing', () => {
|
||||
const transaction: Transaction = {
|
||||
transaction: {}
|
||||
} as any;
|
||||
expect(getAgentMarks(transaction)).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { sortBy } from 'lodash';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||
|
||||
export interface AgentMark {
|
||||
name: string;
|
||||
us: number;
|
||||
}
|
||||
|
||||
export function getAgentMarks(transaction: Transaction): AgentMark[] {
|
||||
if (!transaction.transaction.marks) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return sortBy(
|
||||
Object.entries(transaction.transaction.marks.agent).map(([name, ms]) => ({
|
||||
name,
|
||||
us: ms * 1000
|
||||
})),
|
||||
'us'
|
||||
);
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
// @ts-ignore
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
TRACE_ID,
|
||||
TRANSACTION_ID
|
||||
} from '../../../../../../common/constants';
|
||||
import { Transaction } from '../../../../../../typings/Transaction';
|
||||
|
||||
import { RRRRender } from 'react-redux-request';
|
||||
import { WaterfallV1Request } from 'x-pack/plugins/apm/public/store/reactReduxRequest/waterfallV1';
|
||||
import { WaterfallV2Request } from 'x-pack/plugins/apm/public/store/reactReduxRequest/waterfallV2';
|
||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||
import { WaterfallResponse } from 'x-pack/plugins/apm/typings/waterfall';
|
||||
import { getAgentMarks } from './get_agent_marks';
|
||||
import { getServiceColors } from './getServiceColors';
|
||||
import { ServiceLegends } from './ServiceLegends';
|
||||
import { Waterfall } from './Waterfall';
|
||||
import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
|
||||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
transaction: Transaction;
|
||||
location: any;
|
||||
}
|
||||
|
||||
interface WaterfallRequestProps {
|
||||
urlParams: IUrlParams;
|
||||
transaction: Transaction;
|
||||
render: RRRRender<WaterfallResponse>;
|
||||
}
|
||||
|
||||
function WaterfallRequest({
|
||||
urlParams,
|
||||
transaction,
|
||||
render
|
||||
}: WaterfallRequestProps) {
|
||||
const hasTrace = transaction.hasOwnProperty('trace');
|
||||
if (hasTrace) {
|
||||
return (
|
||||
<WaterfallV2Request
|
||||
urlParams={urlParams}
|
||||
transaction={transaction}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<WaterfallV1Request
|
||||
urlParams={urlParams}
|
||||
transaction={transaction}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function WaterfallContainer({
|
||||
location,
|
||||
urlParams,
|
||||
transaction
|
||||
}: Props) {
|
||||
return (
|
||||
<WaterfallRequest
|
||||
urlParams={urlParams}
|
||||
transaction={transaction}
|
||||
render={({ data }) => {
|
||||
const agentMarks = getAgentMarks(transaction);
|
||||
const waterfall = getWaterfall(data.hits, data.services, transaction);
|
||||
if (!waterfall) {
|
||||
return null;
|
||||
}
|
||||
const serviceColors = getServiceColors(waterfall.services);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ServiceLegends serviceColors={serviceColors} />
|
||||
<Waterfall
|
||||
agentMarks={agentMarks}
|
||||
location={location}
|
||||
serviceColors={serviceColors}
|
||||
urlParams={urlParams}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getAgentMarks } from '../view';
|
||||
|
||||
describe('TransactionDetailsView', () => {
|
||||
describe('getAgentMarks', () => {
|
||||
it('should be sorted', () => {
|
||||
const transaction = {
|
||||
transaction: {
|
||||
marks: {
|
||||
agent: {
|
||||
domInteractive: 117,
|
||||
timeToFirstByte: 10,
|
||||
domComplete: 118
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
expect(getAgentMarks(transaction)).toEqual([
|
||||
{ name: 'timeToFirstByte', timeLabel: 10000, timeAxis: 10000 },
|
||||
{ name: 'domInteractive', timeLabel: 117000, timeAxis: 117000 },
|
||||
{ name: 'domComplete', timeLabel: 118000, timeAxis: 118000 }
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ensure they are not too close', () => {
|
||||
const transaction = {
|
||||
transaction: {
|
||||
duration: {
|
||||
us: 1000 * 1000
|
||||
},
|
||||
marks: {
|
||||
agent: {
|
||||
a: 0,
|
||||
b: 10,
|
||||
c: 11,
|
||||
d: 12,
|
||||
e: 968,
|
||||
f: 969,
|
||||
timeToFirstByte: 970,
|
||||
domInteractive: 980,
|
||||
domComplete: 990
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
expect(getAgentMarks(transaction)).toEqual([
|
||||
{ timeLabel: 0, name: 'a', timeAxis: 0 },
|
||||
{ timeLabel: 10000, name: 'b', timeAxis: 20000 },
|
||||
{ timeLabel: 11000, name: 'c', timeAxis: 40000 },
|
||||
{ timeLabel: 12000, name: 'd', timeAxis: 60000 },
|
||||
{ timeLabel: 968000, name: 'e', timeAxis: 910000 },
|
||||
{ timeLabel: 969000, name: 'f', timeAxis: 930000 },
|
||||
{ timeLabel: 970000, name: 'timeToFirstByte', timeAxis: 950000 },
|
||||
{ timeLabel: 980000, name: 'domInteractive', timeAxis: 970000 },
|
||||
{ timeLabel: 990000, name: 'domComplete', timeAxis: 990000 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import Transaction from './view';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
location: state.location
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {};
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Transaction);
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiToolTip
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React from 'react';
|
||||
import { Transaction as ITransaction } from '../../../../../typings/Transaction';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import EmptyMessage from '../../../shared/EmptyMessage';
|
||||
import { TraceLink } from '../../../shared/TraceLink';
|
||||
import { ActionMenu } from './ActionMenu';
|
||||
import { StickyTransactionProperties } from './StickyTransactionProperties';
|
||||
// @ts-ignore
|
||||
import { TransactionPropertiesTable } from './TransactionPropertiesTable';
|
||||
|
||||
function MaybeViewTraceLink({
|
||||
root,
|
||||
transaction
|
||||
}: {
|
||||
root: ITransaction;
|
||||
transaction: ITransaction;
|
||||
}) {
|
||||
const isRoot = transaction.transaction.id === root.transaction.id;
|
||||
let button;
|
||||
|
||||
if (isRoot || !root) {
|
||||
button = (
|
||||
<EuiToolTip content="Currently viewing the full trace">
|
||||
<EuiButton iconType="apmApp" disabled={true}>
|
||||
View full trace
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
);
|
||||
} else {
|
||||
button = <EuiButton iconType="apmApp">View full trace</EuiButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<TraceLink transaction={root}>{button}</TraceLink>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
transaction: ITransaction;
|
||||
urlParams: IUrlParams;
|
||||
location: Location;
|
||||
waterfallRoot?: ITransaction;
|
||||
}
|
||||
|
||||
export const Transaction: React.SFC<Props> = ({
|
||||
transaction,
|
||||
urlParams,
|
||||
location,
|
||||
waterfallRoot
|
||||
}) => {
|
||||
if (isEmpty(transaction)) {
|
||||
return (
|
||||
<EmptyMessage
|
||||
heading="No transaction sample available."
|
||||
subheading="Try another time range, reset the search filter or select another bucket from the distribution histogram."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const root = waterfallRoot || transaction;
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="m" hasShadow={true}>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<span>Transaction sample</span>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ActionMenu transaction={transaction} />
|
||||
</EuiFlexItem>
|
||||
<MaybeViewTraceLink transaction={transaction} root={root} />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<StickyTransactionProperties transaction={transaction} root={root} />
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<TransactionPropertiesTable
|
||||
transaction={transaction}
|
||||
location={location}
|
||||
urlParams={urlParams}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -1,218 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
unit,
|
||||
units,
|
||||
colors,
|
||||
px,
|
||||
borderRadius
|
||||
} from '../../../../style/variables';
|
||||
import { Tab, HeaderMedium } from '../../../shared/UIComponents';
|
||||
import { isEmpty, capitalize, get, sortBy, last } from 'lodash';
|
||||
|
||||
import StickyTransactionProperties from './StickyTransactionProperties';
|
||||
import {
|
||||
PropertiesTable,
|
||||
getPropertyTabNames
|
||||
} from '../../../shared/PropertiesTable';
|
||||
import Spans from './Spans';
|
||||
import DiscoverButton from '../../../shared/DiscoverButton';
|
||||
import {
|
||||
TRANSACTION_ID,
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_AGENT_NAME,
|
||||
TRANSACTION_DURATION
|
||||
} from '../../../../../common/constants';
|
||||
import { fromQuery, toQuery, history } from '../../../../utils/url';
|
||||
import EmptyMessage from '../../../shared/EmptyMessage';
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
border: 1px solid ${colors.gray4};
|
||||
border-radius: ${borderRadius};
|
||||
margin-top: ${px(units.plus)};
|
||||
`;
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: ${px(units.plus)} ${px(units.plus)} 0;
|
||||
margin-bottom: ${px(unit)};
|
||||
`;
|
||||
|
||||
const TabContainer = styled.div`
|
||||
padding: 0 ${px(units.plus)};
|
||||
border-bottom: 1px solid ${colors.gray4};
|
||||
`;
|
||||
|
||||
const TabContentContainer = styled.div`
|
||||
border-radius: 0 0 ${borderRadius} ${borderRadius};
|
||||
`;
|
||||
|
||||
const PropertiesTableContainer = styled.div`
|
||||
padding: ${px(units.plus)} ${px(units.plus)} 0;
|
||||
`;
|
||||
|
||||
const DEFAULT_TAB = 'timeline';
|
||||
|
||||
export function getAgentMarks(transaction) {
|
||||
const duration = get(transaction, TRANSACTION_DURATION);
|
||||
const threshold = (duration / 100) * 2;
|
||||
|
||||
return sortBy(
|
||||
Object.entries(get(transaction, 'transaction.marks.agent', [])),
|
||||
'1'
|
||||
)
|
||||
.map(([name, ms]) => ({
|
||||
name,
|
||||
timeLabel: ms * 1000,
|
||||
timeAxis: ms * 1000
|
||||
}))
|
||||
.reduce((acc, curItem) => {
|
||||
const prevTime = get(last(acc), 'timeAxis');
|
||||
const nextValidTime = prevTime + threshold;
|
||||
const isTooClose = prevTime != null && nextValidTime > curItem.timeAxis;
|
||||
const canFit = nextValidTime <= duration;
|
||||
|
||||
if (isTooClose && canFit) {
|
||||
acc.push({ ...curItem, timeAxis: nextValidTime });
|
||||
} else {
|
||||
acc.push(curItem);
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
.reduceRight((acc, curItem) => {
|
||||
const prevTime = get(last(acc), 'timeAxis');
|
||||
const nextValidTime = prevTime - threshold;
|
||||
const isTooClose = prevTime != null && nextValidTime < curItem.timeAxis;
|
||||
const canFit = nextValidTime >= 0;
|
||||
|
||||
if (isTooClose && canFit) {
|
||||
acc.push({ ...curItem, timeAxis: nextValidTime });
|
||||
} else {
|
||||
acc.push(curItem);
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
.reverse();
|
||||
}
|
||||
|
||||
// Ensure the selected tab exists or use the default
|
||||
function getCurrentTab(tabs = [], detailTab) {
|
||||
return tabs.includes(detailTab) ? detailTab : DEFAULT_TAB;
|
||||
}
|
||||
|
||||
function getTabs(transactionData) {
|
||||
const dynamicProps = Object.keys(transactionData.context || {});
|
||||
return getPropertyTabNames(dynamicProps);
|
||||
}
|
||||
|
||||
function Transaction({ transaction, location, urlParams }) {
|
||||
const { transactionId } = urlParams;
|
||||
|
||||
if (isEmpty(transaction)) {
|
||||
return (
|
||||
<EmptyMessage
|
||||
heading="No transaction sample available."
|
||||
subheading="Try another time range, reset the search filter or select another bucket from the distribution histogram."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const agentName = get(transaction, SERVICE_AGENT_NAME);
|
||||
const tabs = getTabs(transaction);
|
||||
const currentTab = getCurrentTab(tabs, urlParams.detailTab);
|
||||
|
||||
const discoverQuery = {
|
||||
_a: {
|
||||
interval: 'auto',
|
||||
query: {
|
||||
language: 'lucene',
|
||||
query: `${PROCESSOR_EVENT}:transaction AND ${TRANSACTION_ID}:${transactionId}`
|
||||
},
|
||||
sort: { '@timestamp': 'desc' }
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<HeaderContainer>
|
||||
<HeaderMedium
|
||||
css={`
|
||||
margin-top: ${px(units.quarter)};
|
||||
margin-bottom: 0;
|
||||
`}
|
||||
>
|
||||
Transaction sample
|
||||
</HeaderMedium>
|
||||
<DiscoverButton query={discoverQuery}>
|
||||
{`View transaction in Discover`}
|
||||
</DiscoverButton>
|
||||
</HeaderContainer>
|
||||
|
||||
<StickyTransactionProperties transaction={transaction} />
|
||||
|
||||
<TabContainer>
|
||||
{[DEFAULT_TAB, ...tabs].map(key => {
|
||||
return (
|
||||
<Tab
|
||||
onClick={() => {
|
||||
history.replace({
|
||||
...location,
|
||||
search: fromQuery({
|
||||
...toQuery(location.search),
|
||||
detailTab: key
|
||||
})
|
||||
});
|
||||
}}
|
||||
selected={currentTab === key}
|
||||
key={key}
|
||||
>
|
||||
{capitalize(key)}
|
||||
</Tab>
|
||||
);
|
||||
})}
|
||||
</TabContainer>
|
||||
|
||||
<TabContentContainer>
|
||||
{currentTab === DEFAULT_TAB ? (
|
||||
<Spans
|
||||
agentName={agentName}
|
||||
agentMarks={getAgentMarks(transaction)}
|
||||
droppedSpans={get(
|
||||
transaction,
|
||||
'transaction.spanCount.dropped.total',
|
||||
0
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<PropertiesTableContainer>
|
||||
<PropertiesTable
|
||||
propData={get(transaction.context, currentTab)}
|
||||
propKey={currentTab}
|
||||
agentName={agentName}
|
||||
/>
|
||||
</PropertiesTableContainer>
|
||||
)}
|
||||
</TabContentContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
Transaction.propTypes = {
|
||||
urlParams: PropTypes.object.isRequired,
|
||||
transaction: PropTypes.object
|
||||
};
|
||||
|
||||
Transaction.defaultProps = {
|
||||
transaction: {}
|
||||
};
|
||||
|
||||
export default Transaction;
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { TransactionDetailsView } from 'x-pack/plugins/apm/public/components/app/TransactionDetails/view';
|
||||
import { selectWaterfallRoot } from 'x-pack/plugins/apm/public/store/selectors/waterfall';
|
||||
import {
|
||||
getUrlParams,
|
||||
IUrlParams
|
||||
} from 'x-pack/plugins/apm/public/store/urlParams';
|
||||
import { Transaction } from '../../../../typings/Transaction';
|
||||
|
||||
interface Props {
|
||||
location: any;
|
||||
urlParams: IUrlParams;
|
||||
waterfallRoot: Transaction;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: any = {}, props: Partial<Props>) {
|
||||
return {
|
||||
location: state.location,
|
||||
urlParams: getUrlParams(state),
|
||||
waterfallRoot: selectWaterfallRoot(state, props)
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {};
|
||||
export const TransactionDetails = connect<{}, {}, Props>(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TransactionDetailsView);
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { HeaderLarge } from '../../shared/UIComponents';
|
||||
import Transaction from './Transaction';
|
||||
import Distribution from './Distribution';
|
||||
import { TransactionDetailsChartsRequest } from '../../../store/reactReduxRequest/transactionDetailsCharts';
|
||||
import Charts from '../../shared/charts/TransactionCharts';
|
||||
import { TransactionDistributionRequest } from '../../../store/reactReduxRequest/transactionDistribution';
|
||||
import { TransactionDetailsRequest } from '../../../store/reactReduxRequest/transactionDetails';
|
||||
import { KueryBar } from '../../shared/KueryBar';
|
||||
|
||||
function TransactionDetails({ urlParams, location }) {
|
||||
return (
|
||||
<div>
|
||||
<HeaderLarge>{urlParams.transactionName}</HeaderLarge>
|
||||
|
||||
<KueryBar />
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<TransactionDetailsChartsRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }) => (
|
||||
<Charts charts={data} urlParams={urlParams} location={location} />
|
||||
)}
|
||||
/>
|
||||
|
||||
<TransactionDistributionRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }) => (
|
||||
<Distribution distribution={data} urlParams={urlParams} />
|
||||
)}
|
||||
/>
|
||||
|
||||
<TransactionDetailsRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }) => (
|
||||
<Transaction transaction={data} urlParams={urlParams} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TransactionDetails;
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { RRRRenderArgs } from 'react-redux-request';
|
||||
import { Transaction as ITransaction } from '../../../../typings/Transaction';
|
||||
// @ts-ignore
|
||||
import { TransactionDetailsRequest } from '../../../store/reactReduxRequest/transactionDetails';
|
||||
// @ts-ignore
|
||||
import { TransactionDetailsChartsRequest } from '../../../store/reactReduxRequest/transactionDetailsCharts';
|
||||
import { TransactionDistributionRequest } from '../../../store/reactReduxRequest/transactionDistribution';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
// @ts-ignore
|
||||
import TransactionCharts from '../../shared/charts/TransactionCharts';
|
||||
// @ts-ignore
|
||||
import { KueryBar } from '../../shared/KueryBar';
|
||||
// @ts-ignore
|
||||
import { HeaderLarge } from '../../shared/UIComponents';
|
||||
import { Distribution } from './Distribution';
|
||||
import { Transaction } from './Transaction';
|
||||
|
||||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
location: any;
|
||||
waterfallRoot: ITransaction;
|
||||
}
|
||||
|
||||
export function TransactionDetailsView({
|
||||
urlParams,
|
||||
location,
|
||||
waterfallRoot
|
||||
}: Props) {
|
||||
return (
|
||||
<div>
|
||||
<HeaderLarge>{urlParams.transactionName}</HeaderLarge>
|
||||
|
||||
<KueryBar />
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<TransactionDetailsChartsRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }: RRRRenderArgs<any>) => (
|
||||
<TransactionCharts
|
||||
charts={data}
|
||||
urlParams={urlParams}
|
||||
location={location}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<TransactionDistributionRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }: RRRRenderArgs<any>) => (
|
||||
<Distribution
|
||||
distribution={data}
|
||||
urlParams={urlParams}
|
||||
location={location}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<TransactionDetailsRequest
|
||||
urlParams={urlParams}
|
||||
render={(res: RRRRenderArgs<any>) => {
|
||||
return (
|
||||
<Transaction
|
||||
location={location}
|
||||
transaction={res.data}
|
||||
urlParams={urlParams}
|
||||
waterfallRoot={waterfallRoot}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -4,11 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
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
|
||||
},
|
||||
sort: {
|
||||
field: 'impactRelative',
|
||||
direction: 'desc'
|
||||
}
|
||||
};
|
||||
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)}`;
|
||||
|
||||
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 (
|
||||
<TooltipOverlay content={transactionName || 'N/A'}>
|
||||
<TransactionNameLink path={`/${transactionUrl}`}>
|
||||
{transactionName || 'N/A'}
|
||||
</TransactionNameLink>
|
||||
</TooltipOverlay>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'avg',
|
||||
name: avgLabel(agentName),
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => asMillisWithDefault(value)
|
||||
},
|
||||
{
|
||||
field: 'p95',
|
||||
name: '95th percentile',
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => asMillisWithDefault(value)
|
||||
},
|
||||
{
|
||||
field: 'tpm',
|
||||
name: tpmLabel(type),
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => `${asDecimal(value)} ${tpmUnit(type)}`
|
||||
},
|
||||
{
|
||||
field: 'impactRelative',
|
||||
name: 'Impact',
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => <ImpactSparkline impact={value} />
|
||||
return (
|
||||
<TooltipOverlay content={transactionName || 'N/A'}>
|
||||
<TransactionNameLink path={`/${transactionUrl}`}>
|
||||
{transactionName || 'N/A'}
|
||||
</TransactionNameLink>
|
||||
</TooltipOverlay>
|
||||
);
|
||||
}
|
||||
];
|
||||
},
|
||||
{
|
||||
field: 'averageResponseTime',
|
||||
name: avgLabel(agentName),
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => asMillisWithDefault(value)
|
||||
},
|
||||
{
|
||||
field: 'p95',
|
||||
name: '95th percentile',
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => asMillisWithDefault(value)
|
||||
},
|
||||
{
|
||||
field: 'transactionsPerMinute',
|
||||
name: tpmLabel(type),
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => `${asDecimal(value)} ${tpmUnit(type)}`
|
||||
},
|
||||
{
|
||||
field: 'impact',
|
||||
name: 'Impact',
|
||||
sortable: true,
|
||||
dataType: 'number',
|
||||
render: value => <ImpactBar value={value} />
|
||||
}
|
||||
];
|
||||
|
||||
const sortedItems = orderBy(
|
||||
this.props.items,
|
||||
this.state.sort.field,
|
||||
this.state.sort.direction
|
||||
);
|
||||
|
||||
const paginatedItems = paginateItems({
|
||||
items: sortedItems,
|
||||
pageIndex: this.state.page.index,
|
||||
pageSize: this.state.page.size
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
noItemsMessage="No transactions found"
|
||||
items={paginatedItems}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
pageIndex: this.state.page.index,
|
||||
pageSize: this.state.page.size,
|
||||
totalItemCount: this.props.items.length
|
||||
}}
|
||||
sorting={{
|
||||
sort: {
|
||||
field: this.state.sort.field,
|
||||
direction: this.state.sort.direction
|
||||
}
|
||||
}}
|
||||
onChange={this.onTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ManagedTable
|
||||
columns={columns}
|
||||
items={items}
|
||||
initialSort={{ field: 'impact', direction: 'desc' }}
|
||||
initialPageSize={25}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
List.propTypes = {
|
||||
agentName: PropTypes.string,
|
||||
items: PropTypes.array,
|
||||
serviceName: PropTypes.string,
|
||||
type: PropTypes.string
|
||||
};
|
||||
|
||||
export default List;
|
||||
|
||||
// const renderFooterText = () => {
|
||||
// return items.length === 500
|
||||
// ? 'Showing first 500 results ordered by response time'
|
||||
// : '';
|
||||
// };
|
||||
|
|
|
@ -8,9 +8,14 @@ import React from 'react';
|
|||
import { KibanaLink } from '../../utils/url';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
|
||||
function DiscoverButton({ query, children }) {
|
||||
function DiscoverButton({ query, children, ...rest }) {
|
||||
return (
|
||||
<KibanaLink pathname={'/app/kibana'} hash={'/discover'} query={query}>
|
||||
<KibanaLink
|
||||
pathname={'/app/kibana'}
|
||||
hash={'/discover'}
|
||||
query={query}
|
||||
{...rest}
|
||||
>
|
||||
<EuiButton iconType="discoverApp">
|
||||
{children || 'View in Discover'}
|
||||
</EuiButton>
|
||||
|
|
|
@ -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 (
|
||||
<EuiEmptyPrompt
|
||||
titleSize="s"
|
||||
|
@ -22,13 +21,5 @@ function EmptyMessage({ heading, subheading, hideSubheading }) {
|
|||
);
|
||||
}
|
||||
|
||||
EmptyMessage.propTypes = {
|
||||
heading: PropTypes.string,
|
||||
hideSubheading: PropTypes.bool
|
||||
};
|
||||
|
||||
EmptyMessage.defaultProps = {
|
||||
hideSubheading: false
|
||||
};
|
||||
|
||||
// tslint:disable-next-line:no-default-export
|
||||
export default EmptyMessage;
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { ImpactBar } from '..';
|
||||
|
||||
describe('ImpactBar component', () => {
|
||||
it('should render with default values', () => {
|
||||
expect(shallow(<ImpactBar value={25} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render with overridden values', () => {
|
||||
expect(
|
||||
shallow(<ImpactBar value={2} max={5} color="danger" size="s" />)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ImpactBar component should render with default values 1`] = `
|
||||
<EuiProgress
|
||||
color="primary"
|
||||
max={100}
|
||||
position="static"
|
||||
size="l"
|
||||
value={25}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`ImpactBar component should render with overridden values 1`] = `
|
||||
<EuiProgress
|
||||
color="danger"
|
||||
max={5}
|
||||
position="static"
|
||||
size="s"
|
||||
value={2}
|
||||
/>
|
||||
`;
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiProgress } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { StringMap } from '../../../../typings/common';
|
||||
|
||||
// TODO: extend from EUI's EuiProgress prop interface
|
||||
export interface ImpactBarProps extends StringMap<any> {
|
||||
value: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export function ImpactBar({ value, max = 100, ...rest }: ImpactBarProps) {
|
||||
return (
|
||||
<EuiProgress size="l" value={value} max={max} color="primary" {...rest} />
|
||||
);
|
||||
}
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import view from './view';
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
import view from './view';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { ManagedTable } from '..';
|
||||
|
||||
describe('ManagedTable component', () => {
|
||||
let people;
|
||||
let columns;
|
||||
|
||||
beforeEach(() => {
|
||||
people = [
|
||||
{ name: 'Jess', age: 29 },
|
||||
{ name: 'Becky', age: 43 },
|
||||
{ name: 'Thomas', age: 31 }
|
||||
];
|
||||
columns = [
|
||||
{
|
||||
field: 'name',
|
||||
name: 'Name',
|
||||
sortable: true,
|
||||
render: name => `Name: ${name}`
|
||||
},
|
||||
{ field: 'age', name: 'Age', render: age => `Age: ${age}` }
|
||||
];
|
||||
});
|
||||
|
||||
it('should render a page-full of items, with defaults', () => {
|
||||
expect(
|
||||
shallow(<ManagedTable columns={columns} items={people} />)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render when specifying initial values', () => {
|
||||
expect(
|
||||
shallow(
|
||||
<ManagedTable
|
||||
columns={columns}
|
||||
items={people}
|
||||
initialSort={{ field: 'age', direction: 'desc' }}
|
||||
initialPageIndex={1}
|
||||
initialPageSize={2}
|
||||
hidePerPageOptions={false}
|
||||
/>
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ManagedTable component should render a page-full of items, with defaults 1`] = `
|
||||
<EuiBasicTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "name",
|
||||
"name": "Name",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"field": "age",
|
||||
"name": "Age",
|
||||
"render": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"age": 43,
|
||||
"name": "Becky",
|
||||
},
|
||||
Object {
|
||||
"age": 29,
|
||||
"name": "Jess",
|
||||
},
|
||||
Object {
|
||||
"age": 31,
|
||||
"name": "Thomas",
|
||||
},
|
||||
]
|
||||
}
|
||||
noItemsMessage="No items found"
|
||||
onChange={[Function]}
|
||||
pagination={
|
||||
Object {
|
||||
"hidePerPageOptions": true,
|
||||
"pageIndex": 0,
|
||||
"pageSize": 10,
|
||||
"totalItemCount": 3,
|
||||
}
|
||||
}
|
||||
responsive={true}
|
||||
sorting={
|
||||
Object {
|
||||
"sort": Object {
|
||||
"direction": "asc",
|
||||
"field": "name",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`ManagedTable component should render when specifying initial values 1`] = `
|
||||
<EuiBasicTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "name",
|
||||
"name": "Name",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"field": "age",
|
||||
"name": "Age",
|
||||
"render": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"age": 29,
|
||||
"name": "Jess",
|
||||
},
|
||||
]
|
||||
}
|
||||
noItemsMessage="No items found"
|
||||
onChange={[Function]}
|
||||
pagination={
|
||||
Object {
|
||||
"hidePerPageOptions": false,
|
||||
"pageIndex": 1,
|
||||
"pageSize": 2,
|
||||
"totalItemCount": 3,
|
||||
}
|
||||
}
|
||||
responsive={true}
|
||||
sorting={
|
||||
Object {
|
||||
"sort": Object {
|
||||
"direction": "desc",
|
||||
"field": "age",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import { EuiBasicTable } from '@elastic/eui';
|
||||
import { get, sortByOrder } from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { StringMap } from '../../../../typings/common';
|
||||
|
||||
// TODO: this should really be imported from EUI
|
||||
export interface ITableColumn {
|
||||
field: string;
|
||||
name: string;
|
||||
dataType?: string;
|
||||
align?: string;
|
||||
width?: string;
|
||||
sortable?: boolean;
|
||||
render: (value: any, item?: any) => any;
|
||||
}
|
||||
|
||||
export interface IManagedTableProps {
|
||||
items: Array<StringMap<any>>;
|
||||
columns: ITableColumn[];
|
||||
initialPageIndex?: number;
|
||||
initialPageSize?: number;
|
||||
hidePerPageOptions?: boolean;
|
||||
initialSort?: {
|
||||
field: string;
|
||||
direction: 'asc' | 'desc';
|
||||
};
|
||||
noItemsMessage?: any;
|
||||
}
|
||||
|
||||
export class ManagedTable extends Component<IManagedTableProps, any> {
|
||||
constructor(props: IManagedTableProps) {
|
||||
super(props);
|
||||
|
||||
const defaultSort = {
|
||||
field: get(props, 'columns[0].field', ''),
|
||||
direction: 'asc'
|
||||
};
|
||||
|
||||
const {
|
||||
initialPageIndex = 0,
|
||||
initialPageSize = 10,
|
||||
initialSort = defaultSort
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
page: { index: initialPageIndex, size: initialPageSize },
|
||||
sort: initialSort
|
||||
};
|
||||
}
|
||||
|
||||
public onTableChange = ({ page = {}, sort = {} }) => {
|
||||
this.setState({ page, sort });
|
||||
};
|
||||
|
||||
public getCurrentItems() {
|
||||
const { items } = this.props;
|
||||
const { sort = {}, page = {} } = this.state;
|
||||
// TODO: Use _.orderBy once we upgrade to lodash 4+
|
||||
const sorted = sortByOrder(items, sort.field, sort.direction);
|
||||
return sorted.slice(page.index * page.size, (page.index + 1) * page.size);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
columns,
|
||||
noItemsMessage,
|
||||
items,
|
||||
hidePerPageOptions = true
|
||||
} = this.props;
|
||||
const { page, sort } = this.state;
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
noItemsMessage={noItemsMessage}
|
||||
items={this.getCurrentItems()}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
hidePerPageOptions,
|
||||
totalItemCount: items.length,
|
||||
pageIndex: page.index,
|
||||
pageSize: page.size
|
||||
}}
|
||||
sorting={{ sort }}
|
||||
onChange={this.onTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,127 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Portal from 'react-portal';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { Close } from './Icons';
|
||||
import { fontSizes, units, colors } from '../../style/variables';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const HeaderTitle = styled.div`
|
||||
font-size: ${fontSizes.xlarge};
|
||||
`;
|
||||
|
||||
const CloseButton = styled(Close)`
|
||||
cursor: pointer;
|
||||
font-size: ${fontSizes.large};
|
||||
`;
|
||||
|
||||
const ModalFixed = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
`;
|
||||
|
||||
const ModalOverlay = styled(ModalFixed)`
|
||||
z-index: 10;
|
||||
background: ${rgba(colors.gray2, 0.8)};
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const ModalOuterContainer = styled(ModalFixed)`
|
||||
z-index: 20;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
const ModalInnerContainer = styled.div`
|
||||
position: relative;
|
||||
background: white;
|
||||
min-width: 800px;
|
||||
width: 80%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: ${units.double}px;
|
||||
border-radius: ${units.quarter}px;
|
||||
margin: ${units.quadruple}px 0;
|
||||
`;
|
||||
|
||||
class Modal extends React.Component {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
// TODO: Make sure this doesn't cause rendering issues
|
||||
return this.props.isOpen !== nextProps.isOpen;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
onOpen = () => {
|
||||
document.body.style.overflow = 'hidden';
|
||||
this.props.onOpen();
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
document.body.style.overflow = '';
|
||||
this.props.onClose();
|
||||
};
|
||||
|
||||
close = () => this.props.close();
|
||||
|
||||
render() {
|
||||
if (!this.props.isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal
|
||||
onClose={this.onClose}
|
||||
onOpen={this.onOpen}
|
||||
closeOnEsc
|
||||
isOpened={this.props.isOpen}
|
||||
>
|
||||
<div>
|
||||
<ModalOverlay />
|
||||
<ModalOuterContainer onClick={this.close}>
|
||||
<ModalInnerContainer onClick={e => e.stopPropagation()}>
|
||||
<Header>
|
||||
<HeaderTitle>{this.props.header}</HeaderTitle>
|
||||
<CloseButton onClick={this.close} />
|
||||
</Header>
|
||||
{this.props.children}
|
||||
</ModalInnerContainer>
|
||||
</ModalOuterContainer>
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Modal.propTypes = {
|
||||
children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
onOpen: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
close: PropTypes.func,
|
||||
isOpen: PropTypes.bool
|
||||
};
|
||||
|
||||
Modal.defaultProps = {
|
||||
onOpen: () => {},
|
||||
onClose: () => {},
|
||||
close: () => {}
|
||||
};
|
||||
|
||||
export default Modal;
|
|
@ -7,6 +7,8 @@
|
|||
import _ from 'lodash';
|
||||
import 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<any>, parentKey?: string) => string[];
|
||||
|
||||
const Table = styled.table`
|
||||
font-family: ${fontFamilyCode};
|
||||
font-size: ${fontSizes.small};
|
||||
|
|
|
@ -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(
|
||||
<AgentFeatureTipMessage
|
||||
featureName={featureName}
|
||||
agentName={agentName}
|
||||
/>
|
||||
)
|
||||
).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(
|
||||
<AgentFeatureTipMessage
|
||||
|
@ -139,10 +120,11 @@ describe('AgentFeatureTipMessage component', () => {
|
|||
/>
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
expect(getAgentFeatureDocsUrl).toHaveBeenCalledWith(featureName, agentName);
|
||||
});
|
||||
|
||||
it('should render null empty string when no docs are returned', () => {
|
||||
mockDocs = null;
|
||||
getAgentFeatureDocsUrl.mockImplementation(() => null);
|
||||
expect(
|
||||
shallow(
|
||||
<AgentFeatureTipMessage
|
||||
|
|
|
@ -8,7 +8,7 @@ exports[`AgentFeatureTipMessage component should render when docs are returned 1
|
|||
size="m"
|
||||
type="iInCircle"
|
||||
/>
|
||||
Mock Docs Text
|
||||
You can configure your agent to add contextual information about your users.
|
||||
|
||||
<ExternalLink
|
||||
href="mock-url"
|
||||
|
@ -18,17 +18,6 @@ exports[`AgentFeatureTipMessage component should render when docs are returned 1
|
|||
</styled.div>
|
||||
`;
|
||||
|
||||
exports[`AgentFeatureTipMessage component should render when docs are returned, but missing a url 1`] = `
|
||||
<styled.div>
|
||||
<EuiIcon
|
||||
size="m"
|
||||
type="iInCircle"
|
||||
/>
|
||||
Mock Docs Text
|
||||
|
||||
</styled.div>
|
||||
`;
|
||||
|
||||
exports[`PropertiesTable component should render empty when data has no keys 1`] = `
|
||||
<styled.div>
|
||||
<styled.div>
|
||||
|
@ -68,7 +57,7 @@ exports[`PropertiesTable component should render with data 1`] = `
|
|||
/>
|
||||
<AgentFeatureTipMessage
|
||||
agentName="testAgentName"
|
||||
featureName="context-testPropKey"
|
||||
featureName="testPropKey"
|
||||
/>
|
||||
</styled.div>
|
||||
`;
|
||||
|
|
|
@ -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 (
|
||||
<TableInfo>
|
||||
<EuiIcon type="iInCircle" />
|
||||
{docs.text}{' '}
|
||||
{docs.url && (
|
||||
<ExternalLink href={docs.url}>
|
||||
Learn more in the documentation.
|
||||
</ExternalLink>
|
||||
)}
|
||||
{getAgentFeatureText(featureName)}{' '}
|
||||
<ExternalLink href={docsUrl}>
|
||||
Learn more in the documentation.
|
||||
</ExternalLink>
|
||||
</TableInfo>
|
||||
);
|
||||
}
|
||||
|
@ -78,7 +88,7 @@ export function PropertiesTable({
|
|||
}: {
|
||||
propData: StringMap<any>;
|
||||
propKey: string;
|
||||
agentName: string;
|
||||
agentName?: string;
|
||||
}) {
|
||||
if (_.isEmpty(propData)) {
|
||||
return (
|
||||
|
@ -98,10 +108,7 @@ export function PropertiesTable({
|
|||
keySorter={sortKeysByConfig}
|
||||
depth={1}
|
||||
/>
|
||||
<AgentFeatureTipMessage
|
||||
featureName={`context-${propKey}`}
|
||||
agentName={agentName}
|
||||
/>
|
||||
<AgentFeatureTipMessage featureName={propKey} agentName={agentName} />
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
// @ts-ignore
|
||||
import { KibanaLink } from '../../utils/url';
|
||||
|
||||
export function SetupInstructionsLink({
|
||||
buttonFill = false
|
||||
}: {
|
||||
buttonFill?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<KibanaLink pathname={'/app/kibana'} hash={'/home/tutorial/apm'}>
|
||||
<EuiButton size="s" color="primary" fill={buttonFill}>
|
||||
Setup Instructions
|
||||
</EuiButton>
|
||||
</KibanaLink>
|
||||
);
|
||||
}
|
|
@ -11,8 +11,7 @@ import CodePreview from '../../shared/CodePreview';
|
|||
import { Ellipsis } from '../../shared/Icons';
|
||||
import { 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 (
|
||||
<div>
|
||||
<HeaderXSmall>Stacktraces</HeaderXSmall>
|
||||
<EuiTitle size="xs">
|
||||
<h3>Stack traces</h3>
|
||||
</EuiTitle>
|
||||
{getCollapsedLibraryFrames(stackframes).map((item, i) => {
|
||||
if (!item.libraryFrame) {
|
||||
return (
|
||||
|
|
|
@ -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`] = `
|
|||
}
|
||||
|
||||
<div
|
||||
className="c0"
|
||||
className="euiFlexGroup euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap"
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": "-1em",
|
||||
"marginTop": "-1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="c1"
|
||||
className="euiFlexItem"
|
||||
style={
|
||||
Object {
|
||||
"minWidth": 0,
|
||||
"padding": "1em 1em 1em 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="c2"
|
||||
className="c0"
|
||||
>
|
||||
<span
|
||||
aria-describedby="overlay1"
|
||||
|
@ -68,12 +59,12 @@ exports[`StickyProperties should render 1`] = `
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="c3"
|
||||
className="c1"
|
||||
>
|
||||
1337 minutes ago (mocking 1536405447)
|
||||
|
||||
<span
|
||||
className="c4"
|
||||
className="c2"
|
||||
>
|
||||
(
|
||||
1st of January (mocking 1536405447)
|
||||
|
@ -82,10 +73,16 @@ exports[`StickyProperties should render 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="c1"
|
||||
className="euiFlexItem"
|
||||
style={
|
||||
Object {
|
||||
"minWidth": 0,
|
||||
"padding": "1em 1em 1em 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="c2"
|
||||
className="c0"
|
||||
>
|
||||
<span
|
||||
aria-describedby="overlay2"
|
||||
|
@ -97,7 +94,7 @@ exports[`StickyProperties should render 1`] = `
|
|||
</div>
|
||||
<span
|
||||
aria-describedby="overlay3"
|
||||
className="c5"
|
||||
className="c3"
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
>
|
||||
|
@ -105,10 +102,16 @@ exports[`StickyProperties should render 1`] = `
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="c1"
|
||||
className="euiFlexItem"
|
||||
style={
|
||||
Object {
|
||||
"minWidth": 0,
|
||||
"padding": "1em 1em 1em 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="c2"
|
||||
className="c0"
|
||||
>
|
||||
<span
|
||||
aria-describedby="overlay4"
|
||||
|
@ -119,16 +122,22 @@ exports[`StickyProperties should render 1`] = `
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="c3"
|
||||
className="c1"
|
||||
>
|
||||
GET
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="c1"
|
||||
className="euiFlexItem"
|
||||
style={
|
||||
Object {
|
||||
"minWidth": 0,
|
||||
"padding": "1em 1em 1em 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="c2"
|
||||
className="c0"
|
||||
>
|
||||
<span
|
||||
aria-describedby="overlay5"
|
||||
|
@ -139,16 +148,22 @@ exports[`StickyProperties should render 1`] = `
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="c3"
|
||||
className="c1"
|
||||
>
|
||||
true
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="c1"
|
||||
className="euiFlexItem"
|
||||
style={
|
||||
Object {
|
||||
"minWidth": 0,
|
||||
"padding": "1em 1em 1em 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="c2"
|
||||
className="c0"
|
||||
>
|
||||
<span
|
||||
aria-describedby="overlay6"
|
||||
|
@ -159,7 +174,7 @@ exports[`StickyProperties should render 1`] = `
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="c3"
|
||||
className="c1"
|
||||
>
|
||||
1337
|
||||
</div>
|
||||
|
|
|
@ -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 (
|
||||
<span>
|
||||
Field name: <br />
|
||||
<TooltipFieldName>{name}</TooltipFieldName>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
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 <PropertyValue>{String(val)}</PropertyValue>;
|
||||
return <PropertyValue>{val}</PropertyValue>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<PropertiesContainer>
|
||||
<EuiFlexGroup wrap={true} gutterSize="none" style={groupStyles}>
|
||||
{stickyProperties &&
|
||||
stickyProperties.map((prop, i) => (
|
||||
<Property key={i}>
|
||||
{getPropertyLabel(prop)}
|
||||
{getPropertyValue(prop)}
|
||||
</Property>
|
||||
))}
|
||||
</PropertiesContainer>
|
||||
stickyProperties.map(({ width = 0, ...prop }, i) => {
|
||||
return (
|
||||
<EuiFlexItem
|
||||
key={i}
|
||||
style={{
|
||||
minWidth: width,
|
||||
...itemStyles
|
||||
}}
|
||||
>
|
||||
{getPropertyLabel(prop)}
|
||||
{getPropertyValue(prop)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<OverlayTrigger
|
||||
|
@ -27,13 +21,4 @@ function TooltipOverlay({ children, content, delay = 1000 }) {
|
|||
);
|
||||
}
|
||||
|
||||
export function fieldNameHelper(name) {
|
||||
return (
|
||||
<span>
|
||||
Field name: <br />
|
||||
<TooltipFieldName>{name}</TooltipFieldName>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default TooltipOverlay;
|
||||
|
|
58
x-pack/plugins/apm/public/components/shared/TraceLink.tsx
Normal file
58
x-pack/plugins/apm/public/components/shared/TraceLink.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Transaction } from '../../../typings/Transaction';
|
||||
import { KibanaLink, legacyEncodeURIComponent } from '../../utils/url';
|
||||
|
||||
interface TraceLinkProps {
|
||||
transaction: Transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path and query used to build a trace link,
|
||||
* given either a v2 Transaction or a Transaction Group
|
||||
*/
|
||||
export function getLinkProps(transaction: Transaction) {
|
||||
const serviceName = transaction.context.service.name;
|
||||
const transactionType = transaction.transaction.type;
|
||||
const traceId =
|
||||
transaction.version === 'v2' ? transaction.trace.id : undefined;
|
||||
const transactionId = transaction.transaction.id;
|
||||
const name = transaction.transaction.name;
|
||||
|
||||
const encodedName = legacyEncodeURIComponent(name);
|
||||
|
||||
return {
|
||||
hash: `/${serviceName}/transactions/${transactionType}/${encodedName}`,
|
||||
query: {
|
||||
traceId,
|
||||
transactionId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const TraceLink: React.SFC<TraceLinkProps> = ({
|
||||
transaction,
|
||||
children
|
||||
}) => {
|
||||
if (!transaction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const linkProps = getLinkProps(transaction);
|
||||
|
||||
if (!linkProps) {
|
||||
// TODO: Should this case return unlinked children, null, or something else?
|
||||
return <React.Fragment>{children}</React.Fragment>;
|
||||
}
|
||||
|
||||
return (
|
||||
<KibanaLink pathname="/app/apm" {...linkProps}>
|
||||
{children}
|
||||
</KibanaLink>
|
||||
);
|
||||
};
|
|
@ -5,14 +5,7 @@
|
|||
*/
|
||||
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
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};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -28,7 +28,8 @@ export default function AgentMarker({ agentMark, x }) {
|
|||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: px(x - legendWidth / 2)
|
||||
left: px(x - legendWidth / 2),
|
||||
bottom: '-6px'
|
||||
}}
|
||||
>
|
||||
<EuiToolTip
|
||||
|
@ -37,7 +38,7 @@ export default function AgentMarker({ agentMark, x }) {
|
|||
content={
|
||||
<div>
|
||||
<NameContainer>{agentMark.name}</NameContainer>
|
||||
<TimeContainer>{asTime(agentMark.timeLabel)}</TimeContainer>
|
||||
<TimeContainer>{asTime(agentMark.us)}</TimeContainer>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
<XYPlot
|
||||
dontCheckIfEmpty
|
||||
width={width}
|
||||
height={40}
|
||||
height={margins.top}
|
||||
margin={{
|
||||
top: 40,
|
||||
top: margins.top,
|
||||
left: margins.left,
|
||||
right: margins.right
|
||||
}}
|
||||
|
@ -65,9 +64,9 @@ function TimelineAxis({ header, plotValues, agentMarks }) {
|
|||
|
||||
{agentMarks.map(agentMark => (
|
||||
<AgentMarker
|
||||
key={agentMark.timeAxis}
|
||||
key={agentMark.name}
|
||||
agentMark={agentMark}
|
||||
x={xScale(agentMark.timeAxis)}
|
||||
x={xScale(agentMark.us)}
|
||||
/>
|
||||
))}
|
||||
</XYPlot>
|
||||
|
|
|
@ -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 (
|
||||
<div
|
||||
|
|
|
@ -57,21 +57,18 @@ exports[`Timeline should render with data 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<div>
|
||||
Hello - i am a header
|
||||
</div>
|
||||
<div
|
||||
className="rv-xy-plot "
|
||||
style={
|
||||
Object {
|
||||
"height": "40px",
|
||||
"height": "100px",
|
||||
"width": "1000px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="rv-xy-plot__inner"
|
||||
height={40}
|
||||
height={100}
|
||||
onClick={[Function]}
|
||||
onDoubleClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
|
@ -94,7 +91,7 @@ exports[`Timeline should render with data 1`] = `
|
|||
>
|
||||
<g
|
||||
className="rv-xy-plot__axis__ticks"
|
||||
transform="translate(0, 40)"
|
||||
transform="translate(0, 100)"
|
||||
>
|
||||
<g
|
||||
className="rv-xy-plot__axis__tick"
|
||||
|
@ -519,7 +516,7 @@ exports[`Timeline should render with data 1`] = `
|
|||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(950, 40)"
|
||||
transform="translate(950, 100)"
|
||||
>
|
||||
<text
|
||||
dy="0"
|
||||
|
@ -533,6 +530,7 @@ exports[`Timeline should render with data 1`] = `
|
|||
<div
|
||||
style={
|
||||
Object {
|
||||
"bottom": "-6px",
|
||||
"left": "484.2043232706185px",
|
||||
"position": "absolute",
|
||||
}
|
||||
|
@ -562,6 +560,7 @@ exports[`Timeline should render with data 1`] = `
|
|||
<div
|
||||
style={
|
||||
Object {
|
||||
"bottom": "-6px",
|
||||
"left": "528.1747555976804px",
|
||||
"position": "absolute",
|
||||
}
|
||||
|
@ -591,6 +590,7 @@ exports[`Timeline should render with data 1`] = `
|
|||
<div
|
||||
style={
|
||||
Object {
|
||||
"bottom": "-6px",
|
||||
"left": "879.9382142141751px",
|
||||
"position": "absolute",
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
},
|
||||
"animation": null,
|
||||
"agentMarks": [
|
||||
{ "timeLabel": 100000, "name": "timeToFirstByte", "timeAxis": 100000 },
|
||||
{ "timeLabel": 110000, "name": "domInteractive", "timeAxis": 110000 },
|
||||
{ "timeLabel": 190000, "name": "domComplete", "timeAxis": 190000 }
|
||||
{ "name": "timeToFirstByte", "us": 100000 },
|
||||
{ "name": "domInteractive", "us": 110000 },
|
||||
{ "name": "domComplete", "us": 190000 }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -8,35 +8,22 @@ import React, { PureComponent } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { makeWidthFlexible } from 'react-vis';
|
||||
import { createSelector } from 'reselect';
|
||||
import { getPlotValues } from './plotUtils';
|
||||
import TimelineAxis from './TimelineAxis';
|
||||
import VerticalLines from './VerticalLines';
|
||||
|
||||
class Timeline extends PureComponent {
|
||||
getPlotValues = createSelector(
|
||||
state => 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 (
|
||||
<div>
|
||||
<TimelineAxis
|
||||
plotValues={plotValues}
|
||||
agentMarks={agentMarks}
|
||||
header={header}
|
||||
/>
|
||||
<TimelineAxis plotValues={plotValues} agentMarks={agentMarks} />
|
||||
<VerticalLines plotValues={plotValues} agentMarks={agentMarks} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<ServiceListItemResponse> {
|
||||
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<ServiceResponse> {
|
||||
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<ITransactionGroup[]> {
|
||||
const groups: ITransactionGroup[] = await callApi({
|
||||
pathname: '/api/apm/traces',
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
esFilterQuery: await getEncodedEsQuery(kuery)
|
||||
}
|
||||
});
|
||||
|
||||
return groups.map(group => {
|
||||
group.sample = addVersion(group.sample);
|
||||
return group;
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadTransactionList({
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
transactionType
|
||||
}) {
|
||||
return callApi({
|
||||
}: IUrlParams): Promise<ITransactionGroup[]> {
|
||||
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<IDistributionResponse> {
|
||||
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<T extends Span | Transaction>(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<Span[]> {
|
||||
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: {
|
|
@ -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: {}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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));
|
||||
|
|
30
x-pack/plugins/apm/public/store/mockData/mockTraceList.json
Normal file
30
x-pack/plugins/apm/public/store/mockData/mockTraceList.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
[
|
||||
{
|
||||
"name": "log",
|
||||
"serviceName": "flask-server",
|
||||
"averageResponseTime": 1329,
|
||||
"tracesPerMinute": 3201,
|
||||
"impact": 70
|
||||
},
|
||||
{
|
||||
"name": "products/item",
|
||||
"serviceName": "client",
|
||||
"averageResponseTime": 2301,
|
||||
"tracesPerMinute": 5432,
|
||||
"impact": 42
|
||||
},
|
||||
{
|
||||
"name": "billing/payment",
|
||||
"serviceName": "client",
|
||||
"averageResponseTime": 789,
|
||||
"tracesPerMinute": 1201,
|
||||
"impact": 14
|
||||
},
|
||||
{
|
||||
"name": "user/profile",
|
||||
"serviceName": "client",
|
||||
"averageResponseTime": 1212,
|
||||
"tracesPerMinute": 904,
|
||||
"impact": 92
|
||||
}
|
||||
]
|
|
@ -20,7 +20,7 @@ export function getErrorDistribution(state) {
|
|||
export function ErrorDistributionRequest({ urlParams, render }) {
|
||||
const { serviceName, start, end, errorGroupId, kuery } = urlParams;
|
||||
|
||||
if (!(serviceName, start, end, errorGroupId)) {
|
||||
if (!(serviceName && start && end && errorGroupId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue