From dc6ecae7fab6b14ef5c6ab502c2372f12d0dbebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Wed, 10 Apr 2019 17:03:11 +0200 Subject: [PATCH] [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. --- .../graphql/shared/fragments.gql_query.ts | 7 + x-pack/plugins/infra/common/graphql/types.ts | 178 +++++----- .../scrollable_log_text_stream_view.tsx | 1 + .../log_text_stream/vertical_scroll_panel.tsx | 4 +- .../components/metrics/invalid_node.tsx | 114 +++---- .../components/source_configuration/index.ts | 4 + .../indices_configuration_panel.tsx | 9 +- .../name_configuration_panel.tsx | 8 +- .../source_configuration_button.tsx | 40 ++- .../source_configuration_flyout.tsx | 313 ++++++++++-------- .../source_configuration_flyout_state.tsx | 30 +- .../source_configuration_form_state.tsx | 289 ++++++++-------- .../source_error_page.tsx | 7 +- .../source_loading_page.tsx | 4 +- .../containers/primitives/with_binary.tsx | 40 --- .../source/create_source.gql_query.ts | 36 ++ .../{primitives => source}/index.ts | 2 +- .../source/query_source.gql_query.ts | 31 ++ .../infra/public/containers/source/source.tsx | 191 +++++++++++ .../source_fields_fragment.gql_query.ts | 37 +++ .../update_source.gql_query.ts | 18 +- .../with_source/create_source.gql_query.ts | 21 -- .../public/containers/with_source/index.ts | 2 - .../with_source/query_source.gql_query.ts | 19 -- .../source_fields_fragment.gql_query.ts | 39 --- .../containers/with_source/with_source.tsx | 292 +++------------- x-pack/plugins/infra/public/graphql/types.ts | 178 +++++----- .../public/pages/infrastructure/index.tsx | 69 ++-- .../pages/infrastructure/snapshot/index.tsx | 212 ++++++------ .../infra/public/pages/link_to/link_to.tsx | 27 +- .../plugins/infra/public/pages/logs/index.ts | 2 +- .../plugins/infra/public/pages/logs/logs.tsx | 174 ---------- .../plugins/infra/public/pages/logs/page.tsx | 21 ++ .../infra/public/pages/logs/page_content.tsx | 129 ++------ .../infra/public/pages/logs/page_header.tsx | 44 +++ .../public/pages/logs/page_logs_content.tsx | 152 +++++++++ .../pages/logs/page_no_indices_content.tsx | 59 ++++ .../public/pages/logs/page_providers.tsx | 19 ++ .../infra/public/pages/logs/page_toolbar.tsx | 109 ++++++ .../infra/public/pages/logs/toolbar.tsx | 112 ------- .../infra/public/pages/metrics/index.tsx | 6 +- .../public/pages/metrics/page_providers.tsx | 19 ++ .../infra/public/utils/memoize_last.ts | 47 --- .../infra/public/utils/operation_status.ts | 98 ------ .../infra/public/utils/typed_constate.tsx | 107 ------ .../infra/public/utils/use_tracked_promise.ts | 260 +++++++++++++++ .../api_integration/apis/infra/sources.ts | 2 +- .../test/functional/apps/infra/constants.ts | 26 ++ .../test/functional/apps/infra/home_page.ts | 5 +- x-pack/test/functional/apps/infra/index.ts | 2 + .../apps/infra/logs_source_configuration.ts | 71 ++++ .../infra/metrics_source_configuration.ts | 75 +++++ x-pack/test/functional/config.js | 19 +- x-pack/test/functional/page_objects/index.js | 1 + .../page_objects/infra_home_page.ts | 5 + .../page_objects/infra_logs_page.ts | 31 ++ x-pack/test/functional/services/index.js | 1 + .../infra_source_configuration_flyout.ts | 43 +++ 58 files changed, 2144 insertions(+), 1717 deletions(-) rename x-pack/plugins/infra/public/{containers/with_source => components}/source_error_page.tsx (81%) rename x-pack/plugins/infra/public/{containers/with_source => components}/source_loading_page.tsx (82%) delete mode 100644 x-pack/plugins/infra/public/containers/primitives/with_binary.tsx create mode 100644 x-pack/plugins/infra/public/containers/source/create_source.gql_query.ts rename x-pack/plugins/infra/public/containers/{primitives => source}/index.ts (87%) create mode 100644 x-pack/plugins/infra/public/containers/source/query_source.gql_query.ts create mode 100644 x-pack/plugins/infra/public/containers/source/source.tsx create mode 100644 x-pack/plugins/infra/public/containers/source/source_fields_fragment.gql_query.ts rename x-pack/plugins/infra/public/containers/{with_source => source}/update_source.gql_query.ts (52%) delete mode 100644 x-pack/plugins/infra/public/containers/with_source/create_source.gql_query.ts delete mode 100644 x-pack/plugins/infra/public/containers/with_source/query_source.gql_query.ts delete mode 100644 x-pack/plugins/infra/public/containers/with_source/source_fields_fragment.gql_query.ts delete mode 100644 x-pack/plugins/infra/public/pages/logs/logs.tsx create mode 100644 x-pack/plugins/infra/public/pages/logs/page.tsx create mode 100644 x-pack/plugins/infra/public/pages/logs/page_header.tsx create mode 100644 x-pack/plugins/infra/public/pages/logs/page_logs_content.tsx create mode 100644 x-pack/plugins/infra/public/pages/logs/page_no_indices_content.tsx create mode 100644 x-pack/plugins/infra/public/pages/logs/page_providers.tsx create mode 100644 x-pack/plugins/infra/public/pages/logs/page_toolbar.tsx delete mode 100644 x-pack/plugins/infra/public/pages/logs/toolbar.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/page_providers.tsx delete mode 100644 x-pack/plugins/infra/public/utils/memoize_last.ts delete mode 100644 x-pack/plugins/infra/public/utils/operation_status.ts delete mode 100644 x-pack/plugins/infra/public/utils/typed_constate.tsx create mode 100644 x-pack/plugins/infra/public/utils/use_tracked_promise.ts create mode 100644 x-pack/test/functional/apps/infra/constants.ts create mode 100644 x-pack/test/functional/apps/infra/logs_source_configuration.ts create mode 100644 x-pack/test/functional/apps/infra/metrics_source_configuration.ts create mode 100644 x-pack/test/functional/page_objects/infra_logs_page.ts create mode 100644 x-pack/test/functional/services/infra_source_configuration_flyout.ts diff --git a/x-pack/plugins/infra/common/graphql/shared/fragments.gql_query.ts b/x-pack/plugins/infra/common/graphql/shared/fragments.gql_query.ts index 44a5be6a8563..c696d5009bb8 100644 --- a/x-pack/plugins/infra/common/graphql/shared/fragments.gql_query.ts +++ b/x-pack/plugins/infra/common/graphql/shared/fragments.gql_query.ts @@ -13,4 +13,11 @@ export const sharedFragments = { tiebreaker } `, + InfraSourceFields: gql` + fragment InfraSourceFields on InfraSource { + id + version + updatedAt + } + `, }; diff --git a/x-pack/plugins/infra/common/graphql/types.ts b/x-pack/plugins/infra/common/graphql/types.ts index f9a1a516bee4..fdd283125a00 100644 --- a/x-pack/plugins/infra/common/graphql/types.ts +++ b/x-pack/plugins/infra/common/graphql/types.ts @@ -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; + }; +} diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx index 201518dc2250..6c320677e65f 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx @@ -145,6 +145,7 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent< onVisibleChildrenChange={this.handleVisibleChildrenChange} target={targetId} hideScrollbar={true} + data-test-subj={'logStream'} > {registerChild => ( <> diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx index 297b57e5a78c..d5ca85309c56 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx @@ -29,6 +29,7 @@ interface VerticalScrollPanelProps { height: number; width: number; hideScrollbar?: boolean; + 'data-test-subj'?: string; } interface VerticalScrollPanelSnapshot { @@ -208,11 +209,12 @@ export class VerticalScrollPanel 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 ( = ({ nodeName }) => ( - - {({ basePath }) => ( - - - - } - body={ -

- -

- } - actions={ - - - - - - - - - {({ enable }) => ( - - - - )} - - - - } - /> - )} -
-); +export const InvalidNodeError: React.FunctionComponent = ({ nodeName }) => { + const { show } = useContext(SourceConfigurationFlyoutState.Context); + + return ( + + {({ basePath }) => ( + + + + } + body={ +

+ +

+ } + actions={ + + + + + + + + + + + + + } + /> + )} +
+ ); +}; const CenteredEmptyPrompt = euiStyled(EuiEmptyPrompt)` align-self: center; diff --git a/x-pack/plugins/infra/public/components/source_configuration/index.ts b/x-pack/plugins/infra/public/components/source_configuration/index.ts index 6b465e7a2b8d..d1640f3e3470 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/index.ts +++ b/x-pack/plugins/infra/public/components/source_configuration/index.ts @@ -6,3 +6,7 @@ export { SourceConfigurationButton } from './source_configuration_button'; export { SourceConfigurationFlyout } from './source_configuration_flyout'; +export { + SourceConfigurationFlyoutState, + useSourceConfigurationFlyoutState, +} from './source_configuration_flyout_state'; diff --git a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx index e941c293e2a3..0f0489594d56 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx @@ -52,6 +52,7 @@ export const IndicesConfigurationPanel = ({ } > } > - + ); diff --git a/x-pack/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx b/x-pack/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx index 664cfbc15533..93f2d03e83cb 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx @@ -37,7 +37,13 @@ export const NameConfigurationPanel = ({ } > - + ); diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_button.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_button.tsx index 56b0f2183c86..a9ce2789830c 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_button.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_button.tsx @@ -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 = () => ( - - {({ toggle }) => ( - - {({ configuration }) => ( - - {configuration && configuration.name} - - )} - - )} - -); +export const SourceConfigurationButton: React.FunctionComponent = () => { + const { toggleIsVisible } = useContext(SourceConfigurationFlyoutState.Context); + const { source } = useContext(Source.Context); + + return ( + + {source && source.configuration && source.configuration.name} + + ); +}; diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx index dd238b1fa798..5416d34a208e 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx @@ -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) => ( - - {({ disable: close, value: isVisible }) => - isVisible ? ( - - {({ create, configuration, exists, isLoading, update }) => - configuration ? ( - + 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 ( + + + +

+ +

+
+
+ + + + + + + + + + + {!isFormDirty ? ( + hide()} + > + + + ) : ( + { + resetForm(); + hide(); }} > - {({ - getCurrentFormState, - getNameFieldProps, - getLogAliasFieldProps, - getMetricAliasFieldProps, - getFieldFieldProps, - isFormValid, - resetForm, - updates, - }) => ( - - - -

- -

-
-
- - - - - - - - - - - {updates.length === 0 ? ( - close()} - > - - - ) : ( - { - resetForm(); - close(); - }} - > - - - )} - - - - {isLoading ? ( - - Loading - - ) : ( - - (exists ? update(updates) : create(getCurrentFormState())).then( - () => resetForm() - ) - } - > - - - )} - - - -
- )} -
- ) : null - } -
- ) : null - } -
-)); + + + )} + + + + {isLoading ? ( + + Loading + + ) : ( + + + + )} + + + + + ); +}; + +const defaultFormState = { + name: '', + description: '', + fields: { + container: '', + host: '', + message: [], + pod: '', + tiebreaker: '', + timestamp: '', + }, + logAlias: '', + metricAlias: '', +}; diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout_state.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout_state.tsx index 8a0a9a2c4a4d..13fdd80238c4 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout_state.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout_state.tsx @@ -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(initialVisibility); -export const WithSourceConfigurationFlyoutState: React.SFC = props => ( - -); + 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); diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx index 986c8f47bbf4..117fc7732e42 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx @@ -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([]); -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 = { - 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 = { - 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; -} - -export const WithSourceConfigurationFormState: React.SFC = ({ - children, - initialFormState, - 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], - }), - }); - }} - -); - -const addOrCombineLastUpdate = (updates: UpdateSourceInput[], newUpdate: UpdateSourceInput) => [ - ...updates.slice(0, -1), - ...maybeCombineUpdates(updates[updates.length - 1], newUpdate), -]; +}; const createInputFieldProps = < Value extends string = string, diff --git a/x-pack/plugins/infra/public/containers/with_source/source_error_page.tsx b/x-pack/plugins/infra/public/components/source_error_page.tsx similarity index 81% rename from x-pack/plugins/infra/public/containers/with_source/source_error_page.tsx rename to x-pack/plugins/infra/public/components/source_error_page.tsx index 418976ca8711..20cb489c7090 100644 --- a/x-pack/plugins/infra/public/containers/with_source/source_error_page.tsx +++ b/x-pack/plugins/infra/public/components/source_error_page.tsx @@ -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 = ({ errorMessage, retry }) => ( +export const SourceErrorPage: React.FunctionComponent = ({ + errorMessage, + retry, +}) => ( ( +export const SourceLoadingPage: React.FunctionComponent = () => ( void; - enable: () => void; - toggle: () => void; -} - -const actions: ActionMap = { - disable: () => state => ({ ...state, value: false }), - enable: () => state => ({ ...state, value: true }), - toggle: () => state => ({ ...state, value: !state.value }), -}; - -export type WithBinaryProps = Omit< - ConstateContainerProps, - 'actions' | 'initialState' | 'pure' -> & { - initialValue?: boolean; -}; - -export const WithBinary: React.SFC = ({ initialValue = false, ...props }) => ( - -); diff --git a/x-pack/plugins/infra/public/containers/source/create_source.gql_query.ts b/x-pack/plugins/infra/public/containers/source/create_source.gql_query.ts new file mode 100644 index 000000000000..52c62fb36966 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/source/create_source.gql_query.ts @@ -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} +`; diff --git a/x-pack/plugins/infra/public/containers/primitives/index.ts b/x-pack/plugins/infra/public/containers/source/index.ts similarity index 87% rename from x-pack/plugins/infra/public/containers/primitives/index.ts rename to x-pack/plugins/infra/public/containers/source/index.ts index 8ba2e499b15c..94331b167750 100644 --- a/x-pack/plugins/infra/public/containers/primitives/index.ts +++ b/x-pack/plugins/infra/public/containers/source/index.ts @@ -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'; diff --git a/x-pack/plugins/infra/public/containers/source/query_source.gql_query.ts b/x-pack/plugins/infra/public/containers/source/query_source.gql_query.ts new file mode 100644 index 000000000000..21b5192e5725 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/source/query_source.gql_query.ts @@ -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} +`; diff --git a/x-pack/plugins/infra/public/containers/source/source.tsx b/x-pack/plugins/infra/public/containers/source/source.tsx new file mode 100644 index 000000000000..bdca56ad00f0 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/source/source.tsx @@ -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(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({ + 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); + } +} diff --git a/x-pack/plugins/infra/public/containers/source/source_fields_fragment.gql_query.ts b/x-pack/plugins/infra/public/containers/source/source_fields_fragment.gql_query.ts new file mode 100644 index 000000000000..e5edc3c24e2b --- /dev/null +++ b/x-pack/plugins/infra/public/containers/source/source_fields_fragment.gql_query.ts @@ -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 + } +`; diff --git a/x-pack/plugins/infra/public/containers/with_source/update_source.gql_query.ts b/x-pack/plugins/infra/public/containers/source/update_source.gql_query.ts similarity index 52% rename from x-pack/plugins/infra/public/containers/with_source/update_source.gql_query.ts rename to x-pack/plugins/infra/public/containers/source/update_source.gql_query.ts index b6e30b74c701..3df1443de0f1 100644 --- a/x-pack/plugins/infra/public/containers/with_source/update_source.gql_query.ts +++ b/x-pack/plugins/infra/public/containers/source/update_source.gql_query.ts @@ -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} `; diff --git a/x-pack/plugins/infra/public/containers/with_source/create_source.gql_query.ts b/x-pack/plugins/infra/public/containers/with_source/create_source.gql_query.ts deleted file mode 100644 index 3bbb960dfad3..000000000000 --- a/x-pack/plugins/infra/public/containers/with_source/create_source.gql_query.ts +++ /dev/null @@ -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} -`; diff --git a/x-pack/plugins/infra/public/containers/with_source/index.ts b/x-pack/plugins/infra/public/containers/with_source/index.ts index f16f20be2a99..1089adbdcd04 100644 --- a/x-pack/plugins/infra/public/containers/with_source/index.ts +++ b/x-pack/plugins/infra/public/containers/with_source/index.ts @@ -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'; diff --git a/x-pack/plugins/infra/public/containers/with_source/query_source.gql_query.ts b/x-pack/plugins/infra/public/containers/with_source/query_source.gql_query.ts deleted file mode 100644 index 0dfe2dbf04b5..000000000000 --- a/x-pack/plugins/infra/public/containers/with_source/query_source.gql_query.ts +++ /dev/null @@ -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} -`; diff --git a/x-pack/plugins/infra/public/containers/with_source/source_fields_fragment.gql_query.ts b/x-pack/plugins/infra/public/containers/with_source/source_fields_fragment.gql_query.ts deleted file mode 100644 index 32e4100ac2de..000000000000 --- a/x-pack/plugins/infra/public/containers/with_source/source_fields_fragment.gql_query.ts +++ /dev/null @@ -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 - } - } -`; diff --git a/x-pack/plugins/infra/public/containers/with_source/with_source.tsx b/x-pack/plugins/infra/public/containers/with_source/with_source.tsx index 8b3437390d23..312525bad8b7 100644 --- a/x-pack/plugins/infra/public/containers/with_source/with_source.tsx +++ b/x-pack/plugins/infra/public/containers/with_source/with_source.tsx @@ -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>; - source: SourceQuery.Query['source'] | undefined; -} - -const createContainerProps = memoizeLast((sourceId: string, apolloClient: ApolloClient) => { - const initialState: State = { - operationStatusHistory: [], - source: undefined, - }; - - const actions = inferActionMap()({ - ...createStatusActions((updater: StatusHistoryUpdater) => (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()({ - ...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()({ - 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({ - 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({ - 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({ - 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 = 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; + create: (sourceConfiguration: CreateSourceInput) => Promise | undefined; derivedIndexPattern: StaticIndexPattern; - exists: boolean; + exists?: boolean; hasFailed: boolean; isLoading: boolean; lastFailureMessage?: string; - load: () => Promise; + load: () => Promise | undefined; logIndicesExist?: boolean; metricAlias?: string; metricIndicesExist?: boolean; sourceId: string; - update: (changes: UpdateSourceInput[]) => Promise; + update: (changes: UpdateSourceInput[]) => Promise | undefined; version?: string; }>; } -export const WithSource: React.SFC = ({ children }) => ( - - {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(), - }) - } - - )} - -); +export const WithSource: React.FunctionComponent = ({ 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, + }); +}; diff --git a/x-pack/plugins/infra/public/graphql/types.ts b/x-pack/plugins/infra/public/graphql/types.ts index f9a1a516bee4..fdd283125a00 100644 --- a/x-pack/plugins/infra/public/graphql/types.ts +++ b/x-pack/plugins/infra/public/graphql/types.ts @@ -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; + }; +} diff --git a/x-pack/plugins/infra/public/pages/infrastructure/index.tsx b/x-pack/plugins/infra/public/pages/infrastructure/index.tsx index 8a5823d18409..6250f2e51cd7 100644 --- a/x-pack/plugins/infra/public/pages/infrastructure/index.tsx +++ b/x-pack/plugins/infra/public/pages/infrastructure/index.tsx @@ -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) => ( - - + + + + - + - + - - - - - + + + + + + + )); diff --git a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/index.tsx b/x-pack/plugins/infra/public/pages/infrastructure/snapshot/index.tsx index 742c7dd4b997..59069290fe6e 100644 --- a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/index.tsx +++ b/x-pack/plugins/infra/public/pages/infrastructure/snapshot/index.tsx @@ -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 { - public static displayName = 'SnapshotPage'; - - public render() { - const { intl } = this.props; - - return ( - - - intl.formatMessage( - { - id: 'xpack.infra.infrastructureSnapshotPage.documentTitle', - defaultMessage: '{previousTitle} | Snapshot', - }, - { - previousTitle, - } - ) + return ( + + + intl.formatMessage( + { + id: 'xpack.infra.infrastructureSnapshotPage.documentTitle', + defaultMessage: '{previousTitle} | Snapshot', + }, + { + previousTitle, } - /> -
- - - {({ - derivedIndexPattern, - hasFailed, - isLoading, - lastFailureMessage, - load, - metricIndicesExist, - }) => - isLoading ? ( - - ) : metricIndicesExist ? ( - <> - - - - - - - ) : hasFailed ? ( - - ) : ( - - {({ basePath }) => ( - +
+ + {isLoading ? ( + + ) : metricIndicesExist ? ( + <> + + + + + + + ) : hasFailedLoadingSource ? ( + + ) : ( + + {({ basePath }) => ( + + + + {intl.formatMessage({ + id: 'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', + defaultMessage: 'View setup instructions', })} - message={intl.formatMessage({ - id: 'xpack.infra.homePage.noMetricsIndicesDescription', - defaultMessage: "Let's add some!", + + + + + {intl.formatMessage({ + id: 'xpack.infra.configureSourceActionLabel', + defaultMessage: 'Change source configuration', })} - actions={ - - - - {intl.formatMessage({ - id: 'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', - defaultMessage: 'View setup instructions', - })} - - - - - {({ enable }) => ( - - {intl.formatMessage({ - id: 'xpack.infra.configureSourceActionLabel', - defaultMessage: 'Change source configuration', - })} - - )} - - - - } - data-test-subj="noMetricsIndicesPrompt" - /> - )} - - ) - } - - - ); - } - } -); + + + + } + data-test-subj="noMetricsIndicesPrompt" + /> + )} + + )} + + ); +}); diff --git a/x-pack/plugins/infra/public/pages/link_to/link_to.tsx b/x-pack/plugins/infra/public/pages/link_to/link_to.tsx index 5560c7d9f873..bdab0b1e280e 100644 --- a/x-pack/plugins/infra/public/pages/link_to/link_to.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/link_to.tsx @@ -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 { const { match } = this.props; return ( - - - - - - + + + + + + + + ); } } diff --git a/x-pack/plugins/infra/public/pages/logs/index.ts b/x-pack/plugins/infra/public/pages/logs/index.ts index c2ae23acdc9b..1d1c8cc65287 100644 --- a/x-pack/plugins/infra/public/pages/logs/index.ts +++ b/x-pack/plugins/infra/public/pages/logs/index.ts @@ -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'; diff --git a/x-pack/plugins/infra/public/pages/logs/logs.tsx b/x-pack/plugins/infra/public/pages/logs/logs.tsx deleted file mode 100644 index 1417ad9e502a..000000000000 --- a/x-pack/plugins/infra/public/pages/logs/logs.tsx +++ /dev/null @@ -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 { - public static displayName = 'LogsPage'; - - public render() { - const { intl } = this.props; - - return ( - - -
- - {({ - derivedIndexPattern, - hasFailed, - isLoading, - lastFailureMessage, - load, - logIndicesExist, - sourceId, - }) => ( - <> - - - - {isLoading ? ( - - ) : logIndicesExist ? ( - <> - - - - - - - - {({ applyFilterQueryFromKueryExpression }) => ( - - - {({ showFlyout, setFlyoutItem }) => ( - - )} - - - {({ flyoutItem, hideFlyout, loading }) => ( - - )} - - - )} - - - ) : hasFailed ? ( - - ) : ( - - {({ basePath }) => ( - - - - {intl.formatMessage({ - id: - 'xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel', - defaultMessage: 'View setup instructions', - })} - - - - - {({ enable }) => ( - - {intl.formatMessage({ - id: 'xpack.infra.configureSourceActionLabel', - defaultMessage: 'Change source configuration', - })} - - )} - - - - } - /> - )} - - )} - - )} - - - - ); - } - } -); diff --git a/x-pack/plugins/infra/public/pages/logs/page.tsx b/x-pack/plugins/infra/public/pages/logs/page.tsx new file mode 100644 index 000000000000..8090316d98d3 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/page.tsx @@ -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 = () => ( + + + + + + +); diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx index 8b2e7ec493d4..a8dde0532283 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -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 = ({ showFlyout, setFlyoutItem }) => { - const { intervalSize, textScale, textWrap } = useContext(LogViewConfiguration.Context); +export const LogsPageContent: React.FunctionComponent = () => { + const { + hasFailedLoadingSource, + isLoadingSource, + logIndicesExist, + loadSource, + loadSourceFailureMessage, + } = useContext(Source.Context); return ( - - - {({ measureRef, content: { width = 0, height = 0 } }) => ( - - - {({ - isAutoReloading, - jumpToTargetPosition, - reportVisiblePositions, - targetPosition, - }) => ( - - {({ - hasMoreAfterEnd, - hasMoreBeforeStart, - isLoadingMore, - isReloading, - items, - lastLoadedTime, - loadNewerEntries, - }) => ( - - )} - - )} - - - )} - - - {({ measureRef, content: { width = 0, height = 0 } }) => { - return ( - - - {({ buckets }) => ( - - {({ jumpToTargetPosition, visibleMidpointTime, visibleTimeInterval }) => ( - - )} - - )} - - - ); - }} - - + <> + {isLoadingSource ? ( + + ) : logIndicesExist ? ( + + ) : hasFailedLoadingSource ? ( + + ) : ( + + )} + ); }; - -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; -`; diff --git a/x-pack/plugins/infra/public/pages/logs/page_header.tsx b/x-pack/plugins/infra/public/pages/logs/page_header.tsx new file mode 100644 index 000000000000..a3acb34bf25c --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/page_header.tsx @@ -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 ( + <> +
+ + + + + ); +}); diff --git a/x-pack/plugins/infra/public/pages/logs/page_logs_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_logs_content.tsx new file mode 100644 index 000000000000..f9c4c7b4d953 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/page_logs_content.tsx @@ -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 ( + <> + + + + + + + + {({ applyFilterQueryFromKueryExpression }) => ( + + {({ flyoutItem, hideFlyout, loading }) => ( + + )} + + )} + + + {({ showFlyout, setFlyoutItem }) => ( + + + {({ measureRef, content: { width = 0, height = 0 } }) => ( + + + {({ + isAutoReloading, + jumpToTargetPosition, + reportVisiblePositions, + targetPosition, + }) => ( + + {({ + hasMoreAfterEnd, + hasMoreBeforeStart, + isLoadingMore, + isReloading, + items, + lastLoadedTime, + loadNewerEntries, + }) => ( + + )} + + )} + + + )} + + + {({ measureRef, content: { width = 0, height = 0 } }) => { + return ( + + + {({ buckets }) => ( + + {({ jumpToTargetPosition, visibleMidpointTime, visibleTimeInterval }) => ( + + )} + + )} + + + ); + }} + + + )} + + + ); +}; + +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; +`; diff --git a/x-pack/plugins/infra/public/pages/logs/page_no_indices_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_no_indices_content.tsx new file mode 100644 index 000000000000..44cda7fc97aa --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/page_no_indices_content.tsx @@ -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 ( + + {({ basePath }) => ( + + + + {intl.formatMessage({ + id: 'xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel', + defaultMessage: 'View setup instructions', + })} + + + + + {intl.formatMessage({ + id: 'xpack.infra.configureSourceActionLabel', + defaultMessage: 'Change source configuration', + })} + + + + } + /> + )} + + ); +}); diff --git a/x-pack/plugins/infra/public/pages/logs/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/page_providers.tsx new file mode 100644 index 000000000000..45a416a21049 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/page_providers.tsx @@ -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 }) => ( + + + {children} + + +); diff --git a/x-pack/plugins/infra/public/pages/logs/page_toolbar.tsx b/x-pack/plugins/infra/public/pages/logs/page_toolbar.tsx new file mode 100644 index 000000000000..d67c863e825e --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/page_toolbar.tsx @@ -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 ( + + + + + {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( + + {({ + applyFilterQueryFromKueryExpression, + filterQueryDraft, + isFilterQueryDraftValid, + setFilterQueryDraftFromKueryExpression, + }) => ( + + )} + + )} + + + + + + + + + + + + + + + {({ + visibleMidpointTime, + isAutoReloading, + jumpToTargetPositionTime, + startLiveStreaming, + stopLiveStreaming, + }) => ( + + )} + + + + + ); +}); diff --git a/x-pack/plugins/infra/public/pages/logs/toolbar.tsx b/x-pack/plugins/infra/public/pages/logs/toolbar.tsx deleted file mode 100644 index 413c5c55c39d..000000000000 --- a/x-pack/plugins/infra/public/pages/logs/toolbar.tsx +++ /dev/null @@ -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 ( - - - {({ derivedIndexPattern }) => ( - - - - {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( - - {({ - applyFilterQueryFromKueryExpression, - filterQueryDraft, - isFilterQueryDraftValid, - setFilterQueryDraftFromKueryExpression, - }) => ( - - )} - - )} - - - - - - - - - - - - - - - {({ - visibleMidpointTime, - isAutoReloading, - jumpToTargetPositionTime, - startLiveStreaming, - stopLiveStreaming, - }) => ( - - )} - - - - )} - - - ); -}); diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 84180a84dfe3..2d5ac5b1c192 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -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 ( - + {({ sourceId }) => ( @@ -241,7 +241,7 @@ export const MetricDetail = withTheme( )} - + ); } diff --git a/x-pack/plugins/infra/public/pages/metrics/page_providers.tsx b/x-pack/plugins/infra/public/pages/metrics/page_providers.tsx new file mode 100644 index 000000000000..49f07837024f --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/page_providers.tsx @@ -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 }) => ( + + + {children} + + +); diff --git a/x-pack/plugins/infra/public/utils/memoize_last.ts b/x-pack/plugins/infra/public/utils/memoize_last.ts deleted file mode 100644 index 2f24f5bb7f6f..000000000000 --- a/x-pack/plugins/infra/public/utils/memoize_last.ts +++ /dev/null @@ -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 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 }; diff --git a/x-pack/plugins/infra/public/utils/operation_status.ts b/x-pack/plugins/infra/public/utils/operation_status.ts deleted file mode 100644 index 922323440cab..000000000000 --- a/x-pack/plugins/infra/public/utils/operation_status.ts +++ /dev/null @@ -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> { - operation: O; - status: 'in-progress'; - time: number; -} - -export interface SucceededStatus> { - operation: O; - status: 'succeeded'; - time: number; -} - -export interface FailedStatus> { - message: string; - operation: O; - status: 'failed'; - time: number; -} - -const isFailedStatus = >( - status: OperationStatus -): status is FailedStatus => status.status === 'failed'; - -export type OperationStatus> = - | InProgressStatus - | SucceededStatus - | FailedStatus; - -export interface Operation { - name: Name; - parameters: Parameters; -} - -export const createStatusSelectors = ( - selectStatusHistory: (state: S) => Array> -) => ({ - 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> = ( - statusHistory: Array> -) => Array>; - -export const createStatusActions = >( - updateStatusHistory: (updater: StatusHistoryUpdater) => (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(), - }, - ]), -}); diff --git a/x-pack/plugins/infra/public/utils/typed_constate.tsx b/x-pack/plugins/infra/public/utils/typed_constate.tsx deleted file mode 100644 index ae471e4c1929..000000000000 --- a/x-pack/plugins/infra/public/utils/typed_constate.tsx +++ /dev/null @@ -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()({ - * increment: (amount: number) => state => ({ ...state, count: state.count + amount }), - * }); - * // actions has type ActionMap void; }> - * ``` - */ - -import { ActionMap, EffectMap, EffectProps, SelectorMap } from 'constate'; - -/** - * actions - */ - -type InferredAction = Action extends (...args: infer A) => (state: State) => State - ? (...args: A) => void - : never; - -type InferredActions = ActionMap< - State, - { [K in keyof Actions]: InferredAction } ->; - -export type ActionsFromMap = M extends ActionMap ? A : never; - -export const inferActionMap = () => < - Actions extends { - [key: string]: (...args: any[]) => (state: State) => State; - } ->( - actionMap: Actions -): InferredActions => actionMap as any; - -/** - * effects - */ - -type InferredEffect = Effect extends ( - ...args: infer A -) => (props: EffectProps) => infer R - ? (...args: A) => R - : never; - -type InferredEffects = EffectMap< - State, - { [K in keyof Effects]: InferredEffect } ->; - -export type EffectsFromMap = M extends EffectMap ? E : never; - -export const inferEffectMap = () => < - Effects extends { - [key: string]: (...args: any[]) => (props: EffectProps) => any; - } ->( - effectMap: Effects -): InferredEffects => effectMap as any; - -/** - * selectors - */ - -type InferredSelector = Selector extends ( - ...args: infer A -) => (state: State) => infer R - ? (...args: A) => R - : never; - -type InferredSelectors = SelectorMap< - State, - { [K in keyof Selectors]: InferredSelector } ->; - -export type SelectorsFromMap = M extends SelectorMap ? S : never; - -export const inferSelectorMap = () => < - Selectors extends { - [key: string]: (...args: any[]) => (state: State) => any; - } ->( - selectorMap: Selectors -): InferredSelectors => selectorMap as any; diff --git a/x-pack/plugins/infra/public/utils/use_tracked_promise.ts b/x-pack/plugins/infra/public/utils/use_tracked_promise.ts new file mode 100644 index 000000000000..366caf0dfb15 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/use_tracked_promise.ts @@ -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 { + createPromise: (...args: Arguments) => Promise; + 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 = ( + { + createPromise, + onResolve = noOp, + onReject = noOp, + cancelPreviousOn = 'never', + }: UseTrackedPromiseArgs, + dependencies: DependencyList +) => { + /** + * If a promise is currently pending, this holds a reference to it and its + * cancellation function. + */ + const pendingPromises = useRef>>([]); + + /** + * The state of the promise most recently created by the `createPromise` + * factory. It could be uninitialized, pending, resolved or rejected. + */ + const [promiseState, setPromiseState] = useState>({ + state: 'uninitialized', + }); + + const execute = useMemo( + () => (...args: Arguments) => { + let rejectCancellationPromise!: (value: any) => void; + const cancellationPromise = new Promise((_, 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 = { + 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 { + state: 'pending'; + promise: Promise; +} + +interface ResolvedPromiseState { + state: 'resolved'; + promise: Promise; + value: ResolvedValue; +} + +interface RejectedPromiseState { + state: 'rejected'; + promise: Promise; + value: RejectedValue; +} + +type SettledPromise = + | ResolvedPromiseState + | RejectedPromiseState; + +type PromiseState = + | UninitializedPromiseState + | PendingPromiseState + | SettledPromise; + +interface CancelablePromise { + // reject the promise prematurely with a CanceledPromiseError + cancel: () => void; + // reject the promise prematurely with a SilentCanceledPromiseError + cancelSilently: () => void; + // the tracked promise + promise: Promise; +} + +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; diff --git a/x-pack/test/api_integration/apis/infra/sources.ts b/x-pack/test/api_integration/apis/infra/sources.ts index 7c211b0ce650..090d3948c6b3 100644 --- a/x-pack/test/api_integration/apis/infra/sources.ts +++ b/x-pack/test/api_integration/apis/infra/sources.ts @@ -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'; diff --git a/x-pack/test/functional/apps/infra/constants.ts b/x-pack/test/functional/apps/infra/constants.ts new file mode 100644 index 000000000000..cf18fea5d82a --- /dev/null +++ b/x-pack/test/functional/apps/infra/constants.ts @@ -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, + }, + }, +}; diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 3b57b48e98b4..fe18d33723ab 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -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) => { diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index 8297a878025c..922e0a48f513 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -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')); }); }; diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts new file mode 100644 index 000000000000..4d12041be11a --- /dev/null +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -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(); + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/infra/metrics_source_configuration.ts b/x-pack/test/functional/apps/infra/metrics_source_configuration.ts new file mode 100644 index 000000000000..6484e27aa151 --- /dev/null +++ b/x-pack/test/functional/apps/infra/metrics_source_configuration.ts @@ -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(); + }); + }); + }); +}; diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index f6326de4d42a..0596de6e622d 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -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', }, }; - } diff --git a/x-pack/test/functional/page_objects/index.js b/x-pack/test/functional/page_objects/index.js index e740873ab24c..0eb5c77fe11f 100644 --- a/x-pack/test/functional/page_objects/index.js +++ b/x-pack/test/functional/page_objects/index.js @@ -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'; diff --git a/x-pack/test/functional/page_objects/infra_home_page.ts b/x-pack/test/functional/page_objects/infra_home_page.ts index 7eb71a1196b7..9810d7313952 100644 --- a/x-pack/test/functional/page_objects/infra_home_page.ts +++ b/x-pack/test/functional/page_objects/infra_home_page.ts @@ -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'); + }, }; } diff --git a/x-pack/test/functional/page_objects/infra_logs_page.ts b/x-pack/test/functional/page_objects/infra_logs_page.ts new file mode 100644 index 000000000000..9b5cb4503c64 --- /dev/null +++ b/x-pack/test/functional/page_objects/infra_logs_page.ts @@ -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'); + }, + }; +} diff --git a/x-pack/test/functional/services/index.js b/x-pack/test/functional/services/index.js index 5c5fd4173c6b..3cc07112c9bf 100644 --- a/x-pack/test/functional/services/index.js +++ b/x-pack/test/functional/services/index.js @@ -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'; diff --git a/x-pack/test/functional/services/infra_source_configuration_flyout.ts b/x-pack/test/functional/services/infra_source_configuration_flyout.ts new file mode 100644 index 000000000000..c8a600f56b8b --- /dev/null +++ b/x-pack/test/functional/services/infra_source_configuration_flyout.ts @@ -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); + }, + }; +}