diff --git a/x-pack/plugins/infra/common/http_api/log_sources/log_source_configuration.ts b/x-pack/plugins/infra/common/http_api/log_sources/log_source_configuration.ts index e8bf63843c62..3fc42b661dda 100644 --- a/x-pack/plugins/infra/common/http_api/log_sources/log_source_configuration.ts +++ b/x-pack/plugins/infra/common/http_api/log_sources/log_source_configuration.ts @@ -20,6 +20,9 @@ export const logSourceConfigurationOriginRT = rt.keyof({ export type LogSourceConfigurationOrigin = rt.TypeOf; const logSourceFieldsConfigurationRT = rt.strict({ + container: rt.string, + host: rt.string, + pod: rt.string, timestamp: rt.string, tiebreaker: rt.string, }); diff --git a/x-pack/plugins/infra/common/inventory_models/index.ts b/x-pack/plugins/infra/common/inventory_models/index.ts index 84bdb7887b1d..9238989609ce 100644 --- a/x-pack/plugins/infra/common/inventory_models/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/index.ts @@ -30,7 +30,6 @@ export const findInventoryModel = (type: InventoryItemType) => { }; interface InventoryFields { - message: string[]; host: string; pod: string; container: string; diff --git a/x-pack/plugins/infra/public/components/loading_page.tsx b/x-pack/plugins/infra/public/components/loading_page.tsx index c410f37e7bf6..ae8e18a2f98e 100644 --- a/x-pack/plugins/infra/public/components/loading_page.tsx +++ b/x-pack/plugins/infra/public/components/loading_page.tsx @@ -17,10 +17,14 @@ import { FlexPage } from './page'; interface LoadingPageProps { message?: ReactNode; + 'data-test-subj'?: string; } -export const LoadingPage = ({ message }: LoadingPageProps) => ( - +export const LoadingPage = ({ + message, + 'data-test-subj': dataTestSubj = 'loadingPage', +}: LoadingPageProps) => ( + diff --git a/x-pack/plugins/infra/public/components/page.tsx b/x-pack/plugins/infra/public/components/page.tsx index 67e82310f080..9636a5fc3a63 100644 --- a/x-pack/plugins/infra/public/components/page.tsx +++ b/x-pack/plugins/infra/public/components/page.tsx @@ -23,5 +23,6 @@ export const PageContent = euiStyled.div` `; export const FlexPage = euiStyled(EuiPage)` + align-self: stretch; flex: 1 0 0%; `; diff --git a/x-pack/plugins/infra/public/components/source_loading_page.tsx b/x-pack/plugins/infra/public/components/source_loading_page.tsx index 11e68e216b47..c24f7876d12f 100644 --- a/x-pack/plugins/infra/public/components/source_loading_page.tsx +++ b/x-pack/plugins/infra/public/components/source_loading_page.tsx @@ -11,6 +11,7 @@ import { LoadingPage } from './loading_page'; export const SourceLoadingPage: React.FunctionComponent = () => ( typeof useLogSource; + +const defaultSourceId = 'default'; + +export const createUninitializedUseLogSourceMock: CreateUseLogSource = ({ + sourceId = defaultSourceId, +} = {}) => () => ({ + derivedIndexPattern: { + fields: [], + title: 'unknown', + }, + hasFailedLoadingSource: false, + hasFailedLoadingSourceStatus: false, + initialize: jest.fn(), + isLoading: false, + isLoadingSourceConfiguration: false, + isLoadingSourceStatus: false, + isUninitialized: true, + loadSource: jest.fn(), + loadSourceConfiguration: jest.fn(), + loadSourceFailureMessage: undefined, + loadSourceStatus: jest.fn(), + sourceConfiguration: undefined, + sourceId, + sourceStatus: undefined, + updateSourceConfiguration: jest.fn(), +}); + +export const createLoadingUseLogSourceMock: CreateUseLogSource = ({ + sourceId = defaultSourceId, +} = {}) => (args) => ({ + ...createUninitializedUseLogSourceMock({ sourceId })(args), + isLoading: true, + isLoadingSourceConfiguration: true, + isLoadingSourceStatus: true, +}); + +export const createLoadedUseLogSourceMock: CreateUseLogSource = ({ + sourceId = defaultSourceId, +} = {}) => (args) => ({ + ...createUninitializedUseLogSourceMock({ sourceId })(args), + sourceConfiguration: createBasicSourceConfiguration(sourceId), + sourceStatus: { + logIndexFields: [], + logIndexStatus: 'available', + }, +}); + +export const createBasicSourceConfiguration = (sourceId: string): LogSourceConfiguration => ({ + id: sourceId, + origin: 'stored', + configuration: { + description: `description for ${sourceId}`, + logAlias: 'LOG_INDICES', + logColumns: [], + fields: { + container: 'CONTAINER_FIELD', + host: 'HOST_FIELD', + pod: 'POD_FIELD', + tiebreaker: 'TIEBREAKER_FIELD', + timestamp: 'TIMESTAMP_FIELD', + }, + name: sourceId, + }, +}); + +export const createAvailableSourceStatus = (logIndexFields = []): LogSourceStatus => ({ + logIndexFields, + logIndexStatus: 'available', +}); diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts index b45ea0a042f4..51b32a4c4eac 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts @@ -5,13 +5,14 @@ */ import createContainer from 'constate'; -import { useState, useMemo, useCallback } from 'react'; +import { useCallback, useMemo, useState } from 'react'; +import { useMountedState } from 'react-use'; import { HttpSetup } from 'src/core/public'; import { LogSourceConfiguration, - LogSourceStatus, - LogSourceConfigurationPropertiesPatch, LogSourceConfigurationProperties, + LogSourceConfigurationPropertiesPatch, + LogSourceStatus, } from '../../../../common/http_api/log_sources'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { callFetchLogSourceConfigurationAPI } from './api/fetch_log_source_configuration'; @@ -32,6 +33,7 @@ export const useLogSource = ({ sourceId: string; fetch: HttpSetup['fetch']; }) => { + const getIsMounted = useMountedState(); const [sourceConfiguration, setSourceConfiguration] = useState< LogSourceConfiguration | undefined >(undefined); @@ -45,6 +47,10 @@ export const useLogSource = ({ return await callFetchLogSourceConfigurationAPI(sourceId, fetch); }, onResolve: ({ data }) => { + if (!getIsMounted()) { + return; + } + setSourceConfiguration(data); }, }, @@ -58,6 +64,10 @@ export const useLogSource = ({ return await callPatchLogSourceConfigurationAPI(sourceId, patchedProperties, fetch); }, onResolve: ({ data }) => { + if (!getIsMounted()) { + return; + } + setSourceConfiguration(data); loadSourceStatus(); }, @@ -72,6 +82,10 @@ export const useLogSource = ({ return await callFetchLogSourceStatusAPI(sourceId, fetch); }, onResolve: ({ data }) => { + if (!getIsMounted()) { + return; + } + setSourceStatus(data); }, }, diff --git a/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx new file mode 100644 index 000000000000..945b299674aa --- /dev/null +++ b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx @@ -0,0 +1,326 @@ +/* + * 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. + */ +/* + * 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 { render } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import React from 'react'; +import { Route, Router, Switch } from 'react-router-dom'; +import { httpServiceMock } from 'src/core/public/mocks'; +// import { HttpSetup } from 'src/core/public'; +import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; +import { useLogSource } from '../../containers/logs/log_source'; +import { + createLoadedUseLogSourceMock, + createLoadingUseLogSourceMock, +} from '../../containers/logs/log_source/log_source.mock'; +import { LinkToLogsPage } from './link_to_logs'; + +jest.mock('../../containers/logs/log_source'); +const useLogSourceMock = useLogSource as jest.MockedFunction; + +const renderRoutes = (routes: React.ReactElement) => { + const history = createMemoryHistory(); + const services = { + http: httpServiceMock.createStartContract(), + }; + const renderResult = render( + + {routes} + + ); + + return { + ...renderResult, + history, + services, + }; +}; + +describe('LinkToLogsPage component', () => { + beforeEach(() => { + useLogSourceMock.mockImplementation(createLoadedUseLogSourceMock()); + }); + + afterEach(() => { + useLogSourceMock.mockRestore(); + }); + + describe('default route', () => { + it('redirects to the stream at a given time filtered for a user-defined criterion', () => { + const { history } = renderRoutes( + + + + ); + + history.push('/link-to?time=1550671089404&filter=FILTER_FIELD:FILTER_VALUE'); + + expect(history.location.pathname).toEqual('/stream'); + + const searchParams = new URLSearchParams(history.location.search); + expect(searchParams.get('sourceId')).toEqual('default'); + expect(searchParams.get('logFilter')).toMatchInlineSnapshot( + `"(expression:'FILTER_FIELD:FILTER_VALUE',kind:kuery)"` + ); + expect(searchParams.get('logPosition')).toMatchInlineSnapshot( + `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` + ); + }); + + it('redirects to the stream using a specific source id', () => { + const { history } = renderRoutes( + + + + ); + + history.push('/link-to/OTHER_SOURCE'); + + expect(history.location.pathname).toEqual('/stream'); + + const searchParams = new URLSearchParams(history.location.search); + expect(searchParams.get('sourceId')).toEqual('OTHER_SOURCE'); + expect(searchParams.get('logFilter')).toMatchInlineSnapshot(`"(expression:'',kind:kuery)"`); + expect(searchParams.get('logPosition')).toEqual(null); + }); + }); + + describe('logs route', () => { + it('redirects to the stream at a given time filtered for a user-defined criterion', () => { + const { history } = renderRoutes( + + + + ); + + history.push('/link-to/logs?time=1550671089404&filter=FILTER_FIELD:FILTER_VALUE'); + + expect(history.location.pathname).toEqual('/stream'); + + const searchParams = new URLSearchParams(history.location.search); + expect(searchParams.get('sourceId')).toEqual('default'); + expect(searchParams.get('logFilter')).toMatchInlineSnapshot( + `"(expression:'FILTER_FIELD:FILTER_VALUE',kind:kuery)"` + ); + expect(searchParams.get('logPosition')).toMatchInlineSnapshot( + `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` + ); + }); + + it('redirects to the stream using a specific source id', () => { + const { history } = renderRoutes( + + + + ); + + history.push('/link-to/OTHER_SOURCE/logs'); + + expect(history.location.pathname).toEqual('/stream'); + + const searchParams = new URLSearchParams(history.location.search); + expect(searchParams.get('sourceId')).toEqual('OTHER_SOURCE'); + expect(searchParams.get('logFilter')).toMatchInlineSnapshot(`"(expression:'',kind:kuery)"`); + expect(searchParams.get('logPosition')).toEqual(null); + }); + }); + + describe('host-logs route', () => { + it('redirects to the stream filtered for a host', () => { + const { history } = renderRoutes( + + + + ); + + history.push('/link-to/host-logs/HOST_NAME'); + + expect(history.location.pathname).toEqual('/stream'); + + const searchParams = new URLSearchParams(history.location.search); + expect(searchParams.get('sourceId')).toEqual('default'); + expect(searchParams.get('logFilter')).toMatchInlineSnapshot( + `"(expression:'HOST_FIELD: HOST_NAME',kind:kuery)"` + ); + expect(searchParams.get('logPosition')).toEqual(null); + }); + + it('redirects to the stream at a given time filtered for a host and a user-defined criterion', () => { + const { history } = renderRoutes( + + + + ); + + history.push( + '/link-to/host-logs/HOST_NAME?time=1550671089404&filter=FILTER_FIELD:FILTER_VALUE' + ); + + expect(history.location.pathname).toEqual('/stream'); + + const searchParams = new URLSearchParams(history.location.search); + expect(searchParams.get('sourceId')).toEqual('default'); + expect(searchParams.get('logFilter')).toMatchInlineSnapshot( + `"(expression:'(HOST_FIELD: HOST_NAME) and (FILTER_FIELD:FILTER_VALUE)',kind:kuery)"` + ); + expect(searchParams.get('logPosition')).toMatchInlineSnapshot( + `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` + ); + }); + + it('redirects to the stream filtered for a host using a specific source id', () => { + const { history } = renderRoutes( + + + + ); + + history.push('/link-to/OTHER_SOURCE/host-logs/HOST_NAME'); + + expect(history.location.pathname).toEqual('/stream'); + + const searchParams = new URLSearchParams(history.location.search); + expect(searchParams.get('sourceId')).toEqual('OTHER_SOURCE'); + expect(searchParams.get('logFilter')).toMatchInlineSnapshot( + `"(expression:'HOST_FIELD: HOST_NAME',kind:kuery)"` + ); + expect(searchParams.get('logPosition')).toEqual(null); + }); + + it('renders a loading page while loading the source configuration', () => { + useLogSourceMock.mockImplementation(createLoadingUseLogSourceMock()); + + const { history, queryByTestId } = renderRoutes( + + + + ); + + history.push('/link-to/host-logs/HOST_NAME'); + + expect(queryByTestId('nodeLoadingPage-host')).not.toBeEmpty(); + }); + }); + + describe('container-logs route', () => { + it('redirects to the stream filtered for a container', () => { + const { history } = renderRoutes( + + + + ); + + history.push('/link-to/container-logs/CONTAINER_ID'); + + expect(history.location.pathname).toEqual('/stream'); + + const searchParams = new URLSearchParams(history.location.search); + expect(searchParams.get('sourceId')).toEqual('default'); + expect(searchParams.get('logFilter')).toMatchInlineSnapshot( + `"(expression:'CONTAINER_FIELD: CONTAINER_ID',kind:kuery)"` + ); + expect(searchParams.get('logPosition')).toEqual(null); + }); + + it('redirects to the stream at a given time filtered for a container and a user-defined criterion', () => { + const { history } = renderRoutes( + + + + ); + + history.push( + '/link-to/container-logs/CONTAINER_ID?time=1550671089404&filter=FILTER_FIELD:FILTER_VALUE' + ); + + expect(history.location.pathname).toEqual('/stream'); + + const searchParams = new URLSearchParams(history.location.search); + expect(searchParams.get('sourceId')).toEqual('default'); + expect(searchParams.get('logFilter')).toMatchInlineSnapshot( + `"(expression:'(CONTAINER_FIELD: CONTAINER_ID) and (FILTER_FIELD:FILTER_VALUE)',kind:kuery)"` + ); + expect(searchParams.get('logPosition')).toMatchInlineSnapshot( + `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` + ); + }); + + it('renders a loading page while loading the source configuration', () => { + useLogSourceMock.mockImplementation(createLoadingUseLogSourceMock()); + + const { history, queryByTestId } = renderRoutes( + + + + ); + + history.push('/link-to/container-logs/CONTAINER_ID'); + + expect(queryByTestId('nodeLoadingPage-container')).not.toBeEmpty(); + }); + }); + + describe('pod-logs route', () => { + it('redirects to the stream filtered for a pod', () => { + const { history } = renderRoutes( + + + + ); + + history.push('/link-to/pod-logs/POD_UID'); + + expect(history.location.pathname).toEqual('/stream'); + + const searchParams = new URLSearchParams(history.location.search); + expect(searchParams.get('sourceId')).toEqual('default'); + expect(searchParams.get('logFilter')).toMatchInlineSnapshot( + `"(expression:'POD_FIELD: POD_UID',kind:kuery)"` + ); + expect(searchParams.get('logPosition')).toEqual(null); + }); + + it('redirects to the stream at a given time filtered for a pod and a user-defined criterion', () => { + const { history } = renderRoutes( + + + + ); + + history.push('/link-to/pod-logs/POD_UID?time=1550671089404&filter=FILTER_FIELD:FILTER_VALUE'); + + expect(history.location.pathname).toEqual('/stream'); + + const searchParams = new URLSearchParams(history.location.search); + expect(searchParams.get('sourceId')).toEqual('default'); + expect(searchParams.get('logFilter')).toMatchInlineSnapshot( + `"(expression:'(POD_FIELD: POD_UID) and (FILTER_FIELD:FILTER_VALUE)',kind:kuery)"` + ); + expect(searchParams.get('logPosition')).toMatchInlineSnapshot( + `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` + ); + }); + + it('renders a loading page while loading the source configuration', () => { + useLogSourceMock.mockImplementation(createLoadingUseLogSourceMock()); + + const { history, queryByTestId } = renderRoutes( + + + + ); + + history.push('/link-to/pod-logs/POD_UID'); + + expect(queryByTestId('nodeLoadingPage-pod')).not.toBeEmpty(); + }); + }); +}); diff --git a/x-pack/plugins/infra/public/pages/link_to/link_to_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.tsx index 7a77b1525aea..68adca83ac90 100644 --- a/x-pack/plugins/infra/public/pages/link_to/link_to_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.tsx @@ -27,6 +27,7 @@ export const LinkToLogsPage: React.FC = (props) => { path={`${props.match.url}/:sourceId?/:nodeType(${ITEM_TYPES})-logs/:nodeId`} component={RedirectToNodeLogs} /> + diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx deleted file mode 100644 index e62b29974674..000000000000 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx +++ /dev/null @@ -1,119 +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 { createLocation } from 'history'; -import React from 'react'; -import { matchPath } from 'react-router-dom'; -import { shallow } from 'enzyme'; - -import { RedirectToNodeLogs } from './redirect_to_node_logs'; - -jest.mock('../../containers/source/source', () => ({ - useSource: ({ sourceId }: { sourceId: string }) => ({ - sourceId, - source: { - configuration: { - fields: { - container: 'CONTAINER_FIELD', - host: 'HOST_FIELD', - pod: 'POD_FIELD', - }, - }, - }, - isLoading: sourceId === 'perpetuallyLoading', - }), -})); - -describe('RedirectToNodeLogs component', () => { - it('renders a redirect with the correct host filter', () => { - const component = shallow( - - ); - - expect(component).toMatchInlineSnapshot(` - - `); - }); - - it('renders a redirect with the correct container filter', () => { - const component = shallow( - - ); - - expect(component).toMatchInlineSnapshot(` - - `); - }); - - it('renders a redirect with the correct pod filter', () => { - const component = shallow( - - ); - - expect(component).toMatchInlineSnapshot(` - - `); - }); - - it('renders a redirect with the correct position', () => { - const component = shallow( - - ); - - expect(component).toMatchInlineSnapshot(` - - `); - }); - - it('renders a redirect with the correct user-defined filter', () => { - const component = shallow( - - ); - - expect(component).toMatchInlineSnapshot(` - - `); - }); - - it('renders a redirect with the correct custom source id', () => { - const component = shallow( - - ); - - expect(component).toMatchInlineSnapshot(` - - `); - }); -}); - -const createRouteComponentProps = (path: string) => { - const location = createLocation(path); - return { - match: matchPath(location.pathname, { path: '/:sourceId?/:nodeType-logs/:nodeId' }) as any, - history: null as any, - location, - }; -}; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx index 37203084124f..d1d4b829fefc 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx @@ -5,21 +5,20 @@ */ import { i18n } from '@kbn/i18n'; - -import { flowRight } from 'lodash'; +import flowRight from 'lodash/flowRight'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; - +import { useMount } from 'react-use'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { findInventoryFields } from '../../../common/inventory_models'; +import { InventoryItemType } from '../../../common/inventory_models/types'; import { LoadingPage } from '../../components/loading_page'; import { replaceLogFilterInQueryString } from '../../containers/logs/log_filter'; import { replaceLogPositionInQueryString } from '../../containers/logs/log_position'; +import { useLogSource } from '../../containers/logs/log_source'; import { replaceSourceIdInQueryString } from '../../containers/source_id'; -import { SourceConfigurationFields } from '../../graphql/types'; -import { getFilterFromLocation, getTimeFromLocation } from './query_params'; -import { useSource } from '../../containers/source/source'; -import { findInventoryFields } from '../../../common/inventory_models'; -import { InventoryItemType } from '../../../common/inventory_models/types'; import { LinkDescriptor } from '../../hooks/use_link_props'; +import { getFilterFromLocation, getTimeFromLocation } from './query_params'; type RedirectToNodeLogsType = RouteComponentProps<{ nodeId: string; @@ -27,26 +26,27 @@ type RedirectToNodeLogsType = RouteComponentProps<{ sourceId?: string; }>; -const getFieldByNodeType = ( - nodeType: InventoryItemType, - fields: SourceConfigurationFields.Fields -) => { - const inventoryFields = findInventoryFields(nodeType, fields); - return inventoryFields.id; -}; - export const RedirectToNodeLogs = ({ match: { params: { nodeId, nodeType, sourceId = 'default' }, }, location, }: RedirectToNodeLogsType) => { - const { source, isLoading } = useSource({ sourceId }); - const configuration = source && source.configuration; + const { services } = useKibana(); + const { isLoading, loadSourceConfiguration, sourceConfiguration } = useLogSource({ + fetch: services.http.fetch, + sourceId, + }); + const fields = sourceConfiguration?.configuration.fields; + + useMount(() => { + loadSourceConfiguration(); + }); if (isLoading) { return ( ); - } - - if (!configuration) { + } else if (fields == null) { return null; } - const nodeFilter = `${getFieldByNodeType(nodeType, configuration.fields)}: ${nodeId}`; + const nodeFilter = `${findInventoryFields(nodeType, fields).id}: ${nodeId}`; const userFilter = getFilterFromLocation(location); const filter = userFilter ? `(${nodeFilter}) and (${userFilter})` : nodeFilter;