mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Logs UI] Support Kibana index patterns in the Logs UI settings UI (#94849)
This enhances the Logs UI settings screen to support both the legacy configuration style with index name patterns as well as the new style based on Kibana index patterns.
This commit is contained in:
parent
83824ab13a
commit
89dd4b6eec
31 changed files with 1745 additions and 667 deletions
|
@ -53,18 +53,21 @@ export const logSourceColumnConfigurationRT = rt.union([
|
|||
export type LogSourceColumnConfiguration = rt.TypeOf<typeof logSourceColumnConfigurationRT>;
|
||||
|
||||
// Kibana index pattern
|
||||
const logIndexPatternReferenceRT = rt.type({
|
||||
export const logIndexPatternReferenceRT = rt.type({
|
||||
type: rt.literal('index_pattern'),
|
||||
indexPatternId: rt.string,
|
||||
});
|
||||
export type LogIndexPatternReference = rt.TypeOf<typeof logIndexPatternReferenceRT>;
|
||||
|
||||
// Legacy support
|
||||
const logIndexNameReferenceRT = rt.type({
|
||||
export const logIndexNameReferenceRT = rt.type({
|
||||
type: rt.literal('index_name'),
|
||||
indexName: rt.string,
|
||||
});
|
||||
export type LogIndexNameReference = rt.TypeOf<typeof logIndexNameReferenceRT>;
|
||||
|
||||
export const logIndexReferenceRT = rt.union([logIndexPatternReferenceRT, logIndexNameReferenceRT]);
|
||||
export type LogIndexReference = rt.TypeOf<typeof logIndexReferenceRT>;
|
||||
|
||||
export const logSourceConfigurationPropertiesRT = rt.strict({
|
||||
name: rt.string,
|
||||
|
|
|
@ -45,5 +45,7 @@ interface DeepPartialArray<T> extends Array<DeepPartial<T>> {}
|
|||
|
||||
type DeepPartialObject<T> = { [P in keyof T]+?: DeepPartial<T[P]> };
|
||||
|
||||
export type ObjectValues<T> = Array<T[keyof T]>;
|
||||
|
||||
export type ObjectEntry<T> = [keyof T, T[keyof T]];
|
||||
export type ObjectEntries<T> = Array<ObjectEntry<T>>;
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { from, of } from 'rxjs';
|
||||
import { delay } from 'rxjs/operators';
|
||||
import { CoreStart } from '../../../../../src/core/public';
|
||||
import { FieldSpec } from '../../../../../src/plugins/data/common';
|
||||
import {
|
||||
IIndexPattern,
|
||||
IndexPattern,
|
||||
IndexPatternField,
|
||||
IndexPatternsContract,
|
||||
} from '../../../../../src/plugins/data/public';
|
||||
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { Pick2 } from '../../common/utility_types';
|
||||
|
||||
type MockIndexPattern = Pick<
|
||||
IndexPattern,
|
||||
'id' | 'title' | 'type' | 'getTimeField' | 'isTimeBased' | 'getFieldByName'
|
||||
>;
|
||||
export type MockIndexPatternSpec = Pick<
|
||||
IIndexPattern,
|
||||
'id' | 'title' | 'type' | 'timeFieldName'
|
||||
> & {
|
||||
fields: FieldSpec[];
|
||||
};
|
||||
|
||||
export const MockIndexPatternsKibanaContextProvider: React.FC<{
|
||||
asyncDelay: number;
|
||||
mockIndexPatterns: MockIndexPatternSpec[];
|
||||
}> = ({ asyncDelay, children, mockIndexPatterns }) => {
|
||||
const indexPatterns = useMemo(
|
||||
() =>
|
||||
createIndexPatternsMock(
|
||||
asyncDelay,
|
||||
mockIndexPatterns.map(({ id, title, type = undefined, fields, timeFieldName }) => {
|
||||
const indexPatternFields = fields.map((fieldSpec) => new IndexPatternField(fieldSpec));
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
type,
|
||||
getTimeField: () => indexPatternFields.find(({ name }) => name === timeFieldName),
|
||||
isTimeBased: () => timeFieldName != null,
|
||||
getFieldByName: (fieldName) =>
|
||||
indexPatternFields.find(({ name }) => name === fieldName),
|
||||
};
|
||||
})
|
||||
),
|
||||
[asyncDelay, mockIndexPatterns]
|
||||
);
|
||||
|
||||
const core = useMemo<Pick2<CoreStart, 'application', 'getUrlForApp'>>(
|
||||
() => ({
|
||||
application: {
|
||||
getUrlForApp: () => '',
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<KibanaContextProvider services={{ ...core, data: { indexPatterns } }}>
|
||||
{children}
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const createIndexPatternsMock = (
|
||||
asyncDelay: number,
|
||||
indexPatterns: MockIndexPattern[]
|
||||
): {
|
||||
getIdsWithTitle: IndexPatternsContract['getIdsWithTitle'];
|
||||
get: (...args: Parameters<IndexPatternsContract['get']>) => Promise<MockIndexPattern>;
|
||||
} => {
|
||||
return {
|
||||
async getIdsWithTitle(_refresh?: boolean) {
|
||||
const indexPatterns$ = of(
|
||||
indexPatterns.map(({ id = 'unknown_id', title }) => ({ id, title }))
|
||||
);
|
||||
return await indexPatterns$.pipe(delay(asyncDelay)).toPromise();
|
||||
},
|
||||
async get(indexPatternId: string) {
|
||||
const indexPatterns$ = from(
|
||||
indexPatterns.filter((indexPattern) => indexPattern.id === indexPatternId)
|
||||
);
|
||||
return await indexPatterns$.pipe(delay(asyncDelay)).toPromise();
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useTrackedPromise } from '../utils/use_tracked_promise';
|
||||
import { useKibanaContextForPlugin } from './use_kibana';
|
||||
|
||||
export const useKibanaIndexPatternService = () => {
|
||||
const {
|
||||
services: {
|
||||
data: { indexPatterns },
|
||||
},
|
||||
} = useKibanaContextForPlugin();
|
||||
|
||||
return indexPatterns;
|
||||
};
|
||||
|
||||
interface IndexPatternDescriptor {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const useKibanaIndexPatternTitles = () => {
|
||||
const indexPatterns = useKibanaIndexPatternService();
|
||||
|
||||
const [indexPatternTitles, setIndexPatternTitles] = useState<IndexPatternDescriptor[]>([]);
|
||||
|
||||
const [indexPatternTitlesRequest, fetchIndexPatternTitles] = useTrackedPromise(
|
||||
{
|
||||
createPromise: () => indexPatterns.getIdsWithTitle(true),
|
||||
onResolve: setIndexPatternTitles,
|
||||
},
|
||||
[indexPatterns]
|
||||
);
|
||||
|
||||
return {
|
||||
fetchIndexPatternTitles,
|
||||
indexPatternTitles,
|
||||
latestIndexPatternTitlesRequest: indexPatternTitlesRequest,
|
||||
};
|
||||
};
|
|
@ -10,7 +10,6 @@ import {
|
|||
EuiCode,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFieldText,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
|
@ -18,27 +17,29 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { InputFieldProps } from '../../../components/source_configuration/input_fields';
|
||||
import React, { useMemo } from 'react';
|
||||
import { FormElement } from './form_elements';
|
||||
import { getFormRowProps, getStringInputFieldProps } from './form_field_props';
|
||||
import { FormValidationError } from './validation_errors';
|
||||
|
||||
interface FieldsConfigurationPanelProps {
|
||||
isLoading: boolean;
|
||||
readOnly: boolean;
|
||||
tiebreakerFieldProps: InputFieldProps;
|
||||
timestampFieldProps: InputFieldProps;
|
||||
isReadOnly: boolean;
|
||||
tiebreakerFieldFormElement: FormElement<string, FormValidationError>;
|
||||
timestampFieldFormElement: FormElement<string, FormValidationError>;
|
||||
}
|
||||
|
||||
export const FieldsConfigurationPanel = ({
|
||||
isLoading,
|
||||
readOnly,
|
||||
tiebreakerFieldProps,
|
||||
timestampFieldProps,
|
||||
isReadOnly,
|
||||
tiebreakerFieldFormElement,
|
||||
timestampFieldFormElement,
|
||||
}: FieldsConfigurationPanelProps) => {
|
||||
const isTimestampValueDefault = timestampFieldProps.value === '@timestamp';
|
||||
const isTiebreakerValueDefault = tiebreakerFieldProps.value === '_doc';
|
||||
const isTimestampValueDefault = timestampFieldFormElement.value === '@timestamp';
|
||||
const isTiebreakerValueDefault = tiebreakerFieldFormElement.value === '_doc';
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
|
@ -101,7 +102,6 @@ export const FieldsConfigurationPanel = ({
|
|||
}
|
||||
>
|
||||
<EuiFormRow
|
||||
error={timestampFieldProps.error}
|
||||
fullWidth
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
|
@ -112,20 +112,24 @@ export const FieldsConfigurationPanel = ({
|
|||
}}
|
||||
/>
|
||||
}
|
||||
isInvalid={timestampFieldProps.isInvalid}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.timestampFieldLabel"
|
||||
defaultMessage="Timestamp"
|
||||
/>
|
||||
}
|
||||
{...useMemo(() => getFormRowProps(timestampFieldFormElement), [
|
||||
timestampFieldFormElement,
|
||||
])}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
disabled={isLoading || isTimestampValueDefault}
|
||||
readOnly={readOnly}
|
||||
readOnly={isReadOnly}
|
||||
isLoading={isLoading}
|
||||
{...timestampFieldProps}
|
||||
{...useMemo(() => getStringInputFieldProps(timestampFieldFormElement), [
|
||||
timestampFieldFormElement,
|
||||
])}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -146,7 +150,6 @@ export const FieldsConfigurationPanel = ({
|
|||
}
|
||||
>
|
||||
<EuiFormRow
|
||||
error={tiebreakerFieldProps.error}
|
||||
fullWidth
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
|
@ -157,23 +160,27 @@ export const FieldsConfigurationPanel = ({
|
|||
}}
|
||||
/>
|
||||
}
|
||||
isInvalid={tiebreakerFieldProps.isInvalid}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.tiebreakerFieldLabel"
|
||||
defaultMessage="Tiebreaker"
|
||||
/>
|
||||
}
|
||||
{...useMemo(() => getFormRowProps(tiebreakerFieldFormElement), [
|
||||
tiebreakerFieldFormElement,
|
||||
])}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
disabled={isLoading || isTiebreakerValueDefault}
|
||||
readOnly={readOnly}
|
||||
readOnly={isReadOnly}
|
||||
isLoading={isLoading}
|
||||
{...tiebreakerFieldProps}
|
||||
{...useMemo(() => getStringInputFieldProps(tiebreakerFieldFormElement), [
|
||||
tiebreakerFieldFormElement,
|
||||
])}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</EuiForm>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import equal from 'fast-deep-equal';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { ObjectEntries } from '../../../../common/utility_types';
|
||||
import { ChildFormValidationError, GenericValidationError } from './validation_errors';
|
||||
|
||||
const unsetValue = Symbol('unset form value');
|
||||
|
||||
type ValueUpdater<Value> = (updater: (previousValue: Value) => Value) => void;
|
||||
|
||||
export interface FormElement<Value, InvalidReason> {
|
||||
initialValue: Value;
|
||||
isDirty: boolean;
|
||||
resetValue: () => void;
|
||||
updateValue: ValueUpdater<Value>;
|
||||
validity: FormElementValidity<InvalidReason | GenericValidationError>;
|
||||
value: Value;
|
||||
}
|
||||
|
||||
type FormElementMap<FormValues extends {}> = {
|
||||
[formElementName in keyof FormValues]: FormElement<FormValues[formElementName], any>;
|
||||
};
|
||||
|
||||
export interface CompositeFormElement<CompositeValue extends {}, InvalidReason>
|
||||
extends FormElement<CompositeValue, InvalidReason | ChildFormValidationError> {
|
||||
childFormElements: FormElementMap<CompositeValue>;
|
||||
}
|
||||
|
||||
export type FormElementValidity<InvalidReason> =
|
||||
| { validity: 'valid' }
|
||||
| { validity: 'invalid'; reasons: InvalidReason[] }
|
||||
| { validity: 'pending' };
|
||||
|
||||
export const useFormElement = <Value, InvalidReason>({
|
||||
initialValue,
|
||||
validate,
|
||||
}: {
|
||||
initialValue: Value;
|
||||
validate?: (value: Value) => Promise<InvalidReason[]>;
|
||||
}): FormElement<Value, InvalidReason> => {
|
||||
const [changedValue, setChangedValue] = useState<Value | typeof unsetValue>(unsetValue);
|
||||
|
||||
const value = changedValue !== unsetValue ? changedValue : initialValue;
|
||||
|
||||
const updateValue = useCallback<ValueUpdater<Value>>(
|
||||
(updater) =>
|
||||
setChangedValue((previousValue) =>
|
||||
previousValue === unsetValue ? updater(initialValue) : updater(previousValue)
|
||||
),
|
||||
[initialValue]
|
||||
);
|
||||
|
||||
const resetValue = useCallback(() => setChangedValue(unsetValue), []);
|
||||
|
||||
const isDirty = useMemo(() => !equal(value, initialValue), [value, initialValue]);
|
||||
|
||||
const validity = useValidity(value, validate);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
initialValue,
|
||||
isDirty,
|
||||
resetValue,
|
||||
updateValue,
|
||||
validity,
|
||||
value,
|
||||
}),
|
||||
[initialValue, isDirty, resetValue, updateValue, validity, value]
|
||||
);
|
||||
};
|
||||
|
||||
export const useCompositeFormElement = <FormValues extends {}, InvalidReason>({
|
||||
childFormElements,
|
||||
validate,
|
||||
}: {
|
||||
childFormElements: FormElementMap<FormValues>;
|
||||
validate?: (values: FormValues) => Promise<InvalidReason[]>;
|
||||
}): CompositeFormElement<FormValues, InvalidReason> => {
|
||||
const childFormElementEntries = useMemo(
|
||||
() => Object.entries(childFormElements) as ObjectEntries<typeof childFormElements>,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
Object.entries(childFormElements).flat()
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() =>
|
||||
childFormElementEntries.reduce<FormValues>(
|
||||
(accumulatedFormValues, [formElementName, formElement]) => ({
|
||||
...accumulatedFormValues,
|
||||
[formElementName]: formElement.value,
|
||||
}),
|
||||
{} as FormValues
|
||||
),
|
||||
[childFormElementEntries]
|
||||
);
|
||||
|
||||
const updateValue = useCallback(
|
||||
(updater: (previousValues: FormValues) => FormValues) => {
|
||||
const newValues = updater(value);
|
||||
|
||||
childFormElementEntries.forEach(([formElementName, formElement]) =>
|
||||
formElement.updateValue(() => newValues[formElementName])
|
||||
);
|
||||
},
|
||||
[childFormElementEntries, value]
|
||||
);
|
||||
|
||||
const isDirty = useMemo(
|
||||
() => childFormElementEntries.some(([, formElement]) => formElement.isDirty),
|
||||
[childFormElementEntries]
|
||||
);
|
||||
|
||||
const formValidity = useValidity(value, validate);
|
||||
const childFormElementsValidity = useMemo<
|
||||
FormElementValidity<InvalidReason | ChildFormValidationError>
|
||||
>(() => {
|
||||
if (
|
||||
childFormElementEntries.some(([, formElement]) => formElement.validity.validity === 'invalid')
|
||||
) {
|
||||
return {
|
||||
validity: 'invalid',
|
||||
reasons: [{ type: 'child' }],
|
||||
};
|
||||
} else if (
|
||||
childFormElementEntries.some(([, formElement]) => formElement.validity.validity === 'pending')
|
||||
) {
|
||||
return {
|
||||
validity: 'pending',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
validity: 'valid',
|
||||
};
|
||||
}
|
||||
}, [childFormElementEntries]);
|
||||
|
||||
const validity = useMemo(() => getCombinedValidity(formValidity, childFormElementsValidity), [
|
||||
formValidity,
|
||||
childFormElementsValidity,
|
||||
]);
|
||||
|
||||
const resetValue = useCallback(() => {
|
||||
childFormElementEntries.forEach(([, formElement]) => formElement.resetValue());
|
||||
}, [childFormElementEntries]);
|
||||
|
||||
const initialValue = useMemo(
|
||||
() =>
|
||||
childFormElementEntries.reduce<FormValues>(
|
||||
(accumulatedFormValues, [formElementName, formElement]) => ({
|
||||
...accumulatedFormValues,
|
||||
[formElementName]: formElement.initialValue,
|
||||
}),
|
||||
{} as FormValues
|
||||
),
|
||||
[childFormElementEntries]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
childFormElements,
|
||||
initialValue,
|
||||
isDirty,
|
||||
resetValue,
|
||||
updateValue,
|
||||
validity,
|
||||
value,
|
||||
}),
|
||||
[childFormElements, initialValue, isDirty, resetValue, updateValue, validity, value]
|
||||
);
|
||||
};
|
||||
|
||||
const useValidity = <Value, InvalidReason>(
|
||||
value: Value,
|
||||
validate?: (value: Value) => Promise<InvalidReason[]>
|
||||
) => {
|
||||
const validationState = useAsync(() => validate?.(value) ?? Promise.resolve([]), [
|
||||
validate,
|
||||
value,
|
||||
]);
|
||||
|
||||
const validity = useMemo<FormElementValidity<InvalidReason | GenericValidationError>>(() => {
|
||||
if (validationState.loading) {
|
||||
return { validity: 'pending' as const };
|
||||
} else if (validationState.error != null) {
|
||||
return {
|
||||
validity: 'invalid' as const,
|
||||
reasons: [
|
||||
{
|
||||
type: 'generic' as const,
|
||||
message: `${validationState.error}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else if (validationState.value && validationState.value.length > 0) {
|
||||
return {
|
||||
validity: 'invalid' as const,
|
||||
reasons: validationState.value,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
validity: 'valid' as const,
|
||||
};
|
||||
}
|
||||
}, [validationState.error, validationState.loading, validationState.value]);
|
||||
|
||||
return validity;
|
||||
};
|
||||
|
||||
export const getCombinedValidity = <FirstInvalidReason, SecondInvalidReason>(
|
||||
first: FormElementValidity<FirstInvalidReason>,
|
||||
second: FormElementValidity<SecondInvalidReason>
|
||||
): FormElementValidity<FirstInvalidReason | SecondInvalidReason> => {
|
||||
if (first.validity === 'invalid' || second.validity === 'invalid') {
|
||||
return {
|
||||
validity: 'invalid',
|
||||
reasons: [
|
||||
...(first.validity === 'invalid' ? first.reasons : []),
|
||||
...(second.validity === 'invalid' ? second.reasons : []),
|
||||
],
|
||||
};
|
||||
} else if (first.validity === 'pending' || second.validity === 'pending') {
|
||||
return {
|
||||
validity: 'pending',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
validity: 'valid',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const isFormElementForType = <Value extends any>(
|
||||
isValue: (value: any) => value is Value
|
||||
) => <InvalidReason extends unknown>(
|
||||
formElement: FormElement<any, InvalidReason>
|
||||
): formElement is FormElement<Value, InvalidReason> => isValue(formElement.value);
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { FormElement } from './form_elements';
|
||||
import { LogSourceConfigurationFormError } from './source_configuration_form_errors';
|
||||
import { FormValidationError } from './validation_errors';
|
||||
|
||||
export const getFormRowProps = (formElement: FormElement<any, FormValidationError>) => ({
|
||||
error:
|
||||
formElement.validity.validity === 'invalid'
|
||||
? formElement.validity.reasons.map((error) => (
|
||||
<LogSourceConfigurationFormError error={error} />
|
||||
))
|
||||
: [],
|
||||
isInvalid: formElement.validity.validity === 'invalid',
|
||||
});
|
||||
|
||||
export const getInputFieldProps = <Value extends unknown>(
|
||||
decodeInputValue: (value: string) => Value,
|
||||
encodeInputValue: (value: Value) => string
|
||||
) => (formElement: FormElement<Value, any>) => ({
|
||||
isInvalid: formElement.validity.validity === 'invalid',
|
||||
onChange: (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = evt.currentTarget.value;
|
||||
formElement.updateValue(() => decodeInputValue(newValue));
|
||||
},
|
||||
value: encodeInputValue(formElement.value),
|
||||
});
|
||||
|
||||
export const getStringInputFieldProps = getInputFieldProps<string>(
|
||||
(value) => `${value}`,
|
||||
(value) => value
|
||||
);
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiCode,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFieldText,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { useTrackPageview } from '../../../../../observability/public';
|
||||
import { LogIndexNameReference } from '../../../../common/log_sources';
|
||||
import { FormElement } from './form_elements';
|
||||
import { getFormRowProps, getInputFieldProps } from './form_field_props';
|
||||
import { FormValidationError } from './validation_errors';
|
||||
|
||||
export const IndexNamesConfigurationPanel: React.FC<{
|
||||
isLoading: boolean;
|
||||
isReadOnly: boolean;
|
||||
indexNamesFormElement: FormElement<LogIndexNameReference, FormValidationError>;
|
||||
onSwitchToIndexPatternReference: () => void;
|
||||
}> = ({ isLoading, isReadOnly, indexNamesFormElement, onSwitchToIndexPatternReference }) => {
|
||||
useTrackPageview({ app: 'infra_logs', path: 'log_source_configuration_index_name' });
|
||||
useTrackPageview({
|
||||
app: 'infra_logs',
|
||||
path: 'log_source_configuration_index_name',
|
||||
delay: 15000,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.indicesSectionTitle"
|
||||
defaultMessage="Indices"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut title={deprecationCalloutTitle} color="warning" iconType="alert">
|
||||
<FormattedMessage
|
||||
tagName="p"
|
||||
id="xpack.infra.logSourceConfiguration.indexNameReferenceDeprecationDescription"
|
||||
defaultMessage="Referring to Elasticsearch indices directly is a deprecated way of configuring a log source. Instead, log source now integrate with Kibana index patterns to configure the used indices."
|
||||
/>
|
||||
<EuiButton color="warning" onClick={onSwitchToIndexPatternReference}>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logSourceConfiguration.switchToIndexPatternReferenceButtonLabel"
|
||||
defaultMessage="Use Kibana index patterns"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
<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
|
||||
fullWidth
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.logIndicesRecommendedValue"
|
||||
defaultMessage="The recommended value is {defaultValue}"
|
||||
values={{
|
||||
defaultValue: <EuiCode>logs-*,filebeat-*</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.logIndicesLabel"
|
||||
defaultMessage="Log indices"
|
||||
/>
|
||||
}
|
||||
{...getFormRowProps(indexNamesFormElement)}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="logIndicesInput"
|
||||
fullWidth
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
readOnly={isReadOnly}
|
||||
{...getIndexNamesInputFieldProps(indexNamesFormElement)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getIndexNamesInputFieldProps = getInputFieldProps<LogIndexNameReference>(
|
||||
(value) => ({
|
||||
type: 'index_name',
|
||||
indexName: value,
|
||||
}),
|
||||
({ indexName }) => indexName
|
||||
);
|
||||
|
||||
const deprecationCalloutTitle = i18n.translate(
|
||||
'xpack.infra.logSourceConfiguration.indexNameReferenceDeprecationTitle',
|
||||
{
|
||||
defaultMessage: 'Deprecated configuration option',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiDescribedFormGroup, EuiFormRow, EuiLink, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTrackPageview } from '../../../../../observability/public';
|
||||
import { LogIndexPatternReference } from '../../../../common/log_sources';
|
||||
import { useLinkProps } from '../../../hooks/use_link_props';
|
||||
import { FormElement } from './form_elements';
|
||||
import { getFormRowProps } from './form_field_props';
|
||||
import { IndexPatternSelector } from './index_pattern_selector';
|
||||
import { FormValidationError } from './validation_errors';
|
||||
|
||||
export const IndexPatternConfigurationPanel: React.FC<{
|
||||
isLoading: boolean;
|
||||
isReadOnly: boolean;
|
||||
indexPatternFormElement: FormElement<LogIndexPatternReference | undefined, FormValidationError>;
|
||||
}> = ({ isLoading, isReadOnly, indexPatternFormElement }) => {
|
||||
useTrackPageview({ app: 'infra_logs', path: 'log_source_configuration_index_pattern' });
|
||||
useTrackPageview({
|
||||
app: 'infra_logs',
|
||||
path: 'log_source_configuration_index_pattern',
|
||||
delay: 15000,
|
||||
});
|
||||
|
||||
const changeIndexPatternId = useCallback(
|
||||
(indexPatternId: string | undefined) => {
|
||||
if (indexPatternId != null) {
|
||||
indexPatternFormElement.updateValue(() => ({
|
||||
type: 'index_pattern',
|
||||
indexPatternId,
|
||||
}));
|
||||
} else {
|
||||
indexPatternFormElement.updateValue(() => undefined);
|
||||
}
|
||||
},
|
||||
[indexPatternFormElement]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logSourceConfiguration.indexPatternSectionTitle"
|
||||
defaultMessage="Index pattern"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<IndexPatternInlineHelpMessage />
|
||||
<EuiSpacer size="m" />
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logSourceConfiguration.logIndexPatternTitle"
|
||||
defaultMessage="Log index pattern"
|
||||
/>
|
||||
</h4>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logSourceConfiguration.logIndexPatternDescription"
|
||||
defaultMessage="Index pattern that contains log data"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logSourceConfiguration.logIndexPatternLabel"
|
||||
defaultMessage="Log index pattern"
|
||||
/>
|
||||
}
|
||||
{...useMemo(() => (isLoading ? {} : getFormRowProps(indexPatternFormElement)), [
|
||||
isLoading,
|
||||
indexPatternFormElement,
|
||||
])}
|
||||
>
|
||||
<IndexPatternSelector
|
||||
isLoading={isLoading || indexPatternFormElement.validity.validity === 'pending'}
|
||||
isReadOnly={isReadOnly}
|
||||
indexPatternId={indexPatternFormElement.value?.indexPatternId}
|
||||
onChangeIndexPatternId={changeIndexPatternId}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const IndexPatternInlineHelpMessage = React.memo(() => {
|
||||
const indexPatternManagementLinkProps = useLinkProps({
|
||||
app: 'management',
|
||||
pathname: '/kibana/indexPatterns',
|
||||
});
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logSourceConfiguration.logIndexPatternHelpText"
|
||||
defaultMessage="Kibana index patterns are shared among apps in the Kibana space and can be managed via the {indexPatternsManagementLink}."
|
||||
values={{
|
||||
indexPatternsManagementLink: (
|
||||
<EuiLink {...indexPatternManagementLinkProps}>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logSourceConfiguration.indexPatternManagementLinkText"
|
||||
defaultMessage="index patterns management screen"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useKibanaIndexPatternTitles } from '../../../hooks/use_kibana_index_patterns';
|
||||
|
||||
type IndexPatternOption = EuiComboBoxOptionOption<string>;
|
||||
|
||||
export const IndexPatternSelector: React.FC<{
|
||||
indexPatternId: string | undefined;
|
||||
isLoading: boolean;
|
||||
isReadOnly: boolean;
|
||||
onChangeIndexPatternId: (indexPatternId: string | undefined) => void;
|
||||
}> = ({ indexPatternId, isLoading, isReadOnly, onChangeIndexPatternId }) => {
|
||||
const {
|
||||
indexPatternTitles: availableIndexPatterns,
|
||||
latestIndexPatternTitlesRequest,
|
||||
fetchIndexPatternTitles,
|
||||
} = useKibanaIndexPatternTitles();
|
||||
|
||||
useEffect(() => {
|
||||
fetchIndexPatternTitles();
|
||||
}, [fetchIndexPatternTitles]);
|
||||
|
||||
const availableOptions = useMemo<IndexPatternOption[]>(
|
||||
() =>
|
||||
availableIndexPatterns.map(({ id, title }) => ({
|
||||
key: id,
|
||||
label: title,
|
||||
value: id,
|
||||
})),
|
||||
[availableIndexPatterns]
|
||||
);
|
||||
|
||||
const selectedOptions = useMemo<IndexPatternOption[]>(
|
||||
() => availableOptions.filter(({ key }) => key === indexPatternId),
|
||||
[availableOptions, indexPatternId]
|
||||
);
|
||||
|
||||
const changeSelectedIndexPatterns = useCallback(
|
||||
([newlySelectedOption]: IndexPatternOption[]) => {
|
||||
if (typeof newlySelectedOption?.key === 'string') {
|
||||
return onChangeIndexPatternId(newlySelectedOption.key);
|
||||
}
|
||||
|
||||
return onChangeIndexPatternId(undefined);
|
||||
},
|
||||
[onChangeIndexPatternId]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiComboBox<string>
|
||||
isLoading={isLoading || latestIndexPatternTitlesRequest.state === 'pending'}
|
||||
isDisabled={isReadOnly}
|
||||
options={availableOptions}
|
||||
placeholder={indexPatternSelectorPlaceholder}
|
||||
selectedOptions={selectedOptions}
|
||||
singleSelection={true}
|
||||
onChange={changeSelectedIndexPatterns}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const indexPatternSelectorPlaceholder = i18n.translate(
|
||||
'xpack.infra.logSourceConfiguration.indexPatternSelectorPlaceholder',
|
||||
{ defaultMessage: 'Choose an index pattern' }
|
||||
);
|
|
@ -5,120 +5,107 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useUiTracker } from '../../../../../observability/public';
|
||||
import {
|
||||
createInputFieldProps,
|
||||
validateInputFieldNotEmpty,
|
||||
} from '../../../components/source_configuration/input_fields';
|
||||
LogIndexNameReference,
|
||||
logIndexNameReferenceRT,
|
||||
LogIndexPatternReference,
|
||||
} from '../../../../common/log_sources';
|
||||
import { useKibanaIndexPatternService } from '../../../hooks/use_kibana_index_patterns';
|
||||
import { useCompositeFormElement, useFormElement } from './form_elements';
|
||||
import {
|
||||
FormValidationError,
|
||||
validateIndexPattern,
|
||||
validateStringNotEmpty,
|
||||
} from './validation_errors';
|
||||
|
||||
interface FormState {
|
||||
name: string;
|
||||
description: string;
|
||||
logAlias: string;
|
||||
export type LogIndicesFormState = LogIndexNameReference | LogIndexPatternReference | undefined;
|
||||
|
||||
export const useLogIndicesFormElement = (initialValue: LogIndicesFormState) => {
|
||||
const indexPatternService = useKibanaIndexPatternService();
|
||||
|
||||
const trackIndexPatternValidationError = useUiTracker({ app: 'infra_logs' });
|
||||
|
||||
const logIndicesFormElement = useFormElement<LogIndicesFormState, FormValidationError>({
|
||||
initialValue,
|
||||
validate: useMemo(
|
||||
() => async (logIndices) => {
|
||||
if (logIndices == null) {
|
||||
return validateStringNotEmpty('log index pattern', '');
|
||||
} else if (logIndexNameReferenceRT.is(logIndices)) {
|
||||
return validateStringNotEmpty('log indices', logIndices.indexName);
|
||||
} else {
|
||||
const emptyStringErrors = validateStringNotEmpty(
|
||||
'log index pattern',
|
||||
logIndices.indexPatternId
|
||||
);
|
||||
|
||||
if (emptyStringErrors.length > 0) {
|
||||
return emptyStringErrors;
|
||||
}
|
||||
|
||||
const indexPatternErrors = validateIndexPattern(
|
||||
await indexPatternService.get(logIndices.indexPatternId)
|
||||
);
|
||||
|
||||
if (indexPatternErrors.length > 0) {
|
||||
trackIndexPatternValidationError({
|
||||
metric: 'configuration_index_pattern_validation_failed',
|
||||
});
|
||||
} else {
|
||||
trackIndexPatternValidationError({
|
||||
metric: 'configuration_index_pattern_validation_succeeded',
|
||||
});
|
||||
}
|
||||
|
||||
return indexPatternErrors;
|
||||
}
|
||||
},
|
||||
[indexPatternService, trackIndexPatternValidationError]
|
||||
),
|
||||
});
|
||||
|
||||
return logIndicesFormElement;
|
||||
};
|
||||
|
||||
export interface FieldsFormState {
|
||||
tiebreakerField: string;
|
||||
timestampField: string;
|
||||
}
|
||||
|
||||
type FormStateChanges = Partial<FormState>;
|
||||
export const useFieldsFormElement = (initialValues: FieldsFormState) => {
|
||||
const tiebreakerFieldFormElement = useFormElement<string, FormValidationError>({
|
||||
initialValue: initialValues.tiebreakerField,
|
||||
validate: useMemo(
|
||||
() => async (tiebreakerField) => validateStringNotEmpty('tiebreaker', tiebreakerField),
|
||||
[]
|
||||
),
|
||||
});
|
||||
|
||||
export const useLogIndicesConfigurationFormState = ({
|
||||
initialFormState = defaultFormState,
|
||||
}: {
|
||||
initialFormState?: FormState;
|
||||
}) => {
|
||||
const [formStateChanges, setFormStateChanges] = useState<FormStateChanges>({});
|
||||
const timestampFieldFormElement = useFormElement<string, FormValidationError>({
|
||||
initialValue: initialValues.timestampField,
|
||||
validate: useMemo(
|
||||
() => async (timestampField) => validateStringNotEmpty('timestamp', timestampField),
|
||||
[]
|
||||
),
|
||||
});
|
||||
|
||||
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,
|
||||
const fieldsFormElement = useCompositeFormElement(
|
||||
useMemo(
|
||||
() => ({
|
||||
childFormElements: {
|
||||
tiebreaker: tiebreakerFieldFormElement,
|
||||
timestamp: timestampFieldFormElement,
|
||||
},
|
||||
}),
|
||||
[formState.name]
|
||||
[tiebreakerFieldFormElement, timestampFieldFormElement]
|
||||
)
|
||||
);
|
||||
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,
|
||||
fieldsFormElement,
|
||||
tiebreakerFieldFormElement,
|
||||
timestampFieldFormElement,
|
||||
};
|
||||
};
|
||||
|
||||
const defaultFormState: FormState = {
|
||||
name: '',
|
||||
description: '',
|
||||
logAlias: '',
|
||||
tiebreakerField: '',
|
||||
timestampField: '',
|
||||
};
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiCodeBlock, EuiPage, EuiPageBody, EuiPageContent, PropsOf } from '@elastic/eui';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { Meta, Story } from '@storybook/react/types-6-0';
|
||||
import React from 'react';
|
||||
import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public';
|
||||
import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common';
|
||||
import {
|
||||
MockIndexPatternsKibanaContextProvider,
|
||||
MockIndexPatternSpec,
|
||||
} from '../../../hooks/use_kibana_index_patterns.mock';
|
||||
import {
|
||||
FieldsFormState,
|
||||
LogIndicesFormState,
|
||||
useFieldsFormElement,
|
||||
useLogIndicesFormElement,
|
||||
} from './indices_configuration_form_state';
|
||||
import { IndicesConfigurationPanel } from './indices_configuration_panel';
|
||||
|
||||
export default {
|
||||
title: 'infra/logsSettings/indicesConfiguration',
|
||||
decorators: [
|
||||
(WrappedStory, { args }) => {
|
||||
return (
|
||||
<I18nProvider>
|
||||
<EuiThemeProvider>
|
||||
<MockIndexPatternsKibanaContextProvider
|
||||
asyncDelay={2000}
|
||||
mockIndexPatterns={args.availableIndexPatterns}
|
||||
>
|
||||
<EuiPage restrictWidth>
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<WrappedStory />
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
</MockIndexPatternsKibanaContextProvider>
|
||||
</EuiThemeProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
},
|
||||
],
|
||||
argTypes: {
|
||||
logIndices: {
|
||||
control: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
availableIndexPatterns: {
|
||||
control: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
type IndicesConfigurationPanelProps = PropsOf<typeof IndicesConfigurationPanel>;
|
||||
|
||||
type IndicesConfigurationPanelStoryArgs = Pick<
|
||||
IndicesConfigurationPanelProps,
|
||||
'isLoading' | 'isReadOnly'
|
||||
> & {
|
||||
availableIndexPatterns: MockIndexPatternSpec[];
|
||||
logIndices: LogIndicesFormState;
|
||||
fields: FieldsFormState;
|
||||
};
|
||||
|
||||
const IndicesConfigurationPanelTemplate: Story<IndicesConfigurationPanelStoryArgs> = ({
|
||||
isLoading,
|
||||
isReadOnly,
|
||||
logIndices,
|
||||
fields,
|
||||
}) => {
|
||||
const logIndicesFormElement = useLogIndicesFormElement(logIndices);
|
||||
const { tiebreakerFieldFormElement, timestampFieldFormElement } = useFieldsFormElement(fields);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IndicesConfigurationPanel
|
||||
isLoading={isLoading}
|
||||
isReadOnly={isReadOnly}
|
||||
indicesFormElement={logIndicesFormElement}
|
||||
tiebreakerFieldFormElement={tiebreakerFieldFormElement}
|
||||
timestampFieldFormElement={timestampFieldFormElement}
|
||||
/>
|
||||
<EuiCodeBlock language="json">
|
||||
// field states{'\n'}
|
||||
{JSON.stringify(
|
||||
{
|
||||
logIndices: {
|
||||
value: logIndicesFormElement.value,
|
||||
validity: logIndicesFormElement.validity,
|
||||
},
|
||||
tiebreakerField: {
|
||||
value: tiebreakerFieldFormElement.value,
|
||||
validity: tiebreakerFieldFormElement.validity,
|
||||
},
|
||||
timestampField: {
|
||||
value: timestampFieldFormElement.value,
|
||||
validity: timestampFieldFormElement.validity,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</EuiCodeBlock>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultArgs: IndicesConfigurationPanelStoryArgs = {
|
||||
isLoading: false,
|
||||
isReadOnly: false,
|
||||
logIndices: {
|
||||
type: 'index_name' as const,
|
||||
indexName: 'logs-*',
|
||||
},
|
||||
fields: {
|
||||
tiebreakerField: '_doc',
|
||||
timestampField: '@timestamp',
|
||||
},
|
||||
availableIndexPatterns: [
|
||||
{
|
||||
id: 'INDEX_PATTERN_A',
|
||||
title: 'pattern-a-*',
|
||||
timeFieldName: '@timestamp',
|
||||
fields: [
|
||||
{
|
||||
name: '@timestamp',
|
||||
type: KBN_FIELD_TYPES.DATE,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: KBN_FIELD_TYPES.STRING,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'INDEX_PATTERN_B',
|
||||
title: 'pattern-b-*',
|
||||
fields: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const IndexNameWithDefaultFields = IndicesConfigurationPanelTemplate.bind({});
|
||||
|
||||
IndexNameWithDefaultFields.args = {
|
||||
...defaultArgs,
|
||||
};
|
||||
|
||||
export const IndexPattern = IndicesConfigurationPanelTemplate.bind({});
|
||||
|
||||
IndexPattern.args = {
|
||||
...defaultArgs,
|
||||
logIndices: undefined,
|
||||
};
|
|
@ -5,85 +5,77 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useUiTracker } from '../../../../../observability/public';
|
||||
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/input_fields';
|
||||
logIndexNameReferenceRT,
|
||||
LogIndexPatternReference,
|
||||
logIndexPatternReferenceRT,
|
||||
LogIndexReference,
|
||||
} from '../../../../common/log_sources';
|
||||
import { FieldsConfigurationPanel } from './fields_configuration_panel';
|
||||
import { FormElement, isFormElementForType } from './form_elements';
|
||||
import { IndexNamesConfigurationPanel } from './index_names_configuration_panel';
|
||||
import { IndexPatternConfigurationPanel } from './index_pattern_configuration_panel';
|
||||
import { FormValidationError } from './validation_errors';
|
||||
|
||||
interface IndicesConfigurationPanelProps {
|
||||
export const IndicesConfigurationPanel = React.memo<{
|
||||
isLoading: boolean;
|
||||
readOnly: boolean;
|
||||
logAliasFieldProps: InputFieldProps;
|
||||
}
|
||||
isReadOnly: boolean;
|
||||
indicesFormElement: FormElement<LogIndexReference | undefined, FormValidationError>;
|
||||
tiebreakerFieldFormElement: FormElement<string, FormValidationError>;
|
||||
timestampFieldFormElement: FormElement<string, FormValidationError>;
|
||||
}>(
|
||||
({
|
||||
isLoading,
|
||||
isReadOnly,
|
||||
indicesFormElement,
|
||||
tiebreakerFieldFormElement,
|
||||
timestampFieldFormElement,
|
||||
}) => {
|
||||
const trackSwitchToIndexPatternReference = useUiTracker({ app: 'infra_logs' });
|
||||
|
||||
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>logs-*,filebeat-*</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
isInvalid={logAliasFieldProps.isInvalid}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.logIndicesLabel"
|
||||
defaultMessage="Log indices"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="logIndicesInput"
|
||||
fullWidth
|
||||
disabled={isLoading}
|
||||
const switchToIndexPatternReference = useCallback(() => {
|
||||
indicesFormElement.updateValue(() => undefined);
|
||||
trackSwitchToIndexPatternReference({
|
||||
metric: 'configuration_switch_to_index_pattern_reference',
|
||||
});
|
||||
}, [indicesFormElement, trackSwitchToIndexPatternReference]);
|
||||
|
||||
if (isIndexPatternFormElement(indicesFormElement)) {
|
||||
return (
|
||||
<IndexPatternConfigurationPanel
|
||||
isLoading={isLoading}
|
||||
readOnly={readOnly}
|
||||
{...logAliasFieldProps}
|
||||
isReadOnly={isReadOnly}
|
||||
indexPatternFormElement={indicesFormElement}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</EuiForm>
|
||||
);
|
||||
} else if (isIndexNamesFormElement(indicesFormElement)) {
|
||||
return (
|
||||
<>
|
||||
<IndexNamesConfigurationPanel
|
||||
isLoading={isLoading}
|
||||
isReadOnly={isReadOnly}
|
||||
indexNamesFormElement={indicesFormElement}
|
||||
onSwitchToIndexPatternReference={switchToIndexPatternReference}
|
||||
/>
|
||||
<FieldsConfigurationPanel
|
||||
isLoading={isLoading}
|
||||
isReadOnly={isReadOnly}
|
||||
tiebreakerFieldFormElement={tiebreakerFieldFormElement}
|
||||
timestampFieldFormElement={timestampFieldFormElement}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const isIndexPatternFormElement = isFormElementForType(
|
||||
(value): value is LogIndexPatternReference | undefined =>
|
||||
value == null || logIndexPatternReferenceRT.is(value)
|
||||
);
|
||||
|
||||
const isIndexNamesFormElement = isFormElementForType(logIndexNameReferenceRT.is);
|
||||
|
|
|
@ -5,150 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
FieldLogColumnConfiguration,
|
||||
isMessageLogColumnConfiguration,
|
||||
isTimestampLogColumnConfiguration,
|
||||
LogColumnConfiguration,
|
||||
MessageLogColumnConfiguration,
|
||||
TimestampLogColumnConfiguration,
|
||||
} from '../../../utils/source_configuration';
|
||||
import { useMemo } from 'react';
|
||||
import { LogColumnConfiguration } from '../../../utils/source_configuration';
|
||||
import { useFormElement } from './form_elements';
|
||||
import { FormValidationError, validateColumnListNotEmpty } from './validation_errors';
|
||||
|
||||
export interface TimestampLogColumnConfigurationProps {
|
||||
logColumnConfiguration: TimestampLogColumnConfiguration['timestampColumn'];
|
||||
remove: () => void;
|
||||
type: 'timestamp';
|
||||
}
|
||||
export const useLogColumnsFormElement = (initialValue: LogColumnConfiguration[]) => {
|
||||
const logColumnsFormElement = useFormElement<LogColumnConfiguration[], FormValidationError>({
|
||||
initialValue,
|
||||
validate: useMemo(() => async (logColumns) => validateColumnListNotEmpty(logColumns), []),
|
||||
});
|
||||
|
||||
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: [],
|
||||
return logColumnsFormElement;
|
||||
};
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiIcon,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
|
@ -24,28 +23,54 @@ 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';
|
||||
FieldLogColumnConfiguration,
|
||||
getLogColumnConfigurationId,
|
||||
isMessageLogColumnConfiguration,
|
||||
isTimestampLogColumnConfiguration,
|
||||
LogColumnConfiguration,
|
||||
MessageLogColumnConfiguration,
|
||||
TimestampLogColumnConfiguration,
|
||||
} from '../../../utils/source_configuration';
|
||||
import { AddLogColumnButtonAndPopover } from './add_log_column_popover';
|
||||
import { FormElement } from './form_elements';
|
||||
import { LogSourceConfigurationFormError } from './source_configuration_form_errors';
|
||||
import { FormValidationError } from './validation_errors';
|
||||
|
||||
interface LogColumnsConfigurationPanelProps {
|
||||
export const LogColumnsConfigurationPanel = React.memo<{
|
||||
availableFields: string[];
|
||||
isLoading: boolean;
|
||||
logColumnConfiguration: LogColumnConfigurationProps[];
|
||||
addLogColumn: (logColumn: LogColumnConfiguration) => void;
|
||||
moveLogColumn: (sourceIndex: number, destinationIndex: number) => void;
|
||||
}
|
||||
logColumnsFormElement: FormElement<LogColumnConfiguration[], FormValidationError>;
|
||||
}>(({ availableFields, isLoading, logColumnsFormElement }) => {
|
||||
const addLogColumn = useCallback(
|
||||
(logColumnConfiguration: LogColumnConfiguration) =>
|
||||
logColumnsFormElement.updateValue((logColumns) => [...logColumns, logColumnConfiguration]),
|
||||
[logColumnsFormElement]
|
||||
);
|
||||
|
||||
const removeLogColumn = useCallback(
|
||||
(logColumn: LogColumnConfiguration) =>
|
||||
logColumnsFormElement.updateValue((logColumns) =>
|
||||
logColumns.filter((item) => item !== logColumn)
|
||||
),
|
||||
[logColumnsFormElement]
|
||||
);
|
||||
|
||||
const moveLogColumn = useCallback(
|
||||
(sourceIndex, destinationIndex) => {
|
||||
logColumnsFormElement.updateValue((logColumns) => {
|
||||
if (destinationIndex >= 0 && sourceIndex <= logColumnsFormElement.value.length - 1) {
|
||||
const newLogColumns = [...logColumnsFormElement.value];
|
||||
newLogColumns.splice(destinationIndex, 0, newLogColumns.splice(sourceIndex, 1)[0]);
|
||||
return newLogColumns;
|
||||
} else {
|
||||
return logColumns;
|
||||
}
|
||||
});
|
||||
},
|
||||
[logColumnsFormElement]
|
||||
);
|
||||
|
||||
export const LogColumnsConfigurationPanel: React.FunctionComponent<LogColumnsConfigurationPanelProps> = ({
|
||||
addLogColumn,
|
||||
moveLogColumn,
|
||||
availableFields,
|
||||
isLoading,
|
||||
logColumnConfiguration,
|
||||
}) => {
|
||||
const onDragEnd = useCallback(
|
||||
({ source, destination }: DropResult) =>
|
||||
destination && moveLogColumn(source.index, destination.index),
|
||||
|
@ -53,7 +78,7 @@ export const LogColumnsConfigurationPanel: React.FunctionComponent<LogColumnsCon
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s" data-test-subj="sourceConfigurationLogColumnsSectionTitle">
|
||||
|
@ -73,63 +98,89 @@ export const LogColumnsConfigurationPanel: React.FunctionComponent<LogColumnsCon
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{logColumnConfiguration.length > 0 ? (
|
||||
{logColumnsFormElement.value.length > 0 ? (
|
||||
<EuiDragDropContext onDragEnd={onDragEnd}>
|
||||
<EuiDroppable droppableId="COLUMN_CONFIG_DROPPABLE_AREA">
|
||||
<>
|
||||
{/* Fragment here necessary for typechecking */}
|
||||
{logColumnConfiguration.map((column, index) => (
|
||||
{logColumnsFormElement.value.map((logColumnConfiguration, index) => {
|
||||
const columnId = getLogColumnConfigurationId(logColumnConfiguration);
|
||||
return (
|
||||
<EuiDraggable
|
||||
key={`logColumnConfigurationPanel-${column.logColumnConfiguration.id}`}
|
||||
key={`logColumnConfigurationPanel-${columnId}`}
|
||||
index={index}
|
||||
draggableId={column.logColumnConfiguration.id}
|
||||
draggableId={columnId}
|
||||
customDragHandle
|
||||
>
|
||||
{(provided) => (
|
||||
<LogColumnConfigurationPanel
|
||||
dragHandleProps={provided.dragHandleProps}
|
||||
logColumnConfigurationProps={column}
|
||||
logColumnConfiguration={logColumnConfiguration}
|
||||
onRemove={removeLogColumn}
|
||||
/>
|
||||
)}
|
||||
</EuiDraggable>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</EuiDroppable>
|
||||
</EuiDragDropContext>
|
||||
) : (
|
||||
<LogColumnConfigurationEmptyPrompt />
|
||||
)}
|
||||
</EuiForm>
|
||||
{logColumnsFormElement.validity.validity === 'invalid'
|
||||
? logColumnsFormElement.validity.reasons.map((error) => (
|
||||
<EuiText key={error.type} textAlign="center" color="danger">
|
||||
<LogSourceConfigurationFormError error={error} />
|
||||
</EuiText>
|
||||
))
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const LogColumnConfigurationPanel: React.FunctionComponent<{
|
||||
logColumnConfiguration: LogColumnConfiguration;
|
||||
dragHandleProps: DragHandleProps;
|
||||
onRemove: (logColumnConfiguration: LogColumnConfiguration) => void;
|
||||
}> = ({ logColumnConfiguration, dragHandleProps, onRemove }) => {
|
||||
const removeColumn = useCallback(() => onRemove(logColumnConfiguration), [
|
||||
logColumnConfiguration,
|
||||
onRemove,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
{isTimestampLogColumnConfiguration(logColumnConfiguration) ? (
|
||||
<TimestampLogColumnConfigurationPanel
|
||||
dragHandleProps={dragHandleProps}
|
||||
logColumnConfiguration={logColumnConfiguration}
|
||||
onRemove={removeColumn}
|
||||
/>
|
||||
) : isMessageLogColumnConfiguration(logColumnConfiguration) ? (
|
||||
<MessageLogColumnConfigurationPanel
|
||||
dragHandleProps={dragHandleProps}
|
||||
logColumnConfiguration={logColumnConfiguration}
|
||||
onRemove={removeColumn}
|
||||
/>
|
||||
) : (
|
||||
<FieldLogColumnConfigurationPanel
|
||||
dragHandleProps={dragHandleProps}
|
||||
logColumnConfiguration={logColumnConfiguration}
|
||||
onRemove={removeColumn}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface LogColumnConfigurationPanelProps {
|
||||
logColumnConfigurationProps: LogColumnConfigurationProps;
|
||||
interface LogColumnConfigurationPanelProps<LogColumnConfigurationType> {
|
||||
logColumnConfiguration: LogColumnConfigurationType;
|
||||
dragHandleProps: DragHandleProps;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
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,
|
||||
}) => (
|
||||
const TimestampLogColumnConfigurationPanel: React.FunctionComponent<
|
||||
LogColumnConfigurationPanelProps<TimestampLogColumnConfiguration>
|
||||
> = ({ dragHandleProps, onRemove }) => (
|
||||
<ExplainedLogColumnConfigurationPanel
|
||||
fieldName="Timestamp"
|
||||
helpText={
|
||||
|
@ -142,15 +193,14 @@ const TimestampLogColumnConfigurationPanel: React.FunctionComponent<LogColumnCon
|
|||
}}
|
||||
/>
|
||||
}
|
||||
removeColumn={logColumnConfigurationProps.remove}
|
||||
onRemove={onRemove}
|
||||
dragHandleProps={dragHandleProps}
|
||||
/>
|
||||
);
|
||||
|
||||
const MessageLogColumnConfigurationPanel: React.FunctionComponent<LogColumnConfigurationPanelProps> = ({
|
||||
logColumnConfigurationProps,
|
||||
dragHandleProps,
|
||||
}) => (
|
||||
const MessageLogColumnConfigurationPanel: React.FunctionComponent<
|
||||
LogColumnConfigurationPanelProps<MessageLogColumnConfiguration>
|
||||
> = ({ dragHandleProps, onRemove }) => (
|
||||
<ExplainedLogColumnConfigurationPanel
|
||||
fieldName="Message"
|
||||
helpText={
|
||||
|
@ -160,29 +210,26 @@ const MessageLogColumnConfigurationPanel: React.FunctionComponent<LogColumnConfi
|
|||
defaultMessage="This system field shows the log entry message as derived from the document fields."
|
||||
/>
|
||||
}
|
||||
removeColumn={logColumnConfigurationProps.remove}
|
||||
onRemove={onRemove}
|
||||
dragHandleProps={dragHandleProps}
|
||||
/>
|
||||
);
|
||||
|
||||
const FieldLogColumnConfigurationPanel: React.FunctionComponent<{
|
||||
logColumnConfigurationProps: FieldLogColumnConfigurationProps;
|
||||
dragHandleProps: DragHandleProps;
|
||||
}> = ({
|
||||
logColumnConfigurationProps: {
|
||||
logColumnConfiguration: { field },
|
||||
remove,
|
||||
},
|
||||
const FieldLogColumnConfigurationPanel: React.FunctionComponent<
|
||||
LogColumnConfigurationPanelProps<FieldLogColumnConfiguration>
|
||||
> = ({
|
||||
dragHandleProps,
|
||||
logColumnConfiguration: {
|
||||
fieldColumn: { field },
|
||||
},
|
||||
onRemove,
|
||||
}) => {
|
||||
const fieldLogColumnTitle = i18n.translate(
|
||||
'xpack.infra.sourceConfiguration.fieldLogColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Field',
|
||||
}
|
||||
);
|
||||
return (
|
||||
<EuiPanel data-test-subj={`logColumnPanel fieldLogColumnPanel fieldLogColumnPanel:${field}`}>
|
||||
<EuiPanel
|
||||
color="subdued"
|
||||
data-test-subj={`logColumnPanel fieldLogColumnPanel fieldLogColumnPanel:${field}`}
|
||||
hasShadow={false}
|
||||
>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<div data-test-subj="moveLogColumnHandle" {...dragHandleProps}>
|
||||
|
@ -195,7 +242,7 @@ const FieldLogColumnConfigurationPanel: React.FunctionComponent<{
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RemoveLogColumnButton
|
||||
onClick={remove}
|
||||
onClick={onRemove}
|
||||
columnDescription={`${fieldLogColumnTitle} - ${field}`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -207,11 +254,13 @@ const FieldLogColumnConfigurationPanel: React.FunctionComponent<{
|
|||
const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{
|
||||
fieldName: React.ReactNode;
|
||||
helpText: React.ReactNode;
|
||||
removeColumn: () => void;
|
||||
onRemove: () => void;
|
||||
dragHandleProps: DragHandleProps;
|
||||
}> = ({ fieldName, helpText, removeColumn, dragHandleProps }) => (
|
||||
}> = ({ fieldName, helpText, onRemove, dragHandleProps }) => (
|
||||
<EuiPanel
|
||||
color="subdued"
|
||||
data-test-subj={`logColumnPanel systemLogColumnPanel systemLogColumnPanel:${fieldName}`}
|
||||
hasShadow={false}
|
||||
>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -226,7 +275,7 @@ const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{
|
|||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RemoveLogColumnButton onClick={removeColumn} columnDescription={String(fieldName)} />
|
||||
<RemoveLogColumnButton onClick={onRemove} columnDescription={String(fieldName)} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
|
@ -277,3 +326,7 @@ const LogColumnConfigurationEmptyPrompt: React.FunctionComponent = () => (
|
|||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const fieldLogColumnTitle = i18n.translate('xpack.infra.sourceConfiguration.fieldLogColumnTitle', {
|
||||
defaultMessage: 'Field',
|
||||
});
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useFormElement } from './form_elements';
|
||||
import { FormValidationError, validateStringNotEmpty } from './validation_errors';
|
||||
|
||||
export const useNameFormElement = (initialValue: string) => {
|
||||
const nameFormElement = useFormElement<string, FormValidationError>({
|
||||
initialValue,
|
||||
validate: useMemo(() => async (name) => validateStringNotEmpty('name', name), []),
|
||||
});
|
||||
|
||||
return nameFormElement;
|
||||
};
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiDescribedFormGroup,
|
||||
EuiFieldText,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { FormElement } from './form_elements';
|
||||
import { getFormRowProps, getStringInputFieldProps } from './form_field_props';
|
||||
import { FormValidationError } from './validation_errors';
|
||||
|
||||
export const NameConfigurationPanel = React.memo<{
|
||||
isLoading: boolean;
|
||||
isReadOnly: boolean;
|
||||
nameFormElement: FormElement<string, FormValidationError>;
|
||||
}>(({ isLoading, isReadOnly, nameFormElement }) => (
|
||||
<EuiForm>
|
||||
<EuiTitle size="s" data-test-subj="sourceConfigurationNameSectionTitle">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.nameSectionTitle"
|
||||
defaultMessage="Name"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<h4>
|
||||
<FormattedMessage id="xpack.infra.sourceConfiguration.nameLabel" defaultMessage="Name" />
|
||||
</h4>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.nameDescription"
|
||||
defaultMessage="A descriptive name for the source configuration"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage id="xpack.infra.sourceConfiguration.nameLabel" defaultMessage="Name" />
|
||||
}
|
||||
{...useMemo(() => getFormRowProps(nameFormElement), [nameFormElement])}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="nameInput"
|
||||
fullWidth
|
||||
disabled={isLoading}
|
||||
readOnly={isReadOnly}
|
||||
isLoading={isLoading}
|
||||
name="name"
|
||||
{...useMemo(() => getStringInputFieldProps(nameFormElement), [nameFormElement])}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</EuiForm>
|
||||
));
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiCallOut, EuiCode } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { FormValidationError } from './validation_errors';
|
||||
|
||||
export const LogSourceConfigurationFormErrors: React.FC<{ errors: FormValidationError[] }> = ({
|
||||
errors,
|
||||
}) => (
|
||||
<EuiCallOut color="danger" iconType="alert" title={logSourceConfigurationFormErrorsCalloutTitle}>
|
||||
<ul>
|
||||
{errors.map((error, errorIndex) => (
|
||||
<li key={errorIndex}>
|
||||
<LogSourceConfigurationFormError error={error} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</EuiCallOut>
|
||||
);
|
||||
|
||||
export const LogSourceConfigurationFormError: React.FC<{ error: FormValidationError }> = ({
|
||||
error,
|
||||
}) => {
|
||||
if (error.type === 'generic') {
|
||||
return <>{error.message}</>;
|
||||
} else if (error.type === 'empty_field') {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logSourceConfiguration.emptyFieldErrorMessage"
|
||||
defaultMessage="The field '{fieldName}' must not be empty."
|
||||
values={{
|
||||
fieldName: error.fieldName,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (error.type === 'empty_column_list') {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logSourceConfiguration.emptyColumnListErrorMessage"
|
||||
defaultMessage="The column list must not be empty."
|
||||
/>
|
||||
);
|
||||
} else if (error.type === 'child') {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logSourceConfiguration.childFormElementErrorMessage"
|
||||
defaultMessage="At least one form field is in an invalid state."
|
||||
/>
|
||||
);
|
||||
} else if (error.type === 'missing_timestamp_field') {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logSourceConfiguration.missingTimestampFieldErrorMessage"
|
||||
defaultMessage="The index pattern must be time-based."
|
||||
/>
|
||||
);
|
||||
} else if (error.type === 'missing_message_field') {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logSourceConfiguration.missingMessageFieldErrorMessage"
|
||||
defaultMessage="The index pattern must contain a {messageField} field."
|
||||
values={{
|
||||
messageField: <EuiCode>message</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (error.type === 'invalid_message_field_type') {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logSourceConfiguration.invalidMessageFieldTypeErrorMessage"
|
||||
defaultMessage="The {messageField} field must be a text field."
|
||||
values={{
|
||||
messageField: <EuiCode>message</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (error.type === 'rollup_index_pattern') {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logSourceConfiguration.rollupIndexPatternErrorMessage"
|
||||
defaultMessage="The index pattern must not be a rollup index pattern."
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const logSourceConfigurationFormErrorsCalloutTitle = i18n.translate(
|
||||
'xpack.infra.logSourceConfiguration.logSourceConfigurationFormErrorsCalloutTitle',
|
||||
{
|
||||
defaultMessage: 'Inconsistent source configuration',
|
||||
}
|
||||
);
|
|
@ -5,103 +5,69 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { ResolvedLogSourceConfiguration } from '../../../../common/log_sources';
|
||||
import { useLogIndicesConfigurationFormState } from './indices_configuration_form_state';
|
||||
import { useLogColumnsConfigurationFormState } from './log_columns_configuration_form_state';
|
||||
import { useMemo } from 'react';
|
||||
import { LogSourceConfigurationProperties } from '../../../containers/logs/log_source';
|
||||
import { useCompositeFormElement } from './form_elements';
|
||||
import { useFieldsFormElement, useLogIndicesFormElement } from './indices_configuration_form_state';
|
||||
import { useLogColumnsFormElement } from './log_columns_configuration_form_state';
|
||||
import { useNameFormElement } from './name_configuration_form_state';
|
||||
|
||||
export const useLogSourceConfigurationFormState = (
|
||||
configuration?: ResolvedLogSourceConfiguration
|
||||
configuration?: LogSourceConfigurationProperties
|
||||
) => {
|
||||
const indicesConfigurationFormState = useLogIndicesConfigurationFormState({
|
||||
initialFormState: useMemo(
|
||||
const nameFormElement = useNameFormElement(configuration?.name ?? '');
|
||||
|
||||
const logIndicesFormElement = useLogIndicesFormElement(
|
||||
useMemo(
|
||||
() =>
|
||||
configuration
|
||||
? {
|
||||
name: configuration.name,
|
||||
description: configuration.description,
|
||||
logAlias: configuration.indices,
|
||||
tiebreakerField: configuration.tiebreakerField,
|
||||
timestampField: configuration.timestampField,
|
||||
}
|
||||
: undefined,
|
||||
configuration?.logIndices ?? {
|
||||
type: 'index_name',
|
||||
indexName: '',
|
||||
},
|
||||
[configuration]
|
||||
),
|
||||
});
|
||||
)
|
||||
);
|
||||
|
||||
const logColumnsConfigurationFormState = useLogColumnsConfigurationFormState({
|
||||
initialFormState: useMemo(
|
||||
() =>
|
||||
configuration
|
||||
? {
|
||||
logColumns: configuration.columns,
|
||||
}
|
||||
: undefined,
|
||||
const {
|
||||
fieldsFormElement,
|
||||
tiebreakerFieldFormElement,
|
||||
timestampFieldFormElement,
|
||||
} = useFieldsFormElement(
|
||||
useMemo(
|
||||
() => ({
|
||||
tiebreakerField: configuration?.fields?.tiebreaker ?? '_doc',
|
||||
timestampField: configuration?.fields?.timestamp ?? '@timestamp',
|
||||
}),
|
||||
[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 logColumnsFormElement = useLogColumnsFormElement(
|
||||
useMemo(() => configuration?.logColumns ?? [], [configuration])
|
||||
);
|
||||
|
||||
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,
|
||||
]
|
||||
const sourceConfigurationFormElement = useCompositeFormElement(
|
||||
useMemo(
|
||||
() => ({
|
||||
childFormElements: {
|
||||
name: nameFormElement,
|
||||
logIndices: logIndicesFormElement,
|
||||
fields: fieldsFormElement,
|
||||
logColumns: logColumnsFormElement,
|
||||
},
|
||||
validate: async () => [],
|
||||
}),
|
||||
[nameFormElement, logIndicesFormElement, fieldsFormElement, logColumnsFormElement]
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
addLogColumn: logColumnsConfigurationFormState.addLogColumn,
|
||||
moveLogColumn: logColumnsConfigurationFormState.moveLogColumn,
|
||||
errors,
|
||||
formState,
|
||||
formStateChanges,
|
||||
isFormDirty,
|
||||
isFormValid,
|
||||
indicesConfigurationProps: indicesConfigurationFormState.fieldProps,
|
||||
logColumnConfigurationProps: logColumnsConfigurationFormState.logColumnConfigurationProps,
|
||||
resetForm,
|
||||
formState: sourceConfigurationFormElement.value,
|
||||
logIndicesFormElement,
|
||||
logColumnsFormElement,
|
||||
nameFormElement,
|
||||
sourceConfigurationFormElement,
|
||||
tiebreakerFieldFormElement,
|
||||
timestampFieldFormElement,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -7,33 +7,40 @@
|
|||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiErrorBoundary,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContentBody,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
} 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 { useTrackPageview } from '../../../../../observability/public';
|
||||
import { SourceLoadingPage } from '../../../components/source_loading_page';
|
||||
import { useLogSourceContext } from '../../../containers/logs/log_source';
|
||||
import { Prompt } from '../../../utils/navigation_warning_prompt';
|
||||
import { LogSourceConfigurationPropertiesPatch } from '../../../../common/http_api/log_sources';
|
||||
import { IndicesConfigurationPanel } from './indices_configuration_panel';
|
||||
import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel';
|
||||
import { NameConfigurationPanel } from './name_configuration_panel';
|
||||
import { LogSourceConfigurationFormErrors } from './source_configuration_form_errors';
|
||||
import { useLogSourceConfigurationFormState } from './source_configuration_form_state';
|
||||
|
||||
export const LogsSettingsPage = () => {
|
||||
const uiCapabilities = useKibana().services.application?.capabilities;
|
||||
const shouldAllowEdit = uiCapabilities?.logs?.configureSource === true;
|
||||
|
||||
useTrackPageview({ app: 'infra_logs', path: 'log_source_configuration' });
|
||||
useTrackPageview({
|
||||
app: 'infra_logs',
|
||||
path: 'log_source_configuration',
|
||||
delay: 15000,
|
||||
});
|
||||
|
||||
const {
|
||||
sourceConfiguration: source,
|
||||
isLoading,
|
||||
|
@ -48,35 +55,19 @@ export const LogsSettingsPage = () => {
|
|||
);
|
||||
|
||||
const {
|
||||
addLogColumn,
|
||||
moveLogColumn,
|
||||
indicesConfigurationProps,
|
||||
logColumnConfigurationProps,
|
||||
errors,
|
||||
resetForm,
|
||||
isFormDirty,
|
||||
isFormValid,
|
||||
formStateChanges,
|
||||
} = useLogSourceConfigurationFormState(resolvedSourceConfiguration);
|
||||
sourceConfigurationFormElement,
|
||||
formState,
|
||||
logIndicesFormElement,
|
||||
logColumnsFormElement,
|
||||
nameFormElement,
|
||||
tiebreakerFieldFormElement,
|
||||
timestampFieldFormElement,
|
||||
} = useLogSourceConfigurationFormState(source?.configuration);
|
||||
|
||||
const persistUpdates = useCallback(async () => {
|
||||
// NOTE / TODO: This is just a temporary workaround until this work is merged with the corresponding UI branch.
|
||||
// Otherwise we would be duplicating work changing the logAlias etc references twice.
|
||||
const patchedProperties: LogSourceConfigurationPropertiesPatch & { logAlias?: string } = {
|
||||
...formStateChanges,
|
||||
...(formStateChanges.logAlias
|
||||
? {
|
||||
logIndices: {
|
||||
type: 'index_name',
|
||||
indexName: formStateChanges.logAlias,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
delete patchedProperties.logAlias;
|
||||
await updateSourceConfiguration(patchedProperties);
|
||||
resetForm();
|
||||
}, [updateSourceConfiguration, resetForm, formStateChanges]);
|
||||
await updateSourceConfiguration(formState);
|
||||
sourceConfigurationFormElement.resetValue();
|
||||
}, [updateSourceConfiguration, sourceConfigurationFormElement, formState]);
|
||||
|
||||
const isWriteable = useMemo(() => shouldAllowEdit && source && source.origin !== 'internal', [
|
||||
shouldAllowEdit,
|
||||
|
@ -92,110 +83,100 @@ export const LogsSettingsPage = () => {
|
|||
|
||||
return (
|
||||
<EuiErrorBoundary>
|
||||
<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}
|
||||
<EuiPage style={{ flex: '1 0 0%' }}>
|
||||
<EuiPageBody data-test-subj="sourceConfigurationContent" restrictWidth>
|
||||
<EuiPageContentBody>
|
||||
<Prompt
|
||||
prompt={sourceConfigurationFormElement.isDirty ? unsavedFormPromptMessage : undefined}
|
||||
/>
|
||||
</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>
|
||||
) : (
|
||||
<>
|
||||
<EuiPanel paddingSize="l">
|
||||
<NameConfigurationPanel
|
||||
isLoading={isLoading}
|
||||
isReadOnly={!isWriteable}
|
||||
nameFormElement={nameFormElement}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer />
|
||||
<EuiPanel paddingSize="l">
|
||||
<IndicesConfigurationPanel
|
||||
isLoading={isLoading}
|
||||
isReadOnly={!isWriteable}
|
||||
indicesFormElement={logIndicesFormElement}
|
||||
tiebreakerFieldFormElement={tiebreakerFieldFormElement}
|
||||
timestampFieldFormElement={timestampFieldFormElement}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer />
|
||||
<EuiPanel paddingSize="l">
|
||||
<LogColumnsConfigurationPanel
|
||||
availableFields={availableFields}
|
||||
isLoading={isLoading}
|
||||
logColumnsFormElement={logColumnsFormElement}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer />
|
||||
{sourceConfigurationFormElement.validity.validity === 'invalid' ? (
|
||||
<>
|
||||
<LogSourceConfigurationFormErrors
|
||||
errors={sourceConfigurationFormElement.validity.reasons}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
) : null}
|
||||
<EuiFlexGroup>
|
||||
{isWriteable && (
|
||||
<EuiFlexItem>
|
||||
{isLoading ? (
|
||||
<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 color="primary" isLoading fill>
|
||||
Loading
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="discardSettingsButton"
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
isDisabled={isLoading || !sourceConfigurationFormElement.isDirty}
|
||||
onClick={() => {
|
||||
sourceConfigurationFormElement.resetValue();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.discardSettingsButtonLabel"
|
||||
defaultMessage="Discard"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="applySettingsButton"
|
||||
color="primary"
|
||||
isDisabled={
|
||||
!sourceConfigurationFormElement.isDirty ||
|
||||
sourceConfigurationFormElement.validity.validity !== 'valid'
|
||||
}
|
||||
fill
|
||||
onClick={persistUpdates}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.sourceConfiguration.applySettingsButtonLabel"
|
||||
defaultMessage="Apply"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiPageContentBody>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
</EuiErrorBoundary>
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IndexPattern, KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public';
|
||||
|
||||
export interface GenericValidationError {
|
||||
type: 'generic';
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ChildFormValidationError {
|
||||
type: 'child';
|
||||
}
|
||||
|
||||
export interface EmptyFieldValidationError {
|
||||
type: 'empty_field';
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
export interface EmptyColumnListValidationError {
|
||||
type: 'empty_column_list';
|
||||
}
|
||||
|
||||
export interface MissingTimestampFieldValidationError {
|
||||
type: 'missing_timestamp_field';
|
||||
indexPatternTitle: string;
|
||||
}
|
||||
|
||||
export interface MissingMessageFieldValidationError {
|
||||
type: 'missing_message_field';
|
||||
indexPatternTitle: string;
|
||||
}
|
||||
|
||||
export interface InvalidMessageFieldTypeValidationError {
|
||||
type: 'invalid_message_field_type';
|
||||
indexPatternTitle: string;
|
||||
}
|
||||
|
||||
export interface RollupIndexPatternValidationError {
|
||||
type: 'rollup_index_pattern';
|
||||
indexPatternTitle: string;
|
||||
}
|
||||
|
||||
export type FormValidationError =
|
||||
| GenericValidationError
|
||||
| ChildFormValidationError
|
||||
| EmptyFieldValidationError
|
||||
| EmptyColumnListValidationError
|
||||
| MissingTimestampFieldValidationError
|
||||
| MissingMessageFieldValidationError
|
||||
| InvalidMessageFieldTypeValidationError
|
||||
| RollupIndexPatternValidationError;
|
||||
|
||||
export const validateStringNotEmpty = (fieldName: string, value: string): FormValidationError[] =>
|
||||
value === '' ? [{ type: 'empty_field', fieldName }] : [];
|
||||
|
||||
export const validateColumnListNotEmpty = (columns: unknown[]): FormValidationError[] =>
|
||||
columns.length <= 0 ? [{ type: 'empty_column_list' }] : [];
|
||||
|
||||
export const validateIndexPattern = (indexPattern: IndexPattern): FormValidationError[] => {
|
||||
return [
|
||||
...validateIndexPatternIsTimeBased(indexPattern),
|
||||
...validateIndexPatternHasStringMessageField(indexPattern),
|
||||
...validateIndexPatternIsntRollup(indexPattern),
|
||||
];
|
||||
};
|
||||
|
||||
export const validateIndexPatternIsTimeBased = (
|
||||
indexPattern: IndexPattern
|
||||
): FormValidationError[] =>
|
||||
indexPattern.isTimeBased()
|
||||
? []
|
||||
: [
|
||||
{
|
||||
type: 'missing_timestamp_field' as const,
|
||||
indexPatternTitle: indexPattern.title,
|
||||
},
|
||||
];
|
||||
|
||||
export const validateIndexPatternHasStringMessageField = (
|
||||
indexPattern: IndexPattern
|
||||
): FormValidationError[] => {
|
||||
const messageField = indexPattern.getFieldByName('message');
|
||||
|
||||
if (messageField == null) {
|
||||
return [
|
||||
{
|
||||
type: 'missing_message_field' as const,
|
||||
indexPatternTitle: indexPattern.title,
|
||||
},
|
||||
];
|
||||
} else if (messageField.type !== KBN_FIELD_TYPES.STRING) {
|
||||
return [
|
||||
{
|
||||
type: 'invalid_message_field_type' as const,
|
||||
indexPatternTitle: indexPattern.title,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const validateIndexPatternIsntRollup = (indexPattern: IndexPattern): FormValidationError[] =>
|
||||
indexPattern.type != null
|
||||
? [
|
||||
{
|
||||
type: 'rollup_index_pattern' as const,
|
||||
indexPatternTitle: indexPattern.title,
|
||||
},
|
||||
]
|
||||
: [];
|
|
@ -19,8 +19,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
|
||||
import { InputFieldProps } from '../../../components/source_configuration/input_fields';
|
||||
import { InputFieldProps } from './input_fields';
|
||||
|
||||
interface FieldsConfigurationPanelProps {
|
||||
containerFieldProps: InputFieldProps;
|
||||
|
|
|
@ -6,12 +6,11 @@
|
|||
*/
|
||||
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
createInputFieldProps,
|
||||
createInputRangeFieldProps,
|
||||
validateInputFieldNotEmpty,
|
||||
} from '../../../components/source_configuration/input_fields';
|
||||
} from './input_fields';
|
||||
|
||||
interface FormState {
|
||||
name: string;
|
||||
|
|
|
@ -16,9 +16,8 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
|
||||
import { METRICS_INDEX_PATTERN } from '../../../../common/constants';
|
||||
import { InputFieldProps } from '../../../components/source_configuration/input_fields';
|
||||
import { InputFieldProps } from './input_fields';
|
||||
|
||||
interface IndicesConfigurationPanelProps {
|
||||
isLoading: boolean;
|
||||
|
|
|
@ -5,15 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiTitle } from '@elastic/eui';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { EuiRange } from '@elastic/eui';
|
||||
import { EuiDescribedFormGroup } from '@elastic/eui';
|
||||
import { EuiForm } from '@elastic/eui';
|
||||
import {
|
||||
EuiDescribedFormGroup,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiRange,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { InputRangeFieldProps } from '../../../components/source_configuration/input_fields';
|
||||
import { InputRangeFieldProps } from './input_fields';
|
||||
|
||||
interface MLConfigurationPanelProps {
|
||||
isLoading: boolean;
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
|
||||
import { InputFieldProps } from './input_fields';
|
||||
|
||||
interface NameConfigurationPanelProps {
|
|
@ -10,24 +10,23 @@ import {
|
|||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
|
||||
import { SourceLoadingPage } from '../../../components/source_loading_page';
|
||||
import { Source } from '../../../containers/metrics_source';
|
||||
import { useInfraMLCapabilitiesContext } from '../../../containers/ml/infra_ml_capabilities';
|
||||
import { Prompt } from '../../../utils/navigation_warning_prompt';
|
||||
import { FieldsConfigurationPanel } from './fields_configuration_panel';
|
||||
import { IndicesConfigurationPanel } from './indices_configuration_panel';
|
||||
import { NameConfigurationPanel } from '../../../components/source_configuration/name_configuration_panel';
|
||||
import { useSourceConfigurationFormState } from './source_configuration_form_state';
|
||||
import { SourceLoadingPage } from '../../../components/source_loading_page';
|
||||
import { Prompt } from '../../../utils/navigation_warning_prompt';
|
||||
import { MLConfigurationPanel } from './ml_configuration_panel';
|
||||
import { useInfraMLCapabilitiesContext } from '../../../containers/ml/infra_ml_capabilities';
|
||||
import { NameConfigurationPanel } from './name_configuration_panel';
|
||||
import { useSourceConfigurationFormState } from './source_configuration_form_state';
|
||||
|
||||
interface SourceConfigurationSettingsProps {
|
||||
shouldAllowEdit: boolean;
|
||||
|
|
|
@ -31,3 +31,15 @@ export const isTimestampLogColumnConfiguration = (
|
|||
logColumnConfiguration: LogColumnConfiguration
|
||||
): logColumnConfiguration is TimestampLogColumnConfiguration =>
|
||||
logColumnConfiguration != null && 'timestampColumn' in logColumnConfiguration;
|
||||
|
||||
export const getLogColumnConfigurationId = (
|
||||
logColumnConfiguration: LogColumnConfiguration
|
||||
): string => {
|
||||
if (isTimestampLogColumnConfiguration(logColumnConfiguration)) {
|
||||
return logColumnConfiguration.timestampColumn.id;
|
||||
} else if (isMessageLogColumnConfiguration(logColumnConfiguration)) {
|
||||
return logColumnConfiguration.messageColumn.id;
|
||||
} else {
|
||||
return logColumnConfiguration.fieldColumn.id;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -11094,7 +11094,6 @@
|
|||
"xpack.infra.sourceConfiguration.hostNameFieldDescription": "ホストの識別に使用されるフィールドです",
|
||||
"xpack.infra.sourceConfiguration.hostNameFieldLabel": "ホスト名",
|
||||
"xpack.infra.sourceConfiguration.indicesSectionTitle": "インデックス",
|
||||
"xpack.infra.sourceConfiguration.logColumnListEmptyErrorMessage": "ログ列リストは未入力のままにできません。",
|
||||
"xpack.infra.sourceConfiguration.logColumnsSectionTitle": "ログ列",
|
||||
"xpack.infra.sourceConfiguration.logIndicesDescription": "ログデータを含む一致するインデックスのインデックスパターンです",
|
||||
"xpack.infra.sourceConfiguration.logIndicesLabel": "ログインデックス",
|
||||
|
|
|
@ -11248,7 +11248,6 @@
|
|||
"xpack.infra.sourceConfiguration.hostNameFieldDescription": "用于标识主机的字段",
|
||||
"xpack.infra.sourceConfiguration.hostNameFieldLabel": "主机名",
|
||||
"xpack.infra.sourceConfiguration.indicesSectionTitle": "索引",
|
||||
"xpack.infra.sourceConfiguration.logColumnListEmptyErrorMessage": "日志列列表不得为空。",
|
||||
"xpack.infra.sourceConfiguration.logColumnsSectionTitle": "日志列",
|
||||
"xpack.infra.sourceConfiguration.logIndicesDescription": "用于匹配包含日志数据的索引的索引模式",
|
||||
"xpack.infra.sourceConfiguration.logIndicesLabel": "日志索引",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue