Rework link components to be separated by destination (#34630)
* Creates new LocationContext and useLocation hook * Re-structures Discover links * Re-structures ML job link, cleans up discover link changes * Removes unused components and props * Adds separated APM and Kibana link components * Adds InfraLink component * Adds integration link tests for new link components * Moves unshared getSearchWithCurrentTimeRange into APMLink component where it is used * Moves persistent APM params out of the generic url helpers util * Refactoring rison more * Clarifies interface names for rison * Removes risonStringify helper * Changes link components to inline function exports with manually created component type * Consolidate APM href generation in one place * Re-ordered imports for linting * Updates breadcrumb snapshots now that they include default values
This commit is contained in:
parent
7a8301e43b
commit
cd532cd8f7
|
@ -18,7 +18,7 @@ import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
|||
import { idx } from '../../../../../common/idx';
|
||||
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
|
||||
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { KibanaLink } from '../../../shared/Links/KibanaLink';
|
||||
import { APMLink } from '../../../shared/Links/APMLink';
|
||||
import { legacyEncodeURIComponent } from '../../../shared/Links/url_helpers';
|
||||
import { StickyProperties } from '../../../shared/StickyProperties';
|
||||
|
||||
|
@ -48,15 +48,16 @@ function TransactionLink({
|
|||
)}/${legacyEncodeURIComponent(transaction.transaction.name)}`;
|
||||
|
||||
return (
|
||||
<KibanaLink
|
||||
hash={path}
|
||||
<APMLink
|
||||
path={path}
|
||||
query={{
|
||||
transactionId: transaction.transaction.id,
|
||||
traceId: transaction.trace.id
|
||||
traceId: transaction.trace.id,
|
||||
banana: 'ok'
|
||||
}}
|
||||
>
|
||||
{transaction.transaction.id}
|
||||
</KibanaLink>
|
||||
</APMLink>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
truncate,
|
||||
unit
|
||||
} from '../../../../style/variables';
|
||||
import { KibanaLink } from '../../../shared/Links/KibanaLink';
|
||||
import { APMLink } from '../../../shared/Links/APMLink';
|
||||
import { fromQuery, history, toQuery } from '../../../shared/Links/url_helpers';
|
||||
|
||||
function paginateItems({
|
||||
|
@ -36,7 +36,7 @@ function paginateItems({
|
|||
return items.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
|
||||
}
|
||||
|
||||
const GroupIdLink = styled(KibanaLink)`
|
||||
const GroupIdLink = styled(APMLink)`
|
||||
font-family: ${fontFamilyCode};
|
||||
`;
|
||||
|
||||
|
@ -44,7 +44,7 @@ const MessageAndCulpritCell = styled.div`
|
|||
${truncate('100%')};
|
||||
`;
|
||||
|
||||
const MessageLink = styled(KibanaLink)`
|
||||
const MessageLink = styled(APMLink)`
|
||||
font-family: ${fontFamilyCode};
|
||||
font-size: ${fontSizes.large};
|
||||
${truncate('100%')};
|
||||
|
@ -115,7 +115,7 @@ export class ErrorGroupList extends Component<Props, State> {
|
|||
width: px(unit * 6),
|
||||
render: (groupId: string) => {
|
||||
return (
|
||||
<GroupIdLink hash={`/${serviceName}/errors/${groupId}`}>
|
||||
<GroupIdLink path={`/${serviceName}/errors/${groupId}`}>
|
||||
{groupId.slice(0, 5) || NOT_AVAILABLE_LABEL}
|
||||
</GroupIdLink>
|
||||
);
|
||||
|
@ -138,7 +138,7 @@ export class ErrorGroupList extends Component<Props, State> {
|
|||
id="error-message-tooltip"
|
||||
content={message || NOT_AVAILABLE_LABEL}
|
||||
>
|
||||
<MessageLink hash={`/${serviceName}/errors/${item.groupId}`}>
|
||||
<MessageLink path={`/${serviceName}/errors/${item.groupId}`}>
|
||||
{message || NOT_AVAILABLE_LABEL}
|
||||
</MessageLink>
|
||||
</EuiToolTip>
|
||||
|
|
|
@ -5,14 +5,10 @@
|
|||
*/
|
||||
|
||||
import { Location } from 'history';
|
||||
import { last, pick } from 'lodash';
|
||||
import { last } from 'lodash';
|
||||
import React from 'react';
|
||||
import chrome from 'ui/chrome';
|
||||
import {
|
||||
fromQuery,
|
||||
PERSISTENT_APM_PARAMS,
|
||||
toQuery
|
||||
} from '../../shared/Links/url_helpers';
|
||||
import { getAPMHref } from '../../shared/Links/APMLink';
|
||||
import { Breadcrumb, ProvideBreadcrumbs } from './ProvideBreadcrumbs';
|
||||
import { routes } from './routeConfig';
|
||||
|
||||
|
@ -23,12 +19,9 @@ interface Props {
|
|||
|
||||
class UpdateBreadcrumbsComponent extends React.Component<Props> {
|
||||
public updateHeaderBreadcrumbs() {
|
||||
const query = toQuery(this.props.location.search);
|
||||
const persistentParams = pick(query, PERSISTENT_APM_PARAMS);
|
||||
const search = fromQuery(persistentParams);
|
||||
const breadcrumbs = this.props.breadcrumbs.map(({ value, match }) => ({
|
||||
text: value,
|
||||
href: `#${match.url}?${search}`
|
||||
href: getAPMHref(match.url, this.props.location.search)
|
||||
}));
|
||||
|
||||
const current = last(breadcrumbs) || { text: '' };
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
exports[`Breadcrumbs /:serviceName 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"href": "#/?kuery=myKuery",
|
||||
"href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "APM",
|
||||
},
|
||||
Object {
|
||||
"href": "#/opbeans-node?kuery=myKuery",
|
||||
"href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "opbeans-node",
|
||||
},
|
||||
]
|
||||
|
@ -16,15 +16,15 @@ Array [
|
|||
exports[`Breadcrumbs /:serviceName/errors 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"href": "#/?kuery=myKuery",
|
||||
"href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "APM",
|
||||
},
|
||||
Object {
|
||||
"href": "#/opbeans-node?kuery=myKuery",
|
||||
"href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "opbeans-node",
|
||||
},
|
||||
Object {
|
||||
"href": "#/opbeans-node/errors?kuery=myKuery",
|
||||
"href": "#/opbeans-node/errors?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "Errors",
|
||||
},
|
||||
]
|
||||
|
@ -33,19 +33,19 @@ Array [
|
|||
exports[`Breadcrumbs /:serviceName/errors/:groupId 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"href": "#/?kuery=myKuery",
|
||||
"href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "APM",
|
||||
},
|
||||
Object {
|
||||
"href": "#/opbeans-node?kuery=myKuery",
|
||||
"href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "opbeans-node",
|
||||
},
|
||||
Object {
|
||||
"href": "#/opbeans-node/errors?kuery=myKuery",
|
||||
"href": "#/opbeans-node/errors?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "Errors",
|
||||
},
|
||||
Object {
|
||||
"href": "#/opbeans-node/errors/myGroupId?kuery=myKuery",
|
||||
"href": "#/opbeans-node/errors/myGroupId?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "myGroupId",
|
||||
},
|
||||
]
|
||||
|
@ -54,15 +54,15 @@ Array [
|
|||
exports[`Breadcrumbs /:serviceName/transactions 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"href": "#/?kuery=myKuery",
|
||||
"href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "APM",
|
||||
},
|
||||
Object {
|
||||
"href": "#/opbeans-node?kuery=myKuery",
|
||||
"href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "opbeans-node",
|
||||
},
|
||||
Object {
|
||||
"href": "#/opbeans-node/transactions?kuery=myKuery",
|
||||
"href": "#/opbeans-node/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "Transactions",
|
||||
},
|
||||
]
|
||||
|
@ -71,15 +71,15 @@ Array [
|
|||
exports[`Breadcrumbs /:serviceName/transactions/:transactionType 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"href": "#/?kuery=myKuery",
|
||||
"href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "APM",
|
||||
},
|
||||
Object {
|
||||
"href": "#/opbeans-node?kuery=myKuery",
|
||||
"href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "opbeans-node",
|
||||
},
|
||||
Object {
|
||||
"href": "#/opbeans-node/transactions?kuery=myKuery",
|
||||
"href": "#/opbeans-node/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "Transactions",
|
||||
},
|
||||
]
|
||||
|
@ -88,19 +88,19 @@ Array [
|
|||
exports[`Breadcrumbs /:serviceName/transactions/:transactionType/:transactionName 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"href": "#/?kuery=myKuery",
|
||||
"href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "APM",
|
||||
},
|
||||
Object {
|
||||
"href": "#/opbeans-node?kuery=myKuery",
|
||||
"href": "#/opbeans-node?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "opbeans-node",
|
||||
},
|
||||
Object {
|
||||
"href": "#/opbeans-node/transactions?kuery=myKuery",
|
||||
"href": "#/opbeans-node/transactions?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "Transactions",
|
||||
},
|
||||
Object {
|
||||
"href": "#/opbeans-node/transactions/request/my-transaction-name?kuery=myKuery",
|
||||
"href": "#/opbeans-node/transactions/request/my-transaction-name?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "my-transaction-name",
|
||||
},
|
||||
]
|
||||
|
@ -109,7 +109,7 @@ Array [
|
|||
exports[`Breadcrumbs Homepage 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"href": "#/?kuery=myKuery",
|
||||
"href": "#/?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=myKuery",
|
||||
"text": "APM",
|
||||
},
|
||||
]
|
||||
|
|
|
@ -5,20 +5,18 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Location } from 'history';
|
||||
import React, { Component } from 'react';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { startMLJob } from '../../../../../services/rest/ml';
|
||||
import { getAPMIndexPattern } from '../../../../../services/rest/savedObjects';
|
||||
import { IUrlParams } from '../../../../../store/urlParams';
|
||||
import { MLJobLink } from '../../../../shared/Links/MLJobLink';
|
||||
import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink';
|
||||
import { MachineLearningFlyoutView } from './view';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
urlParams: IUrlParams;
|
||||
location: Location;
|
||||
serviceTransactionTypes: string[];
|
||||
}
|
||||
|
||||
|
@ -116,7 +114,7 @@ export class MachineLearningFlyout extends Component<Props, State> {
|
|||
};
|
||||
|
||||
public addSuccessToast = () => {
|
||||
const { location, urlParams } = this.props;
|
||||
const { urlParams } = this.props;
|
||||
const { serviceName, transactionType } = urlParams;
|
||||
|
||||
if (!serviceName) {
|
||||
|
@ -146,7 +144,6 @@ export class MachineLearningFlyout extends Component<Props, State> {
|
|||
<MLJobLink
|
||||
serviceName={serviceName}
|
||||
transactionType={transactionType}
|
||||
location={location}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText',
|
||||
|
@ -167,13 +164,7 @@ export class MachineLearningFlyout extends Component<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
isOpen,
|
||||
onClose,
|
||||
urlParams,
|
||||
location,
|
||||
serviceTransactionTypes
|
||||
} = this.props;
|
||||
const { isOpen, onClose, urlParams, serviceTransactionTypes } = this.props;
|
||||
const { serviceName, transactionType } = urlParams;
|
||||
const {
|
||||
isCreatingJob,
|
||||
|
@ -189,7 +180,6 @@ export class MachineLearningFlyout extends Component<Props, State> {
|
|||
<MachineLearningFlyoutView
|
||||
hasIndexPattern={hasIndexPattern}
|
||||
isCreatingJob={isCreatingJob}
|
||||
location={location}
|
||||
onChangeTransaction={this.onChangeTransaction}
|
||||
onClickCreate={this.onClickCreate}
|
||||
onClose={onClose}
|
||||
|
|
|
@ -20,19 +20,18 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { getMlJobId } from '../../../../../../common/ml_job_constants';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../../../hooks/useFetcher';
|
||||
import { getMLJob } from '../../../../../services/rest/ml';
|
||||
import { KibanaLink } from '../../../../shared/Links/KibanaLink';
|
||||
import { MLJobLink } from '../../../../shared/Links/MLJobLink';
|
||||
import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink';
|
||||
import { MLLink } from '../../../../shared/Links/MachineLearningLinks/MLLink';
|
||||
import { TransactionSelect } from './TransactionSelect';
|
||||
|
||||
interface Props {
|
||||
hasIndexPattern: boolean;
|
||||
isCreatingJob: boolean;
|
||||
location: Location;
|
||||
onChangeTransaction: (value: string) => void;
|
||||
onClickCreate: () => void;
|
||||
onClose: () => void;
|
||||
|
@ -46,7 +45,6 @@ const INITIAL_DATA = { count: 0, jobs: [] };
|
|||
export function MachineLearningFlyoutView({
|
||||
hasIndexPattern,
|
||||
isCreatingJob,
|
||||
location,
|
||||
onChangeTransaction,
|
||||
onClickCreate,
|
||||
onClose,
|
||||
|
@ -111,7 +109,6 @@ export function MachineLearningFlyoutView({
|
|||
<MLJobLink
|
||||
serviceName={serviceName}
|
||||
transactionType={transactionType}
|
||||
location={location}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText',
|
||||
|
@ -136,10 +133,7 @@ export function MachineLearningFlyoutView({
|
|||
defaultMessage="No APM index pattern available. To create a job, please import the APM index pattern via the {setupInstructionLink}"
|
||||
values={{
|
||||
setupInstructionLink: (
|
||||
<KibanaLink
|
||||
pathname={'/app/kibana'}
|
||||
hash={`/home/tutorial/apm`}
|
||||
>
|
||||
<KibanaLink path={`/home/tutorial/apm`}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.noPatternTitle.setupInstructionLinkText',
|
||||
{
|
||||
|
@ -188,14 +182,14 @@ export function MachineLearningFlyoutView({
|
|||
Once a job is created, you can manage it and see more details in the {mlJobsPageLink}."
|
||||
values={{
|
||||
mlJobsPageLink: (
|
||||
<KibanaLink pathname={'/app/ml'}>
|
||||
<MLLink>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.mlJobsPageLinkText',
|
||||
{
|
||||
defaultMessage: 'Machine Learning jobs management page'
|
||||
}
|
||||
)}
|
||||
</KibanaLink>
|
||||
</MLLink>
|
||||
)
|
||||
}}
|
||||
/>{' '}
|
||||
|
|
|
@ -26,7 +26,6 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { Location } from 'history';
|
||||
import { memoize, padLeft, range } from 'lodash';
|
||||
import moment from 'moment-timezone';
|
||||
import React, { Component } from 'react';
|
||||
|
@ -35,7 +34,7 @@ import chrome from 'ui/chrome';
|
|||
import { toastNotifications } from 'ui/notify';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { XPACK_DOCS } from '../../../../utils/documentation/xpack';
|
||||
import { UnconnectedKibanaLink } from '../../../shared/Links/KibanaLink';
|
||||
import { KibanaLink } from '../../../shared/Links/KibanaLink';
|
||||
import { createErrorGroupWatch, Schedule } from './createErrorGroupWatch';
|
||||
|
||||
type ScheduleKey = keyof Schedule;
|
||||
|
@ -59,7 +58,6 @@ const SmallInput = styled.div`
|
|||
interface WatcherFlyoutProps {
|
||||
urlParams: IUrlParams;
|
||||
onClose: () => void;
|
||||
location: Location;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
|
@ -253,10 +251,8 @@ export class WatcherFlyout extends Component<
|
|||
}
|
||||
}
|
||||
)}{' '}
|
||||
<UnconnectedKibanaLink
|
||||
location={this.props.location}
|
||||
pathname={'/app/kibana'}
|
||||
hash={`/management/elasticsearch/watcher/watches/watch/${id}`}
|
||||
<KibanaLink
|
||||
path={`/management/elasticsearch/watcher/watches/watch/${id}`}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText.viewWatchLinkText',
|
||||
|
@ -264,7 +260,7 @@ export class WatcherFlyout extends Component<
|
|||
defaultMessage: 'View watch'
|
||||
}
|
||||
)}
|
||||
</UnconnectedKibanaLink>
|
||||
</KibanaLink>
|
||||
</p>
|
||||
)
|
||||
});
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
EuiPopover
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Location } from 'history';
|
||||
import { memoize } from 'lodash';
|
||||
import React, { Fragment } from 'react';
|
||||
import chrome from 'ui/chrome';
|
||||
|
@ -21,7 +20,6 @@ import { MachineLearningFlyout } from './MachineLearningFlyout';
|
|||
import { WatcherFlyout } from './WatcherFlyout';
|
||||
|
||||
interface Props {
|
||||
location: Location;
|
||||
transactionTypes: string[];
|
||||
urlParams: IUrlParams;
|
||||
}
|
||||
|
@ -164,14 +162,12 @@ export class ServiceIntegrations extends React.Component<Props, State> {
|
|||
/>
|
||||
</EuiPopover>
|
||||
<MachineLearningFlyout
|
||||
location={this.props.location}
|
||||
isOpen={this.state.activeFlyout === 'ML'}
|
||||
onClose={this.closeFlyouts}
|
||||
urlParams={this.props.urlParams}
|
||||
serviceTransactionTypes={this.props.transactionTypes}
|
||||
/>
|
||||
<WatcherFlyout
|
||||
location={this.props.location}
|
||||
isOpen={this.state.activeFlyout === 'Watcher'}
|
||||
onClose={this.closeFlyouts}
|
||||
urlParams={this.props.urlParams}
|
||||
|
|
|
@ -42,7 +42,6 @@ export function ServiceDetailsView({ urlParams, location }: Props) {
|
|||
<EuiFlexItem grow={false}>
|
||||
<ServiceIntegrations
|
||||
transactionTypes={serviceDetailsData.types}
|
||||
location={location}
|
||||
urlParams={urlParams}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -56,10 +56,7 @@ export function NoServicesMessage({ historicalDataFound }: Props) {
|
|||
defaultMessage:
|
||||
'You may also have old data that needs to be migrated.'
|
||||
})}{' '}
|
||||
<KibanaLink
|
||||
pathname="/app/kibana"
|
||||
hash="/management/elasticsearch/upgrade_assistant"
|
||||
>
|
||||
<KibanaLink path="/management/elasticsearch/upgrade_assistant">
|
||||
{i18n.translate(
|
||||
'xpack.apm.servicesTable.UpgradeAssistantLink',
|
||||
{
|
||||
|
|
|
@ -7,11 +7,11 @@ exports[`ErrorGroupOverview -> List should render columns correctly 1`] = `
|
|||
id="service-name-tooltip"
|
||||
position="top"
|
||||
>
|
||||
<Styled(Connect(UnconnectedKibanaLink))
|
||||
hash="/opbeans-python/transactions"
|
||||
<Styled(APMLink)
|
||||
path="/opbeans-python/transactions"
|
||||
>
|
||||
opbeans-python
|
||||
</Styled(Connect(UnconnectedKibanaLink))>
|
||||
</Styled(APMLink)>
|
||||
</EuiToolTip>
|
||||
`;
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
|||
import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services';
|
||||
import { fontSizes, truncate } from '../../../../style/variables';
|
||||
import { asDecimal, asMillis } from '../../../../utils/formatters';
|
||||
import { KibanaLink } from '../../../shared/Links/KibanaLink';
|
||||
import { APMLink } from '../../../shared/Links/APMLink';
|
||||
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
|
||||
|
||||
interface Props {
|
||||
|
@ -34,7 +34,7 @@ function formatString(value?: string | null) {
|
|||
return value || NOT_AVAILABLE_LABEL;
|
||||
}
|
||||
|
||||
const AppLink = styled(KibanaLink)`
|
||||
const AppLink = styled(APMLink)`
|
||||
font-size: ${fontSizes.large};
|
||||
${truncate('100%')};
|
||||
`;
|
||||
|
@ -51,7 +51,7 @@ export const SERVICE_COLUMNS: Array<
|
|||
sortable: true,
|
||||
render: (serviceName: string) => (
|
||||
<EuiToolTip content={formatString(serviceName)} id="service-name-tooltip">
|
||||
<AppLink hash={`/${serviceName}/transactions`}>
|
||||
<AppLink path={`/${serviceName}/transactions`}>
|
||||
{formatString(serviceName)}
|
||||
</AppLink>
|
||||
</EuiToolTip>
|
||||
|
|
|
@ -16,12 +16,11 @@ exports[`NoServicesMessage should show a "no services installed" message, a link
|
|||
<p>
|
||||
You may also have old data that needs to be migrated.
|
||||
|
||||
<Connect(UnconnectedKibanaLink)
|
||||
hash="/management/elasticsearch/upgrade_assistant"
|
||||
pathname="/app/kibana"
|
||||
<KibanaLink
|
||||
path="/management/elasticsearch/upgrade_assistant"
|
||||
>
|
||||
Learn more by visiting the Kibana Upgrade Assistant
|
||||
</Connect(UnconnectedKibanaLink)>
|
||||
</KibanaLink>
|
||||
.
|
||||
</p>
|
||||
</React.Fragment>
|
||||
|
|
|
@ -79,7 +79,7 @@ NodeList [
|
|||
|
||||
<a
|
||||
class="euiLink euiLink--primary"
|
||||
href="/app/kibana#/management/elasticsearch/upgrade_assistant?"
|
||||
href="/app/kibana#/management/elasticsearch/upgrade_assistant"
|
||||
>
|
||||
Learn more by visiting the Kibana Upgrade Assistant
|
||||
</a>
|
||||
|
@ -95,7 +95,7 @@ NodeList [
|
|||
/>
|
||||
<a
|
||||
class="euiLink euiLink--primary"
|
||||
href="/app/kibana#/home/tutorial/apm?"
|
||||
href="/app/kibana#/home/tutorial/apm"
|
||||
>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill"
|
||||
|
|
|
@ -12,7 +12,7 @@ import styled from 'styled-components';
|
|||
import { idx } from '../../../../../common/idx';
|
||||
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { fontSize } from '../../../../style/variables';
|
||||
import { KibanaLink } from '../../../shared/Links/KibanaLink';
|
||||
import { APMLink } from '../../../shared/Links/APMLink';
|
||||
import { legacyEncodeURIComponent } from '../../../shared/Links/url_helpers';
|
||||
|
||||
const LinkLabel = styled.span`
|
||||
|
@ -52,9 +52,8 @@ export const ErrorCountBadge: React.SFC<Props> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<KibanaLink
|
||||
pathname={'/app/apm'}
|
||||
hash={`/${idx(transaction, _ => _.service.name)}/errors`}
|
||||
<APMLink
|
||||
path={`/${idx(transaction, _ => _.service.name)}/errors`}
|
||||
query={{
|
||||
kuery: legacyEncodeURIComponent(
|
||||
`trace.id : "${transaction.trace.id}" and transaction.id : "${
|
||||
|
@ -80,6 +79,6 @@ export const ErrorCountBadge: React.SFC<Props> = ({
|
|||
) : (
|
||||
<EuiToolTip content={toolTipContent}>{errorCountBadge}</EuiToolTip>
|
||||
)}
|
||||
</KibanaLink>
|
||||
</APMLink>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
TRANSACTION_NAME
|
||||
} from '../../../../../../../common/elasticsearch_fieldnames';
|
||||
import { Transaction } from '../../../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { KibanaLink } from '../../../../../shared/Links/KibanaLink';
|
||||
import { APMLink } from '../../../../../shared/Links/APMLink';
|
||||
import { TransactionLink } from '../../../../../shared/Links/TransactionLink';
|
||||
import { StickyProperties } from '../../../../../shared/StickyProperties';
|
||||
|
||||
|
@ -31,9 +31,9 @@ export function FlyoutTopLevelProperties({ transaction }: Props) {
|
|||
}),
|
||||
fieldName: SERVICE_NAME,
|
||||
val: (
|
||||
<KibanaLink hash={`/${transaction.service.name}`}>
|
||||
<APMLink path={`/${transaction.service.name}`}>
|
||||
{transaction.service.name}
|
||||
</KibanaLink>
|
||||
</APMLink>
|
||||
),
|
||||
width: '50%'
|
||||
},
|
||||
|
|
|
@ -103,10 +103,7 @@ export function TransactionFlyout({
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<TransactionActionMenu
|
||||
transaction={transactionDoc}
|
||||
location={location}
|
||||
/>
|
||||
<TransactionActionMenu transaction={transactionDoc} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
|
|
|
@ -124,10 +124,7 @@ export const Transaction: React.SFC<Props> = ({
|
|||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<TransactionActionMenu
|
||||
transaction={transaction}
|
||||
location={location}
|
||||
/>
|
||||
<TransactionActionMenu transaction={transaction} />
|
||||
</EuiFlexItem>
|
||||
<MaybeViewTraceLink
|
||||
transaction={transaction}
|
||||
|
|
|
@ -13,11 +13,11 @@ import { ITransactionGroup } from '../../../../../server/lib/transaction_groups/
|
|||
import { fontFamilyCode, truncate } from '../../../../style/variables';
|
||||
import { asDecimal, asMillis } from '../../../../utils/formatters';
|
||||
import { ImpactBar } from '../../../shared/ImpactBar';
|
||||
import { KibanaLink } from '../../../shared/Links/KibanaLink';
|
||||
import { APMLink } from '../../../shared/Links/APMLink';
|
||||
import { legacyEncodeURIComponent } from '../../../shared/Links/url_helpers';
|
||||
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
|
||||
|
||||
const TransactionNameLink = styled(KibanaLink)`
|
||||
const TransactionNameLink = styled(APMLink)`
|
||||
${truncate('100%')};
|
||||
font-family: ${fontFamilyCode};
|
||||
`;
|
||||
|
@ -48,7 +48,7 @@ export function TransactionList({ items, serviceName, ...rest }: Props) {
|
|||
id="transaction-name-link-tooltip"
|
||||
content={transactionName || NOT_AVAILABLE_LABEL}
|
||||
>
|
||||
<TransactionNameLink hash={transactionPath}>
|
||||
<TransactionNameLink path={transactionPath}>
|
||||
{transactionName || NOT_AVAILABLE_LABEL}
|
||||
</TransactionNameLink>
|
||||
</EuiToolTip>
|
||||
|
|
|
@ -129,10 +129,7 @@ class KueryBarView extends Component {
|
|||
values={{
|
||||
apmIndexPatternTitle: `"${apmIndexPatternTitle}"`,
|
||||
setupInstructionsLink: (
|
||||
<KibanaLink
|
||||
pathname={'/app/kibana'}
|
||||
hash={`/home/tutorial/apm`}
|
||||
>
|
||||
<KibanaLink path={`/home/tutorial/apm`}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.kueryBar.setupInstructionsLinkLabel',
|
||||
{ defaultMessage: 'Setup Instructions' }
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { getRenderedHref } from '../../../utils/testHelpers';
|
||||
import { APMLink } from './APMLink';
|
||||
|
||||
test('APMLink should produce the correct URL', async () => {
|
||||
const href = await getRenderedHref(
|
||||
() => <APMLink path="/some/path" query={{ transactionId: 'blah' }} />,
|
||||
{
|
||||
search: '?rangeFrom=now-5h&rangeTo=now-2h'
|
||||
} as Location
|
||||
);
|
||||
|
||||
expect(href).toMatchInlineSnapshot(
|
||||
`"#/some/path?rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0&transactionId=blah"`
|
||||
);
|
||||
});
|
||||
|
||||
test('APMLink should retain current kuery value if it exists', async () => {
|
||||
const href = await getRenderedHref(
|
||||
() => <APMLink path="/some/path" query={{ transactionId: 'blah' }} />,
|
||||
{
|
||||
search: '?kuery=host.hostname~20~3A~20~22fakehostname~22'
|
||||
} as Location
|
||||
);
|
||||
|
||||
expect(href).toMatchInlineSnapshot(
|
||||
`"#/some/path?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=host.hostname~20~3A~20~22fakehostname~22&transactionId=blah"`
|
||||
);
|
||||
});
|
||||
|
||||
test('APMLink should overwrite current kuery value if new kuery value is provided', async () => {
|
||||
const href = await getRenderedHref(
|
||||
() => (
|
||||
<APMLink
|
||||
path="/some/path"
|
||||
query={{ kuery: 'host.os~20~3A~20~22linux~22' }}
|
||||
/>
|
||||
),
|
||||
{
|
||||
search: '?kuery=host.hostname~20~3A~20~22fakehostname~22'
|
||||
} as Location
|
||||
);
|
||||
|
||||
expect(href).toMatchInlineSnapshot(
|
||||
`"#/some/path?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=host.os~20~3A~20~22linux~22"`
|
||||
);
|
||||
});
|
|
@ -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 { EuiLink, EuiLinkAnchorProps } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import url from 'url';
|
||||
import { pick } from 'lodash';
|
||||
import { useLocation } from '../../../hooks/useLocation';
|
||||
import { APMQueryParams, toQuery, fromQuery } from './url_helpers';
|
||||
import { TIMEPICKER_DEFAULTS } from '../../../store/urlParams';
|
||||
|
||||
interface Props extends EuiLinkAnchorProps {
|
||||
path?: string;
|
||||
query?: APMQueryParams;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const PERSISTENT_APM_PARAMS = [
|
||||
'kuery',
|
||||
'rangeFrom',
|
||||
'rangeTo',
|
||||
'refreshPaused',
|
||||
'refreshInterval'
|
||||
];
|
||||
|
||||
export function getAPMHref(
|
||||
path: string,
|
||||
currentSearch: string, // TODO: Replace with passing in URL PARAMS here
|
||||
query: APMQueryParams = {}
|
||||
) {
|
||||
const currentQuery = toQuery(currentSearch);
|
||||
const nextQuery = {
|
||||
...TIMEPICKER_DEFAULTS,
|
||||
...pick(currentQuery, PERSISTENT_APM_PARAMS),
|
||||
...query
|
||||
};
|
||||
const nextSearch = fromQuery(nextQuery);
|
||||
|
||||
return url.format({
|
||||
pathname: '',
|
||||
hash: `${path}?${nextSearch}`
|
||||
});
|
||||
}
|
||||
|
||||
export function APMLink({ path = '', query, ...rest }: Props) {
|
||||
const { search } = useLocation();
|
||||
const href = getAPMHref(path, search, query);
|
||||
return <EuiLink {...rest} href={href} />;
|
||||
}
|
|
@ -4,27 +4,54 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { KibanaRisonLink } from '../KibanaRisonLink';
|
||||
import { RisonAPMQueryParams } from '../rison_helpers';
|
||||
import { QueryWithIndexPattern } from './QueryWithIndexPattern';
|
||||
import chrome from 'ui/chrome';
|
||||
import url from 'url';
|
||||
import rison, { RisonValue } from 'rison-node';
|
||||
import { useAPMIndexPattern } from '../../../../hooks/useAPMIndexPattern';
|
||||
import { useLocation } from '../../../../hooks/useLocation';
|
||||
import { getTimepickerRisonData } from '../rison_helpers';
|
||||
|
||||
interface Props {
|
||||
query: RisonAPMQueryParams;
|
||||
query: {
|
||||
_a?: {
|
||||
index?: string;
|
||||
interval?: string;
|
||||
query?: {
|
||||
language: string;
|
||||
query: string;
|
||||
};
|
||||
sort?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function DiscoverLink({ query, ...rest }: Props) {
|
||||
return (
|
||||
<QueryWithIndexPattern query={query}>
|
||||
{queryWithIndexPattern => (
|
||||
<KibanaRisonLink
|
||||
pathname={'/app/kibana'}
|
||||
hash={'/discover'}
|
||||
query={queryWithIndexPattern}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
</QueryWithIndexPattern>
|
||||
);
|
||||
export function DiscoverLink({ query = {}, ...rest }: Props) {
|
||||
const apmIndexPattern = useAPMIndexPattern();
|
||||
const location = useLocation();
|
||||
|
||||
if (!apmIndexPattern.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const risonQuery = {
|
||||
_g: getTimepickerRisonData(location.search),
|
||||
_a: {
|
||||
...query._a,
|
||||
index: apmIndexPattern.id
|
||||
}
|
||||
};
|
||||
|
||||
const href = url.format({
|
||||
pathname: chrome.addBasePath('/app/kibana'),
|
||||
hash: `/discover?_g=${rison.encode(risonQuery._g)}&_a=${rison.encode(
|
||||
risonQuery._a as RisonValue
|
||||
)}`
|
||||
});
|
||||
|
||||
return <EuiLink {...rest} href={href} />;
|
||||
}
|
||||
|
|
|
@ -1,55 +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, { ReactElement } from 'react';
|
||||
import {
|
||||
getAPMIndexPattern,
|
||||
ISavedObject
|
||||
} from '../../../../services/rest/savedObjects';
|
||||
import { RisonAPMQueryParams } from '../rison_helpers';
|
||||
|
||||
export function getQueryWithIndexPattern(
|
||||
query: RisonAPMQueryParams,
|
||||
indexPattern?: ISavedObject
|
||||
) {
|
||||
if ((query._a && query._a.index) || !indexPattern) {
|
||||
return query;
|
||||
}
|
||||
|
||||
const id = indexPattern && indexPattern.id;
|
||||
return {
|
||||
...query,
|
||||
_a: {
|
||||
...query._a,
|
||||
index: id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
query: RisonAPMQueryParams;
|
||||
children: (query: RisonAPMQueryParams) => ReactElement<unknown>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
indexPattern?: ISavedObject;
|
||||
}
|
||||
|
||||
export class QueryWithIndexPattern extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
getAPMIndexPattern().then(indexPattern => {
|
||||
this.setState({ indexPattern });
|
||||
});
|
||||
this.state = {};
|
||||
}
|
||||
public render() {
|
||||
const { children, query } = this.props;
|
||||
const { indexPattern } = this.state;
|
||||
const renderWithQuery = children;
|
||||
return renderWithQuery(getQueryWithIndexPattern(query, indexPattern));
|
||||
}
|
||||
}
|
|
@ -21,6 +21,14 @@ jest
|
|||
Promise.resolve({ id: 'apm-index-pattern-id' } as savedObjects.ISavedObject)
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => null);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('DiscoverTransactionLink should produce the correct URL', async () => {
|
||||
const transaction = {
|
||||
transaction: {
|
||||
|
@ -30,11 +38,12 @@ test('DiscoverTransactionLink should produce the correct URL', async () => {
|
|||
id: '8b60bd32ecc6e1506735a8b6cfcf175c'
|
||||
}
|
||||
} as Transaction;
|
||||
|
||||
const href = await getRenderedHref(
|
||||
() => <DiscoverTransactionLink transaction={transaction} />,
|
||||
{
|
||||
location: { search: '?rangeFrom=now/w&rangeTo=now' } as Location
|
||||
}
|
||||
search: '?rangeFrom=now/w&rangeTo=now'
|
||||
} as Location
|
||||
);
|
||||
|
||||
expect(href).toEqual(
|
||||
|
@ -48,9 +57,10 @@ test('DiscoverSpanLink should produce the correct URL', async () => {
|
|||
id: 'test-span-id'
|
||||
}
|
||||
} as Span;
|
||||
|
||||
const href = await getRenderedHref(() => <DiscoverSpanLink span={span} />, {
|
||||
location: { search: '?rangeFrom=now/w&rangeTo=now' } as Location
|
||||
});
|
||||
search: '?rangeFrom=now/w&rangeTo=now'
|
||||
} as Location);
|
||||
|
||||
expect(href).toEqual(
|
||||
`/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm-index-pattern-id,interval:auto,query:(language:lucene,query:'span.id:"test-span-id"'))`
|
||||
|
@ -69,8 +79,8 @@ test('DiscoverErrorLink should produce the correct URL', async () => {
|
|||
const href = await getRenderedHref(
|
||||
() => <DiscoverErrorLink error={error} />,
|
||||
{
|
||||
location: { search: '?rangeFrom=now/w&rangeTo=now' } as Location
|
||||
}
|
||||
search: '?rangeFrom=now/w&rangeTo=now'
|
||||
} as Location
|
||||
);
|
||||
|
||||
expect(href).toEqual(
|
||||
|
@ -87,11 +97,12 @@ test('DiscoverErrorLink should include optional kuery string in URL', async () =
|
|||
grouping_key: 'grouping-key'
|
||||
}
|
||||
} as APMError;
|
||||
|
||||
const href = await getRenderedHref(
|
||||
() => <DiscoverErrorLink error={error} kuery="some:kuery-string" />,
|
||||
{
|
||||
location: { search: '?rangeFrom=now/w&rangeTo=now' } as Location
|
||||
}
|
||||
search: '?rangeFrom=now/w&rangeTo=now'
|
||||
} as Location
|
||||
);
|
||||
|
||||
expect(href).toEqual(
|
||||
|
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { getRenderedHref } from '../../../utils/testHelpers';
|
||||
import { InfraLink } from './InfraLink';
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
jest
|
||||
.spyOn(chrome, 'addBasePath')
|
||||
.mockImplementation(path => `/basepath${path}`);
|
||||
|
||||
test('InfraLink produces the correct URL', async () => {
|
||||
const href = await getRenderedHref(
|
||||
() => <InfraLink path="/some/path" query={{ time: 1554687198 }} />,
|
||||
{
|
||||
search: '?rangeFrom=now-5h&rangeTo=now-2h'
|
||||
} as Location
|
||||
);
|
||||
|
||||
expect(href).toMatchInlineSnapshot(
|
||||
`"/basepath/app/infra#/some/path?time=1554687198"`
|
||||
);
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { EuiLink, EuiLinkAnchorProps } from '@elastic/eui';
|
||||
import { compact } from 'lodash';
|
||||
import React from 'react';
|
||||
import chrome from 'ui/chrome';
|
||||
import url from 'url';
|
||||
import { fromQuery } from './url_helpers';
|
||||
|
||||
interface InfraQueryParams {
|
||||
time?: number;
|
||||
from?: number;
|
||||
to?: number;
|
||||
}
|
||||
|
||||
interface Props extends EuiLinkAnchorProps {
|
||||
path?: string;
|
||||
query: InfraQueryParams;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function InfraLink({ path, query = {}, ...rest }: Props) {
|
||||
const nextSearch = fromQuery(query);
|
||||
const href = url.format({
|
||||
pathname: chrome.addBasePath('/app/infra'),
|
||||
hash: compact([path, nextSearch]).join('?')
|
||||
});
|
||||
return <EuiLink {...rest} href={href} />;
|
||||
}
|
|
@ -4,67 +4,20 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { UnconnectedKibanaLink } from './KibanaLink';
|
||||
import { getRenderedHref } from '../../../utils/testHelpers';
|
||||
import { KibanaLink } from './KibanaLink';
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
const getLinkWrapper = ({
|
||||
search = '',
|
||||
pathname = '/app/kibana',
|
||||
hash = '/something',
|
||||
children = 'Some link text',
|
||||
query = {}
|
||||
} = {}) =>
|
||||
shallow(
|
||||
<UnconnectedKibanaLink
|
||||
location={{ search } as Location}
|
||||
pathname={pathname}
|
||||
hash={hash}
|
||||
children={children}
|
||||
query={query}
|
||||
/>
|
||||
);
|
||||
jest
|
||||
.spyOn(chrome, 'addBasePath')
|
||||
.mockImplementation(path => `/basepath${path}`);
|
||||
|
||||
describe('UnconnectedKibanaLink', () => {
|
||||
it('should render correct markup', () => {
|
||||
expect(getLinkWrapper()).toMatchSnapshot();
|
||||
});
|
||||
test('KibanaLink produces the correct URL', async () => {
|
||||
const href = await getRenderedHref(() => <KibanaLink path="/some/path" />, {
|
||||
search: '?rangeFrom=now-5h&rangeTo=now-2h'
|
||||
} as Location);
|
||||
|
||||
it('should include valid query params', () => {
|
||||
const wrapper = getLinkWrapper({ query: { transactionId: 'test-id' } });
|
||||
expect(wrapper.find('EuiLink').props().href).toEqual(
|
||||
'/app/kibana#/something?transactionId=test-id'
|
||||
);
|
||||
});
|
||||
|
||||
it('should include existing APM params for APM links', () => {
|
||||
const wrapper = getLinkWrapper({
|
||||
pathname: '/app/apm',
|
||||
search: '?rangeFrom=now-5w&rangeTo=now-2w'
|
||||
});
|
||||
expect(wrapper.find('EuiLink').props().href).toEqual(
|
||||
`/app/apm#/something?rangeFrom=now-5w&rangeTo=now-2w&refreshPaused=true&refreshInterval=0`
|
||||
);
|
||||
});
|
||||
|
||||
it('should include APM params when the pathname is an empty string', () => {
|
||||
const wrapper = getLinkWrapper({
|
||||
pathname: '',
|
||||
search: '?rangeFrom=now-5w&rangeTo=now-2w'
|
||||
});
|
||||
expect(wrapper.find('EuiLink').props().href).toEqual(
|
||||
`#/something?rangeFrom=now-5w&rangeTo=now-2w&refreshPaused=true&refreshInterval=0`
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT include APM params for non-APM links', () => {
|
||||
const wrapper = getLinkWrapper({
|
||||
pathname: '/app/something-else',
|
||||
search: '?rangeFrom=now-5w&rangeTo=now-2w'
|
||||
});
|
||||
expect(wrapper.find('EuiLink').props().href).toEqual(
|
||||
`/app/something-else#/something?`
|
||||
);
|
||||
});
|
||||
expect(href).toMatchInlineSnapshot(`"/basepath/app/kibana#/some/path"`);
|
||||
});
|
||||
|
|
|
@ -4,47 +4,20 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { Location } from 'history';
|
||||
import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { StringMap } from '../../../../typings/common';
|
||||
import { getKibanaHref, KibanaHrefArgs } from './url_helpers';
|
||||
import chrome from 'ui/chrome';
|
||||
import url from 'url';
|
||||
|
||||
interface Props extends KibanaHrefArgs {
|
||||
disabled?: boolean;
|
||||
to?: StringMap;
|
||||
className?: string;
|
||||
[prop: string]: any;
|
||||
interface Props extends EuiLinkAnchorProps {
|
||||
path?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: Use this component directly if you have to use a link that is
|
||||
* going to be rendered outside of React, e.g. in the Kibana global toast loader.
|
||||
*
|
||||
* You must remember to pass in location in that case.
|
||||
*/
|
||||
const UnconnectedKibanaLink: React.FunctionComponent<Props> = ({
|
||||
location,
|
||||
pathname,
|
||||
hash,
|
||||
query,
|
||||
...props
|
||||
}) => {
|
||||
const href = getKibanaHref({
|
||||
location,
|
||||
pathname,
|
||||
hash,
|
||||
query
|
||||
export function KibanaLink({ path, ...rest }: Props) {
|
||||
const href = url.format({
|
||||
pathname: chrome.addBasePath('/app/kibana'),
|
||||
hash: path
|
||||
});
|
||||
return <EuiLink {...props} href={href} />;
|
||||
};
|
||||
|
||||
const withLocation = connect(
|
||||
({ location }: { location: Location }) => ({ location }),
|
||||
{}
|
||||
);
|
||||
|
||||
const KibanaLink = withLocation(UnconnectedKibanaLink);
|
||||
|
||||
export { UnconnectedKibanaLink, KibanaLink };
|
||||
return <EuiLink {...rest} href={href} />;
|
||||
}
|
||||
|
|
|
@ -1,90 +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 { shallow } from 'enzyme';
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { UnconnectedKibanaRisonLink } from './KibanaRisonLink';
|
||||
|
||||
const getLinkWrapper = ({
|
||||
search = '',
|
||||
pathname = '/app/kibana',
|
||||
hash = '/discover',
|
||||
children = 'Some discover link text',
|
||||
query = {}
|
||||
} = {}) =>
|
||||
shallow(
|
||||
<UnconnectedKibanaRisonLink
|
||||
location={{ search } as Location}
|
||||
pathname={pathname}
|
||||
hash={hash}
|
||||
children={children}
|
||||
query={query}
|
||||
/>
|
||||
);
|
||||
|
||||
const DEFAULT_RISON_G = `(refreshInterval:(pause:true,value:'0'),time:(from:now-24h,to:now))`;
|
||||
|
||||
describe('UnconnectedKibanaLink', () => {
|
||||
it('should render correct markup', () => {
|
||||
expect(getLinkWrapper()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should include default time picker values, rison-encoded', () => {
|
||||
const wrapper = getLinkWrapper();
|
||||
expect(wrapper.find('EuiLink').props().href).toEqual(
|
||||
expect.stringContaining(DEFAULT_RISON_G)
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore new query params except for _g and _a', () => {
|
||||
const wrapper = getLinkWrapper({ query: { transactionId: 'test-id' } });
|
||||
expect(wrapper.find('EuiLink').props().href).not.toEqual(
|
||||
expect.stringContaining('transactionId')
|
||||
);
|
||||
});
|
||||
|
||||
it('should rison-encode and merge in custom _g value', () => {
|
||||
const wrapper = getLinkWrapper({
|
||||
query: {
|
||||
_g: {
|
||||
something: {
|
||||
nested: 'custom g value'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(wrapper.find('EuiLink').props().href).toEqual(
|
||||
expect.stringContaining(`something:(nested:'custom g value')`)
|
||||
);
|
||||
});
|
||||
|
||||
it('should rison-encode custom _a value', () => {
|
||||
const wrapper = getLinkWrapper({
|
||||
query: {
|
||||
_a: {
|
||||
something: {
|
||||
nested: 'custom a value'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(wrapper.find('EuiLink').props().href).toEqual(
|
||||
expect.stringContaining(`_a=(something:(nested:'custom a value'))`)
|
||||
);
|
||||
});
|
||||
|
||||
it('should convert, url-encode, and rison-encode existing time picker values', () => {
|
||||
const wrapper = getLinkWrapper({
|
||||
search:
|
||||
'?rangeFrom=now/w&rangeTo=now&refreshPaused=false&refreshInterval=30000'
|
||||
});
|
||||
expect(wrapper.find('EuiLink').props().href).toEqual(
|
||||
"/app/kibana#/discover?_g=(refreshInterval:(pause:false,value:'30000'),time:(from:now%2Fw,to:now))"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,49 +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 { EuiLink } from '@elastic/eui';
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { StringMap } from '../../../../typings/common';
|
||||
import { getRisonHref, RisonHrefArgs } from './rison_helpers';
|
||||
|
||||
interface Props extends RisonHrefArgs {
|
||||
disabled?: boolean;
|
||||
to?: StringMap;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: Use this component directly if you have to use a link that is
|
||||
* going to be rendered outside of React, e.g. in the Kibana global toast loader.
|
||||
*
|
||||
* You must remember to pass in location in that case.
|
||||
*/
|
||||
const UnconnectedKibanaRisonLink: React.FunctionComponent<Props> = ({
|
||||
location,
|
||||
pathname,
|
||||
hash,
|
||||
query,
|
||||
...props
|
||||
}) => {
|
||||
const href = getRisonHref({
|
||||
location,
|
||||
pathname,
|
||||
hash,
|
||||
query
|
||||
});
|
||||
return <EuiLink {...props} href={href} />;
|
||||
};
|
||||
|
||||
const withLocation = connect(
|
||||
({ location }: { location: Location }) => ({ location }),
|
||||
{}
|
||||
);
|
||||
|
||||
const KibanaRisonLink = withLocation(UnconnectedKibanaRisonLink);
|
||||
|
||||
export { UnconnectedKibanaRisonLink, KibanaRisonLink };
|
|
@ -4,35 +4,22 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { getRenderedHref } from '../../../utils/testHelpers';
|
||||
import { getRenderedHref } from '../../../../utils/testHelpers';
|
||||
import { MLJobLink } from './MLJobLink';
|
||||
|
||||
describe('MLJobLink', () => {
|
||||
it('should render component', () => {
|
||||
const location = { search: '' } as Location;
|
||||
const wrapper = shallow(
|
||||
<MLJobLink
|
||||
serviceName="myServiceName"
|
||||
transactionType="myTransactionType"
|
||||
location={location}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should produce the correct URL', async () => {
|
||||
const location = { search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location;
|
||||
const href = await getRenderedHref(() => (
|
||||
<MLJobLink
|
||||
serviceName="myServiceName"
|
||||
transactionType="myTransactionType"
|
||||
location={location}
|
||||
/>
|
||||
));
|
||||
const href = await getRenderedHref(
|
||||
() => (
|
||||
<MLJobLink
|
||||
serviceName="myServiceName"
|
||||
transactionType="myTransactionType"
|
||||
/>
|
||||
),
|
||||
{ search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location
|
||||
);
|
||||
|
||||
expect(href).toEqual(
|
||||
`/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))`
|
|
@ -4,36 +4,26 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { getMlJobId } from '../../../../common/ml_job_constants';
|
||||
import { getRisonHref } from './rison_helpers';
|
||||
import { getMlJobId } from '../../../../../common/ml_job_constants';
|
||||
import { MLLink } from './MLLink';
|
||||
|
||||
interface Props {
|
||||
serviceName: string;
|
||||
transactionType?: string;
|
||||
location: Location;
|
||||
}
|
||||
|
||||
export const MLJobLink: React.SFC<Props> = ({
|
||||
serviceName,
|
||||
transactionType,
|
||||
location,
|
||||
children
|
||||
}) => {
|
||||
const pathname = '/app/ml';
|
||||
const hash = '/timeseriesexplorer';
|
||||
const jobId = getMlJobId(serviceName, transactionType);
|
||||
const query = {
|
||||
_g: { ml: { jobIds: [jobId] } }
|
||||
ml: { jobIds: [jobId] }
|
||||
};
|
||||
|
||||
const href = getRisonHref({
|
||||
location,
|
||||
pathname,
|
||||
hash,
|
||||
query
|
||||
});
|
||||
return <EuiLink children={children} href={href} />;
|
||||
return (
|
||||
<MLLink children={children} query={query} path="/timeseriesexplorer" />
|
||||
);
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { getRenderedHref } from '../../../../utils/testHelpers';
|
||||
import { MLLink } from './MLLink';
|
||||
import chrome from 'ui/chrome';
|
||||
import * as savedObjects from '../../../../services/rest/savedObjects';
|
||||
|
||||
jest
|
||||
.spyOn(chrome, 'addBasePath')
|
||||
.mockImplementation(path => `/basepath${path}`);
|
||||
|
||||
jest
|
||||
.spyOn(savedObjects, 'getAPMIndexPattern')
|
||||
.mockReturnValue(
|
||||
Promise.resolve({ id: 'apm-index-pattern-id' } as savedObjects.ISavedObject)
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => null);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('MLLink produces the correct URL', async () => {
|
||||
const href = await getRenderedHref(
|
||||
() => (
|
||||
<MLLink path="/some/path" query={{ ml: { jobIds: ['something'] } }} />
|
||||
),
|
||||
{
|
||||
search: '?rangeFrom=now-5h&rangeTo=now-2h'
|
||||
} as Location
|
||||
);
|
||||
|
||||
expect(href).toMatchInlineSnapshot(
|
||||
`"/basepath/app/ml#/some/path?_g=(ml:(jobIds:!(something)),refreshInterval:(pause:true,value:'0'),time:(from:now-5h,to:now-2h))"`
|
||||
);
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { EuiLink } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import chrome from 'ui/chrome';
|
||||
import url from 'url';
|
||||
import rison, { RisonValue } from 'rison-node';
|
||||
import { useLocation } from '../../../../hooks/useLocation';
|
||||
import { getTimepickerRisonData, TimepickerRisonData } from '../rison_helpers';
|
||||
|
||||
interface MlRisonData {
|
||||
ml?: {
|
||||
jobIds: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
query?: MlRisonData;
|
||||
path?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function MLLink({ children, path = '', query = {} }: Props) {
|
||||
const location = useLocation();
|
||||
|
||||
const risonQuery: MlRisonData & TimepickerRisonData = getTimepickerRisonData(
|
||||
location.search
|
||||
);
|
||||
|
||||
if (query.ml) {
|
||||
risonQuery.ml = query.ml;
|
||||
}
|
||||
|
||||
const href = url.format({
|
||||
pathname: chrome.addBasePath('/app/ml'),
|
||||
hash: `${path}?_g=${rison.encode(risonQuery as RisonValue)}`
|
||||
});
|
||||
|
||||
return <EuiLink children={children} href={href} />;
|
||||
}
|
|
@ -15,7 +15,7 @@ export function SetupInstructionsLink({
|
|||
buttonFill?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<KibanaLink pathname={'/app/kibana'} hash={'/home/tutorial/apm'}>
|
||||
<KibanaLink path={'/home/tutorial/apm'}>
|
||||
<EuiButton size="s" color="primary" fill={buttonFill}>
|
||||
{i18n.translate('xpack.apm.setupInstructionsButtonLabel', {
|
||||
defaultMessage: 'Setup Instructions'
|
||||
|
|
|
@ -6,33 +6,13 @@
|
|||
|
||||
import React from 'react';
|
||||
import { Transaction } from '../../../../typings/es_schemas/ui/Transaction';
|
||||
import { KibanaLink } from './KibanaLink';
|
||||
import { APMLink } from './APMLink';
|
||||
import { legacyEncodeURIComponent } from './url_helpers';
|
||||
|
||||
interface TransactionLinkProps {
|
||||
transaction?: Transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path and query used to build a trace link
|
||||
*/
|
||||
export function getLinkProps(transaction: Transaction) {
|
||||
const serviceName = transaction.service.name;
|
||||
const transactionType = transaction.transaction.type;
|
||||
const traceId = transaction.trace.id;
|
||||
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 TransactionLink: React.SFC<TransactionLinkProps> = ({
|
||||
transaction,
|
||||
children
|
||||
|
@ -41,12 +21,19 @@ export const TransactionLink: React.SFC<TransactionLinkProps> = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
const linkProps = getLinkProps(transaction);
|
||||
const serviceName = transaction.service.name;
|
||||
const transactionType = transaction.transaction.type;
|
||||
const traceId = transaction.trace.id;
|
||||
const transactionId = transaction.transaction.id;
|
||||
const name = transaction.transaction.name;
|
||||
const encodedName = legacyEncodeURIComponent(name);
|
||||
|
||||
if (!linkProps) {
|
||||
// TODO: Should this case return unlinked children, null, or something else?
|
||||
return <React.Fragment>{children}</React.Fragment>;
|
||||
}
|
||||
|
||||
return <KibanaLink {...linkProps}>{children}</KibanaLink>;
|
||||
return (
|
||||
<APMLink
|
||||
path={`/${serviceName}/transactions/${transactionType}/${encodedName}`}
|
||||
query={{ traceId, transactionId }}
|
||||
>
|
||||
{children}
|
||||
</APMLink>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UnconnectedKibanaLink should render correct markup 1`] = `
|
||||
<EuiLink
|
||||
color="primary"
|
||||
href="/app/kibana#/something?"
|
||||
type="button"
|
||||
>
|
||||
Some link text
|
||||
</EuiLink>
|
||||
`;
|
|
@ -1,11 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UnconnectedKibanaLink should render correct markup 1`] = `
|
||||
<EuiLink
|
||||
color="primary"
|
||||
href="/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now-24h,to:now))"
|
||||
type="button"
|
||||
>
|
||||
Some discover link text
|
||||
</EuiLink>
|
||||
`;
|
|
@ -1,9 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MLJobLink should render component 1`] = `
|
||||
<EuiLink
|
||||
color="primary"
|
||||
href="/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now-24h,to:now))"
|
||||
type="button"
|
||||
/>
|
||||
`;
|
|
@ -5,88 +5,34 @@
|
|||
*/
|
||||
|
||||
import { Location } from 'history';
|
||||
import { pick, set } from 'lodash';
|
||||
import qs from 'querystring';
|
||||
import rison from 'rison-node';
|
||||
import chrome from 'ui/chrome';
|
||||
import url from 'url';
|
||||
import { StringMap } from '../../../../typings/common';
|
||||
import { TIMEPICKER_DEFAULTS } from '../../../store/urlParams';
|
||||
import { APMQueryParams, PERSISTENT_APM_PARAMS, toQuery } from './url_helpers';
|
||||
import { toQuery } from './url_helpers';
|
||||
|
||||
interface RisonEncoded {
|
||||
_g?: string;
|
||||
_a?: string;
|
||||
export interface TimepickerRisonData {
|
||||
time?: {
|
||||
from?: string;
|
||||
to?: string;
|
||||
};
|
||||
refreshInterval?: {
|
||||
pause?: boolean | string;
|
||||
value?: number | string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RisonDecoded {
|
||||
_g?: StringMap<any>;
|
||||
_a?: StringMap<any>;
|
||||
}
|
||||
|
||||
export type RisonAPMQueryParams = APMQueryParams & RisonDecoded;
|
||||
export interface RisonHrefArgs {
|
||||
location: Location;
|
||||
pathname?: string;
|
||||
hash?: string;
|
||||
query?: RisonAPMQueryParams;
|
||||
}
|
||||
|
||||
function createG(query: RisonAPMQueryParams) {
|
||||
const { _g: nextG = {} } = query;
|
||||
const g: RisonDecoded['_g'] = { ...nextG };
|
||||
|
||||
if (typeof query.rangeFrom !== 'undefined') {
|
||||
set(g, 'time.from', encodeURIComponent(query.rangeFrom));
|
||||
}
|
||||
if (typeof query.rangeTo !== 'undefined') {
|
||||
set(g, 'time.to', encodeURIComponent(query.rangeTo));
|
||||
}
|
||||
|
||||
if (typeof query.refreshPaused !== 'undefined') {
|
||||
set(g, 'refreshInterval.pause', String(query.refreshPaused));
|
||||
}
|
||||
if (typeof query.refreshInterval !== 'undefined') {
|
||||
set(g, 'refreshInterval.value', String(query.refreshInterval));
|
||||
}
|
||||
|
||||
return g;
|
||||
}
|
||||
|
||||
export function getRisonHref({
|
||||
location,
|
||||
pathname,
|
||||
hash,
|
||||
query = {}
|
||||
}: RisonHrefArgs) {
|
||||
const currentQuery = toQuery(location.search);
|
||||
export function getTimepickerRisonData(currentSearch: Location['search']) {
|
||||
const currentQuery = toQuery(currentSearch);
|
||||
const nextQuery = {
|
||||
...TIMEPICKER_DEFAULTS,
|
||||
...pick(currentQuery, PERSISTENT_APM_PARAMS),
|
||||
...query
|
||||
...currentQuery
|
||||
};
|
||||
|
||||
// Create _g value for non-apm links
|
||||
const g = createG(nextQuery);
|
||||
const encodedG = rison.encode(g);
|
||||
const encodedA = query._a ? rison.encode(query._a) : ''; // TODO: Do we need to url-encode the _a values before rison encoding _a?
|
||||
const risonQuery: RisonEncoded = {
|
||||
_g: encodedG
|
||||
return {
|
||||
time: {
|
||||
from: encodeURIComponent(nextQuery.rangeFrom),
|
||||
to: encodeURIComponent(nextQuery.rangeTo)
|
||||
},
|
||||
refreshInterval: {
|
||||
pause: String(nextQuery.refreshPaused),
|
||||
value: String(nextQuery.refreshInterval)
|
||||
}
|
||||
};
|
||||
|
||||
if (encodedA) {
|
||||
risonQuery._a = encodedA;
|
||||
}
|
||||
|
||||
// don't URI-encode the already-encoded rison
|
||||
const search = qs.stringify(risonQuery, undefined, undefined, {
|
||||
encodeURIComponent: (v: string) => v
|
||||
});
|
||||
|
||||
const href = url.format({
|
||||
pathname: chrome.addBasePath(pathname),
|
||||
hash: `${hash}?${search}`
|
||||
});
|
||||
|
||||
return href;
|
||||
}
|
||||
|
|
|
@ -4,13 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Location } from 'history';
|
||||
import url from 'url';
|
||||
// @ts-ignore
|
||||
import { toJson } from '../testHelpers';
|
||||
import {
|
||||
fromQuery,
|
||||
getKibanaHref,
|
||||
legacyDecodeURIComponent,
|
||||
legacyEncodeURIComponent,
|
||||
toQuery
|
||||
|
@ -58,59 +55,6 @@ describe('fromQuery', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getKibanaHref', () => {
|
||||
it('should build correct URL for APM paths, merging in existing date range params', () => {
|
||||
const location = { search: '?rangeFrom=now/w&rangeTo=now-24h' } as Location;
|
||||
const pathname = '/app/apm';
|
||||
const hash = '/services/x/transactions';
|
||||
const query = { transactionId: 'something' };
|
||||
const href = getKibanaHref({ location, pathname, hash, query });
|
||||
expect(href).toEqual(
|
||||
'/app/apm#/services/x/transactions?rangeFrom=now%2Fw&rangeTo=now-24h&refreshPaused=true&refreshInterval=0&transactionId=something'
|
||||
);
|
||||
});
|
||||
|
||||
it('should build correct url for non-APM paths, ignoring date range params', () => {
|
||||
const location = { search: '?rangeFrom=now/w&rangeTo=now-24h' } as Location;
|
||||
const pathname = '/app/kibana';
|
||||
const hash = '/outside';
|
||||
const query = { transactionId: 'something' };
|
||||
const href = getKibanaHref({ location, pathname, hash, query });
|
||||
expect(href).toEqual('/app/kibana#/outside?transactionId=something');
|
||||
});
|
||||
|
||||
describe('when location contains kuery', () => {
|
||||
const location = {
|
||||
search: '?kuery=transaction.duration.us~20~3E~201'
|
||||
} as Location;
|
||||
|
||||
it('should preserve kql for apm links', () => {
|
||||
const pathname = '/app/apm';
|
||||
const href = getKibanaHref({ location, pathname });
|
||||
const { kuery } = getUrlQuery(href);
|
||||
expect(kuery).toEqual('transaction.duration.us~20~3E~201');
|
||||
});
|
||||
|
||||
it('should preserve kql for links without path', () => {
|
||||
const href = getKibanaHref({ location });
|
||||
const { kuery } = getUrlQuery(href);
|
||||
expect(kuery).toEqual('transaction.duration.us~20~3E~201');
|
||||
});
|
||||
|
||||
it('should not preserve kql for non-apm links', () => {
|
||||
const pathname = '/app/kibana';
|
||||
const href = getKibanaHref({ location, pathname });
|
||||
const { kuery } = getUrlQuery(href);
|
||||
expect(kuery).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getUrlQuery(href: string) {
|
||||
const hash = url.parse(href).hash!.slice(1);
|
||||
return url.parse(hash, true).query;
|
||||
}
|
||||
|
||||
describe('legacyEncodeURIComponent', () => {
|
||||
it('should encode a string with forward slashes', () => {
|
||||
expect(legacyEncodeURIComponent('a/b/c')).toBe('a~2Fb~2Fc');
|
||||
|
|
|
@ -4,77 +4,18 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Location } from 'history';
|
||||
import createHistory from 'history/createHashHistory';
|
||||
import { pick } from 'lodash';
|
||||
import qs from 'querystring';
|
||||
import chrome from 'ui/chrome';
|
||||
import url from 'url';
|
||||
import { StringMap } from '../../../../typings/common';
|
||||
import { TIMEPICKER_DEFAULTS } from '../../../store/urlParams';
|
||||
|
||||
export function toQuery(search?: string): APMQueryParamsRaw {
|
||||
return search ? qs.parse(search.slice(1)) : {};
|
||||
}
|
||||
|
||||
export function fromQuery(query: APMQueryParams) {
|
||||
export function fromQuery(query: StringMap<any>) {
|
||||
return qs.stringify(query);
|
||||
}
|
||||
|
||||
export const PERSISTENT_APM_PARAMS = [
|
||||
'kuery',
|
||||
'rangeFrom',
|
||||
'rangeTo',
|
||||
'refreshPaused',
|
||||
'refreshInterval'
|
||||
];
|
||||
|
||||
function getSearchString(
|
||||
location: Location,
|
||||
pathname: string,
|
||||
query: APMQueryParams = {}
|
||||
) {
|
||||
const currentQuery = toQuery(location.search);
|
||||
|
||||
// Preserve existing params for apm links
|
||||
const isApmLink = pathname.includes('app/apm') || pathname === '';
|
||||
if (isApmLink) {
|
||||
const nextQuery = {
|
||||
...TIMEPICKER_DEFAULTS,
|
||||
...pick(currentQuery, PERSISTENT_APM_PARAMS),
|
||||
...query
|
||||
};
|
||||
return fromQuery(nextQuery);
|
||||
}
|
||||
|
||||
return fromQuery(query);
|
||||
}
|
||||
|
||||
export type QueryStringMap = StringMap<
|
||||
string | number | boolean | undefined | null
|
||||
>;
|
||||
|
||||
export interface KibanaHrefArgs {
|
||||
location: Location;
|
||||
pathname?: string;
|
||||
hash?: string;
|
||||
query?: QueryStringMap;
|
||||
}
|
||||
|
||||
export function getKibanaHref({
|
||||
location,
|
||||
pathname = '',
|
||||
hash,
|
||||
query = {}
|
||||
}: KibanaHrefArgs): string {
|
||||
const search = getSearchString(location, pathname, query);
|
||||
const href = url.format({
|
||||
pathname: chrome.addBasePath(pathname),
|
||||
hash: `${hash}?${search}`
|
||||
});
|
||||
return href;
|
||||
}
|
||||
|
||||
export interface APMQueryParams {
|
||||
transactionId?: string;
|
||||
traceId?: string;
|
||||
|
|
|
@ -11,18 +11,14 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiPopover
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { idx } from '../../../../common/idx';
|
||||
import { Transaction } from '../../../../typings/es_schemas/ui/Transaction';
|
||||
import { getDiscoverQuery } from '../Links/DiscoverLinks/DiscoverTransactionLink';
|
||||
import { QueryWithIndexPattern } from '../Links/DiscoverLinks/QueryWithIndexPattern';
|
||||
import { getRisonHref } from '../Links/rison_helpers';
|
||||
import { getKibanaHref } from '../Links/url_helpers';
|
||||
import { DiscoverTransactionLink } from '../Links/DiscoverLinks/DiscoverTransactionLink';
|
||||
import { InfraLink } from '../Links/InfraLink';
|
||||
|
||||
function getInfraMetricsQuery(transaction: Transaction) {
|
||||
const plus5 = new Date(transaction['@timestamp']);
|
||||
|
@ -49,7 +45,6 @@ function ActionMenuButton({ onClick }: { onClick: () => void }) {
|
|||
|
||||
interface Props {
|
||||
readonly transaction: Transaction;
|
||||
readonly location: Location;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -70,12 +65,11 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
};
|
||||
|
||||
public getInfraActions() {
|
||||
const { transaction, location } = this.props;
|
||||
const { transaction } = this.props;
|
||||
const hostName = idx(transaction, _ => _.host.hostname);
|
||||
const podId = idx(transaction, _ => _.kubernetes.pod.uid);
|
||||
const containerId = idx(transaction, _ => _.container.id);
|
||||
const traceId = idx(transaction, _ => _.trace.id);
|
||||
const pathname = '/app/infra';
|
||||
const time = new Date(transaction['@timestamp']).getTime();
|
||||
const infraMetricsQuery = getInfraMetricsQuery(transaction);
|
||||
|
||||
|
@ -86,8 +80,8 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
'xpack.apm.transactionActionMenu.showPodLogsLinkLabel',
|
||||
{ defaultMessage: 'Show pod logs' }
|
||||
),
|
||||
target: podId,
|
||||
hash: `/link-to/pod-logs/${podId}`,
|
||||
condition: podId,
|
||||
path: `/link-to/pod-logs/${podId}`,
|
||||
query: { time }
|
||||
},
|
||||
{
|
||||
|
@ -96,8 +90,8 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
'xpack.apm.transactionActionMenu.showContainerLogsLinkLabel',
|
||||
{ defaultMessage: 'Show container logs' }
|
||||
),
|
||||
target: containerId,
|
||||
hash: `/link-to/container-logs/${containerId}`,
|
||||
condition: containerId,
|
||||
path: `/link-to/container-logs/${containerId}`,
|
||||
query: { time }
|
||||
},
|
||||
{
|
||||
|
@ -106,8 +100,8 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
'xpack.apm.transactionActionMenu.showHostLogsLinkLabel',
|
||||
{ defaultMessage: 'Show host logs' }
|
||||
),
|
||||
target: hostName,
|
||||
hash: `/link-to/host-logs/${hostName}`,
|
||||
condition: hostName,
|
||||
path: `/link-to/host-logs/${hostName}`,
|
||||
query: { time }
|
||||
},
|
||||
{
|
||||
|
@ -126,8 +120,8 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
'xpack.apm.transactionActionMenu.showPodMetricsLinkLabel',
|
||||
{ defaultMessage: 'Show pod metrics' }
|
||||
),
|
||||
target: podId,
|
||||
hash: `/link-to/pod-detail/${podId}`,
|
||||
condition: podId,
|
||||
path: `/link-to/pod-detail/${podId}`,
|
||||
query: infraMetricsQuery
|
||||
},
|
||||
{
|
||||
|
@ -136,8 +130,8 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
'xpack.apm.transactionActionMenu.showContainerMetricsLinkLabel',
|
||||
{ defaultMessage: 'Show container metrics' }
|
||||
),
|
||||
target: containerId,
|
||||
hash: `/link-to/container-detail/${containerId}`,
|
||||
condition: containerId,
|
||||
path: `/link-to/container-detail/${containerId}`,
|
||||
query: infraMetricsQuery
|
||||
},
|
||||
{
|
||||
|
@ -146,20 +140,20 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
'xpack.apm.transactionActionMenu.showHostMetricsLinkLabel',
|
||||
{ defaultMessage: 'Show host metrics' }
|
||||
),
|
||||
target: hostName,
|
||||
hash: `/link-to/host-detail/${hostName}`,
|
||||
condition: hostName,
|
||||
path: `/link-to/host-detail/${hostName}`,
|
||||
query: infraMetricsQuery
|
||||
}
|
||||
]
|
||||
.filter(({ target }) => Boolean(target))
|
||||
.map(({ icon, label, hash, query }, index) => {
|
||||
const href = getKibanaHref({ location, pathname, hash, query });
|
||||
|
||||
.filter(({ condition }) => Boolean(condition))
|
||||
.map(({ icon, label, path, query }, index) => {
|
||||
return (
|
||||
<EuiContextMenuItem icon={icon} href={href} key={index}>
|
||||
<EuiContextMenuItem icon={icon} key={index}>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiLink>{label}</EuiLink>
|
||||
<InfraLink path={path} query={query}>
|
||||
{label}
|
||||
</InfraLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="popout" />
|
||||
|
@ -171,62 +165,46 @@ export class TransactionActionMenu extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { transaction, location } = this.props;
|
||||
const { transaction } = this.props;
|
||||
|
||||
const items = [
|
||||
...this.getInfraActions(),
|
||||
<EuiContextMenuItem icon="discoverApp" key="discover-transaction">
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<DiscoverTransactionLink transaction={transaction}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel',
|
||||
{
|
||||
defaultMessage: 'View sample document'
|
||||
}
|
||||
)}
|
||||
</DiscoverTransactionLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="popout" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>
|
||||
];
|
||||
|
||||
return (
|
||||
<QueryWithIndexPattern query={getDiscoverQuery(transaction)}>
|
||||
{query => {
|
||||
const discoverTransactionHref = getRisonHref({
|
||||
location,
|
||||
pathname: '/app/kibana',
|
||||
hash: '/discover',
|
||||
query
|
||||
});
|
||||
|
||||
const items = [
|
||||
...this.getInfraActions(),
|
||||
<EuiContextMenuItem
|
||||
icon="discoverApp"
|
||||
href={discoverTransactionHref}
|
||||
key="discover-transaction"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiLink>
|
||||
{i18n.translate(
|
||||
'xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel',
|
||||
{
|
||||
defaultMessage: 'View sample document'
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="popout" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</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={i18n.translate(
|
||||
'xpack.apm.transactionActionMenu.actionsLabel',
|
||||
{ defaultMessage: 'Actions' }
|
||||
)}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}}
|
||||
</QueryWithIndexPattern>
|
||||
<EuiPopover
|
||||
id="transactionActionMenu"
|
||||
button={<ActionMenuButton onClick={this.toggle} />}
|
||||
isOpen={this.state.isOpen}
|
||||
closePopover={this.close}
|
||||
anchorPosition="downRight"
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
items={items}
|
||||
title={i18n.translate(
|
||||
'xpack.apm.transactionActionMenu.actionsLabel',
|
||||
{ defaultMessage: 'Actions' }
|
||||
)}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,14 +8,12 @@ import { shallow } from 'enzyme';
|
|||
import 'jest-styled-components';
|
||||
import React from 'react';
|
||||
import { TransactionActionMenu } from '../TransactionActionMenu';
|
||||
import { location, transaction } from './mockData';
|
||||
import { transaction } from './mockData';
|
||||
|
||||
describe('TransactionActionMenu component', () => {
|
||||
it('should render with data', () => {
|
||||
expect(
|
||||
shallow(
|
||||
<TransactionActionMenu transaction={transaction} location={location} />
|
||||
).shallow()
|
||||
shallow(<TransactionActionMenu transaction={transaction} />).shallow()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,235 +1,22 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TransactionActionMenu component should render with data 1`] = `
|
||||
<EuiPopover
|
||||
anchorPosition="downRight"
|
||||
button={
|
||||
<ActionMenuButton
|
||||
onClick={[Function]}
|
||||
/>
|
||||
}
|
||||
closePopover={[Function]}
|
||||
hasArrow={true}
|
||||
id="transactionActionMenu"
|
||||
isOpen={false}
|
||||
ownFocus={false}
|
||||
panelPaddingSize="none"
|
||||
<EuiOutsideClickDetector
|
||||
isDisabled={true}
|
||||
onOutsideClick={[Function]}
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
hasFocus={true}
|
||||
items={
|
||||
Array [
|
||||
<EuiContextMenuItem
|
||||
href="/app/infra#/link-to/pod-logs/pod123456abcdef?time=1545092070952"
|
||||
icon="loggingApp"
|
||||
layoutAlign="center"
|
||||
toolTipPosition="right"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
Show pod logs
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
type="popout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
href="/app/infra#/link-to/container-logs/container123456abcdef?time=1545092070952"
|
||||
icon="loggingApp"
|
||||
layoutAlign="center"
|
||||
toolTipPosition="right"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
Show container logs
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
type="popout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
href="/app/infra#/link-to/host-logs/227453131a17?time=1545092070952"
|
||||
icon="loggingApp"
|
||||
layoutAlign="center"
|
||||
toolTipPosition="right"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
Show host logs
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
type="popout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
href="/app/infra#/link-to/logs?time=1545092070952&filter=trace.id%3A8b60bd32ecc6e1506735a8b6cfcf175c"
|
||||
icon="loggingApp"
|
||||
layoutAlign="center"
|
||||
toolTipPosition="right"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
Show trace logs
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
type="popout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
href="/app/infra#/link-to/pod-detail/pod123456abcdef?from=1545091770952&to=1545092370952"
|
||||
icon="infraApp"
|
||||
layoutAlign="center"
|
||||
toolTipPosition="right"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
Show pod metrics
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
type="popout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
href="/app/infra#/link-to/container-detail/container123456abcdef?from=1545091770952&to=1545092370952"
|
||||
icon="infraApp"
|
||||
layoutAlign="center"
|
||||
toolTipPosition="right"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
Show container metrics
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
type="popout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
href="/app/infra#/link-to/host-detail/227453131a17?from=1545091770952&to=1545092370952"
|
||||
icon="infraApp"
|
||||
layoutAlign="center"
|
||||
toolTipPosition="right"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
Show host metrics
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
type="popout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
href="/app/kibana#/discover?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now-24h,to:now))&_a=(interval:auto,query:(language:lucene,query:'processor.event:\\"transaction\\" AND transaction.id:\\"8b60bd32ecc6e150\\" AND trace.id:\\"8b60bd32ecc6e1506735a8b6cfcf175c\\"'))"
|
||||
icon="discoverApp"
|
||||
layoutAlign="center"
|
||||
toolTipPosition="right"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
View sample document
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
type="popout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>,
|
||||
]
|
||||
}
|
||||
title="Actions"
|
||||
/>
|
||||
</EuiPopover>
|
||||
<div
|
||||
className="euiPopover euiPopover--anchorDownRight"
|
||||
id="transactionActionMenu"
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<div
|
||||
className="euiPopover__anchor"
|
||||
>
|
||||
<ActionMenuButton
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</EuiOutsideClickDetector>
|
||||
`;
|
||||
|
|
|
@ -22,7 +22,7 @@ import { ITransactionChartData } from '../../../../store/selectors/chartSelector
|
|||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { asInteger, asMillis, tpmUnit } from '../../../../utils/formatters';
|
||||
import { LicenseContext } from '../../../app/Main/LicenseCheck';
|
||||
import { MLJobLink } from '../../Links/MLJobLink';
|
||||
import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink';
|
||||
// @ts-ignore
|
||||
import CustomPlot from '../CustomPlot';
|
||||
import { SyncChartGroup } from '../SyncChartGroup';
|
||||
|
@ -112,7 +112,6 @@ export class TransactionCharts extends Component<TransactionChartProps> {
|
|||
<MLJobLink
|
||||
serviceName={serviceName}
|
||||
transactionType={transactionType}
|
||||
location={this.props.location}
|
||||
>
|
||||
View Job
|
||||
</MLJobLink>
|
||||
|
|
23
x-pack/plugins/apm/public/context/LocationContext.tsx
Normal file
23
x-pack/plugins/apm/public/context/LocationContext.tsx
Normal file
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { History, Location } from 'history';
|
||||
import React, { createContext, useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
history: History;
|
||||
}
|
||||
|
||||
const initialLocation = {} as Location;
|
||||
|
||||
const LocationContext = createContext(initialLocation);
|
||||
const LocationProvider: React.FC<Props> = ({ history, ...props }) => {
|
||||
const [location, setLocation] = useState(history.location);
|
||||
history.listen(updatedLocation => setLocation(updatedLocation));
|
||||
return <LocationContext.Provider {...props} value={location} />;
|
||||
};
|
||||
|
||||
export { LocationContext, LocationProvider };
|
28
x-pack/plugins/apm/public/hooks/useAPMIndexPattern.tsx
Normal file
28
x-pack/plugins/apm/public/hooks/useAPMIndexPattern.tsx
Normal file
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
getAPMIndexPattern,
|
||||
ISavedObject
|
||||
} from '../services/rest/savedObjects';
|
||||
|
||||
export function useAPMIndexPattern() {
|
||||
const [pattern, setPattern] = useState({} as ISavedObject);
|
||||
|
||||
async function fetchPattern() {
|
||||
const indexPattern = await getAPMIndexPattern();
|
||||
if (indexPattern) {
|
||||
setPattern(indexPattern);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchPattern();
|
||||
}, []);
|
||||
|
||||
return pattern;
|
||||
}
|
12
x-pack/plugins/apm/public/hooks/useLocation.tsx
Normal file
12
x-pack/plugins/apm/public/hooks/useLocation.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { useContext } from 'react';
|
||||
import { LocationContext } from '../context/LocationContext';
|
||||
|
||||
export function useLocation() {
|
||||
return useContext(LocationContext);
|
||||
}
|
|
@ -19,6 +19,7 @@ import 'uiExports/autocompleteProviders';
|
|||
import { GlobalHelpExtension } from './components/app/GlobalHelpExtension';
|
||||
import { Main } from './components/app/Main';
|
||||
import { history } from './components/shared/Links/url_helpers';
|
||||
import { LocationProvider } from './context/LocationContext';
|
||||
// @ts-ignore
|
||||
import configureStore from './store/config/configureStore';
|
||||
import './style/global_overrides.css';
|
||||
|
@ -53,7 +54,9 @@ waitForRoot.then(() => {
|
|||
<I18nContext>
|
||||
<Provider store={store}>
|
||||
<Router history={history}>
|
||||
<Main />
|
||||
<LocationProvider history={history}>
|
||||
<Main />
|
||||
</LocationProvider>
|
||||
</Router>
|
||||
</Provider>
|
||||
</I18nContext>,
|
||||
|
|
|
@ -6,19 +6,15 @@
|
|||
|
||||
/* global jest */
|
||||
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import enzymeToJson from 'enzyme-to-json';
|
||||
import { History, Location } from 'history';
|
||||
import 'jest-styled-components';
|
||||
import moment from 'moment';
|
||||
import { Moment } from 'moment-timezone';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
// @ts-ignore
|
||||
import { createMockStore } from 'redux-test-utils';
|
||||
// @ts-ignore
|
||||
import configureStore from '../store/config/configureStore';
|
||||
import { IReduxState } from '../store/rootReducer';
|
||||
import { render, waitForElement } from 'react-testing-library';
|
||||
import { LocationProvider } from '../context/LocationContext';
|
||||
|
||||
export function toJson(wrapper: ReactWrapper) {
|
||||
return enzymeToJson(wrapper, {
|
||||
|
@ -44,22 +40,25 @@ export function mockMoment() {
|
|||
}
|
||||
|
||||
// Useful for getting the rendered href from any kind of link component
|
||||
export async function getRenderedHref(
|
||||
Component: React.FunctionComponent<{}>,
|
||||
globalState: Partial<IReduxState> = {}
|
||||
) {
|
||||
const store = configureStore(globalState);
|
||||
const mounted = mount(
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<Component />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
export async function getRenderedHref(Component: React.FC, location: Location) {
|
||||
const el = render(
|
||||
<LocationProvider
|
||||
history={
|
||||
({
|
||||
listen: jest.fn(),
|
||||
location
|
||||
} as unknown) as History
|
||||
}
|
||||
>
|
||||
<Component />
|
||||
</LocationProvider>
|
||||
);
|
||||
|
||||
await tick();
|
||||
await waitForElement(() => el.container.querySelector('a'));
|
||||
|
||||
return mounted.render().attr('href');
|
||||
const a = el.container.querySelector('a');
|
||||
return a ? a.getAttribute('href') : '';
|
||||
}
|
||||
|
||||
export function mockNow(date: string) {
|
||||
|
|
Loading…
Reference in a new issue