From 0f1128281e924370eb609e8d7d4d53a5970922dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Felix=20St=C3=BCrmer?= <weltenwort@users.noreply.github.com>
Date: Fri, 14 Aug 2020 19:50:20 +0200
Subject: [PATCH] [Logs UI] Remove apollo deps from log link-to routes (#74502)

This replaces the use of the old graphql-based `useSource` hook with the new plain JSON `useLogSource` hook.

It also fixes two more problems:

- A rendering problem with the source configuration loading screen and a `setState` race condition in the `useLogSource` hook.
- A non-backwards-compatible change of the `/link-to/:sourceId/logs` route in #61162.
---
 .../log_sources/log_source_configuration.ts   |   3 +
 .../infra/common/inventory_models/index.ts    |   1 -
 .../infra/public/components/loading_page.tsx  |   8 +-
 .../plugins/infra/public/components/page.tsx  |   1 +
 .../public/components/source_loading_page.tsx |   1 +
 .../logs/log_source/log_source.mock.ts        |  78 +++++
 .../containers/logs/log_source/log_source.ts  |  20 +-
 .../pages/link_to/link_to_logs.test.tsx       | 326 ++++++++++++++++++
 .../public/pages/link_to/link_to_logs.tsx     |   1 +
 .../link_to/redirect_to_node_logs.test.tsx    | 119 -------
 .../pages/link_to/redirect_to_node_logs.tsx   |  42 ++-
 11 files changed, 453 insertions(+), 147 deletions(-)
 create mode 100644 x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts
 create mode 100644 x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx
 delete mode 100644 x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx

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 e8bf63843c623..3fc42b661ddab 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<typeof logSourceConfigurationOriginRT>;
 
 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 84bdb7887b1d1..9238989609ce5 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 c410f37e7bf6b..ae8e18a2f98ea 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) => (
-  <FlexPage>
+export const LoadingPage = ({
+  message,
+  'data-test-subj': dataTestSubj = 'loadingPage',
+}: LoadingPageProps) => (
+  <FlexPage data-test-subj={dataTestSubj}>
     <EuiPageBody>
       <EuiPageContent verticalPosition="center" horizontalPosition="center">
         <EuiFlexGroup alignItems="center">
diff --git a/x-pack/plugins/infra/public/components/page.tsx b/x-pack/plugins/infra/public/components/page.tsx
index 67e82310f0807..9636a5fc3a631 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 11e68e216b470..c24f7876d12f0 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 = () => (
   <LoadingPage
+    data-test-subj="sourceLoadingPage"
     message={
       <FormattedMessage
         id="xpack.infra.sourceLoadingPage.loadingDataSourcesMessage"
diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts
new file mode 100644
index 0000000000000..8e16ec1258736
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts
@@ -0,0 +1,78 @@
+/*
+ * 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 { LogSourceConfiguration, LogSourceStatus, useLogSource } from './log_source';
+
+type CreateUseLogSource = (sourceConfiguration?: { sourceId?: string }) => 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 b45ea0a042f49..51b32a4c4eacf 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 0000000000000..945b299674aaa
--- /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<typeof useLogSource>;
+
+const renderRoutes = (routes: React.ReactElement) => {
+  const history = createMemoryHistory();
+  const services = {
+    http: httpServiceMock.createStartContract(),
+  };
+  const renderResult = render(
+    <KibanaContextProvider services={services}>
+      <Router history={history}>{routes}</Router>
+    </KibanaContextProvider>
+  );
+
+  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(
+        <Switch>
+          <Route path="/link-to" component={LinkToLogsPage} />
+        </Switch>
+      );
+
+      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(
+        <Switch>
+          <Route path="/link-to" component={LinkToLogsPage} />
+        </Switch>
+      );
+
+      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(
+        <Switch>
+          <Route path="/link-to" component={LinkToLogsPage} />
+        </Switch>
+      );
+
+      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(
+        <Switch>
+          <Route path="/link-to" component={LinkToLogsPage} />
+        </Switch>
+      );
+
+      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(
+        <Switch>
+          <Route path="/link-to" component={LinkToLogsPage} />
+        </Switch>
+      );
+
+      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(
+        <Switch>
+          <Route path="/link-to" component={LinkToLogsPage} />
+        </Switch>
+      );
+
+      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(
+        <Switch>
+          <Route path="/link-to" component={LinkToLogsPage} />
+        </Switch>
+      );
+
+      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(
+        <Switch>
+          <Route path="/link-to" component={LinkToLogsPage} />
+        </Switch>
+      );
+
+      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(
+        <Switch>
+          <Route path="/link-to" component={LinkToLogsPage} />
+        </Switch>
+      );
+
+      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(
+        <Switch>
+          <Route path="/link-to" component={LinkToLogsPage} />
+        </Switch>
+      );
+
+      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(
+        <Switch>
+          <Route path="/link-to" component={LinkToLogsPage} />
+        </Switch>
+      );
+
+      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(
+        <Switch>
+          <Route path="/link-to" component={LinkToLogsPage} />
+        </Switch>
+      );
+
+      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(
+        <Switch>
+          <Route path="/link-to" component={LinkToLogsPage} />
+        </Switch>
+      );
+
+      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(
+        <Switch>
+          <Route path="/link-to" component={LinkToLogsPage} />
+        </Switch>
+      );
+
+      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 7a77b1525aea3..68adca83ac903 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<LinkToPageProps> = (props) => {
         path={`${props.match.url}/:sourceId?/:nodeType(${ITEM_TYPES})-logs/:nodeId`}
         component={RedirectToNodeLogs}
       />
+      <Route path={`${props.match.url}/:sourceId?/logs`} component={RedirectToLogs} />
       <Route path={`${props.match.url}/:sourceId?`} component={RedirectToLogs} />
       <Redirect to="/" />
     </Switch>
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 e62b29974674a..0000000000000
--- 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(
-      <RedirectToNodeLogs {...createRouteComponentProps('/host-logs/HOST_NAME')} />
-    );
-
-    expect(component).toMatchInlineSnapshot(`
-      <Redirect
-        to="/stream?sourceId=default&logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)"
-      />
-    `);
-  });
-
-  it('renders a redirect with the correct container filter', () => {
-    const component = shallow(
-      <RedirectToNodeLogs {...createRouteComponentProps('/container-logs/CONTAINER_ID')} />
-    );
-
-    expect(component).toMatchInlineSnapshot(`
-      <Redirect
-        to="/stream?sourceId=default&logFilter=(expression:'CONTAINER_FIELD:%20CONTAINER_ID',kind:kuery)"
-      />
-    `);
-  });
-
-  it('renders a redirect with the correct pod filter', () => {
-    const component = shallow(
-      <RedirectToNodeLogs {...createRouteComponentProps('/pod-logs/POD_ID')} />
-    );
-
-    expect(component).toMatchInlineSnapshot(`
-      <Redirect
-        to="/stream?sourceId=default&logFilter=(expression:'POD_FIELD:%20POD_ID',kind:kuery)"
-      />
-    `);
-  });
-
-  it('renders a redirect with the correct position', () => {
-    const component = shallow(
-      <RedirectToNodeLogs
-        {...createRouteComponentProps('/host-logs/HOST_NAME?time=1550671089404')}
-      />
-    );
-
-    expect(component).toMatchInlineSnapshot(`
-      <Redirect
-        to="/stream?logPosition=(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)&sourceId=default&logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)"
-      />
-    `);
-  });
-
-  it('renders a redirect with the correct user-defined filter', () => {
-    const component = shallow(
-      <RedirectToNodeLogs
-        {...createRouteComponentProps(
-          '/host-logs/HOST_NAME?time=1550671089404&filter=FILTER_FIELD:FILTER_VALUE'
-        )}
-      />
-    );
-
-    expect(component).toMatchInlineSnapshot(`
-      <Redirect
-        to="/stream?logPosition=(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)&sourceId=default&logFilter=(expression:'(HOST_FIELD:%20HOST_NAME)%20and%20(FILTER_FIELD:FILTER_VALUE)',kind:kuery)"
-      />
-    `);
-  });
-
-  it('renders a redirect with the correct custom source id', () => {
-    const component = shallow(
-      <RedirectToNodeLogs
-        {...createRouteComponentProps('/SOME-OTHER-SOURCE/host-logs/HOST_NAME')}
-      />
-    );
-
-    expect(component).toMatchInlineSnapshot(`
-      <Redirect
-        to="/stream?sourceId=SOME-OTHER-SOURCE&logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)"
-      />
-    `);
-  });
-});
-
-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 37203084124f5..d1d4b829fefc1 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 (
       <LoadingPage
+        data-test-subj={`nodeLoadingPage-${nodeType}`}
         message={i18n.translate('xpack.infra.redirectToNodeLogs.loadingNodeLogsMessage', {
           defaultMessage: 'Loading {nodeType} logs',
           values: {
@@ -55,13 +55,11 @@ export const RedirectToNodeLogs = ({
         })}
       />
     );
-  }
-
-  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;