mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* [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 * hits.total is a number in 7.x * Fixing typing error and code for response.hits.total * removing rest_total_hits_as_int flag
This commit is contained in:
parent
6092760ba8
commit
405c914c7c
11 changed files with 283 additions and 5 deletions
|
@ -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>
|
||||
|
|
|
@ -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 }
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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 };
|
||||
};
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -116,7 +116,7 @@ export interface InfraDatabaseSearchResponse<Hit = {}, Aggregations = undefined>
|
|||
};
|
||||
aggregations?: Aggregations;
|
||||
hits: {
|
||||
total: number;
|
||||
total: { value: number };
|
||||
hits: Hit[];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ export const checkValidNode = async (
|
|||
): Promise<boolean> => {
|
||||
const params = {
|
||||
index: indexPattern,
|
||||
rest_total_hits_as_int: true,
|
||||
terminateAfter: 1,
|
||||
body: {
|
||||
size: 0,
|
||||
|
@ -25,5 +24,6 @@ export const checkValidNode = async (
|
|||
},
|
||||
},
|
||||
};
|
||||
return (await search(params)).hits.total > 0;
|
||||
const response = await search(params);
|
||||
return response.hits.total.value > 0;
|
||||
};
|
||||
|
|
|
@ -205,7 +205,7 @@ const getAllCompositeAggregationData = async <BucketType>(
|
|||
);
|
||||
|
||||
// Nothing available, return the previous buckets.
|
||||
if (response.hits.total === 0) {
|
||||
if (response.hits.total.value === 0) {
|
||||
return previousBuckets;
|
||||
}
|
||||
|
||||
|
|
67
x-pack/plugins/infra/server/routes/ip_to_hostname.ts
Normal file
67
x-pack/plugins/infra/server/routes/ip_to_hostname.ts
Normal 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);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
49
x-pack/test/api_integration/apis/infra/ip_to_hostname.ts
Normal file
49
x-pack/test/api_integration/apis/infra/ip_to_hostname.ts
Normal 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;
|
Loading…
Add table
Add a link
Reference in a new issue