[7.x] [Logs UI] Allow for plugins to inject internal source configurations (#36066) (#36470)

Backports the following commits to 7.x:
 - [Logs UI] Allow for plugins to inject internal source configurations  (#36066)
This commit is contained in:
Felix Stürmer 2019-05-10 22:53:16 +02:00 committed by GitHub
parent 862c67ae0e
commit 4dd925b4d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 475 additions and 166 deletions

View file

@ -18,6 +18,7 @@ export const sharedFragments = {
id
version
updatedAt
origin
}
`,
InfraLogEntryFields: gql`

View file

@ -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;
};
}

View file

@ -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) {
<ApolloProvider client={libs.apolloClient}>
<ApolloClientContext.Provider value={libs.apolloClient}>
<EuiThemeProvider darkMode={darkMode}>
<PageRouter history={history} />
<HistoryContext.Provider value={history}>
<PageRouter history={history} />
</HistoryContext.Provider>
</EuiThemeProvider>
</ApolloClientContext.Provider>
</ApolloProvider>

View file

@ -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(
<NameConfigurationPanel
isLoading={isLoading}
nameFieldProps={indicesConfigurationProps.name}
readOnly={!shouldAllowEdit}
readOnly={!isWriteable}
/>
<EuiSpacer />
<IndicesConfigurationPanel
isLoading={isLoading}
logAliasFieldProps={indicesConfigurationProps.logAlias}
metricAliasFieldProps={indicesConfigurationProps.metricAlias}
readOnly={!shouldAllowEdit}
readOnly={!isWriteable}
/>
<EuiSpacer />
<FieldsConfigurationPanel
@ -116,7 +121,7 @@ export const SourceConfigurationFlyout = injectI18n(
hostFieldProps={indicesConfigurationProps.hostField}
isLoading={isLoading}
podFieldProps={indicesConfigurationProps.podField}
readOnly={!shouldAllowEdit}
readOnly={!isWriteable}
tiebreakerFieldProps={indicesConfigurationProps.tiebreakerField}
timestampFieldProps={indicesConfigurationProps.timestampField}
/>
@ -153,7 +158,7 @@ export const SourceConfigurationFlyout = injectI18n(
<EuiFlyoutHeader hasBorder>
<EuiTitle>
<h2 id="sourceConfigurationTitle">
{shouldAllowEdit ? (
{isWriteable ? (
<FormattedMessage
id="xpack.infra.sourceConfiguration.sourceConfigurationTitle"
defaultMessage="Configure source"
@ -216,7 +221,7 @@ export const SourceConfigurationFlyout = injectI18n(
)}
</EuiFlexItem>
<EuiFlexItem />
{shouldAllowEdit && (
{isWriteable && (
<EuiFlexItem grow={false}>
{isLoading ? (
<EuiButton color="primary" isLoading fill>

View file

@ -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<boolean>(false);
const [flyoutId, setFlyoutId] = useState<string | null>(null);
const [flyoutItem, setFlyoutItem] = useState<InfraLogItem | null>(null);

View file

@ -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 });
}

View file

@ -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;
}
);

View file

@ -144,7 +144,7 @@ export const useSource = ({ sourceId }: { sourceId: string }) => {
() => {
loadSource();
},
[loadSource]
[loadSource, sourceId]
);
return {

View file

@ -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';

View file

@ -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);

View file

@ -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",

View file

@ -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;
};
}

View file

@ -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<LinkToPageProps> {
const { match } = this.props;
return (
<Source.Provider sourceId="default">
<Switch>
<Route
path={`${match.url}/:nodeType(host|container|pod)-logs/:nodeId`}
component={RedirectToNodeLogs}
/>
<Route
path={`${match.url}/:nodeType(host|container|pod)-detail/:nodeId`}
component={RedirectToNodeDetail}
/>
<Route path={`${match.url}/logs`} component={RedirectToLogs} />
<Redirect to="/infrastructure" />
</Switch>
</Source.Provider>
<Switch>
<Route
path={`${match.url}/:sourceId?/:nodeType(host|container|pod)-logs/:nodeId`}
component={RedirectToNodeLogs}
/>
<Route
path={`${match.url}/:nodeType(host|container|pod)-detail/:nodeId`}
component={RedirectToNodeDetail}
/>
<Route path={`${match.url}/:sourceId?/logs`} component={RedirectToLogs} />
<Redirect to="/infrastructure" />
</Switch>
);
}
}

View file

@ -16,12 +16,11 @@ describe('RedirectToLogs component', () => {
const component = shallowWithIntl(
<RedirectToLogs {...createRouteComponentProps('/logs?time=1550671089404')} />
).dive();
const withSourceChildFunction = component.prop('children') as any;
expect(withSourceChildFunction(testSourceChildArgs)).toMatchInlineSnapshot(`
expect(component).toMatchInlineSnapshot(`
<Redirect
push={false}
to="/logs?logFilter=(expression:'',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1550671089404))"
to="/logs?logFilter=(expression:'',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1550671089404))&sourceId=default"
/>
`);
});
@ -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(`
<Redirect
push={false}
to="/logs?logFilter=(expression:'FILTER_FIELD:FILTER_VALUE',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1550671089404))"
to="/logs?logFilter=(expression:'FILTER_FIELD:FILTER_VALUE',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1550671089404))&sourceId=default"
/>
`);
});
it('renders a redirect with the correct custom source id', () => {
const component = shallowWithIntl(
<RedirectToLogs {...createRouteComponentProps('/SOME-OTHER-SOURCE/logs')} />
).dive();
expect(component).toMatchInlineSnapshot(`
<Redirect
push={false}
to="/logs?logFilter=(expression:'',kind:kuery)&sourceId=SOME-OTHER-SOURCE"
/>
`);
});
});
const testSourceChildArgs = {
configuration: {
fields: {
container: 'CONTAINER_FIELD',
host: 'HOST_FIELD',
pod: 'POD_FIELD',
},
},
isLoading: false,
};
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,
};

View file

@ -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) => (
<WithSource>
{({ configuration, isLoading }) => {
if (isLoading) {
return (
<LoadingPage
message={intl.formatMessage({
id: 'xpack.infra.redirectToLogs.loadingLogsMessage',
defaultMessage: 'Loading logs',
})}
/>
);
}
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 <Redirect to={`/logs?${searchString}`} />;
}}
</WithSource>
));
const filter = getFilterFromLocation(location);
const searchString = compose(
replaceLogFilterInQueryString(filter),
replaceLogPositionInQueryString(getTimeFromLocation(location)),
replaceSourceIdInQueryString(sourceId)
)('');
return <Redirect to={`/logs?${searchString}`} />;
});

View file

@ -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(
<RedirectToNodeLogs {...createRouteComponentProps('/host-logs/HOST_NAME')} />
).dive();
const withSourceChildFunction = component.prop('children') as any;
expect(withSourceChildFunction(testSourceChildArgs)).toMatchInlineSnapshot(`
expect(component).toMatchInlineSnapshot(`
<Redirect
push={false}
to="/logs?logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)"
to="/logs?logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)&sourceId=default"
/>
`);
});
@ -30,12 +45,11 @@ describe('RedirectToNodeLogs component', () => {
const component = shallowWithIntl(
<RedirectToNodeLogs {...createRouteComponentProps('/container-logs/CONTAINER_ID')} />
).dive();
const withSourceChildFunction = component.prop('children') as any;
expect(withSourceChildFunction(testSourceChildArgs)).toMatchInlineSnapshot(`
expect(component).toMatchInlineSnapshot(`
<Redirect
push={false}
to="/logs?logFilter=(expression:'CONTAINER_FIELD:%20CONTAINER_ID',kind:kuery)"
to="/logs?logFilter=(expression:'CONTAINER_FIELD:%20CONTAINER_ID',kind:kuery)&sourceId=default"
/>
`);
});
@ -44,12 +58,11 @@ describe('RedirectToNodeLogs component', () => {
const component = shallowWithIntl(
<RedirectToNodeLogs {...createRouteComponentProps('/pod-logs/POD_ID')} />
).dive();
const withSourceChildFunction = component.prop('children') as any;
expect(withSourceChildFunction(testSourceChildArgs)).toMatchInlineSnapshot(`
expect(component).toMatchInlineSnapshot(`
<Redirect
push={false}
to="/logs?logFilter=(expression:'POD_FIELD:%20POD_ID',kind:kuery)"
to="/logs?logFilter=(expression:'POD_FIELD:%20POD_ID',kind:kuery)&sourceId=default"
/>
`);
});
@ -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(`
<Redirect
push={false}
to="/logs?logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1550671089404))"
to="/logs?logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1550671089404))&sourceId=default"
/>
`);
});
@ -78,32 +90,35 @@ describe('RedirectToNodeLogs component', () => {
)}
/>
).dive();
const withSourceChildFunction = component.prop('children') as any;
expect(withSourceChildFunction(testSourceChildArgs)).toMatchInlineSnapshot(`
expect(component).toMatchInlineSnapshot(`
<Redirect
push={false}
to="/logs?logFilter=(expression:'(HOST_FIELD:%20HOST_NAME)%20and%20(FILTER_FIELD:FILTER_VALUE)',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1550671089404))"
to="/logs?logFilter=(expression:'(HOST_FIELD:%20HOST_NAME)%20and%20(FILTER_FIELD:FILTER_VALUE)',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1550671089404))&sourceId=default"
/>
`);
});
it('renders a redirect with the correct custom source id', () => {
const component = shallowWithIntl(
<RedirectToNodeLogs
{...createRouteComponentProps('/SOME-OTHER-SOURCE/host-logs/HOST_NAME')}
/>
).dive();
expect(component).toMatchInlineSnapshot(`
<Redirect
push={false}
to="/logs?logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)&sourceId=SOME-OTHER-SOURCE"
/>
`);
});
});
const testSourceChildArgs = {
configuration: {
fields: {
container: 'CONTAINER_FIELD',
host: 'HOST_FIELD',
pod: 'POD_FIELD',
},
},
isLoading: false,
};
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,
};

View file

@ -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) => (
<WithSource>
{({ configuration, isLoading }) => {
if (isLoading) {
return (
<LoadingPage
message={intl.formatMessage(
{
id: 'xpack.infra.redirectToNodeLogs.loadingNodeLogsMessage',
defaultMessage: 'Loading {nodeType} logs',
},
{
nodeType,
}
)}
/>
);
}
}: RedirectToNodeLogsProps) => {
const { source, isLoading } = useSource({ sourceId });
const configuration = source && source.configuration;
if (!configuration) {
return null;
}
if (isLoading) {
return (
<LoadingPage
message={intl.formatMessage(
{
id: 'xpack.infra.redirectToNodeLogs.loadingNodeLogsMessage',
defaultMessage: 'Loading {nodeType} logs',
},
{
nodeType,
}
)}
/>
);
}
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 <Redirect to={`/logs?${searchString}`} />;
}}
</WithSource>
)
const searchString = compose(
replaceLogFilterInQueryString(filter),
replaceLogPositionInQueryString(getTimeFromLocation(location)),
replaceSourceIdInQueryString(sourceId)
)('');
return <Redirect to={`/logs?${searchString}`} />;
}
);
export const getNodeLogsUrl = ({

View file

@ -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 (
<>
<ReduxSourceIdBridge sourceId={sourceId} />
<WithLogFilterUrlState indexPattern={derivedIndexPattern} />
<WithLogPositionUrlState />
<WithLogMinimapUrlState />

View file

@ -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 }) => (
<Source.Provider sourceId="default">
<SourceConfigurationFlyoutState.Provider>
<LogViewConfiguration.Provider>
<LogFlyout.Provider sourceId="default">{children}</LogFlyout.Provider>
</LogViewConfiguration.Provider>
</SourceConfigurationFlyoutState.Provider>
</Source.Provider>
);
export const LogsPageProviders: React.FunctionComponent = ({ children }) => {
const [sourceId] = useSourceId();
return (
<Source.Provider sourceId={sourceId}>
<SourceConfigurationFlyoutState.Provider>
<LogViewConfiguration.Provider>
<LogFlyout.Provider>{children}</LogFlyout.Provider>
</LogViewConfiguration.Provider>
</SourceConfigurationFlyoutState.Provider>
</Source.Provider>
);
};

View file

@ -11,6 +11,8 @@ import { loadMoreEntriesActionCreators } from './operations/load_more';
const actionCreator = actionCreatorFactory('x-pack/infra/remote/log_entries');
export const setSourceId = actionCreator<string>('SET_SOURCE_ID');
export const loadEntries = loadEntriesActionCreators.resolve;
export const loadMoreEntries = loadMoreEntriesActionCreators.resolve;

View file

@ -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 = <State>(): 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 = <State>(): 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 = <State>(): 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 = <State>(): 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 = <State>(): 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 = <State>(): 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 = <State>(): 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,

View file

@ -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<History | undefined>(undefined);
export const useHistory = () => {
return useContext(HistoryContext);
};

View file

@ -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 = <State>({
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 = <UrlState extends any>(
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}`,
};
}
};

View file

@ -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"

View file

@ -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<string | null, TypeParent, Context>;
/** The timestamp the source configuration was last persisted at */
updatedAt?: UpdatedAtResolver<number | null, TypeParent, Context>;
/** The origin of the source (one of 'fallback', 'internal', 'stored') */
origin?: OriginResolver<string, TypeParent, Context>;
/** The raw configuration of the source */
configuration?: ConfigurationResolver<InfraSourceConfiguration, TypeParent, Context>;
/** The status of the source */
@ -662,6 +666,11 @@ export namespace InfraSourceResolvers {
Parent = InfraSource,
Context = InfraContext
> = Resolver<R, Parent, Context>;
export type OriginResolver<R = string, Parent = InfraSource, Context = InfraContext> = Resolver<
R,
Parent,
Context
>;
export type ConfigurationResolver<
R = InfraSourceConfiguration,
Parent = InfraSource,

View file

@ -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));

View file

@ -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);
}
}

View file

@ -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<string, InfraStaticSourceConfiguration> = 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 => {