From cb9d263e851259f41192d16a7393d8bc56abcef4 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Fri, 3 Apr 2020 15:54:03 +0200 Subject: [PATCH 1/9] lazy loading for infra assets (#62363) --- x-pack/plugins/infra/public/compose_libs.ts | 99 +++++++++++++++++ x-pack/plugins/infra/public/plugin.ts | 114 ++++---------------- 2 files changed, 118 insertions(+), 95 deletions(-) create mode 100644 x-pack/plugins/infra/public/compose_libs.ts diff --git a/x-pack/plugins/infra/public/compose_libs.ts b/x-pack/plugins/infra/public/compose_libs.ts new file mode 100644 index 000000000000000..debd83f43d52cf6 --- /dev/null +++ b/x-pack/plugins/infra/public/compose_libs.ts @@ -0,0 +1,99 @@ +/* + * 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 { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; +import ApolloClient from 'apollo-client'; +import { ApolloLink } from 'apollo-link'; +import { createHttpLink } from 'apollo-link-http'; +import { withClientState } from 'apollo-link-state'; +import { CoreStart, HttpFetchOptions } from 'src/core/public'; +import { InfraFrontendLibs } from './lib/lib'; +import introspectionQueryResultData from './graphql/introspection.json'; +import { InfraKibanaObservableApiAdapter } from './lib/adapters/observable_api/kibana_observable_api'; + +export function composeLibs(core: CoreStart) { + const cache = new InMemoryCache({ + addTypename: false, + fragmentMatcher: new IntrospectionFragmentMatcher({ + introspectionQueryResultData, + }), + }); + + const observableApi = new InfraKibanaObservableApiAdapter({ + basePath: core.http.basePath.get(), + }); + + const wrappedFetch = (path: string, options: HttpFetchOptions) => { + return new Promise(async (resolve, reject) => { + // core.http.fetch isn't 100% compatible with the Fetch API and will + // throw Errors on 401s. This top level try / catch handles those scenarios. + try { + core.http + .fetch(path, { + ...options, + // Set headers to undefined due to this bug: https://github.com/apollographql/apollo-link/issues/249, + // Apollo will try to set a "content-type" header which will conflict with the "Content-Type" header that + // core.http.fetch correctly sets. + headers: undefined, + asResponse: true, + }) + .then(res => { + if (!res.response) { + return reject(); + } + // core.http.fetch will parse the Response and set a body before handing it back. As such .text() / .json() + // will have already been called on the Response instance. However, Apollo will also want to call + // .text() / .json() on the instance, as it expects the raw Response instance, rather than core's wrapper. + // .text() / .json() can only be called once, and an Error will be thrown if those methods are accessed again. + // This hacks around that by setting up a new .text() method that will restringify the JSON response we already have. + // This does result in an extra stringify / parse cycle, which isn't ideal, but as we only have a few endpoints left using + // GraphQL this shouldn't create excessive overhead. + // Ref: https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-http/src/httpLink.ts#L134 + // and + // https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-http-common/src/index.ts#L125 + return resolve({ + ...res.response, + text: () => { + return new Promise(async (resolveText, rejectText) => { + if (res.body) { + return resolveText(JSON.stringify(res.body)); + } else { + return rejectText(); + } + }); + }, + }); + }); + } catch (error) { + reject(error); + } + }); + }; + + const HttpLink = createHttpLink({ + fetch: wrappedFetch, + uri: `/api/infra/graphql`, + }); + + const graphQLOptions = { + cache, + link: ApolloLink.from([ + withClientState({ + cache, + resolvers: {}, + }), + HttpLink, + ]), + }; + + const apolloClient = new ApolloClient(graphQLOptions); + + const libs: InfraFrontendLibs = { + apolloClient, + observableApi, + }; + return libs; +} diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 15796f35856bdbb..3b6647b9bfbbeb6 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -12,23 +12,14 @@ import { PluginInitializerContext, AppMountParameters, } from 'kibana/public'; -import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; -import ApolloClient from 'apollo-client'; -import { ApolloLink } from 'apollo-link'; -import { createHttpLink } from 'apollo-link-http'; -import { withClientState } from 'apollo-link-state'; -import { HttpFetchOptions } from 'src/core/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; -import { InfraFrontendLibs } from './lib/lib'; -import introspectionQueryResultData from './graphql/introspection.json'; -import { InfraKibanaObservableApiAdapter } from './lib/adapters/observable_api/kibana_observable_api'; import { registerStartSingleton } from './legacy_singletons'; import { registerFeatures } from './register_feature'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public'; -import { LogsRouter, MetricsRouter } from './routers'; + import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; import { getAlertType } from './components/alerting/metrics/metric_threshold_alert_type'; @@ -75,9 +66,10 @@ export class Plugin mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); const plugins = getMergedPlugins(pluginsSetup, pluginsStart as ClientPluginsStart); - const { startApp } = await import('./apps/start_app'); + const { startApp, composeLibs, LogsRouter } = await this.downloadAssets(); + return startApp( - this.composeLibs(coreStart, plugins), + composeLibs(coreStart), coreStart, plugins, params, @@ -99,9 +91,10 @@ export class Plugin mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); const plugins = getMergedPlugins(pluginsSetup, pluginsStart as ClientPluginsStart); - const { startApp } = await import('./apps/start_app'); + const { startApp, composeLibs, MetricsRouter } = await this.downloadAssets(); + return startApp( - this.composeLibs(coreStart, plugins), + composeLibs(coreStart), coreStart, plugins, params, @@ -129,87 +122,18 @@ export class Plugin registerStartSingleton(core); } - composeLibs(core: CoreStart, plugins: ClientPluginsStart) { - const cache = new InMemoryCache({ - addTypename: false, - fragmentMatcher: new IntrospectionFragmentMatcher({ - introspectionQueryResultData, - }), - }); - - const observableApi = new InfraKibanaObservableApiAdapter({ - basePath: core.http.basePath.get(), - }); - - const wrappedFetch = (path: string, options: HttpFetchOptions) => { - return new Promise(async (resolve, reject) => { - // core.http.fetch isn't 100% compatible with the Fetch API and will - // throw Errors on 401s. This top level try / catch handles those scenarios. - try { - core.http - .fetch(path, { - ...options, - // Set headers to undefined due to this bug: https://github.com/apollographql/apollo-link/issues/249, - // Apollo will try to set a "content-type" header which will conflict with the "Content-Type" header that - // core.http.fetch correctly sets. - headers: undefined, - asResponse: true, - }) - .then(res => { - if (!res.response) { - return reject(); - } - // core.http.fetch will parse the Response and set a body before handing it back. As such .text() / .json() - // will have already been called on the Response instance. However, Apollo will also want to call - // .text() / .json() on the instance, as it expects the raw Response instance, rather than core's wrapper. - // .text() / .json() can only be called once, and an Error will be thrown if those methods are accessed again. - // This hacks around that by setting up a new .text() method that will restringify the JSON response we already have. - // This does result in an extra stringify / parse cycle, which isn't ideal, but as we only have a few endpoints left using - // GraphQL this shouldn't create excessive overhead. - // Ref: https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-http/src/httpLink.ts#L134 - // and - // https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-http-common/src/index.ts#L125 - return resolve({ - ...res.response, - text: () => { - return new Promise(async (resolveText, rejectText) => { - if (res.body) { - return resolveText(JSON.stringify(res.body)); - } else { - return rejectText(); - } - }); - }, - }); - }); - } catch (error) { - reject(error); - } - }); - }; - - const HttpLink = createHttpLink({ - fetch: wrappedFetch, - uri: `/api/infra/graphql`, - }); - - const graphQLOptions = { - cache, - link: ApolloLink.from([ - withClientState({ - cache, - resolvers: {}, - }), - HttpLink, - ]), - }; - - const apolloClient = new ApolloClient(graphQLOptions); - - const libs: InfraFrontendLibs = { - apolloClient, - observableApi, + private async downloadAssets() { + const [{ startApp }, { composeLibs }, { LogsRouter, MetricsRouter }] = await Promise.all([ + import('./apps/start_app'), + import('./compose_libs'), + import('./routers'), + ]); + + return { + startApp, + composeLibs, + LogsRouter, + MetricsRouter, }; - return libs; } } From a44b020b983b9ce121bd46572da05fd7c8336b5f Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Fri, 3 Apr 2020 10:03:05 -0400 Subject: [PATCH 2/9] Task/malware protections (#62326) Malware Protections form for endpoint policy details --- .../applications/endpoint/models/policy.ts | 6 +- .../public/applications/endpoint/types.ts | 48 ++++- .../endpoint/view/policy/policy_details.tsx | 12 ++ .../view/policy/policy_forms/config_form.tsx | 32 +--- .../policy/policy_forms/eventing/windows.tsx | 56 ++++-- .../policy_forms/protections/malware.tsx | 180 ++++++++++++++++++ 6 files changed, 281 insertions(+), 53 deletions(-) create mode 100644 x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts index e1ac9defc858eb2..9ac53f9be609ffe 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/models/policy.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PolicyConfig } from '../types'; +import { PolicyConfig, ProtectionModes } from '../types'; /** * Generate a new Policy model. @@ -19,7 +19,7 @@ export const generatePolicy = (): PolicyConfig => { network: true, }, malware: { - mode: 'prevent', + mode: ProtectionModes.prevent, }, logging: { stdout: 'debug', @@ -44,7 +44,7 @@ export const generatePolicy = (): PolicyConfig => { process: true, }, malware: { - mode: 'detect', + mode: ProtectionModes.detect, }, logging: { stdout: 'debug', diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index 4215edb4d681089..d4f6d2457254e2f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -123,10 +123,8 @@ export interface PolicyConfig { process: boolean; network: boolean; }; - /** malware mode can be detect, prevent or prevent and notify user */ - malware: { - mode: string; - }; + /** malware mode can be off, detect, prevent or prevent and notify user */ + malware: MalwareFields; logging: { stdout: string; file: string; @@ -137,9 +135,7 @@ export interface PolicyConfig { events: { process: boolean; }; - malware: { - mode: string; - }; + malware: MalwareFields; logging: { stdout: string; file: string; @@ -209,6 +205,44 @@ export enum EventingFields { network = 'network', } +/** + * Returns the keys of an object whose values meet a criteria. + * Ex) interface largeNestedObject = { + * a: { + * food: Foods; + * toiletPaper: true; + * }; + * b: { + * food: Foods; + * streamingServices: Streams; + * }; + * c: {}; + * } + * + * type hasFoods = KeysByValueCriteria; + * The above type will be: [a, b] only, and will not include c. + * + */ +export type KeysByValueCriteria = { + [K in keyof O]: O[K] extends Criteria ? K : never; +}[keyof O]; + +/** Returns an array of the policy OSes that have a malware protection field */ + +export type MalwareProtectionOSes = KeysByValueCriteria; +/** Policy: Malware protection fields */ +export interface MalwareFields { + mode: ProtectionModes; +} + +/** Policy protection mode options */ +export enum ProtectionModes { + detect = 'detect', + prevent = 'prevent', + preventNotify = 'preventNotify', + off = 'off', +} + export interface GlobalState { readonly hostList: HostListState; readonly alertList: AlertListState; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx index f2c79155f3c23ff..2dba301bf453763 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx @@ -35,6 +35,7 @@ import { AppAction } from '../../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; +import { MalwareProtections } from './policy_forms/protections/malware'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); @@ -181,6 +182,17 @@ export const PolicyDetails = React.memo(() => { headerLeft={headerLeftContent} headerRight={headerRightContent} > + +

+ +

+
+ + +

= React.memo(({ type, supportedOss, children, id, selectedEventing, totalEventing }) => { + /** Takes a react component to be put on the right corner of the card */ + rightCorner: React.ReactNode; +}> = React.memo(({ type, supportedOss, children, id, rightCorner }) => { const typeTitle = () => { return ( @@ -63,32 +62,11 @@ export const ConfigForm: React.FC<{ {supportedOss.join(', ')} - - - - - + {rightCorner} ); }; - const events = () => { - return ( - -
- -
-
- ); - }; - return ( - {events()} - {children} } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx index e92e22fc97fe6e1..7bec2c4c742d2b5 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/eventing/windows.tsx @@ -6,6 +6,8 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; import { EventingCheckbox } from './checkbox'; import { OS, EventingFields } from '../../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; @@ -16,6 +18,9 @@ import { import { ConfigForm } from '../config_form'; export const WindowsEventing = React.memo(() => { + const selected = usePolicyDetailsSelector(selectedWindowsEventing); + const total = usePolicyDetailsSelector(totalWindowsEventing); + const checkboxes = useMemo( () => [ { @@ -37,21 +42,43 @@ export const WindowsEventing = React.memo(() => { ); const renderCheckboxes = () => { - return checkboxes.map((item, index) => { - return ( - - ); - }); + return ( + <> + +
+ +
+
+ + {checkboxes.map((item, index) => { + return ( + + ); + })} + + ); }; - const selected = usePolicyDetailsSelector(selectedWindowsEventing); - const total = usePolicyDetailsSelector(totalWindowsEventing); + const collectionsEnabled = () => { + return ( + + + + ); + }; return ( { i18n.translate('xpack.endpoint.policy.details.windows', { defaultMessage: 'Windows' }), ]} id="windowsEventingForm" + rightCorner={collectionsEnabled()} children={renderCheckboxes()} - selectedEventing={selected} - totalEventing={total} /> ); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx new file mode 100644 index 000000000000000..66b22178607b943 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx @@ -0,0 +1,180 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; +import { EuiRadio, EuiSwitch, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { htmlIdGenerator } from '@elastic/eui'; +import { Immutable } from '../../../../../../../common/types'; +import { OS, ProtectionModes, MalwareProtectionOSes } from '../../../../types'; +import { ConfigForm } from '../config_form'; +import { policyConfig } from '../../../../store/policy_details/selectors'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { clone } from '../../../../models/policy_details_config'; + +const ProtectionRadioGroup = styled.div` + display: flex; + .policyDetailsProtectionRadio { + margin-right: ${props => props.theme.eui.euiSizeXXL}; + } +`; + +const OSes: Immutable = [OS.windows, OS.mac]; +const protection = 'malware'; + +const ProtectionRadio = React.memo(({ id, label }: { id: ProtectionModes; label: string }) => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch(); + // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value + const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; + + const handleRadioChange = useCallback(() => { + if (policyDetailsConfig) { + const newPayload = clone(policyDetailsConfig); + for (const os of OSes) { + newPayload[os][protection].mode = id; + } + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, [dispatch, id, policyDetailsConfig]); + + /** + * Passing an arbitrary id because EuiRadio + * requires an id if label is passed + */ + + return ( + htmlIdGenerator()(), [])} + checked={selected === id} + onChange={handleRadioChange} + disabled={selected === ProtectionModes.off} + /> + ); +}); + +/** The Malware Protections form for policy details + * which will configure for all relevant OSes. + */ +export const MalwareProtections = React.memo(() => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch(); + // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value + const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; + + const radios: Array<{ + id: ProtectionModes; + label: string; + protection: 'malware'; + }> = useMemo(() => { + return [ + { + id: ProtectionModes.detect, + label: i18n.translate('xpack.endpoint.policy.details.detect', { defaultMessage: 'Detect' }), + protection: 'malware', + }, + { + id: ProtectionModes.prevent, + label: i18n.translate('xpack.endpoint.policy.details.prevent', { + defaultMessage: 'Prevent', + }), + protection: 'malware', + }, + { + id: ProtectionModes.preventNotify, + label: i18n.translate('xpack.endpoint.policy.details.preventAndNotify', { + defaultMessage: 'Prevent and notify user', + }), + protection: 'malware', + }, + ]; + }, []); + + const handleSwitchChange = useCallback( + event => { + if (policyDetailsConfig) { + const newPayload = clone(policyDetailsConfig); + if (event.target.checked === false) { + for (const os of OSes) { + newPayload[os][protection].mode = ProtectionModes.off; + } + } else { + for (const os of OSes) { + newPayload[os][protection].mode = ProtectionModes.prevent; + } + } + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, + [dispatch, policyDetailsConfig] + ); + + const RadioButtons = () => { + return ( + <> + +
+ +
+
+ + + {radios.map(radio => { + return ( + + ); + })} + + + ); + }; + + const ProtectionSwitch = () => { + return ( + + ); + }; + + return ( + + ); +}); From 3d4ca2f61ee3a8ef1b6ecdca0e91bfafcdf614f8 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Fri, 3 Apr 2020 16:56:25 +0200 Subject: [PATCH 3/9] async assets laoding for advances settings management section (#62434) --- .../public/management_app/index.tsx | 102 ------------------ .../mount_management_section.tsx | 82 ++++++++++++++ .../advanced_settings/public/plugin.ts | 32 ++++-- 3 files changed, 108 insertions(+), 108 deletions(-) delete mode 100644 src/plugins/advanced_settings/public/management_app/index.tsx create mode 100644 src/plugins/advanced_settings/public/management_app/mount_management_section.tsx diff --git a/src/plugins/advanced_settings/public/management_app/index.tsx b/src/plugins/advanced_settings/public/management_app/index.tsx deleted file mode 100644 index 53b8f9983aa2709..000000000000000 --- a/src/plugins/advanced_settings/public/management_app/index.tsx +++ /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 from 'react'; -import ReactDOM from 'react-dom'; -import { HashRouter, Switch, Route } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n/react'; -import { AdvancedSettings } from './advanced_settings'; -import { ManagementSetup } from '../../../management/public'; -import { StartServicesAccessor } from '../../../../core/public'; -import { ComponentRegistry } from '../types'; - -const title = i18n.translate('advancedSettings.advancedSettingsLabel', { - defaultMessage: 'Advanced Settings', -}); -const crumb = [{ text: title }]; - -const readOnlyBadge = { - text: i18n.translate('advancedSettings.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - tooltip: i18n.translate('advancedSettings.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save advanced settings', - }), - iconType: 'glasses', -}; - -export async function registerAdvSettingsMgmntApp({ - management, - getStartServices, - componentRegistry, -}: { - management: ManagementSetup; - getStartServices: StartServicesAccessor; - componentRegistry: ComponentRegistry['start']; -}) { - const kibanaSection = management.sections.getSection('kibana'); - if (!kibanaSection) { - throw new Error('`kibana` management section not found.'); - } - - const advancedSettingsManagementApp = kibanaSection.registerApp({ - id: 'settings', - title, - order: 20, - async mount(params) { - params.setBreadcrumbs(crumb); - const [ - { uiSettings, notifications, docLinks, application, chrome }, - ] = await getStartServices(); - - const canSave = application.capabilities.advancedSettings.save as boolean; - - if (!canSave) { - chrome.setBadge(readOnlyBadge); - } - - ReactDOM.render( - - - - - - - - - , - params.element - ); - return () => { - ReactDOM.unmountComponentAtNode(params.element); - }; - }, - }); - const [{ application }] = await getStartServices(); - if (!application.capabilities.management.kibana.settings) { - advancedSettingsManagementApp.disable(); - } -} diff --git a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx new file mode 100644 index 000000000000000..df44ea45e9d01c2 --- /dev/null +++ b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx @@ -0,0 +1,82 @@ +/* + * 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 ReactDOM from 'react-dom'; +import { HashRouter, Switch, Route } from 'react-router-dom'; + +import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n/react'; +import { StartServicesAccessor } from 'src/core/public'; + +import { AdvancedSettings } from './advanced_settings'; +import { ManagementAppMountParams } from '../../../management/public'; +import { ComponentRegistry } from '../types'; + +const title = i18n.translate('advancedSettings.advancedSettingsLabel', { + defaultMessage: 'Advanced Settings', +}); +const crumb = [{ text: title }]; + +const readOnlyBadge = { + text: i18n.translate('advancedSettings.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + tooltip: i18n.translate('advancedSettings.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save advanced settings', + }), + iconType: 'glasses', +}; + +export async function mountManagementSection( + getStartServices: StartServicesAccessor, + params: ManagementAppMountParams, + componentRegistry: ComponentRegistry['start'] +) { + params.setBreadcrumbs(crumb); + const [{ uiSettings, notifications, docLinks, application, chrome }] = await getStartServices(); + + const canSave = application.capabilities.advancedSettings.save as boolean; + + if (!canSave) { + chrome.setBadge(readOnlyBadge); + } + + ReactDOM.render( + + + + + + + + + , + params.element + ); + return () => { + ReactDOM.unmountComponentAtNode(params.element); + }; +} diff --git a/src/plugins/advanced_settings/public/plugin.ts b/src/plugins/advanced_settings/public/plugin.ts index e9472fbdee0e67a..04eeff1e1f3ce33 100644 --- a/src/plugins/advanced_settings/public/plugin.ts +++ b/src/plugins/advanced_settings/public/plugin.ts @@ -16,21 +16,37 @@ * specific language governing permissions and limitations * under the License. */ - +import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { ManagementApp } from '../../management/public'; import { ComponentRegistry } from './component_registry'; import { AdvancedSettingsSetup, AdvancedSettingsStart, AdvancedSettingsPluginSetup } from './types'; -import { registerAdvSettingsMgmntApp } from './management_app'; const component = new ComponentRegistry(); +const title = i18n.translate('advancedSettings.advancedSettingsLabel', { + defaultMessage: 'Advanced Settings', +}); + export class AdvancedSettingsPlugin implements Plugin { + private managementApp?: ManagementApp; public setup(core: CoreSetup, { management }: AdvancedSettingsPluginSetup) { - registerAdvSettingsMgmntApp({ - management, - getStartServices: core.getStartServices, - componentRegistry: component.start, + const kibanaSection = management.sections.getSection('kibana'); + if (!kibanaSection) { + throw new Error('`kibana` management section not found.'); + } + + this.managementApp = kibanaSection.registerApp({ + id: 'settings', + title, + order: 20, + async mount(params) { + const { mountManagementSection } = await import( + './management_app/mount_management_section' + ); + return mountManagementSection(core.getStartServices, params, component.start); + }, }); return { @@ -39,6 +55,10 @@ export class AdvancedSettingsPlugin } public start(core: CoreStart) { + if (!core.application.capabilities.management.kibana.settings) { + this.managementApp!.disable(); + } + return { component: component.start, }; From fbf504086ba59fab42571b91e697801e064da7ea Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Fri, 3 Apr 2020 17:07:31 +0200 Subject: [PATCH 4/9] fixing parse interval (#62267) --- .../search/aggs/date_interval_utils/parse_interval.test.ts | 4 ++++ .../search/aggs/date_interval_utils/parse_interval.ts | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.test.ts b/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.test.ts index 0c02b02a25af0d6..ef6eaa196b06a12 100644 --- a/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.test.ts +++ b/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.test.ts @@ -46,6 +46,10 @@ describe('parseInterval', () => { validateDuration(parseInterval('5m'), 'm', 5); }); + test('should correctly parse 500m interval', () => { + validateDuration(parseInterval('500m'), 'm', 500); + }); + test('should correctly parse 250ms interval', () => { validateDuration(parseInterval('250ms'), 'ms', 250); }); diff --git a/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.ts b/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.ts index ef1d89e400b729f..857c8594720ee60 100644 --- a/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.ts +++ b/src/plugins/data/common/search/aggs/date_interval_utils/parse_interval.ts @@ -49,6 +49,13 @@ export function parseInterval(interval: string): moment.Duration | null { u => Math.abs(duration.as(u)) >= 1 ) as unitOfTime.Base; + // however if we do this fhe other way around it will also fail + // go from 500m to hours as this will result in infinite number (dividing 500/60 = 8.3*) + // so we can only do this if we are changing to smaller units + if (dateMath.units.indexOf(selectedUnit as any) < dateMath.units.indexOf(unit as any)) { + return duration; + } + return moment.duration(duration.as(selectedUnit), selectedUnit); } catch (e) { return null; From cccb66e57fa63265abc270a7fb990d795cc292ea Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Fri, 3 Apr 2020 08:33:17 -0700 Subject: [PATCH 5/9] XPack-Accessibility- Grok Debugger Test (#62104) * accessibility tests for dashboard panel * added back the skipped test as it is still required to pass through th ea11ySnapshot * accessibility grok debugger test - currently skipped due to aria label violation * deleting a file which was added accidentally * deleting a file which was added accidentally * incorporated feedback split tests into seperate tests and skipped the test as there is a aria violation * commented out the grok debugger config file entry * re-added the test in config file * updated the tests to match ...the actions they are performing * fixed syntax Co-authored-by: Elastic Machine --- .../test/accessibility/apps/grok_debugger.ts | 36 +++++++++++++++++++ x-pack/test/accessibility/config.ts | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/accessibility/apps/grok_debugger.ts diff --git a/x-pack/test/accessibility/apps/grok_debugger.ts b/x-pack/test/accessibility/apps/grok_debugger.ts new file mode 100644 index 000000000000000..0b052d39a4db852 --- /dev/null +++ b/x-pack/test/accessibility/apps/grok_debugger.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 { FtrProviderContext } from '../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'security']); + const a11y = getService('a11y'); + const grokDebugger = getService('grokDebugger'); + + // this test is failing as there is a violation https://github.com/elastic/kibana/issues/62102 + describe.skip('Dev tools grok debugger', () => { + before(async () => { + await PageObjects.common.navigateToApp('grokDebugger'); + await grokDebugger.assertExists(); + }); + + it('Dev tools grok debugger set input', async () => { + await grokDebugger.setEventInput('SegerCommaBob'); + await a11y.testAppSnapshot(); + }); + + it('Dev tools grok debugger set pattern', async () => { + await grokDebugger.setPatternInput('%{USERNAME:u}'); + await a11y.testAppSnapshot(); + }); + + it('Dev tools grok debugger simulate', async () => { + await grokDebugger.clickSimulate(); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index a9ac7c71d3e79ef..c8a31ab4ceba849 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -13,7 +13,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { return { ...functionalConfig.getAll(), - testFiles: [require.resolve('./apps/login_page')], + testFiles: [require.resolve('./apps/login_page'), require.resolve('./apps/grok_debugger')], pageObjects, services, From f7bbf3366732f0b263c314e1f61c757f75af6ec0 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Fri, 3 Apr 2020 16:46:22 +0100 Subject: [PATCH 6/9] fix persisting note (#62444) --- .../siem/server/lib/timeline/routes/utils/import_timelines.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts index 5596d0c70f5ea31..f69a715f9b2c99e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts @@ -127,7 +127,7 @@ export const saveNotes = ( existingNoteIds?: string[], newNotes?: NoteResult[] ) => { - return ( + return Promise.all( newNotes?.map(note => { const newNote: SavedNote = { eventId: note.eventId, From cfe519f5a7d2e5a9bff4afb7efe84f28bca2c7de Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Fri, 3 Apr 2020 11:04:48 -0500 Subject: [PATCH 7/9] =?UTF-8?q?[Metrics=20Alerts]=20Set=20default=20aggreg?= =?UTF-8?q?ator=20to=20"average"=20instead=20o=E2=80=A6=20(#62216)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Elastic Machine --- .../infra/public/components/alerting/metrics/expression.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx index 0909a3c2ed569f9..cd3ba43c3607c77 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx @@ -89,7 +89,7 @@ export const Expressions: React.FC = props => { const defaultExpression = useMemo( () => ({ - aggType: AGGREGATION_TYPES.MAX, + aggType: AGGREGATION_TYPES.AVERAGE, comparator: '>', threshold: [], timeSize: 1, From 4b05ac2dee69c24b9f3d39afca9073be44144afc Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Fri, 3 Apr 2020 11:29:02 -0500 Subject: [PATCH 8/9] Ensure rule message do not span multiple lines (#62391) Because these messages are used for logging, we should ensure they do not span multiple lines and confuse log parsers. Since the frontend does not currently display these newlines, anyway, there is no impact to the UI. --- .../signals/rule_messages.test.ts | 20 +++++++++---------- .../detection_engine/signals/rule_messages.ts | 2 +- .../signals/signal_rule_alert_type.ts | 2 +- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts index 8e4b5ce3c992422..bdbb6ff7d1052bc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts @@ -28,25 +28,23 @@ describe('buildRuleMessageFactory', () => { expect(message).toEqual(expect.stringContaining('signals index: "index"')); }); - it('joins message parts with newlines', () => { + it('joins message parts with spaces', () => { const buildMessage = buildRuleMessageFactory(factoryParams); const message = buildMessage('my message'); - const messageParts = message.split('\n'); - expect(messageParts).toContain('my message'); - expect(messageParts).toContain('name: "name"'); - expect(messageParts).toContain('id: "id"'); - expect(messageParts).toContain('rule id: "ruleId"'); - expect(messageParts).toContain('signals index: "index"'); + expect(message).toEqual(expect.stringContaining('my message ')); + expect(message).toEqual(expect.stringContaining(' name: "name" ')); + expect(message).toEqual(expect.stringContaining(' id: "id" ')); + expect(message).toEqual(expect.stringContaining(' rule id: "ruleId" ')); + expect(message).toEqual(expect.stringContaining(' signals index: "index"')); }); - it('joins multiple arguments with newlines', () => { + it('joins multiple arguments with spaces', () => { const buildMessage = buildRuleMessageFactory(factoryParams); const message = buildMessage('my message', 'here is more'); - const messageParts = message.split('\n'); - expect(messageParts).toContain('my message'); - expect(messageParts).toContain('here is more'); + expect(message).toEqual(expect.stringContaining('my message ')); + expect(message).toEqual(expect.stringContaining(' here is more')); }); it('defaults the rule ID if not provided ', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts index d5f9d332bbcddb5..cc97a1f8a9f0b2e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts @@ -24,4 +24,4 @@ export const buildRuleMessageFactory = ({ `id: "${id}"`, `rule id: "${ruleId ?? '(unknown rule id)'}"`, `signals index: "${index}"`, - ].join('\n'); + ].join(' '); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 91905722fbca317..27074be1b5cf43b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -126,7 +126,7 @@ export const signalRulesAlertType = ({ 'Machine learning rule is missing job id and/or anomaly threshold:', `job id: "${machineLearningJobId}"`, `anomaly threshold: "${anomalyThreshold}"`, - ].join('\n') + ].join(' ') ); } From 020e573768d8b51f20cb073c8dafc205d3d816b7 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 3 Apr 2020 19:28:34 +0200 Subject: [PATCH 9/9] [Mappings Editor] Support unknown types (#62149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * First iteration of supporting unknown types e2e * Add missing files * Fix types issues * When creating a new field, we check if we actually know the type If we do know the type, convert the new field to it and throw away the customTypeJson. * Fix i18n * Updated naming to be more consistent customType -> otherType * Clean up of custom type in comments and validation feedback * Codre review suggestions * Add missing serializer * Add Array validator to json * Fix types issues Do not use otherTypeName in call to getConfig rather wrap it in it's own component also add some comments. * Remove otherTypeJson from parameters * Move fieldConfig to variable outside of the UseField * Copy update Change the instruction from "Manually specify" to something more declarative. Also, manually may sound misleading (suggests there is an automatic alternative). Also change the JSON parameter label to something more accurate. Co-authored-by: Elastic Machine Co-authored-by: SeĢbastien Loix --- .../forms/helpers/field_validators/is_json.ts | 19 ---- .../document_fields/field_parameters/index.ts | 4 + .../other_type_json_parameter.tsx | 92 +++++++++++++++++++ .../other_type_name_parameter.tsx | 42 +++++++++ .../fields/create_field/create_field.tsx | 13 ++- .../fields/edit_field/edit_field.tsx | 44 ++++----- .../edit_field/edit_field_header_form.tsx | 12 ++- .../fields/field_types/index.ts | 2 + .../fields/field_types/other_type.tsx | 17 ++++ .../fields/fields_list_item.tsx | 4 +- .../search_fields/search_result_item.tsx | 3 +- .../constants/data_types_definition.tsx | 15 +++ .../mappings_editor/lib/search_fields.tsx | 2 - .../mappings_editor/lib/serializers.ts | 27 ++++-- .../components/mappings_editor/lib/utils.ts | 8 +- .../components/mappings_editor/types.ts | 11 ++- 16 files changed, 257 insertions(+), 58 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/other_type_json_parameter.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/other_type_name_parameter.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/other_type.tsx diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts index 5626fc80bb749af..dc8321aa07004a7 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/is_json.ts @@ -17,25 +17,6 @@ * under the License. */ -/* - * 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 { ValidationFunc } from '../../hook_form_lib'; import { isJSON } from '../../../validators/string'; import { ERROR_CODE } from './types'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts index 663017e2e47afde..cc4c17c5c63a379 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts @@ -63,6 +63,10 @@ export * from './max_shingle_size_parameter'; export * from './relations_parameter'; +export * from './other_type_name_parameter'; + +export * from './other_type_json_parameter'; + export const PARAMETER_SERIALIZERS = [relationsSerializer, dynamicSerializer]; export const PARAMETER_DESERIALIZERS = [relationsDeserializer, dynamicDeserializer]; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/other_type_json_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/other_type_json_parameter.tsx new file mode 100644 index 000000000000000..64e50f711a249a4 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/other_type_json_parameter.tsx @@ -0,0 +1,92 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { + UseField, + JsonEditorField, + ValidationFuncArg, + fieldValidators, + FieldConfig, +} from '../../../shared_imports'; + +const { isJsonField } = fieldValidators; + +/** + * This is a special component that does not have an explicit entry in {@link PARAMETERS_DEFINITION}. + * + * We use it to store custom defined parameters in a field called "otherTypeJson". + */ + +const fieldConfig: FieldConfig = { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.otherTypeJsonFieldLabel', { + defaultMessage: 'Type Parameters JSON', + }), + defaultValue: {}, + validations: [ + { + validator: isJsonField( + i18n.translate( + 'xpack.idxMgmt.mappingsEditor.parameters.validations.otherTypeJsonInvalidJSONErrorMessage', + { + defaultMessage: 'Invalid JSON.', + } + ) + ), + }, + { + validator: ({ value }: ValidationFuncArg) => { + const json = JSON.parse(value); + if (Array.isArray(json)) { + return { + message: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.parameters.validations.otherTypeJsonArrayNotAllowedErrorMessage', + { + defaultMessage: 'Arrays are not allowed.', + } + ), + }; + } + }, + }, + { + validator: ({ value }: ValidationFuncArg) => { + const json = JSON.parse(value); + if (json.type) { + return { + code: 'ERR_CUSTOM_TYPE_OVERRIDDEN', + message: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.parameters.validations.otherTypeJsonTypeFieldErrorMessage', + { + defaultMessage: 'Cannot override the "type" field.', + } + ), + }; + } + }, + }, + ], + deserializer: (value: any) => { + if (value === '') { + return value; + } + return JSON.stringify(value, null, 2); + }, + serializer: (value: string) => { + try { + return JSON.parse(value); + } catch (error) { + // swallow error and return non-parsed value; + return value; + } + }, +}; + +export const OtherTypeJsonParameter = () => ( + +); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/other_type_name_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/other_type_name_parameter.tsx new file mode 100644 index 000000000000000..6004e484323a141 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/other_type_name_parameter.tsx @@ -0,0 +1,42 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { UseField, TextField, FieldConfig } from '../../../shared_imports'; +import { fieldValidators } from '../../../shared_imports'; + +const { emptyField } = fieldValidators; + +/** + * This is a special component that does not have an explicit entry in {@link PARAMETERS_DEFINITION}. + * + * We use it to store the name of types unknown to the mappings editor in the "subType" path. + */ + +const fieldConfig: FieldConfig = { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.otherTypeNameFieldLabel', { + defaultMessage: 'Type Name', + }), + defaultValue: '', + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.idxMgmt.mappingsEditor.parameters.validations.otherTypeNameIsRequiredErrorMessage', + { + defaultMessage: 'The type name is required.', + } + ) + ), + }, + ], +}; + +export const OtherTypeNameParameter = () => ( + +); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx index 60b025ce644efea..b41f35b98388511 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/create_field.tsx @@ -5,6 +5,7 @@ */ import React, { useEffect, useCallback } from 'react'; import classNames from 'classnames'; +import * as _ from 'lodash'; import { i18n } from '@kbn/i18n'; @@ -31,7 +32,7 @@ import { filterTypesForNonRootFields, } from '../../../../lib'; import { Field, MainType, SubType, NormalizedFields, ComboBoxOption } from '../../../../types'; -import { NameParameter, TypeParameter } from '../../field_parameters'; +import { NameParameter, TypeParameter, OtherTypeNameParameter } from '../../field_parameters'; import { getParametersFormForType } from './required_parameters_forms'; const formWrapper = (props: any) =>
; @@ -155,9 +156,9 @@ export const CreateField = React.memo(function CreateFieldComponent({ }, [form, getSubTypeMeta] ); - const renderFormFields = useCallback( ({ type }) => { + const isOtherType = type === 'other'; const { subTypeOptions, subTypeLabel } = getSubTypeMeta(type); const docLink = documentationService.getTypeDocLink(type) as string; @@ -178,7 +179,13 @@ export const CreateField = React.memo(function CreateFieldComponent({ docLink={docLink} /> - {/* Field sub type (if any) */} + {/* Other type */} + {isOtherType && ( + + + + )} + {/* Field sub type (if any) - will never be the case if we have an "other" type */} {subTypeOptions && ( {/* Documentation link */} - - - {i18n.translate( - 'xpack.idxMgmt.mappingsEditor.editField.typeDocumentation', - { - defaultMessage: '{type} documentation', - values: { - type: subTypeDefinition - ? subTypeDefinition.label - : typeDefinition.label, - }, - } - )} - - + {linkDocumentation && ( + + + {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.editField.typeDocumentation', + { + defaultMessage: '{type} documentation', + values: { + type: subTypeDefinition + ? subTypeDefinition.label + : typeDefinition.label, + }, + } + )} + + + )} {/* Field path */} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx index ddb808094428d9d..75a083d64b6db57 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field_header_form.tsx @@ -17,7 +17,7 @@ import { } from '../../../../lib'; import { TYPE_DEFINITION } from '../../../../constants'; -import { NameParameter, TypeParameter } from '../../field_parameters'; +import { NameParameter, TypeParameter, OtherTypeNameParameter } from '../../field_parameters'; import { FieldDescriptionSection } from './field_description_section'; interface Props { @@ -80,9 +80,17 @@ export const EditFieldHeaderForm = React.memo( /> - {/* Field sub type (if any) */} + {/* Other type */} + {type === 'other' && ( + + + + )} + + {/* Field sub type (if any) - will never be the case if we have an "other" type */} {hasSubType && ( + {' '} } = { shape: ShapeType, dense_vector: DenseVectorType, object: ObjectType, + other: OtherType, nested: NestedType, join: JoinType, }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/other_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/other_type.tsx new file mode 100644 index 000000000000000..c403bbfb79056ef --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/other_type.tsx @@ -0,0 +1,17 @@ +/* + * 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 { OtherTypeJsonParameter } from '../../field_parameters'; +import { BasicParametersSection } from '../edit_field'; + +export const OtherType = () => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx index 4c1c8bc1da1143b..f274159bd6c3089 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/fields_list_item.tsx @@ -16,11 +16,13 @@ import { import { i18n } from '@kbn/i18n'; import { NormalizedField, NormalizedFields } from '../../../types'; +import { getTypeLabelFromType } from '../../../lib'; import { TYPE_DEFINITION, CHILD_FIELD_INDENT_SIZE, LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER, } from '../../../constants'; + import { FieldsList } from './fields_list'; import { CreateField } from './create_field'; import { DeleteFieldProvider } from './delete_field_provider'; @@ -265,7 +267,7 @@ function FieldListItemComponent( dataType: TYPE_DEFINITION[source.type].label, }, }) - : TYPE_DEFINITION[source.type].label} + : getTypeLabelFromType(source.type)} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx index dbb8a788514bcb2..614b7cb56bef641 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/search_fields/search_result_item.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { SearchResult } from '../../../types'; import { TYPE_DEFINITION } from '../../../constants'; import { useDispatch } from '../../../mappings_state'; +import { getTypeLabelFromType } from '../../../lib'; import { DeleteFieldProvider } from '../fields/delete_field_provider'; interface Props { @@ -115,7 +116,7 @@ export const SearchResultItem = React.memo(function FieldListItemFlatComponent({ dataType: TYPE_DEFINITION[source.type].label, }, }) - : TYPE_DEFINITION[source.type].label} + : getTypeLabelFromType(source.type)} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx index f904281181c4851..4206fe8b696da78 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx @@ -784,6 +784,20 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = {

), }, + other: { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.otherDescription', { + defaultMessage: 'Other', + }), + value: 'other', + description: () => ( +

+ +

+ ), + }, }; export const MAIN_TYPES: MainType[] = [ @@ -811,6 +825,7 @@ export const MAIN_TYPES: MainType[] = [ 'shape', 'text', 'token_count', + 'other', ]; export const MAIN_DATA_TYPE_DEFINITION: { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.tsx index 5a277073c5f1a34..618d106b0e7a112 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/search_fields.tsx @@ -185,8 +185,6 @@ const getSearchMetadata = (searchData: SearchData, fieldData: FieldData): Search const score = calculateScore(metadata); const display = getJSXdisplayFromMeta(searchData, fieldData, metadata); - // console.log(fieldData.path, score, metadata); - return { ...metadata, display, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/serializers.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/serializers.ts index 131d886ff05d959..6b817c829251f63 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/serializers.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/serializers.ts @@ -45,16 +45,19 @@ const runParametersDeserializers = (field: Field): Field => ); export const fieldSerializer: SerializerFunc = (field: Field) => { + const { otherTypeJson, ...rest } = field; + const updatedField: Field = Boolean(otherTypeJson) ? { ...otherTypeJson, ...rest } : { ...rest }; + // If a subType is present, use it as type for ES - if ({}.hasOwnProperty.call(field, 'subType')) { - field.type = field.subType as DataType; - delete field.subType; + if ({}.hasOwnProperty.call(updatedField, 'subType')) { + updatedField.type = updatedField.subType as DataType; + delete updatedField.subType; } // Delete temp fields - delete (field as any).useSameAnalyzerForSearch; + delete (updatedField as any).useSameAnalyzerForSearch; - return sanitizeField(runParametersSerializers(field)); + return sanitizeField(runParametersSerializers(updatedField)); }; export const fieldDeserializer: SerializerFunc = (field: Field): Field => { @@ -70,8 +73,18 @@ export const fieldDeserializer: SerializerFunc = (field: Field): Field => field.type = type; } - (field as any).useSameAnalyzerForSearch = - {}.hasOwnProperty.call(field, 'search_analyzer') === false; + if (field.type === 'other') { + const { type, subType, name, ...otherTypeJson } = field; + /** + * For "other" type (type we don't support through a form) + * we grab all the parameters and put them in the "otherTypeJson" object + * that we will render in a JSON editor. + */ + field.otherTypeJson = otherTypeJson; + } else { + (field as any).useSameAnalyzerForSearch = + {}.hasOwnProperty.call(field, 'search_analyzer') === false; + } return runParametersDeserializers(field); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts index 337554ab5fa5a1f..cece26618ced87f 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts @@ -25,6 +25,7 @@ import { PARAMETERS_DEFINITION, TYPE_NOT_ALLOWED_MULTIFIELD, TYPE_ONLY_ALLOWED_AT_ROOT_LEVEL, + TYPE_DEFINITION, } from '../constants'; import { State } from '../reducer'; @@ -71,6 +72,9 @@ export const getFieldMeta = (field: Field, isMultiField?: boolean): FieldMeta => }; }; +export const getTypeLabelFromType = (type: DataType) => + TYPE_DEFINITION[type] ? TYPE_DEFINITION[type].label : `${TYPE_DEFINITION.other.label}: ${type}`; + export const getFieldConfig = (param: ParameterName, prop?: string): FieldConfig => { if (prop !== undefined) { if ( @@ -122,7 +126,7 @@ const replaceAliasPathByAliasId = ( }; export const getMainTypeFromSubType = (subType: SubType): MainType => - SUB_TYPE_MAP_TO_MAIN[subType] as MainType; + (SUB_TYPE_MAP_TO_MAIN[subType] ?? 'other') as MainType; /** * In order to better work with the recursive pattern of the mappings `properties`, this method flatten the fields @@ -287,7 +291,9 @@ export const deNormalize = ({ rootLevelFields, byId, aliases }: NormalizedFields const { source, childFields, childFieldsName } = serializedFieldsById[id]; const { name, ...normalizedField } = source; const field: Omit = normalizedField; + to[name] = field; + if (childFields) { field[childFieldsName!] = {}; return deNormalizePaths(childFields, field[childFieldsName!]); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts index dbbffe5a0bd3160..5b18af68ed55b93 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types.ts @@ -56,7 +56,12 @@ export type MainType = | 'date_nanos' | 'geo_point' | 'geo_shape' - | 'token_count'; + | 'token_count' + /** + * 'other' is a special type that only exists inside of MappingsEditor as a placeholder + * for undocumented field types. + */ + | 'other'; export type SubType = NumericType | RangeType; @@ -156,6 +161,10 @@ interface FieldBasic { subType?: SubType; properties?: { [key: string]: Omit }; fields?: { [key: string]: Omit }; + + // other* exist together as a holder of types that the mappings editor does not yet know about but + // enables the user to create mappings with them. + otherTypeJson?: GenericObject; } type FieldParams = {