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:
Jason Rhodes 2019-04-08 15:47:16 -04:00 committed by GitHub
parent 7a8301e43b
commit cd532cd8f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 634 additions and 1062 deletions

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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: '' };

View file

@ -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",
},
]

View file

@ -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}

View file

@ -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>
)
}}
/>{' '}

View file

@ -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>
)
});

View file

@ -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}

View file

@ -42,7 +42,6 @@ export function ServiceDetailsView({ urlParams, location }: Props) {
<EuiFlexItem grow={false}>
<ServiceIntegrations
transactionTypes={serviceDetailsData.types}
location={location}
urlParams={urlParams}
/>
</EuiFlexItem>

View file

@ -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',
{

View file

@ -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>
`;

View file

@ -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>

View file

@ -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>

View file

@ -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"

View file

@ -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>
);
};

View file

@ -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%'
},

View file

@ -103,10 +103,7 @@ export function TransactionFlyout({
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TransactionActionMenu
transaction={transactionDoc}
location={location}
/>
<TransactionActionMenu transaction={transactionDoc} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>

View file

@ -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}

View file

@ -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>

View file

@ -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' }

View file

@ -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"`
);
});

View file

@ -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} />;
}

View file

@ -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} />;
}

View file

@ -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));
}
}

View file

@ -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(

View 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 { 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"`
);
});

View file

@ -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} />;
}

View file

@ -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"`);
});

View file

@ -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} />;
}

View file

@ -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))"
);
});
});

View file

@ -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 };

View file

@ -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))`

View file

@ -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" />
);
};

View file

@ -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))"`
);
});

View file

@ -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} />;
}

View file

@ -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'

View file

@ -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>
);
};

View file

@ -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>
`;

View file

@ -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>
`;

View file

@ -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"
/>
`;

View file

@ -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;
}

View file

@ -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');

View file

@ -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;

View file

@ -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>
);
}
}

View file

@ -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();
});
});

View file

@ -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>
`;

View file

@ -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>

View 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 };

View 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;
}

View 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);
}

View file

@ -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>,

View file

@ -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) {