From c54344adb615d8f39413a83d6ef1841c058a07c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Fri, 10 May 2019 21:23:19 +0200 Subject: [PATCH] [Logs UI] Allow for plugins to inject internal source configurations (#36066) This exposes an API on the Kibana server object (old platform style) to define internal source configurations, which can not be edited by the user and take precedence above any stored configurations. --- .../graphql/shared/fragments.gql_query.ts | 1 + x-pack/plugins/infra/common/graphql/types.ts | 4 + .../plugins/infra/public/apps/start_app.tsx | 5 +- .../source_configuration_flyout.tsx | 15 ++- .../public/containers/logs/log_flyout.tsx | 5 +- .../logs/log_summary/with_summary.ts | 4 +- .../containers/logs/with_stream_items.ts | 25 ++++ .../infra/public/containers/source/source.tsx | 2 +- .../public/containers/source_id/index.ts | 7 ++ .../public/containers/source_id/source_id.ts | 28 +++++ .../infra/public/graphql/introspection.json | 12 ++ x-pack/plugins/infra/public/graphql/types.ts | 4 + .../infra/public/pages/link_to/link_to.tsx | 27 ++-- .../pages/link_to/redirect_to_logs.test.tsx | 36 +++--- .../public/pages/link_to/redirect_to_logs.tsx | 44 +++---- .../link_to/redirect_to_node_logs.test.tsx | 69 +++++++---- .../pages/link_to/redirect_to_node_logs.tsx | 70 ++++++----- .../public/pages/logs/page_logs_content.tsx | 3 +- .../public/pages/logs/page_providers.tsx | 23 ++-- .../store/remote/log_entries/actions.ts | 2 + .../public/store/remote/log_entries/epic.ts | 45 ++++--- .../infra/public/utils/history_context.ts | 14 +++ .../infra/public/utils/use_url_state.ts | 116 ++++++++++++++++++ .../server/graphql/sources/schema.gql.ts | 2 + x-pack/plugins/infra/server/graphql/types.ts | 9 ++ x-pack/plugins/infra/server/kibana.index.ts | 5 + .../infra/server/lib/sources/errors.ts | 12 ++ .../infra/server/lib/sources/sources.ts | 52 ++++++-- 28 files changed, 475 insertions(+), 166 deletions(-) create mode 100644 x-pack/plugins/infra/public/containers/source_id/index.ts create mode 100644 x-pack/plugins/infra/public/containers/source_id/source_id.ts create mode 100644 x-pack/plugins/infra/public/utils/history_context.ts create mode 100644 x-pack/plugins/infra/public/utils/use_url_state.ts create mode 100644 x-pack/plugins/infra/server/lib/sources/errors.ts diff --git a/x-pack/plugins/infra/common/graphql/shared/fragments.gql_query.ts b/x-pack/plugins/infra/common/graphql/shared/fragments.gql_query.ts index b62048b126b23..1fa70feb555d9 100644 --- a/x-pack/plugins/infra/common/graphql/shared/fragments.gql_query.ts +++ b/x-pack/plugins/infra/common/graphql/shared/fragments.gql_query.ts @@ -18,6 +18,7 @@ export const sharedFragments = { id version updatedAt + origin } `, InfraLogEntryFields: gql` diff --git a/x-pack/plugins/infra/common/graphql/types.ts b/x-pack/plugins/infra/common/graphql/types.ts index 534592e2ad968..21b8931dded70 100644 --- a/x-pack/plugins/infra/common/graphql/types.ts +++ b/x-pack/plugins/infra/common/graphql/types.ts @@ -22,6 +22,8 @@ export interface InfraSource { version?: string | null; /** The timestamp the source configuration was last persisted at */ updatedAt?: number | null; + /** The origin of the source (one of 'fallback', 'internal', 'stored') */ + origin: string; /** The raw configuration of the source */ configuration: InfraSourceConfiguration; /** The status of the source */ @@ -1047,6 +1049,8 @@ export namespace InfraSourceFields { version?: string | null; updatedAt?: number | null; + + origin: string; }; } diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx index 96cac959ced75..f25bba28c8305 100644 --- a/x-pack/plugins/infra/public/apps/start_app.tsx +++ b/x-pack/plugins/infra/public/apps/start_app.tsx @@ -21,6 +21,7 @@ import { InfraFrontendLibs } from '../lib/lib'; import { PageRouter } from '../routes'; import { createStore } from '../store'; import { ApolloClientContext } from '../utils/apollo_context'; +import { HistoryContext } from '../utils/history_context'; import { useKibanaUiSetting } from '../utils/use_kibana_ui_setting'; export async function startApp(libs: InfraFrontendLibs) { @@ -44,7 +45,9 @@ export async function startApp(libs: InfraFrontendLibs) { - + + + diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx index 0b57c42bfe09f..2189e2e43507e 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx @@ -84,6 +84,11 @@ export const SourceConfigurationFlyout = injectI18n( ] ); + const isWriteable = useMemo(() => shouldAllowEdit && source && source.origin !== 'internal', [ + shouldAllowEdit, + source, + ]); + if (!isVisible || !source || !source.configuration) { return null; } @@ -101,14 +106,14 @@ export const SourceConfigurationFlyout = injectI18n( @@ -153,7 +158,7 @@ export const SourceConfigurationFlyout = injectI18n(

- {shouldAllowEdit ? ( + {isWriteable ? ( - {shouldAllowEdit && ( + {isWriteable && ( {isLoading ? ( diff --git a/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx b/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx index 262b6a01d6e71..104c94f8f26a4 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx @@ -7,10 +7,12 @@ import createContainer from 'constate-latest'; import { isString } from 'lodash'; import React, { useContext, useEffect, useMemo, useState } from 'react'; + import { FlyoutItemQuery, InfraLogItem } from '../../graphql/types'; import { useApolloClient } from '../../utils/apollo_context'; import { UrlStateContainer } from '../../utils/url_state'; import { useTrackedPromise } from '../../utils/use_tracked_promise'; +import { Source } from '../source'; import { flyoutItemQuery } from './flyout_item.gql_query'; export enum FlyoutVisibility { @@ -24,7 +26,8 @@ interface FlyoutOptionsUrlState { surroundingLogsId?: string | null; } -export const useLogFlyout = ({ sourceId }: { sourceId: string }) => { +export const useLogFlyout = () => { + const { sourceId } = useContext(Source.Context); const [flyoutVisible, setFlyoutVisibility] = useState(false); const [flyoutId, setFlyoutId] = useState(null); const [flyoutItem, setFlyoutItem] = useState(null); diff --git a/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts b/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts index 74b6decc41399..5df603f0d92b6 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts @@ -9,6 +9,7 @@ import { connect } from 'react-redux'; import { logFilterSelectors, logPositionSelectors, State } from '../../../store'; import { RendererFunction } from '../../../utils/typed_react'; +import { Source } from '../../source'; import { LogViewConfiguration } from '../log_view_configuration'; import { LogSummaryBuckets, useLogSummary } from './log_summary'; @@ -26,8 +27,9 @@ export const WithSummary = connect((state: State) => ({ visibleMidpointTime: number | null; }) => { const { intervalSize } = useContext(LogViewConfiguration.Context); + const { sourceId } = useContext(Source.Context); - const { buckets } = useLogSummary('default', visibleMidpointTime, intervalSize, filterQuery); + const { buckets } = useLogSummary(sourceId, visibleMidpointTime, intervalSize, filterQuery); return children({ buckets }); } diff --git a/x-pack/plugins/infra/public/containers/logs/with_stream_items.ts b/x-pack/plugins/infra/public/containers/logs/with_stream_items.ts index eb4ff4edeb6c4..ffee1620d2593 100644 --- a/x-pack/plugins/infra/public/containers/logs/with_stream_items.ts +++ b/x-pack/plugins/infra/public/containers/logs/with_stream_items.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { useEffect } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; @@ -24,6 +25,7 @@ export const withStreamItems = connect( bindPlainActionCreators({ loadNewerEntries: logEntriesActions.loadNewerEntries, reloadEntries: logEntriesActions.reloadEntries, + setSourceId: logEntriesActions.setSourceId, }) ); @@ -52,3 +54,26 @@ const createLogEntryStreamItem = (logEntry: LogEntry) => ({ kind: 'logEntry' as 'logEntry', logEntry, }); + +/** + * This component serves as connection between the state and side-effects + * managed by redux and the state and effects managed by hooks. In particular, + * it forwards changes of the source id to redux via the action creator + * `setSourceId`. + * + * It will be mounted beneath the hierachy level where the redux store and the + * source state are initialized. Once the log entry state and loading + * side-effects have been migrated from redux to hooks it can be removed. + */ +export const ReduxSourceIdBridge = withStreamItems( + ({ setSourceId, sourceId }: { setSourceId: (sourceId: string) => void; sourceId: string }) => { + useEffect( + () => { + setSourceId(sourceId); + }, + [setSourceId, sourceId] + ); + + return null; + } +); diff --git a/x-pack/plugins/infra/public/containers/source/source.tsx b/x-pack/plugins/infra/public/containers/source/source.tsx index effce4bd50191..38cae99176dd1 100644 --- a/x-pack/plugins/infra/public/containers/source/source.tsx +++ b/x-pack/plugins/infra/public/containers/source/source.tsx @@ -144,7 +144,7 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { () => { loadSource(); }, - [loadSource] + [loadSource, sourceId] ); return { diff --git a/x-pack/plugins/infra/public/containers/source_id/index.ts b/x-pack/plugins/infra/public/containers/source_id/index.ts new file mode 100644 index 0000000000000..bc6f51b0efba0 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/source_id/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export * from './source_id'; diff --git a/x-pack/plugins/infra/public/containers/source_id/source_id.ts b/x-pack/plugins/infra/public/containers/source_id/source_id.ts new file mode 100644 index 0000000000000..a35d17d90c7d7 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/source_id/source_id.ts @@ -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 * as runtimeTypes from 'io-ts'; + +import { useUrlState, replaceStateKeyInQueryString } from '../../utils/use_url_state'; + +const SOURCE_ID_URL_STATE_KEY = 'sourceId'; + +export const useSourceId = () => { + return useUrlState({ + defaultState: 'default', + decodeUrlState: decodeSourceIdUrlState, + encodeUrlState: encodeSourceIdUrlState, + urlStateKey: SOURCE_ID_URL_STATE_KEY, + }); +}; + +export const replaceSourceIdInQueryString = (sourceId: string) => + replaceStateKeyInQueryString(SOURCE_ID_URL_STATE_KEY, sourceId); + +const sourceIdRuntimeType = runtimeTypes.union([runtimeTypes.string, runtimeTypes.undefined]); +const encodeSourceIdUrlState = sourceIdRuntimeType.encode; +const decodeSourceIdUrlState = (value: unknown) => + sourceIdRuntimeType.decode(value).getOrElse(undefined); diff --git a/x-pack/plugins/infra/public/graphql/introspection.json b/x-pack/plugins/infra/public/graphql/introspection.json index ff86a20c760ee..5dda58949702f 100644 --- a/x-pack/plugins/infra/public/graphql/introspection.json +++ b/x-pack/plugins/infra/public/graphql/introspection.json @@ -101,6 +101,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "origin", + "description": "The origin of the source (one of 'fallback', 'internal', 'stored')", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "configuration", "description": "The raw configuration of the source", diff --git a/x-pack/plugins/infra/public/graphql/types.ts b/x-pack/plugins/infra/public/graphql/types.ts index 534592e2ad968..21b8931dded70 100644 --- a/x-pack/plugins/infra/public/graphql/types.ts +++ b/x-pack/plugins/infra/public/graphql/types.ts @@ -22,6 +22,8 @@ export interface InfraSource { version?: string | null; /** The timestamp the source configuration was last persisted at */ updatedAt?: number | null; + /** The origin of the source (one of 'fallback', 'internal', 'stored') */ + origin: string; /** The raw configuration of the source */ configuration: InfraSourceConfiguration; /** The status of the source */ @@ -1047,6 +1049,8 @@ export namespace InfraSourceFields { version?: string | null; updatedAt?: number | null; + + origin: string; }; } diff --git a/x-pack/plugins/infra/public/pages/link_to/link_to.tsx b/x-pack/plugins/infra/public/pages/link_to/link_to.tsx index bdab0b1e280e6..18ba829e0953a 100644 --- a/x-pack/plugins/infra/public/pages/link_to/link_to.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/link_to.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { match as RouteMatch, Redirect, Route, Switch } from 'react-router-dom'; -import { Source } from '../../containers/source'; import { RedirectToLogs } from './redirect_to_logs'; import { RedirectToNodeDetail } from './redirect_to_node_detail'; import { RedirectToNodeLogs } from './redirect_to_node_logs'; @@ -21,20 +20,18 @@ export class LinkToPage extends React.Component { const { match } = this.props; return ( - - - - - - - - + + + + + + ); } } diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx index e993a0ce45052..a3f7f38b480b1 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx @@ -16,12 +16,11 @@ describe('RedirectToLogs component', () => { const component = shallowWithIntl( ).dive(); - const withSourceChildFunction = component.prop('children') as any; - expect(withSourceChildFunction(testSourceChildArgs)).toMatchInlineSnapshot(` + expect(component).toMatchInlineSnapshot(` `); }); @@ -32,32 +31,33 @@ describe('RedirectToLogs component', () => { {...createRouteComponentProps('/logs?time=1550671089404&filter=FILTER_FIELD:FILTER_VALUE')} /> ).dive(); - const withSourceChildFunction = component.prop('children') as any; - expect(withSourceChildFunction(testSourceChildArgs)).toMatchInlineSnapshot(` + expect(component).toMatchInlineSnapshot(` `); }); -}); -const testSourceChildArgs = { - configuration: { - fields: { - container: 'CONTAINER_FIELD', - host: 'HOST_FIELD', - pod: 'POD_FIELD', - }, - }, - isLoading: false, -}; + it('renders a redirect with the correct custom source id', () => { + const component = shallowWithIntl( + + ).dive(); + + expect(component).toMatchInlineSnapshot(` + +`); + }); +}); const createRouteComponentProps = (path: string) => { const location = createLocation(path); return { - match: matchPath(location.pathname, { path: '/logs' }) as any, + match: matchPath(location.pathname, { path: '/:sourceId?/logs' }) as any, history: null as any, location, }; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx index ff9b38336ed1a..e7f7db5267b82 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx @@ -7,44 +7,30 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import compose from 'lodash/fp/compose'; import React from 'react'; -import { Redirect, RouteComponentProps } from 'react-router-dom'; +import { match as RouteMatch, Redirect, RouteComponentProps } from 'react-router-dom'; -import { LoadingPage } from '../../components/loading_page'; import { replaceLogFilterInQueryString } from '../../containers/logs/with_log_filter'; import { replaceLogPositionInQueryString } from '../../containers/logs/with_log_position'; -import { WithSource } from '../../containers/with_source'; +import { replaceSourceIdInQueryString } from '../../containers/source_id'; import { getFilterFromLocation, getTimeFromLocation } from './query_params'; type RedirectToLogsType = RouteComponentProps<{}>; interface RedirectToLogsProps extends RedirectToLogsType { + match: RouteMatch<{ + sourceId?: string; + }>; intl: InjectedIntl; } -export const RedirectToLogs = injectI18n(({ location, intl }: RedirectToLogsProps) => ( - - {({ configuration, isLoading }) => { - if (isLoading) { - return ( - - ); - } +export const RedirectToLogs = injectI18n(({ location, match }: RedirectToLogsProps) => { + const sourceId = match.params.sourceId || 'default'; - if (!configuration) { - return null; - } - - const filter = getFilterFromLocation(location); - const searchString = compose( - replaceLogFilterInQueryString(filter), - replaceLogPositionInQueryString(getTimeFromLocation(location)) - )(''); - return ; - }} - -)); + const filter = getFilterFromLocation(location); + const searchString = compose( + replaceLogFilterInQueryString(filter), + replaceLogPositionInQueryString(getTimeFromLocation(location)), + replaceSourceIdInQueryString(sourceId) + )(''); + return ; +}); 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 index d2e7cbd44f010..bf2e65bfbbeb8 100644 --- 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 @@ -11,17 +11,32 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; 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 = shallowWithIntl( ).dive(); - const withSourceChildFunction = component.prop('children') as any; - expect(withSourceChildFunction(testSourceChildArgs)).toMatchInlineSnapshot(` + expect(component).toMatchInlineSnapshot(` `); }); @@ -30,12 +45,11 @@ describe('RedirectToNodeLogs component', () => { const component = shallowWithIntl( ).dive(); - const withSourceChildFunction = component.prop('children') as any; - expect(withSourceChildFunction(testSourceChildArgs)).toMatchInlineSnapshot(` + expect(component).toMatchInlineSnapshot(` `); }); @@ -44,12 +58,11 @@ describe('RedirectToNodeLogs component', () => { const component = shallowWithIntl( ).dive(); - const withSourceChildFunction = component.prop('children') as any; - expect(withSourceChildFunction(testSourceChildArgs)).toMatchInlineSnapshot(` + expect(component).toMatchInlineSnapshot(` `); }); @@ -60,12 +73,11 @@ describe('RedirectToNodeLogs component', () => { {...createRouteComponentProps('/host-logs/HOST_NAME?time=1550671089404')} /> ).dive(); - const withSourceChildFunction = component.prop('children') as any; - expect(withSourceChildFunction(testSourceChildArgs)).toMatchInlineSnapshot(` + expect(component).toMatchInlineSnapshot(` `); }); @@ -78,32 +90,35 @@ describe('RedirectToNodeLogs component', () => { )} /> ).dive(); - const withSourceChildFunction = component.prop('children') as any; - expect(withSourceChildFunction(testSourceChildArgs)).toMatchInlineSnapshot(` + expect(component).toMatchInlineSnapshot(` `); }); -}); -const testSourceChildArgs = { - configuration: { - fields: { - container: 'CONTAINER_FIELD', - host: 'HOST_FIELD', - pod: 'POD_FIELD', - }, - }, - isLoading: false, -}; + it('renders a redirect with the correct custom source id', () => { + const component = shallowWithIntl( + + ).dive(); + + expect(component).toMatchInlineSnapshot(` + +`); + }); +}); const createRouteComponentProps = (path: string) => { const location = createLocation(path); return { - match: matchPath(location.pathname, { path: '/:nodeType-logs/:nodeId' }) as any, + 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 fc8863cc495f9..96ad552dd8c23 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 @@ -12,13 +12,15 @@ import { Redirect, RouteComponentProps } from 'react-router-dom'; import { LoadingPage } from '../../components/loading_page'; import { replaceLogFilterInQueryString } from '../../containers/logs/with_log_filter'; import { replaceLogPositionInQueryString } from '../../containers/logs/with_log_position'; -import { WithSource } from '../../containers/with_source'; +import { replaceSourceIdInQueryString } from '../../containers/source_id'; import { InfraNodeType } from '../../graphql/types'; import { getFilterFromLocation, getTimeFromLocation } from './query_params'; +import { useSource } from '../../containers/source/source'; type RedirectToNodeLogsType = RouteComponentProps<{ nodeId: string; nodeType: InfraNodeType; + sourceId?: string; }>; interface RedirectToNodeLogsProps extends RedirectToNodeLogsType { @@ -28,46 +30,46 @@ interface RedirectToNodeLogsProps extends RedirectToNodeLogsType { export const RedirectToNodeLogs = injectI18n( ({ match: { - params: { nodeId, nodeType }, + params: { nodeId, nodeType, sourceId = 'default' }, }, location, intl, - }: RedirectToNodeLogsProps) => ( - - {({ configuration, isLoading }) => { - if (isLoading) { - return ( - - ); - } + }: RedirectToNodeLogsProps) => { + const { source, isLoading } = useSource({ sourceId }); + const configuration = source && source.configuration; - if (!configuration) { - return null; - } + if (isLoading) { + return ( + + ); + } - const nodeFilter = `${configuration.fields[nodeType]}: ${nodeId}`; - const userFilter = getFilterFromLocation(location); - const filter = userFilter ? `(${nodeFilter}) and (${userFilter})` : nodeFilter; + if (!configuration) { + return null; + } - const searchString = compose( - replaceLogFilterInQueryString(filter), - replaceLogPositionInQueryString(getTimeFromLocation(location)) - )(''); + const nodeFilter = `${configuration.fields[nodeType]}: ${nodeId}`; + const userFilter = getFilterFromLocation(location); + const filter = userFilter ? `(${nodeFilter}) and (${userFilter})` : nodeFilter; - return ; - }} - - ) + const searchString = compose( + replaceLogFilterInQueryString(filter), + replaceLogPositionInQueryString(getTimeFromLocation(location)), + replaceSourceIdInQueryString(sourceId) + )(''); + + return ; + } ); export const getNodeLogsUrl = ({ diff --git a/x-pack/plugins/infra/public/pages/logs/page_logs_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_logs_content.tsx index 2f62a40112fc7..bbe179e33906e 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_logs_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_logs_content.tsx @@ -24,7 +24,7 @@ import { WithLogMinimapUrlState } from '../../containers/logs/with_log_minimap'; import { WithLogPositionUrlState } from '../../containers/logs/with_log_position'; import { WithLogPosition } from '../../containers/logs/with_log_position'; import { WithLogTextviewUrlState } from '../../containers/logs/with_log_textview'; -import { WithStreamItems } from '../../containers/logs/with_stream_items'; +import { ReduxSourceIdBridge, WithStreamItems } from '../../containers/logs/with_stream_items'; import { Source } from '../../containers/source'; import { LogsToolbar } from './page_toolbar'; @@ -44,6 +44,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { return ( <> + diff --git a/x-pack/plugins/infra/public/pages/logs/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/page_providers.tsx index fe5b886883a5b..27cdaa8bbedee 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_providers.tsx @@ -10,13 +10,18 @@ import { SourceConfigurationFlyoutState } from '../../components/source_configur import { LogFlyout } from '../../containers/logs/log_flyout'; import { LogViewConfiguration } from '../../containers/logs/log_view_configuration'; import { Source } from '../../containers/source'; +import { useSourceId } from '../../containers/source_id'; -export const LogsPageProviders: React.FunctionComponent = ({ children }) => ( - - - - {children} - - - -); +export const LogsPageProviders: React.FunctionComponent = ({ children }) => { + const [sourceId] = useSourceId(); + + return ( + + + + {children} + + + + ); +}; diff --git a/x-pack/plugins/infra/public/store/remote/log_entries/actions.ts b/x-pack/plugins/infra/public/store/remote/log_entries/actions.ts index 5788c993fec1a..02964bcb27e11 100644 --- a/x-pack/plugins/infra/public/store/remote/log_entries/actions.ts +++ b/x-pack/plugins/infra/public/store/remote/log_entries/actions.ts @@ -11,6 +11,8 @@ import { loadMoreEntriesActionCreators } from './operations/load_more'; const actionCreator = actionCreatorFactory('x-pack/infra/remote/log_entries'); +export const setSourceId = actionCreator('SET_SOURCE_ID'); + export const loadEntries = loadEntriesActionCreators.resolve; export const loadMoreEntries = loadMoreEntriesActionCreators.resolve; diff --git a/x-pack/plugins/infra/public/store/remote/log_entries/epic.ts b/x-pack/plugins/infra/public/store/remote/log_entries/epic.ts index 1e4e2d5092a7e..0894a31996042 100644 --- a/x-pack/plugins/infra/public/store/remote/log_entries/epic.ts +++ b/x-pack/plugins/infra/public/store/remote/log_entries/epic.ts @@ -11,7 +11,13 @@ import { exhaustMap, filter, map, withLatestFrom } from 'rxjs/operators'; import { logFilterActions, logPositionActions } from '../..'; import { pickTimeKey, TimeKey, timeKeyIsBetween } from '../../../../common/time'; -import { loadEntries, loadMoreEntries, loadNewerEntries, reloadEntries } from './actions'; +import { + loadEntries, + loadMoreEntries, + loadNewerEntries, + reloadEntries, + setSourceId, +} from './actions'; import { loadEntriesEpic } from './operations/load'; import { loadMoreEntriesEpic } from './operations/load_more'; @@ -62,6 +68,11 @@ export const createEntriesEffectsEpic = (): Epic< map(pickTimeKey) ); + const sourceId$ = action$.pipe( + filter(setSourceId.match), + map(({ payload }) => payload) + ); + const shouldLoadAroundNewPosition$ = action$.pipe( filter(logPositionActions.jumpToTargetPosition.match), withLatestFrom(state$), @@ -81,7 +92,7 @@ export const createEntriesEffectsEpic = (): Epic< withLatestFrom(filterQuery$, (filterQuery, filterQueryString) => filterQueryString) ); - const shouldReload$ = action$.pipe(filter(reloadEntries.match)); + const shouldReload$ = merge(action$.pipe(filter(reloadEntries.match)), sourceId$); const shouldLoadMoreBefore$ = action$.pipe( filter(logPositionActions.reportVisiblePositions.match), @@ -122,10 +133,10 @@ export const createEntriesEffectsEpic = (): Epic< return merge( shouldLoadAroundNewPosition$.pipe( - withLatestFrom(filterQuery$), - exhaustMap(([timeKey, filterQuery]) => [ + withLatestFrom(filterQuery$, sourceId$), + exhaustMap(([timeKey, filterQuery, sourceId]) => [ loadEntries({ - sourceId: 'default', + sourceId, timeKey, countBefore: LOAD_CHUNK_SIZE, countAfter: LOAD_CHUNK_SIZE, @@ -134,10 +145,10 @@ export const createEntriesEffectsEpic = (): Epic< ]) ), shouldLoadWithNewFilter$.pipe( - withLatestFrom(visibleMidpointOrTarget$), - exhaustMap(([filterQuery, timeKey]) => [ + withLatestFrom(visibleMidpointOrTarget$, sourceId$), + exhaustMap(([filterQuery, timeKey, sourceId]) => [ loadEntries({ - sourceId: 'default', + sourceId, timeKey, countBefore: LOAD_CHUNK_SIZE, countAfter: LOAD_CHUNK_SIZE, @@ -146,10 +157,10 @@ export const createEntriesEffectsEpic = (): Epic< ]) ), shouldReload$.pipe( - withLatestFrom(visibleMidpointOrTarget$, filterQuery$), - exhaustMap(([_, timeKey, filterQuery]) => [ + withLatestFrom(visibleMidpointOrTarget$, filterQuery$, sourceId$), + exhaustMap(([_, timeKey, filterQuery, sourceId]) => [ loadEntries({ - sourceId: 'default', + sourceId, timeKey, countBefore: LOAD_CHUNK_SIZE, countAfter: LOAD_CHUNK_SIZE, @@ -158,10 +169,10 @@ export const createEntriesEffectsEpic = (): Epic< ]) ), shouldLoadMoreAfter$.pipe( - withLatestFrom(filterQuery$), - exhaustMap(([timeKey, filterQuery]) => [ + withLatestFrom(filterQuery$, sourceId$), + exhaustMap(([timeKey, filterQuery, sourceId]) => [ loadMoreEntries({ - sourceId: 'default', + sourceId, timeKey, countBefore: 0, countAfter: LOAD_CHUNK_SIZE, @@ -170,10 +181,10 @@ export const createEntriesEffectsEpic = (): Epic< ]) ), shouldLoadMoreBefore$.pipe( - withLatestFrom(filterQuery$), - exhaustMap(([timeKey, filterQuery]) => [ + withLatestFrom(filterQuery$, sourceId$), + exhaustMap(([timeKey, filterQuery, sourceId]) => [ loadMoreEntries({ - sourceId: 'default', + sourceId, timeKey, countBefore: LOAD_CHUNK_SIZE, countAfter: 0, diff --git a/x-pack/plugins/infra/public/utils/history_context.ts b/x-pack/plugins/infra/public/utils/history_context.ts new file mode 100644 index 0000000000000..fe036e3179ec1 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/history_context.ts @@ -0,0 +1,14 @@ +/* + * 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 { createContext, useContext } from 'react'; +import { History } from 'history'; + +export const HistoryContext = createContext(undefined); + +export const useHistory = () => { + return useContext(HistoryContext); +}; diff --git a/x-pack/plugins/infra/public/utils/use_url_state.ts b/x-pack/plugins/infra/public/utils/use_url_state.ts new file mode 100644 index 0000000000000..1526bc82f3983 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/use_url_state.ts @@ -0,0 +1,116 @@ +/* + * 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 { useMemo, useCallback } from 'react'; +import { decode, encode, RisonValue } from 'rison-node'; + +import { QueryString } from 'ui/utils/query_string'; +import { useHistory } from './history_context'; + +export const useUrlState = ({ + defaultState, + decodeUrlState, + encodeUrlState, + urlStateKey, +}: { + defaultState: State; + decodeUrlState: (value: RisonValue | undefined) => State | undefined; + encodeUrlState: (value: State) => RisonValue | undefined; + urlStateKey: string; +}) => { + const history = useHistory(); + + const urlStateString = useMemo( + () => { + if (!history) { + return; + } + + return getParamFromQueryString(getQueryStringFromLocation(history.location), urlStateKey); + }, + [history && history.location, urlStateKey] + ); + + const decodedState = useMemo(() => decodeUrlState(decodeRisonUrlState(urlStateString)), [ + decodeUrlState, + urlStateString, + ]); + + const state = useMemo(() => (typeof decodedState !== 'undefined' ? decodedState : defaultState), [ + defaultState, + decodedState, + ]); + + const setState = useCallback( + (newState: State | undefined) => { + if (!history) { + return; + } + + const location = history.location; + + const newLocation = replaceQueryStringInLocation( + location, + replaceStateKeyInQueryString( + urlStateKey, + typeof newState !== 'undefined' ? encodeUrlState(newState) : undefined + )(getQueryStringFromLocation(location)) + ); + + if (newLocation !== location) { + history.replace(newLocation); + } + }, + [encodeUrlState, history, history && history.location, urlStateKey] + ); + + return [state, setState] as [typeof state, typeof setState]; +}; + +const decodeRisonUrlState = (value: string | undefined): RisonValue | undefined => { + try { + return value ? decode(value) : undefined; + } catch (error) { + if (error instanceof Error && error.message.startsWith('rison decoder error')) { + return {}; + } + throw error; + } +}; + +const encodeRisonUrlState = (state: any) => encode(state); + +const getQueryStringFromLocation = (location: Location) => location.search.substring(1); + +const getParamFromQueryString = (queryString: string, key: string): string | undefined => { + const queryParam = QueryString.decode(queryString)[key]; + return Array.isArray(queryParam) ? queryParam[0] : queryParam; +}; + +export const replaceStateKeyInQueryString = ( + stateKey: string, + urlState: UrlState | undefined +) => (queryString: string) => { + const previousQueryValues = QueryString.decode(queryString); + const encodedUrlState = + typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; + return QueryString.encode({ + ...previousQueryValues, + [stateKey]: encodedUrlState, + }); +}; + +const replaceQueryStringInLocation = (location: Location, queryString: string): Location => { + if (queryString === getQueryStringFromLocation(location)) { + return location; + } else { + return { + ...location, + search: `?${queryString}`, + }; + } +}; diff --git a/x-pack/plugins/infra/server/graphql/sources/schema.gql.ts b/x-pack/plugins/infra/server/graphql/sources/schema.gql.ts index 1df03060d1b32..a39399cec7c32 100644 --- a/x-pack/plugins/infra/server/graphql/sources/schema.gql.ts +++ b/x-pack/plugins/infra/server/graphql/sources/schema.gql.ts @@ -15,6 +15,8 @@ export const sourcesSchema = gql` version: String "The timestamp the source configuration was last persisted at" updatedAt: Float + "The origin of the source (one of 'fallback', 'internal', 'stored')" + origin: String! "The raw configuration of the source" configuration: InfraSourceConfiguration! "The status of the source" diff --git a/x-pack/plugins/infra/server/graphql/types.ts b/x-pack/plugins/infra/server/graphql/types.ts index bd7f22b5a3f2e..67d9aa8672b11 100644 --- a/x-pack/plugins/infra/server/graphql/types.ts +++ b/x-pack/plugins/infra/server/graphql/types.ts @@ -50,6 +50,8 @@ export interface InfraSource { version?: string | null; /** The timestamp the source configuration was last persisted at */ updatedAt?: number | null; + /** The origin of the source (one of 'fallback', 'internal', 'stored') */ + origin: string; /** The raw configuration of the source */ configuration: InfraSourceConfiguration; /** The status of the source */ @@ -627,6 +629,8 @@ export namespace InfraSourceResolvers { version?: VersionResolver; /** The timestamp the source configuration was last persisted at */ updatedAt?: UpdatedAtResolver; + /** The origin of the source (one of 'fallback', 'internal', 'stored') */ + origin?: OriginResolver; /** The raw configuration of the source */ configuration?: ConfigurationResolver; /** The status of the source */ @@ -662,6 +666,11 @@ export namespace InfraSourceResolvers { Parent = InfraSource, Context = InfraContext > = Resolver; + export type OriginResolver = Resolver< + R, + Parent, + Context + >; export type ConfigurationResolver< R = InfraSourceConfiguration, Parent = InfraSource, diff --git a/x-pack/plugins/infra/server/kibana.index.ts b/x-pack/plugins/infra/server/kibana.index.ts index b8e96e4acf60e..1b691e577f2d1 100644 --- a/x-pack/plugins/infra/server/kibana.index.ts +++ b/x-pack/plugins/infra/server/kibana.index.ts @@ -19,6 +19,11 @@ export const initServerWithKibana = (kbnServer: KbnServer) => { const libs = compose(kbnServer); initInfraServer(libs); + kbnServer.expose( + 'defineInternalSourceConfiguration', + libs.sources.defineInternalSourceConfiguration.bind(libs.sources) + ); + // Register a function with server to manage the collection of usage stats kbnServer.usage.collectorSet.register(UsageCollector.getUsageCollector(kbnServer)); diff --git a/x-pack/plugins/infra/server/lib/sources/errors.ts b/x-pack/plugins/infra/server/lib/sources/errors.ts new file mode 100644 index 0000000000000..9f835f21443c6 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/sources/errors.ts @@ -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. + */ + +export class NotFoundError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index 087aa8802398e..b1bd22b9287e2 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -12,6 +12,7 @@ import { Pick3 } from '../../../common/utility_types'; import { InfraConfigurationAdapter } from '../adapters/configuration'; import { InfraFrameworkRequest, internalInfraFrameworkRequest } from '../adapters/framework'; import { defaultSourceConfiguration } from './defaults'; +import { NotFoundError } from './errors'; import { infraSourceConfigurationSavedObjectType } from './saved_object_mappings'; import { InfraSavedSourceConfiguration, @@ -23,6 +24,8 @@ import { } from './types'; export class InfraSources { + private internalSourceConfigurations: Map = new Map(); + constructor( private readonly libs: { configuration: InfraConfigurationAdapter; @@ -34,24 +37,39 @@ export class InfraSources { public async getSourceConfiguration(request: InfraFrameworkRequest, sourceId: string) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const savedSourceConfiguration = await this.getSavedSourceConfiguration(request, sourceId).then( - result => ({ - ...result, + const savedSourceConfiguration = await this.getInternalSourceConfiguration(sourceId) + .then(internalSourceConfiguration => ({ + id: sourceId, + version: undefined, + updatedAt: undefined, + origin: 'internal' as 'internal', configuration: mergeSourceConfiguration( staticDefaultSourceConfiguration, - result.configuration + internalSourceConfiguration ), - }), - err => + })) + .catch(err => + err instanceof NotFoundError + ? this.getSavedSourceConfiguration(request, sourceId).then(result => ({ + ...result, + configuration: mergeSourceConfiguration( + staticDefaultSourceConfiguration, + result.configuration + ), + })) + : Promise.reject(err) + ) + .catch(err => this.libs.savedObjects.SavedObjectsClient.errors.isNotFoundError(err) ? Promise.resolve({ id: sourceId, version: undefined, updatedAt: undefined, + origin: 'fallback' as 'fallback', configuration: staticDefaultSourceConfiguration, }) : Promise.reject(err) - ); + ); return savedSourceConfiguration; } @@ -143,6 +161,25 @@ export class InfraSources { }; } + public async defineInternalSourceConfiguration( + sourceId: string, + sourceProperties: InfraStaticSourceConfiguration + ) { + this.internalSourceConfigurations.set(sourceId, sourceProperties); + } + + public async getInternalSourceConfiguration(sourceId: string) { + const internalSourceConfiguration = this.internalSourceConfigurations.get(sourceId); + + if (!internalSourceConfiguration) { + throw new NotFoundError( + `Failed to load internal source configuration: no configuration "${sourceId}" found.` + ); + } + + return internalSourceConfiguration; + } + private async getStaticDefaultSourceConfiguration() { const staticConfiguration = await this.libs.configuration.get(); const staticSourceConfiguration = runtimeTypes @@ -206,6 +243,7 @@ const convertSavedObjectToSavedSourceConfiguration = (savedObject: unknown) => id: savedSourceConfiguration.id, version: savedSourceConfiguration.version, updatedAt: savedSourceConfiguration.updated_at, + origin: 'stored' as 'stored', configuration: savedSourceConfiguration.attributes, })) .getOrElseL(errors => {