[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 { pipe } from 'fp-ts/lib/pipeable';
|
||||||
import { Errors, Type } from 'io-ts';
|
import { Errors, Type } from 'io-ts';
|
||||||
import { failure } from 'io-ts/lib/PathReporter';
|
import { failure } from 'io-ts/lib/PathReporter';
|
||||||
|
import { RouteValidationFunction } from 'kibana/server';
|
||||||
|
|
||||||
type ErrorFactory = (message: string) => Error;
|
type ErrorFactory = (message: string) => Error;
|
||||||
|
|
||||||
|
@ -18,8 +19,21 @@ export const throwErrors = (createError: ErrorFactory) => (errors: Errors) => {
|
||||||
throw createError(failure(errors).join('\n'));
|
throw createError(failure(errors).join('\n'));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const decodeOrThrow = <A, O, I>(
|
export const decodeOrThrow = <DecodedValue, EncodedValue, InputValue>(
|
||||||
runtimeType: Type<A, O, I>,
|
runtimeType: Type<DecodedValue, EncodedValue, InputValue>,
|
||||||
createError: ErrorFactory = createPlainError
|
createError: ErrorFactory = createPlainError
|
||||||
) => (inputValue: I) =>
|
) => (inputValue: InputValue) =>
|
||||||
pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity));
|
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.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from './input_fields';
|
||||||
export { SourceConfigurationSettings } from './source_configuration_settings';
|
export { SourceConfigurationSettings } from './source_configuration_settings';
|
||||||
export { ViewSourceConfigurationButton } from './view_source_configuration_button';
|
export { ViewSourceConfigurationButton } from './view_source_configuration_button';
|
||||||
|
|
|
@ -8,11 +8,11 @@ import createContainer from 'constate';
|
||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { LogEntriesItem } from '../../../common/http_api';
|
||||||
import { UrlStateContainer } from '../../utils/url_state';
|
import { UrlStateContainer } from '../../utils/url_state';
|
||||||
import { useTrackedPromise } from '../../utils/use_tracked_promise';
|
import { useTrackedPromise } from '../../utils/use_tracked_promise';
|
||||||
import { Source } from '../source';
|
|
||||||
import { fetchLogEntriesItem } from './log_entries/api/fetch_log_entries_item';
|
import { fetchLogEntriesItem } from './log_entries/api/fetch_log_entries_item';
|
||||||
import { LogEntriesItem } from '../../../common/http_api';
|
import { useLogSourceContext } from './log_source';
|
||||||
|
|
||||||
export enum FlyoutVisibility {
|
export enum FlyoutVisibility {
|
||||||
hidden = 'hidden',
|
hidden = 'hidden',
|
||||||
|
@ -26,7 +26,7 @@ export interface FlyoutOptionsUrlState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useLogFlyout = () => {
|
export const useLogFlyout = () => {
|
||||||
const { sourceId } = useContext(Source.Context);
|
const { sourceId } = useLogSourceContext();
|
||||||
const [flyoutVisible, setFlyoutVisibility] = useState<boolean>(false);
|
const [flyoutVisible, setFlyoutVisibility] = useState<boolean>(false);
|
||||||
const [flyoutId, setFlyoutId] = useState<string | null>(null);
|
const [flyoutId, setFlyoutId] = useState<string | null>(null);
|
||||||
const [flyoutItem, setFlyoutItem] = useState<LogEntriesItem | 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 { useThrottle } from 'react-use';
|
||||||
|
|
||||||
import { RendererFunction } from '../../../utils/typed_react';
|
import { RendererFunction } from '../../../utils/typed_react';
|
||||||
import { Source } from '../../source';
|
|
||||||
import { LogSummaryBuckets, useLogSummary } from './log_summary';
|
import { LogSummaryBuckets, useLogSummary } from './log_summary';
|
||||||
import { LogFilterState } from '../log_filter';
|
import { LogFilterState } from '../log_filter';
|
||||||
import { LogPositionState } from '../log_position';
|
import { LogPositionState } from '../log_position';
|
||||||
|
import { useLogSourceContext } from '../log_source';
|
||||||
|
|
||||||
const FETCH_THROTTLE_INTERVAL = 3000;
|
const FETCH_THROTTLE_INTERVAL = 3000;
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ export const WithSummary = ({
|
||||||
end: number | null;
|
end: number | null;
|
||||||
}>;
|
}>;
|
||||||
}) => {
|
}) => {
|
||||||
const { sourceId } = useContext(Source.Context);
|
const { sourceId } = useLogSourceContext();
|
||||||
const { filterQuery } = useContext(LogFilterState.Context);
|
const { filterQuery } = useContext(LogFilterState.Context);
|
||||||
const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context);
|
const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context);
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
import { SourceErrorPage } from '../../../components/source_error_page';
|
import { SourceErrorPage } from '../../../components/source_error_page';
|
||||||
import { SourceLoadingPage } from '../../../components/source_loading_page';
|
import { SourceLoadingPage } from '../../../components/source_loading_page';
|
||||||
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
|
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 { LogEntryCategoriesResultsContent } from './page_results_content';
|
||||||
import { LogEntryCategoriesSetupContent } from './page_setup_content';
|
import { LogEntryCategoriesSetupContent } from './page_setup_content';
|
||||||
import { useLogEntryCategoriesModuleContext } from './use_log_entry_categories_module';
|
import { useLogEntryCategoriesModuleContext } from './use_log_entry_categories_module';
|
||||||
|
@ -25,11 +25,11 @@ import { useLogEntryCategoriesModuleContext } from './use_log_entry_categories_m
|
||||||
export const LogEntryCategoriesPageContent = () => {
|
export const LogEntryCategoriesPageContent = () => {
|
||||||
const {
|
const {
|
||||||
hasFailedLoadingSource,
|
hasFailedLoadingSource,
|
||||||
isLoadingSource,
|
isLoading,
|
||||||
isUninitialized,
|
isUninitialized,
|
||||||
loadSource,
|
loadSource,
|
||||||
loadSourceFailureMessage,
|
loadSourceFailureMessage,
|
||||||
} = useSourceContext();
|
} = useLogSourceContext();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hasLogAnalysisCapabilites,
|
hasLogAnalysisCapabilites,
|
||||||
|
@ -45,7 +45,7 @@ export const LogEntryCategoriesPageContent = () => {
|
||||||
}
|
}
|
||||||
}, [fetchJobStatus, hasLogAnalysisReadCapabilities]);
|
}, [fetchJobStatus, hasLogAnalysisReadCapabilities]);
|
||||||
|
|
||||||
if (isLoadingSource || isUninitialized) {
|
if (isLoading || isUninitialized) {
|
||||||
return <SourceLoadingPage />;
|
return <SourceLoadingPage />;
|
||||||
} else if (hasFailedLoadingSource) {
|
} else if (hasFailedLoadingSource) {
|
||||||
return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />;
|
return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />;
|
||||||
|
|
|
@ -6,20 +6,20 @@
|
||||||
|
|
||||||
import React from 'react';
|
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 { useKibanaSpaceId } from '../../../utils/use_kibana_space_id';
|
||||||
import { LogEntryCategoriesModuleProvider } from './use_log_entry_categories_module';
|
import { LogEntryCategoriesModuleProvider } from './use_log_entry_categories_module';
|
||||||
|
|
||||||
export const LogEntryCategoriesPageProviders: React.FunctionComponent = ({ children }) => {
|
export const LogEntryCategoriesPageProviders: React.FunctionComponent = ({ children }) => {
|
||||||
const { sourceId, source } = useSourceContext();
|
const { sourceId, sourceConfiguration } = useLogSourceContext();
|
||||||
const spaceId = useKibanaSpaceId();
|
const spaceId = useKibanaSpaceId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LogEntryCategoriesModuleProvider
|
<LogEntryCategoriesModuleProvider
|
||||||
indexPattern={source ? source.configuration.logAlias : ''}
|
indexPattern={sourceConfiguration?.configuration.logAlias ?? ''}
|
||||||
sourceId={sourceId}
|
sourceId={sourceId}
|
||||||
spaceId={spaceId}
|
spaceId={spaceId}
|
||||||
timestampField={source ? source.configuration.fields.timestamp : ''}
|
timestampField={sourceConfiguration?.configuration.fields.timestamp ?? ''}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</LogEntryCategoriesModuleProvider>
|
</LogEntryCategoriesModuleProvider>
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
import { SourceErrorPage } from '../../../components/source_error_page';
|
import { SourceErrorPage } from '../../../components/source_error_page';
|
||||||
import { SourceLoadingPage } from '../../../components/source_loading_page';
|
import { SourceLoadingPage } from '../../../components/source_loading_page';
|
||||||
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
|
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 { LogEntryRateResultsContent } from './page_results_content';
|
||||||
import { LogEntryRateSetupContent } from './page_setup_content';
|
import { LogEntryRateSetupContent } from './page_setup_content';
|
||||||
import { useLogEntryRateModuleContext } from './use_log_entry_rate_module';
|
import { useLogEntryRateModuleContext } from './use_log_entry_rate_module';
|
||||||
|
@ -25,11 +25,11 @@ import { useLogEntryRateModuleContext } from './use_log_entry_rate_module';
|
||||||
export const LogEntryRatePageContent = () => {
|
export const LogEntryRatePageContent = () => {
|
||||||
const {
|
const {
|
||||||
hasFailedLoadingSource,
|
hasFailedLoadingSource,
|
||||||
isLoadingSource,
|
isLoading,
|
||||||
isUninitialized,
|
isUninitialized,
|
||||||
loadSource,
|
loadSource,
|
||||||
loadSourceFailureMessage,
|
loadSourceFailureMessage,
|
||||||
} = useSourceContext();
|
} = useLogSourceContext();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hasLogAnalysisCapabilites,
|
hasLogAnalysisCapabilites,
|
||||||
|
@ -45,7 +45,7 @@ export const LogEntryRatePageContent = () => {
|
||||||
}
|
}
|
||||||
}, [fetchJobStatus, hasLogAnalysisReadCapabilities]);
|
}, [fetchJobStatus, hasLogAnalysisReadCapabilities]);
|
||||||
|
|
||||||
if (isLoadingSource || isUninitialized) {
|
if (isLoading || isUninitialized) {
|
||||||
return <SourceLoadingPage />;
|
return <SourceLoadingPage />;
|
||||||
} else if (hasFailedLoadingSource) {
|
} else if (hasFailedLoadingSource) {
|
||||||
return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />;
|
return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />;
|
||||||
|
|
|
@ -6,20 +6,20 @@
|
||||||
|
|
||||||
import React from 'react';
|
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 { useKibanaSpaceId } from '../../../utils/use_kibana_space_id';
|
||||||
import { LogEntryRateModuleProvider } from './use_log_entry_rate_module';
|
import { LogEntryRateModuleProvider } from './use_log_entry_rate_module';
|
||||||
|
|
||||||
export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => {
|
export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => {
|
||||||
const { sourceId, source } = useSourceContext();
|
const { sourceId, sourceConfiguration } = useLogSourceContext();
|
||||||
const spaceId = useKibanaSpaceId();
|
const spaceId = useKibanaSpaceId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LogEntryRateModuleProvider
|
<LogEntryRateModuleProvider
|
||||||
indexPattern={source ? source.configuration.logAlias : ''}
|
indexPattern={sourceConfiguration?.configuration.logAlias ?? ''}
|
||||||
sourceId={sourceId}
|
sourceId={sourceId}
|
||||||
spaceId={spaceId}
|
spaceId={spaceId}
|
||||||
timestampField={source ? source.configuration.fields.timestamp : ''}
|
timestampField={sourceConfiguration?.configuration.fields.timestamp ?? ''}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</LogEntryRateModuleProvider>
|
</LogEntryRateModuleProvider>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Route, Switch } from 'react-router-dom';
|
import { Route, Switch } from 'react-router-dom';
|
||||||
|
import { useMount } from 'react-use';
|
||||||
|
|
||||||
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
|
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
|
||||||
import { DocumentTitle } from '../../components/document_title';
|
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 { RoutedTabs } from '../../components/navigation/routed_tabs';
|
||||||
import { ColumnarPage } from '../../components/page';
|
import { ColumnarPage } from '../../components/page';
|
||||||
import { useLogAnalysisCapabilitiesContext } from '../../containers/logs/log_analysis';
|
import { useLogAnalysisCapabilitiesContext } from '../../containers/logs/log_analysis';
|
||||||
|
import { useLogSourceContext } from '../../containers/logs/log_source';
|
||||||
import { RedirectWithQueryParams } from '../../utils/redirect_with_query_params';
|
import { RedirectWithQueryParams } from '../../utils/redirect_with_query_params';
|
||||||
import { LogEntryCategoriesPage } from './log_entry_categories';
|
import { LogEntryCategoriesPage } from './log_entry_categories';
|
||||||
import { LogEntryRatePage } from './log_entry_rate';
|
import { LogEntryRatePage } from './log_entry_rate';
|
||||||
|
@ -28,6 +30,12 @@ export const LogsPageContent: React.FunctionComponent = () => {
|
||||||
const uiCapabilities = useKibana().services.application?.capabilities;
|
const uiCapabilities = useKibana().services.application?.capabilities;
|
||||||
const logAnalysisCapabilities = useLogAnalysisCapabilitiesContext();
|
const logAnalysisCapabilities = useLogAnalysisCapabilitiesContext();
|
||||||
|
|
||||||
|
const { initialize } = useLogSourceContext();
|
||||||
|
|
||||||
|
useMount(() => {
|
||||||
|
initialize();
|
||||||
|
});
|
||||||
|
|
||||||
const streamTab = {
|
const streamTab = {
|
||||||
app: 'logs',
|
app: 'logs',
|
||||||
title: streamTabTitle,
|
title: streamTabTitle,
|
||||||
|
|
|
@ -6,15 +6,16 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LogAnalysisCapabilitiesProvider } from '../../containers/logs/log_analysis';
|
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';
|
import { useSourceId } from '../../containers/source_id';
|
||||||
|
|
||||||
export const LogsPageProviders: React.FunctionComponent = ({ children }) => {
|
export const LogsPageProviders: React.FunctionComponent = ({ children }) => {
|
||||||
const [sourceId] = useSourceId();
|
const [sourceId] = useSourceId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SourceProvider sourceId={sourceId}>
|
<LogSourceProvider sourceId={sourceId}>
|
||||||
<LogAnalysisCapabilitiesProvider>{children}</LogAnalysisCapabilitiesProvider>
|
<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 React from 'react';
|
||||||
|
import { useTrackPageview } from '../../../../../observability/public';
|
||||||
import { ColumnarPage } from '../../../components/page';
|
import { ColumnarPage } from '../../../components/page';
|
||||||
import { StreamPageContent } from './page_content';
|
import { StreamPageContent } from './page_content';
|
||||||
import { StreamPageHeader } from './page_header';
|
import { StreamPageHeader } from './page_header';
|
||||||
import { LogsPageProviders } from './page_providers';
|
import { LogsPageProviders } from './page_providers';
|
||||||
import { PageViewLogInContext } from './page_view_log_in_context';
|
|
||||||
import { useTrackPageview } from '../../../../../observability/public';
|
|
||||||
|
|
||||||
export const StreamPage = () => {
|
export const StreamPage = () => {
|
||||||
useTrackPageview({ app: 'infra_logs', path: 'stream' });
|
useTrackPageview({ app: 'infra_logs', path: 'stream' });
|
||||||
|
@ -22,7 +20,6 @@ export const StreamPage = () => {
|
||||||
<StreamPageHeader />
|
<StreamPageHeader />
|
||||||
<StreamPageContent />
|
<StreamPageContent />
|
||||||
</ColumnarPage>
|
</ColumnarPage>
|
||||||
<PageViewLogInContext />
|
|
||||||
</LogsPageProviders>
|
</LogsPageProviders>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,21 +7,21 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { SourceErrorPage } from '../../../components/source_error_page';
|
import { SourceErrorPage } from '../../../components/source_error_page';
|
||||||
import { SourceLoadingPage } from '../../../components/source_loading_page';
|
import { SourceLoadingPage } from '../../../components/source_loading_page';
|
||||||
import { useSourceContext } from '../../../containers/source';
|
|
||||||
import { LogsPageLogsContent } from './page_logs_content';
|
import { LogsPageLogsContent } from './page_logs_content';
|
||||||
import { LogsPageNoIndicesContent } from './page_no_indices_content';
|
import { LogsPageNoIndicesContent } from './page_no_indices_content';
|
||||||
|
import { useLogSourceContext } from '../../../containers/logs/log_source';
|
||||||
|
|
||||||
export const StreamPageContent: React.FunctionComponent = () => {
|
export const StreamPageContent: React.FunctionComponent = () => {
|
||||||
const {
|
const {
|
||||||
hasFailedLoadingSource,
|
hasFailedLoadingSource,
|
||||||
isLoadingSource,
|
isLoading,
|
||||||
isUninitialized,
|
isUninitialized,
|
||||||
loadSource,
|
loadSource,
|
||||||
loadSourceFailureMessage,
|
loadSourceFailureMessage,
|
||||||
logIndicesExist,
|
logIndicesExist,
|
||||||
} = useSourceContext();
|
} = useLogSourceContext();
|
||||||
|
|
||||||
if (isLoadingSource || isUninitialized) {
|
if (isLoading || isUninitialized) {
|
||||||
return <SourceLoadingPage />;
|
return <SourceLoadingPage />;
|
||||||
} else if (hasFailedLoadingSource) {
|
} else if (hasFailedLoadingSource) {
|
||||||
return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />;
|
return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />;
|
||||||
|
|
|
@ -5,32 +5,30 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
|
||||||
import { euiStyled } from '../../../../../observability/public';
|
import { euiStyled } from '../../../../../observability/public';
|
||||||
import { AutoSizer } from '../../../components/auto_sizer';
|
import { AutoSizer } from '../../../components/auto_sizer';
|
||||||
import { LogEntryFlyout } from '../../../components/logging/log_entry_flyout';
|
import { LogEntryFlyout } from '../../../components/logging/log_entry_flyout';
|
||||||
import { LogMinimap } from '../../../components/logging/log_minimap';
|
import { LogMinimap } from '../../../components/logging/log_minimap';
|
||||||
import { ScrollableLogTextStreamView } from '../../../components/logging/log_text_stream';
|
import { ScrollableLogTextStreamView } from '../../../components/logging/log_text_stream';
|
||||||
import { PageContent } from '../../../components/page';
|
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 { LogFilterState } from '../../../containers/logs/log_filter';
|
||||||
import {
|
import {
|
||||||
LogFlyout as LogFlyoutState,
|
LogFlyout as LogFlyoutState,
|
||||||
WithFlyoutOptionsUrlState,
|
WithFlyoutOptionsUrlState,
|
||||||
} from '../../../containers/logs/log_flyout';
|
} from '../../../containers/logs/log_flyout';
|
||||||
|
import { LogHighlightsState } from '../../../containers/logs/log_highlights';
|
||||||
import { LogPositionState } from '../../../containers/logs/log_position';
|
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 { WithLogTextviewUrlState } from '../../../containers/logs/with_log_textview';
|
||||||
import { WithStreamItems } from '../../../containers/logs/with_stream_items';
|
import { WithStreamItems } from '../../../containers/logs/with_stream_items';
|
||||||
import { Source } from '../../../containers/source';
|
|
||||||
|
|
||||||
import { LogsToolbar } from './page_toolbar';
|
import { LogsToolbar } from './page_toolbar';
|
||||||
import { LogHighlightsState } from '../../../containers/logs/log_highlights';
|
import { PageViewLogInContext } from './page_view_log_in_context';
|
||||||
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
|
|
||||||
|
|
||||||
export const LogsPageLogsContent: React.FunctionComponent = () => {
|
export const LogsPageLogsContent: React.FunctionComponent = () => {
|
||||||
const { source, sourceId, version } = useContext(Source.Context);
|
const { sourceConfiguration, sourceId } = useLogSourceContext();
|
||||||
const { textScale, textWrap } = useContext(LogViewConfiguration.Context);
|
const { textScale, textWrap } = useContext(LogViewConfiguration.Context);
|
||||||
const {
|
const {
|
||||||
setFlyoutVisibility,
|
setFlyoutVisibility,
|
||||||
|
@ -64,6 +62,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
|
||||||
<WithLogTextviewUrlState />
|
<WithLogTextviewUrlState />
|
||||||
<WithFlyoutOptionsUrlState />
|
<WithFlyoutOptionsUrlState />
|
||||||
<LogsToolbar />
|
<LogsToolbar />
|
||||||
|
<PageViewLogInContext />
|
||||||
{flyoutVisible ? (
|
{flyoutVisible ? (
|
||||||
<LogEntryFlyout
|
<LogEntryFlyout
|
||||||
setFilter={applyLogFilterQuery}
|
setFilter={applyLogFilterQuery}
|
||||||
|
@ -77,7 +76,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<PageContent key={`${sourceId}-${version}`}>
|
<PageContent key={`${sourceId}-${sourceConfiguration?.version}`}>
|
||||||
<WithStreamItems>
|
<WithStreamItems>
|
||||||
{({
|
{({
|
||||||
currentHighlightKey,
|
currentHighlightKey,
|
||||||
|
@ -91,7 +90,9 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
|
||||||
checkForNewEntries,
|
checkForNewEntries,
|
||||||
}) => (
|
}) => (
|
||||||
<ScrollableLogTextStreamView
|
<ScrollableLogTextStreamView
|
||||||
columnConfigurations={(source && source.configuration.logColumns) || []}
|
columnConfigurations={
|
||||||
|
(sourceConfiguration && sourceConfiguration.configuration.logColumns) || []
|
||||||
|
}
|
||||||
hasMoreAfterEnd={hasMoreAfterEnd}
|
hasMoreAfterEnd={hasMoreAfterEnd}
|
||||||
hasMoreBeforeStart={hasMoreBeforeStart}
|
hasMoreBeforeStart={hasMoreBeforeStart}
|
||||||
isLoadingMore={isLoadingMore}
|
isLoadingMore={isLoadingMore}
|
||||||
|
|
|
@ -12,13 +12,11 @@ import { LogHighlightsState } from '../../../containers/logs/log_highlights/log_
|
||||||
import { LogPositionState, WithLogPositionUrlState } from '../../../containers/logs/log_position';
|
import { LogPositionState, WithLogPositionUrlState } from '../../../containers/logs/log_position';
|
||||||
import { LogFilterState, WithLogFilterUrlState } from '../../../containers/logs/log_filter';
|
import { LogFilterState, WithLogFilterUrlState } from '../../../containers/logs/log_filter';
|
||||||
import { LogEntriesState } from '../../../containers/logs/log_entries';
|
import { LogEntriesState } from '../../../containers/logs/log_entries';
|
||||||
|
import { useLogSourceContext } from '../../../containers/logs/log_source';
|
||||||
import { Source } from '../../../containers/source';
|
|
||||||
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
|
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
|
||||||
|
|
||||||
const LogFilterStateProvider: React.FC = ({ children }) => {
|
const LogFilterStateProvider: React.FC = ({ children }) => {
|
||||||
const { createDerivedIndexPattern } = useContext(Source.Context);
|
const { derivedIndexPattern } = useLogSourceContext();
|
||||||
const derivedIndexPattern = createDerivedIndexPattern('logs');
|
|
||||||
return (
|
return (
|
||||||
<LogFilterState.Provider indexPattern={derivedIndexPattern}>
|
<LogFilterState.Provider indexPattern={derivedIndexPattern}>
|
||||||
<WithLogFilterUrlState />
|
<WithLogFilterUrlState />
|
||||||
|
@ -29,7 +27,7 @@ const LogFilterStateProvider: React.FC = ({ children }) => {
|
||||||
|
|
||||||
const ViewLogInContextProvider: React.FC = ({ children }) => {
|
const ViewLogInContextProvider: React.FC = ({ children }) => {
|
||||||
const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context);
|
const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context);
|
||||||
const { sourceId } = useContext(Source.Context);
|
const { sourceId } = useLogSourceContext();
|
||||||
|
|
||||||
if (!startTimestamp || !endTimestamp) {
|
if (!startTimestamp || !endTimestamp) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -47,7 +45,7 @@ const ViewLogInContextProvider: React.FC = ({ children }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const LogEntriesStateProvider: React.FC = ({ children }) => {
|
const LogEntriesStateProvider: React.FC = ({ children }) => {
|
||||||
const { sourceId } = useContext(Source.Context);
|
const { sourceId } = useLogSourceContext();
|
||||||
const {
|
const {
|
||||||
startTimestamp,
|
startTimestamp,
|
||||||
endTimestamp,
|
endTimestamp,
|
||||||
|
@ -89,13 +87,13 @@ const LogEntriesStateProvider: React.FC = ({ children }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const LogHighlightsStateProvider: 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 [{ topCursor, bottomCursor, centerCursor, entries }] = useContext(LogEntriesState.Context);
|
||||||
const { filterQuery } = useContext(LogFilterState.Context);
|
const { filterQuery } = useContext(LogFilterState.Context);
|
||||||
|
|
||||||
const highlightsProps = {
|
const highlightsProps = {
|
||||||
sourceId,
|
sourceId,
|
||||||
sourceVersion: version,
|
sourceVersion: sourceConfiguration?.version,
|
||||||
entriesStart: topCursor,
|
entriesStart: topCursor,
|
||||||
entriesEnd: bottomCursor,
|
entriesEnd: bottomCursor,
|
||||||
centerCursor,
|
centerCursor,
|
||||||
|
@ -106,6 +104,13 @@ const LogHighlightsStateProvider: React.FC = ({ children }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LogsPageProviders: React.FunctionComponent = ({ 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 (
|
return (
|
||||||
<LogViewConfiguration.Provider>
|
<LogViewConfiguration.Provider>
|
||||||
<LogFlyout.Provider>
|
<LogFlyout.Provider>
|
||||||
|
|
|
@ -19,13 +19,12 @@ import { LogFlyout } from '../../../containers/logs/log_flyout';
|
||||||
import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration';
|
import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration';
|
||||||
import { LogFilterState } from '../../../containers/logs/log_filter';
|
import { LogFilterState } from '../../../containers/logs/log_filter';
|
||||||
import { LogPositionState } from '../../../containers/logs/log_position';
|
import { LogPositionState } from '../../../containers/logs/log_position';
|
||||||
import { Source } from '../../../containers/source';
|
|
||||||
import { WithKueryAutocompletion } from '../../../containers/with_kuery_autocompletion';
|
import { WithKueryAutocompletion } from '../../../containers/with_kuery_autocompletion';
|
||||||
import { LogDatepicker } from '../../../components/logging/log_datepicker';
|
import { LogDatepicker } from '../../../components/logging/log_datepicker';
|
||||||
|
import { useLogSourceContext } from '../../../containers/logs/log_source';
|
||||||
|
|
||||||
export const LogsToolbar = () => {
|
export const LogsToolbar = () => {
|
||||||
const { createDerivedIndexPattern } = useContext(Source.Context);
|
const { derivedIndexPattern } = useLogSourceContext();
|
||||||
const derivedIndexPattern = createDerivedIndexPattern('logs');
|
|
||||||
const { availableTextScales, setTextScale, setTextWrap, textScale, textWrap } = useContext(
|
const { availableTextScales, setTextScale, setTextWrap, textScale, textWrap } = useContext(
|
||||||
LogViewConfiguration.Context
|
LogViewConfiguration.Context
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,32 +4,32 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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 {
|
import {
|
||||||
EuiOverlayMask,
|
|
||||||
EuiModal,
|
|
||||||
EuiModalBody,
|
|
||||||
EuiText,
|
|
||||||
EuiTextColor,
|
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
EuiFlexItem,
|
EuiFlexItem,
|
||||||
|
EuiModal,
|
||||||
|
EuiModalBody,
|
||||||
|
EuiOverlayMask,
|
||||||
|
EuiText,
|
||||||
|
EuiTextColor,
|
||||||
EuiToolTip,
|
EuiToolTip,
|
||||||
} from '@elastic/eui';
|
} 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 { 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 { 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';
|
import { useViewportDimensions } from '../../../utils/use_viewport_dimensions';
|
||||||
|
|
||||||
const MODAL_MARGIN = 25;
|
const MODAL_MARGIN = 25;
|
||||||
|
|
||||||
export const PageViewLogInContext: React.FC = () => {
|
export const PageViewLogInContext: React.FC = () => {
|
||||||
const { source } = useContext(Source.Context);
|
const { sourceConfiguration } = useLogSourceContext();
|
||||||
const { textScale, textWrap } = useContext(LogViewConfiguration.Context);
|
const { textScale, textWrap } = useContext(LogViewConfiguration.Context);
|
||||||
const columnConfigurations = useMemo(() => (source && source.configuration.logColumns) || [], [
|
const columnConfigurations = useMemo(() => sourceConfiguration?.configuration.logColumns ?? [], [
|
||||||
source,
|
sourceConfiguration,
|
||||||
]);
|
]);
|
||||||
const [{ contextEntry, entries, isLoading }, { setContextEntry }] = useContext(
|
const [{ contextEntry, entries, isLoading }, { setContextEntry }] = useContext(
|
||||||
ViewLogInContext.Context
|
ViewLogInContext.Context
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
initLogEntriesItemRoute,
|
initLogEntriesItemRoute,
|
||||||
} from './routes/log_entries';
|
} from './routes/log_entries';
|
||||||
import { initInventoryMetaRoute } from './routes/inventory_metadata';
|
import { initInventoryMetaRoute } from './routes/inventory_metadata';
|
||||||
|
import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './routes/log_sources';
|
||||||
import { initSourceRoute } from './routes/source';
|
import { initSourceRoute } from './routes/source';
|
||||||
|
|
||||||
export const initInfraServer = (libs: InfraBackendLibs) => {
|
export const initInfraServer = (libs: InfraBackendLibs) => {
|
||||||
|
@ -59,4 +60,6 @@ export const initInfraServer = (libs: InfraBackendLibs) => {
|
||||||
initMetricExplorerRoute(libs);
|
initMetricExplorerRoute(libs);
|
||||||
initMetadataRoute(libs);
|
initMetadataRoute(libs);
|
||||||
initInventoryMetaRoute(libs);
|
initInventoryMetaRoute(libs);
|
||||||
|
initLogSourceConfigurationRoutes(libs);
|
||||||
|
initLogSourceStatusRoutes(libs);
|
||||||
};
|
};
|
||||||
|
|
|
@ -70,6 +70,9 @@ export class KibanaFramework {
|
||||||
case 'put':
|
case 'put':
|
||||||
this.router.put(routeConfig, handler);
|
this.router.put(routeConfig, handler);
|
||||||
break;
|
break;
|
||||||
|
case 'patch':
|
||||||
|
this.router.patch(routeConfig, handler);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,9 +29,10 @@ export class InfraFieldsDomain {
|
||||||
|
|
||||||
const fields = await this.adapter.getIndexFields(
|
const fields = await this.adapter.getIndexFields(
|
||||||
requestContext,
|
requestContext,
|
||||||
`${includeMetricIndices ? configuration.metricAlias : ''},${
|
[
|
||||||
includeLogIndices ? configuration.logAlias : ''
|
...(includeMetricIndices ? [configuration.metricAlias] : []),
|
||||||
}`
|
...(includeLogIndices ? [configuration.logAlias] : []),
|
||||||
|
].join(',')
|
||||||
);
|
);
|
||||||
|
|
||||||
return fields;
|
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_entries'));
|
||||||
loadTestFile(require.resolve('./log_entry_highlights'));
|
loadTestFile(require.resolve('./log_entry_highlights'));
|
||||||
loadTestFile(require.resolve('./logs_without_millis'));
|
loadTestFile(require.resolve('./logs_without_millis'));
|
||||||
|
loadTestFile(require.resolve('./log_sources'));
|
||||||
loadTestFile(require.resolve('./log_summary'));
|
loadTestFile(require.resolve('./log_summary'));
|
||||||
loadTestFile(require.resolve('./metrics'));
|
loadTestFile(require.resolve('./metrics'));
|
||||||
loadTestFile(require.resolve('./sources'));
|
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';
|
} from './infraops_graphql_client';
|
||||||
import { SiemGraphQLClientProvider, SiemGraphQLClientFactoryProvider } from './siem_graphql_client';
|
import { SiemGraphQLClientProvider, SiemGraphQLClientFactoryProvider } from './siem_graphql_client';
|
||||||
import { InfraOpsSourceConfigurationProvider } from './infraops_source_configuration';
|
import { InfraOpsSourceConfigurationProvider } from './infraops_source_configuration';
|
||||||
|
import { InfraLogSourceConfigurationProvider } from './infra_log_source_configuration';
|
||||||
import { MachineLearningProvider } from './ml';
|
import { MachineLearningProvider } from './ml';
|
||||||
import { IngestManagerProvider } from './ingest_manager';
|
import { IngestManagerProvider } from './ingest_manager';
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@ export const services = {
|
||||||
infraOpsGraphQLClient: InfraOpsGraphQLClientProvider,
|
infraOpsGraphQLClient: InfraOpsGraphQLClientProvider,
|
||||||
infraOpsGraphQLClientFactory: InfraOpsGraphQLClientFactoryProvider,
|
infraOpsGraphQLClientFactory: InfraOpsGraphQLClientFactoryProvider,
|
||||||
infraOpsSourceConfiguration: InfraOpsSourceConfigurationProvider,
|
infraOpsSourceConfiguration: InfraOpsSourceConfigurationProvider,
|
||||||
|
infraLogSourceConfiguration: InfraLogSourceConfigurationProvider,
|
||||||
siemGraphQLClient: SiemGraphQLClientProvider,
|
siemGraphQLClient: SiemGraphQLClientProvider,
|
||||||
siemGraphQLClientFactory: SiemGraphQLClientFactoryProvider,
|
siemGraphQLClientFactory: SiemGraphQLClientFactoryProvider,
|
||||||
supertestWithoutAuth: SupertestWithoutAuthProvider,
|
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 expect from '@kbn/expect';
|
||||||
|
import { URL } from 'url';
|
||||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||||
|
|
||||||
const ONE_HOUR = 60 * 60 * 1000;
|
const ONE_HOUR = 60 * 60 * 1000;
|
||||||
|
@ -28,8 +29,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||||
search: `time=${timestamp}&filter=trace.id:${traceId}`,
|
search: `time=${timestamp}&filter=trace.id:${traceId}`,
|
||||||
state: undefined,
|
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(
|
await pageObjects.common.navigateToUrlWithBrowserHistory(
|
||||||
'infraLogs',
|
'infraLogs',
|
||||||
|
@ -41,9 +40,16 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||||
);
|
);
|
||||||
await retry.tryForTime(5000, async () => {
|
await retry.tryForTime(5000, async () => {
|
||||||
const currentUrl = await browser.getCurrentUrl();
|
const currentUrl = await browser.getCurrentUrl();
|
||||||
const decodedUrl = decodeURIComponent(currentUrl);
|
const parsedUrl = new URL(currentUrl);
|
||||||
expect(decodedUrl).to.contain(expectedRedirectPath);
|
|
||||||
expect(decodedUrl).to.contain(expectedSearchString);
|
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