[Logs UI] Reimplement log source configuration routes in plain HTTP+JSON (#64021)
This commit is contained in:
parent
f9c81a30cb
commit
2fba7ed9f7
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 rt from 'io-ts';
|
||||
import { badRequestErrorRT, forbiddenErrorRT, routeTimingMetadataRT } from '../shared';
|
||||
import { logSourceConfigurationRT } from './log_source_configuration';
|
||||
|
||||
/**
|
||||
* request
|
||||
*/
|
||||
|
||||
export const getLogSourceConfigurationRequestParamsRT = rt.type({
|
||||
// the id of the source configuration
|
||||
sourceId: rt.string,
|
||||
});
|
||||
|
||||
export type GetLogSourceConfigurationRequestParams = rt.TypeOf<
|
||||
typeof getLogSourceConfigurationRequestParamsRT
|
||||
>;
|
||||
|
||||
/**
|
||||
* response
|
||||
*/
|
||||
|
||||
export const getLogSourceConfigurationSuccessResponsePayloadRT = rt.intersection([
|
||||
rt.type({
|
||||
data: logSourceConfigurationRT,
|
||||
}),
|
||||
rt.partial({
|
||||
timing: routeTimingMetadataRT,
|
||||
}),
|
||||
]);
|
||||
|
||||
export type GetLogSourceConfigurationSuccessResponsePayload = rt.TypeOf<
|
||||
typeof getLogSourceConfigurationSuccessResponsePayloadRT
|
||||
>;
|
||||
|
||||
export const getLogSourceConfigurationErrorResponsePayloadRT = rt.union([
|
||||
badRequestErrorRT,
|
||||
forbiddenErrorRT,
|
||||
]);
|
||||
|
||||
export type GetLogSourceConfigurationErrorReponsePayload = rt.TypeOf<
|
||||
typeof getLogSourceConfigurationErrorResponsePayloadRT
|
||||
>;
|
||||
|
||||
export const getLogSourceConfigurationResponsePayloadRT = rt.union([
|
||||
getLogSourceConfigurationSuccessResponsePayloadRT,
|
||||
getLogSourceConfigurationErrorResponsePayloadRT,
|
||||
]);
|
||||
|
||||
export type GetLogSourceConfigurationReponsePayload = rt.TypeOf<
|
||||
typeof getLogSourceConfigurationResponsePayloadRT
|
||||
>;
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 rt from 'io-ts';
|
||||
import { routeTimingMetadataRT } from '../shared';
|
||||
import {
|
||||
getLogSourceConfigurationPath,
|
||||
LOG_SOURCE_CONFIGURATION_PATH,
|
||||
} from './log_source_configuration';
|
||||
|
||||
export const LOG_SOURCE_STATUS_PATH_SUFFIX = 'status';
|
||||
export const LOG_SOURCE_STATUS_PATH = `${LOG_SOURCE_CONFIGURATION_PATH}/${LOG_SOURCE_STATUS_PATH_SUFFIX}`;
|
||||
export const getLogSourceStatusPath = (sourceId: string) =>
|
||||
`${getLogSourceConfigurationPath(sourceId)}/${LOG_SOURCE_STATUS_PATH_SUFFIX}`;
|
||||
|
||||
/**
|
||||
* request
|
||||
*/
|
||||
|
||||
export const getLogSourceStatusRequestParamsRT = rt.type({
|
||||
// the id of the source configuration
|
||||
sourceId: rt.string,
|
||||
});
|
||||
|
||||
export type GetLogSourceStatusRequestParams = rt.TypeOf<typeof getLogSourceStatusRequestParamsRT>;
|
||||
|
||||
/**
|
||||
* response
|
||||
*/
|
||||
|
||||
const logIndexFieldRT = rt.strict({
|
||||
name: rt.string,
|
||||
type: rt.string,
|
||||
searchable: rt.boolean,
|
||||
aggregatable: rt.boolean,
|
||||
});
|
||||
|
||||
export type LogIndexField = rt.TypeOf<typeof logIndexFieldRT>;
|
||||
|
||||
const logSourceStatusRT = rt.strict({
|
||||
logIndexFields: rt.array(logIndexFieldRT),
|
||||
logIndexNames: rt.array(rt.string),
|
||||
});
|
||||
|
||||
export type LogSourceStatus = rt.TypeOf<typeof logSourceStatusRT>;
|
||||
|
||||
export const getLogSourceStatusSuccessResponsePayloadRT = rt.intersection([
|
||||
rt.type({
|
||||
data: logSourceStatusRT,
|
||||
}),
|
||||
rt.partial({
|
||||
timing: routeTimingMetadataRT,
|
||||
}),
|
||||
]);
|
||||
|
||||
export type GetLogSourceStatusSuccessResponsePayload = rt.TypeOf<
|
||||
typeof getLogSourceStatusSuccessResponsePayloadRT
|
||||
>;
|
10
x-pack/plugins/infra/common/http_api/log_sources/index.ts
Normal file
10
x-pack/plugins/infra/common/http_api/log_sources/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 './get_log_source_configuration';
|
||||
export * from './get_log_source_status';
|
||||
export * from './log_source_configuration';
|
||||
export * from './patch_log_source_configuration';
|
|
@ -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 * as rt from 'io-ts';
|
||||
|
||||
export const LOG_SOURCE_CONFIGURATION_PATH_PREFIX = '/api/infra/log_source_configurations';
|
||||
export const LOG_SOURCE_CONFIGURATION_PATH = `${LOG_SOURCE_CONFIGURATION_PATH_PREFIX}/{sourceId}`;
|
||||
export const getLogSourceConfigurationPath = (sourceId: string) =>
|
||||
`${LOG_SOURCE_CONFIGURATION_PATH_PREFIX}/${sourceId}`;
|
||||
|
||||
export const logSourceConfigurationOriginRT = rt.keyof({
|
||||
fallback: null,
|
||||
internal: null,
|
||||
stored: null,
|
||||
});
|
||||
|
||||
export type LogSourceConfigurationOrigin = rt.TypeOf<typeof logSourceConfigurationOriginRT>;
|
||||
|
||||
const logSourceFieldsConfigurationRT = rt.strict({
|
||||
timestamp: rt.string,
|
||||
tiebreaker: rt.string,
|
||||
});
|
||||
|
||||
const logSourceCommonColumnConfigurationRT = rt.strict({
|
||||
id: rt.string,
|
||||
});
|
||||
|
||||
const logSourceTimestampColumnConfigurationRT = rt.strict({
|
||||
timestampColumn: logSourceCommonColumnConfigurationRT,
|
||||
});
|
||||
|
||||
const logSourceMessageColumnConfigurationRT = rt.strict({
|
||||
messageColumn: logSourceCommonColumnConfigurationRT,
|
||||
});
|
||||
|
||||
const logSourceFieldColumnConfigurationRT = rt.strict({
|
||||
fieldColumn: rt.intersection([
|
||||
logSourceCommonColumnConfigurationRT,
|
||||
rt.strict({
|
||||
field: rt.string,
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
const logSourceColumnConfigurationRT = rt.union([
|
||||
logSourceTimestampColumnConfigurationRT,
|
||||
logSourceMessageColumnConfigurationRT,
|
||||
logSourceFieldColumnConfigurationRT,
|
||||
]);
|
||||
|
||||
export const logSourceConfigurationPropertiesRT = rt.strict({
|
||||
name: rt.string,
|
||||
description: rt.string,
|
||||
logAlias: rt.string,
|
||||
fields: logSourceFieldsConfigurationRT,
|
||||
logColumns: rt.array(logSourceColumnConfigurationRT),
|
||||
});
|
||||
|
||||
export type LogSourceConfigurationProperties = rt.TypeOf<typeof logSourceConfigurationPropertiesRT>;
|
||||
|
||||
export const logSourceConfigurationRT = rt.exact(
|
||||
rt.intersection([
|
||||
rt.type({
|
||||
id: rt.string,
|
||||
origin: logSourceConfigurationOriginRT,
|
||||
configuration: logSourceConfigurationPropertiesRT,
|
||||
}),
|
||||
rt.partial({
|
||||
updatedAt: rt.number,
|
||||
version: rt.string,
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
export type LogSourceConfiguration = rt.TypeOf<typeof logSourceConfigurationRT>;
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 rt from 'io-ts';
|
||||
import { badRequestErrorRT, forbiddenErrorRT } from '../shared';
|
||||
import { getLogSourceConfigurationSuccessResponsePayloadRT } from './get_log_source_configuration';
|
||||
import { logSourceConfigurationPropertiesRT } from './log_source_configuration';
|
||||
|
||||
/**
|
||||
* request
|
||||
*/
|
||||
|
||||
export const patchLogSourceConfigurationRequestParamsRT = rt.type({
|
||||
// the id of the source configuration
|
||||
sourceId: rt.string,
|
||||
});
|
||||
|
||||
export type PatchLogSourceConfigurationRequestParams = rt.TypeOf<
|
||||
typeof patchLogSourceConfigurationRequestParamsRT
|
||||
>;
|
||||
|
||||
const logSourceConfigurationProperiesPatchRT = rt.partial({
|
||||
...logSourceConfigurationPropertiesRT.type.props,
|
||||
fields: rt.partial(logSourceConfigurationPropertiesRT.type.props.fields.type.props),
|
||||
});
|
||||
|
||||
export type LogSourceConfigurationPropertiesPatch = rt.TypeOf<
|
||||
typeof logSourceConfigurationProperiesPatchRT
|
||||
>;
|
||||
|
||||
export const patchLogSourceConfigurationRequestBodyRT = rt.type({
|
||||
data: logSourceConfigurationProperiesPatchRT,
|
||||
});
|
||||
|
||||
export type PatchLogSourceConfigurationRequestBody = rt.TypeOf<
|
||||
typeof patchLogSourceConfigurationRequestBodyRT
|
||||
>;
|
||||
|
||||
/**
|
||||
* response
|
||||
*/
|
||||
|
||||
export const patchLogSourceConfigurationSuccessResponsePayloadRT = getLogSourceConfigurationSuccessResponsePayloadRT;
|
||||
|
||||
export type PatchLogSourceConfigurationSuccessResponsePayload = rt.TypeOf<
|
||||
typeof patchLogSourceConfigurationSuccessResponsePayloadRT
|
||||
>;
|
||||
|
||||
export const patchLogSourceConfigurationResponsePayloadRT = rt.union([
|
||||
patchLogSourceConfigurationSuccessResponsePayloadRT,
|
||||
badRequestErrorRT,
|
||||
forbiddenErrorRT,
|
||||
]);
|
||||
|
||||
export type PatchLogSourceConfigurationReponsePayload = rt.TypeOf<
|
||||
typeof patchLogSourceConfigurationResponsePayloadRT
|
||||
>;
|
|
@ -9,6 +9,7 @@ import { identity } from 'fp-ts/lib/function';
|
|||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { Errors, Type } from 'io-ts';
|
||||
import { failure } from 'io-ts/lib/PathReporter';
|
||||
import { RouteValidationFunction } from 'kibana/server';
|
||||
|
||||
type ErrorFactory = (message: string) => Error;
|
||||
|
||||
|
@ -18,8 +19,21 @@ export const throwErrors = (createError: ErrorFactory) => (errors: Errors) => {
|
|||
throw createError(failure(errors).join('\n'));
|
||||
};
|
||||
|
||||
export const decodeOrThrow = <A, O, I>(
|
||||
runtimeType: Type<A, O, I>,
|
||||
export const decodeOrThrow = <DecodedValue, EncodedValue, InputValue>(
|
||||
runtimeType: Type<DecodedValue, EncodedValue, InputValue>,
|
||||
createError: ErrorFactory = createPlainError
|
||||
) => (inputValue: I) =>
|
||||
) => (inputValue: InputValue) =>
|
||||
pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity));
|
||||
|
||||
type ValdidationResult<Value> = ReturnType<RouteValidationFunction<Value>>;
|
||||
|
||||
export const createValidationFunction = <DecodedValue, EncodedValue, InputValue>(
|
||||
runtimeType: Type<DecodedValue, EncodedValue, InputValue>
|
||||
): RouteValidationFunction<DecodedValue> => (inputValue, { badRequest, ok }) =>
|
||||
pipe(
|
||||
runtimeType.decode(inputValue),
|
||||
fold<Errors, DecodedValue, ValdidationResult<DecodedValue>>(
|
||||
(errors: Errors) => badRequest(failure(errors).join('\n')),
|
||||
(result: DecodedValue) => ok(result)
|
||||
)
|
||||
);
|
||||
|
|
|
@ -4,5 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './input_fields';
|
||||
export { SourceConfigurationSettings } from './source_configuration_settings';
|
||||
export { ViewSourceConfigurationButton } from './view_source_configuration_button';
|
||||
|
|
|
@ -8,11 +8,11 @@ import createContainer from 'constate';
|
|||
import { isString } from 'lodash';
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { LogEntriesItem } from '../../../common/http_api';
|
||||
import { UrlStateContainer } from '../../utils/url_state';
|
||||
import { useTrackedPromise } from '../../utils/use_tracked_promise';
|
||||
import { Source } from '../source';
|
||||
import { fetchLogEntriesItem } from './log_entries/api/fetch_log_entries_item';
|
||||
import { LogEntriesItem } from '../../../common/http_api';
|
||||
import { useLogSourceContext } from './log_source';
|
||||
|
||||
export enum FlyoutVisibility {
|
||||
hidden = 'hidden',
|
||||
|
@ -26,7 +26,7 @@ export interface FlyoutOptionsUrlState {
|
|||
}
|
||||
|
||||
export const useLogFlyout = () => {
|
||||
const { sourceId } = useContext(Source.Context);
|
||||
const { sourceId } = useLogSourceContext();
|
||||
const [flyoutVisible, setFlyoutVisibility] = useState<boolean>(false);
|
||||
const [flyoutId, setFlyoutId] = useState<string | null>(null);
|
||||
const [flyoutItem, setFlyoutItem] = useState<LogEntriesItem | null>(null);
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 {
|
||||
getLogSourceConfigurationPath,
|
||||
getLogSourceConfigurationSuccessResponsePayloadRT,
|
||||
} from '../../../../../common/http_api/log_sources';
|
||||
import { decodeOrThrow } from '../../../../../common/runtime_types';
|
||||
import { npStart } from '../../../../legacy_singletons';
|
||||
|
||||
export const callFetchLogSourceConfigurationAPI = async (sourceId: string) => {
|
||||
const response = await npStart.http.fetch(getLogSourceConfigurationPath(sourceId), {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
return decodeOrThrow(getLogSourceConfigurationSuccessResponsePayloadRT)(response);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 {
|
||||
getLogSourceStatusPath,
|
||||
getLogSourceStatusSuccessResponsePayloadRT,
|
||||
} from '../../../../../common/http_api/log_sources';
|
||||
import { decodeOrThrow } from '../../../../../common/runtime_types';
|
||||
import { npStart } from '../../../../legacy_singletons';
|
||||
|
||||
export const callFetchLogSourceStatusAPI = async (sourceId: string) => {
|
||||
const response = await npStart.http.fetch(getLogSourceStatusPath(sourceId), {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
return decodeOrThrow(getLogSourceStatusSuccessResponsePayloadRT)(response);
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 {
|
||||
getLogSourceConfigurationPath,
|
||||
patchLogSourceConfigurationSuccessResponsePayloadRT,
|
||||
patchLogSourceConfigurationRequestBodyRT,
|
||||
LogSourceConfigurationPropertiesPatch,
|
||||
} from '../../../../../common/http_api/log_sources';
|
||||
import { decodeOrThrow } from '../../../../../common/runtime_types';
|
||||
import { npStart } from '../../../../legacy_singletons';
|
||||
|
||||
export const callPatchLogSourceConfigurationAPI = async (
|
||||
sourceId: string,
|
||||
patchedProperties: LogSourceConfigurationPropertiesPatch
|
||||
) => {
|
||||
const response = await npStart.http.fetch(getLogSourceConfigurationPath(sourceId), {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(
|
||||
patchLogSourceConfigurationRequestBodyRT.encode({
|
||||
data: patchedProperties,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
return decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response);
|
||||
};
|
|
@ -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 './log_source';
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* 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 createContainer from 'constate';
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
LogSourceConfiguration,
|
||||
LogSourceStatus,
|
||||
LogSourceConfigurationPropertiesPatch,
|
||||
LogSourceConfigurationProperties,
|
||||
} from '../../../../common/http_api/log_sources';
|
||||
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
|
||||
import { callFetchLogSourceConfigurationAPI } from './api/fetch_log_source_configuration';
|
||||
import { callFetchLogSourceStatusAPI } from './api/fetch_log_source_status';
|
||||
import { callPatchLogSourceConfigurationAPI } from './api/patch_log_source_configuration';
|
||||
|
||||
export {
|
||||
LogSourceConfiguration,
|
||||
LogSourceConfigurationProperties,
|
||||
LogSourceConfigurationPropertiesPatch,
|
||||
LogSourceStatus,
|
||||
};
|
||||
|
||||
export const useLogSource = ({ sourceId }: { sourceId: string }) => {
|
||||
const [sourceConfiguration, setSourceConfiguration] = useState<
|
||||
LogSourceConfiguration | undefined
|
||||
>(undefined);
|
||||
|
||||
const [sourceStatus, setSourceStatus] = useState<LogSourceStatus | undefined>(undefined);
|
||||
|
||||
const [loadSourceConfigurationRequest, loadSourceConfiguration] = useTrackedPromise(
|
||||
{
|
||||
cancelPreviousOn: 'resolution',
|
||||
createPromise: async () => {
|
||||
return await callFetchLogSourceConfigurationAPI(sourceId);
|
||||
},
|
||||
onResolve: ({ data }) => {
|
||||
setSourceConfiguration(data);
|
||||
},
|
||||
},
|
||||
[sourceId]
|
||||
);
|
||||
|
||||
const [updateSourceConfigurationRequest, updateSourceConfiguration] = useTrackedPromise(
|
||||
{
|
||||
cancelPreviousOn: 'resolution',
|
||||
createPromise: async (patchedProperties: LogSourceConfigurationPropertiesPatch) => {
|
||||
return await callPatchLogSourceConfigurationAPI(sourceId, patchedProperties);
|
||||
},
|
||||
onResolve: ({ data }) => {
|
||||
setSourceConfiguration(data);
|
||||
loadSourceStatus();
|
||||
},
|
||||
},
|
||||
[sourceId]
|
||||
);
|
||||
|
||||
const [loadSourceStatusRequest, loadSourceStatus] = useTrackedPromise(
|
||||
{
|
||||
cancelPreviousOn: 'resolution',
|
||||
createPromise: async () => {
|
||||
return await callFetchLogSourceStatusAPI(sourceId);
|
||||
},
|
||||
onResolve: ({ data }) => {
|
||||
setSourceStatus(data);
|
||||
},
|
||||
},
|
||||
[sourceId]
|
||||
);
|
||||
|
||||
const logIndicesExist = useMemo(() => (sourceStatus?.logIndexNames?.length ?? 0) > 0, [
|
||||
sourceStatus,
|
||||
]);
|
||||
|
||||
const derivedIndexPattern = useMemo(
|
||||
() => ({
|
||||
fields: sourceStatus?.logIndexFields ?? [],
|
||||
title: sourceConfiguration?.configuration.name ?? 'unknown',
|
||||
}),
|
||||
[sourceConfiguration, sourceStatus]
|
||||
);
|
||||
|
||||
const isLoadingSourceConfiguration = useMemo(
|
||||
() => loadSourceConfigurationRequest.state === 'pending',
|
||||
[loadSourceConfigurationRequest.state]
|
||||
);
|
||||
|
||||
const isUpdatingSourceConfiguration = useMemo(
|
||||
() => updateSourceConfigurationRequest.state === 'pending',
|
||||
[updateSourceConfigurationRequest.state]
|
||||
);
|
||||
|
||||
const isLoadingSourceStatus = useMemo(() => loadSourceStatusRequest.state === 'pending', [
|
||||
loadSourceStatusRequest.state,
|
||||
]);
|
||||
|
||||
const isLoading = useMemo(
|
||||
() => isLoadingSourceConfiguration || isLoadingSourceStatus || isUpdatingSourceConfiguration,
|
||||
[isLoadingSourceConfiguration, isLoadingSourceStatus, isUpdatingSourceConfiguration]
|
||||
);
|
||||
|
||||
const isUninitialized = useMemo(
|
||||
() =>
|
||||
loadSourceConfigurationRequest.state === 'uninitialized' ||
|
||||
loadSourceStatusRequest.state === 'uninitialized',
|
||||
[loadSourceConfigurationRequest.state, loadSourceStatusRequest.state]
|
||||
);
|
||||
|
||||
const hasFailedLoadingSource = useMemo(
|
||||
() => loadSourceConfigurationRequest.state === 'rejected',
|
||||
[loadSourceConfigurationRequest.state]
|
||||
);
|
||||
|
||||
const loadSourceFailureMessage = useMemo(
|
||||
() =>
|
||||
loadSourceConfigurationRequest.state === 'rejected'
|
||||
? `${loadSourceConfigurationRequest.value}`
|
||||
: undefined,
|
||||
[loadSourceConfigurationRequest]
|
||||
);
|
||||
|
||||
const loadSource = useCallback(() => {
|
||||
return Promise.all([loadSourceConfiguration(), loadSourceStatus()]);
|
||||
}, [loadSourceConfiguration, loadSourceStatus]);
|
||||
|
||||
const initialize = useCallback(async () => {
|
||||
if (!isUninitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await loadSource();
|
||||
}, [isUninitialized, loadSource]);
|
||||
|
||||
return {
|
||||
derivedIndexPattern,
|
||||
hasFailedLoadingSource,
|
||||
initialize,
|
||||
isLoading,
|
||||
isLoadingSourceConfiguration,
|
||||
isLoadingSourceStatus,
|
||||
isUninitialized,
|
||||
loadSource,
|
||||
loadSourceFailureMessage,
|
||||
loadSourceConfiguration,
|
||||
loadSourceStatus,
|
||||
logIndicesExist,
|
||||
sourceConfiguration,
|
||||
sourceId,
|
||||
sourceStatus,
|
||||
updateSourceConfiguration,
|
||||
};
|
||||
};
|
||||
|
||||
export const [LogSourceProvider, useLogSourceContext] = createContainer(useLogSource);
|
|
@ -8,10 +8,10 @@ import { useContext } from 'react';
|
|||
import { useThrottle } from 'react-use';
|
||||
|
||||
import { RendererFunction } from '../../../utils/typed_react';
|
||||
import { Source } from '../../source';
|
||||
import { LogSummaryBuckets, useLogSummary } from './log_summary';
|
||||
import { LogFilterState } from '../log_filter';
|
||||
import { LogPositionState } from '../log_position';
|
||||
import { useLogSourceContext } from '../log_source';
|
||||
|
||||
const FETCH_THROTTLE_INTERVAL = 3000;
|
||||
|
||||
|
@ -24,7 +24,7 @@ export const WithSummary = ({
|
|||
end: number | null;
|
||||
}>;
|
||||
}) => {
|
||||
const { sourceId } = useContext(Source.Context);
|
||||
const { sourceId } = useLogSourceContext();
|
||||
const { filterQuery } = useContext(LogFilterState.Context);
|
||||
const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context);
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { SourceErrorPage } from '../../../components/source_error_page';
|
||||
import { SourceLoadingPage } from '../../../components/source_loading_page';
|
||||
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
|
||||
import { useSourceContext } from '../../../containers/source';
|
||||
import { useLogSourceContext } from '../../../containers/logs/log_source';
|
||||
import { LogEntryCategoriesResultsContent } from './page_results_content';
|
||||
import { LogEntryCategoriesSetupContent } from './page_setup_content';
|
||||
import { useLogEntryCategoriesModuleContext } from './use_log_entry_categories_module';
|
||||
|
@ -25,11 +25,11 @@ import { useLogEntryCategoriesModuleContext } from './use_log_entry_categories_m
|
|||
export const LogEntryCategoriesPageContent = () => {
|
||||
const {
|
||||
hasFailedLoadingSource,
|
||||
isLoadingSource,
|
||||
isLoading,
|
||||
isUninitialized,
|
||||
loadSource,
|
||||
loadSourceFailureMessage,
|
||||
} = useSourceContext();
|
||||
} = useLogSourceContext();
|
||||
|
||||
const {
|
||||
hasLogAnalysisCapabilites,
|
||||
|
@ -45,7 +45,7 @@ export const LogEntryCategoriesPageContent = () => {
|
|||
}
|
||||
}, [fetchJobStatus, hasLogAnalysisReadCapabilities]);
|
||||
|
||||
if (isLoadingSource || isUninitialized) {
|
||||
if (isLoading || isUninitialized) {
|
||||
return <SourceLoadingPage />;
|
||||
} else if (hasFailedLoadingSource) {
|
||||
return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />;
|
||||
|
|
|
@ -6,20 +6,20 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { useSourceContext } from '../../../containers/source';
|
||||
import { useLogSourceContext } from '../../../containers/logs/log_source';
|
||||
import { useKibanaSpaceId } from '../../../utils/use_kibana_space_id';
|
||||
import { LogEntryCategoriesModuleProvider } from './use_log_entry_categories_module';
|
||||
|
||||
export const LogEntryCategoriesPageProviders: React.FunctionComponent = ({ children }) => {
|
||||
const { sourceId, source } = useSourceContext();
|
||||
const { sourceId, sourceConfiguration } = useLogSourceContext();
|
||||
const spaceId = useKibanaSpaceId();
|
||||
|
||||
return (
|
||||
<LogEntryCategoriesModuleProvider
|
||||
indexPattern={source ? source.configuration.logAlias : ''}
|
||||
indexPattern={sourceConfiguration?.configuration.logAlias ?? ''}
|
||||
sourceId={sourceId}
|
||||
spaceId={spaceId}
|
||||
timestampField={source ? source.configuration.fields.timestamp : ''}
|
||||
timestampField={sourceConfiguration?.configuration.fields.timestamp ?? ''}
|
||||
>
|
||||
{children}
|
||||
</LogEntryCategoriesModuleProvider>
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { SourceErrorPage } from '../../../components/source_error_page';
|
||||
import { SourceLoadingPage } from '../../../components/source_loading_page';
|
||||
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
|
||||
import { useSourceContext } from '../../../containers/source';
|
||||
import { useLogSourceContext } from '../../../containers/logs/log_source';
|
||||
import { LogEntryRateResultsContent } from './page_results_content';
|
||||
import { LogEntryRateSetupContent } from './page_setup_content';
|
||||
import { useLogEntryRateModuleContext } from './use_log_entry_rate_module';
|
||||
|
@ -25,11 +25,11 @@ import { useLogEntryRateModuleContext } from './use_log_entry_rate_module';
|
|||
export const LogEntryRatePageContent = () => {
|
||||
const {
|
||||
hasFailedLoadingSource,
|
||||
isLoadingSource,
|
||||
isLoading,
|
||||
isUninitialized,
|
||||
loadSource,
|
||||
loadSourceFailureMessage,
|
||||
} = useSourceContext();
|
||||
} = useLogSourceContext();
|
||||
|
||||
const {
|
||||
hasLogAnalysisCapabilites,
|
||||
|
@ -45,7 +45,7 @@ export const LogEntryRatePageContent = () => {
|
|||
}
|
||||
}, [fetchJobStatus, hasLogAnalysisReadCapabilities]);
|
||||
|
||||
if (isLoadingSource || isUninitialized) {
|
||||
if (isLoading || isUninitialized) {
|
||||
return <SourceLoadingPage />;
|
||||
} else if (hasFailedLoadingSource) {
|
||||
return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />;
|
||||
|
|
|
@ -6,20 +6,20 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { useSourceContext } from '../../../containers/source';
|
||||
import { useLogSourceContext } from '../../../containers/logs/log_source';
|
||||
import { useKibanaSpaceId } from '../../../utils/use_kibana_space_id';
|
||||
import { LogEntryRateModuleProvider } from './use_log_entry_rate_module';
|
||||
|
||||
export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => {
|
||||
const { sourceId, source } = useSourceContext();
|
||||
const { sourceId, sourceConfiguration } = useLogSourceContext();
|
||||
const spaceId = useKibanaSpaceId();
|
||||
|
||||
return (
|
||||
<LogEntryRateModuleProvider
|
||||
indexPattern={source ? source.configuration.logAlias : ''}
|
||||
indexPattern={sourceConfiguration?.configuration.logAlias ?? ''}
|
||||
sourceId={sourceId}
|
||||
spaceId={spaceId}
|
||||
timestampField={source ? source.configuration.fields.timestamp : ''}
|
||||
timestampField={sourceConfiguration?.configuration.fields.timestamp ?? ''}
|
||||
>
|
||||
{children}
|
||||
</LogEntryRateModuleProvider>
|
||||
|
|
|
@ -8,6 +8,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { useMount } from 'react-use';
|
||||
|
||||
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { DocumentTitle } from '../../components/document_title';
|
||||
|
@ -17,6 +18,7 @@ import { AppNavigation } from '../../components/navigation/app_navigation';
|
|||
import { RoutedTabs } from '../../components/navigation/routed_tabs';
|
||||
import { ColumnarPage } from '../../components/page';
|
||||
import { useLogAnalysisCapabilitiesContext } from '../../containers/logs/log_analysis';
|
||||
import { useLogSourceContext } from '../../containers/logs/log_source';
|
||||
import { RedirectWithQueryParams } from '../../utils/redirect_with_query_params';
|
||||
import { LogEntryCategoriesPage } from './log_entry_categories';
|
||||
import { LogEntryRatePage } from './log_entry_rate';
|
||||
|
@ -28,6 +30,12 @@ export const LogsPageContent: React.FunctionComponent = () => {
|
|||
const uiCapabilities = useKibana().services.application?.capabilities;
|
||||
const logAnalysisCapabilities = useLogAnalysisCapabilitiesContext();
|
||||
|
||||
const { initialize } = useLogSourceContext();
|
||||
|
||||
useMount(() => {
|
||||
initialize();
|
||||
});
|
||||
|
||||
const streamTab = {
|
||||
app: 'logs',
|
||||
title: streamTabTitle,
|
||||
|
|
|
@ -6,15 +6,16 @@
|
|||
|
||||
import React from 'react';
|
||||
import { LogAnalysisCapabilitiesProvider } from '../../containers/logs/log_analysis';
|
||||
import { SourceProvider } from '../../containers/source';
|
||||
import { LogSourceProvider } from '../../containers/logs/log_source';
|
||||
// import { SourceProvider } from '../../containers/source';
|
||||
import { useSourceId } from '../../containers/source_id';
|
||||
|
||||
export const LogsPageProviders: React.FunctionComponent = ({ children }) => {
|
||||
const [sourceId] = useSourceId();
|
||||
|
||||
return (
|
||||
<SourceProvider sourceId={sourceId}>
|
||||
<LogSourceProvider sourceId={sourceId}>
|
||||
<LogAnalysisCapabilitiesProvider>{children}</LogAnalysisCapabilitiesProvider>
|
||||
</SourceProvider>
|
||||
</LogSourceProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,19 +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 React from 'react';
|
||||
import { SourceConfigurationSettings } from '../../components/source_configuration/source_configuration_settings';
|
||||
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
export const LogsSettingsPage = () => {
|
||||
const uiCapabilities = useKibana().services.application?.capabilities;
|
||||
return (
|
||||
<SourceConfigurationSettings
|
||||
shouldAllowEdit={uiCapabilities?.logs?.configureSource as boolean}
|
||||
displaySettings="logs"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiBadge,
|
||||
EuiButton,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiSelectable,
|
||||
EuiSelectableOption,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { euiStyled } from '../../../../../observability/public';
|
||||
import { LogColumnConfiguration } from '../../../utils/source_configuration';
|
||||
import { useVisibilityState } from '../../../utils/use_visibility_state';
|
||||
|
||||
interface SelectableColumnOption {
|
||||
optionProps: EuiSelectableOption;
|
||||
columnConfiguration: LogColumnConfiguration;
|
||||
}
|
||||
|
||||
export const AddLogColumnButtonAndPopover: React.FunctionComponent<{
|
||||
addLogColumn: (logColumnConfiguration: LogColumnConfiguration) => void;
|
||||
availableFields: string[];
|
||||
isDisabled?: boolean;
|
||||
}> = ({ addLogColumn, availableFields, isDisabled }) => {
|
||||
const { isVisible: isOpen, show: openPopover, hide: closePopover } = useVisibilityState(false);
|
||||
|
||||
const availableColumnOptions = useMemo<SelectableColumnOption[]>(
|
||||
() => [
|
||||
{
|
||||
optionProps: {
|
||||
append: <SystemColumnBadge />,
|
||||
'data-test-subj': 'addTimestampLogColumn',
|
||||
// this key works around EuiSelectable using a lowercased label as
|
||||
// key, which leads to conflicts with field names
|
||||
key: 'timestamp',
|
||||
label: 'Timestamp',
|
||||
},
|
||||
columnConfiguration: {
|
||||
timestampColumn: {
|
||||
id: uuidv4(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
optionProps: {
|
||||
'data-test-subj': 'addMessageLogColumn',
|
||||
append: <SystemColumnBadge />,
|
||||
// this key works around EuiSelectable using a lowercased label as
|
||||
// key, which leads to conflicts with field names
|
||||
key: 'message',
|
||||
label: 'Message',
|
||||
},
|
||||
columnConfiguration: {
|
||||
messageColumn: {
|
||||
id: uuidv4(),
|
||||
},
|
||||
},
|
||||
},
|
||||
...availableFields.map<SelectableColumnOption>(field => ({
|
||||
optionProps: {
|
||||
'data-test-subj': `addFieldLogColumn addFieldLogColumn:${field}`,
|
||||
// this key works around EuiSelectable using a lowercased label as
|
||||
// key, which leads to conflicts with fields that only differ in the
|
||||
// case (e.g. the metricbeat mongodb module)
|
||||
key: `field-${field}`,
|
||||
label: field,
|
||||
},
|
||||
columnConfiguration: {
|
||||
fieldColumn: {
|
||||
id: uuidv4(),
|
||||
field,
|
||||
},
|
||||
},
|
||||
})),
|
||||
],
|
||||
[availableFields]
|
||||
);
|
||||
|
||||
const availableOptions = useMemo<EuiSelectableOption[]>(
|
||||
() => availableColumnOptions.map(availableColumnOption => availableColumnOption.optionProps),
|
||||
[availableColumnOptions]
|
||||
);
|
||||
|
||||
const handleColumnSelection = useCallback(
|
||||
(selectedOptions: EuiSelectableOption[]) => {
|
||||
closePopover();
|
||||
|
||||
const selectedOptionIndex = selectedOptions.findIndex(
|
||||
selectedOption => selectedOption.checked === 'on'
|
||||
);
|
||||
const selectedOption = availableColumnOptions[selectedOptionIndex];
|
||||
|
||||
addLogColumn(selectedOption.columnConfiguration);
|
||||
},
|
||||
[addLogColumn, availableColumnOptions, closePopover]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
anchorPosition="downRight"
|
||||
button={
|
||||
<EuiButton
|
||||
data-test-subj="addLogColumnButton"
|
||||
isDisabled={isDisabled}
|
||||
iconType="plusInCircle"
|
||||
onClick={openPopover}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.addLogColumnButtonLabel"
|
||||
defaultMessage="Add column"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
closePopover={closePopover}
|
||||
id="addLogColumn"
|
||||
isOpen={isOpen}
|
||||
ownFocus
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiSelectable
|
||||
height={600}
|
||||
listProps={selectableListProps}
|
||||
onChange={handleColumnSelection}
|
||||
options={availableOptions}
|
||||
searchable
|
||||
searchProps={searchProps}
|
||||
singleSelection
|
||||
>
|
||||
{(list, search) => (
|
||||
<SelectableContent data-test-subj="addLogColumnPopover">
|
||||
<EuiPopoverTitle>{search}</EuiPopoverTitle>
|
||||
{list}
|
||||
</SelectableContent>
|
||||
)}
|
||||
</EuiSelectable>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
||||
const searchProps = {
|
||||
'data-test-subj': 'fieldSearchInput',
|
||||
};
|
||||
|
||||
const selectableListProps = {
|
||||
showIcons: false,
|
||||
};
|
||||
|
||||
const SystemColumnBadge: React.FunctionComponent = () => (
|
||||
<EuiBadge>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.systemColumnBadgeLabel"
|
||||
defaultMessage="System"
|
||||
/>
|
||||
</EuiBadge>
|
||||
);
|
||||
|
||||
const SelectableContent = euiStyled.div`
|
||||
width: 400px;
|
||||
`;
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiCallOut,
|
||||
EuiCode,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFieldText,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { InputFieldProps } from '../../../components/source_configuration';
|
||||
|
||||
interface FieldsConfigurationPanelProps {
|
||||
isLoading: boolean;
|
||||
readOnly: boolean;
|
||||
tiebreakerFieldProps: InputFieldProps;
|
||||
timestampFieldProps: InputFieldProps;
|
||||
}
|
||||
|
||||
export const FieldsConfigurationPanel = ({
|
||||
isLoading,
|
||||
readOnly,
|
||||
tiebreakerFieldProps,
|
||||
timestampFieldProps,
|
||||
}: FieldsConfigurationPanelProps) => {
|
||||
const isTimestampValueDefault = timestampFieldProps.value === '@timestamp';
|
||||
const isTiebreakerValueDefault = tiebreakerFieldProps.value === '_doc';
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.fieldsSectionTitle"
|
||||
defaultMessage="Fields"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.infra.sourceConfiguration.deprecationNotice', {
|
||||
defaultMessage: 'Deprecation Notice',
|
||||
})}
|
||||
color="warning"
|
||||
iconType="help"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.deprecationMessage"
|
||||
defaultMessage="Configuring these fields have been deprecated and will be removed in 8.0.0. This application is designed to work with {ecsLink}, you should adjust your indexing to use the {documentationLink}."
|
||||
values={{
|
||||
documentationLink: (
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/guide/en/infrastructure/guide/7.4/infrastructure-metrics.html"
|
||||
target="BLANK"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.documentedFields"
|
||||
defaultMessage="documented fields"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
ecsLink: (
|
||||
<EuiLink
|
||||
href="https://www.elastic.co/guide/en/ecs/current/index.html"
|
||||
target="BLANK"
|
||||
>
|
||||
ECS
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.timestampFieldLabel"
|
||||
defaultMessage="Timestamp"
|
||||
/>
|
||||
</h4>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.timestampFieldDescription"
|
||||
defaultMessage="Timestamp used to sort log entries"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFormRow
|
||||
error={timestampFieldProps.error}
|
||||
fullWidth
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.timestampFieldRecommendedValue"
|
||||
defaultMessage="The recommended value is {defaultValue}"
|
||||
values={{
|
||||
defaultValue: <EuiCode>@timestamp</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
isInvalid={timestampFieldProps.isInvalid}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.timestampFieldLabel"
|
||||
defaultMessage="Timestamp"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
disabled={isLoading || isTimestampValueDefault}
|
||||
readOnly={readOnly}
|
||||
isLoading={isLoading}
|
||||
{...timestampFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.tiebreakerFieldLabel"
|
||||
defaultMessage="Tiebreaker"
|
||||
/>
|
||||
</h4>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.tiebreakerFieldDescription"
|
||||
defaultMessage="Field used to break ties between two entries with the same timestamp"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFormRow
|
||||
error={tiebreakerFieldProps.error}
|
||||
fullWidth
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.tiebreakerFieldRecommendedValue"
|
||||
defaultMessage="The recommended value is {defaultValue}"
|
||||
values={{
|
||||
defaultValue: <EuiCode>_doc</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
isInvalid={tiebreakerFieldProps.isInvalid}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.tiebreakerFieldLabel"
|
||||
defaultMessage="Tiebreaker"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
disabled={isLoading || isTiebreakerValueDefault}
|
||||
readOnly={readOnly}
|
||||
isLoading={isLoading}
|
||||
{...tiebreakerFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</EuiForm>
|
||||
);
|
||||
};
|
7
x-pack/plugins/infra/public/pages/logs/settings/index.ts
Normal file
7
x-pack/plugins/infra/public/pages/logs/settings/index.ts
Normal 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_configuration_settings';
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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 { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
createInputFieldProps,
|
||||
validateInputFieldNotEmpty,
|
||||
} from '../../../components/source_configuration/input_fields';
|
||||
|
||||
interface FormState {
|
||||
name: string;
|
||||
description: string;
|
||||
logAlias: string;
|
||||
tiebreakerField: string;
|
||||
timestampField: string;
|
||||
}
|
||||
|
||||
type FormStateChanges = Partial<FormState>;
|
||||
|
||||
export const useLogIndicesConfigurationFormState = ({
|
||||
initialFormState = defaultFormState,
|
||||
}: {
|
||||
initialFormState?: FormState;
|
||||
}) => {
|
||||
const [formStateChanges, setFormStateChanges] = useState<FormStateChanges>({});
|
||||
|
||||
const resetForm = useCallback(() => setFormStateChanges({}), []);
|
||||
|
||||
const formState = useMemo(
|
||||
() => ({
|
||||
...initialFormState,
|
||||
...formStateChanges,
|
||||
}),
|
||||
[initialFormState, formStateChanges]
|
||||
);
|
||||
|
||||
const nameFieldProps = useMemo(
|
||||
() =>
|
||||
createInputFieldProps({
|
||||
errors: validateInputFieldNotEmpty(formState.name),
|
||||
name: 'name',
|
||||
onChange: name => setFormStateChanges(changes => ({ ...changes, name })),
|
||||
value: formState.name,
|
||||
}),
|
||||
[formState.name]
|
||||
);
|
||||
const logAliasFieldProps = useMemo(
|
||||
() =>
|
||||
createInputFieldProps({
|
||||
errors: validateInputFieldNotEmpty(formState.logAlias),
|
||||
name: 'logAlias',
|
||||
onChange: logAlias => setFormStateChanges(changes => ({ ...changes, logAlias })),
|
||||
value: formState.logAlias,
|
||||
}),
|
||||
[formState.logAlias]
|
||||
);
|
||||
const tiebreakerFieldFieldProps = useMemo(
|
||||
() =>
|
||||
createInputFieldProps({
|
||||
errors: validateInputFieldNotEmpty(formState.tiebreakerField),
|
||||
name: `tiebreakerField`,
|
||||
onChange: tiebreakerField =>
|
||||
setFormStateChanges(changes => ({ ...changes, tiebreakerField })),
|
||||
value: formState.tiebreakerField,
|
||||
}),
|
||||
[formState.tiebreakerField]
|
||||
);
|
||||
const timestampFieldFieldProps = useMemo(
|
||||
() =>
|
||||
createInputFieldProps({
|
||||
errors: validateInputFieldNotEmpty(formState.timestampField),
|
||||
name: `timestampField`,
|
||||
onChange: timestampField =>
|
||||
setFormStateChanges(changes => ({ ...changes, timestampField })),
|
||||
value: formState.timestampField,
|
||||
}),
|
||||
[formState.timestampField]
|
||||
);
|
||||
|
||||
const fieldProps = useMemo(
|
||||
() => ({
|
||||
name: nameFieldProps,
|
||||
logAlias: logAliasFieldProps,
|
||||
tiebreakerField: tiebreakerFieldFieldProps,
|
||||
timestampField: timestampFieldFieldProps,
|
||||
}),
|
||||
[nameFieldProps, logAliasFieldProps, tiebreakerFieldFieldProps, timestampFieldFieldProps]
|
||||
);
|
||||
|
||||
const errors = useMemo(
|
||||
() =>
|
||||
Object.values(fieldProps).reduce<ReactNode[]>(
|
||||
(accumulatedErrors, { error }) => [...accumulatedErrors, ...error],
|
||||
[]
|
||||
),
|
||||
[fieldProps]
|
||||
);
|
||||
|
||||
const isFormValid = useMemo(() => errors.length <= 0, [errors]);
|
||||
|
||||
const isFormDirty = useMemo(() => Object.keys(formStateChanges).length > 0, [formStateChanges]);
|
||||
|
||||
return {
|
||||
errors,
|
||||
fieldProps,
|
||||
formState,
|
||||
formStateChanges,
|
||||
isFormDirty,
|
||||
isFormValid,
|
||||
resetForm,
|
||||
};
|
||||
};
|
||||
|
||||
const defaultFormState: FormState = {
|
||||
name: '',
|
||||
description: '',
|
||||
logAlias: '',
|
||||
tiebreakerField: '',
|
||||
timestampField: '',
|
||||
};
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiCode,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFieldText,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { InputFieldProps } from '../../../components/source_configuration';
|
||||
|
||||
interface IndicesConfigurationPanelProps {
|
||||
isLoading: boolean;
|
||||
readOnly: boolean;
|
||||
logAliasFieldProps: InputFieldProps;
|
||||
}
|
||||
|
||||
export const IndicesConfigurationPanel = ({
|
||||
isLoading,
|
||||
readOnly,
|
||||
logAliasFieldProps,
|
||||
}: IndicesConfigurationPanelProps) => (
|
||||
<EuiForm>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.indicesSectionTitle"
|
||||
defaultMessage="Indices"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.logIndicesTitle"
|
||||
defaultMessage="Log indices"
|
||||
/>
|
||||
</h4>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.logIndicesDescription"
|
||||
defaultMessage="Index pattern for matching indices that contain log data"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFormRow
|
||||
error={logAliasFieldProps.error}
|
||||
fullWidth
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.logIndicesRecommendedValue"
|
||||
defaultMessage="The recommended value is {defaultValue}"
|
||||
values={{
|
||||
defaultValue: <EuiCode>filebeat-*</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
isInvalid={logAliasFieldProps.isInvalid}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.logIndicesLabel"
|
||||
defaultMessage="Log indices"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="logIndicesInput"
|
||||
fullWidth
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
readOnly={readOnly}
|
||||
{...logAliasFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</EuiForm>
|
||||
);
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
FieldLogColumnConfiguration,
|
||||
isMessageLogColumnConfiguration,
|
||||
isTimestampLogColumnConfiguration,
|
||||
LogColumnConfiguration,
|
||||
MessageLogColumnConfiguration,
|
||||
TimestampLogColumnConfiguration,
|
||||
} from '../../../utils/source_configuration';
|
||||
|
||||
export interface TimestampLogColumnConfigurationProps {
|
||||
logColumnConfiguration: TimestampLogColumnConfiguration['timestampColumn'];
|
||||
remove: () => void;
|
||||
type: 'timestamp';
|
||||
}
|
||||
|
||||
export interface MessageLogColumnConfigurationProps {
|
||||
logColumnConfiguration: MessageLogColumnConfiguration['messageColumn'];
|
||||
remove: () => void;
|
||||
type: 'message';
|
||||
}
|
||||
|
||||
export interface FieldLogColumnConfigurationProps {
|
||||
logColumnConfiguration: FieldLogColumnConfiguration['fieldColumn'];
|
||||
remove: () => void;
|
||||
type: 'field';
|
||||
}
|
||||
|
||||
export type LogColumnConfigurationProps =
|
||||
| TimestampLogColumnConfigurationProps
|
||||
| MessageLogColumnConfigurationProps
|
||||
| FieldLogColumnConfigurationProps;
|
||||
|
||||
interface FormState {
|
||||
logColumns: LogColumnConfiguration[];
|
||||
}
|
||||
|
||||
type FormStateChanges = Partial<FormState>;
|
||||
|
||||
export const useLogColumnsConfigurationFormState = ({
|
||||
initialFormState = defaultFormState,
|
||||
}: {
|
||||
initialFormState?: FormState;
|
||||
}) => {
|
||||
const [formStateChanges, setFormStateChanges] = useState<FormStateChanges>({});
|
||||
|
||||
const resetForm = useCallback(() => setFormStateChanges({}), []);
|
||||
|
||||
const formState = useMemo(
|
||||
() => ({
|
||||
...initialFormState,
|
||||
...formStateChanges,
|
||||
}),
|
||||
[initialFormState, formStateChanges]
|
||||
);
|
||||
|
||||
const logColumnConfigurationProps = useMemo<LogColumnConfigurationProps[]>(
|
||||
() =>
|
||||
formState.logColumns.map(
|
||||
(logColumn): LogColumnConfigurationProps => {
|
||||
const remove = () =>
|
||||
setFormStateChanges(changes => ({
|
||||
...changes,
|
||||
logColumns: formState.logColumns.filter(item => item !== logColumn),
|
||||
}));
|
||||
|
||||
if (isTimestampLogColumnConfiguration(logColumn)) {
|
||||
return {
|
||||
logColumnConfiguration: logColumn.timestampColumn,
|
||||
remove,
|
||||
type: 'timestamp',
|
||||
};
|
||||
} else if (isMessageLogColumnConfiguration(logColumn)) {
|
||||
return {
|
||||
logColumnConfiguration: logColumn.messageColumn,
|
||||
remove,
|
||||
type: 'message',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
logColumnConfiguration: logColumn.fieldColumn,
|
||||
remove,
|
||||
type: 'field',
|
||||
};
|
||||
}
|
||||
}
|
||||
),
|
||||
[formState.logColumns]
|
||||
);
|
||||
|
||||
const addLogColumn = useCallback(
|
||||
(logColumnConfiguration: LogColumnConfiguration) =>
|
||||
setFormStateChanges(changes => ({
|
||||
...changes,
|
||||
logColumns: [...formState.logColumns, logColumnConfiguration],
|
||||
})),
|
||||
[formState.logColumns]
|
||||
);
|
||||
|
||||
const moveLogColumn = useCallback(
|
||||
(sourceIndex, destinationIndex) => {
|
||||
if (destinationIndex >= 0 && sourceIndex <= formState.logColumns.length - 1) {
|
||||
const newLogColumns = [...formState.logColumns];
|
||||
newLogColumns.splice(destinationIndex, 0, newLogColumns.splice(sourceIndex, 1)[0]);
|
||||
setFormStateChanges(changes => ({
|
||||
...changes,
|
||||
logColumns: newLogColumns,
|
||||
}));
|
||||
}
|
||||
},
|
||||
[formState.logColumns]
|
||||
);
|
||||
|
||||
const errors = useMemo(
|
||||
() =>
|
||||
logColumnConfigurationProps.length <= 0
|
||||
? [
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.logColumnListEmptyErrorMessage"
|
||||
defaultMessage="The log column list must not be empty."
|
||||
/>,
|
||||
]
|
||||
: [],
|
||||
[logColumnConfigurationProps]
|
||||
);
|
||||
|
||||
const isFormValid = useMemo(() => (errors.length <= 0 ? true : false), [errors]);
|
||||
|
||||
const isFormDirty = useMemo(() => Object.keys(formStateChanges).length > 0, [formStateChanges]);
|
||||
|
||||
return {
|
||||
addLogColumn,
|
||||
moveLogColumn,
|
||||
errors,
|
||||
logColumnConfigurationProps,
|
||||
formState,
|
||||
formStateChanges,
|
||||
isFormDirty,
|
||||
isFormValid,
|
||||
resetForm,
|
||||
};
|
||||
};
|
||||
|
||||
const defaultFormState: FormState = {
|
||||
logColumns: [],
|
||||
};
|
|
@ -0,0 +1,276 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonIcon,
|
||||
EuiDragDropContext,
|
||||
EuiDraggable,
|
||||
EuiDroppable,
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiIcon,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { DragHandleProps, DropResult } from '../../../../../observability/public';
|
||||
import { LogColumnConfiguration } from '../../../utils/source_configuration';
|
||||
import { AddLogColumnButtonAndPopover } from './add_log_column_popover';
|
||||
import {
|
||||
FieldLogColumnConfigurationProps,
|
||||
LogColumnConfigurationProps,
|
||||
} from './log_columns_configuration_form_state';
|
||||
|
||||
interface LogColumnsConfigurationPanelProps {
|
||||
availableFields: string[];
|
||||
isLoading: boolean;
|
||||
logColumnConfiguration: LogColumnConfigurationProps[];
|
||||
addLogColumn: (logColumn: LogColumnConfiguration) => void;
|
||||
moveLogColumn: (sourceIndex: number, destinationIndex: number) => void;
|
||||
}
|
||||
|
||||
export const LogColumnsConfigurationPanel: React.FunctionComponent<LogColumnsConfigurationPanelProps> = ({
|
||||
addLogColumn,
|
||||
moveLogColumn,
|
||||
availableFields,
|
||||
isLoading,
|
||||
logColumnConfiguration,
|
||||
}) => {
|
||||
const onDragEnd = useCallback(
|
||||
({ source, destination }: DropResult) =>
|
||||
destination && moveLogColumn(source.index, destination.index),
|
||||
[moveLogColumn]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s" data-test-subj="sourceConfigurationLogColumnsSectionTitle">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.logColumnsSectionTitle"
|
||||
defaultMessage="Log Columns"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AddLogColumnButtonAndPopover
|
||||
addLogColumn={addLogColumn}
|
||||
availableFields={availableFields}
|
||||
isDisabled={isLoading}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{logColumnConfiguration.length > 0 ? (
|
||||
<EuiDragDropContext onDragEnd={onDragEnd}>
|
||||
<EuiDroppable droppableId="COLUMN_CONFIG_DROPPABLE_AREA">
|
||||
<>
|
||||
{/* Fragment here necessary for typechecking */}
|
||||
{logColumnConfiguration.map((column, index) => (
|
||||
<EuiDraggable
|
||||
key={`logColumnConfigurationPanel-${column.logColumnConfiguration.id}`}
|
||||
index={index}
|
||||
draggableId={column.logColumnConfiguration.id}
|
||||
customDragHandle
|
||||
>
|
||||
{provided => (
|
||||
<LogColumnConfigurationPanel
|
||||
dragHandleProps={provided.dragHandleProps}
|
||||
logColumnConfigurationProps={column}
|
||||
/>
|
||||
)}
|
||||
</EuiDraggable>
|
||||
))}
|
||||
</>
|
||||
</EuiDroppable>
|
||||
</EuiDragDropContext>
|
||||
) : (
|
||||
<LogColumnConfigurationEmptyPrompt />
|
||||
)}
|
||||
</EuiForm>
|
||||
);
|
||||
};
|
||||
|
||||
interface LogColumnConfigurationPanelProps {
|
||||
logColumnConfigurationProps: LogColumnConfigurationProps;
|
||||
dragHandleProps: DragHandleProps;
|
||||
}
|
||||
|
||||
const LogColumnConfigurationPanel: React.FunctionComponent<LogColumnConfigurationPanelProps> = props => (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
{props.logColumnConfigurationProps.type === 'timestamp' ? (
|
||||
<TimestampLogColumnConfigurationPanel {...props} />
|
||||
) : props.logColumnConfigurationProps.type === 'message' ? (
|
||||
<MessageLogColumnConfigurationPanel {...props} />
|
||||
) : (
|
||||
<FieldLogColumnConfigurationPanel
|
||||
logColumnConfigurationProps={props.logColumnConfigurationProps}
|
||||
dragHandleProps={props.dragHandleProps}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const TimestampLogColumnConfigurationPanel: React.FunctionComponent<LogColumnConfigurationPanelProps> = ({
|
||||
logColumnConfigurationProps,
|
||||
dragHandleProps,
|
||||
}) => (
|
||||
<ExplainedLogColumnConfigurationPanel
|
||||
fieldName="Timestamp"
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
tagName="span"
|
||||
id="xpack.infra.sourceConfiguration.timestampLogColumnDescription"
|
||||
defaultMessage="This system field shows the log entry's time as determined by the {timestampSetting} field setting."
|
||||
values={{
|
||||
timestampSetting: <code>timestamp</code>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
removeColumn={logColumnConfigurationProps.remove}
|
||||
dragHandleProps={dragHandleProps}
|
||||
/>
|
||||
);
|
||||
|
||||
const MessageLogColumnConfigurationPanel: React.FunctionComponent<LogColumnConfigurationPanelProps> = ({
|
||||
logColumnConfigurationProps,
|
||||
dragHandleProps,
|
||||
}) => (
|
||||
<ExplainedLogColumnConfigurationPanel
|
||||
fieldName="Message"
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
tagName="span"
|
||||
id="xpack.infra.sourceConfiguration.messageLogColumnDescription"
|
||||
defaultMessage="This system field shows the log entry message as derived from the document fields."
|
||||
/>
|
||||
}
|
||||
removeColumn={logColumnConfigurationProps.remove}
|
||||
dragHandleProps={dragHandleProps}
|
||||
/>
|
||||
);
|
||||
|
||||
const FieldLogColumnConfigurationPanel: React.FunctionComponent<{
|
||||
logColumnConfigurationProps: FieldLogColumnConfigurationProps;
|
||||
dragHandleProps: DragHandleProps;
|
||||
}> = ({
|
||||
logColumnConfigurationProps: {
|
||||
logColumnConfiguration: { field },
|
||||
remove,
|
||||
},
|
||||
dragHandleProps,
|
||||
}) => {
|
||||
const fieldLogColumnTitle = i18n.translate(
|
||||
'xpack.infra.sourceConfiguration.fieldLogColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Field',
|
||||
}
|
||||
);
|
||||
return (
|
||||
<EuiPanel data-test-subj={`logColumnPanel fieldLogColumnPanel fieldLogColumnPanel:${field}`}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div data-test-subj="moveLogColumnHandle" {...dragHandleProps}>
|
||||
<EuiIcon type="grab" />
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={1}>{fieldLogColumnTitle}</EuiFlexItem>
|
||||
<EuiFlexItem grow={3}>
|
||||
<code>{field}</code>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RemoveLogColumnButton
|
||||
onClick={remove}
|
||||
columnDescription={`${fieldLogColumnTitle} - ${field}`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{
|
||||
fieldName: React.ReactNode;
|
||||
helpText: React.ReactNode;
|
||||
removeColumn: () => void;
|
||||
dragHandleProps: DragHandleProps;
|
||||
}> = ({ fieldName, helpText, removeColumn, dragHandleProps }) => (
|
||||
<EuiPanel
|
||||
data-test-subj={`logColumnPanel systemLogColumnPanel systemLogColumnPanel:${fieldName}`}
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div data-test-subj="moveLogColumnHandle" {...dragHandleProps}>
|
||||
<EuiIcon type="grab" />
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={1}>{fieldName}</EuiFlexItem>
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiText size="s" color="subdued">
|
||||
{helpText}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RemoveLogColumnButton onClick={removeColumn} columnDescription={String(fieldName)} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
|
||||
const RemoveLogColumnButton: React.FunctionComponent<{
|
||||
onClick?: () => void;
|
||||
columnDescription: string;
|
||||
}> = ({ onClick, columnDescription }) => {
|
||||
const removeColumnLabel = i18n.translate(
|
||||
'xpack.infra.sourceConfiguration.removeLogColumnButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Remove {columnDescription} column',
|
||||
values: { columnDescription },
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
color="danger"
|
||||
data-test-subj="removeLogColumnButton"
|
||||
iconType="trash"
|
||||
onClick={onClick}
|
||||
title={removeColumnLabel}
|
||||
aria-label={removeColumnLabel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const LogColumnConfigurationEmptyPrompt: React.FunctionComponent = () => (
|
||||
<EuiEmptyPrompt
|
||||
iconType="list"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.noLogColumnsTitle"
|
||||
defaultMessage="No columns"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.noLogColumnsDescription"
|
||||
defaultMessage="Add a column to this list using the button above."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
);
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 { useCallback, useMemo } from 'react';
|
||||
import { LogSourceConfigurationProperties } from '../../../containers/logs/log_source';
|
||||
import { useLogIndicesConfigurationFormState } from './indices_configuration_form_state';
|
||||
import { useLogColumnsConfigurationFormState } from './log_columns_configuration_form_state';
|
||||
|
||||
export const useLogSourceConfigurationFormState = (
|
||||
configuration?: LogSourceConfigurationProperties
|
||||
) => {
|
||||
const indicesConfigurationFormState = useLogIndicesConfigurationFormState({
|
||||
initialFormState: useMemo(
|
||||
() =>
|
||||
configuration
|
||||
? {
|
||||
name: configuration.name,
|
||||
description: configuration.description,
|
||||
logAlias: configuration.logAlias,
|
||||
tiebreakerField: configuration.fields.tiebreaker,
|
||||
timestampField: configuration.fields.timestamp,
|
||||
}
|
||||
: undefined,
|
||||
[configuration]
|
||||
),
|
||||
});
|
||||
|
||||
const logColumnsConfigurationFormState = useLogColumnsConfigurationFormState({
|
||||
initialFormState: useMemo(
|
||||
() =>
|
||||
configuration
|
||||
? {
|
||||
logColumns: configuration.logColumns,
|
||||
}
|
||||
: undefined,
|
||||
[configuration]
|
||||
),
|
||||
});
|
||||
|
||||
const errors = useMemo(
|
||||
() => [...indicesConfigurationFormState.errors, ...logColumnsConfigurationFormState.errors],
|
||||
[indicesConfigurationFormState.errors, logColumnsConfigurationFormState.errors]
|
||||
);
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
indicesConfigurationFormState.resetForm();
|
||||
logColumnsConfigurationFormState.resetForm();
|
||||
}, [indicesConfigurationFormState, logColumnsConfigurationFormState]);
|
||||
|
||||
const isFormDirty = useMemo(
|
||||
() => indicesConfigurationFormState.isFormDirty || logColumnsConfigurationFormState.isFormDirty,
|
||||
[indicesConfigurationFormState.isFormDirty, logColumnsConfigurationFormState.isFormDirty]
|
||||
);
|
||||
|
||||
const isFormValid = useMemo(
|
||||
() => indicesConfigurationFormState.isFormValid && logColumnsConfigurationFormState.isFormValid,
|
||||
[indicesConfigurationFormState.isFormValid, logColumnsConfigurationFormState.isFormValid]
|
||||
);
|
||||
|
||||
const formState = useMemo(
|
||||
() => ({
|
||||
name: indicesConfigurationFormState.formState.name,
|
||||
description: indicesConfigurationFormState.formState.description,
|
||||
logAlias: indicesConfigurationFormState.formState.logAlias,
|
||||
fields: {
|
||||
tiebreaker: indicesConfigurationFormState.formState.tiebreakerField,
|
||||
timestamp: indicesConfigurationFormState.formState.timestampField,
|
||||
},
|
||||
logColumns: logColumnsConfigurationFormState.formState.logColumns,
|
||||
}),
|
||||
[indicesConfigurationFormState.formState, logColumnsConfigurationFormState.formState]
|
||||
);
|
||||
|
||||
const formStateChanges = useMemo(
|
||||
() => ({
|
||||
name: indicesConfigurationFormState.formStateChanges.name,
|
||||
description: indicesConfigurationFormState.formStateChanges.description,
|
||||
logAlias: indicesConfigurationFormState.formStateChanges.logAlias,
|
||||
fields: {
|
||||
tiebreaker: indicesConfigurationFormState.formStateChanges.tiebreakerField,
|
||||
timestamp: indicesConfigurationFormState.formStateChanges.timestampField,
|
||||
},
|
||||
logColumns: logColumnsConfigurationFormState.formStateChanges.logColumns,
|
||||
}),
|
||||
[
|
||||
indicesConfigurationFormState.formStateChanges,
|
||||
logColumnsConfigurationFormState.formStateChanges,
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
addLogColumn: logColumnsConfigurationFormState.addLogColumn,
|
||||
moveLogColumn: logColumnsConfigurationFormState.moveLogColumn,
|
||||
errors,
|
||||
formState,
|
||||
formStateChanges,
|
||||
isFormDirty,
|
||||
isFormValid,
|
||||
indicesConfigurationProps: indicesConfigurationFormState.fieldProps,
|
||||
logColumnConfigurationProps: logColumnsConfigurationFormState.logColumnConfigurationProps,
|
||||
resetForm,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { FieldsConfigurationPanel } from './fields_configuration_panel';
|
||||
import { IndicesConfigurationPanel } from './indices_configuration_panel';
|
||||
import { NameConfigurationPanel } from '../../../components/source_configuration/name_configuration_panel';
|
||||
import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel';
|
||||
import { useLogSourceConfigurationFormState } from './source_configuration_form_state';
|
||||
import { useLogSourceContext } from '../../../containers/logs/log_source';
|
||||
import { SourceLoadingPage } from '../../../components/source_loading_page';
|
||||
import { Prompt } from '../../../utils/navigation_warning_prompt';
|
||||
|
||||
export const LogsSettingsPage = () => {
|
||||
const uiCapabilities = useKibana().services.application?.capabilities;
|
||||
const shouldAllowEdit = uiCapabilities?.logs?.configureSource === true;
|
||||
|
||||
const {
|
||||
sourceConfiguration: source,
|
||||
sourceStatus,
|
||||
isLoading,
|
||||
isUninitialized,
|
||||
updateSourceConfiguration,
|
||||
} = useLogSourceContext();
|
||||
|
||||
const availableFields = useMemo(
|
||||
() => sourceStatus?.logIndexFields.map(field => field.name) ?? [],
|
||||
[sourceStatus]
|
||||
);
|
||||
|
||||
const {
|
||||
addLogColumn,
|
||||
moveLogColumn,
|
||||
indicesConfigurationProps,
|
||||
logColumnConfigurationProps,
|
||||
errors,
|
||||
resetForm,
|
||||
isFormDirty,
|
||||
isFormValid,
|
||||
formStateChanges,
|
||||
} = useLogSourceConfigurationFormState(source?.configuration);
|
||||
|
||||
const persistUpdates = useCallback(async () => {
|
||||
await updateSourceConfiguration(formStateChanges);
|
||||
resetForm();
|
||||
}, [updateSourceConfiguration, resetForm, formStateChanges]);
|
||||
|
||||
const isWriteable = useMemo(() => shouldAllowEdit && source && source.origin !== 'internal', [
|
||||
shouldAllowEdit,
|
||||
source,
|
||||
]);
|
||||
|
||||
if ((isLoading || isUninitialized) && !source) {
|
||||
return <SourceLoadingPage />;
|
||||
}
|
||||
if (!source?.configuration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPage>
|
||||
<EuiPageBody
|
||||
className="eui-displayBlock"
|
||||
restrictWidth
|
||||
data-test-subj="sourceConfigurationContent"
|
||||
>
|
||||
<Prompt prompt={isFormDirty ? unsavedFormPromptMessage : undefined} />
|
||||
<EuiPanel paddingSize="l">
|
||||
<NameConfigurationPanel
|
||||
isLoading={isLoading}
|
||||
nameFieldProps={indicesConfigurationProps.name}
|
||||
readOnly={!isWriteable}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer />
|
||||
<EuiPanel paddingSize="l">
|
||||
<IndicesConfigurationPanel
|
||||
isLoading={isLoading}
|
||||
logAliasFieldProps={indicesConfigurationProps.logAlias}
|
||||
readOnly={!isWriteable}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer />
|
||||
<EuiPanel paddingSize="l">
|
||||
<FieldsConfigurationPanel
|
||||
isLoading={isLoading}
|
||||
readOnly={!isWriteable}
|
||||
tiebreakerFieldProps={indicesConfigurationProps.tiebreakerField}
|
||||
timestampFieldProps={indicesConfigurationProps.timestampField}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer />
|
||||
<EuiPanel paddingSize="l">
|
||||
<LogColumnsConfigurationPanel
|
||||
addLogColumn={addLogColumn}
|
||||
moveLogColumn={moveLogColumn}
|
||||
availableFields={availableFields}
|
||||
isLoading={isLoading}
|
||||
logColumnConfiguration={logColumnConfigurationProps}
|
||||
/>
|
||||
</EuiPanel>
|
||||
{errors.length > 0 ? (
|
||||
<>
|
||||
<EuiCallOut color="danger">
|
||||
<ul>
|
||||
{errors.map((error, errorIndex) => (
|
||||
<li key={errorIndex}>{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
) : null}
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
{isWriteable && (
|
||||
<EuiFlexItem>
|
||||
{isLoading ? (
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton color="primary" isLoading fill>
|
||||
Loading
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="discardSettingsButton"
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
isDisabled={isLoading || !isFormDirty}
|
||||
onClick={() => {
|
||||
resetForm();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.discardSettingsButtonLabel"
|
||||
defaultMessage="Discard"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="applySettingsButton"
|
||||
color="primary"
|
||||
isDisabled={!isFormDirty || !isFormValid}
|
||||
fill
|
||||
onClick={persistUpdates}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.applySettingsButtonLabel"
|
||||
defaultMessage="Apply"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const unsavedFormPromptMessage = i18n.translate(
|
||||
'xpack.infra.logSourceConfiguration.unsavedFormPromptMessage',
|
||||
{
|
||||
defaultMessage: 'Are you sure you want to leave? Changes will be lost',
|
||||
}
|
||||
);
|
|
@ -5,13 +5,11 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useTrackPageview } from '../../../../../observability/public';
|
||||
import { ColumnarPage } from '../../../components/page';
|
||||
import { StreamPageContent } from './page_content';
|
||||
import { StreamPageHeader } from './page_header';
|
||||
import { LogsPageProviders } from './page_providers';
|
||||
import { PageViewLogInContext } from './page_view_log_in_context';
|
||||
import { useTrackPageview } from '../../../../../observability/public';
|
||||
|
||||
export const StreamPage = () => {
|
||||
useTrackPageview({ app: 'infra_logs', path: 'stream' });
|
||||
|
@ -22,7 +20,6 @@ export const StreamPage = () => {
|
|||
<StreamPageHeader />
|
||||
<StreamPageContent />
|
||||
</ColumnarPage>
|
||||
<PageViewLogInContext />
|
||||
</LogsPageProviders>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,21 +7,21 @@
|
|||
import React from 'react';
|
||||
import { SourceErrorPage } from '../../../components/source_error_page';
|
||||
import { SourceLoadingPage } from '../../../components/source_loading_page';
|
||||
import { useSourceContext } from '../../../containers/source';
|
||||
import { LogsPageLogsContent } from './page_logs_content';
|
||||
import { LogsPageNoIndicesContent } from './page_no_indices_content';
|
||||
import { useLogSourceContext } from '../../../containers/logs/log_source';
|
||||
|
||||
export const StreamPageContent: React.FunctionComponent = () => {
|
||||
const {
|
||||
hasFailedLoadingSource,
|
||||
isLoadingSource,
|
||||
isLoading,
|
||||
isUninitialized,
|
||||
loadSource,
|
||||
loadSourceFailureMessage,
|
||||
logIndicesExist,
|
||||
} = useSourceContext();
|
||||
} = useLogSourceContext();
|
||||
|
||||
if (isLoadingSource || isUninitialized) {
|
||||
if (isLoading || isUninitialized) {
|
||||
return <SourceLoadingPage />;
|
||||
} else if (hasFailedLoadingSource) {
|
||||
return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />;
|
||||
|
|
|
@ -5,32 +5,30 @@
|
|||
*/
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { euiStyled } from '../../../../../observability/public';
|
||||
import { AutoSizer } from '../../../components/auto_sizer';
|
||||
import { LogEntryFlyout } from '../../../components/logging/log_entry_flyout';
|
||||
import { LogMinimap } from '../../../components/logging/log_minimap';
|
||||
import { ScrollableLogTextStreamView } from '../../../components/logging/log_text_stream';
|
||||
import { PageContent } from '../../../components/page';
|
||||
|
||||
import { WithSummary } from '../../../containers/logs/log_summary';
|
||||
import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration';
|
||||
import { LogFilterState } from '../../../containers/logs/log_filter';
|
||||
import {
|
||||
LogFlyout as LogFlyoutState,
|
||||
WithFlyoutOptionsUrlState,
|
||||
} from '../../../containers/logs/log_flyout';
|
||||
import { LogHighlightsState } from '../../../containers/logs/log_highlights';
|
||||
import { LogPositionState } from '../../../containers/logs/log_position';
|
||||
import { useLogSourceContext } from '../../../containers/logs/log_source';
|
||||
import { WithSummary } from '../../../containers/logs/log_summary';
|
||||
import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration';
|
||||
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
|
||||
import { WithLogTextviewUrlState } from '../../../containers/logs/with_log_textview';
|
||||
import { WithStreamItems } from '../../../containers/logs/with_stream_items';
|
||||
import { Source } from '../../../containers/source';
|
||||
|
||||
import { LogsToolbar } from './page_toolbar';
|
||||
import { LogHighlightsState } from '../../../containers/logs/log_highlights';
|
||||
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
|
||||
import { PageViewLogInContext } from './page_view_log_in_context';
|
||||
|
||||
export const LogsPageLogsContent: React.FunctionComponent = () => {
|
||||
const { source, sourceId, version } = useContext(Source.Context);
|
||||
const { sourceConfiguration, sourceId } = useLogSourceContext();
|
||||
const { textScale, textWrap } = useContext(LogViewConfiguration.Context);
|
||||
const {
|
||||
setFlyoutVisibility,
|
||||
|
@ -64,6 +62,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
|
|||
<WithLogTextviewUrlState />
|
||||
<WithFlyoutOptionsUrlState />
|
||||
<LogsToolbar />
|
||||
<PageViewLogInContext />
|
||||
{flyoutVisible ? (
|
||||
<LogEntryFlyout
|
||||
setFilter={applyLogFilterQuery}
|
||||
|
@ -77,7 +76,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
|
|||
loading={isLoading}
|
||||
/>
|
||||
) : null}
|
||||
<PageContent key={`${sourceId}-${version}`}>
|
||||
<PageContent key={`${sourceId}-${sourceConfiguration?.version}`}>
|
||||
<WithStreamItems>
|
||||
{({
|
||||
currentHighlightKey,
|
||||
|
@ -91,7 +90,9 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
|
|||
checkForNewEntries,
|
||||
}) => (
|
||||
<ScrollableLogTextStreamView
|
||||
columnConfigurations={(source && source.configuration.logColumns) || []}
|
||||
columnConfigurations={
|
||||
(sourceConfiguration && sourceConfiguration.configuration.logColumns) || []
|
||||
}
|
||||
hasMoreAfterEnd={hasMoreAfterEnd}
|
||||
hasMoreBeforeStart={hasMoreBeforeStart}
|
||||
isLoadingMore={isLoadingMore}
|
||||
|
|
|
@ -12,13 +12,11 @@ import { LogHighlightsState } from '../../../containers/logs/log_highlights/log_
|
|||
import { LogPositionState, WithLogPositionUrlState } from '../../../containers/logs/log_position';
|
||||
import { LogFilterState, WithLogFilterUrlState } from '../../../containers/logs/log_filter';
|
||||
import { LogEntriesState } from '../../../containers/logs/log_entries';
|
||||
|
||||
import { Source } from '../../../containers/source';
|
||||
import { useLogSourceContext } from '../../../containers/logs/log_source';
|
||||
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
|
||||
|
||||
const LogFilterStateProvider: React.FC = ({ children }) => {
|
||||
const { createDerivedIndexPattern } = useContext(Source.Context);
|
||||
const derivedIndexPattern = createDerivedIndexPattern('logs');
|
||||
const { derivedIndexPattern } = useLogSourceContext();
|
||||
return (
|
||||
<LogFilterState.Provider indexPattern={derivedIndexPattern}>
|
||||
<WithLogFilterUrlState />
|
||||
|
@ -29,7 +27,7 @@ const LogFilterStateProvider: React.FC = ({ children }) => {
|
|||
|
||||
const ViewLogInContextProvider: React.FC = ({ children }) => {
|
||||
const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context);
|
||||
const { sourceId } = useContext(Source.Context);
|
||||
const { sourceId } = useLogSourceContext();
|
||||
|
||||
if (!startTimestamp || !endTimestamp) {
|
||||
return null;
|
||||
|
@ -47,7 +45,7 @@ const ViewLogInContextProvider: React.FC = ({ children }) => {
|
|||
};
|
||||
|
||||
const LogEntriesStateProvider: React.FC = ({ children }) => {
|
||||
const { sourceId } = useContext(Source.Context);
|
||||
const { sourceId } = useLogSourceContext();
|
||||
const {
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
|
@ -89,13 +87,13 @@ const LogEntriesStateProvider: React.FC = ({ children }) => {
|
|||
};
|
||||
|
||||
const LogHighlightsStateProvider: React.FC = ({ children }) => {
|
||||
const { sourceId, version } = useContext(Source.Context);
|
||||
const { sourceId, sourceConfiguration } = useLogSourceContext();
|
||||
const [{ topCursor, bottomCursor, centerCursor, entries }] = useContext(LogEntriesState.Context);
|
||||
const { filterQuery } = useContext(LogFilterState.Context);
|
||||
|
||||
const highlightsProps = {
|
||||
sourceId,
|
||||
sourceVersion: version,
|
||||
sourceVersion: sourceConfiguration?.version,
|
||||
entriesStart: topCursor,
|
||||
entriesEnd: bottomCursor,
|
||||
centerCursor,
|
||||
|
@ -106,6 +104,13 @@ const LogHighlightsStateProvider: React.FC = ({ children }) => {
|
|||
};
|
||||
|
||||
export const LogsPageProviders: React.FunctionComponent = ({ children }) => {
|
||||
const { logIndicesExist } = useLogSourceContext();
|
||||
|
||||
// The providers assume the source is loaded, so short-circuit them otherwise
|
||||
if (!logIndicesExist) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<LogViewConfiguration.Provider>
|
||||
<LogFlyout.Provider>
|
||||
|
|
|
@ -19,13 +19,12 @@ import { LogFlyout } from '../../../containers/logs/log_flyout';
|
|||
import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration';
|
||||
import { LogFilterState } from '../../../containers/logs/log_filter';
|
||||
import { LogPositionState } from '../../../containers/logs/log_position';
|
||||
import { Source } from '../../../containers/source';
|
||||
import { WithKueryAutocompletion } from '../../../containers/with_kuery_autocompletion';
|
||||
import { LogDatepicker } from '../../../components/logging/log_datepicker';
|
||||
import { useLogSourceContext } from '../../../containers/logs/log_source';
|
||||
|
||||
export const LogsToolbar = () => {
|
||||
const { createDerivedIndexPattern } = useContext(Source.Context);
|
||||
const derivedIndexPattern = createDerivedIndexPattern('logs');
|
||||
const { derivedIndexPattern } = useLogSourceContext();
|
||||
const { availableTextScales, setTextScale, setTextWrap, textScale, textWrap } = useContext(
|
||||
LogViewConfiguration.Context
|
||||
);
|
||||
|
|
|
@ -4,32 +4,32 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useContext, useCallback, useMemo } from 'react';
|
||||
import { noop } from 'lodash';
|
||||
import {
|
||||
EuiOverlayMask,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiOverlayMask,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
|
||||
import { noop } from 'lodash';
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import { LogEntry } from '../../../../common/http_api';
|
||||
import { Source } from '../../../containers/source';
|
||||
import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration';
|
||||
import { ScrollableLogTextStreamView } from '../../../components/logging/log_text_stream';
|
||||
import { useLogSourceContext } from '../../../containers/logs/log_source';
|
||||
import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration';
|
||||
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
|
||||
import { useViewportDimensions } from '../../../utils/use_viewport_dimensions';
|
||||
|
||||
const MODAL_MARGIN = 25;
|
||||
|
||||
export const PageViewLogInContext: React.FC = () => {
|
||||
const { source } = useContext(Source.Context);
|
||||
const { sourceConfiguration } = useLogSourceContext();
|
||||
const { textScale, textWrap } = useContext(LogViewConfiguration.Context);
|
||||
const columnConfigurations = useMemo(() => (source && source.configuration.logColumns) || [], [
|
||||
source,
|
||||
const columnConfigurations = useMemo(() => sourceConfiguration?.configuration.logColumns ?? [], [
|
||||
sourceConfiguration,
|
||||
]);
|
||||
const [{ contextEntry, entries, isLoading }, { setContextEntry }] = useContext(
|
||||
ViewLogInContext.Context
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
initLogEntriesItemRoute,
|
||||
} from './routes/log_entries';
|
||||
import { initInventoryMetaRoute } from './routes/inventory_metadata';
|
||||
import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './routes/log_sources';
|
||||
import { initSourceRoute } from './routes/source';
|
||||
|
||||
export const initInfraServer = (libs: InfraBackendLibs) => {
|
||||
|
@ -59,4 +60,6 @@ export const initInfraServer = (libs: InfraBackendLibs) => {
|
|||
initMetricExplorerRoute(libs);
|
||||
initMetadataRoute(libs);
|
||||
initInventoryMetaRoute(libs);
|
||||
initLogSourceConfigurationRoutes(libs);
|
||||
initLogSourceStatusRoutes(libs);
|
||||
};
|
||||
|
|
|
@ -70,6 +70,9 @@ export class KibanaFramework {
|
|||
case 'put':
|
||||
this.router.put(routeConfig, handler);
|
||||
break;
|
||||
case 'patch':
|
||||
this.router.patch(routeConfig, handler);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,9 +29,10 @@ export class InfraFieldsDomain {
|
|||
|
||||
const fields = await this.adapter.getIndexFields(
|
||||
requestContext,
|
||||
`${includeMetricIndices ? configuration.metricAlias : ''},${
|
||||
includeLogIndices ? configuration.logAlias : ''
|
||||
}`
|
||||
[
|
||||
...(includeMetricIndices ? [configuration.metricAlias] : []),
|
||||
...(includeLogIndices ? [configuration.logAlias] : []),
|
||||
].join(',')
|
||||
);
|
||||
|
||||
return fields;
|
||||
|
|
114
x-pack/plugins/infra/server/routes/log_sources/configuration.ts
Normal file
114
x-pack/plugins/infra/server/routes/log_sources/configuration.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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 Boom from 'boom';
|
||||
import {
|
||||
getLogSourceConfigurationRequestParamsRT,
|
||||
getLogSourceConfigurationSuccessResponsePayloadRT,
|
||||
LOG_SOURCE_CONFIGURATION_PATH,
|
||||
patchLogSourceConfigurationRequestBodyRT,
|
||||
patchLogSourceConfigurationRequestParamsRT,
|
||||
patchLogSourceConfigurationSuccessResponsePayloadRT,
|
||||
} from '../../../common/http_api/log_sources';
|
||||
import { createValidationFunction } from '../../../common/runtime_types';
|
||||
import { InfraBackendLibs } from '../../lib/infra_types';
|
||||
|
||||
export const initLogSourceConfigurationRoutes = ({ framework, sources }: InfraBackendLibs) => {
|
||||
framework.registerRoute(
|
||||
{
|
||||
method: 'get',
|
||||
path: LOG_SOURCE_CONFIGURATION_PATH,
|
||||
validate: {
|
||||
params: createValidationFunction(getLogSourceConfigurationRequestParamsRT),
|
||||
},
|
||||
},
|
||||
framework.router.handleLegacyErrors(async (requestContext, request, response) => {
|
||||
const { sourceId } = request.params;
|
||||
|
||||
try {
|
||||
const sourceConfiguration = await sources.getSourceConfiguration(
|
||||
requestContext.core.savedObjects.client,
|
||||
sourceId
|
||||
);
|
||||
|
||||
return response.ok({
|
||||
body: getLogSourceConfigurationSuccessResponsePayloadRT.encode({
|
||||
data: sourceConfiguration,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
if (Boom.isBoom(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response.customError({
|
||||
statusCode: error.statusCode ?? 500,
|
||||
body: {
|
||||
message: error.message ?? 'An unexpected error occurred',
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
framework.registerRoute(
|
||||
{
|
||||
method: 'patch',
|
||||
path: LOG_SOURCE_CONFIGURATION_PATH,
|
||||
validate: {
|
||||
params: createValidationFunction(patchLogSourceConfigurationRequestParamsRT),
|
||||
body: createValidationFunction(patchLogSourceConfigurationRequestBodyRT),
|
||||
},
|
||||
},
|
||||
framework.router.handleLegacyErrors(async (requestContext, request, response) => {
|
||||
const { sourceId } = request.params;
|
||||
const { data: patchedSourceConfigurationProperties } = request.body;
|
||||
|
||||
try {
|
||||
const sourceConfiguration = await sources.getSourceConfiguration(
|
||||
requestContext.core.savedObjects.client,
|
||||
sourceId
|
||||
);
|
||||
|
||||
if (sourceConfiguration.origin === 'internal') {
|
||||
response.conflict({
|
||||
body: 'A conflicting read-only source configuration already exists.',
|
||||
});
|
||||
}
|
||||
|
||||
const sourceConfigurationExists = sourceConfiguration.origin === 'stored';
|
||||
const patchedSourceConfiguration = await (sourceConfigurationExists
|
||||
? sources.updateSourceConfiguration(
|
||||
requestContext,
|
||||
sourceId,
|
||||
patchedSourceConfigurationProperties
|
||||
)
|
||||
: sources.createSourceConfiguration(
|
||||
requestContext,
|
||||
sourceId,
|
||||
patchedSourceConfigurationProperties
|
||||
));
|
||||
|
||||
return response.ok({
|
||||
body: patchLogSourceConfigurationSuccessResponsePayloadRT.encode({
|
||||
data: patchedSourceConfiguration,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
if (Boom.isBoom(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response.customError({
|
||||
statusCode: error.statusCode ?? 500,
|
||||
body: {
|
||||
message: error.message ?? 'An unexpected error occurred',
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
8
x-pack/plugins/infra/server/routes/log_sources/index.ts
Normal file
8
x-pack/plugins/infra/server/routes/log_sources/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 './configuration';
|
||||
export * from './status';
|
62
x-pack/plugins/infra/server/routes/log_sources/status.ts
Normal file
62
x-pack/plugins/infra/server/routes/log_sources/status.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 Boom from 'boom';
|
||||
import {
|
||||
getLogSourceStatusRequestParamsRT,
|
||||
getLogSourceStatusSuccessResponsePayloadRT,
|
||||
LOG_SOURCE_STATUS_PATH,
|
||||
} from '../../../common/http_api/log_sources';
|
||||
import { createValidationFunction } from '../../../common/runtime_types';
|
||||
import { InfraIndexType } from '../../graphql/types';
|
||||
import { InfraBackendLibs } from '../../lib/infra_types';
|
||||
|
||||
export const initLogSourceStatusRoutes = ({
|
||||
framework,
|
||||
sourceStatus,
|
||||
fields,
|
||||
}: InfraBackendLibs) => {
|
||||
framework.registerRoute(
|
||||
{
|
||||
method: 'get',
|
||||
path: LOG_SOURCE_STATUS_PATH,
|
||||
validate: {
|
||||
params: createValidationFunction(getLogSourceStatusRequestParamsRT),
|
||||
},
|
||||
},
|
||||
framework.router.handleLegacyErrors(async (requestContext, request, response) => {
|
||||
const { sourceId } = request.params;
|
||||
|
||||
try {
|
||||
const logIndexNames = await sourceStatus.getLogIndexNames(requestContext, sourceId);
|
||||
const logIndexFields =
|
||||
logIndexNames.length > 0
|
||||
? await fields.getFields(requestContext, sourceId, InfraIndexType.LOGS)
|
||||
: [];
|
||||
|
||||
return response.ok({
|
||||
body: getLogSourceStatusSuccessResponsePayloadRT.encode({
|
||||
data: {
|
||||
logIndexFields,
|
||||
logIndexNames,
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
if (Boom.isBoom(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response.customError({
|
||||
statusCode: error.statusCode ?? 500,
|
||||
body: {
|
||||
message: error.message ?? 'An unexpected error occurred',
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
|
@ -11,6 +11,7 @@ export default function({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./log_entries'));
|
||||
loadTestFile(require.resolve('./log_entry_highlights'));
|
||||
loadTestFile(require.resolve('./logs_without_millis'));
|
||||
loadTestFile(require.resolve('./log_sources'));
|
||||
loadTestFile(require.resolve('./log_summary'));
|
||||
loadTestFile(require.resolve('./metrics'));
|
||||
loadTestFile(require.resolve('./sources'));
|
||||
|
|
179
x-pack/test/api_integration/apis/infra/log_sources.ts
Normal file
179
x-pack/test/api_integration/apis/infra/log_sources.ts
Normal file
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { beforeEach } from 'mocha';
|
||||
import {
|
||||
getLogSourceConfigurationSuccessResponsePayloadRT,
|
||||
patchLogSourceConfigurationSuccessResponsePayloadRT,
|
||||
} from '../../../../plugins/infra/common/http_api/log_sources';
|
||||
import { decodeOrThrow } from '../../../../plugins/infra/common/runtime_types';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const logSourceConfiguration = getService('infraLogSourceConfiguration');
|
||||
|
||||
describe('log sources api', () => {
|
||||
before(() => esArchiver.load('infra/metrics_and_logs'));
|
||||
after(() => esArchiver.unload('infra/metrics_and_logs'));
|
||||
beforeEach(() => esArchiver.load('empty_kibana'));
|
||||
afterEach(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
describe('source configuration get method for non-existant source', () => {
|
||||
it('returns the default source configuration', async () => {
|
||||
const response = await logSourceConfiguration
|
||||
.createGetLogSourceConfigurationAgent('default')
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
data: { configuration, origin },
|
||||
} = decodeOrThrow(getLogSourceConfigurationSuccessResponsePayloadRT)(response.body);
|
||||
|
||||
expect(origin).to.be('fallback');
|
||||
expect(configuration.name).to.be('Default');
|
||||
expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*');
|
||||
expect(configuration.fields.timestamp).to.be('@timestamp');
|
||||
expect(configuration.fields.tiebreaker).to.be('_doc');
|
||||
expect(configuration.logColumns[0]).to.have.key('timestampColumn');
|
||||
expect(configuration.logColumns[1]).to.have.key('fieldColumn');
|
||||
expect(configuration.logColumns[2]).to.have.key('messageColumn');
|
||||
});
|
||||
});
|
||||
|
||||
describe('source configuration patch method for non-existant source', () => {
|
||||
it('creates a source configuration', async () => {
|
||||
const response = await logSourceConfiguration
|
||||
.createUpdateLogSourceConfigurationAgent('default', {
|
||||
name: 'NAME',
|
||||
description: 'DESCRIPTION',
|
||||
logAlias: 'filebeat-**',
|
||||
fields: {
|
||||
tiebreaker: 'TIEBREAKER',
|
||||
timestamp: 'TIMESTAMP',
|
||||
},
|
||||
logColumns: [
|
||||
{
|
||||
messageColumn: {
|
||||
id: 'MESSAGE_COLUMN',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// check direct response
|
||||
const {
|
||||
data: { configuration, origin },
|
||||
} = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body);
|
||||
|
||||
expect(configuration.name).to.be('NAME');
|
||||
expect(origin).to.be('stored');
|
||||
expect(configuration.logAlias).to.be('filebeat-**');
|
||||
expect(configuration.fields.timestamp).to.be('TIMESTAMP');
|
||||
expect(configuration.fields.tiebreaker).to.be('TIEBREAKER');
|
||||
expect(configuration.logColumns).to.have.length(1);
|
||||
expect(configuration.logColumns[0]).to.have.key('messageColumn');
|
||||
|
||||
// check for persistence
|
||||
const {
|
||||
data: { configuration: persistedConfiguration },
|
||||
} = await logSourceConfiguration.getLogSourceConfiguration('default');
|
||||
|
||||
expect(configuration).to.eql(persistedConfiguration);
|
||||
});
|
||||
|
||||
it('creates a source configuration with default values for unspecified properties', async () => {
|
||||
const response = await logSourceConfiguration
|
||||
.createUpdateLogSourceConfigurationAgent('default', {})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
data: { configuration, origin },
|
||||
} = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body);
|
||||
|
||||
expect(configuration.name).to.be('Default');
|
||||
expect(origin).to.be('stored');
|
||||
expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*');
|
||||
expect(configuration.fields.timestamp).to.be('@timestamp');
|
||||
expect(configuration.fields.tiebreaker).to.be('_doc');
|
||||
expect(configuration.logColumns).to.have.length(3);
|
||||
expect(configuration.logColumns[0]).to.have.key('timestampColumn');
|
||||
expect(configuration.logColumns[1]).to.have.key('fieldColumn');
|
||||
expect(configuration.logColumns[2]).to.have.key('messageColumn');
|
||||
|
||||
// check for persistence
|
||||
const {
|
||||
data: { configuration: persistedConfiguration, origin: persistedOrigin },
|
||||
} = await logSourceConfiguration.getLogSourceConfiguration('default');
|
||||
|
||||
expect(persistedOrigin).to.be('stored');
|
||||
expect(configuration).to.eql(persistedConfiguration);
|
||||
});
|
||||
});
|
||||
|
||||
describe('source configuration patch method for existing source', () => {
|
||||
beforeEach(async () => {
|
||||
await logSourceConfiguration.updateLogSourceConfiguration('default', {});
|
||||
});
|
||||
|
||||
it('updates a source configuration', async () => {
|
||||
const response = await logSourceConfiguration
|
||||
.createUpdateLogSourceConfigurationAgent('default', {
|
||||
name: 'NAME',
|
||||
description: 'DESCRIPTION',
|
||||
logAlias: 'filebeat-**',
|
||||
fields: {
|
||||
tiebreaker: 'TIEBREAKER',
|
||||
timestamp: 'TIMESTAMP',
|
||||
},
|
||||
logColumns: [
|
||||
{
|
||||
messageColumn: {
|
||||
id: 'MESSAGE_COLUMN',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
data: { configuration, origin },
|
||||
} = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body);
|
||||
|
||||
expect(configuration.name).to.be('NAME');
|
||||
expect(origin).to.be('stored');
|
||||
expect(configuration.logAlias).to.be('filebeat-**');
|
||||
expect(configuration.fields.timestamp).to.be('TIMESTAMP');
|
||||
expect(configuration.fields.tiebreaker).to.be('TIEBREAKER');
|
||||
expect(configuration.logColumns).to.have.length(1);
|
||||
expect(configuration.logColumns[0]).to.have.key('messageColumn');
|
||||
});
|
||||
|
||||
it('partially updates a source configuration', async () => {
|
||||
const response = await logSourceConfiguration
|
||||
.createUpdateLogSourceConfigurationAgent('default', {
|
||||
name: 'NAME',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
data: { configuration, origin },
|
||||
} = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body);
|
||||
|
||||
expect(configuration.name).to.be('NAME');
|
||||
expect(origin).to.be('stored');
|
||||
expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*');
|
||||
expect(configuration.fields.timestamp).to.be('@timestamp');
|
||||
expect(configuration.fields.tiebreaker).to.be('_doc');
|
||||
expect(configuration.logColumns).to.have.length(3);
|
||||
expect(configuration.logColumns[0]).to.have.key('timestampColumn');
|
||||
expect(configuration.logColumns[1]).to.have.key('fieldColumn');
|
||||
expect(configuration.logColumns[2]).to.have.key('messageColumn');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from './infraops_graphql_client';
|
||||
import { SiemGraphQLClientProvider, SiemGraphQLClientFactoryProvider } from './siem_graphql_client';
|
||||
import { InfraOpsSourceConfigurationProvider } from './infraops_source_configuration';
|
||||
import { InfraLogSourceConfigurationProvider } from './infra_log_source_configuration';
|
||||
import { MachineLearningProvider } from './ml';
|
||||
import { IngestManagerProvider } from './ingest_manager';
|
||||
|
||||
|
@ -35,6 +36,7 @@ export const services = {
|
|||
infraOpsGraphQLClient: InfraOpsGraphQLClientProvider,
|
||||
infraOpsGraphQLClientFactory: InfraOpsGraphQLClientFactoryProvider,
|
||||
infraOpsSourceConfiguration: InfraOpsSourceConfigurationProvider,
|
||||
infraLogSourceConfiguration: InfraLogSourceConfigurationProvider,
|
||||
siemGraphQLClient: SiemGraphQLClientProvider,
|
||||
siemGraphQLClientFactory: SiemGraphQLClientFactoryProvider,
|
||||
supertestWithoutAuth: SupertestWithoutAuthProvider,
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 {
|
||||
getLogSourceConfigurationPath,
|
||||
getLogSourceConfigurationSuccessResponsePayloadRT,
|
||||
PatchLogSourceConfigurationRequestBody,
|
||||
patchLogSourceConfigurationRequestBodyRT,
|
||||
patchLogSourceConfigurationResponsePayloadRT,
|
||||
} from '../../../plugins/infra/common/http_api/log_sources';
|
||||
import { decodeOrThrow } from '../../../plugins/infra/common/runtime_types';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export function InfraLogSourceConfigurationProvider({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const log = getService('log');
|
||||
|
||||
const createGetLogSourceConfigurationAgent = (sourceId: string) =>
|
||||
supertest
|
||||
.get(getLogSourceConfigurationPath(sourceId))
|
||||
.set({
|
||||
'kbn-xsrf': 'some-xsrf-token',
|
||||
})
|
||||
.send();
|
||||
|
||||
const getLogSourceConfiguration = async (sourceId: string) => {
|
||||
log.debug(`Fetching Logs UI source configuration "${sourceId}"`);
|
||||
|
||||
const response = await createGetLogSourceConfigurationAgent(sourceId);
|
||||
|
||||
return decodeOrThrow(getLogSourceConfigurationSuccessResponsePayloadRT)(response.body);
|
||||
};
|
||||
|
||||
const createUpdateLogSourceConfigurationAgent = (
|
||||
sourceId: string,
|
||||
sourceProperties: PatchLogSourceConfigurationRequestBody['data']
|
||||
) =>
|
||||
supertest
|
||||
.patch(getLogSourceConfigurationPath(sourceId))
|
||||
.set({
|
||||
'kbn-xsrf': 'some-xsrf-token',
|
||||
})
|
||||
.send(patchLogSourceConfigurationRequestBodyRT.encode({ data: sourceProperties }));
|
||||
|
||||
const updateLogSourceConfiguration = async (
|
||||
sourceId: string,
|
||||
sourceProperties: PatchLogSourceConfigurationRequestBody['data']
|
||||
) => {
|
||||
log.debug(
|
||||
`Updating Logs UI source configuration "${sourceId}" with properties ${JSON.stringify(
|
||||
sourceProperties
|
||||
)}`
|
||||
);
|
||||
|
||||
const response = await createUpdateLogSourceConfigurationAgent(sourceId, sourceProperties);
|
||||
|
||||
return decodeOrThrow(patchLogSourceConfigurationResponsePayloadRT)(response.body);
|
||||
};
|
||||
|
||||
return {
|
||||
createGetLogSourceConfigurationAgent,
|
||||
createUpdateLogSourceConfigurationAgent,
|
||||
getLogSourceConfiguration,
|
||||
updateLogSourceConfiguration,
|
||||
};
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { URL } from 'url';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
const ONE_HOUR = 60 * 60 * 1000;
|
||||
|
@ -28,8 +29,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
search: `time=${timestamp}&filter=trace.id:${traceId}`,
|
||||
state: undefined,
|
||||
};
|
||||
const expectedSearchString = `logFilter=(expression:'trace.id:${traceId}',kind:kuery)&logPosition=(end:'${endDate}',position:(tiebreaker:0,time:${timestamp}),start:'${startDate}',streamLive:!f)&sourceId=default`;
|
||||
const expectedRedirectPath = '/logs/stream?';
|
||||
|
||||
await pageObjects.common.navigateToUrlWithBrowserHistory(
|
||||
'infraLogs',
|
||||
|
@ -41,9 +40,16 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
);
|
||||
await retry.tryForTime(5000, async () => {
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
const decodedUrl = decodeURIComponent(currentUrl);
|
||||
expect(decodedUrl).to.contain(expectedRedirectPath);
|
||||
expect(decodedUrl).to.contain(expectedSearchString);
|
||||
const parsedUrl = new URL(currentUrl);
|
||||
|
||||
expect(parsedUrl.pathname).to.be('/app/logs/stream');
|
||||
expect(parsedUrl.searchParams.get('logFilter')).to.be(
|
||||
`(expression:'trace.id:${traceId}',kind:kuery)`
|
||||
);
|
||||
expect(parsedUrl.searchParams.get('logPosition')).to.be(
|
||||
`(end:'${endDate}',position:(tiebreaker:0,time:${timestamp}),start:'${startDate}',streamLive:!f)`
|
||||
);
|
||||
expect(parsedUrl.searchParams.get('sourceId')).to.be('default');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue