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 fab241163f470..48cbf4ba00d87 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 @@ -54,6 +54,7 @@ const defaultIndexing = { incremental: 'PT2H', delete: 'PT10M', permissions: 'PT3H', + blockedWindows: [], estimates: { full: { nextStart: '2021-09-30T15:37:38+00:00', 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 691b52c9f51e4..ec515eed91179 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 @@ -45,10 +45,9 @@ export const CUSTOM_SOURCE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-custom-ap export const CUSTOM_API_DOCS_URL = `${DOCS_PREFIX}/workplace-search-custom-sources-api.html`; export const CUSTOM_API_DOCUMENT_PERMISSIONS_DOCS_URL = `${CUSTOM_SOURCE_DOCS_URL}#custom-api-source-document-level-access-control`; export const ENT_SEARCH_LICENSE_MANAGEMENT = `${docLinks.enterpriseSearchBase}/license-management.html`; -export const SYNCHRONIZATION_DOCS_URL = '#TODO'; -export const DIFFERENT_SYNC_TYPES_DOCS_URL = '#TODO'; -export const SYNC_BEST_PRACTICES_DOCS_URL = '#TODO'; -export const OBJECTS_AND_ASSETS_DOCS_URL = '#TODO'; +export const SYNCHRONIZATION_DOCS_URL = `${DOCS_PREFIX}}/workplace-search-customizing-indexing-rules.html#workplace-search-customizing-indexing-rules`; +export const DIFFERENT_SYNC_TYPES_DOCS_URL = `${DOCS_PREFIX}}/workplace-search-customizing-indexing-rules.html#_indexing_schedule`; +export const OBJECTS_AND_ASSETS_DOCS_URL = `${DOCS_PREFIX}}/workplace-search-customizing-indexing-rules.html#workplace-search-customizing-indexing-rules`; export const PERSONAL_PATH = '/p'; 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 72bcf850fbcd9..ab45c54cc5c57 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 @@ -5,8 +5,6 @@ * 2.0. */ -import { Moment } from 'moment'; - import { RoleMapping } from '../shared/types'; export * from '../../../common/types/workplace_search'; @@ -166,8 +164,8 @@ export type DayOfWeek = typeof DAYS_OF_WEEK_VALUES[number]; export interface BlockedWindow { jobType: SyncJobType; day: DayOfWeek | 'all'; - start: Moment; - end: Moment; + start: string; + end: string; } export interface IndexingConfig { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/__mocks__/syncronization.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/__mocks__/synchronization.mock.ts similarity index 75% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/__mocks__/syncronization.mock.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/__mocks__/synchronization.mock.ts index a71be55339f48..af6a0552fc27b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/__mocks__/syncronization.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/__mocks__/synchronization.mock.ts @@ -5,13 +5,11 @@ * 2.0. */ -import moment from 'moment'; - import { SyncJobType, DayOfWeek } from '../../../../../types'; export const blockedWindow = { jobType: 'incremental' as SyncJobType, day: 'sunday' as DayOfWeek, - start: moment().set('hour', 11).set('minutes', 0), - end: moment().set('hour', 13).set('minutes', 0), + start: '11:00:00Z', + end: '13:00:00Z', }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_item.test.tsx index 703b1f9d8c5fe..f3c01b8d94d37 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_item.test.tsx @@ -5,18 +5,44 @@ * 2.0. */ -import { blockedWindow } from './__mocks__/syncronization.mock'; +import '../../../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; +import { fullContentSources } from '../../../../__mocks__/content_sources.mock'; +import { blockedWindow } from './__mocks__/synchronization.mock'; import React from 'react'; import { shallow } from 'enzyme'; +import moment from 'moment'; -import { EuiDatePickerRange, EuiSelect, EuiSuperSelect } from '@elastic/eui'; +import { + EuiButton, + EuiDatePicker, + EuiDatePickerRange, + EuiSelect, + EuiSuperSelect, +} from '@elastic/eui'; import { BlockedWindowItem } from './blocked_window_item'; describe('BlockedWindowItem', () => { - const props = { blockedWindow }; + const removeBlockedWindow = jest.fn(); + const setBlockedTimeWindow = jest.fn(); + const mockActions = { + removeBlockedWindow, + setBlockedTimeWindow, + }; + const mockValues = { + contentSource: fullContentSources[0], + }; + + beforeEach(() => { + setMockActions(mockActions); + setMockValues(mockValues); + }); + + const props = { blockedWindow, index: 0 }; + it('renders', () => { const wrapper = shallow(); @@ -24,4 +50,47 @@ describe('BlockedWindowItem', () => { expect(wrapper.find(EuiSuperSelect)).toHaveLength(1); expect(wrapper.find(EuiDatePickerRange)).toHaveLength(1); }); + + it('handles remove button click', () => { + const wrapper = shallow(); + wrapper.find(EuiButton).simulate('click'); + + expect(removeBlockedWindow).toHaveBeenCalledWith(0); + }); + + it('handles "jobType" select change', () => { + const wrapper = shallow(); + wrapper.find(EuiSuperSelect).simulate('change', 'delete'); + + expect(setBlockedTimeWindow).toHaveBeenCalledWith(0, 'jobType', 'delete'); + }); + + it('handles "day" select change', () => { + const wrapper = shallow(); + wrapper.find(EuiSelect).simulate('change', { target: { value: 'tuesday' } }); + + expect(setBlockedTimeWindow).toHaveBeenCalledWith(0, 'day', 'tuesday'); + }); + + it('handles "start" time change', () => { + const wrapper = shallow(); + const dayRange = wrapper.find(EuiDatePickerRange).dive(); + dayRange + .find(EuiDatePicker) + .first() + .simulate('change', moment().utc().set({ hour: 10, minute: 0, seconds: 0 })); + + expect(setBlockedTimeWindow).toHaveBeenCalledWith(0, 'start', '10:00:00Z'); + }); + + it('handles "end" time change', () => { + const wrapper = shallow(); + const dayRange = wrapper.find(EuiDatePickerRange).dive(); + dayRange + .find(EuiDatePicker) + .last() + .simulate('change', moment().utc().set({ hour: 12, minute: 0, seconds: 0 })); + + expect(setBlockedTimeWindow).toHaveBeenCalledWith(0, 'end', '12:00:00Z'); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_item.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_item.tsx index 5aec23b5faaea..272efc6fc3c50 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_item.tsx @@ -7,6 +7,7 @@ import React from 'react'; +import { useActions, useValues } from 'kea'; import moment from 'moment'; import { @@ -40,8 +41,13 @@ import { UTC_TITLE, } from '../../constants'; +import { SourceLogic } from '../../source_logic'; + +import { SynchronizationLogic } from './synchronization_logic'; + interface Props { blockedWindow: BlockedWindow; + index: number; } const syncOptions = [ @@ -66,7 +72,7 @@ const syncOptions = [ ), }, { - value: 'deletion', + value: 'delete', inputDisplay: DELETION_SYNC_LABEL, dropdownDisplay: ( <> @@ -93,10 +99,11 @@ const daySelectOptions = DAYS_OF_WEEK_VALUES.map((day) => ({ })) as EuiSelectOption[]; daySelectOptions.push({ text: ALL_DAYS_LABEL, value: 'all' }); -export const BlockedWindowItem: React.FC = ({ blockedWindow }) => { - const handleSyncTypeChange = () => '#TODO'; - const handleStartDateChange = () => '#TODO'; - const handleEndDateChange = () => '#TODO'; +export const BlockedWindowItem: React.FC = ({ blockedWindow, index }) => { + const { contentSource } = useValues(SourceLogic); + const { removeBlockedWindow, setBlockedTimeWindow } = useActions( + SynchronizationLogic({ contentSource }) + ); return ( <> @@ -109,7 +116,7 @@ export const BlockedWindowItem: React.FC = ({ blockedWindow }) => { setBlockedTimeWindow(index, 'jobType', value)} itemClassName="blockedWindowSelectItem" popoverClassName="blockedWindowSelectPopover" /> @@ -118,7 +125,11 @@ export const BlockedWindowItem: React.FC = ({ blockedWindow }) => { {ON_LABEL} - + setBlockedTimeWindow(index, 'day', e.target.value)} + options={daySelectOptions} + /> {BETWEEN_LABEL} @@ -129,8 +140,11 @@ export const BlockedWindowItem: React.FC = ({ blockedWindow }) => { + value && + setBlockedTimeWindow(index, 'start', `${value.utc().format('HH:mm:ss')}Z`) + } dateFormat="h:mm A" timeFormat="h:mm A" /> @@ -139,8 +153,10 @@ export const BlockedWindowItem: React.FC = ({ blockedWindow }) => { + value && setBlockedTimeWindow(index, 'end', `${value.utc().format('HH:mm:ss')}Z`) + } dateFormat="h:mm A" timeFormat="h:mm A" /> @@ -163,7 +179,7 @@ export const BlockedWindowItem: React.FC = ({ blockedWindow }) => { /> - + removeBlockedWindow(index)}> {REMOVE_BUTTON} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_tab.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_tab.test.tsx index 7cada1d39fb6e..44973cef966ee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_tab.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_tab.test.tsx @@ -8,7 +8,7 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; import { fullContentSources } from '../../../../__mocks__/content_sources.mock'; -import { blockedWindow } from './__mocks__/syncronization.mock'; +import { blockedWindow } from './__mocks__/synchronization.mock'; import React from 'react'; @@ -24,9 +24,11 @@ describe('BlockedWindows', () => { const mockActions = { addBlockedWindow, }; + const contentSource = { ...fullContentSources[0] }; + contentSource.indexing.schedule.blockedWindows = [blockedWindow] as any; const mockValues = { - blockedWindows: [blockedWindow], - contentSource: fullContentSources[0], + contentSource, + schedule: contentSource.indexing.schedule, }; beforeEach(() => { @@ -41,7 +43,7 @@ describe('BlockedWindows', () => { }); it('renders empty state', () => { - setMockValues({ blockedWindows: [] }); + setMockValues({ schedule: { blockedWindows: [] } }); const wrapper = shallow(); expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_tab.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_tab.tsx index f0227f76d4aa5..488346c2d6e30 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_tab.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/blocked_window_tab.tsx @@ -20,10 +20,12 @@ import { SynchronizationLogic } from './synchronization_logic'; export const BlockedWindows: React.FC = () => { const { contentSource } = useValues(SourceLogic); - const { blockedWindows } = useValues(SynchronizationLogic({ contentSource })); + const { + schedule: { blockedWindows }, + } = useValues(SynchronizationLogic({ contentSource })); const { addBlockedWindow } = useActions(SynchronizationLogic({ contentSource })); - const hasBlockedWindows = blockedWindows.length > 0; + const hasBlockedWindows = blockedWindows && blockedWindows.length > 0; const emptyState = ( <> @@ -43,8 +45,8 @@ export const BlockedWindows: React.FC = () => { const blockedWindowItems = ( <> - {blockedWindows.map((blockedWindow, i) => ( - + {blockedWindows?.map((blockedWindow, i) => ( + ))} {ADD_LABEL} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx index 2ada5b64be889..a682e10269e6c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx @@ -24,13 +24,12 @@ import { SAVE_BUTTON_LABEL } from '../../../../../shared/constants'; import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; import { NAV, RESET_BUTTON } from '../../../../constants'; -import { DIFFERENT_SYNC_TYPES_DOCS_URL, SYNC_BEST_PRACTICES_DOCS_URL } from '../../../../routes'; +import { DIFFERENT_SYNC_TYPES_DOCS_URL } from '../../../../routes'; import { SOURCE_FREQUENCY_DESCRIPTION, SOURCE_SYNC_FREQUENCY_TITLE, BLOCKED_TIME_WINDOWS_TITLE, - DIFFERENT_SYNC_TYPES_LINK_LABEL, - SYNC_BEST_PRACTICES_LINK_LABEL, + SYNC_FREQUENCY_LINK_LABEL, SYNC_UNSAVED_CHANGES_MESSAGE, } from '../../constants'; import { SourceLogic } from '../../source_logic'; @@ -46,7 +45,9 @@ interface FrequencyProps { export const Frequency: React.FC = ({ tabId }) => { const { contentSource } = useValues(SourceLogic); - const { hasUnsavedFrequencyChanges } = useValues(SynchronizationLogic({ contentSource })); + const { hasUnsavedFrequencyChanges, navigatingBetweenTabs } = useValues( + SynchronizationLogic({ contentSource }) + ); const { handleSelectedTabChanged, resetSyncSettings, updateFrequencySettings } = useActions( SynchronizationLogic({ contentSource }) ); @@ -87,12 +88,7 @@ export const Frequency: React.FC = ({ tabId }) => { - {DIFFERENT_SYNC_TYPES_LINK_LABEL} - - - - - {SYNC_BEST_PRACTICES_LINK_LABEL} + {SYNC_FREQUENCY_LINK_LABEL} @@ -108,7 +104,7 @@ export const Frequency: React.FC = ({ tabId }) => { isLoading={false} > { action={actions} /> - {SYNC_OBJECTS_TYPES_LINK_LABEL} + {OBJECTS_AND_ASSETS_LINK_LABEL} {SOURCE_OBJECTS_AND_ASSETS_LABEL} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx index 21c44225615ea..e88d4d251fa54 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx @@ -15,11 +15,11 @@ import { ViewContentHeader } from '../../../../components/shared/view_content_he import { NAV } from '../../../../constants'; import { SYNCHRONIZATION_DOCS_URL } from '../../../../routes'; import { - SOURCE_SYNCRONIZATION_DESCRIPTION, + SOURCE_SYNCHRONIZATION_DESCRIPTION, SYNCHRONIZATION_DISABLED_TITLE, SYNCHRONIZATION_DISABLED_DESCRIPTION, - SOURCE_SYNCRONIZATION_TOGGLE_LABEL, - SOURCE_SYNCRONIZATION_TOGGLE_DESCRIPTION, + SOURCE_SYNCHRONIZATION_TOGGLE_LABEL, + SOURCE_SYNCHRONIZATION_TOGGLE_DESCRIPTION, SYNCHRONIZATION_LINK_LABEL, } from '../../constants'; import { SourceLogic } from '../../source_logic'; @@ -40,13 +40,13 @@ export const Synchronization: React.FC = () => { const syncToggle = ( onChange(e.target.checked)} /> - {SOURCE_SYNCRONIZATION_TOGGLE_DESCRIPTION} + {SOURCE_SYNCHRONIZATION_TOGGLE_DESCRIPTION} ); @@ -65,7 +65,7 @@ export const Synchronization: React.FC = () => { > {SYNCHRONIZATION_LINK_LABEL} 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 cfd64118c3358..25fb256e85f01 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 @@ -38,6 +38,16 @@ describe('SynchronizationLogic', () => { const { navigateToUrl } = mockKibanaValues; const { mount } = new LogicMounter(SynchronizationLogic); const contentSource = fullContentSources[0]; + const sourceWithNoBlockedWindows = { + ...contentSource, + indexing: { + ...contentSource.indexing, + schedule: { + ...contentSource.indexing.schedule, + blockedWindows: undefined, + }, + }, + }; const defaultValues = { navigatingBetweenTabs: false, @@ -45,7 +55,6 @@ describe('SynchronizationLogic', () => { hasUnsavedFrequencyChanges: false, contentExtractionChecked: true, thumbnailsChecked: true, - blockedWindows: [], schedule: contentSource.indexing.schedule, cachedSchedule: contentSource.indexing.schedule, }; @@ -66,10 +75,23 @@ describe('SynchronizationLogic', () => { expect(SynchronizationLogic.values.navigatingBetweenTabs).toEqual(true); }); - it('addBlockedWindow', () => { - SynchronizationLogic.actions.addBlockedWindow(); + describe('addBlockedWindow', () => { + it('creates and populates empty array when undefined', () => { + mount({}, { contentSource: sourceWithNoBlockedWindows }); + SynchronizationLogic.actions.addBlockedWindow(); + + expect(SynchronizationLogic.values.schedule.blockedWindows).toEqual([emptyBlockedWindow]); + }); - expect(SynchronizationLogic.values.blockedWindows).toEqual([emptyBlockedWindow]); + it('adds item when list has items', () => { + SynchronizationLogic.actions.addBlockedWindow(); + SynchronizationLogic.actions.addBlockedWindow(); + + expect(SynchronizationLogic.values.schedule.blockedWindows).toEqual([ + emptyBlockedWindow, + emptyBlockedWindow, + ]); + }); }); it('setThumbnailsChecked', () => { @@ -112,6 +134,55 @@ describe('SynchronizationLogic', () => { expect(SynchronizationLogic.values.schedule.full).toEqual('P1DT30M'); }); }); + + describe('removeBlockedWindow', () => { + it('removes window', () => { + SynchronizationLogic.actions.addBlockedWindow(); + SynchronizationLogic.actions.addBlockedWindow(); + SynchronizationLogic.actions.removeBlockedWindow(0); + + expect(SynchronizationLogic.values.schedule.blockedWindows).toEqual([emptyBlockedWindow]); + }); + + it('returns "undefined" when last window removed', () => { + SynchronizationLogic.actions.addBlockedWindow(); + SynchronizationLogic.actions.removeBlockedWindow(0); + + expect(SynchronizationLogic.values.schedule.blockedWindows).toBeUndefined(); + }); + }); + }); + + 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].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'); + }); }); describe('listeners', () => { @@ -177,6 +248,35 @@ describe('SynchronizationLogic', () => { describe('updateFrequencySettings', () => { it('calls updateServerSettings method', async () => { + SynchronizationLogic.actions.addBlockedWindow(); + const updateServerSettingsSpy = jest.spyOn( + SynchronizationLogic.actions, + 'updateServerSettings' + ); + SynchronizationLogic.actions.updateFrequencySettings(); + + expect(updateServerSettingsSpy).toHaveBeenCalledWith({ + content_source: { + indexing: { + schedule: { + full: 'P1D', + incremental: 'PT2H', + delete: 'PT10M', + blocked_windows: [ + { + day: 'monday', + end: '13:00:00Z', + job_type: 'full', + start: '11:00:00Z', + }, + ], + }, + }, + }, + }); + }); + + it('handles case where blockedWindows undefined', async () => { const updateServerSettingsSpy = jest.spyOn( SynchronizationLogic.actions, 'updateServerSettings' @@ -190,6 +290,7 @@ describe('SynchronizationLogic', () => { full: 'P1D', incremental: 'PT2H', delete: 'PT10M', + blocked_windows: [], }, }, }, 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 95dbb8c75fce4..87a55f0e7dd3a 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 @@ -20,11 +20,19 @@ import { BLOCKED_TIME_WINDOWS_PATH, getContentSourcePath, } from '../../../../routes'; -import { BlockedWindow, IndexingSchedule, SyncJobType, TimeUnit } from '../../../../types'; +import { + BlockedWindow, + DayOfWeek, + IndexingSchedule, + SyncJobType, + TimeUnit, +} from '../../../../types'; import { SYNC_SETTINGS_UPDATED_MESSAGE } from '../../constants'; import { SourceLogic } from '../../source_logic'; +type BlockedWindowPropType = 'jobType' | 'day' | 'start' | 'end'; + interface ServerBlockedWindow { job_type: string; day: string; @@ -55,6 +63,7 @@ interface SynchronizationActions { setNavigatingBetweenTabs(navigatingBetweenTabs: boolean): boolean; handleSelectedTabChanged(tabId: TabId): TabId; addBlockedWindow(): void; + removeBlockedWindow(index: number): number; updateFrequencySettings(): void; updateObjectsAndAssetsSettings(): void; resetSyncSettings(): void; @@ -65,6 +74,15 @@ interface SynchronizationActions { value: string, unit: TimeUnit ): { type: SyncJobType; value: number; unit: TimeUnit }; + setBlockedTimeWindow( + index: number, + prop: BlockedWindowPropType, + value: string + ): { + index: number; + prop: BlockedWindowPropType; + value: string; + }; setContentExtractionChecked(checked: boolean): boolean; setServerSchedule(schedule: IndexingSchedule): IndexingSchedule; updateServerSettings(body: ServerSyncSettingsBody): ServerSyncSettingsBody; @@ -76,7 +94,6 @@ interface SynchronizationValues { hasUnsavedObjectsAndAssetsChanges: boolean; thumbnailsChecked: boolean; contentExtractionChecked: boolean; - blockedWindows: BlockedWindow[]; cachedSchedule: IndexingSchedule; schedule: IndexingSchedule; } @@ -84,8 +101,12 @@ interface SynchronizationValues { export const emptyBlockedWindow: BlockedWindow = { jobType: 'full', day: 'monday', - start: moment().set('hour', 11).set('minutes', 0), - end: moment().set('hour', 13).set('minutes', 0), + start: '11:00:00Z', + end: '13:00:00Z', +}; + +type BlockedWindowMap = { + [prop in keyof BlockedWindow]: SyncJobType | DayOfWeek | 'all' | string; }; export const SynchronizationLogic = kea< @@ -102,9 +123,15 @@ export const SynchronizationLogic = kea< value, unit, }), + setBlockedTimeWindow: (index: number, prop: BlockedWindowPropType, value: string) => ({ + index, + prop, + value, + }), setContentExtractionChecked: (checked: boolean) => checked, updateServerSettings: (body: ServerSyncSettingsBody) => body, setServerSchedule: (schedule: IndexingSchedule) => schedule, + removeBlockedWindow: (index: number) => index, updateFrequencySettings: true, updateObjectsAndAssetsSettings: true, resetSyncSettings: true, @@ -117,12 +144,6 @@ export const SynchronizationLogic = kea< setNavigatingBetweenTabs: (_, navigatingBetweenTabs) => navigatingBetweenTabs, }, ], - blockedWindows: [ - props.contentSource.indexing.schedule.blockedWindows || [], - { - addBlockedWindow: (state, _) => [...state, emptyBlockedWindow], - }, - ], thumbnailsChecked: [ props.contentSource.indexing.features.thumbnails.enabled, { @@ -176,6 +197,33 @@ export const SynchronizationLogic = kea< return schedule; }, + addBlockedWindow: (state, _) => { + const schedule = cloneDeep(state); + const blockedWindows = schedule.blockedWindows || []; + blockedWindows.push(emptyBlockedWindow); + schedule.blockedWindows = blockedWindows; + return schedule; + }, + removeBlockedWindow: (state, index) => { + const schedule = cloneDeep(state); + const blockedWindows = schedule.blockedWindows; + blockedWindows!.splice(index, 1); + if (blockedWindows!.length > 0) { + schedule.blockedWindows = blockedWindows; + } else { + delete schedule.blockedWindows; + } + return schedule; + }, + setBlockedTimeWindow: (state, { index, prop, value }) => { + const schedule = cloneDeep(state); + const blockedWindows = schedule.blockedWindows; + const blockedWindow = blockedWindows![index] as BlockedWindowMap; + blockedWindow[prop] = value; + (blockedWindows![index] as BlockedWindowMap) = blockedWindow; + schedule.blockedWindows = blockedWindows; + return schedule; + }, }, ], }), @@ -254,6 +302,7 @@ export const SynchronizationLogic = kea< full: values.schedule.full, incremental: values.schedule.incremental, delete: values.schedule.delete, + blocked_windows: formatBlockedWindowsForServer(values.schedule.blockedWindows), }, }, }, @@ -296,3 +345,16 @@ export const stripScheduleSeconds = (schedule: IndexingSchedule): IndexingSchedu return _schedule; }; + +const formatBlockedWindowsForServer = ( + blockedWindows?: BlockedWindow[] +): ServerBlockedWindow[] | undefined => { + if (!blockedWindows || blockedWindows.length < 1) return []; + + return blockedWindows.map(({ jobType, day, start, end }) => ({ + job_type: jobType, + day, + start, + end, + })); +}; 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 91e32834f3fbd..14d0a7f196ae8 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 @@ -527,11 +527,11 @@ export const SOURCE_OVERVIEW_TITLE = i18n.translate( } ); -export const SOURCE_SYNCRONIZATION_DESCRIPTION = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncronizationDescription', +export const SOURCE_SYNCHRONIZATION_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceSynchronizationDescription', { defaultMessage: - 'DO NOT TRANSLATE, temporary placeholder: Sync chupa chups dragée gummi bears jelly beans brownie. Fruitcake pie chocolate cake caramels carrot cake cotton candy dragée sweet roll soufflé.', + 'Synchronization provides control over data being indexed from the content source. Enable synchronization of data from the content source to Workplace Search.', } ); @@ -539,7 +539,7 @@ export const SOURCE_FREQUENCY_DESCRIPTION = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.sourceFrequencyDescription', { defaultMessage: - 'DO NOT TRANSLATE, temporary placeholder: Frequency chupa chups dragée gummi bears jelly beans brownie. Fruitcake pie chocolate cake caramels carrot cake cotton candy dragée sweet roll soufflé.', + 'Schedule the frequency of data synchronization between Workplace search and the content source. Indexing schedules that occur less frequently lower the burden on third-party servers, while more frequent will ensure your data is up-to-date.', } ); @@ -547,7 +547,7 @@ export const SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsDescription', { defaultMessage: - 'DO NOT TRANSLATE, temporary placeholder: Objects chupa chups dragée gummi bears jelly beans brownie. Fruitcake pie chocolate cake caramels carrot cake cotton candy dragée sweet roll soufflé.', + 'Customize the indexing rules that determine what data is synchronized from this content source to Workplace Search.', } ); @@ -558,24 +558,24 @@ export const SOURCE_OBJECTS_AND_ASSETS_LABEL = i18n.translate( } ); -export const SOURCE_SYNCRONIZATION_TOGGLE_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncronizationToggleLabel', +export const SOURCE_SYNCHRONIZATION_TOGGLE_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceSynchronizationToggleLabel', { defaultMessage: 'Synchronize this source', } ); -export const SOURCE_SYNCRONIZATION_TOGGLE_DESCRIPTION = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncronizationToggleDescription', +export const SOURCE_SYNCHRONIZATION_TOGGLE_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceSynchronizationToggleDescription', { defaultMessage: 'Source content will automatically be kept in sync.', } ); -export const SOURCE_SYNCRONIZATION_FREQUENCY_TITLE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncronizationFrequencyTitle', +export const SOURCE_SYNCHRONIZATION_FREQUENCY_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceSynchronizationFrequencyTitle', { - defaultMessage: 'Syncronization frequency', + defaultMessage: 'Synchronization frequency', } ); @@ -614,24 +614,17 @@ export const SYNCHRONIZATION_DISABLED_DESCRIPTION = i18n.translate( } ); -export const DIFFERENT_SYNC_TYPES_LINK_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.differentSyncTypesLinkLabel', +export const SYNC_FREQUENCY_LINK_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.syncFrequencyLinkLabel', { - defaultMessage: 'Learn more about different sync types', + defaultMessage: 'Learn more about synchronization frequency', } ); -export const SYNC_BEST_PRACTICES_LINK_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.syncBestPracticesLinkLabel', +export const OBJECTS_AND_ASSETS_LINK_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.objectsAndAssetsLinkLabel', { - defaultMessage: 'Learn more about sync best practices', - } -); - -export const SYNC_OBJECTS_TYPES_LINK_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.syncObjectsTypesLinkLabel', - { - defaultMessage: 'Learn more about sync objects types', + defaultMessage: 'Learn more about Objects and assets', } );