[Workplace Search] Wire up write view for Blocked windows (#114696)

* Fix unsaved changes prompt showing up between tabs

We already added a reducer for this but forgot to implement this. Because we have shared state between the tabs, we need to overrule the unsaved changes prompt when simply navigating between tabs.

* Fix some timezone issues

After wiring up the backend and converting to UTC, some changes to mocks and time formats had to be made.

* Refactor to remove blockedWindows reducer

This commit refactors to make use of the already-in-state schedule object. Previously, while wiring up the static views, I used a blockedWindows array directly on the state tree. This simplifies things so that equality checks can be done with one object.

* Wire up ability to remove blocked window

* Fix key and remove fallback

It was hard to test removing an item from an array that doesn’t exist so I changed the code to expect the array to be present (! operator), since the other path is not possible.

Also updated the server value from deletion to delete to match the API

* Wire up blocked windows form to change values and update state

* Pass formatted blocked_windows to server

(test was covered in previous commit)

* Update link text, hrefs, and replace temp copy

One of the links was removed intentionally

* Fix typo

* Fix edge case where unsaved changes shown when removing last item

The API omits the key when there are no items so we need to have the item removed as well in the UI state. Otherwise, removing the last item will cause the UI to say there are unsaved changes when there are not.

I tried setting  it as:

schedule.blockedWindows = undefined

but the selector did not see those as equal but deleting the key does.

* More typo fixes

Syncronization -> Synchronization (+h)

* Fix link address

* Refactor for simplicity

Was unable to figure out the TypeScript  but did some more digging
This commit is contained in:
Scotty Bollinger 2021-10-12 20:32:08 -05:00 committed by GitHub
parent 690c25112c
commit 3025942e1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 331 additions and 94 deletions

View file

@ -54,6 +54,7 @@ const defaultIndexing = {
incremental: 'PT2H',
delete: 'PT10M',
permissions: 'PT3H',
blockedWindows: [],
estimates: {
full: {
nextStart: '2021-09-30T15:37:38+00:00',

View file

@ -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';

View file

@ -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 {

View file

@ -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',
};

View file

@ -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(<BlockedWindowItem {...props} />);
@ -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(<BlockedWindowItem {...props} />);
wrapper.find(EuiButton).simulate('click');
expect(removeBlockedWindow).toHaveBeenCalledWith(0);
});
it('handles "jobType" select change', () => {
const wrapper = shallow(<BlockedWindowItem {...props} />);
wrapper.find(EuiSuperSelect).simulate('change', 'delete');
expect(setBlockedTimeWindow).toHaveBeenCalledWith(0, 'jobType', 'delete');
});
it('handles "day" select change', () => {
const wrapper = shallow(<BlockedWindowItem {...props} />);
wrapper.find(EuiSelect).simulate('change', { target: { value: 'tuesday' } });
expect(setBlockedTimeWindow).toHaveBeenCalledWith(0, 'day', 'tuesday');
});
it('handles "start" time change', () => {
const wrapper = shallow(<BlockedWindowItem {...props} />);
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(<BlockedWindowItem {...props} />);
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');
});
});

View file

@ -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<Props> = ({ blockedWindow }) => {
const handleSyncTypeChange = () => '#TODO';
const handleStartDateChange = () => '#TODO';
const handleEndDateChange = () => '#TODO';
export const BlockedWindowItem: React.FC<Props> = ({ blockedWindow, index }) => {
const { contentSource } = useValues(SourceLogic);
const { removeBlockedWindow, setBlockedTimeWindow } = useActions(
SynchronizationLogic({ contentSource })
);
return (
<>
@ -109,7 +116,7 @@ export const BlockedWindowItem: React.FC<Props> = ({ blockedWindow }) => {
<EuiSuperSelect
valueOfSelected={blockedWindow.jobType}
options={syncOptions}
onChange={handleSyncTypeChange}
onChange={(value) => setBlockedTimeWindow(index, 'jobType', value)}
itemClassName="blockedWindowSelectItem"
popoverClassName="blockedWindowSelectPopover"
/>
@ -118,7 +125,11 @@ export const BlockedWindowItem: React.FC<Props> = ({ blockedWindow }) => {
<EuiText>{ON_LABEL}</EuiText>
</EuiFlexItem>
<EuiFlexItem style={{ minWidth: 130 }}>
<EuiSelect value={blockedWindow.day} options={daySelectOptions} />
<EuiSelect
value={blockedWindow.day}
onChange={(e) => setBlockedTimeWindow(index, 'day', e.target.value)}
options={daySelectOptions}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>{BETWEEN_LABEL}</EuiText>
@ -129,8 +140,11 @@ export const BlockedWindowItem: React.FC<Props> = ({ blockedWindow }) => {
<EuiDatePicker
showTimeSelect
showTimeSelectOnly
selected={moment(blockedWindow.start, 'HH:mm:ssZ')}
onChange={handleStartDateChange}
selected={moment(blockedWindow.start, 'HH:mm:ssZ').utc()}
onChange={(value) =>
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<Props> = ({ blockedWindow }) => {
<EuiDatePicker
showTimeSelect
showTimeSelectOnly
selected={moment(blockedWindow.end, 'HH:mm:ssZ')}
onChange={handleEndDateChange}
selected={moment(blockedWindow.end, 'HH:mm:ssZ').utc()}
onChange={(value) =>
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<Props> = ({ blockedWindow }) => {
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill color="danger">
<EuiButton fill color="danger" onClick={() => removeBlockedWindow(index)}>
{REMOVE_BUTTON}
</EuiButton>
</EuiFlexItem>

View file

@ -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(<BlockedWindows />);
expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);

View file

@ -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) => (
<BlockedWindowItem key={i} blockedWindow={blockedWindow} />
{blockedWindows?.map((blockedWindow, i) => (
<BlockedWindowItem key={i} index={i} blockedWindow={blockedWindow} />
))}
<EuiSpacer />
<EuiButton onClick={addBlockedWindow}>{ADD_LABEL}</EuiButton>

View file

@ -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<FrequencyProps> = ({ 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<FrequencyProps> = ({ tabId }) => {
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiLink href={DIFFERENT_SYNC_TYPES_DOCS_URL} external>
{DIFFERENT_SYNC_TYPES_LINK_LABEL}
</EuiLink>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink href={SYNC_BEST_PRACTICES_DOCS_URL} external>
{SYNC_BEST_PRACTICES_LINK_LABEL}
{SYNC_FREQUENCY_LINK_LABEL}
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
@ -108,7 +104,7 @@ export const Frequency: React.FC<FrequencyProps> = ({ tabId }) => {
isLoading={false}
>
<UnsavedChangesPrompt
hasUnsavedChanges={hasUnsavedFrequencyChanges}
hasUnsavedChanges={!navigatingBetweenTabs && hasUnsavedFrequencyChanges}
messageText={SYNC_UNSAVED_CHANGES_MESSAGE}
/>
<ViewContentHeader

View file

@ -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';

View file

@ -31,7 +31,7 @@ import {
SYNC_MANAGEMENT_THUMBNAILS_LABEL,
SYNC_MANAGEMENT_THUMBNAILS_GLOBAL_CONFIG_LABEL,
SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION,
SYNC_OBJECTS_TYPES_LINK_LABEL,
OBJECTS_AND_ASSETS_LINK_LABEL,
SOURCE_OBJECTS_AND_ASSETS_LABEL,
SYNC_UNSAVED_CHANGES_MESSAGE,
} from '../../constants';
@ -88,7 +88,7 @@ export const ObjectsAndAssets: React.FC = () => {
action={actions}
/>
<EuiLink href={OBJECTS_AND_ASSETS_DOCS_URL} external>
{SYNC_OBJECTS_TYPES_LINK_LABEL}
{OBJECTS_AND_ASSETS_LINK_LABEL}
</EuiLink>
<EuiHorizontalRule />
<EuiText size="m">{SOURCE_OBJECTS_AND_ASSETS_LABEL}</EuiText>

View file

@ -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 = (
<EuiPanel hasBorder>
<EuiSwitch
label={SOURCE_SYNCRONIZATION_TOGGLE_LABEL}
label={SOURCE_SYNCHRONIZATION_TOGGLE_LABEL}
checked={enabled}
onChange={(e) => onChange(e.target.checked)}
/>
<EuiSpacer size="m" />
<EuiText size="s" color="subdued">
{SOURCE_SYNCRONIZATION_TOGGLE_DESCRIPTION}
{SOURCE_SYNCHRONIZATION_TOGGLE_DESCRIPTION}
</EuiText>
</EuiPanel>
);
@ -65,7 +65,7 @@ export const Synchronization: React.FC = () => {
>
<ViewContentHeader
title={NAV.SYNCHRONIZATION}
description={SOURCE_SYNCRONIZATION_DESCRIPTION}
description={SOURCE_SYNCHRONIZATION_DESCRIPTION}
/>
<EuiLink href={SYNCHRONIZATION_DOCS_URL} external>
{SYNCHRONIZATION_LINK_LABEL}

View file

@ -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.blockedWindows).toEqual([emptyBlockedWindow]);
expect(SynchronizationLogic.values.schedule.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,7 @@ describe('SynchronizationLogic', () => {
describe('updateFrequencySettings', () => {
it('calls updateServerSettings method', async () => {
SynchronizationLogic.actions.addBlockedWindow();
const updateServerSettingsSpy = jest.spyOn(
SynchronizationLogic.actions,
'updateServerSettings'
@ -190,6 +262,35 @@ describe('SynchronizationLogic', () => {
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'
);
SynchronizationLogic.actions.updateFrequencySettings();
expect(updateServerSettingsSpy).toHaveBeenCalledWith({
content_source: {
indexing: {
schedule: {
full: 'P1D',
incremental: 'PT2H',
delete: 'PT10M',
blocked_windows: [],
},
},
},

View file

@ -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,
}));
};

View file

@ -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',
}
);