From b00deeb7543512c7af59619ad07a00776d58e274 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 23 Jun 2020 06:55:03 -0400 Subject: [PATCH 1/8] [IM] Move template wizard steps to shared directory + update legacy details panel (#69559) --- .../helpers/test_subjects.ts | 6 +-- .../home/index_templates_tab.test.ts | 12 +++--- .../components/shared/components/index.ts | 2 + .../shared/components/wizard_steps/index.ts | 9 ++++ .../components/wizard_steps}/step_aliases.tsx | 22 +++++----- .../wizard_steps}/step_mappings.tsx | 16 ++++---- .../wizard_steps}/step_settings.tsx | 20 ++++----- .../components/wizard_steps}/use_json_step.ts | 2 +- .../application/components/shared/index.ts | 9 +++- .../steps/step_aliases_container.tsx | 11 ++++- .../steps/step_mappings_container.tsx | 4 +- .../steps/step_settings_container.tsx | 11 ++++- .../template_details/template_details.tsx | 29 +++++++------ .../template_details/tabs/index.ts | 3 -- .../template_details/tabs/tab_aliases.tsx | 41 ------------------- .../template_details/tabs/tab_mappings.tsx | 41 ------------------- .../template_details/tabs/tab_settings.tsx | 41 ------------------- .../application/services/documentation.ts | 4 ++ .../translations/translations/ja-JP.json | 18 -------- .../translations/translations/zh-CN.json | 18 -------- 20 files changed, 99 insertions(+), 220 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts rename x-pack/plugins/index_management/public/application/components/{template_form/steps => shared/components/wizard_steps}/step_aliases.tsx (80%) rename x-pack/plugins/index_management/public/application/components/{template_form/steps => shared/components/wizard_steps}/step_mappings.tsx (84%) rename x-pack/plugins/index_management/public/application/components/{template_form/steps => shared/components/wizard_steps}/step_settings.tsx (81%) rename x-pack/plugins/index_management/public/application/components/{template_form/steps => shared/components/wizard_steps}/use_json_step.ts (96%) delete mode 100644 x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx delete mode 100644 x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx delete mode 100644 x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts index 4e297118b0fdd9..9889ebe16ba1e0 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts @@ -5,7 +5,7 @@ */ export type TestSubjects = - | 'aliasesTab' + | 'aliasesTabContent' | 'appTitle' | 'cell' | 'closeDetailsButton' @@ -27,7 +27,7 @@ export type TestSubjects = | 'indicesTab' | 'legacyTemplateTable' | 'manageTemplateButton' - | 'mappingsTab' + | 'mappingsTabContent' | 'noAliasesCallout' | 'noMappingsCallout' | 'noSettingsCallout' @@ -36,7 +36,7 @@ export type TestSubjects = | 'row' | 'sectionError' | 'sectionLoading' - | 'settingsTab' + | 'settingsTabContent' | 'summaryTab' | 'summaryTitle' | 'systemTemplatesSwitch' diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts index 7c79c7e61174ea..2ff3743cd866c6 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts @@ -432,18 +432,18 @@ describe('Index Templates tab', () => { // Navigate and verify all tabs actions.selectDetailsTab('settings'); expect(exists('summaryTab')).toBe(false); - expect(exists('settingsTab')).toBe(true); + expect(exists('settingsTabContent')).toBe(true); actions.selectDetailsTab('aliases'); expect(exists('summaryTab')).toBe(false); - expect(exists('settingsTab')).toBe(false); - expect(exists('aliasesTab')).toBe(true); + expect(exists('settingsTabContent')).toBe(false); + expect(exists('aliasesTabContent')).toBe(true); actions.selectDetailsTab('mappings'); expect(exists('summaryTab')).toBe(false); - expect(exists('settingsTab')).toBe(false); - expect(exists('aliasesTab')).toBe(false); - expect(exists('mappingsTab')).toBe(true); + expect(exists('settingsTabContent')).toBe(false); + expect(exists('aliasesTabContent')).toBe(false); + expect(exists('mappingsTabContent')).toBe(true); }); test('should show an info callout if data is not present', async () => { diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/index.ts b/x-pack/plugins/index_management/public/application/components/shared/components/index.ts index 90d66bd1a5da49..e1700ad6a632d2 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/index.ts +++ b/x-pack/plugins/index_management/public/application/components/shared/components/index.ts @@ -5,3 +5,5 @@ */ export { TabAliases, TabMappings, TabSettings } from './details_panel'; + +export { StepAliases, StepMappings, StepSettings } from './wizard_steps'; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts new file mode 100644 index 00000000000000..90ce6227c09c88 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.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. + */ + +export { StepAliases } from './step_aliases'; +export { StepMappings } from './step_mappings'; +export { StepSettings } from './step_settings'; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases.tsx similarity index 80% rename from x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx rename to x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases.tsx index e18846a69b8474..0d28ec4b50c9ab 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases.tsx @@ -19,17 +19,17 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Forms } from '../../../../shared_imports'; -import { documentationService } from '../../../services/documentation'; +import { Forms } from '../../../../../shared_imports'; import { useJsonStep } from './use_json_step'; interface Props { defaultValue: { [key: string]: any }; onChange: (content: Forms.Content) => void; + esDocsBase: string; } export const StepAliases: React.FunctionComponent = React.memo( - ({ defaultValue, onChange }) => { + ({ defaultValue, onChange, esDocsBase }) => { const { jsonContent, setJsonContent, error } = useJsonStep({ defaultValue, onChange, @@ -42,7 +42,7 @@ export const StepAliases: React.FunctionComponent = React.memo(

@@ -53,7 +53,7 @@ export const StepAliases: React.FunctionComponent = React.memo(

@@ -64,13 +64,13 @@ export const StepAliases: React.FunctionComponent = React.memo( @@ -82,13 +82,13 @@ export const StepAliases: React.FunctionComponent = React.memo( } helpText={ = React.memo( showGutter={false} minLines={6} aria-label={i18n.translate( - 'xpack.idxMgmt.templateForm.stepAliases.fieldAliasesAriaLabel', + 'xpack.idxMgmt.formWizard.stepAliases.fieldAliasesAriaLabel', { defaultMessage: 'Aliases code editor', } diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx similarity index 84% rename from x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx rename to x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx index 800cb519a9393f..2b9b689e17cb92 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx @@ -15,23 +15,23 @@ import { EuiText, } from '@elastic/eui'; -import { Forms } from '../../../../shared_imports'; -import { documentationService } from '../../../services/documentation'; +import { Forms } from '../../../../../shared_imports'; import { MappingsEditor, OnUpdateHandler, LoadMappingsFromJsonButton, IndexSettings, -} from '../../mappings_editor'; +} from '../../../mappings_editor'; interface Props { defaultValue: { [key: string]: any }; onChange: (content: Forms.Content) => void; indexSettings?: IndexSettings; + esDocsBase: string; } export const StepMappings: React.FunctionComponent = React.memo( - ({ defaultValue, onChange, indexSettings }) => { + ({ defaultValue, onChange, indexSettings, esDocsBase }) => { const [mappings, setMappings] = useState(defaultValue); const onMappingsEditorUpdate = useCallback( @@ -58,7 +58,7 @@ export const StepMappings: React.FunctionComponent = React.memo(

@@ -69,7 +69,7 @@ export const StepMappings: React.FunctionComponent = React.memo(

@@ -86,12 +86,12 @@ export const StepMappings: React.FunctionComponent = React.memo( diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx similarity index 81% rename from x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx rename to x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx index 4325852d68aaa2..4eafcee0ef519c 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx @@ -19,17 +19,17 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Forms } from '../../../../shared_imports'; -import { documentationService } from '../../../services/documentation'; +import { Forms } from '../../../../../shared_imports'; import { useJsonStep } from './use_json_step'; interface Props { defaultValue: { [key: string]: any }; onChange: (content: Forms.Content) => void; + esDocsBase: string; } export const StepSettings: React.FunctionComponent = React.memo( - ({ defaultValue, onChange }) => { + ({ defaultValue, onChange, esDocsBase }) => { const { jsonContent, setJsonContent, error } = useJsonStep({ defaultValue, onChange, @@ -42,7 +42,7 @@ export const StepSettings: React.FunctionComponent = React.memo(

@@ -53,7 +53,7 @@ export const StepSettings: React.FunctionComponent = React.memo(

@@ -64,12 +64,12 @@ export const StepSettings: React.FunctionComponent = React.memo( @@ -82,13 +82,13 @@ export const StepSettings: React.FunctionComponent = React.memo( } helpText={ {JSON.stringify({ number_of_replicas: 1 })}, @@ -114,7 +114,7 @@ export const StepSettings: React.FunctionComponent = React.memo( showGutter={false} minLines={6} aria-label={i18n.translate( - 'xpack.idxMgmt.templateForm.stepSettings.fieldIndexSettingsAriaLabel', + 'xpack.idxMgmt.formWizard.stepSettings.fieldIndexSettingsAriaLabel', { defaultMessage: 'Index settings editor', } diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/use_json_step.ts similarity index 96% rename from x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts rename to x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/use_json_step.ts index 4c1b36e3abba52..67799f1e76d855 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/use_json_step.ts +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/use_json_step.ts @@ -7,7 +7,7 @@ import { useEffect, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import { isJSON, Forms } from '../../../../shared_imports'; +import { isJSON, Forms } from '../../../../../shared_imports'; interface Parameters { onChange: (content: Forms.Content) => void; diff --git a/x-pack/plugins/index_management/public/application/components/shared/index.ts b/x-pack/plugins/index_management/public/application/components/shared/index.ts index e015ef72e244fa..5ec1f717102709 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/index.ts +++ b/x-pack/plugins/index_management/public/application/components/shared/index.ts @@ -4,4 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export { TabAliases, TabMappings, TabSettings } from './components'; +export { + TabAliases, + TabMappings, + TabSettings, + StepAliases, + StepMappings, + StepSettings, +} from './components'; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx index 634887436f816f..a0e0c59be6622e 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx @@ -6,11 +6,18 @@ import React from 'react'; import { Forms } from '../../../../shared_imports'; +import { documentationService } from '../../../services/documentation'; +import { StepAliases } from '../../shared'; import { WizardContent } from '../template_form'; -import { StepAliases } from './step_aliases'; export const StepAliasesContainer = () => { const { defaultValue, updateContent } = Forms.useContent('aliases'); - return ; + return ( + + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx index 545aec9851592e..80c0d1d4df4890 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx @@ -6,8 +6,9 @@ import React from 'react'; import { Forms } from '../../../../shared_imports'; +import { documentationService } from '../../../services/documentation'; +import { StepMappings } from '../../shared'; import { WizardContent } from '../template_form'; -import { StepMappings } from './step_mappings'; export const StepMappingsContainer = () => { const { defaultValue, updateContent, getData } = Forms.useContent('mappings'); @@ -17,6 +18,7 @@ export const StepMappingsContainer = () => { defaultValue={defaultValue} onChange={updateContent} indexSettings={getData().settings} + esDocsBase={documentationService.getEsDocsBase()} /> ); }; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx index 4d7de644a1442b..b79c6804d382b3 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx @@ -6,11 +6,18 @@ import React from 'react'; import { Forms } from '../../../../shared_imports'; +import { documentationService } from '../../../services/documentation'; +import { StepSettings } from '../../shared'; import { WizardContent } from '../template_form'; -import { StepSettings } from './step_settings'; export const StepSettingsContainer = React.memo(() => { const { defaultValue, updateContent } = Forms.useContent('settings'); - return ; + return ( + + ); }); diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx index 807229fb362676..ab4ce6a61a9b64 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx @@ -30,7 +30,6 @@ import { UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, } from '../../../../../../../common/constants'; -import { TemplateDeserialized } from '../../../../../../../common'; import { TemplateDeleteModal, SectionLoading, @@ -41,7 +40,8 @@ import { useLoadIndexTemplate } from '../../../../../services/api'; import { decodePathFromReactRouter } from '../../../../../services/routing'; import { SendRequestResponse } from '../../../../../../shared_imports'; import { useServices } from '../../../../../app_context'; -import { TabSummary, TabMappings, TabSettings, TabAliases } from '../../template_details/tabs'; +import { TabAliases, TabMappings, TabSettings } from '../../../../../components/shared'; +import { TabSummary } from '../../template_details/tabs'; interface Props { template: { name: string; isLegacy?: boolean }; @@ -83,15 +83,6 @@ const TABS = [ }, ]; -const tabToComponentMap: { - [key: string]: React.FunctionComponent<{ templateDetails: TemplateDeserialized }>; -} = { - [SUMMARY_TAB_ID]: TabSummary, - [SETTINGS_TAB_ID]: TabSettings, - [MAPPINGS_TAB_ID]: TabMappings, - [ALIASES_TAB_ID]: TabAliases, -}; - const tabToUiMetricMap: { [key: string]: string } = { [SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, @@ -144,7 +135,19 @@ export const LegacyTemplateDetails: React.FunctionComponent = ({ /> ); } else if (templateDetails) { - const Content = tabToComponentMap[activeTab]; + const { + template: { settings, mappings, aliases }, + } = templateDetails; + + const tabToComponentMap: Record = { + [SUMMARY_TAB_ID]: , + [SETTINGS_TAB_ID]: , + [MAPPINGS_TAB_ID]: , + [ALIASES_TAB_ID]: , + }; + + const tabContent = tabToComponentMap[activeTab]; + const managedTemplateCallout = isManaged ? ( = ({ - + {tabContent} ); } diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts index 7af28f4688f48b..08ebda2b5e437c 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/index.ts @@ -5,6 +5,3 @@ */ export { TabSummary } from './tab_summary'; -export { TabMappings } from './tab_mappings'; -export { TabSettings } from './tab_settings'; -export { TabAliases } from './tab_aliases'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx deleted file mode 100644 index fa7d734ad0d2ba..00000000000000 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_aliases.tsx +++ /dev/null @@ -1,41 +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 { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCodeBlock, EuiCallOut } from '@elastic/eui'; -import { TemplateDeserialized } from '../../../../../../../common'; - -interface Props { - templateDetails: TemplateDeserialized; -} - -export const TabAliases: React.FunctionComponent = ({ templateDetails }) => { - const { - template: { aliases }, - } = templateDetails; - - if (aliases && Object.keys(aliases).length) { - return ( -
- {JSON.stringify(aliases, null, 2)} -
- ); - } - - return ( - - } - iconType="pin" - data-test-subj="noAliasesCallout" - /> - ); -}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx deleted file mode 100644 index 6e0257c6b377bb..00000000000000 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_mappings.tsx +++ /dev/null @@ -1,41 +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 { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCodeBlock, EuiCallOut } from '@elastic/eui'; -import { TemplateDeserialized } from '../../../../../../../common'; - -interface Props { - templateDetails: TemplateDeserialized; -} - -export const TabMappings: React.FunctionComponent = ({ templateDetails }) => { - const { - template: { mappings }, - } = templateDetails; - - if (mappings && Object.keys(mappings).length) { - return ( -
- {JSON.stringify(mappings, null, 2)} -
- ); - } - - return ( - - } - iconType="pin" - data-test-subj="noMappingsCallout" - /> - ); -}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx deleted file mode 100644 index 8f75c2cb77801b..00000000000000 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_settings.tsx +++ /dev/null @@ -1,41 +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 { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCodeBlock, EuiCallOut } from '@elastic/eui'; -import { TemplateDeserialized } from '../../../../../../../common'; - -interface Props { - templateDetails: TemplateDeserialized; -} - -export const TabSettings: React.FunctionComponent = ({ templateDetails }) => { - const { - template: { settings }, - } = templateDetails; - - if (settings && Object.keys(settings).length) { - return ( -
- {JSON.stringify(settings, null, 2)} -
- ); - } - - return ( - - } - iconType="pin" - data-test-subj="noSettingsCallout" - /> - ); -}; diff --git a/x-pack/plugins/index_management/public/application/services/documentation.ts b/x-pack/plugins/index_management/public/application/services/documentation.ts index fdf07c43a0c8b2..ccccccce197662 100644 --- a/x-pack/plugins/index_management/public/application/services/documentation.ts +++ b/x-pack/plugins/index_management/public/application/services/documentation.ts @@ -20,6 +20,10 @@ class DocumentationService { this.kibanaDocsBase = `${docsBase}/kibana/${DOC_LINK_VERSION}`; } + public getEsDocsBase() { + return this.esDocsBase; + } + public getSettingsDocumentationLink() { return `${this.esDocsBase}/index-modules.html#index-modules-settings`; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fb8a4c3464d118..0567ee675ee759 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7065,9 +7065,6 @@ "xpack.idxMgmt.summary.summaryTitle": "一般", "xpack.idxMgmt.templateCreate.loadingTemplateToCloneDescription": "クローンを作成するテンプレートを読み込み中…", "xpack.idxMgmt.templateCreate.loadingTemplateToCloneErrorMessage": "クローンを作成するテンプレートを読み込み中にエラーが発生", - "xpack.idxMgmt.templateDetails.aliasesTab.noAliasesTitle": "エイリアスが定義されていません。", - "xpack.idxMgmt.templateDetails.mappingsTab.noMappingsTitle": "マッピングが定義されていません。", - "xpack.idxMgmt.templateDetails.settingsTab.noSettingsTitle": "設定が定義されていません。", "xpack.idxMgmt.templateDetails.summaryTab.ilmPolicyDescriptionListTitle": "ILM ポリシー", "xpack.idxMgmt.templateDetails.summaryTab.indexPatternsDescriptionListTitle": "インデックス{numIndexPatterns, plural, one {パターン} other {パターン}}", "xpack.idxMgmt.templateDetails.summaryTab.noneDescriptionText": "なし", @@ -7082,12 +7079,6 @@ "xpack.idxMgmt.templateForm.createButtonLabel": "テンプレートを作成", "xpack.idxMgmt.templateForm.saveButtonLabel": "テンプレートを保存", "xpack.idxMgmt.templateForm.saveTemplateError": "テンプレートを作成できません", - "xpack.idxMgmt.templateForm.stepAliases.aliasesDescription": "エイリアスをセットアップして、インデックスに関連付けてください。", - "xpack.idxMgmt.templateForm.stepAliases.aliasesEditorHelpText": "JSON フォーマットを使用: {code}", - "xpack.idxMgmt.templateForm.stepAliases.docsButtonLabel": "インデックステンプレートドキュメント", - "xpack.idxMgmt.templateForm.stepAliases.fieldAliasesAriaLabel": "エイリアスコードエディター", - "xpack.idxMgmt.templateForm.stepAliases.fieldAliasesLabel": "エイリアス", - "xpack.idxMgmt.templateForm.stepAliases.stepTitle": "エイリアス (任意)", "xpack.idxMgmt.templateForm.stepLogistics.docsButtonLabel": "インデックステンプレートドキュメント", "xpack.idxMgmt.templateForm.stepLogistics.fieldIndexPatternsHelpText": "スペースと {invalidCharactersList} は使用できません。", "xpack.idxMgmt.templateForm.stepLogistics.fieldIndexPatternsLabel": "インデックスパターン", @@ -7103,9 +7094,6 @@ "xpack.idxMgmt.templateForm.stepLogistics.stepTitle": "ロジスティクス", "xpack.idxMgmt.templateForm.stepLogistics.versionDescription": "テンプレートを外部管理システムで識別するための番号です。", "xpack.idxMgmt.templateForm.stepLogistics.versionTitle": "バージョン", - "xpack.idxMgmt.templateForm.stepMappings.docsButtonLabel": "マッピングドキュメント", - "xpack.idxMgmt.templateForm.stepMappings.mappingsDescription": "ドキュメントの保存とインデックス方法を定義します。", - "xpack.idxMgmt.templateForm.stepMappings.stepTitle": "マッピング (任意)", "xpack.idxMgmt.templateForm.stepReview.requestTab.descriptionText": "このリクエストは次のインデックステンプレートを作成します。", "xpack.idxMgmt.templateForm.stepReview.requestTabTitle": "リクエスト", "xpack.idxMgmt.templateForm.stepReview.stepTitle": "「{templateName}」の詳細の確認", @@ -7127,12 +7115,6 @@ "xpack.idxMgmt.templateForm.steps.mappingsStepName": "マッピング", "xpack.idxMgmt.templateForm.steps.settingsStepName": "インデックス設定", "xpack.idxMgmt.templateForm.steps.summaryStepName": "テンプレートのレビュー", - "xpack.idxMgmt.templateForm.stepSettings.docsButtonLabel": "インデックス設定ドキュメント", - "xpack.idxMgmt.templateForm.stepSettings.fieldIndexSettingsAriaLabel": "インデックス設定エディター", - "xpack.idxMgmt.templateForm.stepSettings.fieldIndexSettingsLabel": "インデックス設定", - "xpack.idxMgmt.templateForm.stepSettings.settingsDescription": "インデックスの動作を定義します。", - "xpack.idxMgmt.templateForm.stepSettings.settingsEditorHelpText": "JSON フォーマットを使用: {code}", - "xpack.idxMgmt.templateForm.stepSettings.stepTitle": "インデックス設定 (任意)", "xpack.idxMgmt.templateList.table.ilmPolicyColumnDescription": "インデックスライフサイクルポリシー「{policyName}」", "xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle": "ILM ポリシー", "xpack.idxMgmt.templateList.table.indexPatternsColumnTitle": "インデックスパターン", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3fe58b62c00c33..86f2c44c809da9 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7069,9 +7069,6 @@ "xpack.idxMgmt.summary.summaryTitle": "常规", "xpack.idxMgmt.templateCreate.loadingTemplateToCloneDescription": "正在加载要克隆的模板……", "xpack.idxMgmt.templateCreate.loadingTemplateToCloneErrorMessage": "加载要克隆的模板时出错", - "xpack.idxMgmt.templateDetails.aliasesTab.noAliasesTitle": "未定义任何别名。", - "xpack.idxMgmt.templateDetails.mappingsTab.noMappingsTitle": "未定义任何映射。", - "xpack.idxMgmt.templateDetails.settingsTab.noSettingsTitle": "未定义任何设置。", "xpack.idxMgmt.templateDetails.summaryTab.ilmPolicyDescriptionListTitle": "ILM 策略", "xpack.idxMgmt.templateDetails.summaryTab.indexPatternsDescriptionListTitle": "索引{numIndexPatterns, plural, one {模式} other {模式}}", "xpack.idxMgmt.templateDetails.summaryTab.noneDescriptionText": "无", @@ -7086,12 +7083,6 @@ "xpack.idxMgmt.templateForm.createButtonLabel": "创建模板", "xpack.idxMgmt.templateForm.saveButtonLabel": "保存模板", "xpack.idxMgmt.templateForm.saveTemplateError": "无法创建模板", - "xpack.idxMgmt.templateForm.stepAliases.aliasesDescription": "设置要与索引关联的别名。", - "xpack.idxMgmt.templateForm.stepAliases.aliasesEditorHelpText": "使用 JSON 格式:{code}", - "xpack.idxMgmt.templateForm.stepAliases.docsButtonLabel": "索引模板文档", - "xpack.idxMgmt.templateForm.stepAliases.fieldAliasesAriaLabel": "别名代码编辑器", - "xpack.idxMgmt.templateForm.stepAliases.fieldAliasesLabel": "别名", - "xpack.idxMgmt.templateForm.stepAliases.stepTitle": "别名(可选)", "xpack.idxMgmt.templateForm.stepLogistics.docsButtonLabel": "索引模板文档", "xpack.idxMgmt.templateForm.stepLogistics.fieldIndexPatternsHelpText": "不允许使用空格和字符 {invalidCharactersList}。", "xpack.idxMgmt.templateForm.stepLogistics.fieldIndexPatternsLabel": "索引模式", @@ -7107,9 +7098,6 @@ "xpack.idxMgmt.templateForm.stepLogistics.stepTitle": "运筹", "xpack.idxMgmt.templateForm.stepLogistics.versionDescription": "在外部管理系统中标识该模板的编号。", "xpack.idxMgmt.templateForm.stepLogistics.versionTitle": "版本", - "xpack.idxMgmt.templateForm.stepMappings.docsButtonLabel": "映射文档", - "xpack.idxMgmt.templateForm.stepMappings.mappingsDescription": "定义如何存储和索引文档。", - "xpack.idxMgmt.templateForm.stepMappings.stepTitle": "映射(可选)", "xpack.idxMgmt.templateForm.stepReview.requestTab.descriptionText": "此请求将创建以下索引模板。", "xpack.idxMgmt.templateForm.stepReview.requestTabTitle": "请求", "xpack.idxMgmt.templateForm.stepReview.stepTitle": "查看 “{templateName}” 的详情", @@ -7131,12 +7119,6 @@ "xpack.idxMgmt.templateForm.steps.mappingsStepName": "映射", "xpack.idxMgmt.templateForm.steps.settingsStepName": "索引设置", "xpack.idxMgmt.templateForm.steps.summaryStepName": "复查模板", - "xpack.idxMgmt.templateForm.stepSettings.docsButtonLabel": "索引设置文档", - "xpack.idxMgmt.templateForm.stepSettings.fieldIndexSettingsAriaLabel": "索引设置编辑器", - "xpack.idxMgmt.templateForm.stepSettings.fieldIndexSettingsLabel": "索引设置", - "xpack.idxMgmt.templateForm.stepSettings.settingsDescription": "定义索引的行为。", - "xpack.idxMgmt.templateForm.stepSettings.settingsEditorHelpText": "使用 JSON 格式:{code}", - "xpack.idxMgmt.templateForm.stepSettings.stepTitle": "索引设置(可选)", "xpack.idxMgmt.templateList.table.ilmPolicyColumnDescription": "“{policyName}”索引生命周期策略", "xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle": "ILM 策略", "xpack.idxMgmt.templateList.table.indexPatternsColumnTitle": "索引模式", From 6cc8ea1861fa087199193b337db03c9144fc7687 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 23 Jun 2020 14:38:39 +0300 Subject: [PATCH 2/8] Migrate dashboard mode (#69305) Closes: #67469 Co-authored-by: Elastic Machine --- x-pack/.i18nrc.json | 2 +- x-pack/index.js | 4 +- x-pack/legacy/plugins/dashboard_mode/index.js | 64 ------- .../dashboard_mode_request_interceptor.js | 163 ------------------ .../dashboard_mode_request_interceptor.js | 73 -------- x-pack/legacy/plugins/ingest_manager/index.ts | 1 + .../dashboard_mode/common/constants.ts} | 4 +- .../dashboard_mode/common/index.ts} | 2 +- x-pack/plugins/dashboard_mode/kibana.json | 7 +- x-pack/plugins/dashboard_mode/server/index.ts | 14 +- ...dashboard_mode_request_interceptor.test.ts | 111 ++++++++++++ .../dashboard_mode_request_interceptor.ts | 79 +++++++++ .../server/interceptors/index.ts} | 2 +- .../plugins/dashboard_mode/server/plugin.ts | 62 +++++++ .../dashboard_mode/server/ui_settings.ts | 34 ++++ .../plugins/ingest_manager/server/plugin.ts | 2 +- .../test/api_integration/apis/fleet/index.js | 2 +- x-pack/test/api_integration/apis/index.js | 4 +- 18 files changed, 311 insertions(+), 319 deletions(-) delete mode 100644 x-pack/legacy/plugins/dashboard_mode/index.js delete mode 100644 x-pack/legacy/plugins/dashboard_mode/server/__tests__/dashboard_mode_request_interceptor.js delete mode 100644 x-pack/legacy/plugins/dashboard_mode/server/dashboard_mode_request_interceptor.js rename x-pack/{legacy/plugins/dashboard_mode/server/index.js => plugins/dashboard_mode/common/constants.ts} (71%) rename x-pack/{legacy/plugins/dashboard_mode/common/index.js => plugins/dashboard_mode/common/index.ts} (84%) create mode 100644 x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts create mode 100644 x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts rename x-pack/{legacy/plugins/dashboard_mode/common/constants.js => plugins/dashboard_mode/server/interceptors/index.ts} (72%) create mode 100644 x-pack/plugins/dashboard_mode/server/plugin.ts create mode 100644 x-pack/plugins/dashboard_mode/server/ui_settings.ts diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 278968cb472315..36cfdf904d6d43 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -11,7 +11,7 @@ "xpack.dashboard": "plugins/dashboard_enhanced", "xpack.discover": "plugins/discover_enhanced", "xpack.crossClusterReplication": "plugins/cross_cluster_replication", - "xpack.dashboardMode": "legacy/plugins/dashboard_mode", + "xpack.dashboardMode": "plugins/dashboard_mode", "xpack.data": "plugins/data_enhanced", "xpack.embeddableEnhanced": "plugins/embeddable_enhanced", "xpack.endpoint": "plugins/endpoint", diff --git a/x-pack/index.js b/x-pack/index.js index e7dd4886e60526..2d2e42650cfa7d 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -7,7 +7,6 @@ import { xpackMain } from './legacy/plugins/xpack_main'; import { monitoring } from './legacy/plugins/monitoring'; import { security } from './legacy/plugins/security'; -import { dashboardMode } from './legacy/plugins/dashboard_mode'; import { beats } from './legacy/plugins/beats_management'; import { spaces } from './legacy/plugins/spaces'; import { ingestManager } from './legacy/plugins/ingest_manager'; @@ -18,8 +17,7 @@ module.exports = function (kibana) { monitoring(kibana), spaces(kibana), security(kibana), - dashboardMode(kibana), - beats(kibana), ingestManager(kibana), + beats(kibana), ]; }; diff --git a/x-pack/legacy/plugins/dashboard_mode/index.js b/x-pack/legacy/plugins/dashboard_mode/index.js deleted file mode 100644 index 1145fad2a8a282..00000000000000 --- a/x-pack/legacy/plugins/dashboard_mode/index.js +++ /dev/null @@ -1,64 +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 { resolve } from 'path'; -import { i18n } from '@kbn/i18n'; -import { CONFIG_DASHBOARD_ONLY_MODE_ROLES } from './common'; -import { createDashboardModeRequestInterceptor } from './server'; - -// Copied largely from plugins/kibana/index.js. The dashboard viewer includes just the dashboard section of -// the standard kibana plugin. We don't want to include code for the other links (visualize, dev tools, etc) -// since it's view only, but we want the urls to be the same, so we are using largely the same setup. -export function dashboardMode(kibana) { - return new kibana.Plugin({ - id: 'dashboard_mode', - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main'], - uiExports: { - uiSettingDefaults: { - [CONFIG_DASHBOARD_ONLY_MODE_ROLES]: { - name: i18n.translate('xpack.dashboardMode.uiSettings.dashboardsOnlyRolesTitle', { - defaultMessage: 'Dashboards only roles', - }), - description: i18n.translate( - 'xpack.dashboardMode.uiSettings.dashboardsOnlyRolesDescription', - { - defaultMessage: 'Roles that belong to View Dashboards Only mode', - } - ), - value: ['kibana_dashboard_only_user'], - category: ['dashboard'], - deprecation: { - message: i18n.translate( - 'xpack.dashboardMode.uiSettings.dashboardsOnlyRolesDeprecation', - { - defaultMessage: 'This setting is deprecated and will be removed in Kibana 8.0.', - } - ), - docLinksKey: 'dashboardSettings', - }, - }, - }, - }, - - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - - init(server) { - server.injectUiAppVars( - 'dashboardViewer', - async () => await server.getInjectedUiAppVars('kibana') - ); - - if (server.plugins.security) { - server.ext(createDashboardModeRequestInterceptor()); - } - }, - }); -} diff --git a/x-pack/legacy/plugins/dashboard_mode/server/__tests__/dashboard_mode_request_interceptor.js b/x-pack/legacy/plugins/dashboard_mode/server/__tests__/dashboard_mode_request_interceptor.js deleted file mode 100644 index 3fd9bf5f59d525..00000000000000 --- a/x-pack/legacy/plugins/dashboard_mode/server/__tests__/dashboard_mode_request_interceptor.js +++ /dev/null @@ -1,163 +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 expect from '@kbn/expect'; -import Hapi from 'hapi'; - -import { createDashboardModeRequestInterceptor } from '../dashboard_mode_request_interceptor'; - -const DASHBOARD_ONLY_MODE_ROLE = 'test_dashboard_only_mode_role'; - -function setup() { - const server = new Hapi.Server(); - - server.decorate('request', 'getUiSettingsService', () => { - return { - get: () => Promise.resolve([DASHBOARD_ONLY_MODE_ROLE]), - }; - }); - - // attach the extension - server.ext(createDashboardModeRequestInterceptor()); - - // allow the extension to fake "render an app" - server.decorate('toolkit', 'renderApp', function (app) { - // `this` is the `h` response toolkit - return this.response({ renderApp: true, app }); - }); - - server.decorate('server', 'newPlatform', { - setup: { - core: { - http: { - basePath: { - get: () => '', - }, - }, - }, - }, - }); - - server.route({ - path: '/app/{appId}', - method: 'GET', - handler(req, h) { - return h.renderApp({ name: req.params.appId }); - }, - }); - - // catch all route for determining when we get through the extensions - server.route({ - path: '/{path*}', - method: 'GET', - handler(req) { - return { catchAll: true, path: `/${req.params.path}` }; - }, - }); - - return { server }; -} - -describe('DashboardOnlyModeRequestInterceptor', () => { - describe('request is not for dashboad-only user', () => { - describe('app route', () => { - it('lets the route render as normal', async () => { - const { server } = setup(); - const response = await server.inject({ - url: '/app/kibana', - credentials: { - roles: ['foo', 'bar'], - }, - }); - - expect(response) - .to.have.property('statusCode', 200) - .and.have.property('result') - .eql({ - renderApp: true, - app: { name: 'kibana' }, - }); - }); - }); - - describe('non-app route', () => { - it('lets the route render as normal', async () => { - const { server } = setup(); - const response = await server.inject({ - url: '/foo/bar', - credentials: { - roles: ['foo', 'bar'], - }, - }); - - expect(response).to.have.property('statusCode', 200).and.have.property('result').eql({ - catchAll: true, - path: '/foo/bar', - }); - }); - }); - }); - - describe('request for dashboard-only user', () => { - describe('non-kibana app route', () => { - it('responds with 404', async () => { - const { server } = setup(); - const response = await server.inject({ - url: '/app/foo', - credentials: { - roles: [DASHBOARD_ONLY_MODE_ROLE], - }, - }); - - expect(response).to.have.property('statusCode', 404); - }); - }); - - describe('requests to dashboard_mode app', () => { - it('lets the route render as normal', async () => { - const { server } = setup(); - const response = await server.inject({ - url: '/app/dashboard_mode', - credentials: { - roles: [DASHBOARD_ONLY_MODE_ROLE], - }, - }); - - expect(response) - .to.have.property('statusCode', 200) - .and.have.property('result') - .eql({ - renderApp: true, - app: { name: 'dashboard_mode' }, - }); - }); - }); - - function testRedirectToDashboardModeApp(url) { - describe(`requests to url:"${url}"`, () => { - it('redirects to the dashboard_mode app instead', async () => { - const { server } = setup(); - const response = await server.inject({ - url: url, - credentials: { - roles: [DASHBOARD_ONLY_MODE_ROLE], - }, - }); - - expect(response).to.have.property('statusCode', 301); - expect(response.headers).to.have.property('location', '/app/dashboard_mode'); - }); - }); - } - - testRedirectToDashboardModeApp('/app/kibana'); - testRedirectToDashboardModeApp('/app/kibana#/foo/bar'); - testRedirectToDashboardModeApp('/app/kibana/foo/bar'); - testRedirectToDashboardModeApp('/app/kibana?foo=bar'); - testRedirectToDashboardModeApp('/app/dashboards?foo=bar'); - testRedirectToDashboardModeApp('/app/home?foo=bar'); - }); -}); diff --git a/x-pack/legacy/plugins/dashboard_mode/server/dashboard_mode_request_interceptor.js b/x-pack/legacy/plugins/dashboard_mode/server/dashboard_mode_request_interceptor.js deleted file mode 100644 index 76a582d6cf2396..00000000000000 --- a/x-pack/legacy/plugins/dashboard_mode/server/dashboard_mode_request_interceptor.js +++ /dev/null @@ -1,73 +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 Boom from 'boom'; - -import { CONFIG_DASHBOARD_ONLY_MODE_ROLES } from '../common'; - -const superuserRole = 'superuser'; - -/** - * Intercept all requests after auth has completed and apply filtering - * logic to enforce dashboard only mode. - * - * @type {Hapi.RequestExtension} - */ -export function createDashboardModeRequestInterceptor() { - return { - type: 'onPostAuth', - async method(request, h) { - const { auth, url } = request; - const user = auth.credentials; - const roles = user ? user.roles : []; - - if (!user) { - return h.continue; - } - - const isAppRequest = url.path.startsWith('/app/'); - - // The act of retrieving this setting ends up creating the config document if it doesn't already exist. - // Various functional tests have come to indirectly rely on this behavior, so changing this is non-trivial. - // This will be addressed once dashboard-only-mode is removed altogether. - const uiSettings = request.getUiSettingsService(); - const dashboardOnlyModeRoles = await uiSettings.get(CONFIG_DASHBOARD_ONLY_MODE_ROLES); - - if (!isAppRequest || !dashboardOnlyModeRoles || !roles || roles.length === 0) { - return h.continue; - } - - const isDashboardOnlyModeUser = user.roles.find((role) => - dashboardOnlyModeRoles.includes(role) - ); - const isSuperUser = user.roles.find((role) => role === superuserRole); - - const enforceDashboardOnlyMode = isDashboardOnlyModeUser && !isSuperUser; - if (enforceDashboardOnlyMode) { - if ( - url.path.startsWith('/app/home') || - url.path.startsWith('/app/kibana') || - url.path.startsWith('/app/dashboards') - ) { - const basePath = request.server.newPlatform.setup.core.http.basePath.get(request); - const url = `${basePath}/app/dashboard_mode`; - // If the user is in "Dashboard only mode" they should only be allowed to see - // the dashboard app and none others. If the kibana app is requested, this might be a old - // url we will migrate on the fly. - return h.redirect(url).permanent().takeover(); - } - if (url.path.startsWith('/app/dashboard_mode')) { - // let through requests to the dashboard_mode app - return h.continue; - } - - throw Boom.notFound(); - } - - return h.continue; - }, - }; -} diff --git a/x-pack/legacy/plugins/ingest_manager/index.ts b/x-pack/legacy/plugins/ingest_manager/index.ts index df9923d9f11ecc..2b20bf16f2400e 100644 --- a/x-pack/legacy/plugins/ingest_manager/index.ts +++ b/x-pack/legacy/plugins/ingest_manager/index.ts @@ -8,6 +8,7 @@ import { resolve } from 'path'; export function ingestManager(kibana: any) { return new kibana.Plugin({ id: 'ingestManager', + require: ['kibana', 'elasticsearch', 'xpack_main'], publicDir: resolve(__dirname, '../../../plugins/ingest_manager/public'), }); } diff --git a/x-pack/legacy/plugins/dashboard_mode/server/index.js b/x-pack/plugins/dashboard_mode/common/constants.ts similarity index 71% rename from x-pack/legacy/plugins/dashboard_mode/server/index.js rename to x-pack/plugins/dashboard_mode/common/constants.ts index a7193bb560ca18..f5d36fe9799c7a 100644 --- a/x-pack/legacy/plugins/dashboard_mode/server/index.js +++ b/x-pack/plugins/dashboard_mode/common/constants.ts @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { createDashboardModeRequestInterceptor } from './dashboard_mode_request_interceptor'; +export const UI_SETTINGS = { + CONFIG_DASHBOARD_ONLY_MODE_ROLES: 'xpackDashboardMode:roles', +}; diff --git a/x-pack/legacy/plugins/dashboard_mode/common/index.js b/x-pack/plugins/dashboard_mode/common/index.ts similarity index 84% rename from x-pack/legacy/plugins/dashboard_mode/common/index.js rename to x-pack/plugins/dashboard_mode/common/index.ts index 358d0d5b7e0766..60cf0060636d7a 100644 --- a/x-pack/legacy/plugins/dashboard_mode/common/index.js +++ b/x-pack/plugins/dashboard_mode/common/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './constants'; +export { UI_SETTINGS } from './constants'; diff --git a/x-pack/plugins/dashboard_mode/kibana.json b/x-pack/plugins/dashboard_mode/kibana.json index dfe32210250928..4777b9b25be238 100644 --- a/x-pack/plugins/dashboard_mode/kibana.json +++ b/x-pack/plugins/dashboard_mode/kibana.json @@ -3,10 +3,13 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": [ - "xpack", "dashboard_mode" + "xpack", + "dashboard_mode" ], + "optionalPlugins": ["security"], "requiredPlugins": [ - "kibanaLegacy", "dashboard" + "kibanaLegacy", + "dashboard" ], "server": true, "ui": true diff --git a/x-pack/plugins/dashboard_mode/server/index.ts b/x-pack/plugins/dashboard_mode/server/index.ts index 2a8890c2f81acf..671a398734ac1d 100644 --- a/x-pack/plugins/dashboard_mode/server/index.ts +++ b/x-pack/plugins/dashboard_mode/server/index.ts @@ -4,17 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginConfigDescriptor } from 'kibana/server'; - +import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server'; import { schema } from '@kbn/config-schema'; +import { DashboardModeServerPlugin } from './plugin'; + export const config: PluginConfigDescriptor = { schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), }), }; -export const plugin = () => ({ - setup() {}, - start() {}, -}); +export function plugin(initializerContext: PluginInitializerContext) { + return new DashboardModeServerPlugin(initializerContext); +} + +export { DashboardModeServerPlugin as Plugin }; diff --git a/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts new file mode 100644 index 00000000000000..2978c48af7414b --- /dev/null +++ b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts @@ -0,0 +1,111 @@ +/* + * 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 { + OnPostAuthHandler, + OnPostAuthToolkit, + KibanaRequest, + LifecycleResponseFactory, + IUiSettingsClient, +} from 'kibana/server'; +import { coreMock } from '../../../../../src/core/server/mocks'; + +import { AuthenticatedUser } from '../../../security/server'; +import { securityMock } from '../../../security/server/mocks'; + +import { setupDashboardModeRequestInterceptor } from './dashboard_mode_request_interceptor'; + +const DASHBOARD_ONLY_MODE_ROLE = 'test_dashboard_only_mode_role'; + +describe('DashboardOnlyModeRequestInterceptor', () => { + const core = coreMock.createSetup(); + const security = securityMock.createSetup(); + + let interceptor: OnPostAuthHandler; + let toolkit: OnPostAuthToolkit; + let uiSettingsMock: any; + + beforeEach(() => { + toolkit = { + next: jest.fn(), + }; + interceptor = setupDashboardModeRequestInterceptor({ + http: core.http, + security, + getUiSettingsClient: () => + (Promise.resolve({ + get: () => Promise.resolve(uiSettingsMock), + }) as unknown) as Promise, + }); + }); + + test('should not redirects for not app/* requests', async () => { + const request = ({ + url: { + path: 'api/test', + }, + } as unknown) as KibanaRequest; + + interceptor(request, {} as LifecycleResponseFactory, toolkit); + + expect(toolkit.next).toHaveBeenCalled(); + }); + + test('should not redirects not authenticated users', async () => { + const request = ({ + url: { + path: '/app/home', + }, + } as unknown) as KibanaRequest; + + interceptor(request, {} as LifecycleResponseFactory, toolkit); + + expect(toolkit.next).toHaveBeenCalled(); + }); + + describe('request for dashboard-only user', () => { + function testRedirectToDashboardModeApp(url: string) { + describe(`requests to url:"${url}"`, () => { + test('redirects to the dashboard_mode app instead', async () => { + const request = ({ + url: { + path: url, + }, + credentials: { + roles: [DASHBOARD_ONLY_MODE_ROLE], + }, + } as unknown) as KibanaRequest; + + const response = ({ + redirected: jest.fn(), + } as unknown) as LifecycleResponseFactory; + + security.authc.getCurrentUser = jest.fn( + (r: KibanaRequest) => + ({ + roles: [DASHBOARD_ONLY_MODE_ROLE], + } as AuthenticatedUser) + ); + + uiSettingsMock = [DASHBOARD_ONLY_MODE_ROLE]; + + await interceptor(request, response, toolkit); + + expect(response.redirected).toHaveBeenCalledWith({ + headers: { location: `/mock-server-basepath/app/dashboard_mode` }, + }); + }); + }); + } + + testRedirectToDashboardModeApp('/app/kibana'); + testRedirectToDashboardModeApp('/app/kibana#/foo/bar'); + testRedirectToDashboardModeApp('/app/kibana/foo/bar'); + testRedirectToDashboardModeApp('/app/kibana?foo=bar'); + testRedirectToDashboardModeApp('/app/dashboards?foo=bar'); + testRedirectToDashboardModeApp('/app/home?foo=bar'); + }); +}); diff --git a/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts new file mode 100644 index 00000000000000..4378c818f087c5 --- /dev/null +++ b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts @@ -0,0 +1,79 @@ +/* + * 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 { HttpServiceSetup, OnPostAuthHandler, IUiSettingsClient } from 'kibana/server'; +import { SecurityPluginSetup } from '../../../security/server'; +import { UI_SETTINGS } from '../../common'; + +const superuserRole = 'superuser'; + +interface DashboardModeRequestInterceptorDependencies { + http: HttpServiceSetup; + security: SecurityPluginSetup; + getUiSettingsClient: () => Promise; +} + +export const setupDashboardModeRequestInterceptor = ({ + http, + security, + getUiSettingsClient, +}: DashboardModeRequestInterceptorDependencies) => + (async (request, response, toolkit) => { + const path = request.url.path || ''; + const isAppRequest = path.startsWith('/app/'); + + if (!isAppRequest) { + return toolkit.next(); + } + + const authenticatedUser = security.authc.getCurrentUser(request); + const roles = authenticatedUser?.roles || []; + + if (!authenticatedUser || roles.length === 0) { + return toolkit.next(); + } + + const uiSettings = await getUiSettingsClient(); + const dashboardOnlyModeRoles = await uiSettings.get( + UI_SETTINGS.CONFIG_DASHBOARD_ONLY_MODE_ROLES + ); + + if (!dashboardOnlyModeRoles) { + return toolkit.next(); + } + + const isDashboardOnlyModeUser = roles.find((role) => dashboardOnlyModeRoles.includes(role)); + const isSuperUser = roles.find((role) => role === superuserRole); + + const enforceDashboardOnlyMode = isDashboardOnlyModeUser && !isSuperUser; + + if (enforceDashboardOnlyMode) { + if ( + path.startsWith('/app/home') || + path.startsWith('/app/kibana') || + path.startsWith('/app/dashboards') + ) { + const dashBoardModeUrl = `${http.basePath.get(request)}/app/dashboard_mode`; + // If the user is in "Dashboard only mode" they should only be allowed to see + // the dashboard app and none others. + + return response.redirected({ + headers: { + location: dashBoardModeUrl, + }, + }); + } + + if (path.startsWith('/app/dashboard_mode')) { + // let through requests to the dashboard_mode app + return toolkit.next(); + } + + return response.notFound(); + } + + return toolkit.next(); + }) as OnPostAuthHandler; diff --git a/x-pack/legacy/plugins/dashboard_mode/common/constants.js b/x-pack/plugins/dashboard_mode/server/interceptors/index.ts similarity index 72% rename from x-pack/legacy/plugins/dashboard_mode/common/constants.js rename to x-pack/plugins/dashboard_mode/server/interceptors/index.ts index c9a2378ac5d823..e0bf175d15029c 100644 --- a/x-pack/legacy/plugins/dashboard_mode/common/constants.js +++ b/x-pack/plugins/dashboard_mode/server/interceptors/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const CONFIG_DASHBOARD_ONLY_MODE_ROLES = 'xpackDashboardMode:roles'; +export { setupDashboardModeRequestInterceptor } from './dashboard_mode_request_interceptor'; diff --git a/x-pack/plugins/dashboard_mode/server/plugin.ts b/x-pack/plugins/dashboard_mode/server/plugin.ts new file mode 100644 index 00000000000000..8b56f71b667cb6 --- /dev/null +++ b/x-pack/plugins/dashboard_mode/server/plugin.ts @@ -0,0 +1,62 @@ +/* + * 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 { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + SavedObjectsClient, + Logger, +} from '../../../../src/core/server'; + +import { SecurityPluginSetup } from '../../security/server'; +import { setupDashboardModeRequestInterceptor } from './interceptors'; + +import { getUiSettings } from './ui_settings'; + +interface DashboardModeServerSetupDependencies { + security?: SecurityPluginSetup; +} + +export class DashboardModeServerPlugin implements Plugin { + private initializerContext: PluginInitializerContext; + private logger?: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public setup(core: CoreSetup, { security }: DashboardModeServerSetupDependencies) { + this.logger = this.initializerContext.logger.get(); + + core.uiSettings.register(getUiSettings()); + + const getUiSettingsClient = async () => { + const [coreStart] = await core.getStartServices(); + const { savedObjects, uiSettings } = coreStart; + const savedObjectsClient = new SavedObjectsClient(savedObjects.createInternalRepository()); + + return uiSettings.asScopedToClient(savedObjectsClient); + }; + + if (security) { + const dashboardModeRequestInterceptor = setupDashboardModeRequestInterceptor({ + http: core.http, + security, + getUiSettingsClient, + }); + + core.http.registerOnPostAuth(dashboardModeRequestInterceptor); + + this.logger.debug(`registered DashboardModeRequestInterceptor`); + } + } + + public start(core: CoreStart) {} + + public stop() {} +} diff --git a/x-pack/plugins/dashboard_mode/server/ui_settings.ts b/x-pack/plugins/dashboard_mode/server/ui_settings.ts new file mode 100644 index 00000000000000..f692ec8a33fc91 --- /dev/null +++ b/x-pack/plugins/dashboard_mode/server/ui_settings.ts @@ -0,0 +1,34 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from 'kibana/server'; +import { UI_SETTINGS } from '../common'; + +const DASHBOARD_ONLY_USER_ROLE = 'kibana_dashboard_only_user'; + +export function getUiSettings(): Record> { + return { + [UI_SETTINGS.CONFIG_DASHBOARD_ONLY_MODE_ROLES]: { + name: i18n.translate('xpack.dashboardMode.uiSettings.dashboardsOnlyRolesTitle', { + defaultMessage: 'Dashboards only roles', + }), + description: i18n.translate('xpack.dashboardMode.uiSettings.dashboardsOnlyRolesDescription', { + defaultMessage: 'Roles that belong to View Dashboards Only mode', + }), + value: [DASHBOARD_ONLY_USER_ROLE], + category: ['dashboard'], + deprecation: { + message: i18n.translate('xpack.dashboardMode.uiSettings.dashboardsOnlyRolesDeprecation', { + defaultMessage: 'This setting is deprecated and will be removed in Kibana 8.0.', + }), + docLinksKey: 'dashboardSettings', + }, + schema: schema.arrayOf(schema.string()), + }, + }; +} diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 13301df471c539..fb1c218e1545b4 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -217,7 +217,7 @@ export class IngestManagerPlugin encryptedSavedObjects: EncryptedSavedObjectsPluginStart; } ) { - appContextService.start({ + await appContextService.start({ encryptedSavedObjectsStart: plugins.encryptedSavedObjects, encryptedSavedObjectsSetup: this.encryptedSavedObjectsSetup, security: this.security, diff --git a/x-pack/test/api_integration/apis/fleet/index.js b/x-pack/test/api_integration/apis/fleet/index.js index a8c026ac6a1bd0..df81b826132a9d 100644 --- a/x-pack/test/api_integration/apis/fleet/index.js +++ b/x-pack/test/api_integration/apis/fleet/index.js @@ -6,6 +6,7 @@ export default function loadTests({ loadTestFile }) { describe('Fleet Endpoints', () => { + loadTestFile(require.resolve('./setup')); loadTestFile(require.resolve('./delete_agent')); loadTestFile(require.resolve('./list_agent')); loadTestFile(require.resolve('./unenroll_agent')); @@ -16,7 +17,6 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./enrollment_api_keys/crud')); loadTestFile(require.resolve('./install')); loadTestFile(require.resolve('./agents/actions')); - loadTestFile(require.resolve('./setup')); loadTestFile(require.resolve('./agent_flow')); }); } diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index b79dc3f3ffe59d..3f3294c85d6df3 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -27,9 +27,9 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./short_urls')); loadTestFile(require.resolve('./lens')); loadTestFile(require.resolve('./fleet')); - loadTestFile(require.resolve('./ingest_manager')); - loadTestFile(require.resolve('./endpoint')); loadTestFile(require.resolve('./ml')); loadTestFile(require.resolve('./transform')); + loadTestFile(require.resolve('./endpoint')); + loadTestFile(require.resolve('./ingest_manager')); }); } From 572d006d9fdc0c544e7188125f10e5d6113b8455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 23 Jun 2020 14:08:17 +0100 Subject: [PATCH 3/8] [Observability]Adding specific Return APIs for each plugin (#69257) * Adding specific apis for each plugin * adding metric hosts stat * addressing PR comment * addressing PR comments * changing series to key/value * exporting interfaces * adding label to stat * refactoring types Co-authored-by: Elastic Machine --- .../observability/public/data_handler.ts | 23 ++++- .../public/typings/data_handler/index.d.ts | 42 --------- .../typings/fetch_data_response/index.d.ts | 86 +++++++++++++++++++ 3 files changed, 107 insertions(+), 44 deletions(-) delete mode 100644 x-pack/plugins/observability/public/typings/data_handler/index.d.ts create mode 100644 x-pack/plugins/observability/public/typings/fetch_data_response/index.d.ts diff --git a/x-pack/plugins/observability/public/data_handler.ts b/x-pack/plugins/observability/public/data_handler.ts index 30a7357404d23b..8f80f79b2e829a 100644 --- a/x-pack/plugins/observability/public/data_handler.ts +++ b/x-pack/plugins/observability/public/data_handler.ts @@ -4,9 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FetchData, HasData } from './typings/data_handler'; +import { ObservabilityFetchDataResponse, FetchDataResponse } from './typings/fetch_data_response'; import { ObservabilityApp } from '../typings/common'; +interface FetchDataParams { + // The start timestamp in milliseconds of the queried time interval + startTime: string; + // The end timestamp in milliseconds of the queried time interval + endTime: string; + // The aggregation bucket size in milliseconds if applicable to the data source + bucketSize: string; +} + +export type FetchData = ( + fetchDataParams: FetchDataParams +) => Promise; +export type HasData = () => Promise; + interface DataHandler { fetchData: FetchData; hasData: HasData; @@ -14,7 +28,12 @@ interface DataHandler { const dataHandlers: Partial> = {}; -export type RegisterDataHandler = (params: { appName: ObservabilityApp } & DataHandler) => void; +export type RegisterDataHandler = (params: { + appName: T; + fetchData: FetchData; + hasData: HasData; +}) => void; + export const registerDataHandler: RegisterDataHandler = ({ appName, fetchData, hasData }) => { dataHandlers[appName] = { fetchData, hasData }; }; diff --git a/x-pack/plugins/observability/public/typings/data_handler/index.d.ts b/x-pack/plugins/observability/public/typings/data_handler/index.d.ts deleted file mode 100644 index a208e4e7c223d5..00000000000000 --- a/x-pack/plugins/observability/public/typings/data_handler/index.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -interface Stat { - label: string; - value: string; - color?: string; -} - -export interface Coordinates { - x: number; - y?: number; -} - -interface Series { - label: string; - coordinates: Coordinates[]; - color?: string; - key?: string; -} - -interface FetchDataResponse { - title: string; - appLink: string; - stats: Stat[]; - series: Series[]; -} -interface FetchDataParams { - // The start timestamp in milliseconds of the queried time interval - startTime: string; - // The end timestamp in milliseconds of the queried time interval - endTime: string; - // The aggregation bucket size in milliseconds if applicable to the data source - bucketSize: string; -} - -export type FetchData = (fetchDataParams: FetchDataParams) => Promise; - -export type HasData = () => Promise; diff --git a/x-pack/plugins/observability/public/typings/fetch_data_response/index.d.ts b/x-pack/plugins/observability/public/typings/fetch_data_response/index.d.ts new file mode 100644 index 00000000000000..30ecb24a58a5a5 --- /dev/null +++ b/x-pack/plugins/observability/public/typings/fetch_data_response/index.d.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface Percentage { + label: string; + pct: number; + color?: string; +} +interface Bytes { + label: string; + bytes: number; + color?: string; +} +interface Numeral { + label: string; + value: number; + color?: string; +} + +export interface Coordinates { + x: number; + y?: number; +} + +interface Series { + label: string; + coordinates: Coordinates[]; + color?: string; +} + +export interface FetchDataResponse { + title: string; + appLink: string; +} + +export interface LogsFetchDataResponse extends FetchDataResponse { + stats: Record; + series: Record; +} + +export interface MetricsFetchDataResponse extends FetchDataResponse { + stats: { + hosts: Numeral; + cpu: Percentage; + memory: Percentage; + disk: Percentage; + inboundTraffic: Bytes; + outboundTraffic: Bytes; + }; + series: { + inboundTraffic: Series; + outboundTraffic: Series; + }; +} + +export interface UptimeFetchDataResponse extends FetchDataResponse { + stats: { + monitors: Numeral; + up: Numeral; + down: Numeral; + }; + series: { + up: Series; + down: Series; + }; +} + +export interface ApmFetchDataResponse extends FetchDataResponse { + stats: { + services: Numeral; + transactions: Numeral; + }; + series: { + transactions: Series; + }; +} + +export interface ObservabilityFetchDataResponse { + apm: ApmFetchDataResponse; + infra_metrics: MetricsFetchDataResponse; + infra_logs: LogsFetchDataResponse; + uptime: UptimeFetchDataResponse; +} From 43ed4e20432bd620ff94048c5ebe294afe75c19d Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 23 Jun 2020 15:55:58 +0200 Subject: [PATCH 4/8] [ML] fix ml api services (#69681) --- .../application/services/ml_api_service/index.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index 6d32fca6a645c1..af6944d7ae2d24 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -109,7 +109,6 @@ export type MlApiServices = ReturnType; export const ml = mlApiServicesProvider(new HttpService(proxyHttpStart)); export function mlApiServicesProvider(httpService: HttpService) { - const { http } = httpService; return { getJobs(obj?: { jobId?: string }) { const jobId = obj && obj.jobId ? `/${obj.jobId}` : ''; @@ -142,14 +141,14 @@ export function mlApiServicesProvider(httpService: HttpService) { }, closeJob({ jobId }: { jobId: string }) { - return http({ + return httpService.http({ path: `${basePath()}/anomaly_detectors/${jobId}/_close`, method: 'POST', }); }, forceCloseJob({ jobId }: { jobId: string }) { - return http({ + return httpService.http({ path: `${basePath()}/anomaly_detectors/${jobId}/_close?force=true`, method: 'POST', }); @@ -278,14 +277,14 @@ export function mlApiServicesProvider(httpService: HttpService) { }, stopDatafeed({ datafeedId }: { datafeedId: string }) { - return http({ + return httpService.http({ path: `${basePath()}/datafeeds/${datafeedId}/_stop`, method: 'POST', }); }, forceStopDatafeed({ datafeedId }: { datafeedId: string }) { - return http({ + return httpService.http({ path: `${basePath()}/datafeeds/${datafeedId}/_stop?force=true`, method: 'POST', }); @@ -697,7 +696,7 @@ export function mlApiServicesProvider(httpService: HttpService) { }, getModelSnapshots(jobId: string, snapshotId?: string) { - return http({ + return httpService.http({ path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots${ snapshotId !== undefined ? `/${snapshotId}` : '' }`, @@ -709,7 +708,7 @@ export function mlApiServicesProvider(httpService: HttpService) { snapshotId: string, body: { description?: string; retain?: boolean } ) { - return http({ + return httpService.http({ path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}/_update`, method: 'POST', body: JSON.stringify(body), @@ -717,7 +716,7 @@ export function mlApiServicesProvider(httpService: HttpService) { }, deleteModelSnapshot(jobId: string, snapshotId: string) { - return http({ + return httpService.http({ path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}`, method: 'DELETE', }); From cc893f3da4ad92c6ec10cfc96d5a2ad362789775 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 23 Jun 2020 16:02:22 +0200 Subject: [PATCH 5/8] [Discover] Unskip context navigation functional test (#68771) --- .../apps/context/_context_navigation.js | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/test/functional/apps/context/_context_navigation.js b/test/functional/apps/context/_context_navigation.js index 92b109c7263ff5..babefe488d7bcf 100644 --- a/test/functional/apps/context/_context_navigation.js +++ b/test/functional/apps/context/_context_navigation.js @@ -17,12 +17,8 @@ * under the License. */ -import expect from '@kbn/expect'; - -const TEST_COLUMN_NAMES = ['@message']; const TEST_FILTER_COLUMN_NAMES = [ ['extension', 'jpg'], - ['geo.src', 'IN'], [ 'agent', 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24', @@ -30,20 +26,15 @@ const TEST_FILTER_COLUMN_NAMES = [ ]; export default function ({ getService, getPageObjects }) { + const retry = getService('retry'); const browser = getService('browser'); const docTable = getService('docTable'); const PageObjects = getPageObjects(['common', 'context', 'discover', 'timePicker']); - // FLAKY: https://github.com/elastic/kibana/issues/62866 - describe.skip('context link in discover', function contextSize() { + describe('discover - context - back navigation', function contextSize() { before(async function () { + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - await Promise.all( - TEST_COLUMN_NAMES.map((columnName) => - PageObjects.discover.clickFieldListItemAdd(columnName) - ) - ); for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { await PageObjects.discover.clickFieldListItem(columnName); await PageObjects.discover.clickFieldListPlusFilter(columnName, value); @@ -51,18 +42,22 @@ export default function ({ getService, getPageObjects }) { }); it('should go back after loading', async function () { - // navigate to the context view - await docTable.clickRowToggle({ rowIndex: 0 }); - await (await docTable.getRowActions({ rowIndex: 0 }))[0].click(); - await PageObjects.context.waitUntilContextLoadingHasFinished(); - await PageObjects.context.clickSuccessorLoadMoreButton(); - await PageObjects.context.clickSuccessorLoadMoreButton(); - await PageObjects.context.clickSuccessorLoadMoreButton(); - await PageObjects.context.waitUntilContextLoadingHasFinished(); - await browser.goBack(); - await PageObjects.discover.waitForDocTableLoadingComplete(); - const hitCount = await PageObjects.discover.getHitCount(); - expect(hitCount).to.be('522'); + await retry.waitFor('user navigating to context and returning to discover', async () => { + // navigate to the context view + const initialHitCount = await PageObjects.discover.getHitCount(); + await docTable.clickRowToggle({ rowIndex: 0 }); + const rowActions = await docTable.getRowActions({ rowIndex: 0 }); + await rowActions[0].click(); + await PageObjects.context.waitUntilContextLoadingHasFinished(); + await PageObjects.context.clickSuccessorLoadMoreButton(); + await PageObjects.context.clickSuccessorLoadMoreButton(); + await PageObjects.context.clickSuccessorLoadMoreButton(); + await PageObjects.context.waitUntilContextLoadingHasFinished(); + await browser.goBack(); + await PageObjects.discover.waitForDocTableLoadingComplete(); + const hitCount = await PageObjects.discover.getHitCount(); + return initialHitCount === hitCount; + }); }); }); } From 8956a33e4c6abcbff31b6d178aaeadce38b6b513 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 23 Jun 2020 16:34:50 +0200 Subject: [PATCH 6/8] [APM] Use asPercent to format breakdown chart (#69384) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Søren Louv-Jansen Co-authored-by: Elastic Machine --- .../TransactionBreakdownGraph/index.tsx | 3 +-- .../TransactionBreakdownKpiList.tsx | 6 ++---- .../utils/formatters/__test__/formatters.test.ts | 10 +++++++--- .../plugins/apm/public/utils/formatters/formatters.ts | 8 ++++++++ 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx index 0afed6c3c1fa8c..2cb3696f880027 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx @@ -5,7 +5,6 @@ */ import React, { useMemo } from 'react'; -import numeral from '@elastic/numeral'; import { throttle } from 'lodash'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; @@ -21,7 +20,7 @@ interface Props { } const tickFormatY = (y: Maybe) => { - return numeral(y || 0).format('0 %'); + return asPercent(y ?? 0, 1); }; const formatTooltipValue = (coordinate: Coordinate) => { diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx index eda8f19c949a3d..3898679f835371 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx @@ -13,7 +13,7 @@ import { EuiIcon, } from '@elastic/eui'; import styled from 'styled-components'; -import { FORMATTERS, InfraFormatterType } from '../../../../../infra/public'; +import { asPercent } from '../../../utils/formatters'; interface TransactionBreakdownKpi { name: string; @@ -65,9 +65,7 @@ const TransactionBreakdownKpiList: React.FC = ({ kpis }) => { - - {FORMATTERS[InfraFormatterType.percent](kpi.percentage)} - + {asPercent(kpi.percentage, 1)} diff --git a/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts b/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts index f6ed88a850a5b2..66101baf3a7461 100644 --- a/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts +++ b/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts @@ -7,12 +7,16 @@ import { asPercent } from '../formatters'; describe('formatters', () => { describe('asPercent', () => { - it('should divide and format item as percent', () => { - expect(asPercent(3725, 10000, 'n/a')).toEqual('37.3%'); + it('should format as integer when number is above 10', () => { + expect(asPercent(3725, 10000, 'n/a')).toEqual('37%'); + }); + + it('should add a decimal when value is below 10', () => { + expect(asPercent(0.092, 1)).toEqual('9.2%'); }); it('should format when numerator is 0', () => { - expect(asPercent(0, 1, 'n/a')).toEqual('0.0%'); + expect(asPercent(0, 1, 'n/a')).toEqual('0%'); }); it('should return fallback when denominator is undefined', () => { diff --git a/x-pack/plugins/apm/public/utils/formatters/formatters.ts b/x-pack/plugins/apm/public/utils/formatters/formatters.ts index 9fdac85c7154ff..649f11063b149a 100644 --- a/x-pack/plugins/apm/public/utils/formatters/formatters.ts +++ b/x-pack/plugins/apm/public/utils/formatters/formatters.ts @@ -34,5 +34,13 @@ export function asPercent( } const decimal = numerator / denominator; + + // 33.2 => 33% + // 3.32 => 3.3% + // 0 => 0% + if (Math.abs(decimal) >= 0.1 || decimal === 0) { + return numeral(decimal).format('0%'); + } + return numeral(decimal).format('0.0%'); } From 0e2f9fc3651d3eb81a2ea76aea6fee8d02d7d447 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 23 Jun 2020 08:59:32 -0700 Subject: [PATCH 7/8] Updates swim lane UI text (#69454) --- .../explorer/add_to_dashboard_control.tsx | 6 ++--- .../services/dashboard_service.test.ts | 8 +++---- .../application/services/explorer_service.ts | 24 +++++++++---------- .../anomaly_swimlane_embeddable.tsx | 2 +- .../anomaly_swimlane_embeddable_factory.ts | 2 +- .../anomaly_swimlane_initializer.tsx | 6 ++--- .../explorer_swimlane_container.tsx | 2 +- .../ui_actions/edit_swimlane_panel_action.tsx | 2 +- 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx index cb11a33ccfd76a..16e2fb47a209d0 100644 --- a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx +++ b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx @@ -57,7 +57,7 @@ interface AddToDashboardControlProps { } /** - * Component for attaching anomaly swimlane embeddable to dashboards. + * Component for attaching anomaly swim lane embeddable to dashboards. */ export const AddToDashboardControl: FC = ({ onClose, @@ -225,7 +225,7 @@ export const AddToDashboardControl: FC = ({ @@ -234,7 +234,7 @@ export const AddToDashboardControl: FC = ({ label={ } > diff --git a/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts index 6cab23eb187c7c..a618534d7ae005 100644 --- a/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts +++ b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts @@ -70,12 +70,12 @@ describe('DashboardService', () => { gridData: { x: 24, y: 0, w: 24, h: 15, i: '0aa334bd-8308-4ded-9462-80dbd37680ee' }, panelIndex: '0aa334bd-8308-4ded-9462-80dbd37680ee', embeddableConfig: { - title: 'ML anomaly swimlane for fb_population_1', + title: 'ML anomaly swim lane for fb_population_1', jobIds: ['fb_population_1'], limit: 5, swimlaneType: 'overall', }, - title: 'ML anomaly swimlane for fb_population_1', + title: 'ML anomaly swim lane for fb_population_1', }, { version: '8.0.0', @@ -118,12 +118,12 @@ describe('DashboardService', () => { gridData: { x: 24, y: 0, w: 24, h: 15, i: '0aa334bd-8308-4ded-9462-80dbd37680ee' }, panelIndex: '0aa334bd-8308-4ded-9462-80dbd37680ee', embeddableConfig: { - title: 'ML anomaly swimlane for fb_population_1', + title: 'ML anomaly swim lane for fb_population_1', jobIds: ['fb_population_1'], limit: 5, swimlaneType: 'overall', }, - title: 'ML anomaly swimlane for fb_population_1', + title: 'ML anomaly swim lane for fb_population_1', }, { version: '8.0.0', diff --git a/x-pack/plugins/ml/public/application/services/explorer_service.ts b/x-pack/plugins/ml/public/application/services/explorer_service.ts index 717ed3ba64c378..0944328db00523 100644 --- a/x-pack/plugins/ml/public/application/services/explorer_service.ts +++ b/x-pack/plugins/ml/public/application/services/explorer_service.ts @@ -55,8 +55,8 @@ export class ExplorerService { const intervalSeconds = this.timeBuckets.getInterval().asSeconds(); - // if the swimlane cell widths are too small they will not be visible - // calculate how many buckets will be drawn before the swimlanes are actually rendered + // if the swim lane cell widths are too small they will not be visible + // calculate how many buckets will be drawn before the swim lanes are actually rendered // and increase the interval to widen the cells if they're going to be smaller than 8px // this has to be done at this stage so all searches use the same interval const timerangeSeconds = (bounds.max!.valueOf() - bounds.min!.valueOf()) / 1000; @@ -81,7 +81,7 @@ export class ExplorerService { } /** - * Loads overall swimlane data + * Loads overall swim lane data * @param selectedJobs * @param chartWidth */ @@ -97,13 +97,13 @@ export class ExplorerService { const bounds = this.getTimeBounds(); - // Ensure the search bounds align to the bucketing interval used in the swimlane so + // Ensure the search bounds align to the bucketing interval used in the swim lane so // that the first and last buckets are complete. const searchBounds = getBoundsRoundedToInterval(bounds, interval, false); const selectedJobIds = selectedJobs.map((d) => d.id); // Load the overall bucket scores by time. - // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets + // Pass the interval in seconds as the swim lane relies on a fixed number of seconds between buckets // which wouldn't be the case if e.g. '1M' was used. // Pass 'true' when obtaining bucket bounds due to the way the overall_buckets endpoint works // to ensure the search is inclusive of end time. @@ -125,7 +125,7 @@ export class ExplorerService { ); // eslint-disable-next-line no-console - console.log('Explorer overall swimlane data set:', overallSwimlaneData); + console.log('Explorer overall swim lane data set:', overallSwimlaneData); return overallSwimlaneData; } @@ -158,7 +158,7 @@ export class ExplorerService { const selectedJobIds = selectedJobs.map((d) => d.id); // load scores by influencer/jobId value and time. - // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets + // Pass the interval in seconds as the swim lane relies on a fixed number of seconds between buckets // which wouldn't be the case if e.g. '1M' was used. const interval = `${swimlaneBucketInterval.asSeconds()}s`; @@ -199,7 +199,7 @@ export class ExplorerService { swimlaneBucketInterval.asSeconds() ); // eslint-disable-next-line no-console - console.log('Explorer view by swimlane data set:', viewBySwimlaneData); + console.log('Explorer view by swim lane data set:', viewBySwimlaneData); return viewBySwimlaneData; } @@ -227,7 +227,7 @@ export class ExplorerService { }; // Store the earliest and latest times of the data returned by the ES aggregations, - // These will be used for calculating the earliest and latest times for the swimlane charts. + // These will be used for calculating the earliest and latest times for the swim lane charts. Object.entries(scoresByTime).forEach(([timeMs, score]) => { const time = Number(timeMs) / 1000; dataset.points.push({ @@ -250,7 +250,7 @@ export class ExplorerService { viewBySwimlaneFieldName: string, interval: number ): OverallSwimlaneData { - // Processes the scores for the 'view by' swimlane. + // Processes the scores for the 'view by' swim lane. // Sorts the lanes according to the supplied array of lane // values in the order in which they should be displayed, // or pass an empty array to sort lanes according to max score over all time. @@ -259,7 +259,7 @@ export class ExplorerService { points: [], laneLabels: [], interval, - // Set the earliest and latest to be the same as the overall swimlane. + // Set the earliest and latest to be the same as the overall swim lane. earliest: bounds.earliest, latest: bounds.latest, }; @@ -295,7 +295,7 @@ export class ExplorerService { }); } else { // Sort lanes according to supplied order - // e.g. when a cell in the overall swimlane has been selected. + // e.g. when a cell in the overall swim lane has been selected. // Find the index of each lane label from the actual data set, // rather than using sortedLaneValues as-is, just in case they differ. dataset.laneLabels = dataset.laneLabels.sort((a, b) => { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx index b4b25db452bdb7..3b4562628051e0 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx @@ -32,7 +32,7 @@ export const ANOMALY_SWIMLANE_EMBEDDABLE_TYPE = 'ml_anomaly_swimlane'; export const getDefaultPanelTitle = (jobIds: JobId[]) => i18n.translate('xpack.ml.swimlaneEmbeddable.title', { - defaultMessage: 'ML anomaly swimlane for {jobIds}', + defaultMessage: 'ML anomaly swim lane for {jobIds}', values: { jobIds: jobIds.join(', ') }, }); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts index 09091b21e49b6d..37c2cfb3e029b8 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts @@ -39,7 +39,7 @@ export class AnomalySwimlaneEmbeddableFactory public getDisplayName() { return i18n.translate('xpack.ml.components.jobAnomalyScoreEmbeddable.displayName', { - defaultMessage: 'ML Anomaly Swimlane', + defaultMessage: 'ML Anomaly Swim Lane', }); } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx index 4c93b9ef232391..4977ece54bb57f 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx @@ -92,7 +92,7 @@ export const AnomalySwimlaneInitializer: FC = ( @@ -121,7 +121,7 @@ export const AnomalySwimlaneInitializer: FC = ( label={ } > @@ -131,7 +131,7 @@ export const AnomalySwimlaneInitializer: FC = ( color="primary" isFullWidth legend={i18n.translate('xpack.ml.swimlaneEmbeddable.setupModal.swimlaneTypeLabel', { - defaultMessage: 'Swimlane type', + defaultMessage: 'Swim lane type', })} options={swimlaneTypeOptions} idSelected={swimlaneType} diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx index 0bba9b59f7bf73..db2b9d55cfabbf 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx @@ -69,7 +69,7 @@ export const ExplorerSwimlaneContainer: FC = ({ title={ } color="danger" diff --git a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx index 915fe3cc8f9481..312b9f31124b13 100644 --- a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx @@ -32,7 +32,7 @@ export function createEditSwimlanePanelAction(getStartServices: CoreSetup['getSt }, getDisplayName: () => i18n.translate('xpack.ml.actions.editSwimlaneTitle', { - defaultMessage: 'Edit swimlane', + defaultMessage: 'Edit swim lane', }), execute: async ({ embeddable }: EditSwimlanePanelContext) => { if (!embeddable) { From 65c3114abc0c12bafbccb90959c9d80d8f5832e5 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 23 Jun 2020 18:09:07 +0200 Subject: [PATCH 8/8] Fix Advanced Settings Panel number editing in Graph (#69672) --- .../settings/advanced_settings_form.tsx | 43 +++++++++++++------ .../components/settings/settings.test.tsx | 22 ++++++++++ 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx b/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx index 191655ec7bc176..51f802edec9d0d 100644 --- a/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx +++ b/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { EuiFormRow, EuiFieldNumber, EuiComboBox, EuiSwitch, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SettingsProps } from './settings'; @@ -26,13 +26,30 @@ export function AdvancedSettingsForm({ updateSettings, allFields, }: Pick) { + // keep a local state during changes + const [formState, updateFormState] = useState({ ...advancedSettings }); + // useEffect update localState only based on the main store + useEffect(() => { + updateFormState(advancedSettings); + }, [updateFormState, advancedSettings]); + function updateSetting(key: K, value: AdvancedSettings[K]) { updateSettings({ ...advancedSettings, [key]: value }); } function getNumberUpdater>(key: K) { - return function ({ target: { valueAsNumber } }: { target: { valueAsNumber: number } }) { - updateSetting(key, Number.isNaN(valueAsNumber) ? 1 : valueAsNumber); + return function ({ + target: { valueAsNumber, value }, + }: { + target: { valueAsNumber: number; value: string }; + }) { + // if the value is valid, then update the central store, otherwise only the local store + if (Number.isNaN(valueAsNumber)) { + // localstate update + return updateFormState({ ...formState, [key]: value }); + } + // do not worry about local store here, the useEffect will pick that up and sync it + updateSetting(key, valueAsNumber); }; } @@ -52,7 +69,7 @@ export function AdvancedSettingsForm({ fullWidth min={1} step={1} - value={advancedSettings.sampleSize} + value={formState.sampleSize} onChange={getNumberUpdater('sampleSize')} />
@@ -73,7 +90,7 @@ export function AdvancedSettingsForm({ { defaultMessage: 'Significant links' } )} id="graphSignificance" - checked={advancedSettings.useSignificance} + checked={formState.useSignificance} onChange={({ target: { checked } }) => updateSetting('useSignificance', checked)} />
@@ -91,7 +108,7 @@ export function AdvancedSettingsForm({ fullWidth min={1} step={1} - value={advancedSettings.minDocCount} + value={formState.minDocCount} onChange={getNumberUpdater('minDocCount')} /> @@ -127,11 +144,11 @@ export function AdvancedSettingsForm({ singleSelection={{ asPlainText: true }} options={allFields.map((field) => ({ label: field.name, value: field }))} selectedOptions={ - advancedSettings.sampleDiversityField + formState.sampleDiversityField ? [ { - label: advancedSettings.sampleDiversityField.name, - value: advancedSettings.sampleDiversityField, + label: formState.sampleDiversityField.name, + value: formState.sampleDiversityField, }, ] : [] @@ -145,7 +162,7 @@ export function AdvancedSettingsForm({ /> - {advancedSettings.sampleDiversityField && ( + {formState.sampleDiversityField && ( {advancedSettings.sampleDiversityField.name}{' '} + {formState.sampleDiversityField.name}{' '} {i18n.translate( 'xpack.graph.settings.advancedSettings.maxValuesInputHelpText.fieldText', { @@ -171,7 +188,7 @@ export function AdvancedSettingsForm({ fullWidth min={1} step={1} - value={advancedSettings.maxValuesPerDoc} + value={formState.maxValuesPerDoc} onChange={getNumberUpdater('maxValuesPerDoc')} /> @@ -190,7 +207,7 @@ export function AdvancedSettingsForm({ fullWidth min={1} step={1} - value={advancedSettings.timeoutMillis} + value={formState.timeoutMillis} onChange={getNumberUpdater('timeoutMillis')} append={ diff --git a/x-pack/plugins/graph/public/components/settings/settings.test.tsx b/x-pack/plugins/graph/public/components/settings/settings.test.tsx index b392a26ecf0d35..1efaead002b52f 100644 --- a/x-pack/plugins/graph/public/components/settings/settings.test.tsx +++ b/x-pack/plugins/graph/public/components/settings/settings.test.tsx @@ -177,6 +177,28 @@ describe('settings', () => { ) ); }); + + it('should let the user edit and empty the field to input a new number', () => { + act(() => { + input('Sample size').prop('onChange')!({ + target: { value: '', valueAsNumber: NaN }, + } as React.ChangeEvent); + }); + // Central state should not be called + expect(dispatchSpy).not.toHaveBeenCalledWith( + updateSettings( + expect.objectContaining({ + timeoutMillis: 10000, + sampleSize: NaN, + }) + ) + ); + + // Update the local state + instance.update(); + // Now check that local state should reflect what the user sent + expect(input('Sample size').prop('value')).toEqual(''); + }); }); describe('blacklist', () => {