[7.x] [Logs UI] Refactor source configuration as hook for consistent data flow (#34455) (#34870)

Backports the following commits to 7.x:
 - [Logs UI] Refactor source configuration as hook for consistent data flow  (#34455)
This commit is contained in:
Felix Stürmer 2019-04-10 19:39:14 +02:00 committed by GitHub
parent d22b156812
commit 13e6a4ad72
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 2144 additions and 1717 deletions

View file

@ -13,4 +13,11 @@ export const sharedFragments = {
tiebreaker
}
`,
InfraSourceFields: gql`
fragment InfraSourceFields on InfraSource {
id
version
updatedAt
}
`,
};

View file

@ -718,6 +718,92 @@ export namespace MetricsQuery {
};
}
export namespace CreateSourceConfigurationMutation {
export type Variables = {
sourceId: string;
sourceConfiguration: CreateSourceInput;
};
export type Mutation = {
__typename?: 'Mutation';
createSource: CreateSource;
};
export type CreateSource = {
__typename?: 'CreateSourceResult';
source: Source;
};
export type Source = {
__typename?: 'InfraSource';
configuration: Configuration;
status: Status;
} & InfraSourceFields.Fragment;
export type Configuration = SourceConfigurationFields.Fragment;
export type Status = SourceStatusFields.Fragment;
}
export namespace SourceQuery {
export type Variables = {
sourceId?: string | null;
};
export type Query = {
__typename?: 'Query';
source: Source;
};
export type Source = {
__typename?: 'InfraSource';
configuration: Configuration;
status: Status;
} & InfraSourceFields.Fragment;
export type Configuration = SourceConfigurationFields.Fragment;
export type Status = SourceStatusFields.Fragment;
}
export namespace UpdateSourceMutation {
export type Variables = {
sourceId?: string | null;
changes: UpdateSourceInput[];
};
export type Mutation = {
__typename?: 'Mutation';
updateSource: UpdateSource;
};
export type UpdateSource = {
__typename?: 'UpdateSourceResult';
source: Source;
};
export type Source = {
__typename?: 'InfraSource';
configuration: Configuration;
status: Status;
} & InfraSourceFields.Fragment;
export type Configuration = SourceConfigurationFields.Fragment;
export type Status = SourceStatusFields.Fragment;
}
export namespace WaffleNodesQuery {
export type Variables = {
sourceId: string;
@ -776,62 +862,6 @@ export namespace WaffleNodesQuery {
};
}
export namespace CreateSourceMutation {
export type Variables = {
sourceId: string;
sourceConfiguration: CreateSourceInput;
};
export type Mutation = {
__typename?: 'Mutation';
createSource: CreateSource;
};
export type CreateSource = {
__typename?: 'CreateSourceResult';
source: Source;
};
export type Source = SourceFields.Fragment;
}
export namespace SourceQuery {
export type Variables = {
sourceId?: string | null;
};
export type Query = {
__typename?: 'Query';
source: Source;
};
export type Source = SourceFields.Fragment;
}
export namespace UpdateSourceMutation {
export type Variables = {
sourceId?: string | null;
changes: UpdateSourceInput[];
};
export type Mutation = {
__typename?: 'Mutation';
updateSource: UpdateSource;
};
export type UpdateSource = {
__typename?: 'UpdateSourceResult';
source: Source;
};
export type Source = SourceFields.Fragment;
}
export namespace LogEntries {
export type Variables = {
sourceId?: string | null;
@ -910,32 +940,18 @@ export namespace LogEntries {
};
}
export namespace SourceFields {
export namespace SourceConfigurationFields {
export type Fragment = {
__typename?: 'InfraSource';
id: string;
version?: string | null;
updatedAt?: number | null;
configuration: Configuration;
status: Status;
};
export type Configuration = {
__typename?: 'InfraSourceConfiguration';
name: string;
description: string;
metricAlias: string;
logAlias: string;
metricAlias: string;
fields: Fields;
};
@ -954,8 +970,10 @@ export namespace SourceFields {
timestamp: string;
};
}
export type Status = {
export namespace SourceStatusFields {
export type Fragment = {
__typename?: 'InfraSourceStatus';
indexFields: IndexFields[];
@ -987,3 +1005,15 @@ export namespace InfraTimeKeyFields {
tiebreaker: number;
};
}
export namespace InfraSourceFields {
export type Fragment = {
__typename?: 'InfraSource';
id: string;
version?: string | null;
updatedAt?: number | null;
};
}

View file

@ -145,6 +145,7 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
onVisibleChildrenChange={this.handleVisibleChildrenChange}
target={targetId}
hideScrollbar={true}
data-test-subj={'logStream'}
>
{registerChild => (
<>

View file

@ -29,6 +29,7 @@ interface VerticalScrollPanelProps<Child> {
height: number;
width: number;
hideScrollbar?: boolean;
'data-test-subj'?: string;
}
interface VerticalScrollPanelSnapshot<Child> {
@ -208,11 +209,12 @@ export class VerticalScrollPanel<Child> extends React.PureComponent<
}
public render() {
const { children, height, width, hideScrollbar } = this.props;
const { children, height, width, hideScrollbar, 'data-test-subj': dataTestSubj } = this.props;
const scrollbarOffset = hideScrollbar ? ASSUMED_SCROLLBAR_WIDTH : 0;
return (
<ScrollPanelWrapper
data-test-subj={dataTestSubj}
style={{ height, width: width + scrollbarOffset }}
scrollbarOffset={scrollbarOffset}
onScroll={this.handleScroll}

View file

@ -6,71 +6,71 @@
import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { useContext } from 'react';
import euiStyled from '../../../../../common/eui_styled_components';
import { WithSourceConfigurationFlyoutState } from '../../components/source_configuration/source_configuration_flyout_state';
import { SourceConfigurationFlyoutState } from '../../components/source_configuration';
import { WithKibanaChrome } from '../../containers/with_kibana_chrome';
interface InvalidNodeErrorProps {
nodeName: string;
}
export const InvalidNodeError: React.SFC<InvalidNodeErrorProps> = ({ nodeName }) => (
<WithKibanaChrome>
{({ basePath }) => (
<CenteredEmptyPrompt
title={
<h2>
<FormattedMessage
id="xpack.infra.metrics.invalidNodeErrorTitle"
defaultMessage="Looks like {nodeName} isn't collecting any metrics data"
values={{
nodeName,
}}
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.infra.metrics.invalidNodeErrorDescription"
defaultMessage="Double check your configuration"
/>
</p>
}
actions={
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
href={`${basePath}/app/kibana#/home/tutorial_directory/metrics`}
color="primary"
fill
>
<FormattedMessage
id="xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel"
defaultMessage="View setup instructions"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<WithSourceConfigurationFlyoutState>
{({ enable }) => (
<EuiButton color="primary" onClick={enable}>
<FormattedMessage
id="xpack.infra.configureSourceActionLabel"
defaultMessage="Change source configuration"
/>
</EuiButton>
)}
</WithSourceConfigurationFlyoutState>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
)}
</WithKibanaChrome>
);
export const InvalidNodeError: React.FunctionComponent<InvalidNodeErrorProps> = ({ nodeName }) => {
const { show } = useContext(SourceConfigurationFlyoutState.Context);
return (
<WithKibanaChrome>
{({ basePath }) => (
<CenteredEmptyPrompt
title={
<h2>
<FormattedMessage
id="xpack.infra.metrics.invalidNodeErrorTitle"
defaultMessage="Looks like {nodeName} isn't collecting any metrics data"
values={{
nodeName,
}}
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.infra.metrics.invalidNodeErrorDescription"
defaultMessage="Double check your configuration"
/>
</p>
}
actions={
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
href={`${basePath}/app/kibana#/home/tutorial_directory/metrics`}
color="primary"
fill
>
<FormattedMessage
id="xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel"
defaultMessage="View setup instructions"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton color="primary" onClick={show}>
<FormattedMessage
id="xpack.infra.configureSourceActionLabel"
defaultMessage="Change source configuration"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
)}
</WithKibanaChrome>
);
};
const CenteredEmptyPrompt = euiStyled(EuiEmptyPrompt)`
align-self: center;

View file

@ -6,3 +6,7 @@
export { SourceConfigurationButton } from './source_configuration_button';
export { SourceConfigurationFlyout } from './source_configuration_flyout';
export {
SourceConfigurationFlyoutState,
useSourceConfigurationFlyoutState,
} from './source_configuration_flyout_state';

View file

@ -52,6 +52,7 @@ export const IndicesConfigurationPanel = ({
}
>
<EuiFieldText
data-test-subj="metricIndicesInput"
fullWidth
disabled={isLoading}
isLoading={isLoading}
@ -78,7 +79,13 @@ export const IndicesConfigurationPanel = ({
/>
}
>
<EuiFieldText fullWidth disabled={isLoading} isLoading={isLoading} {...logAliasFieldProps} />
<EuiFieldText
data-test-subj="logIndicesInput"
fullWidth
disabled={isLoading}
isLoading={isLoading}
{...logAliasFieldProps}
/>
</EuiFormRow>
</EuiForm>
);

View file

@ -37,7 +37,13 @@ export const NameConfigurationPanel = ({
<FormattedMessage id="xpack.infra.sourceConfiguration.nameLabel" defaultMessage="Name" />
}
>
<EuiFieldText fullWidth disabled={isLoading} isLoading={isLoading} {...nameFieldProps} />
<EuiFieldText
data-test-subj="nameInput"
fullWidth
disabled={isLoading}
isLoading={isLoading}
{...nameFieldProps}
/>
</EuiFormRow>
</EuiForm>
);

View file

@ -5,26 +5,24 @@
*/
import { EuiButtonEmpty } from '@elastic/eui';
import React from 'react';
import React, { useContext } from 'react';
import { WithSource } from '../../containers/with_source';
import { WithSourceConfigurationFlyoutState } from './source_configuration_flyout_state';
import { Source } from '../../containers/source';
import { SourceConfigurationFlyoutState } from './source_configuration_flyout_state';
export const SourceConfigurationButton: React.SFC = () => (
<WithSourceConfigurationFlyoutState>
{({ toggle }) => (
<WithSource>
{({ configuration }) => (
<EuiButtonEmpty
aria-label="Configure source"
color="text"
iconType="gear"
onClick={toggle}
>
{configuration && configuration.name}
</EuiButtonEmpty>
)}
</WithSource>
)}
</WithSourceConfigurationFlyoutState>
);
export const SourceConfigurationButton: React.FunctionComponent = () => {
const { toggleIsVisible } = useContext(SourceConfigurationFlyoutState.Context);
const { source } = useContext(Source.Context);
return (
<EuiButtonEmpty
aria-label="Configure source"
color="text"
data-test-subj="configureSourceButton"
iconType="gear"
onClick={toggleIsVisible}
>
{source && source.configuration && source.configuration.name}
</EuiButtonEmpty>
);
};

View file

@ -16,155 +16,184 @@ import {
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import React from 'react';
import React, { useCallback, useContext, useMemo } from 'react';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { WithSource } from '../../containers/with_source';
import { FormattedMessage } from '@kbn/i18n/react';
import { Source } from '../../containers/source';
import { FieldsConfigurationPanel } from './fields_configuration_panel';
import { IndicesConfigurationPanel } from './indices_configuration_panel';
import { NameConfigurationPanel } from './name_configuration_panel';
import { WithSourceConfigurationFlyoutState } from './source_configuration_flyout_state';
import { WithSourceConfigurationFormState } from './source_configuration_form_state';
import { SourceConfigurationFlyoutState } from './source_configuration_flyout_state';
import { useSourceConfigurationFormState } from './source_configuration_form_state';
const noop = () => undefined;
interface SourceConfigurationFlyoutProps {
intl: InjectedIntl;
}
export const SourceConfigurationFlyout: React.FunctionComponent = () => {
const { isVisible, hide } = useContext(SourceConfigurationFlyoutState.Context);
export const SourceConfigurationFlyout = injectI18n(({ intl }: SourceConfigurationFlyoutProps) => (
<WithSourceConfigurationFlyoutState>
{({ disable: close, value: isVisible }) =>
isVisible ? (
<WithSource>
{({ create, configuration, exists, isLoading, update }) =>
configuration ? (
<WithSourceConfigurationFormState
initialFormState={{
name: configuration.name,
description: configuration.description,
fields: {
container: configuration.fields.container,
host: configuration.fields.host,
message: configuration.fields.message,
pod: configuration.fields.pod,
tiebreaker: configuration.fields.tiebreaker,
timestamp: configuration.fields.timestamp,
},
logAlias: configuration.logAlias,
metricAlias: configuration.metricAlias,
const {
createSourceConfiguration,
source,
sourceExists,
isLoading,
updateSourceConfiguration,
} = useContext(Source.Context);
const configuration = source && source.configuration;
const initialFormState = useMemo(
() =>
configuration
? {
name: configuration.name,
description: configuration.description,
fields: {
container: configuration.fields.container,
host: configuration.fields.host,
message: configuration.fields.message,
pod: configuration.fields.pod,
tiebreaker: configuration.fields.tiebreaker,
timestamp: configuration.fields.timestamp,
},
logAlias: configuration.logAlias,
metricAlias: configuration.metricAlias,
}
: defaultFormState,
[configuration]
);
const {
fieldProps,
formState,
isFormDirty,
isFormValid,
resetForm,
updates,
} = useSourceConfigurationFormState({
initialFormState,
});
const persistUpdates = useCallback(
async () => {
if (sourceExists) {
await updateSourceConfiguration(updates);
} else {
await createSourceConfiguration(formState);
}
resetForm();
},
[sourceExists, updateSourceConfiguration, createSourceConfiguration, resetForm, formState]
);
if (!isVisible || !configuration) {
return null;
}
return (
<EuiFlyout
aria-labelledby="sourceConfigurationTitle"
data-test-subj="sourceConfigurationFlyout"
hideCloseButton
onClose={noop}
>
<EuiFlyoutHeader>
<EuiTitle>
<h2 id="sourceConfigurationTitle">
<FormattedMessage
id="xpack.infra.sourceConfiguration.sourceConfigurationTitle"
defaultMessage="Configure source"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<NameConfigurationPanel isLoading={isLoading} nameFieldProps={fieldProps.name} />
<EuiSpacer />
<IndicesConfigurationPanel
isLoading={isLoading}
logAliasFieldProps={fieldProps.logAlias}
metricAliasFieldProps={fieldProps.metricAlias}
/>
<EuiSpacer />
<FieldsConfigurationPanel
containerFieldProps={fieldProps.containerField}
hostFieldProps={fieldProps.hostField}
isLoading={isLoading}
podFieldProps={fieldProps.podField}
tiebreakerFieldProps={fieldProps.tiebreakerField}
timestampFieldProps={fieldProps.timestampField}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
{!isFormDirty ? (
<EuiButtonEmpty
data-test-subj="closeFlyoutButton"
iconType="cross"
isDisabled={isLoading}
onClick={() => hide()}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
) : (
<EuiButtonEmpty
data-test-subj="discardAndCloseFlyoutButton"
color="danger"
iconType="cross"
isDisabled={isLoading}
onClick={() => {
resetForm();
hide();
}}
>
{({
getCurrentFormState,
getNameFieldProps,
getLogAliasFieldProps,
getMetricAliasFieldProps,
getFieldFieldProps,
isFormValid,
resetForm,
updates,
}) => (
<EuiFlyout
aria-labelledby="sourceConfigurationTitle"
hideCloseButton
onClose={noop}
>
<EuiFlyoutHeader>
<EuiTitle>
<h2 id="sourceConfigurationTitle">
<FormattedMessage
id="xpack.infra.sourceConfiguration.sourceConfigurationTitle"
defaultMessage="Configure source"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<NameConfigurationPanel
isLoading={isLoading}
nameFieldProps={getNameFieldProps()}
/>
<EuiSpacer />
<IndicesConfigurationPanel
isLoading={isLoading}
logAliasFieldProps={getLogAliasFieldProps()}
metricAliasFieldProps={getMetricAliasFieldProps()}
/>
<EuiSpacer />
<FieldsConfigurationPanel
containerFieldProps={getFieldFieldProps('container')}
hostFieldProps={getFieldFieldProps('host')}
isLoading={isLoading}
podFieldProps={getFieldFieldProps('pod')}
tiebreakerFieldProps={getFieldFieldProps('tiebreaker')}
timestampFieldProps={getFieldFieldProps('timestamp')}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
{updates.length === 0 ? (
<EuiButtonEmpty
iconType="cross"
isDisabled={isLoading}
onClick={() => close()}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
) : (
<EuiButtonEmpty
color="danger"
iconType="cross"
isDisabled={isLoading}
onClick={() => {
resetForm();
close();
}}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.discardAndCloseButtonLabel"
defaultMessage="Discard and Close"
/>
</EuiButtonEmpty>
)}
</EuiFlexItem>
<EuiFlexItem />
<EuiFlexItem grow={false}>
{isLoading ? (
<EuiButton color="primary" isLoading fill>
Loading
</EuiButton>
) : (
<EuiButton
color="primary"
isDisabled={updates.length === 0 || !isFormValid()}
fill
onClick={() =>
(exists ? update(updates) : create(getCurrentFormState())).then(
() => resetForm()
)
}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.updateSourceConfigurationButtonLabel"
defaultMessage="Update Source"
/>
</EuiButton>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
)}
</WithSourceConfigurationFormState>
) : null
}
</WithSource>
) : null
}
</WithSourceConfigurationFlyoutState>
));
<FormattedMessage
id="xpack.infra.sourceConfiguration.discardAndCloseButtonLabel"
defaultMessage="Discard and Close"
/>
</EuiButtonEmpty>
)}
</EuiFlexItem>
<EuiFlexItem />
<EuiFlexItem grow={false}>
{isLoading ? (
<EuiButton color="primary" isLoading fill>
Loading
</EuiButton>
) : (
<EuiButton
data-test-subj="updateSourceConfigurationButton"
color="primary"
isDisabled={!isFormDirty || !isFormValid}
fill
onClick={persistUpdates}
>
<FormattedMessage
id="xpack.infra.sourceConfiguration.updateSourceConfigurationButtonLabel"
defaultMessage="Update Source"
/>
</EuiButton>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};
const defaultFormState = {
name: '',
description: '',
fields: {
container: '',
host: '',
message: [],
pod: '',
tiebreaker: '',
timestamp: '',
},
logAlias: '',
metricAlias: '',
};

View file

@ -4,10 +4,30 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import createContainer from 'constate-latest';
import { useCallback, useState } from 'react';
import { WithBinary, WithBinaryProps } from '../../containers/primitives/with_binary';
export const useSourceConfigurationFlyoutState = ({
initialVisibility = false,
}: {
initialVisibility?: boolean;
} = {}) => {
const [isVisible, setIsVisible] = useState<boolean>(initialVisibility);
export const WithSourceConfigurationFlyoutState: React.SFC<WithBinaryProps> = props => (
<WithBinary {...props} context="source-configuration-flyout" />
);
const toggleIsVisible = useCallback(
() => setIsVisible(isCurrentlyVisible => !isCurrentlyVisible),
[setIsVisible]
);
const show = useCallback(() => setIsVisible(true), [setIsVisible]);
const hide = useCallback(() => setIsVisible(false), [setIsVisible]);
return {
hide,
isVisible,
show,
toggleIsVisible,
};
};
export const SourceConfigurationFlyoutState = createContainer(useSourceConfigurationFlyoutState);

View file

@ -4,15 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ActionMap, Container as ConstateContainer, OnMount, SelectorMap } from 'constate';
import mergeAll from 'lodash/fp/mergeAll';
import React from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { memoizeLast } from 'ui/utils/memoize';
import { convertChangeToUpdater } from '../../../common/source_configuration';
import { UpdateSourceInput } from '../../graphql/types';
import { RendererFunction } from '../../utils/typed_react';
export interface InputFieldProps<
Value extends string = string,
@ -27,8 +24,6 @@ export interface InputFieldProps<
type FieldErrorMessage = string | JSX.Element;
type EditableFieldName = 'container' | 'host' | 'pod' | 'tiebreaker' | 'timestamp';
interface FormState {
name: string;
description: string;
@ -44,154 +39,152 @@ interface FormState {
};
}
interface State {
updates: UpdateSourceInput[];
}
export const useSourceConfigurationFormState = ({
initialFormState,
}: {
initialFormState: FormState;
}) => {
const [updates, setUpdates] = useState<UpdateSourceInput[]>([]);
interface Actions {
resetForm: () => void;
updateName: (name: string) => void;
updateLogAlias: (value: string) => void;
updateMetricAlias: (value: string) => void;
updateField: (field: EditableFieldName, value: string) => void;
}
interface Selectors {
getCurrentFormState: () => FormState;
getNameFieldValidationErrors: () => FieldErrorMessage[];
getLogAliasFieldValidationErrors: () => FieldErrorMessage[];
getMetricAliasFieldValidationErrors: () => FieldErrorMessage[];
getFieldFieldValidationErrors: (field: EditableFieldName) => FieldErrorMessage[];
isFormValid: () => boolean;
}
const createContainerProps = memoizeLast((initialFormState: FormState) => {
const actions: ActionMap<State, Actions> = {
resetForm: () => state => ({
...state,
updates: [],
}),
updateName: name => state => ({
...state,
updates: addOrCombineLastUpdate(state.updates, { setName: { name } }),
}),
updateLogAlias: logAlias => state => ({
...state,
updates: addOrCombineLastUpdate(state.updates, { setAliases: { logAlias } }),
}),
updateMetricAlias: metricAlias => state => ({
...state,
updates: addOrCombineLastUpdate(state.updates, { setAliases: { metricAlias } }),
}),
updateField: (field, value) => state => ({
...state,
updates: addOrCombineLastUpdate(state.updates, { setFields: { [field]: value } }),
}),
};
const getCurrentFormState = memoizeLast(
(previousFormState: FormState, updates: UpdateSourceInput[]) =>
updates
.map(convertChangeToUpdater)
.reduce((state, updater) => updater(state), previousFormState)
const addOrCombineLastUpdate = useCallback(
(newUpdate: UpdateSourceInput) =>
setUpdates(currentUpdates => [
...currentUpdates.slice(0, -1),
...maybeCombineUpdates(currentUpdates[currentUpdates.length - 1], newUpdate),
]),
[setUpdates]
);
const selectors: SelectorMap<State, Selectors> = {
getCurrentFormState: () => ({ updates }) => getCurrentFormState(initialFormState, updates),
getNameFieldValidationErrors: () => state =>
validateInputFieldNotEmpty(selectors.getCurrentFormState()(state).name),
getLogAliasFieldValidationErrors: () => state =>
validateInputFieldNotEmpty(selectors.getCurrentFormState()(state).logAlias),
getMetricAliasFieldValidationErrors: () => state =>
validateInputFieldNotEmpty(selectors.getCurrentFormState()(state).metricAlias),
getFieldFieldValidationErrors: field => state =>
validateInputFieldNotEmpty(selectors.getCurrentFormState()(state).fields[field]),
isFormValid: () => state =>
[
selectors.getNameFieldValidationErrors()(state),
selectors.getLogAliasFieldValidationErrors()(state),
selectors.getMetricAliasFieldValidationErrors()(state),
selectors.getFieldFieldValidationErrors('container')(state),
selectors.getFieldFieldValidationErrors('host')(state),
selectors.getFieldFieldValidationErrors('pod')(state),
selectors.getFieldFieldValidationErrors('tiebreaker')(state),
selectors.getFieldFieldValidationErrors('timestamp')(state),
].every(errors => errors.length === 0),
};
const resetForm = useCallback(() => setUpdates([]), []);
const formState = useMemo(
() =>
updates
.map(convertChangeToUpdater)
.reduce((state, updater) => updater(state), initialFormState),
[updates, initialFormState]
);
const nameFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.name),
name: 'name',
onChange: name => addOrCombineLastUpdate({ setName: { name } }),
value: formState.name,
}),
[formState.name, addOrCombineLastUpdate]
);
const logAliasFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.logAlias),
name: 'logAlias',
onChange: logAlias => addOrCombineLastUpdate({ setAliases: { logAlias } }),
value: formState.logAlias,
}),
[formState.logAlias, addOrCombineLastUpdate]
);
const metricAliasFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.metricAlias),
name: 'metricAlias',
onChange: metricAlias => addOrCombineLastUpdate({ setAliases: { metricAlias } }),
value: formState.metricAlias,
}),
[formState.metricAlias, addOrCombineLastUpdate]
);
const containerFieldFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.fields.container),
name: `containerField`,
onChange: value => addOrCombineLastUpdate({ setFields: { container: value } }),
value: formState.fields.container,
}),
[formState.fields.container, addOrCombineLastUpdate]
);
const hostFieldFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.fields.host),
name: `hostField`,
onChange: value => addOrCombineLastUpdate({ setFields: { host: value } }),
value: formState.fields.host,
}),
[formState.fields.host, addOrCombineLastUpdate]
);
const podFieldFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.fields.pod),
name: `podField`,
onChange: value => addOrCombineLastUpdate({ setFields: { pod: value } }),
value: formState.fields.pod,
}),
[formState.fields.pod, addOrCombineLastUpdate]
);
const tiebreakerFieldFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.fields.tiebreaker),
name: `tiebreakerField`,
onChange: value => addOrCombineLastUpdate({ setFields: { tiebreaker: value } }),
value: formState.fields.tiebreaker,
}),
[formState.fields.tiebreaker, addOrCombineLastUpdate]
);
const timestampFieldFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.fields.timestamp),
name: `timestampField`,
onChange: value => addOrCombineLastUpdate({ setFields: { timestamp: value } }),
value: formState.fields.timestamp,
}),
[formState.fields.timestamp, addOrCombineLastUpdate]
);
const fieldProps = useMemo(
() => ({
name: nameFieldProps,
logAlias: logAliasFieldProps,
metricAlias: metricAliasFieldProps,
containerField: containerFieldFieldProps,
hostField: hostFieldFieldProps,
podField: podFieldFieldProps,
tiebreakerField: tiebreakerFieldFieldProps,
timestampField: timestampFieldFieldProps,
}),
[
nameFieldProps,
logAliasFieldProps,
metricAliasFieldProps,
containerFieldFieldProps,
hostFieldFieldProps,
podFieldFieldProps,
tiebreakerFieldFieldProps,
timestampFieldFieldProps,
]
);
const isFormValid = useMemo(
() => Object.values(fieldProps).every(({ error }) => error.length <= 0),
[fieldProps]
);
const isFormDirty = useMemo(() => updates.length > 0, [updates]);
return {
actions,
initialState: { updates: [] } as State,
selectors,
fieldProps,
formState,
isFormDirty,
isFormValid,
resetForm,
updates,
};
});
interface WithSourceConfigurationFormStateProps {
children: RendererFunction<
State &
Actions &
Selectors & {
getFieldFieldProps: (field: EditableFieldName) => InputFieldProps;
getLogAliasFieldProps: () => InputFieldProps;
getMetricAliasFieldProps: () => InputFieldProps;
getNameFieldProps: () => InputFieldProps;
}
>;
initialFormState: FormState;
onMount?: OnMount<State>;
}
export const WithSourceConfigurationFormState: React.SFC<WithSourceConfigurationFormStateProps> = ({
children,
initialFormState,
onMount,
}) => (
<ConstateContainer
{...createContainerProps(initialFormState)}
context="source-configuration-form"
onMount={onMount}
>
{args => {
const currentFormState = args.getCurrentFormState();
return children({
...args,
getNameFieldProps: () =>
createInputFieldProps({
errors: args.getNameFieldValidationErrors(),
name: 'name',
onChange: args.updateName,
value: currentFormState.name,
}),
getLogAliasFieldProps: () =>
createInputFieldProps({
errors: args.getLogAliasFieldValidationErrors(),
name: 'logAlias',
onChange: args.updateLogAlias,
value: currentFormState.logAlias,
}),
getMetricAliasFieldProps: () =>
createInputFieldProps({
errors: args.getMetricAliasFieldValidationErrors(),
name: 'metricAlias',
onChange: args.updateMetricAlias,
value: currentFormState.metricAlias,
}),
getFieldFieldProps: field =>
createInputFieldProps({
errors: args.getFieldFieldValidationErrors(field),
name: `${field}Field`,
onChange: newValue => args.updateField(field, newValue),
value: currentFormState.fields[field],
}),
});
}}
</ConstateContainer>
);
const addOrCombineLastUpdate = (updates: UpdateSourceInput[], newUpdate: UpdateSourceInput) => [
...updates.slice(0, -1),
...maybeCombineUpdates(updates[updates.length - 1], newUpdate),
];
};
const createInputFieldProps = <
Value extends string = string,

View file

@ -7,14 +7,17 @@
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { ErrorPage } from '../../components/error_page';
import { ErrorPage } from './error_page';
interface SourceErrorPageProps {
errorMessage: string;
retry: () => void;
}
export const SourceErrorPage: React.SFC<SourceErrorPageProps> = ({ errorMessage, retry }) => (
export const SourceErrorPage: React.FunctionComponent<SourceErrorPageProps> = ({
errorMessage,
retry,
}) => (
<ErrorPage
shortMessage={
<FormattedMessage

View file

@ -7,9 +7,9 @@
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { LoadingPage } from '../../components/loading_page';
import { LoadingPage } from './loading_page';
export const SourceLoadingPage: React.SFC = () => (
export const SourceLoadingPage: React.FunctionComponent = () => (
<LoadingPage
message={
<FormattedMessage

View file

@ -1,40 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
ActionMap,
Container as ConstateContainer,
ContainerProps as ConstateContainerProps,
Omit,
} from 'constate';
import React from 'react';
interface State {
value: boolean;
}
interface Actions {
disable: () => void;
enable: () => void;
toggle: () => void;
}
const actions: ActionMap<State, Actions> = {
disable: () => state => ({ ...state, value: false }),
enable: () => state => ({ ...state, value: true }),
toggle: () => state => ({ ...state, value: !state.value }),
};
export type WithBinaryProps = Omit<
ConstateContainerProps<State, Actions>,
'actions' | 'initialState' | 'pure'
> & {
initialValue?: boolean;
};
export const WithBinary: React.SFC<WithBinaryProps> = ({ initialValue = false, ...props }) => (
<ConstateContainer {...props} actions={actions} initialState={{ value: initialValue }} pure />
);

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import gql from 'graphql-tag';
import { sharedFragments } from '../../../common/graphql/shared';
import {
sourceConfigurationFieldsFragment,
sourceStatusFieldsFragment,
} from './source_fields_fragment.gql_query';
export const createSourceMutation = gql`
mutation CreateSourceConfigurationMutation(
$sourceId: ID!
$sourceConfiguration: CreateSourceInput!
) {
createSource(id: $sourceId, source: $sourceConfiguration) {
source {
...InfraSourceFields
configuration {
...SourceConfigurationFields
}
status {
...SourceStatusFields
}
}
}
}
${sharedFragments.InfraSourceFields}
${sourceConfigurationFieldsFragment}
${sourceStatusFieldsFragment}
`;

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export * from './with_binary';
export { Source } from './source';

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import gql from 'graphql-tag';
import { sharedFragments } from '../../../common/graphql/shared';
import {
sourceConfigurationFieldsFragment,
sourceStatusFieldsFragment,
} from './source_fields_fragment.gql_query';
export const sourceQuery = gql`
query SourceQuery($sourceId: ID = "default") {
source(id: $sourceId) {
...InfraSourceFields
configuration {
...SourceConfigurationFields
}
status {
...SourceStatusFields
}
}
}
${sharedFragments.InfraSourceFields}
${sourceConfigurationFieldsFragment}
${sourceStatusFieldsFragment}
`;

View file

@ -0,0 +1,191 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import createContainer from 'constate-latest';
import { useEffect, useMemo, useState } from 'react';
import {
CreateSourceConfigurationMutation,
CreateSourceInput,
SourceQuery,
UpdateSourceInput,
UpdateSourceMutation,
} from '../../graphql/types';
import { useApolloClient } from '../../utils/apollo_context';
import { useTrackedPromise } from '../../utils/use_tracked_promise';
import { createSourceMutation } from './create_source.gql_query';
import { sourceQuery } from './query_source.gql_query';
import { updateSourceMutation } from './update_source.gql_query';
type Source = SourceQuery.Query['source'];
export const useSource = ({ sourceId }: { sourceId: string }) => {
const apolloClient = useApolloClient();
const [source, setSource] = useState<Source | undefined>(undefined);
const [loadSourceRequest, loadSource] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
if (!apolloClient) {
throw new DependencyError('Failed to load source: No apollo client available.');
}
return await apolloClient.query<SourceQuery.Query, SourceQuery.Variables>({
fetchPolicy: 'no-cache',
query: sourceQuery,
variables: {
sourceId,
},
});
},
onResolve: response => {
setSource(response.data.source);
},
},
[apolloClient, sourceId]
);
const [createSourceConfigurationRequest, createSourceConfiguration] = useTrackedPromise(
{
createPromise: async (newSourceConfiguration: CreateSourceInput) => {
if (!apolloClient) {
throw new DependencyError(
'Failed to create source configuration: No apollo client available.'
);
}
return await apolloClient.mutate<
CreateSourceConfigurationMutation.Mutation,
CreateSourceConfigurationMutation.Variables
>({
mutation: createSourceMutation,
fetchPolicy: 'no-cache',
variables: {
sourceId,
sourceConfiguration: {
name: newSourceConfiguration.name,
description: newSourceConfiguration.description,
metricAlias: newSourceConfiguration.metricAlias,
logAlias: newSourceConfiguration.logAlias,
fields: newSourceConfiguration.fields
? {
container: newSourceConfiguration.fields.container,
host: newSourceConfiguration.fields.host,
pod: newSourceConfiguration.fields.pod,
tiebreaker: newSourceConfiguration.fields.tiebreaker,
timestamp: newSourceConfiguration.fields.timestamp,
}
: undefined,
},
},
});
},
onResolve: response => {
if (response.data) {
setSource(response.data.createSource.source);
}
},
},
[apolloClient, sourceId]
);
const [updateSourceConfigurationRequest, updateSourceConfiguration] = useTrackedPromise(
{
createPromise: async (changes: UpdateSourceInput[]) => {
if (!apolloClient) {
throw new DependencyError(
'Failed to update source configuration: No apollo client available.'
);
}
return await apolloClient.mutate<
UpdateSourceMutation.Mutation,
UpdateSourceMutation.Variables
>({
mutation: updateSourceMutation,
fetchPolicy: 'no-cache',
variables: {
sourceId,
changes,
},
});
},
onResolve: response => {
if (response.data) {
setSource(response.data.updateSource.source);
}
},
},
[apolloClient, sourceId]
);
const derivedIndexPattern = useMemo(
() => ({
fields: source ? source.status.indexFields : [],
title: source ? `${source.configuration.logAlias}` : 'unknown-index',
}),
[source]
);
const isLoading = useMemo(
() =>
[
loadSourceRequest.state,
createSourceConfigurationRequest.state,
updateSourceConfigurationRequest.state,
].some(state => state === 'pending'),
[
loadSourceRequest.state,
createSourceConfigurationRequest.state,
updateSourceConfigurationRequest.state,
]
);
const sourceExists = useMemo(() => (source ? !!source.version : undefined), [source]);
const logIndicesExist = useMemo(() => source && source.status && source.status.logIndicesExist, [
source,
]);
const metricIndicesExist = useMemo(
() => source && source.status && source.status.metricIndicesExist,
[source]
);
useEffect(
() => {
loadSource();
},
[loadSource]
);
return {
createSourceConfiguration,
derivedIndexPattern,
logIndicesExist,
isLoading,
isLoadingSource: loadSourceRequest.state === 'pending',
hasFailedLoadingSource: loadSourceRequest.state === 'rejected',
loadSource,
loadSourceFailureMessage:
loadSourceRequest.state === 'rejected' ? `${loadSourceRequest.value}` : undefined,
metricIndicesExist,
source,
sourceExists,
sourceId,
updateSourceConfiguration,
version: source && source.version ? source.version : undefined,
};
};
export const Source = createContainer(useSource);
class DependencyError extends Error {
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import gql from 'graphql-tag';
export const sourceConfigurationFieldsFragment = gql`
fragment SourceConfigurationFields on InfraSourceConfiguration {
name
description
logAlias
metricAlias
fields {
container
host
message
pod
tiebreaker
timestamp
}
}
`;
export const sourceStatusFieldsFragment = gql`
fragment SourceStatusFields on InfraSourceStatus {
indexFields {
name
type
searchable
aggregatable
}
logIndicesExist
metricIndicesExist
}
`;

View file

@ -6,16 +6,28 @@
import gql from 'graphql-tag';
import { sourceFieldsFragment } from './source_fields_fragment.gql_query';
import { sharedFragments } from '../../../common/graphql/shared';
import {
sourceConfigurationFieldsFragment,
sourceStatusFieldsFragment,
} from './source_fields_fragment.gql_query';
export const updateSourceMutation = gql`
mutation UpdateSourceMutation($sourceId: ID = "default", $changes: [UpdateSourceInput!]!) {
updateSource(id: $sourceId, changes: $changes) {
source {
...SourceFields
...InfraSourceFields
configuration {
...SourceConfigurationFields
}
status {
...SourceStatusFields
}
}
}
}
${sourceFieldsFragment}
${sharedFragments.InfraSourceFields}
${sourceConfigurationFieldsFragment}
${sourceStatusFieldsFragment}
`;

View file

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import gql from 'graphql-tag';
import { sourceFieldsFragment } from './source_fields_fragment.gql_query';
export const createSourceMutation = gql`
mutation createSourceMutation($sourceId: ID!, $sourceConfiguration: CreateSourceInput!) {
createSource(id: $sourceId, source: $sourceConfiguration) {
source {
...SourceFields
}
}
}
${sourceFieldsFragment}
`;

View file

@ -4,6 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { SourceErrorPage } from './source_error_page';
export { SourceLoadingPage } from './source_loading_page';
export { WithSource } from './with_source';

View file

@ -1,19 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import gql from 'graphql-tag';
import { sourceFieldsFragment } from './source_fields_fragment.gql_query';
export const sourceQuery = gql`
query SourceQuery($sourceId: ID = "default") {
source(id: $sourceId) {
...SourceFields
}
}
${sourceFieldsFragment}
`;

View file

@ -1,39 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import gql from 'graphql-tag';
export const sourceFieldsFragment = gql`
fragment SourceFields on InfraSource {
id
version
updatedAt
configuration {
name
description
metricAlias
logAlias
fields {
container
host
message
pod
tiebreaker
timestamp
}
}
status {
indexFields {
name
type
searchable
aggregatable
}
logIndicesExist
metricIndicesExist
}
}
`;

View file

@ -4,274 +4,62 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ApolloClient } from 'apollo-client';
import { Container as ConstateContainer, OnMount } from 'constate';
import React from 'react';
import { ApolloConsumer } from 'react-apollo';
import { createSelector } from 'reselect';
import React, { useContext } from 'react';
import { StaticIndexPattern } from 'ui/index_patterns';
import { memoizeLast } from 'ui/utils/memoize';
import {
CreateSourceInput,
CreateSourceMutation,
SourceQuery,
UpdateSourceInput,
UpdateSourceMutation,
} from '../../graphql/types';
import {
createStatusActions,
createStatusSelectors,
Operation,
OperationStatus,
StatusHistoryUpdater,
} from '../../utils/operation_status';
import { inferActionMap, inferEffectMap, inferSelectorMap } from '../../utils/typed_constate';
import { CreateSourceInput, SourceQuery, UpdateSourceInput } from '../../graphql/types';
import { RendererFunction } from '../../utils/typed_react';
import { createSourceMutation } from './create_source.gql_query';
import { sourceQuery } from './query_source.gql_query';
import { updateSourceMutation } from './update_source.gql_query';
type Operations =
| Operation<'create', CreateSourceMutation.Variables>
| Operation<'load', SourceQuery.Variables>
| Operation<'update', UpdateSourceMutation.Variables>;
interface State {
operationStatusHistory: Array<OperationStatus<Operations>>;
source: SourceQuery.Query['source'] | undefined;
}
const createContainerProps = memoizeLast((sourceId: string, apolloClient: ApolloClient<any>) => {
const initialState: State = {
operationStatusHistory: [],
source: undefined,
};
const actions = inferActionMap<State>()({
...createStatusActions((updater: StatusHistoryUpdater<Operations>) => (state: State) => ({
...state,
operationStatusHistory: updater(state.operationStatusHistory),
})),
});
const getDerivedIndexPattern = createSelector(
(state: State) =>
(state && state.source && state.source.status && state.source.status.indexFields) || [],
(state: State) =>
(state &&
state.source &&
state.source.configuration &&
state.source.configuration.logAlias) ||
undefined,
(state: State) =>
(state &&
state.source &&
state.source.configuration &&
state.source.configuration.metricAlias) ||
undefined,
(indexFields, logAlias, metricAlias) => ({
fields: indexFields,
title: `${logAlias},${metricAlias}`,
})
);
const selectors = inferSelectorMap<State>()({
...createStatusSelectors(({ operationStatusHistory }: State) => operationStatusHistory),
getConfiguration: () => state =>
(state && state.source && state.source.configuration) || undefined,
getSourceId: () => () => sourceId,
getLogIndicesExist: () => state =>
(state && state.source && state.source.status && state.source.status.logIndicesExist) ||
undefined,
getMetricIndicesExist: () => state =>
(state && state.source && state.source.status && state.source.status.metricIndicesExist) ||
undefined,
getDerivedIndexPattern: () => getDerivedIndexPattern,
getVersion: () => state => (state && state.source && state.source.version) || undefined,
getExists: () => state => (state && state.source && !!state.source.version) || false,
});
const effects = inferEffectMap<State>()({
create: (sourceConfiguration: CreateSourceInput) => ({ setState }) => {
const variables = {
sourceId,
sourceConfiguration: {
name: sourceConfiguration.name,
description: sourceConfiguration.description,
metricAlias: sourceConfiguration.metricAlias,
logAlias: sourceConfiguration.logAlias,
fields: sourceConfiguration.fields
? {
container: sourceConfiguration.fields.container,
host: sourceConfiguration.fields.host,
pod: sourceConfiguration.fields.pod,
tiebreaker: sourceConfiguration.fields.tiebreaker,
timestamp: sourceConfiguration.fields.timestamp,
}
: undefined,
},
};
setState(actions.startOperation({ name: 'create', parameters: variables }));
return apolloClient
.mutate<CreateSourceMutation.Mutation, CreateSourceMutation.Variables>({
mutation: createSourceMutation,
fetchPolicy: 'no-cache',
variables,
})
.then(
result => {
setState(state => ({
...actions.finishOperation({ name: 'create', parameters: variables })(state),
source: result.data ? result.data.createSource.source : state.source,
}));
return result;
},
error => {
setState(state => ({
...actions.failOperation({ name: 'create', parameters: variables }, `${error}`)(
state
),
}));
throw error;
}
);
},
load: () => ({ setState }) => {
const variables = {
sourceId,
};
setState(actions.startOperation({ name: 'load', parameters: variables }));
return apolloClient
.query<SourceQuery.Query, SourceQuery.Variables>({
query: sourceQuery,
fetchPolicy: 'no-cache',
variables,
})
.then(
result => {
setState(state => ({
...actions.finishOperation({ name: 'load', parameters: variables })(state),
source: result.data.source,
}));
return result;
},
error => {
setState(state => ({
...actions.failOperation({ name: 'load', parameters: variables }, `${error}`)(state),
}));
throw error;
}
);
},
update: (changes: UpdateSourceInput[]) => ({ setState }) => {
const variables = {
sourceId,
changes,
};
setState(actions.startOperation({ name: 'update', parameters: variables }));
return apolloClient
.mutate<UpdateSourceMutation.Mutation, UpdateSourceMutation.Variables>({
mutation: updateSourceMutation,
fetchPolicy: 'no-cache',
variables,
})
.then(
result => {
setState(state => ({
...actions.finishOperation({ name: 'update', parameters: variables })(state),
source: result.data ? result.data.updateSource.source : state.source,
}));
return result;
},
error => {
setState(state => ({
...actions.failOperation({ name: 'update', parameters: variables }, `${error}`)(
state
),
}));
throw error;
}
);
},
});
const onMount: OnMount<State> = props => {
effects.load()(props);
};
return {
actions,
context: `source-${sourceId}`,
effects,
initialState,
key: `source-${sourceId}`,
onMount,
selectors,
};
});
import { Source } from '../source';
interface WithSourceProps {
children: RendererFunction<{
configuration?: SourceQuery.Query['source']['configuration'];
create: (sourceConfiguration: CreateSourceInput) => Promise<any>;
create: (sourceConfiguration: CreateSourceInput) => Promise<any> | undefined;
derivedIndexPattern: StaticIndexPattern;
exists: boolean;
exists?: boolean;
hasFailed: boolean;
isLoading: boolean;
lastFailureMessage?: string;
load: () => Promise<any>;
load: () => Promise<any> | undefined;
logIndicesExist?: boolean;
metricAlias?: string;
metricIndicesExist?: boolean;
sourceId: string;
update: (changes: UpdateSourceInput[]) => Promise<any>;
update: (changes: UpdateSourceInput[]) => Promise<any> | undefined;
version?: string;
}>;
}
export const WithSource: React.SFC<WithSourceProps> = ({ children }) => (
<ApolloConsumer>
{client => (
<ConstateContainer {...createContainerProps('default', client)}>
{({
create,
getConfiguration,
getDerivedIndexPattern,
getExists,
getHasFailed,
getIsInProgress,
getLastFailureMessage,
getLogIndicesExist,
getMetricIndicesExist,
getSourceId,
getVersion,
load,
update,
}) =>
children({
create,
configuration: getConfiguration(),
derivedIndexPattern: getDerivedIndexPattern(),
exists: getExists(),
hasFailed: getHasFailed(),
isLoading: getIsInProgress(),
lastFailureMessage: getLastFailureMessage(),
load,
logIndicesExist: getLogIndicesExist(),
metricIndicesExist: getMetricIndicesExist(),
sourceId: getSourceId(),
update,
version: getVersion(),
})
}
</ConstateContainer>
)}
</ApolloConsumer>
);
export const WithSource: React.FunctionComponent<WithSourceProps> = ({ children }) => {
const {
createSourceConfiguration,
derivedIndexPattern,
source,
sourceExists,
sourceId,
metricIndicesExist,
logIndicesExist,
isLoading,
loadSource,
hasFailedLoadingSource,
loadSourceFailureMessage,
updateSourceConfiguration,
version,
} = useContext(Source.Context);
return children({
create: createSourceConfiguration,
configuration: source && source.configuration,
derivedIndexPattern,
exists: sourceExists,
hasFailed: hasFailedLoadingSource,
isLoading,
lastFailureMessage: loadSourceFailureMessage,
load: loadSource,
logIndicesExist,
metricIndicesExist,
sourceId,
update: updateSourceConfiguration,
version,
});
};

View file

@ -718,6 +718,92 @@ export namespace MetricsQuery {
};
}
export namespace CreateSourceConfigurationMutation {
export type Variables = {
sourceId: string;
sourceConfiguration: CreateSourceInput;
};
export type Mutation = {
__typename?: 'Mutation';
createSource: CreateSource;
};
export type CreateSource = {
__typename?: 'CreateSourceResult';
source: Source;
};
export type Source = {
__typename?: 'InfraSource';
configuration: Configuration;
status: Status;
} & InfraSourceFields.Fragment;
export type Configuration = SourceConfigurationFields.Fragment;
export type Status = SourceStatusFields.Fragment;
}
export namespace SourceQuery {
export type Variables = {
sourceId?: string | null;
};
export type Query = {
__typename?: 'Query';
source: Source;
};
export type Source = {
__typename?: 'InfraSource';
configuration: Configuration;
status: Status;
} & InfraSourceFields.Fragment;
export type Configuration = SourceConfigurationFields.Fragment;
export type Status = SourceStatusFields.Fragment;
}
export namespace UpdateSourceMutation {
export type Variables = {
sourceId?: string | null;
changes: UpdateSourceInput[];
};
export type Mutation = {
__typename?: 'Mutation';
updateSource: UpdateSource;
};
export type UpdateSource = {
__typename?: 'UpdateSourceResult';
source: Source;
};
export type Source = {
__typename?: 'InfraSource';
configuration: Configuration;
status: Status;
} & InfraSourceFields.Fragment;
export type Configuration = SourceConfigurationFields.Fragment;
export type Status = SourceStatusFields.Fragment;
}
export namespace WaffleNodesQuery {
export type Variables = {
sourceId: string;
@ -776,62 +862,6 @@ export namespace WaffleNodesQuery {
};
}
export namespace CreateSourceMutation {
export type Variables = {
sourceId: string;
sourceConfiguration: CreateSourceInput;
};
export type Mutation = {
__typename?: 'Mutation';
createSource: CreateSource;
};
export type CreateSource = {
__typename?: 'CreateSourceResult';
source: Source;
};
export type Source = SourceFields.Fragment;
}
export namespace SourceQuery {
export type Variables = {
sourceId?: string | null;
};
export type Query = {
__typename?: 'Query';
source: Source;
};
export type Source = SourceFields.Fragment;
}
export namespace UpdateSourceMutation {
export type Variables = {
sourceId?: string | null;
changes: UpdateSourceInput[];
};
export type Mutation = {
__typename?: 'Mutation';
updateSource: UpdateSource;
};
export type UpdateSource = {
__typename?: 'UpdateSourceResult';
source: Source;
};
export type Source = SourceFields.Fragment;
}
export namespace LogEntries {
export type Variables = {
sourceId?: string | null;
@ -910,32 +940,18 @@ export namespace LogEntries {
};
}
export namespace SourceFields {
export namespace SourceConfigurationFields {
export type Fragment = {
__typename?: 'InfraSource';
id: string;
version?: string | null;
updatedAt?: number | null;
configuration: Configuration;
status: Status;
};
export type Configuration = {
__typename?: 'InfraSourceConfiguration';
name: string;
description: string;
metricAlias: string;
logAlias: string;
metricAlias: string;
fields: Fields;
};
@ -954,8 +970,10 @@ export namespace SourceFields {
timestamp: string;
};
}
export type Status = {
export namespace SourceStatusFields {
export type Fragment = {
__typename?: 'InfraSourceStatus';
indexFields: IndexFields[];
@ -987,3 +1005,15 @@ export namespace InfraTimeKeyFields {
tiebreaker: number;
};
}
export namespace InfraSourceFields {
export type Fragment = {
__typename?: 'InfraSource';
id: string;
version?: string | null;
updatedAt?: number | null;
};
}

View file

@ -7,10 +7,13 @@
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
import { DocumentTitle } from '../../components/document_title';
import { HelpCenterContent } from '../../components/help_center_content';
import { RoutedTabs } from '../../components/navigation/routed_tabs';
import { ColumnarPage } from '../../components/page';
import { SourceConfigurationFlyoutState } from '../../components/source_configuration';
import { Source } from '../../containers/source';
import { MetricsExplorerPage } from './metrics_explorer';
import { SnapshotPage } from './snapshot';
@ -19,38 +22,42 @@ interface InfrastructurePageProps extends RouteComponentProps {
}
export const InfrastructurePage = injectI18n(({ match, intl }: InfrastructurePageProps) => (
<ColumnarPage>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.homePage.documentTitle',
defaultMessage: 'Infrastructure',
})}
/>
<Source.Provider sourceId="default">
<SourceConfigurationFlyoutState.Provider>
<ColumnarPage>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.homePage.documentTitle',
defaultMessage: 'Infrastructure',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/infrastructure"
feedbackLinkText={intl.formatMessage({
id: 'xpack.infra.infrastructure.infrastructureHelpContent.feedbackLinkText',
defaultMessage: 'Provide feedback for Infrastructure',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/infrastructure"
feedbackLinkText={intl.formatMessage({
id: 'xpack.infra.infrastructure.infrastructureHelpContent.feedbackLinkText',
defaultMessage: 'Provide feedback for Infrastructure',
})}
/>
<RoutedTabs
tabs={[
{
title: 'Snapshot',
path: `${match.path}/snapshot`,
},
// {
// title: 'Metrics explorer',
// path: `${match.path}/metrics-explorer`,
// },
]}
/>
<RoutedTabs
tabs={[
{
title: 'Snapshot',
path: `${match.path}/snapshot`,
},
// {
// title: 'Metrics explorer',
// path: `${match.path}/metrics-explorer`,
// },
]}
/>
<Switch>
<Route path={`${match.path}/snapshot`} component={SnapshotPage} />
<Route path={`${match.path}/metrics-explorer`} component={MetricsExplorerPage} />
</Switch>
</ColumnarPage>
<Switch>
<Route path={`${match.path}/snapshot`} component={SnapshotPage} />
<Route path={`${match.path}/metrics-explorer`} component={MetricsExplorerPage} />
</Switch>
</ColumnarPage>
</SourceConfigurationFlyoutState.Provider>
</Source.Provider>
));

View file

@ -5,9 +5,8 @@
*/
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { injectI18n } from '@kbn/i18n/react';
import React, { useContext } from 'react';
import { SnapshotPageContent } from './page_content';
import { SnapshotToolbar } from './toolbar';
@ -18,121 +17,110 @@ import { Header } from '../../../components/header';
import { ColumnarPage } from '../../../components/page';
import { SourceConfigurationFlyout } from '../../../components/source_configuration';
import { WithSourceConfigurationFlyoutState } from '../../../components/source_configuration/source_configuration_flyout_state';
import { SourceConfigurationFlyoutState } from '../../../components/source_configuration';
import { SourceErrorPage } from '../../../components/source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { Source } from '../../../containers/source';
import { WithWaffleFilterUrlState } from '../../../containers/waffle/with_waffle_filters';
import { WithWaffleOptionsUrlState } from '../../../containers/waffle/with_waffle_options';
import { WithWaffleTimeUrlState } from '../../../containers/waffle/with_waffle_time';
import { WithKibanaChrome } from '../../../containers/with_kibana_chrome';
import { SourceErrorPage, SourceLoadingPage, WithSource } from '../../../containers/with_source';
interface SnapshotPageProps extends RouteComponentProps {
intl: InjectedIntl;
}
export const SnapshotPage = injectI18n(({ intl }) => {
const { show } = useContext(SourceConfigurationFlyoutState.Context);
const {
derivedIndexPattern,
hasFailedLoadingSource,
isLoading,
loadSourceFailureMessage,
loadSource,
metricIndicesExist,
} = useContext(Source.Context);
export const SnapshotPage = injectI18n(
class extends React.Component<SnapshotPageProps, {}> {
public static displayName = 'SnapshotPage';
public render() {
const { intl } = this.props;
return (
<ColumnarPage>
<DocumentTitle
title={(previousTitle: string) =>
intl.formatMessage(
{
id: 'xpack.infra.infrastructureSnapshotPage.documentTitle',
defaultMessage: '{previousTitle} | Snapshot',
},
{
previousTitle,
}
)
return (
<ColumnarPage>
<DocumentTitle
title={(previousTitle: string) =>
intl.formatMessage(
{
id: 'xpack.infra.infrastructureSnapshotPage.documentTitle',
defaultMessage: '{previousTitle} | Snapshot',
},
{
previousTitle,
}
/>
<Header
breadcrumbs={[
{
href: '#/',
text: intl.formatMessage({
id: 'xpack.infra.header.infrastructureTitle',
defaultMessage: 'Infrastructure',
}),
},
]}
/>
<SourceConfigurationFlyout />
<WithSource>
{({
derivedIndexPattern,
hasFailed,
isLoading,
lastFailureMessage,
load,
metricIndicesExist,
}) =>
isLoading ? (
<SourceLoadingPage />
) : metricIndicesExist ? (
<>
<WithWaffleTimeUrlState />
<WithWaffleFilterUrlState indexPattern={derivedIndexPattern} />
<WithWaffleOptionsUrlState />
<SnapshotToolbar />
<SnapshotPageContent />
</>
) : hasFailed ? (
<SourceErrorPage errorMessage={lastFailureMessage || ''} retry={load} />
) : (
<WithKibanaChrome>
{({ basePath }) => (
<NoIndices
title={intl.formatMessage({
id: 'xpack.infra.homePage.noMetricsIndicesTitle',
defaultMessage: "Looks like you don't have any metrics indices.",
)
}
/>
<Header
breadcrumbs={[
{
href: '#/',
text: intl.formatMessage({
id: 'xpack.infra.header.infrastructureTitle',
defaultMessage: 'Infrastructure',
}),
},
]}
/>
<SourceConfigurationFlyout />
{isLoading ? (
<SourceLoadingPage />
) : metricIndicesExist ? (
<>
<WithWaffleTimeUrlState />
<WithWaffleFilterUrlState indexPattern={derivedIndexPattern} />
<WithWaffleOptionsUrlState />
<SnapshotToolbar />
<SnapshotPageContent />
</>
) : hasFailedLoadingSource ? (
<SourceErrorPage errorMessage={loadSourceFailureMessage || ''} retry={loadSource} />
) : (
<WithKibanaChrome>
{({ basePath }) => (
<NoIndices
title={intl.formatMessage({
id: 'xpack.infra.homePage.noMetricsIndicesTitle',
defaultMessage: "Looks like you don't have any metrics indices.",
})}
message={intl.formatMessage({
id: 'xpack.infra.homePage.noMetricsIndicesDescription',
defaultMessage: "Let's add some!",
})}
actions={
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
href={`${basePath}/app/kibana#/home/tutorial_directory/metrics`}
color="primary"
fill
>
{intl.formatMessage({
id: 'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel',
defaultMessage: 'View setup instructions',
})}
message={intl.formatMessage({
id: 'xpack.infra.homePage.noMetricsIndicesDescription',
defaultMessage: "Let's add some!",
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton
data-test-subj="configureSourceButton"
color="primary"
onClick={show}
>
{intl.formatMessage({
id: 'xpack.infra.configureSourceActionLabel',
defaultMessage: 'Change source configuration',
})}
actions={
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
href={`${basePath}/app/kibana#/home/tutorial_directory/metrics`}
color="primary"
fill
>
{intl.formatMessage({
id: 'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel',
defaultMessage: 'View setup instructions',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<WithSourceConfigurationFlyoutState>
{({ enable }) => (
<EuiButton color="primary" onClick={enable}>
{intl.formatMessage({
id: 'xpack.infra.configureSourceActionLabel',
defaultMessage: 'Change source configuration',
})}
</EuiButton>
)}
</WithSourceConfigurationFlyoutState>
</EuiFlexItem>
</EuiFlexGroup>
}
data-test-subj="noMetricsIndicesPrompt"
/>
)}
</WithKibanaChrome>
)
}
</WithSource>
</ColumnarPage>
);
}
}
);
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
}
data-test-subj="noMetricsIndicesPrompt"
/>
)}
</WithKibanaChrome>
)}
</ColumnarPage>
);
});

View file

@ -7,6 +7,7 @@
import React from 'react';
import { match as RouteMatch, Redirect, Route, Switch } from 'react-router-dom';
import { Source } from '../../containers/source';
import { RedirectToLogs } from './redirect_to_logs';
import { RedirectToNodeDetail } from './redirect_to_node_detail';
import { RedirectToNodeLogs } from './redirect_to_node_logs';
@ -20,18 +21,20 @@ export class LinkToPage extends React.Component<LinkToPageProps> {
const { match } = this.props;
return (
<Switch>
<Route
path={`${match.url}/:nodeType(host|container|pod)-logs/:nodeId`}
component={RedirectToNodeLogs}
/>
<Route
path={`${match.url}/:nodeType(host|container|pod)-detail/:nodeId`}
component={RedirectToNodeDetail}
/>
<Route path={`${match.url}/logs`} component={RedirectToLogs} />
<Redirect to="/infrastructure" />
</Switch>
<Source.Provider sourceId="default">
<Switch>
<Route
path={`${match.url}/:nodeType(host|container|pod)-logs/:nodeId`}
component={RedirectToNodeLogs}
/>
<Route
path={`${match.url}/:nodeType(host|container|pod)-detail/:nodeId`}
component={RedirectToNodeDetail}
/>
<Route path={`${match.url}/logs`} component={RedirectToLogs} />
<Redirect to="/infrastructure" />
</Switch>
</Source.Provider>
);
}
}

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { LogsPage } from './logs';
export { LogsPage } from './page';

View file

@ -1,174 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { LogsPageContent } from './page_content';
import { LogsToolbar } from './toolbar';
import { DocumentTitle } from '../../components/document_title';
import { NoIndices } from '../../components/empty_states/no_indices';
import { Header } from '../../components/header';
import { HelpCenterContent } from '../../components/help_center_content';
import { LogFlyout } from '../../components/logging/log_flyout';
import { ColumnarPage } from '../../components/page';
import { SourceConfigurationFlyout } from '../../components/source_configuration';
import { WithSourceConfigurationFlyoutState } from '../../components/source_configuration/source_configuration_flyout_state';
import { LogViewConfiguration } from '../../containers/logs/log_view_configuration';
import { WithLogFilter, WithLogFilterUrlState } from '../../containers/logs/with_log_filter';
import { WithLogFlyout } from '../../containers/logs/with_log_flyout';
import { WithFlyoutOptions } from '../../containers/logs/with_log_flyout_options';
import { WithFlyoutOptionsUrlState } from '../../containers/logs/with_log_flyout_options';
import { WithLogMinimapUrlState } from '../../containers/logs/with_log_minimap';
import { WithLogPositionUrlState } from '../../containers/logs/with_log_position';
import { WithLogTextviewUrlState } from '../../containers/logs/with_log_textview';
import { WithKibanaChrome } from '../../containers/with_kibana_chrome';
import { SourceErrorPage, SourceLoadingPage, WithSource } from '../../containers/with_source';
interface Props {
intl: InjectedIntl;
}
export const LogsPage = injectI18n(
class extends React.Component<Props> {
public static displayName = 'LogsPage';
public render() {
const { intl } = this.props;
return (
<LogViewConfiguration.Provider>
<ColumnarPage>
<Header
breadcrumbs={[
{
text: intl.formatMessage({
id: 'xpack.infra.logsPage.logsBreadcrumbsText',
defaultMessage: 'Logs',
}),
},
]}
/>
<WithSource>
{({
derivedIndexPattern,
hasFailed,
isLoading,
lastFailureMessage,
load,
logIndicesExist,
sourceId,
}) => (
<>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.logsPage.documentTitle',
defaultMessage: 'Logs',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/logs"
feedbackLinkText={intl.formatMessage({
id: 'xpack.infra.logsPage.logsHelpContent.feedbackLinkText',
defaultMessage: 'Provide feedback for Logs',
})}
/>
<SourceConfigurationFlyout />
{isLoading ? (
<SourceLoadingPage />
) : logIndicesExist ? (
<>
<WithLogFilterUrlState indexPattern={derivedIndexPattern} />
<WithLogPositionUrlState />
<WithLogMinimapUrlState />
<WithLogTextviewUrlState />
<WithFlyoutOptionsUrlState />
<LogsToolbar />
<WithLogFilter indexPattern={derivedIndexPattern}>
{({ applyFilterQueryFromKueryExpression }) => (
<React.Fragment>
<WithFlyoutOptions>
{({ showFlyout, setFlyoutItem }) => (
<LogsPageContent
showFlyout={showFlyout}
setFlyoutItem={setFlyoutItem}
/>
)}
</WithFlyoutOptions>
<WithLogFlyout sourceId={sourceId}>
{({ flyoutItem, hideFlyout, loading }) => (
<LogFlyout
setFilter={applyFilterQueryFromKueryExpression}
flyoutItem={flyoutItem}
hideFlyout={hideFlyout}
loading={loading}
/>
)}
</WithLogFlyout>
</React.Fragment>
)}
</WithLogFilter>
</>
) : hasFailed ? (
<SourceErrorPage errorMessage={lastFailureMessage || ''} retry={load} />
) : (
<WithKibanaChrome>
{({ basePath }) => (
<NoIndices
title={intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesTitle',
defaultMessage: "Looks like you don't have any logging indices.",
})}
message={intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesDescription',
defaultMessage: "Let's add some!",
})}
actions={
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
href={`${basePath}/app/kibana#/home/tutorial_directory/logging`}
color="primary"
fill
>
{intl.formatMessage({
id:
'xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel',
defaultMessage: 'View setup instructions',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<WithSourceConfigurationFlyoutState>
{({ enable }) => (
<EuiButton color="primary" onClick={enable}>
{intl.formatMessage({
id: 'xpack.infra.configureSourceActionLabel',
defaultMessage: 'Change source configuration',
})}
</EuiButton>
)}
</WithSourceConfigurationFlyoutState>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
)}
</WithKibanaChrome>
)}
</>
)}
</WithSource>
</ColumnarPage>
</LogViewConfiguration.Provider>
);
}
}
);

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { ColumnarPage } from '../../components/page';
import { LogsPageContent } from './page_content';
import { LogsPageHeader } from './page_header';
import { LogsPageProviders } from './page_providers';
export const LogsPage = () => (
<LogsPageProviders>
<ColumnarPage>
<LogsPageHeader />
<LogsPageContent />
</ColumnarPage>
</LogsPageProviders>
);

View file

@ -6,113 +6,32 @@
import React, { useContext } from 'react';
import euiStyled from '../../../../../common/eui_styled_components';
import { AutoSizer } from '../../components/auto_sizer';
import { LogMinimap } from '../../components/logging/log_minimap';
import { ScrollableLogTextStreamView } from '../../components/logging/log_text_stream';
import { PageContent } from '../../components/page';
import { WithSummary } from '../../containers/logs/log_summary';
import { LogViewConfiguration } from '../../containers/logs/log_view_configuration';
import { WithLogPosition } from '../../containers/logs/with_log_position';
import { WithStreamItems } from '../../containers/logs/with_stream_items';
import { SourceErrorPage } from '../../components/source_error_page';
import { SourceLoadingPage } from '../../components/source_loading_page';
import { Source } from '../../containers/source';
import { LogsPageLogsContent } from './page_logs_content';
import { LogsPageNoIndicesContent } from './page_no_indices_content';
interface Props {
setFlyoutItem: (id: string) => void;
showFlyout: () => void;
}
export const LogsPageContent: React.FunctionComponent<Props> = ({ showFlyout, setFlyoutItem }) => {
const { intervalSize, textScale, textWrap } = useContext(LogViewConfiguration.Context);
export const LogsPageContent: React.FunctionComponent = () => {
const {
hasFailedLoadingSource,
isLoadingSource,
logIndicesExist,
loadSource,
loadSourceFailureMessage,
} = useContext(Source.Context);
return (
<PageContent>
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => (
<LogPageEventStreamColumn innerRef={measureRef}>
<WithLogPosition>
{({
isAutoReloading,
jumpToTargetPosition,
reportVisiblePositions,
targetPosition,
}) => (
<WithStreamItems initializeOnMount={!isAutoReloading}>
{({
hasMoreAfterEnd,
hasMoreBeforeStart,
isLoadingMore,
isReloading,
items,
lastLoadedTime,
loadNewerEntries,
}) => (
<ScrollableLogTextStreamView
hasMoreAfterEnd={hasMoreAfterEnd}
hasMoreBeforeStart={hasMoreBeforeStart}
height={height}
isLoadingMore={isLoadingMore}
isReloading={isReloading}
isStreaming={isAutoReloading}
items={items}
jumpToTarget={jumpToTargetPosition}
lastLoadedTime={lastLoadedTime}
loadNewerItems={loadNewerEntries}
reportVisibleInterval={reportVisiblePositions}
scale={textScale}
target={targetPosition}
width={width}
wrap={textWrap}
setFlyoutItem={setFlyoutItem}
showFlyout={showFlyout}
/>
)}
</WithStreamItems>
)}
</WithLogPosition>
</LogPageEventStreamColumn>
)}
</AutoSizer>
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => {
return (
<LogPageMinimapColumn innerRef={measureRef}>
<WithSummary>
{({ buckets }) => (
<WithLogPosition>
{({ jumpToTargetPosition, visibleMidpointTime, visibleTimeInterval }) => (
<LogMinimap
height={height}
width={width}
highlightedInterval={visibleTimeInterval}
intervalSize={intervalSize}
jumpToTarget={jumpToTargetPosition}
summaryBuckets={buckets}
target={visibleMidpointTime}
/>
)}
</WithLogPosition>
)}
</WithSummary>
</LogPageMinimapColumn>
);
}}
</AutoSizer>
</PageContent>
<>
{isLoadingSource ? (
<SourceLoadingPage />
) : logIndicesExist ? (
<LogsPageLogsContent />
) : hasFailedLoadingSource ? (
<SourceErrorPage errorMessage={loadSourceFailureMessage || ''} retry={loadSource} />
) : (
<LogsPageNoIndicesContent />
)}
</>
);
};
const LogPageEventStreamColumn = euiStyled.div`
flex: 1 0 0%;
overflow: hidden;
display: flex;
flex-direction: column;
`;
const LogPageMinimapColumn = euiStyled.div`
flex: 1 0 0%;
overflow: hidden;
min-width: 100px;
max-width: 100px;
display: flex;
flex-direction: column;
`;

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { DocumentTitle } from '../../components/document_title';
import { Header } from '../../components/header';
import { HelpCenterContent } from '../../components/help_center_content';
import { SourceConfigurationFlyout } from '../../components/source_configuration';
export const LogsPageHeader = injectI18n(({ intl }) => {
return (
<>
<Header
breadcrumbs={[
{
text: intl.formatMessage({
id: 'xpack.infra.logsPage.logsBreadcrumbsText',
defaultMessage: 'Logs',
}),
},
]}
/>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.logsPage.documentTitle',
defaultMessage: 'Logs',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/logs"
feedbackLinkText={intl.formatMessage({
id: 'xpack.infra.logsPage.logsHelpContent.feedbackLinkText',
defaultMessage: 'Provide feedback for Logs',
})}
/>
<SourceConfigurationFlyout />
</>
);
});

View file

@ -0,0 +1,152 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useContext } from 'react';
import euiStyled from '../../../../../common/eui_styled_components';
import { AutoSizer } from '../../components/auto_sizer';
import { LogFlyout } from '../../components/logging/log_flyout';
import { LogMinimap } from '../../components/logging/log_minimap';
import { ScrollableLogTextStreamView } from '../../components/logging/log_text_stream';
import { PageContent } from '../../components/page';
import { WithSummary } from '../../containers/logs/log_summary';
import { LogViewConfiguration } from '../../containers/logs/log_view_configuration';
import { WithLogFilter, WithLogFilterUrlState } from '../../containers/logs/with_log_filter';
import { WithLogFlyout } from '../../containers/logs/with_log_flyout';
import { WithFlyoutOptionsUrlState } from '../../containers/logs/with_log_flyout_options';
import { WithFlyoutOptions } from '../../containers/logs/with_log_flyout_options';
import { WithLogMinimapUrlState } from '../../containers/logs/with_log_minimap';
import { WithLogPositionUrlState } from '../../containers/logs/with_log_position';
import { WithLogPosition } from '../../containers/logs/with_log_position';
import { WithLogTextviewUrlState } from '../../containers/logs/with_log_textview';
import { WithStreamItems } from '../../containers/logs/with_stream_items';
import { Source } from '../../containers/source';
import { LogsToolbar } from './page_toolbar';
export const LogsPageLogsContent: React.FunctionComponent = () => {
const { derivedIndexPattern, sourceId } = useContext(Source.Context);
const { intervalSize, textScale, textWrap } = useContext(LogViewConfiguration.Context);
return (
<>
<WithLogFilterUrlState indexPattern={derivedIndexPattern} />
<WithLogPositionUrlState />
<WithLogMinimapUrlState />
<WithLogTextviewUrlState />
<WithFlyoutOptionsUrlState />
<LogsToolbar />
<WithLogFilter indexPattern={derivedIndexPattern}>
{({ applyFilterQueryFromKueryExpression }) => (
<WithLogFlyout sourceId={sourceId}>
{({ flyoutItem, hideFlyout, loading }) => (
<LogFlyout
setFilter={applyFilterQueryFromKueryExpression}
flyoutItem={flyoutItem}
hideFlyout={hideFlyout}
loading={loading}
/>
)}
</WithLogFlyout>
)}
</WithLogFilter>
<WithFlyoutOptions>
{({ showFlyout, setFlyoutItem }) => (
<PageContent>
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => (
<LogPageEventStreamColumn innerRef={measureRef}>
<WithLogPosition>
{({
isAutoReloading,
jumpToTargetPosition,
reportVisiblePositions,
targetPosition,
}) => (
<WithStreamItems initializeOnMount={!isAutoReloading}>
{({
hasMoreAfterEnd,
hasMoreBeforeStart,
isLoadingMore,
isReloading,
items,
lastLoadedTime,
loadNewerEntries,
}) => (
<ScrollableLogTextStreamView
hasMoreAfterEnd={hasMoreAfterEnd}
hasMoreBeforeStart={hasMoreBeforeStart}
height={height}
isLoadingMore={isLoadingMore}
isReloading={isReloading}
isStreaming={isAutoReloading}
items={items}
jumpToTarget={jumpToTargetPosition}
lastLoadedTime={lastLoadedTime}
loadNewerItems={loadNewerEntries}
reportVisibleInterval={reportVisiblePositions}
scale={textScale}
target={targetPosition}
width={width}
wrap={textWrap}
setFlyoutItem={setFlyoutItem}
showFlyout={showFlyout}
/>
)}
</WithStreamItems>
)}
</WithLogPosition>
</LogPageEventStreamColumn>
)}
</AutoSizer>
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => {
return (
<LogPageMinimapColumn innerRef={measureRef}>
<WithSummary>
{({ buckets }) => (
<WithLogPosition>
{({ jumpToTargetPosition, visibleMidpointTime, visibleTimeInterval }) => (
<LogMinimap
height={height}
width={width}
highlightedInterval={visibleTimeInterval}
intervalSize={intervalSize}
jumpToTarget={jumpToTargetPosition}
summaryBuckets={buckets}
target={visibleMidpointTime}
/>
)}
</WithLogPosition>
)}
</WithSummary>
</LogPageMinimapColumn>
);
}}
</AutoSizer>
</PageContent>
)}
</WithFlyoutOptions>
</>
);
};
const LogPageEventStreamColumn = euiStyled.div`
flex: 1 0 0%;
overflow: hidden;
display: flex;
flex-direction: column;
`;
const LogPageMinimapColumn = euiStyled.div`
flex: 1 0 0%;
overflow: hidden;
min-width: 100px;
max-width: 100px;
display: flex;
flex-direction: column;
`;

View file

@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
import React, { useContext } from 'react';
import { NoIndices } from '../../components/empty_states/no_indices';
import { SourceConfigurationFlyoutState } from '../../components/source_configuration';
import { WithKibanaChrome } from '../../containers/with_kibana_chrome';
export const LogsPageNoIndicesContent = injectI18n(({ intl }) => {
const { show } = useContext(SourceConfigurationFlyoutState.Context);
return (
<WithKibanaChrome>
{({ basePath }) => (
<NoIndices
data-test-subj="noLogsIndicesPrompt"
title={intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesTitle',
defaultMessage: "Looks like you don't have any logging indices.",
})}
message={intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesDescription',
defaultMessage: "Let's add some!",
})}
actions={
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton
href={`${basePath}/app/kibana#/home/tutorial_directory/logging`}
color="primary"
fill
>
{intl.formatMessage({
id: 'xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel',
defaultMessage: 'View setup instructions',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton data-test-subj="configureSourceButton" color="primary" onClick={show}>
{intl.formatMessage({
id: 'xpack.infra.configureSourceActionLabel',
defaultMessage: 'Change source configuration',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
)}
</WithKibanaChrome>
);
});

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { SourceConfigurationFlyoutState } from '../../components/source_configuration';
import { LogViewConfiguration } from '../../containers/logs/log_view_configuration';
import { Source } from '../../containers/source';
export const LogsPageProviders: React.FunctionComponent = ({ children }) => (
<Source.Provider sourceId="default">
<SourceConfigurationFlyoutState.Provider>
<LogViewConfiguration.Provider>{children}</LogViewConfiguration.Provider>
</SourceConfigurationFlyoutState.Provider>
</Source.Provider>
);

View file

@ -0,0 +1,109 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
import React, { useContext } from 'react';
import { AutocompleteField } from '../../components/autocomplete_field';
import { Toolbar } from '../../components/eui';
import { LogCustomizationMenu } from '../../components/logging/log_customization_menu';
import { LogMinimapScaleControls } from '../../components/logging/log_minimap_scale_controls';
import { LogTextScaleControls } from '../../components/logging/log_text_scale_controls';
import { LogTextWrapControls } from '../../components/logging/log_text_wrap_controls';
import { LogTimeControls } from '../../components/logging/log_time_controls';
import { SourceConfigurationButton } from '../../components/source_configuration';
import { LogViewConfiguration } from '../../containers/logs/log_view_configuration';
import { WithLogFilter } from '../../containers/logs/with_log_filter';
import { WithLogPosition } from '../../containers/logs/with_log_position';
import { Source } from '../../containers/source';
import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion';
export const LogsToolbar = injectI18n(({ intl }) => {
const { derivedIndexPattern } = useContext(Source.Context);
const {
availableIntervalSizes,
availableTextScales,
intervalSize,
setIntervalSize,
setTextScale,
setTextWrap,
textScale,
textWrap,
} = useContext(LogViewConfiguration.Context);
return (
<Toolbar>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="s">
<EuiFlexItem>
<WithKueryAutocompletion indexPattern={derivedIndexPattern}>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<WithLogFilter indexPattern={derivedIndexPattern}>
{({
applyFilterQueryFromKueryExpression,
filterQueryDraft,
isFilterQueryDraftValid,
setFilterQueryDraftFromKueryExpression,
}) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
placeholder={intl.formatMessage({
id: 'xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder',
defaultMessage: 'Search for log entries… (e.g. host.name:host-1)',
})}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
/>
)}
</WithLogFilter>
)}
</WithKueryAutocompletion>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SourceConfigurationButton />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LogCustomizationMenu>
<LogMinimapScaleControls
availableIntervalSizes={availableIntervalSizes}
setIntervalSize={setIntervalSize}
intervalSize={intervalSize}
/>
<LogTextWrapControls wrap={textWrap} setTextWrap={setTextWrap} />
<LogTextScaleControls
availableTextScales={availableTextScales}
textScale={textScale}
setTextScale={setTextScale}
/>
</LogCustomizationMenu>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<WithLogPosition resetOnUnmount>
{({
visibleMidpointTime,
isAutoReloading,
jumpToTargetPositionTime,
startLiveStreaming,
stopLiveStreaming,
}) => (
<LogTimeControls
currentTime={visibleMidpointTime}
isLiveStreaming={isAutoReloading}
jumpToTime={jumpToTargetPositionTime}
startLiveStreaming={startLiveStreaming}
stopLiveStreaming={stopLiveStreaming}
/>
)}
</WithLogPosition>
</EuiFlexItem>
</EuiFlexGroup>
</Toolbar>
);
});

View file

@ -1,112 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
import React, { useContext } from 'react';
import { AutocompleteField } from '../../components/autocomplete_field';
import { Toolbar } from '../../components/eui';
import { LogCustomizationMenu } from '../../components/logging/log_customization_menu';
import { LogMinimapScaleControls } from '../../components/logging/log_minimap_scale_controls';
import { LogTextScaleControls } from '../../components/logging/log_text_scale_controls';
import { LogTextWrapControls } from '../../components/logging/log_text_wrap_controls';
import { LogTimeControls } from '../../components/logging/log_time_controls';
import { SourceConfigurationButton } from '../../components/source_configuration';
import { LogViewConfiguration } from '../../containers/logs/log_view_configuration';
import { WithLogFilter } from '../../containers/logs/with_log_filter';
import { WithLogPosition } from '../../containers/logs/with_log_position';
import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion';
import { WithSource } from '../../containers/with_source';
export const LogsToolbar = injectI18n(({ intl }) => {
const {
availableIntervalSizes,
availableTextScales,
intervalSize,
setIntervalSize,
setTextScale,
setTextWrap,
textScale,
textWrap,
} = useContext(LogViewConfiguration.Context);
return (
<Toolbar>
<WithSource>
{({ derivedIndexPattern }) => (
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="s">
<EuiFlexItem>
<WithKueryAutocompletion indexPattern={derivedIndexPattern}>
{({ isLoadingSuggestions, loadSuggestions, suggestions }) => (
<WithLogFilter indexPattern={derivedIndexPattern}>
{({
applyFilterQueryFromKueryExpression,
filterQueryDraft,
isFilterQueryDraftValid,
setFilterQueryDraftFromKueryExpression,
}) => (
<AutocompleteField
isLoadingSuggestions={isLoadingSuggestions}
isValid={isFilterQueryDraftValid}
loadSuggestions={loadSuggestions}
onChange={setFilterQueryDraftFromKueryExpression}
onSubmit={applyFilterQueryFromKueryExpression}
placeholder={intl.formatMessage({
id: 'xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder',
defaultMessage: 'Search for log entries… (e.g. host.name:host-1)',
})}
suggestions={suggestions}
value={filterQueryDraft ? filterQueryDraft.expression : ''}
/>
)}
</WithLogFilter>
)}
</WithKueryAutocompletion>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SourceConfigurationButton />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LogCustomizationMenu>
<LogMinimapScaleControls
availableIntervalSizes={availableIntervalSizes}
setIntervalSize={setIntervalSize}
intervalSize={intervalSize}
/>
<LogTextWrapControls wrap={textWrap} setTextWrap={setTextWrap} />
<LogTextScaleControls
availableTextScales={availableTextScales}
textScale={textScale}
setTextScale={setTextScale}
/>
</LogCustomizationMenu>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<WithLogPosition resetOnUnmount>
{({
visibleMidpointTime,
isAutoReloading,
jumpToTargetPositionTime,
startLiveStreaming,
stopLiveStreaming,
}) => (
<LogTimeControls
currentTime={visibleMidpointTime}
isLiveStreaming={isAutoReloading}
jumpToTime={jumpToTargetPositionTime}
startLiveStreaming={startLiveStreaming}
stopLiveStreaming={stopLiveStreaming}
/>
)}
</WithLogPosition>
</EuiFlexItem>
</EuiFlexGroup>
)}
</WithSource>
</Toolbar>
);
});

View file

@ -31,7 +31,6 @@ import { SourceConfigurationFlyout } from '../../components/source_configuration
import { WithMetadata } from '../../containers/metadata/with_metadata';
import { WithMetrics } from '../../containers/metrics/with_metrics';
import {
MetricsTimeContainer,
WithMetricsTime,
WithMetricsTimeUrlState,
} from '../../containers/metrics/with_metrics_time';
@ -40,6 +39,7 @@ import { InfraNodeType, InfraTimerangeInput } from '../../graphql/types';
import { Error, ErrorPageBody } from '../error';
import { layoutCreators } from './layouts';
import { InfraMetricLayoutSection } from './layouts/types';
import { MetricDetailPageProviders } from './page_providers';
const DetailPageContent = euiStyled(PageContent)`
overflow: auto;
@ -89,7 +89,7 @@ export const MetricDetail = withTheme(
const layouts = layoutCreator(this.props.theme);
return (
<MetricsTimeContainer.Provider>
<MetricDetailPageProviders>
<WithSource>
{({ sourceId }) => (
<WithMetricsTime>
@ -241,7 +241,7 @@ export const MetricDetail = withTheme(
</WithMetricsTime>
)}
</WithSource>
</MetricsTimeContainer.Provider>
</MetricDetailPageProviders>
);
}

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { SourceConfigurationFlyoutState } from '../../components/source_configuration';
import { MetricsTimeContainer } from '../../containers/metrics/with_metrics_time';
import { Source } from '../../containers/source';
export const MetricDetailPageProviders: React.FunctionComponent = ({ children }) => (
<Source.Provider sourceId="default">
<SourceConfigurationFlyoutState.Provider>
<MetricsTimeContainer.Provider>{children}</MetricsTimeContainer.Provider>
</SourceConfigurationFlyoutState.Provider>
</Source.Provider>
);

View file

@ -1,47 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
interface MemoizedCall {
args: any[];
returnValue: any;
this: any;
}
// A symbol expressing, that the memoized function has never been called
const neverCalled: unique symbol = Symbol();
type NeverCalled = typeof neverCalled;
/**
* A simple memoize function, that only stores the last returned value
* and uses the identity of all passed parameters as a cache key.
*/
function memoizeLast<T extends (...args: any[]) => any>(func: T): T {
let prevCall: MemoizedCall | NeverCalled = neverCalled;
// We need to use a `function` here for proper this passing.
const memoizedFunction = function(this: any, ...args: any[]) {
if (
prevCall !== neverCalled &&
prevCall.this === this &&
prevCall.args.length === args.length &&
prevCall.args.every((arg, index) => arg === args[index])
) {
return prevCall.returnValue;
}
prevCall = {
args,
this: this,
returnValue: func.apply(this, args),
};
return prevCall.returnValue;
} as T;
return memoizedFunction;
}
export { memoizeLast };

View file

@ -1,98 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import last from 'lodash/fp/last';
export interface InProgressStatus<O extends Operation<string, any>> {
operation: O;
status: 'in-progress';
time: number;
}
export interface SucceededStatus<O extends Operation<string, any>> {
operation: O;
status: 'succeeded';
time: number;
}
export interface FailedStatus<O extends Operation<string, any>> {
message: string;
operation: O;
status: 'failed';
time: number;
}
const isFailedStatus = <O extends Operation<string, any>>(
status: OperationStatus<O>
): status is FailedStatus<O> => status.status === 'failed';
export type OperationStatus<O extends Operation<string, any>> =
| InProgressStatus<O>
| SucceededStatus<O>
| FailedStatus<O>;
export interface Operation<Name extends string, Parameters> {
name: Name;
parameters: Parameters;
}
export const createStatusSelectors = <S extends {}>(
selectStatusHistory: (state: S) => Array<OperationStatus<any>>
) => ({
getIsInProgress: () => (state: S) => {
const lastStatus = last(selectStatusHistory(state));
return lastStatus ? lastStatus.status === 'in-progress' : false;
},
getHasSucceeded: () => (state: S) => {
const lastStatus = last(selectStatusHistory(state));
return lastStatus ? lastStatus.status === 'succeeded' : false;
},
getHasFailed: () => (state: S) => {
const lastStatus = last(selectStatusHistory(state));
return lastStatus ? lastStatus.status === 'failed' : false;
},
getLastFailureMessage: () => (state: S) => {
const lastStatus = last(selectStatusHistory(state).filter(isFailedStatus));
return lastStatus ? lastStatus.message : undefined;
},
});
export type StatusHistoryUpdater<Operations extends Operation<string, any>> = (
statusHistory: Array<OperationStatus<Operations>>
) => Array<OperationStatus<Operations>>;
export const createStatusActions = <S extends {}, Operations extends Operation<string, any>>(
updateStatusHistory: (updater: StatusHistoryUpdater<Operations>) => (state: S) => S
) => ({
startOperation: (operation: Operations) =>
updateStatusHistory(statusHistory => [
...statusHistory,
{
operation,
status: 'in-progress',
time: Date.now(),
},
]),
finishOperation: (operation: Operations) =>
updateStatusHistory(statusHistory => [
...statusHistory,
{
operation,
status: 'succeeded',
time: Date.now(),
},
]),
failOperation: (operation: Operations, message: string) =>
updateStatusHistory(statusHistory => [
...statusHistory,
{
message,
operation,
status: 'failed',
time: Date.now(),
},
]),
});

View file

@ -1,107 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/**
* The helper types and functions below are designed to be used with constate
* v0.9. From version 1.0 the use of react hooks probably makes them
* unnecessary.
*
* The `inferActionMap`, `inferEffectMap` and `inferSelectorMap` functions
* remove the necessity to type out the child-facing interfaces as suggested in
* the constate typescript documentation by inferring the `ActionMap`,
* `EffectMap` and `SelectorMap` types from the object passed as an argument.
* At runtime these functions just return their first argument without
* modification.
*
* Until partial type argument inference is (hopefully) introduced with
* TypeScript 3.3, the functions are split into two nested functions to allow
* for specifying the `State` type argument while leaving the other type
* arguments for inference by the compiler.
*
* Example Usage:
*
* ```typescript
* const actions = inferActionMap<State>()({
* increment: (amount: number) => state => ({ ...state, count: state.count + amount }),
* });
* // actions has type ActionMap<State, { increment: (amount: number) => void; }>
* ```
*/
import { ActionMap, EffectMap, EffectProps, SelectorMap } from 'constate';
/**
* actions
*/
type InferredAction<State, Action> = Action extends (...args: infer A) => (state: State) => State
? (...args: A) => void
: never;
type InferredActions<State, Actions> = ActionMap<
State,
{ [K in keyof Actions]: InferredAction<State, Actions[K]> }
>;
export type ActionsFromMap<M> = M extends ActionMap<any, infer A> ? A : never;
export const inferActionMap = <State extends any>() => <
Actions extends {
[key: string]: (...args: any[]) => (state: State) => State;
}
>(
actionMap: Actions
): InferredActions<State, Actions> => actionMap as any;
/**
* effects
*/
type InferredEffect<State, Effect> = Effect extends (
...args: infer A
) => (props: EffectProps<State>) => infer R
? (...args: A) => R
: never;
type InferredEffects<State, Effects> = EffectMap<
State,
{ [K in keyof Effects]: InferredEffect<State, Effects[K]> }
>;
export type EffectsFromMap<M> = M extends EffectMap<any, infer E> ? E : never;
export const inferEffectMap = <State extends any>() => <
Effects extends {
[key: string]: (...args: any[]) => (props: EffectProps<State>) => any;
}
>(
effectMap: Effects
): InferredEffects<State, Effects> => effectMap as any;
/**
* selectors
*/
type InferredSelector<State, Selector> = Selector extends (
...args: infer A
) => (state: State) => infer R
? (...args: A) => R
: never;
type InferredSelectors<State, Selectors> = SelectorMap<
State,
{ [K in keyof Selectors]: InferredSelector<State, Selectors[K]> }
>;
export type SelectorsFromMap<M> = M extends SelectorMap<any, infer S> ? S : never;
export const inferSelectorMap = <State extends any>() => <
Selectors extends {
[key: string]: (...args: any[]) => (state: State) => any;
}
>(
selectorMap: Selectors
): InferredSelectors<State, Selectors> => selectorMap as any;

View file

@ -0,0 +1,260 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable max-classes-per-file */
import { DependencyList, useEffect, useMemo, useRef, useState } from 'react';
interface UseTrackedPromiseArgs<Arguments extends any[], Result> {
createPromise: (...args: Arguments) => Promise<Result>;
onResolve?: (result: Result) => void;
onReject?: (value: unknown) => void;
cancelPreviousOn?: 'creation' | 'settlement' | 'resolution' | 'rejection' | 'never';
}
/**
* This hook manages a Promise factory and can create new Promises from it. The
* state of these Promises is tracked and they can be canceled when superseded
* to avoid race conditions.
*
* ```
* const [requestState, performRequest] = useTrackedPromise(
* {
* cancelPreviousOn: 'resolution',
* createPromise: async (url: string) => {
* return await fetchSomething(url)
* },
* onResolve: response => {
* setSomeState(response.data);
* },
* onReject: response => {
* setSomeError(response);
* },
* },
* [fetchSomething]
* );
* ```
*
* The `onResolve` and `onReject` handlers are registered separately, because
* the hook will inject a rejection when in case of a canellation. The
* `cancelPreviousOn` attribute can be used to indicate when the preceding
* pending promises should be canceled:
*
* 'never': No preceding promises will be canceled.
*
* 'creation': Any preceding promises will be canceled as soon as a new one is
* created.
*
* 'settlement': Any preceding promise will be canceled when a newer promise is
* resolved or rejected.
*
* 'resolution': Any preceding promise will be canceled when a newer promise is
* resolved.
*
* 'rejection': Any preceding promise will be canceled when a newer promise is
* rejected.
*
* Any pending promises will be canceled when the component using the hook is
* unmounted, but their status will not be tracked to avoid React warnings
* about memory leaks.
*
* The last argument is a normal React hook dependency list that indicates
* under which conditions a new reference to the configuration object should be
* used.
*/
export const useTrackedPromise = <Arguments extends any[], Result>(
{
createPromise,
onResolve = noOp,
onReject = noOp,
cancelPreviousOn = 'never',
}: UseTrackedPromiseArgs<Arguments, Result>,
dependencies: DependencyList
) => {
/**
* If a promise is currently pending, this holds a reference to it and its
* cancellation function.
*/
const pendingPromises = useRef<ReadonlyArray<CancelablePromise<Result>>>([]);
/**
* The state of the promise most recently created by the `createPromise`
* factory. It could be uninitialized, pending, resolved or rejected.
*/
const [promiseState, setPromiseState] = useState<PromiseState<Result>>({
state: 'uninitialized',
});
const execute = useMemo(
() => (...args: Arguments) => {
let rejectCancellationPromise!: (value: any) => void;
const cancellationPromise = new Promise<any>((_, reject) => {
rejectCancellationPromise = reject;
});
// remember the list of prior pending promises for cancellation
const previousPendingPromises = pendingPromises.current;
const cancelPreviousPendingPromises = () => {
previousPendingPromises.forEach(promise => promise.cancel());
};
const newPromise = createPromise(...args);
const newCancelablePromise = Promise.race([newPromise, cancellationPromise]);
// track this new state
setPromiseState({
state: 'pending',
promise: newCancelablePromise,
});
if (cancelPreviousOn === 'creation') {
cancelPreviousPendingPromises();
}
const newPendingPromise: CancelablePromise<Result> = {
cancel: () => {
rejectCancellationPromise(new CanceledPromiseError());
},
cancelSilently: () => {
rejectCancellationPromise(new SilentCanceledPromiseError());
},
promise: newCancelablePromise.then(
value => {
setPromiseState(previousPromiseState =>
previousPromiseState.state === 'pending' &&
previousPromiseState.promise === newCancelablePromise
? {
state: 'resolved',
promise: newPendingPromise.promise,
value,
}
: previousPromiseState
);
if (['settlement', 'resolution'].includes(cancelPreviousOn)) {
cancelPreviousPendingPromises();
}
// remove itself from the list of pending promises
pendingPromises.current = pendingPromises.current.filter(
pendingPromise => pendingPromise.promise !== newPendingPromise.promise
);
if (onResolve) {
onResolve(value);
}
return value;
},
value => {
if (!(value instanceof SilentCanceledPromiseError)) {
setPromiseState(previousPromiseState =>
previousPromiseState.state === 'pending' &&
previousPromiseState.promise === newCancelablePromise
? {
state: 'rejected',
promise: newCancelablePromise,
value,
}
: previousPromiseState
);
}
if (['settlement', 'rejection'].includes(cancelPreviousOn)) {
cancelPreviousPendingPromises();
}
// remove itself from the list of pending promises
pendingPromises.current = pendingPromises.current.filter(
pendingPromise => pendingPromise.promise !== newPendingPromise.promise
);
if (onReject) {
onReject(value);
}
throw value;
}
),
};
// add the new promise to the list of pending promises
pendingPromises.current = [...pendingPromises.current, newPendingPromise];
// silence "unhandled rejection" warnings
newPendingPromise.promise.catch(noOp);
return newPendingPromise.promise;
},
dependencies
);
/**
* Cancel any pending promises silently to avoid memory leaks and race
* conditions.
*/
useEffect(
() => () => {
pendingPromises.current.forEach(promise => promise.cancelSilently());
},
[]
);
return [promiseState, execute] as [typeof promiseState, typeof execute];
};
interface UninitializedPromiseState {
state: 'uninitialized';
}
interface PendingPromiseState<ResolvedValue> {
state: 'pending';
promise: Promise<ResolvedValue>;
}
interface ResolvedPromiseState<ResolvedValue> {
state: 'resolved';
promise: Promise<ResolvedValue>;
value: ResolvedValue;
}
interface RejectedPromiseState<ResolvedValue, RejectedValue> {
state: 'rejected';
promise: Promise<ResolvedValue>;
value: RejectedValue;
}
type SettledPromise<ResolvedValue, RejectedValue> =
| ResolvedPromiseState<ResolvedValue>
| RejectedPromiseState<ResolvedValue, RejectedValue>;
type PromiseState<ResolvedValue, RejectedValue = unknown> =
| UninitializedPromiseState
| PendingPromiseState<ResolvedValue>
| SettledPromise<ResolvedValue, RejectedValue>;
interface CancelablePromise<ResolvedValue> {
// reject the promise prematurely with a CanceledPromiseError
cancel: () => void;
// reject the promise prematurely with a SilentCanceledPromiseError
cancelSilently: () => void;
// the tracked promise
promise: Promise<ResolvedValue>;
}
class CanceledPromiseError extends Error {
public isCanceled = true;
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}
class SilentCanceledPromiseError extends CanceledPromiseError {}
const noOp = () => undefined;

View file

@ -7,7 +7,7 @@
import expect from '@kbn/expect';
import gql from 'graphql-tag';
import { sourceQuery } from '../../../../plugins/infra/public/containers/with_source/query_source.gql_query';
import { sourceQuery } from '../../../../plugins/infra/public/containers/source/query_source.gql_query';
import { SourceQuery } from '../../../../plugins/infra/public/graphql/types';
import { KbnTestProvider } from './types';

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const DATES = {
'7.0.0': {
hosts: {
min: 1547571261002,
max: 1547571831033,
},
},
'6.6.0': {
docker: {
min: 1547578132289,
max: 1547579090048,
},
},
metricsAndLogs: {
hosts: {
withData: 1539806283000,
withoutData: 1539122400000,
},
},
};

View file

@ -5,9 +5,10 @@
*/
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
import { DATES } from './constants';
const DATE_WITH_DATA = new Date(1539806283000);
const DATE_WITHOUT_DATA = new Date(1539122400000);
const DATE_WITH_DATA = new Date(DATES.metricsAndLogs.hosts.withData);
const DATE_WITHOUT_DATA = new Date(DATES.metricsAndLogs.hosts.withoutData);
// eslint-disable-next-line import/no-default-export
export default ({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) => {

View file

@ -12,5 +12,7 @@ export default ({ loadTestFile }: KibanaFunctionalTestDefaultProviders) => {
this.tags('ciGroup7');
loadTestFile(require.resolve('./home_page'));
loadTestFile(require.resolve('./logs_source_configuration'));
loadTestFile(require.resolve('./metrics_source_configuration'));
});
};

View file

@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
// eslint-disable-next-line import/no-default-export
export default ({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) => {
const esArchiver = getService('esArchiver');
const infraSourceConfigurationFlyout = getService('infraSourceConfigurationFlyout');
const pageObjects = getPageObjects(['common', 'infraLogs']);
describe('Logs Page', () => {
before(async () => {
await esArchiver.load('empty_kibana');
});
after(async () => {
await esArchiver.unload('empty_kibana');
});
describe('with logs present', () => {
before(async () => {
await esArchiver.load('infra/metrics_and_logs');
});
after(async () => {
await esArchiver.unload('infra/metrics_and_logs');
});
it('renders the log stream', async () => {
await pageObjects.common.navigateToApp('infraLogs');
await pageObjects.infraLogs.getLogStream();
});
it('can change the log indices to a pattern that matches nothing', async () => {
await pageObjects.infraLogs.openSourceConfigurationFlyout();
const nameInput = await infraSourceConfigurationFlyout.getNameInput();
await nameInput.clearValue();
await nameInput.type('Modified Source');
const logIndicesInput = await infraSourceConfigurationFlyout.getLogIndicesInput();
await logIndicesInput.clearValue();
await logIndicesInput.type('does-not-exist-*');
await infraSourceConfigurationFlyout.saveConfiguration();
await infraSourceConfigurationFlyout.closeFlyout();
});
it('renders the no indices screen when no indices match the pattern', async () => {
await pageObjects.infraLogs.getNoLogsIndicesPrompt();
});
it('can change the log indices back to a pattern that matches something', async () => {
await pageObjects.infraLogs.openSourceConfigurationFlyout();
const logIndicesInput = await infraSourceConfigurationFlyout.getLogIndicesInput();
await logIndicesInput.clearValue();
await logIndicesInput.type('filebeat-*');
await infraSourceConfigurationFlyout.saveConfiguration();
await infraSourceConfigurationFlyout.closeFlyout();
});
it('renders the log stream again', async () => {
await pageObjects.infraLogs.getLogStream();
});
});
});
};

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
import { DATES } from './constants';
const DATE_WITH_DATA = new Date(DATES.metricsAndLogs.hosts.withData);
// eslint-disable-next-line import/no-default-export
export default ({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) => {
const esArchiver = getService('esArchiver');
const infraSourceConfigurationFlyout = getService('infraSourceConfigurationFlyout');
const pageObjects = getPageObjects(['common', 'infraHome']);
describe('Infrastructure Snapshot Page', () => {
before(async () => {
await esArchiver.load('empty_kibana');
});
after(async () => {
await esArchiver.unload('empty_kibana');
});
describe('with metrics present', () => {
before(async () => {
await esArchiver.load('infra/metrics_and_logs');
});
after(async () => {
await esArchiver.unload('infra/metrics_and_logs');
});
it('renders the waffle map', async () => {
await pageObjects.common.navigateToApp('infraOps');
await pageObjects.infraHome.goToTime(DATE_WITH_DATA);
await pageObjects.infraHome.getWaffleMap();
});
it('can change the metric indices to a pattern that matches nothing', async () => {
await pageObjects.infraHome.openSourceConfigurationFlyout();
const nameInput = await infraSourceConfigurationFlyout.getNameInput();
await nameInput.clearValue();
await nameInput.type('Modified Source');
const metricIndicesInput = await infraSourceConfigurationFlyout.getMetricIndicesInput();
await metricIndicesInput.clearValue();
await metricIndicesInput.type('does-not-exist-*');
await infraSourceConfigurationFlyout.saveConfiguration();
await infraSourceConfigurationFlyout.closeFlyout();
});
it('renders the no indices screen when no indices match the pattern', async () => {
await pageObjects.infraHome.getNoMetricsIndicesPrompt();
});
it('can change the log indices back to a pattern that matches something', async () => {
await pageObjects.infraHome.openSourceConfigurationFlyout();
const metricIndicesInput = await infraSourceConfigurationFlyout.getMetricIndicesInput();
await metricIndicesInput.clearValue();
await metricIndicesInput.type('metricbeat-*');
await infraSourceConfigurationFlyout.saveConfiguration();
await infraSourceConfigurationFlyout.closeFlyout();
});
it('renders the log stream again', async () => {
await pageObjects.infraHome.getWaffleMap();
});
});
});
};

View file

@ -19,12 +19,12 @@ import {
SpaceSelectorPageProvider,
AccountSettingProvider,
InfraHomePageProvider,
InfraLogsPageProvider,
GisPageProvider,
StatusPagePageProvider,
UpgradeAssistantProvider,
RollupPageProvider,
UptimePageProvider,
} from './page_objects';
import {
@ -56,7 +56,7 @@ import {
GrokDebuggerProvider,
UserMenuProvider,
UptimeProvider,
InfraSourceConfigurationFlyoutProvider,
} from './services';
// the default export of config files must be a config provider
@ -89,7 +89,7 @@ export default async function ({ readConfigFile }) {
resolve(__dirname, './apps/maps'),
resolve(__dirname, './apps/status_page'),
resolve(__dirname, './apps/upgrade_assistant'),
resolve(__dirname, './apps/uptime')
resolve(__dirname, './apps/uptime'),
],
// define the name and providers for services that should be
@ -127,6 +127,7 @@ export default async function ({ readConfigFile }) {
userMenu: UserMenuProvider,
uptime: UptimeProvider,
rollup: RollupPageProvider,
infraSourceConfigurationFlyout: InfraSourceConfigurationFlyoutProvider,
},
// just like services, PageObjects are defined as a map of
@ -143,11 +144,12 @@ export default async function ({ readConfigFile }) {
reporting: ReportingPageProvider,
spaceSelector: SpaceSelectorPageProvider,
infraHome: InfraHomePageProvider,
infraLogs: InfraLogsPageProvider,
maps: GisPageProvider,
statusPage: StatusPagePageProvider,
upgradeAssistant: UpgradeAssistantProvider,
uptime: UptimePageProvider,
rollup: RollupPageProvider
rollup: RollupPageProvider,
},
servers: kibanaFunctionalConfig.get('servers'),
@ -206,6 +208,10 @@ export default async function ({ readConfigFile }) {
infraOps: {
pathname: '/app/infra',
},
infraLogs: {
pathname: '/app/infra',
hash: '/logs',
},
canvas: {
pathname: '/app/canvas',
hash: '/',
@ -215,8 +221,8 @@ export default async function ({ readConfigFile }) {
},
rollupJob: {
pathname: '/app/kibana',
hash: '/management/elasticsearch/rollup_jobs/'
}
hash: '/management/elasticsearch/rollup_jobs/',
},
},
// choose where esArchiver should load archives from
@ -233,5 +239,4 @@ export default async function ({ readConfigFile }) {
reportName: 'X-Pack Functional Tests',
},
};
}

View file

@ -14,6 +14,7 @@ export { ReportingPageProvider } from './reporting_page';
export { SpaceSelectorPageProvider } from './space_selector_page';
export { AccountSettingProvider } from './accountsetting_page';
export { InfraHomePageProvider } from './infra_home_page';
export { InfraLogsPageProvider } from './infra_logs_page';
export { GisPageProvider } from './gis_page';
export { StatusPagePageProvider } from './status_page';
export { UpgradeAssistantProvider } from './upgrade_assistant';

View file

@ -35,5 +35,10 @@ export function InfraHomePageProvider({ getService }: KibanaFunctionalTestDefaul
async getNoMetricsDataPrompt() {
return await testSubjects.find('noMetricsDataPrompt');
},
async openSourceConfigurationFlyout() {
await testSubjects.click('configureSourceButton');
await testSubjects.exists('sourceConfigurationFlyout');
},
};
}

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// import testSubjSelector from '@kbn/test-subj-selector';
// import moment from 'moment';
import { KibanaFunctionalTestDefaultProviders } from '../../types/providers';
export function InfraLogsPageProvider({ getService }: KibanaFunctionalTestDefaultProviders) {
const testSubjects = getService('testSubjects');
// const find = getService('find');
// const browser = getService('browser');
return {
async getLogStream() {
return await testSubjects.find('logStream');
},
async getNoLogsIndicesPrompt() {
return await testSubjects.find('noLogsIndicesPrompt');
},
async openSourceConfigurationFlyout() {
await testSubjects.click('configureSourceButton');
await testSubjects.exists('sourceConfigurationFlyout');
},
};
}

View file

@ -12,3 +12,4 @@ export { AceEditorProvider } from './ace_editor';
export { GrokDebuggerProvider } from './grok_debugger';
export { UserMenuProvider } from './user_menu';
export { UptimeProvider } from './uptime';
export { InfraSourceConfigurationFlyoutProvider } from './infra_source_configuration_flyout';

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { KibanaFunctionalTestDefaultProviders } from '../../types/providers';
export function InfraSourceConfigurationFlyoutProvider({
getService,
}: KibanaFunctionalTestDefaultProviders) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
return {
async getNameInput() {
return await testSubjects.find('nameInput');
},
async getLogIndicesInput() {
return await testSubjects.find('logIndicesInput');
},
async getMetricIndicesInput() {
return await testSubjects.find('metricIndicesInput');
},
async saveConfiguration() {
await testSubjects.click('updateSourceConfigurationButton');
await retry.try(async () => {
const element = await testSubjects.find('updateSourceConfigurationButton');
return !(await element.isEnabled());
});
},
async closeFlyout() {
const flyout = await testSubjects.find('sourceConfigurationFlyout');
await testSubjects.click('closeFlyoutButton');
await testSubjects.waitForDeleted(flyout);
},
};
}