[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:
parent
690c25112c
commit
3025942e1c
15 changed files with 331 additions and 94 deletions
|
@ -54,6 +54,7 @@ const defaultIndexing = {
|
|||
incremental: 'PT2H',
|
||||
delete: 'PT10M',
|
||||
permissions: 'PT3H',
|
||||
blockedWindows: [],
|
||||
estimates: {
|
||||
full: {
|
||||
nextStart: '2021-09-30T15:37:38+00:00',
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in a new issue