mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Discover][APM] Custom overview tab for data_stream.type: "traces" in the new trace data source profile detail flyout (#210612)
## Summary Closes https://github.com/elastic/kibana/issues/208693 and https://github.com/elastic/kibana/issues/211785 This PR introduces a new Overview tab that will appear when the new traces data source profile is enabled, and a `data_stream.type: "traces"` document is being checked.  ### Fields highlighted for clarity |Transaction|Span| |-|-| ||| ### Actions available for each field  ### Detailed flat name for the dependency field Since the source of the Dependency field cannot be easily assumed, we've decided to add some metadata for it.  ### Filtering by transaction name on a span It's important to note that `transaction.name` may not be present in a span document. As a result, filtering by this field would likely return only transaction documents, as they have `transaction.name` defined. cc: @patpscal  ## How to test * Add the following to your `kibana.dev.yml`: ```yaml discover.experimental.enabledProfiles: - traces-data-source-profile ``` * Run synthtrace with `traces_logs_entitities.ts` ``` node scripts/synthtrace traces_logs_entities.ts --from=now-24h --to=now ``` * Go to Discover and make sure the APM data view is selected * Open the flyout for any document with `data_stream.type: "traces"` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com> Co-authored-by: Carlos Crespo <crespocarlos@users.noreply.github.com>
This commit is contained in:
parent
4e4819ca20
commit
af3409518f
44 changed files with 2046 additions and 18 deletions
|
@ -16,3 +16,21 @@ export interface TransactionDetailsByTraceIdLocatorParams extends SerializableRe
|
|||
rangeTo?: string;
|
||||
traceId: string;
|
||||
}
|
||||
|
||||
export const DEPENDENCY_OVERVIEW_LOCATOR_ID = 'dependencyOverviewLocator';
|
||||
|
||||
export interface DependencyOverviewParams extends SerializableRecord {
|
||||
dependencyName: string;
|
||||
environment?: string;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
}
|
||||
|
||||
export const TRANSACTION_DETAILS_BY_NAME_LOCATOR = 'TransactionDetailsByNameLocator';
|
||||
|
||||
export interface TransactionDetailsByNameParams extends SerializableRecord {
|
||||
serviceName: string;
|
||||
transactionName: string;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
|
||||
export interface DependencyOverviewParams extends SerializableRecord {
|
||||
dependencyName: string;
|
||||
environment?: string;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
}
|
||||
|
||||
export const DEPENDENCY_OVERVIEW_LOCATOR_ID = 'dependencyOverviewLocator';
|
|
@ -37,6 +37,7 @@ export {
|
|||
formatHit,
|
||||
getDocId,
|
||||
getLogDocumentOverview,
|
||||
getTraceDocumentOverview,
|
||||
getIgnoredReason,
|
||||
getMessageFieldWithFallbacks,
|
||||
getShouldShowFieldHandler,
|
||||
|
|
|
@ -15,13 +15,21 @@ export const MESSAGE_FIELD = 'message';
|
|||
export const ERROR_MESSAGE_FIELD = 'error.message';
|
||||
export const EVENT_ORIGINAL_FIELD = 'event.original';
|
||||
export const EVENT_OUTCOME_FIELD = 'event.outcome';
|
||||
export const INDEX_FIELD = '_index';
|
||||
|
||||
// Trace fields
|
||||
export const TRACE_ID_FIELD = 'trace.id';
|
||||
export const PARENT_ID_FIELD = 'parent.id';
|
||||
export const TRANSACTION_ID_FIELD = 'transaction.id';
|
||||
export const TRANSACTION_NAME_FIELD = 'transaction.name';
|
||||
export const TRANSACTION_DURATION_FIELD = 'transaction.duration.us';
|
||||
export const SPAN_NAME_FIELD = 'span.name';
|
||||
export const SPAN_ACTION_FIELD = 'span.action';
|
||||
export const SPAN_DURATION_FIELD = 'span.duration.us';
|
||||
export const SPAN_TYPE_FIELD = 'span.type';
|
||||
export const SPAN_SUBTYPE_FIELD = 'span.subtype';
|
||||
export const SPAN_DESTINATION_SERVICE_RESOURCE_FIELD = 'span.destination.service.resource';
|
||||
export const PROCESSOR_EVENT_FIELD = 'processor.event';
|
||||
|
||||
export const LOG_FILE_PATH_FIELD = 'log.file.path';
|
||||
export const DATASTREAM_NAMESPACE_FIELD = 'data_stream.namespace';
|
||||
|
@ -36,12 +44,16 @@ export const CLOUD_AVAILABILITY_ZONE_FIELD = 'cloud.availability_zone';
|
|||
export const CLOUD_PROJECT_ID_FIELD = 'cloud.project.id';
|
||||
export const CLOUD_INSTANCE_ID_FIELD = 'cloud.instance.id';
|
||||
export const SERVICE_NAME_FIELD = 'service.name';
|
||||
export const SERVICE_ENVIRONMENT_FIELD = 'service.environment';
|
||||
export const ORCHESTRATOR_CLUSTER_NAME_FIELD = 'orchestrator.cluster.name';
|
||||
export const ORCHESTRATOR_CLUSTER_ID_FIELD = 'orchestrator.cluster.id';
|
||||
export const ORCHESTRATOR_RESOURCE_ID_FIELD = 'orchestrator.resource.id';
|
||||
export const ORCHESTRATOR_NAMESPACE_FIELD = 'orchestrator.namespace';
|
||||
export const CONTAINER_NAME_FIELD = 'container.name';
|
||||
export const CONTAINER_ID_FIELD = 'container.id';
|
||||
export const USER_AGENT_NAME_FIELD = 'user_agent.name';
|
||||
export const USER_AGENT_VERSION_FIELD = 'user_agent.version';
|
||||
export const HTTP_RESPONSE_STATUS_CODE_FIELD = 'http.response.status_code';
|
||||
|
||||
// Degraded Docs
|
||||
export const IGNORED_FIELD = '_ignored';
|
||||
|
@ -52,5 +64,3 @@ export const DEGRADED_DOCS_FIELDS = [IGNORED_FIELD, IGNORED_FIELD_VALUES_FIELD]
|
|||
export const ERROR_STACK_TRACE = 'error.stack_trace';
|
||||
export const ERROR_EXCEPTION_STACKTRACE_ABS_PATH = 'error.exception.stacktrace.abs_path';
|
||||
export const ERROR_LOG_STACKTRACE_ABS_PATH = 'error.log.stacktrace.abs_path';
|
||||
|
||||
export const INDEX_FIELD = '_index';
|
||||
|
|
|
@ -112,3 +112,41 @@ export interface LogCloudFields {
|
|||
'cloud.project.id'?: string;
|
||||
'cloud.instance.id'?: string;
|
||||
}
|
||||
|
||||
export interface TraceDocumentOverview
|
||||
extends ServiceFields,
|
||||
TransactionTraceFields,
|
||||
SpanTraceFields,
|
||||
UserAgentTraceFields {
|
||||
'@timestamp': number;
|
||||
'trace.id': string;
|
||||
'parent.id'?: string;
|
||||
'http.response.status_code'?: number;
|
||||
'processor.event'?: 'span' | 'transaction';
|
||||
}
|
||||
|
||||
export interface ServiceFields {
|
||||
'service.name': string;
|
||||
'service.environment': string;
|
||||
'agent.name': string;
|
||||
}
|
||||
|
||||
export interface TransactionTraceFields {
|
||||
'transaction.id'?: string;
|
||||
'transaction.name'?: string;
|
||||
'transaction.duration.us'?: number;
|
||||
}
|
||||
|
||||
export interface SpanTraceFields {
|
||||
'span.name'?: string;
|
||||
'span.action'?: string;
|
||||
'span.duration.us'?: number;
|
||||
'span.type'?: string;
|
||||
'span.subtype'?: string;
|
||||
'span.destination.service.resource'?: string;
|
||||
}
|
||||
|
||||
export interface UserAgentTraceFields {
|
||||
'user_agent.name'?: string;
|
||||
'user_agent.version'?: string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { castArray } from 'lodash';
|
||||
import { DataTableRecord, TraceDocumentOverview, fieldConstants } from '../..';
|
||||
|
||||
export function getTraceDocumentOverview(doc: DataTableRecord): TraceDocumentOverview {
|
||||
const formatField = <T extends keyof TraceDocumentOverview>(field: T) =>
|
||||
castArray(doc.flattened[field])[0] as TraceDocumentOverview[T];
|
||||
|
||||
const fields: Array<keyof TraceDocumentOverview> = [
|
||||
fieldConstants.TIMESTAMP_FIELD,
|
||||
fieldConstants.PARENT_ID_FIELD,
|
||||
fieldConstants.HTTP_RESPONSE_STATUS_CODE_FIELD,
|
||||
fieldConstants.TRACE_ID_FIELD,
|
||||
fieldConstants.SERVICE_NAME_FIELD,
|
||||
fieldConstants.SERVICE_ENVIRONMENT_FIELD,
|
||||
fieldConstants.AGENT_NAME_FIELD,
|
||||
fieldConstants.TRANSACTION_ID_FIELD,
|
||||
fieldConstants.TRANSACTION_NAME_FIELD,
|
||||
fieldConstants.TRANSACTION_DURATION_FIELD,
|
||||
fieldConstants.SPAN_NAME_FIELD,
|
||||
fieldConstants.SPAN_ACTION_FIELD,
|
||||
fieldConstants.SPAN_DURATION_FIELD,
|
||||
fieldConstants.SPAN_TYPE_FIELD,
|
||||
fieldConstants.SPAN_SUBTYPE_FIELD,
|
||||
fieldConstants.SPAN_DESTINATION_SERVICE_RESOURCE_FIELD,
|
||||
fieldConstants.USER_AGENT_NAME_FIELD,
|
||||
fieldConstants.USER_AGENT_VERSION_FIELD,
|
||||
fieldConstants.PROCESSOR_EVENT_FIELD,
|
||||
];
|
||||
|
||||
return fields.reduce((acc, field) => {
|
||||
acc[field] = formatField(field);
|
||||
return acc;
|
||||
}, {} as { [key in keyof TraceDocumentOverview]?: string | number }) as TraceDocumentOverview;
|
||||
}
|
|
@ -14,6 +14,7 @@ export * from './format_value';
|
|||
export * from './get_doc_id';
|
||||
export * from './get_ignored_reason';
|
||||
export * from './get_log_document_overview';
|
||||
export * from './get_trace_document_overview';
|
||||
export * from './get_message_field_with_fallbacks';
|
||||
export * from './get_should_show_field_handler';
|
||||
export * from './get_stack_trace_fields';
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { UnifiedDocViewerTracesOverview } from '@kbn/unified-doc-viewer-plugin/public';
|
||||
import { DocViewsRegistry } from '@kbn/unified-doc-viewer';
|
||||
import { DATASTREAM_TYPE_FIELD, PROCESSOR_EVENT_FIELD, getFieldValue } from '@kbn/discover-utils';
|
||||
import { DocViewerExtensionParams, DocViewerExtension } from '../../../../types';
|
||||
|
||||
export const getDocViewer =
|
||||
(prev: (params: DocViewerExtensionParams) => DocViewerExtension) =>
|
||||
(params: DocViewerExtensionParams) => {
|
||||
const prevValue = prev(params);
|
||||
const isTrace = getFieldValue(params.record, DATASTREAM_TYPE_FIELD) === 'traces';
|
||||
|
||||
if (!isTrace) {
|
||||
return prevValue;
|
||||
}
|
||||
const processorEvent = getFieldValue(params.record, PROCESSOR_EVENT_FIELD);
|
||||
|
||||
const documentType =
|
||||
processorEvent === 'span'
|
||||
? i18n.translate('discover.docViews.tracesOverview.spanTitle', {
|
||||
defaultMessage: 'Span',
|
||||
})
|
||||
: i18n.translate('discover.docViews.tracesOverview.transactionTitle', {
|
||||
defaultMessage: 'Transaction',
|
||||
});
|
||||
|
||||
return {
|
||||
...prevValue,
|
||||
docViewsRegistry: (registry: DocViewsRegistry) => {
|
||||
registry.add({
|
||||
id: 'doc_view_traces_overview',
|
||||
title: i18n.translate('discover.docViews.tracesOverview.title', {
|
||||
defaultMessage: '{documentType} overview',
|
||||
values: { documentType },
|
||||
}),
|
||||
order: 0,
|
||||
component: (props) => {
|
||||
return <UnifiedDocViewerTracesOverview {...props} />;
|
||||
},
|
||||
});
|
||||
|
||||
return prevValue.docViewsRegistry(registry);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import { DataSourceType, isDataSourceType } from '../../../../../common/data_sources';
|
||||
import { DataSourceCategory, DataSourceProfileProvider } from '../../../profiles';
|
||||
import { getDocViewer } from './accessors/get_doc_viewer';
|
||||
import { getCellRenderers } from './accessors';
|
||||
|
||||
export const createTracesDataSourceProfileProvider = (): DataSourceProfileProvider => ({
|
||||
|
@ -27,6 +28,7 @@ export const createTracesDataSourceProfileProvider = (): DataSourceProfileProvid
|
|||
],
|
||||
rowHeight: 5,
|
||||
}),
|
||||
getDocViewer,
|
||||
getCellRenderers,
|
||||
},
|
||||
resolve: ({ dataSource }) => {
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { getUnifiedDocViewerServices } from '../../../plugin';
|
||||
import { TransactionProvider, useTransactionContext } from './use_transaction';
|
||||
|
||||
jest.mock('../../../plugin', () => ({
|
||||
getUnifiedDocViewerServices: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('rxjs', () => {
|
||||
const originalModule = jest.requireActual('rxjs');
|
||||
return {
|
||||
...originalModule,
|
||||
lastValueFrom: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockSearch = jest.fn();
|
||||
const mockAddDanger = jest.fn();
|
||||
(getUnifiedDocViewerServices as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
search: {
|
||||
search: mockSearch,
|
||||
},
|
||||
},
|
||||
core: {
|
||||
notifications: {
|
||||
toasts: {
|
||||
addDanger: mockAddDanger,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(lastValueFrom as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
describe('useTransaction hook', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<TransactionProvider transactionId="test-transaction" indexPattern="test-index">
|
||||
{children}
|
||||
</TransactionProvider>
|
||||
);
|
||||
|
||||
it('should start with loading true and transaction as null', async () => {
|
||||
(lastValueFrom as jest.Mock).mockResolvedValue({});
|
||||
|
||||
const { result } = renderHook(() => useTransactionContext(), { wrapper });
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
expect(lastValueFrom).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should update transaction when data is fetched successfully', async () => {
|
||||
const transactionName = 'Test Transaction';
|
||||
(lastValueFrom as jest.Mock).mockResolvedValue({
|
||||
rawResponse: {
|
||||
hits: {
|
||||
hits: [{ _source: { transaction: { name: transactionName } } }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useTransactionContext(), { wrapper });
|
||||
|
||||
await waitFor(() => !result.current.loading);
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.transaction?.name).toBe(transactionName);
|
||||
expect(lastValueFrom).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle errors and set transaction.name as empty string, and show a toast error', async () => {
|
||||
const errorMessage = 'Search error';
|
||||
(lastValueFrom as jest.Mock).mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
const { result } = renderHook(() => useTransactionContext(), { wrapper });
|
||||
|
||||
await waitFor(() => !result.current.loading);
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.transaction).toEqual({ name: '' });
|
||||
expect(lastValueFrom).toHaveBeenCalledTimes(1);
|
||||
expect(mockAddDanger).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: 'An error occurred while fetching the transaction',
|
||||
text: errorMessage,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should set transaction.name as empty string and stop loading when transactionId is not provided', async () => {
|
||||
const wrapperWithoutTransactionId = ({ children }: { children: React.ReactNode }) => (
|
||||
<TransactionProvider transactionId={undefined} indexPattern="test-index">
|
||||
{children}
|
||||
</TransactionProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useTransactionContext(), {
|
||||
wrapper: wrapperWithoutTransactionId,
|
||||
});
|
||||
|
||||
await waitFor(() => !result.current.loading);
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.transaction).toEqual({ name: '' });
|
||||
expect(lastValueFrom).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import createContainer from 'constate';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getUnifiedDocViewerServices } from '../../../plugin';
|
||||
|
||||
interface UseTransactionPrams {
|
||||
transactionId?: string;
|
||||
indexPattern: string;
|
||||
}
|
||||
|
||||
interface GetTransactionParams {
|
||||
transactionId: string;
|
||||
indexPattern: string;
|
||||
data: DataPublicPluginStart;
|
||||
}
|
||||
|
||||
async function getTransactionData({ transactionId, indexPattern, data }: GetTransactionParams) {
|
||||
return lastValueFrom(
|
||||
data.search.search({
|
||||
params: {
|
||||
index: indexPattern,
|
||||
size: 1,
|
||||
body: {
|
||||
timeout: '20s',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'transaction.id': transactionId,
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'processor.event': 'transaction',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const useTransaction = ({ transactionId, indexPattern }: UseTransactionPrams) => {
|
||||
const { data, core } = getUnifiedDocViewerServices();
|
||||
const [transaction, setTransaction] = useState<{ [key: string]: string } | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (transactionId) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await getTransactionData({ transactionId, indexPattern, data });
|
||||
const transactionName = result.rawResponse.hits.hits[0]?._source.transaction?.name;
|
||||
|
||||
setTransaction(transactionName ? { name: transactionName } : null);
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
core.notifications.toasts.addDanger({
|
||||
title: i18n.translate('unifiedDocViewer.docViewerTracesOverview.useTransaction.error', {
|
||||
defaultMessage: 'An error occurred while fetching the transaction',
|
||||
}),
|
||||
text: error.message,
|
||||
});
|
||||
|
||||
setTransaction({ name: '' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
setTransaction({ name: '' });
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [core.notifications.toasts, data, indexPattern, transactionId]);
|
||||
|
||||
return { loading, transaction };
|
||||
};
|
||||
|
||||
export const [TransactionProvider, useTransactionContext] = createContainer(useTransaction);
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { TracesOverview } from './traces_overview';
|
||||
|
||||
// Required for usage in React.lazy
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default TracesOverview;
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import {
|
||||
HTTP_RESPONSE_STATUS_CODE_FIELD,
|
||||
SERVICE_NAME_FIELD,
|
||||
SPAN_DESTINATION_SERVICE_RESOURCE_FIELD,
|
||||
SPAN_DURATION_FIELD,
|
||||
SPAN_NAME_FIELD,
|
||||
SPAN_SUBTYPE_FIELD,
|
||||
SPAN_TYPE_FIELD,
|
||||
TIMESTAMP_FIELD,
|
||||
TRACE_ID_FIELD,
|
||||
TRANSACTION_DURATION_FIELD,
|
||||
TRANSACTION_NAME_FIELD,
|
||||
USER_AGENT_NAME_FIELD,
|
||||
USER_AGENT_VERSION_FIELD,
|
||||
} from '@kbn/discover-utils';
|
||||
|
||||
export const spanFields = [
|
||||
SPAN_NAME_FIELD,
|
||||
TRANSACTION_NAME_FIELD,
|
||||
SERVICE_NAME_FIELD,
|
||||
TRACE_ID_FIELD,
|
||||
SPAN_DESTINATION_SERVICE_RESOURCE_FIELD,
|
||||
TIMESTAMP_FIELD,
|
||||
SPAN_DURATION_FIELD,
|
||||
HTTP_RESPONSE_STATUS_CODE_FIELD,
|
||||
SPAN_TYPE_FIELD,
|
||||
SPAN_SUBTYPE_FIELD,
|
||||
];
|
||||
export const transactionFields = [
|
||||
TRANSACTION_NAME_FIELD,
|
||||
SERVICE_NAME_FIELD,
|
||||
TRACE_ID_FIELD,
|
||||
TIMESTAMP_FIELD,
|
||||
TRANSACTION_DURATION_FIELD,
|
||||
HTTP_RESPONSE_STATUS_CODE_FIELD,
|
||||
USER_AGENT_NAME_FIELD,
|
||||
USER_AGENT_VERSION_FIELD,
|
||||
];
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import {
|
||||
SPAN_NAME_FIELD,
|
||||
TRANSACTION_NAME_FIELD,
|
||||
SERVICE_NAME_FIELD,
|
||||
TRACE_ID_FIELD,
|
||||
SPAN_DESTINATION_SERVICE_RESOURCE_FIELD,
|
||||
TIMESTAMP_FIELD,
|
||||
SPAN_DURATION_FIELD,
|
||||
TRANSACTION_DURATION_FIELD,
|
||||
HTTP_RESPONSE_STATUS_CODE_FIELD,
|
||||
SPAN_TYPE_FIELD,
|
||||
USER_AGENT_NAME_FIELD,
|
||||
TraceDocumentOverview,
|
||||
SPAN_SUBTYPE_FIELD,
|
||||
USER_AGENT_VERSION_FIELD,
|
||||
AGENT_NAME_FIELD,
|
||||
SERVICE_ENVIRONMENT_FIELD,
|
||||
} from '@kbn/discover-utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { EuiBadge, EuiText } from '@elastic/eui';
|
||||
import { PartialFieldMetadataPlain } from '@kbn/fields-metadata-plugin/common';
|
||||
import { ServiceNameLink } from '../sub_components/service_name_link';
|
||||
import { TraceIdLink } from '../sub_components/trace_id_link';
|
||||
import { TransactionNameLink } from '../sub_components/transaction_name_link';
|
||||
import { Timestamp } from '../sub_components/timestamp';
|
||||
import { DependencyNameLink } from '../sub_components/dependency_name_link';
|
||||
import { HttpStatusCode } from '../sub_components/http_status_code';
|
||||
import { asDuration } from '../utils';
|
||||
|
||||
type FieldConfigValue = string | number | undefined;
|
||||
|
||||
export interface FieldConfiguration {
|
||||
title: string;
|
||||
content: (value: FieldConfigValue) => React.ReactNode;
|
||||
value: FieldConfigValue;
|
||||
fieldMetadata?: PartialFieldMetadataPlain;
|
||||
}
|
||||
|
||||
export const getFieldConfiguration = (
|
||||
attributes: TraceDocumentOverview
|
||||
): Record<string, FieldConfiguration> => {
|
||||
return {
|
||||
[SPAN_NAME_FIELD]: {
|
||||
title: i18n.translate('unifiedDocViewer.tracesOverview.details.spanName.title', {
|
||||
defaultMessage: 'Span name',
|
||||
}),
|
||||
content: (value) => <EuiText size="xs">{value}</EuiText>,
|
||||
value: attributes[SPAN_NAME_FIELD],
|
||||
},
|
||||
[TRANSACTION_NAME_FIELD]: {
|
||||
title: i18n.translate('unifiedDocViewer.tracesOverview.details.transactionName.title', {
|
||||
defaultMessage: 'Transaction name',
|
||||
}),
|
||||
content: (value) => (
|
||||
<TransactionNameLink
|
||||
serviceName={attributes[SERVICE_NAME_FIELD]}
|
||||
transactionName={value as string}
|
||||
/>
|
||||
),
|
||||
value: attributes[TRANSACTION_NAME_FIELD],
|
||||
},
|
||||
[SERVICE_NAME_FIELD]: {
|
||||
title: i18n.translate('unifiedDocViewer.tracesOverview.details.service.title', {
|
||||
defaultMessage: 'Service',
|
||||
}),
|
||||
content: (value) => (
|
||||
<ServiceNameLink serviceName={value as string} agentName={attributes[AGENT_NAME_FIELD]} />
|
||||
),
|
||||
value: attributes[SERVICE_NAME_FIELD],
|
||||
},
|
||||
[TRACE_ID_FIELD]: {
|
||||
title: i18n.translate('unifiedDocViewer.tracesOverview.details.traceId.title', {
|
||||
defaultMessage: 'Trace ID',
|
||||
}),
|
||||
content: (value) => <TraceIdLink traceId={value as string} />,
|
||||
value: attributes[TRACE_ID_FIELD],
|
||||
},
|
||||
[SPAN_DESTINATION_SERVICE_RESOURCE_FIELD]: {
|
||||
title: i18n.translate(
|
||||
'unifiedDocViewer.tracesOverview.details.spanDestinationServiceResource.title',
|
||||
{
|
||||
defaultMessage: 'Dependency',
|
||||
}
|
||||
),
|
||||
content: (value) => (
|
||||
<DependencyNameLink
|
||||
dependencyName={value as string}
|
||||
environment={attributes[SERVICE_ENVIRONMENT_FIELD]}
|
||||
/>
|
||||
),
|
||||
value: attributes[SPAN_DESTINATION_SERVICE_RESOURCE_FIELD],
|
||||
fieldMetadata: {
|
||||
flat_name: 'span.destination.service.resource',
|
||||
},
|
||||
},
|
||||
[TIMESTAMP_FIELD]: {
|
||||
title: i18n.translate('unifiedDocViewer.tracesOverview.details.timestamp.title', {
|
||||
defaultMessage: 'Start time',
|
||||
}),
|
||||
content: (value) => <Timestamp timestamp={value as number} />,
|
||||
value: attributes[TIMESTAMP_FIELD],
|
||||
},
|
||||
[SPAN_DURATION_FIELD]: {
|
||||
title: i18n.translate('unifiedDocViewer.tracesOverview.details.spanDuration.title', {
|
||||
defaultMessage: 'Duration',
|
||||
}),
|
||||
content: (value) => <EuiText size="xs">{asDuration(value as number)}</EuiText>,
|
||||
value: attributes[SPAN_DURATION_FIELD] ?? 0,
|
||||
},
|
||||
[TRANSACTION_DURATION_FIELD]: {
|
||||
title: i18n.translate('unifiedDocViewer.tracesOverview.details.transactionDuration.title', {
|
||||
defaultMessage: 'Duration',
|
||||
}),
|
||||
content: (value) => <EuiText size="xs">{asDuration(value as number)}</EuiText>,
|
||||
value: attributes[TRANSACTION_DURATION_FIELD] ?? 0,
|
||||
},
|
||||
[SPAN_TYPE_FIELD]: {
|
||||
title: i18n.translate('unifiedDocViewer.tracesOverview.details.spanType.title', {
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
content: (value) => <EuiBadge color="hollow">{value}</EuiBadge>,
|
||||
value: attributes[SPAN_TYPE_FIELD],
|
||||
},
|
||||
[SPAN_SUBTYPE_FIELD]: {
|
||||
title: i18n.translate('unifiedDocViewer.tracesOverview.details.spanSubtype.title', {
|
||||
defaultMessage: 'Subtype',
|
||||
}),
|
||||
content: (value) => <EuiBadge color="hollow">{value}</EuiBadge>,
|
||||
value: attributes[SPAN_SUBTYPE_FIELD],
|
||||
},
|
||||
[HTTP_RESPONSE_STATUS_CODE_FIELD]: {
|
||||
title: i18n.translate(
|
||||
'unifiedDocViewer.tracesOverview.details.httpResponseStatusCode.title',
|
||||
{
|
||||
defaultMessage: 'Status code',
|
||||
}
|
||||
),
|
||||
content: (value) => <HttpStatusCode code={value as number} />,
|
||||
value: attributes[HTTP_RESPONSE_STATUS_CODE_FIELD],
|
||||
},
|
||||
[USER_AGENT_NAME_FIELD]: {
|
||||
title: i18n.translate('unifiedDocViewer.tracesOverview.details.userAgent.title', {
|
||||
defaultMessage: 'User agent',
|
||||
}),
|
||||
content: (value) => <EuiText size="xs">{value}</EuiText>,
|
||||
value: attributes[USER_AGENT_NAME_FIELD],
|
||||
},
|
||||
[USER_AGENT_VERSION_FIELD]: {
|
||||
title: i18n.translate('unifiedDocViewer.tracesOverview.details.userAgentVersion.title', {
|
||||
defaultMessage: 'User agent version',
|
||||
}),
|
||||
content: (value) => <EuiText size="xs">{value}</EuiText>,
|
||||
value: attributes[USER_AGENT_VERSION_FIELD],
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui';
|
||||
import { getRouterLinkProps } from '@kbn/router-utils';
|
||||
import { DEPENDENCY_OVERVIEW_LOCATOR_ID } from '@kbn/deeplinks-observability';
|
||||
import { getUnifiedDocViewerServices } from '../../../plugin';
|
||||
|
||||
interface DependencyNameLinkProps {
|
||||
dependencyName: string;
|
||||
environment: string;
|
||||
}
|
||||
|
||||
export function DependencyNameLink({ dependencyName, environment }: DependencyNameLinkProps) {
|
||||
const {
|
||||
share: { url: urlService },
|
||||
core,
|
||||
data: dataService,
|
||||
} = getUnifiedDocViewerServices();
|
||||
|
||||
const canViewApm = core.application.capabilities.apm?.show || false;
|
||||
const { from: timeRangeFrom, to: timeRangeTo } =
|
||||
dataService.query.timefilter.timefilter.getTime();
|
||||
|
||||
const apmLinkToDependencyOverviewLocator = urlService.locators.get<{
|
||||
dependencyName: string;
|
||||
environment: string;
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
}>(DEPENDENCY_OVERVIEW_LOCATOR_ID);
|
||||
|
||||
const href = apmLinkToDependencyOverviewLocator?.getRedirectUrl({
|
||||
dependencyName,
|
||||
environment,
|
||||
rangeFrom: timeRangeFrom,
|
||||
rangeTo: timeRangeTo,
|
||||
});
|
||||
|
||||
const routeLinkProps = href
|
||||
? getRouterLinkProps({
|
||||
href,
|
||||
onClick: () => {
|
||||
// TODO add telemetry (https://github.com/elastic/kibana/issues/208919)
|
||||
apmLinkToDependencyOverviewLocator?.navigate({
|
||||
dependencyName,
|
||||
environment,
|
||||
rangeFrom: timeRangeFrom,
|
||||
rangeTo: timeRangeTo,
|
||||
});
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const content = (
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiText size="xs">{dependencyName}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
return canViewApm && routeLinkProps ? (
|
||||
<EuiLink {...routeLinkProps} data-test-subj="unifiedDocViewTracesOverviewDependencyNameLink">
|
||||
{content}
|
||||
</EuiLink>
|
||||
) : (
|
||||
content
|
||||
);
|
||||
}
|
|
@ -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
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiPopover,
|
||||
EuiButtonIcon,
|
||||
EuiPopoverTitle,
|
||||
EuiToolTip,
|
||||
PopoverAnchorPosition,
|
||||
type EuiPopoverProps,
|
||||
} from '@elastic/eui';
|
||||
import { useUIFieldActions } from '../../../../hooks/use_field_actions';
|
||||
|
||||
interface HoverPopoverActionProps {
|
||||
children: React.ReactChild;
|
||||
field: string;
|
||||
value: unknown;
|
||||
formattedValue?: string;
|
||||
title: string;
|
||||
anchorPosition?: PopoverAnchorPosition;
|
||||
display?: EuiPopoverProps['display'];
|
||||
}
|
||||
|
||||
export const FieldHoverActionPopover = ({
|
||||
children,
|
||||
title,
|
||||
field,
|
||||
value,
|
||||
formattedValue,
|
||||
anchorPosition = 'upCenter',
|
||||
display = 'inline-block',
|
||||
}: HoverPopoverActionProps) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const leaveTimer = useRef<NodeJS.Timeout | null>(null);
|
||||
const uiFieldActions = useUIFieldActions({ field, value, formattedValue });
|
||||
|
||||
const clearTimeoutIfExists = () => {
|
||||
if (leaveTimer.current) {
|
||||
clearTimeout(leaveTimer.current);
|
||||
}
|
||||
};
|
||||
|
||||
// The timeout hack is required because we are using a Popover which ideally should be used with a mouseclick,
|
||||
// but we are using it as a Tooltip. Which means we now need to manually handle the open and close
|
||||
// state using the mouse hover events. This cause the popover to close even before the user could
|
||||
// navigate actions inside it. Hence, to prevent this, we need this hack
|
||||
const onMouseEnter = () => {
|
||||
clearTimeoutIfExists();
|
||||
setIsPopoverOpen(true);
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
leaveTimer.current = setTimeout(() => {
|
||||
return setIsPopoverOpen(false);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
useEffect(function onUnmount() {
|
||||
return () => {
|
||||
clearTimeoutIfExists();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<span onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<EuiPopover
|
||||
button={children}
|
||||
isOpen={isPopoverOpen}
|
||||
anchorPosition={anchorPosition}
|
||||
closePopover={closePopoverPlaceholder}
|
||||
panelPaddingSize="s"
|
||||
panelStyle={{ minWidth: '24px' }}
|
||||
display={display}
|
||||
>
|
||||
<EuiPopoverTitle className="eui-textBreakWord" css={{ maxWidth: '200px' }}>
|
||||
{title}
|
||||
</EuiPopoverTitle>
|
||||
<EuiFlexGroup wrap gutterSize="none" alignItems="center" justifyContent="spaceBetween">
|
||||
{uiFieldActions.map((action) => (
|
||||
<EuiToolTip content={action.label} key={action.id}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="unifiedDocViewTracesOverviewFieldHoverActionPopoverButton"
|
||||
size="xs"
|
||||
iconType={action.iconType}
|
||||
aria-label={action.label}
|
||||
onClick={action.onClick}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiPopover>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const closePopoverPlaceholder = () => {};
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiLoadingSpinner, EuiTitle } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { PartialFieldMetadataPlain } from '@kbn/fields-metadata-plugin/common';
|
||||
import { FieldHoverActionPopover } from './field_hover_popover_action';
|
||||
|
||||
export interface FieldWithActionsProps {
|
||||
field: string;
|
||||
fieldMetadata?: PartialFieldMetadataPlain;
|
||||
formattedValue: string;
|
||||
label: string;
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function FieldWithActions({
|
||||
field,
|
||||
fieldMetadata,
|
||||
formattedValue,
|
||||
label,
|
||||
value,
|
||||
loading,
|
||||
children,
|
||||
...props
|
||||
}: FieldWithActionsProps) {
|
||||
const hasFieldDescription = !!fieldMetadata?.flat_name;
|
||||
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxxs">
|
||||
<h3>{label}</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{hasFieldDescription && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip content={fieldMetadata.flat_name} color="subdued" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={2}>
|
||||
<FieldHoverActionPopover title={value} value={value} field={field}>
|
||||
<div className="eui-textBreakWord">
|
||||
{loading && <EuiLoadingSpinner size="m" />}
|
||||
{children}
|
||||
</div>
|
||||
</FieldHoverActionPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { HttpStatusCode } from '.';
|
||||
import { httpStatusCodes } from './http_status_codes';
|
||||
|
||||
jest.mock('@elastic/eui', () => ({
|
||||
...jest.requireActual('@elastic/eui'),
|
||||
useEuiTheme: jest.fn(),
|
||||
}));
|
||||
|
||||
const euiColorVisGrey0 = '111';
|
||||
const euiColorVisSuccess0 = '222';
|
||||
const euiColorVisWarning0 = '333';
|
||||
const euiColorVisDanger0 = '444';
|
||||
|
||||
const expectTextInBadge = (text: string) =>
|
||||
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeText')?.innerHTML).toEqual(
|
||||
text
|
||||
);
|
||||
|
||||
describe('HttpStatusCode', () => {
|
||||
beforeEach(() => {
|
||||
(useEuiTheme as jest.Mock).mockReturnValue({
|
||||
euiTheme: {
|
||||
colors: {
|
||||
vis: {
|
||||
euiColorVisGrey0,
|
||||
euiColorVisSuccess0,
|
||||
euiColorVisWarning0,
|
||||
euiColorVisDanger0,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render the status code and its description correctly', () => {
|
||||
const code = 200;
|
||||
render(<HttpStatusCode code={code} />);
|
||||
|
||||
expectTextInBadge(`${code} ${httpStatusCodes[code.toString()]}`);
|
||||
});
|
||||
|
||||
it('should return correct color for status codes starting with 1', () => {
|
||||
const code = 100;
|
||||
render(<HttpStatusCode code={code} />);
|
||||
|
||||
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeBadge')).toHaveStyle(
|
||||
`background-color: ${euiColorVisGrey0}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should return correct color for status codes starting with 2', () => {
|
||||
const code = 200;
|
||||
render(<HttpStatusCode code={code} />);
|
||||
|
||||
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeBadge')).toHaveStyle(
|
||||
`background-color: ${euiColorVisSuccess0}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should return correct color for status codes starting with 3', () => {
|
||||
const code = 300;
|
||||
render(<HttpStatusCode code={code} />);
|
||||
|
||||
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeBadge')).toHaveStyle(
|
||||
`background-color: ${euiColorVisGrey0}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should return correct color for status codes starting with 4', () => {
|
||||
const code = 400;
|
||||
render(<HttpStatusCode code={code} />);
|
||||
|
||||
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeBadge')).toHaveStyle(
|
||||
`background-color: ${euiColorVisWarning0}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should return correct color for status codes starting with 5', () => {
|
||||
const code = 500;
|
||||
render(<HttpStatusCode code={code} />);
|
||||
|
||||
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeBadge')).toHaveStyle(
|
||||
`background-color: ${euiColorVisDanger0}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should return correct color for status codes starting with 7', () => {
|
||||
const code = 700;
|
||||
render(<HttpStatusCode code={code} />);
|
||||
|
||||
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeBadge')).toHaveStyle(
|
||||
`background-color: ${euiColorVisDanger0}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should return default color for unknown status codes', () => {
|
||||
const code = 999;
|
||||
render(<HttpStatusCode code={code} />);
|
||||
|
||||
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeBadge')).toHaveStyle(
|
||||
'background-color: default'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
// From https://github.com/for-GET/know-your-http-well/blob/master/json/status-codes.json
|
||||
export const httpStatusCodes: { [key: string]: string } = {
|
||||
'100': 'Continue',
|
||||
'101': 'Switching Protocols',
|
||||
'102': 'Processing',
|
||||
'200': 'OK',
|
||||
'201': 'Created',
|
||||
'202': 'Accepted',
|
||||
'203': 'Non-Authoritative Information',
|
||||
'204': 'No Content',
|
||||
'205': 'Reset Content',
|
||||
'206': 'Partial Content',
|
||||
'207': 'Multi-Status',
|
||||
'226': 'IM Used',
|
||||
'300': 'Multiple Choices',
|
||||
'301': 'Moved Permanently',
|
||||
'302': 'Found',
|
||||
'303': 'See Other',
|
||||
'304': 'Not Modified',
|
||||
'305': 'Use Proxy',
|
||||
'307': 'Temporary Redirect',
|
||||
'308': 'Permanent Redirect',
|
||||
'400': 'Bad Request',
|
||||
'401': 'Unauthorized',
|
||||
'402': 'Payment Required',
|
||||
'403': 'Forbidden',
|
||||
'404': 'Not Found',
|
||||
'405': 'Method Not Allowed',
|
||||
'406': 'Not Acceptable',
|
||||
'407': 'Proxy Authentication Required',
|
||||
'408': 'Request Timeout',
|
||||
'409': 'Conflict',
|
||||
'410': 'Gone',
|
||||
'411': 'Length Required',
|
||||
'412': 'Precondition Failed',
|
||||
'413': 'Payload Too Large',
|
||||
'414': 'URI Too Long',
|
||||
'415': 'Unsupported Media Type',
|
||||
'416': 'Range Not Satisfiable',
|
||||
'417': 'Expectation Failed',
|
||||
'418': "I'm a teapot",
|
||||
'422': 'Unprocessable Entity',
|
||||
'423': 'Locked',
|
||||
'424': 'Failed Dependency',
|
||||
'426': 'Upgrade Required',
|
||||
'428': 'Precondition Required',
|
||||
'429': 'Too Many Requests',
|
||||
'431': 'Request Header Fields Too Large',
|
||||
'451': 'Unavailable For Legal Reasons',
|
||||
'500': 'Internal Server Error',
|
||||
'501': 'Not Implemented',
|
||||
'502': 'Bad Gateway',
|
||||
'503': 'Service Unavailable',
|
||||
'504': 'Gateway Time-out',
|
||||
'505': 'HTTP Version Not Supported',
|
||||
'506': 'Variant Also Negotiates',
|
||||
'507': 'Insufficient Storage',
|
||||
'511': 'Network Authentication Required',
|
||||
'1xx': '**Informational**',
|
||||
'2xx': '**Successful**',
|
||||
'3xx': '**Redirection**',
|
||||
'4xx': '**Client Error**',
|
||||
'5xx': '**Server Error**',
|
||||
'7xx': '**Developer Error**',
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiBadge, EuiText } from '@elastic/eui';
|
||||
import { httpStatusCodes } from './http_status_codes';
|
||||
import { useGetHttpStatusColor } from './use_get_http_status_color';
|
||||
|
||||
interface HttpStatusCodeProps {
|
||||
code: number;
|
||||
}
|
||||
|
||||
export function HttpStatusCode({ code }: HttpStatusCodeProps) {
|
||||
return (
|
||||
<EuiBadge
|
||||
color={useGetHttpStatusColor(code)}
|
||||
data-test-subj="docViewerTracesOverviewHttpStatusCodeBadge"
|
||||
>
|
||||
<EuiText size="xs" data-test-subj="docViewerTracesOverviewHttpStatusCodeText">
|
||||
{code} {httpStatusCodes[code.toString()]}
|
||||
</EuiText>
|
||||
</EuiBadge>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
|
||||
export const useGetHttpStatusColor = (status: string | number) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const httpStatusCodeColors: Record<string, string> = {
|
||||
1: euiTheme.colors.vis.euiColorVisGrey0,
|
||||
2: euiTheme.colors.vis.euiColorVisSuccess0,
|
||||
3: euiTheme.colors.vis.euiColorVisGrey0,
|
||||
4: euiTheme.colors.vis.euiColorVisWarning0,
|
||||
5: euiTheme.colors.vis.euiColorVisDanger0,
|
||||
7: euiTheme.colors.vis.euiColorVisGrey0,
|
||||
};
|
||||
|
||||
const firstStatusDigit = String(status).charAt(0);
|
||||
return httpStatusCodeColors[firstStatusDigit] || 'default';
|
||||
};
|
|
@ -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
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui';
|
||||
import { getRouterLinkProps } from '@kbn/router-utils';
|
||||
import { AgentIcon } from '@kbn/custom-icons';
|
||||
import { AgentName } from '@kbn/elastic-agent-utils';
|
||||
import { getUnifiedDocViewerServices } from '../../../plugin';
|
||||
|
||||
const SERVICE_OVERVIEW_LOCATOR_ID = 'serviceOverviewLocator';
|
||||
|
||||
interface ServiceNameLinkProps {
|
||||
serviceName: string;
|
||||
agentName: string;
|
||||
}
|
||||
|
||||
export function ServiceNameLink({ serviceName, agentName }: ServiceNameLinkProps) {
|
||||
const {
|
||||
share: { url: urlService },
|
||||
core,
|
||||
data: dataService,
|
||||
} = getUnifiedDocViewerServices();
|
||||
|
||||
const canViewApm = core.application.capabilities.apm?.show || false;
|
||||
const { from: timeRangeFrom, to: timeRangeTo } =
|
||||
dataService.query.timefilter.timefilter.getTime();
|
||||
|
||||
const apmLinkToServiceEntityLocator = urlService.locators.get<{
|
||||
serviceName: string;
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
}>(SERVICE_OVERVIEW_LOCATOR_ID);
|
||||
|
||||
const href = apmLinkToServiceEntityLocator?.getRedirectUrl({
|
||||
serviceName,
|
||||
rangeFrom: timeRangeFrom,
|
||||
rangeTo: timeRangeTo,
|
||||
});
|
||||
|
||||
const routeLinkProps = href
|
||||
? getRouterLinkProps({
|
||||
href,
|
||||
onClick: () => {
|
||||
// TODO add telemetry (https://github.com/elastic/kibana/issues/208919)
|
||||
apmLinkToServiceEntityLocator?.navigate({
|
||||
serviceName,
|
||||
rangeFrom: timeRangeFrom,
|
||||
rangeTo: timeRangeTo,
|
||||
});
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const content = (
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
||||
{agentName && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AgentIcon agentName={agentName as AgentName} size="m" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<EuiText size="xs">{serviceName}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{canViewApm && routeLinkProps ? (
|
||||
<EuiLink {...routeLinkProps} data-test-subj="unifiedDocViewTracesOverviewServiceNameLink">
|
||||
{content}
|
||||
</EuiLink>
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import moment from 'moment';
|
||||
import { Timestamp } from './timestamp';
|
||||
|
||||
const timestamp = 1617549061000;
|
||||
|
||||
describe('Timestamp', () => {
|
||||
it('should render both absolute and relative time correctly', () => {
|
||||
render(<Timestamp timestamp={timestamp} />);
|
||||
const absoluteTime = moment(timestamp).format('MMM D, YYYY @ HH:mm:ss');
|
||||
const relativeTime = moment(timestamp).fromNow();
|
||||
|
||||
expect(screen.queryByTestId('docViewerTracesOverviewTimestamp')?.innerHTML).toEqual(
|
||||
`${absoluteTime} (${relativeTime})`
|
||||
);
|
||||
});
|
||||
|
||||
it('should display the relative time correctly for a recent timestamp', () => {
|
||||
const recentTimestamp = moment().subtract(10, 'minutes').valueOf();
|
||||
render(<Timestamp timestamp={recentTimestamp} />);
|
||||
const absoluteTime = moment(recentTimestamp).format('MMM D, YYYY @ HH:mm:ss');
|
||||
const relativeTime = moment(recentTimestamp).fromNow();
|
||||
|
||||
expect(screen.queryByTestId('docViewerTracesOverviewTimestamp')?.innerHTML).toEqual(
|
||||
`${absoluteTime} (${relativeTime})`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import moment from 'moment';
|
||||
|
||||
interface TimestampProps {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export function Timestamp({ timestamp }: TimestampProps) {
|
||||
const momentTime = moment(timestamp);
|
||||
const relativeTime = momentTime.fromNow();
|
||||
const absoluteTime = momentTime.format('MMM D, YYYY @ HH:mm:ss');
|
||||
|
||||
return (
|
||||
<EuiText size="xs" data-test-subj="docViewerTracesOverviewTimestamp">
|
||||
{absoluteTime} ({relativeTime})
|
||||
</EuiText>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiLink, EuiText } from '@elastic/eui';
|
||||
import { getRouterLinkProps } from '@kbn/router-utils';
|
||||
import { TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR } from '@kbn/deeplinks-observability';
|
||||
import { getUnifiedDocViewerServices } from '../../../plugin';
|
||||
|
||||
interface TraceIdLinkProps {
|
||||
traceId: string;
|
||||
}
|
||||
|
||||
export function TraceIdLink({ traceId }: TraceIdLinkProps) {
|
||||
const {
|
||||
share: { url: urlService },
|
||||
core,
|
||||
data: dataService,
|
||||
} = getUnifiedDocViewerServices();
|
||||
|
||||
const canViewApm = core.application.capabilities.apm?.show || false;
|
||||
const { from: timeRangeFrom, to: timeRangeTo } =
|
||||
dataService.query.timefilter.timefilter.getTime();
|
||||
|
||||
const apmLinkToTransactionByTraceIdLocator = urlService.locators.get<{
|
||||
traceId: string;
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
}>(TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR);
|
||||
|
||||
const href = apmLinkToTransactionByTraceIdLocator?.getRedirectUrl({
|
||||
traceId,
|
||||
rangeFrom: timeRangeFrom,
|
||||
rangeTo: timeRangeTo,
|
||||
});
|
||||
const routeLinkProps = href
|
||||
? getRouterLinkProps({
|
||||
href,
|
||||
onClick: () => {
|
||||
// TODO add telemetry (https://github.com/elastic/kibana/issues/208919)
|
||||
apmLinkToTransactionByTraceIdLocator?.navigate({
|
||||
traceId,
|
||||
rangeFrom: timeRangeFrom,
|
||||
rangeTo: timeRangeTo,
|
||||
});
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const content = <EuiText size="xs">{traceId}</EuiText>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{canViewApm && routeLinkProps ? (
|
||||
<EuiLink {...routeLinkProps} data-test-subj="unifiedDocViewTracesOverviewTraceIdLink">
|
||||
{content}
|
||||
</EuiLink>
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { TRANSACTION_NAME_FIELD } from '@kbn/discover-utils';
|
||||
import { EuiHorizontalRule } from '@elastic/eui';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTransactionContext } from '../hooks/use_transaction';
|
||||
import { FieldWithActions } from './field_with_actions/field_with_actions';
|
||||
import { FieldConfiguration } from '../resources/get_field_configuration';
|
||||
|
||||
export interface TraceSummaryProps {
|
||||
fieldId: string;
|
||||
fieldConfiguration: FieldConfiguration;
|
||||
}
|
||||
|
||||
export function TraceSummary({ fieldConfiguration, fieldId }: TraceSummaryProps) {
|
||||
const { transaction, loading } = useTransactionContext();
|
||||
const [fieldValue, setFieldValue] = useState(fieldConfiguration.value);
|
||||
const isTransactionNameField = fieldId === TRANSACTION_NAME_FIELD;
|
||||
const isTransactionNameFieldWithoutValue = isTransactionNameField && !fieldValue;
|
||||
|
||||
useEffect(() => {
|
||||
if (isTransactionNameField && !fieldValue && transaction?.name && !loading) {
|
||||
setFieldValue(transaction.name);
|
||||
}
|
||||
}, [transaction?.name, loading, fieldValue, isTransactionNameField]);
|
||||
|
||||
if (
|
||||
(!isTransactionNameFieldWithoutValue && !fieldValue) ||
|
||||
(isTransactionNameFieldWithoutValue && !loading)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FieldWithActions
|
||||
data-test-subj={`unifiedDocViewTracesOverviewAttribute-${fieldId}`}
|
||||
label={fieldConfiguration.title}
|
||||
field={fieldId}
|
||||
value={fieldValue as string}
|
||||
formattedValue={fieldValue as string}
|
||||
fieldMetadata={fieldConfiguration.fieldMetadata}
|
||||
loading={isTransactionNameFieldWithoutValue && loading}
|
||||
>
|
||||
<div>{fieldConfiguration.content(fieldValue)}</div>
|
||||
</FieldWithActions>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiLink, EuiText } from '@elastic/eui';
|
||||
import { getRouterLinkProps } from '@kbn/router-utils';
|
||||
import { TRANSACTION_DETAILS_BY_NAME_LOCATOR } from '@kbn/deeplinks-observability';
|
||||
import { getUnifiedDocViewerServices } from '../../../plugin';
|
||||
|
||||
interface TransactionNameLinkProps {
|
||||
serviceName: string;
|
||||
transactionName: string;
|
||||
}
|
||||
|
||||
export function TransactionNameLink({ transactionName, serviceName }: TransactionNameLinkProps) {
|
||||
const {
|
||||
share: { url: urlService },
|
||||
core,
|
||||
data: dataService,
|
||||
} = getUnifiedDocViewerServices();
|
||||
|
||||
const canViewApm = core.application.capabilities.apm?.show || false;
|
||||
const { from: timeRangeFrom, to: timeRangeTo } =
|
||||
dataService.query.timefilter.timefilter.getTime();
|
||||
|
||||
const apmLinkToTransactionByNameLocator = urlService.locators.get<{
|
||||
serviceName: string;
|
||||
transactionName: string;
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
}>(TRANSACTION_DETAILS_BY_NAME_LOCATOR);
|
||||
|
||||
const href = apmLinkToTransactionByNameLocator?.getRedirectUrl({
|
||||
serviceName,
|
||||
transactionName,
|
||||
rangeFrom: timeRangeFrom,
|
||||
rangeTo: timeRangeTo,
|
||||
});
|
||||
const routeLinkProps = href
|
||||
? getRouterLinkProps({
|
||||
href,
|
||||
onClick: () => {
|
||||
// TODO add telemetry (https://github.com/elastic/kibana/issues/208919)
|
||||
apmLinkToTransactionByNameLocator?.navigate({
|
||||
serviceName,
|
||||
transactionName,
|
||||
rangeFrom: timeRangeFrom,
|
||||
rangeTo: timeRangeTo,
|
||||
});
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const content = <EuiText size="xs">{transactionName}</EuiText>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{canViewApm && routeLinkProps ? (
|
||||
<EuiLink
|
||||
{...routeLinkProps}
|
||||
data-test-subj="unifiedDocViewTracesOverviewTransactionNameLink"
|
||||
>
|
||||
{content}
|
||||
</EuiLink>
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { DocViewRenderProps } from '@kbn/unified-doc-viewer/types';
|
||||
import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PROCESSOR_EVENT_FIELD, getTraceDocumentOverview } from '@kbn/discover-utils';
|
||||
import { spanFields, transactionFields } from './resources/fields';
|
||||
import { getFieldConfiguration } from './resources/get_field_configuration';
|
||||
import { FieldActionsProvider } from '../../hooks/use_field_actions';
|
||||
import { TransactionProvider } from './hooks/use_transaction';
|
||||
import { TraceSummary } from './sub_components/trace_summary';
|
||||
export type TracesOverviewProps = DocViewRenderProps;
|
||||
|
||||
export function TracesOverview({
|
||||
columns,
|
||||
dataView,
|
||||
hit,
|
||||
filter,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
}: TracesOverviewProps) {
|
||||
const parsedDoc = getTraceDocumentOverview(hit);
|
||||
const isTransaction = parsedDoc[PROCESSOR_EVENT_FIELD] === 'transaction';
|
||||
|
||||
const detailType = isTransaction
|
||||
? i18n.translate('unifiedDocViewer.docViewerTracesOverview.transactionTitle', {
|
||||
defaultMessage: 'Transaction',
|
||||
})
|
||||
: i18n.translate('unifiedDocViewer.docViewerTracesOverview.spanTitle', {
|
||||
defaultMessage: 'Span',
|
||||
});
|
||||
|
||||
const detailTitle = i18n.translate('unifiedDocViewer.docViewerTracesOverview.title', {
|
||||
defaultMessage: '{detailType} detail',
|
||||
values: { detailType },
|
||||
});
|
||||
|
||||
return (
|
||||
<TransactionProvider
|
||||
transactionId={parsedDoc['transaction.id']}
|
||||
indexPattern={dataView.getIndexPattern()}
|
||||
>
|
||||
<FieldActionsProvider
|
||||
columns={columns}
|
||||
filter={filter}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
>
|
||||
<EuiPanel color="transparent" hasShadow={false} paddingSize="none">
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="s">
|
||||
<h2>{detailTitle}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
{(isTransaction ? transactionFields : spanFields).map((fieldId) => {
|
||||
const fieldConfiguration = getFieldConfiguration(parsedDoc)[fieldId];
|
||||
|
||||
return (
|
||||
<TraceSummary
|
||||
key={fieldId}
|
||||
fieldId={fieldId}
|
||||
fieldConfiguration={fieldConfiguration}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</EuiPanel>
|
||||
</FieldActionsProvider>
|
||||
</TransactionProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type Maybe<T> = T | null | undefined;
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export * from './common';
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { asDuration, toMicroseconds } from './duration';
|
||||
|
||||
describe('duration formatters', () => {
|
||||
describe('asDuration', () => {
|
||||
it('formats correctly with defaults', () => {
|
||||
expect(asDuration(null)).toEqual('N/A');
|
||||
expect(asDuration(undefined)).toEqual('N/A');
|
||||
expect(asDuration(0)).toEqual('0 μs');
|
||||
expect(asDuration(1)).toEqual('1 μs');
|
||||
expect(asDuration(1)).toEqual('1 μs');
|
||||
expect(asDuration(toMicroseconds(1, 'milliseconds'))).toEqual('1,000 μs');
|
||||
expect(asDuration(toMicroseconds(1000, 'milliseconds'))).toEqual('1,000 ms');
|
||||
expect(asDuration(toMicroseconds(10000, 'milliseconds'))).toEqual('10,000 ms');
|
||||
expect(asDuration(toMicroseconds(20, 'seconds'))).toEqual('20 s');
|
||||
expect(asDuration(toMicroseconds(10, 'minutes'))).toEqual('600 s');
|
||||
expect(asDuration(toMicroseconds(11, 'minutes'))).toEqual('11 min');
|
||||
expect(asDuration(toMicroseconds(1, 'hours'))).toEqual('60 min');
|
||||
expect(asDuration(toMicroseconds(1.5, 'hours'))).toEqual('90 min');
|
||||
expect(asDuration(toMicroseconds(10, 'hours'))).toEqual('600 min');
|
||||
expect(asDuration(toMicroseconds(11, 'hours'))).toEqual('11 h');
|
||||
});
|
||||
|
||||
it('falls back to default value', () => {
|
||||
expect(asDuration(undefined, { defaultValue: 'nope' })).toEqual('nope');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toMicroseconds', () => {
|
||||
it('transformes to microseconds', () => {
|
||||
expect(toMicroseconds(1, 'hours')).toEqual(3600000000);
|
||||
expect(toMicroseconds(10, 'minutes')).toEqual(600000000);
|
||||
expect(toMicroseconds(10, 'seconds')).toEqual(10000000);
|
||||
expect(toMicroseconds(10, 'milliseconds')).toEqual(10000);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
import { memoize, isFinite } from 'lodash';
|
||||
import { asDecimalOrInteger, asInteger, NOT_AVAILABLE_LABEL } from './numeric';
|
||||
import { Maybe } from '../../typings';
|
||||
|
||||
type TimeUnit = 'hours' | 'minutes' | 'seconds' | 'milliseconds';
|
||||
type DurationTimeUnit = TimeUnit | 'microseconds';
|
||||
|
||||
interface FormatterOptions {
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
interface ConvertedDuration {
|
||||
value: string;
|
||||
unit?: string;
|
||||
formatted: string;
|
||||
}
|
||||
|
||||
export type TimeFormatter = (value: number, options?: FormatterOptions) => ConvertedDuration;
|
||||
|
||||
type TimeFormatterBuilder = (
|
||||
max: number,
|
||||
threshold?: number,
|
||||
scalingFactor?: number
|
||||
) => TimeFormatter;
|
||||
|
||||
export const toMicroseconds = (value: number, timeUnit: TimeUnit) =>
|
||||
moment.duration(value, timeUnit).asMilliseconds() * 1000;
|
||||
|
||||
function getUnitLabelAndConvertedValue(
|
||||
unitKey: DurationTimeUnit,
|
||||
value: number,
|
||||
threshold: number = 10
|
||||
) {
|
||||
const ms = value / 1000;
|
||||
|
||||
switch (unitKey) {
|
||||
case 'hours': {
|
||||
return {
|
||||
unitLabel: i18n.translate('unifiedDocViewer.formatters.duration.hoursTimeUnitLabel', {
|
||||
defaultMessage: 'h',
|
||||
}),
|
||||
convertedValue: asDecimalOrInteger(moment.duration(ms).asHours(), threshold),
|
||||
};
|
||||
}
|
||||
case 'minutes': {
|
||||
return {
|
||||
unitLabel: i18n.translate('unifiedDocViewer.formatters.duration.minutesTimeUnitLabel', {
|
||||
defaultMessage: 'min',
|
||||
}),
|
||||
convertedValue: asDecimalOrInteger(moment.duration(ms).asMinutes(), threshold),
|
||||
};
|
||||
}
|
||||
case 'seconds': {
|
||||
return {
|
||||
unitLabel: i18n.translate('unifiedDocViewer.formatters.duration.secondsTimeUnitLabel', {
|
||||
defaultMessage: 's',
|
||||
}),
|
||||
convertedValue: asDecimalOrInteger(moment.duration(ms).asSeconds(), threshold),
|
||||
};
|
||||
}
|
||||
case 'milliseconds': {
|
||||
return {
|
||||
unitLabel: i18n.translate('unifiedDocViewer.formatters.duration.millisTimeUnitLabel', {
|
||||
defaultMessage: 'ms',
|
||||
}),
|
||||
convertedValue: asDecimalOrInteger(moment.duration(ms).asMilliseconds(), threshold),
|
||||
};
|
||||
}
|
||||
case 'microseconds': {
|
||||
return {
|
||||
unitLabel: i18n.translate('unifiedDocViewer.formatters.duration.microsTimeUnitLabel', {
|
||||
defaultMessage: 'μs',
|
||||
}),
|
||||
convertedValue: asInteger(value),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convertTo({
|
||||
unit,
|
||||
microseconds,
|
||||
defaultValue = NOT_AVAILABLE_LABEL,
|
||||
threshold = 10,
|
||||
}: {
|
||||
unit: DurationTimeUnit;
|
||||
microseconds: number;
|
||||
defaultValue?: string;
|
||||
threshold?: number;
|
||||
}): ConvertedDuration {
|
||||
if (!isFinite(microseconds)) {
|
||||
return { value: defaultValue, formatted: defaultValue };
|
||||
}
|
||||
|
||||
const { convertedValue, unitLabel } = getUnitLabelAndConvertedValue(
|
||||
unit,
|
||||
microseconds,
|
||||
threshold
|
||||
);
|
||||
|
||||
return {
|
||||
value: convertedValue,
|
||||
unit: unitLabel,
|
||||
formatted: `${convertedValue} ${unitLabel}`,
|
||||
};
|
||||
}
|
||||
|
||||
function getDurationUnitKey(max: number, threshold = 10): DurationTimeUnit {
|
||||
if (max > toMicroseconds(threshold, 'hours')) {
|
||||
return 'hours';
|
||||
}
|
||||
if (max > toMicroseconds(threshold, 'minutes')) {
|
||||
return 'minutes';
|
||||
}
|
||||
if (max > toMicroseconds(threshold, 'seconds')) {
|
||||
return 'seconds';
|
||||
}
|
||||
if (max > toMicroseconds(1, 'milliseconds')) {
|
||||
return 'milliseconds';
|
||||
}
|
||||
return 'microseconds';
|
||||
}
|
||||
|
||||
// memoizer with a custom resolver to consider both arguments max/threshold.
|
||||
// by default lodash's memoize only considers the first argument.
|
||||
const getDurationFormatter: TimeFormatterBuilder = memoize(
|
||||
(max: number, threshold: number = 10, scalingFactor: number = 1) => {
|
||||
const unit = getDurationUnitKey(max, threshold);
|
||||
return (value: number, { defaultValue }: FormatterOptions = {}) => {
|
||||
return convertTo({
|
||||
unit,
|
||||
microseconds: isFinite(value) ? value * scalingFactor : value,
|
||||
defaultValue,
|
||||
threshold,
|
||||
});
|
||||
};
|
||||
},
|
||||
(max, threshold) => `${max}_${threshold}`
|
||||
);
|
||||
|
||||
export function asDuration(
|
||||
value: Maybe<number>,
|
||||
{ defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {}
|
||||
) {
|
||||
if (value === null || value === undefined || !isFinite(value)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const formatter = getDurationFormatter(value);
|
||||
return formatter(value, { defaultValue }).formatted;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export * from './duration';
|
||||
export * from './numeric';
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { asDecimal, asInteger, asDecimalOrInteger } from './numeric';
|
||||
|
||||
describe('formatters', () => {
|
||||
describe('asDecimal', () => {
|
||||
it.each([
|
||||
[Infinity, 'N/A'],
|
||||
[-Infinity, 'N/A'],
|
||||
[null, 'N/A'],
|
||||
[undefined, 'N/A'],
|
||||
[NaN, 'N/A'],
|
||||
])(
|
||||
'displays the not available label when the number is not finite',
|
||||
(value, formattedValue) => {
|
||||
expect(asDecimal(value)).toBe(formattedValue);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
[0, '0.0'],
|
||||
[0.005, '0.0'],
|
||||
[1.23, '1.2'],
|
||||
[12.34, '12.3'],
|
||||
[123.45, '123.5'],
|
||||
[1234.56, '1,234.6'],
|
||||
[1234567.89, '1,234,567.9'],
|
||||
])('displays the correct label when the number is finite', (value, formattedValue) => {
|
||||
expect(asDecimal(value)).toBe(formattedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('asInteger', () => {
|
||||
it.each([
|
||||
[Infinity, 'N/A'],
|
||||
[-Infinity, 'N/A'],
|
||||
[null, 'N/A'],
|
||||
[undefined, 'N/A'],
|
||||
[NaN, 'N/A'],
|
||||
])(
|
||||
'displays the not available label when the number is not finite',
|
||||
(value, formattedValue) => {
|
||||
expect(asInteger(value)).toBe(formattedValue);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
[0, '0'],
|
||||
[0.005, '0'],
|
||||
[1.23, '1'],
|
||||
[12.34, '12'],
|
||||
[123.45, '123'],
|
||||
[1234.56, '1,235'],
|
||||
[1234567.89, '1,234,568'],
|
||||
])('displays the correct label when the number is finite', (value, formattedValue) => {
|
||||
expect(asInteger(value)).toBe(formattedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('asDecimalOrInteger', () => {
|
||||
describe('with default threshold of 10', () => {
|
||||
it('formats as integer when number equals to 0 ', () => {
|
||||
expect(asDecimalOrInteger(0)).toEqual('0');
|
||||
});
|
||||
it('formats as integer when number is above or equals 10 ', () => {
|
||||
expect(asDecimalOrInteger(10.123)).toEqual('10');
|
||||
expect(asDecimalOrInteger(15.123)).toEqual('15');
|
||||
});
|
||||
it('formats as decimal when number is below 10 ', () => {
|
||||
expect(asDecimalOrInteger(0.25435632645)).toEqual('0.3');
|
||||
expect(asDecimalOrInteger(1)).toEqual('1.0');
|
||||
expect(asDecimalOrInteger(3.374329704990765)).toEqual('3.4');
|
||||
expect(asDecimalOrInteger(5)).toEqual('5.0');
|
||||
expect(asDecimalOrInteger(9)).toEqual('9.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with custom threshold of 1', () => {
|
||||
it('formats as integer when number equals to 0 ', () => {
|
||||
expect(asDecimalOrInteger(0, 1)).toEqual('0');
|
||||
});
|
||||
it('formats as integer when number is above or equals 1 ', () => {
|
||||
expect(asDecimalOrInteger(1, 1)).toEqual('1');
|
||||
expect(asDecimalOrInteger(1.123, 1)).toEqual('1');
|
||||
expect(asDecimalOrInteger(3.374329704990765, 1)).toEqual('3');
|
||||
expect(asDecimalOrInteger(5, 1)).toEqual('5');
|
||||
expect(asDecimalOrInteger(9, 1)).toEqual('9');
|
||||
expect(asDecimalOrInteger(10, 1)).toEqual('10');
|
||||
expect(asDecimalOrInteger(10.123, 1)).toEqual('10');
|
||||
expect(asDecimalOrInteger(15.123, 1)).toEqual('15');
|
||||
});
|
||||
it('formats as decimal when number is below 1 ', () => {
|
||||
expect(asDecimalOrInteger(0.25435632645, 1)).toEqual('0.3');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns fallback when valueNaN', () => {
|
||||
expect(asDecimalOrInteger(NaN)).toEqual('N/A');
|
||||
expect(asDecimalOrInteger(null)).toEqual('N/A');
|
||||
expect(asDecimalOrInteger(undefined)).toEqual('N/A');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isFinite } from 'lodash';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { Maybe } from '../../typings';
|
||||
|
||||
export const NOT_AVAILABLE_LABEL = i18n.translate(
|
||||
'unifiedDocViewer.formatters.numeric.notAvailableLabel',
|
||||
{
|
||||
defaultMessage: 'N/A',
|
||||
}
|
||||
);
|
||||
|
||||
export function asDecimal(value: Maybe<number> | null) {
|
||||
if (!isFinite(value)) {
|
||||
return NOT_AVAILABLE_LABEL;
|
||||
}
|
||||
|
||||
return numeral(value).format('0,0.0');
|
||||
}
|
||||
|
||||
export function asInteger(value: Maybe<number> | null) {
|
||||
if (!isFinite(value)) {
|
||||
return NOT_AVAILABLE_LABEL;
|
||||
}
|
||||
|
||||
return numeral(value).format('0,0');
|
||||
}
|
||||
|
||||
export function asDecimalOrInteger(value: Maybe<number>, threshold = 10) {
|
||||
if (!isFinite(value)) {
|
||||
return NOT_AVAILABLE_LABEL;
|
||||
}
|
||||
|
||||
if (value === null || value === undefined || value === 0 || value >= threshold) {
|
||||
return asInteger(value);
|
||||
}
|
||||
return asDecimal(value);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export * from './formatters';
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiDelayRender, EuiSkeletonText } from '@elastic/eui';
|
||||
import { dynamic } from '@kbn/shared-ux-utility';
|
||||
|
||||
export const UnifiedDocViewerTracesOverview = dynamic(
|
||||
() => import('./doc_viewer_traces_overview'),
|
||||
{
|
||||
fallback: (
|
||||
<EuiDelayRender delay={300}>
|
||||
<EuiSkeletonText />
|
||||
</EuiDelayRender>
|
||||
),
|
||||
}
|
||||
);
|
|
@ -33,4 +33,6 @@ export { UnifiedDocViewerFlyout } from './components/lazy_doc_viewer_flyout';
|
|||
export type { LogsOverviewProps as UnifiedDocViewerLogsOverviewProps } from './components/doc_viewer_logs_overview/logs_overview';
|
||||
export { UnifiedDocViewerLogsOverview } from './components/lazy_doc_viewer_logs_overview';
|
||||
|
||||
export { UnifiedDocViewerTracesOverview } from './components/lazy_doc_viewer_traces_overview';
|
||||
|
||||
export const plugin = () => new UnifiedDocViewerPublicPlugin();
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": [ "../../../../../typings/**/*", "common/**/*", "public/**/*", "server/**/*"],
|
||||
"include": ["common/**/*", "public/**/*", "server/**/*"],
|
||||
"kbn_references": [
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/monaco",
|
||||
|
@ -39,10 +39,8 @@
|
|||
"@kbn/management-settings-ids",
|
||||
"@kbn/apm-types",
|
||||
"@kbn/event-stacktrace",
|
||||
"@kbn/elastic-agent-utils",
|
||||
"@kbn/data-view-utils"
|
||||
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
]
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -167,7 +167,6 @@ export const LOGS_EXPLORER_FEEDBACK_LINK = 'https://ela.st/explorer-feedback';
|
|||
export type {
|
||||
ServiceOverviewParams,
|
||||
ServiceOverviewLocator,
|
||||
TransactionDetailsByNameParams,
|
||||
TransactionDetailsByNameLocator,
|
||||
AssetDetailsFlyoutLocator,
|
||||
AssetDetailsFlyoutLocatorParams,
|
||||
|
@ -195,6 +194,7 @@ export type {
|
|||
export {
|
||||
ServiceOverviewLocatorDefinition,
|
||||
SERVICE_OVERVIEW_LOCATOR_ID,
|
||||
DependencyOverviewLocatorDefinition,
|
||||
TransactionDetailsByNameLocatorDefinition,
|
||||
ASSET_DETAILS_FLYOUT_LOCATOR_ID,
|
||||
AssetDetailsFlyoutLocatorDefinition,
|
||||
|
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import qs from 'query-string';
|
||||
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
|
||||
import {
|
||||
DEPENDENCY_OVERVIEW_LOCATOR_ID,
|
||||
DependencyOverviewParams,
|
||||
} from '@kbn/deeplinks-observability/locators';
|
||||
|
||||
export type DependencyOverviewLocator = LocatorPublic<DependencyOverviewParams>;
|
||||
|
||||
export class DependencyOverviewLocatorDefinition
|
||||
implements LocatorDefinition<DependencyOverviewParams>
|
||||
{
|
||||
public readonly id = DEPENDENCY_OVERVIEW_LOCATOR_ID;
|
||||
|
||||
public readonly getLocation = async ({
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
dependencyName,
|
||||
environment,
|
||||
}: DependencyOverviewParams) => {
|
||||
const params = { rangeFrom, rangeTo, environment, dependencyName };
|
||||
return {
|
||||
app: 'apm',
|
||||
path: `/dependencies/overview?${qs.stringify(params)}`,
|
||||
state: {},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -5,22 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import qs from 'query-string';
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
|
||||
|
||||
export interface TransactionDetailsByNameParams extends SerializableRecord {
|
||||
serviceName: string;
|
||||
transactionName: string;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
}
|
||||
import {
|
||||
TRANSACTION_DETAILS_BY_NAME_LOCATOR,
|
||||
TransactionDetailsByNameParams,
|
||||
} from '@kbn/deeplinks-observability';
|
||||
|
||||
export type TransactionDetailsByNameLocator = LocatorPublic<TransactionDetailsByNameParams>;
|
||||
|
||||
export class TransactionDetailsByNameLocatorDefinition
|
||||
implements LocatorDefinition<TransactionDetailsByNameParams>
|
||||
{
|
||||
public readonly id = 'TransactionDetailsByNameLocator';
|
||||
public readonly id = TRANSACTION_DETAILS_BY_NAME_LOCATOR;
|
||||
|
||||
public readonly getLocation = async ({
|
||||
rangeFrom,
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
export * from './apm/service_overview_locator';
|
||||
export * from './apm/dependency_overview_locator';
|
||||
export * from './apm/transaction_details_by_name_locator';
|
||||
export * from './apm/transaction_details_by_trace_id_locator';
|
||||
export * from './apm/service_entity_locator';
|
||||
|
|
|
@ -49,6 +49,10 @@ import {
|
|||
EntitiesInventoryLocatorDefinition,
|
||||
} from '../common';
|
||||
import { updateGlobalNavigation } from './services/update_global_navigation';
|
||||
import {
|
||||
DependencyOverviewLocator,
|
||||
DependencyOverviewLocatorDefinition,
|
||||
} from '../common/locators/apm/dependency_overview_locator';
|
||||
export interface ObservabilitySharedSetup {
|
||||
share: SharePluginSetup;
|
||||
}
|
||||
|
@ -80,6 +84,7 @@ interface ObservabilitySharedLocators {
|
|||
};
|
||||
apm: {
|
||||
serviceOverview: ServiceOverviewLocator;
|
||||
dependencyOverview: DependencyOverviewLocator;
|
||||
transactionDetailsByName: TransactionDetailsByNameLocator;
|
||||
transactionDetailsByTraceId: TransactionDetailsByTraceIdLocator;
|
||||
serviceEntity: ServiceEntityLocator;
|
||||
|
@ -154,6 +159,7 @@ export class ObservabilitySharedPlugin implements Plugin {
|
|||
},
|
||||
apm: {
|
||||
serviceOverview: urlService.locators.create(new ServiceOverviewLocatorDefinition()),
|
||||
dependencyOverview: urlService.locators.create(new DependencyOverviewLocatorDefinition()),
|
||||
transactionDetailsByName: urlService.locators.create(
|
||||
new TransactionDetailsByNameLocatorDefinition()
|
||||
),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue