[Infra UI] Link-to page for resolving IP to Host Details (#36149)

* Adding link-to page for resolving IP to Host Details

* Adding intl support

* Adding source provider for link to via IP

* removing Source.Provider in favor of useSource hook

* rollback yarn.lock changes

* rollback package.json changes
This commit is contained in:
Chris Cowan 2019-05-20 14:20:49 -04:00 committed by GitHub
parent d7c4442fda
commit 2d13071554
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 279 additions and 1 deletions

View file

@ -10,6 +10,7 @@ import { match as RouteMatch, Redirect, Route, Switch } from 'react-router-dom';
import { RedirectToLogs } from './redirect_to_logs';
import { RedirectToNodeDetail } from './redirect_to_node_detail';
import { RedirectToNodeLogs } from './redirect_to_node_logs';
import { RedirectToHostDetailViaIP } from './redirect_to_host_detail_via_ip';
interface LinkToPageProps {
match: RouteMatch<{}>;
@ -29,6 +30,10 @@ export class LinkToPage extends React.Component<LinkToPageProps> {
path={`${match.url}/:nodeType(host|container|pod)-detail/:nodeId`}
component={RedirectToNodeDetail}
/>
<Route
path={`${match.url}/host-detail-via-ip/:hostIp`}
component={RedirectToHostDetailViaIP}
/>
<Route path={`${match.url}/:sourceId?/logs`} component={RedirectToLogs} />
<Redirect to="/infrastructure" />
</Switch>

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { replaceMetricTimeInQueryString } from '../../containers/metrics/with_metrics_time';
import { useHostIpToName } from './use_host_ip_to_name';
import { getFromFromLocation, getToFromLocation } from './query_params';
import { LoadingPage } from '../../components/loading_page';
import { Error } from '../error';
import { useSource } from '../../containers/source/source';
type RedirectToHostDetailType = RouteComponentProps<{
hostIp: string;
}>;
interface RedirectToHostDetailProps extends RedirectToHostDetailType {
intl: InjectedIntl;
}
export const RedirectToHostDetailViaIP = injectI18n(
({
match: {
params: { hostIp },
},
location,
intl,
}: RedirectToHostDetailProps) => {
const { source } = useSource({ sourceId: 'default' });
const { error, name } = useHostIpToName(
hostIp,
(source && source.configuration && source.configuration.metricAlias) || null
);
if (error) {
return (
<Error
message={intl.formatMessage(
{
id: 'xpack.infra.linkTo.hostWithIp.error',
defaultMessage: 'Host not found with IP address "{hostIp}".',
},
{ hostIp }
)}
/>
);
}
const searchString = replaceMetricTimeInQueryString(
getFromFromLocation(location),
getToFromLocation(location)
)('');
if (name) {
return <Redirect to={`/metrics/host/${name}?${searchString}`} />;
}
return (
<LoadingPage
message={intl.formatMessage(
{
id: 'xpack.infra.linkTo.hostWithIp.loading',
defaultMessage: 'Loading host with IP address "{hostIp}".',
},
{ hostIp }
)}
/>
);
}
);

View file

@ -0,0 +1,41 @@
/*
* 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 { useHostIpToName } from './use_host_ip_to_name';
import { fetch } from '../../utils/fetch';
import { renderHook } from 'react-hooks-testing-library';
const renderUseHostIpToNameHook = () =>
renderHook(props => useHostIpToName(props.ipAddress, props.indexPattern), {
initialProps: { ipAddress: '127.0.0.1', indexPattern: 'metricbest-*' },
});
jest.mock('../../utils/fetch');
const mockedFetch = fetch as jest.Mocked<typeof fetch>;
describe('useHostIpToName Hook', () => {
it('should basically work', async () => {
mockedFetch.post.mockResolvedValue({ data: { host: 'example-01' } } as any);
const { result, waitForNextUpdate } = renderUseHostIpToNameHook();
expect(result.current.name).toBe(null);
expect(result.current.loading).toBe(true);
await waitForNextUpdate();
expect(result.current.name).toBe('example-01');
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(null);
});
it('should handle errors', async () => {
const error = new Error('Host not found');
mockedFetch.post.mockRejectedValue(error);
const { result, waitForNextUpdate } = renderUseHostIpToNameHook();
expect(result.current.name).toBe(null);
expect(result.current.loading).toBe(true);
await waitForNextUpdate();
expect(result.current.name).toBe(null);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(error);
});
});

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useState, useEffect } from 'react';
import { IpToHostResponse } from '../../../server/routes/ip_to_hostname';
import { fetch } from '../../utils/fetch';
export const useHostIpToName = (ipAddress: string | null, indexPattern: string | null) => {
const [error, setError] = useState<Error | null>(null);
const [loading, setLoadingState] = useState<boolean>(true);
const [data, setData] = useState<IpToHostResponse | null>(null);
useEffect(
() => {
(async () => {
setLoadingState(true);
try {
if (ipAddress && indexPattern) {
const response = await fetch.post<IpToHostResponse>('../api/infra/ip_to_host', {
ip: ipAddress,
index_pattern: indexPattern,
});
setLoadingState(false);
setData(response.data);
}
} catch (err) {
setLoadingState(false);
setError(err);
}
})();
},
[ipAddress, indexPattern]
);
return { name: (data && data.host) || null, loading, error };
};

View file

@ -5,6 +5,7 @@
*/
import { IResolvers, makeExecutableSchema } from 'graphql-tools';
import { initIpToHostName } from './routes/ip_to_hostname';
import { schemas } from './graphql';
import { createLogEntriesResolvers } from './graphql/log_entries';
import { createMetadataResolvers } from './graphql/metadata';
@ -32,5 +33,6 @@ export const initInfraServer = (libs: InfraBackendLibs) => {
libs.framework.registerGraphQLEndpoint('/api/infra/graphql', schema);
initLegacyLoggingRoutes(libs.framework);
initIpToHostName(libs);
initMetricExplorerRoute(libs);
};

View file

@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Joi from 'joi';
import { boomify, notFound } from 'boom';
import { first } from 'lodash';
import { InfraBackendLibs } from '../lib/infra_types';
import { InfraWrappableRequest } from '../lib/adapters/framework';
interface IpToHostRequest {
ip: string;
index_pattern: string;
}
type IpToHostWrappedRequest = InfraWrappableRequest<IpToHostRequest>;
export interface IpToHostResponse {
host: string;
}
interface HostDoc {
_source: {
host: {
name: string;
};
};
}
const ipToHostSchema = Joi.object({
ip: Joi.string().required(),
index_pattern: Joi.string().required(),
});
export const initIpToHostName = ({ framework }: InfraBackendLibs) => {
const { callWithRequest } = framework;
framework.registerRoute<IpToHostWrappedRequest, Promise<IpToHostResponse>>({
method: 'POST',
path: '/api/infra/ip_to_host',
options: {
validate: { payload: ipToHostSchema },
},
handler: async req => {
try {
const params = {
index: req.payload.index_pattern,
body: {
size: 1,
query: {
match: { 'host.ip': req.payload.ip },
},
_source: ['host.name'],
},
};
const response = await callWithRequest<HostDoc>(req, 'search', params);
if (response.hits.total.value === 0) {
throw notFound('Host with matching IP address not found.');
}
const hostDoc = first(response.hits.hits);
return { host: hostDoc._source.host.name };
} catch (e) {
throw boomify(e);
}
},
});
};

View file

@ -5,7 +5,7 @@
*/
export default function ({ loadTestFile }) {
describe('InfraOps GraphQL Endpoints', () => {
describe('InfraOps Endpoints', () => {
loadTestFile(require.resolve('./metadata'));
loadTestFile(require.resolve('./log_entries'));
loadTestFile(require.resolve('./log_summary'));
@ -16,5 +16,6 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./log_item'));
loadTestFile(require.resolve('./metrics_explorer'));
loadTestFile(require.resolve('./feature_controls'));
loadTestFile(require.resolve('./ip_to_hostname'));
});
}

View file

@ -0,0 +1,49 @@
/*
* 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 expect from '@kbn/expect';
import { KbnTestProvider } from './types';
import { IpToHostResponse } from '../../../../plugins/infra/server/routes/ip_to_hostname';
const ipToHostNameTest: KbnTestProvider = ({ getService }) => {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('Ip to Host API', () => {
before(() => esArchiver.load('infra/metrics_and_logs'));
after(() => esArchiver.unload('infra/metrics_and_logs'));
it('should basically work', async () => {
const postBody = {
index_pattern: 'metricbeat-*',
ip: '10.128.0.7',
};
const response = await supertest
.post('/api/infra/ip_to_host')
.set('kbn-xsrf', 'xxx')
.send(postBody)
.expect(200);
const body: IpToHostResponse = response.body;
expect(body).to.have.property('host', 'demo-stack-mysql-01');
});
it('should return 404 for invalid ip', async () => {
const postBody = {
index_pattern: 'metricbeat-*',
ip: '192.168.1.1',
};
return supertest
.post('/api/infra/ip_to_host')
.set('kbn-xsrf', 'xxx')
.send(postBody)
.expect(404);
});
});
};
// eslint-disable-next-line import/no-default-export
export default ipToHostNameTest;