From 335d9f376a8af2b6518628f73777fcf02141587c Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Wed, 16 Feb 2022 15:19:51 +0100 Subject: [PATCH 1/3] Update token used for Fleet QA labeling action (#125774) --- .github/workflows/label-qa-fixed-in.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/label-qa-fixed-in.yml b/.github/workflows/label-qa-fixed-in.yml index e1dafa061f6233..7ef6b0666e6c6f 100644 --- a/.github/workflows/label-qa-fixed-in.yml +++ b/.github/workflows/label-qa-fixed-in.yml @@ -37,7 +37,7 @@ jobs: } } prnumber: ${{ github.event.number }} - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.FLEET_TECH_KIBANA_USER_TOKEN }} - uses: sergeysova/jq-action@v2 id: issues_to_label with: @@ -75,4 +75,4 @@ jobs: } issueid: ${{ matrix.issueNodeId }} labelids: ${{ needs.fetch_issues_to_label.outputs.label_ids }} - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.FLEET_TECH_KIBANA_USER_TOKEN }} From 995c1776291485d1d7079dab914a0e41576b9181 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Wed, 16 Feb 2022 15:39:49 +0100 Subject: [PATCH 2/3] [Workplace Search] Add indexing rules table (#124353) [Workplace Search] Add indexing rules table --- .../inline_editable_table.tsx | 4 + .../inline_editable_table_logic.ts | 8 +- .../__mocks__/content_sources.mock.ts | 18 +- .../workplace_search/constants.ts | 6 +- .../applications/workplace_search/routes.ts | 3 +- .../applications/workplace_search/types.ts | 13 + .../components/source_sub_nav.test.tsx | 6 +- ...s.test.tsx => assets_and_objects.test.tsx} | 18 +- ..._and_assets.tsx => assets_and_objects.tsx} | 65 +-- .../indexing_rules_table.test.tsx | 250 +++++++++++ .../synchronization/indexing_rules_table.tsx | 235 ++++++++++ .../synchronization_logic.test.ts | 406 ++++++++++++++++-- .../synchronization/synchronization_logic.ts | 179 +++++++- .../synchronization_router.test.tsx | 7 +- .../synchronization_router.tsx | 15 +- .../synchronization_sub_nav.test.tsx | 6 +- .../synchronization_sub_nav.tsx | 8 +- .../views/content_sources/constants.ts | 22 +- .../routes/workplace_search/sources.test.ts | 80 ++++ .../server/routes/workplace_search/sources.ts | 63 +++ .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 22 files changed, 1312 insertions(+), 106 deletions(-) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/{objects_and_assets.test.tsx => assets_and_objects.test.tsx} (83%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/{objects_and_assets.tsx => assets_and_objects.tsx} (71%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/indexing_rules_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/indexing_rules_table.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx index 3ccd5f15e29e91..73380c02708217 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx @@ -27,6 +27,7 @@ import './inline_editable_tables.scss'; export interface InlineEditableTableProps { columns: Array>; items: Item[]; + defaultItem?: Partial; title: string; addButtonText?: string; canRemoveLastItem?: boolean; @@ -53,6 +54,7 @@ export const InlineEditableTable = ( const { instanceId, columns, + defaultItem, onAdd, onDelete, onReorder, @@ -67,6 +69,7 @@ export const InlineEditableTable = ( props={{ instanceId, columns, + defaultItem, onAdd, onDelete, onReorder, @@ -90,6 +93,7 @@ export const InlineEditableTableContents = ({ description, isLoading, lastItemWarning, + defaultItem, noItemsMessage = () => null, uneditableItems, ...rest diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.ts index d62d894d18ef53..a4a25d0ed5a664 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table_logic.ts @@ -48,6 +48,7 @@ interface InlineEditableTableValues { export interface InlineEditableTableProps { columns: Array>; instanceId: string; + defaultItem: Item; // TODO Because these callbacks are params, they are only set on the logic once (i.e., they are cached) // which makes using "useState" to back this really hard. onAdd(item: Item, onSuccess: () => void): void; @@ -79,12 +80,15 @@ export const InlineEditableTableLogic = kea ({ fieldErrors }), setRowErrors: (rowErrors) => ({ rowErrors }), }), - reducers: ({ props: { columns } }) => ({ + reducers: ({ props: { columns, defaultItem } }) => ({ editingItemValue: [ null, { doneEditing: () => null, - editNewItem: () => generateEmptyItem(columns), + editNewItem: () => + defaultItem + ? { ...generateEmptyItem(columns), ...defaultItem } + : generateEmptyItem(columns), editExistingItem: (_, { item }) => item, setEditingItemValue: (_, { item }) => item, }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index 7af40b23d9f645..b5309d8fedc1bf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -7,6 +7,7 @@ import { groups } from './groups.mock'; +import { IndexingRule } from '../types'; import { staticSourceData } from '../views/content_sources/source_data'; import { mergeServerAndStaticData } from '../views/content_sources/sources_logic'; @@ -45,10 +46,25 @@ export const contentSources = [ }, ]; +const defaultIndexingRules: IndexingRule[] = [ + { + filterType: 'object_type', + include: 'value', + }, + { + filterType: 'path_template', + exclude: 'value', + }, + { + filterType: 'file_extension', + include: 'value', + }, +]; + const defaultIndexing = { enabled: true, defaultAction: 'include', - rules: [], + rules: defaultIndexingRules, schedule: { full: 'P1D', incremental: 'PT2H', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index 9d3b2cb8aaefd7..45104984657938 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -60,10 +60,10 @@ export const NAV = { defaultMessage: 'Frequency', } ), - SYNCHRONIZATION_OBJECTS_AND_ASSETS: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.nav.synchronizationObjectsAndAssets', + SYNCHRONIZATION_ASSETS_AND_OBJECTS: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.nav.synchronizationAssetsAndObjects', { - defaultMessage: 'Objects and assets', + defaultMessage: 'Assets and objects', } ), DISPLAY_SETTINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.displaySettings', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index ee180ae52e0b77..4857fa2a158a0b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -76,7 +76,8 @@ export const DISPLAY_SETTINGS_RESULT_DETAIL_PATH = `${SOURCE_DISPLAY_SETTINGS_PA export const SYNC_FREQUENCY_PATH = `${SOURCE_SYNCHRONIZATION_PATH}/frequency`; export const BLOCKED_TIME_WINDOWS_PATH = `${SOURCE_SYNCHRONIZATION_PATH}/frequency/blocked_windows`; -export const OBJECTS_AND_ASSETS_PATH = `${SOURCE_SYNCHRONIZATION_PATH}/objects_and_assets`; +export const OLD_OBJECTS_AND_ASSETS_PATH = `${SOURCE_SYNCHRONIZATION_PATH}/objects_and_assets`; +export const ASSETS_AND_OBJECTS_PATH = `${SOURCE_SYNCHRONIZATION_PATH}/assets_and_objects`; export const ORG_SETTINGS_PATH = '/settings'; export const ORG_SETTINGS_CUSTOMIZE_PATH = `${ORG_SETTINGS_PATH}/customize`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index 2e933d7bdf94a3..b01700b8bce340 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -168,6 +168,18 @@ export interface BlockedWindow { end: string; } +export interface IndexingRuleExclude { + filterType: 'object_type' | 'path_template' | 'file_extension'; + exclude: string; +} + +export interface IndexingRuleInclude { + filterType: 'object_type' | 'path_template' | 'file_extension'; + include: string; +} + +export type IndexingRule = IndexingRuleInclude | IndexingRuleExclude; + export interface IndexingConfig { enabled: boolean; features: { @@ -178,6 +190,7 @@ export interface IndexingConfig { enabled: boolean; }; }; + rules: IndexingRule[]; schedule: IndexingSchedule; } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx index d5ba030e582b87..66c7f5df0b5b24 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx @@ -119,9 +119,9 @@ describe('useSourceSubNav', () => { href: '/sources/2/synchronization/frequency', }, { - id: 'sourceSynchronizationObjectsAndAssets', - name: 'Objects and assets', - href: '/sources/2/synchronization/objects_and_assets', + id: 'sourceSynchronizationAssetsAndObjects', + name: 'Assets and objects', + href: '/sources/2/synchronization/assets_and_objects', }, ], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/assets_and_objects.test.tsx similarity index 83% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/assets_and_objects.test.tsx index 13dc2872037c12..aee83b31be0451 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/assets_and_objects.test.tsx @@ -16,19 +16,19 @@ import { shallow } from 'enzyme'; import { EuiSwitch } from '@elastic/eui'; -import { ObjectsAndAssets } from './objects_and_assets'; +import { AssetsAndObjects } from './assets_and_objects'; -describe('ObjectsAndAssets', () => { +describe('AssetsAndObjects', () => { const setThumbnailsChecked = jest.fn(); const setContentExtractionChecked = jest.fn(); - const updateObjectsAndAssetsSettings = jest.fn(); + const updateAssetsAndObjectsSettings = jest.fn(); const resetSyncSettings = jest.fn(); const contentSource = fullContentSources[0]; const mockActions = { setThumbnailsChecked, setContentExtractionChecked, - updateObjectsAndAssetsSettings, + updateAssetsAndObjectsSettings, resetSyncSettings, }; const mockValues = { @@ -37,7 +37,7 @@ describe('ObjectsAndAssets', () => { contentSource, thumbnailsChecked: true, contentExtractionChecked: true, - hasUnsavedObjectsAndAssetsChanges: false, + hasUnsavedAssetsAndObjectsChanges: false, }; beforeEach(() => { @@ -46,13 +46,13 @@ describe('ObjectsAndAssets', () => { }); it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(EuiSwitch)).toHaveLength(2); }); it('handles thumbnails switch change', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper .find('[data-test-subj="ThumbnailsToggle"]') .simulate('change', { target: { checked: false } }); @@ -61,7 +61,7 @@ describe('ObjectsAndAssets', () => { }); it('handles content extraction switch change', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper .find('[data-test-subj="ContentExtractionToggle"]') .simulate('change', { target: { checked: false } }); @@ -77,7 +77,7 @@ describe('ObjectsAndAssets', () => { areThumbnailsConfigEnabled: false, }, }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ThumbnailsToggle"]').prop('label')).toEqual( 'Sync thumbnails - disabled at global configuration level' diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/assets_and_objects.tsx similarity index 71% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/assets_and_objects.tsx index 460f7e7f420552..1d92398f535392 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/assets_and_objects.tsx @@ -18,7 +18,7 @@ import { EuiLink, EuiSpacer, EuiSwitch, - EuiText, + EuiTitle, } from '@elastic/eui'; import { SAVE_BUTTON_LABEL } from '../../../../../shared/constants'; @@ -27,27 +27,29 @@ import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prom import { ViewContentHeader } from '../../../../components/shared/view_content_header'; import { NAV, RESET_BUTTON } from '../../../../constants'; import { - LEARN_MORE_LINK, SYNC_MANAGEMENT_CONTENT_EXTRACTION_LABEL, SYNC_MANAGEMENT_THUMBNAILS_LABEL, SYNC_MANAGEMENT_THUMBNAILS_GLOBAL_CONFIG_LABEL, - SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION, - SOURCE_OBJECTS_AND_ASSETS_LABEL, + SOURCE_ASSETS_AND_OBJECTS_DESCRIPTION, + SOURCE_ASSETS_AND_OBJECTS_ASSETS_LABEL, SYNC_UNSAVED_CHANGES_MESSAGE, + SOURCE_ASSETS_AND_OBJECTS_LEARN_MORE_LINK, + SOURCE_ASSETS_AND_OBJECTS_OBJECTS_LABEL, } from '../../constants'; import { SourceLogic } from '../../source_logic'; import { SourceLayout } from '../source_layout'; +import { IndexingRulesTable } from './indexing_rules_table'; import { SynchronizationLogic } from './synchronization_logic'; -export const ObjectsAndAssets: React.FC = () => { +export const AssetsAndObjects: React.FC = () => { const { contentSource, dataLoading } = useValues(SourceLogic); - const { thumbnailsChecked, contentExtractionChecked, hasUnsavedObjectsAndAssetsChanges } = + const { thumbnailsChecked, contentExtractionChecked, hasUnsavedAssetsAndObjectsChanges } = useValues(SynchronizationLogic({ contentSource })); const { setThumbnailsChecked, setContentExtractionChecked, - updateObjectsAndAssetsSettings, + updateAssetsAndObjectsSettings, resetSyncSettings, } = useActions(SynchronizationLogic({ contentSource })); @@ -55,47 +57,43 @@ export const ObjectsAndAssets: React.FC = () => { const actions = ( - - - {RESET_BUTTON} - - {SAVE_BUTTON_LABEL} + + + {RESET_BUTTON} + + ); return ( - - {SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION}{' '} - - {LEARN_MORE_LINK} - - - } - action={actions} - /> + + {SOURCE_ASSETS_AND_OBJECTS_DESCRIPTION} + + + {SOURCE_ASSETS_AND_OBJECTS_LEARN_MORE_LINK} + - {SOURCE_OBJECTS_AND_ASSETS_LABEL} + +

{SOURCE_ASSETS_AND_OBJECTS_ASSETS_LABEL}

+
@@ -122,6 +120,15 @@ export const ObjectsAndAssets: React.FC = () => { /> + + +

{SOURCE_ASSETS_AND_OBJECTS_OBJECTS_LABEL}

+
+ + + + +
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/indexing_rules_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/indexing_rules_table.test.tsx new file mode 100644 index 00000000000000..7d4b47e7cd8e62 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/indexing_rules_table.test.tsx @@ -0,0 +1,250 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + LogicMounter, + mockFlashMessageHelpers, + setMockActions, + setMockValues, +} from '../../../../../__mocks__/kea_logic'; +import { fullContentSources } from '../../../../__mocks__/content_sources.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiFieldText, EuiSelect } from '@elastic/eui'; + +import { InlineEditableTable } from '../../../../../shared/tables/inline_editable_table'; + +import { SourceLogic } from '../../source_logic'; + +import { IndexingRulesTable } from './indexing_rules_table'; +import { SynchronizationLogic } from './synchronization_logic'; + +describe('IndexingRulesTable', () => { + const { clearFlashMessages } = mockFlashMessageHelpers; + const { mount: sourceMount } = new LogicMounter(SourceLogic); + const { mount: syncMount } = new LogicMounter(SynchronizationLogic); + + const indexingRules = [ + { id: 0, valueType: 'exclude', filterType: 'path_template', value: 'value' }, + { id: 1, valueType: 'include', filterType: 'file_extension', value: 'value' }, + { id: 2, valueType: 'include', filterType: 'object_type', value: 'value 2' }, + { id: 3, valueType: 'broken', filterType: 'not allowed', value: 'value 2' }, + ]; + const contentSource = fullContentSources[0]; + + beforeEach(() => { + jest.clearAllMocks(); + sourceMount({}, {}); + setMockValues({ contentSource }); + syncMount({}, { contentSource }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(InlineEditableTable).exists()).toBe(true); + }); + + describe('columns', () => { + let wrapper: ShallowWrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + const renderColumn = (index: number, ruleIndex: number) => { + const columns = wrapper.find(InlineEditableTable).prop('columns'); + return shallow(
{columns[index].render(indexingRules[ruleIndex])}
); + }; + + const onChange = jest.fn(); + const renderColumnInEditingMode = (index: number, ruleIndex: number) => { + const columns = wrapper.find(InlineEditableTable).prop('columns'); + return shallow( +
+ {columns[index].editingRender(indexingRules[ruleIndex], onChange, { + isInvalid: false, + isLoading: false, + })} +
+ ); + }; + + describe(' column', () => { + it('shows the value type of an indexing rule', () => { + expect(renderColumn(0, 0).html()).toContain('Exclude'); + expect(renderColumn(0, 1).html()).toContain('Include'); + expect(renderColumn(0, 3).html()).toContain(''); + }); + + it('can show the value type of an indexing rule as editable', () => { + const column = renderColumnInEditingMode(0, 0); + + const selectField = column.find(EuiSelect); + expect(selectField.props()).toEqual( + expect.objectContaining({ + value: 'exclude', + disabled: false, + isInvalid: false, + options: [ + { text: 'Include', value: 'include' }, + { text: 'Exclude', value: 'exclude' }, + ], + }) + ); + + selectField.simulate('change', { target: { value: 'include' } }); + expect(onChange).toHaveBeenCalledWith('include'); + }); + }); + + describe('filter type column', () => { + it('shows the filter type of an indexing rule', () => { + expect(renderColumn(1, 0).html()).toContain('Path'); + expect(renderColumn(1, 1).html()).toContain('File'); + expect(renderColumn(1, 2).html()).toContain('Item'); + expect(renderColumn(1, 3).html()).toContain(''); + }); + + it('can show the filter type of an indexing rule as editable', () => { + const column = renderColumnInEditingMode(1, 0); + + const selectField = column.find(EuiSelect); + expect(selectField.props()).toEqual( + expect.objectContaining({ + value: 'path_template', + disabled: false, + isInvalid: false, + options: [ + { text: 'Item', value: 'object_type' }, + { text: 'Path', value: 'path_template' }, + { text: 'File type', value: 'file_extension' }, + ], + }) + ); + + selectField.simulate('change', { target: { value: 'object_type' } }); + expect(onChange).toHaveBeenCalledWith('object_type'); + }); + }); + + describe('pattern column', () => { + it('shows the value of an indexing rule', () => { + expect(renderColumn(2, 0).html()).toContain('value'); + }); + + it('can show the value of a indexing rule as editable', () => { + const column = renderColumnInEditingMode(2, 0); + + const field = column.find(EuiFieldText); + expect(field.props()).toEqual( + expect.objectContaining({ + value: 'value', + disabled: false, + isInvalid: false, + }) + ); + + field.simulate('change', { target: { value: 'foo' } }); + expect(onChange).toHaveBeenCalledWith('foo'); + }); + }); + }); + + describe('when an indexing rule is added', () => { + it('should update the indexing rules for the current domain, and clear flash messages', () => { + const initAddIndexingRule = jest.fn(); + const done = jest.fn(); + setMockActions({ + initAddIndexingRule, + }); + const wrapper = shallow(); + const table = wrapper.find(InlineEditableTable); + + const newIndexingRule = { + id: 2, + value: 'new value', + filterType: 'path_template', + valueType: 'include', + }; + table.prop('onAdd')(newIndexingRule, done); + expect(initAddIndexingRule).toHaveBeenCalledWith(newIndexingRule); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + + describe('when an indexing rule is updated', () => { + it('should update the indexing rules for the current domain, and clear flash messages', () => { + const initSetIndexingRule = jest.fn(); + const done = jest.fn(); + setMockActions({ + initSetIndexingRule, + }); + const wrapper = shallow(); + const table = wrapper.find(InlineEditableTable); + + const newIndexingRule = { + id: 2, + value: 'new value', + filterType: 'path_template', + valueType: 'include', + }; + table.prop('onUpdate')(newIndexingRule, done); + expect(initSetIndexingRule).toHaveBeenCalledWith(newIndexingRule); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + + describe('when a indexing rule is deleted', () => { + it('should update the indexing rules for the current domain, and clear flash messages', () => { + const deleteIndexingRule = jest.fn(); + const done = jest.fn(); + setMockActions({ + deleteIndexingRule, + }); + const wrapper = shallow(); + const table = wrapper.find(InlineEditableTable); + + const newIndexingRule = { + id: 2, + value: 'new value', + filterType: 'path_template', + valueType: 'include', + }; + table.prop('onDelete')(newIndexingRule, done); + expect(deleteIndexingRule).toHaveBeenCalledWith(newIndexingRule); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + + describe('when an indexing rule is reordered', () => { + it('should update the indexing rules for the current domain, and clear flash messages', () => { + const setIndexingRules = jest.fn(); + const done = jest.fn(); + setMockActions({ + setIndexingRules, + }); + const wrapper = shallow(); + const table = wrapper.find(InlineEditableTable); + + const newIndexingRules = [ + { + id: 2, + value: 'new value', + filterType: 'path_template', + valueType: 'include', + }, + ]; + table.prop('onReorder')!(newIndexingRules, indexingRules, done); + expect(setIndexingRules).toHaveBeenCalledWith(newIndexingRules); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/indexing_rules_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/indexing_rules_table.tsx new file mode 100644 index 00000000000000..ed1eb40c61ce52 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/indexing_rules_table.tsx @@ -0,0 +1,235 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiSelect, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { docLinks } from '../../../../../shared/doc_links'; +import { clearFlashMessages } from '../../../../../shared/flash_messages'; +import { InlineEditableTable } from '../../../../../shared/tables/inline_editable_table/inline_editable_table'; +import { InlineEditableTableColumn } from '../../../../../shared/tables/inline_editable_table/types'; + +import { SourceLogic } from '../../source_logic'; + +import { EditableIndexingRule, SynchronizationLogic } from './synchronization_logic'; + +const SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_POLICY_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTablePolicyLabel', + { + defaultMessage: 'Policy', + } +); + +const SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_PATH_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTablePathLabel', + { + defaultMessage: 'Path', + } +); + +export const SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_ITEM_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTableItemLabel', + { + defaultMessage: 'Item', + } +); + +export const SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_FILE_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTableFileLabel', + { + defaultMessage: 'File type', + } +); + +export const SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_INCLUDE_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTableIncludeLabel', + { + defaultMessage: 'Include', + } +); + +export const SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_EXCLUDE_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTableExcludeLabel', + { + defaultMessage: 'Exclude', + } +); + +export const IndexingRulesTable: React.FC = () => { + const { contentSource } = useValues(SourceLogic); + const indexingRulesInstanceId = 'IndexingRulesTable'; + const { indexingRules } = useValues( + SynchronizationLogic({ contentSource, indexingRulesInstanceId }) + ); + const { initAddIndexingRule, deleteIndexingRule, initSetIndexingRule, setIndexingRules } = + useActions(SynchronizationLogic({ contentSource })); + + const description = ( + + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsDescription', + { + defaultMessage: + 'Include or exclude high level items, file types and (file or folder) paths to synchronize from {contentSourceName}. Everything is included by default. Each document is tested against the rules below and the first rule that matches will be applied.', + values: { contentSourceName: contentSource.name }, + } + )} + + + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsSyncLearnMoreLink', + { + defaultMessage: 'Learn more about sync rules.', + } + )} + + + ); + + const valueTypeToString = (input: string): string => { + switch (input) { + case 'include': + return SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_INCLUDE_LABEL; + case 'exclude': + return SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_EXCLUDE_LABEL; + default: + return ''; + } + }; + + const filterTypeToString = (input: string): string => { + switch (input) { + case 'object_type': + return SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_ITEM_LABEL; + case 'path_template': + return SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_PATH_LABEL; + case 'file_extension': + return SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_FILE_LABEL; + default: + return ''; + } + }; + + const columns: Array> = [ + { + editingRender: (indexingRule, onChange, { isInvalid, isLoading }) => ( + onChange(e.target.value)} + disabled={isLoading} + isInvalid={isInvalid} + options={[ + { text: SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_INCLUDE_LABEL, value: 'include' }, + { text: SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_EXCLUDE_LABEL, value: 'exclude' }, + ]} + /> + ), + render: (indexingRule) => ( + {valueTypeToString(indexingRule.valueType)} + ), + name: SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_POLICY_LABEL, + field: 'valueType', + }, + { + editingRender: (indexingRule, onChange, { isInvalid, isLoading }) => ( + onChange(e.target.value)} + disabled={isLoading} + isInvalid={isInvalid} + options={[ + { text: SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_ITEM_LABEL, value: 'object_type' }, + { text: SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_PATH_LABEL, value: 'path_template' }, + { text: SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_FILE_LABEL, value: 'file_extension' }, + ]} + /> + ), + render: (indexingRule) => ( + {filterTypeToString(indexingRule.filterType)} + ), + name: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTableRuleLabel', + { + defaultMessage: 'Rule', + } + ), + field: 'filterType', + }, + { + editingRender: (indexingRule, onChange, { isInvalid, isLoading }) => ( + + + onChange(e.target.value)} + disabled={isLoading} + isInvalid={isInvalid} + /> + + + ), + render: (indexingRule) => {indexingRule.value}, + name: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTableValueLabel', + { + defaultMessage: 'Value', + } + ), + field: 'value', + }, + ]; + + return ( + { + initAddIndexingRule(newRule); + clearFlashMessages(); + }} + onDelete={(rule) => { + deleteIndexingRule(rule); + clearFlashMessages(); + }} + onUpdate={(rule) => { + initSetIndexingRule(rule); + clearFlashMessages(); + }} + onReorder={(newIndexingRules) => { + setIndexingRules(newIndexingRules); + clearFlashMessages(); + }} + title="" + canRemoveLastItem + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.test.ts index 20a6ba238a2f43..63de2e4d55838f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.test.ts @@ -15,6 +15,11 @@ import { fullContentSources } from '../../../../__mocks__/content_sources.mock'; import { nextTick } from '@kbn/test-jest-helpers'; +import { + InlineEditableTableLogic, + InlineEditableTableProps, +} from '../../../../../shared/tables/inline_editable_table/inline_editable_table_logic'; +import { ItemWithAnID } from '../../../../../shared/tables/types'; import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; jest.mock('../../source_logic', () => ({ @@ -30,11 +35,12 @@ import { SynchronizationLogic, emptyBlockedWindow, stripScheduleSeconds, + EditableIndexingRule, } from './synchronization_logic'; describe('SynchronizationLogic', () => { const { http } = mockHttpValues; - const { flashSuccessToast } = mockFlashMessageHelpers; + const { flashSuccessToast, flashAPIErrors } = mockFlashMessageHelpers; const { navigateToUrl } = mockKibanaValues; const { mount } = new LogicMounter(SynchronizationLogic); const contentSource = fullContentSources[0]; @@ -49,12 +55,49 @@ describe('SynchronizationLogic', () => { }, }; + const defaultIndexingRules: EditableIndexingRule[] = [ + { + filterType: 'object_type', + id: 0, + value: 'value', + valueType: 'include', + }, + { + filterType: 'path_template', + id: 1, + value: 'value', + valueType: 'exclude', + }, + { + filterType: 'file_extension', + id: 2, + value: 'value', + valueType: 'include', + }, + ]; + const defaultValues = { navigatingBetweenTabs: false, - hasUnsavedObjectsAndAssetsChanges: false, + hasUnsavedAssetsAndObjectsChanges: false, + hasUnsavedIndexingRulesChanges: false, hasUnsavedFrequencyChanges: false, contentExtractionChecked: true, thumbnailsChecked: true, + indexingRules: defaultIndexingRules, + indexingRulesForAPI: [ + { + filter_type: 'object_type', + include: 'value', + }, + { + filter_type: 'path_template', + exclude: 'value', + }, + { + filter_type: 'file_extension', + include: 'value', + }, + ], schedule: contentSource.indexing.schedule, cachedSchedule: contentSource.indexing.schedule, }; @@ -109,10 +152,17 @@ describe('SynchronizationLogic', () => { it('resetSyncSettings', () => { SynchronizationLogic.actions.setContentExtractionChecked(false); SynchronizationLogic.actions.setThumbnailsChecked(false); + SynchronizationLogic.actions.addIndexingRule({ + filterType: 'file_extension', + valueType: 'exclude', + value: 'value', + }); SynchronizationLogic.actions.resetSyncSettings(); expect(SynchronizationLogic.values.thumbnailsChecked).toEqual(true); expect(SynchronizationLogic.values.contentExtractionChecked).toEqual(true); + expect(SynchronizationLogic.values.indexingRules).toEqual(defaultIndexingRules); + expect(SynchronizationLogic.values.hasUnsavedIndexingRulesChanges).toEqual(false); }); describe('setSyncFrequency', () => { @@ -151,37 +201,133 @@ describe('SynchronizationLogic', () => { expect(SynchronizationLogic.values.schedule.blockedWindows).toBeUndefined(); }); }); - }); - describe('setBlockedTimeWindow', () => { - it('sets "jobType"', () => { - SynchronizationLogic.actions.addBlockedWindow(); - SynchronizationLogic.actions.setBlockedTimeWindow(0, 'jobType', 'incremental'); + describe('setBlockedTimeWindow', () => { + it('sets "jobType"', () => { + SynchronizationLogic.actions.addBlockedWindow(); + SynchronizationLogic.actions.setBlockedTimeWindow(0, 'jobType', 'incremental'); + + expect(SynchronizationLogic.values.schedule.blockedWindows![0].jobType).toEqual( + 'incremental' + ); + }); + + it('sets "day"', () => { + SynchronizationLogic.actions.addBlockedWindow(); + SynchronizationLogic.actions.setBlockedTimeWindow(0, 'day', 'tuesday'); + + expect(SynchronizationLogic.values.schedule.blockedWindows![0].day).toEqual('tuesday'); + }); + + it('sets "start"', () => { + SynchronizationLogic.actions.addBlockedWindow(); + SynchronizationLogic.actions.setBlockedTimeWindow(0, 'start', '9:00:00Z'); - expect(SynchronizationLogic.values.schedule.blockedWindows![0].jobType).toEqual( - 'incremental' - ); + expect(SynchronizationLogic.values.schedule.blockedWindows![0].start).toEqual('9:00:00Z'); + }); + + it('sets "end"', () => { + SynchronizationLogic.actions.addBlockedWindow(); + SynchronizationLogic.actions.setBlockedTimeWindow(0, 'end', '11:00:00Z'); + + expect(SynchronizationLogic.values.schedule.blockedWindows![0].end).toEqual('11:00:00Z'); + }); }); - it('sets "day"', () => { - SynchronizationLogic.actions.addBlockedWindow(); - SynchronizationLogic.actions.setBlockedTimeWindow(0, 'day', 'tuesday'); + describe('addIndexingRule', () => { + const indexingRule: EditableIndexingRule = { + filterType: 'file_extension', + valueType: 'exclude', + value: 'value', + id: 10, + }; - expect(SynchronizationLogic.values.schedule.blockedWindows![0].day).toEqual('tuesday'); + it('adds indexing rule with id 0', () => { + SynchronizationLogic.actions.setIndexingRules([]); + SynchronizationLogic.actions.addIndexingRule(indexingRule); + + expect(SynchronizationLogic.values.indexingRules).toEqual([{ ...indexingRule, id: 0 }]); + expect(SynchronizationLogic.values.hasUnsavedIndexingRulesChanges).toEqual(true); + }); + + it('adds indexing rule with id existing length + 1', () => { + SynchronizationLogic.actions.addIndexingRule(indexingRule); + + expect(SynchronizationLogic.values.indexingRules).toEqual([ + ...defaultValues.indexingRules, + { ...indexingRule, id: 3 }, + ]); + expect(SynchronizationLogic.values.hasUnsavedIndexingRulesChanges).toEqual(true); + }); + it('adds indexing rule with unique id in case of previous deletions', () => { + SynchronizationLogic.actions.deleteIndexingRule({ ...indexingRule, id: 1 }); + SynchronizationLogic.actions.addIndexingRule(indexingRule); + + expect(SynchronizationLogic.values.indexingRules).toEqual([ + defaultValues.indexingRules[0], + defaultValues.indexingRules[2], + { ...indexingRule, id: 3 }, + ]); + expect(SynchronizationLogic.values.hasUnsavedIndexingRulesChanges).toEqual(true); + }); }); - it('sets "start"', () => { - SynchronizationLogic.actions.addBlockedWindow(); - SynchronizationLogic.actions.setBlockedTimeWindow(0, 'start', '9:00:00Z'); + describe('setIndexingRule', () => { + const indexingRule: EditableIndexingRule = { + filterType: 'file_extension', + valueType: 'exclude', + value: 'value', + id: 1, + }; + + it('updates indexing rule', () => { + SynchronizationLogic.actions.setIndexingRule(indexingRule); - expect(SynchronizationLogic.values.schedule.blockedWindows![0].start).toEqual('9:00:00Z'); + expect(SynchronizationLogic.values.indexingRules).toEqual([ + defaultValues.indexingRules[0], + indexingRule, + defaultValues.indexingRules[2], + ]); + expect(SynchronizationLogic.values.hasUnsavedIndexingRulesChanges).toEqual(true); + }); }); - it('sets "end"', () => { - SynchronizationLogic.actions.addBlockedWindow(); - SynchronizationLogic.actions.setBlockedTimeWindow(0, 'end', '11:00:00Z'); + describe('setIndexingRules', () => { + const indexingRule: EditableIndexingRule = { + filterType: 'file_extension', + valueType: 'exclude', + value: 'value', + id: 1, + }; - expect(SynchronizationLogic.values.schedule.blockedWindows![0].end).toEqual('11:00:00Z'); + it('updates indexing rules', () => { + SynchronizationLogic.actions.setIndexingRules([indexingRule, indexingRule]); + + expect(SynchronizationLogic.values.indexingRules).toEqual([ + { ...indexingRule, id: 0 }, + { ...indexingRule, id: 1 }, + ]); + expect(SynchronizationLogic.values.hasUnsavedIndexingRulesChanges).toEqual(true); + }); + }); + + describe('deleteIndexingRule', () => { + const indexingRule: EditableIndexingRule = { + filterType: 'file_extension', + valueType: 'exclude', + value: 'value', + id: 1, + }; + + it('updates indexing rules', () => { + const newIndexingRules = defaultValues.indexingRules.filter( + (val) => val.id !== indexingRule.id + ); + SynchronizationLogic.actions.deleteIndexingRule(indexingRule); + expect(SynchronizationLogic.values.indexingRules).toEqual(newIndexingRules); + + expect(SynchronizationLogic.values.hasUnsavedIndexingRulesChanges).toEqual(true); + }); }); }); @@ -209,6 +355,204 @@ describe('SynchronizationLogic', () => { }); }); + describe('initAddIndexingRule', () => { + const indexingRule: EditableIndexingRule = { + filterType: 'file_extension', + valueType: 'exclude', + value: 'value', + id: 1, + }; + it('calls validate endpoint and continues if no errors happen', async () => { + const addIndexingRuleSpy = jest.spyOn(SynchronizationLogic.actions, 'addIndexingRule'); + const promise = Promise.resolve({ rules: [] }); + const doneSpy = jest.spyOn( + InlineEditableTableLogic({ + instanceId: 'IndexingRulesTable', + } as InlineEditableTableProps).actions, + 'doneEditing' + ); + http.post.mockReturnValue(promise); + SynchronizationLogic.actions.initAddIndexingRule(indexingRule); + + expect(http.post).toHaveBeenCalledWith( + '/internal/workplace_search/org/sources/123/indexing_rules/validate', + { + body: JSON.stringify({ + rules: [ + { + filter_type: 'file_extension', + exclude: 'value', + }, + ], + }), + } + ); + await promise; + expect(addIndexingRuleSpy).toHaveBeenCalledWith(indexingRule); + expect(doneSpy).toHaveBeenCalled(); + }); + + it('calls validate endpoint and sets errors if there is an error', async () => { + const addIndexingRuleSpy = jest.spyOn(SynchronizationLogic.actions, 'addIndexingRule'); + const promise = Promise.resolve({ rules: [{ valid: false, error: 'error' }] }); + http.post.mockReturnValue(promise); + SynchronizationLogic.actions.initAddIndexingRule({ ...indexingRule, valueType: 'include' }); + const doneSpy = jest.spyOn( + InlineEditableTableLogic({ + instanceId: 'IndexingRulesTable', + } as InlineEditableTableProps).actions, + 'doneEditing' + ); + + expect(http.post).toHaveBeenCalledWith( + '/internal/workplace_search/org/sources/123/indexing_rules/validate', + { + body: JSON.stringify({ + rules: [ + { + filter_type: 'file_extension', + include: 'value', + }, + ], + }), + } + ); + await promise; + expect(addIndexingRuleSpy).not.toHaveBeenCalled(); + expect(doneSpy).toHaveBeenCalled(); + }); + + it('flashes an error if the API call fails', async () => { + const addIndexingRuleSpy = jest.spyOn(SynchronizationLogic.actions, 'addIndexingRule'); + const promise = Promise.reject('error'); + http.post.mockReturnValue(promise); + const doneSpy = jest.spyOn( + InlineEditableTableLogic({ + instanceId: 'IndexingRulesTable', + } as InlineEditableTableProps).actions, + 'doneEditing' + ); + SynchronizationLogic.actions.initAddIndexingRule(indexingRule); + + expect(http.post).toHaveBeenCalledWith( + '/internal/workplace_search/org/sources/123/indexing_rules/validate', + { + body: JSON.stringify({ + rules: [ + { + filter_type: 'file_extension', + exclude: 'value', + }, + ], + }), + } + ); + await nextTick(); + expect(addIndexingRuleSpy).not.toHaveBeenCalled(); + expect(doneSpy).not.toHaveBeenCalled(); + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + + describe('initSetIndexingRule', () => { + const indexingRule: EditableIndexingRule = { + filterType: 'file_extension', + valueType: 'exclude', + value: 'value', + id: 1, + }; + + it('calls validate endpoint and continues if no errors happen', async () => { + const setIndexingRuleSpy = jest.spyOn(SynchronizationLogic.actions, 'setIndexingRule'); + const promise = Promise.resolve({ rules: [] }); + http.post.mockReturnValue(promise); + const doneSpy = jest.spyOn( + InlineEditableTableLogic({ + instanceId: 'IndexingRulesTable', + } as InlineEditableTableProps).actions, + 'doneEditing' + ); + SynchronizationLogic.actions.initSetIndexingRule(indexingRule); + + expect(http.post).toHaveBeenCalledWith( + '/internal/workplace_search/org/sources/123/indexing_rules/validate', + { + body: JSON.stringify({ + rules: [ + { + filter_type: 'file_extension', + exclude: 'value', + }, + ], + }), + } + ); + await promise; + expect(setIndexingRuleSpy).toHaveBeenCalledWith(indexingRule); + expect(doneSpy).toHaveBeenCalled(); + }); + + it('calls validate endpoint and sets errors if there is an error', async () => { + const setIndexingRuleSpy = jest.spyOn(SynchronizationLogic.actions, 'setIndexingRule'); + const promise = Promise.resolve({ rules: [{ valid: false, error: 'error' }] }); + http.post.mockReturnValue(promise); + const doneSpy = jest.spyOn( + InlineEditableTableLogic({ + instanceId: 'IndexingRulesTable', + } as InlineEditableTableProps).actions, + 'doneEditing' + ); + SynchronizationLogic.actions.initSetIndexingRule({ ...indexingRule, valueType: 'include' }); + + expect(http.post).toHaveBeenCalledWith( + '/internal/workplace_search/org/sources/123/indexing_rules/validate', + { + body: JSON.stringify({ + rules: [ + { + filter_type: 'file_extension', + include: 'value', + }, + ], + }), + } + ); + await promise; + expect(setIndexingRuleSpy).not.toHaveBeenCalled(); + expect(doneSpy).toHaveBeenCalled(); + }); + it('flashes an error if the API call fails', async () => { + const setIndexingRuleSpy = jest.spyOn(SynchronizationLogic.actions, 'setIndexingRule'); + const promise = Promise.reject('error'); + http.post.mockReturnValue(promise); + const doneSpy = jest.spyOn( + InlineEditableTableLogic({ + instanceId: 'IndexingRulesTable', + } as InlineEditableTableProps).actions, + 'doneEditing' + ); + SynchronizationLogic.actions.initSetIndexingRule(indexingRule); + + expect(http.post).toHaveBeenCalledWith( + '/internal/workplace_search/org/sources/123/indexing_rules/validate', + { + body: JSON.stringify({ + rules: [ + { + filter_type: 'file_extension', + exclude: 'value', + }, + ], + }), + } + ); + await nextTick(); + expect(setIndexingRuleSpy).not.toHaveBeenCalled(); + expect(doneSpy).not.toHaveBeenCalled(); + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + describe('updateSyncEnabled', () => { it('calls updateServerSettings method', async () => { const updateServerSettingsSpy = jest.spyOn( @@ -225,13 +569,13 @@ describe('SynchronizationLogic', () => { }); }); - describe('updateObjectsAndAssetsSettings', () => { + describe('updateAssetsAndObjectsSettings', () => { it('calls updateServerSettings method', async () => { const updateServerSettingsSpy = jest.spyOn( SynchronizationLogic.actions, 'updateServerSettings' ); - SynchronizationLogic.actions.updateObjectsAndAssetsSettings(); + SynchronizationLogic.actions.updateAssetsAndObjectsSettings(); expect(updateServerSettingsSpy).toHaveBeenCalledWith({ content_source: { @@ -240,6 +584,20 @@ describe('SynchronizationLogic', () => { content_extraction: { enabled: true }, thumbnails: { enabled: true }, }, + rules: [ + { + filter_type: 'object_type', + include: 'value', + }, + { + filter_type: 'path_template', + exclude: 'value', + }, + { + filter_type: 'file_extension', + include: 'value', + }, + ], }, }, }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.ts index 2f4fdca44d4415..547ff1e8497348 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.ts @@ -14,6 +14,11 @@ export type TabId = 'source_sync_frequency' | 'blocked_time_windows'; import { flashAPIErrors, flashSuccessToast } from '../../../../../shared/flash_messages'; import { HttpLogic } from '../../../../../shared/http'; import { KibanaLogic } from '../../../../../shared/kibana'; +import { + InlineEditableTableLogic, + InlineEditableTableProps, +} from '../../../../../shared/tables/inline_editable_table/inline_editable_table_logic'; +import { ItemWithAnID } from '../../../../../shared/tables/types'; import { AppLogic } from '../../../../app_logic'; import { SYNC_FREQUENCY_PATH, @@ -25,9 +30,11 @@ import { BlockedWindow, DayOfWeek, IndexingSchedule, + IndexingRule, ContentSourceFullData, SyncJobType, TimeUnit, + IndexingRuleInclude, } from '../../../../types'; import { SYNC_SETTINGS_UPDATED_MESSAGE } from '../../constants'; @@ -57,6 +64,7 @@ interface ServerSyncSettingsBody { permissions?: string; blocked_windows?: ServerBlockedWindow[]; }; + rules?: IndexingRule[]; }; }; } @@ -67,7 +75,7 @@ interface SynchronizationActions { addBlockedWindow(): void; removeBlockedWindow(index: number): number; updateFrequencySettings(): void; - updateObjectsAndAssetsSettings(): void; + updateAssetsAndObjectsSettings(): void; resetSyncSettings(): void; updateSyncEnabled(enabled: boolean): boolean; setThumbnailsChecked(checked: boolean): boolean; @@ -88,16 +96,26 @@ interface SynchronizationActions { setContentExtractionChecked(checked: boolean): boolean; setServerSchedule(schedule: IndexingSchedule): IndexingSchedule; updateServerSettings(body: ServerSyncSettingsBody): ServerSyncSettingsBody; + addIndexingRule(indexingRule: EditableIndexingRuleBase): EditableIndexingRuleBase; + initAddIndexingRule(rule: EditableIndexingRule): { rule: EditableIndexingRule }; + setIndexingRules(indexingRules: EditableIndexingRule[]): EditableIndexingRule[]; + setIndexingRule(indexingRule: EditableIndexingRule): EditableIndexingRule; + initSetIndexingRule(indexingRule: EditableIndexingRule): { rule: EditableIndexingRule }; + deleteIndexingRule(indexingRule: EditableIndexingRule): EditableIndexingRule; } interface SynchronizationValues { navigatingBetweenTabs: boolean; + hasUnsavedIndexingRulesChanges: boolean; hasUnsavedFrequencyChanges: boolean; - hasUnsavedObjectsAndAssetsChanges: boolean; + hasUnsavedAssetsAndObjectsChanges: boolean; + indexingRules: EditableIndexingRule[]; thumbnailsChecked: boolean; contentExtractionChecked: boolean; cachedSchedule: IndexingSchedule; schedule: IndexingSchedule; + indexingRulesForAPI: IndexingRule[]; + errors: string[]; } export const emptyBlockedWindow: BlockedWindow = { @@ -111,6 +129,26 @@ type BlockedWindowMap = { [prop in keyof BlockedWindow]: SyncJobType | DayOfWeek | 'all' | string; }; +interface EditableIndexingRuleBase { + filterType: 'object_type' | 'path_template' | 'file_extension'; + valueType: 'include' | 'exclude'; + value: string; +} + +export interface EditableIndexingRule extends EditableIndexingRuleBase { + id: number; +} + +interface IndexingRuleForAPI { + filter_type: 'object_type' | 'path_template' | 'file_extension'; + include?: string; + exclude?: string; +} + +const isIncludeRule = (rule: IndexingRule): rule is IndexingRuleInclude => { + return !!(rule as IndexingRuleInclude).include; +}; + export const SynchronizationLogic = kea< MakeLogicType >({ @@ -135,11 +173,28 @@ export const SynchronizationLogic = kea< setServerSchedule: (schedule: IndexingSchedule) => schedule, removeBlockedWindow: (index: number) => index, updateFrequencySettings: true, - updateObjectsAndAssetsSettings: true, + updateAssetsAndObjectsSettings: true, resetSyncSettings: true, addBlockedWindow: true, + addIndexingRule: (rule: EditableIndexingRuleBase) => rule, + deleteIndexingRule: (rule: EditableIndexingRule) => rule, + initAddIndexingRule: (rule: EditableIndexingRule) => ({ rule }), + initSetIndexingRule: (rule: EditableIndexingRule) => ({ rule }), + setIndexingRules: (indexingRules: EditableIndexingRule[]) => indexingRules, + setIndexingRule: (rule: EditableIndexingRule) => rule, }, reducers: ({ props }) => ({ + hasUnsavedIndexingRulesChanges: [ + false, + { + setIndexingRule: () => true, + setIndexingRules: () => true, + addIndexingRule: () => true, + deleteIndexingRule: () => true, + resetSyncSettings: () => false, + updateServerSettings: () => false, + }, + ], navigatingBetweenTabs: [ false, { @@ -228,15 +283,55 @@ export const SynchronizationLogic = kea< }, }, ], + indexingRules: [ + (props.contentSource.indexing.rules as IndexingRule[]).map((rule, index) => ({ + filterType: rule.filterType, + id: index, + valueType: isIncludeRule(rule) ? 'include' : 'exclude', + value: isIncludeRule(rule) ? rule.include : rule.exclude, + })), + { + addIndexingRule: (indexingRules, rule) => [ + ...indexingRules, + { + ...rule, + // make sure that we get a unique number, in case of multiple deletions and additions + id: indexingRules.reduce( + (prev, curr) => (curr.id >= prev ? curr.id + 1 : prev), + indexingRules.length + ), + }, + ], + deleteIndexingRule: (indexingRules, rule) => + indexingRules.filter((currentRule) => currentRule.id !== rule.id), + resetSyncSettings: () => + (props.contentSource.indexing.rules as IndexingRule[]).map((rule, index) => ({ + filterType: rule.filterType, + id: index, + valueType: isIncludeRule(rule) ? 'include' : 'exclude', + value: isIncludeRule(rule) ? rule.include : rule.exclude, + })), + setIndexingRules: (_, indexingRules) => + indexingRules.map((val, index) => ({ ...val, id: index })), + setIndexingRule: (state, rule) => + state.map((currentRule) => (currentRule.id === rule.id ? rule : currentRule)), + }, + ], }), selectors: ({ selectors }) => ({ - hasUnsavedObjectsAndAssetsChanges: [ + hasUnsavedAssetsAndObjectsChanges: [ () => [ selectors.thumbnailsChecked, selectors.contentExtractionChecked, + selectors.hasUnsavedIndexingRulesChanges, (_, props) => props.contentSource, ], - (thumbnailsChecked, contentExtractionChecked, contentSource) => { + ( + thumbnailsChecked, + contentExtractionChecked, + hasUnsavedIndexingRulesChanges, + contentSource + ) => { const { indexing: { features: { @@ -248,7 +343,8 @@ export const SynchronizationLogic = kea< return ( thumbnailsChecked !== thumbnailsEnabled || - contentExtractionChecked !== contentExtractionEnabled + contentExtractionChecked !== contentExtractionEnabled || + hasUnsavedIndexingRulesChanges ); }, ], @@ -256,6 +352,11 @@ export const SynchronizationLogic = kea< () => [selectors.cachedSchedule, selectors.schedule], (cachedSchedule, schedule) => !isEqual(cachedSchedule, schedule), ], + indexingRulesForAPI: [ + () => [selectors.indexingRules], + (indexingRules: EditableIndexingRule[]) => + indexingRules.map((indexingRule) => indexingRuleToApiFormat(indexingRule)), + ], }), listeners: ({ actions, values, props }) => ({ handleSelectedTabChanged: async (tabId, breakpoint) => { @@ -277,6 +378,62 @@ export const SynchronizationLogic = kea< KibanaLogic.values.navigateToUrl(path); actions.setNavigatingBetweenTabs(false); }, + initAddIndexingRule: async ({ rule }) => { + const { id: sourceId } = props.contentSource; + const route = `/internal/workplace_search/org/sources/${sourceId}/indexing_rules/validate`; + try { + const response = await HttpLogic.values.http.post<{ + rules: Array<{ + valid: boolean; + error?: string; + }>; + }>(route, { + body: JSON.stringify({ + rules: [indexingRuleToApiFormat(rule)], + }), + }); + const error = response.rules[0]?.error; + const tableLogic = InlineEditableTableLogic({ + instanceId: 'IndexingRulesTable', + } as InlineEditableTableProps); + if (error) { + tableLogic.actions.setRowErrors([error]); + } else { + actions.addIndexingRule(rule); + } + tableLogic.actions.doneEditing(); + } catch (e) { + flashAPIErrors(e); + } + }, + initSetIndexingRule: async ({ rule }) => { + const { id: sourceId } = props.contentSource; + const route = `/internal/workplace_search/org/sources/${sourceId}/indexing_rules/validate`; + try { + const response = await HttpLogic.values.http.post<{ + rules: Array<{ + valid: boolean; + error?: string; + }>; + }>(route, { + body: JSON.stringify({ + rules: [indexingRuleToApiFormat(rule)], + }), + }); + const error = response.rules[0]?.error; + const tableLogic = InlineEditableTableLogic({ + instanceId: 'IndexingRulesTable', + } as InlineEditableTableProps); + if (error) { + tableLogic.actions.setRowErrors([error]); + } else { + actions.setIndexingRule(rule); + } + tableLogic.actions.doneEditing(); + } catch (e) { + flashAPIErrors(e); + } + }, updateSyncEnabled: async (enabled) => { actions.updateServerSettings({ content_source: { @@ -284,7 +441,7 @@ export const SynchronizationLogic = kea< }, }); }, - updateObjectsAndAssetsSettings: () => { + updateAssetsAndObjectsSettings: () => { actions.updateServerSettings({ content_source: { indexing: { @@ -292,6 +449,7 @@ export const SynchronizationLogic = kea< content_extraction: { enabled: values.contentExtractionChecked }, thumbnails: { enabled: values.thumbnailsChecked }, }, + rules: values.indexingRulesForAPI, }, }, }); @@ -360,3 +518,10 @@ const formatBlockedWindowsForServer = ( end, })); }; + +const indexingRuleToApiFormat = (indexingRule: EditableIndexingRule): IndexingRuleForAPI => { + const { valueType, filterType, value } = indexingRule; + return valueType === 'include' + ? { filter_type: filterType, include: value } + : { filter_type: filterType, exclude: value }; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_router.test.tsx index cf130d2c21a57d..b863c43edcb3d8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_router.test.tsx @@ -10,12 +10,12 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues } from '../../../../../__mocks__/kea_logic'; import React from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Redirect, Route, Switch } from 'react-router-dom'; import { shallow } from 'enzyme'; +import { AssetsAndObjects } from './assets_and_objects'; import { Frequency } from './frequency'; -import { ObjectsAndAssets } from './objects_and_assets'; import { Synchronization } from './synchronization'; import { SynchronizationRouter } from './synchronization_router'; @@ -25,9 +25,10 @@ describe('SynchronizationRouter', () => { const wrapper = shallow(); expect(wrapper.find(Synchronization)).toHaveLength(1); - expect(wrapper.find(ObjectsAndAssets)).toHaveLength(1); + expect(wrapper.find(AssetsAndObjects)).toHaveLength(1); expect(wrapper.find(Frequency)).toHaveLength(2); expect(wrapper.find(Switch)).toHaveLength(1); expect(wrapper.find(Route)).toHaveLength(4); + expect(wrapper.find(Redirect)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_router.tsx index ede0f293377cfa..ad91dfb1bd65ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_router.tsx @@ -6,18 +6,19 @@ */ import React from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Redirect, Route, Switch } from 'react-router-dom'; import { SYNC_FREQUENCY_PATH, BLOCKED_TIME_WINDOWS_PATH, - OBJECTS_AND_ASSETS_PATH, + ASSETS_AND_OBJECTS_PATH, SOURCE_SYNCHRONIZATION_PATH, getSourcesPath, + OLD_OBJECTS_AND_ASSETS_PATH, } from '../../../../routes'; +import { AssetsAndObjects } from './assets_and_objects'; import { Frequency } from './frequency'; -import { ObjectsAndAssets } from './objects_and_assets'; import { Synchronization } from './synchronization'; export const SynchronizationRouter: React.FC = () => ( @@ -31,8 +32,12 @@ export const SynchronizationRouter: React.FC = () => ( - - + + + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_sub_nav.test.tsx index a2978c34475db7..80e4e1fc06b103 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_sub_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_sub_nav.test.tsx @@ -24,9 +24,9 @@ describe('useSynchronizationSubNav', () => { href: '/sources/1/synchronization/frequency', }, { - id: 'sourceSynchronizationObjectsAndAssets', - name: 'Objects and assets', - href: '/sources/1/synchronization/objects_and_assets', + id: 'sourceSynchronizationAssetsAndObjects', + name: 'Assets and objects', + href: '/sources/1/synchronization/assets_and_objects', }, ]); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_sub_nav.tsx index 2df6e9177211f2..ab4f0cb3c0c095 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_sub_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_sub_nav.tsx @@ -14,7 +14,7 @@ import { NAV } from '../../../../constants'; import { getContentSourcePath, SYNC_FREQUENCY_PATH, - OBJECTS_AND_ASSETS_PATH, + ASSETS_AND_OBJECTS_PATH, } from '../../../../routes'; import { SourceLogic } from '../../source_logic'; @@ -35,9 +35,9 @@ export const useSynchronizationSubNav = () => { }), }, { - id: 'sourceSynchronizationObjectsAndAssets', - name: NAV.SYNCHRONIZATION_OBJECTS_AND_ASSETS, - ...generateNavLink({ to: getContentSourcePath(OBJECTS_AND_ASSETS_PATH, id, true) }), + id: 'sourceSynchronizationAssetsAndObjects', + name: NAV.SYNCHRONIZATION_ASSETS_AND_OBJECTS, + ...generateNavLink({ to: getContentSourcePath(ASSETS_AND_OBJECTS_PATH, id, true) }), }, ]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts index 92749f51291972..4dd41a79301815 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts @@ -543,21 +543,31 @@ export const SOURCE_FREQUENCY_DESCRIPTION = i18n.translate( } ); -export const SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsDescription', +export const SOURCE_ASSETS_AND_OBJECTS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsDescription', { defaultMessage: - 'Customize the indexing rules that determine which objects and assets are synchronized from this content source to Workplace Search.', + 'Flexibly manage the documents to be synchronized and made available for search using granular controls below.', } ); -export const SOURCE_OBJECTS_AND_ASSETS_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsLabel', +export const SOURCE_ASSETS_AND_OBJECTS_LEARN_MORE_LINK = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsLearnMoreLink', { - defaultMessage: 'Object and details to include in search results', + defaultMessage: 'Learn more about sync objects types.', } ); +export const SOURCE_ASSETS_AND_OBJECTS_ASSETS_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsAssetsLabel', + { defaultMessage: 'Assets' } +); + +export const SOURCE_ASSETS_AND_OBJECTS_OBJECTS_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsLabel', + { defaultMessage: 'Objects' } +); + export const SOURCE_SYNCHRONIZATION_TOGGLE_LABEL = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.sourceSynchronizationToggleLabel', { diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts index 3702298e8bcae5..bbcb102c6c4ed8 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts @@ -44,6 +44,8 @@ import { registerOrgSourceOauthConfigurationRoute, registerOrgSourceSynchronizeRoute, registerOauthConnectorParamsRoute, + registerAccountSourceValidateIndexingRulesRoute, + registerOrgSourceValidateIndexingRulesRoute, } from './sources'; const mockConfig = { @@ -310,6 +312,45 @@ describe('sources routes', () => { }); }); + describe('POST /internal/workplace_search/account/sources/{id}/indexing_rules/validate', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/internal/workplace_search/account/sources/{id}/indexing_rules/validate', + }); + + registerAccountSourceValidateIndexingRulesRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources/:id/indexing_rules/validate', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + rules: [ + { + filter_type: 'path_template', + exclude: '', + }, + ], + }, + }; + mockRouter.shouldValidate(request); + }); + }); + }); + describe('GET /internal/workplace_search/account/pre_sources/{id}', () => { let mockRouter: MockRouter; @@ -818,6 +859,45 @@ describe('sources routes', () => { }); }); + describe('POST /internal/workplace_search/org/sources/{id}/indexing_rules/validate', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/internal/workplace_search/org/sources/{id}/indexing_rules/validate', + }); + + registerOrgSourceValidateIndexingRulesRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources/:id/indexing_rules/validate', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + rules: [ + { + filter_type: 'path_template', + exclude: '', + }, + ], + }, + }; + mockRouter.shouldValidate(request); + }); + }); + }); + describe('GET /internal/workplace_search/org/pre_sources/{id}', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 12f4844461409f..222288d369fdb6 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -96,11 +96,32 @@ const sourceSettingsSchema = schema.object({ ), }) ), + rules: schema.maybe( + schema.arrayOf( + schema.object({ + filter_type: schema.string(), + exclude: schema.maybe(schema.string()), + include: schema.maybe(schema.string()), + }) + ) + ), }) ), }), }); +const validateRulesSchema = schema.object({ + rules: schema.maybe( + schema.arrayOf( + schema.object({ + filter_type: schema.string(), + exclude: schema.maybe(schema.string()), + include: schema.maybe(schema.string()), + }) + ) + ), +}); + // Account routes export function registerAccountSourcesRoute({ router, @@ -273,6 +294,26 @@ export function registerAccountSourceSettingsRoute({ ); } +export function registerAccountSourceValidateIndexingRulesRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/internal/workplace_search/account/sources/{id}/indexing_rules/validate', + validate: { + body: validateRulesSchema, + params: schema.object({ + id: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/sources/:id/indexing_rules/validate', + }) + ); +} + export function registerAccountPreSourceRoute({ router, enterpriseSearchRequestHandler, @@ -620,6 +661,26 @@ export function registerOrgSourceSettingsRoute({ ); } +export function registerOrgSourceValidateIndexingRulesRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/internal/workplace_search/org/sources/{id}/indexing_rules/validate', + validate: { + body: validateRulesSchema, + params: schema.object({ + id: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/sources/:id/indexing_rules/validate', + }) + ); +} + export function registerOrgPreSourceRoute({ router, enterpriseSearchRequestHandler, @@ -955,6 +1016,7 @@ export const registerSourcesRoutes = (dependencies: RouteDependencies) => { registerAccountSourceFederatedSummaryRoute(dependencies); registerAccountSourceReauthPrepareRoute(dependencies); registerAccountSourceSettingsRoute(dependencies); + registerAccountSourceValidateIndexingRulesRoute(dependencies); registerAccountPreSourceRoute(dependencies); registerAccountPrepareSourcesRoute(dependencies); registerAccountSourceSearchableRoute(dependencies); @@ -970,6 +1032,7 @@ export const registerSourcesRoutes = (dependencies: RouteDependencies) => { registerOrgSourceFederatedSummaryRoute(dependencies); registerOrgSourceReauthPrepareRoute(dependencies); registerOrgSourceSettingsRoute(dependencies); + registerOrgSourceValidateIndexingRulesRoute(dependencies); registerOrgPreSourceRoute(dependencies); registerOrgPrepareSourcesRoute(dependencies); registerOrgSourceSearchableRoute(dependencies); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 481c7e39117533..5956b7ad84108c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10666,7 +10666,6 @@ "xpack.enterpriseSearch.workplaceSearch.nav.sources": "ソース", "xpack.enterpriseSearch.workplaceSearch.nav.synchronization": "同期", "xpack.enterpriseSearch.workplaceSearch.nav.synchronizationFrequency": "頻度", - "xpack.enterpriseSearch.workplaceSearch.nav.synchronizationObjectsAndAssets": "オブジェクトとアセット", "xpack.enterpriseSearch.workplaceSearch.nonPlatinumOauthDescription": "Workplace Search検索APIを安全に使用するために、OAuthアプリケーションを構成します。プラチナライセンスにアップグレードして、検索APIを有効にし、OAuthアプリケーションを作成します。", "xpack.enterpriseSearch.workplaceSearch.nonPlatinumOauthTitle": "カスタム検索アプリケーションのOAuthを構成", "xpack.enterpriseSearch.workplaceSearch.oauth.description": "組織のOAuthクライアントを作成します。", @@ -10920,8 +10919,6 @@ "xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.sharePoint": "SharePoint Online", "xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.slack": "Slack", "xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.zendesk": "Zendesk", - "xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsDescription": "このコンテンツソースからWorkplace Searchに同期されるオブジェクトとアセットを決定するインデックスルールをカスタマイズします。", - "xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsLabel": "検索結果に含めるオブジェクトと詳細", "xpack.enterpriseSearch.workplaceSearch.sources.sourceOverviewTitle": "ソース概要", "xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncConfirmMessage": "この要求を続行し、他のすべての同期を停止しますか?", "xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncConfirmTitle": "新しいコンテンツ同期を開始しますか?", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9fe16bef172a0a..49f8d466523acc 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10524,7 +10524,6 @@ "xpack.enterpriseSearch.workplaceSearch.nav.sources": "源", "xpack.enterpriseSearch.workplaceSearch.nav.synchronization": "同步", "xpack.enterpriseSearch.workplaceSearch.nav.synchronizationFrequency": "频率", - "xpack.enterpriseSearch.workplaceSearch.nav.synchronizationObjectsAndAssets": "对象和资产", "xpack.enterpriseSearch.workplaceSearch.nonPlatinumOauthDescription": "配置 OAuth 应用程序,以安全使用 Workplace Search 搜索 API。升级到白金级许可证,以启用搜索 API 并创建您的 OAuth 应用程序。", "xpack.enterpriseSearch.workplaceSearch.nonPlatinumOauthTitle": "正在为定制搜索应用程序配置 OAuth", "xpack.enterpriseSearch.workplaceSearch.oauth.description": "为您的组织创建 OAuth 客户端。", @@ -10779,8 +10778,6 @@ "xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.sharePoint": "Sharepoint", "xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.slack": "Slack", "xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.zendesk": "Zendesk", - "xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsDescription": "定制确定将哪些对象和资产从此内容源同步到 Workplace Search 的索引规则。", - "xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsLabel": "要包括在搜索结果中的对象和详情", "xpack.enterpriseSearch.workplaceSearch.sources.sourceOverviewTitle": "源概览", "xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncConfirmMessage": "是否确定要继续处理此请求并停止所有其他同步?", "xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncConfirmTitle": "开始新内容同步?", From 3fe08aff12834c1c0d556a7ba4cf8ff1488cb585 Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Wed, 16 Feb 2022 16:10:14 +0100 Subject: [PATCH 3/3] [Fleet] readded missing packages to keep up to date list (#125787) * readded missing packages to keep up to date list * rename --- .../fleet/common/constants/preconfiguration.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/common/constants/preconfiguration.ts b/x-pack/plugins/fleet/common/constants/preconfiguration.ts index 5689223852a325..e1dc87a3ebd060 100644 --- a/x-pack/plugins/fleet/common/constants/preconfiguration.ts +++ b/x-pack/plugins/fleet/common/constants/preconfiguration.ts @@ -7,6 +7,8 @@ import { uniqBy } from 'lodash'; +import { FLEET_ELASTIC_AGENT_PACKAGE, FLEET_SERVER_PACKAGE, FLEET_SYSTEM_PACKAGE } from '.'; + import { autoUpdatePackages, autoUpgradePoliciesPackages } from './epm'; // UUID v5 values require a namespace. We use UUID v5 for some of our preconfigured ID values. @@ -28,9 +30,18 @@ export const AUTO_UPGRADE_POLICIES_PACKAGES = autoUpgradePoliciesPackages.map((n version: PRECONFIGURATION_LATEST_KEYWORD, })); +export const FLEET_PACKAGES = [ + FLEET_SYSTEM_PACKAGE, + FLEET_ELASTIC_AGENT_PACKAGE, + FLEET_SERVER_PACKAGE, +].map((name) => ({ + name, + version: PRECONFIGURATION_LATEST_KEYWORD, +})); + // Controls whether the `Keep Policies up to date` setting is exposed to the user export const KEEP_POLICIES_UP_TO_DATE_PACKAGES = uniqBy( - [...AUTO_UPGRADE_POLICIES_PACKAGES, ...AUTO_UPDATE_PACKAGES], + [...AUTO_UPGRADE_POLICIES_PACKAGES, ...FLEET_PACKAGES, ...AUTO_UPDATE_PACKAGES], ({ name }) => name );