From ad111bed1f7cd1c9484f82c9008b77594ba49997 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Tue, 6 Aug 2019 11:34:25 +0100 Subject: [PATCH 01/26] [LogsUI] [InfraUI] Turn source configuration into a tab and standardize the main navigation (#42243) * Setup Logs routing for multiple pages * Adds nested routing for logs * Adds an index page to handle shared concerns * Adds the Stream page at /logs/stream * Introduce shared settings page * Introduces shared/settings page * Adds shared/settings page as a tab in the Logs routes * Removes previous source configuration flyout traces from Logs pages * Begin styling adjustments to settings page * Implements use of EUI Panels * Centers page content * Add discard button * Add discard button to allow resetting the form * Fix button alignment * Align Apply and Discard buttons to the right * Align Loading button to the right * Add EuiDescribedFormGroup for all form fields * Add EuiDescribedFormGroup for name panel * Add EuiDescribedFormGroup for indices panel * Add EuiDescribedFormGroup for fields panel * Remove all SourceConfigurationFlyout traces from the Infrastructure UI * Add a ViewSourceConfigurationButton * Adds a ViewSourceConfigurationButton component that will route to the /settings page * Replace all instances of "View Configuration" buttons that were opening the flyout with the new button * Enable settings tab amongst Infrastructure routes * Change navigation to mimic SIEM * Introduces an AppNavigation component * Amends styling / handling of RoutedTabs to match SIEM implementation * Adds new AppNavigation component to Infrastructure and Logs indexe pages * Functional test amendments (WIP) * Temporarily disable certain functional tests * Remove unused imports * Disable imports so build can pass * Amend I18N errors * I18N * Automatically fix issues with i18n (node scripts/i18n_check --fix result) * Functional tests * Amend tests so they pass locally. Pending CI test. * Amend RoutedTabs (without link focus style) * Tweak RoutedTabs and AppNavigation for better performance / visuals * Ensure outline isn't cut off * Ensure only the react-router instance is hit for performance * Ensure links still have href attributes for things like "Open in a new tab" even if history.push ultimately navigates * Fix i18n usages * node scripts/i18n_check --fix * Amend functional test config (post Master merge fix) * Remove unused function and fix unused import * Add a Prompt to notify users when form changes will be lost * Add aria-label to Button --- .../log_text_stream/column_headers.tsx | 23 +- .../scrollable_log_text_stream_view.tsx | 3 - .../components/metrics/invalid_node.tsx | 16 +- .../components/navigation/app_navigation.tsx | 33 ++ .../components/navigation/routed_tabs.tsx | 34 +- .../fields_configuration_panel.tsx | 295 ++++++++++++------ .../components/source_configuration/index.ts | 9 +- .../indices_configuration_panel.tsx | 128 +++++--- .../log_columns_configuration_panel.tsx | 3 +- .../name_configuration_panel.tsx | 49 ++- .../source_configuration_button.tsx | 31 -- .../source_configuration_flyout.tsx | 281 ----------------- .../source_configuration_flyout_state.tsx | 51 --- .../source_configuration_settings.tsx | 213 +++++++++++++ .../view_source_configuration_button.tsx | 40 +++ .../public/pages/infrastructure/index.tsx | 85 ++--- .../pages/infrastructure/snapshot/index.tsx | 17 +- .../pages/infrastructure/snapshot/toolbar.tsx | 4 - .../plugins/infra/public/pages/logs/index.tsx | 69 ++++ .../infra/public/pages/logs/page_header.tsx | 57 ---- .../public/pages/logs/page_providers.tsx | 33 -- .../public/pages/logs/{ => stream}/index.ts | 2 +- .../public/pages/logs/{ => stream}/page.tsx | 14 +- .../pages/logs/{ => stream}/page_content.tsx | 8 +- .../public/pages/logs/stream/page_header.tsx | 45 +++ .../logs/{ => stream}/page_logs_content.tsx | 37 +-- .../{ => stream}/page_no_indices_content.tsx | 19 +- .../pages/logs/stream/page_providers.tsx | 30 ++ .../pages/logs/{ => stream}/page_toolbar.tsx | 34 +- .../infra/public/pages/metrics/index.tsx | 4 - .../public/pages/metrics/page_providers.tsx | 9 +- .../public/pages/shared/settings/index.tsx | 18 ++ x-pack/legacy/plugins/infra/public/routes.tsx | 1 + .../translations/translations/ja-JP.json | 29 +- .../translations/translations/zh-CN.json | 29 +- .../apps/infra/logs_source_configuration.ts | 63 ++-- .../infra/metrics_source_configuration.ts | 29 +- .../page_objects/infra_home_page.ts | 7 + .../page_objects/infra_logs_page.ts | 5 - x-pack/test/functional/services/index.ts | 4 +- .../functional/services/infra_log_stream.ts | 10 +- ....ts => infra_source_configuration_form.ts} | 71 ++--- 42 files changed, 1013 insertions(+), 929 deletions(-) create mode 100644 x-pack/legacy/plugins/infra/public/components/navigation/app_navigation.tsx delete mode 100644 x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_button.tsx delete mode 100644 x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx delete mode 100644 x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_flyout_state.tsx create mode 100644 x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx create mode 100644 x-pack/legacy/plugins/infra/public/components/source_configuration/view_source_configuration_button.tsx create mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/index.tsx delete mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/page_header.tsx delete mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/page_providers.tsx rename x-pack/legacy/plugins/infra/public/pages/logs/{ => stream}/index.ts (86%) rename x-pack/legacy/plugins/infra/public/pages/logs/{ => stream}/page.tsx (65%) rename x-pack/legacy/plugins/infra/public/pages/logs/{ => stream}/page_content.tsx (77%) create mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/stream/page_header.tsx rename x-pack/legacy/plugins/infra/public/pages/logs/{ => stream}/page_logs_content.tsx (77%) rename x-pack/legacy/plugins/infra/public/pages/logs/{ => stream}/page_no_indices_content.tsx (82%) create mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/stream/page_providers.tsx rename x-pack/legacy/plugins/infra/public/pages/logs/{ => stream}/page_toolbar.tsx (78%) create mode 100644 x-pack/legacy/plugins/infra/public/pages/shared/settings/index.tsx rename x-pack/test/functional/services/{infra_source_configuration_flyout.ts => infra_source_configuration_form.ts} (66%) diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx index 398ef42048d70..ba8e6d90ca7ad 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonIcon } from '@elastic/eui'; import { injectI18n } from '@kbn/i18n/react'; import React from 'react'; @@ -20,20 +19,13 @@ import { LogEntryColumnContent, LogEntryColumnWidth, LogEntryColumnWidths, - iconColumnId, } from './log_entry_column'; import { ASSUMED_SCROLLBAR_WIDTH } from './vertical_scroll_panel'; export const LogColumnHeaders = injectI18n<{ columnConfigurations: LogColumnConfiguration[]; columnWidths: LogEntryColumnWidths; - showColumnConfiguration: () => void; -}>(({ columnConfigurations, columnWidths, intl, showColumnConfiguration }) => { - const showColumnConfigurationLabel = intl.formatMessage({ - id: 'xpack.infra.logColumnHeaders.configureColumnsLabel', - defaultMessage: 'Configure source', - }); - +}>(({ columnConfigurations, columnWidths, intl }) => { return ( {columnConfigurations.map(columnConfiguration => { @@ -69,19 +61,6 @@ export const LogColumnHeaders = injectI18n<{ ); } })} - - - ); }); diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx index c765531f90831..b9a4d4244a1ff 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx @@ -48,7 +48,6 @@ interface ScrollableLogTextStreamViewProps { loadNewerItems: () => void; setFlyoutItem: (id: string) => void; setFlyoutVisibility: (visible: boolean) => void; - showColumnConfiguration: () => void; intl: InjectedIntl; highlightedItem: string | null; currentHighlightKey: UniqueTimeKey | null; @@ -109,7 +108,6 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent< items, lastLoadedTime, scale, - showColumnConfiguration, wrap, } = this.props; const { targetId } = this.state; @@ -153,7 +151,6 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent< {({ measureRef, bounds: { height = 0 }, content: { width = 0 } }) => ( diff --git a/x-pack/legacy/plugins/infra/public/components/metrics/invalid_node.tsx b/x-pack/legacy/plugins/infra/public/components/metrics/invalid_node.tsx index 60bd51f0ba6de..673bca91904c7 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics/invalid_node.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics/invalid_node.tsx @@ -6,19 +6,20 @@ import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useContext } from 'react'; +import React from 'react'; import euiStyled from '../../../../../common/eui_styled_components'; -import { SourceConfigurationFlyoutState } from '../../components/source_configuration'; import { WithKibanaChrome } from '../../containers/with_kibana_chrome'; +import { + ViewSourceConfigurationButton, + ViewSourceConfigurationButtonHrefBase, +} from '../../components/source_configuration'; interface InvalidNodeErrorProps { nodeName: string; } export const InvalidNodeError: React.FunctionComponent = ({ nodeName }) => { - const { showIndicesConfiguration } = useContext(SourceConfigurationFlyoutState.Context); - return ( {({ basePath }) => ( @@ -57,12 +58,15 @@ export const InvalidNodeError: React.FunctionComponent = - + - + } diff --git a/x-pack/legacy/plugins/infra/public/components/navigation/app_navigation.tsx b/x-pack/legacy/plugins/infra/public/components/navigation/app_navigation.tsx new file mode 100644 index 0000000000000..b1eef34001750 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/components/navigation/app_navigation.tsx @@ -0,0 +1,33 @@ +/* + * 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 React from 'react'; +import euiStyled from '../../../../../common/eui_styled_components'; + +interface AppNavigationProps { + children: React.ReactNode; +} + +export const AppNavigation = ({ children }: AppNavigationProps) => ( + +); + +const Nav = euiStyled.nav` + background: ${props => props.theme.eui.euiColorEmptyShade}; + border-bottom: ${props => props.theme.eui.euiBorderThin}; + padding: ${props => + `${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL} ${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL}`} + + .euiTabs { + padding-left: 3px; + margin-left: -3px; + } +`; diff --git a/x-pack/legacy/plugins/infra/public/components/navigation/routed_tabs.tsx b/x-pack/legacy/plugins/infra/public/components/navigation/routed_tabs.tsx index 5c510fb2d3a6a..5fe36d8c5af0c 100644 --- a/x-pack/legacy/plugins/infra/public/components/navigation/routed_tabs.tsx +++ b/x-pack/legacy/plugins/infra/public/components/navigation/routed_tabs.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiTab, EuiTabs } from '@elastic/eui'; +import { EuiTab, EuiTabs, EuiLink } from '@elastic/eui'; import React from 'react'; import { Route } from 'react-router-dom'; +import euiStyled from '../../../../../common/eui_styled_components'; interface TabConfiguration { title: string; @@ -17,27 +18,42 @@ interface RoutedTabsProps { tabs: TabConfiguration[]; } +const noop = () => {}; + export class RoutedTabs extends React.Component { public render() { - return {this.renderTabs()}; + return {this.renderTabs()}; } private renderTabs() { return this.props.tabs.map(tab => { return ( ( - (match ? undefined : history.push(tab.path))} - isSelected={match !== null} - > - {tab.title} - + + { + e.preventDefault(); + history.push(tab.path); + }} + > + + {tab.title} + + + )} /> ); }); } } + +const TabContainer = euiStyled.div` + .euiLink { + color: inherit !important; + } +`; diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx index 5b79132ecf1f5..771285e8ccee4 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx @@ -4,7 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiCode, EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { + EuiDescribedFormGroup, + EuiCode, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; @@ -39,145 +47,230 @@ export const FieldsConfigurationPanel = ({ - @timestamp, - }} + id="xpack.infra.sourceConfiguration.timestampFieldLabel" + defaultMessage="Timestamp" /> } - isInvalid={timestampFieldProps.isInvalid} - label={ + description={ } > - - - _doc, - }} + helpText={ + @timestamp, + }} + /> + } + isInvalid={timestampFieldProps.isInvalid} + label={ + + } + > + - } - isInvalid={tiebreakerFieldProps.isInvalid} - label={ + + + } - > - - - container.id, - }} + id="xpack.infra.sourceConfiguration.tiebreakerFieldDescription" + defaultMessage="Field used to break ties between two entries with the same timestamp" /> } - isInvalid={containerFieldProps.isInvalid} - label={ + > + _doc, + }} + /> + } + isInvalid={tiebreakerFieldProps.isInvalid} + label={ + + } + > + + + + } + description={ + + } > - - - container.id, + }} + /> + } + isInvalid={containerFieldProps.isInvalid} + label={ + + } + > + + + + host.name, - }} + id="xpack.infra.sourceConfiguration.hostNameFieldLabel" + defaultMessage="Host name" /> } - isInvalid={hostFieldProps.isInvalid} - label={ + description={ } > - - - kubernetes.pod.uid, - }} + helpText={ + host.name, + }} + /> + } + isInvalid={hostFieldProps.isInvalid} + label={ + + } + > + - } - isInvalid={podFieldProps.isInvalid} - label={ + + + } + description={ + + } > - - + helpText={ + kubernetes.pod.uid, + }} + /> + } + isInvalid={podFieldProps.isInvalid} + label={ + + } + > + + + ); diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/index.ts b/x-pack/legacy/plugins/infra/public/components/source_configuration/index.ts index d1640f3e34708..4879a53ca329d 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/index.ts +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/index.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { SourceConfigurationButton } from './source_configuration_button'; -export { SourceConfigurationFlyout } from './source_configuration_flyout'; +export { SourceConfigurationSettings } from './source_configuration_settings'; export { - SourceConfigurationFlyoutState, - useSourceConfigurationFlyoutState, -} from './source_configuration_flyout_state'; + ViewSourceConfigurationButton, + ViewSourceConfigurationButtonHrefBase, +} from './view_source_configuration_button'; diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx index d2129345d71b3..ee0e605baaf5c 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx @@ -4,7 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiCode, EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { + EuiCode, + EuiDescribedFormGroup, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; @@ -33,63 +41,97 @@ export const IndicesConfigurationPanel = ({ - metricbeat-*, - }} + id="xpack.infra.sourceConfiguration.metricIndicesTitle" + defaultMessage="Metric Indices" /> } - isInvalid={metricAliasFieldProps.isInvalid} - label={ + description={ } > - - - metricbeat-*, + }} + /> + } + isInvalid={metricAliasFieldProps.isInvalid} + label={ + + } + > + + + + filebeat-*, - }} + id="xpack.infra.sourceConfiguration.logIndicesTitle" + defaultMessage="Log Indices" /> } - isInvalid={logAliasFieldProps.isInvalid} - label={ + description={ } > - - + helpText={ + filebeat-*, + }} + /> + } + isInvalid={logAliasFieldProps.isInvalid} + label={ + + } + > + + + ); diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx index 53d426eba0449..708fd34f23257 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx @@ -56,7 +56,7 @@ export const LogColumnsConfigurationPanel: React.FunctionComponent<

@@ -245,6 +245,7 @@ const RemoveLogColumnButton: React.FunctionComponent<{ iconType="trash" onClick={onClick} title={removeColumnLabel} + aria-label={removeColumnLabel} /> ); }; diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx index 5d4f63f2c019f..7129097cff160 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { + EuiDescribedFormGroup, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; @@ -31,22 +38,36 @@ export const NameConfigurationPanel = ({ - } + description={ + + } > - - + isInvalid={nameFieldProps.isInvalid} + label={ + + } + > + + + ); diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_button.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_button.tsx deleted file mode 100644 index b05e5d9e34e08..0000000000000 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_button.tsx +++ /dev/null @@ -1,31 +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 { EuiButtonEmpty } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useContext } from 'react'; - -import { SourceConfigurationFlyoutState } from './source_configuration_flyout_state'; - -export const SourceConfigurationButton: React.FunctionComponent = () => { - const { toggleIsVisible } = useContext(SourceConfigurationFlyoutState.Context); - - return ( - - - - ); -}; diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx deleted file mode 100644 index 4fea7b76e4f1f..0000000000000 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx +++ /dev/null @@ -1,281 +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, - EuiButtonEmpty, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiTabbedContent, - EuiTabbedContentTab, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { FormattedMessage, injectI18n, InjectedIntl } from '@kbn/i18n/react'; -import React, { useCallback, useContext, useMemo } from '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 { LogColumnsConfigurationPanel } from './log_columns_configuration_panel'; -import { isValidTabId, SourceConfigurationFlyoutState } from './source_configuration_flyout_state'; -import { useSourceConfigurationFormState } from './source_configuration_form_state'; - -const noop = () => undefined; - -interface SourceConfigurationFlyoutProps { - intl: InjectedIntl; - shouldAllowEdit: boolean; -} - -export const SourceConfigurationFlyout = injectI18n( - ({ intl, shouldAllowEdit }: SourceConfigurationFlyoutProps) => { - const { activeTabId, hide, isVisible, setActiveTab } = useContext( - SourceConfigurationFlyoutState.Context - ); - - const { - createSourceConfiguration, - source, - sourceExists, - isLoading, - updateSourceConfiguration, - } = useContext(Source.Context); - const availableFields = useMemo( - () => (source && source.status ? source.status.indexFields.map(field => field.name) : []), - [source] - ); - - const { - addLogColumn, - moveLogColumn, - indicesConfigurationProps, - logColumnConfigurationProps, - errors, - resetForm, - isFormDirty, - isFormValid, - formState, - formStateChanges, - } = useSourceConfigurationFormState(source && source.configuration); - - const persistUpdates = useCallback(async () => { - if (sourceExists) { - await updateSourceConfiguration(formStateChanges); - } else { - await createSourceConfiguration(formState); - } - resetForm(); - }, [ - sourceExists, - updateSourceConfiguration, - createSourceConfiguration, - resetForm, - formState, - formStateChanges, - ]); - - const isWriteable = useMemo(() => shouldAllowEdit && source && source.origin !== 'internal', [ - shouldAllowEdit, - source, - ]); - - const tabs: EuiTabbedContentTab[] = useMemo( - () => - isVisible - ? [ - { - id: 'indicesAndFieldsTab', - name: intl.formatMessage({ - id: 'xpack.infra.sourceConfiguration.sourceConfigurationIndicesTabTitle', - defaultMessage: 'Indices and fields', - }), - content: ( - <> - - - - - - - - ), - }, - { - id: 'logsTab', - name: intl.formatMessage({ - id: 'xpack.infra.sourceConfiguration.sourceConfigurationLogColumnsTabTitle', - defaultMessage: 'Log Columns', - }), - content: ( - <> - - - - ), - }, - ] - : [], - [ - addLogColumn, - moveLogColumn, - availableFields, - indicesConfigurationProps, - intl.formatMessage, - isLoading, - isVisible, - logColumnConfigurationProps, - isWriteable, - ] - ); - const activeTab = useMemo(() => tabs.filter(tab => tab.id === activeTabId)[0] || tabs[0], [ - activeTabId, - tabs, - ]); - const activateTab = useCallback( - (tab: EuiTabbedContentTab) => { - const tabId = tab.id; - if (isValidTabId(tabId)) { - setActiveTab(tabId); - } - }, - [setActiveTab, isValidTabId] - ); - - if (!isVisible || !source || !source.configuration) { - return null; - } - - return ( - - - -

- {isWriteable ? ( - - ) : ( - - )} -

-
-
- - - - - {errors.length > 0 ? ( - <> - -
    - {errors.map((error, errorIndex) => ( -
  • {error}
  • - ))} -
-
- - - ) : null} - - - {!isFormDirty ? ( - hide()} - > - - - ) : ( - { - resetForm(); - hide(); - }} - > - - - )} - - - {isWriteable && ( - - {isLoading ? ( - - Loading - - ) : ( - - - - )} - - )} - -
-
- ); - } -); diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_flyout_state.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_flyout_state.tsx deleted file mode 100644 index 6b12a4638d1e6..0000000000000 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_flyout_state.tsx +++ /dev/null @@ -1,51 +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 createContainer from 'constate-latest'; -import { useCallback, useState } from 'react'; - -import { useVisibilityState } from '../../utils/use_visibility_state'; - -type TabId = 'indicesAndFieldsTab' | 'logsTab'; -const validTabIds: TabId[] = ['indicesAndFieldsTab', 'logsTab']; - -export const useSourceConfigurationFlyoutState = ({ - initialVisibility = false, - initialTab = 'indicesAndFieldsTab', -}: { - initialVisibility?: boolean; - initialTab?: TabId; -} = {}) => { - const { isVisible, show, hide, toggle: toggleIsVisible } = useVisibilityState(initialVisibility); - const [activeTabId, setActiveTab] = useState(initialTab); - - const showWithTab = useCallback( - (tabId?: TabId) => { - if (tabId != null) { - setActiveTab(tabId); - } - show(); - }, - [show] - ); - const showIndicesConfiguration = useCallback(() => showWithTab('indicesAndFieldsTab'), [show]); - const showLogsConfiguration = useCallback(() => showWithTab('logsTab'), [show]); - - return { - activeTabId, - hide, - isVisible, - setActiveTab, - show: showWithTab, - showIndicesConfiguration, - showLogsConfiguration, - toggleIsVisible, - }; -}; - -export const isValidTabId = (value: any): value is TabId => validTabIds.includes(value); - -export const SourceConfigurationFlyoutState = createContainer(useSourceConfigurationFlyoutState); diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx new file mode 100644 index 0000000000000..1afdeb887ae1f --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx @@ -0,0 +1,213 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButton, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, injectI18n, InjectedIntl } from '@kbn/i18n/react'; +import React, { useCallback, useContext, useMemo } from 'react'; +import { Prompt } from 'react-router-dom'; + +import { Source } from '../../containers/source'; +import { FieldsConfigurationPanel } from './fields_configuration_panel'; +import { IndicesConfigurationPanel } from './indices_configuration_panel'; +import { NameConfigurationPanel } from './name_configuration_panel'; +import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel'; +import { useSourceConfigurationFormState } from './source_configuration_form_state'; + +interface SourceConfigurationSettingsProps { + intl: InjectedIntl; + shouldAllowEdit: boolean; +} + +export const SourceConfigurationSettings = injectI18n( + ({ shouldAllowEdit }: SourceConfigurationSettingsProps) => { + const { + createSourceConfiguration, + source, + sourceExists, + isLoading, + updateSourceConfiguration, + } = useContext(Source.Context); + + const availableFields = useMemo( + () => (source && source.status ? source.status.indexFields.map(field => field.name) : []), + [source] + ); + + const { + addLogColumn, + moveLogColumn, + indicesConfigurationProps, + logColumnConfigurationProps, + errors, + resetForm, + isFormDirty, + isFormValid, + formState, + formStateChanges, + } = useSourceConfigurationFormState(source && source.configuration); + + const persistUpdates = useCallback(async () => { + if (sourceExists) { + await updateSourceConfiguration(formStateChanges); + } else { + await createSourceConfiguration(formState); + } + resetForm(); + }, [ + sourceExists, + updateSourceConfiguration, + createSourceConfiguration, + resetForm, + formState, + formStateChanges, + ]); + + const isWriteable = useMemo(() => shouldAllowEdit && source && source.origin !== 'internal', [ + shouldAllowEdit, + source, + ]); + + if (!source || !source.configuration) { + return null; + } + + return ( + <> + + + + + + + + + + + + + + + + + + + + + {errors.length > 0 ? ( + <> + +
    + {errors.map((error, errorIndex) => ( +
  • {error}
  • + ))} +
+
+ + + ) : null} + + + {isWriteable && ( + + {isLoading ? ( + + + + Loading + + + + ) : ( + <> + + + { + resetForm(); + }} + > + + + + + + + + + + + )} + + )} + +
+
+
+
+ + ); + } +); diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/view_source_configuration_button.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/view_source_configuration_button.tsx new file mode 100644 index 0000000000000..9b584b2ef3bd0 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/view_source_configuration_button.tsx @@ -0,0 +1,40 @@ +/* + * 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 } from '@elastic/eui'; +import React from 'react'; +import { Route } from 'react-router-dom'; + +export enum ViewSourceConfigurationButtonHrefBase { + infrastructure = 'infrastructure', + logs = 'logs', +} + +interface ViewSourceConfigurationButtonProps { + 'data-test-subj'?: string; + hrefBase: ViewSourceConfigurationButtonHrefBase; + children: React.ReactNode; +} + +export const ViewSourceConfigurationButton = ({ + 'data-test-subj': dataTestSubj, + hrefBase, + children, +}: ViewSourceConfigurationButtonProps) => { + const href = `/${hrefBase}/settings`; + + return ( + ( + history.push(href)}> + {children} + + )} + /> + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx index 3cc40be7e5fe0..6105a241c0164 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx @@ -15,10 +15,11 @@ import { ColumnarPage } from '../../components/page'; import { MetricsExplorerOptionsContainer } from '../../containers/metrics_explorer/use_metrics_explorer_options'; import { WithMetricsExplorerOptionsUrlState } from '../../containers/metrics_explorer/with_metrics_explorer_options_url_state'; import { WithSource } from '../../containers/with_source'; -import { SourceConfigurationFlyoutState } from '../../components/source_configuration'; import { Source } from '../../containers/source'; import { MetricsExplorerPage } from './metrics_explorer'; import { SnapshotPage } from './snapshot'; +import { SettingsPage } from '../shared/settings'; +import { AppNavigation } from '../../components/navigation/app_navigation'; interface InfrastructurePageProps extends RouteComponentProps { intl: InjectedIntl; @@ -26,23 +27,23 @@ interface InfrastructurePageProps extends RouteComponentProps { export const InfrastructurePage = injectI18n(({ match, intl }: InfrastructurePageProps) => ( - - - + + - + + + - - - ( - - {({ configuration, createDerivedIndexPattern }) => ( - - - - - )} - - )} - /> - - - + + + ( + + {({ configuration, createDerivedIndexPattern }) => ( + + + + + )} + + )} + /> + + + )); diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/index.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/index.tsx index 9b4efa44d483a..7e3c2110f1d12 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/index.tsx @@ -17,10 +17,12 @@ import { NoIndices } from '../../../components/empty_states/no_indices'; import { Header } from '../../../components/header'; import { ColumnarPage } from '../../../components/page'; -import { SourceConfigurationFlyout } from '../../../components/source_configuration'; -import { SourceConfigurationFlyoutState } from '../../../components/source_configuration'; import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; +import { + ViewSourceConfigurationButton, + ViewSourceConfigurationButtonHrefBase, +} from '../../../components/source_configuration'; import { Source } from '../../../containers/source'; import { WithWaffleFilterUrlState } from '../../../containers/waffle/with_waffle_filters'; import { WithWaffleOptionsUrlState } from '../../../containers/waffle/with_waffle_options'; @@ -36,7 +38,6 @@ interface SnapshotPageProps { export const SnapshotPage = injectUICapabilities( injectI18n((props: SnapshotPageProps) => { const { intl, uiCapabilities } = props; - const { showIndicesConfiguration } = useContext(SourceConfigurationFlyoutState.Context); const { createDerivedIndexPattern, hasFailedLoadingSource, @@ -76,9 +77,6 @@ export const SnapshotPage = injectUICapabilities( ]} readOnlyBadge={!uiCapabilities.infrastructure.save} /> - {isLoading ? ( ) : metricIndicesExist ? ( @@ -120,16 +118,15 @@ export const SnapshotPage = injectUICapabilities( {uiCapabilities.infrastructure.configureSource ? ( - {intl.formatMessage({ id: 'xpack.infra.configureSourceActionLabel', defaultMessage: 'Change source configuration', })} - + ) : null} diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx index 0d3fda093b223..5387d4901d24c 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { AutocompleteField } from '../../../components/autocomplete_field'; import { Toolbar } from '../../../components/eui/toolbar'; -import { SourceConfigurationButton } from '../../../components/source_configuration'; import { WaffleGroupByControls } from '../../../components/waffle/waffle_group_by_controls'; import { WaffleMetricControls } from '../../../components/waffle/waffle_metric_controls'; import { WaffleNodeTypeSwitcher } from '../../../components/waffle/waffle_node_type_switcher'; @@ -111,9 +110,6 @@ export const SnapshotToolbar = injectI18n(({ intl }) => ( customOptions={customOptions} /> - - - )} diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx new file mode 100644 index 0000000000000..949ad4b222ced --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { 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 { Source } from '../../containers/source'; +import { StreamPage } from './stream'; +import { SettingsPage } from '../shared/settings'; +import { AppNavigation } from '../../components/navigation/app_navigation'; + +interface LogsPageProps extends RouteComponentProps { + intl: InjectedIntl; +} + +export const LogsPage = injectI18n(({ match, intl }: LogsPageProps) => ( + + + + + + + + + + + + + + + + +)); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/page_header.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/page_header.tsx deleted file mode 100644 index d42a13b0e4de1..0000000000000 --- a/x-pack/legacy/plugins/infra/public/pages/logs/page_header.tsx +++ /dev/null @@ -1,57 +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 { injectI18n, InjectedIntl } from '@kbn/i18n/react'; -import React from 'react'; - -import { UICapabilities } from 'ui/capabilities'; -import { injectUICapabilities } from 'ui/capabilities/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'; - -interface LogsPageHeaderProps { - intl: InjectedIntl; - uiCapabilities: UICapabilities; -} - -export const LogsPageHeader = injectUICapabilities( - injectI18n((props: LogsPageHeaderProps) => { - const { intl, uiCapabilities } = props; - return ( - <> -
- - - - - ); - }) -); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/page_providers.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/page_providers.tsx deleted file mode 100644 index 82aea7b380034..0000000000000 --- a/x-pack/legacy/plugins/infra/public/pages/logs/page_providers.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { SourceConfigurationFlyoutState } from '../../components/source_configuration'; -import { LogFlyout } from '../../containers/logs/log_flyout'; -import { LogViewConfiguration } from '../../containers/logs/log_view_configuration'; -import { LogHighlightsState } from '../../containers/logs/log_highlights/log_highlights'; -import { Source, useSource } from '../../containers/source'; -import { useSourceId } from '../../containers/source_id'; - -export const LogsPageProviders: React.FunctionComponent = ({ children }) => { - const [sourceId] = useSourceId(); - const source = useSource({ sourceId }); - - return ( - - - - - - {children} - - - - - - ); -}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/index.ts b/x-pack/legacy/plugins/infra/public/pages/logs/stream/index.ts similarity index 86% rename from x-pack/legacy/plugins/infra/public/pages/logs/index.ts rename to x-pack/legacy/plugins/infra/public/pages/logs/stream/index.ts index 1d1c8cc65287f..928c6187ec078 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/index.ts +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { LogsPage } from './page'; +export { StreamPage } from './page'; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/page.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page.tsx similarity index 65% rename from x-pack/legacy/plugins/infra/public/pages/logs/page.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/stream/page.tsx index 32c86641d6755..9031a8cb4785d 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/page.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page.tsx @@ -6,20 +6,20 @@ import React from 'react'; -import { ColumnarPage } from '../../components/page'; -import { LogsPageContent } from './page_content'; -import { LogsPageHeader } from './page_header'; +import { ColumnarPage } from '../../../components/page'; +import { StreamPageContent } from './page_content'; +import { StreamPageHeader } from './page_header'; import { LogsPageProviders } from './page_providers'; -import { useTrackPageview } from '../../hooks/use_track_metric'; +import { useTrackPageview } from '../../../hooks/use_track_metric'; -export const LogsPage = () => { +export const StreamPage = () => { useTrackPageview({ app: 'infra_logs', path: 'stream' }); useTrackPageview({ app: 'infra_logs', path: 'stream', delay: 15000 }); return ( - - + + ); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_content.tsx similarity index 77% rename from x-pack/legacy/plugins/infra/public/pages/logs/page_content.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/stream/page_content.tsx index a8dde0532283a..abcc881dd250e 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_content.tsx @@ -6,13 +6,13 @@ import React, { useContext } from 'react'; -import { SourceErrorPage } from '../../components/source_error_page'; -import { SourceLoadingPage } from '../../components/source_loading_page'; -import { Source } from '../../containers/source'; +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'; -export const LogsPageContent: React.FunctionComponent = () => { +export const StreamPageContent: React.FunctionComponent = () => { const { hasFailedLoadingSource, isLoadingSource, diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_header.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_header.tsx new file mode 100644 index 0000000000000..b46189c462c6b --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_header.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; +import { DocumentTitle } from '../../../components/document_title'; +import { Header } from '../../../components/header'; + +interface StreamPageHeaderProps { + uiCapabilities: UICapabilities; +} + +export const StreamPageHeader = injectUICapabilities((props: StreamPageHeaderProps) => { + const { uiCapabilities } = props; + return ( + <> +
+ + i18n.translate('xpack.infra.logs.streamPage.documentTitle', { + defaultMessage: '{previousTitle} | Stream', + values: { + previousTitle, + }, + }) + } + /> + + ); +}); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/page_logs_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx similarity index 77% rename from x-pack/legacy/plugins/infra/public/pages/logs/page_logs_content.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx index 007584d4cacc8..2ead89f94f812 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/page_logs_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_logs_content.tsx @@ -6,30 +6,29 @@ import React, { useContext } from 'react'; -import euiStyled from '../../../../../common/eui_styled_components'; -import { AutoSizer } from '../../components/auto_sizer'; -import { LogEntryFlyout } from '../../components/logging/log_entry_flyout'; -import { LogMinimap } from '../../components/logging/log_minimap'; -import { ScrollableLogTextStreamView } from '../../components/logging/log_text_stream'; -import { PageContent } from '../../components/page'; +import euiStyled from '../../../../../../common/eui_styled_components'; +import { AutoSizer } from '../../../components/auto_sizer'; +import { LogEntryFlyout } from '../../../components/logging/log_entry_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 { WithSummary } from '../../../containers/logs/log_summary'; +import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration'; +import { WithLogFilter, WithLogFilterUrlState } from '../../../containers/logs/with_log_filter'; import { LogFlyout as LogFlyoutState, WithFlyoutOptionsUrlState, -} from '../../containers/logs/log_flyout'; -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 { ReduxSourceIdBridge, WithStreamItems } from '../../containers/logs/with_stream_items'; -import { Source } from '../../containers/source'; +} from '../../../containers/logs/log_flyout'; +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 { ReduxSourceIdBridge, WithStreamItems } from '../../../containers/logs/with_stream_items'; +import { Source } from '../../../containers/source'; import { LogsToolbar } from './page_toolbar'; -import { SourceConfigurationFlyoutState } from '../../components/source_configuration'; -import { LogHighlightsBridge } from '../../containers/logs/log_highlights'; +import { LogHighlightsBridge } from '../../../containers/logs/log_highlights'; export const LogsPageLogsContent: React.FunctionComponent = () => { const { createDerivedIndexPattern, source, sourceId, version } = useContext(Source.Context); @@ -43,7 +42,6 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { flyoutItem, isLoading, } = useContext(LogFlyoutState.Context); - const { showLogsConfiguration } = useContext(SourceConfigurationFlyoutState.Context); const derivedIndexPattern = createDerivedIndexPattern('logs'); @@ -105,7 +103,6 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { loadNewerItems={loadNewerEntries} reportVisibleInterval={reportVisiblePositions} scale={textScale} - showColumnConfiguration={showLogsConfiguration} target={targetPosition} wrap={textWrap} setFlyoutItem={setFlyoutId} diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/page_no_indices_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx similarity index 82% rename from x-pack/legacy/plugins/infra/public/pages/logs/page_no_indices_content.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx index a7392fafbeeae..70de44c37ef54 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/page_no_indices_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx @@ -6,13 +6,16 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { injectI18n, InjectedIntl } from '@kbn/i18n/react'; -import React, { useContext } from 'react'; +import React from 'react'; import { UICapabilities } from 'ui/capabilities'; import { injectUICapabilities } from 'ui/capabilities/react'; -import { NoIndices } from '../../components/empty_states/no_indices'; -import { SourceConfigurationFlyoutState } from '../../components/source_configuration'; -import { WithKibanaChrome } from '../../containers/with_kibana_chrome'; +import { NoIndices } from '../../../components/empty_states/no_indices'; +import { WithKibanaChrome } from '../../../containers/with_kibana_chrome'; +import { + ViewSourceConfigurationButton, + ViewSourceConfigurationButtonHrefBase, +} from '../../../components/source_configuration'; interface LogsPageNoIndicesContentProps { intl: InjectedIntl; @@ -22,7 +25,6 @@ interface LogsPageNoIndicesContentProps { export const LogsPageNoIndicesContent = injectUICapabilities( injectI18n((props: LogsPageNoIndicesContentProps) => { const { intl, uiCapabilities } = props; - const { showIndicesConfiguration } = useContext(SourceConfigurationFlyoutState.Context); return ( @@ -54,16 +56,15 @@ export const LogsPageNoIndicesContent = injectUICapabilities( {uiCapabilities.logs.configureSource ? ( - {intl.formatMessage({ id: 'xpack.infra.configureSourceActionLabel', defaultMessage: 'Change source configuration', })} - + ) : null} diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_providers.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_providers.tsx new file mode 100644 index 0000000000000..b93cf48bde5e7 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_providers.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { LogFlyout } from '../../../containers/logs/log_flyout'; +import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration'; +import { LogHighlightsState } from '../../../containers/logs/log_highlights/log_highlights'; +import { Source, useSource } from '../../../containers/source'; +import { useSourceId } from '../../../containers/source_id'; + +export const LogsPageProviders: React.FunctionComponent = ({ children }) => { + const [sourceId] = useSourceId(); + const source = useSource({ sourceId }); + + return ( + + + + + {children} + + + + + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/page_toolbar.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_toolbar.tsx similarity index 78% rename from x-pack/legacy/plugins/infra/public/pages/logs/page_toolbar.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/stream/page_toolbar.tsx index 8a132fd9e0c71..46cf3aab40e9f 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/page_toolbar.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_toolbar.tsx @@ -8,22 +8,21 @@ 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 { LogHighlightsMenu } from '../../components/logging/log_highlights_menu'; -import { LogHighlightsState } from '../../containers/logs/log_highlights/log_highlights'; -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 { LogFlyout } from '../../containers/logs/log_flyout'; -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'; +import { AutocompleteField } from '../../../components/autocomplete_field'; +import { Toolbar } from '../../../components/eui'; +import { LogCustomizationMenu } from '../../../components/logging/log_customization_menu'; +import { LogHighlightsMenu } from '../../../components/logging/log_highlights_menu'; +import { LogHighlightsState } from '../../../containers/logs/log_highlights/log_highlights'; +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 { LogFlyout } from '../../../containers/logs/log_flyout'; +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 { createDerivedIndexPattern } = useContext(Source.Context); @@ -91,9 +90,6 @@ export const LogsToolbar = injectI18n(({ intl }) => { )} - - - - (Component: React.Compo props: T ) => ( - - - - - + + + ); diff --git a/x-pack/legacy/plugins/infra/public/pages/shared/settings/index.tsx b/x-pack/legacy/plugins/infra/public/pages/shared/settings/index.tsx new file mode 100644 index 0000000000000..daea6cfabdc2a --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/shared/settings/index.tsx @@ -0,0 +1,18 @@ +/* + * 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 { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; +import { SourceConfigurationSettings } from '../../../components/source_configuration/source_configuration_settings'; + +interface SettingsPageProps { + uiCapabilities: UICapabilities; +} + +export const SettingsPage = injectUICapabilities(({ uiCapabilities }: SettingsPageProps) => ( + +)); diff --git a/x-pack/legacy/plugins/infra/public/routes.tsx b/x-pack/legacy/plugins/infra/public/routes.tsx index e9343c963549f..dcdb40cb5ed7e 100644 --- a/x-pack/legacy/plugins/infra/public/routes.tsx +++ b/x-pack/legacy/plugins/infra/public/routes.tsx @@ -37,6 +37,7 @@ const PageRouterComponent: React.SFC = ({ history, uiCapabilities } {uiCapabilities.infrastructure.show && ( )} + {uiCapabilities.logs.show && } {uiCapabilities.logs.show && } {uiCapabilities.infrastructure.show && ( diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4b7924a07b291..0092cf098da0a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -481,11 +481,6 @@ "common.ui.indexPattern.unableWriteLabel": "インデックスパターンを書き込めません!このインデックスパターンへの最新の変更を取得するにな、ページを更新してください。", "common.ui.indexPattern.unknownFieldErrorMessage": "インデックスパターン「{title}」のフィールド「{name}」が不明なフィールドタイプを使用しています。", "common.ui.indexPattern.unknownFieldHeader": "不明なフィールドタイプ {type}", - "inspector.closeButton": "インスペクターを閉じる", - "inspector.reqTimestampDescription": "リクエストの開始が記録された時刻です", - "inspector.reqTimestampKey": "リクエストのタイムスタンプ", - "inspector.title": "インスペクター", - "inspector.view": "{viewName} を表示", "common.ui.legacyBrowserMessage": "この Kibana インストレーションは、現在ご使用のブラウザが満たしていない厳格なセキュリティ要件が有効になっています。", "common.ui.legacyBrowserTitle": "ブラウザをアップグレードしてください", "common.ui.management.breadcrumb": "管理", @@ -609,6 +604,11 @@ "common.ui.visualize.queryGeohashBounds.unableToGetBoundErrorTitle": "バウンドを取得できませんでした", "common.ui.welcomeErrorMessage": "Kibana が正常に読み込まれませんでした。詳細はサーバーアウトプットを確認してください。", "common.ui.welcomeMessage": "Kibana を読み込み中", + "inspector.closeButton": "インスペクターを閉じる", + "inspector.reqTimestampDescription": "リクエストの開始が記録された時刻です", + "inspector.reqTimestampKey": "リクエストのタイムスタンプ", + "inspector.title": "インスペクター", + "inspector.view": "{viewName} を表示", "core.chrome.legacyBrowserWarning": "ご使用のブラウザが Kibana のセキュリティ要件を満たしていません。", "core.euiBasicTable.selectAllRows": "すべての行を選択", "core.euiBasicTable.selectThisRow": "この行を選択", @@ -5055,7 +5055,6 @@ "xpack.infra.linkLogsTitle": "ログ", "xpack.infra.linkTo.hostWithIp.error": "IP アドレス「{hostIp}」でホストが見つかりません.", "xpack.infra.linkTo.hostWithIp.loading": "IP アドレス「{hostIp}」のホストを読み込み中", - "xpack.infra.logColumnHeaders.configureColumnsLabel": "列を構成", "xpack.infra.logEntryActionsMenu.buttonLabel": "アクション", "xpack.infra.logEntryActionsMenu.uptimeActionLabel": "監視ステータスを表示", "xpack.infra.logEntryItemView.viewDetailsToolTip": "詳細を表示", @@ -5088,8 +5087,6 @@ "xpack.infra.logs.stopStreamingButtonLabel": "ストリーム停止", "xpack.infra.logs.streamingDescription": "新しいエントリーをストリーム中...", "xpack.infra.logs.streamingNewEntriesText": "新しいエントリーをストリーム中", - "xpack.infra.logsPage.documentTitle": "ログ", - "xpack.infra.logsPage.logsBreadcrumbsText": "ログ", "xpack.infra.logsPage.logsHelpContent.feedbackLinkText": "ログのフィードバックを提供", "xpack.infra.logsPage.noLoggingIndicesDescription": "追加しましょう!", "xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel": "セットアップの手順を表示", @@ -5211,10 +5208,7 @@ "xpack.infra.registerFeatures.logsDescription": "ログをリアルタイムでストリーするか、コンソール式の UI で履歴ビューをスクロールします。", "xpack.infra.registerFeatures.logsTitle": "ログ", "xpack.infra.sourceConfiguration.addLogColumnButtonLabel": "列を追加", - "xpack.infra.sourceConfiguration.closeButtonLabel": "閉じる", - "xpack.infra.sourceConfiguration.containerFieldDescription": "Docker コンテナーの識別に使用されるフィールドです。推奨値は {defaultValue} です。", "xpack.infra.sourceConfiguration.containerFieldLabel": "コンテナー ID", - "xpack.infra.sourceConfiguration.discardAndCloseButtonLabel": "破棄して閉じる", "xpack.infra.sourceConfiguration.fieldEmptyErrorMessage": "このフィールドは未入力のままにできません。", "xpack.infra.sourceConfiguration.fieldLogColumnTitle": "フィールド", "xpack.infra.sourceConfiguration.fieldsSectionTitle": "フィールド", @@ -5223,29 +5217,18 @@ "xpack.infra.sourceConfiguration.indicesSectionTitle": "インデックス", "xpack.infra.sourceConfiguration.logColumnListEmptyErrorMessage": "ログ列リストは未入力のままにできません。", "xpack.infra.sourceConfiguration.logColumnsSectionTitle": "列", - "xpack.infra.sourceConfiguration.logIndicesDescription": "ログデータを含む一致するインデックスのインデックスパターンです。推奨値は {defaultValue} です。", "xpack.infra.sourceConfiguration.logIndicesLabel": "ログインデックス", "xpack.infra.sourceConfiguration.messageLogColumnDescription": "このシステムフィールドは、ドキュメントフィールドから取得されたログエントリーメッセージを表示します。", - "xpack.infra.sourceConfiguration.metricIndicesDescription": "Metricbeat データを含む一致するインデックスのインデックスパターンです。推奨値は {defaultValue} です。", "xpack.infra.sourceConfiguration.metricIndicesLabel": "メトリックインデックス", "xpack.infra.sourceConfiguration.nameLabel": "名前", "xpack.infra.sourceConfiguration.nameSectionTitle": "名前", "xpack.infra.sourceConfiguration.noLogColumnsDescription": "上のボタンでこのリストに列を追加します。", "xpack.infra.sourceConfiguration.noLogColumnsTitle": "列がありません", - "xpack.infra.sourceConfiguration.podFieldDescription": "Kubernetes ポッドの識別に使用されるフィールドです。推奨値は {defaultValue} です。", "xpack.infra.sourceConfiguration.podFieldLabel": "ポッド ID", - "xpack.infra.sourceConfiguration.sourceConfigurationButtonLabel": "構成", - "xpack.infra.sourceConfiguration.sourceConfigurationIndicesTabTitle": "インデックスとフィールド", - "xpack.infra.sourceConfiguration.sourceConfigurationLogColumnsTabTitle": "ログ列", - "xpack.infra.sourceConfiguration.sourceConfigurationReadonlyTitle": "ソース構成を表示", - "xpack.infra.sourceConfiguration.sourceConfigurationTitle": "ソースの構成", "xpack.infra.sourceConfiguration.systemColumnBadgeLabel": "システム", - "xpack.infra.sourceConfiguration.tiebreakerFieldDescription": "同じタイムスタンプの 2 つのエントリーを識別するのに使用されるフィールドです。推奨値は {defaultValue} です。", "xpack.infra.sourceConfiguration.tiebreakerFieldLabel": "タイブレーカー", - "xpack.infra.sourceConfiguration.timestampFieldDescription": "ログエントリーの並べ替えに使用されるタイムスタンプです。推奨値は {defaultValue} です。", "xpack.infra.sourceConfiguration.timestampFieldLabel": "タイムスタンプ", "xpack.infra.sourceConfiguration.timestampLogColumnDescription": "このシステムフィールドは、{timestampSetting} フィールド設定から判断されたログエントリーの時刻を表示します。", - "xpack.infra.sourceConfiguration.updateSourceConfigurationButtonLabel": "ソースを更新", "xpack.infra.sourceErrorPage.failedToLoadDataSourcesMessage": "データソースの読み込みに失敗しました。", "xpack.infra.sourceLoadingPage.loadingDataSourcesMessage": "データソースを読み込み中", "xpack.infra.tableView.columnName.avg": "平均", @@ -10611,4 +10594,4 @@ "xpack.watcher.watchActions.logging.logTextIsRequiredValidationMessage": "ログテキストが必要です。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b01660b901010..a68c753659ae1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -481,11 +481,6 @@ "common.ui.indexPattern.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", "common.ui.indexPattern.unknownFieldErrorMessage": "indexPattern “{title}” 中的字段 “{name}” 使用未知字段类型。", "common.ui.indexPattern.unknownFieldHeader": "未知字段类型 {type}", - "inspector.closeButton": "关闭检查器", - "inspector.reqTimestampDescription": "记录请求启动的时间", - "inspector.reqTimestampKey": "请求时间戳", - "inspector.title": "检查器", - "inspector.view": "视图:{viewName}", "common.ui.legacyBrowserMessage": "此 Kibana 安装启用了当前浏览器未满足的严格安全要求。", "common.ui.legacyBrowserTitle": "请升级您的浏览器", "common.ui.management.breadcrumb": "管理", @@ -609,6 +604,11 @@ "common.ui.visualize.queryGeohashBounds.unableToGetBoundErrorTitle": "无法获取边界", "common.ui.welcomeErrorMessage": "Kibana 未正确加载。检查服务器输出以了解详情。", "common.ui.welcomeMessage": "正在加载 Kibana", + "inspector.closeButton": "关闭检查器", + "inspector.reqTimestampDescription": "记录请求启动的时间", + "inspector.reqTimestampKey": "请求时间戳", + "inspector.title": "检查器", + "inspector.view": "视图:{viewName}", "core.chrome.legacyBrowserWarning": "您的浏览器不满足 Kibana 的安全要求。", "core.euiBasicTable.selectAllRows": "选择所有行", "core.euiBasicTable.selectThisRow": "选择此行", @@ -5197,7 +5197,6 @@ "xpack.infra.linkLogsTitle": "Logs", "xpack.infra.linkTo.hostWithIp.error": "未找到 IP 地址为“{hostIp}”的主机。", "xpack.infra.linkTo.hostWithIp.loading": "正在加载 IP 地址为“{hostIp}”的主机。", - "xpack.infra.logColumnHeaders.configureColumnsLabel": "配置列", "xpack.infra.logEntryActionsMenu.buttonLabel": "操作", "xpack.infra.logEntryActionsMenu.uptimeActionLabel": "查看监测状态", "xpack.infra.logEntryItemView.viewDetailsToolTip": "查看详情", @@ -5230,8 +5229,6 @@ "xpack.infra.logs.stopStreamingButtonLabel": "停止流式传输", "xpack.infra.logs.streamingDescription": "正在流式传输新条目……", "xpack.infra.logs.streamingNewEntriesText": "正在流式传输新条目", - "xpack.infra.logsPage.documentTitle": "Logs", - "xpack.infra.logsPage.logsBreadcrumbsText": "Logs", "xpack.infra.logsPage.logsHelpContent.feedbackLinkText": "提供 Logs 的反馈", "xpack.infra.logsPage.noLoggingIndicesDescription": "让我们添加一些!", "xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel": "查看设置说明", @@ -5353,10 +5350,7 @@ "xpack.infra.registerFeatures.logsDescription": "实时流式传输日志或在类似控制台的工具中滚动浏览历史视图。", "xpack.infra.registerFeatures.logsTitle": "Logs", "xpack.infra.sourceConfiguration.addLogColumnButtonLabel": "添加列", - "xpack.infra.sourceConfiguration.closeButtonLabel": "关闭", - "xpack.infra.sourceConfiguration.containerFieldDescription": "用于标识 Docker 容器的字段。推荐值为 {defaultValue}。", "xpack.infra.sourceConfiguration.containerFieldLabel": "容器 ID", - "xpack.infra.sourceConfiguration.discardAndCloseButtonLabel": "丢弃并关闭", "xpack.infra.sourceConfiguration.fieldEmptyErrorMessage": "字段不得为空。", "xpack.infra.sourceConfiguration.fieldLogColumnTitle": "字段", "xpack.infra.sourceConfiguration.fieldsSectionTitle": "字段", @@ -5365,29 +5359,18 @@ "xpack.infra.sourceConfiguration.indicesSectionTitle": "索引", "xpack.infra.sourceConfiguration.logColumnListEmptyErrorMessage": "日志列列表不得为空。", "xpack.infra.sourceConfiguration.logColumnsSectionTitle": "列", - "xpack.infra.sourceConfiguration.logIndicesDescription": "用于匹配包含日志数据的索引的索引模式。推荐值为 {defaultValue}。", "xpack.infra.sourceConfiguration.logIndicesLabel": "日志索引", "xpack.infra.sourceConfiguration.messageLogColumnDescription": "此系统字段显示派生自文档字段的日志条目消息。", - "xpack.infra.sourceConfiguration.metricIndicesDescription": "用于匹配包含 Metricbeat 数据的索引的索引模式。推荐值为 {defaultValue}。", "xpack.infra.sourceConfiguration.metricIndicesLabel": "指标索引", "xpack.infra.sourceConfiguration.nameLabel": "名称", "xpack.infra.sourceConfiguration.nameSectionTitle": "名称", "xpack.infra.sourceConfiguration.noLogColumnsDescription": "使用上面的按钮将列添加到此列表。", "xpack.infra.sourceConfiguration.noLogColumnsTitle": "无列", - "xpack.infra.sourceConfiguration.podFieldDescription": "用于标识 Kubernetes Pod 的字段。推荐值为 {defaultValue}。", "xpack.infra.sourceConfiguration.podFieldLabel": "Pod ID", - "xpack.infra.sourceConfiguration.sourceConfigurationButtonLabel": "配置", - "xpack.infra.sourceConfiguration.sourceConfigurationIndicesTabTitle": "索引和字段", - "xpack.infra.sourceConfiguration.sourceConfigurationLogColumnsTabTitle": "日志列", - "xpack.infra.sourceConfiguration.sourceConfigurationReadonlyTitle": "查看源配置", - "xpack.infra.sourceConfiguration.sourceConfigurationTitle": "配置源", "xpack.infra.sourceConfiguration.systemColumnBadgeLabel": "系统", - "xpack.infra.sourceConfiguration.tiebreakerFieldDescription": "用于时间戳相同的两个条目间决胜的字段。推荐值为 {defaultValue}。", "xpack.infra.sourceConfiguration.tiebreakerFieldLabel": "决胜属性", - "xpack.infra.sourceConfiguration.timestampFieldDescription": "用于排序日志条目的时间戳。推荐值为 {defaultValue}。", "xpack.infra.sourceConfiguration.timestampFieldLabel": "时间戳", "xpack.infra.sourceConfiguration.timestampLogColumnDescription": "此系统字段显示 {timestampSetting} 字段设置所确定的日志条目时间。", - "xpack.infra.sourceConfiguration.updateSourceConfigurationButtonLabel": "更新源", "xpack.infra.sourceErrorPage.failedToLoadDataSourcesMessage": "无法加载数据源。", "xpack.infra.sourceLoadingPage.loadingDataSourcesMessage": "正在加载数据源", "xpack.infra.tableView.columnName.avg": "平均值", @@ -10752,4 +10735,4 @@ "xpack.watcher.watchActions.logging.logTextIsRequiredValidationMessage": "“日志文本”必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts index 27a950ef827ee..3447e03a680e1 100644 --- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -11,10 +11,10 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const infraLogStream = getService('infraLogStream'); - const infraSourceConfigurationFlyout = getService('infraSourceConfigurationFlyout'); - const pageObjects = getPageObjects(['infraLogs']); + const infraSourceConfigurationForm = getService('infraSourceConfigurationForm'); + const pageObjects = getPageObjects(['common', 'infraLogs']); - describe('Logs Page', function() { + describe('Logs Source Configuration', function() { this.tags('smoke'); before(async () => { await esArchiver.load('empty_kibana'); @@ -23,7 +23,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await esArchiver.unload('empty_kibana'); }); - describe('with logs present', () => { + describe('Allows indices configuration', () => { before(async () => { await esArchiver.load('infra/metrics_and_logs'); }); @@ -31,54 +31,44 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await esArchiver.unload('infra/metrics_and_logs'); }); - it('renders the log stream', async () => { - await pageObjects.infraLogs.navigateTo(); - await pageObjects.infraLogs.getLogStream(); - }); - it('can change the log indices to a pattern that matches nothing', async () => { - await pageObjects.infraLogs.openSourceConfigurationFlyout(); - await infraSourceConfigurationFlyout.switchToIndicesAndFieldsTab(); + await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/settings'); + await infraSourceConfigurationForm.getForm(); - const nameInput = await infraSourceConfigurationFlyout.getNameInput(); + const nameInput = await infraSourceConfigurationForm.getNameInput(); await nameInput.clearValueWithKeyboard({ charByChar: true }); await nameInput.type('Modified Source'); - const logIndicesInput = await infraSourceConfigurationFlyout.getLogIndicesInput(); + const logIndicesInput = await infraSourceConfigurationForm.getLogIndicesInput(); await logIndicesInput.clearValueWithKeyboard({ charByChar: true }); await logIndicesInput.type('does-not-exist-*'); - await infraSourceConfigurationFlyout.saveConfiguration(); - await infraSourceConfigurationFlyout.closeFlyout(); + await infraSourceConfigurationForm.saveConfiguration(); }); it('renders the no indices screen when no indices match the pattern', async () => { + await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream'); await pageObjects.infraLogs.getNoLogsIndicesPrompt(); }); it('can change the log indices back to a pattern that matches something', async () => { - await pageObjects.infraLogs.openSourceConfigurationFlyout(); - await infraSourceConfigurationFlyout.switchToIndicesAndFieldsTab(); + await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/settings'); + await infraSourceConfigurationForm.getForm(); - const logIndicesInput = await infraSourceConfigurationFlyout.getLogIndicesInput(); + const logIndicesInput = await infraSourceConfigurationForm.getLogIndicesInput(); await logIndicesInput.clearValueWithKeyboard({ charByChar: true }); await logIndicesInput.type('filebeat-*'); - await infraSourceConfigurationFlyout.saveConfiguration(); - await infraSourceConfigurationFlyout.closeFlyout(); - }); - - it('renders the log stream again', async () => { - await pageObjects.infraLogs.getLogStream(); + await infraSourceConfigurationForm.saveConfiguration(); }); it('renders the default log columns with their headers', async () => { + await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream'); const columnHeaderLabels = await infraLogStream.getColumnHeaderLabels(); - expect(columnHeaderLabels).to.eql(['Timestamp', 'event.dataset', 'Message', '']); + expect(columnHeaderLabels).to.eql(['Timestamp', 'event.dataset', 'Message']); const logStreamEntries = await infraLogStream.getStreamEntries(); - expect(logStreamEntries.length).to.be.greaterThan(0); const firstLogStreamEntry = logStreamEntries[0]; @@ -90,26 +80,25 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('can change the log columns', async () => { - await pageObjects.infraLogs.openSourceConfigurationFlyout(); - await infraSourceConfigurationFlyout.switchToLogsTab(); + await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/settings'); + await infraSourceConfigurationForm.getForm(); - await infraSourceConfigurationFlyout.removeAllLogColumns(); - await infraSourceConfigurationFlyout.addTimestampLogColumn(); - await infraSourceConfigurationFlyout.addFieldLogColumn('host.name'); + await infraSourceConfigurationForm.removeAllLogColumns(); + await infraSourceConfigurationForm.addTimestampLogColumn(); + await infraSourceConfigurationForm.addFieldLogColumn('host.name'); - // TODO: make test more robust - // await infraSourceConfigurationFlyout.moveLogColumn(0, 1); + // await infraSourceConfigurationForm.moveLogColumn(0, 1); - await infraSourceConfigurationFlyout.saveConfiguration(); - await infraSourceConfigurationFlyout.closeFlyout(); + await infraSourceConfigurationForm.saveConfiguration(); }); it('renders the changed log columns with their headers', async () => { + await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream'); const columnHeaderLabels = await infraLogStream.getColumnHeaderLabels(); // TODO: make test more robust - // expect(columnHeaderLabels).to.eql(['host.name', 'Timestamp', '']); - expect(columnHeaderLabels).to.eql(['Timestamp', 'host.name', '']); + // expect(columnHeaderLabels).to.eql(['host.name', 'Timestamp']); + expect(columnHeaderLabels).to.eql(['Timestamp', 'host.name']); const logStreamEntries = await infraLogStream.getStreamEntries(); diff --git a/x-pack/test/functional/apps/infra/metrics_source_configuration.ts b/x-pack/test/functional/apps/infra/metrics_source_configuration.ts index 9b22c7ceffb33..8f5c765cec6ad 100644 --- a/x-pack/test/functional/apps/infra/metrics_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/metrics_source_configuration.ts @@ -11,10 +11,10 @@ const DATE_WITH_DATA = DATES.metricsAndLogs.hosts.withData; export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); - const infraSourceConfigurationFlyout = getService('infraSourceConfigurationFlyout'); + const infraSourceConfigurationForm = getService('infraSourceConfigurationForm'); const pageObjects = getPageObjects(['common', 'infraHome']); - describe('Infrastructure Snapshot Page', function() { + describe('Infrastructure Source Configuration', function() { this.tags('smoke'); before(async () => { await esArchiver.load('empty_kibana'); @@ -38,38 +38,37 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('can change the metric indices to a pattern that matches nothing', async () => { - await pageObjects.infraHome.openSourceConfigurationFlyout(); - await infraSourceConfigurationFlyout.switchToIndicesAndFieldsTab(); + await pageObjects.common.navigateToActualUrl('infraOps', 'infrastructure/settings'); - const nameInput = await infraSourceConfigurationFlyout.getNameInput(); + const nameInput = await infraSourceConfigurationForm.getNameInput(); await nameInput.clearValueWithKeyboard({ charByChar: true }); await nameInput.type('Modified Source'); - const metricIndicesInput = await infraSourceConfigurationFlyout.getMetricIndicesInput(); + const metricIndicesInput = await infraSourceConfigurationForm.getMetricIndicesInput(); await metricIndicesInput.clearValueWithKeyboard({ charByChar: true }); await metricIndicesInput.type('does-not-exist-*'); - await infraSourceConfigurationFlyout.saveConfiguration(); - await infraSourceConfigurationFlyout.closeFlyout(); + await infraSourceConfigurationForm.saveConfiguration(); }); it('renders the no indices screen when no indices match the pattern', async () => { + await pageObjects.common.navigateToApp('infraOps'); await pageObjects.infraHome.getNoMetricsIndicesPrompt(); }); - it('can change the log indices back to a pattern that matches something', async () => { - await pageObjects.infraHome.openSourceConfigurationFlyout(); - await infraSourceConfigurationFlyout.switchToIndicesAndFieldsTab(); + it('can change the metric indices back to a pattern that matches something', async () => { + await pageObjects.common.navigateToActualUrl('infraOps', 'infrastructure/settings'); - const metricIndicesInput = await infraSourceConfigurationFlyout.getMetricIndicesInput(); + const metricIndicesInput = await infraSourceConfigurationForm.getMetricIndicesInput(); await metricIndicesInput.clearValueWithKeyboard({ charByChar: true }); await metricIndicesInput.type('metricbeat-*'); - await infraSourceConfigurationFlyout.saveConfiguration(); - await infraSourceConfigurationFlyout.closeFlyout(); + await infraSourceConfigurationForm.saveConfiguration(); }); - it('renders the log stream again', async () => { + it('renders the waffle map again', async () => { + await pageObjects.common.navigateToApp('infraOps'); + await pageObjects.infraHome.goToTime(DATE_WITH_DATA); await pageObjects.infraHome.getWaffleMap(); }); }); 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 479468d1f82c6..88501aad57b4a 100644 --- a/x-pack/test/functional/page_objects/infra_home_page.ts +++ b/x-pack/test/functional/page_objects/infra_home_page.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function InfraHomePageProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const retry = getService('retry'); const find = getService('find'); const browser = getService('browser'); @@ -23,6 +24,12 @@ export function InfraHomePageProvider({ getService }: FtrProviderContext) { }, async getWaffleMap() { + await retry.try(async () => { + const element = await testSubjects.find('waffleMap'); + if (!element) { + throw new Error(); + } + }); return await testSubjects.find('waffleMap'); }, diff --git a/x-pack/test/functional/page_objects/infra_logs_page.ts b/x-pack/test/functional/page_objects/infra_logs_page.ts index f38740dbb6d13..6eb1349210bae 100644 --- a/x-pack/test/functional/page_objects/infra_logs_page.ts +++ b/x-pack/test/functional/page_objects/infra_logs_page.ts @@ -25,10 +25,5 @@ export function InfraLogsPageProvider({ getPageObjects, getService }: FtrProvide 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.ts b/x-pack/test/functional/services/index.ts index 87e27a5955571..1f509315d7fdc 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -44,7 +44,7 @@ import { GrokDebuggerProvider } from './grok_debugger'; // @ts-ignore not ts yet import { UserMenuProvider } from './user_menu'; import { UptimeProvider } from './uptime'; -import { InfraSourceConfigurationFlyoutProvider } from './infra_source_configuration_flyout'; +import { InfraSourceConfigurationFormProvider } from './infra_source_configuration_form'; import { InfraLogStreamProvider } from './infra_log_stream'; import { MachineLearningProvider } from './machine_learning'; @@ -86,7 +86,7 @@ export const services = { spaces: SpacesServiceProvider, userMenu: UserMenuProvider, uptime: UptimeProvider, - infraSourceConfigurationFlyout: InfraSourceConfigurationFlyoutProvider, + infraSourceConfigurationForm: InfraSourceConfigurationFormProvider, infraLogStream: InfraLogStreamProvider, ml: MachineLearningProvider, }; diff --git a/x-pack/test/functional/services/infra_log_stream.ts b/x-pack/test/functional/services/infra_log_stream.ts index 265c4b8d13b3c..d41b0062dcfda 100644 --- a/x-pack/test/functional/services/infra_log_stream.ts +++ b/x-pack/test/functional/services/infra_log_stream.ts @@ -9,6 +9,7 @@ import { WebElementWrapper } from '../../../../test/functional/services/lib/web_ export function InfraLogStreamProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const retry = getService('retry'); return { async getColumnHeaderLabels(): Promise { @@ -18,7 +19,14 @@ export function InfraLogStreamProvider({ getService }: FtrProviderContext) { return await Promise.all(columnHeaderElements.map(element => element.getVisibleText())); }, - async getStreamEntries(): Promise { + async getStreamEntries(minimumItems = 1): Promise { + await retry.try(async () => { + const elements = await testSubjects.findAll('streamEntry'); + if (!elements || elements.length < minimumItems) { + throw new Error(); + } + }); + return await testSubjects.findAll('streamEntry'); }, diff --git a/x-pack/test/functional/services/infra_source_configuration_flyout.ts b/x-pack/test/functional/services/infra_source_configuration_form.ts similarity index 66% rename from x-pack/test/functional/services/infra_source_configuration_flyout.ts rename to x-pack/test/functional/services/infra_source_configuration_form.ts index fb77e2dced045..fb4415ed74118 100644 --- a/x-pack/test/functional/services/infra_source_configuration_flyout.ts +++ b/x-pack/test/functional/services/infra_source_configuration_form.ts @@ -7,68 +7,56 @@ import { FtrProviderContext } from '../ftr_provider_context'; import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; -export function InfraSourceConfigurationFlyoutProvider({ getService }: FtrProviderContext) { - const find = getService('find'); +export function InfraSourceConfigurationFormProvider({ getService }: FtrProviderContext) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); return { - /** - * Tab navigation - */ - async switchToIndicesAndFieldsTab() { - await (await find.descendantDisplayedByCssSelector( - '#indicesAndFieldsTab', - await this.getFlyout() - )).click(); - await testSubjects.find('sourceConfigurationNameSectionTitle'); - }, - async switchToLogsTab() { - await (await find.descendantDisplayedByCssSelector( - '#logsTab', - await this.getFlyout() - )).click(); - await testSubjects.find('sourceConfigurationLogColumnsSectionTitle'); - }, - /** * Indices and fields */ async getNameInput(): Promise { - return await testSubjects.findDescendant('nameInput', await this.getFlyout()); + return await testSubjects.findDescendant('nameInput', await this.getForm()); }, async getLogIndicesInput(): Promise { - return await testSubjects.findDescendant('logIndicesInput', await this.getFlyout()); + return await testSubjects.findDescendant('logIndicesInput', await this.getForm()); }, async getMetricIndicesInput(): Promise { - return await testSubjects.findDescendant('metricIndicesInput', await this.getFlyout()); + return await testSubjects.findDescendant('metricIndicesInput', await this.getForm()); }, /** * Logs */ async getAddLogColumnButton(): Promise { - return await testSubjects.findDescendant('addLogColumnButton', await this.getFlyout()); + return await testSubjects.findDescendant('addLogColumnButton', await this.getForm()); }, async getAddLogColumnPopover(): Promise { return await testSubjects.find('addLogColumnPopover'); }, async addTimestampLogColumn() { await (await this.getAddLogColumnButton()).click(); - await (await testSubjects.findDescendant( - 'addTimestampLogColumn', - await this.getAddLogColumnPopover() - )).click(); + await retry.try(async () => { + await (await testSubjects.findDescendant( + 'addTimestampLogColumn', + await this.getAddLogColumnPopover() + )).click(); + }); }, async addFieldLogColumn(fieldName: string) { await (await this.getAddLogColumnButton()).click(); - const popover = await this.getAddLogColumnPopover(); - await (await testSubjects.findDescendant('fieldSearchInput', popover)).type(fieldName); - await (await testSubjects.findDescendant(`addFieldLogColumn:${fieldName}`, popover)).click(); + await retry.try(async () => { + const popover = await this.getAddLogColumnPopover(); + await (await testSubjects.findDescendant('fieldSearchInput', popover)).type(fieldName); + await (await testSubjects.findDescendant( + `addFieldLogColumn:${fieldName}`, + popover + )).click(); + }); }, async getLogColumnPanels(): Promise { - return await testSubjects.findAllDescendant('logColumnPanel', await this.getFlyout()); + return await testSubjects.findAllDescendant('logColumnPanel', await this.getForm()); }, async removeLogColumn(columnIndex: number) { const logColumnPanel = (await this.getLogColumnPanels())[columnIndex]; @@ -104,29 +92,24 @@ export function InfraSourceConfigurationFlyoutProvider({ getService }: FtrProvid }, /** - * Form and flyout + * Form */ - async getFlyout(): Promise { - return await testSubjects.find('sourceConfigurationFlyout'); + async getForm(): Promise { + return await testSubjects.find('sourceConfigurationContent'); }, async saveConfiguration() { await (await testSubjects.findDescendant( - 'updateSourceConfigurationButton', - await this.getFlyout() + 'applySettingsButton', + await this.getForm() )).click(); await retry.try(async () => { const element = await testSubjects.findDescendant( - 'updateSourceConfigurationButton', - await this.getFlyout() + 'applySettingsButton', + await this.getForm() ); return !(await element.isEnabled()); }); }, - async closeFlyout() { - const flyout = await this.getFlyout(); - await (await testSubjects.findDescendant('closeFlyoutButton', flyout)).click(); - await testSubjects.waitForDeleted(flyout); - }, }; } From 9f6188f3140cff728388b55087d9bab654c68b0c Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 6 Aug 2019 14:01:12 +0200 Subject: [PATCH 02/26] [Rollup] Clone an existing rollup job (#41891) * WIP: Clone Rollup functionality * WIP: Course correct for better UX * Fix formatting issues * wip continues, refactoring to clone job pattern. * First pass of clone functionality working end to end. * Only re-type when necessary * Show cloned errors immediately after choosing to clone a rollupjob * Remove pluralised label for now * cherry pick master changes * First pass at new start-on-create feature with error handling * Added tests and did a minor naming refactor for tests * Combine job create review tests into one file, following steps performed: 1) correctly mock out http requests 2) increase the wait time given the 500ms sleep * Go with potentially incorrect pluralisation for now * Use JS paramter default * Simplify on click event listener * Cleanup use of lodash imports, fix spec after change to event listener, make code a bit more defensive * Remove unnecessary span * Updated checkbox copy * Moved start after create checkbox to immediate right of start button * Update checkbox vertical alignment * Re-integrate changes with master * Switch job action menu ui component to using i18n.translate * More descriptive variable name * Use initialState for job clear * Simplify and rephrase comment * Remove lodash import and add missing period to end of sentence. * Fix defaulting to something when it should be nothing * Fix i18n compatibility checks * Tests for cloning jobs * Test cleanup * Fix whitespace noise * Fix button labels assertion to be more robust * Updated validation logic for rollups - rollups names are non unique Added "-copy" to end of cloned rollup job name/id * Whitespace * clean up unused code and double clicking of component in test * Update test suite name * Added comments to create from clone test * Added better expectations for results of smoke test * Whitespace --- .../client_integration/helpers/constants.js | 195 ++++++++++++------ .../client_integration/helpers/index.js | 2 + .../helpers/job_clone.helpers.js | 40 ++++ .../job_create_clone.test.js | 147 +++++++++++++ .../client_integration/job_list_clone.test.js | 73 +++++++ .../job_action_menu.container.js | 4 + .../job_action_menu/job_action_menu.js | 76 ++++--- .../job_create/job_create.container.js | 6 + .../sections/job_create/job_create.js | 165 +++++++++------ .../job_create/navigation/navigation.js | 1 - .../sections/job_create/steps/step_review.js | 2 - .../sections/job_create/steps_config/index.js | 131 +++++++----- .../job_create/steps_config/validate_id.js | 18 +- .../steps_config/validate_rollup_index.js | 36 ++-- .../sections/job_list/job_list.container.js | 4 + .../job_list/job_table/job_table.test.js | 13 +- .../rollup/public/crud_app/services/index.js | 9 +- .../crud_app/services/retype_metrics.js | 25 +++ .../public/crud_app/store/action_types.js | 4 + .../crud_app/store/actions/clone_job.js | 20 ++ .../public/crud_app/store/actions/index.js | 5 + .../crud_app/store/middleware/clone_job.js | 23 +++ .../public/crud_app/store/middleware/index.js | 3 +- .../crud_app/store/reducers/clone_job.js | 25 +++ .../public/crud_app/store/reducers/index.js | 2 + .../public/crud_app/store/reducers/jobs.js | 2 +- .../public/crud_app/store/selectors/index.js | 2 + .../rollup/public/crud_app/store/store.js | 3 +- 28 files changed, 788 insertions(+), 248 deletions(-) create mode 100644 x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/job_clone.helpers.js create mode 100644 x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_clone.test.js create mode 100644 x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list_clone.test.js create mode 100644 x-pack/legacy/plugins/rollup/public/crud_app/services/retype_metrics.js create mode 100644 x-pack/legacy/plugins/rollup/public/crud_app/store/actions/clone_job.js create mode 100644 x-pack/legacy/plugins/rollup/public/crud_app/store/middleware/clone_job.js create mode 100644 x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/clone_job.js diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/constants.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/constants.js index 2a29b066004f0..31e8a35b1178f 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/constants.js +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/constants.js @@ -15,74 +15,143 @@ export const JOB_TO_CREATE = { }; export const JOBS = { - jobs: [{ - config: { - id: 'my-rollup-job', - index_pattern: 'kibana_sample*', - rollup_index: 'rollup-index', - cron: '0 0 0 ? * 7', - groups: { - date_histogram: { - interval: '24h', - field: 'timestamp', - delay: '1d', - time_zone: 'UTC', + jobs: [ + { + config: { + id: 'my-rollup-job', + index_pattern: 'kibana_sample*', + rollup_index: 'rollup-index', + cron: '0 0 0 ? * 7', + groups: { + date_histogram: { + interval: '24h', + field: 'timestamp', + delay: '1d', + time_zone: 'UTC', + }, }, + metrics: [], + timeout: '20s', + page_size: 1000, + }, + status: { + job_state: 'stopped', + upgraded_doc_id: true, + }, + stats: { + pages_processed: 0, + documents_processed: 0, + rollups_indexed: 0, + trigger_count: 0, + index_time_in_ms: 0, + index_total: 0, + index_failures: 0, + search_time_in_ms: 0, + search_total: 0, + search_failures: 0, }, - metrics: [], - timeout: '20s', - page_size: 1000, - }, - status: { - job_state: 'stopped', - upgraded_doc_id: true, - }, - stats: { - pages_processed: 0, - documents_processed: 0, - rollups_indexed: 0, - trigger_count: 0, - index_time_in_ms: 0, - index_total: 0, - index_failures: 0, - search_time_in_ms: 0, - search_total: 0, - search_failures: 0, }, - }, - { - config: { - id: 'my-rollup-job', - index_pattern: 'kibana_sample*', - rollup_index: 'rollup-index', - cron: '0 0 0 ? * 7', - groups: { - date_histogram: { - interval: '24h', - field: 'timestamp', - delay: '1d', - time_zone: 'UTC', + { + config: { + id: 'my-rollup-job', + index_pattern: 'kibana_sample*', + rollup_index: 'rollup-index', + cron: '0 0 0 ? * 7', + groups: { + date_histogram: { + interval: '24h', + field: 'timestamp', + delay: '1d', + time_zone: 'UTC', + }, }, + metrics: [], + timeout: '20s', + page_size: 1000, + }, + status: { + job_state: 'not_a_known_state', + upgraded_doc_id: true, + }, + stats: { + pages_processed: 0, + documents_processed: 0, + rollups_indexed: 0, + trigger_count: 0, + index_time_in_ms: 0, + index_total: 0, + index_failures: 0, + search_time_in_ms: 0, + search_total: 0, + search_failures: 0, }, - metrics: [], - timeout: '20s', - page_size: 1000, - }, - status: { - job_state: 'not_a_known_state', - upgraded_doc_id: true, }, - stats: { - pages_processed: 0, - documents_processed: 0, - rollups_indexed: 0, - trigger_count: 0, - index_time_in_ms: 0, - index_total: 0, - index_failures: 0, - search_time_in_ms: 0, - search_total: 0, - search_failures: 0, + ], +}; + +export const JOB_TO_CLONE = { + jobs: [ + { + config: { + id: 't2', + index_pattern: 'k*', + rollup_index: 't2', + cron: '0 0 0 ? * 7', + groups: { + date_histogram: { + calendar_interval: '1s', + field: '@timestamp', + delay: '1d', + time_zone: 'UTC', + }, + histogram: { interval: 5, fields: ['phpmemory'] }, + terms: { + fields: ['geo.dest', 'machine.os.keyword', 'memory', 'message.keyword', 'referer'], + }, + }, + metrics: [ + { field: 'bytes', metrics: ['avg', 'max'] }, + { field: 'timestamp', metrics: ['value_count', 'min'] }, + ], + timeout: '20s', + page_size: 1000, + }, + status: { job_state: 'stopped' }, + stats: { + pages_processed: 0, + documents_processed: 0, + rollups_indexed: 0, + trigger_count: 0, + index_time_in_ms: 0, + index_total: 0, + index_failures: 0, + search_time_in_ms: 0, + search_total: 0, + search_failures: 0, + }, }, - }], + ], +}; + +export const JOB_CLONE_INDEX_PATTERN_CHECK = { + doesMatchIndices: true, + doesMatchRollupIndices: false, + dateFields: ['utc_time', 'timestamp', '@timestamp'], + numericFields: ['memory', 'machine.ram', 'phpmemory', 'bytes'], + keywordFields: [ + 'referer', + 'tags.keyword', + 'geo.dest', + 'response.keyword', + 'agent.keyword', + 'extension.keyword', + 'machine.os.keyword', + 'host.keyword', + 'geo.srcdest', + 'request.keyword', + 'geo.src', + 'index.keyword', + 'url.keyword', + 'message.keyword', + ], }; diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/index.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/index.js index dbd81023df2bb..9573e2f405b84 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/index.js +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/index.js @@ -6,6 +6,7 @@ import { setup as jobCreateSetup } from './job_create.helpers'; import { setup as jobListSetup } from './job_list.helpers'; +import { setup as jobCloneSetup } from './job_clone.helpers'; export { nextTick, getRandomString, findTestSubject } from '../../../../../../test_utils'; @@ -14,4 +15,5 @@ export { setupEnvironment } from './setup_environment'; export const pageHelpers = { jobCreate: { setup: jobCreateSetup }, jobList: { setup: jobListSetup }, + jobClone: { setup: jobCloneSetup }, }; diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/job_clone.helpers.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/job_clone.helpers.js new file mode 100644 index 0000000000000..48672a8bc13dd --- /dev/null +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/helpers/job_clone.helpers.js @@ -0,0 +1,40 @@ +/* + * 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 { registerTestBed } from '../../../../../../test_utils'; +import { createRollupJobsStore } from '../../../public/crud_app/store'; +import { JobCreate } from '../../../public/crud_app/sections'; +import { JOB_TO_CLONE } from './constants'; +import { deserializeJob } from '../../../public/crud_app/services'; + +const initTestBed = registerTestBed(JobCreate, { + store: createRollupJobsStore({ + cloneJob: { job: deserializeJob(JOB_TO_CLONE.jobs[0]) }, + }), +}); + +export const setup = props => { + const testBed = initTestBed(props); + const { component } = testBed; + + // User actions + const clickNextStep = () => { + const button = testBed.find('rollupJobNextButton'); + button.simulate('click'); + component.update(); + }; + + return { + ...testBed, + actions: { + clickNextStep, + }, + form: { + ...testBed.form, + // fillFormFields, + }, + }; +}; diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_clone.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_clone.test.js new file mode 100644 index 0000000000000..f50fa3fe4859b --- /dev/null +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_clone.test.js @@ -0,0 +1,147 @@ +/* + * 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 { setupEnvironment, pageHelpers } from './helpers'; +import { JOB_TO_CLONE, JOB_CLONE_INDEX_PATTERN_CHECK } from './helpers/constants'; + +jest.mock('ui/index_patterns', () => { + const { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } = require.requireActual( + '../../../../../../src/legacy/ui/public/index_patterns/constants' + ); + return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE }; +}); + +jest.mock('ui/chrome', () => ({ + addBasePath: () => '/api/rollup', + breadcrumbs: { set: () => {} }, + getInjected: () => ({}), +})); + +jest.mock('lodash/function/debounce', () => fn => fn); + +const { setup } = pageHelpers.jobClone; +const { + jobs: [{ config: jobConfig }], +} = JOB_TO_CLONE; + +describe('Cloning a rollup job through create job wizard', () => { + let httpRequestsMockHelpers; + let server; + let find; + let exists; + let form; + let table; + let actions; + + beforeAll(() => { + ({ server, httpRequestsMockHelpers } = setupEnvironment()); + }); + + beforeEach(() => { + httpRequestsMockHelpers.setIndexPatternValidityResponse(JOB_CLONE_INDEX_PATTERN_CHECK); + + ({ exists, find, form, actions, table } = setup()); + }); + + afterAll(() => { + server.restore(); + }); + + it('should have fields correctly pre-populated', async () => { + // Step 1: Logistics + + expect(find('rollupJobName').props().value).toBe(jobConfig.id + '-copy'); + expect(form.getErrorsMessages()).toEqual([]); + // Advanced cron should automatically show when we are cloning a job + expect(exists('rollupAdvancedCron')).toBe(true); + + expect(find('rollupAdvancedCron').props().value).toBe(jobConfig.cron); + expect(find('rollupPageSize').props().value).toBe(jobConfig.page_size); + const { + groups: { date_histogram: dateHistogram }, + } = jobConfig; + expect(find('rollupDelay').props().value).toBe(dateHistogram.delay); + + form.setInputValue('rollupJobName', 't3'); + form.setInputValue('rollupIndexName', 't3'); + + await actions.clickNextStep(); + + // Step 2: Date histogram + + expect(find('rollupJobCreateDateFieldSelect').props().value).toBe(dateHistogram.field); + expect(find('rollupJobInterval').props().value).toBe(dateHistogram.calendar_interval); + expect(find('rollupJobCreateTimeZoneSelect').props().value).toBe(dateHistogram.time_zone); + + await actions.clickNextStep(); + + // Step 3: Terms + + const { tableCellsValues: tableCellValuesTerms } = table.getMetaData('rollupJobTermsFieldList'); + const { + groups: { + terms: { fields: terms }, + }, + } = jobConfig; + + expect(tableCellValuesTerms.length).toBe(terms.length); + for (const [keyword] of tableCellValuesTerms) { + expect(terms.find(term => term === keyword)).toBe(keyword); + } + + await actions.clickNextStep(); + + // Step 4: Histogram + + const { tableCellsValues: tableCellValuesHisto } = table.getMetaData( + 'rollupJobHistogramFieldList' + ); + + const { + groups: { + histogram: { fields: histogramsTerms }, + }, + } = jobConfig; + + expect(tableCellValuesHisto.length).toBe(histogramsTerms.length); + for (const [keyword] of tableCellValuesHisto) { + expect(histogramsTerms.find(term => term === keyword)).toBe(keyword); + } + + await actions.clickNextStep(); + + // Step 5: Metrics + + const { metrics } = jobConfig; + const { rows: metricsRows } = table.getMetaData('rollupJobMetricsFieldList'); + + // Slight nastiness due to nested arrays: + // For each row in the metrics table we want to assert that the checkboxes + // are either checked, or not checked, according to the job config we are cloning + metricsRows.forEach((metricRow, idx) => { + // Assumption: metrics from the jobConfig and metrics displayed on the UI + // are parallel arrays; so we can use row index to get the corresponding config. + const { metrics: checkedMetrics } = metrics[idx]; + const { + columns: [, , { reactWrapper: checkboxColumn }], + } = metricRow; + + let checkedCountActual = 0; + const checkedCountExpected = checkedMetrics.length; + + checkboxColumn.find('input').forEach(el => { + const props = el.props(); + const shouldBeChecked = checkedMetrics.some( + checkedMetric => props['data-test-subj'] === `rollupJobMetricsCheckbox-${checkedMetric}` + ); + if (shouldBeChecked) ++checkedCountActual; + expect(props.checked).toBe(shouldBeChecked); + }); + // All inputs from job config have been accounted for on the UI + expect(checkedCountActual).toBe(checkedCountExpected); + }); + }); +}); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list_clone.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list_clone.test.js new file mode 100644 index 0000000000000..11efc9940ac11 --- /dev/null +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list_clone.test.js @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setupEnvironment, pageHelpers, nextTick } from './helpers'; +import { JOB_TO_CLONE, JOB_CLONE_INDEX_PATTERN_CHECK } from './helpers/constants'; +import { getRouter } from '../../public/crud_app/services/routing'; +import { CRUD_APP_BASE_PATH } from '../../public/crud_app/constants'; + +jest.mock('ui/index_patterns', () => { + const { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } = require.requireActual( + '../../../../../../src/legacy/ui/public/index_patterns/constants' + ); // eslint-disable-line max-len + return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE }; +}); + +jest.mock('ui/chrome', () => ({ + addBasePath: () => '/api/rollup', + breadcrumbs: { set: () => {} }, + getInjected: () => ({}), +})); + +jest.mock('lodash/function/debounce', () => fn => fn); + +const { setup } = pageHelpers.jobList; + +describe('Smoke test cloning an existing rollup job from job list', () => { + let server; + let httpRequestsMockHelpers; + let table; + let find; + let component; + let exists; + + beforeAll(() => { + ({ server, httpRequestsMockHelpers } = setupEnvironment()); + }); + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setIndexPatternValidityResponse(JOB_CLONE_INDEX_PATTERN_CHECK); + httpRequestsMockHelpers.setLoadJobsResponse(JOB_TO_CLONE); + + ({ find, exists, table, component } = setup()); + + await nextTick(); // We need to wait next tick for the mock server response to comes in + component.update(); + }); + + it('should navigate to create view with default values set', async () => { + const router = getRouter(); + const { rows } = table.getMetaData('rollupJobsListTable'); + const button = rows[0].columns[1].reactWrapper.find('button'); + + expect(exists('rollupJobDetailFlyout')).toBe(false); // make sure it is not shown + + button.simulate('click'); + + expect(exists('rollupJobDetailFlyout')).toBe(true); + expect(exists('jobActionMenuButton')).toBe(true); + + find('jobActionMenuButton').simulate('click'); + + expect(router.history.location.pathname).not.toBe(`${CRUD_APP_BASE_PATH}/create`); + find('jobCloneActionContextMenu').simulate('click'); + expect(router.history.location.pathname).toBe(`${CRUD_APP_BASE_PATH}/create`); + }); +}); diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.container.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.container.js index 2b1b0641c1528..f5c87ebbad131 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.container.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.container.js @@ -12,6 +12,7 @@ import { startJobs, stopJobs, deleteJobs, + cloneJob } from '../../../store/actions'; import { JobActionMenu as JobActionMenuComponent } from './job_action_menu'; @@ -34,6 +35,9 @@ const mapDispatchToProps = (dispatch, { jobs }) => { deleteJobs: () => { dispatch(deleteJobs(jobIds)); }, + cloneJob: (jobConfig) => { + dispatch(cloneJob(jobConfig)); + }, }; }; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.js index 83113071d7d7f..a94dfc202604e 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/components/job_action_menu/job_action_menu.js @@ -6,7 +6,8 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiButton, @@ -27,27 +28,28 @@ class JobActionMenuUi extends Component { startJobs: PropTypes.func.isRequired, stopJobs: PropTypes.func.isRequired, deleteJobs: PropTypes.func.isRequired, + cloneJob: PropTypes.func.isRequired, isUpdating: PropTypes.bool.isRequired, iconSide: PropTypes.string, anchorPosition: PropTypes.string, label: PropTypes.node, iconType: PropTypes.string, jobs: PropTypes.array, - } + }; static defaultProps = { iconSide: 'right', anchorPosition: 'rightUp', iconType: 'arrowDown', jobs: [], - } + }; constructor(props) { super(props); this.state = { isPopoverOpen: false, - showDeleteConfirmation: false + showDeleteConfirmation: false, }; } @@ -55,7 +57,7 @@ class JobActionMenuUi extends Component { const { startJobs, stopJobs, - intl, + cloneJob } = this.props; const isSingleSelection = this.isSingleSelection() ? 1 : 0; @@ -64,11 +66,9 @@ class JobActionMenuUi extends Component { if (this.canStartJobs()) { items.push({ - name: intl.formatMessage({ - id: 'xpack.rollupJobs.jobActionMenu.startJobLabel', + name: i18n.translate('xpack.rollupJobs.jobActionMenu.startJobLabel', { defaultMessage: 'Start {isSingleSelection, plural, one {job} other {jobs}}', - }, { - isSingleSelection, + values: { isSingleSelection }, }), icon: , onClick: () => { @@ -80,11 +80,9 @@ class JobActionMenuUi extends Component { if (this.canStopJobs()) { items.push({ - name: intl.formatMessage({ - id: 'xpack.rollupJobs.jobActionMenu.stopJobLabel', + name: i18n.translate('xpack.rollupJobs.jobActionMenu.stopJobLabel', { defaultMessage: 'Stop {isSingleSelection, plural, one {job} other {jobs}}', - }, { - isSingleSelection, + values: { isSingleSelection }, }), icon: , onClick: () => { @@ -94,13 +92,27 @@ class JobActionMenuUi extends Component { }); } + if (this.canCloneJob()) { + items.push({ + name: i18n.translate('xpack.rollupJobs.jobActionMenu.cloneJobLabel', { + defaultMessage: 'Clone job', + }), + icon: , + onClick: () => { + this.closePopover(); + const { jobs } = this.props; + cloneJob(jobs[0]); + }, + }); + } + if (this.canDeleteJobs()) { items.push({ - name: intl.formatMessage({ - id: 'xpack.rollupJobs.jobActionMenu.deleteJobLabel', + name: i18n.translate('xpack.rollupJobs.jobActionMenu.deleteJobLabel', { defaultMessage: 'Delete {isSingleSelection, plural, one {job} other {jobs}}', - }, { - isSingleSelection, + values: { + isSingleSelection, + }, }), icon: , onClick: () => { @@ -112,8 +124,7 @@ class JobActionMenuUi extends Component { const panelTree = { id: 0, - title: intl.formatMessage({ - id: 'xpack.rollupJobs.jobActionMenu.panelTitle', + title: i18n.translate('xpack.rollupJobs.jobActionMenu.panelTitle', { defaultMessage: 'Job options', }), items, @@ -152,6 +163,11 @@ class JobActionMenuUi extends Component { return jobs.some(job => job.status === 'started'); } + canCloneJob() { + const { jobs } = this.props; + return Boolean(jobs && jobs.length === 1); + } + canDeleteJobs() { const { jobs } = this.props; const areAllJobsStopped = jobs.findIndex(job => job.status === 'started') === -1; @@ -192,13 +208,13 @@ class JobActionMenuUi extends Component { }; render() { - const { intl, isUpdating } = this.props; + const { isUpdating } = this.props; if (isUpdating) { return ( - + @@ -219,18 +235,20 @@ class JobActionMenuUi extends Component { iconSide, anchorPosition, iconType, - label = intl.formatMessage({ - id: 'xpack.rollupJobs.jobActionMenu.buttonLabel', + label = i18n.translate('xpack.rollupJobs.jobActionMenu.buttonLabel', { defaultMessage: 'Manage {jobCount, plural, one {job} other {jobs}}', - }, { jobCount }), + values: { jobCount }, + }), } = this.props; const panels = this.panels(); - const actionsAriaLabel = intl.formatMessage({ - id: 'xpack.rollupJobs.jobActionMenu.jobActionMenuButtonAriaLabel', - defaultMessage: 'Job options', - }); + const actionsAriaLabel = i18n.translate( + 'xpack.rollupJobs.jobActionMenu.jobActionMenuButtonAriaLabel', + { + defaultMessage: 'Job options', + } + ); const button = ( { return { isSaving: isSaving(state), saveError: getCreateJobError(state), + jobToClone: getCloneJobConfig(state), }; }; @@ -32,6 +35,9 @@ const mapDispatchToProps = (dispatch) => { clearCreateJobErrors: () => { dispatch(clearCreateJobErrors()); }, + clearCloneJob: () => { + dispatch(clearCloneJob()); + }, }; }; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/job_create.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/job_create.js index 8a7a705d0ce54..3e7d5603ca95b 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/job_create.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/job_create.js @@ -9,6 +9,8 @@ import PropTypes from 'prop-types'; import mapValues from 'lodash/object/mapValues'; import cloneDeep from 'lodash/lang/cloneDeep'; import debounce from 'lodash/function/debounce'; +import first from 'lodash/array/first'; + import { i18n } from '@kbn/i18n'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import chrome from 'ui/chrome'; @@ -32,6 +34,7 @@ import { formatFields, listBreadcrumb, createBreadcrumb, + retypeMetrics, } from '../../services'; import { Navigation } from './navigation'; @@ -80,24 +83,30 @@ const stepIdToTitleMap = { export class JobCreateUi extends Component { static propTypes = { createJob: PropTypes.func, + clearCloneJob: PropTypes.func, isSaving: PropTypes.bool, createJobError: PropTypes.node, + jobToClone: PropTypes.object, }; constructor(props) { super(props); - chrome.breadcrumbs.set([ MANAGEMENT_BREADCRUMB, listBreadcrumb, createBreadcrumb ]); - - const stepsFields = mapValues(stepIdToStepConfigMap, step => cloneDeep(step.defaultFields || {})); + chrome.breadcrumbs.set([MANAGEMENT_BREADCRUMB, listBreadcrumb, createBreadcrumb]); + const { jobToClone: stepDefaultOverrides } = props; + const stepsFields = mapValues(stepIdToStepConfigMap, step => + cloneDeep(step.getDefaultFields(stepDefaultOverrides)) + ); this.state = { + jobToClone: stepDefaultOverrides || null, checkpointStepId: stepIds[0], currentStepId: stepIds[0], nextStepId: stepIds[1], previousStepId: undefined, stepsFieldErrors: this.getStepsFieldsErrors(stepsFields), - areStepErrorsVisible: false, + // Show step errors immediately if we are cloning a job. + areStepErrorsVisible: !!stepDefaultOverrides, stepsFields, isValidatingIndexPattern: false, indexPatternAsyncErrors: undefined, @@ -113,6 +122,11 @@ export class JobCreateUi extends Component { componentDidMount() { this._isMounted = true; + const { clearCloneJob, jobToClone } = this.props; + if (jobToClone) { + clearCloneJob(); + this.requestIndexPatternValidation(); + } } componentDidUpdate(prevProps, prevState) { @@ -155,7 +169,7 @@ export class JobCreateUi extends Component { return; } - // Ignore all responses except that to the most recent request. + // Only re-request if the index pattern changed. if (lastIndexPatternValidationTime !== this.lastIndexPatternValidationTime) { return; } @@ -171,40 +185,58 @@ export class JobCreateUi extends Component { let indexPatternAsyncErrors; if (doesIndexPatternMatchRollupIndices) { - indexPatternAsyncErrors = [( + indexPatternAsyncErrors = [ - )]; + />, + ]; } else if (!doesIndexPatternMatchIndices) { - indexPatternAsyncErrors = [( + indexPatternAsyncErrors = [ - )]; + />, + ]; } else if (!indexPatternDateFields.length) { - indexPatternAsyncErrors = [( + indexPatternAsyncErrors = [ - )]; + />, + ]; } - const formattedNumericFields = formatFields( - numericFields, - i18n.translate('xpack.rollupJobs.create.numericTypeField', { defaultMessage: 'numeric' }) - ); - const formattedKeywordFields = formatFields( - keywordFields, - i18n.translate('xpack.rollupJobs.create.keywordTypeField', { defaultMessage: 'keyword' }) - ); - const formattedDateFields = formatFields( - indexPatternDateFields, - i18n.translate('xpack.rollupJobs.create.dateTypeField', { defaultMessage: 'date' }) - ); + const numericType = i18n.translate('xpack.rollupJobs.create.numericTypeField', { + defaultMessage: 'numeric', + }); + const keywordType = i18n.translate('xpack.rollupJobs.create.keywordTypeField', { + defaultMessage: 'keyword', + }); + const dateType = i18n.translate('xpack.rollupJobs.create.dateTypeField', { + defaultMessage: 'date', + }); + + const formattedNumericFields = formatFields(numericFields, numericType); + const formattedKeywordFields = formatFields(keywordFields, keywordType); + const formattedDateFields = formatFields(indexPatternDateFields, dateType); + + const { jobToClone, stepsFields } = this.state; + const { + [STEP_METRICS]: { metrics }, + } = stepsFields; + + // Only re-type metrics if they haven't been typed already + if (jobToClone && metrics && metrics.length && !first(metrics).type) { + // Re-type any pre-existing metrics entries for the job we are cloning. + const typeMaps = [ + { fields: formattedNumericFields, type: numericType }, + { fields: formattedKeywordFields, type: keywordType }, + { fields: formattedDateFields, type: dateType }, + ]; + const retypedMetrics = retypeMetrics({ metrics, typeMaps }); + this.onFieldsChange({ metrics: retypedMetrics }, STEP_METRICS); + } function sortFields(a, b) { const nameA = a.name.toUpperCase(); @@ -221,17 +253,15 @@ export class JobCreateUi extends Component { return 0; } - const indexPatternTermsFields = [ - ...formattedNumericFields, - ...formattedKeywordFields, - ].sort(sortFields); + const indexPatternTermsFields = [...formattedNumericFields, ...formattedKeywordFields].sort( + sortFields + ); - const indexPatternHistogramFields = [ ...formattedNumericFields ].sort(sortFields); + const indexPatternHistogramFields = [...formattedNumericFields].sort(sortFields); - const indexPatternMetricsFields = [ - ...formattedNumericFields, - ...formattedDateFields, - ].sort(sortFields); + const indexPatternMetricsFields = [...formattedNumericFields, ...formattedDateFields].sort( + sortFields + ); indexPatternDateFields.sort(); @@ -244,11 +274,16 @@ export class JobCreateUi extends Component { isValidatingIndexPattern: false, }); - // Select first time field by default. - this.onFieldsChange({ - dateHistogramField: indexPatternDateFields.length ? indexPatternDateFields[0] : null, - }, STEP_DATE_HISTOGRAM); - }).catch((error) => { + if (!jobToClone) { + // Select first time field by default. + this.onFieldsChange( + { + dateHistogramField: indexPatternDateFields.length ? indexPatternDateFields[0] : null, + }, + STEP_DATE_HISTOGRAM + ); + } + }).catch(error => { // We don't need to do anything if this component has been unmounted. if (!this._isMounted) { return; @@ -263,13 +298,13 @@ export class JobCreateUi extends Component { if (error && error.data) { const { error: errorString, statusCode } = error.data; - const indexPatternAsyncErrors = [( + const indexPatternAsyncErrors = [ - )]; + />, + ]; this.setState({ indexPatternAsyncErrors, @@ -285,9 +320,12 @@ export class JobCreateUi extends Component { // This error isn't an HTTP error, so let the fatal error screen tell the user something // unexpected happened. - fatalError(error, i18n.translate('xpack.rollupJobs.create.errors.indexPatternValidationFatalErrorTitle', { - defaultMessage: 'Rollup Job Wizard index pattern validation', - })); + fatalError( + error, + i18n.translate('xpack.rollupJobs.create.errors.indexPatternValidationFatalErrorTitle', { + defaultMessage: 'Rollup Job Wizard index pattern validation', + }) + ); }); }, 300); @@ -300,11 +338,12 @@ export class JobCreateUi extends Component { isComplete: index < indexOfCurrentStep, isSelected: index === indexOfCurrentStep, onClick: () => this.goToStep(stepId), - disabled: ( - !this.canGoToStep(stepId) - || stepIds.indexOf(stepId) > stepIds.indexOf(checkpointStepId) - ), - 'data-test-subj': index === indexOfCurrentStep ? `createRollupStep${index + 1}--active` : `createRollupStep${index + 1}`, + disabled: + !this.canGoToStep(stepId) || stepIds.indexOf(stepId) > stepIds.indexOf(checkpointStepId), + 'data-test-subj': + index === indexOfCurrentStep + ? `createRollupStep${index + 1}--active` + : `createRollupStep${index + 1}`, })); } @@ -353,10 +392,7 @@ export class JobCreateUi extends Component { } hasStepErrors(stepId) { - const { - indexPatternAsyncErrors, - stepsFieldErrors, - } = this.state; + const { indexPatternAsyncErrors, stepsFieldErrors } = this.state; if (stepId === STEP_LOGISTICS) { if (Boolean(indexPatternAsyncErrors)) { @@ -372,7 +408,8 @@ export class JobCreateUi extends Component { return Object.keys(newStepsFields).reduce((stepsFieldErrors, stepId) => { const stepFields = newStepsFields[stepId]; const fieldsValidator = stepIdToStepConfigMap[stepId].fieldsValidator; - stepsFieldErrors[stepId] = typeof fieldsValidator === `function` ? fieldsValidator(stepFields) : {}; + stepsFieldErrors[stepId] = + typeof fieldsValidator === `function` ? fieldsValidator(stepFields) : {}; return stepsFieldErrors; }, {}); } @@ -425,7 +462,7 @@ export class JobCreateUi extends Component { [STEP_METRICS]: { metrics, }, - [STEP_REVIEW]: {} + [STEP_REVIEW]: {}, }, startJobAfterCreation, } = this.state; @@ -618,13 +655,7 @@ export class JobCreateUi extends Component { ); case STEP_REVIEW: - return ( - - ); + return ; default: return null; @@ -650,9 +681,9 @@ export class JobCreateUi extends Component { // Users can click the next step button as long as validation hasn't executed, and as long // as we're not waiting on async validation to complete. const canGoToNextStep = - !isValidatingIndexPattern - && hasNextStep - && (!areStepErrorsVisible || this.canGoToStep(nextStepId)); + !isValidatingIndexPattern && + hasNextStep && + (!areStepErrorsVisible || this.canGoToStep(nextStepId)); return ( { - console.log(startJobAfterCreation); if (isSaving) { return ( diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_review.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_review.js index 1cfca928838bb..755fc02dcb8c4 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_review.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps/step_review.js @@ -32,9 +32,7 @@ const JOB_DETAILS_TABS = [ export class StepReviewUi extends Component { static propTypes = { - fields: PropTypes.object.isRequired, job: PropTypes.object.isRequired, - onFieldsChange: PropTypes.func.isRequired, }; constructor(props) { diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js index 444eb929bc151..09c427a49f028 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js @@ -5,6 +5,8 @@ */ import cloneDeep from 'lodash/lang/cloneDeep'; +import get from 'lodash/object/get'; +import pick from 'lodash/object/pick'; import { WEEK } from '../../../services'; @@ -35,25 +37,48 @@ export const stepIds = [ STEP_REVIEW, ]; +/** + * Map a specific wizard step to two functions: + * 1. getDefaultFields: (overrides) => object + * 2. fieldValidations + * + * See x-pack/legacy/plugins/rollup/public/crud_app/services/jobs.js for more information on override's shape + */ export const stepIdToStepConfigMap = { [STEP_LOGISTICS]: { - defaultFields: { - id: '', - indexPattern: '', - rollupIndex: '', - // Every week on Saturday, at 00:00:00 - rollupCron: '0 0 0 ? * 7', - simpleRollupCron: '0 0 0 ? * 7', + getDefaultFields: (overrides = {}) => { + // We don't display the simple editor if there are overrides for the rollup's cron + const isAdvancedCronVisible = !!overrides.rollupCron; + // The best page size boils down to how much memory the user has, e.g. how many buckets should // be accumulated at one time. 1000 is probably a safe size without being too small. - rollupPageSize: 1000, - // Though the API doesn't require a delay, in many real-world cases, servers will go down for - // a few hours as they're being restarted. A delay of 1d would allow them that period to reboot - // and the "expense" is pretty negligible in most cases: 1 day of extra non-rolled-up data. - rollupDelay: '1d', - cronFrequency: WEEK, - isAdvancedCronVisible: false, - fieldToPreferredValueMap: {}, + const rollupPageSize = get(overrides, ['json', 'config', 'page_size'], 1000); + const clonedRollupId = overrides.id || undefined; + const id = overrides.id ? `${overrides.id}-copy` : ''; + + const defaults = { + indexPattern: '', + rollupIndex: '', + // Every week on Saturday, at 00:00:00 + rollupCron: '0 0 0 ? * 7', + simpleRollupCron: '0 0 0 ? * 7', + rollupPageSize, + // Though the API doesn't require a delay, in many real-world cases, servers will go down for + // a few hours as they're being restarted. A delay of 1d would allow them that period to reboot + // and the "expense" is pretty negligible in most cases: 1 day of extra non-rolled-up data. + rollupDelay: '1d', + cronFrequency: WEEK, + fieldToPreferredValueMap: {}, + }; + + return { + ...defaults, + ...pick(overrides, Object.keys(defaults)), + id, + isAdvancedCronVisible, + rollupPageSize, + clonedRollupId, + }; }, fieldsValidator: fields => { const { @@ -63,83 +88,81 @@ export const stepIdToStepConfigMap = { rollupCron, rollupPageSize, rollupDelay, + clonedRollupId, } = fields; - - const errors = { - id: validateId(id), + return { + id: validateId(id, clonedRollupId), indexPattern: validateIndexPattern(indexPattern, rollupIndex), rollupIndex: validateRollupIndex(rollupIndex, indexPattern), rollupCron: validateRollupCron(rollupCron), rollupPageSize: validateRollupPageSize(rollupPageSize), rollupDelay: validateRollupDelay(rollupDelay), }; - - return errors; }, }, [STEP_DATE_HISTOGRAM]: { - defaultFields: { - dateHistogramField: null, - dateHistogramInterval: null, - dateHistogramTimeZone: 'UTC', + getDefaultFields: (overrides = {}) => { + const defaults = { + dateHistogramField: null, + dateHistogramInterval: null, + dateHistogramTimeZone: 'UTC', + }; + + return { + ...defaults, + ...pick(overrides, Object.keys(defaults)), + }; }, fieldsValidator: fields => { - const { - dateHistogramField, - dateHistogramInterval, - } = fields; + const { dateHistogramField, dateHistogramInterval } = fields; - const errors = { + return { dateHistogramField: validateDateHistogramField(dateHistogramField), dateHistogramInterval: validateDateHistogramInterval(dateHistogramInterval), }; - - return errors; }, }, [STEP_TERMS]: { - defaultFields: { - terms: [], + getDefaultFields: (overrides = {}) => { + return { + terms: [], + ...pick(overrides, ['terms']), + }; }, }, [STEP_HISTOGRAM]: { - defaultFields: { - histogram: [], - histogramInterval: undefined, + getDefaultFields: overrides => { + return { + histogram: [], + histogramInterval: undefined, + ...pick(overrides, ['histogram', 'histogramInterval']), + }; }, fieldsValidator: fields => { - const { - histogram, - histogramInterval, - } = fields; + const { histogram, histogramInterval } = fields; - const errors = { + return { histogramInterval: validateHistogramInterval(histogram, histogramInterval), }; - - return errors; }, }, [STEP_METRICS]: { - defaultFields: { - metrics: [], + getDefaultFields: (overrides = {}) => { + return { + metrics: [], + ...pick(overrides, ['metrics']), + }; }, fieldsValidator: fields => { - const { - metrics, - } = fields; + const { metrics } = fields; - const errors = { + return { metrics: validateMetrics(metrics), }; - - return errors; }, }, [STEP_REVIEW]: { - defaultFields: { - startJobAfterCreation: false - } + getDefaultFields: () => ({}), }, }; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_id.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_id.js index 9cfa1fb5eebe3..4dbf4e1b71702 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_id.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_id.js @@ -7,14 +7,24 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -export function validateId(id) { +export function validateId(id, clonedId) { + if (clonedId && id === clonedId) { + return [ + , + ]; + } + if (!id || !id.trim()) { - return [( + return [ - )]; + />, + ]; } return undefined; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js index e631a7b6c0877..c1a4af34d0e4e 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_rollup_index.js @@ -10,60 +10,60 @@ import { findIllegalCharactersInIndexName } from 'ui/indices'; export function validateRollupIndex(rollupIndex, indexPattern) { if (!rollupIndex || !rollupIndex.trim()) { - return [( + return [ - )]; + />, + ]; } if (rollupIndex === indexPattern) { - return [( + return [ - )]; + />, + ]; } const illegalCharacters = findIllegalCharactersInIndexName(rollupIndex); if (illegalCharacters.length) { - return [( + return [ {illegalCharacters.join(' ')} }} - /> - )]; + />, + ]; } if (rollupIndex.includes(',')) { - return [( + return [ - )]; + />, + ]; } if (rollupIndex.includes(' ')) { - return [( + return [ - )]; + />, + ]; } if (rollupIndex[0] === '.') { - return [( + return [ - )]; + />, + ]; } return undefined; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.container.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.container.js index e0ad3dc852caf..bf0fa1a4727be 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.container.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.container.js @@ -17,6 +17,7 @@ import { refreshJobs, openDetailPanel, closeDetailPanel, + cloneJob, } from '../../store/actions'; import { JobList as JobListView } from './job_list'; @@ -43,6 +44,9 @@ const mapDispatchToProps = (dispatch) => { closeDetailPanel: () => { dispatch(closeDetailPanel()); }, + cloneJob: (jobConfig) => { + dispatch(cloneJob(jobConfig)); + } }; }; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js index e5e332a196b84..a68c805390807 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js @@ -165,7 +165,10 @@ describe('', () => { const contextMenuButtons = contextMenu.find('button'); const buttonsLabel = contextMenuButtons.map(btn => btn.text()); - expect(buttonsLabel).toEqual(['Start job', 'Delete job']); + const hasExpectedLabels = ['Start job', 'Delete job'] + .every(expectedLabel => buttonsLabel.includes(expectedLabel)); + + expect(hasExpectedLabels).toBe(true); }); it('should only have a "stop" action when the job is started', () => { @@ -178,7 +181,8 @@ describe('', () => { const contextMenuButtons = find('jobActionContextMenu').find('button'); const buttonsLabel = contextMenuButtons.map(btn => btn.text()); - expect(buttonsLabel).toEqual(['Stop job']); + const hasExpectedLabels = buttonsLabel.includes('Stop job'); + expect(hasExpectedLabels).toBe(true); }); it('should offer both "start" and "stop" actions when selecting job with different a status', () => { @@ -193,7 +197,10 @@ describe('', () => { const contextMenuButtons = find('jobActionContextMenu').find('button'); const buttonsLabel = contextMenuButtons.map(btn => btn.text()); - expect(buttonsLabel).toEqual(['Start jobs', 'Stop jobs']); + const hasExpectedLabels = ['Start jobs', 'Stop jobs'] + .every(expectedLabel => buttonsLabel.includes(expectedLabel)); + + expect(hasExpectedLabels).toBe(true); }); }); }); diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js b/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js index fdff4ebc89733..b3a7cdb9a286d 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js @@ -45,7 +45,7 @@ export { } from './documentation_links'; export { - filterItems, + filterItems } from './filter_items'; export { @@ -94,6 +94,7 @@ export { } from './sort_table'; export { - trackUiMetric, - METRIC_TYPE, -} from './track_ui_metric'; + retypeMetrics, +} from './retype_metrics'; + +export { trackUiMetric, METRIC_TYPE } from './track_ui_metric'; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/retype_metrics.js b/x-pack/legacy/plugins/rollup/public/crud_app/services/retype_metrics.js new file mode 100644 index 0000000000000..cdbb8e891b6ee --- /dev/null +++ b/x-pack/legacy/plugins/rollup/public/crud_app/services/retype_metrics.js @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** + * Re-associate type information with the metric type (e.g., 'date', or 'numeric'). + * + * When a job is being cloned the metrics returned from the server do not have + * type information (e.g., numeric, date etc) associated with them. + * + * @param object { metrics: deserialized job metric object, typeMaps: { fields: string[], type: string } } + * @returns { { : string, type: string, types: string[] }[] } + */ +export function retypeMetrics({ metrics, typeMaps }) { + return metrics.map(metric => { + const { name: metricName } = metric; + const { type } = typeMaps.find(type => type.fields.some(field => field.name === metricName)); + return { + ...metric, + type, + }; + }); +} diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/action_types.js b/x-pack/legacy/plugins/rollup/public/crud_app/store/action_types.js index 9c3235d3ac427..ba8238b348f9a 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/store/action_types.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/store/action_types.js @@ -16,6 +16,10 @@ export const LOAD_JOBS_FAILURE = 'LOAD_JOBS_FAILURE'; // Refresh jobs export const REFRESH_JOBS_SUCCESS = 'REFRESH_JOBS_SUCCESS'; +// Clone job +export const CLONE_JOB_START = 'CLONE_JOB_START'; +export const CLONE_JOB_CLEAR = 'CLONE_JOB_CLEAR'; + // Create job export const CREATE_JOB_START = 'CREATE_JOB_START'; export const CREATE_JOB_SUCCESS = 'CREATE_JOB_SUCCESS'; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/clone_job.js b/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/clone_job.js new file mode 100644 index 0000000000000..3f587f65d7de5 --- /dev/null +++ b/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/clone_job.js @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CLONE_JOB_START, CLONE_JOB_CLEAR } from '../action_types'; + +export const cloneJob = (jobToClone) => (dispatch) => { + dispatch({ + type: CLONE_JOB_START, + payload: jobToClone, + }); +}; + +export const clearCloneJob = () => (dispatch) => { + dispatch({ + type: CLONE_JOB_CLEAR, + }); +}; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/index.js b/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/index.js index ef74867497e67..104ba523d93b1 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/index.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/store/actions/index.js @@ -9,6 +9,11 @@ export { stopJobs, } from './change_job_status'; +export { + cloneJob, + clearCloneJob, +} from './clone_job'; + export { createJob, clearCreateJobErrors, diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/middleware/clone_job.js b/x-pack/legacy/plugins/rollup/public/crud_app/store/middleware/clone_job.js new file mode 100644 index 0000000000000..e424232cce0a1 --- /dev/null +++ b/x-pack/legacy/plugins/rollup/public/crud_app/store/middleware/clone_job.js @@ -0,0 +1,23 @@ +/* + * 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 { getRouter, getUserHasLeftApp } from '../../services'; +import { CLONE_JOB_START } from '../action_types'; +import { CRUD_APP_BASE_PATH } from '../../constants'; + +export const cloneJob = () => next => action => { + const { type } = action; + + if (type === CLONE_JOB_START) { + if (!getUserHasLeftApp()) { + getRouter().history.push({ + pathname: `${CRUD_APP_BASE_PATH}/create`, + }); + } + } + + return next(action); +}; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/middleware/index.js b/x-pack/legacy/plugins/rollup/public/crud_app/store/middleware/index.js index a898bc493bd50..2af2d85c847c1 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/store/middleware/index.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/store/middleware/index.js @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { detailPanel } from './detail_panel'; +export { cloneJob } from './clone_job'; +export { detailPanel } from './detail_panel'; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/clone_job.js b/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/clone_job.js new file mode 100644 index 0000000000000..e705d5324c6ea --- /dev/null +++ b/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/clone_job.js @@ -0,0 +1,25 @@ +/* + * 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 { CLONE_JOB_START, CLONE_JOB_CLEAR } from '../action_types'; + +const initialState = { + job: undefined, +}; + +export function cloneJob(state = initialState, action) { + const { type, payload } = action; + + if (type === CLONE_JOB_START) { + return { job: payload }; + } + + if (type === CLONE_JOB_CLEAR) { + return { ...initialState }; + } + + return state; +} diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/index.js b/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/index.js index 1653adae82ea5..b5127ecfda400 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/index.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/index.js @@ -8,11 +8,13 @@ import { combineReducers } from 'redux'; import { jobs } from './jobs'; import { tableState } from './table_state'; import { detailPanel } from './detail_panel'; +import { cloneJob } from './clone_job'; import { createJob } from './create_job'; import { updateJob } from './update_job'; export const rollupJobs = combineReducers({ jobs, + cloneJob, tableState, detailPanel, createJob, diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/jobs.js b/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/jobs.js index 5e6f7834aacb7..cb5011688b874 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/jobs.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/store/reducers/jobs.js @@ -57,7 +57,7 @@ export function jobs(state = initialState, action) { return { ...state, isLoading: false, - jobLoadError: payload.error + jobLoadError: payload.error, }; case CREATE_JOB_SUCCESS: diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/selectors/index.js b/x-pack/legacy/plugins/rollup/public/crud_app/store/selectors/index.js index 5a0de43c8c8d6..7966c39e13663 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/store/selectors/index.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/store/selectors/index.js @@ -26,6 +26,8 @@ export const isSaving = (state) => state.createJob.isSaving; export const getCreateJobError = (state) => state.createJob.error; export const isUpdating = (state) => state.updateJob.isUpdating; +export const getCloneJobConfig = (state) => state.cloneJob.job; + export const getJobStatusByJobName = (state, jobName) => { const jobs = getJobs(state); const { status } = jobs[jobName] || {}; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/store/store.js b/x-pack/legacy/plugins/rollup/public/crud_app/store/store.js index 418b781ef6e4d..80198dbbc6473 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/store/store.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/store/store.js @@ -11,10 +11,11 @@ import { rollupJobs } from './reducers'; import { detailPanel, + cloneJob, } from './middleware'; export function createRollupJobsStore(initialState = {}) { - const enhancers = [ applyMiddleware(thunk, detailPanel) ]; + const enhancers = [ applyMiddleware(thunk, detailPanel, cloneJob) ]; window.__REDUX_DEVTOOLS_EXTENSION__ && enhancers.push(window.__REDUX_DEVTOOLS_EXTENSION__()); return createStore( From 25026381ec0da6e92d3b444b939d907aa59abdf2 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Tue, 6 Aug 2019 09:46:00 -0400 Subject: [PATCH 03/26] Adds config-schema nullable composite type (#41728) The `nullable` type is very similar to the `maybe` type, except that it validates nulls and undefined values to null, instead of undefined. Eg, maybe(T): T | undefined nullable(T): T | null --- packages/kbn-config-schema/src/index.ts | 6 ++ .../__snapshots__/maybe_type.test.ts.snap | 2 + .../__snapshots__/nullable_type.test.ts.snap | 7 +++ packages/kbn-config-schema/src/types/index.ts | 1 + .../src/types/maybe_type.test.ts | 6 ++ .../src/types/nullable_type.test.ts | 63 +++++++++++++++++++ .../src/types/nullable_type.ts | 32 ++++++++++ 7 files changed, 117 insertions(+) create mode 100644 packages/kbn-config-schema/src/types/__snapshots__/nullable_type.test.ts.snap create mode 100644 packages/kbn-config-schema/src/types/nullable_type.test.ts create mode 100644 packages/kbn-config-schema/src/types/nullable_type.ts diff --git a/packages/kbn-config-schema/src/index.ts b/packages/kbn-config-schema/src/index.ts index b4308dc3f80d1..4f0a1fc02adf1 100644 --- a/packages/kbn-config-schema/src/index.ts +++ b/packages/kbn-config-schema/src/index.ts @@ -36,6 +36,7 @@ import { MapOfOptions, MapOfType, MaybeType, + NullableType, NeverType, NumberOptions, NumberType, @@ -100,6 +101,10 @@ function maybe(type: Type): Type { return new MaybeType(type); } +function nullable(type: Type): Type { + return new NullableType(type); +} + function object

(props: P, options?: ObjectTypeOptions

): ObjectType

{ return new ObjectType(props, options); } @@ -191,6 +196,7 @@ export const schema = { literal, mapOf, maybe, + nullable, never, number, object, diff --git a/packages/kbn-config-schema/src/types/__snapshots__/maybe_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/maybe_type.test.ts.snap index ba3ac821a97cb..fdb172df356a7 100644 --- a/packages/kbn-config-schema/src/types/__snapshots__/maybe_type.test.ts.snap +++ b/packages/kbn-config-schema/src/types/__snapshots__/maybe_type.test.ts.snap @@ -4,4 +4,6 @@ exports[`fails if null 1`] = `"expected value of type [string] but got [null]"`; exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [string] but got [null]"`; +exports[`validates basic type 1`] = `"expected value of type [string] but got [number]"`; + exports[`validates contained type 1`] = `"value is [foo] but it must have a maximum length of [1]."`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/nullable_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/nullable_type.test.ts.snap new file mode 100644 index 0000000000000..ae1a34c00d3a8 --- /dev/null +++ b/packages/kbn-config-schema/src/types/__snapshots__/nullable_type.test.ts.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`includes namespace in failure 1`] = `"[foo-namespace]: value is [foo] but it must have a maximum length of [1]."`; + +exports[`validates basic type 1`] = `"expected value of type [string] but got [number]"`; + +exports[`validates contained type 1`] = `"value is [foo] but it must have a maximum length of [1]."`; diff --git a/packages/kbn-config-schema/src/types/index.ts b/packages/kbn-config-schema/src/types/index.ts index cfa8cc4b7553d..4c25cade2ec61 100644 --- a/packages/kbn-config-schema/src/types/index.ts +++ b/packages/kbn-config-schema/src/types/index.ts @@ -26,6 +26,7 @@ export { ConditionalType, ConditionalTypeValue } from './conditional_type'; export { DurationOptions, DurationType } from './duration_type'; export { LiteralType } from './literal_type'; export { MaybeType } from './maybe_type'; +export { NullableType } from './nullable_type'; export { MapOfOptions, MapOfType } from './map_type'; export { NumberOptions, NumberType } from './number_type'; export { ObjectType, ObjectTypeOptions, Props, TypeOf } from './object_type'; diff --git a/packages/kbn-config-schema/src/types/maybe_type.test.ts b/packages/kbn-config-schema/src/types/maybe_type.test.ts index b29f504c03b32..ecc1d218e186d 100644 --- a/packages/kbn-config-schema/src/types/maybe_type.test.ts +++ b/packages/kbn-config-schema/src/types/maybe_type.test.ts @@ -45,6 +45,12 @@ test('validates contained type', () => { expect(() => type.validate('foo')).toThrowErrorMatchingSnapshot(); }); +test('validates basic type', () => { + const type = schema.maybe(schema.string()); + + expect(() => type.validate(666)).toThrowErrorMatchingSnapshot(); +}); + test('fails if null', () => { const type = schema.maybe(schema.string()); expect(() => type.validate(null)).toThrowErrorMatchingSnapshot(); diff --git a/packages/kbn-config-schema/src/types/nullable_type.test.ts b/packages/kbn-config-schema/src/types/nullable_type.test.ts new file mode 100644 index 0000000000000..e9d6f3ca2fe35 --- /dev/null +++ b/packages/kbn-config-schema/src/types/nullable_type.test.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '..'; + +test('returns value if specified', () => { + const type = schema.nullable(schema.string()); + expect(type.validate('test')).toEqual('test'); +}); + +test('returns null if null', () => { + const type = schema.nullable(schema.string()); + expect(type.validate(null)).toEqual(null); +}); + +test('returns null if undefined', () => { + const type = schema.nullable(schema.string()); + expect(type.validate(undefined)).toEqual(null); +}); + +test('returns null even if contained type has a default value', () => { + const type = schema.nullable( + schema.string({ + defaultValue: 'abc', + }) + ); + + expect(type.validate(undefined)).toEqual(null); +}); + +test('validates contained type', () => { + const type = schema.nullable(schema.string({ maxLength: 1 })); + + expect(() => type.validate('foo')).toThrowErrorMatchingSnapshot(); +}); + +test('validates basic type', () => { + const type = schema.nullable(schema.string()); + + expect(() => type.validate(666)).toThrowErrorMatchingSnapshot(); +}); + +test('includes namespace in failure', () => { + const type = schema.nullable(schema.string({ maxLength: 1 })); + + expect(() => type.validate('foo', {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); +}); diff --git a/packages/kbn-config-schema/src/types/nullable_type.ts b/packages/kbn-config-schema/src/types/nullable_type.ts new file mode 100644 index 0000000000000..c89f3e44c37c4 --- /dev/null +++ b/packages/kbn-config-schema/src/types/nullable_type.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Type } from './type'; + +export class NullableType extends Type { + constructor(type: Type) { + super( + type + .getSchema() + .optional() + .allow(null) + .default(null) + ); + } +} From 3b739144bc5f2c4c97407205d828757c6d166bd0 Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Tue, 6 Aug 2019 09:33:48 -0500 Subject: [PATCH 04/26] Updating canvas layout flex basis to auto for IE11 (#42544) --- .../canvas/public/apps/workpad/workpad_app/workpad_app.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss index fb53225bdd388..a63d75ce9a7d8 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss @@ -22,7 +22,7 @@ $canvasLayoutFontSize: $euiFontSizeS; .canvasLayout__stage { flex-grow: 1; - flex-basis: 0%; + flex-basis: auto; display: flex; flex-direction: column; } @@ -43,7 +43,7 @@ $canvasLayoutFontSize: $euiFontSizeS; .canvasLayout__stageContent { flex-grow: 1; - flex-basis: 0%; + flex-basis: auto; position: relative; } From e32ae12fea744ee5b2d55253e9bbb53bd4238765 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 6 Aug 2019 08:44:59 -0600 Subject: [PATCH 05/26] [Maps] fix flaky tests caused by missed Add Layer clicks (#42659) * [Maps] fix flaky tests caused by missed Add Layer clicks * unskip add layer import panel tests * fix call to waitForLayerAddPanelClosed --- .../functional/apps/maps/add_layer_panel.js | 65 ++++------ .../import_geojson/add_layer_import_panel.js | 112 ++++++++---------- .../apps/maps/import_geojson/index.js | 3 +- .../test/functional/page_objects/gis_page.js | 20 ++-- 4 files changed, 84 insertions(+), 116 deletions(-) diff --git a/x-pack/test/functional/apps/maps/add_layer_panel.js b/x-pack/test/functional/apps/maps/add_layer_panel.js index 91df973f75248..60a719f046674 100644 --- a/x-pack/test/functional/apps/maps/add_layer_panel.js +++ b/x-pack/test/functional/apps/maps/add_layer_panel.js @@ -11,60 +11,35 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['maps']); describe('Add layer panel', () => { + const LAYER_NAME = 'World Countries'; + before(async () => { await PageObjects.maps.openNewMap(); + await PageObjects.maps.clickAddLayer(); + await PageObjects.maps.selectVectorSource(); + await PageObjects.maps.selectVectorLayer(LAYER_NAME); }); - describe('visibility', () => { - - - //skip for now - //cf. https://github.com/elastic/kibana/issues/42626 - it.skip('should open on clicking "Add layer"', async () => { - await PageObjects.maps.clickAddLayer(); - const panelOpen = await PageObjects.maps.isLayerAddPanelOpen(); - expect(panelOpen).to.be(true); - }); - - it.skip('should close on clicking "Cancel"', async () => { - await PageObjects.maps.cancelLayerAdd(); - const panelOpen = await PageObjects.maps.isLayerAddPanelOpen(); - expect(panelOpen).to.be(false); - }); + it('should show unsaved layer in layer TOC', async () => { + const vectorLayerExists = await PageObjects.maps.doesLayerExist(LAYER_NAME); + expect(vectorLayerExists).to.be(true); }); - describe('with unsaved layer', () => { - const LAYER_NAME = 'World Countries'; + it('should disable Map application save button', async () => { + // saving map should be a no-op because its diabled + await testSubjects.click('mapSaveButton'); - before(async () => { - await PageObjects.maps.clickAddLayer(); - await PageObjects.maps.selectVectorSource(); - await PageObjects.maps.selectVectorLayer(LAYER_NAME); - }); - - it('should show unsaved layer in layer TOC', async () => { - const vectorLayerExists = await PageObjects.maps.doesLayerExist(LAYER_NAME); - expect(vectorLayerExists).to.be(true); - }); - - it('should disable Map application save button', async () => { - // saving map should be a no-op because its diabled - await testSubjects.click('mapSaveButton'); - - const panelOpen = await PageObjects.maps.isLayerAddPanelOpen(); - expect(panelOpen).to.be(true); - const vectorLayerExists = await PageObjects.maps.doesLayerExist(LAYER_NAME); - expect(vectorLayerExists).to.be(true); - }); + const panelOpen = await PageObjects.maps.isLayerAddPanelOpen(); + expect(panelOpen).to.be(true); + const vectorLayerExists = await PageObjects.maps.doesLayerExist(LAYER_NAME); + expect(vectorLayerExists).to.be(true); + }); - it('should close & remove layer on clicking "Cancel"', async () => { - await PageObjects.maps.cancelLayerAdd(LAYER_NAME); + it('should remove layer on cancel', async () => { + await PageObjects.maps.cancelLayerAdd(LAYER_NAME); - const panelOpen = await PageObjects.maps.isLayerAddPanelOpen(); - expect(panelOpen).to.be(false); - const vectorLayerExists = await PageObjects.maps.doesLayerExist(LAYER_NAME); - expect(vectorLayerExists).to.be(false); - }); + const vectorLayerExists = await PageObjects.maps.doesLayerExist(LAYER_NAME); + expect(vectorLayerExists).to.be(false); }); }); } diff --git a/x-pack/test/functional/apps/maps/import_geojson/add_layer_import_panel.js b/x-pack/test/functional/apps/maps/import_geojson/add_layer_import_panel.js index d86c77e1d59b2..1f80be1463325 100644 --- a/x-pack/test/functional/apps/maps/import_geojson/add_layer_import_panel.js +++ b/x-pack/test/functional/apps/maps/import_geojson/add_layer_import_panel.js @@ -14,8 +14,7 @@ export default function ({ getPageObjects }) { const FILE_LOAD_DIR = 'test_upload_files'; const DEFAULT_LOAD_FILE_NAME = 'point.json'; - // FAILING: https://github.com/elastic/kibana/issues/42630 - describe.skip('GeoJSON import layer panel', () => { + describe('GeoJSON import layer panel', () => { before(async () => { await PageObjects.maps.openNewMap(); }); @@ -33,75 +32,66 @@ export default function ({ getPageObjects }) { }); it('should add GeoJSON file to map', async () => { - const layerLoadedInToc = await PageObjects.maps - .doesLayerExist(IMPORT_FILE_PREVIEW_NAME); + const layerLoadedInToc = await PageObjects.maps.doesLayerExist(IMPORT_FILE_PREVIEW_NAME); expect(layerLoadedInToc).to.be(true); - const filePickerLoadedFile = await PageObjects.maps - .hasFilePickerLoadedFile(DEFAULT_LOAD_FILE_NAME); + const filePickerLoadedFile = await PageObjects.maps.hasFilePickerLoadedFile(DEFAULT_LOAD_FILE_NAME); expect(filePickerLoadedFile).to.be(true); }); - it('should close & remove preview layer on clicking "Cancel" after uploading file', - async () => { - await PageObjects.maps.cancelLayerAdd(); - const panelOpen = await PageObjects.maps.isLayerAddPanelOpen(); - expect(panelOpen).to.be(false); + it('should remove layer on cancel', async () => { + await PageObjects.maps.cancelLayerAdd(); - await PageObjects.maps.waitForLayerDeleted(IMPORT_FILE_PREVIEW_NAME); - const layerLoadedInToc = await PageObjects.maps.doesLayerExist(IMPORT_FILE_PREVIEW_NAME); - expect(layerLoadedInToc).to.be(false); - }); + await PageObjects.maps.waitForLayerDeleted(IMPORT_FILE_PREVIEW_NAME); + const layerLoadedInToc = await PageObjects.maps.doesLayerExist(IMPORT_FILE_PREVIEW_NAME); + expect(layerLoadedInToc).to.be(false); + }); - it('should replace layer on input change', - async () => { - // Upload second file - const secondLoadFileName = 'polygon.json'; - await PageObjects.maps.uploadJsonFileForIndexing( - path.join(__dirname, FILE_LOAD_DIR, secondLoadFileName) - ); - await PageObjects.maps.waitForLayersToLoad(); - // Check second file is loaded in file picker - const filePickerLoadedFile = await PageObjects.maps.hasFilePickerLoadedFile(secondLoadFileName); - expect(filePickerLoadedFile).to.be(true); - }); + it('should replace layer on input change', async () => { + // Upload second file + const secondLoadFileName = 'polygon.json'; + await PageObjects.maps.uploadJsonFileForIndexing( + path.join(__dirname, FILE_LOAD_DIR, secondLoadFileName) + ); + await PageObjects.maps.waitForLayersToLoad(); + // Check second file is loaded in file picker + const filePickerLoadedFile = await PageObjects.maps.hasFilePickerLoadedFile(secondLoadFileName); + expect(filePickerLoadedFile).to.be(true); + }); - it('should clear layer on replacement layer load error', - async () => { - // Upload second file - const secondLoadFileName = 'not_json.txt'; - await PageObjects.maps.uploadJsonFileForIndexing( - path.join(__dirname, FILE_LOAD_DIR, secondLoadFileName) - ); - await PageObjects.maps.waitForLayersToLoad(); - // Check second file is loaded in file picker - const filePickerLoadedFile = await PageObjects.maps - .hasFilePickerLoadedFile(secondLoadFileName); - expect(filePickerLoadedFile).to.be(true); - // Check that no file is loaded in layer preview - const layerLoadedInToc = await PageObjects.maps.doesLayerExist(IMPORT_FILE_PREVIEW_NAME); - expect(layerLoadedInToc).to.be(false); - }); + it('should clear layer on replacement layer load error', async () => { + // Upload second file + const secondLoadFileName = 'not_json.txt'; + await PageObjects.maps.uploadJsonFileForIndexing( + path.join(__dirname, FILE_LOAD_DIR, secondLoadFileName) + ); + await PageObjects.maps.waitForLayersToLoad(); + // Check second file is loaded in file picker + const filePickerLoadedFile = await PageObjects.maps.hasFilePickerLoadedFile(secondLoadFileName); + expect(filePickerLoadedFile).to.be(true); + // Check that no file is loaded in layer preview + const layerLoadedInToc = await PageObjects.maps.doesLayerExist(IMPORT_FILE_PREVIEW_NAME); + expect(layerLoadedInToc).to.be(false); + }); - it('should prevent import button from activating unless valid index name provided', - async () => { - // Set index to invalid name - await PageObjects.maps.setIndexName('NoCapitalLetters'); - // Check button - let importButtonActive = await PageObjects.maps.importFileButtonEnabled(); - expect(importButtonActive).to.be(false); + it('should prevent import button from activating unless valid index name provided', async () => { + // Set index to invalid name + await PageObjects.maps.setIndexName('NoCapitalLetters'); + // Check button + let importButtonActive = await PageObjects.maps.importFileButtonEnabled(); + expect(importButtonActive).to.be(false); - // Set index to valid name - await PageObjects.maps.setIndexName('validindexname'); - // Check button - importButtonActive = await PageObjects.maps.importFileButtonEnabled(); - expect(importButtonActive).to.be(true); + // Set index to valid name + await PageObjects.maps.setIndexName('validindexname'); + // Check button + importButtonActive = await PageObjects.maps.importFileButtonEnabled(); + expect(importButtonActive).to.be(true); - // Set index back to invalid name - await PageObjects.maps.setIndexName('?noquestionmarks?'); - // Check button - importButtonActive = await PageObjects.maps.importFileButtonEnabled(); - expect(importButtonActive).to.be(false); - }); + // Set index back to invalid name + await PageObjects.maps.setIndexName('?noquestionmarks?'); + // Check button + importButtonActive = await PageObjects.maps.importFileButtonEnabled(); + expect(importButtonActive).to.be(false); + }); }); } diff --git a/x-pack/test/functional/apps/maps/import_geojson/index.js b/x-pack/test/functional/apps/maps/import_geojson/index.js index ac421599f89ca..ecad6a46a168a 100644 --- a/x-pack/test/functional/apps/maps/import_geojson/index.js +++ b/x-pack/test/functional/apps/maps/import_geojson/index.js @@ -7,7 +7,6 @@ export default function ({ loadTestFile }) { describe('import_geojson', function () { loadTestFile(require.resolve('./add_layer_import_panel')); - // FAILING/FLAKY: https://github.com/elastic/kibana/pull/42638 - //loadTestFile(require.resolve('./file_indexing_panel')); + loadTestFile(require.resolve('./file_indexing_panel')); }); } diff --git a/x-pack/test/functional/page_objects/gis_page.js b/x-pack/test/functional/page_objects/gis_page.js index 0b49cab5ae60b..553ad6edc72d7 100644 --- a/x-pack/test/functional/page_objects/gis_page.js +++ b/x-pack/test/functional/page_objects/gis_page.js @@ -215,14 +215,6 @@ export function GisPageProvider({ getService, getPageObjects }) { return links.length; } - /* - * Layer TOC (table to contents) utility functions - */ - async clickAddLayer() { - log.debug('Click add layer'); - await testSubjects.click('addLayerButton'); - } - async openSetViewPopover() { const isOpen = await testSubjects.exists('mapSetViewForm'); if (!isOpen) { @@ -362,11 +354,23 @@ export function GisPageProvider({ getService, getPageObjects }) { }); } + async clickAddLayer() { + log.debug('Click add layer'); + await retry.try(async () => { + await testSubjects.click('addLayerButton'); + const isOpen = await this.isLayerAddPanelOpen(); + if (!isOpen) { + throw new Error('Add layer panel still not open, trying again.'); + } + }); + } + async cancelLayerAdd(layerName) { log.debug(`Cancel layer add`); const cancelExists = await testSubjects.exists('layerAddCancelButton'); if (cancelExists) { await testSubjects.click('layerAddCancelButton'); + await this.waitForLayerAddPanelClosed(); if (layerName) { await this.waitForLayerDeleted(layerName); } From 85b0f22eb258dbc829e25cbeefb404658158a36b Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Tue, 6 Aug 2019 10:01:44 -0500 Subject: [PATCH 06/26] Fix autocomplete not working (#42183) --- .../canvas/public/components/workpad_header/workpad_header.js | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.js b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.js index 498d17db4e0dc..9c7f3f4e31152 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.js @@ -133,6 +133,7 @@ export class WorkpadHeader extends React.PureComponent { handler={this._keyHandler} targetNodeSelector="body" global + isolate /> )} From ef02bf9e0ee1acb3199016047b3ace61f288655f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Tue, 6 Aug 2019 11:22:59 -0400 Subject: [PATCH 07/26] Make task manager index configurable again (#42394) * Initial work * Fix type check * Accept core API changes * Fix broken tests * Destructure index pattern --- ...edobjectsschema.getconverttoaliasscript.md | 22 +++++++ ...rver.savedobjectsschema.getindexfortype.md | 3 +- ...kibana-plugin-server.savedobjectsschema.md | 3 +- .../migrations/core/build_index_map.test.ts | 58 +++++++++++-------- .../migrations/core/build_index_map.ts | 27 ++++++--- .../migrations/kibana/kibana_migrator.ts | 18 +++--- .../saved_objects/schema/schema.mock.ts | 1 + .../server/saved_objects/schema/schema.ts | 16 ++++- .../saved_objects/service/lib/repository.ts | 9 ++- src/core/server/server.api.md | 4 +- src/es_archiver/lib/indices/kibana_index.js | 1 + .../saved_objects/saved_objects_mixin.js | 4 +- .../legacy/plugins/task_manager/constants.ts | 7 --- x-pack/legacy/plugins/task_manager/index.js | 8 ++- .../plugins/task_manager/task_manager.ts | 1 + .../plugins/task_manager/task_store.test.ts | 7 ++- .../legacy/plugins/task_manager/task_store.ts | 7 ++- 17 files changed, 134 insertions(+), 62 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md delete mode 100644 x-pack/legacy/plugins/task_manager/constants.ts diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md new file mode 100644 index 0000000000000..5baf075463558 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [getConvertToAliasScript](./kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md) + +## SavedObjectsSchema.getConvertToAliasScript() method + +Signature: + +```typescript +getConvertToAliasScript(type: string): string | undefined; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | + +Returns: + +`string | undefined` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getindexfortype.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getindexfortype.md index 3c9b810cfe1a6..ba1c439c8c6b4 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getindexfortype.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getindexfortype.md @@ -7,13 +7,14 @@ Signature: ```typescript -getIndexForType(type: string): string | undefined; +getIndexForType(config: Config, type: string): string | undefined; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | +| config | Config | | | type | string | | Returns: diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.md index 1b9cb2ad94c22..11962007c4c3c 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.md @@ -20,7 +20,8 @@ export declare class SavedObjectsSchema | Method | Modifiers | Description | | --- | --- | --- | -| [getIndexForType(type)](./kibana-plugin-server.savedobjectsschema.getindexfortype.md) | | | +| [getConvertToAliasScript(type)](./kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md) | | | +| [getIndexForType(config, type)](./kibana-plugin-server.savedobjectsschema.getindexfortype.md) | | | | [isHiddenType(type)](./kibana-plugin-server.savedobjectsschema.ishiddentype.md) | | | | [isNamespaceAgnostic(type)](./kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md) | | | diff --git a/src/core/server/saved_objects/migrations/core/build_index_map.test.ts b/src/core/server/saved_objects/migrations/core/build_index_map.test.ts index c4a3319fa22c9..d596ade8f476f 100644 --- a/src/core/server/saved_objects/migrations/core/build_index_map.test.ts +++ b/src/core/server/saved_objects/migrations/core/build_index_map.test.ts @@ -18,16 +18,19 @@ */ import { createIndexMap } from './build_index_map'; +import { ObjectToConfigAdapter } from '../../../config'; +import { SavedObjectsSchema } from '../../schema'; test('mappings without index pattern goes to default index', () => { - const result = createIndexMap( - '.kibana', - { + const result = createIndexMap({ + config: new ObjectToConfigAdapter({}), + kibanaIndexName: '.kibana', + schema: new SavedObjectsSchema({ type1: { isNamespaceAgnostic: false, }, - }, - { + }), + indexMap: { type1: { properties: { field1: { @@ -35,8 +38,8 @@ test('mappings without index pattern goes to default index', () => { }, }, }, - } - ); + }, + }); expect(result).toEqual({ '.kibana': { typeMappings: { @@ -53,15 +56,16 @@ test('mappings without index pattern goes to default index', () => { }); test(`mappings with custom index pattern doesn't go to default index`, () => { - const result = createIndexMap( - '.kibana', - { + const result = createIndexMap({ + config: new ObjectToConfigAdapter({}), + kibanaIndexName: '.kibana', + schema: new SavedObjectsSchema({ type1: { isNamespaceAgnostic: false, indexPattern: '.other_kibana', }, - }, - { + }), + indexMap: { type1: { properties: { field1: { @@ -69,8 +73,8 @@ test(`mappings with custom index pattern doesn't go to default index`, () => { }, }, }, - } - ); + }, + }); expect(result).toEqual({ '.other_kibana': { typeMappings: { @@ -87,16 +91,17 @@ test(`mappings with custom index pattern doesn't go to default index`, () => { }); test('creating a script gets added to the index pattern', () => { - const result = createIndexMap( - '.kibana', - { + const result = createIndexMap({ + config: new ObjectToConfigAdapter({}), + kibanaIndexName: '.kibana', + schema: new SavedObjectsSchema({ type1: { isNamespaceAgnostic: false, indexPattern: '.other_kibana', convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, }, - }, - { + }), + indexMap: { type1: { properties: { field1: { @@ -104,8 +109,8 @@ test('creating a script gets added to the index pattern', () => { }, }, }, - } - ); + }, + }); expect(result).toEqual({ '.other_kibana': { script: `ctx._id = ctx._source.type + ':' + ctx._id`, @@ -124,7 +129,7 @@ test('creating a script gets added to the index pattern', () => { test('throws when two scripts are defined for an index pattern', () => { const defaultIndex = '.kibana'; - const savedObjectSchemas = { + const schema = new SavedObjectsSchema({ type1: { isNamespaceAgnostic: false, convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, @@ -133,7 +138,7 @@ test('throws when two scripts are defined for an index pattern', () => { isNamespaceAgnostic: false, convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, }, - }; + }); const indexMap = { type1: { properties: { @@ -151,7 +156,12 @@ test('throws when two scripts are defined for an index pattern', () => { }, }; expect(() => - createIndexMap(defaultIndex, savedObjectSchemas, indexMap) + createIndexMap({ + config: new ObjectToConfigAdapter({}), + kibanaIndexName: defaultIndex, + schema, + indexMap, + }) ).toThrowErrorMatchingInlineSnapshot( `"convertToAliasScript has been defined more than once for index pattern \\".kibana\\""` ); diff --git a/src/core/server/saved_objects/migrations/core/build_index_map.ts b/src/core/server/saved_objects/migrations/core/build_index_map.ts index e08bc9e972767..d7a26b7728f44 100644 --- a/src/core/server/saved_objects/migrations/core/build_index_map.ts +++ b/src/core/server/saved_objects/migrations/core/build_index_map.ts @@ -17,8 +17,16 @@ * under the License. */ +import { Config } from '../../../config'; import { MappingProperties } from '../../mappings'; -import { SavedObjectsSchemaDefinition } from '../../schema'; +import { SavedObjectsSchema } from '../../schema'; + +export interface CreateIndexMapOptions { + config: Config; + kibanaIndexName: string; + schema: SavedObjectsSchema; + indexMap: MappingProperties; +} export interface IndexMap { [index: string]: { @@ -30,16 +38,17 @@ export interface IndexMap { /* * This file contains logic to convert savedObjectSchemas into a dictonary of indexes and documents */ -export function createIndexMap( - defaultIndex: string, - savedObjectSchemas: SavedObjectsSchemaDefinition, - indexMap: MappingProperties -) { +export function createIndexMap({ + config, + kibanaIndexName, + schema, + indexMap, +}: CreateIndexMapOptions) { const map: IndexMap = {}; Object.keys(indexMap).forEach(type => { - const schema = savedObjectSchemas[type] || {}; - const script = schema.convertToAliasScript; - const indexPattern = schema.indexPattern || defaultIndex; + const script = schema.getConvertToAliasScript(type); + // Defaults to kibanaIndexName if indexPattern isn't defined + const indexPattern = schema.getIndexForType(config, type) || kibanaIndexName; if (!map.hasOwnProperty(indexPattern as string)) { map[indexPattern] = { typeMappings: {} }; } diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index 0c2cb768fc011..78a8507e0c41d 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -31,6 +31,7 @@ import { docValidator } from '../../validation'; import { buildActiveMappings, CallCluster, IndexMigrator, LogFn } from '../core'; import { DocumentMigrator, VersionedTransformer } from '../core/document_migrator'; import { createIndexMap } from '../core/build_index_map'; +import { Config } from '../../../config'; export interface KbnServer { server: Server; version: string; @@ -92,13 +93,14 @@ export class KibanaMigrator { // Wait until elasticsearch is green... await server.plugins.elasticsearch.waitUntilReady(); - const config = server.config(); + const config = server.config() as Config; const kibanaIndexName = config.get('kibana.index'); - const indexMap = createIndexMap( + const indexMap = createIndexMap({ + config, kibanaIndexName, - this.kbnServer.uiExports.savedObjectSchemas, - this.mappingProperties - ); + indexMap: this.mappingProperties, + schema: this.schema, + }); const migrators = Object.keys(indexMap).map(index => { return new IndexMigrator({ @@ -130,6 +132,7 @@ export class KibanaMigrator { private mappingProperties: MappingProperties; private log: LogFn; private serializer: SavedObjectsSerializer; + private readonly schema: SavedObjectsSchema; /** * Creates an instance of KibanaMigrator. @@ -141,9 +144,8 @@ export class KibanaMigrator { constructor({ kbnServer }: { kbnServer: KbnServer }) { this.kbnServer = kbnServer; - this.serializer = new SavedObjectsSerializer( - new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas) - ); + this.schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas); + this.serializer = new SavedObjectsSerializer(this.schema); this.mappingProperties = mergeProperties(kbnServer.uiExports.savedObjectMappings || []); diff --git a/src/core/server/saved_objects/schema/schema.mock.ts b/src/core/server/saved_objects/schema/schema.mock.ts index 7fec7d54294d6..89c318f087ab0 100644 --- a/src/core/server/saved_objects/schema/schema.mock.ts +++ b/src/core/server/saved_objects/schema/schema.mock.ts @@ -25,6 +25,7 @@ const createSchemaMock = () => { getIndexForType: jest.fn().mockReturnValue('.kibana-test'), isHiddenType: jest.fn().mockReturnValue(false), isNamespaceAgnostic: jest.fn((type: string) => type === 'global'), + getConvertToAliasScript: jest.fn().mockReturnValue(undefined), }; return mocked; }; diff --git a/src/core/server/saved_objects/schema/schema.ts b/src/core/server/saved_objects/schema/schema.ts index 1f098d0b6e21d..09676fb504012 100644 --- a/src/core/server/saved_objects/schema/schema.ts +++ b/src/core/server/saved_objects/schema/schema.ts @@ -16,10 +16,13 @@ * specific language governing permissions and limitations * under the License. */ + +import { Config } from '../../config'; + interface SavedObjectsSchemaTypeDefinition { isNamespaceAgnostic: boolean; hidden?: boolean; - indexPattern?: string; + indexPattern?: ((config: Config) => string) | string; convertToAliasScript?: string; } @@ -41,14 +44,21 @@ export class SavedObjectsSchema { return false; } - public getIndexForType(type: string): string | undefined { + public getIndexForType(config: Config, type: string): string | undefined { if (this.definition != null && this.definition.hasOwnProperty(type)) { - return this.definition[type].indexPattern; + const { indexPattern } = this.definition[type]; + return typeof indexPattern === 'function' ? indexPattern(config) : indexPattern; } else { return undefined; } } + public getConvertToAliasScript(type: string): string | undefined { + if (this.definition != null && this.definition.hasOwnProperty(type)) { + return this.definition[type].convertToAliasScript; + } + } + public isNamespaceAgnostic(type: string) { // if no plugins have registered a uiExports.savedObjectSchemas, // this.schema will be undefined, and no types are namespace agnostic diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index eb41df3a19d2d..35b5eedb2da6e 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -27,6 +27,7 @@ import { SavedObjectsErrorHelpers } from './errors'; import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version'; import { SavedObjectsSchema } from '../../schema'; import { KibanaMigrator } from '../../migrations'; +import { Config } from '../../../config'; import { SavedObjectsSerializer, SanitizedSavedObjectDoc, RawDoc } from '../../serialization'; import { SavedObject, @@ -64,6 +65,7 @@ const isLeft = (either: Either): either is Left => { export interface SavedObjectsRepositoryOptions { index: string; + config: Config; mappings: IndexMapping; callCluster: CallCluster; schema: SavedObjectsSchema; @@ -80,6 +82,7 @@ export interface IncrementCounterOptions extends SavedObjectsBaseOptions { export class SavedObjectsRepository { private _migrator: KibanaMigrator; private _index: string; + private _config: Config; private _mappings: IndexMapping; private _schema: SavedObjectsSchema; private _allowedTypes: string[]; @@ -90,6 +93,7 @@ export class SavedObjectsRepository { constructor(options: SavedObjectsRepositoryOptions) { const { index, + config, mappings, callCluster, schema, @@ -108,6 +112,7 @@ export class SavedObjectsRepository { // to returning them. this._migrator = migrator; this._index = index; + this._config = config; this._mappings = mappings; this._schema = schema; if (allowedTypes.length === 0) { @@ -741,7 +746,7 @@ export class SavedObjectsRepository { * @param type - the type */ private getIndexForType(type: string) { - return this._schema.getIndexForType(type) || this._index; + return this._schema.getIndexForType(this._config, type) || this._index; } /** @@ -753,7 +758,7 @@ export class SavedObjectsRepository { */ private getIndicesForTypes(types: string[]) { const unique = (array: string[]) => [...new Set(array)]; - return unique(types.map(t => this._schema.getIndexForType(t) || this._index)); + return unique(types.map(t => this._schema.getIndexForType(this._config, t) || this._index)); } private _getCurrentTime() { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 94bed2ad01e64..74624020fe79f 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -900,7 +900,9 @@ export class SavedObjectsSchema { // Warning: (ae-forgotten-export) The symbol "SavedObjectsSchemaDefinition" needs to be exported by the entry point index.d.ts constructor(schemaDefinition?: SavedObjectsSchemaDefinition); // (undocumented) - getIndexForType(type: string): string | undefined; + getConvertToAliasScript(type: string): string | undefined; + // (undocumented) + getIndexForType(config: Config, type: string): string | undefined; // (undocumented) isHiddenType(type: string): boolean; // (undocumented) diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.js index 3cbc5375e71be..335e2acf23933 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.js @@ -83,6 +83,7 @@ export async function migrateKibanaIndex({ client, log, kibanaPluginIds }) { 'migrations.scrollDuration': '5m', 'migrations.batchSize': 100, 'migrations.pollInterval': 100, + 'xpack.task_manager.index': '.kibana_task_manager', }; const ready = async () => undefined; const elasticsearch = { diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js index d6ca2698395f3..924bea1559073 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.js @@ -114,9 +114,11 @@ export function savedObjectsMixin(kbnServer, server) { }); const combinedTypes = visibleTypes.concat(extraTypes); const allowedTypes = [...new Set(combinedTypes)]; + const config = server.config(); return new SavedObjectsRepository({ - index: server.config().get('kibana.index'), + index: config.get('kibana.index'), + config, migrator, mappings, schema, diff --git a/x-pack/legacy/plugins/task_manager/constants.ts b/x-pack/legacy/plugins/task_manager/constants.ts deleted file mode 100644 index 1b0d0b001071c..0000000000000 --- a/x-pack/legacy/plugins/task_manager/constants.ts +++ /dev/null @@ -1,7 +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. - */ - -export const TASK_MANAGER_INDEX = '.kibana_task_manager'; diff --git a/x-pack/legacy/plugins/task_manager/index.js b/x-pack/legacy/plugins/task_manager/index.js index ba92e9db50601..2ab880938c792 100644 --- a/x-pack/legacy/plugins/task_manager/index.js +++ b/x-pack/legacy/plugins/task_manager/index.js @@ -8,7 +8,6 @@ import { SavedObjectsSerializer, SavedObjectsSchema } from '../../../../src/core import { TaskManager } from './task_manager'; import mappings from './mappings.json'; import { migrations } from './migrations'; -import { TASK_MANAGER_INDEX } from './constants'; export function taskManager(kibana) { return new kibana.Plugin({ @@ -26,6 +25,9 @@ export function taskManager(kibana) { .description('How often, in milliseconds, the task manager will look for more work.') .min(1000) .default(3000), + index: Joi.string() + .description('The name of the index used to store task information.') + .default('.kibana_task_manager'), max_workers: Joi.number() .description('The maximum number of tasks that this Kibana instance will run simultaneously.') .min(1) // disable the task manager rather than trying to specify it with 0 workers @@ -61,8 +63,10 @@ export function taskManager(kibana) { task: { hidden: true, isNamespaceAgnostic: true, - indexPattern: TASK_MANAGER_INDEX, convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, + indexPattern(config) { + return config.get('xpack.task_manager.index'); + }, }, }, }, diff --git a/x-pack/legacy/plugins/task_manager/task_manager.ts b/x-pack/legacy/plugins/task_manager/task_manager.ts index dfd57a0bdbb9c..445a359e12dfb 100644 --- a/x-pack/legacy/plugins/task_manager/task_manager.ts +++ b/x-pack/legacy/plugins/task_manager/task_manager.ts @@ -68,6 +68,7 @@ export class TaskManager { serializer: opts.serializer, savedObjectsRepository: opts.savedObjectsRepository, callCluster: server.plugins.elasticsearch.getCluster('admin').callWithInternalUser, + index: opts.config.get('xpack.task_manager.index'), maxAttempts: opts.config.get('xpack.task_manager.max_attempts'), definitions: this.definitions, }); diff --git a/x-pack/legacy/plugins/task_manager/task_store.test.ts b/x-pack/legacy/plugins/task_manager/task_store.test.ts index 1d50ec79a150b..ccc7322682f4c 100644 --- a/x-pack/legacy/plugins/task_manager/task_store.test.ts +++ b/x-pack/legacy/plugins/task_manager/task_store.test.ts @@ -62,6 +62,7 @@ describe('TaskStore', () => { }) ); const store = new TaskStore({ + index: 'tasky', serializer, callCluster, maxAttempts: 2, @@ -160,6 +161,7 @@ describe('TaskStore', () => { async function testFetch(opts?: FetchOpts, hits: any[] = []) { const callCluster = sinon.spy(async () => ({ hits: { hits } })); const store = new TaskStore({ + index: 'tasky', serializer, callCluster, maxAttempts: 2, @@ -181,7 +183,7 @@ describe('TaskStore', () => { test('empty call filters by type, sorts by runAt and id', async () => { const { args } = await testFetch(); expect(args).toMatchObject({ - index: '.kibana_task_manager', + index: 'tasky', body: { sort: [{ 'task.runAt': 'asc' }, { _id: 'desc' }], query: { term: { type: 'task' } }, @@ -350,6 +352,7 @@ describe('TaskStore', () => { test('it returns normally with no tasks when the index does not exist.', async () => { const callCluster = sinon.spy(async () => ({ hits: { hits: [] } })); const store = new TaskStore({ + index: 'tasky', serializer, callCluster, definitions: taskDefinitions, @@ -580,6 +583,7 @@ describe('TaskStore', () => { ); const store = new TaskStore({ + index: 'tasky', serializer, callCluster: jest.fn(), maxAttempts: 2, @@ -626,6 +630,7 @@ describe('TaskStore', () => { const id = `id-${_.random(1, 20)}`; const callCluster = jest.fn(); const store = new TaskStore({ + index: 'tasky', serializer, callCluster, maxAttempts: 2, diff --git a/x-pack/legacy/plugins/task_manager/task_store.ts b/x-pack/legacy/plugins/task_manager/task_store.ts index 79ad72259c996..84b81aef687f0 100644 --- a/x-pack/legacy/plugins/task_manager/task_store.ts +++ b/x-pack/legacy/plugins/task_manager/task_store.ts @@ -23,10 +23,10 @@ import { TaskDictionary, TaskInstance, } from './task'; -import { TASK_MANAGER_INDEX } from './constants'; export interface StoreOpts { callCluster: ElasticJs; + index: string; maxAttempts: number; definitions: TaskDictionary; savedObjectsRepository: SavedObjectsClientContract; @@ -50,6 +50,7 @@ export interface FetchResult { */ export class TaskStore { public readonly maxAttempts: number; + public readonly index: string; private callCluster: ElasticJs; private definitions: TaskDictionary; private savedObjectsRepository: SavedObjectsClientContract; @@ -59,6 +60,7 @@ export class TaskStore { * Constructs a new TaskStore. * @param {StoreOpts} opts * @prop {CallCluster} callCluster - The elastic search connection + * @prop {string} index - The name of the task manager index * @prop {number} maxAttempts - The maximum number of attempts before a task will be abandoned * @prop {TaskDefinition} definition - The definition of the task being run * @prop {serializer} - The saved object serializer @@ -66,6 +68,7 @@ export class TaskStore { */ constructor(opts: StoreOpts) { this.callCluster = opts.callCluster; + this.index = opts.index; this.maxAttempts = opts.maxAttempts; this.definitions = opts.definitions; this.serializer = opts.serializer; @@ -229,7 +232,7 @@ export class TaskStore { : queryOnlyTasks; const result = await this.callCluster('search', { - index: TASK_MANAGER_INDEX, + index: this.index, ignoreUnavailable: true, body: { ...opts, From c571d2fab3bc6f65d0aeebde9a4780516e85543f Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 6 Aug 2019 08:48:48 -0700 Subject: [PATCH 08/26] Fix flakiness in request lib tests (#42647) * Remove flakiness from 'pollIntervalMs' test by making timing irrelevant. * Remove flakiness from 'state' tests by increasing wait time. * Fix flakiness with 'resets the pollIntervalMs' test by increasing the wait time. --- .../public/request/request.test.js | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/src/plugins/es_ui_shared/public/request/request.test.js b/src/plugins/es_ui_shared/public/request/request.test.js index 03bed758b1209..60b05a8dc8a27 100644 --- a/src/plugins/es_ui_shared/public/request/request.test.js +++ b/src/plugins/es_ui_shared/public/request/request.test.js @@ -98,20 +98,19 @@ describe('request lib', () => { describe('path, method, body', () => { it('is used to send the request', async () => { initUseRequest({ ...successRequest }); - await wait(10); + await wait(50); expect(hook.data).toBe(successResponse.data); }); }); - // FLAKY: https://github.com/elastic/kibana/issues/42561 - describe.skip('pollIntervalMs', () => { + describe('pollIntervalMs', () => { it('sends another request after the specified time has elapsed', async () => { - initUseRequest({ ...successRequest, pollIntervalMs: 30 }); - await wait(5); - sinon.assert.calledOnce(sendPost); - - await wait(40); - sinon.assert.calledTwice(sendPost); + initUseRequest({ ...successRequest, pollIntervalMs: 10 }); + await wait(50); + // We just care that multiple requests have been sent out. We don't check the specific + // timing because that risks introducing flakiness into the tests, and it's unlikely + // we could break the implementation by getting the exact timing wrong. + expect(sendPost.callCount).toBeGreaterThan(1); // We have to manually clean up or else the interval will continue to fire requests, // interfering with other tests. @@ -132,14 +131,14 @@ describe('request lib', () => { initUseRequest({ ...successRequest, deserializer }); sinon.assert.notCalled(deserializer); - await wait(5); + await wait(50); sinon.assert.calledOnce(deserializer); sinon.assert.calledWith(deserializer, successResponse.data); }); it('processes data', async () => { initUseRequest({ ...successRequest, deserializer: () => 'intercepted' }); - await wait(5); + await wait(50); expect(hook.data).toBe('intercepted'); }); }); @@ -152,7 +151,7 @@ describe('request lib', () => { expect(hook.isInitialRequest).toBe(true); hook.sendRequest(); - await wait(5); + await wait(50); expect(hook.isInitialRequest).toBe(false); }); }); @@ -162,7 +161,7 @@ describe('request lib', () => { initUseRequest({ ...successRequest }); expect(hook.isLoading).toBe(true); - await wait(5); + await wait(50); expect(hook.isLoading).toBe(false); }); }); @@ -170,14 +169,13 @@ describe('request lib', () => { describe('error', () => { it('surfaces errors from requests', async () => { initUseRequest({ ...errorRequest }); - await wait(10); + await wait(50); expect(hook.error).toBe(errorResponse); }); - // FLAKY: https://github.com/elastic/kibana/issues/42563 - it.skip('persists while a request is in-flight', async () => { + it('persists while a request is in-flight', async () => { initUseRequest({ ...errorRequest }); - await wait(5); + await wait(50); hook.sendRequest(); expect(hook.isLoading).toBe(true); expect(hook.error).toBe(errorResponse); @@ -185,7 +183,7 @@ describe('request lib', () => { it('is undefined when the request is successful', async () => { initUseRequest({ ...successRequest }); - await wait(10); + await wait(50); expect(hook.isLoading).toBe(false); expect(hook.error).toBeUndefined(); }); @@ -194,30 +192,28 @@ describe('request lib', () => { describe('data', () => { it('surfaces payloads from requests', async () => { initUseRequest({ ...successRequest }); - await wait(10); + await wait(50); expect(hook.data).toBe(successResponse.data); }); it('persists while a request is in-flight', async () => { initUseRequest({ ...successRequest }); - await wait(5); + await wait(50); hook.sendRequest(); expect(hook.isLoading).toBe(true); expect(hook.data).toBe(successResponse.data); }); - // FLAKY: https://github.com/elastic/kibana/issues/42562 - it.skip('is undefined when the request fails', async () => { + it('is undefined when the request fails', async () => { initUseRequest({ ...errorRequest }); - await wait(10); + await wait(50); expect(hook.isLoading).toBe(false); expect(hook.data).toBeUndefined(); }); }); }); - // FLAKY: https://github.com/elastic/kibana/issues/42225 - describe.skip('callbacks', () => { + describe('callbacks', () => { describe('sendRequest', () => { it('sends the request', () => { initUseRequest({ ...successRequest }); @@ -227,19 +223,26 @@ describe('request lib', () => { }); it('resets the pollIntervalMs', async () => { - initUseRequest({ ...successRequest, pollIntervalMs: 30 }); - await wait(5); - sinon.assert.calledOnce(sendPost); + initUseRequest({ ...successRequest, pollIntervalMs: 800 }); + await wait(200); // 200ms + hook.sendRequest(); + expect(sendPost.callCount).toBe(2); - await wait(20); + await wait(200); // 400ms hook.sendRequest(); - // If the request didn't reset the interval, there would have been three requests sent by now. - await wait(20); - sinon.assert.calledTwice(sendPost); + await wait(200); // 600ms + hook.sendRequest(); + + await wait(200); // 800ms + hook.sendRequest(); + + await wait(200); // 1000ms + hook.sendRequest(); - await wait(20); - sinon.assert.calledThrice(sendPost); + // If sendRequest didn't reset the interval, the interval would have triggered another + // request by now, and the callCount would be 7. + expect(sendPost.callCount).toBe(6); // We have to manually clean up or else the interval will continue to fire requests, // interfering with other tests. From 98b445a6e031a42e3d9c244718b7b2e2aa1ca9d2 Mon Sep 17 00:00:00 2001 From: Bart Van Remortele Date: Tue, 6 Aug 2019 18:42:57 +0200 Subject: [PATCH 09/26] [APM] Transaction duration chart always shows duration in `ms` (#42375) * make the transaction chart use the appropriate unit depending on the max value * fix TimeFormatter type not being imported --- .../shared/charts/TransactionCharts/index.tsx | 41 +++++++++++++++---- .../plugins/apm/public/utils/formatters.ts | 11 +++-- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index 76eb7816687dc..5130161e21b11 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -16,17 +16,22 @@ import { import { i18n } from '@kbn/i18n'; import { Location } from 'history'; import React, { Component } from 'react'; -import { isEmpty } from 'lodash'; +import { isEmpty, flatten } from 'lodash'; import styled from 'styled-components'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { Coordinate } from '../../../../../typings/timeseries'; +import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; import { ITransactionChartData } from '../../../../selectors/chartSelectors'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { asInteger, asMillis, tpmUnit } from '../../../../utils/formatters'; +import { + asInteger, + tpmUnit, + TimeFormatter +} from '../../../../utils/formatters'; import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink'; import { LicenseContext } from '../../../../context/LicenseContext'; import { TransactionLineChart } from './TransactionLineChart'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; +import { getTimeFormatter } from '../../../../utils/formatters'; interface TransactionChartProps { hasMLJob: boolean; @@ -48,12 +53,26 @@ const ShiftedEuiText = styled(EuiText)` `; export class TransactionCharts extends Component { - public getResponseTimeTickFormatter = (t: number) => { - return asMillis(t); + public getMaxY = (responseTimeSeries: TimeSeries[]) => { + const coordinates = flatten( + responseTimeSeries.map((serie: TimeSeries) => serie.data as Coordinate[]) + ); + + const numbers: number[] = coordinates.map((c: Coordinate) => + c.y ? c.y : 0 + ); + + return Math.max(...numbers, 0); + }; + + public getResponseTimeTickFormatter = (formatter: TimeFormatter) => { + return (t: number) => formatter(t); }; - public getResponseTimeTooltipFormatter = (p: Coordinate) => { - return isValidCoordinateValue(p.y) ? asMillis(p.y) : NOT_AVAILABLE_LABEL; + public getResponseTimeTooltipFormatter = (formatter: TimeFormatter) => { + return (p: Coordinate) => { + return isValidCoordinateValue(p.y) ? formatter(p.y) : NOT_AVAILABLE_LABEL; + }; }; public getTPMFormatter = (t: number) => { @@ -126,6 +145,8 @@ export class TransactionCharts extends Component { const { charts, urlParams } = this.props; const { responseTimeSeries, tpmSeries } = charts; const { transactionType } = urlParams; + const maxY = this.getMaxY(responseTimeSeries); + const formatter = getTimeFormatter(maxY); return ( @@ -146,8 +167,10 @@ export class TransactionCharts extends Component { diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters.ts b/x-pack/legacy/plugins/apm/public/utils/formatters.ts index fa144d2f64276..d2e82dda35c53 100644 --- a/x-pack/legacy/plugins/apm/public/utils/formatters.ts +++ b/x-pack/legacy/plugins/apm/public/utils/formatters.ts @@ -108,11 +108,14 @@ export function asMicros( return `${formatted}${withUnit ? microsLabel : ''}`; } -type TimeFormatter = ( - max: number -) => (value: FormatterValue, options: FormatterOptions) => string; +export type TimeFormatter = ( + value: FormatterValue, + options?: FormatterOptions +) => string; + +type TimeFormatterBuilder = (max: number) => TimeFormatter; -export const getTimeFormatter: TimeFormatter = memoize((max: number) => { +export const getTimeFormatter: TimeFormatterBuilder = memoize((max: number) => { const unit = timeUnit(max); switch (unit) { case 'h': From 5192dac0b6c027bc7bd489f1559f997e1f6c8961 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Tue, 6 Aug 2019 12:24:49 -0500 Subject: [PATCH 10/26] Add ContextService to server (#42395) --- .../kibana-plugin-public.contextsetup.md | 8 +- .../core/public/kibana-plugin-public.md | 1 + .../kibana-plugin-public.pluginopaqueid.md | 12 ++ ...ver.contextsetup.createcontextcontainer.md | 17 ++ .../kibana-plugin-server.contextsetup.md | 78 ++++++++ .../kibana-plugin-server.coresetup.context.md | 13 ++ .../server/kibana-plugin-server.coresetup.md | 1 + .../core/server/kibana-plugin-server.md | 2 + ...-plugin-server.plugininitializercontext.md | 1 + ...erver.plugininitializercontext.opaqueid.md | 11 ++ .../kibana-plugin-server.pluginopaqueid.md | 12 ++ .../public/context/context_service.mock.ts | 2 +- .../context/context_service.test.mocks.ts | 4 +- .../public/context/context_service.test.ts | 2 +- src/core/public/context/context_service.ts | 12 +- src/core/public/context/index.ts | 2 +- src/core/public/core_system.ts | 4 +- src/core/public/index.ts | 3 +- src/core/public/plugins/index.ts | 3 +- src/core/public/plugins/plugin.ts | 5 +- src/core/public/plugins/plugin_context.ts | 4 +- src/core/public/plugins/plugins_service.ts | 4 +- src/core/public/public.api.md | 4 +- .../server/context/context_service.mock.ts | 42 +++++ .../context/context_service.test.mocks.ts | 25 +++ .../server/context/context_service.test.ts | 37 ++++ src/core/server/context/context_service.ts | 120 ++++++++++++ src/core/server/context/index.ts | 21 +++ src/core/server/core_context.ts | 4 + .../elasticsearch_service.test.ts | 2 +- src/core/server/http/http_service.test.ts | 16 +- src/core/server/index.ts | 9 +- src/core/server/legacy/legacy_service.test.ts | 62 ++++++- src/core/server/mocks.ts | 3 + .../discovery/plugin_manifest_parser.ts | 2 +- .../discovery/plugins_discovery.test.ts | 1 + .../plugins/discovery/plugins_discovery.ts | 8 +- src/core/server/plugins/index.ts | 10 +- src/core/server/plugins/plugin.test.ts | 159 ++++++++++------ src/core/server/plugins/plugin.ts | 173 +++-------------- src/core/server/plugins/plugin_context.ts | 26 +-- .../server/plugins/plugins_service.mock.ts | 1 + .../server/plugins/plugins_service.test.ts | 146 ++++++++------- src/core/server/plugins/plugins_service.ts | 18 +- .../server/plugins/plugins_system.test.ts | 39 +++- src/core/server/plugins/plugins_system.ts | 22 ++- src/core/server/plugins/types.ts | 175 ++++++++++++++++++ src/core/server/server.api.md | 25 ++- src/core/server/server.ts | 11 +- src/core/server/types.ts | 22 +++ .../{public/context => utils}/context.mock.ts | 0 .../{public/context => utils}/context.test.ts | 2 +- src/core/{public/context => utils}/context.ts | 5 +- src/core/utils/index.ts | 5 +- test/tsconfig.json | 1 + 55 files changed, 1028 insertions(+), 369 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.pluginopaqueid.md create mode 100644 docs/development/core/server/kibana-plugin-server.contextsetup.createcontextcontainer.md create mode 100644 docs/development/core/server/kibana-plugin-server.contextsetup.md create mode 100644 docs/development/core/server/kibana-plugin-server.coresetup.context.md create mode 100644 docs/development/core/server/kibana-plugin-server.plugininitializercontext.opaqueid.md create mode 100644 docs/development/core/server/kibana-plugin-server.pluginopaqueid.md create mode 100644 src/core/server/context/context_service.mock.ts create mode 100644 src/core/server/context/context_service.test.mocks.ts create mode 100644 src/core/server/context/context_service.test.ts create mode 100644 src/core/server/context/context_service.ts create mode 100644 src/core/server/context/index.ts create mode 100644 src/core/server/plugins/types.ts create mode 100644 src/core/server/types.ts rename src/core/{public/context => utils}/context.mock.ts (100%) rename src/core/{public/context => utils}/context.test.ts (99%) rename src/core/{public/context => utils}/context.ts (98%) diff --git a/docs/development/core/public/kibana-plugin-public.contextsetup.md b/docs/development/core/public/kibana-plugin-public.contextsetup.md index 43b4042d04ed6..d9c158fcaae05 100644 --- a/docs/development/core/public/kibana-plugin-public.contextsetup.md +++ b/docs/development/core/public/kibana-plugin-public.contextsetup.md @@ -95,8 +95,10 @@ export type VizRenderer = (context: VizRenderContext, domElement: HTMLElement) = class VizRenderingPlugin { private readonly vizRenderers = new Map () => void)>(); + constructor(private readonly initContext: PluginInitializerContext) {} + setup(core) { - this.contextContainer = core.createContextContainer< + this.contextContainer = core.context.createContextContainer< VizRenderContext, ReturnType, [HTMLElement] @@ -110,8 +112,8 @@ class VizRenderingPlugin { } start(core) { - // Register the core context available to all renderers. Use the VizRendererContext's pluginId as the first arg. - this.contextContainer.registerContext('viz_rendering', 'core', () => ({ + // Register the core context available to all renderers. Use the VizRendererContext's opaqueId as the first arg. + this.contextContainer.registerContext(this.initContext.opaqueId, 'core', () => ({ i18n: core.i18n, uiSettings: core.uiSettings })); diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 8da53487d5e7a..afd22ecf174b4 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -74,6 +74,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IContextHandler](./kibana-plugin-public.icontexthandler.md) | A function registered by a plugin to perform some action. | | [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | | [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | +| [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md) | | | [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | | | [ToastInput](./kibana-plugin-public.toastinput.md) | | | [UiSettingsClientContract](./kibana-plugin-public.uisettingsclientcontract.md) | [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | diff --git a/docs/development/core/public/kibana-plugin-public.pluginopaqueid.md b/docs/development/core/public/kibana-plugin-public.pluginopaqueid.md new file mode 100644 index 0000000000000..8a8202ae1334e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.pluginopaqueid.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md) + +## PluginOpaqueId type + + +Signature: + +```typescript +export declare type PluginOpaqueId = symbol; +``` diff --git a/docs/development/core/server/kibana-plugin-server.contextsetup.createcontextcontainer.md b/docs/development/core/server/kibana-plugin-server.contextsetup.createcontextcontainer.md new file mode 100644 index 0000000000000..e028e1391f284 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.contextsetup.createcontextcontainer.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ContextSetup](./kibana-plugin-server.contextsetup.md) > [createContextContainer](./kibana-plugin-server.contextsetup.createcontextcontainer.md) + +## ContextSetup.createContextContainer() method + +Creates a new for a service owner. + +Signature: + +```typescript +createContextContainer(): IContextContainer; +``` +Returns: + +`IContextContainer` + diff --git a/docs/development/core/server/kibana-plugin-server.contextsetup.md b/docs/development/core/server/kibana-plugin-server.contextsetup.md new file mode 100644 index 0000000000000..972266583e336 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.contextsetup.md @@ -0,0 +1,78 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ContextSetup](./kibana-plugin-server.contextsetup.md) + +## ContextSetup interface + +Signature: + +```typescript +export interface ContextSetup +``` + +## Methods + +| Method | Description | +| --- | --- | +| [createContextContainer()](./kibana-plugin-server.contextsetup.createcontextcontainer.md) | Creates a new for a service owner. | + +## Example + +Say we're creating a plugin for rendering visualizations that allows new rendering methods to be registered. If we want to offer context to these rendering methods, we can leverage the ContextService to manage these contexts. + +```ts +export interface VizRenderContext { + core: { + i18n: I18nStart; + uiSettings: UISettingsClientContract; + } + [contextName: string]: unknown; +} + +export type VizRenderer = (context: VizRenderContext, domElement: HTMLElement) => () => void; + +class VizRenderingPlugin { + private readonly vizRenderers = new Map () => void)>(); + + constructor(private readonly initContext: PluginInitializerContext) {} + + setup(core) { + this.contextContainer = core.context.createContextContainer< + VizRenderContext, + ReturnType, + [HTMLElement] + >(); + + return { + registerContext: this.contextContainer.registerContext, + registerVizRenderer: (plugin: PluginOpaqueId, renderMethod: string, renderer: VizTypeRenderer) => + this.vizRenderers.set(renderMethod, this.contextContainer.createHandler(plugin, renderer)), + }; + } + + start(core) { + // Register the core context available to all renderers. Use the VizRendererContext's opaqueId as the first arg. + this.contextContainer.registerContext(this.initContext.opaqueId, 'core', () => ({ + i18n: core.i18n, + uiSettings: core.uiSettings + })); + + return { + registerContext: this.contextContainer.registerContext, + + renderVizualization: (renderMethod: string, domElement: HTMLElement) => { + if (!this.vizRenderer.has(renderMethod)) { + throw new Error(`Render method '${renderMethod}' has not been registered`); + } + + // The handler can now be called directly with only an `HTMLElement` and will automatically + // have a new `context` object created and populated by the context container. + const handler = this.vizRenderers.get(renderMethod) + return handler(domElement); + } + }; + } +} + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.context.md b/docs/development/core/server/kibana-plugin-server.coresetup.context.md new file mode 100644 index 0000000000000..e98cd6a0d04e1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.coresetup.context.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [context](./kibana-plugin-server.coresetup.context.md) + +## CoreSetup.context property + +Signature: + +```typescript +context: { + createContextContainer: ContextSetup['createContextContainer']; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index 38990f2797677..8af0c6e62fb59 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -16,6 +16,7 @@ export interface CoreSetup | Property | Type | Description | | --- | --- | --- | +| [context](./kibana-plugin-server.coresetup.context.md) | {
createContextContainer: ContextSetup['createContextContainer'];
} | | | [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | {
adminClient$: Observable<ClusterClient>;
dataClient$: Observable<ClusterClient>;
createClient: (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ClusterClient;
} | | | [http](./kibana-plugin-server.coresetup.http.md) | {
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
basePath: HttpServiceSetup['basePath'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
} | | diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 4cb19665a5dd6..295c94574ab6a 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -36,6 +36,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AuthResultParams](./kibana-plugin-server.authresultparams.md) | Result of an incoming request authentication. | | [AuthToolkit](./kibana-plugin-server.authtoolkit.md) | A tool set defining an outcome of Auth interceptor for incoming request. | | [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. | +| [ContextSetup](./kibana-plugin-server.contextsetup.md) | | | [CoreSetup](./kibana-plugin-server.coresetup.md) | Context passed to the plugins setup method. | | [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins start method. | | [CustomHttpResponseOptions](./kibana-plugin-server.customhttpresponseoptions.md) | HTTP response parameters for a response with adjustable status code. | @@ -118,6 +119,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | | | [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | | [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | +| [PluginOpaqueId](./kibana-plugin-server.pluginopaqueid.md) | | | [RecursiveReadonly](./kibana-plugin-server.recursivereadonly.md) | | | [RedirectResponseOptions](./kibana-plugin-server.redirectresponseoptions.md) | HTTP response parameters for redirection response | | [RequestHandler](./kibana-plugin-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) functions. | diff --git a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md index 8eab84e2531d7..2bba3d408f68e 100644 --- a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md +++ b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md @@ -19,4 +19,5 @@ export interface PluginInitializerContext | [config](./kibana-plugin-server.plugininitializercontext.config.md) | {
create: <T = ConfigSchema>() => Observable<T>;
createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;
} | | | [env](./kibana-plugin-server.plugininitializercontext.env.md) | {
mode: EnvironmentMode;
} | | | [logger](./kibana-plugin-server.plugininitializercontext.logger.md) | LoggerFactory | | +| [opaqueId](./kibana-plugin-server.plugininitializercontext.opaqueid.md) | PluginOpaqueId | | diff --git a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.opaqueid.md b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.opaqueid.md new file mode 100644 index 0000000000000..7ac177f039c91 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.opaqueid.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) > [opaqueId](./kibana-plugin-server.plugininitializercontext.opaqueid.md) + +## PluginInitializerContext.opaqueId property + +Signature: + +```typescript +opaqueId: PluginOpaqueId; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginopaqueid.md b/docs/development/core/server/kibana-plugin-server.pluginopaqueid.md new file mode 100644 index 0000000000000..3b2399d95137d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginopaqueid.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginOpaqueId](./kibana-plugin-server.pluginopaqueid.md) + +## PluginOpaqueId type + + +Signature: + +```typescript +export declare type PluginOpaqueId = symbol; +``` diff --git a/src/core/public/context/context_service.mock.ts b/src/core/public/context/context_service.mock.ts index 289732247b379..eb55ced69dc04 100644 --- a/src/core/public/context/context_service.mock.ts +++ b/src/core/public/context/context_service.mock.ts @@ -18,7 +18,7 @@ */ import { ContextService, ContextSetup } from './context_service'; -import { contextMock } from './context.mock'; +import { contextMock } from '../../utils/context.mock'; const createSetupContractMock = () => { const setupContract: jest.Mocked = { diff --git a/src/core/public/context/context_service.test.mocks.ts b/src/core/public/context/context_service.test.mocks.ts index 765d7d94b19c5..5cf492d97aaf2 100644 --- a/src/core/public/context/context_service.test.mocks.ts +++ b/src/core/public/context/context_service.test.mocks.ts @@ -17,9 +17,9 @@ * under the License. */ -import { contextMock } from './context.mock'; +import { contextMock } from '../../utils/context.mock'; export const MockContextConstructor = jest.fn(contextMock.create); -jest.doMock('./context', () => ({ +jest.doMock('../../utils/context', () => ({ ContextContainer: MockContextConstructor, })); diff --git a/src/core/public/context/context_service.test.ts b/src/core/public/context/context_service.test.ts index 4441a1c5ae6b2..d575d57a6b275 100644 --- a/src/core/public/context/context_service.test.ts +++ b/src/core/public/context/context_service.test.ts @@ -17,9 +17,9 @@ * under the License. */ +import { PluginOpaqueId } from '../../server'; import { MockContextConstructor } from './context_service.test.mocks'; import { ContextService } from './context_service'; -import { PluginOpaqueId } from '../plugins'; const pluginDependencies = new Map(); diff --git a/src/core/public/context/context_service.ts b/src/core/public/context/context_service.ts index 7c2d151177f19..704524d838636 100644 --- a/src/core/public/context/context_service.ts +++ b/src/core/public/context/context_service.ts @@ -17,9 +17,9 @@ * under the License. */ -import { IContextContainer, ContextContainer } from './context'; +import { PluginOpaqueId } from '../../server'; +import { IContextContainer, ContextContainer } from '../../utils/context'; import { CoreContext } from '../core_system'; -import { PluginOpaqueId } from '../plugins'; interface StartDeps { pluginDependencies: ReadonlyMap; @@ -64,8 +64,10 @@ export class ContextService { * class VizRenderingPlugin { * private readonly vizRenderers = new Map () => void)>(); * + * constructor(private readonly initContext: PluginInitializerContext) {} + * * setup(core) { - * this.contextContainer = core.createContextContainer< + * this.contextContainer = core.context.createContextContainer< * VizRenderContext, * ReturnType, * [HTMLElement] @@ -79,8 +81,8 @@ export class ContextService { * } * * start(core) { - * // Register the core context available to all renderers. Use the VizRendererContext's pluginId as the first arg. - * this.contextContainer.registerContext('viz_rendering', 'core', () => ({ + * // Register the core context available to all renderers. Use the VizRendererContext's opaqueId as the first arg. + * this.contextContainer.registerContext(this.initContext.opaqueId, 'core', () => ({ * i18n: core.i18n, * uiSettings: core.uiSettings * })); diff --git a/src/core/public/context/index.ts b/src/core/public/context/index.ts index 0e63e1a6426f0..28b2641b2a5a7 100644 --- a/src/core/public/context/index.ts +++ b/src/core/public/context/index.ts @@ -18,4 +18,4 @@ */ export { ContextService, ContextSetup } from './context_service'; -export { IContextContainer, IContextProvider, IContextHandler } from './context'; +export { IContextContainer, IContextProvider, IContextHandler } from '../../utils/context'; diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index a8d071746085c..2451feff1b4b0 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -19,6 +19,7 @@ import './core.css'; +import { CoreId } from '../server'; import { InternalCoreSetup, InternalCoreStart } from '.'; import { ChromeService } from './chrome'; import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors'; @@ -44,9 +45,6 @@ interface Params { useLegacyTestHarness?: LegacyPlatformParams['useLegacyTestHarness']; } -/** @internal */ -export type CoreId = symbol; - /** @internal */ export interface CoreContext { coreId: CoreId; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 40a6fbf065164..78e7160656f05 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -62,7 +62,7 @@ import { ToastsApi, } from './notifications'; import { OverlayRef, OverlayStart } from './overlays'; -import { Plugin, PluginInitializer, PluginInitializerContext } from './plugins'; +import { Plugin, PluginInitializer, PluginInitializerContext, PluginOpaqueId } from './plugins'; import { UiSettingsClient, UiSettingsState, UiSettingsClientContract } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; import { DocLinksStart } from './doc_links'; @@ -181,6 +181,7 @@ export { Plugin, PluginInitializer, PluginInitializerContext, + PluginOpaqueId, Toast, ToastInput, ToastsApi, diff --git a/src/core/public/plugins/index.ts b/src/core/public/plugins/index.ts index 544d4cf49c632..8adcf8ca40c10 100644 --- a/src/core/public/plugins/index.ts +++ b/src/core/public/plugins/index.ts @@ -18,5 +18,6 @@ */ export * from './plugins_service'; -export { Plugin, PluginInitializer, PluginOpaqueId } from './plugin'; +export { Plugin, PluginInitializer } from './plugin'; export { PluginInitializerContext } from './plugin_context'; +export { PluginOpaqueId } from '../../server/types'; diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index fe870bd23c7a0..a8e52cc57cb66 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -17,14 +17,11 @@ * under the License. */ -import { DiscoveredPlugin } from '../../server'; +import { DiscoveredPlugin, PluginOpaqueId } from '../../server'; import { PluginInitializerContext } from './plugin_context'; import { loadPluginBundle } from './plugin_loader'; import { CoreStart, CoreSetup } from '..'; -/** @public */ -export type PluginOpaqueId = symbol; - /** * The interface that should be returned by a `PluginInitializer`. * diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 3711ce08c9992..75e31192e0de7 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -19,9 +19,9 @@ import { omit } from 'lodash'; -import { DiscoveredPlugin } from '../../server'; +import { DiscoveredPlugin, PluginOpaqueId } from '../../server'; import { CoreContext } from '../core_system'; -import { PluginWrapper, PluginOpaqueId } from './plugin'; +import { PluginWrapper } from './plugin'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; import { CoreSetup, CoreStart } from '../'; diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 902c883c74bbc..13a52d78d72fc 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -17,10 +17,10 @@ * under the License. */ -import { DiscoveredPlugin, PluginName } from '../../server'; +import { DiscoveredPlugin, PluginName, PluginOpaqueId } from '../../server'; import { CoreService } from '../../types'; import { CoreContext } from '../core_system'; -import { PluginWrapper, PluginOpaqueId } from './plugin'; +import { PluginWrapper } from './plugin'; import { createPluginInitializerContext, createPluginSetupContext, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 6058bc3dcb809..26c2670141da6 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -492,7 +492,6 @@ export interface I18nStart { export interface IContextContainer { // Warning: (ae-forgotten-export) The symbol "Promisify" needs to be exported by the entry point index.d.ts createHandler(pluginOpaqueId: PluginOpaqueId, handler: IContextHandler): (...rest: THandlerParameters) => Promisify; - // Warning: (ae-forgotten-export) The symbol "PluginOpaqueId" needs to be exported by the entry point index.d.ts registerContext(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider): this; } @@ -595,6 +594,9 @@ export interface PluginInitializerContext { readonly opaqueId: PluginOpaqueId; } +// @public (undocumented) +export type PluginOpaqueId = symbol; + // Warning: (ae-forgotten-export) The symbol "RecursiveReadonlyArray" needs to be exported by the entry point index.d.ts // // @public (undocumented) diff --git a/src/core/server/context/context_service.mock.ts b/src/core/server/context/context_service.mock.ts new file mode 100644 index 0000000000000..eb55ced69dc04 --- /dev/null +++ b/src/core/server/context/context_service.mock.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ContextService, ContextSetup } from './context_service'; +import { contextMock } from '../../utils/context.mock'; + +const createSetupContractMock = () => { + const setupContract: jest.Mocked = { + createContextContainer: jest.fn().mockImplementation(() => contextMock.create()), + }; + return setupContract; +}; + +type ContextServiceContract = PublicMethodsOf; +const createMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn(), + }; + mocked.setup.mockReturnValue(createSetupContractMock()); + return mocked; +}; + +export const contextServiceMock = { + create: createMock, + createSetupContract: createSetupContractMock, +}; diff --git a/src/core/server/context/context_service.test.mocks.ts b/src/core/server/context/context_service.test.mocks.ts new file mode 100644 index 0000000000000..5cf492d97aaf2 --- /dev/null +++ b/src/core/server/context/context_service.test.mocks.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { contextMock } from '../../utils/context.mock'; + +export const MockContextConstructor = jest.fn(contextMock.create); +jest.doMock('../../utils/context', () => ({ + ContextContainer: MockContextConstructor, +})); diff --git a/src/core/server/context/context_service.test.ts b/src/core/server/context/context_service.test.ts new file mode 100644 index 0000000000000..611ba8cac92a7 --- /dev/null +++ b/src/core/server/context/context_service.test.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginOpaqueId } from '../../server'; +import { MockContextConstructor } from './context_service.test.mocks'; +import { ContextService } from './context_service'; +import { CoreContext } from '../core_context'; + +const pluginDependencies = new Map(); + +describe('ContextService', () => { + describe('#setup()', () => { + test('createContextContainer returns a new container configured with pluginDependencies', () => { + const coreId = Symbol(); + const service = new ContextService({ coreId } as CoreContext); + const setup = service.setup({ pluginDependencies }); + expect(setup.createContextContainer()).toBeDefined(); + expect(MockContextConstructor).toHaveBeenCalledWith(pluginDependencies, coreId); + }); + }); +}); diff --git a/src/core/server/context/context_service.ts b/src/core/server/context/context_service.ts new file mode 100644 index 0000000000000..80935840c5536 --- /dev/null +++ b/src/core/server/context/context_service.ts @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginOpaqueId } from '../../server'; +import { IContextContainer, ContextContainer } from '../../utils/context'; +import { CoreContext } from '../core_context'; + +interface SetupDeps { + pluginDependencies: ReadonlyMap; +} + +/** @internal */ +export class ContextService { + constructor(private readonly core: CoreContext) {} + + public setup({ pluginDependencies }: SetupDeps): ContextSetup { + return { + createContextContainer: < + TContext extends {}, + THandlerReturn, + THandlerParameters extends any[] = [] + >() => { + return new ContextContainer( + pluginDependencies, + this.core.coreId + ); + }, + }; + } +} + +/** + * {@inheritdoc IContextContainer} + * + * @example + * Say we're creating a plugin for rendering visualizations that allows new rendering methods to be registered. If we + * want to offer context to these rendering methods, we can leverage the ContextService to manage these contexts. + * ```ts + * export interface VizRenderContext { + * core: { + * i18n: I18nStart; + * uiSettings: UISettingsClientContract; + * } + * [contextName: string]: unknown; + * } + * + * export type VizRenderer = (context: VizRenderContext, domElement: HTMLElement) => () => void; + * + * class VizRenderingPlugin { + * private readonly vizRenderers = new Map () => void)>(); + * + * constructor(private readonly initContext: PluginInitializerContext) {} + * + * setup(core) { + * this.contextContainer = core.context.createContextContainer< + * VizRenderContext, + * ReturnType, + * [HTMLElement] + * >(); + * + * return { + * registerContext: this.contextContainer.registerContext, + * registerVizRenderer: (plugin: PluginOpaqueId, renderMethod: string, renderer: VizTypeRenderer) => + * this.vizRenderers.set(renderMethod, this.contextContainer.createHandler(plugin, renderer)), + * }; + * } + * + * start(core) { + * // Register the core context available to all renderers. Use the VizRendererContext's opaqueId as the first arg. + * this.contextContainer.registerContext(this.initContext.opaqueId, 'core', () => ({ + * i18n: core.i18n, + * uiSettings: core.uiSettings + * })); + * + * return { + * registerContext: this.contextContainer.registerContext, + * + * renderVizualization: (renderMethod: string, domElement: HTMLElement) => { + * if (!this.vizRenderer.has(renderMethod)) { + * throw new Error(`Render method '${renderMethod}' has not been registered`); + * } + * + * // The handler can now be called directly with only an `HTMLElement` and will automatically + * // have a new `context` object created and populated by the context container. + * const handler = this.vizRenderers.get(renderMethod) + * return handler(domElement); + * } + * }; + * } + * } + * ``` + * + * @public + */ +export interface ContextSetup { + /** + * Creates a new {@link IContextContainer} for a service owner. + */ + createContextContainer< + TContext extends {}, + THandlerReturn, + THandlerParmaters extends any[] = [] + >(): IContextContainer; +} diff --git a/src/core/server/context/index.ts b/src/core/server/context/index.ts new file mode 100644 index 0000000000000..28b2641b2a5a7 --- /dev/null +++ b/src/core/server/context/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { ContextService, ContextSetup } from './context_service'; +export { IContextContainer, IContextProvider, IContextHandler } from '../../utils/context'; diff --git a/src/core/server/core_context.ts b/src/core/server/core_context.ts index a08bd03f148cb..701f5a83a81c2 100644 --- a/src/core/server/core_context.ts +++ b/src/core/server/core_context.ts @@ -20,12 +20,16 @@ import { ConfigService, Env } from './config'; import { LoggerFactory } from './logging'; +/** @internal */ +export type CoreId = symbol; + /** * Groups all main Kibana's core modules/systems/services that are consumed in a * variety of places within the core itself. * @internal */ export interface CoreContext { + coreId: CoreId; env: Env; configService: ConfigService; logger: LoggerFactory; diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index e84ddb2baa30c..58fe2b52727ef 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -54,7 +54,7 @@ const logger = loggingServiceMock.create(); beforeEach(() => { env = Env.createDefault(getEnvOptions()); - coreContext = { env, logger, configService: configService as any }; + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; elasticsearchService = new ElasticsearchService(coreContext); }); diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index cde06dc31802f..6ed19d6f97e82 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -30,6 +30,7 @@ import { getEnvOptions } from '../config/__mocks__/env'; const logger = loggingServiceMock.create(); const env = Env.createDefault(getEnvOptions()); +const coreId = Symbol(); const createConfigService = (value: Partial = {}) => { const configService = new ConfigService( @@ -68,7 +69,7 @@ test('creates and sets up http server', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService({ configService, env, logger }); + const service = new HttpService({ coreId, configService, env, logger }); expect(mockHttpServer.mock.instances.length).toBe(1); @@ -103,6 +104,7 @@ test('spins up notReady server until started if configured with `autoListen:true })); const service = new HttpService({ + coreId, configService, env: new Env('.', getEnvOptions()), logger, @@ -144,7 +146,7 @@ test('logs error if already set up', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService({ configService, env, logger }); + const service = new HttpService({ coreId, configService, env, logger }); await service.setup(); @@ -162,7 +164,7 @@ test('stops http server', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService({ configService, env, logger }); + const service = new HttpService({ coreId, configService, env, logger }); await service.setup(); await service.start(); @@ -189,7 +191,7 @@ test('stops not ready server if it is running', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService({ configService, env, logger }); + const service = new HttpService({ coreId, configService, env, logger }); await service.setup(); @@ -212,7 +214,7 @@ test('register route handler', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService({ configService, env, logger }); + const service = new HttpService({ coreId, configService, env, logger }); const router = new Router('/foo'); const { registerRouter } = await service.setup(); @@ -232,7 +234,7 @@ test('returns http server contract on setup', async () => { stop: noop, })); - const service = new HttpService({ configService, env, logger }); + const service = new HttpService({ coreId, configService, env, logger }); const setupHttpServer = await service.setup(); expect(setupHttpServer).toEqual(httpServer); }); @@ -248,6 +250,7 @@ test('does not start http server if process is dev cluster master', async () => mockHttpServer.mockImplementation(() => httpServer); const service = new HttpService({ + coreId, configService, env: new Env('.', getEnvOptions({ isDevClusterMaster: true })), logger, @@ -272,6 +275,7 @@ test('does not start http server if configured with `autoListen:false`', async ( mockHttpServer.mockImplementation(() => httpServer); const service = new HttpService({ + coreId, configService, env: new Env('.', getEnvOptions()), logger, diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 72a183f2dd1db..424f1730683e4 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -42,10 +42,12 @@ import { ElasticsearchServiceSetup, } from './elasticsearch'; import { HttpServiceSetup, HttpServiceStart } from './http'; -import { PluginsServiceSetup, PluginsServiceStart } from './plugins'; +import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; +import { ContextSetup } from './context'; export { bootstrap } from './bootstrap'; export { ConfigService } from './config'; +export { CoreId } from './core_context'; export { CallAPIOptions, ClusterClient, @@ -146,6 +148,9 @@ export { RecursiveReadonly } from '../utils'; * @public */ export interface CoreSetup { + context: { + createContextContainer: ContextSetup['createContextContainer']; + }; elasticsearch: { adminClient$: Observable; dataClient$: Observable; @@ -186,9 +191,11 @@ export interface InternalCoreStart { } export { + ContextSetup, HttpServiceSetup, HttpServiceStart, ElasticsearchServiceSetup, PluginsServiceSetup, PluginsServiceStart, + PluginOpaqueId, }; diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index fa4d60520818e..7da013dbe2174 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -37,6 +37,7 @@ import { PluginsServiceSetup, PluginsServiceStart } from '../plugins/plugins_ser const MockKbnServer: jest.Mock = KbnServer as any; +let coreId: symbol; let env: Env; let config$: BehaviorSubject; let setupDeps: { @@ -60,6 +61,7 @@ const logger = loggingServiceMock.create(); let configService: ReturnType; beforeEach(() => { + coreId = Symbol(); env = Env.createDefault(getEnvOptions()); configService = configServiceMock.create(); @@ -112,7 +114,12 @@ afterEach(() => { describe('once LegacyService is set up with connection info', () => { test('creates legacy kbnServer and calls `listen`.', async () => { configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -136,7 +143,12 @@ describe('once LegacyService is set up with connection info', () => { test('creates legacy kbnServer but does not call `listen` if `autoListen: false`.', async () => { configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: false })); - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -160,7 +172,12 @@ describe('once LegacyService is set up with connection info', () => { test('creates legacy kbnServer and closes it if `listen` fails.', async () => { configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); MockKbnServer.prototype.listen.mockRejectedValue(new Error('something failed')); - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await legacyService.setup(setupDeps); await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingSnapshot(); @@ -172,7 +189,12 @@ describe('once LegacyService is set up with connection info', () => { test('throws if fails to retrieve initial config.', async () => { configService.getConfig$.mockReturnValue(throwError(new Error('something failed'))); - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await legacyService.setup(setupDeps); await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingSnapshot(); @@ -182,7 +204,12 @@ describe('once LegacyService is set up with connection info', () => { }); test('reconfigures logging configuration if new config is received.', async () => { - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -197,7 +224,12 @@ describe('once LegacyService is set up with connection info', () => { }); test('logs error if re-configuring fails.', async () => { - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -216,7 +248,12 @@ describe('once LegacyService is set up with connection info', () => { }); test('logs error if config service fails.', async () => { - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -235,7 +272,7 @@ describe('once LegacyService is set up with connection info', () => { describe('once LegacyService is set up without connection info', () => { let legacyService: LegacyService; beforeEach(async () => { - legacyService = new LegacyService({ env, logger, configService: configService as any }); + legacyService = new LegacyService({ coreId, env, logger, configService: configService as any }); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -277,6 +314,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { test('creates ClusterManager without base path proxy.', async () => { const devClusterLegacyService = new LegacyService({ + coreId, env: Env.createDefault( getEnvOptions({ cliArgs: { silent: true, basePath: false }, @@ -297,6 +335,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { test('creates ClusterManager with base path proxy.', async () => { const devClusterLegacyService = new LegacyService({ + coreId, env: Env.createDefault( getEnvOptions({ cliArgs: { quiet: true, basePath: true }, @@ -320,7 +359,12 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { }); test('Cannot start without setup phase', async () => { - const legacyService = new LegacyService({ env, logger, configService: configService as any }); + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( `"Legacy service is not setup yet."` ); diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 518221d2b9bd5..da547e437648d 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -21,6 +21,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart } from '.'; import { loggingServiceMock } from './logging/logging_service.mock'; import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; import { httpServiceMock } from './http/http_service.mock'; +import { contextServiceMock } from './context/context_service.mock'; export { httpServerMock } from './http/http_server.mocks'; export { sessionStorageMock } from './http/cookie_session_storage.mocks'; @@ -41,6 +42,7 @@ export function pluginInitializerContextConfigMock(config: T) { function pluginInitializerContextMock(config: T) { const mock: PluginInitializerContext = { + opaqueId: Symbol(), logger: loggingServiceMock.create(), env: { mode: { @@ -57,6 +59,7 @@ function pluginInitializerContextMock(config: T) { function createCoreSetupMock() { const mock: MockedKeys = { + context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), }; diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.ts index aedda9e3c92ee..93c993a0fa373 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.ts @@ -22,7 +22,7 @@ import { resolve } from 'path'; import { coerce } from 'semver'; import { promisify } from 'util'; import { isConfigPath, PackageInfo } from '../../config'; -import { PluginManifest } from '../plugin'; +import { PluginManifest } from '../types'; import { PluginDiscoveryError } from './plugin_discovery_error'; const fsReadFileAsync = promisify(readFile); diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts index 6e174e03cfcb1..224259bc121ec 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.test.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts @@ -128,6 +128,7 @@ test('properly iterates through plugin search locations', async () => { .pipe(first()) .toPromise(); const { plugin$, error$ } = discover(new PluginsConfig(rawConfig, env), { + coreId: Symbol(), configService, env, logger, diff --git a/src/core/server/plugins/discovery/plugins_discovery.ts b/src/core/server/plugins/discovery/plugins_discovery.ts index f674444c921e1..74e9dd709bb23 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.ts @@ -115,11 +115,13 @@ function createPlugin$(path: string, log: Logger, coreContext: CoreContext) { return from(parseManifest(path, coreContext.env.packageInfo)).pipe( map(manifest => { log.debug(`Successfully discovered plugin "${manifest.id}" at "${path}"`); - return new PluginWrapper( + const opaqueId = Symbol(manifest.id); + return new PluginWrapper({ path, manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); }), catchError(err => [err]) ); diff --git a/src/core/server/plugins/index.ts b/src/core/server/plugins/index.ts index c2e66cbb0bb7d..c7ef213c8f187 100644 --- a/src/core/server/plugins/index.ts +++ b/src/core/server/plugins/index.ts @@ -21,12 +21,4 @@ export { PluginsService, PluginsServiceSetup, PluginsServiceStart } from './plug export { config } from './plugins_config'; /** @internal */ export { isNewPlatformPlugin } from './discovery'; -/** @internal */ -export { - DiscoveredPlugin, - DiscoveredPluginInternal, - Plugin, - PluginInitializer, - PluginName, -} from './plugin'; -export { PluginInitializerContext } from './plugin_context'; +export * from './types'; diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index 0ce4ba2666198..5c57a5fa2c8d1 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -29,8 +29,10 @@ import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service import { httpServiceMock } from '../http/http_service.mock'; import { loggingServiceMock } from '../logging/logging_service.mock'; -import { PluginWrapper, PluginManifest } from './plugin'; +import { PluginWrapper } from './plugin'; +import { PluginManifest } from './types'; import { createPluginInitializerContext, createPluginSetupContext } from './plugin_context'; +import { contextServiceMock } from '../context/context_service.mock'; const mockPluginInitializer = jest.fn(); const logger = loggingServiceMock.create(); @@ -63,16 +65,19 @@ function createPluginManifest(manifestProps: Partial = {}): Plug const configService = configServiceMock.create(); configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); +let coreId: symbol; let env: Env; let coreContext: CoreContext; const setupDeps = { + context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), }; beforeEach(() => { + coreId = Symbol('core'); env = Env.createDefault(getEnvOptions()); - coreContext = { env, logger, configService: configService as any }; + coreContext = { coreId, env, logger, configService: configService as any }; }); afterEach(() => { @@ -81,11 +86,13 @@ afterEach(() => { test('`constructor` correctly initializes plugin instance', () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'some-plugin-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'some-plugin-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); expect(plugin.name).toBe('some-plugin-id'); expect(plugin.configPath).toBe('path'); @@ -96,11 +103,13 @@ test('`constructor` correctly initializes plugin instance', () => { test('`setup` fails if `plugin` initializer is not exported', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-without-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-without-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); await expect( plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) @@ -111,11 +120,13 @@ test('`setup` fails if `plugin` initializer is not exported', async () => { test('`setup` fails if plugin initializer is not a function', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-wrong-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-wrong-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); await expect( plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}) @@ -126,11 +137,13 @@ test('`setup` fails if plugin initializer is not a function', async () => { test('`setup` fails if initializer does not return object', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); mockPluginInitializer.mockReturnValue(null); @@ -143,11 +156,13 @@ test('`setup` fails if initializer does not return object', async () => { test('`setup` fails if object returned from initializer does not define `setup` function', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); const mockPluginInstance = { run: jest.fn() }; mockPluginInitializer.mockReturnValue(mockPluginInstance); @@ -161,8 +176,14 @@ test('`setup` fails if object returned from initializer does not define `setup` test('`setup` initializes plugin and calls appropriate lifecycle hook', async () => { const manifest = createPluginManifest(); - const initializerContext = createPluginInitializerContext(coreContext, manifest); - const plugin = new PluginWrapper('plugin-with-initializer-path', manifest, initializerContext); + const opaqueId = Symbol(); + const initializerContext = createPluginInitializerContext(coreContext, opaqueId, manifest); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', + manifest, + opaqueId, + initializerContext, + }); const mockPluginInstance = { setup: jest.fn().mockResolvedValue({ contract: 'yes' }) }; mockPluginInitializer.mockReturnValue(mockPluginInstance); @@ -180,11 +201,13 @@ test('`setup` initializes plugin and calls appropriate lifecycle hook', async () test('`start` fails if setup is not called first', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'some-plugin-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'some-plugin-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( `"Plugin \\"some-plugin-id\\" can't be started since it isn't set up."` @@ -193,11 +216,13 @@ test('`start` fails if setup is not called first', async () => { test('`start` calls plugin.start with context and dependencies', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); const context = { any: 'thing' } as any; const deps = { otherDep: 'value' }; @@ -218,11 +243,13 @@ test('`start` calls plugin.start with context and dependencies', async () => { test('`stop` fails if plugin is not set up', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); const mockPluginInstance = { setup: jest.fn(), stop: jest.fn() }; mockPluginInitializer.mockReturnValue(mockPluginInstance); @@ -235,11 +262,13 @@ test('`stop` fails if plugin is not set up', async () => { test('`stop` does nothing if plugin does not define `stop` function', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); mockPluginInitializer.mockReturnValue({ setup: jest.fn() }); await plugin.setup(createPluginSetupContext(coreContext, setupDeps, plugin), {}); @@ -249,11 +278,13 @@ test('`stop` does nothing if plugin does not define `stop` function', async () = test('`stop` calls `stop` defined by the plugin instance', async () => { const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-initializer-path', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); const mockPluginInstance = { setup: jest.fn(), stop: jest.fn() }; mockPluginInitializer.mockReturnValue(mockPluginInstance); @@ -276,11 +307,13 @@ describe('#getConfigSchema()', () => { { virtual: true } ); const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-schema', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-schema', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); expect(plugin.getConfigSchema()).toBe(pluginSchema); }); @@ -288,21 +321,25 @@ describe('#getConfigSchema()', () => { it('returns null if config definition not specified', () => { jest.doMock('plugin-with-no-definition/server', () => ({}), { virtual: true }); const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-with-no-definition', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-no-definition', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); expect(plugin.getConfigSchema()).toBe(null); }); it('returns null for plugins without a server part', () => { const manifest = createPluginManifest({ server: false }); - const plugin = new PluginWrapper( - 'plugin-with-no-definition', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-no-definition', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); expect(plugin.getConfigSchema()).toBe(null); }); @@ -319,11 +356,13 @@ describe('#getConfigSchema()', () => { { virtual: true } ); const manifest = createPluginManifest(); - const plugin = new PluginWrapper( - 'plugin-invalid-schema', + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-invalid-schema', manifest, - createPluginInitializerContext(coreContext, manifest) - ); + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); expect(() => plugin.getConfigSchema()).toThrowErrorMatchingInlineSnapshot( `"Configuration schema expected to be an instance of Type"` ); diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index 289f6f7cda7ac..0101862ad32cc 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -22,143 +22,17 @@ import typeDetect from 'type-detect'; import { Type } from '@kbn/config-schema'; -import { ConfigPath } from '../config'; import { Logger } from '../logging'; -import { PluginInitializerContext } from './plugin_context'; +import { + Plugin, + PluginInitializerContext, + PluginManifest, + PluginConfigSchema, + PluginInitializer, + PluginOpaqueId, +} from './types'; import { CoreSetup, CoreStart } from '..'; -export type PluginConfigSchema = Type | null; - -/** - * Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays - * that use it as a key or value more obvious. - * - * @public - */ -export type PluginName = string; - -/** - * Describes the set of required and optional properties plugin can define in its - * mandatory JSON manifest file. - * @internal - */ -export interface PluginManifest { - /** - * Identifier of the plugin. - */ - readonly id: PluginName; - - /** - * Version of the plugin. - */ - readonly version: string; - - /** - * The version of Kibana the plugin is compatible with, defaults to "version". - */ - readonly kibanaVersion: string; - - /** - * Root configuration path used by the plugin, defaults to "id". - */ - readonly configPath: ConfigPath; - - /** - * An optional list of the other plugins that **must be** installed and enabled - * for this plugin to function properly. - */ - readonly requiredPlugins: readonly PluginName[]; - - /** - * An optional list of the other plugins that if installed and enabled **may be** - * leveraged by this plugin for some additional functionality but otherwise are - * not required for this plugin to work properly. - */ - readonly optionalPlugins: readonly PluginName[]; - - /** - * Specifies whether plugin includes some client/browser specific functionality - * that should be included into client bundle via `public/ui_plugin.js` file. - */ - readonly ui: boolean; - - /** - * Specifies whether plugin includes some server-side specific functionality. - */ - readonly server: boolean; -} - -/** - * Small container object used to expose information about discovered plugins that may - * or may not have been started. - * @public - */ -export interface DiscoveredPlugin { - /** - * Identifier of the plugin. - */ - readonly id: PluginName; - - /** - * Root configuration path used by the plugin, defaults to "id". - */ - readonly configPath: ConfigPath; - - /** - * An optional list of the other plugins that **must be** installed and enabled - * for this plugin to function properly. - */ - readonly requiredPlugins: readonly PluginName[]; - - /** - * An optional list of the other plugins that if installed and enabled **may be** - * leveraged by this plugin for some additional functionality but otherwise are - * not required for this plugin to work properly. - */ - readonly optionalPlugins: readonly PluginName[]; -} - -/** - * An extended `DiscoveredPlugin` that exposes more sensitive information. Should never - * be exposed to client-side code. - * @internal - */ -export interface DiscoveredPluginInternal extends DiscoveredPlugin { - /** - * Path on the filesystem where plugin was loaded from. - */ - readonly path: string; -} - -/** - * The interface that should be returned by a `PluginInitializer`. - * - * @public - */ -export interface Plugin< - TSetup = void, - TStart = void, - TPluginsSetup extends object = object, - TPluginsStart extends object = object -> { - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; - start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; - stop?(): void; -} - -/** - * The `plugin` export at the root of a plugin's `server` directory should conform - * to this interface. - * - * @public - */ -export type PluginInitializer< - TSetup, - TStart, - TPluginsSetup extends object = object, - TPluginsStart extends object = object -> = (core: PluginInitializerContext) => Plugin; - /** * Lightweight wrapper around discovered plugin that is responsible for instantiating * plugin and dispatching proper context and dependencies into plugin's lifecycle hooks. @@ -171,6 +45,9 @@ export class PluginWrapper< TPluginsSetup extends object = object, TPluginsStart extends object = object > { + public readonly path: string; + public readonly manifest: PluginManifest; + public readonly opaqueId: PluginOpaqueId; public readonly name: PluginManifest['id']; public readonly configPath: PluginManifest['configPath']; public readonly requiredPlugins: PluginManifest['requiredPlugins']; @@ -179,21 +56,29 @@ export class PluginWrapper< public readonly includesUiPlugin: PluginManifest['ui']; private readonly log: Logger; + private readonly initializerContext: PluginInitializerContext; private instance?: Plugin; constructor( - public readonly path: string, - public readonly manifest: PluginManifest, - private readonly initializerContext: PluginInitializerContext + readonly params: { + readonly path: string; + readonly manifest: PluginManifest; + readonly opaqueId: PluginOpaqueId; + readonly initializerContext: PluginInitializerContext; + } ) { - this.log = initializerContext.logger.get(); - this.name = manifest.id; - this.configPath = manifest.configPath; - this.requiredPlugins = manifest.requiredPlugins; - this.optionalPlugins = manifest.optionalPlugins; - this.includesServerPlugin = manifest.server; - this.includesUiPlugin = manifest.ui; + this.path = params.path; + this.manifest = params.manifest; + this.opaqueId = params.opaqueId; + this.initializerContext = params.initializerContext; + this.log = params.initializerContext.logger.get(); + this.name = params.manifest.id; + this.configPath = params.manifest.configPath; + this.requiredPlugins = params.manifest.requiredPlugins; + this.optionalPlugins = params.manifest.optionalPlugins; + this.includesServerPlugin = params.manifest.server; + this.includesUiPlugin = params.manifest.ui; } /** diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 8ebd3e6b3c8b2..01c9aa2f8d1a8 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -17,28 +17,12 @@ * under the License. */ -import { Observable } from 'rxjs'; -import { EnvironmentMode } from '../config'; import { CoreContext } from '../core_context'; -import { LoggerFactory } from '../logging'; -import { PluginWrapper, PluginManifest } from './plugin'; +import { PluginWrapper } from './plugin'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; +import { PluginInitializerContext, PluginManifest, PluginOpaqueId } from './types'; import { CoreSetup, CoreStart } from '..'; -/** - * Context that's available to plugins during initialization stage. - * - * @public - */ -export interface PluginInitializerContext { - env: { mode: EnvironmentMode }; - logger: LoggerFactory; - config: { - create: () => Observable; - createIfExists: () => Observable; - }; -} - /** * This returns a facade for `CoreContext` that will be exposed to the plugin initializer. * This facade should be safe to use across entire plugin lifespan. @@ -54,9 +38,12 @@ export interface PluginInitializerContext { */ export function createPluginInitializerContext( coreContext: CoreContext, + opaqueId: PluginOpaqueId, pluginManifest: PluginManifest ): PluginInitializerContext { return { + opaqueId, + /** * Environment information that is safe to expose to plugins and may be beneficial for them. */ @@ -112,6 +99,9 @@ export function createPluginSetupContext( plugin: PluginWrapper ): CoreSetup { return { + context: { + createContextContainer: deps.context.createContextContainer, + }, elasticsearch: { adminClient$: deps.elasticsearch.adminClient$, dataClient$: deps.elasticsearch.dataClient$, diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index c9045ad04e1eb..c8b6bed044fd7 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -22,6 +22,7 @@ import { PluginsService } from './plugins_service'; type ServiceContract = PublicMethodsOf; const createServiceMock = () => { const mocked: jest.Mocked = { + discover: jest.fn(), setup: jest.fn(), start: jest.fn(), stop: jest.fn(), diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index fbb37f40362b4..fdbb5efbfafec 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -33,14 +33,17 @@ import { PluginWrapper } from './plugin'; import { PluginsService } from './plugins_service'; import { PluginsSystem } from './plugins_system'; import { config } from './plugins_config'; +import { contextServiceMock } from '../context/context_service.mock'; const MockPluginsSystem: jest.Mock = PluginsSystem as any; let pluginsService: PluginsService; let configService: ConfigService; +let coreId: symbol; let env: Env; let mockPluginSystem: jest.Mocked; const setupDeps = { + context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), }; @@ -63,6 +66,7 @@ beforeEach(async () => { }, }; + coreId = Symbol('core'); env = Env.createDefault(getEnvOptions()); configService = new ConfigService( @@ -71,7 +75,7 @@ beforeEach(async () => { logger ); await configService.setSchema(config.path, config.schema); - pluginsService = new PluginsService({ env, logger, configService }); + pluginsService = new PluginsService({ coreId, env, logger, configService }); [mockPluginSystem] = MockPluginsSystem.mock.instances as any; }); @@ -80,13 +84,13 @@ afterEach(() => { jest.clearAllMocks(); }); -test('`setup` throws if plugin has an invalid manifest', async () => { +test('`discover` throws if plugin has an invalid manifest', async () => { mockDiscover.mockReturnValue({ error$: from([PluginDiscoveryError.invalidManifest('path-1', new Error('Invalid JSON'))]), plugin$: from([]), }); - await expect(pluginsService.setup(setupDeps)).rejects.toMatchInlineSnapshot(` + await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` [Error: Failed to initialize plugins: Invalid JSON (invalid-manifest, path-1)] `); @@ -99,7 +103,7 @@ Array [ `); }); -test('`setup` throws if plugin required Kibana version is incompatible with the current version', async () => { +test('`discover` throws if plugin required Kibana version is incompatible with the current version', async () => { mockDiscover.mockReturnValue({ error$: from([ PluginDiscoveryError.incompatibleVersion('path-3', new Error('Incompatible version')), @@ -107,7 +111,7 @@ test('`setup` throws if plugin required Kibana version is incompatible with the plugin$: from([]), }); - await expect(pluginsService.setup(setupDeps)).rejects.toMatchInlineSnapshot(` + await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot(` [Error: Failed to initialize plugins: Incompatible version (incompatible-version, path-3)] `); @@ -120,13 +124,13 @@ Array [ `); }); -test('`setup` throws if discovered plugins with conflicting names', async () => { +test('`discover` throws if discovered plugins with conflicting names', async () => { mockDiscover.mockReturnValue({ error$: from([]), plugin$: from([ - new PluginWrapper( - 'path-4', - { + new PluginWrapper({ + path: 'path-4', + manifest: { id: 'conflicting-id', version: 'some-version', configPath: 'path', @@ -136,11 +140,12 @@ test('`setup` throws if discovered plugins with conflicting names', async () => server: true, ui: true, }, - { logger } as any - ), - new PluginWrapper( - 'path-5', - { + opaqueId: Symbol(), + initializerContext: { logger } as any, + }), + new PluginWrapper({ + path: 'path-5', + manifest: { id: 'conflicting-id', version: 'some-other-version', configPath: ['plugin', 'path'], @@ -150,12 +155,13 @@ test('`setup` throws if discovered plugins with conflicting names', async () => server: true, ui: false, }, - { logger } as any - ), + opaqueId: Symbol(), + initializerContext: { logger } as any, + }), ]), }); - await expect(pluginsService.setup(setupDeps)).rejects.toMatchInlineSnapshot( + await expect(pluginsService.discover()).rejects.toMatchInlineSnapshot( `[Error: Plugin with id "conflicting-id" is already registered!]` ); @@ -163,7 +169,7 @@ test('`setup` throws if discovered plugins with conflicting names', async () => expect(mockPluginSystem.setupPlugins).not.toHaveBeenCalled(); }); -test('`setup` properly detects plugins that should be disabled.', async () => { +test('`discover` properly detects plugins that should be disabled.', async () => { jest .spyOn(configService, 'isEnabledAtPath') .mockImplementation(path => Promise.resolve(!path.includes('disabled'))); @@ -174,9 +180,9 @@ test('`setup` properly detects plugins that should be disabled.', async () => { mockDiscover.mockReturnValue({ error$: from([]), plugin$: from([ - new PluginWrapper( - 'path-1', - { + new PluginWrapper({ + path: 'path-1', + manifest: { id: 'explicitly-disabled-plugin', version: 'some-version', configPath: 'path-1-disabled', @@ -186,11 +192,12 @@ test('`setup` properly detects plugins that should be disabled.', async () => { server: true, ui: true, }, - { logger } as any - ), - new PluginWrapper( - 'path-2', - { + opaqueId: Symbol(), + initializerContext: { logger } as any, + }), + new PluginWrapper({ + path: 'path-2', + manifest: { id: 'plugin-with-missing-required-deps', version: 'some-version', configPath: 'path-2', @@ -200,11 +207,12 @@ test('`setup` properly detects plugins that should be disabled.', async () => { server: true, ui: true, }, - { logger } as any - ), - new PluginWrapper( - 'path-3', - { + opaqueId: Symbol(), + initializerContext: { logger } as any, + }), + new PluginWrapper({ + path: 'path-3', + manifest: { id: 'plugin-with-disabled-transitive-dep', version: 'some-version', configPath: 'path-3', @@ -214,11 +222,12 @@ test('`setup` properly detects plugins that should be disabled.', async () => { server: true, ui: true, }, - { logger } as any - ), - new PluginWrapper( - 'path-4', - { + opaqueId: Symbol(), + initializerContext: { logger } as any, + }), + new PluginWrapper({ + path: 'path-4', + manifest: { id: 'another-explicitly-disabled-plugin', version: 'some-version', configPath: 'path-4-disabled', @@ -228,16 +237,18 @@ test('`setup` properly detects plugins that should be disabled.', async () => { server: true, ui: true, }, - { logger } as any - ), + opaqueId: Symbol(), + initializerContext: { logger } as any, + }), ]), }); - const start = await pluginsService.setup(setupDeps); + await pluginsService.discover(); + const setup = await pluginsService.setup(setupDeps); - expect(start.contracts).toBeInstanceOf(Map); - expect(start.uiPlugins.public).toBeInstanceOf(Map); - expect(start.uiPlugins.internal).toBeInstanceOf(Map); + expect(setup.contracts).toBeInstanceOf(Map); + expect(setup.uiPlugins.public).toBeInstanceOf(Map); + expect(setup.uiPlugins.internal).toBeInstanceOf(Map); expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps); @@ -260,10 +271,10 @@ Array [ `); }); -test('`setup` properly invokes `discover` and ignores non-critical errors.', async () => { - const firstPlugin = new PluginWrapper( - 'path-1', - { +test('`discover` properly invokes plugin discovery and ignores non-critical errors.', async () => { + const firstPlugin = new PluginWrapper({ + path: 'path-1', + manifest: { id: 'some-id', version: 'some-version', configPath: 'path', @@ -273,12 +284,13 @@ test('`setup` properly invokes `discover` and ignores non-critical errors.', asy server: true, ui: true, }, - { logger } as any - ); + opaqueId: Symbol(), + initializerContext: { logger } as any, + }); - const secondPlugin = new PluginWrapper( - 'path-2', - { + const secondPlugin = new PluginWrapper({ + path: 'path-2', + manifest: { id: 'some-other-id', version: 'some-other-version', configPath: ['plugin', 'path'], @@ -288,8 +300,9 @@ test('`setup` properly invokes `discover` and ignores non-critical errors.', asy server: true, ui: false, }, - { logger } as any - ); + opaqueId: Symbol(), + initializerContext: { logger } as any, + }); mockDiscover.mockReturnValue({ error$: from([ @@ -300,15 +313,7 @@ test('`setup` properly invokes `discover` and ignores non-critical errors.', asy plugin$: from([firstPlugin, secondPlugin]), }); - const contracts = new Map(); - const discoveredPlugins = { public: new Map(), internal: new Map() }; - mockPluginSystem.setupPlugins.mockResolvedValue(contracts); - mockPluginSystem.uiPlugins.mockReturnValue(discoveredPlugins); - - const setup = await pluginsService.setup(setupDeps); - - expect(setup.contracts).toBe(contracts); - expect(setup.uiPlugins).toBe(discoveredPlugins); + await pluginsService.discover(); expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(firstPlugin); expect(mockPluginSystem.addPlugin).toHaveBeenCalledWith(secondPlugin); @@ -325,7 +330,7 @@ test('`setup` properly invokes `discover` and ignores non-critical errors.', asy resolve(process.cwd(), '..', 'kibana-extra'), ], }, - { env, logger, configService } + { coreId, env, logger, configService } ); const logs = loggingServiceMock.collect(logger); @@ -338,7 +343,7 @@ test('`stop` stops plugins system', async () => { expect(mockPluginSystem.stopPlugins).toHaveBeenCalledTimes(1); }); -test('`setup` registers plugin config schema in config service', async () => { +test('`discover` registers plugin config schema in config service', async () => { const configSchema = schema.string(); jest.spyOn(configService, 'setSchema').mockImplementation(() => Promise.resolve()); jest.doMock( @@ -355,9 +360,9 @@ test('`setup` registers plugin config schema in config service', async () => { mockDiscover.mockReturnValue({ error$: from([]), plugin$: from([ - new PluginWrapper( - 'path-with-schema', - { + new PluginWrapper({ + path: 'path-with-schema', + manifest: { id: 'some-id', version: 'some-version', configPath: 'path', @@ -367,10 +372,11 @@ test('`setup` registers plugin config schema in config service', async () => { server: true, ui: true, }, - { logger } as any - ), + opaqueId: Symbol(), + initializerContext: { logger } as any, + }), ]), }); - await pluginsService.setup(setupDeps); + await pluginsService.discover(); expect(configService.setSchema).toBeCalledWith('path', configSchema); }); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 95d3f26fff91e..0fe20e7e59c31 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -25,9 +25,11 @@ import { ElasticsearchServiceSetup } from '../elasticsearch/elasticsearch_servic import { HttpServiceSetup } from '../http/http_service'; import { Logger } from '../logging'; import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery'; -import { DiscoveredPlugin, DiscoveredPluginInternal, PluginWrapper, PluginName } from './plugin'; +import { PluginWrapper } from './plugin'; +import { DiscoveredPlugin, DiscoveredPluginInternal, PluginName } from './types'; import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { PluginsSystem } from './plugins_system'; +import { ContextSetup } from '../context'; /** @public */ export interface PluginsServiceSetup { @@ -45,6 +47,7 @@ export interface PluginsServiceStart { /** @internal */ export interface PluginsServiceSetupDeps { + context: ContextSetup; elasticsearch: ElasticsearchServiceSetup; http: HttpServiceSetup; } @@ -66,8 +69,8 @@ export class PluginsService implements CoreService new PluginsConfig(rawConfig, coreContext.env))); } - public async setup(deps: PluginsServiceSetupDeps) { - this.log.debug('Setting up plugins service'); + public async discover() { + this.log.debug('Discovering plugins'); const config = await this.config$.pipe(first()).toPromise(); @@ -75,6 +78,15 @@ export class PluginsService implements CoreService { env = Env.createDefault(getEnvOptions()); - coreContext = { env, logger, configService: configService as any }; + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; pluginsSystem = new PluginsSystem(coreContext); }); @@ -87,6 +91,27 @@ test('can be setup even without plugins', async () => { expect(pluginsSetup.size).toBe(0); }); +test('getPluginDependencies returns dependency tree of symbols', () => { + pluginsSystem.addPlugin(createPlugin('plugin-a', { required: ['no-dep'] })); + pluginsSystem.addPlugin( + createPlugin('plugin-b', { required: ['plugin-a'], optional: ['no-dep', 'other'] }) + ); + pluginsSystem.addPlugin(createPlugin('no-dep')); + + expect(pluginsSystem.getPluginDependencies()).toMatchInlineSnapshot(` + Map { + Symbol(plugin-a) => Array [ + Symbol(no-dep), + ], + Symbol(plugin-b) => Array [ + Symbol(plugin-a), + Symbol(no-dep), + ], + Symbol(no-dep) => Array [], + } + `); +}); + test('`setupPlugins` throws plugin has missing required dependency', async () => { pluginsSystem.addPlugin(createPlugin('some-id', { required: ['missing-dep'] })); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 37eab8226af72..1f797525ba14f 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -21,7 +21,8 @@ import { pick } from 'lodash'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; -import { DiscoveredPlugin, DiscoveredPluginInternal, PluginWrapper, PluginName } from './plugin'; +import { PluginWrapper } from './plugin'; +import { DiscoveredPlugin, DiscoveredPluginInternal, PluginName, PluginOpaqueId } from './types'; import { createPluginSetupContext, createPluginStartContext } from './plugin_context'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; @@ -40,6 +41,25 @@ export class PluginsSystem { this.plugins.set(plugin.name, plugin); } + /** + * @returns a ReadonlyMap of each plugin and an Array of its available dependencies + * @internal + */ + public getPluginDependencies(): ReadonlyMap { + // Return dependency map of opaque ids + return new Map( + [...this.plugins].map(([name, plugin]) => [ + plugin.opaqueId, + [ + ...new Set([ + ...plugin.requiredPlugins, + ...plugin.optionalPlugins.filter(optPlugin => this.plugins.has(optPlugin)), + ]), + ].map(depId => this.plugins.get(depId)!.opaqueId), + ]) + ); + } + public async setupPlugins(deps: PluginsServiceSetupDeps) { const contracts = new Map(); if (this.plugins.size === 0) { diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts new file mode 100644 index 0000000000000..1372c60eb860e --- /dev/null +++ b/src/core/server/plugins/types.ts @@ -0,0 +1,175 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { Type } from '@kbn/config-schema'; + +import { ConfigPath, EnvironmentMode } from '../config'; +import { LoggerFactory } from '../logging'; +import { CoreSetup, CoreStart } from '..'; + +export type PluginConfigSchema = Type | null; + +/** + * Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays + * that use it as a key or value more obvious. + * + * @public + */ +export type PluginName = string; + +/** @public */ +export type PluginOpaqueId = symbol; + +/** + * Describes the set of required and optional properties plugin can define in its + * mandatory JSON manifest file. + * @internal + */ +export interface PluginManifest { + /** + * Identifier of the plugin. + */ + readonly id: PluginName; + + /** + * Version of the plugin. + */ + readonly version: string; + + /** + * The version of Kibana the plugin is compatible with, defaults to "version". + */ + readonly kibanaVersion: string; + + /** + * Root configuration path used by the plugin, defaults to "id". + */ + readonly configPath: ConfigPath; + + /** + * An optional list of the other plugins that **must be** installed and enabled + * for this plugin to function properly. + */ + readonly requiredPlugins: readonly PluginName[]; + + /** + * An optional list of the other plugins that if installed and enabled **may be** + * leveraged by this plugin for some additional functionality but otherwise are + * not required for this plugin to work properly. + */ + readonly optionalPlugins: readonly PluginName[]; + + /** + * Specifies whether plugin includes some client/browser specific functionality + * that should be included into client bundle via `public/ui_plugin.js` file. + */ + readonly ui: boolean; + + /** + * Specifies whether plugin includes some server-side specific functionality. + */ + readonly server: boolean; +} + +/** + * Small container object used to expose information about discovered plugins that may + * or may not have been started. + * @public + */ +export interface DiscoveredPlugin { + /** + * Identifier of the plugin. + */ + readonly id: PluginName; + + /** + * Root configuration path used by the plugin, defaults to "id". + */ + readonly configPath: ConfigPath; + + /** + * An optional list of the other plugins that **must be** installed and enabled + * for this plugin to function properly. + */ + readonly requiredPlugins: readonly PluginName[]; + + /** + * An optional list of the other plugins that if installed and enabled **may be** + * leveraged by this plugin for some additional functionality but otherwise are + * not required for this plugin to work properly. + */ + readonly optionalPlugins: readonly PluginName[]; +} + +/** + * An extended `DiscoveredPlugin` that exposes more sensitive information. Should never + * be exposed to client-side code. + * @internal + */ +export interface DiscoveredPluginInternal extends DiscoveredPlugin { + /** + * Path on the filesystem where plugin was loaded from. + */ + readonly path: string; +} + +/** + * The interface that should be returned by a `PluginInitializer`. + * + * @public + */ +export interface Plugin< + TSetup = void, + TStart = void, + TPluginsSetup extends object = object, + TPluginsStart extends object = object +> { + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + stop?(): void; +} + +/** + * Context that's available to plugins during initialization stage. + * + * @public + */ +export interface PluginInitializerContext { + opaqueId: PluginOpaqueId; + env: { mode: EnvironmentMode }; + logger: LoggerFactory; + config: { + create: () => Observable; + createIfExists: () => Observable; + }; +} + +/** + * The `plugin` export at the root of a plugin's `server` directory should conform + * to this interface. + * + * @public + */ +export type PluginInitializer< + TSetup, + TStart, + TPluginsSetup extends object = object, + TPluginsStart extends object = object +> = (core: PluginInitializerContext) => Plugin; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 74624020fe79f..b345677b5e89e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -92,8 +92,24 @@ export class ConfigService { setSchema(path: ConfigPath, schema: Type): Promise; } +// Warning: (ae-unresolved-inheritdoc-reference) The @inheritDoc reference could not be resolved: The package "kibana" does not have an export "IContextContainer" +// +// @public (undocumented) +export interface ContextSetup { + // Warning: (ae-forgotten-export) The symbol "IContextContainer" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "IContextContainer" + createContextContainer(): IContextContainer; +} + +// @internal (undocumented) +export type CoreId = symbol; + // @public export interface CoreSetup { + // (undocumented) + context: { + createContextContainer: ContextSetup['createContextContainer']; + }; // (undocumented) elasticsearch: { adminClient$: Observable; @@ -438,11 +454,16 @@ export interface PluginInitializerContext { }; // (undocumented) logger: LoggerFactory; + // (undocumented) + opaqueId: PluginOpaqueId; } // @public export type PluginName = string; +// @public (undocumented) +export type PluginOpaqueId = symbol; + // @public (undocumented) export interface PluginsServiceSetup { // (undocumented) @@ -995,7 +1016,7 @@ export interface SessionStorageFactory { // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:188:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/plugin_context.ts:34:10 - (ae-forgotten-export) The symbol "EnvironmentMode" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/plugins_service.ts:37:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/plugins_service.ts:39:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:156:10 - (ae-forgotten-export) The symbol "EnvironmentMode" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 01f2673c3f9e5..4c7f1b6ad1a53 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -31,9 +31,11 @@ import { config as httpConfig } from './http'; import { config as loggingConfig } from './logging'; import { config as devConfig } from './dev'; import { mapToObject } from '../utils/'; +import { ContextService } from './context'; export class Server { public readonly configService: ConfigService; + private readonly context: ContextService; private readonly elasticsearch: ElasticsearchService; private readonly http: HttpService; private readonly plugins: PluginsService; @@ -48,7 +50,8 @@ export class Server { this.log = this.logger.get('server'); this.configService = new ConfigService(config$, env, logger); - const core = { configService: this.configService, env, logger }; + const core = { coreId: Symbol('core'), configService: this.configService, env, logger }; + this.context = new ContextService(core); this.http = new HttpService(core); this.plugins = new PluginsService(core); this.legacy = new LegacyService(core); @@ -58,19 +61,25 @@ export class Server { public async setup() { this.log.debug('setting up server'); + // Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph. + const pluginDependencies = await this.plugins.discover(); + const httpSetup = await this.http.setup(); this.registerDefaultRoute(httpSetup); + const contextServiceSetup = this.context.setup({ pluginDependencies }); const elasticsearchServiceSetup = await this.elasticsearch.setup({ http: httpSetup, }); const pluginsSetup = await this.plugins.setup({ + context: contextServiceSetup, elasticsearch: elasticsearchServiceSetup, http: httpSetup, }); const coreSetup = { + context: contextServiceSetup, elasticsearch: elasticsearchServiceSetup, http: httpSetup, plugins: pluginsSetup, diff --git a/src/core/server/types.ts b/src/core/server/types.ts new file mode 100644 index 0000000000000..9b55da17a40a8 --- /dev/null +++ b/src/core/server/types.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** This module is intended for consumption by public to avoid import issues with server-side code */ + +export { PluginOpaqueId } from './plugins/types'; diff --git a/src/core/public/context/context.mock.ts b/src/core/utils/context.mock.ts similarity index 100% rename from src/core/public/context/context.mock.ts rename to src/core/utils/context.mock.ts diff --git a/src/core/public/context/context.test.ts b/src/core/utils/context.test.ts similarity index 99% rename from src/core/public/context/context.test.ts rename to src/core/utils/context.test.ts index d05662af73908..78d067388853a 100644 --- a/src/core/public/context/context.test.ts +++ b/src/core/utils/context.test.ts @@ -18,7 +18,7 @@ */ import { ContextContainer } from './context'; -import { PluginOpaqueId } from '../plugins'; +import { PluginOpaqueId } from '../server'; const pluginA = Symbol('pluginA'); const pluginB = Symbol('pluginB'); diff --git a/src/core/public/context/context.ts b/src/core/utils/context.ts similarity index 98% rename from src/core/public/context/context.ts rename to src/core/utils/context.ts index 28f1b8e6ea878..2efe68cc5d764 100644 --- a/src/core/public/context/context.ts +++ b/src/core/utils/context.ts @@ -18,9 +18,8 @@ */ import { flatten } from 'lodash'; -import { pick } from '../../utils'; -import { CoreId } from '../core_system'; -import { PluginOpaqueId } from '../plugins'; +import { pick } from '.'; +import { CoreId, PluginOpaqueId } from '../server'; /** * A function that returns a context value for a specific key of given context type. diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index d4391f576df4a..b6ffb57db975c 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -17,9 +17,10 @@ * under the License. */ +export * from './assert_never'; +export * from './context'; +export * from './deep_freeze'; export * from './get'; export * from './map_to_object'; export * from './pick'; -export * from './assert_never'; export * from './url'; -export * from './deep_freeze'; diff --git a/test/tsconfig.json b/test/tsconfig.json index 34782c7b7598b..26d69347df5a9 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -13,6 +13,7 @@ "include": [ "**/*.ts", "**/*.tsx", + "../typings/lodash.topath/*.ts", ], "exclude": [ "plugin_functional/plugins/**/*" From f7568e6fbfe62136f09a110efba37504cadb6eb2 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Tue, 6 Aug 2019 19:49:07 +0200 Subject: [PATCH 11/26] Fixes #40714: Import API don't override SavedObjectClient errors (#41125) * Import API: Don't override SavedObjectClient errors * Review comments: rely on default hapi error behaviour --- .../core_plugins/kibana/server/routes/api/export/index.js | 8 ++++---- .../core_plugins/kibana/server/routes/api/import/index.js | 7 +------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/legacy/core_plugins/kibana/server/routes/api/export/index.js b/src/legacy/core_plugins/kibana/server/routes/api/export/index.js index a2469d8f5c5ae..d47aa4b52a0b6 100644 --- a/src/legacy/core_plugins/kibana/server/routes/api/export/index.js +++ b/src/legacy/core_plugins/kibana/server/routes/api/export/index.js @@ -17,10 +17,11 @@ * under the License. */ -import { exportDashboards } from '../../../lib/export/export_dashboards'; -import Boom from 'boom'; import Joi from 'joi'; import moment from 'moment'; + +import { exportDashboards } from '../../../lib/export/export_dashboards'; + export function exportApi(server) { server.route({ path: '/api/kibana/dashboards/export', @@ -46,8 +47,7 @@ export function exportApi(server) { .header('Content-Disposition', `attachment; filename="${filename}"`) .header('Content-Type', 'application/json') .header('Content-Length', Buffer.byteLength(json, 'utf8')); - }) - .catch(err => Boom.boomify(err, { statusCode: 400 })); + }); } }); } diff --git a/src/legacy/core_plugins/kibana/server/routes/api/import/index.js b/src/legacy/core_plugins/kibana/server/routes/api/import/index.js index c7291798c6d65..7926e8775eeef 100644 --- a/src/legacy/core_plugins/kibana/server/routes/api/import/index.js +++ b/src/legacy/core_plugins/kibana/server/routes/api/import/index.js @@ -17,7 +17,6 @@ * under the License. */ -import Boom from 'boom'; import Joi from 'joi'; import { importDashboards } from '../../../lib/import/import_dashboards'; @@ -40,11 +39,7 @@ export function importApi(server) { }, handler: async (req) => { - try { - return await importDashboards(req); - } catch (err) { - throw Boom.boomify(err, { statusCode: 400 }); - } + return await importDashboards(req); } }); } From fe1389cc90832dd4e4d22b41f7a22e11029538d7 Mon Sep 17 00:00:00 2001 From: Mengwei Ding Date: Tue, 6 Aug 2019 11:43:37 -0700 Subject: [PATCH 12/26] [Code] Do not persist git update error (#42629) * [Code] Update log level for scheduler * [Code] Do not persist git update error * remove console.log --- .../code/server/queue/update_worker.test.ts | 56 ++++++++++++++++++- .../code/server/queue/update_worker.ts | 17 +++++- .../code/server/scheduler/index_scheduler.ts | 10 ++-- .../code/server/scheduler/update_scheduler.ts | 4 +- 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/x-pack/legacy/plugins/code/server/queue/update_worker.test.ts b/x-pack/legacy/plugins/code/server/queue/update_worker.test.ts index 57b936dc827b4..199a5f372fd7a 100644 --- a/x-pack/legacy/plugins/code/server/queue/update_worker.test.ts +++ b/x-pack/legacy/plugins/code/server/queue/update_worker.test.ts @@ -20,7 +20,10 @@ import { UpdateWorker } from './update_worker'; const log: Logger = new ConsoleLoggerFactory().getLogger(['test']); -const esClient = {}; +const esClient = { + update: emptyAsyncFunc, + get: emptyAsyncFunc, +}; const esQueue = {}; afterEach(() => { @@ -213,3 +216,54 @@ test('Execute update job failed because of low disk watermark ', async () => { expect(updateSpy.notCalled).toBeTruthy(); } }); + +test('On update job error or timeout will not persis error', async () => { + // Setup EsClient + const esUpdateSpy = sinon.spy(); + esClient.update = esUpdateSpy; + + // Setup CancellationService + const cancelUpdateJobSpy = sinon.spy(); + const registerCancelableUpdateJobSpy = sinon.spy(); + const cancellationService: any = { + cancelUpdateJob: emptyAsyncFunc, + registerCancelableUpdateJob: emptyAsyncFunc, + }; + cancellationService.cancelUpdateJob = cancelUpdateJobSpy; + cancellationService.registerCancelableUpdateJob = registerCancelableUpdateJobSpy; + + const updateWorker = new UpdateWorker( + esQueue as Esqueue, + log, + esClient as EsClient, + { + security: { + enableGitCertCheck: true, + }, + disk: { + thresholdEnabled: true, + watermarkLowMb: 100, + }, + } as ServerOptions, + {} as GitOperations, + {} as RepositoryServiceFactory, + cancellationService as CancellationSerivce, + {} as DiskWatermarkService + ); + + await updateWorker.onJobExecutionError({ + job: { + payload: { + uri: 'mockrepo', + }, + options: {}, + timestamp: 0, + }, + error: 'mock error message', + }); + + // The elasticsearch update will be called and the progress should be 'Completed' + expect(esUpdateSpy.calledOnce).toBeTruthy(); + const updateBody = JSON.parse(esUpdateSpy.getCall(0).args[0].body); + expect(updateBody.doc.repository_git_status.progress).toBe(100); +}); diff --git a/x-pack/legacy/plugins/code/server/queue/update_worker.ts b/x-pack/legacy/plugins/code/server/queue/update_worker.ts index ee709f1116728..60411c4dcae64 100644 --- a/x-pack/legacy/plugins/code/server/queue/update_worker.ts +++ b/x-pack/legacy/plugins/code/server/queue/update_worker.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CloneWorkerResult, Repository } from '../../model'; +import { CloneWorkerResult, Repository, WorkerReservedProgress } from '../../model'; import { EsClient, Esqueue } from '../lib/esqueue'; import { DiskWatermarkService } from '../disk_watermark'; import { GitOperations } from '../git_operations'; @@ -77,4 +77,19 @@ export class UpdateWorker extends AbstractGitWorker { this.log.info(`Update job done for ${job.payload.uri}`); return await super.onJobCompleted(job, res); } + + public async onJobExecutionError(res: any) { + return await this.overrideUpdateErrorProgress(res); + } + + public async onJobTimeOut(res: any) { + return await this.overrideUpdateErrorProgress(res); + } + + private async overrideUpdateErrorProgress(res: any) { + this.log.warn(`Update job error`); + this.log.warn(res.error); + // Do not persist update errors assuming the next update trial is scheduling soon. + return await this.updateProgress(res.job, WorkerReservedProgress.COMPLETED); + } } diff --git a/x-pack/legacy/plugins/code/server/scheduler/index_scheduler.ts b/x-pack/legacy/plugins/code/server/scheduler/index_scheduler.ts index 8008f44e6a91e..e8f8ba1928a5c 100644 --- a/x-pack/legacy/plugins/code/server/scheduler/index_scheduler.ts +++ b/x-pack/legacy/plugins/code/server/scheduler/index_scheduler.ts @@ -32,7 +32,7 @@ export class IndexScheduler extends AbstractScheduler { } protected async executeSchedulingJob(repo: Repository) { - this.log.info(`Schedule index repo request for ${repo.uri}`); + this.log.debug(`Schedule index repo request for ${repo.uri}`); try { // This repository is too soon to execute the next index job. if (repo.nextIndexTimestamp && new Date() < new Date(repo.nextIndexTimestamp)) { @@ -44,7 +44,7 @@ export class IndexScheduler extends AbstractScheduler { !RepositoryUtils.hasFullyCloned(cloneStatus.cloneProgress) || cloneStatus.progress !== WorkerReservedProgress.COMPLETED ) { - this.log.info(`Repo ${repo.uri} has not been fully cloned yet or in update. Skip index.`); + this.log.debug(`Repo ${repo.uri} has not been fully cloned yet or in update. Skip index.`); return; } @@ -52,19 +52,19 @@ export class IndexScheduler extends AbstractScheduler { // Schedule index job only when the indexed revision is different from the current repository // revision. - this.log.info( + this.log.debug( `Current repo revision: ${repo.revision}, indexed revision ${repoIndexStatus.revision}.` ); if ( repoIndexStatus.progress >= 0 && repoIndexStatus.progress < WorkerReservedProgress.COMPLETED ) { - this.log.info(`Repo is still in indexing. Skip index for ${repo.uri}`); + this.log.debug(`Repo is still in indexing. Skip index for ${repo.uri}`); } else if ( repoIndexStatus.progress === WorkerReservedProgress.COMPLETED && repoIndexStatus.revision === repo.revision ) { - this.log.info(`Repo does not change since last index. Skip index for ${repo.uri}.`); + this.log.debug(`Repo does not change since last index. Skip index for ${repo.uri}.`); } else { const payload = { uri: repo.uri, diff --git a/x-pack/legacy/plugins/code/server/scheduler/update_scheduler.ts b/x-pack/legacy/plugins/code/server/scheduler/update_scheduler.ts index 1ff0f29095c6a..7cc2daa0fbe60 100644 --- a/x-pack/legacy/plugins/code/server/scheduler/update_scheduler.ts +++ b/x-pack/legacy/plugins/code/server/scheduler/update_scheduler.ts @@ -41,7 +41,7 @@ export class UpdateScheduler extends AbstractScheduler { this.log.debug(`Repo ${repo.uri} is too soon to execute the next update job.`); return; } - this.log.info(`Start to schedule update repo request for ${repo.uri}`); + this.log.debug(`Start to schedule update repo request for ${repo.uri}`); let inDelete = false; try { @@ -69,7 +69,7 @@ export class UpdateScheduler extends AbstractScheduler { await this.updateWorker.enqueueJob(payload, {}); } else { - this.log.info( + this.log.debug( `Repo ${repo.uri} has not been fully cloned yet or in update/delete. Skip update.` ); } From e137477b5a00b8c83e63acda1121c1d2ee10541c Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Tue, 6 Aug 2019 20:53:05 +0200 Subject: [PATCH 13/26] [Core] Public saved objects client (#39891) * Move SavedObjectClient files to core/public * Initial SavedObjectsService in public Core * Public SavedObjectsClient tests * Import SimpleSavedObject from src/core/public * Use types from source files for kibana.d.ts * Add html raw loader to x-pack/jest * Cleanup * Drop case utilities and improve test coverage * Update types and documentation * Fix build breaking when importing directly from /server in /public * Ensure that all option paramaters are picked and renamed * Fix option mapping and introduce stronger types * Eslint: allow imports from src/core/*/types * Add compatibility layer for kfetch vs http.fetch error responses * Improve documentation * Expose SavedObjectsMigrationLogger * Documentation and type tweaks * Revert type changes from 73e601f and update api docs * Refactor request into savedObjectFetch + test * Make legacy SavedObject compatible with http.fetch exceptions * Fix types and tests * simple_saved_object import from src/core/server * server imports from root instead of server/types * Make SavedObjectsService a class * Don't pick unkown keys from server response * Rename SavedObjectsFindResponse to SavedObjectsFindResponsePublic * Remove err.response from SavedObjects fetch errors * Revert "Remove err.response from SavedObjects fetch errors" This reverts commit 61705ca3611bccefe12e0a6569f7c3b8c6abaa67. * Don't introduce err.response until we deprecate err.res --- .../public/kibana-plugin-public.corestart.md | 1 + ...na-plugin-public.corestart.savedobjects.md | 13 + .../core/public/kibana-plugin-public.md | 17 + ...na-plugin-public.savedobject.attributes.md | 13 + .../kibana-plugin-public.savedobject.error.md | 14 + .../kibana-plugin-public.savedobject.id.md | 13 + .../kibana-plugin-public.savedobject.md | 26 ++ ...gin-public.savedobject.migrationversion.md | 13 + ...na-plugin-public.savedobject.references.md | 13 + .../kibana-plugin-public.savedobject.type.md | 13 + ...na-plugin-public.savedobject.updated_at.md | 13 + ...ibana-plugin-public.savedobject.version.md | 13 + ...bana-plugin-public.savedobjectattribute.md | 12 + ...ana-plugin-public.savedobjectattributes.md | 13 + ...a-plugin-public.savedobjectreference.id.md | 11 + ...bana-plugin-public.savedobjectreference.md | 22 + ...plugin-public.savedobjectreference.name.md | 11 + ...plugin-public.savedobjectreference.type.md | 11 + ...a-plugin-public.savedobjectsbaseoptions.md | 19 + ...ublic.savedobjectsbaseoptions.namespace.md | 13 + ...plugin-public.savedobjectsbatchresponse.md | 19 + ....savedobjectsbatchresponse.savedobjects.md | 11 + ...savedobjectsbulkcreateobject.attributes.md | 11 + ...gin-public.savedobjectsbulkcreateobject.md | 19 + ...ublic.savedobjectsbulkcreateobject.type.md | 11 + ...in-public.savedobjectsbulkcreateoptions.md | 19 + ...savedobjectsbulkcreateoptions.overwrite.md | 13 + ...in-public.savedobjectsclient.bulkcreate.md | 13 + ...lugin-public.savedobjectsclient.bulkget.md | 21 + ...plugin-public.savedobjectsclient.create.md | 13 + ...plugin-public.savedobjectsclient.delete.md | 13 + ...a-plugin-public.savedobjectsclient.find.md | 13 + ...na-plugin-public.savedobjectsclient.get.md | 13 + ...kibana-plugin-public.savedobjectsclient.md | 35 ++ ...plugin-public.savedobjectsclient.update.md | 28 ++ ...lugin-public.savedobjectsclientcontract.md | 13 + ...gin-public.savedobjectscreateoptions.id.md | 13 + ...plugin-public.savedobjectscreateoptions.md | 22 + ...edobjectscreateoptions.migrationversion.md | 13 + ...lic.savedobjectscreateoptions.overwrite.md | 13 + ...ic.savedobjectscreateoptions.references.md | 11 + ...bjectsfindoptions.defaultsearchoperator.md | 11 + ...n-public.savedobjectsfindoptions.fields.md | 18 + ...ic.savedobjectsfindoptions.hasreference.md | 14 + ...a-plugin-public.savedobjectsfindoptions.md | 28 ++ ...gin-public.savedobjectsfindoptions.page.md | 11 + ...-public.savedobjectsfindoptions.perpage.md | 11 + ...n-public.savedobjectsfindoptions.search.md | 13 + ...ic.savedobjectsfindoptions.searchfields.md | 13 + ...ublic.savedobjectsfindoptions.sortfield.md | 11 + ...ublic.savedobjectsfindoptions.sortorder.md | 11 + ...gin-public.savedobjectsfindoptions.type.md | 11 + ...n-public.savedobjectsfindresponsepublic.md | 24 + ...lic.savedobjectsfindresponsepublic.page.md | 11 + ....savedobjectsfindresponsepublic.perpage.md | 11 + ...ic.savedobjectsfindresponsepublic.total.md | 11 + ...gin-public.savedobjectsmigrationversion.md | 18 + ...-plugin-public.savedobjectsstart.client.md | 13 + .../kibana-plugin-public.savedobjectsstart.md | 19 + ...plugin-public.savedobjectsupdateoptions.md | 21 + ...edobjectsupdateoptions.migrationversion.md | 13 + ...ic.savedobjectsupdateoptions.references.md | 11 + ...ublic.savedobjectsupdateoptions.version.md | 11 + ...-public.simplesavedobject.(constructor).md | 21 + ...lugin-public.simplesavedobject._version.md | 11 + ...gin-public.simplesavedobject.attributes.md | 11 + ...-plugin-public.simplesavedobject.delete.md | 15 + ...a-plugin-public.simplesavedobject.error.md | 11 + ...ana-plugin-public.simplesavedobject.get.md | 22 + ...ana-plugin-public.simplesavedobject.has.md | 22 + ...bana-plugin-public.simplesavedobject.id.md | 11 + .../kibana-plugin-public.simplesavedobject.md | 44 ++ ...blic.simplesavedobject.migrationversion.md | 11 + ...gin-public.simplesavedobject.references.md | 11 + ...na-plugin-public.simplesavedobject.save.md | 15 + ...ana-plugin-public.simplesavedobject.set.md | 23 + ...na-plugin-public.simplesavedobject.type.md | 11 + .../core/server/kibana-plugin-server.md | 10 +- ...na-plugin-server.savedobject.attributes.md | 2 + .../kibana-plugin-server.savedobject.id.md | 2 + .../kibana-plugin-server.savedobject.md | 14 +- ...gin-server.savedobject.migrationversion.md | 2 + ...na-plugin-server.savedobject.references.md | 2 + .../kibana-plugin-server.savedobject.type.md | 2 + ...na-plugin-server.savedobject.updated_at.md | 2 + ...ibana-plugin-server.savedobject.version.md | 2 + ...bana-plugin-server.savedobjectattribute.md | 12 + ...ana-plugin-server.savedobjectattributes.md | 1 + ...gin-server.savedobjectsbulkcreateobject.md | 2 +- ...bjectsbulkcreateobject.migrationversion.md | 2 + ...lugin-server.savedobjectsclientcontract.md | 2 + ...plugin-server.savedobjectscreateoptions.md | 2 +- ...edobjectscreateoptions.migrationversion.md | 2 + ...n-server.savedobjectsfindoptions.fields.md | 7 + ...a-plugin-server.savedobjectsfindoptions.md | 6 +- ...n-server.savedobjectsfindoptions.search.md | 2 + ...er.savedobjectsfindoptions.searchfields.md | 2 +- ...-plugin-server.savedobjectsfindresponse.md | 3 + ...erver.savedobjectsmigrationlogger.debug.md | 11 + ...server.savedobjectsmigrationlogger.info.md | 11 + ...ugin-server.savedobjectsmigrationlogger.md | 21 + ...ver.savedobjectsmigrationlogger.warning.md | 11 + ...gin-server.savedobjectsmigrationversion.md | 7 +- kibana.d.ts | 4 +- src/core/public/core_system.ts | 5 + src/core/public/http/http_service.mock.ts | 4 +- src/core/public/index.ts | 23 + src/core/public/legacy/legacy_service.test.ts | 3 + src/core/public/mocks.ts | 2 + src/core/public/plugins/plugin_context.ts | 1 + .../public/plugins/plugins_service.test.ts | 2 + src/core/public/public.api.md | 173 +++++++ src/core/public/saved_objects/index.ts | 40 ++ .../saved_objects_client.test.ts | 433 ++++++++++++++++++ .../saved_objects/saved_objects_client.ts | 218 ++++++--- .../saved_objects_service.mock.ts | 50 ++ .../saved_objects/saved_objects_service.ts | 38 ++ .../saved_objects/simple_saved_object.ts | 11 +- src/core/server/index.ts | 41 +- .../export/get_sorted_objects_for_export.ts | 2 +- .../inject_nested_depdendencies.test.ts | 2 +- .../export/inject_nested_depdendencies.ts | 2 +- .../saved_objects/export/sort_objects.ts | 2 +- .../import/collect_saved_objects.ts | 2 +- .../import/create_objects_filter.ts | 2 +- .../import/extract_errors.test.ts | 2 +- .../saved_objects/import/extract_errors.ts | 3 +- .../import/import_saved_objects.test.ts | 2 +- .../import/resolve_import_errors.test.ts | 2 +- .../import/resolve_import_errors.ts | 1 - .../saved_objects/import/split_overwrites.ts | 2 +- src/core/server/saved_objects/import/types.ts | 2 +- .../import/validate_references.ts | 2 +- src/core/server/saved_objects/index.ts | 2 + .../saved_objects/management/management.ts | 2 +- .../migrations/core/document_migrator.ts | 21 +- .../migrations/core/elastic_index.ts | 2 +- .../migrations/core/migration_context.ts | 4 +- .../migrations/core/migration_coordinator.ts | 6 +- .../migrations/core/migration_logger.ts | 5 +- .../saved_objects/serialization/index.ts | 5 +- .../saved_objects/service/lib/repository.ts | 12 +- .../service/lib/scoped_client_provider.ts | 2 +- .../service/saved_objects_client.mock.ts | 2 +- .../service/saved_objects_client.ts | 169 +------ src/core/server/saved_objects/types.ts | 203 ++++++++ src/core/server/server.api.md | 28 +- src/core/server/types.ts | 2 +- .../server/types.ts~master} | 10 +- src/dev/run_check_core_api_changes.ts | 2 +- .../stubbed_saved_object_index_pattern.js | 2 +- .../embeddable/dashboard_container_factory.ts | 2 +- .../add_panel/add_panel_flyout.tsx | 3 +- .../dashboard/migrations/migrations_730.ts | 4 +- .../__tests__/directives/field_chooser.js | 2 +- .../public/chrome/api/saved_object_client.ts | 29 ++ src/legacy/ui/public/chrome/index.d.ts | 4 +- .../public/index_patterns/_index_pattern.ts | 9 +- .../public/index_patterns/index_patterns.ts | 7 +- .../__tests__/find_object_by_title.js | 2 +- .../__tests__/simple_saved_object.js | 2 +- .../components/saved_object_finder.tsx | 2 +- .../saved_objects/find_object_by_title.ts | 8 +- src/legacy/ui/public/saved_objects/index.ts | 2 - .../saved_objects_client.test.ts | 357 --------------- .../saved_objects_client_provider.ts | 2 +- x-pack/dev-tools/jest/create_jest_config.js | 1 + 167 files changed, 2586 insertions(+), 712 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.corestart.savedobjects.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobject.attributes.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobject.error.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobject.id.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobject.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobject.migrationversion.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobject.references.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobject.type.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobject.updated_at.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobject.version.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectattribute.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectattributes.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectreference.id.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectreference.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectreference.name.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectreference.type.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsbaseoptions.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsbaseoptions.namespace.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.savedobjects.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.attributes.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.type.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateoptions.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateoptions.overwrite.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkcreate.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkget.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsclient.create.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsclient.delete.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsclient.get.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsclient.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsclient.update.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsclientcontract.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.id.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.migrationversion.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.overwrite.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.references.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.fields.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.hasreference.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.page.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.perpage.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.search.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.searchfields.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.sortfield.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.sortorder.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.type.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.page.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.perpage.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.total.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsmigrationversion.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsstart.client.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsstart.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.migrationversion.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.references.md create mode 100644 docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.version.md create mode 100644 docs/development/core/public/kibana-plugin-public.simplesavedobject.(constructor).md create mode 100644 docs/development/core/public/kibana-plugin-public.simplesavedobject._version.md create mode 100644 docs/development/core/public/kibana-plugin-public.simplesavedobject.attributes.md create mode 100644 docs/development/core/public/kibana-plugin-public.simplesavedobject.delete.md create mode 100644 docs/development/core/public/kibana-plugin-public.simplesavedobject.error.md create mode 100644 docs/development/core/public/kibana-plugin-public.simplesavedobject.get.md create mode 100644 docs/development/core/public/kibana-plugin-public.simplesavedobject.has.md create mode 100644 docs/development/core/public/kibana-plugin-public.simplesavedobject.id.md create mode 100644 docs/development/core/public/kibana-plugin-public.simplesavedobject.md create mode 100644 docs/development/core/public/kibana-plugin-public.simplesavedobject.migrationversion.md create mode 100644 docs/development/core/public/kibana-plugin-public.simplesavedobject.references.md create mode 100644 docs/development/core/public/kibana-plugin-public.simplesavedobject.save.md create mode 100644 docs/development/core/public/kibana-plugin-public.simplesavedobject.set.md create mode 100644 docs/development/core/public/kibana-plugin-public.simplesavedobject.type.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectattribute.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.debug.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.info.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.warning.md create mode 100644 src/core/public/saved_objects/index.ts create mode 100644 src/core/public/saved_objects/saved_objects_client.test.ts rename src/{legacy/ui => core}/public/saved_objects/saved_objects_client.ts (58%) create mode 100644 src/core/public/saved_objects/saved_objects_service.mock.ts create mode 100644 src/core/public/saved_objects/saved_objects_service.ts rename src/{legacy/ui => core}/public/saved_objects/simple_saved_object.ts (90%) create mode 100644 src/core/server/saved_objects/types.ts rename src/{legacy/ui/public/chrome/api/saved_object_client.js => core/server/types.ts~master} (76%) create mode 100644 src/legacy/ui/public/chrome/api/saved_object_client.ts delete mode 100644 src/legacy/ui/public/saved_objects/saved_objects_client.test.ts diff --git a/docs/development/core/public/kibana-plugin-public.corestart.md b/docs/development/core/public/kibana-plugin-public.corestart.md index d22bcb09a1054..446e458735214 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.md @@ -23,5 +23,6 @@ export interface CoreStart | [i18n](./kibana-plugin-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-public.i18nstart.md) | | [notifications](./kibana-plugin-public.corestart.notifications.md) | NotificationsStart | [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | | [overlays](./kibana-plugin-public.corestart.overlays.md) | OverlayStart | [OverlayStart](./kibana-plugin-public.overlaystart.md) | +| [savedObjects](./kibana-plugin-public.corestart.savedobjects.md) | SavedObjectsStart | [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) | | [uiSettings](./kibana-plugin-public.corestart.uisettings.md) | UiSettingsClientContract | [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | diff --git a/docs/development/core/public/kibana-plugin-public.corestart.savedobjects.md b/docs/development/core/public/kibana-plugin-public.corestart.savedobjects.md new file mode 100644 index 0000000000000..5e6e0e33c7f80 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.corestart.savedobjects.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [savedObjects](./kibana-plugin-public.corestart.savedobjects.md) + +## CoreStart.savedObjects property + +[SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) + +Signature: + +```typescript +savedObjects: SavedObjectsStart; +``` diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index afd22ecf174b4..5fda9f9159306 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -14,6 +14,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | Class | Description | | --- | --- | +| [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state. The client-side SavedObjectsClient is a thin convenience library around the SavedObjects HTTP API for interacting with Saved Objects. | +| [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) | This class is a very simple wrapper for SavedObjects loaded from the server with the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md).It provides basic functionality for creating/saving/deleting saved objects, but doesn't include any type-specific implementations. | | [ToastsApi](./kibana-plugin-public.toastsapi.md) | | | [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | | @@ -59,6 +61,19 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OverlayStart](./kibana-plugin-public.overlaystart.md) | | | [Plugin](./kibana-plugin-public.plugin.md) | The interface that should be returned by a PluginInitializer. | | [PluginInitializerContext](./kibana-plugin-public.plugininitializercontext.md) | The available core services passed to a PluginInitializer | +| [SavedObject](./kibana-plugin-public.savedobject.md) | | +| [SavedObjectAttributes](./kibana-plugin-public.savedobjectattributes.md) | The data for a Saved Object is stored in the attributes key as either an object or an array of objects. | +| [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) | A reference to another saved object. | +| [SavedObjectsBaseOptions](./kibana-plugin-public.savedobjectsbaseoptions.md) | | +| [SavedObjectsBatchResponse](./kibana-plugin-public.savedobjectsbatchresponse.md) | | +| [SavedObjectsBulkCreateObject](./kibana-plugin-public.savedobjectsbulkcreateobject.md) | | +| [SavedObjectsBulkCreateOptions](./kibana-plugin-public.savedobjectsbulkcreateoptions.md) | | +| [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) | | +| [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) | | +| [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | +| [SavedObjectsMigrationVersion](./kibana-plugin-public.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) | | +| [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) | | | [UiSettingsState](./kibana-plugin-public.uisettingsstate.md) | | ## Type Aliases @@ -76,6 +91,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | | [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md) | | | [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | | +| [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) | | +| [SavedObjectsClientContract](./kibana-plugin-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | | [ToastInput](./kibana-plugin-public.toastinput.md) | | | [UiSettingsClientContract](./kibana-plugin-public.uisettingsclientcontract.md) | [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.attributes.md b/docs/development/core/public/kibana-plugin-public.savedobject.attributes.md new file mode 100644 index 0000000000000..f9d39c15fcff4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.attributes.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [attributes](./kibana-plugin-public.savedobject.attributes.md) + +## SavedObject.attributes property + +The data for a Saved Object is stored in the `attributes` key as either an object or an array of objects. + +Signature: + +```typescript +attributes: T; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.error.md b/docs/development/core/public/kibana-plugin-public.savedobject.error.md new file mode 100644 index 0000000000000..1d00863ef6ecf --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.error.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [error](./kibana-plugin-public.savedobject.error.md) + +## SavedObject.error property + +Signature: + +```typescript +error?: { + message: string; + statusCode: number; + }; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.id.md b/docs/development/core/public/kibana-plugin-public.savedobject.id.md new file mode 100644 index 0000000000000..7b54e0a7c2a74 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [id](./kibana-plugin-public.savedobject.id.md) + +## SavedObject.id property + +The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.md b/docs/development/core/public/kibana-plugin-public.savedobject.md new file mode 100644 index 0000000000000..9bf0149f0854e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) + +## SavedObject interface + + +Signature: + +```typescript +export interface SavedObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [attributes](./kibana-plugin-public.savedobject.attributes.md) | T | The data for a Saved Object is stored in the attributes key as either an object or an array of objects. | +| [error](./kibana-plugin-public.savedobject.error.md) | {
message: string;
statusCode: number;
} | | +| [id](./kibana-plugin-public.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | +| [migrationVersion](./kibana-plugin-public.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [references](./kibana-plugin-public.savedobject.references.md) | SavedObjectReference[] | A reference to another saved object. | +| [type](./kibana-plugin-public.savedobject.type.md) | string | The type of Saved Object. Each plugin can define it's own custom Saved Object types. | +| [updated\_at](./kibana-plugin-public.savedobject.updated_at.md) | string | Timestamp of the last time this document had been updated. | +| [version](./kibana-plugin-public.savedobject.version.md) | string | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.migrationversion.md b/docs/development/core/public/kibana-plugin-public.savedobject.migrationversion.md new file mode 100644 index 0000000000000..d07b664f11ff2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.migrationversion.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [migrationVersion](./kibana-plugin-public.savedobject.migrationversion.md) + +## SavedObject.migrationVersion property + +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. + +Signature: + +```typescript +migrationVersion?: SavedObjectsMigrationVersion; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.references.md b/docs/development/core/public/kibana-plugin-public.savedobject.references.md new file mode 100644 index 0000000000000..3c3ecdd283b5f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.references.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [references](./kibana-plugin-public.savedobject.references.md) + +## SavedObject.references property + +A reference to another saved object. + +Signature: + +```typescript +references: SavedObjectReference[]; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.type.md b/docs/development/core/public/kibana-plugin-public.savedobject.type.md new file mode 100644 index 0000000000000..2bce5b8a15634 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [type](./kibana-plugin-public.savedobject.type.md) + +## SavedObject.type property + +The type of Saved Object. Each plugin can define it's own custom Saved Object types. + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.updated_at.md b/docs/development/core/public/kibana-plugin-public.savedobject.updated_at.md new file mode 100644 index 0000000000000..861128c69ae2a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.updated_at.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [updated\_at](./kibana-plugin-public.savedobject.updated_at.md) + +## SavedObject.updated\_at property + +Timestamp of the last time this document had been updated. + +Signature: + +```typescript +updated_at?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobject.version.md b/docs/development/core/public/kibana-plugin-public.savedobject.version.md new file mode 100644 index 0000000000000..26356f444f2ca --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobject.version.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObject](./kibana-plugin-public.savedobject.md) > [version](./kibana-plugin-public.savedobject.version.md) + +## SavedObject.version property + +An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. + +Signature: + +```typescript +version?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectattribute.md b/docs/development/core/public/kibana-plugin-public.savedobjectattribute.md new file mode 100644 index 0000000000000..f8d51390863eb --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectattribute.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) + +## SavedObjectAttribute type + + +Signature: + +```typescript +export declare type SavedObjectAttribute = string | number | boolean | null | undefined | SavedObjectAttributes | SavedObjectAttributes[]; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectattributes.md b/docs/development/core/public/kibana-plugin-public.savedobjectattributes.md new file mode 100644 index 0000000000000..4a9e096cc25b7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectattributes.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectAttributes](./kibana-plugin-public.savedobjectattributes.md) + +## SavedObjectAttributes interface + +The data for a Saved Object is stored in the `attributes` key as either an object or an array of objects. + +Signature: + +```typescript +export interface SavedObjectAttributes +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectreference.id.md b/docs/development/core/public/kibana-plugin-public.savedobjectreference.id.md new file mode 100644 index 0000000000000..27b820607f860 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectreference.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) > [id](./kibana-plugin-public.savedobjectreference.id.md) + +## SavedObjectReference.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectreference.md b/docs/development/core/public/kibana-plugin-public.savedobjectreference.md new file mode 100644 index 0000000000000..7ae05e24a5d89 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectreference.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) + +## SavedObjectReference interface + +A reference to another saved object. + +Signature: + +```typescript +export interface SavedObjectReference +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-public.savedobjectreference.id.md) | string | | +| [name](./kibana-plugin-public.savedobjectreference.name.md) | string | | +| [type](./kibana-plugin-public.savedobjectreference.type.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectreference.name.md b/docs/development/core/public/kibana-plugin-public.savedobjectreference.name.md new file mode 100644 index 0000000000000..104a8c313b528 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectreference.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) > [name](./kibana-plugin-public.savedobjectreference.name.md) + +## SavedObjectReference.name property + +Signature: + +```typescript +name: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectreference.type.md b/docs/development/core/public/kibana-plugin-public.savedobjectreference.type.md new file mode 100644 index 0000000000000..5b55a18becab7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectreference.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectReference](./kibana-plugin-public.savedobjectreference.md) > [type](./kibana-plugin-public.savedobjectreference.type.md) + +## SavedObjectReference.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbaseoptions.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbaseoptions.md new file mode 100644 index 0000000000000..00ea2fd158291 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbaseoptions.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBaseOptions](./kibana-plugin-public.savedobjectsbaseoptions.md) + +## SavedObjectsBaseOptions interface + + +Signature: + +```typescript +export interface SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [namespace](./kibana-plugin-public.savedobjectsbaseoptions.namespace.md) | string | Specify the namespace for this operation | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbaseoptions.namespace.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbaseoptions.namespace.md new file mode 100644 index 0000000000000..fb8d0d957a275 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbaseoptions.namespace.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBaseOptions](./kibana-plugin-public.savedobjectsbaseoptions.md) > [namespace](./kibana-plugin-public.savedobjectsbaseoptions.namespace.md) + +## SavedObjectsBaseOptions.namespace property + +Specify the namespace for this operation + +Signature: + +```typescript +namespace?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.md new file mode 100644 index 0000000000000..2ccddb8f71bd6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBatchResponse](./kibana-plugin-public.savedobjectsbatchresponse.md) + +## SavedObjectsBatchResponse interface + + +Signature: + +```typescript +export interface SavedObjectsBatchResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [savedObjects](./kibana-plugin-public.savedobjectsbatchresponse.savedobjects.md) | Array<SimpleSavedObject<T>> | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.savedobjects.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.savedobjects.md new file mode 100644 index 0000000000000..f83b6268431c7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbatchresponse.savedobjects.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBatchResponse](./kibana-plugin-public.savedobjectsbatchresponse.md) > [savedObjects](./kibana-plugin-public.savedobjectsbatchresponse.savedobjects.md) + +## SavedObjectsBatchResponse.savedObjects property + +Signature: + +```typescript +savedObjects: Array>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.attributes.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.attributes.md new file mode 100644 index 0000000000000..e3f7e0d676087 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.attributes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-public.savedobjectsbulkcreateobject.md) > [attributes](./kibana-plugin-public.savedobjectsbulkcreateobject.attributes.md) + +## SavedObjectsBulkCreateObject.attributes property + +Signature: + +```typescript +attributes: T; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.md new file mode 100644 index 0000000000000..8f95c0533dded --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-public.savedobjectsbulkcreateobject.md) + +## SavedObjectsBulkCreateObject interface + +Signature: + +```typescript +export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [attributes](./kibana-plugin-public.savedobjectsbulkcreateobject.attributes.md) | T | | +| [type](./kibana-plugin-public.savedobjectsbulkcreateobject.type.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.type.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.type.md new file mode 100644 index 0000000000000..37497b9178782 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateobject.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-public.savedobjectsbulkcreateobject.md) > [type](./kibana-plugin-public.savedobjectsbulkcreateobject.type.md) + +## SavedObjectsBulkCreateObject.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateoptions.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateoptions.md new file mode 100644 index 0000000000000..697084d8eee38 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateoptions.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkCreateOptions](./kibana-plugin-public.savedobjectsbulkcreateoptions.md) + +## SavedObjectsBulkCreateOptions interface + + +Signature: + +```typescript +export interface SavedObjectsBulkCreateOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [overwrite](./kibana-plugin-public.savedobjectsbulkcreateoptions.overwrite.md) | boolean | If a document with the given id already exists, overwrite it's contents (default=false). | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateoptions.overwrite.md b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateoptions.overwrite.md new file mode 100644 index 0000000000000..e3b425da892b2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsbulkcreateoptions.overwrite.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsBulkCreateOptions](./kibana-plugin-public.savedobjectsbulkcreateoptions.md) > [overwrite](./kibana-plugin-public.savedobjectsbulkcreateoptions.overwrite.md) + +## SavedObjectsBulkCreateOptions.overwrite property + +If a document with the given `id` already exists, overwrite it's contents (default=false). + +Signature: + +```typescript +overwrite?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkcreate.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkcreate.md new file mode 100644 index 0000000000000..426096d96c9ba --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkcreate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) > [bulkCreate](./kibana-plugin-public.savedobjectsclient.bulkcreate.md) + +## SavedObjectsClient.bulkCreate property + +Creates multiple documents at once + +Signature: + +```typescript +bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkget.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkget.md new file mode 100644 index 0000000000000..fc8b3c8258f9c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.bulkget.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) > [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) + +## SavedObjectsClient.bulkGet property + +Returns an array of objects by id + +Signature: + +```typescript +bulkGet: (objects?: { + id: string; + type: string; + }[]) => Promise>; +``` + +## Example + +bulkGet(\[ { id: 'one', type: 'config' }, { id: 'foo', type: 'index-pattern' } \]) + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.create.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.create.md new file mode 100644 index 0000000000000..d6366494f0037 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.create.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) > [create](./kibana-plugin-public.savedobjectsclient.create.md) + +## SavedObjectsClient.create property + +Persists an object + +Signature: + +```typescript +create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.delete.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.delete.md new file mode 100644 index 0000000000000..03658cbd9fcfd --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.delete.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) > [delete](./kibana-plugin-public.savedobjectsclient.delete.md) + +## SavedObjectsClient.delete property + +Deletes an object + +Signature: + +```typescript +delete: (type: string, id: string) => Promise<{}>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md new file mode 100644 index 0000000000000..20b9ff35779f9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) > [find](./kibana-plugin-public.savedobjectsclient.find.md) + +## SavedObjectsClient.find property + +Search for objects + +Signature: + +```typescript +find: (options?: Pick) => Promise>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.get.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.get.md new file mode 100644 index 0000000000000..88500f4e3a269 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) > [get](./kibana-plugin-public.savedobjectsclient.get.md) + +## SavedObjectsClient.get property + +Fetches a single object + +Signature: + +```typescript +get: (type: string, id: string) => Promise>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md new file mode 100644 index 0000000000000..d0de26bb9b0a1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -0,0 +1,35 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) + +## SavedObjectsClient class + +Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state. The client-side SavedObjectsClient is a thin convenience library around the SavedObjects HTTP API for interacting with Saved Objects. + +Signature: + +```typescript +export declare class SavedObjectsClient +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [bulkCreate](./kibana-plugin-public.savedobjectsclient.bulkcreate.md) | | (objects?: SavedObjectsBulkCreateObject<SavedObjectAttributes>[], options?: SavedObjectsBulkCreateOptions) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Creates multiple documents at once | +| [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | +| [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | +| [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options?: Pick<SavedObjectFindOptionsServer, "search" | "type" | "defaultSearchOperator" | "searchFields" | "sortField" | "hasReference" | "page" | "perPage" | "fields">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [update(type, id, attributes, { version, migrationVersion, references })](./kibana-plugin-public.savedobjectsclient.update.md) | | Updates an object | + +## Remarks + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `SavedObjectsClient` class. + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.update.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.update.md new file mode 100644 index 0000000000000..5f87f46d6206f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.update.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) > [update](./kibana-plugin-public.savedobjectsclient.update.md) + +## SavedObjectsClient.update() method + +Updates an object + +Signature: + +```typescript +update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| attributes | T | | +| { version, migrationVersion, references } | SavedObjectsUpdateOptions | | + +Returns: + +`Promise>` + + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclientcontract.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclientcontract.md new file mode 100644 index 0000000000000..876f3164feec2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclientcontract.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsClientContract](./kibana-plugin-public.savedobjectsclientcontract.md) + +## SavedObjectsClientContract type + +SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) + +Signature: + +```typescript +export declare type SavedObjectsClientContract = PublicMethodsOf; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.id.md b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.id.md new file mode 100644 index 0000000000000..fc0532a10d639 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) > [id](./kibana-plugin-public.savedobjectscreateoptions.id.md) + +## SavedObjectsCreateOptions.id property + +(Not recommended) Specify an id instead of having the saved objects service generate one for you. + +Signature: + +```typescript +id?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.md b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.md new file mode 100644 index 0000000000000..08090c0f8d8c3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) + +## SavedObjectsCreateOptions interface + + +Signature: + +```typescript +export interface SavedObjectsCreateOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-public.savedobjectscreateoptions.id.md) | string | (Not recommended) Specify an id instead of having the saved objects service generate one for you. | +| [migrationVersion](./kibana-plugin-public.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [overwrite](./kibana-plugin-public.savedobjectscreateoptions.overwrite.md) | boolean | If a document with the given id already exists, overwrite it's contents (default=false). | +| [references](./kibana-plugin-public.savedobjectscreateoptions.references.md) | SavedObjectReference[] | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.migrationversion.md b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.migrationversion.md new file mode 100644 index 0000000000000..5bc6b62f6680e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.migrationversion.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) > [migrationVersion](./kibana-plugin-public.savedobjectscreateoptions.migrationversion.md) + +## SavedObjectsCreateOptions.migrationVersion property + +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. + +Signature: + +```typescript +migrationVersion?: SavedObjectsMigrationVersion; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.overwrite.md b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.overwrite.md new file mode 100644 index 0000000000000..d83541fc9e874 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.overwrite.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) > [overwrite](./kibana-plugin-public.savedobjectscreateoptions.overwrite.md) + +## SavedObjectsCreateOptions.overwrite property + +If a document with the given `id` already exists, overwrite it's contents (default=false). + +Signature: + +```typescript +overwrite?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.references.md b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.references.md new file mode 100644 index 0000000000000..f6bcd47a3e8d5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectscreateoptions.references.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) > [references](./kibana-plugin-public.savedobjectscreateoptions.references.md) + +## SavedObjectsCreateOptions.references property + +Signature: + +```typescript +references?: SavedObjectReference[]; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md new file mode 100644 index 0000000000000..181e2bb237c53 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [defaultSearchOperator](./kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md) + +## SavedObjectsFindOptions.defaultSearchOperator property + +Signature: + +```typescript +defaultSearchOperator?: 'AND' | 'OR'; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.fields.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.fields.md new file mode 100644 index 0000000000000..20cbf04418222 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.fields.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [fields](./kibana-plugin-public.savedobjectsfindoptions.fields.md) + +## SavedObjectsFindOptions.fields property + +An array of fields to include in the results + +Signature: + +```typescript +fields?: string[]; +``` + +## Example + +SavedObjects.find({type: 'dashboard', fields: \['attributes.name', 'attributes.location'\]}) + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.hasreference.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.hasreference.md new file mode 100644 index 0000000000000..63f65d22cc33b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.hasreference.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [hasReference](./kibana-plugin-public.savedobjectsfindoptions.hasreference.md) + +## SavedObjectsFindOptions.hasReference property + +Signature: + +```typescript +hasReference?: { + type: string; + id: string; + }; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md new file mode 100644 index 0000000000000..f90c60ebdd0dc --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) + +## SavedObjectsFindOptions interface + + +Signature: + +```typescript +export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [defaultSearchOperator](./kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | +| [fields](./kibana-plugin-public.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | +| [hasReference](./kibana-plugin-public.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
} | | +| [page](./kibana-plugin-public.savedobjectsfindoptions.page.md) | number | | +| [perPage](./kibana-plugin-public.savedobjectsfindoptions.perpage.md) | number | | +| [search](./kibana-plugin-public.savedobjectsfindoptions.search.md) | string | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | +| [searchFields](./kibana-plugin-public.savedobjectsfindoptions.searchfields.md) | string[] | The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | +| [sortField](./kibana-plugin-public.savedobjectsfindoptions.sortfield.md) | string | | +| [sortOrder](./kibana-plugin-public.savedobjectsfindoptions.sortorder.md) | string | | +| [type](./kibana-plugin-public.savedobjectsfindoptions.type.md) | string | string[] | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.page.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.page.md new file mode 100644 index 0000000000000..982005adb2454 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.page.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [page](./kibana-plugin-public.savedobjectsfindoptions.page.md) + +## SavedObjectsFindOptions.page property + +Signature: + +```typescript +page?: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.perpage.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.perpage.md new file mode 100644 index 0000000000000..3c61f690d82c0 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.perpage.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [perPage](./kibana-plugin-public.savedobjectsfindoptions.perpage.md) + +## SavedObjectsFindOptions.perPage property + +Signature: + +```typescript +perPage?: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.search.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.search.md new file mode 100644 index 0000000000000..f8f95e5329826 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.search.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [search](./kibana-plugin-public.savedobjectsfindoptions.search.md) + +## SavedObjectsFindOptions.search property + +Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String `query` argument for more information + +Signature: + +```typescript +search?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.searchfields.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.searchfields.md new file mode 100644 index 0000000000000..5e97ef00b4417 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.searchfields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [searchFields](./kibana-plugin-public.savedobjectsfindoptions.searchfields.md) + +## SavedObjectsFindOptions.searchFields property + +The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information + +Signature: + +```typescript +searchFields?: string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.sortfield.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.sortfield.md new file mode 100644 index 0000000000000..14ab40894cecd --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.sortfield.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [sortField](./kibana-plugin-public.savedobjectsfindoptions.sortfield.md) + +## SavedObjectsFindOptions.sortField property + +Signature: + +```typescript +sortField?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.sortorder.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.sortorder.md new file mode 100644 index 0000000000000..b1e58658c0083 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.sortorder.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [sortOrder](./kibana-plugin-public.savedobjectsfindoptions.sortorder.md) + +## SavedObjectsFindOptions.sortOrder property + +Signature: + +```typescript +sortOrder?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.type.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.type.md new file mode 100644 index 0000000000000..6706e8344a1e3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [type](./kibana-plugin-public.savedobjectsfindoptions.type.md) + +## SavedObjectsFindOptions.type property + +Signature: + +```typescript +type?: string | string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.md new file mode 100644 index 0000000000000..61a2daa59f16a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) + +## SavedObjectsFindResponsePublic interface + +Return type of the Saved Objects `find()` method. + +\*Note\*: this type is different between the Public and Server Saved Objects clients. + +Signature: + +```typescript +export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [page](./kibana-plugin-public.savedobjectsfindresponsepublic.page.md) | number | | +| [perPage](./kibana-plugin-public.savedobjectsfindresponsepublic.perpage.md) | number | | +| [total](./kibana-plugin-public.savedobjectsfindresponsepublic.total.md) | number | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.page.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.page.md new file mode 100644 index 0000000000000..20e96d1e0df40 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.page.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) > [page](./kibana-plugin-public.savedobjectsfindresponsepublic.page.md) + +## SavedObjectsFindResponsePublic.page property + +Signature: + +```typescript +page: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.perpage.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.perpage.md new file mode 100644 index 0000000000000..f706f9cb03b26 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.perpage.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) > [perPage](./kibana-plugin-public.savedobjectsfindresponsepublic.perpage.md) + +## SavedObjectsFindResponsePublic.perPage property + +Signature: + +```typescript +perPage: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.total.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.total.md new file mode 100644 index 0000000000000..0a44c73436a2c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindresponsepublic.total.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) > [total](./kibana-plugin-public.savedobjectsfindresponsepublic.total.md) + +## SavedObjectsFindResponsePublic.total property + +Signature: + +```typescript +total: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsmigrationversion.md b/docs/development/core/public/kibana-plugin-public.savedobjectsmigrationversion.md new file mode 100644 index 0000000000000..675adb9498c50 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsmigrationversion.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsMigrationVersion](./kibana-plugin-public.savedobjectsmigrationversion.md) + +## SavedObjectsMigrationVersion interface + +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. + +Signature: + +```typescript +export interface SavedObjectsMigrationVersion +``` + +## Example + +migrationVersion: { dashboard: '7.1.1', space: '6.6.6', } + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsstart.client.md b/docs/development/core/public/kibana-plugin-public.savedobjectsstart.client.md new file mode 100644 index 0000000000000..d3e0da7a414b0 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsstart.client.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) > [client](./kibana-plugin-public.savedobjectsstart.client.md) + +## SavedObjectsStart.client property + +[SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) + +Signature: + +```typescript +client: SavedObjectsClientContract; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsstart.md b/docs/development/core/public/kibana-plugin-public.savedobjectsstart.md new file mode 100644 index 0000000000000..07a70f306cd26 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsstart.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) + +## SavedObjectsStart interface + + +Signature: + +```typescript +export interface SavedObjectsStart +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [client](./kibana-plugin-public.savedobjectsstart.client.md) | SavedObjectsClientContract | [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.md b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.md new file mode 100644 index 0000000000000..800a78d65486b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) + +## SavedObjectsUpdateOptions interface + + +Signature: + +```typescript +export interface SavedObjectsUpdateOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [migrationVersion](./kibana-plugin-public.savedobjectsupdateoptions.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [references](./kibana-plugin-public.savedobjectsupdateoptions.references.md) | SavedObjectReference[] | | +| [version](./kibana-plugin-public.savedobjectsupdateoptions.version.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.migrationversion.md b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.migrationversion.md new file mode 100644 index 0000000000000..e5fe20acd3994 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.migrationversion.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) > [migrationVersion](./kibana-plugin-public.savedobjectsupdateoptions.migrationversion.md) + +## SavedObjectsUpdateOptions.migrationVersion property + +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. + +Signature: + +```typescript +migrationVersion?: SavedObjectsMigrationVersion; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.references.md b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.references.md new file mode 100644 index 0000000000000..eda84ec8e0bfa --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.references.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) > [references](./kibana-plugin-public.savedobjectsupdateoptions.references.md) + +## SavedObjectsUpdateOptions.references property + +Signature: + +```typescript +references?: SavedObjectReference[]; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.version.md b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.version.md new file mode 100644 index 0000000000000..9aacfa9124016 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsupdateoptions.version.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) > [version](./kibana-plugin-public.savedobjectsupdateoptions.version.md) + +## SavedObjectsUpdateOptions.version property + +Signature: + +```typescript +version?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.(constructor).md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.(constructor).md new file mode 100644 index 0000000000000..e22154bc795fd --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.(constructor).md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [(constructor)](./kibana-plugin-public.simplesavedobject.(constructor).md) + +## SimpleSavedObject.(constructor) + +Constructs a new instance of the `SimpleSavedObject` class + +Signature: + +```typescript +constructor(client: SavedObjectsClient, { id, type, version, attributes, error, references, migrationVersion }: SavedObjectType); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| client | SavedObjectsClient | | +| { id, type, version, attributes, error, references, migrationVersion } | SavedObjectType<T> | | + diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject._version.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject._version.md new file mode 100644 index 0000000000000..7cbe08b8de760 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject._version.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [\_version](./kibana-plugin-public.simplesavedobject._version.md) + +## SimpleSavedObject.\_version property + +Signature: + +```typescript +_version?: SavedObjectType['version']; +``` diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.attributes.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.attributes.md new file mode 100644 index 0000000000000..1c57136a1952e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.attributes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [attributes](./kibana-plugin-public.simplesavedobject.attributes.md) + +## SimpleSavedObject.attributes property + +Signature: + +```typescript +attributes: T; +``` diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.delete.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.delete.md new file mode 100644 index 0000000000000..8a04acfedec62 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.delete.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [delete](./kibana-plugin-public.simplesavedobject.delete.md) + +## SimpleSavedObject.delete() method + +Signature: + +```typescript +delete(): Promise<{}>; +``` +Returns: + +`Promise<{}>` + diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.error.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.error.md new file mode 100644 index 0000000000000..0b4f914ac92e8 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.error.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [error](./kibana-plugin-public.simplesavedobject.error.md) + +## SimpleSavedObject.error property + +Signature: + +```typescript +error: SavedObjectType['error']; +``` diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.get.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.get.md new file mode 100644 index 0000000000000..39a899e4a6cd3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.get.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [get](./kibana-plugin-public.simplesavedobject.get.md) + +## SimpleSavedObject.get() method + +Signature: + +```typescript +get(key: string): any; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| key | string | | + +Returns: + +`any` + diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.has.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.has.md new file mode 100644 index 0000000000000..5f3019d55c3f6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.has.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [has](./kibana-plugin-public.simplesavedobject.has.md) + +## SimpleSavedObject.has() method + +Signature: + +```typescript +has(key: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| key | string | | + +Returns: + +`boolean` + diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.id.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.id.md new file mode 100644 index 0000000000000..ed97976c4100f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [id](./kibana-plugin-public.simplesavedobject.id.md) + +## SimpleSavedObject.id property + +Signature: + +```typescript +id: SavedObjectType['id']; +``` diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.md new file mode 100644 index 0000000000000..a40a04c17a582 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.md @@ -0,0 +1,44 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) + +## SimpleSavedObject class + +This class is a very simple wrapper for SavedObjects loaded from the server with the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md). + +It provides basic functionality for creating/saving/deleting saved objects, but doesn't include any type-specific implementations. + +Signature: + +```typescript +export declare class SimpleSavedObject +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(client, { id, type, version, attributes, error, references, migrationVersion })](./kibana-plugin-public.simplesavedobject.(constructor).md) | | Constructs a new instance of the SimpleSavedObject class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [\_version](./kibana-plugin-public.simplesavedobject._version.md) | | SavedObjectType<T>['version'] | | +| [attributes](./kibana-plugin-public.simplesavedobject.attributes.md) | | T | | +| [error](./kibana-plugin-public.simplesavedobject.error.md) | | SavedObjectType<T>['error'] | | +| [id](./kibana-plugin-public.simplesavedobject.id.md) | | SavedObjectType<T>['id'] | | +| [migrationVersion](./kibana-plugin-public.simplesavedobject.migrationversion.md) | | SavedObjectType<T>['migrationVersion'] | | +| [references](./kibana-plugin-public.simplesavedobject.references.md) | | SavedObjectType<T>['references'] | | +| [type](./kibana-plugin-public.simplesavedobject.type.md) | | SavedObjectType<T>['type'] | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [delete()](./kibana-plugin-public.simplesavedobject.delete.md) | | | +| [get(key)](./kibana-plugin-public.simplesavedobject.get.md) | | | +| [has(key)](./kibana-plugin-public.simplesavedobject.has.md) | | | +| [save()](./kibana-plugin-public.simplesavedobject.save.md) | | | +| [set(key, value)](./kibana-plugin-public.simplesavedobject.set.md) | | | + diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.migrationversion.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.migrationversion.md new file mode 100644 index 0000000000000..6f7b3af03099d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.migrationversion.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [migrationVersion](./kibana-plugin-public.simplesavedobject.migrationversion.md) + +## SimpleSavedObject.migrationVersion property + +Signature: + +```typescript +migrationVersion: SavedObjectType['migrationVersion']; +``` diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.references.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.references.md new file mode 100644 index 0000000000000..159f855538f62 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.references.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [references](./kibana-plugin-public.simplesavedobject.references.md) + +## SimpleSavedObject.references property + +Signature: + +```typescript +references: SavedObjectType['references']; +``` diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.save.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.save.md new file mode 100644 index 0000000000000..05f8880fbcdd6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.save.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [save](./kibana-plugin-public.simplesavedobject.save.md) + +## SimpleSavedObject.save() method + +Signature: + +```typescript +save(): Promise>; +``` +Returns: + +`Promise>` + diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.set.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.set.md new file mode 100644 index 0000000000000..ce3f9c5919d7c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.set.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [set](./kibana-plugin-public.simplesavedobject.set.md) + +## SimpleSavedObject.set() method + +Signature: + +```typescript +set(key: string, value: any): T; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| key | string | | +| value | any | | + +Returns: + +`T` + diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject.type.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject.type.md new file mode 100644 index 0000000000000..b004c70013d6d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SimpleSavedObject](./kibana-plugin-public.simplesavedobject.md) > [type](./kibana-plugin-public.simplesavedobject.type.md) + +## SimpleSavedObject.type property + +Signature: + +```typescript +type: SavedObjectType['type']; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 295c94574ab6a..bf56b1d2380ab 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -63,7 +63,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RouteConfig](./kibana-plugin-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Additional route options. | | [SavedObject](./kibana-plugin-server.savedobject.md) | | -| [SavedObjectAttributes](./kibana-plugin-server.savedobjectattributes.md) | | +| [SavedObjectAttributes](./kibana-plugin-server.savedobjectattributes.md) | The data for a Saved Object is stored in the attributes key as either an object or an array of objects. | | [SavedObjectReference](./kibana-plugin-server.savedobjectreference.md) | A reference to another saved object. | | [SavedObjectsBaseOptions](./kibana-plugin-server.savedobjectsbaseoptions.md) | | | [SavedObjectsBulkCreateObject](./kibana-plugin-server.savedobjectsbulkcreateobject.md) | | @@ -74,7 +74,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) | | | [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) | Options controlling the export operation. | | [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) | | -| [SavedObjectsFindResponse](./kibana-plugin-server.savedobjectsfindresponse.md) | | +| [SavedObjectsFindResponse](./kibana-plugin-server.savedobjectsfindresponse.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | | [SavedObjectsImportConflictError](./kibana-plugin-server.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | | [SavedObjectsImportError](./kibana-plugin-server.savedobjectsimporterror.md) | Represents a failure to import. | | [SavedObjectsImportMissingReferencesError](./kibana-plugin-server.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. | @@ -83,7 +83,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsImportRetry](./kibana-plugin-server.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. | | [SavedObjectsImportUnknownError](./kibana-plugin-server.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. | | [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-server.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. | -| [SavedObjectsMigrationVersion](./kibana-plugin-server.savedobjectsmigrationversion.md) | A dictionary of saved object type -> version used to determine what migrations need to be applied to a saved object. | +| [SavedObjectsMigrationLogger](./kibana-plugin-server.savedobjectsmigrationlogger.md) | | +| [SavedObjectsMigrationVersion](./kibana-plugin-server.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [SavedObjectsRawDoc](./kibana-plugin-server.savedobjectsrawdoc.md) | A raw document as represented directly in the saved object index. | | [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) | Options to control the "resolve import" operation. | | [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) | | @@ -125,6 +126,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RequestHandler](./kibana-plugin-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) functions. | | [ResponseError](./kibana-plugin-server.responseerror.md) | Error message and optional data send to the client in case of error. | | [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. | -| [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | \#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | +| [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | | +| [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | [SavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.attributes.md b/docs/development/core/server/kibana-plugin-server.savedobject.attributes.md index b58408fe5f6b6..c3d521aa7bc2c 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.attributes.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.attributes.md @@ -4,6 +4,8 @@ ## SavedObject.attributes property +The data for a Saved Object is stored in the `attributes` key as either an object or an array of objects. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.id.md b/docs/development/core/server/kibana-plugin-server.savedobject.id.md index f0ee1e9770f85..cc0127eb6ab04 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.id.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.id.md @@ -4,6 +4,8 @@ ## SavedObject.id property +The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.md b/docs/development/core/server/kibana-plugin-server.savedobject.md index 3b2417c56d5e5..1514980588746 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.md @@ -15,12 +15,12 @@ export interface SavedObject | Property | Type | Description | | --- | --- | --- | -| [attributes](./kibana-plugin-server.savedobject.attributes.md) | T | | +| [attributes](./kibana-plugin-server.savedobject.attributes.md) | T | The data for a Saved Object is stored in the attributes key as either an object or an array of objects. | | [error](./kibana-plugin-server.savedobject.error.md) | {
message: string;
statusCode: number;
} | | -| [id](./kibana-plugin-server.savedobject.id.md) | string | | -| [migrationVersion](./kibana-plugin-server.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | | -| [references](./kibana-plugin-server.savedobject.references.md) | SavedObjectReference[] | | -| [type](./kibana-plugin-server.savedobject.type.md) | string | | -| [updated\_at](./kibana-plugin-server.savedobject.updated_at.md) | string | | -| [version](./kibana-plugin-server.savedobject.version.md) | string | | +| [id](./kibana-plugin-server.savedobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | +| [migrationVersion](./kibana-plugin-server.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | +| [references](./kibana-plugin-server.savedobject.references.md) | SavedObjectReference[] | A reference to another saved object. | +| [type](./kibana-plugin-server.savedobject.type.md) | string | The type of Saved Object. Each plugin can define it's own custom Saved Object types. | +| [updated\_at](./kibana-plugin-server.savedobject.updated_at.md) | string | Timestamp of the last time this document had been updated. | +| [version](./kibana-plugin-server.savedobject.version.md) | string | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.migrationversion.md b/docs/development/core/server/kibana-plugin-server.savedobject.migrationversion.md index f9150a96b22cb..63c353a8b2e75 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.migrationversion.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.migrationversion.md @@ -4,6 +4,8 @@ ## SavedObject.migrationVersion property +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.references.md b/docs/development/core/server/kibana-plugin-server.savedobject.references.md index 08476527a446d..22d4c84e9bcad 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.references.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.references.md @@ -4,6 +4,8 @@ ## SavedObject.references property +A reference to another saved object. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.type.md b/docs/development/core/server/kibana-plugin-server.savedobject.type.md index 172de52d305f7..0498b2d780484 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.type.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.type.md @@ -4,6 +4,8 @@ ## SavedObject.type property +The type of Saved Object. Each plugin can define it's own custom Saved Object types. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.updated_at.md b/docs/development/core/server/kibana-plugin-server.savedobject.updated_at.md index 0de1b1a0e816f..bf57cbc08dbb4 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.updated_at.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.updated_at.md @@ -4,6 +4,8 @@ ## SavedObject.updated\_at property +Timestamp of the last time this document had been updated. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.version.md b/docs/development/core/server/kibana-plugin-server.savedobject.version.md index 25fbd536fcc38..a1c2f2c6c3b44 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobject.version.md +++ b/docs/development/core/server/kibana-plugin-server.savedobject.version.md @@ -4,6 +4,8 @@ ## SavedObject.version property +An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectattribute.md b/docs/development/core/server/kibana-plugin-server.savedobjectattribute.md new file mode 100644 index 0000000000000..6a6c7c4d36bc6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectattribute.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) + +## SavedObjectAttribute type + + +Signature: + +```typescript +export declare type SavedObjectAttribute = string | number | boolean | null | undefined | SavedObjectAttributes | SavedObjectAttributes[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectattributes.md b/docs/development/core/server/kibana-plugin-server.savedobjectattributes.md index b629d8fad0c7b..7ff1247e89f2d 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectattributes.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectattributes.md @@ -4,6 +4,7 @@ ## SavedObjectAttributes interface +The data for a Saved Object is stored in the `attributes` key as either an object or an array of objects. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md index 056de4b634b50..bae275777310a 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md @@ -17,7 +17,7 @@ export interface SavedObjectsBulkCreateObjectT | | | [id](./kibana-plugin-server.savedobjectsbulkcreateobject.id.md) | string | | -| [migrationVersion](./kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion | | +| [migrationVersion](./kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [references](./kibana-plugin-server.savedobjectsbulkcreateobject.references.md) | SavedObjectReference[] | | | [type](./kibana-plugin-server.savedobjectsbulkcreateobject.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md index 9b33ab9a1b077..6065988a8d0fc 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md @@ -4,6 +4,8 @@ ## SavedObjectsBulkCreateObject.migrationVersion property +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclientcontract.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclientcontract.md index 3603904c2f89c..ae948b090146b 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclientcontract.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclientcontract.md @@ -4,6 +4,8 @@ ## SavedObjectsClientContract type +Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state. + \#\# SavedObjectsClient errors Since the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either: diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.md index 61d65bfbf7b90..16549e420ac01 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.md @@ -16,7 +16,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions | Property | Type | Description | | --- | --- | --- | | [id](./kibana-plugin-server.savedobjectscreateoptions.id.md) | string | (not recommended) Specify an id for the document | -| [migrationVersion](./kibana-plugin-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | | +| [migrationVersion](./kibana-plugin-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [overwrite](./kibana-plugin-server.savedobjectscreateoptions.overwrite.md) | boolean | Overwrite existing documents (defaults to false) | | [references](./kibana-plugin-server.savedobjectscreateoptions.references.md) | SavedObjectReference[] | | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.migrationversion.md b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.migrationversion.md index fcbec639312e6..33432b1138d91 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.migrationversion.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.migrationversion.md @@ -4,6 +4,8 @@ ## SavedObjectsCreateOptions.migrationVersion property +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.fields.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.fields.md index 6d2cac4f14439..394363abb5d4d 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.fields.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.fields.md @@ -4,8 +4,15 @@ ## SavedObjectsFindOptions.fields property +An array of fields to include in the results + Signature: ```typescript fields?: string[]; ``` + +## Example + +SavedObjects.find({type: 'dashboard', fields: \['attributes.name', 'attributes.location'\]}) + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md index 140b447c0002a..ad81c439d902c 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md @@ -16,12 +16,12 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions | Property | Type | Description | | --- | --- | --- | | [defaultSearchOperator](./kibana-plugin-server.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | -| [fields](./kibana-plugin-server.savedobjectsfindoptions.fields.md) | string[] | | +| [fields](./kibana-plugin-server.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | | [hasReference](./kibana-plugin-server.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
} | | | [page](./kibana-plugin-server.savedobjectsfindoptions.page.md) | number | | | [perPage](./kibana-plugin-server.savedobjectsfindoptions.perpage.md) | number | | -| [search](./kibana-plugin-server.savedobjectsfindoptions.search.md) | string | | -| [searchFields](./kibana-plugin-server.savedobjectsfindoptions.searchfields.md) | string[] | see Elasticsearch Simple Query String Query field argument for more information | +| [search](./kibana-plugin-server.savedobjectsfindoptions.search.md) | string | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | +| [searchFields](./kibana-plugin-server.savedobjectsfindoptions.searchfields.md) | string[] | The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | | [sortField](./kibana-plugin-server.savedobjectsfindoptions.sortfield.md) | string | | | [sortOrder](./kibana-plugin-server.savedobjectsfindoptions.sortorder.md) | string | | | [type](./kibana-plugin-server.savedobjectsfindoptions.type.md) | string | string[] | | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.search.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.search.md index 7dca45e58123f..a29dda1892918 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.search.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.search.md @@ -4,6 +4,8 @@ ## SavedObjectsFindOptions.search property +Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String `query` argument for more information + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.searchfields.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.searchfields.md index fdd157299c144..2505bac8aec49 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.searchfields.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.searchfields.md @@ -4,7 +4,7 @@ ## SavedObjectsFindOptions.searchFields property -see Elasticsearch Simple Query String Query field argument for more information +The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information Signature: diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md index e4f7d15042985..23299e22d8004 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md @@ -4,6 +4,9 @@ ## SavedObjectsFindResponse interface +Return type of the Saved Objects `find()` method. + +\*Note\*: this type is different between the Public and Server Saved Objects clients. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.debug.md b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.debug.md new file mode 100644 index 0000000000000..be44bc7422d6c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.debug.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsMigrationLogger](./kibana-plugin-server.savedobjectsmigrationlogger.md) > [debug](./kibana-plugin-server.savedobjectsmigrationlogger.debug.md) + +## SavedObjectsMigrationLogger.debug property + +Signature: + +```typescript +debug: (msg: string) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.info.md b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.info.md new file mode 100644 index 0000000000000..f8bbd5e4e6250 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.info.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsMigrationLogger](./kibana-plugin-server.savedobjectsmigrationlogger.md) > [info](./kibana-plugin-server.savedobjectsmigrationlogger.info.md) + +## SavedObjectsMigrationLogger.info property + +Signature: + +```typescript +info: (msg: string) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.md b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.md new file mode 100644 index 0000000000000..9e21cb0641baf --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsMigrationLogger](./kibana-plugin-server.savedobjectsmigrationlogger.md) + +## SavedObjectsMigrationLogger interface + + +Signature: + +```typescript +export interface SavedObjectsMigrationLogger +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [debug](./kibana-plugin-server.savedobjectsmigrationlogger.debug.md) | (msg: string) => void | | +| [info](./kibana-plugin-server.savedobjectsmigrationlogger.info.md) | (msg: string) => void | | +| [warning](./kibana-plugin-server.savedobjectsmigrationlogger.warning.md) | (msg: string) => void | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.warning.md b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.warning.md new file mode 100644 index 0000000000000..978090f9fc885 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.warning.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsMigrationLogger](./kibana-plugin-server.savedobjectsmigrationlogger.md) > [warning](./kibana-plugin-server.savedobjectsmigrationlogger.warning.md) + +## SavedObjectsMigrationLogger.warning property + +Signature: + +```typescript +warning: (msg: string) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationversion.md b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationversion.md index 434e46041cf7d..b7f9c8fd8fe98 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationversion.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationversion.md @@ -4,10 +4,15 @@ ## SavedObjectsMigrationVersion interface -A dictionary of saved object type -> version used to determine what migrations need to be applied to a saved object. +Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. Signature: ```typescript export interface SavedObjectsMigrationVersion ``` + +## Example + +migrationVersion: { dashboard: '7.1.1', space: '6.6.6', } + diff --git a/kibana.d.ts b/kibana.d.ts index 45cf4405a8edc..e0b20f6fa28af 100644 --- a/kibana.d.ts +++ b/kibana.d.ts @@ -20,8 +20,8 @@ /** * All exports from TS source files (where the implementation is actually done in TS). */ -import * as Public from 'target/types/public'; -import * as Server from 'target/types/server'; +import * as Public from 'src/core/public'; +import * as Server from 'src/core/server'; export { Public, Server }; diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 2451feff1b4b0..7782c93c7bbb1 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -35,6 +35,7 @@ import { ApplicationService } from './application'; import { mapToObject } from '../utils/'; import { DocLinksService } from './doc_links'; import { RenderingService } from './rendering'; +import { SavedObjectsService } from './saved_objects/saved_objects_service'; import { ContextService } from './context'; interface Params { @@ -64,6 +65,7 @@ export class CoreSystem { private readonly legacyPlatform: LegacyPlatformService; private readonly notifications: NotificationsService; private readonly http: HttpService; + private readonly savedObjects: SavedObjectsService; private readonly uiSettings: UiSettingsService; private readonly chrome: ChromeService; private readonly i18n: I18nService; @@ -101,6 +103,7 @@ export class CoreSystem { this.notifications = new NotificationsService(); this.http = new HttpService(); + this.savedObjects = new SavedObjectsService(); this.uiSettings = new UiSettingsService(); this.overlay = new OverlayService(); this.application = new ApplicationService(); @@ -166,6 +169,7 @@ export class CoreSystem { const injectedMetadata = await this.injectedMetadata.start(); const docLinks = await this.docLinks.start({ injectedMetadata }); const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); + const savedObjects = await this.savedObjects.start({ http }); const i18n = await this.i18n.start(); const application = await this.application.start({ injectedMetadata }); @@ -201,6 +205,7 @@ export class CoreSystem { chrome, docLinks, http, + savedObjects, i18n, injectedMetadata, notifications, diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index bcc94086e2a10..4ce84f8ab38d1 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -46,8 +46,8 @@ const createServiceMock = (): ServiceSetupMockType => ({ removeAllInterceptors: jest.fn(), }); -const createSetupContractMock = () => createServiceMock(); -const createStartContractMock = () => createServiceMock(); +const createSetupContractMock = createServiceMock; +const createStartContractMock = createServiceMock; const createMock = () => { const mocked: jest.Mocked> = { diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 78e7160656f05..abc922ff97c1d 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -66,10 +66,30 @@ import { Plugin, PluginInitializer, PluginInitializerContext, PluginOpaqueId } f import { UiSettingsClient, UiSettingsState, UiSettingsClientContract } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; import { DocLinksStart } from './doc_links'; +import { SavedObjectsStart } from './saved_objects'; import { IContextContainer, IContextProvider, ContextSetup, IContextHandler } from './context'; export { CoreContext, CoreSystem } from './core_system'; export { RecursiveReadonly } from '../utils'; +export { + SavedObjectsBatchResponse, + SavedObjectsBulkCreateObject, + SavedObjectsBulkCreateOptions, + SavedObjectsCreateOptions, + SavedObjectsFindResponsePublic, + SavedObjectsUpdateOptions, + SavedObject, + SavedObjectAttribute, + SavedObjectAttributes, + SavedObjectReference, + SavedObjectsBaseOptions, + SavedObjectsFindOptions, + SavedObjectsMigrationVersion, + SavedObjectsClientContract, + SavedObjectsClient, + SimpleSavedObject, +} from './saved_objects'; + export { HttpServiceBase, HttpHeadersInit, @@ -124,6 +144,8 @@ export interface CoreStart { docLinks: DocLinksStart; /** {@link HttpStart} */ http: HttpStart; + /** {@link SavedObjectsStart} */ + savedObjects: SavedObjectsStart; /** {@link I18nStart} */ i18n: I18nStart; /** {@link NotificationsStart} */ @@ -181,6 +203,7 @@ export { Plugin, PluginInitializer, PluginInitializerContext, + SavedObjectsStart, PluginOpaqueId, Toast, ToastInput, diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index 5f1c4e1cf6bf9..eb5b3e90f1a52 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -58,6 +58,7 @@ import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { LegacyPlatformService } from './legacy_service'; import { applicationServiceMock } from '../application/application_service.mock'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; +import { savedObjectsMock } from '../saved_objects/saved_objects_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; const applicationSetup = applicationServiceMock.createSetupContract(); @@ -96,6 +97,7 @@ const injectedMetadataStart = injectedMetadataServiceMock.createStartContract(); const notificationsStart = notificationServiceMock.createStartContract(); const overlayStart = overlayServiceMock.createStartContract(); const uiSettingsStart = uiSettingsServiceMock.createStartContract(); +const savedObjectsStart = savedObjectsMock.createStartContract(); const defaultStartDeps = { core: { @@ -108,6 +110,7 @@ const defaultStartDeps = { notifications: notificationsStart, overlays: overlayStart, uiSettings: uiSettingsStart, + savedObjects: savedObjectsStart, }, targetDomElement: document.createElement('div'), plugins: {}, diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 3682d86168dcd..0f3a01c793ae3 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -26,6 +26,7 @@ import { i18nServiceMock } from './i18n/i18n_service.mock'; import { notificationServiceMock } from './notifications/notifications_service.mock'; import { overlayServiceMock } from './overlays/overlay_service.mock'; import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; +import { savedObjectsMock } from './saved_objects/saved_objects_service.mock'; import { contextServiceMock } from './context/context_service.mock'; export { chromeServiceMock } from './chrome/chrome_service.mock'; @@ -61,6 +62,7 @@ function createCoreStartMock() { notifications: notificationServiceMock.createStartContract(), overlays: overlayServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), + savedObjects: savedObjectsMock.createStartContract(), }; return mock; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 75e31192e0de7..66cb7c4a1171e 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -115,5 +115,6 @@ export function createPluginStartContext< notifications: deps.notifications, overlays: deps.overlays, uiSettings: deps.uiSettings, + savedObjects: deps.savedObjects, }; } diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index bc3fe95cf4c9c..2b689e45b4f1a 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -43,6 +43,7 @@ import { injectedMetadataServiceMock } from '../injected_metadata/injected_metad import { httpServiceMock } from '../http/http_service.mock'; import { CoreSetup, CoreStart } from '..'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; +import { savedObjectsMock } from '../saved_objects/saved_objects_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; export let mockPluginInitializers: Map; @@ -92,6 +93,7 @@ beforeEach(() => { notifications: notificationServiceMock.createStartContract(), overlays: overlayServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), + savedObjects: savedObjectsMock.createStartContract(), }; mockStartContext = { ...omit(mockStartDeps, 'injectedMetadata'), diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 26c2670141da6..077dfac06de3a 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -211,6 +211,8 @@ export interface CoreStart { // (undocumented) overlays: OverlayStart; // (undocumented) + savedObjects: SavedObjectsStart; + // (undocumented) uiSettings: UiSettingsClientContract; } @@ -604,6 +606,177 @@ export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T ext [K in keyof T]: RecursiveReadonly; }> : T; +// @public (undocumented) +export interface SavedObject { + attributes: T; + // (undocumented) + error?: { + message: string; + statusCode: number; + }; + id: string; + migrationVersion?: SavedObjectsMigrationVersion; + references: SavedObjectReference[]; + type: string; + updated_at?: string; + version?: string; +} + +// @public (undocumented) +export type SavedObjectAttribute = string | number | boolean | null | undefined | SavedObjectAttributes | SavedObjectAttributes[]; + +// @public +export interface SavedObjectAttributes { + // (undocumented) + [key: string]: SavedObjectAttribute | SavedObjectAttribute[]; +} + +// @public +export interface SavedObjectReference { + // (undocumented) + id: string; + // (undocumented) + name: string; + // (undocumented) + type: string; +} + +// @public (undocumented) +export interface SavedObjectsBaseOptions { + namespace?: string; +} + +// @public (undocumented) +export interface SavedObjectsBatchResponse { + // (undocumented) + savedObjects: Array>; +} + +// @public (undocumented) +export interface SavedObjectsBulkCreateObject extends SavedObjectsCreateOptions { + // (undocumented) + attributes: T; + // (undocumented) + type: string; +} + +// @public (undocumented) +export interface SavedObjectsBulkCreateOptions { + overwrite?: boolean; +} + +// @public +export class SavedObjectsClient { + // @internal + constructor(http: HttpServiceBase); + bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; + bulkGet: (objects?: { + id: string; + type: string; + }[]) => Promise>; + create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; + delete: (type: string, id: string) => Promise<{}>; + find: (options?: Pick) => Promise>; + get: (type: string, id: string) => Promise>; + update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; +} + +// @public +export type SavedObjectsClientContract = PublicMethodsOf; + +// @public (undocumented) +export interface SavedObjectsCreateOptions { + id?: string; + migrationVersion?: SavedObjectsMigrationVersion; + overwrite?: boolean; + // (undocumented) + references?: SavedObjectReference[]; +} + +// @public (undocumented) +export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { + // (undocumented) + defaultSearchOperator?: 'AND' | 'OR'; + fields?: string[]; + // (undocumented) + hasReference?: { + type: string; + id: string; + }; + // (undocumented) + page?: number; + // (undocumented) + perPage?: number; + search?: string; + searchFields?: string[]; + // (undocumented) + sortField?: string; + // (undocumented) + sortOrder?: string; + // (undocumented) + type?: string | string[]; +} + +// @public +export interface SavedObjectsFindResponsePublic extends SavedObjectsBatchResponse { + // (undocumented) + page: number; + // (undocumented) + perPage: number; + // (undocumented) + total: number; +} + +// @public +export interface SavedObjectsMigrationVersion { + // (undocumented) + [pluginName: string]: string; +} + +// @public (undocumented) +export interface SavedObjectsStart { + // (undocumented) + client: SavedObjectsClientContract; +} + +// @public (undocumented) +export interface SavedObjectsUpdateOptions { + migrationVersion?: SavedObjectsMigrationVersion; + // (undocumented) + references?: SavedObjectReference[]; + // (undocumented) + version?: string; +} + +// @public +export class SimpleSavedObject { + constructor(client: SavedObjectsClient, { id, type, version, attributes, error, references, migrationVersion }: SavedObject); + // (undocumented) + attributes: T; + // (undocumented) + delete(): Promise<{}>; + // (undocumented) + error: SavedObject['error']; + // (undocumented) + get(key: string): any; + // (undocumented) + has(key: string): boolean; + // (undocumented) + id: SavedObject['id']; + // (undocumented) + migrationVersion: SavedObject['migrationVersion']; + // (undocumented) + references: SavedObject['references']; + // (undocumented) + save(): Promise>; + // (undocumented) + set(key: string, value: any): T; + // (undocumented) + type: SavedObject['type']; + // (undocumented) + _version?: SavedObject['version']; +} + export { Toast } // Warning: (ae-forgotten-export) The symbol "ToastInputFields" needs to be exported by the entry point index.d.ts diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts new file mode 100644 index 0000000000000..f82112c7a65bd --- /dev/null +++ b/src/core/public/saved_objects/index.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + SavedObjectsBatchResponse, + SavedObjectsBulkCreateObject, + SavedObjectsBulkCreateOptions, + SavedObjectsClient, + SavedObjectsClientContract, + SavedObjectsCreateOptions, + SavedObjectsFindResponsePublic, + SavedObjectsUpdateOptions, +} from './saved_objects_client'; +export { SimpleSavedObject } from './simple_saved_object'; +export { SavedObjectsStart } from './saved_objects_service'; +export { + SavedObject, + SavedObjectAttribute, + SavedObjectAttributes, + SavedObjectReference, + SavedObjectsBaseOptions, + SavedObjectsFindOptions, + SavedObjectsMigrationVersion, +} from '../../server/types'; diff --git a/src/core/public/saved_objects/saved_objects_client.test.ts b/src/core/public/saved_objects/saved_objects_client.test.ts new file mode 100644 index 0000000000000..4c0fe90a5bfbd --- /dev/null +++ b/src/core/public/saved_objects/saved_objects_client.test.ts @@ -0,0 +1,433 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsClient } from './saved_objects_client'; +import { SimpleSavedObject } from './simple_saved_object'; +import { httpServiceMock } from '../http/http_service.mock'; + +describe('SavedObjectsClient', () => { + const doc = { + id: 'AVwSwFxtcMV38qjDZoQg', + type: 'config', + attributes: { title: 'Example title' }, + version: 'foo', + }; + + const http = httpServiceMock.createStartContract(); + let savedObjectsClient: SavedObjectsClient; + + beforeEach(() => { + savedObjectsClient = new SavedObjectsClient(http); + http.fetch.mockClear(); + }); + + describe('#get', () => { + beforeEach(() => { + http.fetch.mockResolvedValue({ saved_objects: [doc] }); + }); + + test('rejects if `type` parameter is undefined', () => { + return expect( + savedObjectsClient.get(undefined as any, undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type and id]`); + }); + + test('rejects if `id` parameter is undefined', () => { + return expect( + savedObjectsClient.get('index-pattern', undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type and id]`); + }); + + test('rejects when HTTP call fails', () => { + http.fetch.mockRejectedValue(new Error('Request failed')); + return expect(savedObjectsClient.get(doc.type, doc.id)).rejects.toMatchInlineSnapshot( + `[Error: Request failed]` + ); + }); + + test('makes HTTP call', async () => { + await savedObjectsClient.get(doc.type, doc.id); + expect(http.fetch.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/saved_objects/_bulk_get", + Object { + "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\"}]", + "method": "POST", + "query": undefined, + }, + ] + `); + }); + + test('batches several #get calls into a single HTTP call', async () => { + // Await #get call to ensure batchQueue is empty and throttle has reset + await savedObjectsClient.get('type2', doc.id); + http.fetch.mockClear(); + + // Make two #get calls right after one another + savedObjectsClient.get('type1', doc.id); + await savedObjectsClient.get('type0', doc.id); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_bulk_get", + Object { + "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"type1\\"},{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"type0\\"}]", + "method": "POST", + "query": undefined, + }, + ], + ] + `); + }); + + test('resolves with SimpleSavedObject instance', async () => { + const response = savedObjectsClient.get(doc.type, doc.id); + await expect(response).resolves.toBeInstanceOf(SimpleSavedObject); + + const result = await response; + expect(result.type).toBe('config'); + expect(result.get('title')).toBe('Example title'); + }); + }); + + describe('#delete', () => { + beforeEach(() => { + http.fetch.mockResolvedValue({}); + }); + + test('rejects if `type` parameter is undefined', async () => { + expect( + savedObjectsClient.delete(undefined as any, undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type and id]`); + }); + + test('throws if `id` parameter is undefined', async () => { + expect( + savedObjectsClient.delete('index-pattern', undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type and id]`); + }); + + test('makes HTTP call', async () => { + await expect(savedObjectsClient.delete('index-pattern', 'logstash-*')).resolves.toEqual({}); + expect(http.fetch.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/saved_objects/index-pattern/logstash-*", + Object { + "body": undefined, + "method": "DELETE", + "query": undefined, + }, + ] + `); + }); + }); + + describe('#update', () => { + const attributes = { foo: 'Foo', bar: 'Bar' }; + const options = { version: '1' }; + + beforeEach(() => { + http.fetch.mockResolvedValue({ type: 'index-pattern', attributes }); + }); + + test('rejects if `type` is undefined', async () => { + expect( + savedObjectsClient.update(undefined as any, undefined as any, undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type, id and attributes]`); + }); + + test('rejects if `id` is undefined', async () => { + expect( + savedObjectsClient.update('index-pattern', undefined as any, undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type, id and attributes]`); + }); + + test('rejects if `attributes` is undefined', async () => { + expect( + savedObjectsClient.update('index-pattern', 'logstash-*', undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type, id and attributes]`); + }); + + test('makes HTTP call', () => { + savedObjectsClient.update('index-pattern', 'logstash-*', attributes, options); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/index-pattern/logstash-*", + Object { + "body": "{\\"attributes\\":{\\"foo\\":\\"Foo\\",\\"bar\\":\\"Bar\\"},\\"version\\":\\"1\\"}", + "method": "PUT", + "query": undefined, + }, + ], + ] + `); + }); + + test('rejects when HTTP call fails', async () => { + http.fetch.mockRejectedValueOnce(new Error('Request failed')); + await expect( + savedObjectsClient.update('index-pattern', 'logstash-*', attributes, options) + ).rejects.toMatchInlineSnapshot(`[Error: Request failed]`); + }); + + test('resolves with SimpleSavedObject instance', async () => { + const response = savedObjectsClient.update( + 'index-pattern', + 'logstash-*', + attributes, + options + ); + await expect(response).resolves.toBeInstanceOf(SimpleSavedObject); + + const result = await response; + expect(result.type).toBe('index-pattern'); + expect(result.get('foo')).toBe('Foo'); + }); + }); + + describe('#create', () => { + const attributes = { foo: 'Foo', bar: 'Bar' }; + + beforeEach(() => { + http.fetch.mockResolvedValue({ id: 'serverId', type: 'server-type', attributes }); + }); + + test('rejects if `type` is undefined', async () => { + await expect( + savedObjectsClient.create(undefined as any, undefined as any) + ).rejects.toMatchInlineSnapshot(`[Error: requires type and attributes]`); + }); + + test('resolves with SimpleSavedObject instance', async () => { + const response = savedObjectsClient.create('index-pattern', attributes, { id: 'myId' }); + await expect(response).resolves.toBeInstanceOf(SimpleSavedObject); + + const result = await response; + + expect(result.type).toBe('server-type'); + expect(result.id).toBe('serverId'); + expect(result.attributes).toBe(attributes); + }); + + test('makes HTTP call with ID', () => { + savedObjectsClient.create('index-pattern', attributes, { id: 'myId' }); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/index-pattern/myId", + Object { + "body": "{\\"attributes\\":{\\"foo\\":\\"Foo\\",\\"bar\\":\\"Bar\\"}}", + "method": "POST", + "query": Object { + "overwrite": undefined, + }, + }, + ], + ] + `); + }); + + test('makes HTTP call without ID', () => { + savedObjectsClient.create('index-pattern', attributes); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/index-pattern", + Object { + "body": "{\\"attributes\\":{\\"foo\\":\\"Foo\\",\\"bar\\":\\"Bar\\"}}", + "method": "POST", + "query": Object { + "overwrite": undefined, + }, + }, + ], + ] + `); + }); + + test('rejects when HTTP call fails', async () => { + http.fetch.mockRejectedValueOnce(new Error('Request failed')); + await expect( + savedObjectsClient.create('index-pattern', attributes, { id: 'myId' }) + ).rejects.toMatchInlineSnapshot(`[Error: Request failed]`); + }); + }); + + describe('#bulk_create', () => { + beforeEach(() => { + http.fetch.mockResolvedValue({ saved_objects: [doc] }); + }); + + test('resolves with array of SimpleSavedObject instances', async () => { + const response = savedObjectsClient.bulkCreate([doc]); + await expect(response).resolves.toHaveProperty('savedObjects'); + + const result = await response; + expect(result.savedObjects).toHaveLength(1); + expect(result.savedObjects[0]).toBeInstanceOf(SimpleSavedObject); + }); + + test('makes HTTP call', async () => { + await savedObjectsClient.bulkCreate([doc]); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_bulk_create", + Object { + "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\",\\"attributes\\":{\\"title\\":\\"Example title\\"},\\"version\\":\\"foo\\"}]", + "method": "POST", + "query": Object { + "overwrite": false, + }, + }, + ], + ] + `); + }); + + test('makes HTTP call with overwrite query paramater', async () => { + await savedObjectsClient.bulkCreate([doc], { overwrite: true }); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_bulk_create", + Object { + "body": "[{\\"id\\":\\"AVwSwFxtcMV38qjDZoQg\\",\\"type\\":\\"config\\",\\"attributes\\":{\\"title\\":\\"Example title\\"},\\"version\\":\\"foo\\"}]", + "method": "POST", + "query": Object { + "overwrite": true, + }, + }, + ], + ] + `); + }); + }); + + describe('#find', () => { + const object = { id: 'logstash-*', type: 'index-pattern', title: 'Test' }; + + beforeEach(() => { + http.fetch.mockResolvedValue({ saved_objects: [object], page: 0, per_page: 1, total: 1 }); + }); + + test('resolves with instances of SimpleSavedObjects', async () => { + const options = { type: 'index-pattern' }; + const resultP = savedObjectsClient.find(options); + await expect(resultP).resolves.toHaveProperty('savedObjects'); + + const result = await resultP; + expect(result.savedObjects).toHaveLength(1); + expect(result.savedObjects[0]).toBeInstanceOf(SimpleSavedObject); + expect(result.page).toBe(0); + expect(result.perPage).toBe(1); + expect(result.total).toBe(1); + }); + + test('makes HTTP call correctly mapping options into snake case query parameters', () => { + const options = { + defaultSearchOperator: 'OR' as const, + fields: ['title'], + hasReference: { id: '1', type: 'reference' }, + page: 10, + perPage: 100, + search: 'what is the meaning of life?|life', + searchFields: ['title^5', 'body'], + sortField: 'sort_field', + type: 'index-pattern', + }; + + savedObjectsClient.find(options); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_find", + Object { + "body": undefined, + "method": "GET", + "query": Object { + "default_search_operator": "OR", + "fields": Array [ + "title", + ], + "has_reference": Object { + "id": "1", + "type": "reference", + }, + "page": 10, + "per_page": 100, + "search": "what is the meaning of life?|life", + "search_fields": Array [ + "title^5", + "body", + ], + "sort_field": "sort_field", + "type": "index-pattern", + }, + }, + ], + ] + `); + }); + + test('ignores invalid options', () => { + const options = { + invalid: true, + namespace: 'default', + sortOrder: 'sort', // Not currently supported by API + }; + + // @ts-ignore + savedObjectsClient.find(options); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/_find", + Object { + "body": undefined, + "method": "GET", + "query": Object {}, + }, + ], + ] + `); + }); + }); + + it('maintains backwards compatibility by transforming http.fetch errors to be compatible with kfetch errors', () => { + const err = { + response: { ok: false, redirected: false, status: 409, statusText: 'Conflict' }, + body: 'response body', + }; + http.fetch.mockRejectedValue(err); + return expect(savedObjectsClient.get(doc.type, doc.id)).rejects.toMatchInlineSnapshot(` + Object { + "body": "response body", + "res": Object { + "ok": false, + "redirected": false, + "status": 409, + "statusText": "Conflict", + }, + } + `); + }); +}); diff --git a/src/legacy/ui/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts similarity index 58% rename from src/legacy/ui/public/saved_objects/saved_objects_client.ts rename to src/core/public/saved_objects/saved_objects_client.ts index d8fb3af1c66ac..b0768826159cc 100644 --- a/src/legacy/ui/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -25,46 +25,80 @@ import { SavedObjectAttributes, SavedObjectReference, SavedObjectsClientContract as SavedObjectsApi, - SavedObjectsFindOptions, + SavedObjectsFindOptions as SavedObjectFindOptionsServer, SavedObjectsMigrationVersion, -} from 'src/core/server'; -import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index'; -import { kfetch, KFetchQuery } from '../kfetch'; -import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../utils/case_conversion'; +} from '../../server'; + +// TODO: Migrate to an error modal powered by the NP? +import { + isAutoCreateIndexError, + showAutoCreateIndexErrorPage, +} from '../../../legacy/ui/public/error_auto_create_index/error_auto_create_index'; import { SimpleSavedObject } from './simple_saved_object'; +import { HttpFetchOptions, HttpServiceBase } from '../http'; -interface RequestParams { - method: 'POST' | 'GET' | 'PUT' | 'DELETE'; - path: string; - query?: KFetchQuery; - body?: object; -} +type SavedObjectsFindOptions = Omit; -interface CreateOptions { +type PromiseType> = T extends Promise ? U : never; + +/** @public */ +export interface SavedObjectsCreateOptions { + /** + * (Not recommended) Specify an id instead of having the saved objects service generate one for you. + */ id?: string; + /** If a document with the given `id` already exists, overwrite it's contents (default=false). */ overwrite?: boolean; + /** {@inheritDoc SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; references?: SavedObjectReference[]; } -interface BulkCreateOptions - extends CreateOptions { +/** + * @param type - Create a SavedObject of the given type + * @param attributes - Create a SavedObject with the given attributes + * + * @public + */ +export interface SavedObjectsBulkCreateObject< + T extends SavedObjectAttributes = SavedObjectAttributes +> extends SavedObjectsCreateOptions { type: string; attributes: T; } -interface UpdateOptions { +/** @public */ +export interface SavedObjectsBulkCreateOptions { + /** If a document with the given `id` already exists, overwrite it's contents (default=false). */ + overwrite?: boolean; +} + +/** @public */ +export interface SavedObjectsUpdateOptions { version?: string; + /** {@inheritDoc SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; references?: SavedObjectReference[]; } -interface BatchResponse { +/** @public */ +export interface SavedObjectsBatchResponse< + T extends SavedObjectAttributes = SavedObjectAttributes +> { savedObjects: Array>; } -interface FindResults - extends BatchResponse { +/** + * Return type of the Saved Objects `find()` method. + * + * *Note*: this type is different between the Public and Server Saved Objects + * clients. + * + * @public + */ +export interface SavedObjectsFindResponsePublic< + T extends SavedObjectAttributes = SavedObjectAttributes +> extends SavedObjectsBatchResponse { total: number; perPage: number; page: number; @@ -92,14 +126,24 @@ const BATCH_INTERVAL = 100; const API_BASE_URL = '/api/saved_objects/'; /** - * The SavedObjectsClient class acts as a generic data fetcher - * and data saver for saved objects regardless of type. + * SavedObjectsClientContract as implemented by the {@link SavedObjectsClient} + * + * @public + */ +export type SavedObjectsClientContract = PublicMethodsOf; + +/** + * Saved Objects is Kibana's data persisentence mechanism allowing plugins to + * use Elasticsearch for storing plugin state. The client-side + * SavedObjectsClient is a thin convenience library around the SavedObjects + * HTTP API for interacting with Saved Objects. * - * If possible, this class should be used to load saved objects - * instead of the SavedObjectLoader class which implements some - * additional functionality. + * @public */ export class SavedObjectsClient { + private http: HttpServiceBase; + private batchQueue: BatchQueueEntry[]; + /** * Throttled processing of get requests into bulk requests at 100ms interval */ @@ -132,27 +176,24 @@ export class SavedObjectsClient { { leading: false } ); - private batchQueue: BatchQueueEntry[]; - - constructor() { + /** @internal */ + constructor(http: HttpServiceBase) { + this.http = http; this.batchQueue = []; } /** * Persists an object * - * @param {string} type - * @param {object} [attributes={}] - * @param {object} [options={}] - * @property {string} [options.id] - force id on creation, not recommended - * @property {boolean} [options.overwrite=false] - * @property {object} [options.migrationVersion] + * @param type + * @param attributes + * @param options * @returns */ public create = ( type: string, attributes: T, - options: CreateOptions = {} + options: SavedObjectsCreateOptions = {} ): Promise> => { if (!type || !attributes) { return Promise.reject(new Error('requires type and attributes')); @@ -163,15 +204,14 @@ export class SavedObjectsClient { overwrite: options.overwrite, }; - const createRequest: Promise> = this.request({ + const createRequest: Promise> = this.savedObjectsFetch(path, { method: 'POST', - path, query, - body: { + body: JSON.stringify({ attributes, migrationVersion: options.migrationVersion, references: options.references, - }, + }), }); return createRequest @@ -193,19 +233,24 @@ export class SavedObjectsClient { * @property {boolean} [options.overwrite=false] * @returns The result of the create operation containing created saved objects. */ - public bulkCreate = (objects: BulkCreateOptions[] = [], options: KFetchQuery = {}) => { + public bulkCreate = ( + objects: SavedObjectsBulkCreateObject[] = [], + options: SavedObjectsBulkCreateOptions = { overwrite: false } + ) => { const path = this.getPath(['_bulk_create']); - const query = pick(options, ['overwrite']) as Pick; + const query = { overwrite: options.overwrite }; - const request: ReturnType = this.request({ + const request: ReturnType = this.savedObjectsFetch(path, { method: 'POST', - path, query, - body: objects, + body: JSON.stringify(objects), }); return request.then(resp => { resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); - return keysToCamelCaseShallow(resp) as BatchResponse; + return renameKeys< + PromiseType>, + SavedObjectsBatchResponse + >({ saved_objects: 'savedObjects' }, resp) as SavedObjectsBatchResponse; }); }; @@ -221,7 +266,7 @@ export class SavedObjectsClient { return Promise.reject(new Error('requires type and id')); } - return this.request({ method: 'DELETE', path: this.getPath([type, id]) }); + return this.savedObjectsFetch(this.getPath([type, id]), { method: 'DELETE' }); }; /** @@ -240,18 +285,41 @@ export class SavedObjectsClient { */ public find = ( options: SavedObjectsFindOptions = {} - ): Promise> => { + ): Promise> => { const path = this.getPath(['_find']); - const query = keysToSnakeCaseShallow(options); + const renameMap = { + defaultSearchOperator: 'default_search_operator', + fields: 'fields', + hasReference: 'has_reference', + page: 'page', + perPage: 'per_page', + search: 'search', + searchFields: 'search_fields', + sortField: 'sort_field', + type: 'type', + }; + + const renamedQuery = renameKeys(renameMap, options); + const query = pick.apply(null, [renamedQuery, ...Object.values(renameMap)]); - const request: ReturnType = this.request({ + const request: ReturnType = this.savedObjectsFetch(path, { method: 'GET', - path, query, }); return request.then(resp => { resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); - return keysToCamelCaseShallow(resp) as FindResults; + return renameKeys< + PromiseType>, + SavedObjectsFindResponsePublic + >( + { + saved_objects: 'savedObjects', + total: 'total', + per_page: 'perPage', + page: 'page', + }, + resp + ) as SavedObjectsFindResponsePublic; }); }; @@ -292,14 +360,16 @@ export class SavedObjectsClient { const path = this.getPath(['_bulk_get']); const filteredObjects = objects.map(obj => pick(obj, ['id', 'type'])); - const request: ReturnType = this.request({ + const request: ReturnType = this.savedObjectsFetch(path, { method: 'POST', - path, - body: filteredObjects, + body: JSON.stringify(filteredObjects), }); return request.then(resp => { resp.saved_objects = resp.saved_objects.map(d => this.createSavedObject(d)); - return keysToCamelCaseShallow(resp) as BatchResponse; + return renameKeys< + PromiseType>, + SavedObjectsBatchResponse + >({ saved_objects: 'savedObjects' }, resp) as SavedObjectsBatchResponse; }); }; @@ -318,7 +388,7 @@ export class SavedObjectsClient { type: string, id: string, attributes: T, - { version, migrationVersion, references }: UpdateOptions = {} + { version, migrationVersion, references }: SavedObjectsUpdateOptions = {} ): Promise> { if (!type || !id || !attributes) { return Promise.reject(new Error('requires type, id and attributes')); @@ -332,10 +402,9 @@ export class SavedObjectsClient { version, }; - return this.request({ + return this.savedObjectsFetch(path, { method: 'PUT', - path, - body, + body: JSON.stringify(body), }).then((resp: SavedObject) => { return this.createSavedObject(resp); }); @@ -351,11 +420,34 @@ export class SavedObjectsClient { return resolveUrl(API_BASE_URL, join(...path)); } - private request({ method, path, query, body }: RequestParams) { - if (method === 'GET' && body) { - return Promise.reject(new Error('body not permitted for GET requests')); - } - - return kfetch({ method, pathname: path, query, body: JSON.stringify(body) }); + /** + * To ensure we don't break backwards compatibility, savedObjectsFetch keeps + * the old kfetch error format of `{res: {status: number}}` whereas `http.fetch` + * uses `{response: {status: number}}`. + */ + private savedObjectsFetch(path: string, { method, query, body }: HttpFetchOptions) { + return this.http.fetch(path, { method, query, body }).catch(err => { + const kfetchError = Object.assign(err, { res: err.response }); + delete kfetchError.response; + return Promise.reject(kfetchError); + }); } } + +/** + * Returns a new object with the own properties of `obj`, but the + * keys renamed according to the `keysMap`. + * + * @param keysMap - a map of the form `{oldKey: newKey}` + * @param obj - the object whose own properties will be renamed + */ +const renameKeys = , U extends Record>( + keysMap: Record, + obj: Record +) => + Object.keys(obj).reduce((acc, key) => { + return { + ...acc, + ...{ [keysMap[key] || key]: obj[key] }, + }; + }, {}); diff --git a/src/core/public/saved_objects/saved_objects_service.mock.ts b/src/core/public/saved_objects/saved_objects_service.mock.ts new file mode 100644 index 0000000000000..feace09806a97 --- /dev/null +++ b/src/core/public/saved_objects/saved_objects_service.mock.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsService, SavedObjectsStart } from './saved_objects_service'; + +const createStartContractMock = () => { + const mock: jest.Mocked = { + client: { + create: jest.fn(), + bulkCreate: jest.fn(), + delete: jest.fn(), + bulkGet: jest.fn(), + find: jest.fn(), + get: jest.fn(), + update: jest.fn(), + }, + }; + return mock; +}; + +const createMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + }; + mocked.start.mockReturnValue(Promise.resolve(createStartContractMock())); + return mocked; +}; + +export const savedObjectsMock = { + create: createMock, + createStartContract: createStartContractMock, +}; diff --git a/src/core/public/saved_objects/saved_objects_service.ts b/src/core/public/saved_objects/saved_objects_service.ts new file mode 100644 index 0000000000000..bb91c6f340c0b --- /dev/null +++ b/src/core/public/saved_objects/saved_objects_service.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreService } from 'src/core/types'; +import { CoreStart } from 'src/core/public'; +import { SavedObjectsClient, SavedObjectsClientContract } from './saved_objects_client'; + +/** + * @public + */ +export interface SavedObjectsStart { + /** {@link SavedObjectsClient} */ + client: SavedObjectsClientContract; +} + +export class SavedObjectsService implements CoreService { + public async setup() {} + public async start({ http }: { http: CoreStart['http'] }): Promise { + return { client: new SavedObjectsClient(http) }; + } + public async stop() {} +} diff --git a/src/legacy/ui/public/saved_objects/simple_saved_object.ts b/src/core/public/saved_objects/simple_saved_object.ts similarity index 90% rename from src/legacy/ui/public/saved_objects/simple_saved_object.ts rename to src/core/public/saved_objects/simple_saved_object.ts index 26488bdeb1ab1..92c228edd5e8e 100644 --- a/src/legacy/ui/public/saved_objects/simple_saved_object.ts +++ b/src/core/public/saved_objects/simple_saved_object.ts @@ -18,16 +18,17 @@ */ import { get, has, set } from 'lodash'; -import { SavedObject as SavedObjectType, SavedObjectAttributes } from 'src/core/server'; +import { SavedObject as SavedObjectType, SavedObjectAttributes } from '../../server'; import { SavedObjectsClient } from './saved_objects_client'; /** - * This class is a very simple wrapper for SavedObjects loaded from the server. + * This class is a very simple wrapper for SavedObjects loaded from the server + * with the {@link SavedObjectsClient}. * - * It provides basic functionality for updating/deleting/etc. saved objects but - * doesn't include any type-specific implementations. + * It provides basic functionality for creating/saving/deleting saved objects, + * but doesn't include any type-specific implementations. * - * For more sophisiticated use cases, the SavedObject class implements additional functions + * @public */ export class SimpleSavedObject { public attributes: T; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 424f1730683e4..65b986a1d78c5 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -105,43 +105,48 @@ export { } from './plugins'; export { - SavedObject, - SavedObjectAttributes, - SavedObjectReference, - SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, SavedObjectsBulkResponse, SavedObjectsClient, - SavedObjectsClientContract, - SavedObjectsCreateOptions, SavedObjectsClientProviderOptions, SavedObjectsClientWrapperFactory, SavedObjectsClientWrapperOptions, + SavedObjectsCreateOptions, SavedObjectsErrorHelpers, - SavedObjectsFindOptions, - SavedObjectsFindResponse, - SavedObjectsMigrationVersion, - SavedObjectsRawDoc, - SavedObjectsSchema, - SavedObjectsSerializer, - SavedObjectsService, - SavedObjectsUpdateOptions, - SavedObjectsUpdateResponse, SavedObjectsExportOptions, - SavedObjectsImportError, + SavedObjectsFindResponse, SavedObjectsImportConflictError, + SavedObjectsImportError, SavedObjectsImportMissingReferencesError, - SavedObjectsImportUnknownError, - SavedObjectsImportUnsupportedTypeError, SavedObjectsImportOptions, SavedObjectsImportResponse, SavedObjectsImportRetry, + SavedObjectsImportUnknownError, + SavedObjectsImportUnsupportedTypeError, + SavedObjectsMigrationLogger, + SavedObjectsRawDoc, SavedObjectsResolveImportErrorsOptions, + SavedObjectsSchema, + SavedObjectsSerializer, + SavedObjectsService, + SavedObjectsUpdateOptions, + SavedObjectsUpdateResponse, } from './saved_objects'; export { RecursiveReadonly } from '../utils'; +export { + SavedObject, + SavedObjectAttribute, + SavedObjectAttributes, + SavedObjectReference, + SavedObjectsBaseOptions, + SavedObjectsClientContract, + SavedObjectsFindOptions, + SavedObjectsMigrationVersion, +} from './types'; + /** * Context passed to the plugins `setup` method. * diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts index d8513bcfe72d3..08795e9fc7738 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts @@ -19,7 +19,7 @@ import Boom from 'boom'; import { createListStream } from '../../../../legacy/utils/streams'; -import { SavedObjectsClientContract } from '../'; +import { SavedObjectsClientContract } from '../types'; import { injectNestedDependencies } from './inject_nested_depdendencies'; import { sortObjects } from './sort_objects'; diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts index 2fa06550727f0..4613553fbd301 100644 --- a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts +++ b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObject } from '../service/saved_objects_client'; +import { SavedObject } from '../types'; import { getObjectReferencesToFetch, injectNestedDependencies, diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.ts b/src/core/server/saved_objects/export/inject_nested_depdendencies.ts index 82cb3e3cfe115..279b06f955571 100644 --- a/src/core/server/saved_objects/export/inject_nested_depdendencies.ts +++ b/src/core/server/saved_objects/export/inject_nested_depdendencies.ts @@ -18,7 +18,7 @@ */ import Boom from 'boom'; -import { SavedObject, SavedObjectsClientContract } from '../service/saved_objects_client'; +import { SavedObject, SavedObjectsClientContract } from '../types'; export function getObjectReferencesToFetch(savedObjectsMap: Map) { const objectsToFetch = new Map(); diff --git a/src/core/server/saved_objects/export/sort_objects.ts b/src/core/server/saved_objects/export/sort_objects.ts index 84640db3635e9..522146737d9cf 100644 --- a/src/core/server/saved_objects/export/sort_objects.ts +++ b/src/core/server/saved_objects/export/sort_objects.ts @@ -18,7 +18,7 @@ */ import Boom from 'boom'; -import { SavedObject } from '../service/saved_objects_client'; +import { SavedObject } from '../types'; export function sortObjects(savedObjects: SavedObject[]): SavedObject[] { const path = new Set(); diff --git a/src/core/server/saved_objects/import/collect_saved_objects.ts b/src/core/server/saved_objects/import/collect_saved_objects.ts index fa2938109f6e7..65ffd4d9a1d57 100644 --- a/src/core/server/saved_objects/import/collect_saved_objects.ts +++ b/src/core/server/saved_objects/import/collect_saved_objects.ts @@ -24,7 +24,7 @@ import { createMapStream, createPromiseFromStreams, } from '../../../../legacy/utils/streams'; -import { SavedObject } from '../service'; +import { SavedObject } from '../types'; import { createLimitStream } from './create_limit_stream'; import { SavedObjectsImportError } from './types'; diff --git a/src/core/server/saved_objects/import/create_objects_filter.ts b/src/core/server/saved_objects/import/create_objects_filter.ts index 684e44944002b..c48cded00f1ec 100644 --- a/src/core/server/saved_objects/import/create_objects_filter.ts +++ b/src/core/server/saved_objects/import/create_objects_filter.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObject } from '../service'; +import { SavedObject } from '../types'; import { SavedObjectsImportRetry } from './types'; export function createObjectsFilter(retries: SavedObjectsImportRetry[]) { diff --git a/src/core/server/saved_objects/import/extract_errors.test.ts b/src/core/server/saved_objects/import/extract_errors.test.ts index ad2b1467923a6..f97cc661c0bca 100644 --- a/src/core/server/saved_objects/import/extract_errors.test.ts +++ b/src/core/server/saved_objects/import/extract_errors.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObject } from '../service'; +import { SavedObject } from '../types'; import { extractErrors } from './extract_errors'; describe('extractErrors()', () => { diff --git a/src/core/server/saved_objects/import/extract_errors.ts b/src/core/server/saved_objects/import/extract_errors.ts index 4c001cf8acf64..725e935f6e21d 100644 --- a/src/core/server/saved_objects/import/extract_errors.ts +++ b/src/core/server/saved_objects/import/extract_errors.ts @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - -import { SavedObject } from '../service'; +import { SavedObject } from '../types'; import { SavedObjectsImportError } from './types'; export function extractErrors( diff --git a/src/core/server/saved_objects/import/import_saved_objects.test.ts b/src/core/server/saved_objects/import/import_saved_objects.test.ts index 6ba4304574f49..194756462fc78 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.test.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.test.ts @@ -18,7 +18,7 @@ */ import { Readable } from 'stream'; -import { SavedObject } from '../service'; +import { SavedObject } from '../types'; import { importSavedObjects } from './import_saved_objects'; describe('importSavedObjects()', () => { diff --git a/src/core/server/saved_objects/import/resolve_import_errors.test.ts b/src/core/server/saved_objects/import/resolve_import_errors.test.ts index 783b0d6a61be6..9d0e133c5951c 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.test.ts @@ -18,7 +18,7 @@ */ import { Readable } from 'stream'; -import { SavedObject } from '../service'; +import { SavedObject } from '../types'; import { resolveImportErrors } from './resolve_import_errors'; describe('resolveImportErrors()', () => { diff --git a/src/core/server/saved_objects/import/resolve_import_errors.ts b/src/core/server/saved_objects/import/resolve_import_errors.ts index c04827dc98ab5..6f56f283b4aec 100644 --- a/src/core/server/saved_objects/import/resolve_import_errors.ts +++ b/src/core/server/saved_objects/import/resolve_import_errors.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import { collectSavedObjects } from './collect_saved_objects'; import { createObjectsFilter } from './create_objects_filter'; import { extractErrors } from './extract_errors'; diff --git a/src/core/server/saved_objects/import/split_overwrites.ts b/src/core/server/saved_objects/import/split_overwrites.ts index f49b634c8d9f2..192d43e9edb24 100644 --- a/src/core/server/saved_objects/import/split_overwrites.ts +++ b/src/core/server/saved_objects/import/split_overwrites.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObject } from '../service'; +import { SavedObject } from '../types'; import { SavedObjectsImportRetry } from './types'; export function splitOverwrites(savedObjects: SavedObject[], retries: SavedObjectsImportRetry[]) { diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index cc16a1697d9a0..44046378a7b97 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -18,7 +18,7 @@ */ import { Readable } from 'stream'; -import { SavedObjectsClientContract } from '../service'; +import { SavedObjectsClientContract } from '../types'; /** * Describes a retry operation for importing a saved object. diff --git a/src/core/server/saved_objects/import/validate_references.ts b/src/core/server/saved_objects/import/validate_references.ts index ad3f73b68f6e0..4d9ee59f9df15 100644 --- a/src/core/server/saved_objects/import/validate_references.ts +++ b/src/core/server/saved_objects/import/validate_references.ts @@ -18,7 +18,7 @@ */ import Boom from 'boom'; -import { SavedObject, SavedObjectsClientContract } from '../'; +import { SavedObject, SavedObjectsClientContract } from '../types'; import { SavedObjectsImportError } from './types'; const REF_TYPES_TO_VLIDATE = ['index-pattern', 'search']; diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index ef0362e0eb915..1a667d6978f1a 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -28,3 +28,5 @@ export * from './import'; export { getSortedObjectsForExport, SavedObjectsExportOptions } from './export'; export { SavedObjectsSerializer, RawDoc as SavedObjectsRawDoc } from './serialization'; + +export { SavedObjectsMigrationLogger } from './migrations/core/migration_logger'; diff --git a/src/core/server/saved_objects/management/management.ts b/src/core/server/saved_objects/management/management.ts index c2c6789615b70..7b5274da91fc8 100644 --- a/src/core/server/saved_objects/management/management.ts +++ b/src/core/server/saved_objects/management/management.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObject } from '../service'; +import { SavedObject } from '../types'; interface SavedObjectsManagementTypeDefinition { isImportableAndExportable?: boolean; diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 578fe49b2d3cc..0576f1d22199f 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -65,10 +65,13 @@ import _ from 'lodash'; import cloneDeep from 'lodash.clonedeep'; import Semver from 'semver'; import { RawSavedObjectDoc } from '../../serialization'; -import { SavedObjectsMigrationVersion } from '../../'; -import { LogFn, Logger, MigrationLogger } from './migration_logger'; +import { SavedObjectsMigrationVersion } from '../../types'; +import { LogFn, SavedObjectsMigrationLogger, MigrationLogger } from './migration_logger'; -export type TransformFn = (doc: RawSavedObjectDoc, log?: Logger) => RawSavedObjectDoc; +export type TransformFn = ( + doc: RawSavedObjectDoc, + log?: SavedObjectsMigrationLogger +) => RawSavedObjectDoc; type ValidateDoc = (doc: RawSavedObjectDoc) => void; @@ -204,7 +207,10 @@ function validateMigrationDefinition(migrations: MigrationDefinition) { * From: { type: { version: fn } } * To: { type: { latestVersion: string, transforms: [{ version: string, transform: fn }] } } */ -function buildActiveMigrations(migrations: MigrationDefinition, log: Logger): ActiveMigrations { +function buildActiveMigrations( + migrations: MigrationDefinition, + log: SavedObjectsMigrationLogger +): ActiveMigrations { return _.mapValues(migrations, (versions, prop) => { const transforms = Object.entries(versions) .map(([version, transform]) => ({ @@ -293,7 +299,12 @@ function markAsUpToDate(doc: RawSavedObjectDoc, migrations: ActiveMigrations) { * If a specific transform function fails, this tacks on a bit of information * about the document and transform that caused the failure. */ -function wrapWithTry(version: string, prop: string, transform: TransformFn, log: Logger) { +function wrapWithTry( + version: string, + prop: string, + transform: TransformFn, + log: SavedObjectsMigrationLogger +) { return function tryTransformDoc(doc: RawSavedObjectDoc) { try { const result = transform(doc, log); diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts index da76905d1c65c..e7621d88f78ee 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts @@ -24,7 +24,7 @@ import _ from 'lodash'; import { IndexMapping } from '../../mappings'; -import { SavedObjectsMigrationVersion } from '../../'; +import { SavedObjectsMigrationVersion } from '../../types'; import { AliasAction, CallCluster, NotFound, RawDoc, ShardsInfo } from './call_cluster'; const settings = { number_of_shards: 1, auto_expand_replicas: '0-1' }; diff --git a/src/core/server/saved_objects/migrations/core/migration_context.ts b/src/core/server/saved_objects/migrations/core/migration_context.ts index a151a8d37a524..633bccf8aceec 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.ts @@ -30,7 +30,7 @@ import { buildActiveMappings } from './build_active_mappings'; import { CallCluster } from './call_cluster'; import { VersionedTransformer } from './document_migrator'; import { fetchInfo, FullIndexInfo } from './elastic_index'; -import { LogFn, Logger, MigrationLogger } from './migration_logger'; +import { LogFn, SavedObjectsMigrationLogger, MigrationLogger } from './migration_logger'; export interface MigrationOpts { batchSize: number; @@ -57,7 +57,7 @@ export interface Context { source: FullIndexInfo; dest: FullIndexInfo; documentMigrator: VersionedTransformer; - log: Logger; + log: SavedObjectsMigrationLogger; batchSize: number; pollInterval: number; scrollDuration: string; diff --git a/src/core/server/saved_objects/migrations/core/migration_coordinator.ts b/src/core/server/saved_objects/migrations/core/migration_coordinator.ts index c424b88335a98..ddd82edd93448 100644 --- a/src/core/server/saved_objects/migrations/core/migration_coordinator.ts +++ b/src/core/server/saved_objects/migrations/core/migration_coordinator.ts @@ -35,7 +35,7 @@ */ import _ from 'lodash'; -import { Logger } from './migration_logger'; +import { SavedObjectsMigrationLogger } from './migration_logger'; const DEFAULT_POLL_INTERVAL = 15000; @@ -52,7 +52,7 @@ export type MigrationResult = interface Opts { runMigration: () => Promise; isMigrated: () => Promise; - log: Logger; + log: SavedObjectsMigrationLogger; pollInterval?: number; } @@ -86,7 +86,7 @@ export async function coordinateMigration(opts: Opts): Promise * and is the cue for us to fall into a polling loop, waiting for some * other Kibana instance to complete the migration. */ -function handleIndexExists(error: any, log: Logger) { +function handleIndexExists(error: any, log: SavedObjectsMigrationLogger) { const isIndexExistsError = _.get(error, 'body.error.type') === 'resource_already_exists_exception'; if (!isIndexExistsError) { diff --git a/src/core/server/saved_objects/migrations/core/migration_logger.ts b/src/core/server/saved_objects/migrations/core/migration_logger.ts index 8b9e3f0a21ac3..9c98b7d85a8d8 100644 --- a/src/core/server/saved_objects/migrations/core/migration_logger.ts +++ b/src/core/server/saved_objects/migrations/core/migration_logger.ts @@ -24,13 +24,14 @@ export type LogFn = (path: string[], message: string) => void; -export interface Logger { +/** @public */ +export interface SavedObjectsMigrationLogger { debug: (msg: string) => void; info: (msg: string) => void; warning: (msg: string) => void; } -export class MigrationLogger implements Logger { +export class MigrationLogger implements SavedObjectsMigrationLogger { private log: LogFn; constructor(log: LogFn) { diff --git a/src/core/server/saved_objects/serialization/index.ts b/src/core/server/saved_objects/serialization/index.ts index 86a448ba8a5be..bd875db2001f4 100644 --- a/src/core/server/saved_objects/serialization/index.ts +++ b/src/core/server/saved_objects/serialization/index.ts @@ -27,10 +27,7 @@ import uuid from 'uuid'; import { SavedObjectsSchema } from '../schema'; import { decodeVersion, encodeVersion } from '../version'; -import { - SavedObjectsMigrationVersion, - SavedObjectReference, -} from '../service/saved_objects_client'; +import { SavedObjectsMigrationVersion, SavedObjectReference } from '../types'; /** * A raw document as represented directly in the saved object index. diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 35b5eedb2da6e..e93d9e4047501 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -30,19 +30,21 @@ import { KibanaMigrator } from '../../migrations'; import { Config } from '../../../config'; import { SavedObjectsSerializer, SanitizedSavedObjectDoc, RawDoc } from '../../serialization'; import { - SavedObject, - SavedObjectAttributes, - SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, SavedObjectsBulkResponse, SavedObjectsCreateOptions, - SavedObjectsFindOptions, SavedObjectsFindResponse, - SavedObjectsMigrationVersion, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, } from '../saved_objects_client'; +import { + SavedObject, + SavedObjectAttributes, + SavedObjectsBaseOptions, + SavedObjectsFindOptions, + SavedObjectsMigrationVersion, +} from '../../types'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. diff --git a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts index e0ca16e254e18..0e93f9c443f14 100644 --- a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts +++ b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts @@ -17,7 +17,7 @@ * under the License. */ import { PriorityCollection } from './priority_collection'; -import { SavedObjectsClientContract } from '..'; +import { SavedObjectsClientContract } from '../../types'; /** * Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. diff --git a/src/core/server/saved_objects/service/saved_objects_client.mock.ts b/src/core/server/saved_objects/service/saved_objects_client.mock.ts index 4d1ceeaf552b6..60bd5f96ff94d 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.mock.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.mock.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObjectsClientContract } from './saved_objects_client'; +import { SavedObjectsClientContract } from '../types'; import { SavedObjectsErrorHelpers } from './lib/errors'; const create = (): jest.Mocked => ({ diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index adc25a6d045e9..039579c5a2d14 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -18,20 +18,18 @@ */ import { SavedObjectsRepository } from './lib'; - +import { + SavedObject, + SavedObjectAttributes, + SavedObjectReference, + SavedObjectsMigrationVersion, + SavedObjectsBaseOptions, + SavedObjectsFindOptions, +} from '../types'; import { SavedObjectsErrorHelpers } from './lib/errors'; type Omit = Pick>; -/** - * - * @public - */ -export interface SavedObjectsBaseOptions { - /** Specify the namespace for this operation */ - namespace?: string; -} - /** * * @public @@ -41,6 +39,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { id?: string; /** Overwrite existing documents (defaults to false) */ overwrite?: boolean; + /** {@inheritDoc SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; references?: SavedObjectReference[]; } @@ -54,6 +53,7 @@ export interface SavedObjectsBulkCreateObject } /** + * Return type of the Saved Objects `find()` method. * - * @public - */ -export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { - type?: string | string[]; - page?: number; - perPage?: number; - sortField?: string; - sortOrder?: string; - fields?: string[]; - search?: string; - /** see Elasticsearch Simple Query String Query field argument for more information */ - searchFields?: string[]; - hasReference?: { type: string; id: string }; - defaultSearchOperator?: 'AND' | 'OR'; -} - -/** + * *Note*: this type is different between the Public and Server Saved Objects + * clients. * * @public */ @@ -132,135 +118,6 @@ export interface SavedObjectsUpdateResponse; } -/** - * A dictionary of saved object type -> version used to determine - * what migrations need to be applied to a saved object. - * - * @public - */ -export interface SavedObjectsMigrationVersion { - [pluginName: string]: string; -} - -/** - * - * @public - */ -export type SavedObjectAttribute = - | string - | number - | boolean - | null - | undefined - | SavedObjectAttributes - | SavedObjectAttributes[]; - -/** - * - * @public - */ -export interface SavedObjectAttributes { - [key: string]: SavedObjectAttribute | SavedObjectAttribute[]; -} - -/** - * - * @public - */ -export interface SavedObject { - id: string; - type: string; - version?: string; - updated_at?: string; - error?: { - message: string; - statusCode: number; - }; - attributes: T; - references: SavedObjectReference[]; - migrationVersion?: SavedObjectsMigrationVersion; -} - -/** - * A reference to another saved object. - * - * @public - */ -export interface SavedObjectReference { - name: string; - type: string; - id: string; -} - -/** - * ## SavedObjectsClient errors - * - * Since the SavedObjectsClient has its hands in everything we - * are a little paranoid about the way we present errors back to - * to application code. Ideally, all errors will be either: - * - * 1. Caused by bad implementation (ie. undefined is not a function) and - * as such unpredictable - * 2. An error that has been classified and decorated appropriately - * by the decorators in {@link SavedObjectsErrorHelpers} - * - * Type 1 errors are inevitable, but since all expected/handle-able errors - * should be Type 2 the `isXYZError()` helpers exposed at - * `SavedObjectsErrorHelpers` should be used to understand and manage error - * responses from the `SavedObjectsClient`. - * - * Type 2 errors are decorated versions of the source error, so if - * the elasticsearch client threw an error it will be decorated based - * on its type. That means that rather than looking for `error.body.error.type` or - * doing substring checks on `error.body.error.reason`, just use the helpers to - * understand the meaning of the error: - * - * ```js - * if (SavedObjectsErrorHelpers.isNotFoundError(error)) { - * // handle 404 - * } - * - * if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { - * // 401 handling should be automatic, but in case you wanted to know - * } - * - * // always rethrow the error unless you handle it - * throw error; - * ``` - * - * ### 404s from missing index - * - * From the perspective of application code and APIs the SavedObjectsClient is - * a black box that persists objects. One of the internal details that users have - * no control over is that we use an elasticsearch index for persistance and that - * index might be missing. - * - * At the time of writing we are in the process of transitioning away from the - * operating assumption that the SavedObjects index is always available. Part of - * this transition is handling errors resulting from an index missing. These used - * to trigger a 500 error in most cases, and in others cause 404s with different - * error messages. - * - * From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The - * object the request/call was targeting could not be found. This is why #14141 - * takes special care to ensure that 404 errors are generic and don't distinguish - * between index missing or document missing. - * - * ### 503s from missing index - * - * Unlike all other methods, create requests are supposed to succeed even when - * the Kibana index does not exist because it will be automatically created by - * elasticsearch. When that is not the case it is because Elasticsearch's - * `action.auto_create_index` setting prevents it from being created automatically - * so we throw a special 503 with the intention of informing the user that their - * Elasticsearch settings need to be updated. - * - * See {@link SavedObjectsErrorHelpers} - * - * @public - */ -export type SavedObjectsClientContract = Pick; - /** * * @internal diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts new file mode 100644 index 0000000000000..0966874aa435b --- /dev/null +++ b/src/core/server/saved_objects/types.ts @@ -0,0 +1,203 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsClient } from './service/saved_objects_client'; + +/** + * Information about the migrations that have been applied to this SavedObject. + * When Kibana starts up, KibanaMigrator detects outdated documents and + * migrates them based on this value. For each migration that has been applied, + * the plugin's name is used as a key and the latest migration version as the + * value. + * + * @example + * migrationVersion: { + * dashboard: '7.1.1', + * space: '6.6.6', + * } + * + * @public + */ +export interface SavedObjectsMigrationVersion { + [pluginName: string]: string; +} + +/** + * + * @public + */ +export type SavedObjectAttribute = + | string + | number + | boolean + | null + | undefined + | SavedObjectAttributes + | SavedObjectAttributes[]; + +/** + * The data for a Saved Object is stored in the `attributes` key as either an + * object or an array of objects. + * + * @public + */ +export interface SavedObjectAttributes { + [key: string]: SavedObjectAttribute | SavedObjectAttribute[]; +} + +/** + * + * @public + */ +export interface SavedObject { + /** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */ + id: string; + /** The type of Saved Object. Each plugin can define it's own custom Saved Object types. */ + type: string; + /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */ + version?: string; + /** Timestamp of the last time this document had been updated. */ + updated_at?: string; + error?: { + message: string; + statusCode: number; + }; + /** {@inheritdoc SavedObjectAttributes} */ + attributes: T; + /** {@inheritdoc SavedObjectReference} */ + references: SavedObjectReference[]; + /** {@inheritdoc SavedObjectsMigrationVersion} */ + migrationVersion?: SavedObjectsMigrationVersion; +} + +/** + * A reference to another saved object. + * + * @public + */ +export interface SavedObjectReference { + name: string; + type: string; + id: string; +} + +/** + * + * @public + */ +export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { + type?: string | string[]; + page?: number; + perPage?: number; + sortField?: string; + sortOrder?: string; + /** + * An array of fields to include in the results + * @example + * SavedObjects.find({type: 'dashboard', fields: ['attributes.name', 'attributes.location']}) + */ + fields?: string[]; + /** Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String `query` argument for more information */ + search?: string; + /** The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information */ + searchFields?: string[]; + hasReference?: { type: string; id: string }; + defaultSearchOperator?: 'AND' | 'OR'; +} + +/** + * + * @public + */ +export interface SavedObjectsBaseOptions { + /** Specify the namespace for this operation */ + namespace?: string; +} + +/** + * Saved Objects is Kibana's data persisentence mechanism allowing plugins to + * use Elasticsearch for storing plugin state. + * + * ## SavedObjectsClient errors + * + * Since the SavedObjectsClient has its hands in everything we + * are a little paranoid about the way we present errors back to + * to application code. Ideally, all errors will be either: + * + * 1. Caused by bad implementation (ie. undefined is not a function) and + * as such unpredictable + * 2. An error that has been classified and decorated appropriately + * by the decorators in {@link SavedObjectsErrorHelpers} + * + * Type 1 errors are inevitable, but since all expected/handle-able errors + * should be Type 2 the `isXYZError()` helpers exposed at + * `SavedObjectsErrorHelpers` should be used to understand and manage error + * responses from the `SavedObjectsClient`. + * + * Type 2 errors are decorated versions of the source error, so if + * the elasticsearch client threw an error it will be decorated based + * on its type. That means that rather than looking for `error.body.error.type` or + * doing substring checks on `error.body.error.reason`, just use the helpers to + * understand the meaning of the error: + * + * ```js + * if (SavedObjectsErrorHelpers.isNotFoundError(error)) { + * // handle 404 + * } + * + * if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { + * // 401 handling should be automatic, but in case you wanted to know + * } + * + * // always rethrow the error unless you handle it + * throw error; + * ``` + * + * ### 404s from missing index + * + * From the perspective of application code and APIs the SavedObjectsClient is + * a black box that persists objects. One of the internal details that users have + * no control over is that we use an elasticsearch index for persistance and that + * index might be missing. + * + * At the time of writing we are in the process of transitioning away from the + * operating assumption that the SavedObjects index is always available. Part of + * this transition is handling errors resulting from an index missing. These used + * to trigger a 500 error in most cases, and in others cause 404s with different + * error messages. + * + * From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The + * object the request/call was targeting could not be found. This is why #14141 + * takes special care to ensure that 404 errors are generic and don't distinguish + * between index missing or document missing. + * + * ### 503s from missing index + * + * Unlike all other methods, create requests are supposed to succeed even when + * the Kibana index does not exist because it will be automatically created by + * elasticsearch. When that is not the case it is because Elasticsearch's + * `action.auto_create_index` setting prevents it from being created automatically + * so we throw a special 503 with the intention of informing the user that their + * Elasticsearch settings need to be updated. + * + * See {@link SavedObjectsErrorHelpers} + * + * @public + */ +export type SavedObjectsClientContract = Pick; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index b345677b5e89e..dd889ea1995e0 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -549,31 +549,25 @@ export class Router { // @public (undocumented) export interface SavedObject { - // (undocumented) attributes: T; // (undocumented) error?: { message: string; statusCode: number; }; - // (undocumented) id: string; - // (undocumented) migrationVersion?: SavedObjectsMigrationVersion; - // (undocumented) references: SavedObjectReference[]; - // (undocumented) type: string; - // (undocumented) updated_at?: string; - // (undocumented) version?: string; } // @public (undocumented) +export type SavedObjectAttribute = string | number | boolean | null | undefined | SavedObjectAttributes | SavedObjectAttributes[]; + +// @public export interface SavedObjectAttributes { - // Warning: (ae-forgotten-export) The symbol "SavedObjectAttribute" needs to be exported by the entry point index.d.ts - // // (undocumented) [key: string]: SavedObjectAttribute | SavedObjectAttribute[]; } @@ -599,7 +593,6 @@ export interface SavedObjectsBulkCreateObject { // @public (undocumented) export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { id?: string; - // (undocumented) migrationVersion?: SavedObjectsMigrationVersion; overwrite?: boolean; // (undocumented) @@ -750,7 +742,6 @@ export interface SavedObjectsExportOptions { export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { // (undocumented) defaultSearchOperator?: 'AND' | 'OR'; - // (undocumented) fields?: string[]; // (undocumented) hasReference?: { @@ -761,7 +752,6 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { page?: number; // (undocumented) perPage?: number; - // (undocumented) search?: string; searchFields?: string[]; // (undocumented) @@ -772,7 +762,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { type?: string | string[]; } -// @public (undocumented) +// @public export interface SavedObjectsFindResponse { // (undocumented) page: number; @@ -876,6 +866,16 @@ export interface SavedObjectsImportUnsupportedTypeError { type: 'unsupported_type'; } +// @public (undocumented) +export interface SavedObjectsMigrationLogger { + // (undocumented) + debug: (msg: string) => void; + // (undocumented) + info: (msg: string) => void; + // (undocumented) + warning: (msg: string) => void; +} + // @public export interface SavedObjectsMigrationVersion { // (undocumented) diff --git a/src/core/server/types.ts b/src/core/server/types.ts index 9b55da17a40a8..d712c804d45d1 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -18,5 +18,5 @@ */ /** This module is intended for consumption by public to avoid import issues with server-side code */ - export { PluginOpaqueId } from './plugins/types'; +export * from './saved_objects/types'; diff --git a/src/legacy/ui/public/chrome/api/saved_object_client.js b/src/core/server/types.ts~master similarity index 76% rename from src/legacy/ui/public/chrome/api/saved_object_client.js rename to src/core/server/types.ts~master index 3c9643556d9a9..9b55da17a40a8 100644 --- a/src/legacy/ui/public/chrome/api/saved_object_client.js +++ b/src/core/server/types.ts~master @@ -17,12 +17,6 @@ * under the License. */ -import { SavedObjectsClient } from '../../saved_objects'; +/** This module is intended for consumption by public to avoid import issues with server-side code */ -export function initSavedObjectClient(chrome) { - const savedObjectClient = new SavedObjectsClient(); - - chrome.getSavedObjectsClient = function () { - return savedObjectClient; - }; -} +export { PluginOpaqueId } from './plugins/types'; diff --git a/src/dev/run_check_core_api_changes.ts b/src/dev/run_check_core_api_changes.ts index f17f6c2e5efb2..4d0be7f388466 100644 --- a/src/dev/run_check_core_api_changes.ts +++ b/src/dev/run_check_core_api_changes.ts @@ -39,7 +39,7 @@ const apiExtractorConfig = (folder: string): ExtractorConfig => { tsconfigFilePath: '/tsconfig.json', }, projectFolder: path.resolve('./'), - mainEntryPointFilePath: `target/types/${folder}/index.d.ts`, + mainEntryPointFilePath: `target/types/core/${folder}/index.d.ts`, apiReport: { enabled: true, reportFileName: `${folder}.api.md`, diff --git a/src/fixtures/stubbed_saved_object_index_pattern.js b/src/fixtures/stubbed_saved_object_index_pattern.js index a983391328134..e737b2563823e 100644 --- a/src/fixtures/stubbed_saved_object_index_pattern.js +++ b/src/fixtures/stubbed_saved_object_index_pattern.js @@ -18,7 +18,7 @@ */ import stubbedLogstashFields from './logstash_fields'; -import { SimpleSavedObject } from '../legacy/ui/public/saved_objects/simple_saved_object'; +import { SimpleSavedObject } from '../core/public'; const mockLogstashFields = stubbedLogstashFields(); diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container_factory.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container_factory.ts index 648204bf987e8..b7f5b948697d2 100644 --- a/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container_factory.ts +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_container_factory.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { SavedObjectMetaData } from 'ui/saved_objects/components/saved_object_finder'; -import { SavedObjectAttributes } from 'target/types/server'; +import { SavedObjectAttributes } from 'src/core/server'; import { ContainerOutput, embeddableFactories, diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx index 42fca6c721b0c..557ed941bbfab 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -38,8 +38,7 @@ import { EuiText, } from '@elastic/eui'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectAttributes } from 'src/core/server/saved_objects'; +import { SavedObjectAttributes } from 'src/core/server'; import { EmbeddableFactoryNotFoundError } from '../../../../embeddables/embeddable_factory_not_found_error'; import { IContainer } from '../../../../containers'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts index cbb695a564190..5300811a6dab9 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Logger } from 'target/types/server/saved_objects/migrations/core/migration_logger'; +import { SavedObjectsMigrationLogger } from 'src/core/server'; import { inspect } from 'util'; import { DashboardDoc730ToLatest, DashboardDoc700To720 } from './types'; import { isDashboardDoc } from './is_dashboard_doc'; @@ -29,7 +29,7 @@ export function migrations730( [key: string]: unknown; } | DashboardDoc700To720, - logger: Logger + logger: SavedObjectsMigrationLogger ): DashboardDoc730ToLatest | { [key: string]: unknown } { if (!isDashboardDoc(doc)) { // NOTE: we should probably throw an error here... but for now following suit and in the diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js index f76a0ab72b212..e316dd3254cc0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js @@ -27,7 +27,7 @@ import 'ui/private'; import '../../components/field_chooser/field_chooser'; import FixturesHitsProvider from 'fixtures/hits'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -import { SimpleSavedObject } from 'ui/saved_objects'; +import { SimpleSavedObject } from '../../../../../../../core/public'; // Load the kibana app dependencies. diff --git a/src/legacy/ui/public/chrome/api/saved_object_client.ts b/src/legacy/ui/public/chrome/api/saved_object_client.ts new file mode 100644 index 0000000000000..b42e74e5a5865 --- /dev/null +++ b/src/legacy/ui/public/chrome/api/saved_object_client.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { npStart } from 'ui/new_platform'; +import { Chrome } from '..'; + +const savedObjectsClient = npStart.core.savedObjects.client; + +export function initSavedObjectClient(chrome: Chrome) { + chrome.getSavedObjectsClient = function() { + return savedObjectsClient; + }; +} diff --git a/src/legacy/ui/public/chrome/index.d.ts b/src/legacy/ui/public/chrome/index.d.ts index e464cc5168182..ec2975e665379 100644 --- a/src/legacy/ui/public/chrome/index.d.ts +++ b/src/legacy/ui/public/chrome/index.d.ts @@ -17,8 +17,8 @@ * under the License. */ +import { SavedObjectsClientContract } from 'src/core/public'; import { ChromeBrand } from '../../../../core/public'; -import { SavedObjectsClient } from '../saved_objects'; import { BadgeApi } from './api/badge'; import { BreadcrumbsApi } from './api/breadcrumbs'; import { HelpExtensionApi } from './api/help_extension'; @@ -37,7 +37,7 @@ declare interface Chrome extends ChromeNavLinks { getBasePath(): string; getXsrfToken(): string; getKibanaVersion(): string; - getSavedObjectsClient(): SavedObjectsClient; + getSavedObjectsClient(): SavedObjectsClientContract; getUiSettingsClient(): any; setVisible(visible: boolean): any; getInjected(key: string, defaultValue?: any): any; diff --git a/src/legacy/ui/public/index_patterns/_index_pattern.ts b/src/legacy/ui/public/index_patterns/_index_pattern.ts index 09623f75442ed..a15a5be57d49e 100644 --- a/src/legacy/ui/public/index_patterns/_index_pattern.ts +++ b/src/legacy/ui/public/index_patterns/_index_pattern.ts @@ -26,9 +26,10 @@ import { fieldFormats } from 'ui/registry/field_formats'; // @ts-ignore import { expandShorthand } from 'ui/utils/mapping_setup'; import { toastNotifications } from 'ui/notify'; -import { findObjectByTitle, SavedObjectsClient } from 'ui/saved_objects'; - +import { findObjectByTitle } from 'ui/saved_objects'; import { IndexPatternsApiClient } from 'ui/index_patterns/index_patterns_api_client'; +import { SavedObjectsClientContract } from 'src/core/public'; + import { IndexPatternMissingIndices } from './errors'; import { getRoutes } from './get_routes'; import { FieldList } from './_field_list'; @@ -75,7 +76,7 @@ export class IndexPattern implements StaticIndexPattern { public metaFields: string[]; private version: string | undefined; - private savedObjectsClient: SavedObjectsClient; + private savedObjectsClient: SavedObjectsClientContract; private patternCache: any; private getConfig: any; private sourceFilters?: []; @@ -108,7 +109,7 @@ export class IndexPattern implements StaticIndexPattern { constructor( id: string | undefined, getConfig: any, - savedObjectsClient: SavedObjectsClient, + savedObjectsClient: SavedObjectsClientContract, apiClient: IndexPatternsApiClient, patternCache: any ) { diff --git a/src/legacy/ui/public/index_patterns/index_patterns.ts b/src/legacy/ui/public/index_patterns/index_patterns.ts index 6f22c1c2a73ea..f1cbc45fd9ff1 100644 --- a/src/legacy/ui/public/index_patterns/index_patterns.ts +++ b/src/legacy/ui/public/index_patterns/index_patterns.ts @@ -17,14 +17,13 @@ * under the License. */ +import { SavedObjectsClientContract, SimpleSavedObject, UiSettingsClient } from 'src/core/public'; // @ts-ignore import { fieldFormats } from '../registry/field_formats'; import { IndexPattern } from './_index_pattern'; import { createIndexPatternCache } from './_pattern_cache'; import { IndexPatternsApiClient } from './index_patterns_api_client'; -import { SavedObjectsClient, SimpleSavedObject } from '../saved_objects'; -import { UiSettingsClient } from '../../../../core/public'; const indexPatternCache = createIndexPatternCache(); const apiClient = new IndexPatternsApiClient(); @@ -33,10 +32,10 @@ export class IndexPatterns { fieldFormats: fieldFormats; private config: UiSettingsClient; - private savedObjectsClient: SavedObjectsClient; + private savedObjectsClient: SavedObjectsClientContract; private savedObjectsCache?: Array> | null; - constructor(config: UiSettingsClient, savedObjectsClient: SavedObjectsClient) { + constructor(config: UiSettingsClient, savedObjectsClient: SavedObjectsClientContract) { this.config = config; this.savedObjectsClient = savedObjectsClient; } diff --git a/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js b/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js index 6539f8c034136..766ed44a4c0fe 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js +++ b/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js @@ -20,7 +20,7 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; import { findObjectByTitle } from '../find_object_by_title'; -import { SimpleSavedObject } from '../simple_saved_object'; +import { SimpleSavedObject } from '../../../../../core/public'; describe('findObjectByTitle', () => { const sandbox = sinon.createSandbox(); diff --git a/src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js b/src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js index a6583b97972ab..f2fc9bfe232e2 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js +++ b/src/legacy/ui/public/saved_objects/__tests__/simple_saved_object.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; -import { SimpleSavedObject } from '../simple_saved_object'; +import { SimpleSavedObject } from '../../../../../core/public'; describe('SimpleSavedObject', () => { it('persists type and id', () => { diff --git a/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx b/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx index a5930ee38a662..9169286fb4171 100644 --- a/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx +++ b/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx @@ -46,7 +46,7 @@ import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; import { i18n } from '@kbn/i18n'; import { SavedObjectAttributes } from 'src/core/server'; -import { SimpleSavedObject } from '../simple_saved_object'; +import { SimpleSavedObject } from 'src/core/public'; // TODO the typings for EuiListGroup are incorrect - maxWidth is missing. This can be removed when the types are adjusted const FixedEuiListGroup = (EuiListGroup as any) as React.FunctionComponent< diff --git a/src/legacy/ui/public/saved_objects/find_object_by_title.ts b/src/legacy/ui/public/saved_objects/find_object_by_title.ts index e27326249f5c9..d6f11bcb80956 100644 --- a/src/legacy/ui/public/saved_objects/find_object_by_title.ts +++ b/src/legacy/ui/public/saved_objects/find_object_by_title.ts @@ -19,19 +19,19 @@ import { find } from 'lodash'; import { SavedObjectAttributes } from 'src/core/server'; -import { SavedObjectsClient } from './saved_objects_client'; -import { SimpleSavedObject } from './simple_saved_object'; +import { SavedObjectsClientContract } from 'src/core/public'; +import { SimpleSavedObject } from 'src/core/public'; /** * Returns an object matching a given title * - * @param savedObjectsClient {SavedObjectsClient} + * @param savedObjectsClient {SavedObjectsClientContract} * @param type {string} * @param title {string} * @returns {Promise} */ export function findObjectByTitle( - savedObjectsClient: SavedObjectsClient, + savedObjectsClient: SavedObjectsClientContract, type: string, title: string ): Promise | void> { diff --git a/src/legacy/ui/public/saved_objects/index.ts b/src/legacy/ui/public/saved_objects/index.ts index 31222bb9f5ebc..8076213f62e9a 100644 --- a/src/legacy/ui/public/saved_objects/index.ts +++ b/src/legacy/ui/public/saved_objects/index.ts @@ -17,10 +17,8 @@ * under the License. */ -export { SavedObjectsClient } from './saved_objects_client'; export { SavedObjectRegistryProvider } from './saved_object_registry'; export { SavedObjectsClientProvider } from './saved_objects_client_provider'; // @ts-ignore export { SavedObjectLoader } from './saved_object_loader'; -export { SimpleSavedObject } from './simple_saved_object'; export { findObjectByTitle } from './find_object_by_title'; diff --git a/src/legacy/ui/public/saved_objects/saved_objects_client.test.ts b/src/legacy/ui/public/saved_objects/saved_objects_client.test.ts deleted file mode 100644 index fed3b8807cddd..0000000000000 --- a/src/legacy/ui/public/saved_objects/saved_objects_client.test.ts +++ /dev/null @@ -1,357 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -jest.mock('ui/kfetch', () => ({})); - -import * as sinon from 'sinon'; -import { SavedObjectsFindOptions } from 'src/core/server'; -import { SavedObjectsClient } from './saved_objects_client'; -import { SimpleSavedObject } from './simple_saved_object'; - -describe('SavedObjectsClient', () => { - const doc = { - id: 'AVwSwFxtcMV38qjDZoQg', - type: 'config', - attributes: { title: 'Example title' }, - version: 'foo', - }; - - let kfetchStub: sinon.SinonStub; - let savedObjectsClient: SavedObjectsClient; - beforeEach(() => { - kfetchStub = sinon.stub(); - require('ui/kfetch').kfetch = async (...args: any[]) => { - return kfetchStub(...args); - }; - savedObjectsClient = new SavedObjectsClient(); - }); - - describe('#get', () => { - beforeEach(() => { - kfetchStub - .withArgs({ - method: 'POST', - pathname: `/api/saved_objects/_bulk_get`, - query: undefined, - body: sinon.match.any, - }) - .returns(Promise.resolve({ saved_objects: [doc] })); - }); - - test('returns a promise', () => { - expect(savedObjectsClient.get('index-pattern', 'logstash-*')).toBeInstanceOf(Promise); - }); - - test('requires type', async () => { - try { - await savedObjectsClient.get(undefined as any, undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe('requires type and id'); - } - }); - - test('requires id', async () => { - try { - await savedObjectsClient.get('index-pattern', undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe('requires type and id'); - } - }); - - test('resolves with instantiated SavedObject', async () => { - const response = await savedObjectsClient.get(doc.type, doc.id); - expect(response).toBeInstanceOf(SimpleSavedObject); - expect(response.type).toBe('config'); - expect(response.get('title')).toBe('Example title'); - }); - - test('makes HTTP call', async () => { - await savedObjectsClient.get(doc.type, doc.id); - sinon.assert.calledOnce(kfetchStub); - }); - - test('handles HTTP call when it fails', async () => { - kfetchStub - .withArgs({ - method: 'POST', - pathname: `/api/saved_objects/_bulk_get`, - query: undefined, - body: sinon.match.any, - }) - .rejects(new Error('Request failed')); - try { - await savedObjectsClient.get(doc.type, doc.id); - throw new Error('should have error'); - } catch (e) { - expect(e.message).toBe('Request failed'); - } - }); - }); - - describe('#delete', () => { - beforeEach(() => { - kfetchStub - .withArgs({ - method: 'DELETE', - pathname: `/api/saved_objects/index-pattern/logstash-*`, - query: undefined, - body: undefined, - }) - .returns(Promise.resolve({})); - }); - - test('returns a promise', () => { - expect(savedObjectsClient.delete('index-pattern', 'logstash-*')).toBeInstanceOf(Promise); - }); - - test('requires type', async () => { - try { - await savedObjectsClient.delete(undefined as any, undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe('requires type and id'); - } - }); - - test('requires id', async () => { - try { - await savedObjectsClient.delete('index-pattern', undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe('requires type and id'); - } - }); - - test('makes HTTP call', () => { - savedObjectsClient.delete('index-pattern', 'logstash-*'); - sinon.assert.calledOnce(kfetchStub); - }); - }); - - describe('#update', () => { - const requireMessage = 'requires type, id and attributes'; - - beforeEach(() => { - kfetchStub - .withArgs({ - method: 'PUT', - pathname: `/api/saved_objects/index-pattern/logstash-*`, - query: undefined, - body: sinon.match.any, - }) - .returns(Promise.resolve({ data: 'api-response' })); - }); - - test('returns a promise', () => { - expect(savedObjectsClient.update('index-pattern', 'logstash-*', {})).toBeInstanceOf(Promise); - }); - - test('requires type', async () => { - try { - await savedObjectsClient.update(undefined as any, undefined as any, undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe(requireMessage); - } - }); - - test('requires id', async () => { - try { - await savedObjectsClient.update('index-pattern', undefined as any, undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe(requireMessage); - } - }); - - test('requires attributes', async () => { - try { - await savedObjectsClient.update('index-pattern', 'logstash-*', undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe(requireMessage); - } - }); - - test('makes HTTP call', () => { - const attributes = { foo: 'Foo', bar: 'Bar' }; - const body = { attributes, version: 'foo' }; - const options = { version: 'foo' }; - - savedObjectsClient.update('index-pattern', 'logstash-*', attributes, options); - sinon.assert.calledOnce(kfetchStub); - sinon.assert.calledWithExactly( - kfetchStub, - sinon.match({ - body: JSON.stringify(body), - }) - ); - }); - }); - - describe('#create', () => { - const requireMessage = 'requires type and attributes'; - - beforeEach(() => { - kfetchStub - .withArgs({ - method: 'POST', - pathname: `/api/saved_objects/index-pattern`, - query: undefined, - body: sinon.match.any, - }) - .returns(Promise.resolve({})); - }); - - test('returns a promise', () => { - expect(savedObjectsClient.create('index-pattern', {})).toBeInstanceOf(Promise); - }); - - test('requires type', async () => { - try { - await savedObjectsClient.create(undefined as any, undefined as any); - fail('should have error'); - } catch (e) { - expect(e.message).toBe(requireMessage); - } - }); - - test('allows for id to be provided', () => { - const attributes = { foo: 'Foo', bar: 'Bar' }; - const path = `/api/saved_objects/index-pattern/myId`; - kfetchStub - .withArgs({ - method: 'POST', - pathname: path, - query: undefined, - body: sinon.match.any, - }) - .returns(Promise.resolve({})); - - savedObjectsClient.create('index-pattern', attributes, { id: 'myId' }); - - sinon.assert.calledOnce(kfetchStub); - sinon.assert.calledWithExactly( - kfetchStub, - sinon.match({ - pathname: path, - }) - ); - }); - - test('makes HTTP call', () => { - const attributes = { foo: 'Foo', bar: 'Bar' }; - savedObjectsClient.create('index-pattern', attributes); - - sinon.assert.calledOnce(kfetchStub); - sinon.assert.calledWithExactly( - kfetchStub, - sinon.match({ - pathname: sinon.match.string, - body: JSON.stringify({ attributes }), - }) - ); - }); - }); - - describe('#bulk_create', () => { - beforeEach(() => { - kfetchStub - .withArgs({ - method: 'POST', - pathname: `/api/saved_objects/_bulk_create`, - query: sinon.match.any, - body: sinon.match.any, - }) - .returns(Promise.resolve({ saved_objects: [doc] })); - }); - - test('returns a promise', () => { - expect(savedObjectsClient.bulkCreate([doc], {})).toBeInstanceOf(Promise); - }); - - test('resolves with instantiated SavedObjects', async () => { - const response = await savedObjectsClient.bulkCreate([doc], {}); - expect(response).toHaveProperty('savedObjects'); - expect(response.savedObjects.length).toBe(1); - expect(response.savedObjects[0]).toBeInstanceOf(SimpleSavedObject); - }); - - test('makes HTTP call', async () => { - await savedObjectsClient.bulkCreate([doc], {}); - sinon.assert.calledOnce(kfetchStub); - }); - }); - - describe('#find', () => { - const object = { id: 'logstash-*', type: 'index-pattern', title: 'Test' }; - - beforeEach(() => { - kfetchStub.returns(Promise.resolve({ saved_objects: [object] })); - }); - - test('returns a promise', () => { - expect(savedObjectsClient.find()).toBeInstanceOf(Promise); - }); - - test('accepts type', () => { - const body = { type: 'index-pattern', invalid: true }; - - savedObjectsClient.find(body); - sinon.assert.calledOnce(kfetchStub); - sinon.assert.calledWithExactly( - kfetchStub, - sinon.match({ - pathname: `/api/saved_objects/_find`, - query: { type: 'index-pattern', invalid: true }, - }) - ); - }); - - test('accepts fields', () => { - const body = { fields: ['title', 'description'] }; - - savedObjectsClient.find(body); - sinon.assert.calledOnce(kfetchStub); - sinon.assert.calledWithExactly( - kfetchStub, - sinon.match({ - pathname: `/api/saved_objects/_find`, - query: { fields: ['title', 'description'] }, - }) - ); - }); - - test('accepts pagination params', () => { - const options: SavedObjectsFindOptions = { perPage: 10, page: 6 }; - - savedObjectsClient.find(options); - sinon.assert.calledOnce(kfetchStub); - sinon.assert.alwaysCalledWith( - kfetchStub, - sinon.match({ - pathname: `/api/saved_objects/_find`, - query: { per_page: 10, page: 6 }, - }) - ); - }); - }); -}); diff --git a/src/legacy/ui/public/saved_objects/saved_objects_client_provider.ts b/src/legacy/ui/public/saved_objects/saved_objects_client_provider.ts index 1ef0d07135fd5..0375eb21c19f0 100644 --- a/src/legacy/ui/public/saved_objects/saved_objects_client_provider.ts +++ b/src/legacy/ui/public/saved_objects/saved_objects_client_provider.ts @@ -17,9 +17,9 @@ * under the License. */ +import { SavedObjectsClient } from 'src/core/public'; import chrome from '../chrome'; import { PromiseService } from '../promises'; -import { SavedObjectsClient } from './saved_objects_client'; type Args any> = T extends (...args: infer X) => any ? X : never; diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index f5f1a5ceac695..e2cb873f54fd0 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -35,6 +35,7 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { testMatch: ['**/*.test.{js,ts,tsx}'], transform: { '^.+\\.(js|tsx?)$': `${kibanaDirectory}/src/dev/jest/babel_transform.js`, + '^.+\\.html?$': 'jest-raw-loader', }, transformIgnorePatterns: [ // ignore all node_modules except @elastic/eui which requires babel transforms to handle dynamic import() From b94c16da75bf7fc414e7f982da33da9ee83aadee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Tue, 6 Aug 2019 14:53:29 -0400 Subject: [PATCH 14/26] Fix task manager flaky middleware test (#42418) * Fix flaky test * Cleanup beforeEach code --- .../task_manager/task_manager_integration.js | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js index bf8bd48bb3fea..5c0b59674bded 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js @@ -11,23 +11,27 @@ import supertestAsPromised from 'supertest-as-promised'; export default function ({ getService }) { const es = getService('es'); + const log = getService('log'); const retry = getService('retry'); const config = getService('config'); const testHistoryIndex = '.task_manager_test_result'; const supertest = supertestAsPromised(url.format(config.get('servers.kibana'))); - // FLAKY: https://github.com/elastic/kibana/issues/42098 - describe.skip('scheduling and running tasks', () => { + describe('scheduling and running tasks', () => { beforeEach(() => supertest.delete('/api/sample_tasks') .set('kbn-xsrf', 'xxx') .expect(200)); - beforeEach(async () => - (await es.indices.exists({ index: testHistoryIndex })) && es.deleteByQuery({ - index: testHistoryIndex, - q: 'type:task', - refresh: true, - })); + beforeEach(async () => { + const exists = await es.indices.exists({ index: testHistoryIndex }); + if (exists) { + await es.deleteByQuery({ + index: testHistoryIndex, + q: 'type:task', + refresh: true, + }); + } + }); function currentTasks() { return supertest.get('/api/sample_tasks') @@ -53,18 +57,22 @@ export default function ({ getService }) { it('should support middleware', async () => { const historyItem = _.random(1, 100); - await scheduleTask({ + const scheduledTask = await scheduleTask({ taskType: 'sampleTask', interval: '30m', params: { historyItem }, }); + log.debug(`Task created: ${scheduledTask.id}`); await retry.try(async () => { expect((await historyDocs()).length).to.eql(1); const [task] = (await currentTasks()).docs; + log.debug(`Task found: ${task.id}`); + log.debug(`Task status: ${task.status}`); + log.debug(`Task state: ${JSON.stringify(task.state, null, 2)}`); + log.debug(`Task params: ${JSON.stringify(task.params, null, 2)}`); - expect(task.attempts).to.eql(0); expect(task.state.count).to.eql(1); expect(task.params).to.eql({ From 6cd3a67d984dd85059a0814d78297c546b27ba5e Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Tue, 6 Aug 2019 13:02:40 -0600 Subject: [PATCH 15/26] Remove deprecated index patterns field methods. (#42643) --- src/legacy/ui/public/index_patterns/_field.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/legacy/ui/public/index_patterns/_field.ts b/src/legacy/ui/public/index_patterns/_field.ts index d4017a1e43e26..34f1a55f7af1d 100644 --- a/src/legacy/ui/public/index_patterns/_field.ts +++ b/src/legacy/ui/public/index_patterns/_field.ts @@ -170,25 +170,6 @@ export class Field implements FieldType { return obj.create(); } - - /** @deprecated */ - public get indexed() { - throw new Error( - 'field.indexed has been removed, see https://github.com/elastic/kibana/pull/11969' - ); - } - /** @deprecated */ - public get analyzed() { - throw new Error( - 'field.analyzed has been removed, see https://github.com/elastic/kibana/pull/11969' - ); - } - /** @deprecated */ - public get doc_values() { // eslint-disable-line - throw new Error( - 'field.doc_values has been removed, see https://github.com/elastic/kibana/pull/11969' - ); - } } Field.prototype.routes = { From 0d7eab77f8065a180c445f5be2bc684e841f9e47 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 6 Aug 2019 12:19:29 -0700 Subject: [PATCH 16/26] [Reporting/Refactor] Break up screenshots.ts, consolidate logging (#42304) * [Reporting] Sanitize 409 error log message * [Reporting] Break up screenshots libs * Revert "[Reporting] Sanitize 409 error log message" This reverts commit 61998b5847f5dcbdce98c9db01a9bf1152f254f1. * [Reporting] Sanitize 409 error log message * this is for later --- .../export_types/common/constants.ts | 2 + .../export_types/common/lib/screenshots.ts | 408 ------------------ .../common/lib/screenshots/check_for_toast.ts | 29 ++ .../screenshots/get_element_position_data.ts | 51 +++ .../lib/screenshots/get_number_of_items.ts | 36 ++ .../common/lib/screenshots/get_screenshots.ts | 51 +++ .../common/lib/screenshots/get_time_range.ts | 46 ++ .../common/lib/screenshots/index.ts | 152 +++++++ .../common/lib/screenshots/inject_css.ts | 33 ++ .../common/lib/screenshots/open_url.ts | 23 + .../common/lib/screenshots/types.ts | 48 +++ .../lib/screenshots/wait_for_dom_elements.ts | 22 + .../common/lib/screenshots/wait_for_render.ts | 65 +++ .../png/server/lib/generate_png.js | 2 +- .../printable_pdf/server/lib/generate_pdf.js | 2 +- 15 files changed, 560 insertions(+), 410 deletions(-) delete mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots.ts create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/check_for_toast.ts create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_dom_elements.ts create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/common/constants.ts b/x-pack/legacy/plugins/reporting/export_types/common/constants.ts index ddc678592760a..02a3e787da750 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/constants.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/constants.ts @@ -8,3 +8,5 @@ export const LayoutTypes = { PRESERVE_LAYOUT: 'preserve_layout', PRINT: 'print', }; + +export const WAITFOR_SELECTOR = '.application'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots.ts deleted file mode 100644 index 6604638690a41..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots.ts +++ /dev/null @@ -1,408 +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 * as Rx from 'rxjs'; -import { first, tap, mergeMap } from 'rxjs/operators'; -import fs from 'fs'; -import { promisify } from 'util'; -import { i18n } from '@kbn/i18n'; -import { PLUGIN_ID } from '../../../common/constants'; -import { LevelLogger } from '../../../server/lib/level_logger'; -import { KbnServer, ElementPosition } from '../../../types'; -import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../server/browsers/chromium/driver'; -import { Layout, LayoutInstance } from '../layouts/layout'; -import { HeadlessChromiumDriverFactory } from '../../../server/browsers/chromium/driver_factory'; - -interface TimeRange { - from: any; - to: any; -} - -interface AttributesMap { - [key: string]: any; -} - -interface ElementsPositionAndAttribute { - position: ElementPosition; - attributes: AttributesMap; -} - -interface ScreenShotOpts { - browser: HeadlessBrowser; - elementsPositionAndAttributes: ElementsPositionAndAttribute[]; -} - -interface Screenshot { - base64EncodedData: string; - title: string; - description: string; -} - -interface TimeRangeOpts { - timeRange: TimeRange; -} - -const fsp = { readFile: promisify(fs.readFile) }; - -export function screenshotsObservableFactory(server: KbnServer) { - const config = server.config(); - const logger = LevelLogger.createForServer(server, [PLUGIN_ID, 'screenshots']); - - const browserDriverFactory: HeadlessChromiumDriverFactory = - server.plugins.reporting.browserDriverFactory; - const captureConfig = config.get('xpack.reporting.capture'); - - const asyncDurationLogger = async (description: string, promise: Promise) => { - const start: number = Date.now(); - const result = await promise; - logger.debug(`${description} took ${Date.now() - start}ms`); - return result; - }; - - const openUrl = async (browser: HeadlessBrowser, url: string, conditionalHeaders: any) => { - const waitForSelector = '.application'; - - await browser.open(url, { - conditionalHeaders, - waitForSelector, - }); - }; - - const injectCustomCss = async (browser: HeadlessBrowser, layout: Layout) => { - const filePath = layout.getCssOverridesPath(); - const buffer = await fsp.readFile(filePath); - await browser.evaluate({ - fn: css => { - const node = document.createElement('style'); - node.type = 'text/css'; - node.innerHTML = css; // eslint-disable-line no-unsanitized/property - document.getElementsByTagName('head')[0].appendChild(node); - }, - args: [buffer.toString()], - }); - }; - - const waitForElementOrItemsCountAttribute = async ( - browser: HeadlessBrowser, - layout: LayoutInstance - ) => { - // the dashboard is using the `itemsCountAttribute` attribute to let us - // know how many items to expect since gridster incrementally adds panels - // we have to use this hint to wait for all of them - await browser.waitForSelector( - `${layout.selectors.renderComplete},[${layout.selectors.itemsCountAttribute}]` - ); - }; - - const checkForToastMessage = async (browser: HeadlessBrowser, layout: LayoutInstance) => { - await browser.waitForSelector(layout.selectors.toastHeader, { silent: true }); - const toastHeaderText = await browser.evaluate({ - fn: selector => { - const nodeList = document.querySelectorAll(selector); - return nodeList.item(0).innerText; - }, - args: [layout.selectors.toastHeader], - }); - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.printablePdf.screenshots.unexpectedErrorMessage', - { - defaultMessage: 'Encountered an unexpected message on the page: {toastHeaderText}', - values: { toastHeaderText }, - } - ) - ); - }; - - const getNumberOfItems = async ( - browser: HeadlessBrowser, - layout: LayoutInstance - ): Promise => { - // returns the value of the `itemsCountAttribute` if it's there, otherwise - // we just count the number of `itemSelector` - const itemsCount = await browser.evaluate({ - fn: (selector, countAttribute) => { - const elementWithCount = document.querySelector(`[${countAttribute}]`); - if (elementWithCount) { - const count = elementWithCount.getAttribute(countAttribute); - // the conditional allows count === "0", not null or undefined - if (count != null && count !== '') { - return parseInt(count, 10); - } - } - - return document.querySelectorAll(selector).length; - }, - args: [layout.selectors.renderComplete, layout.selectors.itemsCountAttribute], - }); - return itemsCount; - }; - - const waitForElementsToBeInDOM = async ( - browser: HeadlessBrowser, - itemsCount: number, - layout: LayoutInstance - ) => { - await browser.waitFor({ - fn: selector => { - return document.querySelectorAll(selector).length; - }, - args: [layout.selectors.renderComplete], - toEqual: itemsCount, - }); - }; - - const setViewport = async ( - browser: HeadlessBrowser, - itemsCount: number, - layout: LayoutInstance - ) => { - const viewport = layout.getViewport(itemsCount); - await browser.setViewport(viewport); - }; - - const positionElements = async (browser: HeadlessBrowser, layout: LayoutInstance) => { - if (layout.positionElements) { - // print layout - await layout.positionElements(browser); - } - }; - - const waitForRenderComplete = async (browser: HeadlessBrowser, layout: LayoutInstance) => { - await browser.evaluate({ - fn: (selector, visLoadDelay) => { - // wait for visualizations to finish loading - const visualizations: NodeListOf = document.querySelectorAll(selector); - const visCount = visualizations.length; - const renderedTasks = []; - - function waitForRender(visualization: Element) { - return new Promise(resolve => { - visualization.addEventListener('renderComplete', () => resolve()); - }); - } - - function waitForRenderDelay() { - return new Promise(resolve => { - setTimeout(resolve, visLoadDelay); - }); - } - - for (let i = 0; i < visCount; i++) { - const visualization = visualizations[i]; - const isRendered = visualization.getAttribute('data-render-complete'); - - if (isRendered === 'disabled') { - renderedTasks.push(waitForRenderDelay()); - } else if (isRendered === 'false') { - renderedTasks.push(waitForRender(visualization)); - } - } - - // The renderComplete fires before the visualizations are in the DOM, so - // we wait for the event loop to flush before telling reporting to continue. This - // seems to correct a timing issue that was causing reporting to occasionally - // capture the first visualization before it was actually in the DOM. - // Note: 100 proved too short, see https://github.com/elastic/kibana/issues/22581, - // bumping to 250. - const hackyWaitForVisualizations = () => new Promise(r => setTimeout(r, 250)); - - return Promise.all(renderedTasks).then(hackyWaitForVisualizations); - }, - args: [layout.selectors.renderComplete, captureConfig.loadDelay], - }); - }; - - const getTimeRange = async ( - browser: HeadlessBrowser, - layout: LayoutInstance - ): Promise => { - const timeRange: TimeRange | null = await browser.evaluate({ - fn: (fromAttribute, toAttribute) => { - const fromElement = document.querySelector(`[${fromAttribute}]`); - const toElement = document.querySelector(`[${toAttribute}]`); - - if (!fromElement || !toElement) { - return null; - } - - const from = fromElement.getAttribute(fromAttribute); - const to = toElement.getAttribute(toAttribute); - if (!to || !from) { - return null; - } - - return { from, to }; - }, - args: [layout.selectors.timefilterFromAttribute, layout.selectors.timefilterToAttribute], - }); - return timeRange; - }; - - const getElementPositionAndAttributes = async ( - browser: HeadlessBrowser, - layout: LayoutInstance - ): Promise => { - const elementsPositionAndAttributes = await browser.evaluate({ - fn: (selector, attributes) => { - const elements: NodeListOf = document.querySelectorAll(selector); - - // NodeList isn't an array, just an iterator, unable to use .map/.forEach - const results: ElementsPositionAndAttribute[] = []; - for (let i = 0; i < elements.length; i++) { - const element = elements[i]; - const boundingClientRect = element.getBoundingClientRect() as DOMRect; - results.push({ - position: { - boundingClientRect: { - // modern browsers support x/y, but older ones don't - top: boundingClientRect.y || boundingClientRect.top, - left: boundingClientRect.x || boundingClientRect.left, - width: boundingClientRect.width, - height: boundingClientRect.height, - }, - scroll: { - x: window.scrollX, - y: window.scrollY, - }, - }, - attributes: Object.keys(attributes).reduce((result: AttributesMap, key) => { - const attribute = attributes[key]; - result[key] = element.getAttribute(attribute); - return result; - }, {}), - }); - } - return results; - }, - args: [layout.selectors.screenshot, { title: 'data-title', description: 'data-description' }], - }); - return elementsPositionAndAttributes; - }; - - const getScreenshots = async ({ - browser, - elementsPositionAndAttributes, - }: ScreenShotOpts): Promise => { - const screenshots: Screenshot[] = []; - for (const item of elementsPositionAndAttributes) { - const base64EncodedData = await asyncDurationLogger( - 'screenshot', - browser.screenshot(item.position) - ); - - screenshots.push({ - base64EncodedData, - title: item.attributes.title, - description: item.attributes.description, - }); - } - return screenshots; - }; - - return function screenshotsObservable( - url: string, - conditionalHeaders: any, - layout: LayoutInstance, - browserTimezone: string - ): Rx.Observable { - logger.debug(`Creating browser driver factory`); - - const create$ = browserDriverFactory.create({ - viewport: layout.getBrowserViewport(), - browserTimezone, - }); - - return create$.pipe( - mergeMap(({ driver$, exit$, message$, consoleMessage$ }) => { - logger.debug('Driver factory created'); - message$.subscribe((line: string) => { - logger.debug(line, ['browser']); - }); - - consoleMessage$.subscribe((line: string) => { - logger.debug(line, ['browserConsole']); - }); - - const screenshot$ = driver$.pipe( - tap(() => logger.debug(`opening ${url}`)), - mergeMap( - (browser: HeadlessBrowser) => openUrl(browser, url, conditionalHeaders), - browser => browser - ), - tap(() => - logger.debug('waiting for elements or items count attribute; or not found to interrupt') - ), - mergeMap( - (browser: HeadlessBrowser) => - Rx.race( - Rx.from(waitForElementOrItemsCountAttribute(browser, layout)), - Rx.from(checkForToastMessage(browser, layout)) - ), - browser => browser - ), - tap(() => logger.debug('determining how many items we have')), - mergeMap( - (browser: HeadlessBrowser) => getNumberOfItems(browser, layout), - (browser, itemsCount) => ({ browser, itemsCount }) - ), - tap(() => logger.debug('setting viewport')), - mergeMap( - ({ browser, itemsCount }) => setViewport(browser, itemsCount, layout), - ({ browser, itemsCount }) => ({ browser, itemsCount }) - ), - tap(({ itemsCount }) => logger.debug(`waiting for ${itemsCount} to be in the DOM`)), - mergeMap( - ({ browser, itemsCount }) => waitForElementsToBeInDOM(browser, itemsCount, layout), - ({ browser, itemsCount }) => ({ browser, itemsCount }) - ), - // Waiting till _after_ elements have rendered before injecting our CSS - // allows for them to be displayed properly in many cases - tap(() => logger.debug('injecting custom css')), - mergeMap( - ({ browser }) => injectCustomCss(browser, layout), - ({ browser }) => ({ browser }) - ), - tap(() => logger.debug('positioning elements')), - mergeMap(({ browser }) => positionElements(browser, layout), ({ browser }) => browser), - tap(() => logger.debug('waiting for rendering to complete')), - mergeMap( - (browser: HeadlessBrowser) => waitForRenderComplete(browser, layout), - browser => browser - ), - tap(() => logger.debug('rendering is complete')), - mergeMap( - (browser: HeadlessBrowser) => getTimeRange(browser, layout), - (browser, timeRange) => ({ browser, timeRange }) - ), - tap(({ timeRange }) => - logger.debug( - timeRange ? `timeRange from ${timeRange.from} to ${timeRange.to}` : 'no timeRange' - ) - ), - mergeMap( - ({ browser }) => getElementPositionAndAttributes(browser, layout), - ( - { browser, timeRange }: { browser: HeadlessBrowser; timeRange: any }, - elementsPositionAndAttributes: ElementsPositionAndAttribute[] - ) => { - return { browser, timeRange, elementsPositionAndAttributes }; - } - ), - tap(() => logger.debug(`taking screenshots`)), - mergeMap( - ({ browser, elementsPositionAndAttributes }: ScreenShotOpts & TimeRangeOpts) => - getScreenshots({ browser, elementsPositionAndAttributes }), - ({ timeRange }, screenshots) => ({ timeRange, screenshots }) - ) - ); - - return Rx.race(screenshot$, exit$); - }), - first() - ); - }; -} diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/check_for_toast.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/check_for_toast.ts new file mode 100644 index 0000000000000..d500dfed92243 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/check_for_toast.ts @@ -0,0 +1,29 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { LayoutInstance } from '../../layouts/layout'; + +export const checkForToastMessage = async ( + browser: HeadlessBrowser, + layout: LayoutInstance +): Promise => { + await browser.waitForSelector(layout.selectors.toastHeader, { silent: true }); + const toastHeaderText = await browser.evaluate({ + fn: selector => { + const nodeList = document.querySelectorAll(selector); + return nodeList.item(0).innerText; + }, + args: [layout.selectors.toastHeader], + }); + throw new Error( + i18n.translate('xpack.reporting.exportTypes.printablePdf.screenshots.unexpectedErrorMessage', { + defaultMessage: 'Encountered an unexpected message on the page: {toastHeaderText}', + values: { toastHeaderText }, + }) + ); +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts new file mode 100644 index 0000000000000..927cf9ec7d801 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts @@ -0,0 +1,51 @@ +/* + * 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 { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { LayoutInstance } from '../../layouts/layout'; +import { AttributesMap, ElementsPositionAndAttribute } from './types'; + +export const getElementPositionAndAttributes = async ( + browser: HeadlessBrowser, + layout: LayoutInstance +): Promise => { + const elementsPositionAndAttributes: ElementsPositionAndAttribute[] = await browser.evaluate({ + fn: (selector, attributes) => { + const elements: NodeListOf = document.querySelectorAll(selector); + + // NodeList isn't an array, just an iterator, unable to use .map/.forEach + const results: ElementsPositionAndAttribute[] = []; + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + const boundingClientRect = element.getBoundingClientRect() as DOMRect; + results.push({ + position: { + boundingClientRect: { + // modern browsers support x/y, but older ones don't + top: boundingClientRect.y || boundingClientRect.top, + left: boundingClientRect.x || boundingClientRect.left, + width: boundingClientRect.width, + height: boundingClientRect.height, + }, + scroll: { + x: window.scrollX, + y: window.scrollY, + }, + }, + attributes: Object.keys(attributes).reduce((result: AttributesMap, key) => { + const attribute = attributes[key]; + result[key] = element.getAttribute(attribute); + return result; + }, {}), + }); + } + return results; + }, + args: [layout.selectors.screenshot, { title: 'data-title', description: 'data-description' }], + }); + + return elementsPositionAndAttributes; +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts new file mode 100644 index 0000000000000..99fb4e057c058 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.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 { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { LevelLogger as Logger } from '../../../../server/lib/level_logger'; +import { LayoutInstance } from '../../layouts/layout'; + +export const getNumberOfItems = async ( + browser: HeadlessBrowser, + layout: LayoutInstance, + logger: Logger +): Promise => { + logger.debug('determining how many items we have'); + + // returns the value of the `itemsCountAttribute` if it's there, otherwise + // we just count the number of `itemSelector` + const itemsCount: number = await browser.evaluate({ + fn: (selector, countAttribute) => { + const elementWithCount = document.querySelector(`[${countAttribute}]`); + if (elementWithCount && elementWithCount != null) { + const count = elementWithCount.getAttribute(countAttribute); + if (count && count != null) { + return parseInt(count, 10); + } + } + + return document.querySelectorAll(selector).length; + }, + args: [layout.selectors.renderComplete, layout.selectors.itemsCountAttribute], + }); + + return itemsCount; +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts new file mode 100644 index 0000000000000..caa01d2651ca5 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts @@ -0,0 +1,51 @@ +/* + * 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 { LevelLogger as Logger } from '../../../../server/lib/level_logger'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { Screenshot, ElementsPositionAndAttribute } from './types'; + +const getAsyncDurationLogger = (logger: Logger) => { + return async (description: string, promise: Promise) => { + const start = Date.now(); + const result = await promise; + logger.debug(`${description} took ${Date.now() - start}ms`); + return result; + }; +}; + +export const getScreenshots = async ({ + browser, + elementsPositionAndAttributes, + logger, +}: { + logger: Logger; + browser: HeadlessBrowser; + elementsPositionAndAttributes: ElementsPositionAndAttribute[]; +}): Promise => { + logger.debug(`taking screenshots`); + + const asyncDurationLogger = getAsyncDurationLogger(logger); + const screenshots: Screenshot[] = []; + + for (let i = 0; i < elementsPositionAndAttributes.length; i++) { + const item = elementsPositionAndAttributes[i]; + const base64EncodedData = await asyncDurationLogger( + `screenshot #${i + 1}`, + browser.screenshot(item.position) + ); + + screenshots.push({ + base64EncodedData, + title: item.attributes.title, + description: item.attributes.description, + }); + } + + logger.debug(`screenshots taken: ${screenshots.length}`); + + return screenshots; +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts new file mode 100644 index 0000000000000..7e7eca612ab7f --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts @@ -0,0 +1,46 @@ +/* + * 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 { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { LevelLogger as Logger } from '../../../../server/lib/level_logger'; +import { LayoutInstance } from '../../layouts/layout'; +import { TimeRange } from './types'; + +export const getTimeRange = async ( + browser: HeadlessBrowser, + layout: LayoutInstance, + logger: Logger +): Promise => { + logger.debug('getting timeRange'); + + const timeRange: TimeRange | null = await browser.evaluate({ + fn: (fromAttribute, toAttribute) => { + const fromElement = document.querySelector(`[${fromAttribute}]`); + const toElement = document.querySelector(`[${toAttribute}]`); + + if (!fromElement || !toElement) { + return null; + } + + const from = fromElement.getAttribute(fromAttribute); + const to = toElement.getAttribute(toAttribute); + if (!to || !from) { + return null; + } + + return { from, to }; + }, + args: [layout.selectors.timefilterFromAttribute, layout.selectors.timefilterToAttribute], + }); + + if (timeRange) { + logger.debug(`timeRange from ${timeRange.from} to ${timeRange.to}`); + } else { + logger.debug('no timeRange'); + } + + return timeRange; +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts new file mode 100644 index 0000000000000..9fc945b7edb55 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts @@ -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 * as Rx from 'rxjs'; +import { first, mergeMap } from 'rxjs/operators'; +import { PLUGIN_ID } from '../../../../common/constants'; +import { LevelLogger as Logger } from '../../../../server/lib/level_logger'; +import { KbnServer } from '../../../../types'; +import { HeadlessChromiumDriverFactory } from '../../../../server/browsers/chromium/driver_factory'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { + ElementsPositionAndAttribute, + ScreenShotOpts, + TimeRangeOpts, + ScreenshotObservableOpts, + BrowserOpts, +} from './types'; + +import { checkForToastMessage } from './check_for_toast'; +import { injectCustomCss } from './inject_css'; +import { openUrl } from './open_url'; +import { waitForRenderComplete } from './wait_for_render'; +import { getNumberOfItems } from './get_number_of_items'; +import { waitForElementsToBeInDOM } from './wait_for_dom_elements'; +import { getTimeRange } from './get_time_range'; +import { getElementPositionAndAttributes } from './get_element_position_data'; +import { getScreenshots } from './get_screenshots'; + +export function screenshotsObservableFactory(server: KbnServer) { + const logger = Logger.createForServer(server, [PLUGIN_ID, 'screenshots']); + const browserDriverFactory: HeadlessChromiumDriverFactory = server.plugins.reporting.browserDriverFactory; // prettier-ignore + const config = server.config(); + const captureConfig = config.get('xpack.reporting.capture'); + + return function screenshotsObservable({ + url, + conditionalHeaders, + layout, + browserTimezone, + }: ScreenshotObservableOpts): Rx.Observable { + logger.debug(`Creating browser driver factory`); + + const create$ = browserDriverFactory.create({ + viewport: layout.getBrowserViewport(), + browserTimezone, + }); + + return create$.pipe( + mergeMap(({ driver$, exit$, message$, consoleMessage$ }) => { + logger.debug('Driver factory created'); + message$.subscribe((line: string) => { + logger.debug(line, ['browser']); + }); + consoleMessage$.subscribe((line: string) => { + logger.debug(line, ['browserConsole']); + }); + const screenshot$ = driver$.pipe( + mergeMap( + (browser: HeadlessBrowser) => openUrl(browser, url, conditionalHeaders, logger), + browser => browser + ), + mergeMap( + (browser: HeadlessBrowser) => { + logger.debug( + 'waiting for elements or items count attribute; or not found to interrupt' + ); + + // the dashboard is using the `itemsCountAttribute` attribute to let us + // know how many items to expect since gridster incrementally adds panels + // we have to use this hint to wait for all of them + const renderSuccess = browser.waitForSelector( + `${layout.selectors.renderComplete},[${layout.selectors.itemsCountAttribute}]` + ); + const renderError = checkForToastMessage(browser, layout); + return Rx.race(Rx.from(renderSuccess), Rx.from(renderError)); + }, + browser => browser + ), + mergeMap( + (browser: HeadlessBrowser) => getNumberOfItems(browser, layout, logger), + (browser, itemsCount) => ({ browser, itemsCount }) + ), + mergeMap( + async ({ browser, itemsCount }) => { + logger.debug('setting viewport'); + const viewport = layout.getViewport(itemsCount); + return await browser.setViewport(viewport); + }, + ({ browser, itemsCount }) => ({ browser, itemsCount }) + ), + mergeMap( + ({ browser, itemsCount }) => { + logger.debug(`waiting for ${itemsCount} to be in the DOM`); + return waitForElementsToBeInDOM(browser, itemsCount, layout); + }, + ({ browser, itemsCount }) => ({ browser, itemsCount }) + ), + mergeMap( + ({ browser }) => { + // Waiting till _after_ elements have rendered before injecting our CSS + // allows for them to be displayed properly in many cases + return injectCustomCss(browser, layout, logger); + }, + ({ browser }) => ({ browser }) + ), + mergeMap( + async ({ browser }) => { + if (layout.positionElements) { + logger.debug('positioning elements'); + // position panel elements for print layout + return await layout.positionElements(browser); + } + }, + ({ browser }) => browser + ), + mergeMap( + (browser: HeadlessBrowser) => { + return waitForRenderComplete(captureConfig, browser, layout, logger); + }, + browser => browser + ), + mergeMap( + (browser: HeadlessBrowser) => getTimeRange(browser, layout, logger), + (browser, timeRange) => ({ browser, timeRange }) + ), + mergeMap( + ({ browser }) => getElementPositionAndAttributes(browser, layout), + ( + { browser, timeRange }: BrowserOpts & TimeRangeOpts, + elementsPositionAndAttributes: ElementsPositionAndAttribute[] + ) => ({ browser, timeRange, elementsPositionAndAttributes }) + ), + mergeMap( + ({ + browser, + elementsPositionAndAttributes, + }: BrowserOpts & ScreenShotOpts & TimeRangeOpts) => { + return getScreenshots({ browser, elementsPositionAndAttributes, logger }); + }, + ({ timeRange }, screenshots) => ({ timeRange, screenshots }) + ) + ); + + return Rx.race(screenshot$, exit$); + }), + first() + ); + }; +} diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts new file mode 100644 index 0000000000000..b5c2bd67a710d --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts @@ -0,0 +1,33 @@ +/* + * 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 fs from 'fs'; +import { promisify } from 'util'; +import { LevelLogger as Logger } from '../../../../server/lib/level_logger'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { Layout } from '../../layouts/layout'; + +const fsp = { readFile: promisify(fs.readFile) }; + +export const injectCustomCss = async ( + browser: HeadlessBrowser, + layout: Layout, + logger: Logger +): Promise => { + logger.debug('injecting custom css'); + + const filePath = layout.getCssOverridesPath(); + const buffer = await fsp.readFile(filePath); + await browser.evaluate({ + fn: css => { + const node = document.createElement('style'); + node.type = 'text/css'; + node.innerHTML = css; // eslint-disable-line no-unsanitized/property + document.getElementsByTagName('head')[0].appendChild(node); + }, + args: [buffer.toString()], + }); +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts new file mode 100644 index 0000000000000..2d4cfe09ef6ee --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts @@ -0,0 +1,23 @@ +/* + * 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 { LevelLogger as Logger } from '../../../../server/lib/level_logger'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { WAITFOR_SELECTOR } from '../../constants'; + +export const openUrl = async ( + browser: HeadlessBrowser, + url: string, + conditionalHeaders: any, + logger: Logger +): Promise => { + logger.debug(`opening ${url}`); + + await browser.open(url, { + conditionalHeaders, + waitForSelector: WAITFOR_SELECTOR, + }); +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts new file mode 100644 index 0000000000000..d5a6d34f41d32 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts @@ -0,0 +1,48 @@ +/* + * 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 { ElementPosition } from '../../../../types'; +import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { LayoutInstance } from '../../layouts/layout'; + +export interface ScreenshotObservableOpts { + url: string; + conditionalHeaders: any; + layout: LayoutInstance; + browserTimezone: string; +} + +export interface TimeRange { + from: any; + to: any; +} + +export interface AttributesMap { + [key: string]: any; +} + +export interface ElementsPositionAndAttribute { + position: ElementPosition; + attributes: AttributesMap; +} + +export interface Screenshot { + base64EncodedData: any; + title: any; + description: any; +} + +export interface ScreenShotOpts { + elementsPositionAndAttributes: ElementsPositionAndAttribute[]; +} + +export interface BrowserOpts { + browser: HeadlessBrowser; +} + +export interface TimeRangeOpts { + timeRange: TimeRange; +} diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_dom_elements.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_dom_elements.ts new file mode 100644 index 0000000000000..206c03e1012c4 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_dom_elements.ts @@ -0,0 +1,22 @@ +/* + * 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 { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { LayoutInstance } from '../../layouts/layout'; + +export const waitForElementsToBeInDOM = async ( + browser: HeadlessBrowser, + itemsCount: number, + layout: LayoutInstance +): Promise => { + await browser.waitFor({ + fn: selector => { + return document.querySelectorAll(selector).length; + }, + args: [layout.selectors.renderComplete], + toEqual: itemsCount, + }); +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts new file mode 100644 index 0000000000000..2a4567bf69bbf --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts @@ -0,0 +1,65 @@ +/* + * 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 { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { LevelLogger as Logger } from '../../../../server/lib/level_logger'; +import { LayoutInstance } from '../../layouts/layout'; + +export const waitForRenderComplete = async ( + captureConfig: any, + browser: HeadlessBrowser, + layout: LayoutInstance, + logger: Logger +) => { + logger.debug('waiting for rendering to complete'); + + return await browser + .evaluate({ + fn: (selector, visLoadDelay) => { + // wait for visualizations to finish loading + const visualizations: NodeListOf = document.querySelectorAll(selector); + const visCount = visualizations.length; + const renderedTasks = []; + + function waitForRender(visualization: Element) { + return new Promise(resolve => { + visualization.addEventListener('renderComplete', () => resolve()); + }); + } + + function waitForRenderDelay() { + return new Promise(resolve => { + setTimeout(resolve, visLoadDelay); + }); + } + + for (let i = 0; i < visCount; i++) { + const visualization = visualizations[i]; + const isRendered = visualization.getAttribute('data-render-complete'); + + if (isRendered === 'disabled') { + renderedTasks.push(waitForRenderDelay()); + } else if (isRendered === 'false') { + renderedTasks.push(waitForRender(visualization)); + } + } + + // The renderComplete fires before the visualizations are in the DOM, so + // we wait for the event loop to flush before telling reporting to continue. This + // seems to correct a timing issue that was causing reporting to occasionally + // capture the first visualization before it was actually in the DOM. + // Note: 100 proved too short, see https://github.com/elastic/kibana/issues/22581, + // bumping to 250. + const hackyWaitForVisualizations = () => new Promise(r => setTimeout(r, 250)); + + return Promise.all(renderedTasks).then(hackyWaitForVisualizations); + }, + args: [layout.selectors.renderComplete, captureConfig.loadDelay], + }) + .then(() => { + logger.debug('rendering is complete'); + }); +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.js b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.js index 1eb80a4e7891f..2305d6ed49aa3 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.js +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.js @@ -16,7 +16,7 @@ function generatePngObservableFn(server) { const urlScreenshotsObservable = (url, conditionalHeaders, layout, browserTimezone) => { return Rx.of(url).pipe( - mergeMap(url => screenshotsObservable(url, conditionalHeaders, layout, browserTimezone), + mergeMap(url => screenshotsObservable({ url, conditionalHeaders, layout, browserTimezone }), (outer, inner) => inner, captureConcurrency ) diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.js index 79794ff4a241a..10af50b12f86b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.js +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.js @@ -34,7 +34,7 @@ function generatePdfObservableFn(server) { const urlScreenshotsObservable = (urls, conditionalHeaders, layout, browserTimezone) => { return Rx.from(urls).pipe( mergeMap( - url => screenshotsObservable(url, conditionalHeaders, layout, browserTimezone), + url => screenshotsObservable({ url, conditionalHeaders, layout, browserTimezone }), captureConcurrency ) ); From 077ae4dc652d13f86a01e23f6646fac251006b79 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Tue, 6 Aug 2019 21:58:03 +0200 Subject: [PATCH 17/26] add await for browser calls (#42515) --- .../test/functional/apps/dashboard_mode/dashboard_view_mode.js | 2 +- x-pack/test/functional/apps/grok_debugger/grok_debugger.js | 2 +- x-pack/test/functional/apps/maps/index.js | 3 +-- .../test/functional/apps/security/doc_level_security_roles.js | 2 +- x-pack/test/functional/apps/security/field_level_security.js | 2 +- x-pack/test/functional/page_objects/security_page.js | 2 +- x-pack/test/visual_regression/tests/maps/index.js | 2 +- 7 files changed, 7 insertions(+), 8 deletions(-) diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index f0fc4b2349b6c..9e5447919c6d0 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -40,7 +40,7 @@ export default function ({ getService, getPageObjects }) { await kibanaServer.uiSettings.replace({ 'defaultIndex': 'logstash-*' }); - browser.setWindowSize(1600, 1000); + await browser.setWindowSize(1600, 1000); await PageObjects.common.navigateToApp('discover'); await PageObjects.dashboard.setTimepickerInHistoricalDataRange(); diff --git a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js index a8092f5acb84f..9279378f7b21f 100644 --- a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js +++ b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js @@ -17,7 +17,7 @@ export default function ({ getService, getPageObjects }) { await esArchiver.load('empty_kibana'); // Increase window height to ensure "Simulate" button is shown above the // fold. Otherwise it can't be clicked by the browser driver. - browser.setWindowSize(1600, 1000); + await browser.setWindowSize(1600, 1000); await PageObjects.grokDebugger.gotoGrokDebugger(); }); diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 0574b9dbd740f..9880fe41076d0 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -19,8 +19,7 @@ export default function ({ loadTestFile, getService }) { await kibanaServer.uiSettings.replace({ 'defaultIndex': 'logstash-*' }); - browser.setWindowSize(1600, 1000); - + await browser.setWindowSize(1600, 1000); }); after(async () => { diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js index 9ea1e30358ff3..0766a1e2b15f1 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -24,7 +24,7 @@ export default function ({ getService, getPageObjects }) { before('initialize tests', async () => { await esArchiver.load('empty_kibana'); await esArchiver.loadIfNeeded('security/dlstest'); - browser.setWindowSize(1600, 1000); + await browser.setWindowSize(1600, 1000); await PageObjects.settings.createIndexPattern('dlstest', null); diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index e425a8056c21c..640289fd119b6 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -18,7 +18,7 @@ export default function ({ getService, getPageObjects }) { before('initialize tests', async () => { await esArchiver.loadIfNeeded('security/flstest/data'); //( data) await esArchiver.load('security/flstest/kibana'); //(savedobject) - browser.setWindowSize(1600, 1000); + await browser.setWindowSize(1600, 1000); }); it('should add new role a_viewssnrole', async function () { diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index 5138113e26671..d3af519a66b57 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -79,7 +79,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { log.debug('SecurityPage:initTests'); await esArchiver.load('empty_kibana'); await esArchiver.loadIfNeeded('logstash_functional'); - browser.setWindowSize(1600, 1000); + await browser.setWindowSize(1600, 1000); } async login(username, password, options = {}) { diff --git a/x-pack/test/visual_regression/tests/maps/index.js b/x-pack/test/visual_regression/tests/maps/index.js index c959667dbd2f8..de5c50e900ca8 100644 --- a/x-pack/test/visual_regression/tests/maps/index.js +++ b/x-pack/test/visual_regression/tests/maps/index.js @@ -17,7 +17,7 @@ export default function ({ loadTestFile, getService }) { await kibanaServer.uiSettings.replace({ 'defaultIndex': 'logstash-*' }); - browser.setWindowSize(1600, 1000); + await browser.setWindowSize(1600, 1000); }); From 8a3b96d768b8ed48313b8cd544a1cfd790dc5a49 Mon Sep 17 00:00:00 2001 From: Nick Peihl Date: Tue, 6 Aug 2019 13:32:04 -0700 Subject: [PATCH 18/26] [Maps] Load Maki icons from spritesheet (#42499) * Load Maki icons from spritesheet --- NOTICE.txt | 33 + packages/kbn-maki/index.js | 378 --- packages/kbn-maki/package.json | 10 - packages/kbn-maki/readme.md | 6 - .../legacy/plugins/maps/common/constants.js | 4 - .../map/mb/image_utils.js | 160 + .../connected_components/map/mb/utils.js | 30 + .../connected_components/map/mb/view.js | 31 +- .../maps/public/layers/styles/symbol_utils.js | 30 +- x-pack/legacy/plugins/maps/server/routes.js | 14 +- .../plugins/maps/server/sprites/maki.json | 2820 ----------------- .../plugins/maps/server/sprites/maki.png | Bin 41303 -> 0 bytes .../plugins/maps/server/sprites/maki@2x.json | 2820 ----------------- .../plugins/maps/server/sprites/maki@2x.png | Bin 96215 -> 0 bytes x-pack/package.json | 2 +- yarn.lock | 5 + 16 files changed, 250 insertions(+), 6093 deletions(-) delete mode 100644 packages/kbn-maki/index.js delete mode 100644 packages/kbn-maki/package.json delete mode 100644 packages/kbn-maki/readme.md create mode 100644 x-pack/legacy/plugins/maps/public/connected_components/map/mb/image_utils.js delete mode 100644 x-pack/legacy/plugins/maps/server/sprites/maki.json delete mode 100644 x-pack/legacy/plugins/maps/server/sprites/maki.png delete mode 100644 x-pack/legacy/plugins/maps/server/sprites/maki@2x.json delete mode 100644 x-pack/legacy/plugins/maps/server/sprites/maki@2x.png diff --git a/NOTICE.txt b/NOTICE.txt index d1903a471341f..4780881be6949 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -107,6 +107,39 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- +This product includes code that is adapted from mapbox-gl-js, which is +available under a "BSD-3-Clause" license. +https://github.com/mapbox/mapbox-gl-js/blob/master/src/util/image.js + +Copyright (c) 2016, Mapbox + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Mapbox GL JS nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + --- This product includes code that is based on facebookincubator/idx, which was available under a "MIT" license. diff --git a/packages/kbn-maki/index.js b/packages/kbn-maki/index.js deleted file mode 100644 index 8158f12794afd..0000000000000 --- a/packages/kbn-maki/index.js +++ /dev/null @@ -1,378 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* eslint-disable quotes */ - -// icons from maki version 6.1.0 -export const maki = { - svgArray: [ - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n", - "\n\n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n", - "\n\n \n \n \n \n", - "\n\n \n \n", - "\n\n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n", - "\n\n \n \n", - "\n\n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n", - "\n\n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n \n \n \n \n", - "\n\n \n \n \n \n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n", - "\n\n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n", - "\n\n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n \n \n \n", - "\n\n \n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n", - "\n\n \n" - ] -}; diff --git a/packages/kbn-maki/package.json b/packages/kbn-maki/package.json deleted file mode 100644 index 862e183800b32..0000000000000 --- a/packages/kbn-maki/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "@kbn/maki", - "version": "6.1.0", - "description": "browser friendly version of @mapbox/maki", - "license": "Apache-2.0", - "main": "index.js", - "devDependencies": {}, - "dependencies": {}, - "peerDependencies": {} -} diff --git a/packages/kbn-maki/readme.md b/packages/kbn-maki/readme.md deleted file mode 100644 index aad509938113b..0000000000000 --- a/packages/kbn-maki/readme.md +++ /dev/null @@ -1,6 +0,0 @@ -# @kbn/maki - -[@mapbox/maki](https://www.npmjs.com/package/@mapbox/maki) only works in node.js. -See https://github.com/mapbox/maki/issues/462 for details. - -@kbn/maki is a browser friendly version of @mapbox/maki diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js index 70018866d27c2..d7f7e353799d7 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.js @@ -12,10 +12,6 @@ export const EMS_TILES_CATALOGUE_PATH = 'ems/tiles'; export const EMS_TILES_RASTER_TILE_PATH = 'ems/tiles/raster/tile'; export const EMS_TILES_RASTER_STYLE_PATH = 'ems/tiles/raster/style'; - -export const SPRITE_PATH = '/maps/sprite'; -export const MAKI_SPRITE_PATH = `${SPRITE_PATH}/maki`; - export const MAP_SAVED_OBJECT_TYPE = 'map'; export const APP_ID = 'maps'; export const APP_ICON = 'gisApp'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/image_utils.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/image_utils.js new file mode 100644 index 0000000000000..eae2d9fd314f6 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/image_utils.js @@ -0,0 +1,160 @@ +/* + * 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. + */ + +/* @notice + * This product includes code that is adapted from mapbox-gl-js, which is + * available under a "BSD-3-Clause" license. + * https://github.com/mapbox/mapbox-gl-js/blob/master/src/util/image.js + * + * Copyright (c) 2016, Mapbox + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Mapbox GL JS nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import assert from 'assert'; + +function createImage(image, { width, height }, channels, data) { + if (!data) { + data = new Uint8Array(width * height * channels); + } else if (data instanceof Uint8ClampedArray) { + data = new Uint8Array(data.buffer); + } else if (data.length !== width * height * channels) { + throw new RangeError('mismatched image size'); + } + image.width = width; + image.height = height; + image.data = data; + return image; +} + +function resizeImage(image, { width, height }, channels) { + if (width === image.width && height === image.height) { + return; + } + + const newImage = createImage({}, { width, height }, channels); + + copyImage(image, newImage, { x: 0, y: 0 }, { x: 0, y: 0 }, { + width: Math.min(image.width, width), + height: Math.min(image.height, height) + }, channels); + + image.width = width; + image.height = height; + image.data = newImage.data; +} + +function copyImage(srcImg, dstImg, srcPt, dstPt, size, channels) { + if (size.width === 0 || size.height === 0) { + return dstImg; + } + + if (size.width > srcImg.width || + size.height > srcImg.height || + srcPt.x > srcImg.width - size.width || + srcPt.y > srcImg.height - size.height) { + throw new RangeError('out of range source coordinates for image copy'); + } + + if (size.width > dstImg.width || + size.height > dstImg.height || + dstPt.x > dstImg.width - size.width || + dstPt.y > dstImg.height - size.height) { + throw new RangeError('out of range destination coordinates for image copy'); + } + + const srcData = srcImg.data; + const dstData = dstImg.data; + + assert(srcData !== dstData); + + for (let y = 0; y < size.height; y++) { + const srcOffset = ((srcPt.y + y) * srcImg.width + srcPt.x) * channels; + const dstOffset = ((dstPt.y + y) * dstImg.width + dstPt.x) * channels; + for (let i = 0; i < size.width * channels; i++) { + dstData[dstOffset + i] = srcData[srcOffset + i]; + } + } + + return dstImg; +} + +export class AlphaImage { + + constructor(size, data) { + createImage(this, size, 1, data); + } + + resize(size) { + resizeImage(this, size, 1); + } + + clone() { + return new AlphaImage({ width: this.width, height: this.height }, new Uint8Array(this.data)); + } + + static copy(srcImg, dstImg, srcPt, dstPt, size) { + copyImage(srcImg, dstImg, srcPt, dstPt, size, 1); + } +} + +// Not premultiplied, because ImageData is not premultiplied. +// UNPACK_PREMULTIPLY_ALPHA_WEBGL must be used when uploading to a texture. +export class RGBAImage { + + // data must be a Uint8Array instead of Uint8ClampedArray because texImage2D does not + // support Uint8ClampedArray in all browsers + + constructor(size, data) { + createImage(this, size, 4, data); + } + + resize(size) { + resizeImage(this, size, 4); + } + + replace(data, copy) { + if (copy) { + this.data.set(data); + } else if (data instanceof Uint8ClampedArray) { + this.data = new Uint8Array(data.buffer); + } else { + this.data = data; + } + } + + clone() { + return new RGBAImage({ width: this.width, height: this.height }, new Uint8Array(this.data)); + } + + static copy(srcImg, dstImg, srcPt, dstPt, size) { + copyImage(srcImg, dstImg, srcPt, dstPt, size, 4); + } +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/utils.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/utils.js index 19fe8f95089af..6019b07fdddf7 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/utils.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/utils.js @@ -5,6 +5,7 @@ */ import _ from 'lodash'; +import { RGBAImage } from './image_utils'; export function removeOrphanedSourcesAndLayers(mbMap, layerList) { @@ -94,3 +95,32 @@ export function syncLayerOrderForSingleLayer(mbMap, layerList) { }); } + +function getImageData(img) { + const canvas = window.document.createElement('canvas'); + const context = canvas.getContext('2d'); + if (!context) { + throw new Error('failed to create canvas 2d context'); + } + canvas.width = img.width; + canvas.height = img.height; + context.drawImage(img, 0, 0, img.width, img.height); + return context.getImageData(0, 0, img.width, img.height); +} + +export async function addSpritesheetToMap(json, img, mbMap) { + const image = new Image(); + image.onload = (el) => { + const imgData = getImageData(el.currentTarget); + for (const imageId in json) { + if (json.hasOwnProperty(imageId)) { + const { width, height, x, y, sdf, pixelRatio } = json[imageId]; + const data = new RGBAImage({ width, height }); + RGBAImage.copy(imgData, data, { x, y }, { x: 0, y: 0 }, { width, height }); + // TODO not sure how to catch errors? + mbMap.addImage(imageId, data, { pixelRatio, sdf }); + } + } + }; + image.src = img; +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js index fe667722840fa..07d6d189437b0 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js @@ -8,12 +8,15 @@ import _ from 'lodash'; import React from 'react'; import ReactDOM from 'react-dom'; import { ResizeChecker } from 'ui/resize_checker'; -import { syncLayerOrderForSingleLayer, removeOrphanedSourcesAndLayers } from './utils'; +import { + syncLayerOrderForSingleLayer, + removeOrphanedSourcesAndLayers, + addSpritesheetToMap +} from './utils'; import { DECIMAL_DEGREES_PRECISION, FEATURE_ID_PROPERTY_NAME, - ZOOM_PRECISION, - MAKI_SPRITE_PATH + ZOOM_PRECISION } from '../../../../common/constants'; import mapboxgl from 'mapbox-gl'; import MapboxDraw from '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw-unminified'; @@ -22,13 +25,11 @@ import { FeatureTooltip } from '../feature_tooltip'; import { DRAW_TYPE } from '../../../actions/map_actions'; import { createShapeFilterWithMeta, createExtentFilterWithMeta } from '../../../elasticsearch_geo_utils'; import chrome from 'ui/chrome'; +import { spritesheet } from '@elastic/maki'; +import sprites1 from '@elastic/maki/dist/sprite@1.png'; +import sprites2 from '@elastic/maki/dist/sprite@2.png'; -function relativeToAbsolute(url) { - const a = document.createElement('a'); - a.setAttribute('href', url); - return a.href; -} - +const isRetina = window.devicePixelRatio === 2; const mbDrawModes = MapboxDraw.modes; mbDrawModes.draw_rectangle = DrawRectangle; @@ -351,7 +352,6 @@ export class MBMapContainer extends React.Component { async _createMbMapInstance() { const initialView = this.props.goto ? this.props.goto.center : null; - const makiUrl = relativeToAbsolute(chrome.addBasePath(MAKI_SPRITE_PATH)); return new Promise((resolve) => { const options = { attributionControl: false, @@ -359,8 +359,7 @@ export class MBMapContainer extends React.Component { style: { version: 8, sources: {}, - layers: [], - sprite: makiUrl + layers: [] }, scrollZoom: this.props.scrollZoom, preserveDrawingBuffer: chrome.getInjected('preserveDrawingBuffer', false) @@ -396,6 +395,8 @@ export class MBMapContainer extends React.Component { return; } + this._loadMakiSprites(); + this._initResizerChecker(); // moveend callback is debounced to avoid updating map extent state while map extent is still changing @@ -436,6 +437,12 @@ export class MBMapContainer extends React.Component { }); } + _loadMakiSprites() { + const sprites = isRetina ? sprites2 : sprites1; + const json = isRetina ? spritesheet[2] : spritesheet[1]; + addSpritesheetToMap(json, sprites, this._mbMap); + } + _hideTooltip() { if (this._mbPopup.isOpen()) { this._mbPopup.remove(); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/symbol_utils.js b/x-pack/legacy/plugins/maps/public/layers/styles/symbol_utils.js index 887b347b6ce97..a32ae8d414b46 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/symbol_utils.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/symbol_utils.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { maki } from '@kbn/maki'; +import maki from '@elastic/maki'; import xml2js from 'xml2js'; import { parseXmlString } from '../../../common/parse_xml_string'; @@ -70,31 +70,3 @@ export async function styleSvg(svgString, fill) { const builder = new xml2js.Builder(); return builder.buildObject(svgXml); } - -function addImageToMap(imageUrl, imageId, symbolId, mbMap) { - return new Promise((resolve, reject) => { - const img = new Image(LARGE_MAKI_ICON_SIZE, LARGE_MAKI_ICON_SIZE); - img.onload = () => { - mbMap.addImage(imageId, img); - resolve(); - }; - img.onerror = (err) => { - reject(err); - }; - img.src = imageUrl; - }); -} - -export async function loadImage(imageId, symbolId, color, mbMap) { - let symbolSvg; - try { - symbolSvg = getMakiSymbolSvg(symbolId); - } catch(error) { - return; - } - - const styledSvg = await styleSvg(symbolSvg, color); - const imageUrl = buildSrcUrl(styledSvg); - - await addImageToMap(imageUrl, imageId, symbolId, mbMap); -} diff --git a/x-pack/legacy/plugins/maps/server/routes.js b/x-pack/legacy/plugins/maps/server/routes.js index fc18fdd325f00..c513a7bd17d35 100644 --- a/x-pack/legacy/plugins/maps/server/routes.js +++ b/x-pack/legacy/plugins/maps/server/routes.js @@ -12,12 +12,10 @@ import { EMS_TILES_CATALOGUE_PATH, EMS_TILES_RASTER_STYLE_PATH, EMS_TILES_RASTER_TILE_PATH, - GIS_API_PATH, - SPRITE_PATH, + GIS_API_PATH } from '../common/constants'; import fetch from 'node-fetch'; import { i18n } from '@kbn/i18n'; -import path from 'path'; import Boom from 'boom'; @@ -270,14 +268,4 @@ export function initRoutes(server, licenseUid) { } } }); - - server.route({ - method: 'GET', - path: `${SPRITE_PATH}/{path*}`, - handler: { - directory: { - path: path.join(__dirname, './sprites') - } - } - }); } diff --git a/x-pack/legacy/plugins/maps/server/sprites/maki.json b/x-pack/legacy/plugins/maps/server/sprites/maki.json deleted file mode 100644 index 8eed0d7333a23..0000000000000 --- a/x-pack/legacy/plugins/maps/server/sprites/maki.json +++ /dev/null @@ -1,2820 +0,0 @@ -{ - "aerialway-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 0, - "y": 165 - }, - "aerialway-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 0 - }, - "airfield-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 11, - "y": 165 - }, - "airfield-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 0 - }, - "airport-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 22, - "y": 165 - }, - "airport-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 15 - }, - "alcohol-shop-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 33, - "y": 165 - }, - "alcohol-shop-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 15 - }, - "american-football-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 44, - "y": 165 - }, - "american-football-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 0 - }, - "amusement-park-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 55, - "y": 165 - }, - "amusement-park-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 0 - }, - "aquarium-11": { - "sdf": true, - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 66, - "y": 165 - }, - "aquarium-15": { - "sdf": true, - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 15 - }, - "art-gallery-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 77, - "y": 165 - }, - "art-gallery-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 15 - }, - "attraction-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 88, - "y": 165 - }, - "attraction-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 30 - }, - "bakery-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 99, - "y": 165 - }, - "bakery-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 30 - }, - "bank-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 110, - "y": 165 - }, - "bank-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 30 - }, - "bar-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 121, - "y": 165 - }, - "bar-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 30 - }, - "barrier-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 132, - "y": 165 - }, - "barrier-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 45 - }, - "baseball-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 143, - "y": 165 - }, - "baseball-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 45 - }, - "basketball-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 154, - "y": 165 - }, - "basketball-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 45 - }, - "bbq-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 165, - "y": 165 - }, - "bbq-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 45 - }, - "beach-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 176, - "y": 165 - }, - "beach-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 0 - }, - "beer-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 187, - "y": 165 - }, - "beer-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 0 - }, - "bicycle-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 198, - "y": 165 - }, - "bicycle-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 0 - }, - "bicycle-share-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 209, - "y": 165 - }, - "bicycle-share-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 0 - }, - "blood-bank-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 220, - "y": 165 - }, - "blood-bank-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 15 - }, - "bowling-alley-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 0, - "y": 176 - }, - "bowling-alley-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 15 - }, - "bridge-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 11, - "y": 176 - }, - "bridge-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 15 - }, - "building-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 22, - "y": 176 - }, - "building-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 15 - }, - "building-alt1-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 33, - "y": 176 - }, - "building-alt1-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 30 - }, - "bus-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 44, - "y": 176 - }, - "bus-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 30 - }, - "cafe-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 55, - "y": 176 - }, - "cafe-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 30 - }, - "campsite-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 66, - "y": 176 - }, - "campsite-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 30 - }, - "car-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 77, - "y": 176 - }, - "car-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 45 - }, - "car-rental-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 88, - "y": 176 - }, - "car-rental-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 45 - }, - "car-repair-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 99, - "y": 176 - }, - "car-repair-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 45 - }, - "casino-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 110, - "y": 176 - }, - "casino-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 45 - }, - "castle-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 121, - "y": 176 - }, - "castle-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 60 - }, - "cemetery-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 132, - "y": 176 - }, - "cemetery-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 60 - }, - "charging-station-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 143, - "y": 176 - }, - "charging-station-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 60 - }, - "cinema-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 154, - "y": 176 - }, - "cinema-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 60 - }, - "circle-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 165, - "y": 176 - }, - "circle-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 60 - }, - "circle-stroked-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 176, - "y": 176 - }, - "circle-stroked-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 60 - }, - "city-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 187, - "y": 176 - }, - "city-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 60 - }, - "clothing-store-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 198, - "y": 176 - }, - "clothing-store-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 60 - }, - "college-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 209, - "y": 176 - }, - "college-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 75 - }, - "commercial-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 220, - "y": 176 - }, - "commercial-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 75 - }, - "communications-tower-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 0, - "y": 187 - }, - "communications-tower-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 75 - }, - "confectionery-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 11, - "y": 187 - }, - "confectionery-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 75 - }, - "convenience-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 22, - "y": 187 - }, - "convenience-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 75 - }, - "cricket-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 33, - "y": 187 - }, - "cricket-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 75 - }, - "cross-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 44, - "y": 187 - }, - "cross-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 75 - }, - "dam-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 55, - "y": 187 - }, - "dam-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 75 - }, - "danger-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 66, - "y": 187 - }, - "danger-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 90 - }, - "defibrillator-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 77, - "y": 187 - }, - "defibrillator-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 90 - }, - "dentist-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 88, - "y": 187 - }, - "dentist-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 90 - }, - "doctor-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 99, - "y": 187 - }, - "doctor-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 90 - }, - "dog-park-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 110, - "y": 187 - }, - "dog-park-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 90 - }, - "drinking-water-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 121, - "y": 187 - }, - "drinking-water-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 90 - }, - "embassy-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 132, - "y": 187 - }, - "embassy-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 90 - }, - "emergency-phone-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 143, - "y": 187 - }, - "emergency-phone-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 90 - }, - "entrance-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 154, - "y": 187 - }, - "entrance-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 105 - }, - "entrance-alt1-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 165, - "y": 187 - }, - "entrance-alt1-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 105 - }, - "farm-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 176, - "y": 187 - }, - "farm-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 105 - }, - "fast-food-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 187, - "y": 187 - }, - "fast-food-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 105 - }, - "fence-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 198, - "y": 187 - }, - "fence-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 105 - }, - "ferry-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 209, - "y": 187 - }, - "ferry-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 105 - }, - "fire-station-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 220, - "y": 187 - }, - "fire-station-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 105 - }, - "fitness-centre-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 0, - "y": 198 - }, - "fitness-centre-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 105 - }, - "florist-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 11, - "y": 198 - }, - "florist-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 0 - }, - "fuel-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 22, - "y": 198 - }, - "fuel-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 0 - }, - "furniture-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 33, - "y": 198 - }, - "furniture-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 0 - }, - "gaming-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 44, - "y": 198 - }, - "gaming-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 0 - }, - "garden-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 55, - "y": 198 - }, - "garden-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 0 - }, - "garden-centre-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 66, - "y": 198 - }, - "garden-centre-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 0 - }, - "gift-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 77, - "y": 198 - }, - "gift-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 0 - }, - "globe-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 88, - "y": 198 - }, - "globe-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 0 - }, - "golf-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 99, - "y": 198 - }, - "golf-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 15 - }, - "grocery-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 110, - "y": 198 - }, - "grocery-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 15 - }, - "hairdresser-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 121, - "y": 198 - }, - "hairdresser-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 15 - }, - "harbor-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 132, - "y": 198 - }, - "harbor-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 15 - }, - "hardware-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 143, - "y": 198 - }, - "hardware-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 15 - }, - "heart-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 154, - "y": 198 - }, - "heart-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 15 - }, - "heliport-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 165, - "y": 198 - }, - "heliport-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 15 - }, - "home-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 176, - "y": 198 - }, - "home-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 15 - }, - "horse-riding-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 187, - "y": 198 - }, - "horse-riding-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 30 - }, - "hospital-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 198, - "y": 198 - }, - "hospital-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 30 - }, - "ice-cream-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 209, - "y": 198 - }, - "ice-cream-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 30 - }, - "industry-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 220, - "y": 198 - }, - "industry-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 30 - }, - "information-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 0, - "y": 209 - }, - "information-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 30 - }, - "jewelry-store-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 11, - "y": 209 - }, - "jewelry-store-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 30 - }, - "karaoke-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 22, - "y": 209 - }, - "karaoke-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 30 - }, - "landmark-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 33, - "y": 209 - }, - "landmark-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 30 - }, - "landuse-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 44, - "y": 209 - }, - "landuse-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 45 - }, - "laundry-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 55, - "y": 209 - }, - "laundry-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 45 - }, - "library-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 66, - "y": 209 - }, - "library-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 45 - }, - "lighthouse-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 77, - "y": 209 - }, - "lighthouse-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 45 - }, - "lodging-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 88, - "y": 209 - }, - "lodging-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 45 - }, - "logging-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 99, - "y": 209 - }, - "logging-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 45 - }, - "marker-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 110, - "y": 209 - }, - "marker-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 45 - }, - "marker-stroked-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 121, - "y": 209 - }, - "marker-stroked-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 45 - }, - "mobile-phone-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 132, - "y": 209 - }, - "mobile-phone-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 60 - }, - "monument-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 143, - "y": 209 - }, - "monument-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 60 - }, - "mountain-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 154, - "y": 209 - }, - "mountain-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 60 - }, - "museum-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 165, - "y": 209 - }, - "museum-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 60 - }, - "music-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 176, - "y": 209 - }, - "music-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 60 - }, - "natural-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 187, - "y": 209 - }, - "natural-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 60 - }, - "optician-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 198, - "y": 209 - }, - "optician-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 60 - }, - "paint-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 209, - "y": 209 - }, - "paint-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 60 - }, - "park-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 220, - "y": 209 - }, - "park-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 75 - }, - "park-alt1-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 0, - "y": 220 - }, - "park-alt1-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 75 - }, - "parking-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 11, - "y": 220 - }, - "parking-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 75 - }, - "parking-garage-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 22, - "y": 220 - }, - "parking-garage-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 75 - }, - "pharmacy-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 33, - "y": 220 - }, - "pharmacy-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 75 - }, - "picnic-site-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 44, - "y": 220 - }, - "picnic-site-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 75 - }, - "pitch-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 55, - "y": 220 - }, - "pitch-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 75 - }, - "place-of-worship-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 66, - "y": 220 - }, - "place-of-worship-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 75 - }, - "playground-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 77, - "y": 220 - }, - "playground-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 90 - }, - "police-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 88, - "y": 220 - }, - "police-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 90 - }, - "post-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 99, - "y": 220 - }, - "post-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 90 - }, - "prison-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 110, - "y": 220 - }, - "prison-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 90 - }, - "rail-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 121, - "y": 220 - }, - "rail-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 90 - }, - "rail-light-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 132, - "y": 220 - }, - "rail-light-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 90 - }, - "rail-metro-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 143, - "y": 220 - }, - "rail-metro-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 90 - }, - "ranger-station-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 154, - "y": 220 - }, - "ranger-station-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 90 - }, - "recycling-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 165, - "y": 220 - }, - "recycling-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 105 - }, - "religious-buddhist-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 176, - "y": 220 - }, - "religious-buddhist-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 105 - }, - "religious-christian-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 187, - "y": 220 - }, - "religious-christian-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 105 - }, - "religious-jewish-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 198, - "y": 220 - }, - "religious-jewish-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 105 - }, - "religious-muslim-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 209, - "y": 220 - }, - "religious-muslim-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 105 - }, - "residential-community-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 220, - "y": 220 - }, - "residential-community-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 105 - }, - "restaurant-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 231, - "y": 165 - }, - "restaurant-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 105 - }, - "restaurant-noodle-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 242, - "y": 165 - }, - "restaurant-noodle-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 105 - }, - "restaurant-pizza-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 253, - "y": 165 - }, - "restaurant-pizza-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 120 - }, - "restaurant-seafood-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 264, - "y": 165 - }, - "restaurant-seafood-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 120 - }, - "roadblock-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 275, - "y": 165 - }, - "roadblock-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 120 - }, - "rocket-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 286, - "y": 165 - }, - "rocket-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 120 - }, - "school-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 297, - "y": 165 - }, - "school-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 120 - }, - "scooter-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 308, - "y": 165 - }, - "scooter-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 120 - }, - "shelter-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 319, - "y": 165 - }, - "shelter-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 120 - }, - "shoe-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 330, - "y": 165 - }, - "shoe-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 120 - }, - "shop-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 341, - "y": 165 - }, - "shop-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 120 - }, - "skateboard-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 352, - "y": 165 - }, - "skateboard-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 120 - }, - "skiing-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 363, - "y": 165 - }, - "skiing-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 120 - }, - "slaughterhouse-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 374, - "y": 165 - }, - "slaughterhouse-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 120 - }, - "slipway-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 385, - "y": 165 - }, - "slipway-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 120 - }, - "snowmobile-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 396, - "y": 165 - }, - "snowmobile-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 120 - }, - "soccer-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 407, - "y": 165 - }, - "soccer-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 120 - }, - "square-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 418, - "y": 165 - }, - "square-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 120 - }, - "square-stroked-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 429, - "y": 165 - }, - "square-stroked-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 135 - }, - "stadium-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 440, - "y": 165 - }, - "stadium-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 135 - }, - "star-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 451, - "y": 165 - }, - "star-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 135 - }, - "star-stroked-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 462, - "y": 165 - }, - "star-stroked-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 135 - }, - "suitcase-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 231, - "y": 176 - }, - "suitcase-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 135 - }, - "sushi-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 242, - "y": 176 - }, - "sushi-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 135 - }, - "swimming-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 253, - "y": 176 - }, - "swimming-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 135 - }, - "table-tennis-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 264, - "y": 176 - }, - "table-tennis-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 135 - }, - "teahouse-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 275, - "y": 176 - }, - "teahouse-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 135 - }, - "telephone-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 286, - "y": 176 - }, - "telephone-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 135 - }, - "tennis-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 297, - "y": 176 - }, - "tennis-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 135 - }, - "theatre-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 308, - "y": 176 - }, - "theatre-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 135 - }, - "toilet-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 319, - "y": 176 - }, - "toilet-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 135 - }, - "town-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 330, - "y": 176 - }, - "town-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 135 - }, - "town-hall-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 341, - "y": 176 - }, - "town-hall-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 135 - }, - "triangle-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 352, - "y": 176 - }, - "triangle-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 135 - }, - "triangle-stroked-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 363, - "y": 176 - }, - "triangle-stroked-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 0, - "y": 150 - }, - "veterinary-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 374, - "y": 176 - }, - "veterinary-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 15, - "y": 150 - }, - "viewpoint-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 385, - "y": 176 - }, - "viewpoint-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 30, - "y": 150 - }, - "village-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 396, - "y": 176 - }, - "village-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 45, - "y": 150 - }, - "volcano-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 407, - "y": 176 - }, - "volcano-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 60, - "y": 150 - }, - "volleyball-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 418, - "y": 176 - }, - "volleyball-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 75, - "y": 150 - }, - "warehouse-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 429, - "y": 176 - }, - "warehouse-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 90, - "y": 150 - }, - "waste-basket-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 440, - "y": 176 - }, - "waste-basket-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 105, - "y": 150 - }, - "watch-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 451, - "y": 176 - }, - "watch-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 120, - "y": 150 - }, - "water-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 462, - "y": 176 - }, - "water-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 135, - "y": 150 - }, - "waterfall-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 231, - "y": 187 - }, - "waterfall-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 150, - "y": 150 - }, - "watermill-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 242, - "y": 187 - }, - "watermill-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 165, - "y": 150 - }, - "wetland-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 253, - "y": 187 - }, - "wetland-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 180, - "y": 150 - }, - "wheelchair-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 264, - "y": 187 - }, - "wheelchair-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 195, - "y": 150 - }, - "windmill-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 275, - "y": 187 - }, - "windmill-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 210, - "y": 150 - }, - "zoo-11": { - "sdf": true, - "height": 11, - "pixelRatio": 1, - "width": 11, - "x": 286, - "y": 187 - }, - "zoo-15": { - "sdf": true, - "height": 15, - "pixelRatio": 1, - "width": 15, - "x": 225, - "y": 150 - } -} diff --git a/x-pack/legacy/plugins/maps/server/sprites/maki.png b/x-pack/legacy/plugins/maps/server/sprites/maki.png deleted file mode 100644 index 9206cf8c0def96d3ea13d31d8784b614e940e937..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41303 zcmX6^Wmwa17p4U1Qo2)8xvP}e-}q0If#X#Q1~KS2-mevE@6;I9b=tyvu6;HP??TiH+vn!Q=LlV%J1eUyR6QwEfiZ3!;7bnE+fUfod5x^u3p( zUx06S7|Zu_K8RbPRE{8^l^T4F1?@)NNA8&JmJCVh;hgkbF_Bl?WFQTymB^nj2^z7Q za#utcXRCEmLb+0uu6$bKkI`EJ{T>YD3W zrd_)z^ORJXUp^l+y7@Xk+^hR3*tDqI`C3=R1>Es-r)GXDkA)H9DActNyt$_oG#ypOB!G67^ z`(~Y~@K$kG@C-7leY2E*6(87$>@(szGBVK*P}n-Ue?FmC$b0b~)#76|)P32l!0O7&v&=(vh!~+zl_v=U zS60B)BjtF=yQW)Q9uel(MLl*YIP*vJYYl0TY~1)ZjcIIrYH>gr7IYhRzqTU!jhnM_ zK%H|%^1NTplRRT`Wrpp25)KTNT)D&qZm=;QaI2>r1YbSjD1nmvj>ouc_2)#TI=9vV zPSR3|vZTV8h71hSg(Oc>Y`s#Il?R^edG2b(e~AS-AQ1PT-zXLxKQ%Lzu|94tF1A3mGy<-EAVWW>PV%0Do{`%t^jeij=~}r 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
6m3$LH@V1@I(HhR+P#AM*lHEUDfk$H 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 C7X;7% diff --git a/x-pack/package.json b/x-pack/package.json index 657e53c8bebf7..563a57d1660ae 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -177,6 +177,7 @@ "@elastic/eui": "13.1.1", "@elastic/javascript-typescript-langserver": "^0.2.2", "@elastic/lsp-extension": "^0.1.2", + "@elastic/maki": "6.1.0", "@elastic/node-crypto": "^1.0.0", "@elastic/nodegit": "0.25.0-alpha.23", "@elastic/numeral": "2.3.3", @@ -188,7 +189,6 @@ "@kbn/es-query": "1.0.0", "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", - "@kbn/maki": "6.1.0", "@kbn/ui-framework": "1.0.0", "@mapbox/mapbox-gl-draw": "^1.1.1", "@samverschueren/stream-to-observable": "^0.3.0", diff --git a/yarn.lock b/yarn.lock index b100341088ca1..67b975a843e5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1746,6 +1746,11 @@ through2 "^2.0.0" update-notifier "^0.5.0" +"@elastic/maki@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@elastic/maki/-/maki-6.1.0.tgz#384bdd53b95e9f87bd6b27e3d9dfaad70e29715a" + integrity sha512-eCNuGV3bVfSpDn1af6qCJ1udwm9DqGFjNN5JXbNIonAQYrbPvrRXNe5CxDKlWXbgxKOaOIhWtJ3/62JN+YKlZA== + "@elastic/node-crypto@^0.1.2": version "0.1.2" resolved "https://registry.yarnpkg.com/@elastic/node-crypto/-/node-crypto-0.1.2.tgz#c18ac282f635e88f041cc1555d806e492ca8f3b1" From 479e971989e2e33240b729ed65c133639333f8f6 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Tue, 6 Aug 2019 14:41:54 -0600 Subject: [PATCH 19/26] TS cleanup on new IndexPatterns implementation. (#42646) --- .../public/index_patterns/index_patterns.ts | 32 +++++++++++-------- .../step_details/step_details_form.tsx | 2 +- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/legacy/ui/public/index_patterns/index_patterns.ts b/src/legacy/ui/public/index_patterns/index_patterns.ts index f1cbc45fd9ff1..908b03963a062 100644 --- a/src/legacy/ui/public/index_patterns/index_patterns.ts +++ b/src/legacy/ui/public/index_patterns/index_patterns.ts @@ -17,6 +17,7 @@ * under the License. */ +import { idx } from '@kbn/elastic-idx'; import { SavedObjectsClientContract, SimpleSavedObject, UiSettingsClient } from 'src/core/public'; // @ts-ignore import { fieldFormats } from '../registry/field_formats'; @@ -33,7 +34,7 @@ export class IndexPatterns { private config: UiSettingsClient; private savedObjectsClient: SavedObjectsClientContract; - private savedObjectsCache?: Array> | null; + private savedObjectsCache?: Array>> | null; constructor(config: UiSettingsClient, savedObjectsClient: SavedObjectsClientContract) { this.config = config; @@ -48,35 +49,38 @@ export class IndexPatterns { })).savedObjects; } - getIds = async (refresh: boolean) => { + getIds = async (refresh: boolean = false) => { if (!this.savedObjectsCache || refresh) { await this.refreshSavedObjectsCache(); } - if (this.savedObjectsCache) { - return this.savedObjectsCache.map(obj => _.get(obj, 'id')); + if (!this.savedObjectsCache) { + return []; } + return this.savedObjectsCache.map(obj => idx(obj, _ => _.id)); }; - getTitles = async (refresh: boolean) => { + getTitles = async (refresh: boolean = false): Promise => { if (!this.savedObjectsCache || refresh) { await this.refreshSavedObjectsCache(); } - if (this.savedObjectsCache) { - return this.savedObjectsCache.map(obj => _.get(obj, 'attributes.title')); + if (!this.savedObjectsCache) { + return []; } + return this.savedObjectsCache.map(obj => idx(obj, _ => _.attributes.title)); }; - getFields = async (fields: string[], refresh: boolean) => { + getFields = async (fields: string[], refresh: boolean = false) => { if (!this.savedObjectsCache || refresh) { await this.refreshSavedObjectsCache(); } - if (this.savedObjectsCache) { - return this.savedObjectsCache.map(obj => { - const result: Record = {}; - fields.forEach(f => (result[f] = _.get(obj, f) || _.get(obj, `attributes.${f}`))); - return result; - }); + if (!this.savedObjectsCache) { + return []; } + return this.savedObjectsCache.map((obj: Record) => { + const result: Record = {}; + fields.forEach((f: string) => (result[f] = obj[f] || idx(obj, _ => _.attributes[f]))); + return result; + }); }; clearCache = (id?: string) => { diff --git a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/step_details/step_details_form.tsx b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/step_details/step_details_form.tsx index f5e97c43b6dda..19d243363a9a2 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/step_details/step_details_form.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/step_details/step_details_form.tsx @@ -118,7 +118,7 @@ export const StepDetailsForm: SFC = React.memo(({ overrides = {}, onChang } try { - setIndexPatternTitles((await kibanaContext.indexPatterns.getTitles(false)) as string[]); + setIndexPatternTitles(await kibanaContext.indexPatterns.getTitles()); } catch (e) { toastNotifications.addDanger( i18n.translate('xpack.ml.dataframe.stepDetailsForm.errorGettingIndexPatternTitles', { From e62ec7c5ecf8b827f9489fca134fef5f247c911b Mon Sep 17 00:00:00 2001 From: Tre Date: Tue, 6 Aug 2019 15:35:16 -0600 Subject: [PATCH 20/26] [FTR] Refactor FTR to live under KBN-TEST (#42547) Refactor the FTR to live under KBN-TEST . Next, refactor any and all files that the FTR needs to live under KBN-TEST, as needed. --- .eslintignore | 2 + packages/kbn-plugin-helpers/lib/utils.js | 2 +- packages/kbn-plugin-helpers/package.json | 1 + .../fixtures/failure_hooks/config.js | 0 .../failure_hooks/tests/after_hook.js | 0 .../failure_hooks/tests/before_hook.js | 0 .../fixtures/failure_hooks/tests/it.js | 0 .../fixtures/simple_project/config.js | 0 .../fixtures/simple_project/tests.js | 0 .../__tests__/integration/basic.js | 2 +- .../__tests__/integration/failure_hooks.js | 16 ++- .../src/functional_test_runner/cli.ts | 107 ++++++++++++++++++ .../fake_mocha_types.d.ts | 0 .../functional_test_runner.ts | 0 .../src}/functional_test_runner/index.ts | 1 + .../lib/config/__tests__/fixtures/config.1.js | 0 .../lib/config/__tests__/fixtures/config.2.js | 0 .../lib/config/__tests__/fixtures/config.3.js | 0 .../lib/config/__tests__/fixtures/config.4.js | 0 .../__tests__/fixtures/config.invalid.js | 0 .../lib/config/__tests__/read_config_file.js | 13 +-- .../lib/config/config.ts | 0 .../lib/config/index.ts | 0 .../lib/config/read_config_file.ts | 0 .../lib/config/schema.ts | 0 .../lib/config/transform_deprecations.ts | 2 +- .../src}/functional_test_runner/lib/index.ts | 0 .../functional_test_runner/lib/lifecycle.ts | 0 .../functional_test_runner/lib/load_tracer.ts | 0 .../lib/mocha/assignment_proxy.js | 4 +- .../lib/mocha/decorate_mocha_ui.js | 26 +++-- .../lib/mocha/filter_suites_by_tags.js | 15 +-- .../lib/mocha/filter_suites_by_tags.test.js | 0 .../functional_test_runner/lib/mocha/index.ts | 0 .../lib/mocha/load_test_files.js | 23 +++- .../lib/mocha/reporter/colors.js | 0 .../lib/mocha/reporter/index.js | 0 .../lib/mocha/reporter/ms.js | 0 .../lib/mocha/reporter/reporter.js | 70 ++++++------ .../lib/mocha/reporter/symbols.js | 8 +- .../lib/mocha/reporter/write_epilogue.js | 16 +-- .../lib/mocha/run_tests.ts | 0 .../lib/mocha/setup_mocha.js | 7 +- .../lib/mocha/wrap_function.js | 5 +- .../lib/mocha/wrap_runnable_args.js | 4 +- .../lib/providers/async_instance.ts | 0 .../lib/providers/index.ts | 0 .../lib/providers/provider_collection.ts | 0 .../lib/providers/read_provider_spec.ts | 0 .../lib/providers/verbose_instance.ts | 0 .../src/functional_tests/lib/run_ftr.js | 2 +- .../kbn-test/src/functional_tests/tasks.js | 2 +- packages/kbn-test/src/index.js | 4 + packages/kbn-test/tsconfig.json | 3 +- packages/kbn-test/types/ftr.d.ts | 2 +- scripts/functional_test_runner.js | 2 +- scripts/functional_tests_server.js | 2 +- src/dev/jest/config.js | 2 +- src/es_archiver/cli.js | 2 +- src/functional_test_runner/cli.ts | 106 ----------------- 60 files changed, 228 insertions(+), 223 deletions(-) rename {src => packages/kbn-test/src}/functional_test_runner/__tests__/fixtures/failure_hooks/config.js (100%) rename {src => packages/kbn-test/src}/functional_test_runner/__tests__/fixtures/failure_hooks/tests/after_hook.js (100%) rename {src => packages/kbn-test/src}/functional_test_runner/__tests__/fixtures/failure_hooks/tests/before_hook.js (100%) rename {src => packages/kbn-test/src}/functional_test_runner/__tests__/fixtures/failure_hooks/tests/it.js (100%) rename {src => packages/kbn-test/src}/functional_test_runner/__tests__/fixtures/simple_project/config.js (100%) rename {src => packages/kbn-test/src}/functional_test_runner/__tests__/fixtures/simple_project/tests.js (100%) rename {src => packages/kbn-test/src}/functional_test_runner/__tests__/integration/basic.js (99%) rename {src => packages/kbn-test/src}/functional_test_runner/__tests__/integration/failure_hooks.js (87%) create mode 100644 packages/kbn-test/src/functional_test_runner/cli.ts rename {src => packages/kbn-test/src}/functional_test_runner/fake_mocha_types.d.ts (100%) rename {src => packages/kbn-test/src}/functional_test_runner/functional_test_runner.ts (100%) rename {src => packages/kbn-test/src}/functional_test_runner/index.ts (96%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/config/__tests__/fixtures/config.1.js (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/config/__tests__/fixtures/config.2.js (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/config/__tests__/fixtures/config.3.js (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/config/__tests__/fixtures/config.4.js (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/config/__tests__/fixtures/config.invalid.js (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/config/__tests__/read_config_file.js (92%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/config/config.ts (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/config/index.ts (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/config/read_config_file.ts (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/config/schema.ts (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/config/transform_deprecations.ts (92%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/index.ts (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/lifecycle.ts (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/load_tracer.ts (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/assignment_proxy.js (97%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/decorate_mocha_ui.js (91%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/filter_suites_by_tags.js (92%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/index.ts (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/load_test_files.js (87%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/reporter/colors.js (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/reporter/index.js (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/reporter/ms.js (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/reporter/reporter.js (83%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/reporter/symbols.js (85%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/reporter/write_epilogue.js (81%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/run_tests.ts (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/setup_mocha.js (91%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/wrap_function.js (99%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/mocha/wrap_runnable_args.js (99%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/providers/async_instance.ts (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/providers/index.ts (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/providers/provider_collection.ts (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/providers/read_provider_spec.ts (100%) rename {src => packages/kbn-test/src}/functional_test_runner/lib/providers/verbose_instance.ts (100%) delete mode 100644 src/functional_test_runner/cli.ts diff --git a/.eslintignore b/.eslintignore index 9eacf2fd47d77..12a3f68486047 100644 --- a/.eslintignore +++ b/.eslintignore @@ -25,6 +25,8 @@ bower_components /packages/kbn-ui-framework/dist /packages/kbn-ui-framework/doc_site/build /packages/kbn-ui-framework/generator-kui/*/templates/ +/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/ +/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/ /x-pack/legacy/plugins/maps/public/vendor/** /x-pack/coverage /x-pack/build diff --git a/packages/kbn-plugin-helpers/lib/utils.js b/packages/kbn-plugin-helpers/lib/utils.js index d9a5b9148208f..6e3d8969802a4 100644 --- a/packages/kbn-plugin-helpers/lib/utils.js +++ b/packages/kbn-plugin-helpers/lib/utils.js @@ -42,7 +42,7 @@ function resolveKibanaPath(path) { } function readFtrConfigFile(log, path, settingOverrides) { - return require(resolveKibanaPath('src/functional_test_runner')) // eslint-disable-line import/no-dynamic-require + return require('@kbn/test') // eslint-disable-line import/no-dynamic-require .readConfigFile(log, path, settingOverrides); } diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index 82747ccd0e073..f13c497e4a9bc 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@babel/core": "^7.4.4", + "@kbn/test": "1.0.0", "argv-split": "^2.0.1", "commander": "^2.9.0", "del": "^4.0.0", diff --git a/src/functional_test_runner/__tests__/fixtures/failure_hooks/config.js b/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/config.js similarity index 100% rename from src/functional_test_runner/__tests__/fixtures/failure_hooks/config.js rename to packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/config.js diff --git a/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/after_hook.js b/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/after_hook.js similarity index 100% rename from src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/after_hook.js rename to packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/after_hook.js diff --git a/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/before_hook.js b/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/before_hook.js similarity index 100% rename from src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/before_hook.js rename to packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/before_hook.js diff --git a/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/it.js b/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/it.js similarity index 100% rename from src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/it.js rename to packages/kbn-test/src/functional_test_runner/__tests__/fixtures/failure_hooks/tests/it.js diff --git a/src/functional_test_runner/__tests__/fixtures/simple_project/config.js b/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/simple_project/config.js similarity index 100% rename from src/functional_test_runner/__tests__/fixtures/simple_project/config.js rename to packages/kbn-test/src/functional_test_runner/__tests__/fixtures/simple_project/config.js diff --git a/src/functional_test_runner/__tests__/fixtures/simple_project/tests.js b/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/simple_project/tests.js similarity index 100% rename from src/functional_test_runner/__tests__/fixtures/simple_project/tests.js rename to packages/kbn-test/src/functional_test_runner/__tests__/fixtures/simple_project/tests.js diff --git a/src/functional_test_runner/__tests__/integration/basic.js b/packages/kbn-test/src/functional_test_runner/__tests__/integration/basic.js similarity index 99% rename from src/functional_test_runner/__tests__/integration/basic.js rename to packages/kbn-test/src/functional_test_runner/__tests__/integration/basic.js index 1c3858a8cfd6d..711ce683ffdf7 100644 --- a/src/functional_test_runner/__tests__/integration/basic.js +++ b/packages/kbn-test/src/functional_test_runner/__tests__/integration/basic.js @@ -25,7 +25,7 @@ import expect from '@kbn/expect'; const SCRIPT = resolve(__dirname, '../../../../scripts/functional_test_runner.js'); const BASIC_CONFIG = resolve(__dirname, '../fixtures/simple_project/config.js'); -describe('basic config file with a single app and test', function () { +describe('basic config file with a single app and test', function() { this.timeout(60 * 1000); it('runs and prints expected output', () => { diff --git a/src/functional_test_runner/__tests__/integration/failure_hooks.js b/packages/kbn-test/src/functional_test_runner/__tests__/integration/failure_hooks.js similarity index 87% rename from src/functional_test_runner/__tests__/integration/failure_hooks.js rename to packages/kbn-test/src/functional_test_runner/__tests__/integration/failure_hooks.js index 0c00e2771bb1f..08e71e2e73245 100644 --- a/src/functional_test_runner/__tests__/integration/failure_hooks.js +++ b/packages/kbn-test/src/functional_test_runner/__tests__/integration/failure_hooks.js @@ -26,7 +26,7 @@ import expect from '@kbn/expect'; const SCRIPT = resolve(__dirname, '../../../../scripts/functional_test_runner.js'); const FAILURE_HOOKS_CONFIG = resolve(__dirname, '../fixtures/failure_hooks/config.js'); -describe('failure hooks', function () { +describe('failure hooks', function() { this.timeout(60 * 1000); it('runs and prints expected output', () => { @@ -37,8 +37,10 @@ describe('failure hooks', function () { flag: '$FAILING_BEFORE_HOOK$', assert(lines) { expect(lines.shift()).to.match(/info\s+testHookFailure\s+\$FAILING_BEFORE_ERROR\$/); - expect(lines.shift()).to.match(/info\s+testHookFailureAfterDelay\s+\$FAILING_BEFORE_ERROR\$/); - } + expect(lines.shift()).to.match( + /info\s+testHookFailureAfterDelay\s+\$FAILING_BEFORE_ERROR\$/ + ); + }, }, { flag: '$FAILING_TEST$', @@ -46,14 +48,16 @@ describe('failure hooks', function () { expect(lines.shift()).to.match(/global before each/); expect(lines.shift()).to.match(/info\s+testFailure\s+\$FAILING_TEST_ERROR\$/); expect(lines.shift()).to.match(/info\s+testFailureAfterDelay\s+\$FAILING_TEST_ERROR\$/); - } + }, }, { flag: '$FAILING_AFTER_HOOK$', assert(lines) { expect(lines.shift()).to.match(/info\s+testHookFailure\s+\$FAILING_AFTER_ERROR\$/); - expect(lines.shift()).to.match(/info\s+testHookFailureAfterDelay\s+\$FAILING_AFTER_ERROR\$/); - } + expect(lines.shift()).to.match( + /info\s+testHookFailureAfterDelay\s+\$FAILING_AFTER_ERROR\$/ + ); + }, }, ]; diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts new file mode 100644 index 0000000000000..88b9f2008487a --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -0,0 +1,107 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; +import { run } from '../../../../src/dev/run'; +import { FunctionalTestRunner } from './functional_test_runner'; + +export function runFtrCli() { + run( + async ({ flags, log }) => { + const resolveConfigPath = (v: string) => resolve(process.cwd(), v); + const toArray = (v: string | string[]) => ([] as string[]).concat(v || []); + + const functionalTestRunner = new FunctionalTestRunner( + log, + resolveConfigPath(flags.config as string), + { + mochaOpts: { + bail: flags.bail, + grep: flags.grep || undefined, + invert: flags.invert, + }, + suiteTags: { + include: toArray(flags['include-tag'] as string | string[]), + exclude: toArray(flags['exclude-tag'] as string | string[]), + }, + updateBaselines: flags.updateBaselines, + excludeTestFiles: flags.exclude || undefined, + } + ); + + let teardownRun = false; + const teardown = async (err?: Error) => { + if (teardownRun) return; + + teardownRun = true; + if (err) { + log.indent(-log.indent()); + log.error(err); + process.exitCode = 1; + } + + try { + await functionalTestRunner.close(); + } finally { + process.exit(); + } + }; + + process.on('unhandledRejection', err => teardown(err)); + process.on('SIGTERM', () => teardown()); + process.on('SIGINT', () => teardown()); + + try { + if (flags['test-stats']) { + process.stderr.write( + JSON.stringify(await functionalTestRunner.getTestStats(), null, 2) + '\n' + ); + } else { + const failureCount = await functionalTestRunner.run(); + process.exitCode = failureCount ? 1 : 0; + } + } catch (err) { + await teardown(err); + } finally { + await teardown(); + } + }, + { + flags: { + string: ['config', 'grep', 'exclude', 'include-tag', 'exclude-tag'], + boolean: ['bail', 'invert', 'test-stats', 'updateBaselines'], + default: { + config: 'test/functional/config.js', + debug: true, + }, + help: ` + --config=path path to a config file + --bail stop tests after the first failure + --grep pattern used to select which tests to run + --invert invert grep to exclude tests + --exclude=file path to a test file that should not be loaded + --include-tag=tag a tag to be included, pass multiple times for multiple tags + --exclude-tag=tag a tag to be excluded, pass multiple times for multiple tags + --test-stats print the number of tests (included and excluded) to STDERR + --updateBaselines replace baseline screenshots with whatever is generated from the test + `, + }, + } + ); +} diff --git a/src/functional_test_runner/fake_mocha_types.d.ts b/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts similarity index 100% rename from src/functional_test_runner/fake_mocha_types.d.ts rename to packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts diff --git a/src/functional_test_runner/functional_test_runner.ts b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts similarity index 100% rename from src/functional_test_runner/functional_test_runner.ts rename to packages/kbn-test/src/functional_test_runner/functional_test_runner.ts diff --git a/src/functional_test_runner/index.ts b/packages/kbn-test/src/functional_test_runner/index.ts similarity index 96% rename from src/functional_test_runner/index.ts rename to packages/kbn-test/src/functional_test_runner/index.ts index 50b4c1c2a63f7..c783117a0ba42 100644 --- a/src/functional_test_runner/index.ts +++ b/packages/kbn-test/src/functional_test_runner/index.ts @@ -19,3 +19,4 @@ export { FunctionalTestRunner } from './functional_test_runner'; export { readConfigFile } from './lib'; +export { runFtrCli } from './cli'; diff --git a/src/functional_test_runner/lib/config/__tests__/fixtures/config.1.js b/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.1.js similarity index 100% rename from src/functional_test_runner/lib/config/__tests__/fixtures/config.1.js rename to packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.1.js diff --git a/src/functional_test_runner/lib/config/__tests__/fixtures/config.2.js b/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.2.js similarity index 100% rename from src/functional_test_runner/lib/config/__tests__/fixtures/config.2.js rename to packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.2.js diff --git a/src/functional_test_runner/lib/config/__tests__/fixtures/config.3.js b/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.3.js similarity index 100% rename from src/functional_test_runner/lib/config/__tests__/fixtures/config.3.js rename to packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.3.js diff --git a/src/functional_test_runner/lib/config/__tests__/fixtures/config.4.js b/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.4.js similarity index 100% rename from src/functional_test_runner/lib/config/__tests__/fixtures/config.4.js rename to packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.4.js diff --git a/src/functional_test_runner/lib/config/__tests__/fixtures/config.invalid.js b/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.invalid.js similarity index 100% rename from src/functional_test_runner/lib/config/__tests__/fixtures/config.invalid.js rename to packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/config.invalid.js diff --git a/src/functional_test_runner/lib/config/__tests__/read_config_file.js b/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/read_config_file.js similarity index 92% rename from src/functional_test_runner/lib/config/__tests__/read_config_file.js rename to packages/kbn-test/src/functional_test_runner/lib/config/__tests__/read_config_file.js index d4a08333e8143..325e972a6cd90 100644 --- a/src/functional_test_runner/lib/config/__tests__/read_config_file.js +++ b/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/read_config_file.js @@ -29,16 +29,14 @@ describe('readConfigFile()', () => { it('reads config from a file, returns an instance of Config class', async () => { const config = await readConfigFile(log, require.resolve('./fixtures/config.1')); expect(config).to.be.a(Config); - expect(config.get('testFiles')).to.eql([ - 'config.1' - ]); + expect(config.get('testFiles')).to.eql(['config.1']); }); it('merges setting overrides into log', async () => { const config = await readConfigFile(log, require.resolve('./fixtures/config.1'), { screenshots: { - directory: 'foo.bar' - } + directory: 'foo.bar', + }, }); expect(config.get('screenshots.directory')).to.be('foo.bar'); @@ -46,10 +44,7 @@ describe('readConfigFile()', () => { it('supports loading config files from within config files', async () => { const config = await readConfigFile(log, require.resolve('./fixtures/config.2')); - expect(config.get('testFiles')).to.eql([ - 'config.1', - 'config.2', - ]); + expect(config.get('testFiles')).to.eql(['config.1', 'config.2']); }); it('throws if settings are invalid', async () => { diff --git a/src/functional_test_runner/lib/config/config.ts b/packages/kbn-test/src/functional_test_runner/lib/config/config.ts similarity index 100% rename from src/functional_test_runner/lib/config/config.ts rename to packages/kbn-test/src/functional_test_runner/lib/config/config.ts diff --git a/src/functional_test_runner/lib/config/index.ts b/packages/kbn-test/src/functional_test_runner/lib/config/index.ts similarity index 100% rename from src/functional_test_runner/lib/config/index.ts rename to packages/kbn-test/src/functional_test_runner/lib/config/index.ts diff --git a/src/functional_test_runner/lib/config/read_config_file.ts b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts similarity index 100% rename from src/functional_test_runner/lib/config/read_config_file.ts rename to packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts diff --git a/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts similarity index 100% rename from src/functional_test_runner/lib/config/schema.ts rename to packages/kbn-test/src/functional_test_runner/lib/config/schema.ts diff --git a/src/functional_test_runner/lib/config/transform_deprecations.ts b/packages/kbn-test/src/functional_test_runner/lib/config/transform_deprecations.ts similarity index 92% rename from src/functional_test_runner/lib/config/transform_deprecations.ts rename to packages/kbn-test/src/functional_test_runner/lib/config/transform_deprecations.ts index 1b8538529b26f..08dfc4a4f61f4 100644 --- a/src/functional_test_runner/lib/config/transform_deprecations.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/transform_deprecations.ts @@ -18,7 +18,7 @@ */ // @ts-ignore -import { createTransform, Deprecations } from '../../../legacy/deprecation'; +import { createTransform, Deprecations } from '../../../../../../src/legacy/deprecation'; type DeprecationTransformer = ( settings: object, diff --git a/src/functional_test_runner/lib/index.ts b/packages/kbn-test/src/functional_test_runner/lib/index.ts similarity index 100% rename from src/functional_test_runner/lib/index.ts rename to packages/kbn-test/src/functional_test_runner/lib/index.ts diff --git a/src/functional_test_runner/lib/lifecycle.ts b/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts similarity index 100% rename from src/functional_test_runner/lib/lifecycle.ts rename to packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts diff --git a/src/functional_test_runner/lib/load_tracer.ts b/packages/kbn-test/src/functional_test_runner/lib/load_tracer.ts similarity index 100% rename from src/functional_test_runner/lib/load_tracer.ts rename to packages/kbn-test/src/functional_test_runner/lib/load_tracer.ts diff --git a/src/functional_test_runner/lib/mocha/assignment_proxy.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/assignment_proxy.js similarity index 97% rename from src/functional_test_runner/lib/mocha/assignment_proxy.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/assignment_proxy.js index 70f8babfb29e5..5c08d566d3d73 100644 --- a/src/functional_test_runner/lib/mocha/assignment_proxy.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/assignment_proxy.js @@ -31,7 +31,7 @@ export function createAssignmentProxy(object, interceptor) { get(target, property) { if (property === 'revertProxiedAssignments') { - return function () { + return function() { for (const [property, value] of originalValues) { object[property] = value; } @@ -39,6 +39,6 @@ export function createAssignmentProxy(object, interceptor) { } return Reflect.get(target, property); - } + }, }); } diff --git a/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js similarity index 91% rename from src/functional_test_runner/lib/mocha/decorate_mocha_ui.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js index a8272ae677179..e65eb2f27d063 100644 --- a/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js @@ -56,12 +56,12 @@ export function decorateMochaUi(lifecycle, context) { throw new Error(`Unexpected arguments to ${name}(${argumentsList.join(', ')})`); } - argumentsList[1] = function () { + argumentsList[1] = function() { before(async () => { await lifecycle.trigger('beforeTestSuite', this); }); - this.tags = (tags) => { + this.tags = tags => { this._tags = [].concat(this._tags || [], tags); }; @@ -77,7 +77,7 @@ export function decorateMochaUi(lifecycle, context) { }, after() { suiteLevel -= 1; - } + }, }); } @@ -91,9 +91,12 @@ export function decorateMochaUi(lifecycle, context) { * @return {Function} */ function wrapTestFunction(name, fn) { - return wrapNonSuiteFunction(name, wrapRunnableArgsWithErrorHandler(fn, async (err, test) => { - await lifecycle.trigger('testFailure', err, test); - })); + return wrapNonSuiteFunction( + name, + wrapRunnableArgsWithErrorHandler(fn, async (err, test) => { + await lifecycle.trigger('testFailure', err, test); + }) + ); } /** @@ -106,9 +109,12 @@ export function decorateMochaUi(lifecycle, context) { * @return {Function} */ function wrapTestHookFunction(name, fn) { - return wrapNonSuiteFunction(name, wrapRunnableArgsWithErrorHandler(fn, async (err, test) => { - await lifecycle.trigger('testHookFailure', err, test); - })); + return wrapNonSuiteFunction( + name, + wrapRunnableArgsWithErrorHandler(fn, async (err, test) => { + await lifecycle.trigger('testHookFailure', err, test); + }) + ); } /** @@ -127,7 +133,7 @@ export function decorateMochaUi(lifecycle, context) { All ${name}() calls in test files must be within a describe() call. `); } - } + }, }); } diff --git a/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js similarity index 92% rename from src/functional_test_runner/lib/mocha/filter_suites_by_tags.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js index 597be9bab32a2..302d43fac3e61 100644 --- a/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js @@ -30,18 +30,15 @@ export function filterSuitesByTags({ log, mocha, include, exclude }) { mocha.excludedTests = []; // collect all the tests from some suite, including it's children - const collectTests = (suite) => - suite.suites.reduce( - (acc, s) => acc.concat(collectTests(s)), - suite.tests - ); + const collectTests = suite => + suite.suites.reduce((acc, s) => acc.concat(collectTests(s)), suite.tests); // if include tags were provided, filter the tree once to // only include branches that are included at some point if (include.length) { log.info('Only running suites (and their sub-suites) if they include the tag(s):', include); - const isIncluded = suite => !suite._tags ? false : suite._tags.some(t => include.includes(t)); + const isIncluded = suite => (!suite._tags ? false : suite._tags.some(t => include.includes(t))); const isChildIncluded = suite => suite.suites.some(s => isIncluded(s) || isChildIncluded(s)); (function recurse(parentSuite) { @@ -55,7 +52,6 @@ export function filterSuitesByTags({ log, mocha, include, exclude }) { continue; } - // this suite has an included child but is not included // itself, so strip out its tests and recurse to filter // out child suites which are not included @@ -69,10 +65,9 @@ export function filterSuitesByTags({ log, mocha, include, exclude }) { mocha.excludedTests = mocha.excludedTests.concat(collectTests(child)); } } - }(mocha.suite)); + })(mocha.suite); } - // if exclude tags were provided, filter the possibly already // filtered tree to remove branches that are excluded if (exclude.length) { @@ -94,6 +89,6 @@ export function filterSuitesByTags({ log, mocha, include, exclude }) { mocha.excludedTests = mocha.excludedTests.concat(collectTests(child)); } } - }(mocha.suite)); + })(mocha.suite); } } diff --git a/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js similarity index 100% rename from src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js diff --git a/src/functional_test_runner/lib/mocha/index.ts b/packages/kbn-test/src/functional_test_runner/lib/mocha/index.ts similarity index 100% rename from src/functional_test_runner/lib/mocha/index.ts rename to packages/kbn-test/src/functional_test_runner/lib/mocha/index.ts diff --git a/src/functional_test_runner/lib/mocha/load_test_files.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js similarity index 87% rename from src/functional_test_runner/lib/mocha/load_test_files.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js index c876c39b9c64f..70b0c0874e5e9 100644 --- a/src/functional_test_runner/lib/mocha/load_test_files.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/load_test_files.js @@ -31,10 +31,18 @@ import { decorateMochaUi } from './decorate_mocha_ui'; * @param {String} path * @return {undefined} - mutates mocha, no return value */ -export const loadTestFiles = ({ mocha, log, lifecycle, providers, paths, excludePaths, updateBaselines }) => { +export const loadTestFiles = ({ + mocha, + log, + lifecycle, + providers, + paths, + excludePaths, + updateBaselines, +}) => { const pendingExcludes = new Set(excludePaths.slice(0)); - const innerLoadTestFile = (path) => { + const innerLoadTestFile = path => { if (typeof path !== 'string' || !isAbsolute(path)) { throw new TypeError('loadTestFile() only accepts absolute paths'); } @@ -49,9 +57,7 @@ export const loadTestFiles = ({ mocha, log, lifecycle, providers, paths, exclude log.verbose('Loading test file %s', path); const testModule = require(path); // eslint-disable-line import/no-dynamic-require - const testProvider = testModule.__esModule - ? testModule.default - : testModule; + const testProvider = testModule.__esModule ? testModule.default : testModule; runTestProvider(testProvider, path); // eslint-disable-line }); @@ -90,6 +96,11 @@ export const loadTestFiles = ({ mocha, log, lifecycle, providers, paths, exclude paths.forEach(innerLoadTestFile); if (pendingExcludes.size) { - throw new Error(`After loading all test files some exclude paths were not consumed:${['', ...pendingExcludes].join('\n -')}`); + throw new Error( + `After loading all test files some exclude paths were not consumed:${[ + '', + ...pendingExcludes, + ].join('\n -')}` + ); } }; diff --git a/src/functional_test_runner/lib/mocha/reporter/colors.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/colors.js similarity index 100% rename from src/functional_test_runner/lib/mocha/reporter/colors.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/colors.js diff --git a/src/functional_test_runner/lib/mocha/reporter/index.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/index.js similarity index 100% rename from src/functional_test_runner/lib/mocha/reporter/index.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/index.js diff --git a/src/functional_test_runner/lib/mocha/reporter/ms.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ms.js similarity index 100% rename from src/functional_test_runner/lib/mocha/reporter/ms.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ms.js diff --git a/src/functional_test_runner/lib/mocha/reporter/reporter.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js similarity index 83% rename from src/functional_test_runner/lib/mocha/reporter/reporter.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js index 386411631c155..969ccff93f3be 100644 --- a/src/functional_test_runner/lib/mocha/reporter/reporter.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js @@ -23,12 +23,12 @@ import Mocha from 'mocha'; import { ToolingLogTextWriter } from '@kbn/dev-utils'; import moment from 'moment'; -import { setupJUnitReportGeneration } from '../../../../dev'; +import { setupJUnitReportGeneration } from '../../../../../../../src/dev'; import * as colors from './colors'; import * as symbols from './symbols'; import { ms } from './ms'; import { writeEpilogue } from './write_epilogue'; -import { recordLog, snapshotLogsForRunnable } from '../../../../dev/mocha/log_cache'; +import { recordLog, snapshotLogsForRunnable } from '../../../../../../../src/dev/mocha/log_cache'; export function MochaReporterProvider({ getService }) { const log = getService('log'); @@ -60,7 +60,9 @@ export function MochaReporterProvider({ getService }) { onStart = () => { if (config.get('mochaReporter.captureLogOutput')) { - log.warning('debug logs are being captured, only error logs will be written to the console'); + log.warning( + 'debug logs are being captured, only error logs will be written to the console' + ); reporterCaptureStartTime = moment(); originalLogWriters = log.getWriters(); @@ -68,47 +70,47 @@ export function MochaReporterProvider({ getService }) { log.setWriters([ new ToolingLogTextWriter({ level: 'error', - writeTo: process.stdout + writeTo: process.stdout, }), new ToolingLogTextWriter({ level: 'debug', writeTo: { - write: (line) => { + write: line => { // if the current runnable is a beforeEach hook then // `runner.suite` is set to the suite that defined the // hook, rather than the suite executing, so instead we // grab the suite from the test, but that's only available // when we are doing something test specific, so for global // hooks we fallback to `runner.suite` - const currentSuite = this.runner.test - ? this.runner.test.parent - : this.runner.suite; + const currentSuite = this.runner.test ? this.runner.test.parent : this.runner.suite; // We are computing the difference between the time when this // reporter has started and the time when each line are being // logged in order to be able to label the test results log lines // with this relative time information const diffTimeSinceStart = moment().diff(reporterCaptureStartTime); - const readableDiffTimeSinceStart = `[${moment(diffTimeSinceStart).format('HH:mm:ss')}] `; + const readableDiffTimeSinceStart = `[${moment(diffTimeSinceStart).format( + 'HH:mm:ss' + )}] `; recordLog(currentSuite, `${readableDiffTimeSinceStart} ${line}`); - } - } - }) + }, + }, + }), ]); } log.write(''); - } + }; onHookStart = hook => { log.write(`-> ${colors.suite(hook.title)}`); log.indent(2); - } + }; onHookEnd = () => { log.indent(-2); - } + }; onSuiteStart = suite => { if (!suite.root) { @@ -116,34 +118,34 @@ export function MochaReporterProvider({ getService }) { } log.indent(2); - } + }; onSuiteEnd = () => { if (log.indent(-2) === 0) { log.write(); } - } + }; onTestStart = test => { log.write(`-> ${test.title}`); log.indent(2); - } + }; - onTestEnd = (test) => { + onTestEnd = test => { snapshotLogsForRunnable(test); log.indent(-2); - } + }; onPending = test => { log.write('-> ' + colors.pending(test.title)); log.indent(2); - } + }; onPass = test => { const time = colors.speed(test.speed, ` (${ms(test.duration)})`); const pass = colors.pass(`${symbols.ok} pass`); log.write(`- ${pass} ${time} "${test.fullTitle()}"`); - } + }; onFail = runnable => { // NOTE: this is super gross @@ -155,7 +157,7 @@ export function MochaReporterProvider({ getService }) { // let output = ''; const realLog = console.log; - console.log = (...args) => output += `${format(...args)}\n`; + console.log = (...args) => (output += `${format(...args)}\n`); try { Mocha.reporters.Base.list([runnable]); } finally { @@ -164,21 +166,21 @@ export function MochaReporterProvider({ getService }) { log.write( `- ${colors.fail(`${symbols.err} fail: "${runnable.fullTitle()}"`)}` + - '\n' + - output - .split('\n') - // drop the first two lines, (empty + test title) - .slice(2) - // move leading colors behind leading spaces - .map(line => line.replace(/^((?:\[.+m)+)(\s+)/, '$2$1')) - .map(line => ` ${line}`) - .join('\n') + '\n' + + output + .split('\n') + // drop the first two lines, (empty + test title) + .slice(2) + // move leading colors behind leading spaces + .map(line => line.replace(/^((?:\[.+m)+)(\s+)/, '$2$1')) + .map(line => ` ${line}`) + .join('\n') ); // failed hooks trigger the `onFail(runnable)` callback, so we snapshot the logs for // them here. Tests will re-capture the snapshot in `onTestEnd()` snapshotLogsForRunnable(runnable); - } + }; onEnd = () => { if (originalLogWriters) { @@ -186,6 +188,6 @@ export function MochaReporterProvider({ getService }) { } writeEpilogue(log, this.stats); - } + }; }; } diff --git a/src/functional_test_runner/lib/mocha/reporter/symbols.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/symbols.js similarity index 85% rename from src/functional_test_runner/lib/mocha/reporter/symbols.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/symbols.js index 4f69229f0f1a4..9819343ff953c 100644 --- a/src/functional_test_runner/lib/mocha/reporter/symbols.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/symbols.js @@ -19,10 +19,6 @@ // originally extracted from mocha https://git.io/v1PGh -export const ok = process.platform === 'win32' - ? '\u221A' - : '✓'; +export const ok = process.platform === 'win32' ? '\u221A' : '✓'; -export const err = process.platform === 'win32' - ? '\u00D7' - : '✖'; +export const err = process.platform === 'win32' ? '\u00D7' : '✖'; diff --git a/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js similarity index 81% rename from src/functional_test_runner/lib/mocha/reporter/write_epilogue.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js index 09964de3e8a3f..003827120e930 100644 --- a/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js @@ -25,26 +25,16 @@ export function writeEpilogue(log, stats) { log.write(); // passes - log.write( - `${colors.pass('%d passing')} (%s)`, - stats.passes || 0, - ms(stats.duration) - ); + log.write(`${colors.pass('%d passing')} (%s)`, stats.passes || 0, ms(stats.duration)); // pending if (stats.pending) { - log.write( - colors.pending('%d pending'), - stats.pending - ); + log.write(colors.pending('%d pending'), stats.pending); } // failures if (stats.failures) { - log.write( - '%d failing', - stats.failures - ); + log.write('%d failing', stats.failures); } // footer diff --git a/src/functional_test_runner/lib/mocha/run_tests.ts b/packages/kbn-test/src/functional_test_runner/lib/mocha/run_tests.ts similarity index 100% rename from src/functional_test_runner/lib/mocha/run_tests.ts rename to packages/kbn-test/src/functional_test_runner/lib/mocha/run_tests.ts diff --git a/src/functional_test_runner/lib/mocha/setup_mocha.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js similarity index 91% rename from src/functional_test_runner/lib/mocha/setup_mocha.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js index c39db58cf9a5d..6746914fe0318 100644 --- a/src/functional_test_runner/lib/mocha/setup_mocha.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js @@ -36,14 +36,11 @@ export async function setupMocha(lifecycle, log, config, providers) { // configure mocha const mocha = new Mocha({ ...config.get('mochaOpts'), - reporter: await providers.loadExternalService( - 'mocha reporter', - MochaReporterProvider - ) + reporter: await providers.loadExternalService('mocha reporter', MochaReporterProvider), }); // global beforeEach hook in root suite triggers before all others - mocha.suite.beforeEach('global before each', async function () { + mocha.suite.beforeEach('global before each', async function() { await lifecycle.trigger('beforeEachTest', this.currentTest); }); diff --git a/src/functional_test_runner/lib/mocha/wrap_function.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/wrap_function.js similarity index 99% rename from src/functional_test_runner/lib/mocha/wrap_function.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/wrap_function.js index b9fe0be4c1aff..eac5200a41747 100644 --- a/src/functional_test_runner/lib/mocha/wrap_function.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/wrap_function.js @@ -17,7 +17,6 @@ * under the License. */ - /** * Get handler that will intercept calls to `toString` * on the function, since Function.prototype.toString() @@ -59,7 +58,7 @@ export function wrapFunction(fn, hooks = {}) { hooks.after(target, thisArg, argumentsList); } } - } + }, }); } @@ -94,6 +93,6 @@ export function wrapAsyncFunction(fn, hooks = {}) { await hooks.after(target, thisArg, argumentsList); } } - } + }, }); } diff --git a/src/functional_test_runner/lib/mocha/wrap_runnable_args.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/wrap_runnable_args.js similarity index 99% rename from src/functional_test_runner/lib/mocha/wrap_runnable_args.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/wrap_runnable_args.js index 39c956ea22aa2..5ee21e81e83cc 100644 --- a/src/functional_test_runner/lib/mocha/wrap_runnable_args.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/wrap_runnable_args.js @@ -36,7 +36,7 @@ export function wrapRunnableArgsWithErrorHandler(fn, handler) { argumentsList[i] = wrapRunnableError(argumentsList[i], handler); } } - } + }, }); } @@ -45,6 +45,6 @@ function wrapRunnableError(runnable, handler) { async handleError(target, thisArg, argumentsList, err) { await handler(err, thisArg.test); throw err; - } + }, }); } diff --git a/src/functional_test_runner/lib/providers/async_instance.ts b/packages/kbn-test/src/functional_test_runner/lib/providers/async_instance.ts similarity index 100% rename from src/functional_test_runner/lib/providers/async_instance.ts rename to packages/kbn-test/src/functional_test_runner/lib/providers/async_instance.ts diff --git a/src/functional_test_runner/lib/providers/index.ts b/packages/kbn-test/src/functional_test_runner/lib/providers/index.ts similarity index 100% rename from src/functional_test_runner/lib/providers/index.ts rename to packages/kbn-test/src/functional_test_runner/lib/providers/index.ts diff --git a/src/functional_test_runner/lib/providers/provider_collection.ts b/packages/kbn-test/src/functional_test_runner/lib/providers/provider_collection.ts similarity index 100% rename from src/functional_test_runner/lib/providers/provider_collection.ts rename to packages/kbn-test/src/functional_test_runner/lib/providers/provider_collection.ts diff --git a/src/functional_test_runner/lib/providers/read_provider_spec.ts b/packages/kbn-test/src/functional_test_runner/lib/providers/read_provider_spec.ts similarity index 100% rename from src/functional_test_runner/lib/providers/read_provider_spec.ts rename to packages/kbn-test/src/functional_test_runner/lib/providers/read_provider_spec.ts diff --git a/src/functional_test_runner/lib/providers/verbose_instance.ts b/packages/kbn-test/src/functional_test_runner/lib/providers/verbose_instance.ts similarity index 100% rename from src/functional_test_runner/lib/providers/verbose_instance.ts rename to packages/kbn-test/src/functional_test_runner/lib/providers/verbose_instance.ts diff --git a/packages/kbn-test/src/functional_tests/lib/run_ftr.js b/packages/kbn-test/src/functional_tests/lib/run_ftr.js index 8b347d9dc595f..a0edfcdb8c7b5 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_ftr.js +++ b/packages/kbn-test/src/functional_tests/lib/run_ftr.js @@ -17,7 +17,7 @@ * under the License. */ -import { FunctionalTestRunner, readConfigFile } from '../../../../../src/functional_test_runner'; +import { FunctionalTestRunner, readConfigFile } from '../../functional_test_runner'; import { CliError } from './run_cli'; async function createFtr({ configPath, options: { log, bail, grep, updateBaselines, suiteTags } }) { diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index 76afe15e9c0ae..966b148f0ce6d 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -31,7 +31,7 @@ import { KIBANA_FTR_SCRIPT, } from './lib'; -import { readConfigFile } from '../../../../src/functional_test_runner/lib'; +import { readConfigFile } from '../functional_test_runner/lib'; const SUCCESS_MESSAGE = ` diff --git a/packages/kbn-test/src/index.js b/packages/kbn-test/src/index.js index add8b470b1c84..e8cc694f5252e 100644 --- a/packages/kbn-test/src/index.js +++ b/packages/kbn-test/src/index.js @@ -28,3 +28,7 @@ export { esTestConfig, createEsTestCluster } from './es'; export { kbnTestConfig, kibanaServerTestUser, kibanaTestUser, adminTestUser } from './kbn'; export { setupUsers, DEFAULT_SUPERUSER_PASS } from './functional_tests/lib/auth'; + +export { readConfigFile } from './functional_test_runner/lib/config/read_config_file'; + +export { runFtrCli } from './functional_test_runner/cli'; diff --git a/packages/kbn-test/tsconfig.json b/packages/kbn-test/tsconfig.json index 8d42818502289..83a0fe04a4b5f 100644 --- a/packages/kbn-test/tsconfig.json +++ b/packages/kbn-test/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "include": [ - "types/**/*" + "types/**/*", + "src/functional_test_runner/**/*" ] } diff --git a/packages/kbn-test/types/ftr.d.ts b/packages/kbn-test/types/ftr.d.ts index d85363892c99f..8289877ed1b98 100644 --- a/packages/kbn-test/types/ftr.d.ts +++ b/packages/kbn-test/types/ftr.d.ts @@ -18,7 +18,7 @@ */ import { ToolingLog } from '@kbn/dev-utils'; -import { Config, Lifecycle } from '../../../src/functional_test_runner/lib'; +import { Config, Lifecycle } from '../src/functional_test_runner/lib'; interface AsyncInstance { /** diff --git a/scripts/functional_test_runner.js b/scripts/functional_test_runner.js index 6b298e9b30317..e0ed92c958ddc 100644 --- a/scripts/functional_test_runner.js +++ b/scripts/functional_test_runner.js @@ -18,4 +18,4 @@ */ require('../src/setup_node_env'); -require('../src/functional_test_runner/cli'); +require('@kbn/test').runFtrCli(); diff --git a/scripts/functional_tests_server.js b/scripts/functional_tests_server.js index 156da2dfbbe1b..53f6d0f67f256 100644 --- a/scripts/functional_tests_server.js +++ b/scripts/functional_tests_server.js @@ -18,6 +18,6 @@ */ require('../src/setup_node_env'); -require('../packages/kbn-test').startServersCli( +require('@kbn/test').startServersCli( require.resolve('../test/functional/config.js'), ); diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index cd800f264957b..e694e9142126e 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -30,7 +30,7 @@ export default { '/src/cli', '/src/cli_keystore', '/src/cli_plugin', - '/src/functional_test_runner', + '/packages/kbn-test/target/functional_test_runner', '/src/dev', '/src/legacy/utils', '/src/setup_node_env', diff --git a/src/es_archiver/cli.js b/src/es_archiver/cli.js index f12e452edf88d..f37b6afdc4ba1 100644 --- a/src/es_archiver/cli.js +++ b/src/es_archiver/cli.js @@ -33,7 +33,7 @@ import elasticsearch from 'elasticsearch'; import { EsArchiver } from './es_archiver'; import { ToolingLog } from '@kbn/dev-utils'; -import { readConfigFile } from '../functional_test_runner'; +import { readConfigFile } from '@kbn/test'; const cmd = new Command('node scripts/es_archiver'); diff --git a/src/functional_test_runner/cli.ts b/src/functional_test_runner/cli.ts deleted file mode 100644 index 17bde6a50cbf1..0000000000000 --- a/src/functional_test_runner/cli.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; - -import { run } from '../dev/run'; -import { FunctionalTestRunner } from './functional_test_runner'; - -run( - async ({ flags, log }) => { - const resolveConfigPath = (v: string) => resolve(process.cwd(), v); - const toArray = (v: string | string[]) => ([] as string[]).concat(v || []); - - const functionalTestRunner = new FunctionalTestRunner( - log, - resolveConfigPath(flags.config as string), - { - mochaOpts: { - bail: flags.bail, - grep: flags.grep || undefined, - invert: flags.invert, - }, - suiteTags: { - include: toArray(flags['include-tag'] as string | string[]), - exclude: toArray(flags['exclude-tag'] as string | string[]), - }, - updateBaselines: flags.updateBaselines, - excludeTestFiles: flags.exclude || undefined, - } - ); - - let teardownRun = false; - const teardown = async (err?: Error) => { - if (teardownRun) return; - - teardownRun = true; - if (err) { - log.indent(-log.indent()); - log.error(err); - process.exitCode = 1; - } - - try { - await functionalTestRunner.close(); - } finally { - process.exit(); - } - }; - - process.on('unhandledRejection', err => teardown(err)); - process.on('SIGTERM', () => teardown()); - process.on('SIGINT', () => teardown()); - - try { - if (flags['test-stats']) { - process.stderr.write( - JSON.stringify(await functionalTestRunner.getTestStats(), null, 2) + '\n' - ); - } else { - const failureCount = await functionalTestRunner.run(); - process.exitCode = failureCount ? 1 : 0; - } - } catch (err) { - await teardown(err); - } finally { - await teardown(); - } - }, - { - flags: { - string: ['config', 'grep', 'exclude', 'include-tag', 'exclude-tag'], - boolean: ['bail', 'invert', 'test-stats', 'updateBaselines'], - default: { - config: 'test/functional/config.js', - debug: true, - }, - help: ` - --config=path path to a config file - --bail stop tests after the first failure - --grep pattern used to select which tests to run - --invert invert grep to exclude tests - --exclude=file path to a test file that should not be loaded - --include-tag=tag a tag to be included, pass multiple times for multiple tags - --exclude-tag=tag a tag to be excluded, pass multiple times for multiple tags - --test-stats print the number of tests (included and excluded) to STDERR - --updateBaselines replace baseline screenshots with whatever is generated from the test - `, - }, - } -); From 1b477672c26b039dbecb9c9a24241e33f4d93936 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Tue, 6 Aug 2019 14:47:45 -0700 Subject: [PATCH 21/26] [Canvas] Format argument for the metric element (#42007) * Added numberFormat arg type and metricFormat arg to metric function Updated function reference doc Fixed default arg value * Added stories for NumberFormatArgInput * Added arg alias * Added stories for metric renderer * Fixed ts errors and added comments * Updated comments * Removed extra test * Added snapshots * Addressing feedback * Fixed typo * Updated metricFormat help text * Removed redundant help prop --- .../canvas/canvas-function-reference.asciidoc | 14 +- .../elements/metric/index.ts | 2 + .../functions/common/metric.test.js | 14 +- .../functions/common/metric.ts | 19 +- .../__snapshots__/metric.examples.storyshot | 263 ++++++++++++++++++ .../__examples__/metric.examples.tsx | 84 ++++++ .../renderers/metric/component/index.ts | 7 + .../renderers/metric/component/metric.tsx | 40 +++ .../renderers/metric/index.js | 34 --- .../renderers/metric/index.tsx | 45 +++ .../strings/functions/metric.ts | 20 +- .../canvas_plugin_src/uis/arguments/index.js | 2 + .../number_format.examples.storyshot | 242 ++++++++++++++++ .../__examples__/number_format.examples.tsx | 43 +++ .../uis/arguments/number_format/index.ts | 38 +++ .../arguments/number_format/number_format.tsx | 73 +++++ .../canvas_plugin_src/uis/views/metric.js | 17 +- .../public/lib/kibana_advanced_settings.ts | 9 + .../legacy/plugins/canvas/types/renderers.ts | 17 +- 19 files changed, 925 insertions(+), 58 deletions(-) create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/__snapshots__/metric.examples.storyshot create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/metric.examples.tsx create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/index.ts create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/metric.tsx delete mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/index.js create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/index.tsx create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.examples.storyshot create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/number_format.examples.tsx create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/number_format.tsx create mode 100644 x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.ts diff --git a/docs/canvas/canvas-function-reference.asciidoc b/docs/canvas/canvas-function-reference.asciidoc index 8cc7f64adc3a5..df4b17e905772 100644 --- a/docs/canvas/canvas-function-reference.asciidoc +++ b/docs/canvas/canvas-function-reference.asciidoc @@ -1209,17 +1209,23 @@ Aliases: `label`, `text`, `description` Default: `""` +|`labelFont` +|`style` +|The CSS font properties for the label. For example, `font-family` or `font-weight`. + +Default: `{font size=14 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center}`. + |`metricFont` |`style` |The CSS font properties for the metric. For example, `font-family` or `font-weight`. Default: `{font size=48 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center lHeight=48}`. -|`labelFont` -|`style` -|The CSS font properties for the label. For example, `font-family` or `font-weight`. +|`metricFormat` -Default: `{font size=14 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center}`. +Alias: `format` +|`string` +|A NumeralJS format string. For example, `"0.0a"` or `"0%"`. See http://numeraljs.com/#format. |=== *Returns:* `render` diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts index acbd1b7c6b1ab..def16f2a4b23a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts @@ -6,6 +6,7 @@ import { openSans } from '../../../common/lib/fonts'; import header from './header.png'; +import { AdvancedSettings } from '../../../public/lib/kibana_advanced_settings'; import { ElementFactory } from '../../../types'; export const metric: ElementFactory = () => ({ @@ -22,5 +23,6 @@ export const metric: ElementFactory = () => ({ | metric "Countries" metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} + metricFormat="${AdvancedSettings.get('format:number:defaultPattern')}" | render`, }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.test.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.test.js index bed30182a2a2a..50a952d836251 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.test.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.test.js @@ -38,7 +38,7 @@ describe('metric', () => { }); }); - describe('metricStyle', () => { + describe('metricFont', () => { it('sets the font style for the metric', () => { const result = fn(null, { metricFont: fontStyle, @@ -51,7 +51,7 @@ describe('metric', () => { // it("sets a default style for the metric when not provided, () => {}); }); - describe('labelStyle', () => { + describe('labelFont', () => { it('sets the font style for the label', () => { const result = fn(null, { labelFont: fontStyle, @@ -63,5 +63,15 @@ describe('metric', () => { // TODO: write test when using an instance of the interpreter // it("sets a default style for the label when not provided, () => {}); }); + + describe('metricFormat', () => { + it('sets the number format of the metric value', () => { + const result = fn(null, { + metricFormat: '0.0%', + }); + + expect(result.value).toHaveProperty('metricFormat', '0.0%'); + }); + }); }); }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.ts index f27749dbe9fc0..02d212f12fdef 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/metric.ts @@ -14,6 +14,7 @@ type Context = number | string | null; interface Arguments { label: string; metricFont: Style; + metricFormat: string; labelFont: Style; } @@ -35,26 +36,32 @@ export function metric(): ExpressionFunction<'metric', Context, Arguments, Rende help: argHelp.label, default: '""', }, + labelFont: { + types: ['style'], + help: argHelp.labelFont, + default: `{font size=14 family="${openSans.value}" color="#000000" align=center}`, + }, metricFont: { types: ['style'], help: argHelp.metricFont, default: `{font size=48 family="${openSans.value}" color="#000000" align=center lHeight=48}`, }, - labelFont: { - types: ['style'], - help: argHelp.labelFont, - default: `{font size=14 family="${openSans.value}" color="#000000" align=center}`, + metricFormat: { + types: ['string'], + aliases: ['format'], + help: argHelp.metricFormat, }, }, - fn: (context, { label, metricFont, labelFont }) => { + fn: (context, { label, labelFont, metricFont, metricFormat }) => { return { type: 'render', as: 'metric', value: { metric: context === null ? '?' : context, label, - metricFont, labelFont, + metricFont, + metricFormat, }, }; }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/__snapshots__/metric.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/__snapshots__/metric.examples.storyshot new file mode 100644 index 0000000000000..22b235bcd7979 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/__snapshots__/metric.examples.storyshot @@ -0,0 +1,263 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/Metric with formatted string metric and a specified format 1`] = ` +

+`; + +exports[`Storyshots renderers/Metric with invalid metricFont 1`] = ` +
+
+
+ $10m +
+
+ Total Revenue +
+
+
+`; + +exports[`Storyshots renderers/Metric with label 1`] = ` +
+
+
+ $12.34 +
+
+ Average price +
+
+
+`; + +exports[`Storyshots renderers/Metric with null metric 1`] = ` +
+
+
+
+
+`; + +exports[`Storyshots renderers/Metric with number metric 1`] = ` +
+
+
+ 12345.6789 +
+
+
+`; + +exports[`Storyshots renderers/Metric with number metric and a specified format 1`] = ` +
+
+
+ -0.24% +
+
+
+`; + +exports[`Storyshots renderers/Metric with string metric 1`] = ` +
+
+
+ $12.34 +
+
+
+`; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/metric.examples.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/metric.examples.tsx new file mode 100644 index 0000000000000..3ab6363ea7dd8 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/__examples__/metric.examples.tsx @@ -0,0 +1,84 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import React, { CSSProperties } from 'react'; +import { Metric } from '../metric'; + +const labelFontSpec: CSSProperties = { + fontFamily: "Baskerville, Georgia, Garamond, 'Times New Roman', Times, serif", + fontWeight: 'normal', + fontStyle: 'italic', + textDecoration: 'none', + textAlign: 'center', + fontSize: '24px', + lineHeight: '1', + color: '#000000', +}; + +const metricFontSpec: CSSProperties = { + fontFamily: + "Optima, 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, Arial, sans-serif", + fontWeight: 'bold', + fontStyle: 'normal', + textDecoration: 'none', + textAlign: 'center', + fontSize: '48px', + lineHeight: '1', + color: '#b83c6f', +}; + +storiesOf('renderers/Metric', module) + .addDecorator(story => ( +
+ {story()} +
+ )) + .add('with null metric', () => ) + .add('with number metric', () => ( + + )) + .add('with string metric', () => ( + + )) + .add('with label', () => ( + + )) + .add('with number metric and a specified format', () => ( + + )) + .add('with formatted string metric and a specified format', () => ( + + )) + .add('with invalid metricFont', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/index.ts new file mode 100644 index 0000000000000..d830090249d5f --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Metric } from './metric'; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/metric.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/metric.tsx new file mode 100644 index 0000000000000..8d2df8bf2233a --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/component/metric.tsx @@ -0,0 +1,40 @@ +/* + * 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, { FunctionComponent, CSSProperties } from 'react'; +import numeral from '@elastic/numeral'; + +interface Props { + /** The text to display under the metric */ + label?: string; + /** CSS font properties for the label */ + labelFont: CSSProperties; + /** Value of the metric to display */ + metric: string | number | null; + /** CSS font properties for the metric */ + metricFont: CSSProperties; + /** NumeralJS format string */ + metricFormat?: string; +} + +export const Metric: FunctionComponent = ({ + label, + metric, + labelFont, + metricFont, + metricFormat, +}) => ( +
+
+ {metricFormat ? numeral(metric).format(metricFormat) : metric} +
+ {label && ( +
+ {label} +
+ )} +
+); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/index.js deleted file mode 100644 index 688b183f9a61b..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/index.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -export const metric = () => ({ - name: 'metric', - displayName: 'Metric', - help: 'Render HTML Markup for the Metric element', - reuseDomNode: true, - render(domNode, config, handlers) { - const metricFontStyle = config.metricFont ? config.metricFont.spec : {}; - const labelFontStyle = config.labelFont ? config.labelFont.spec : {}; - - ReactDOM.render( -
-
- {config.metric} -
-
- {config.label} -
-
, - domNode, - () => handlers.done() - ); - - handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); - }, -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/index.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/index.tsx new file mode 100644 index 0000000000000..dd4d36aa72294 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/metric/index.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { CSSProperties } from 'react'; +import ReactDOM from 'react-dom'; +import { RendererFactory, Style } from '../../../types'; +import { Metric } from './component/metric'; + +export interface Config { + /** The text to display under the metric */ + label: string; + /** Font settings for the label */ + labelFont: Style; + /** Value of the metric to display */ + metric: string | number | null; + /** Font settings for the metric */ + metricFont: Style; + /** NumeralJS format string */ + metricFormat: string; +} + +export const metric: RendererFactory = () => ({ + name: 'metric', + displayName: 'Metric', + help: 'Render HTML Markup for the Metric element', + reuseDomNode: true, + render(domNode, config, handlers) { + ReactDOM.render( + , + domNode, + () => handlers.done() + ); + + handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); + }, +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/metric.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/metric.ts index 8e92cd9efa9bd..77bf71800d38a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/metric.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/metric.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { metric } from '../../functions/common/metric'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { FONT_FAMILY, FONT_WEIGHT, CSS } from '../constants'; +import { FONT_FAMILY, FONT_WEIGHT, CSS, NUMERALJS } from '../constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.metricHelpText', { @@ -18,23 +18,33 @@ export const help: FunctionHelp> = { label: i18n.translate('xpack.canvas.functions.metric.args.labelHelpText', { defaultMessage: 'The text describing the metric.', }), - metricFont: i18n.translate('xpack.canvas.functions.metric.args.metricFontHelpText', { + labelFont: i18n.translate('xpack.canvas.functions.metric.args.labelFontHelpText', { defaultMessage: - 'The {CSS} font properties for the metric. For example, {FONT_FAMILY} or {FONT_WEIGHT}.', + 'The {CSS} font properties for the label. For example, {FONT_FAMILY} or {FONT_WEIGHT}.', values: { CSS, FONT_FAMILY, FONT_WEIGHT, }, }), - labelFont: i18n.translate('xpack.canvas.functions.metric.args.labelFontHelpText', { + metricFont: i18n.translate('xpack.canvas.functions.metric.args.metricFontHelpText', { defaultMessage: - 'The {CSS} font properties for the label. For example, {FONT_FAMILY} or {FONT_WEIGHT}.', + 'The {CSS} font properties for the metric. For example, {FONT_FAMILY} or {FONT_WEIGHT}.', values: { CSS, FONT_FAMILY, FONT_WEIGHT, }, }), + metricFormat: i18n.translate('xpack.canvas.functions.metric.args.metricFormatHelpText', { + defaultMessage: + 'A {NUMERALJS} format string. For example, {example1} or {example2}. See {url}.', + values: { + example1: `"0.0a"`, + example2: `"0%"`, + NUMERALJS, + url: 'http://numeraljs.com/#format', + }, + }), }, }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js index db13083c97382..b714cdcaab094 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js @@ -9,6 +9,7 @@ import { datacolumn } from './datacolumn'; import { filterGroup } from './filter_group'; import { imageUpload } from './image_upload'; import { number } from './number'; +import { numberFormat } from './number_format'; import { palette } from './palette'; import { percentage } from './percentage'; import { range } from './range'; @@ -24,6 +25,7 @@ export const args = [ filterGroup, imageUpload, number, + numberFormat, palette, percentage, range, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.examples.storyshot new file mode 100644 index 0000000000000..6fcfc922e5d07 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.examples.storyshot @@ -0,0 +1,242 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots arguments/NumberFormat with custom format 1`] = ` +Array [ +
+
+ +
+ + + +
+
+
, +
, +
+
+ +
+
, +] +`; + +exports[`Storyshots arguments/NumberFormat with no format 1`] = ` +Array [ +
+
+ +
+ + + +
+
+
, +
, +
+
+ +
+
, +] +`; + +exports[`Storyshots arguments/NumberFormat with preset format 1`] = ` +
+
+ +
+ + + +
+
+
+`; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/number_format.examples.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/number_format.examples.tsx new file mode 100644 index 0000000000000..4a078a38c4a35 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/number_format.examples.tsx @@ -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 { storiesOf } from '@storybook/react'; +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { NumberFormatArgInput } from '../number_format'; + +const numberFormats = [ + { value: '0.0[000]', text: 'Number' }, + { value: '0.0%', text: 'Percent' }, + { value: '$0.00', text: 'Currency' }, + { value: '00:00:00', text: 'Duration' }, + { value: '0.0b', text: 'Bytes' }, +]; + +storiesOf('arguments/NumberFormat', module) + .add('with no format', () => ( + + )) + .add('with preset format', () => ( + + )) + .add('with custom format', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts new file mode 100644 index 0000000000000..4eca6e41bd3f5 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts @@ -0,0 +1,38 @@ +/* + * 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 { compose, withProps } from 'recompose'; +import { NumberFormatArgInput as Component, Props as ComponentProps } from './number_format'; +import { AdvancedSettings } from '../../../../public/lib/kibana_advanced_settings'; +// @ts-ignore untyped local lib +import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; + +const formatMap = { + NUMBER: AdvancedSettings.get('format:number:defaultPattern'), + PERCENT: AdvancedSettings.get('format:percent:defaultPattern'), + CURRENCY: AdvancedSettings.get('format:currency:defaultPattern'), + DURATION: '00:00:00', + BYTES: AdvancedSettings.get('format:bytes:defaultPattern'), +}; + +const numberFormats = [ + { value: formatMap.NUMBER, text: 'Number' }, + { value: formatMap.PERCENT, text: 'Percent' }, + { value: formatMap.CURRENCY, text: 'Currency' }, + { value: formatMap.DURATION, text: 'Duration' }, + { value: formatMap.BYTES, text: 'Bytes' }, +]; + +export const NumberFormatArgInput = compose(withProps({ numberFormats }))( + Component +); + +export const numberFormat = () => ({ + name: 'numberFormat', + displayName: 'Number Format', + help: 'Select or enter a valid NumeralJS format', + simpleTemplate: templateFromReactComponent(NumberFormatArgInput), +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/number_format.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/number_format.tsx new file mode 100644 index 0000000000000..b9d122dcf03d0 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/number_format.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, ChangeEvent, FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +import { EuiSelect, EuiFieldText, EuiSpacer } from '@elastic/eui'; + +interface NumberFormatOption { + /** A NumeralJS format string */ + value: string; + /** The name to display for the format */ + text: string; +} + +export interface Props { + /** An array of number formats options */ + numberFormats: NumberFormatOption[]; + /** The handler to invoke when value changes */ + onValueChange: (value: string) => void; + /** The value of the argument */ + argValue: string; + /** The ID for the argument */ + argId: string; +} + +export const NumberFormatArgInput: FunctionComponent = ({ + numberFormats, + onValueChange, + argValue, + argId, +}) => { + const formatOptions = numberFormats.concat({ value: '', text: 'Custom' }); + const handleTextChange = (ev: ChangeEvent) => onValueChange(ev.target.value); + const handleSelectChange = (ev: ChangeEvent) => { + const { value } = formatOptions[ev.target.selectedIndex]; + return onValueChange(value || '0.0a'); + }; + + // checks if the argValue is one of the preset formats + const isCustomFormat = !argValue || !formatOptions.map(({ value }) => value).includes(argValue); + + return ( + + + {isCustomFormat && ( + + + + + )} + + ); +}; + +NumberFormatArgInput.propTypes = { + onValueChange: PropTypes.func.isRequired, + argValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]).isRequired, + argId: PropTypes.string.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js index 15da536c56e46..5df4003615113 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js @@ -5,6 +5,7 @@ */ import { openSans } from '../../../common/lib/fonts'; +import { AdvancedSettings } from '../../../public/lib/kibana_advanced_settings'; export const metric = () => ({ name: 'metric', @@ -19,6 +20,13 @@ export const metric = () => ({ argType: 'string', default: '""', }, + { + name: 'labelFont', + displayName: 'Label text settings', + help: 'Fonts, alignment and color', + argType: 'font', + default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, + }, { name: 'metricFont', displayName: 'Metric text settings', @@ -27,11 +35,10 @@ export const metric = () => ({ default: `{font size=48 family="${openSans.value}" color="#000000" align=center lHeight=48}`, }, { - name: 'labelFont', - displayName: 'Label text settings', - help: 'Fonts, alignment and color', - argType: 'font', - default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, + name: 'metricFormat', + displayName: 'Metric Format', + argType: 'numberFormat', + default: `"${AdvancedSettings.get('format:number:defaultPattern')}"`, }, ], }); diff --git a/x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.ts b/x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.ts new file mode 100644 index 0000000000000..33f3d801c22d6 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.ts @@ -0,0 +1,9 @@ +/* + * 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 chrome from 'ui/chrome'; + +export const AdvancedSettings = chrome.getUiSettingsClient(); diff --git a/x-pack/legacy/plugins/canvas/types/renderers.ts b/x-pack/legacy/plugins/canvas/types/renderers.ts index 2167c542c3548..282a1c820e346 100644 --- a/x-pack/legacy/plugins/canvas/types/renderers.ts +++ b/x-pack/legacy/plugins/canvas/types/renderers.ts @@ -7,19 +7,32 @@ type GenericCallback = (callback: () => void) => void; export interface RendererHandlers { + /** Handler to invoke when an element has finished rendering */ done: () => void; - getFilter: () => string; + /** Handler to invoke when an element is deleted or changes to a different render type */ onDestroy: GenericCallback; + /** Handler to invoke when an element's dimensions have changed*/ onResize: GenericCallback; + /** Retrieves the value of the filter property on the element object persisted on the workpad */ + getFilter: () => string; + /** Sets the value of the filter property on the element object persisted on the workpad */ setFilter: (filter: string) => void; } export interface RendererSpec { + /** The render type */ name: string; + /** The name to display */ displayName: string; + /** A description of what is rendered */ help: string; + /** Indicate whether the element should reuse the existing DOM element when re-rendering */ reuseDomNode: boolean; - height: number; + /** The default width of the element in pixels */ + width?: number; + /** The default height of the element in pixels */ + height?: number; + /** A function that renders an element into the specified DOM element */ render: (domNode: HTMLElement, config: RendererConfig, handlers: RendererHandlers) => void; } From 04a9547612b46c7ae391705a48a79bba93282448 Mon Sep 17 00:00:00 2001 From: "dave.snider@gmail.com" Date: Tue, 6 Aug 2019 17:50:46 -0700 Subject: [PATCH 22/26] Remove unused KUI code (#42748) * Remove unused KUI code * delete a bunch of dead code * feedback * linting --- packages/kbn-ui-framework/dist/kui_dark.css | 431 ------------------ packages/kbn-ui-framework/dist/kui_light.css | 431 ------------------ .../doc_site/src/services/routes/routes.js | 268 +++++------ .../src/views/expression/expression.js | 220 --------- .../views/expression/expression_example.js | 59 --- .../doc_site/src/views/menu/menu.js | 43 -- .../doc_site/src/views/menu/menu_contained.js | 43 -- .../doc_site/src/views/menu/menu_example.js | 73 --- .../views/menu_button/menu_button_basic.html | 9 - .../views/menu_button/menu_button_danger.html | 9 - .../menu_button/menu_button_elements.html | 17 - .../views/menu_button/menu_button_example.js | 119 ----- .../views/menu_button/menu_button_group.html | 19 - .../menu_button/menu_button_primary.html | 9 - .../menu_button/menu_button_with_icon.html | 17 - .../doc_site/src/views/modal/confirm_modal.js | 82 ---- .../doc_site/src/views/modal/modal.js | 102 ----- .../doc_site/src/views/modal/modal_example.js | 81 ---- .../src/views/panel_simple/panel_simple.js | 56 --- .../panel_simple/panel_simple_example.js | 64 --- .../doc_site/src/views/popover/popover.js | 68 --- .../views/popover/popover_anchor_position.js | 98 ---- .../views/popover/popover_body_class_name.js | 67 --- .../src/views/popover/popover_example.js | 160 ------- .../views/popover/popover_panel_class_name.js | 68 --- .../src/views/popover/popover_with_title.js | 79 ---- .../doc_site/src/views/popover/trap_focus.js | 96 ---- .../views/table/table_with_menu_buttons.js | 81 +--- .../views/toggle_button/toggle_button.html | 4 - .../src/views/toggle_button/toggle_button.js | 17 - .../toggle_button/toggle_button_disabled.html | 4 - .../toggle_button/toggle_button_example.js | 86 ---- .../src/views/toggle_button/toggle_panel.html | 21 - .../src/views/toggle_button/toggle_panel.js | 24 - .../__snapshots__/expression.test.js.snap | 17 - .../expression_button.test.js.snap | 57 --- .../components/expression/_expression.scss | 27 -- .../src/components/expression/_index.scss | 3 - .../src/components/expression/expression.js | 44 -- .../components/expression/expression.test.js | 51 --- .../expression/expression_button.js | 58 --- .../expression/expression_button.test.js | 93 ---- .../src/components/expression/index.js | 21 - .../kbn-ui-framework/src/components/index.js | 71 +-- .../src/components/index.scss | 47 +- .../menu/__snapshots__/menu.test.js.snap | 19 - .../menu/__snapshots__/menu_item.test.js.snap | 11 - .../src/components/menu/_index.scss | 1 - .../src/components/menu/_menu.scss | 26 -- .../src/components/menu/index.js | 21 - .../src/components/menu/menu.js | 48 -- .../src/components/menu/menu.test.js | 36 -- .../src/components/menu/menu_item.js | 43 -- .../src/components/menu/menu_item.test.js | 31 -- .../src/components/menu_button/_index.scss | 20 - .../components/menu_button/_menu_button.scss | 118 ----- .../menu_button/_menu_button_group.scss | 11 - .../__snapshots__/confirm_modal.test.js.snap | 64 --- .../modal/__snapshots__/modal.test.js.snap | 14 - .../__snapshots__/modal_body.test.js.snap | 11 - .../__snapshots__/modal_footer.test.js.snap | 11 - .../__snapshots__/modal_header.test.js.snap | 11 - .../modal_header_title.test.js.snap | 11 - .../__snapshots__/modal_overlay.test.js.snap | 11 - .../src/components/modal/_index.scss | 11 - .../src/components/modal/_modal.scss | 63 --- .../src/components/modal/_modal_overlay.scss | 13 - .../src/components/modal/confirm_modal.js | 120 ----- .../components/modal/confirm_modal.test.js | 145 ------ .../src/components/modal/index.js | 30 -- .../src/components/modal/modal.js | 74 --- .../src/components/modal/modal.test.js | 39 -- .../src/components/modal/modal_body.js | 36 -- .../src/components/modal/modal_body.test.js | 31 -- .../src/components/modal/modal_footer.js | 36 -- .../src/components/modal/modal_footer.test.js | 31 -- .../src/components/modal/modal_header.js | 36 -- .../src/components/modal/modal_header.test.js | 31 -- .../components/modal/modal_header_title.js | 36 -- .../modal/modal_header_title.test.js | 31 -- .../src/components/modal/modal_overlay.js | 36 -- .../components/modal/modal_overlay.test.js | 31 -- .../outside_click_detector.test.js.snap | 3 - .../outside_click_detector/index.js | 22 - .../outside_click_detector.js | 68 --- .../outside_click_detector.test.js | 36 -- .../__snapshots__/panel_simple.test.js.snap | 9 - .../src/components/panel_simple/_index.scss | 1 - .../panel_simple/_panel_simple.scss | 27 -- .../src/components/panel_simple/index.js | 23 - .../components/panel_simple/panel_simple.js | 78 ---- .../panel_simple/panel_simple.test.js | 35 -- .../__snapshots__/popover.test.js.snap | 125 ----- .../__snapshots__/popover_title.test.js.snap | 9 - .../src/components/popover/_index.scss | 3 - .../src/components/popover/_mixins.scss | 6 - .../src/components/popover/_popover.scss | 96 ---- .../components/popover/_popover_title.scss | 3 - .../src/components/popover/index.js | 21 - .../src/components/popover/popover.js | 218 --------- .../src/components/popover/popover.test.js | 218 --------- .../src/components/popover/popover_title.js | 40 -- .../components/popover/popover_title.test.js | 35 -- .../src/components/toggle_button/_index.scss | 2 - .../toggle_button/_toggle_button.scss | 40 -- .../toggle_button/_toggle_panel.scss | 13 - 106 files changed, 150 insertions(+), 5843 deletions(-) delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/expression/expression.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/expression/expression_example.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/menu/menu.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/menu/menu_contained.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/menu/menu_example.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_basic.html delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_danger.html delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_elements.html delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_example.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_group.html delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_primary.html delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_with_icon.html delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/modal/confirm_modal.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/modal/modal.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/modal/modal_example.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple_example.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/popover/popover.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/popover/popover_anchor_position.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/popover/popover_body_class_name.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/popover/popover_example.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/popover/popover_panel_class_name.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/popover/popover_with_title.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/popover/trap_focus.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button.html delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_disabled.html delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_example.js delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_panel.html delete mode 100644 packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_panel.js delete mode 100644 packages/kbn-ui-framework/src/components/expression/__snapshots__/expression.test.js.snap delete mode 100644 packages/kbn-ui-framework/src/components/expression/__snapshots__/expression_button.test.js.snap delete mode 100644 packages/kbn-ui-framework/src/components/expression/_expression.scss delete mode 100644 packages/kbn-ui-framework/src/components/expression/_index.scss delete mode 100644 packages/kbn-ui-framework/src/components/expression/expression.js delete mode 100644 packages/kbn-ui-framework/src/components/expression/expression.test.js delete mode 100644 packages/kbn-ui-framework/src/components/expression/expression_button.js delete mode 100644 packages/kbn-ui-framework/src/components/expression/expression_button.test.js delete mode 100644 packages/kbn-ui-framework/src/components/expression/index.js delete mode 100644 packages/kbn-ui-framework/src/components/menu/__snapshots__/menu.test.js.snap delete mode 100644 packages/kbn-ui-framework/src/components/menu/__snapshots__/menu_item.test.js.snap delete mode 100644 packages/kbn-ui-framework/src/components/menu/_index.scss delete mode 100644 packages/kbn-ui-framework/src/components/menu/_menu.scss delete mode 100644 packages/kbn-ui-framework/src/components/menu/index.js delete mode 100644 packages/kbn-ui-framework/src/components/menu/menu.js delete mode 100644 packages/kbn-ui-framework/src/components/menu/menu.test.js delete mode 100644 packages/kbn-ui-framework/src/components/menu/menu_item.js delete mode 100644 packages/kbn-ui-framework/src/components/menu/menu_item.test.js delete mode 100644 packages/kbn-ui-framework/src/components/menu_button/_index.scss delete mode 100644 packages/kbn-ui-framework/src/components/menu_button/_menu_button.scss delete mode 100644 packages/kbn-ui-framework/src/components/menu_button/_menu_button_group.scss delete mode 100644 packages/kbn-ui-framework/src/components/modal/__snapshots__/confirm_modal.test.js.snap delete mode 100644 packages/kbn-ui-framework/src/components/modal/__snapshots__/modal.test.js.snap delete mode 100644 packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_body.test.js.snap delete mode 100644 packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_footer.test.js.snap delete mode 100644 packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_header.test.js.snap delete mode 100644 packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_header_title.test.js.snap delete mode 100644 packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_overlay.test.js.snap delete mode 100644 packages/kbn-ui-framework/src/components/modal/_index.scss delete mode 100644 packages/kbn-ui-framework/src/components/modal/_modal.scss delete mode 100644 packages/kbn-ui-framework/src/components/modal/_modal_overlay.scss delete mode 100644 packages/kbn-ui-framework/src/components/modal/confirm_modal.js delete mode 100644 packages/kbn-ui-framework/src/components/modal/confirm_modal.test.js delete mode 100644 packages/kbn-ui-framework/src/components/modal/index.js delete mode 100644 packages/kbn-ui-framework/src/components/modal/modal.js delete mode 100644 packages/kbn-ui-framework/src/components/modal/modal.test.js delete mode 100644 packages/kbn-ui-framework/src/components/modal/modal_body.js delete mode 100644 packages/kbn-ui-framework/src/components/modal/modal_body.test.js delete mode 100644 packages/kbn-ui-framework/src/components/modal/modal_footer.js delete mode 100644 packages/kbn-ui-framework/src/components/modal/modal_footer.test.js delete mode 100644 packages/kbn-ui-framework/src/components/modal/modal_header.js delete mode 100644 packages/kbn-ui-framework/src/components/modal/modal_header.test.js delete mode 100644 packages/kbn-ui-framework/src/components/modal/modal_header_title.js delete mode 100644 packages/kbn-ui-framework/src/components/modal/modal_header_title.test.js delete mode 100644 packages/kbn-ui-framework/src/components/modal/modal_overlay.js delete mode 100644 packages/kbn-ui-framework/src/components/modal/modal_overlay.test.js delete mode 100644 packages/kbn-ui-framework/src/components/outside_click_detector/__snapshots__/outside_click_detector.test.js.snap delete mode 100644 packages/kbn-ui-framework/src/components/outside_click_detector/index.js delete mode 100644 packages/kbn-ui-framework/src/components/outside_click_detector/outside_click_detector.js delete mode 100644 packages/kbn-ui-framework/src/components/outside_click_detector/outside_click_detector.test.js delete mode 100644 packages/kbn-ui-framework/src/components/panel_simple/__snapshots__/panel_simple.test.js.snap delete mode 100644 packages/kbn-ui-framework/src/components/panel_simple/_index.scss delete mode 100644 packages/kbn-ui-framework/src/components/panel_simple/_panel_simple.scss delete mode 100644 packages/kbn-ui-framework/src/components/panel_simple/index.js delete mode 100644 packages/kbn-ui-framework/src/components/panel_simple/panel_simple.js delete mode 100644 packages/kbn-ui-framework/src/components/panel_simple/panel_simple.test.js delete mode 100644 packages/kbn-ui-framework/src/components/popover/__snapshots__/popover.test.js.snap delete mode 100644 packages/kbn-ui-framework/src/components/popover/__snapshots__/popover_title.test.js.snap delete mode 100644 packages/kbn-ui-framework/src/components/popover/_index.scss delete mode 100644 packages/kbn-ui-framework/src/components/popover/_mixins.scss delete mode 100644 packages/kbn-ui-framework/src/components/popover/_popover.scss delete mode 100644 packages/kbn-ui-framework/src/components/popover/_popover_title.scss delete mode 100644 packages/kbn-ui-framework/src/components/popover/index.js delete mode 100644 packages/kbn-ui-framework/src/components/popover/popover.js delete mode 100644 packages/kbn-ui-framework/src/components/popover/popover.test.js delete mode 100644 packages/kbn-ui-framework/src/components/popover/popover_title.js delete mode 100644 packages/kbn-ui-framework/src/components/popover/popover_title.test.js delete mode 100644 packages/kbn-ui-framework/src/components/toggle_button/_index.scss delete mode 100644 packages/kbn-ui-framework/src/components/toggle_button/_toggle_button.scss delete mode 100644 packages/kbn-ui-framework/src/components/toggle_button/_toggle_panel.scss diff --git a/packages/kbn-ui-framework/dist/kui_dark.css b/packages/kbn-ui-framework/dist/kui_dark.css index 7a962b510fc0a..dcbd65fbca520 100644 --- a/packages/kbn-ui-framework/dist/kui_dark.css +++ b/packages/kbn-ui-framework/dist/kui_dark.css @@ -492,29 +492,6 @@ main { .kuiCollapseButton:hover { opacity: 1; } -.kuiExpression { - padding: 20px; - white-space: nowrap; } - -.kuiExpressionButton { - background-color: transparent; - padding: 5px 0px; - border: none; - border-bottom: dotted 2px #343741; - font-size: 16px; - cursor: pointer; } - -.kuiExpressionButton__description { - color: #7DE2D1; - text-transform: uppercase; } - -.kuiExpressionButton__value { - color: #DFE5EF; - text-transform: lowercase; } - -.kuiExpressionButton-isActive { - border-bottom: solid 2px #7DE2D1; } - /** * 1. Set inline-block so this wrapper shrinks to fit the input. */ @@ -1635,262 +1612,6 @@ main { padding: 0; display: none; } -/** - * 1. Allow class to be applied to `ul` and `ol` elements - */ -.kuiMenu { - padding-left: 0; - /* 1 */ } - -.kuiMenu--contained { - border: 1px solid #343741; } - .kuiMenu--contained .kuiMenuItem { - padding: 6px 10px; } - -/** - * 1. Allow class to be applied to `li` elements - */ -.kuiMenuItem { - list-style: none; - /* 1 */ - padding: 6px 0; } - .kuiMenuItem + .kuiMenuItem { - border-top: 1px solid #343741; } - -/** - * 1. Setting to inline-block guarantees the same height when applied to both - * button elements and anchor tags. - * 2. Disable for Angular. - * 3. Make the button just tall enough to fit inside an Option Layout. - */ -.kuiMenuButton { - display: inline-block; - /* 1 */ - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - cursor: pointer; - padding: 2px 10px; - /* 3 */ - font-size: 12px; - font-weight: 400; - line-height: 1.5; - text-decoration: none; - border: none; - border-radius: 4px; } - .kuiMenuButton:disabled { - cursor: default; - pointer-events: none; - /* 2 */ } - .kuiMenuButton:active:enabled { - -webkit-transform: translateY(1px); - transform: translateY(1px); } - .kuiMenuButton:focus { - z-index: 1; - /* 1 */ - outline: none !important; - /* 2 */ - -webkit-box-shadow: 0 0 0 1px #1D1E24, 0 0 0 2px #1BA9F5; - box-shadow: 0 0 0 1px #1D1E24, 0 0 0 2px #1BA9F5; - /* 3 */ } - -.kuiMenuButton--iconText .kuiMenuButton__icon:first-child { - margin-right: 4px; } - -.kuiMenuButton--iconText .kuiMenuButton__icon:last-child { - margin-left: 4px; } - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--basic { - color: #DFE5EF; - background-color: #1D1E24; } - .kuiMenuButton--basic:focus { - color: #DFE5EF !important; - /* 1 */ } - .kuiMenuButton--basic:hover, .kuiMenuButton--basic:active { - /* 2 */ - color: #DFE5EF !important; - /* 1 */ - background-color: #25262E; } - .kuiMenuButton--basic:disabled { - color: #535966; - cursor: not-allowed; } - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--primary { - color: #FFF; - background-color: #1BA9F5; } - .kuiMenuButton--primary:focus { - color: #FFF !important; - /* 1 */ } - .kuiMenuButton--primary:hover, .kuiMenuButton--primary:active { - /* 2 */ - color: #FFF !important; - /* 1 */ - background-color: #098dd4; } - .kuiMenuButton--primary:disabled { - background-color: #535966; - cursor: not-allowed; } - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--danger { - color: #FFF; - background-color: #F66; } - .kuiMenuButton--danger:hover, .kuiMenuButton--danger:active { - /* 2 */ - color: #FFF !important; - /* 1 */ - background-color: #ff3333; } - .kuiMenuButton--danger:disabled { - color: #FFF; - background-color: #535966; - cursor: not-allowed; } - .kuiMenuButton--danger:focus { - z-index: 1; - /* 1 */ - outline: none !important; - /* 2 */ - -webkit-box-shadow: 0 0 0 1px #1D1E24, 0 0 0 2px #F66; - box-shadow: 0 0 0 1px #1D1E24, 0 0 0 2px #F66; - /* 3 */ } - -.kuiMenuButtonGroup { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; } - .kuiMenuButtonGroup .kuiMenuButton + .kuiMenuButton { - margin-left: 4px; } - -.kuiMenuButtonGroup--alignRight { - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - -ms-flex-pack: end; - justify-content: flex-end; } - -.kuiModalOverlay { - position: fixed; - z-index: 1000; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -webkit-align-items: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - padding-bottom: 10vh; - background: rgba(52, 55, 65, 0.8); } - -.kuiModal { - -webkit-box-shadow: 0 12px 24px 0 rgba(0, 0, 0, 0.1), 0 6px 12px 0 rgba(0, 0, 0, 0.1), 0 4px 4px 0 rgba(0, 0, 0, 0.1), 0 2px 2px 0 rgba(0, 0, 0, 0.1); - box-shadow: 0 12px 24px 0 rgba(0, 0, 0, 0.1), 0 6px 12px 0 rgba(0, 0, 0, 0.1), 0 4px 4px 0 rgba(0, 0, 0, 0.1), 0 2px 2px 0 rgba(0, 0, 0, 0.1); - line-height: 1.5; - background-color: #1D1E24; - border: 1px solid #343741; - border-radius: 4px; - z-index: 1001; - -webkit-animation: kuiModal 350ms cubic-bezier(0.34, 1.61, 0.7, 1); - animation: kuiModal 350ms cubic-bezier(0.34, 1.61, 0.7, 1); } - -.kuiModal--confirmation { - width: 450px; - min-width: auto; } - -.kuiModalHeader { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: justify; - -webkit-justify-content: space-between; - -ms-flex-pack: justify; - justify-content: space-between; - -webkit-box-align: center; - -webkit-align-items: center; - -ms-flex-align: center; - align-items: center; - padding: 10px; - padding-left: 20px; - border-bottom: 1px solid #343741; } - -.kuiModalHeader__title { - font-size: 20px; } - -.kuiModalHeaderCloseButton { - display: inline-block; - /* 1 */ - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - cursor: pointer; - padding: 2px 5px; - border: 1px solid transparent; - color: #98A2B3; - background-color: transparent; - line-height: 1; - /* 2 */ - font-size: 20px; } - .kuiModalHeaderCloseButton:hover { - color: #DFE5EF; } - -.kuiModalBody { - padding: 20px; } - -.kuiModalBodyText { - font-size: 14px; } - -.kuiModalFooter { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - -ms-flex-pack: end; - justify-content: flex-end; - padding: 20px; - padding-top: 10px; } - .kuiModalFooter > * + * { - margin-left: 5px; } - -@-webkit-keyframes kuiModal { - 0% { - opacity: 0; - -webkit-transform: translateY(32px); - transform: translateY(32px); } - 100% { - opacity: 1; - -webkit-transform: translateY(0); - transform: translateY(0); } } - -@keyframes kuiModal { - 0% { - opacity: 0; - -webkit-transform: translateY(32px); - transform: translateY(32px); } - 100% { - opacity: 1; - -webkit-transform: translateY(0); - transform: translateY(0); } } - /** * 1. Put 10px of space between each child. */ @@ -2090,107 +1811,6 @@ main { .kuiPanelBody { padding: 10px; } -.kuiPanelSimple { - -webkit-box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.3), 0 1px 5px -2px rgba(0, 0, 0, 0.3); - box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.3), 0 1px 5px -2px rgba(0, 0, 0, 0.3); - background-color: #1D1E24; - border: 1px solid #343741; - border-radius: 4px; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - -ms-flex-positive: 1; - flex-grow: 1; } - .kuiPanelSimple.kuiPanelSimple--paddingSmall { - padding: 8px; } - .kuiPanelSimple.kuiPanelSimple--paddingMedium { - padding: 16px; } - .kuiPanelSimple.kuiPanelSimple--paddingLarge { - padding: 24px; } - .kuiPanelSimple.kuiPanelSimple--shadow { - -webkit-box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); - box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); } - .kuiPanelSimple.kuiPanelSimple--flexGrowZero { - -webkit-box-flex: 0; - -webkit-flex-grow: 0; - -ms-flex-positive: 0; - flex-grow: 0; } - -.kuiPopover { - display: inline-block; - position: relative; } - .kuiPopover.kuiPopover-isOpen .kuiPopover__panel { - opacity: 1; - visibility: visible; - margin-top: 8px; - pointer-events: auto; } - -.kuiPopover__panel { - position: absolute; - z-index: 2000; - top: 100%; - left: 50%; - -webkit-transform: translateX(-50%) translateY(8px) translateZ(0); - transform: translateX(-50%) translateY(8px) translateZ(0); - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - -webkit-transition: opacity cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, visibility cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, margin-top cubic-bezier(0.34, 1.61, 0.7, 1) 350ms; - transition: opacity cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, visibility cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, margin-top cubic-bezier(0.34, 1.61, 0.7, 1) 350ms; - -webkit-transform-origin: center top; - transform-origin: center top; - opacity: 0; - visibility: hidden; - pointer-events: none; - margin-top: 24px; } - .kuiPopover__panel:before { - position: absolute; - content: ""; - top: -16px; - height: 0; - width: 0; - left: 50%; - margin-left: -16px; - border-left: 16px solid transparent; - border-right: 16px solid transparent; - border-bottom: 16px solid #343741; } - .kuiPopover__panel:after { - position: absolute; - content: ""; - top: -15px; - right: 0; - height: 0; - left: 50%; - margin-left: -16px; - width: 0; - border-left: 16px solid transparent; - border-right: 16px solid transparent; - border-bottom: 16px solid #1D1E24; } - -.kuiPopover--withTitle .kuiPopover__panel:after { - border-bottom-color: #25262E; } - -.kuiPopover--anchorLeft .kuiPopover__panel { - left: 0; - -webkit-transform: translateX(0%) translateY(8px) translateZ(0); - transform: translateX(0%) translateY(8px) translateZ(0); } - .kuiPopover--anchorLeft .kuiPopover__panel:before, .kuiPopover--anchorLeft .kuiPopover__panel:after { - right: auto; - left: 16px; - margin: 0; } - -.kuiPopover--anchorRight .kuiPopover__panel { - left: 100%; - -webkit-transform: translateX(-100%) translateY(8px) translateZ(0); - transform: translateX(-100%) translateY(8px) translateZ(0); } - .kuiPopover--anchorRight .kuiPopover__panel:before, .kuiPopover--anchorRight .kuiPopover__panel:after { - right: 16px; - left: auto; } - -.kuiPopoverTitle { - background-color: #25262E; - border-bottom: 1px solid #343741; - padding: 12px; - font-size: 16px; } - .kuiEmptyTablePrompt { display: -webkit-box; display: -webkit-flex; @@ -2497,57 +2117,6 @@ main { background-color: transparent; border-bottom-color: transparent; } -/** - * 1. Allow container to determine font-size and line-height. - * 2. Override inherited Bootstrap styles. - */ -.kuiToggleButton { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - cursor: pointer; - background-color: transparent; - border: none; - padding: 0; - font-size: inherit; - /* 1 */ - line-height: inherit; - /* 1 */ - color: #DFE5EF; } - .kuiToggleButton:focus { - color: #DFE5EF; } - .kuiToggleButton:active { - color: #1BA9F5 !important; - /* 2 */ } - .kuiToggleButton:hover:not(:disabled) { - color: #098dd4 !important; - /* 2 */ - text-decoration: underline; } - .kuiToggleButton:disabled { - cursor: not-allowed; - opacity: .5; } - -/** - * 1. Make icon a consistent width so the text doesn't get pushed around as the icon changes - * between "expand" and "collapse". Use ems to be relative to inherited font-size. - */ -.kuiToggleButton__icon { - width: 0.8em; - /* 1 */ } - -.kuiTogglePanelHeader { - padding-bottom: 4px; - margin-bottom: 15px; - border-bottom: 1px solid #343741; - /** - * 1. Allow the user to click anywhere on the header, not just on the button text. - */ } - .kuiTogglePanelHeader .kuiToggleButton { - width: 100%; - /* 1 */ - text-align: left; - /* 1 */ } - .kuiToolBar { display: -webkit-box; display: -webkit-flex; diff --git a/packages/kbn-ui-framework/dist/kui_light.css b/packages/kbn-ui-framework/dist/kui_light.css index 8668bac99b641..3e82873d53aa7 100644 --- a/packages/kbn-ui-framework/dist/kui_light.css +++ b/packages/kbn-ui-framework/dist/kui_light.css @@ -492,29 +492,6 @@ main { .kuiCollapseButton:hover { opacity: 1; } -.kuiExpression { - padding: 20px; - white-space: nowrap; } - -.kuiExpressionButton { - background-color: transparent; - padding: 5px 0px; - border: none; - border-bottom: dotted 2px #D3DAE6; - font-size: 16px; - cursor: pointer; } - -.kuiExpressionButton__description { - color: #017D73; - text-transform: uppercase; } - -.kuiExpressionButton__value { - color: #343741; - text-transform: lowercase; } - -.kuiExpressionButton-isActive { - border-bottom: solid 2px #017D73; } - /** * 1. Set inline-block so this wrapper shrinks to fit the input. */ @@ -1635,262 +1612,6 @@ main { padding: 0; display: none; } -/** - * 1. Allow class to be applied to `ul` and `ol` elements - */ -.kuiMenu { - padding-left: 0; - /* 1 */ } - -.kuiMenu--contained { - border: 1px solid #D3DAE6; } - .kuiMenu--contained .kuiMenuItem { - padding: 6px 10px; } - -/** - * 1. Allow class to be applied to `li` elements - */ -.kuiMenuItem { - list-style: none; - /* 1 */ - padding: 6px 0; } - .kuiMenuItem + .kuiMenuItem { - border-top: 1px solid #D3DAE6; } - -/** - * 1. Setting to inline-block guarantees the same height when applied to both - * button elements and anchor tags. - * 2. Disable for Angular. - * 3. Make the button just tall enough to fit inside an Option Layout. - */ -.kuiMenuButton { - display: inline-block; - /* 1 */ - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - cursor: pointer; - padding: 2px 10px; - /* 3 */ - font-size: 12px; - font-weight: 400; - line-height: 1.5; - text-decoration: none; - border: none; - border-radius: 4px; } - .kuiMenuButton:disabled { - cursor: default; - pointer-events: none; - /* 2 */ } - .kuiMenuButton:active:enabled { - -webkit-transform: translateY(1px); - transform: translateY(1px); } - .kuiMenuButton:focus { - z-index: 1; - /* 1 */ - outline: none !important; - /* 2 */ - -webkit-box-shadow: 0 0 0 1px #FFF, 0 0 0 2px #006BB4; - box-shadow: 0 0 0 1px #FFF, 0 0 0 2px #006BB4; - /* 3 */ } - -.kuiMenuButton--iconText .kuiMenuButton__icon:first-child { - margin-right: 4px; } - -.kuiMenuButton--iconText .kuiMenuButton__icon:last-child { - margin-left: 4px; } - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--basic { - color: #343741; - background-color: #FFF; } - .kuiMenuButton--basic:focus { - color: #343741 !important; - /* 1 */ } - .kuiMenuButton--basic:hover, .kuiMenuButton--basic:active { - /* 2 */ - color: #343741 !important; - /* 1 */ - background-color: #F5F7FA; } - .kuiMenuButton--basic:disabled { - color: #98A2B3; - cursor: not-allowed; } - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--primary { - color: #FFF; - background-color: #006BB4; } - .kuiMenuButton--primary:focus { - color: #FFF !important; - /* 1 */ } - .kuiMenuButton--primary:hover, .kuiMenuButton--primary:active { - /* 2 */ - color: #FFF !important; - /* 1 */ - background-color: #004d81; } - .kuiMenuButton--primary:disabled { - background-color: #98A2B3; - cursor: not-allowed; } - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--danger { - color: #FFF; - background-color: #BD271E; } - .kuiMenuButton--danger:hover, .kuiMenuButton--danger:active { - /* 2 */ - color: #FFF !important; - /* 1 */ - background-color: #911e17; } - .kuiMenuButton--danger:disabled { - color: #FFF; - background-color: #98A2B3; - cursor: not-allowed; } - .kuiMenuButton--danger:focus { - z-index: 1; - /* 1 */ - outline: none !important; - /* 2 */ - -webkit-box-shadow: 0 0 0 1px #FFF, 0 0 0 2px #BD271E; - box-shadow: 0 0 0 1px #FFF, 0 0 0 2px #BD271E; - /* 3 */ } - -.kuiMenuButtonGroup { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; } - .kuiMenuButtonGroup .kuiMenuButton + .kuiMenuButton { - margin-left: 4px; } - -.kuiMenuButtonGroup--alignRight { - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - -ms-flex-pack: end; - justify-content: flex-end; } - -.kuiModalOverlay { - position: fixed; - z-index: 1000; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -webkit-align-items: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - padding-bottom: 10vh; - background: rgba(255, 255, 255, 0.8); } - -.kuiModal { - -webkit-box-shadow: 0 12px 24px 0 rgba(65, 78, 101, 0.1), 0 6px 12px 0 rgba(65, 78, 101, 0.1), 0 4px 4px 0 rgba(65, 78, 101, 0.1), 0 2px 2px 0 rgba(65, 78, 101, 0.1); - box-shadow: 0 12px 24px 0 rgba(65, 78, 101, 0.1), 0 6px 12px 0 rgba(65, 78, 101, 0.1), 0 4px 4px 0 rgba(65, 78, 101, 0.1), 0 2px 2px 0 rgba(65, 78, 101, 0.1); - line-height: 1.5; - background-color: #FFF; - border: 1px solid #D3DAE6; - border-radius: 4px; - z-index: 1001; - -webkit-animation: kuiModal 350ms cubic-bezier(0.34, 1.61, 0.7, 1); - animation: kuiModal 350ms cubic-bezier(0.34, 1.61, 0.7, 1); } - -.kuiModal--confirmation { - width: 450px; - min-width: auto; } - -.kuiModalHeader { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: justify; - -webkit-justify-content: space-between; - -ms-flex-pack: justify; - justify-content: space-between; - -webkit-box-align: center; - -webkit-align-items: center; - -ms-flex-align: center; - align-items: center; - padding: 10px; - padding-left: 20px; - border-bottom: 1px solid #D3DAE6; } - -.kuiModalHeader__title { - font-size: 20px; } - -.kuiModalHeaderCloseButton { - display: inline-block; - /* 1 */ - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - cursor: pointer; - padding: 2px 5px; - border: 1px solid transparent; - color: #69707D; - background-color: transparent; - line-height: 1; - /* 2 */ - font-size: 20px; } - .kuiModalHeaderCloseButton:hover { - color: #343741; } - -.kuiModalBody { - padding: 20px; } - -.kuiModalBodyText { - font-size: 14px; } - -.kuiModalFooter { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - -ms-flex-pack: end; - justify-content: flex-end; - padding: 20px; - padding-top: 10px; } - .kuiModalFooter > * + * { - margin-left: 5px; } - -@-webkit-keyframes kuiModal { - 0% { - opacity: 0; - -webkit-transform: translateY(32px); - transform: translateY(32px); } - 100% { - opacity: 1; - -webkit-transform: translateY(0); - transform: translateY(0); } } - -@keyframes kuiModal { - 0% { - opacity: 0; - -webkit-transform: translateY(32px); - transform: translateY(32px); } - 100% { - opacity: 1; - -webkit-transform: translateY(0); - transform: translateY(0); } } - /** * 1. Put 10px of space between each child. */ @@ -2090,107 +1811,6 @@ main { .kuiPanelBody { padding: 10px; } -.kuiPanelSimple { - -webkit-box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3), 0 1px 5px -2px rgba(152, 162, 179, 0.3); - box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3), 0 1px 5px -2px rgba(152, 162, 179, 0.3); - background-color: #FFF; - border: 1px solid #D3DAE6; - border-radius: 4px; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - -ms-flex-positive: 1; - flex-grow: 1; } - .kuiPanelSimple.kuiPanelSimple--paddingSmall { - padding: 8px; } - .kuiPanelSimple.kuiPanelSimple--paddingMedium { - padding: 16px; } - .kuiPanelSimple.kuiPanelSimple--paddingLarge { - padding: 24px; } - .kuiPanelSimple.kuiPanelSimple--shadow { - -webkit-box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); - box-shadow: 0 16px 16px -8px rgba(0, 0, 0, 0.1); } - .kuiPanelSimple.kuiPanelSimple--flexGrowZero { - -webkit-box-flex: 0; - -webkit-flex-grow: 0; - -ms-flex-positive: 0; - flex-grow: 0; } - -.kuiPopover { - display: inline-block; - position: relative; } - .kuiPopover.kuiPopover-isOpen .kuiPopover__panel { - opacity: 1; - visibility: visible; - margin-top: 8px; - pointer-events: auto; } - -.kuiPopover__panel { - position: absolute; - z-index: 2000; - top: 100%; - left: 50%; - -webkit-transform: translateX(-50%) translateY(8px) translateZ(0); - transform: translateX(-50%) translateY(8px) translateZ(0); - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - -webkit-transition: opacity cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, visibility cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, margin-top cubic-bezier(0.34, 1.61, 0.7, 1) 350ms; - transition: opacity cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, visibility cubic-bezier(0.34, 1.61, 0.7, 1) 350ms, margin-top cubic-bezier(0.34, 1.61, 0.7, 1) 350ms; - -webkit-transform-origin: center top; - transform-origin: center top; - opacity: 0; - visibility: hidden; - pointer-events: none; - margin-top: 24px; } - .kuiPopover__panel:before { - position: absolute; - content: ""; - top: -16px; - height: 0; - width: 0; - left: 50%; - margin-left: -16px; - border-left: 16px solid transparent; - border-right: 16px solid transparent; - border-bottom: 16px solid #D3DAE6; } - .kuiPopover__panel:after { - position: absolute; - content: ""; - top: -15px; - right: 0; - height: 0; - left: 50%; - margin-left: -16px; - width: 0; - border-left: 16px solid transparent; - border-right: 16px solid transparent; - border-bottom: 16px solid #FFF; } - -.kuiPopover--withTitle .kuiPopover__panel:after { - border-bottom-color: #F5F7FA; } - -.kuiPopover--anchorLeft .kuiPopover__panel { - left: 0; - -webkit-transform: translateX(0%) translateY(8px) translateZ(0); - transform: translateX(0%) translateY(8px) translateZ(0); } - .kuiPopover--anchorLeft .kuiPopover__panel:before, .kuiPopover--anchorLeft .kuiPopover__panel:after { - right: auto; - left: 16px; - margin: 0; } - -.kuiPopover--anchorRight .kuiPopover__panel { - left: 100%; - -webkit-transform: translateX(-100%) translateY(8px) translateZ(0); - transform: translateX(-100%) translateY(8px) translateZ(0); } - .kuiPopover--anchorRight .kuiPopover__panel:before, .kuiPopover--anchorRight .kuiPopover__panel:after { - right: 16px; - left: auto; } - -.kuiPopoverTitle { - background-color: #F5F7FA; - border-bottom: 1px solid #D3DAE6; - padding: 12px; - font-size: 16px; } - .kuiEmptyTablePrompt { display: -webkit-box; display: -webkit-flex; @@ -2497,57 +2117,6 @@ main { background-color: transparent; border-bottom-color: transparent; } -/** - * 1. Allow container to determine font-size and line-height. - * 2. Override inherited Bootstrap styles. - */ -.kuiToggleButton { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - cursor: pointer; - background-color: transparent; - border: none; - padding: 0; - font-size: inherit; - /* 1 */ - line-height: inherit; - /* 1 */ - color: #343741; } - .kuiToggleButton:focus { - color: #343741; } - .kuiToggleButton:active { - color: #006BB4 !important; - /* 2 */ } - .kuiToggleButton:hover:not(:disabled) { - color: #004d81 !important; - /* 2 */ - text-decoration: underline; } - .kuiToggleButton:disabled { - cursor: not-allowed; - opacity: .5; } - -/** - * 1. Make icon a consistent width so the text doesn't get pushed around as the icon changes - * between "expand" and "collapse". Use ems to be relative to inherited font-size. - */ -.kuiToggleButton__icon { - width: 0.8em; - /* 1 */ } - -.kuiTogglePanelHeader { - padding-bottom: 4px; - margin-bottom: 15px; - border-bottom: 1px solid #D3DAE6; - /** - * 1. Allow the user to click anywhere on the header, not just on the button text. - */ } - .kuiTogglePanelHeader .kuiToggleButton { - width: 100%; - /* 1 */ - text-align: left; - /* 1 */ } - .kuiToolBar { display: -webkit-box; display: -webkit-flex; diff --git a/packages/kbn-ui-framework/doc_site/src/services/routes/routes.js b/packages/kbn-ui-framework/doc_site/src/services/routes/routes.js index e7cd43d35e2b9..32912d5eb9c86 100644 --- a/packages/kbn-ui-framework/doc_site/src/services/routes/routes.js +++ b/packages/kbn-ui-framework/doc_site/src/services/routes/routes.js @@ -19,184 +19,136 @@ import Slugify from '../string/slugify'; -import BarExample - from '../../views/bar/bar_example'; +import BarExample from '../../views/bar/bar_example'; -import ButtonExample - from '../../views/button/button_example'; +import ButtonExample from '../../views/button/button_example'; -import CollapseButtonExample - from '../../views/collapse_button/collapse_button_example'; +import CollapseButtonExample from '../../views/collapse_button/collapse_button_example'; -import ExpressionExample - from '../../views/expression/expression_example'; -import FormExample - from '../../views/form/form_example'; +import FormExample from '../../views/form/form_example'; -import FormLayoutExample - from '../../views/form_layout/form_layout_example'; +import FormLayoutExample from '../../views/form_layout/form_layout_example'; -import IconExample - from '../../views/icon/icon_example'; +import IconExample from '../../views/icon/icon_example'; -import InfoPanelExample - from '../../views/info_panel/info_panel_example'; +import InfoPanelExample from '../../views/info_panel/info_panel_example'; -import LinkExample - from '../../views/link/link_example'; +import LinkExample from '../../views/link/link_example'; -import LocalNavExample - from '../../views/local_nav/local_nav_example'; +import LocalNavExample from '../../views/local_nav/local_nav_example'; -import MenuExample - from '../../views/menu/menu_example'; +import PagerExample from '../../views/pager/pager_example'; -import MenuButtonExample - from '../../views/menu_button/menu_button_example'; +import PanelExample from '../../views/panel/panel_example'; -import ModalExample - from '../../views/modal/modal_example'; +import EmptyTablePromptExample from '../../views/empty_table_prompt/empty_table_prompt_example'; -import PagerExample - from '../../views/pager/pager_example'; +import StatusTextExample from '../../views/status_text/status_text_example'; -import PanelExample - from '../../views/panel/panel_example'; +import TableExample from '../../views/table/table_example'; -import PanelSimpleExample - from '../../views/panel_simple/panel_simple_example'; +import TabsExample from '../../views/tabs/tabs_example'; -import PopoverExample - from '../../views/popover/popover_example'; +import ToolBarExample from '../../views/tool_bar/tool_bar_example'; -import EmptyTablePromptExample - from '../../views/empty_table_prompt/empty_table_prompt_example'; +import TypographyExample from '../../views/typography/typography_example'; -import StatusTextExample - from '../../views/status_text/status_text_example'; +import VerticalRhythmExample from '../../views/vertical_rhythm/vertical_rhythm_example'; -import TableExample - from '../../views/table/table_example'; - -import TabsExample - from '../../views/tabs/tabs_example'; - -import ToggleButtonExample - from '../../views/toggle_button/toggle_button_example'; - -import ToolBarExample - from '../../views/tool_bar/tool_bar_example'; - -import TypographyExample - from '../../views/typography/typography_example'; - -import VerticalRhythmExample - from '../../views/vertical_rhythm/vertical_rhythm_example'; - -import ViewSandbox - from '../../views/view/view_sandbox'; +import ViewSandbox from '../../views/view/view_sandbox'; // Component route names should match the component name exactly. -const components = [{ - name: 'Bar', - component: BarExample, - hasReact: true, -}, { - name: 'Button', - component: ButtonExample, - hasReact: true, -}, { - name: 'CollapseButton', - component: CollapseButtonExample, - hasReact: true, -}, { - name: 'CollapseButton', - component: CollapseButtonExample, - hasReact: true, -}, { - name: 'EmptyTablePrompt', - component: EmptyTablePromptExample, - hasReact: true, -}, { - name: 'Expression', - component: ExpressionExample, - hasReact: true, -}, { - name: 'Form', - component: FormExample, -}, { - name: 'FormLayout', - component: FormLayoutExample, - hasReact: true, -}, { - name: 'Icon', - component: IconExample, -}, { - name: 'InfoPanel', - component: InfoPanelExample, -}, { - name: 'Link', - component: LinkExample, -}, { - name: 'LocalNav', - component: LocalNavExample, - hasReact: true, -}, { - name: 'Menu', - component: MenuExample, - hasReact: true, -}, { - name: 'MenuButton', - component: MenuButtonExample, -}, { - name: 'Modal', - component: ModalExample, - hasReact: true, -}, { - name: 'Pager', - component: PagerExample, - hasReact: true, -}, { - name: 'Panel', - component: PanelExample, -}, { - name: 'PanelSimple', - component: PanelSimpleExample, - hasReact: true, -}, { - name: 'Popover', - component: PopoverExample, - hasReact: true, -}, { - name: 'StatusText', - component: StatusTextExample, -}, { - name: 'Table', - component: TableExample, - hasReact: true, -}, { - name: 'Tabs', - component: TabsExample, - hasReact: true, -}, { - name: 'ToggleButton', - component: ToggleButtonExample, -}, { - name: 'ToolBar', - component: ToolBarExample, - hasReact: true, -}, { - name: 'Typography', - component: TypographyExample, -}, { - name: 'VerticalRhythm', - component: VerticalRhythmExample, -}]; - -const sandboxes = [{ - name: 'View', - component: ViewSandbox, -}]; +const components = [ + { + name: 'Bar', + component: BarExample, + hasReact: true, + }, + { + name: 'Button', + component: ButtonExample, + hasReact: true, + }, + { + name: 'CollapseButton', + component: CollapseButtonExample, + hasReact: true, + }, + { + name: 'EmptyTablePrompt', + component: EmptyTablePromptExample, + hasReact: true, + }, + { + name: 'Form', + component: FormExample, + }, + { + name: 'FormLayout', + component: FormLayoutExample, + hasReact: true, + }, + { + name: 'Icon', + component: IconExample, + }, + { + name: 'InfoPanel', + component: InfoPanelExample, + }, + { + name: 'Link', + component: LinkExample, + }, + { + name: 'LocalNav', + component: LocalNavExample, + hasReact: true, + }, + { + name: 'Pager', + component: PagerExample, + hasReact: true, + }, + { + name: 'Panel', + component: PanelExample, + }, + { + name: 'StatusText', + component: StatusTextExample, + }, + { + name: 'Table', + component: TableExample, + hasReact: true, + }, + { + name: 'Tabs', + component: TabsExample, + hasReact: true, + }, + { + name: 'ToolBar', + component: ToolBarExample, + hasReact: true, + }, + { + name: 'Typography', + component: TypographyExample, + }, + { + name: 'VerticalRhythm', + component: VerticalRhythmExample, + }, +]; + +const sandboxes = [ + { + name: 'View', + component: ViewSandbox, + }, +]; const allRoutes = components.concat(sandboxes); diff --git a/packages/kbn-ui-framework/doc_site/src/views/expression/expression.js b/packages/kbn-ui-framework/doc_site/src/views/expression/expression.js deleted file mode 100644 index 754b083c72cd1..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/expression/expression.js +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { - KuiExpression, - KuiExpressionButton, - KuiFieldGroup, - KuiFieldGroupSection, - KuiPopover, - KuiPopoverTitle, -} from '../../../../components'; - - -class KuiExpressionItemExample extends React.Component { - constructor(props) { - super(props); - - this.state = { - example1: { - isOpen: false, - value: 'count()' - }, - example2: { - object: 'A', - value: '100', - description: 'Is above' - }, - }; - } - - openExample1 = () => { - this.setState({ - example1: { - ...this.state.example1, - isOpen: true, - }, - example2: { - ...this.state.example2, - isOpen: false, - }, - }); - }; - - closeExample1 = () => { - this.setState({ - example1: { - ...this.state.example1, - isOpen: false, - }, - }); - }; - - openExample2 = () => { - this.setState({ - example1: { - ...this.state.example1, - isOpen: false, - }, - example2: { - ...this.state.example2, - isOpen: true, - }, - }); - }; - - closeExample2 = () => { - this.setState({ - example2: { - ...this.state.example2, - isOpen: false, - }, - }); - }; - - changeExample1 = (event) => { - this.setState({ example1: { ...this.state.example1, value: event.target.value } }); - } - - changeExample2Object = (event) => { - this.setState({ example2: { ...this.state.example2, object: event.target.value } }); - } - - changeExample2Value = (event) => { - this.setState({ example2: { ...this.state.example2, value: event.target.value } }); - } - - changeExample2Description = (event) => { - this.setState({ example2: { ...this.state.example2, description: event.target.value } }); - } - - render() { - // Rise the popovers above GuidePageSideNav - const popoverStyle = { zIndex: '200' }; - - return ( - - - - )} - isOpen={this.state.example1.isOpen} - closePopover={this.closeExample1} - panelPaddingSize="none" - withTitle - > - {this.getPopover1(popoverStyle)} - - - - - - )} - isOpen={this.state.example2.isOpen} - closePopover={this.closeExample2} - panelPaddingSize="none" - withTitle - anchorPosition="left" - > - {this.getPopover2(popoverStyle)} - - - - ); - } - - getPopover1(popoverStyle) { - return ( -
- When - - - -
- ); - } - - getPopover2(popoverStyle) { - return ( -
- {this.state.example2.description} - - - - - - - -
- ); - } -} - -KuiExpressionItemExample.propTypes = { - defaultActiveButton: PropTypes.string.isRequired -}; - -export default KuiExpressionItemExample; diff --git a/packages/kbn-ui-framework/doc_site/src/views/expression/expression_example.js b/packages/kbn-ui-framework/doc_site/src/views/expression/expression_example.js deleted file mode 100644 index c85c8305d6852..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/expression/expression_example.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* eslint-disable import/no-duplicates */ - -import React from 'react'; -import { renderToHtml } from '../../services'; - -import { - GuideDemo, - GuidePage, - GuideSection, - GuideSectionTypes, - GuideText, -} from '../../components'; - -import Expression from './expression'; -import expressionSource from '!!raw-loader!./expression'; -const expressionHtml = renderToHtml(Expression, { defaultActiveButton: 'example2' }); - -export default props => ( - - - - ExpressionButtons allow you to compress a complicated form into a small space. - - - - - - - - -); diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu/menu.js b/packages/kbn-ui-framework/doc_site/src/views/menu/menu.js deleted file mode 100644 index ca9b6ccc1925f..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu/menu.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; - -import { - KuiMenu, - KuiMenuItem, -} from '../../../../components'; - -export default () => ( -
- - -

Item A

-
- - -

Item B

-
- - -

Item C

-
-
-
-); diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu/menu_contained.js b/packages/kbn-ui-framework/doc_site/src/views/menu/menu_contained.js deleted file mode 100644 index 0ab679613942b..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu/menu_contained.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; - -import { - KuiMenu, - KuiMenuItem, -} from '../../../../components'; - -export default () => ( -
- - -

Item A

-
- - -

Item B

-
- - -

Item C

-
-
-
-); diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu/menu_example.js b/packages/kbn-ui-framework/doc_site/src/views/menu/menu_example.js deleted file mode 100644 index 8e31432a1cccf..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu/menu_example.js +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* eslint-disable import/no-duplicates */ - -import React from 'react'; - -import { renderToHtml } from '../../services'; - -import { - GuideDemo, - GuidePage, - GuideSection, - GuideSectionTypes, -} from '../../components'; - -import Menu from './menu'; -import menuSource from '!!raw-loader!./menu'; -const menuHtml = renderToHtml(Menu); - -import MenuContained from './menu_contained'; -import menuContainedSource from '!!raw-loader!./menu_contained'; -const menuContainedHtml = renderToHtml(MenuContained); - -export default props => ( - - - -
- - - - - - - - - -); diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_basic.html b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_basic.html deleted file mode 100644 index 580701220dfef..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_basic.html +++ /dev/null @@ -1,9 +0,0 @@ - - -
- - diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_danger.html b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_danger.html deleted file mode 100644 index 07463ba68ef55..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_danger.html +++ /dev/null @@ -1,9 +0,0 @@ - - -
- - diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_elements.html b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_elements.html deleted file mode 100644 index b8090e21c2991..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_elements.html +++ /dev/null @@ -1,17 +0,0 @@ - - -  - - - -  - -
- Anchor element - diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_example.js b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_example.js deleted file mode 100644 index 6677a7b29e528..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_example.js +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; - -import { - GuideDemo, - GuidePage, - GuideSection, - GuideSectionTypes, - GuideText, -} from '../../components'; - -import basicHtml from './menu_button_basic.html'; -import primaryHtml from './menu_button_primary.html'; -import dangerHtml from './menu_button_danger.html'; -import withIconHtml from './menu_button_with_icon.html'; -import groupHtml from './menu_button_group.html'; -import elementsHtml from './menu_button_elements.html'; - -export default props => ( - - - - - - - - - - - - - - - - You can use a MenuButton with an Icon, with or without text. - - - - - - - - - - - - You can create a MenuButton using a button element, link, or input[type=“submit”]. - - - - - -); diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_group.html b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_group.html deleted file mode 100644 index 05d959d730dcf..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_group.html +++ /dev/null @@ -1,19 +0,0 @@ -
- - - -
- -
- -
- -
diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_primary.html b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_primary.html deleted file mode 100644 index 67bed3fcbf81a..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_primary.html +++ /dev/null @@ -1,9 +0,0 @@ - - -
- - diff --git a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_with_icon.html b/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_with_icon.html deleted file mode 100644 index 9ef05dca41a10..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/menu_button/menu_button_with_icon.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- - - -
- - diff --git a/packages/kbn-ui-framework/doc_site/src/views/modal/confirm_modal.js b/packages/kbn-ui-framework/doc_site/src/views/modal/confirm_modal.js deleted file mode 100644 index 8ef7e88fe1496..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/modal/confirm_modal.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { - Component, -} from 'react'; - -import { - KuiButton, - KuiConfirmModal, - KuiModalOverlay, - KUI_MODAL_CONFIRM_BUTTON, -} from '../../../../components'; - -export class ConfirmModalExample extends Component { - constructor(props) { - super(props); - - this.state = { - isModalVisible: false, - }; - - this.closeModal = this.closeModal.bind(this); - this.showModal = this.showModal.bind(this); - } - - closeModal() { - this.setState({ isModalVisible: false }); - } - - showModal() { - this.setState({ isModalVisible: true }); - } - - render() { - let modal; - - if (this.state.isModalVisible) { - modal = ( - - -

You’re about to do something.

-

Are you sure you want to do this?

-
-
- ); - } - - return ( -
- - Show ConfirmModal - - - {modal} -
- ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/modal/modal.js b/packages/kbn-ui-framework/doc_site/src/views/modal/modal.js deleted file mode 100644 index 52fd031ccf428..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/modal/modal.js +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { - Component, -} from 'react'; - -import { - KuiButton, - KuiModal, - KuiModalBody, - KuiModalFooter, - KuiModalHeader, - KuiModalHeaderTitle, - KuiModalOverlay, -} from '../../../../components'; - -export class ModalExample extends Component { - constructor(props) { - super(props); - - this.state = { - isModalVisible: false, - }; - } - - closeModal = () => { - this.setState({ isModalVisible: false }); - }; - - showModal = () => { - this.setState({ isModalVisible: true }); - }; - - render() { - let modal; - - if (this.state.isModalVisible) { - modal = ( - - - - - Modal - - - - -

- You can put anything you want in here! -

-
- - - - Cancel - - - - Save - - -
-
- ); - } - return ( -
- - Show Modal - - - {modal} -
- ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/modal/modal_example.js b/packages/kbn-ui-framework/doc_site/src/views/modal/modal_example.js deleted file mode 100644 index b21ee91ddb193..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/modal/modal_example.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* eslint-disable import/no-duplicates */ - -import React from 'react'; - -import { renderToHtml } from '../../services'; - -import { - GuideCode, - GuideDemo, - GuidePage, - GuideSection, - GuideSectionTypes, - GuideText, -} from '../../components'; - -import { ModalExample } from './modal'; -import modalSource from '!!raw-loader!./modal'; // eslint-disable-line import/default -const modalHtml = renderToHtml(ModalExample); - -import { ConfirmModalExample } from './confirm_modal'; -import confirmModalSource from '!!raw-loader!./confirm_modal'; // eslint-disable-line import/default -const confirmModalHtml = renderToHtml(ConfirmModalExample); - -export default props => ( - - - - - Use a KuiModal to temporarily escape the current UX and create - another UX within it. - - - - - - - - - - - - - -); diff --git a/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple.js b/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple.js deleted file mode 100644 index 376e603358338..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple.js +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; - -import { - KuiPanelSimple, -} from '../../../../components'; - -export default () => ( -
- - sizePadding="none" - - -
- - - sizePadding="s" - - -
- - - sizePadding="m" - - -
- - - sizePadding="l" - - -
- - - sizePadding="l", hasShadow - -
-); diff --git a/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple_example.js b/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple_example.js deleted file mode 100644 index 3f7ffa2eec089..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/panel_simple/panel_simple_example.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* eslint-disable import/no-duplicates */ - -import React from 'react'; - -import { Link } from 'react-router'; - -import { renderToHtml } from '../../services'; - -import { - GuideCode, - GuideDemo, - GuidePage, - GuideSection, - GuideSectionTypes, - GuideText, -} from '../../components'; - -import PanelSimple from './panel_simple'; -import panelSimpleSource from '!!raw-loader!./panel_simple'; -const panelSimpleHtml = renderToHtml(PanelSimple); - -export default props => ( - - - - PanelSimple is a simple wrapper component to add - depth to a contained layout. It it commonly used as a base for - other larger components like Popover. - - - - - - - -); diff --git a/packages/kbn-ui-framework/doc_site/src/views/popover/popover.js b/packages/kbn-ui-framework/doc_site/src/views/popover/popover.js deleted file mode 100644 index 59e47ef348b74..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/popover/popover.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { - Component, -} from 'react'; - -import { - KuiPopover, - KuiButton, -} from '../../../../components'; - -export default class extends Component { - constructor(props) { - super(props); - - this.state = { - isPopoverOpen: false, - }; - } - - onButtonClick() { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); - } - - closePopover() { - this.setState({ - isPopoverOpen: false, - }); - } - - render() { - const button = ( - - Show popover - - ); - - return ( - -
Popover content that’s wider than the default width
-
- ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_anchor_position.js b/packages/kbn-ui-framework/doc_site/src/views/popover/popover_anchor_position.js deleted file mode 100644 index 54e0f97542acd..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_anchor_position.js +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { - Component, -} from 'react'; - -import { - KuiPopover, - KuiButton, -} from '../../../../components'; - -export default class extends Component { - constructor(props) { - super(props); - - this.state = { - isPopoverOpen1: false, - isPopoverOpen2: false, - }; - } - - onButtonClick1() { - this.setState({ - isPopoverOpen1: !this.state.isPopoverOpen1, - }); - } - - closePopover1() { - this.setState({ - isPopoverOpen1: false, - }); - } - - onButtonClick2() { - this.setState({ - isPopoverOpen2: !this.state.isPopoverOpen2, - }); - } - - closePopover2() { - this.setState({ - isPopoverOpen2: false, - }); - } - - render() { - return ( -
- - Popover anchored to the right. - - )} - isOpen={this.state.isPopoverOpen1} - closePopover={this.closePopover1.bind(this)} - anchorPosition="right" - > - Popover content - - -   - - - Popover anchored to the left. - - )} - isOpen={this.state.isPopoverOpen2} - closePopover={this.closePopover2.bind(this)} - anchorPosition="left" - > - Popover content - -
- ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_body_class_name.js b/packages/kbn-ui-framework/doc_site/src/views/popover/popover_body_class_name.js deleted file mode 100644 index e347c5a321187..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_body_class_name.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { - Component, -} from 'react'; - -import { - KuiPopover, - KuiButton -} from '../../../../components'; - -export default class extends Component { - constructor(props) { - super(props); - - this.state = { - isPopoverOpen: false, - }; - } - - onButtonClick() { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); - } - - closePopover() { - this.setState({ - isPopoverOpen: false, - }); - } - - render() { - return ( - - Custom class - - )} - isOpen={this.state.isPopoverOpen} - closePopover={this.closePopover.bind(this)} - bodyClassName="yourClassNameHere" - > - It’s hard to tell but there’s a custom class on this element - - ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_example.js b/packages/kbn-ui-framework/doc_site/src/views/popover/popover_example.js deleted file mode 100644 index 90317ad5c56cd..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_example.js +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* eslint-disable import/no-duplicates */ - -import React from 'react'; - -import { renderToHtml } from '../../services'; - -import { - GuideCode, - GuideDemo, - GuidePage, - GuideSection, - GuideSectionTypes, - GuideText, -} from '../../components'; - -import Popover from './popover'; -import popoverSource from '!!raw-loader!./popover'; -const popoverHtml = renderToHtml(Popover); - -import TrapFocus from './trap_focus'; -import trapFocusSource from '!!raw-loader!./trap_focus'; -const trapFocusHtml = renderToHtml(TrapFocus); - -import PopoverAnchorPosition from './popover_anchor_position'; -import popoverAnchorPositionSource from '!!raw-loader!./popover_anchor_position'; -const popoverAnchorPositionHtml = renderToHtml(PopoverAnchorPosition); - -import PopoverPanelClassName from './popover_panel_class_name'; -import popoverPanelClassNameSource from '!!raw-loader!./popover_panel_class_name'; -const popoverPanelClassNameHtml = renderToHtml(PopoverPanelClassName); - -import PopoverWithTitle from './popover_with_title'; -import popoverWithTitleSource from '!!raw-loader!./popover_with_title'; -const popoverWithTitleHtml = renderToHtml(PopoverWithTitle); - -export default props => ( - - - - Use the Popover component to hide controls or options behind a clickable element. - - - - - - - - - - If the Popover should be responsible for trapping the focus within itself (as opposed - to a child component), then you should set ownFocus. - - - - - - - - - - Popovers often have need for titling. This can be applied through - a prop or used separately as its own component - KuiPopoverTitle nested somewhere in the child - prop. - - - - - - - - - - The alignment and arrow on your popover can be set with - the anchorPosition prop. - - - - - - - - - - Use the panelPaddingSize prop to adjust the padding - on the panel within the panel. Use the panelClassName - prop to pass a custom class to the panel. - inside a popover. - - - - - - - -); diff --git a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_panel_class_name.js b/packages/kbn-ui-framework/doc_site/src/views/popover/popover_panel_class_name.js deleted file mode 100644 index c3b98fc6ca3d1..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_panel_class_name.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { - Component, -} from 'react'; - -import { - KuiPopover, - KuiButton, -} from '../../../../components'; - -export default class extends Component { - constructor(props) { - super(props); - - this.state = { - isPopoverOpen: false, - }; - } - - onButtonClick() { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); - } - - closePopover() { - this.setState({ - isPopoverOpen: false, - }); - } - - render() { - return ( - - Turn padding off and apply a custom class - - )} - isOpen={this.state.isPopoverOpen} - closePopover={this.closePopover.bind(this)} - panelClassName="yourClassNameHere" - panelPaddingSize="none" - > - This should have no padding, and if you inspect, also a custom class. - - ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_with_title.js b/packages/kbn-ui-framework/doc_site/src/views/popover/popover_with_title.js deleted file mode 100644 index ff25a54e1d60e..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/popover/popover_with_title.js +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { - Component, -} from 'react'; - -import { - KuiPopover, - KuiPopoverTitle, - KuiButton, -} from '../../../../components'; - -export default class extends Component { - constructor(props) { - super(props); - - this.state = { - isPopoverOpen: false, - }; - } - - onButtonClick() { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); - } - - closePopover() { - this.setState({ - isPopoverOpen: false, - }); - } - - render() { - const button = ( - - Show popover with Title - - ); - - return ( - -
- Hello, I’m a popover title -

- Popover content that’s wider than the default width -

-
-
- ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/popover/trap_focus.js b/packages/kbn-ui-framework/doc_site/src/views/popover/trap_focus.js deleted file mode 100644 index b034da504f3d4..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/popover/trap_focus.js +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { - Component, -} from 'react'; - -import { - KuiButton, - KuiFieldGroup, - KuiFieldGroupSection, - KuiPopover, -} from '../../../../components'; - -export default class extends Component { - constructor(props) { - super(props); - - this.state = { - isPopoverOpen: false, - }; - } - - onButtonClick() { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); - } - - closePopover() { - this.setState({ - isPopoverOpen: false, - }); - } - - render() { - const button = ( - - Show popover - - ); - - return ( - -
-
- - -
-
- -
- - - - - - -
- -
- Save -
-
- - ); - } -} diff --git a/packages/kbn-ui-framework/doc_site/src/views/table/table_with_menu_buttons.js b/packages/kbn-ui-framework/doc_site/src/views/table/table_with_menu_buttons.js index 4b1b063876861..49b4952de63a6 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/table/table_with_menu_buttons.js +++ b/packages/kbn-ui-framework/doc_site/src/views/table/table_with_menu_buttons.js @@ -28,86 +28,29 @@ import { KuiTableBody, } from '../../../../components'; -import { - RIGHT_ALIGNMENT -} from '../../../../src/services'; - export function TableWithMenuButtons() { return ( - - Reminder - - - A - - - B - - - C - - - Actions - + Reminder + A + B + C - - Core temperature critical - - - A - - - B - - - C - - -
- - - -
-
+ Core temperature critical + A + B + C
- - Time for your snack - - - A - - - B - - - C - - -
- - - -
-
+ Time for your snack + A + B + C
diff --git a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button.html b/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button.html deleted file mode 100644 index 33ae80390fe38..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button.html +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button.js b/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button.js deleted file mode 100644 index 90fec3871cdc8..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button.js +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable */ - -let isButtonCollapsed = true; -const $toggleButton = $('[data-id="toggleButton"]'); -const $toggleButtonIcon = $('[data-id="toggleButtonIcon"]'); - -$toggleButton.on('click', () => { - isButtonCollapsed = !isButtonCollapsed; - - if (isButtonCollapsed) { - $toggleButtonIcon.addClass('fa-caret-right'); - $toggleButtonIcon.removeClass('fa-caret-down'); - } else { - $toggleButtonIcon.removeClass('fa-caret-right'); - $toggleButtonIcon.addClass('fa-caret-down'); - } -}); diff --git a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_disabled.html b/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_disabled.html deleted file mode 100644 index 0b3790f5084ec..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_disabled.html +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_example.js b/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_example.js deleted file mode 100644 index 6fd3a816e4fc4..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_button_example.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; - -import { - GuideDemo, - GuidePage, - GuideSection, - GuideSectionTypes, - GuideText, -} from '../../components'; - -import { - Link, -} from 'react-router'; - -import toggleButtonHtml from './toggle_button.html'; -import toggleButtonJs from 'raw-loader!./toggle_button.js'; -import toggleButtonDisabledHtml from './toggle_button_disabled.html'; -import togglePanelHtml from './toggle_panel.html'; -import togglePanelJs from 'raw-loader!./toggle_panel.js'; - -export default props => ( - - - - You can use this button to reveal and hide content. For a complete example - on how to make an collapsable panel proper accessible, read - the CollapseButton documentation. - - - - - - - - - - - - - -); diff --git a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_panel.html b/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_panel.html deleted file mode 100644 index 561a307d4c99e..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_panel.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
- -
- -
-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis egestas dictum enim non lobortis. Curabitur vel viverra metus. Ut non dignissim neque. Nulla metus lorem, maximus et vehicula vel, pharetra vitae sem. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Etiam mi risus, varius in elit a, scelerisque blandit dolor. Donec ut leo mi. Duis ac tincidunt urna. Sed finibus eros odio, vitae euismod turpis ullamcorper pulvinar. Sed in ipsum at magna euismod tristique. Donec eget orci blandit, convallis odio sed, hendrerit dui. Aenean augue nibh, hendrerit sit amet aliquet et, efficitur sit amet nulla. Vivamus placerat pulvinar ipsum, dignissim sodales libero pulvinar sit amet. Maecenas pellentesque neque at quam varius aliquam. Ut at pretium augue, a pellentesque neque.

-
-
diff --git a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_panel.js b/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_panel.js deleted file mode 100644 index 897662a814cf8..0000000000000 --- a/packages/kbn-ui-framework/doc_site/src/views/toggle_button/toggle_panel.js +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable */ - -let isButtonCollapsed = true; -const $togglePanelButton = $('[data-id="togglePanelButton"]'); -const $togglePanelButtonIcon = $('[data-id="togglePanelButtonIcon"]'); -const $togglePanelContent = $('[data-id="togglePanelContent"]'); - -$togglePanelButton.on('click', () => { - isButtonCollapsed = !isButtonCollapsed; - - $togglePanelButton.attr('aria-expanded', !isButtonCollapsed); - - if (isButtonCollapsed) { - $togglePanelButtonIcon.addClass('fa-caret-right'); - $togglePanelButtonIcon.removeClass('fa-caret-down'); - $togglePanelContent.hide(); - } else { - $togglePanelButtonIcon.removeClass('fa-caret-right'); - $togglePanelButtonIcon.addClass('fa-caret-down'); - $togglePanelContent.show(); - } -}); - -$togglePanelContent.hide(); diff --git a/packages/kbn-ui-framework/src/components/expression/__snapshots__/expression.test.js.snap b/packages/kbn-ui-framework/src/components/expression/__snapshots__/expression.test.js.snap deleted file mode 100644 index 6d83f6aad8da3..0000000000000 --- a/packages/kbn-ui-framework/src/components/expression/__snapshots__/expression.test.js.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`KuiExpression Props children is rendered 1`] = ` -
- some expression -
-`; - -exports[`KuiExpression renders 1`] = ` -
-`; diff --git a/packages/kbn-ui-framework/src/components/expression/__snapshots__/expression_button.test.js.snap b/packages/kbn-ui-framework/src/components/expression/__snapshots__/expression_button.test.js.snap deleted file mode 100644 index c3bbae52bd653..0000000000000 --- a/packages/kbn-ui-framework/src/components/expression/__snapshots__/expression_button.test.js.snap +++ /dev/null @@ -1,57 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`KuiExpressionButton Props isActive false renders inactive 1`] = ` - -`; - -exports[`KuiExpressionButton Props isActive true renders active 1`] = ` - -`; - -exports[`KuiExpressionButton renders 1`] = ` - -`; diff --git a/packages/kbn-ui-framework/src/components/expression/_expression.scss b/packages/kbn-ui-framework/src/components/expression/_expression.scss deleted file mode 100644 index bb01f5fa83593..0000000000000 --- a/packages/kbn-ui-framework/src/components/expression/_expression.scss +++ /dev/null @@ -1,27 +0,0 @@ -.kuiExpression { - padding: 20px; - white-space: nowrap; -} - -.kuiExpressionButton { - background-color: transparent; - padding: 5px 0px; - border: none; - border-bottom: dotted 2px $kuiBorderColor; - font-size: $kuiFontSize; - cursor: pointer; -} - -.kuiExpressionButton__description { - color: $expressionColorHighlight; - text-transform: uppercase; -} - -.kuiExpressionButton__value { - color: $kuiTextColor; - text-transform: lowercase; -} - -.kuiExpressionButton-isActive { - border-bottom: solid 2px $expressionColorHighlight; -} diff --git a/packages/kbn-ui-framework/src/components/expression/_index.scss b/packages/kbn-ui-framework/src/components/expression/_index.scss deleted file mode 100644 index 7d806e314a273..0000000000000 --- a/packages/kbn-ui-framework/src/components/expression/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -$expressionColorHighlight: $euiColorSecondary; - -@import "expression"; diff --git a/packages/kbn-ui-framework/src/components/expression/expression.js b/packages/kbn-ui-framework/src/components/expression/expression.js deleted file mode 100644 index 4464a1b742ba1..0000000000000 --- a/packages/kbn-ui-framework/src/components/expression/expression.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export const KuiExpression = ({ - children, - className, - ...rest -}) => { - const classes = classNames('kuiExpression', className); - - return ( -
- {children} -
- ); -}; - -KuiExpression.propTypes = { - children: PropTypes.node, - className: PropTypes.string -}; diff --git a/packages/kbn-ui-framework/src/components/expression/expression.test.js b/packages/kbn-ui-framework/src/components/expression/expression.test.js deleted file mode 100644 index 5043dff09aa34..0000000000000 --- a/packages/kbn-ui-framework/src/components/expression/expression.test.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiExpression, -} from './expression'; - -describe('KuiExpression', () => { - test('renders', () => { - const component = ( - - ); - - expect(render(component)).toMatchSnapshot(); - }); - - describe('Props', () => { - describe('children', () => { - test('is rendered', () => { - const component = render( - - some expression - - ); - - expect(component) - .toMatchSnapshot(); - }); - }); - }); -}); diff --git a/packages/kbn-ui-framework/src/components/expression/expression_button.js b/packages/kbn-ui-framework/src/components/expression/expression_button.js deleted file mode 100644 index fd63469c1eff3..0000000000000 --- a/packages/kbn-ui-framework/src/components/expression/expression_button.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export const KuiExpressionButton = ({ - className, - description, - buttonValue, - isActive, - onClick, - ...rest -}) => { - const classes = classNames('kuiExpressionButton', className, { - 'kuiExpressionButton-isActive': isActive - }); - - return ( - - ); -}; - -KuiExpressionButton.propTypes = { - className: PropTypes.string, - description: PropTypes.string.isRequired, - buttonValue: PropTypes.string.isRequired, - isActive: PropTypes.bool.isRequired, - onClick: PropTypes.func.isRequired, -}; - -KuiExpressionButton.defaultProps = { - isActive: false, -}; diff --git a/packages/kbn-ui-framework/src/components/expression/expression_button.test.js b/packages/kbn-ui-framework/src/components/expression/expression_button.test.js deleted file mode 100644 index 69149b947662f..0000000000000 --- a/packages/kbn-ui-framework/src/components/expression/expression_button.test.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render, shallow } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; -import sinon from 'sinon'; - -import { - KuiExpressionButton, -} from './expression_button'; - -describe('KuiExpressionButton', () => { - test('renders', () => { - const component = ( - {}} - {...requiredProps} - /> - ); - - expect(render(component)).toMatchSnapshot(); - }); - - describe('Props', () => { - describe('isActive', () => { - test('true renders active', () => { - const component = ( - {}} - /> - ); - - expect(render(component)).toMatchSnapshot(); - }); - - test('false renders inactive', () => { - const component = ( - {}} - /> - ); - - expect(render(component)).toMatchSnapshot(); - }); - }); - - describe('onClick', () => { - test('is called when the button is clicked', () => { - const onClickHandler = sinon.spy(); - - const button = shallow( - - ); - - button.simulate('click'); - - sinon.assert.calledOnce(onClickHandler); - }); - }); - }); -}); diff --git a/packages/kbn-ui-framework/src/components/expression/index.js b/packages/kbn-ui-framework/src/components/expression/index.js deleted file mode 100644 index 387e42553a622..0000000000000 --- a/packages/kbn-ui-framework/src/components/expression/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { KuiExpression } from './expression'; -export { KuiExpressionButton } from './expression_button'; diff --git a/packages/kbn-ui-framework/src/components/index.js b/packages/kbn-ui-framework/src/components/index.js index d1a94257abc9a..7bc0f4aaf2cf1 100644 --- a/packages/kbn-ui-framework/src/components/index.js +++ b/packages/kbn-ui-framework/src/components/index.js @@ -17,22 +17,11 @@ * under the License. */ -export { - KuiBar, - KuiBarSection, -} from './bar'; +export { KuiBar, KuiBarSection } from './bar'; -export { - KuiButton, - KuiButtonGroup, - KuiButtonIcon, - KuiLinkButton, - KuiSubmitButton, -} from './button'; +export { KuiButton, KuiButtonGroup, KuiButtonIcon, KuiLinkButton, KuiSubmitButton } from './button'; -export { - KuiCollapseButton, -} from './collapse_button'; +export { KuiCollapseButton } from './collapse_button'; export { KuiEmptyTablePrompt, @@ -40,15 +29,7 @@ export { KuiEmptyTablePromptPanel, } from './empty_table_prompt'; -export { - KuiExpression, - KuiExpressionButton, -} from './expression'; - -export { - KuiFieldGroup, - KuiFieldGroupSection, -} from './form_layout'; +export { KuiFieldGroup, KuiFieldGroupSection } from './form_layout'; export { KuiLabel, @@ -68,45 +49,9 @@ export { KuiLocalTitle, } from './local_nav'; -export { - KuiMenu, - KuiMenuItem, -} from './menu'; - -export { - KUI_MODAL_CANCEL_BUTTON, - KUI_MODAL_CONFIRM_BUTTON, - KuiConfirmModal, - KuiModal, - KuiModalBody, - KuiModalFooter, - KuiModalHeader, - KuiModalHeaderTitle, - KuiModalOverlay, -} from './modal'; - -export { - KuiOutsideClickDetector, -} from './outside_click_detector'; +export { KuiPager, KuiPagerButtonGroup } from './pager'; -export { - KuiPager, - KuiPagerButtonGroup, -} from './pager'; - -export { - KuiPanelSimple, -} from './panel_simple'; - -export { - KuiPopover, - KuiPopoverTitle, -} from './popover'; - -export { - KuiTabs, - KuiTab -} from './tabs'; +export { KuiTabs, KuiTab } from './tabs'; export { KuiTable, @@ -123,7 +68,7 @@ export { KuiListingTableCreateButton, KuiListingTableDeleteButton, KuiListingTableNoMatchesPrompt, - KuiListingTableLoadingPrompt + KuiListingTableLoadingPrompt, } from './table'; export { @@ -132,5 +77,5 @@ export { KuiToolBarFooter, KuiToolBarSection, KuiToolBarFooterSection, - KuiToolBarText + KuiToolBarText, } from './tool_bar'; diff --git a/packages/kbn-ui-framework/src/components/index.scss b/packages/kbn-ui-framework/src/components/index.scss index 084e7045a393e..5e12ef30c8c9f 100644 --- a/packages/kbn-ui-framework/src/components/index.scss +++ b/packages/kbn-ui-framework/src/components/index.scss @@ -12,30 +12,23 @@ // When possible, if making changes to those legacy components, please think about // instead adding them to this library and deprecating that dependency. -@import "bar/index"; -@import "button/index"; -@import "collapse_button/index"; -@import "expression/index"; -@import "form/index"; -@import "form_layout/index"; -@import "icon/index"; -@import "info_panel/index"; -@import "link/index"; -@import "local_nav/index"; -@import "menu/index"; -@import "menu_button/index"; -@import "modal/index"; -@import "pager/index"; -@import "panel/index"; -@import "panel_simple/index"; -@import "popover/index"; -@import "empty_table_prompt/index"; -@import "status_text/index"; -@import "table/index"; -@import "table_info/index"; -@import "tabs/index"; -@import "toggle_button/index"; -@import "tool_bar/index"; -@import "typography/index"; -@import "vertical_rhythm/index"; -@import "view/index"; +@import 'bar/index'; +@import 'button/index'; +@import 'collapse_button/index'; +@import 'form/index'; +@import 'form_layout/index'; +@import 'icon/index'; +@import 'info_panel/index'; +@import 'link/index'; +@import 'local_nav/index'; +@import 'pager/index'; +@import 'panel/index'; +@import 'empty_table_prompt/index'; +@import 'status_text/index'; +@import 'table/index'; +@import 'table_info/index'; +@import 'tabs/index'; +@import 'tool_bar/index'; +@import 'typography/index'; +@import 'vertical_rhythm/index'; +@import 'view/index'; diff --git a/packages/kbn-ui-framework/src/components/menu/__snapshots__/menu.test.js.snap b/packages/kbn-ui-framework/src/components/menu/__snapshots__/menu.test.js.snap deleted file mode 100644 index d33aa9635e935..0000000000000 --- a/packages/kbn-ui-framework/src/components/menu/__snapshots__/menu.test.js.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`contained prop 1`] = ` -
    - children -
-`; - -exports[`renders KuiMenu 1`] = ` -
    - children -
-`; diff --git a/packages/kbn-ui-framework/src/components/menu/__snapshots__/menu_item.test.js.snap b/packages/kbn-ui-framework/src/components/menu/__snapshots__/menu_item.test.js.snap deleted file mode 100644 index a7e54006a6dc0..0000000000000 --- a/packages/kbn-ui-framework/src/components/menu/__snapshots__/menu_item.test.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiMenuItem 1`] = ` -
  • - children -
  • -`; diff --git a/packages/kbn-ui-framework/src/components/menu/_index.scss b/packages/kbn-ui-framework/src/components/menu/_index.scss deleted file mode 100644 index 1884d62ee83b9..0000000000000 --- a/packages/kbn-ui-framework/src/components/menu/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import "menu"; diff --git a/packages/kbn-ui-framework/src/components/menu/_menu.scss b/packages/kbn-ui-framework/src/components/menu/_menu.scss deleted file mode 100644 index 812a216153a26..0000000000000 --- a/packages/kbn-ui-framework/src/components/menu/_menu.scss +++ /dev/null @@ -1,26 +0,0 @@ -/** - * 1. Allow class to be applied to `ul` and `ol` elements - */ -.kuiMenu { - padding-left: 0; /* 1 */ -} - -.kuiMenu--contained { - border: $kuiBorderThin; - - .kuiMenuItem { - padding: 6px 10px; - } -} - -/** - * 1. Allow class to be applied to `li` elements - */ -.kuiMenuItem { - list-style: none; /* 1 */ - padding: 6px 0; - - & + & { - border-top: $kuiBorderThin; - } -} diff --git a/packages/kbn-ui-framework/src/components/menu/index.js b/packages/kbn-ui-framework/src/components/menu/index.js deleted file mode 100644 index 54a6e4e1d82dd..0000000000000 --- a/packages/kbn-ui-framework/src/components/menu/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { KuiMenu } from './menu'; -export { KuiMenuItem } from './menu_item'; diff --git a/packages/kbn-ui-framework/src/components/menu/menu.js b/packages/kbn-ui-framework/src/components/menu/menu.js deleted file mode 100644 index e0707293cca65..0000000000000 --- a/packages/kbn-ui-framework/src/components/menu/menu.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import PropTypes from 'prop-types'; -import React from 'react'; -import classNames from 'classnames'; - -export const KuiMenu = ({ - contained, - className, - children, - ...rest -}) => { - const classes = classNames('kuiMenu', className, { - 'kuiMenu--contained': contained - }); - - return ( -
      - {children} -
    - ); -}; - -KuiMenu.propTypes = { - contained: PropTypes.bool, - className: PropTypes.string, - children: PropTypes.node -}; diff --git a/packages/kbn-ui-framework/src/components/menu/menu.test.js b/packages/kbn-ui-framework/src/components/menu/menu.test.js deleted file mode 100644 index 1ea1e5044faa7..0000000000000 --- a/packages/kbn-ui-framework/src/components/menu/menu.test.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiMenu, -} from './menu'; - -test('renders KuiMenu', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); - -test('contained prop', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/menu/menu_item.js b/packages/kbn-ui-framework/src/components/menu/menu_item.js deleted file mode 100644 index b81d2053f75b1..0000000000000 --- a/packages/kbn-ui-framework/src/components/menu/menu_item.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import PropTypes from 'prop-types'; -import React from 'react'; - -import classNames from 'classnames'; - -export const KuiMenuItem = ({ - className, - children, - ...rest -}) => { - return ( -
  • - {children} -
  • - ); -}; - -KuiMenuItem.propTypes = { - className: PropTypes.string, - children: PropTypes.node -}; diff --git a/packages/kbn-ui-framework/src/components/menu/menu_item.test.js b/packages/kbn-ui-framework/src/components/menu/menu_item.test.js deleted file mode 100644 index e06c110777841..0000000000000 --- a/packages/kbn-ui-framework/src/components/menu/menu_item.test.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiMenuItem, -} from './menu_item'; - -test('renders KuiMenuItem', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/menu_button/_index.scss b/packages/kbn-ui-framework/src/components/menu_button/_index.scss deleted file mode 100644 index 1548f7e600bef..0000000000000 --- a/packages/kbn-ui-framework/src/components/menu_button/_index.scss +++ /dev/null @@ -1,20 +0,0 @@ -$menuButtonFontSize: 12px; - -$menuButtonBasicTextColor: $euiTextColor; -$menuButtonBasicBackgroundColor: $euiColorEmptyShade; -$menuButtonBasicHoverBackgroundColor: $euiColorLightestShade; -$menuButtonBasicDisabledTextColor: $euiColorMediumShade; -$menuButtonPrimaryTextColor: $euiColorGhost; -$menuButtonPrimaryBackgroundColor: $euiColorPrimary; -$menuButtonPrimaryHoverBackgroundColor: darken($euiColorPrimary, 10%); -$menuButtonPrimaryDisabledBackgroundColor: $euiColorMediumShade; - -$menuButtonDangerTextColor: $euiColorGhost; -$menuButtonDangerBackgroundColor: $euiColorDanger; -$menuButtonDangerHoverTextColor: $euiColorGhost; -$menuButtonDangerHoverBackgroundColor: darken($euiColorDanger, 10%); -$menuButtonDangerDisabledBackgroundColor: $euiColorMediumShade; -$menuButtonDangerHoverDisabledTextColor: $euiColorGhost; - -@import "menu_button"; -@import "menu_button_group"; diff --git a/packages/kbn-ui-framework/src/components/menu_button/_menu_button.scss b/packages/kbn-ui-framework/src/components/menu_button/_menu_button.scss deleted file mode 100644 index 2b9a8048746e9..0000000000000 --- a/packages/kbn-ui-framework/src/components/menu_button/_menu_button.scss +++ /dev/null @@ -1,118 +0,0 @@ -/** - * 1. Setting to inline-block guarantees the same height when applied to both - * button elements and anchor tags. - * 2. Disable for Angular. - * 3. Make the button just tall enough to fit inside an Option Layout. - */ -.kuiMenuButton { - display: inline-block; /* 1 */ - appearance: none; - cursor: pointer; - padding: 2px 10px; /* 3 */ - font-size: $menuButtonFontSize; - font-weight: $kuiFontWeightRegular; - line-height: $kuiLineHeight; - text-decoration: none; - border: none; - border-radius: $kuiBorderRadius; - - &:disabled { - cursor: default; - pointer-events: none; /* 2 */ - } - - &:active:enabled { - transform: translateY(1px); - } - - &:focus { - @include focus; - } -} - -.kuiMenuButton--iconText { - .kuiMenuButton__icon { - &:first-child { - margin-right: 4px; - } - - &:last-child { - margin-left: 4px; - } - } -} - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--basic { - color: $menuButtonBasicTextColor; - background-color: $menuButtonBasicBackgroundColor; - - // Goes before hover, so that hover can override it. - &:focus { - color: $menuButtonBasicTextColor !important; /* 1 */ - } - - &:hover, /* 2 */ - &:active { /* 2 */ - color: $menuButtonBasicTextColor !important; /* 1 */ - background-color: $menuButtonBasicHoverBackgroundColor; - } - - &:disabled { - color: $menuButtonBasicDisabledTextColor; - cursor: not-allowed; - } -} - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--primary { - color: $menuButtonPrimaryTextColor; - background-color: $menuButtonPrimaryBackgroundColor; - - // Goes before hover, so that hover can override it. - &:focus { - color: $menuButtonPrimaryTextColor !important; /* 1 */ - } - - &:hover, /* 2 */ - &:active { /* 2 */ - color: $menuButtonPrimaryTextColor !important; /* 1 */ - background-color: $menuButtonPrimaryHoverBackgroundColor; - } - - &:disabled { - background-color: $menuButtonPrimaryDisabledBackgroundColor; - cursor: not-allowed; - } -} - -/** - * 1. Override Bootstrap. - * 2. Safari won't respect :enabled:hover/active on links. - */ -.kuiMenuButton--danger { - color: $menuButtonDangerTextColor; - background-color: $menuButtonDangerBackgroundColor; - - &:hover, /* 2 */ - &:active { /* 2 */ - color: $menuButtonDangerHoverTextColor !important; /* 1 */ - background-color: $menuButtonDangerHoverBackgroundColor; - } - - &:disabled { - color: $menuButtonDangerHoverDisabledTextColor; - background-color: $menuButtonDangerDisabledBackgroundColor; - cursor: not-allowed; - } - - &:focus { - @include focus($kuiFocusDangerColor); - } -} diff --git a/packages/kbn-ui-framework/src/components/menu_button/_menu_button_group.scss b/packages/kbn-ui-framework/src/components/menu_button/_menu_button_group.scss deleted file mode 100644 index e8a53967e8b3a..0000000000000 --- a/packages/kbn-ui-framework/src/components/menu_button/_menu_button_group.scss +++ /dev/null @@ -1,11 +0,0 @@ -.kuiMenuButtonGroup { - display: flex; - - .kuiMenuButton + .kuiMenuButton { - margin-left: 4px; - } -} - -.kuiMenuButtonGroup--alignRight { - justify-content: flex-end; -} diff --git a/packages/kbn-ui-framework/src/components/modal/__snapshots__/confirm_modal.test.js.snap b/packages/kbn-ui-framework/src/components/modal/__snapshots__/confirm_modal.test.js.snap deleted file mode 100644 index b42842a6959c5..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/__snapshots__/confirm_modal.test.js.snap +++ /dev/null @@ -1,64 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiConfirmModal 1`] = ` -
    -
    -
    -
    - A confirmation modal -
    -
    -
    -
    -

    - This is a confirmation modal example -

    -
    -
    -
    - - -
    -
    -
    -`; diff --git a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal.test.js.snap b/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal.test.js.snap deleted file mode 100644 index f2c725e3b38e8..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal.test.js.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiModal 1`] = ` -
    -
    - children -
    -
    -`; diff --git a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_body.test.js.snap b/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_body.test.js.snap deleted file mode 100644 index b0ddad312d8bc..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_body.test.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiModalBody 1`] = ` -
    - children -
    -`; diff --git a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_footer.test.js.snap b/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_footer.test.js.snap deleted file mode 100644 index c4aaebfe75773..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_footer.test.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiModalFooter 1`] = ` -
    - children -
    -`; diff --git a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_header.test.js.snap b/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_header.test.js.snap deleted file mode 100644 index 61fa239e72322..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_header.test.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiModalHeader 1`] = ` -
    - children -
    -`; diff --git a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_header_title.test.js.snap b/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_header_title.test.js.snap deleted file mode 100644 index 938e828985786..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_header_title.test.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiModalHeaderTitle 1`] = ` -
    - children -
    -`; diff --git a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_overlay.test.js.snap b/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_overlay.test.js.snap deleted file mode 100644 index 536461942dd3e..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/__snapshots__/modal_overlay.test.js.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders KuiModalOverlay 1`] = ` -
    - children -
    -`; diff --git a/packages/kbn-ui-framework/src/components/modal/_index.scss b/packages/kbn-ui-framework/src/components/modal/_index.scss deleted file mode 100644 index fc6f1747b5073..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/_index.scss +++ /dev/null @@ -1,11 +0,0 @@ -$modalPadding: 10px; -$modalBorderColor: $euiBorderColor; -$modalBackgroundColor: $euiColorEmptyShade; -$kuiModalDepth: 1000; -$modalOverlayBackground: $euiColorEmptyShade; -@if (lightness($euiTextColor) > 50) { - $modalOverlayBackground: $euiColorLightShade; -} - -@import "modal_overlay"; -@import "modal"; diff --git a/packages/kbn-ui-framework/src/components/modal/_modal.scss b/packages/kbn-ui-framework/src/components/modal/_modal.scss deleted file mode 100644 index 0564db6998378..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/_modal.scss +++ /dev/null @@ -1,63 +0,0 @@ -.kuiModal { - @include euiBottomShadow; - - line-height: $kuiLineHeight; - background-color: $modalBackgroundColor; - border: 1px solid $modalBorderColor; - border-radius: $kuiBorderRadius; - z-index: $kuiModalDepth + 1; - animation: kuiModal $kuiAnimSpeedSlow $kuiAnimSlightBounce; -} - -.kuiModal--confirmation { - width: 450px; - min-width: auto; -} - -.kuiModalHeader { - display: flex; - justify-content: space-between; - align-items: center; - padding: $modalPadding; - padding-left: $modalPadding * 2; - border-bottom: $kuiBorderThin; -} - - .kuiModalHeader__title { - font-size: $kuiTitleFontSize; - } - -.kuiModalHeaderCloseButton { - @include microButton; - font-size: $kuiTitleFontSize; -} - -.kuiModalBody { - padding: $modalPadding * 2; -} - -.kuiModalBodyText { - font-size: 14px; -} - -.kuiModalFooter { - display: flex; - justify-content: flex-end; - padding: $modalPadding * 2; - padding-top: $modalPadding; - - > * + * { - margin-left: 5px; - } -} - -@keyframes kuiModal { - 0% { - opacity: 0; - transform: translateY($kuiSizeXL); - } - 100% { - opacity: 1; - transform: translateY(0); - } -} diff --git a/packages/kbn-ui-framework/src/components/modal/_modal_overlay.scss b/packages/kbn-ui-framework/src/components/modal/_modal_overlay.scss deleted file mode 100644 index 6af8154e85c7b..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/_modal_overlay.scss +++ /dev/null @@ -1,13 +0,0 @@ -.kuiModalOverlay { - position: fixed; - z-index: $kuiModalDepth; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - align-items: center; - justify-content: center; - padding-bottom: 10vh; - background: transparentize($modalOverlayBackground, .2); -} diff --git a/packages/kbn-ui-framework/src/components/modal/confirm_modal.js b/packages/kbn-ui-framework/src/components/modal/confirm_modal.js deleted file mode 100644 index f6578231cec8f..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/confirm_modal.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import { KuiModal } from './modal'; -import { KuiModalFooter } from './modal_footer'; -import { KuiModalHeader } from './modal_header'; -import { KuiModalHeaderTitle } from './modal_header_title'; -import { KuiModalBody } from './modal_body'; -import { - KuiButton, -} from '../../components/'; - -export const CONFIRM_BUTTON = 'confirm'; -export const CANCEL_BUTTON = 'cancel'; - -const CONFIRM_MODAL_BUTTONS = [ - CONFIRM_BUTTON, - CANCEL_BUTTON, -]; - -export function KuiConfirmModal({ - children, - title, - onCancel, - onConfirm, - cancelButtonText, - confirmButtonText, - className, - defaultFocusedButton, - ...rest -}) { - const classes = classnames('kuiModal--confirmation', className); - - let modalTitle; - - if (title) { - modalTitle = ( - - - {title} - - - ); - } - - let message; - - if (typeof children === 'string') { - message =

    {children}

    ; - } else { - message = children; - } - - return ( - - {modalTitle} - - -
    - {message} -
    -
    - - - - {cancelButtonText} - - - - {confirmButtonText} - - -
    - ); -} - -KuiConfirmModal.propTypes = { - children: PropTypes.node, - title: PropTypes.string, - cancelButtonText: PropTypes.string, - confirmButtonText: PropTypes.string, - onCancel: PropTypes.func.isRequired, - onConfirm: PropTypes.func, - className: PropTypes.string, - defaultFocusedButton: PropTypes.oneOf(CONFIRM_MODAL_BUTTONS) -}; diff --git a/packages/kbn-ui-framework/src/components/modal/confirm_modal.test.js b/packages/kbn-ui-framework/src/components/modal/confirm_modal.test.js deleted file mode 100644 index c2713bebffd91..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/confirm_modal.test.js +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import sinon from 'sinon'; -import { mount, render } from 'enzyme'; - -import { findTestSubject, requiredProps } from '../../test'; -import { keyCodes } from '../../services'; - -import { - CANCEL_BUTTON, CONFIRM_BUTTON, KuiConfirmModal, -} from './confirm_modal'; - -let onConfirm; -let onCancel; - -beforeEach(() => { - onConfirm = sinon.spy(); - onCancel = sinon.spy(); -}); - -test('renders KuiConfirmModal', () => { - const component = render( - {}} - onConfirm={onConfirm} - cancelButtonText="Cancel Button Text" - confirmButtonText="Confirm Button Text" - {...requiredProps} - > - This is a confirmation modal example - - ); - expect(component).toMatchSnapshot(); -}); - -test('onConfirm', () => { - const component = mount( - - ); - - findTestSubject(component, 'confirmModalConfirmButton').simulate('click'); - sinon.assert.calledOnce(onConfirm); - sinon.assert.notCalled(onCancel); -}); - -describe('onCancel', () => { - test('triggered by click', () => { - const component = mount( - - ); - - findTestSubject(component, 'confirmModalCancelButton').simulate('click'); - sinon.assert.notCalled(onConfirm); - sinon.assert.calledOnce(onCancel); - }); - - test('triggered by esc key', () => { - const component = mount( - - ); - - findTestSubject(component, 'modal').simulate('keydown', { keyCode: keyCodes.ESCAPE }); - sinon.assert.notCalled(onConfirm); - sinon.assert.calledOnce(onCancel); - }); -}); - -describe('defaultFocusedButton', () => { - test('is cancel', () => { - const component = mount( - - ); - - const button = findTestSubject(component, 'confirmModalCancelButton').getDOMNode(); - expect(document.activeElement).toEqual(button); - }); - - test('is confirm', () => { - const component = mount( - - ); - - const button = findTestSubject(component, 'confirmModalConfirmButton').getDOMNode(); - expect(document.activeElement).toEqual(button); - }); - - test('when not given gives focus to the modal', () => { - const component = mount( - - ); - expect(document.activeElement).toEqual(component.getDOMNode().firstChild); - }); -}); diff --git a/packages/kbn-ui-framework/src/components/modal/index.js b/packages/kbn-ui-framework/src/components/modal/index.js deleted file mode 100644 index 7ce9e7e38606b..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/index.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { - KuiConfirmModal, - CONFIRM_BUTTON as KUI_MODAL_CONFIRM_BUTTON, - CANCEL_BUTTON as KUI_MODAL_CANCEL_BUTTON, -} from './confirm_modal'; -export { KuiModal } from './modal'; -export { KuiModalFooter } from './modal_footer'; -export { KuiModalHeader } from './modal_header'; -export { KuiModalOverlay } from './modal_overlay'; -export { KuiModalBody } from './modal_body'; -export { KuiModalHeaderTitle } from './modal_header_title'; diff --git a/packages/kbn-ui-framework/src/components/modal/modal.js b/packages/kbn-ui-framework/src/components/modal/modal.js deleted file mode 100644 index 0aa5d9ca50495..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { - Component, -} from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; -import FocusTrap from 'focus-trap-react'; - -import { keyCodes } from '../../services'; - -export class KuiModal extends Component { - onKeyDown = event => { - if (event.keyCode === keyCodes.ESCAPE) { - this.props.onClose(); - } - }; - - render() { - const { - className, - children, - onClose, // eslint-disable-line no-unused-vars - ...rest - } = this.props; - - const classes = classnames('kuiModal', className); - - return ( - this.modal, - }} - > - { - // Create a child div instead of applying these props directly to FocusTrap, or else - // fallbackFocus won't work. - } -
    { this.modal = node; }} - className={classes} - onKeyDown={this.onKeyDown} - tabIndex={0} - {...rest} - > - {children} -
    -
    - ); - } -} - -KuiModal.propTypes = { - className: PropTypes.string, - children: PropTypes.node, - onClose: PropTypes.func.isRequired, -}; diff --git a/packages/kbn-ui-framework/src/components/modal/modal.test.js b/packages/kbn-ui-framework/src/components/modal/modal.test.js deleted file mode 100644 index 0700079f59057..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal.test.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiModal, -} from './modal'; - -test('renders KuiModal', () => { - const component = ( - {}} - {...requiredProps} - > - children - - ); - - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/modal/modal_body.js b/packages/kbn-ui-framework/src/components/modal/modal_body.js deleted file mode 100644 index 36bef25ae9b1b..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_body.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; - -export function KuiModalBody({ className, children, ...rest }) { - const classes = classnames('kuiModalBody', className); - return ( -
    - { children } -
    - ); -} - -KuiModalBody.propTypes = { - className: PropTypes.string, - children: PropTypes.node -}; diff --git a/packages/kbn-ui-framework/src/components/modal/modal_body.test.js b/packages/kbn-ui-framework/src/components/modal/modal_body.test.js deleted file mode 100644 index b2a70839a12ac..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_body.test.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiModalBody, -} from './modal_body'; - -test('renders KuiModalBody', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/modal/modal_footer.js b/packages/kbn-ui-framework/src/components/modal/modal_footer.js deleted file mode 100644 index 1ded35b7046e2..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_footer.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; - -export function KuiModalFooter({ className, children, ...rest }) { - const classes = classnames('kuiModalFooter', className); - return ( -
    - { children } -
    - ); -} - -KuiModalFooter.propTypes = { - className: PropTypes.string, - children: PropTypes.node -}; diff --git a/packages/kbn-ui-framework/src/components/modal/modal_footer.test.js b/packages/kbn-ui-framework/src/components/modal/modal_footer.test.js deleted file mode 100644 index e8404292ce1e8..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_footer.test.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiModalFooter, -} from './modal_footer'; - -test('renders KuiModalFooter', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/modal/modal_header.js b/packages/kbn-ui-framework/src/components/modal/modal_header.js deleted file mode 100644 index 9165e0c0a5b19..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_header.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; - -export function KuiModalHeader({ className, children, ...rest }) { - const classes = classnames('kuiModalHeader', className); - return ( -
    - { children } -
    - ); -} - -KuiModalHeader.propTypes = { - className: PropTypes.string, - children: PropTypes.node -}; diff --git a/packages/kbn-ui-framework/src/components/modal/modal_header.test.js b/packages/kbn-ui-framework/src/components/modal/modal_header.test.js deleted file mode 100644 index 10a51a1da4a8a..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_header.test.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiModalHeader, -} from './modal_header'; - -test('renders KuiModalHeader', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/modal/modal_header_title.js b/packages/kbn-ui-framework/src/components/modal/modal_header_title.js deleted file mode 100644 index 47d1960e02ae7..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_header_title.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; - -export function KuiModalHeaderTitle({ className, children, ...rest }) { - const classes = classnames('kuiModalHeader__title', className); - return ( -
    - { children } -
    - ); -} - -KuiModalHeaderTitle.propTypes = { - className: PropTypes.string, - children: PropTypes.node -}; diff --git a/packages/kbn-ui-framework/src/components/modal/modal_header_title.test.js b/packages/kbn-ui-framework/src/components/modal/modal_header_title.test.js deleted file mode 100644 index 5ca29edfa9722..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_header_title.test.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiModalHeaderTitle, -} from './modal_header_title'; - -test('renders KuiModalHeaderTitle', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/modal/modal_overlay.js b/packages/kbn-ui-framework/src/components/modal/modal_overlay.js deleted file mode 100644 index 4d9ed64b8eadc..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_overlay.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; - -export function KuiModalOverlay({ className, ...rest }) { - const classes = classnames('kuiModalOverlay', className); - return ( -
    - ); -} - -KuiModalOverlay.propTypes = { - className: PropTypes.string, -}; diff --git a/packages/kbn-ui-framework/src/components/modal/modal_overlay.test.js b/packages/kbn-ui-framework/src/components/modal/modal_overlay.test.js deleted file mode 100644 index 75c3feee14e3c..0000000000000 --- a/packages/kbn-ui-framework/src/components/modal/modal_overlay.test.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { - KuiModalOverlay, -} from './modal_overlay'; - -test('renders KuiModalOverlay', () => { - const component = children; - expect(render(component)).toMatchSnapshot(); -}); diff --git a/packages/kbn-ui-framework/src/components/outside_click_detector/__snapshots__/outside_click_detector.test.js.snap b/packages/kbn-ui-framework/src/components/outside_click_detector/__snapshots__/outside_click_detector.test.js.snap deleted file mode 100644 index 6345a66dd3abd..0000000000000 --- a/packages/kbn-ui-framework/src/components/outside_click_detector/__snapshots__/outside_click_detector.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`KuiOutsideClickDetector is rendered 1`] = `
    `; diff --git a/packages/kbn-ui-framework/src/components/outside_click_detector/index.js b/packages/kbn-ui-framework/src/components/outside_click_detector/index.js deleted file mode 100644 index 52e22b67af175..0000000000000 --- a/packages/kbn-ui-framework/src/components/outside_click_detector/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { - KuiOutsideClickDetector, -} from './outside_click_detector'; diff --git a/packages/kbn-ui-framework/src/components/outside_click_detector/outside_click_detector.js b/packages/kbn-ui-framework/src/components/outside_click_detector/outside_click_detector.js deleted file mode 100644 index 0d107c2494674..0000000000000 --- a/packages/kbn-ui-framework/src/components/outside_click_detector/outside_click_detector.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - Children, - cloneElement, - Component, -} from 'react'; -import PropTypes from 'prop-types'; - -export class KuiOutsideClickDetector extends Component { - static propTypes = { - children: PropTypes.node.isRequired, - onOutsideClick: PropTypes.func.isRequired, - }; - - onClickOutside = event => { - if (!this.wrapperRef) { - return; - } - - if (this.wrapperRef === event.target) { - return; - } - - if (this.wrapperRef.contains(event.target)) { - return; - } - - this.props.onOutsideClick(); - }; - - componentDidMount() { - document.addEventListener('mousedown', this.onClickOutside); - } - - componentWillUnmount() { - document.removeEventListener('mousedown', this.onClickOutside); - } - - render() { - const props = { - ...this.props.children.props, - ref: node => { - this.wrapperRef = node; - }, - }; - - const child = Children.only(this.props.children); - return cloneElement(child, props); - } -} diff --git a/packages/kbn-ui-framework/src/components/outside_click_detector/outside_click_detector.test.js b/packages/kbn-ui-framework/src/components/outside_click_detector/outside_click_detector.test.js deleted file mode 100644 index 5d872531f07bb..0000000000000 --- a/packages/kbn-ui-framework/src/components/outside_click_detector/outside_click_detector.test.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render } from 'enzyme'; - -import { KuiOutsideClickDetector } from './outside_click_detector'; - -describe('KuiOutsideClickDetector', () => { - test('is rendered', () => { - const component = render( - {}}> -
    - - ); - - expect(component) - .toMatchSnapshot(); - }); -}); diff --git a/packages/kbn-ui-framework/src/components/panel_simple/__snapshots__/panel_simple.test.js.snap b/packages/kbn-ui-framework/src/components/panel_simple/__snapshots__/panel_simple.test.js.snap deleted file mode 100644 index f826cf6182732..0000000000000 --- a/packages/kbn-ui-framework/src/components/panel_simple/__snapshots__/panel_simple.test.js.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`KuiPanelSimple is rendered 1`] = ` -
    -`; diff --git a/packages/kbn-ui-framework/src/components/panel_simple/_index.scss b/packages/kbn-ui-framework/src/components/panel_simple/_index.scss deleted file mode 100644 index 31e793246dfee..0000000000000 --- a/packages/kbn-ui-framework/src/components/panel_simple/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'panel_simple'; diff --git a/packages/kbn-ui-framework/src/components/panel_simple/_panel_simple.scss b/packages/kbn-ui-framework/src/components/panel_simple/_panel_simple.scss deleted file mode 100644 index debc03f0cface..0000000000000 --- a/packages/kbn-ui-framework/src/components/panel_simple/_panel_simple.scss +++ /dev/null @@ -1,27 +0,0 @@ -.kuiPanelSimple { - @include euiBottomShadowSmall; - background-color: $euiColorEmptyShade; - border: $euiBorderThin; - border-radius: $euiBorderRadius; - flex-grow: 1; - - &.kuiPanelSimple--paddingSmall { - padding: $kuiSizeS; - } - - &.kuiPanelSimple--paddingMedium { - padding: $kuiSize; - } - - &.kuiPanelSimple--paddingLarge { - padding: $kuiSizeL; - } - - &.kuiPanelSimple--shadow { - @include kuiBottomShadow; - } - - &.kuiPanelSimple--flexGrowZero { - flex-grow: 0; - } -} diff --git a/packages/kbn-ui-framework/src/components/panel_simple/index.js b/packages/kbn-ui-framework/src/components/panel_simple/index.js deleted file mode 100644 index c9375a9488831..0000000000000 --- a/packages/kbn-ui-framework/src/components/panel_simple/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { - KuiPanelSimple, - SIZES, -} from './panel_simple'; diff --git a/packages/kbn-ui-framework/src/components/panel_simple/panel_simple.js b/packages/kbn-ui-framework/src/components/panel_simple/panel_simple.js deleted file mode 100644 index 8e902201ced0b..0000000000000 --- a/packages/kbn-ui-framework/src/components/panel_simple/panel_simple.js +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -const paddingSizeToClassNameMap = { - 'none': null, - 's': 'kuiPanelSimple--paddingSmall', - 'm': 'kuiPanelSimple--paddingMedium', - 'l': 'kuiPanelSimple--paddingLarge', -}; - -export const SIZES = Object.keys(paddingSizeToClassNameMap); - -export const KuiPanelSimple = ({ - children, - className, - paddingSize, - hasShadow, - grow, - panelRef, - ...rest -}) => { - - const classes = classNames( - 'kuiPanelSimple', - paddingSizeToClassNameMap[paddingSize], - { - 'kuiPanelSimple--shadow': hasShadow, - 'kuiPanelSimple--flexGrowZero': !grow, - }, - className - ); - - return ( -
    - {children} -
    - ); - -}; - -KuiPanelSimple.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - hasShadow: PropTypes.bool, - paddingSize: PropTypes.oneOf(SIZES), - grow: PropTypes.bool, - panelRef: PropTypes.func, -}; - -KuiPanelSimple.defaultProps = { - paddingSize: 'm', - hasShadow: false, - grow: true, -}; diff --git a/packages/kbn-ui-framework/src/components/panel_simple/panel_simple.test.js b/packages/kbn-ui-framework/src/components/panel_simple/panel_simple.test.js deleted file mode 100644 index 977011207a160..0000000000000 --- a/packages/kbn-ui-framework/src/components/panel_simple/panel_simple.test.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { KuiPanelSimple } from './panel_simple'; - -describe('KuiPanelSimple', () => { - test('is rendered', () => { - const component = render( - - ); - - expect(component) - .toMatchSnapshot(); - }); -}); diff --git a/packages/kbn-ui-framework/src/components/popover/__snapshots__/popover.test.js.snap b/packages/kbn-ui-framework/src/components/popover/__snapshots__/popover.test.js.snap deleted file mode 100644 index a409fcd28b806..0000000000000 --- a/packages/kbn-ui-framework/src/components/popover/__snapshots__/popover.test.js.snap +++ /dev/null @@ -1,125 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`KuiPopover children is rendered 1`] = ` -
    -
    -`; - -exports[`KuiPopover is rendered 1`] = ` -
    -
    -`; - -exports[`KuiPopover props anchorPosition defaults to center 1`] = ` -
    -
    -`; - -exports[`KuiPopover props anchorPosition left is rendered 1`] = ` -
    -
    -`; - -exports[`KuiPopover props anchorPosition right is rendered 1`] = ` -
    -
    -`; - -exports[`KuiPopover props isOpen defaults to false 1`] = ` -
    -
    -`; - -exports[`KuiPopover props isOpen renders true 1`] = ` -
    -
    -`; diff --git a/packages/kbn-ui-framework/src/components/popover/__snapshots__/popover_title.test.js.snap b/packages/kbn-ui-framework/src/components/popover/__snapshots__/popover_title.test.js.snap deleted file mode 100644 index adb2c559e5f95..0000000000000 --- a/packages/kbn-ui-framework/src/components/popover/__snapshots__/popover_title.test.js.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`KuiPopoverTitle is rendered 1`] = ` -
    -`; diff --git a/packages/kbn-ui-framework/src/components/popover/_index.scss b/packages/kbn-ui-framework/src/components/popover/_index.scss deleted file mode 100644 index e6b242e72507a..0000000000000 --- a/packages/kbn-ui-framework/src/components/popover/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'mixins'; -@import 'popover'; -@import 'popover_title'; diff --git a/packages/kbn-ui-framework/src/components/popover/_mixins.scss b/packages/kbn-ui-framework/src/components/popover/_mixins.scss deleted file mode 100644 index c1abba14151bd..0000000000000 --- a/packages/kbn-ui-framework/src/components/popover/_mixins.scss +++ /dev/null @@ -1,6 +0,0 @@ -@mixin kuiPopoverTitle { - background-color: $kuiColorLightestShade; - border-bottom: $kuiBorderThin; - padding: $kuiSizeM; - font-size: $kuiFontSize; -} diff --git a/packages/kbn-ui-framework/src/components/popover/_popover.scss b/packages/kbn-ui-framework/src/components/popover/_popover.scss deleted file mode 100644 index 2783fbe910274..0000000000000 --- a/packages/kbn-ui-framework/src/components/popover/_popover.scss +++ /dev/null @@ -1,96 +0,0 @@ -// Pop menu is an animated popover relatively positioned to a button / action. -// By default it positions in the middle, but can be anchored left and right. - -.kuiPopover { - display: inline-block; - position: relative; - - // Open state happens on the wrapper and applies to the panel. - &.kuiPopover-isOpen { - .kuiPopover__panel { - opacity: 1; - visibility: visible; - margin-top: $kuiSizeS; - pointer-events: auto; - } - } -} - - // Animation happens on the panel. - .kuiPopover__panel { - position: absolute; - z-index: $kuiZContentMenu; - top: 100%; - left: 50%; - transform: translateX(-50%) translateY($kuiSizeS) translateZ(0); - backface-visibility: hidden; - transition: - opacity $kuiAnimSlightBounce $kuiAnimSpeedSlow, - visibility $kuiAnimSlightBounce $kuiAnimSpeedSlow, - margin-top $kuiAnimSlightBounce $kuiAnimSpeedSlow; - transform-origin: center top; - opacity: 0; - visibility: hidden; - pointer-events: none; - margin-top: $kuiSizeL; - - // This fakes a border on the arrow. - &:before { - position: absolute; - content: ""; - top: -$kuiSize; - height: 0; - width: 0; - left: 50%; - margin-left: -$kuiSize; - border-left: $kuiSize solid transparent; - border-right: $kuiSize solid transparent; - border-bottom: $kuiSize solid $kuiBorderColor; - } - - // This part of the arrow matches the panel. - &:after { - position: absolute; - content: ""; - top: -$kuiSize + 1; - right: 0; - height: 0; - left: 50%; - margin-left: -$kuiSize; - width: 0; - border-left: $kuiSize solid transparent; - border-right: $kuiSize solid transparent; - border-bottom: $kuiSize solid $euiColorEmptyShade; - } - } - -.kuiPopover--withTitle .kuiPopover__panel:after { - border-bottom-color: $kuiColorLightestShade; -} - -// Positions the menu and arrow to the left of the parent. -.kuiPopover--anchorLeft { - .kuiPopover__panel { - left: 0; - transform: translateX(0%) translateY($kuiSizeS) translateZ(0); - - &:before, &:after { - right: auto; - left: $kuiSize; - margin: 0; - } - } -} - -// Positions the menu and arrow to the right of the parent. -.kuiPopover--anchorRight { - .kuiPopover__panel { - left: 100%; - transform: translateX(-100%) translateY($kuiSizeS) translateZ(0); - - &:before, &:after { - right: $kuiSize; - left: auto; - } - } -} diff --git a/packages/kbn-ui-framework/src/components/popover/_popover_title.scss b/packages/kbn-ui-framework/src/components/popover/_popover_title.scss deleted file mode 100644 index a5a60f1dfbffd..0000000000000 --- a/packages/kbn-ui-framework/src/components/popover/_popover_title.scss +++ /dev/null @@ -1,3 +0,0 @@ -.kuiPopoverTitle { - @include kuiPopoverTitle; -} diff --git a/packages/kbn-ui-framework/src/components/popover/index.js b/packages/kbn-ui-framework/src/components/popover/index.js deleted file mode 100644 index 397993ed3b9f4..0000000000000 --- a/packages/kbn-ui-framework/src/components/popover/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { KuiPopover, } from './popover'; -export { KuiPopoverTitle } from './popover_title'; diff --git a/packages/kbn-ui-framework/src/components/popover/popover.js b/packages/kbn-ui-framework/src/components/popover/popover.js deleted file mode 100644 index 46360ee869d43..0000000000000 --- a/packages/kbn-ui-framework/src/components/popover/popover.js +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { - Component, -} from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import FocusTrap from 'focus-trap-react'; -import tabbable from 'tabbable'; - -import { cascadingMenuKeyCodes } from '../../services'; - -import { KuiOutsideClickDetector } from '../outside_click_detector'; - -import { KuiPanelSimple, SIZES } from '../../components/panel_simple'; - -const anchorPositionToClassNameMap = { - 'center': '', - 'left': 'kuiPopover--anchorLeft', - 'right': 'kuiPopover--anchorRight', -}; - -export const ANCHOR_POSITIONS = Object.keys(anchorPositionToClassNameMap); - -export class KuiPopover extends Component { - constructor(props) { - super(props); - - this.closingTransitionTimeout = undefined; - - this.state = { - isClosing: false, - isOpening: false, - }; - } - - onKeyDown = e => { - if (e.keyCode === cascadingMenuKeyCodes.ESCAPE) { - this.props.closePopover(); - } - }; - - updateFocus() { - // Wait for the DOM to update. - window.requestAnimationFrame(() => { - if (!this.panel) { - return; - } - - // If we've already focused on something inside the panel, everything's fine. - if (this.panel.contains(document.activeElement)) { - return; - } - - // Otherwise let's focus the first tabbable item and expedite input from the user. - const tabbableItems = tabbable(this.panel); - if (tabbableItems.length) { - tabbableItems[0].focus(); - } - }); - } - - componentDidMount() { - this.updateFocus(); - } - - componentWillReceiveProps(nextProps) { - // The popover is being opened. - if (!this.props.isOpen && nextProps.isOpen) { - clearTimeout(this.closingTransitionTimeout); - // We need to set this state a beat after the render takes place, so that the CSS - // transition can take effect. - window.requestAnimationFrame(() => { - this.setState({ - isOpening: true, - }); - }); - } - - // The popover is being closed. - if (this.props.isOpen && !nextProps.isOpen) { - // If the user has just closed the popover, queue up the removal of the content after the - // transition is complete. - this.setState({ - isClosing: true, - isOpening: false, - }); - - this.closingTransitionTimeout = setTimeout(() => { - this.setState({ - isClosing: false, - }); - }, 250); - } - } - - componentDidUpdate() { - this.updateFocus(); - } - - componentWillUnmount() { - clearTimeout(this.closingTransitionTimeout); - } - - panelRef = node => { - if (this.props.ownFocus) { - this.panel = node; - } - }; - - render() { - const { - anchorPosition, - button, - isOpen, - ownFocus, - withTitle, - children, - className, - closePopover, - panelClassName, - panelPaddingSize, - ...rest - } = this.props; - - const classes = classNames( - 'kuiPopover', - anchorPositionToClassNameMap[anchorPosition], - className, - { - 'kuiPopover-isOpen': this.state.isOpening, - 'kuiPopover--withTitle': withTitle, - }, - ); - - const panelClasses = classNames('kuiPopover__panel', panelClassName); - - let panel; - - if (isOpen || this.state.isClosing) { - let tabIndex; - let initialFocus; - - if (ownFocus) { - tabIndex = '0'; - initialFocus = () => this.panel; - } - - panel = ( - - - {children} - - - ); - } - - return ( - -
    - {button} - {panel} -
    -
    - ); - } -} - -KuiPopover.propTypes = { - isOpen: PropTypes.bool, - ownFocus: PropTypes.bool, - withTitle: PropTypes.bool, - closePopover: PropTypes.func.isRequired, - button: PropTypes.node.isRequired, - children: PropTypes.node, - anchorPosition: PropTypes.oneOf(ANCHOR_POSITIONS), - panelClassName: PropTypes.string, - panelPaddingSize: PropTypes.oneOf(SIZES), -}; - -KuiPopover.defaultProps = { - isOpen: false, - ownFocus: false, - anchorPosition: 'center', - panelPaddingSize: 'm', -}; diff --git a/packages/kbn-ui-framework/src/components/popover/popover.test.js b/packages/kbn-ui-framework/src/components/popover/popover.test.js deleted file mode 100644 index 683fdaf0525c3..0000000000000 --- a/packages/kbn-ui-framework/src/components/popover/popover.test.js +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render, mount } from 'enzyme'; -import sinon from 'sinon'; -import { requiredProps } from '../../test/required_props'; - -import { KuiPopover } from './popover'; - -import { keyCodes } from '../../services'; - -describe('KuiPopover', () => { - test('is rendered', () => { - const component = render( - } - closePopover={() => {}} - {...requiredProps} - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - - test('children is rendered', () => { - const component = render( - } - closePopover={() => {}} - > - Children - - ); - - expect(component) - .toMatchSnapshot(); - }); - - describe('props', () => { - describe('withTitle', () => { - test('is rendered', () => { - const component = render( - } - closePopover={() => {}} - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - }); - - describe('closePopover', () => { - it('is called when ESC key is hit', () => { - const closePopoverHandler = sinon.stub(); - - const component = mount( - } - closePopover={closePopoverHandler} - /> - ); - - component.simulate('keydown', { keyCode: keyCodes.ESCAPE }); - sinon.assert.calledOnce(closePopoverHandler); - }); - }); - - describe('anchorPosition', () => { - test('defaults to center', () => { - const component = render( - } - closePopover={() => {}} - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - - test('left is rendered', () => { - const component = render( - } - closePopover={() => {}} - anchorPosition="left" - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - - test('right is rendered', () => { - const component = render( - } - closePopover={() => {}} - anchorPosition="right" - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - }); - - describe('isOpen', () => { - test('defaults to false', () => { - const component = render( - } - closePopover={() => {}} - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - - test('renders true', () => { - const component = render( - } - closePopover={() => {}} - isOpen - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - }); - - describe('ownFocus', () => { - test('defaults to false', () => { - const component = render( - } - closePopover={() => {}} - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - - test('renders true', () => { - const component = render( - } - closePopover={() => {}} - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - }); - - describe('panelClassName', () => { - test('is rendered', () => { - const component = render( - } - closePopover={() => {}} - panelClassName="test" - isOpen - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - }); - - describe('panelPaddingSize', () => { - test('is rendered', () => { - const component = render( - } - closePopover={() => {}} - panelPaddingSize="s" - isOpen - /> - ); - - expect(component) - .toMatchSnapshot(); - }); - }); - }); -}); diff --git a/packages/kbn-ui-framework/src/components/popover/popover_title.js b/packages/kbn-ui-framework/src/components/popover/popover_title.js deleted file mode 100644 index 88aae95d400cd..0000000000000 --- a/packages/kbn-ui-framework/src/components/popover/popover_title.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export const KuiPopoverTitle = ({ children, className, ...rest }) => { - const classes = classNames('kuiPopoverTitle', className); - - return ( -
    - {children} -
    - ); -}; - -KuiPopoverTitle.propTypes = { - children: PropTypes.node, - className: PropTypes.string, -}; diff --git a/packages/kbn-ui-framework/src/components/popover/popover_title.test.js b/packages/kbn-ui-framework/src/components/popover/popover_title.test.js deleted file mode 100644 index 308afcfa97cac..0000000000000 --- a/packages/kbn-ui-framework/src/components/popover/popover_title.test.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render } from 'enzyme'; -import { requiredProps } from '../../test/required_props'; - -import { KuiPopoverTitle } from './popover_title'; - -describe('KuiPopoverTitle', () => { - test('is rendered', () => { - const component = render( - - ); - - expect(component) - .toMatchSnapshot(); - }); -}); diff --git a/packages/kbn-ui-framework/src/components/toggle_button/_index.scss b/packages/kbn-ui-framework/src/components/toggle_button/_index.scss deleted file mode 100644 index 80ad91ff370f8..0000000000000 --- a/packages/kbn-ui-framework/src/components/toggle_button/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import "toggle_button"; -@import "toggle_panel"; diff --git a/packages/kbn-ui-framework/src/components/toggle_button/_toggle_button.scss b/packages/kbn-ui-framework/src/components/toggle_button/_toggle_button.scss deleted file mode 100644 index 81e4198f7978f..0000000000000 --- a/packages/kbn-ui-framework/src/components/toggle_button/_toggle_button.scss +++ /dev/null @@ -1,40 +0,0 @@ -/** - * 1. Allow container to determine font-size and line-height. - * 2. Override inherited Bootstrap styles. - */ -.kuiToggleButton { - appearance: none; - cursor: pointer; - background-color: transparent; - border: none; - padding: 0; - font-size: inherit; /* 1 */ - line-height: inherit; /* 1 */ - color: $kuiFontColor; - - &:focus { - color: $kuiFontColor; - } - - &:active { - color: $kuiLinkColor !important; /* 2 */ - } - - &:hover:not(:disabled) { - color: $kuiLinkHoverColor !important; /* 2 */ - text-decoration: underline; - } - - &:disabled { - cursor: not-allowed; - opacity: .5; - } -} - - /** - * 1. Make icon a consistent width so the text doesn't get pushed around as the icon changes - * between "expand" and "collapse". Use ems to be relative to inherited font-size. - */ - .kuiToggleButton__icon { - width: 0.8em; /* 1 */ - } diff --git a/packages/kbn-ui-framework/src/components/toggle_button/_toggle_panel.scss b/packages/kbn-ui-framework/src/components/toggle_button/_toggle_panel.scss deleted file mode 100644 index afa779b374814..0000000000000 --- a/packages/kbn-ui-framework/src/components/toggle_button/_toggle_panel.scss +++ /dev/null @@ -1,13 +0,0 @@ -.kuiTogglePanelHeader { - padding-bottom: 4px; - margin-bottom: 15px; - border-bottom: $kuiBorderThin; - - /** - * 1. Allow the user to click anywhere on the header, not just on the button text. - */ - .kuiToggleButton { - width: 100%; /* 1 */ - text-align: left; /* 1 */ - } -} From 336257254716f3f777ce0abe4c04f9ecea38ede1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Tue, 6 Aug 2019 21:05:23 -0400 Subject: [PATCH 23/26] Scope task manager api integration tests (#42806) --- .../plugins/task_manager/init_routes.js | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js index c266d756377d2..0892b64870c65 100644 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js +++ b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js @@ -6,6 +6,23 @@ import Joi from 'joi'; +const scope = 'testing'; +const taskManagerQuery = { + bool: { + filter: { + bool: { + must: [ + { + term: { + 'task.scope': scope, + } + } + ] + } + } + } +}; + export function initRoutes(server) { const { taskManager } = server; @@ -25,7 +42,10 @@ export function initRoutes(server) { }, async handler(request) { try { - const task = await taskManager.schedule(request.payload, { request }); + const task = await taskManager.schedule({ + ...request.payload, + scope: [scope], + }, { request }); return task; } catch (err) { return err; @@ -38,7 +58,9 @@ export function initRoutes(server) { method: 'GET', async handler() { try { - return taskManager.fetch(); + return taskManager.fetch({ + query: taskManagerQuery, + }); } catch (err) { return err; } @@ -50,7 +72,9 @@ export function initRoutes(server) { method: 'DELETE', async handler() { try { - const { docs: tasks } = await taskManager.fetch(); + const { docs: tasks } = await taskManager.fetch({ + query: taskManagerQuery, + }); return Promise.all(tasks.map((task) => taskManager.remove(task.id))); } catch (err) { return err; From 44953ced190860ca716c9c526814afd49b486445 Mon Sep 17 00:00:00 2001 From: Henry Wong Date: Wed, 7 Aug 2019 10:40:29 +0800 Subject: [PATCH 24/26] [code] Enable go langserver under production mode (#42678) Go langserver will be published with v7.4.0, make it visible under production mode. --- x-pack/legacy/plugins/code/server/lsp/language_servers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/code/server/lsp/language_servers.ts b/x-pack/legacy/plugins/code/server/lsp/language_servers.ts index fba455bd66502..d30f48638b84b 100644 --- a/x-pack/legacy/plugins/code/server/lsp/language_servers.ts +++ b/x-pack/legacy/plugins/code/server/lsp/language_servers.ts @@ -69,8 +69,8 @@ export const CTAGS: LanguageServerDefinition = { embedPath: require.resolve('@elastic/ctags-langserver/lib/cli.js'), priority: 1, }; -export const LanguageServers: LanguageServerDefinition[] = [TYPESCRIPT, JAVA, CTAGS]; -export const LanguageServersDeveloping: LanguageServerDefinition[] = [GO]; +export const LanguageServers: LanguageServerDefinition[] = [TYPESCRIPT, JAVA, CTAGS, GO]; +export const LanguageServersDeveloping: LanguageServerDefinition[] = []; export function enabledLanguageServers(server: ServerFacade) { const devMode: boolean = server.config().get('env.dev'); From 8b57570b2615b187a9883b85c95257b750c0e841 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 7 Aug 2019 00:17:57 -0700 Subject: [PATCH 25/26] [APM] Always get the root transaction when fetching trace items (#42508) * [APM] Always get the root transaction when fetching trace items Fixes #33210 * code tweaks * displays message notifying user that trace items exceeds maximum displayed * remove getTraceRoot query by adjusting the score and order of trace items with no parent.id * add `apm_oss.maxTraceItems` config options to control the number of displayed trace items * changed config `apm_oss.maxTraceItems` to `xpack.apm.ui.maxTraceItems` * added missing configs to apm settings doc and docker template * minor code tweak --- docs/settings/apm-settings.asciidoc | 4 ++ .../resources/bin/kibana-docker | 8 ++++ src/legacy/core_plugins/apm_oss/index.js | 4 +- x-pack/legacy/plugins/apm/index.ts | 3 +- .../Transaction/TransactionTabs.tsx | 5 ++- .../WaterfallContainer/Waterfall/index.tsx | 16 +++++++- .../waterfall_helpers.test.ts | 37 ++++++++++++------- .../waterfall_helpers/waterfall_helpers.ts | 12 +++--- .../Transaction/WaterfallContainer/index.tsx | 5 ++- .../TransactionDetails/Transaction/index.tsx | 5 ++- .../app/TransactionDetails/index.tsx | 3 +- .../plugins/apm/public/hooks/useWaterfall.ts | 8 +++- .../apm/server/lib/traces/get_trace_items.ts | 25 ++++++++++--- 13 files changed, 100 insertions(+), 35 deletions(-) diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index 615dc98c066ba..e47326a1d2068 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -21,6 +21,10 @@ xpack.apm.ui.enabled:: Set to `false` to hide the APM plugin {kib} from the menu xpack.apm.ui.transactionGroupBucketSize:: Number of top transaction groups displayed in APM plugin in Kibana. Defaults to `100`. +xpack.apm.ui.maxTraceItems:: Max number of child items displayed when viewing trace details. Defaults to `1000`. + +apm_oss.apmAgentConfigurationIndex:: Index containing agent configuration settings. Defaults to `.apm-agent-configuration`. + apm_oss.indexPattern:: Index pattern is used for integrations with Machine Learning and Kuery Bar. It must match all apm indices. Defaults to `apm-*`. apm_oss.errorIndices:: Matcher for indices containing error documents. Defaults to `apm-*`. diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index e599fa54593fc..1ec359d881972 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -82,6 +82,14 @@ kibana_vars=( vega.enableExternalUrls xpack.apm.enabled xpack.apm.ui.enabled + xpack.apm.ui.maxTraceItems + apm_oss.apmAgentConfigurationIndex + apm_oss.indexPattern + apm_oss.errorIndices + apm_oss.onboardingIndices + apm_oss.spanIndices + apm_oss.transactionIndices + apm_oss.metricsIndices xpack.canvas.enabled xpack.graph.enabled xpack.grokdebugger.enabled diff --git a/src/legacy/core_plugins/apm_oss/index.js b/src/legacy/core_plugins/apm_oss/index.js index 6c0c6d0e5fe52..0c281ec939bb1 100644 --- a/src/legacy/core_plugins/apm_oss/index.js +++ b/src/legacy/core_plugins/apm_oss/index.js @@ -38,7 +38,7 @@ export default function apmOss(kibana) { spanIndices: Joi.string().default('apm-*'), metricsIndices: Joi.string().default('apm-*'), onboardingIndices: Joi.string().default('apm-*'), - apmAgentConfigurationIndex: Joi.string().default('.apm-agent-configuration') + apmAgentConfigurationIndex: Joi.string().default('.apm-agent-configuration'), }).default(); }, @@ -49,7 +49,7 @@ export default function apmOss(kibana) { 'transactionIndices', 'spanIndices', 'metricsIndices', - 'onboardingIndices' + 'onboardingIndices', ].map(type => server.config().get(`apm_oss.${type}`)))); } }); diff --git a/x-pack/legacy/plugins/apm/index.ts b/x-pack/legacy/plugins/apm/index.ts index aac946a36b458..d39df60849356 100644 --- a/x-pack/legacy/plugins/apm/index.ts +++ b/x-pack/legacy/plugins/apm/index.ts @@ -59,7 +59,8 @@ export const apm: LegacyPluginInitializer = kibana => { // display menu item ui: Joi.object({ enabled: Joi.boolean().default(true), - transactionGroupBucketSize: Joi.number().default(100) + transactionGroupBucketSize: Joi.number().default(100), + maxTraceItems: Joi.number().default(1000) }).default(), // enable plugin diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/TransactionTabs.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/TransactionTabs.tsx index d3852014c3732..e5be12509e3c9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/TransactionTabs.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/TransactionTabs.tsx @@ -35,13 +35,15 @@ interface Props { transaction: Transaction; urlParams: IUrlParams; waterfall: IWaterfall; + exceedsMax: boolean; } export function TransactionTabs({ location, transaction, urlParams, - waterfall + waterfall, + exceedsMax }: Props) { const tabs = [timelineTab, metadataTab]; const currentTab = @@ -79,6 +81,7 @@ export function TransactionTabs({ location={location} urlParams={urlParams} waterfall={waterfall} + exceedsMax={exceedsMax} /> ) : ( diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/index.tsx index 1c89cbf9f6e69..d53b4077d9759 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/index.tsx @@ -9,6 +9,8 @@ import React, { Component } from 'react'; // @ts-ignore import { StickyContainer } from 'react-sticky'; import styled from 'styled-components'; +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { IUrlParams } from '../../../../../../context/UrlParamsContext/types'; // @ts-ignore import Timeline from '../../../../../shared/charts/Timeline'; @@ -47,6 +49,7 @@ interface Props { waterfall: IWaterfall; location: Location; serviceColors: IServiceColors; + exceedsMax: boolean; } export class Waterfall extends Component { @@ -126,12 +129,23 @@ export class Waterfall extends Component { }; public render() { - const { waterfall } = this.props; + const { waterfall, exceedsMax } = this.props; const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found const waterfallHeight = itemContainerHeight * waterfall.orderedItems.length; return ( + {exceedsMax ? ( + + ) : null} { describe('getWaterfall', () => { const hits = [ + { + processor: { event: 'transaction' }, + trace: { id: 'myTraceId' }, + service: { name: 'opbeans-node' }, + transaction: { + duration: { us: 49660 }, + name: 'GET /api', + id: 'myTransactionId1' + }, + timestamp: { us: 1549324795784006 } + } as Transaction, { parent: { id: 'mySpanIdA' }, processor: { event: 'span' }, @@ -80,17 +91,6 @@ describe('waterfall_helpers', () => { id: 'myTransactionId2' }, timestamp: { us: 1549324795823304 } - } as Transaction, - { - processor: { event: 'transaction' }, - trace: { id: 'myTraceId' }, - service: { name: 'opbeans-node' }, - transaction: { - duration: { us: 49660 }, - name: 'GET /api', - id: 'myTransactionId1' - }, - timestamp: { us: 1549324795784006 } } as Transaction ]; @@ -101,7 +101,10 @@ describe('waterfall_helpers', () => { myTransactionId2: 3 }; const waterfall = getWaterfall( - { trace: hits, errorsPerTransaction }, + { + trace: { items: hits, exceedsMax: false }, + errorsPerTransaction + }, entryTransactionId ); expect(waterfall.orderedItems.length).toBe(6); @@ -116,7 +119,10 @@ describe('waterfall_helpers', () => { myTransactionId2: 3 }; const waterfall = getWaterfall( - { trace: hits, errorsPerTransaction }, + { + trace: { items: hits, exceedsMax: false }, + errorsPerTransaction + }, entryTransactionId ); expect(waterfall.orderedItems.length).toBe(4); @@ -131,7 +137,10 @@ describe('waterfall_helpers', () => { myTransactionId2: 3 }; const waterfall = getWaterfall( - { trace: hits, errorsPerTransaction }, + { + trace: { items: hits, exceedsMax: false }, + errorsPerTransaction + }, entryTransactionId ); const transaction = waterfall.getTransactionById('myTransactionId2'); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 80a8474c29305..7835d47468b7f 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -6,14 +6,14 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { - first, flatten, groupBy, indexBy, - isEmpty, sortBy, uniq, - zipObject + zipObject, + isEmpty, + first } from 'lodash'; import { idx } from '@kbn/elastic-idx'; import { TraceAPIResponse } from '../../../../../../../../server/lib/traces/get_trace'; @@ -22,7 +22,7 @@ import { Span } from '../../../../../../../../typings/es_schemas/ui/Span'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction'; interface IWaterfallIndex { - [key: string]: IWaterfallItem; + [key: string]: IWaterfallItem | undefined; } interface IWaterfallGroup { @@ -234,7 +234,7 @@ export function getWaterfall( { trace, errorsPerTransaction }: TraceAPIResponse, entryTransactionId?: Transaction['transaction']['id'] ): IWaterfall { - if (isEmpty(trace) || !entryTransactionId) { + if (isEmpty(trace.items) || !entryTransactionId) { return { services: [], duration: 0, @@ -246,7 +246,7 @@ export function getWaterfall( }; } - const waterfallItems = trace.map(traceItem => { + const waterfallItems = trace.items.map(traceItem => { const docType = traceItem.processor.event; switch (docType) { case 'span': diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/index.tsx index cd437325e57f8..2f34cc86c5cfc 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/index.tsx @@ -18,13 +18,15 @@ interface Props { transaction: Transaction; location: Location; waterfall: IWaterfall; + exceedsMax: boolean; } export function WaterfallContainer({ location, urlParams, transaction, - waterfall + waterfall, + exceedsMax }: Props) { const agentMarks = getAgentMarks(transaction); if (!waterfall) { @@ -40,6 +42,7 @@ export function WaterfallContainer({ serviceColors={waterfall.serviceColors} urlParams={urlParams} waterfall={waterfall} + exceedsMax={exceedsMax} />
    ); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx index 659eca2c06f03..d9e024c5560e9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx @@ -97,13 +97,15 @@ interface Props { urlParams: IUrlParams; location: Location; waterfall: IWaterfall; + exceedsMax: boolean; } export const Transaction: React.SFC = ({ transaction, urlParams, location, - waterfall + waterfall, + exceedsMax }) => { return ( @@ -149,6 +151,7 @@ export const Transaction: React.SFC = ({ location={location} urlParams={urlParams} waterfall={waterfall} + exceedsMax={exceedsMax} /> ); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx index e08285ae9b960..f242690beab03 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -31,7 +31,7 @@ export function TransactionDetails() { const { data: transactionChartsData } = useTransactionCharts(); - const { data: waterfall } = useWaterfall(urlParams); + const { data: waterfall, exceedsMax } = useWaterfall(urlParams); const transaction = waterfall.getTransactionById(urlParams.transactionId); const { transactionName } = urlParams; @@ -81,6 +81,7 @@ export function TransactionDetails() { transaction={transaction} urlParams={urlParams} waterfall={waterfall} + exceedsMax={exceedsMax} /> )}
    diff --git a/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts b/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts index 050041724a905..fd2ed152c79f7 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts @@ -10,7 +10,11 @@ import { loadTrace } from '../services/rest/apm/traces'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { useFetcher } from './useFetcher'; -const INITIAL_DATA = { trace: [], errorsPerTransaction: {} }; +const INITIAL_DATA = { + root: undefined, + trace: { items: [], exceedsMax: false }, + errorsPerTransaction: {} +}; export function useWaterfall(urlParams: IUrlParams) { const { traceId, start, end, transactionId } = urlParams; @@ -25,5 +29,5 @@ export function useWaterfall(urlParams: IUrlParams) { transactionId ]); - return { data: waterfall, status, error }; + return { data: waterfall, status, error, exceedsMax: data.trace.exceedsMax }; } diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts index 11599d09c1d65..00eeefb4b4fcc 100644 --- a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts @@ -7,7 +7,10 @@ import { SearchParams } from 'elasticsearch'; import { PROCESSOR_EVENT, - TRACE_ID + TRACE_ID, + PARENT_ID, + TRANSACTION_DURATION, + SPAN_DURATION } from '../../../common/elasticsearch_fieldnames'; import { Span } from '../../../typings/es_schemas/ui/Span'; import { Transaction } from '../../../typings/es_schemas/ui/Transaction'; @@ -16,6 +19,7 @@ import { Setup } from '../helpers/setup_request'; export async function getTraceItems(traceId: string, setup: Setup) { const { start, end, client, config } = setup; + const maxTraceItems = config.get('xpack.apm.ui.maxTraceItems'); const params: SearchParams = { index: [ @@ -23,20 +27,31 @@ export async function getTraceItems(traceId: string, setup: Setup) { config.get('apm_oss.transactionIndices') ], body: { - size: 1000, + size: maxTraceItems, query: { bool: { filter: [ { term: { [TRACE_ID]: traceId } }, { terms: { [PROCESSOR_EVENT]: ['span', 'transaction'] } }, { range: rangeFilter(start, end) } - ] + ], + should: { + exists: { field: PARENT_ID } + } } - } + }, + sort: [ + { _score: { order: 'asc' } }, + { [TRANSACTION_DURATION]: { order: 'desc' } }, + { [SPAN_DURATION]: { order: 'desc' } } + ] } }; const resp = await client.search(params); - return resp.hits.hits.map(hit => hit._source); + return { + items: resp.hits.hits.map(hit => hit._source), + exceedsMax: resp.hits.total > maxTraceItems + }; } From 08bd1ad016e5f3b0438eeb34c68f526aa94083e8 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 7 Aug 2019 10:52:59 +0300 Subject: [PATCH 26/26] [Table Vis] Shim new platform - table_vis_fn.js -> table_vis_fn.ts , use ExpressionFunction (#42585) * [Table Vis] Shim new platform - table_vis_fn.js -> table_vis_fn.ts , use ExpressionFunction * fix PR comments --- ...test.js.snap => table_vis_fn.test.ts.snap} | 0 .../vis_type_table/public/plugin.ts | 6 +-- ...le_vis_fn.test.js => table_vis_fn.test.ts} | 25 ++++++----- .../{table_vis_fn.js => table_vis_fn.ts} | 45 +++++++++++++++---- ...andler.js => table_vis_request_handler.ts} | 1 + .../{table_vis_type.js => table_vis_type.ts} | 29 ++++++------ 6 files changed, 69 insertions(+), 37 deletions(-) rename src/legacy/core_plugins/vis_type_table/public/__snapshots__/{table_vis_fn.test.js.snap => table_vis_fn.test.ts.snap} (100%) rename src/legacy/core_plugins/vis_type_table/public/{table_vis_fn.test.js => table_vis_fn.test.ts} (89%) rename src/legacy/core_plugins/vis_type_table/public/{table_vis_fn.js => table_vis_fn.ts} (64%) rename src/legacy/core_plugins/vis_type_table/public/{table_vis_request_handler.js => table_vis_request_handler.ts} (98%) rename src/legacy/core_plugins/vis_type_table/public/{table_vis_type.js => table_vis_type.ts} (85%) diff --git a/src/legacy/core_plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.js.snap b/src/legacy/core_plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.js.snap rename to src/legacy/core_plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap diff --git a/src/legacy/core_plugins/vis_type_table/public/plugin.ts b/src/legacy/core_plugins/vis_type_table/public/plugin.ts index 1f1d6b7dc7453..21b6e21d6d639 100644 --- a/src/legacy/core_plugins/vis_type_table/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_table/public/plugin.ts @@ -29,12 +29,10 @@ import { import { LegacyDependenciesPluginSetup, LegacyDependenciesPlugin } from './shim'; -// @ts-ignore import { createTableVisFn } from './table_vis_fn'; -// @ts-ignore import { createTableVisTypeDefinition } from './table_vis_type'; -/** @private */ +/** @internal */ export interface TableVisualizationDependencies extends LegacyDependenciesPluginSetup { uiSettings: UiSettingsClientContract; } @@ -63,7 +61,7 @@ export class TableVisPlugin implements Plugin, void> { ...(await __LEGACY.setup()), }; - data.expressions.registerFunction(() => createTableVisFn(visualizationDependencies)); + data.expressions.registerFunction(createTableVisFn); visualizations.types.VisTypesRegistryProvider.register(() => createTableVisTypeDefinition(visualizationDependencies) diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.js b/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.ts similarity index 89% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.js rename to src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.ts index a35eb85b07f1a..281303ca4e96a 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.js +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.ts @@ -17,14 +17,19 @@ * under the License. */ -import { functionWrapper } from '../../interpreter/test_helpers'; import { createTableVisFn } from './table_vis_fn'; +// @ts-ignore +import { functionWrapper } from '../../interpreter/test_helpers'; + jest.mock('ui/new_platform'); -const mockResponseHandler = jest.fn().mockReturnValue(Promise.resolve({ - tables: [{ columns: [], rows: [] }], -})); +const mockResponseHandler = jest.fn().mockReturnValue( + Promise.resolve({ + tables: [{ columns: [], rows: [] }], + }) +); + jest.mock('ui/vis/response_handlers/legacy', () => ({ legacyResponseHandlerProvider: () => ({ handler: mockResponseHandler }), })); @@ -42,7 +47,7 @@ describe('interpreter/functions#table', () => { showMetricsAtAllLevels: false, sort: { columnIndex: null, - direction: null + direction: null, }, showTotal: false, totalFunc: 'sum', @@ -51,14 +56,14 @@ describe('interpreter/functions#table', () => { { accessor: 0, format: { - id: 'number' + id: 'number', }, params: {}, - aggType: 'count' - } + aggType: 'count', + }, ], - buckets: [] - } + buckets: [], + }, }; beforeEach(() => { diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.js b/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.ts similarity index 64% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_fn.js rename to src/legacy/core_plugins/vis_type_table/public/table_vis_fn.ts index 315cb3e22e8b0..cc1c07d2c89a6 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.js +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.ts @@ -20,26 +20,53 @@ import { i18n } from '@kbn/i18n'; import { createTableVisResponseHandler } from './table_vis_request_handler'; -export const createTableVisFn = (dependencies) => ({ - name: 'kibana_table', +import { ExpressionFunction, KibanaDatatable, Render } from '../../interpreter/types'; + +const name = 'kibana_table'; + +type Context = KibanaDatatable; + +interface Arguments { + visConfig: string | null; +} + +type VisParams = Required; + +interface RenderValue { + visData: Context; + visType: 'table'; + visConfig: VisParams; + params: { + listenOnChange: boolean; + }; +} + +type Return = Promise>; + +export const createTableVisFn = (): ExpressionFunction< + typeof name, + Context, + Arguments, + Return +> => ({ + name, type: 'render', context: { - types: [ - 'kibana_datatable' - ], + types: ['kibana_datatable'], }, help: i18n.translate('visTypeTable.function.help', { - defaultMessage: 'Table visualization' + defaultMessage: 'Table visualization', }), args: { visConfig: { types: ['string', 'null'], default: '"{}"', + help: '', }, }, async fn(context, args) { - const visConfig = JSON.parse(args.visConfig); - const responseHandler = createTableVisResponseHandler(dependencies); + const visConfig = args.visConfig && JSON.parse(args.visConfig); + const responseHandler = createTableVisResponseHandler(); const convertedData = await responseHandler(context, visConfig.dimensions); return { @@ -51,7 +78,7 @@ export const createTableVisFn = (dependencies) => ({ visConfig, params: { listenOnChange: true, - } + }, }, }; }, diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_request_handler.js b/src/legacy/core_plugins/vis_type_table/public/table_vis_request_handler.ts similarity index 98% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_request_handler.js rename to src/legacy/core_plugins/vis_type_table/public/table_vis_request_handler.ts index bfec2711413a1..80016a67c9dd7 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_request_handler.js +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_request_handler.ts @@ -17,6 +17,7 @@ * under the License. */ +// @ts-ignore import { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; export const createTableVisResponseHandler = () => legacyResponseHandlerProvider().handler; diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_type.js b/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts similarity index 85% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_type.js rename to src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts index 4853241fcf776..3d4d7c41b072d 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_type.js +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts @@ -18,13 +18,17 @@ */ import { i18n } from '@kbn/i18n'; -import { createTableVisResponseHandler } from './table_vis_request_handler'; +import { Vis } from 'ui/vis'; + +// @ts-ignore import { Schemas } from 'ui/vis/editors/default/schemas'; +import { createTableVisResponseHandler } from './table_vis_request_handler'; +import { TableVisualizationDependencies } from './plugin'; import tableVisTemplate from './table_vis.html'; -export const createTableVisTypeDefinition = (dependencies) => { - const responseHandler = createTableVisResponseHandler(dependencies); +export const createTableVisTypeDefinition = (dependencies: TableVisualizationDependencies) => { + const responseHandler = createTableVisResponseHandler(); return dependencies.createAngularVisualization({ type: 'table', @@ -43,7 +47,7 @@ export const createTableVisTypeDefinition = (dependencies) => { showMetricsAtAllLevels: false, sort: { columnIndex: null, - direction: null + direction: null, }, showTotal: false, totalFunc: 'sum', @@ -67,9 +71,7 @@ export const createTableVisTypeDefinition = (dependencies) => { }, }, min: 1, - defaults: [ - { type: 'count', schema: 'metric' } - ] + defaults: [{ type: 'count', schema: 'metric' }], }, { group: 'buckets', @@ -77,7 +79,7 @@ export const createTableVisTypeDefinition = (dependencies) => { title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { defaultMessage: 'Split rows', }), - aggFilter: ['!filter'] + aggFilter: ['!filter'], }, { group: 'buckets', @@ -87,14 +89,13 @@ export const createTableVisTypeDefinition = (dependencies) => { }), min: 0, max: 1, - aggFilter: ['!filter'] - } - ]) + aggFilter: ['!filter'], + }, + ]), }, responseHandler, - hierarchicalData: function (vis) { + hierarchicalData: (vis: Vis) => { return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); - } + }, }); }; -