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

This PR refactors the source configuration state and source configuration form state to hooks. Aside from slightly improved performance due to memoization, it should not lead to visible differences.
This commit is contained in:
Felix Stürmer 2019-04-10 17:03:11 +02:00 committed by GitHub
parent 63ea74c43c
commit dc6ecae7fa
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);
},
};
}