[Workplace Search] Migrate Objects and assets from Source settings to Synchronization section (#113982) (#114015)

* Rename method

We have to set the source from the sync logic file and this naming makes more sense

* Wire up Enable Synchronization toggle

* Remove sync controls from source settings

* Refactor to pass in contentSource as prop

Because we have a child logic file, SynchronizationLogic, we have to pass the content source into it for reading its values from SourceLogic. There are 3 ways to do this:

1. Access the source directly at SourceLogic.values.contentSource
  - This how we normally do it. The problem here is that SourceLogic is not mounted when the default values are set in the reducers. This caused the UI to break and I could not find a way to safely mount SourceLogic before this logic file needed it.

2. Use the connect property and connect to Sourcelogic to access contentSource
  - This actually worked great but our test helper does not work well with it and after an hour or so trying to make it work, I punted and decided to go with #3 below.

3. Pass the contentSource as a prop
  - This works great and is easy to test. The only drawback is that all other components that use the SynchronizationLogic file have to also pass in the content source. This commit does just that.

* Add logic for Objects and assets view

* Add content to Objects and assets view

* Add fallback for `nextStart` that is in the past

This is slightly beyond the scope of this PR but trying to make the final PR more manageable.

There is an edge case where a running job lists the nextStart in the past if it is is running. After a lengthy Slack convo, it was decided to catch these in the UI and show a fallback string instead of something like “Next run 3 hours ago”

* reduce -> map

From previous PR feedback

* Fix casing on i18n ID

Co-authored-by: Scotty Bollinger <scotty.bollinger@elastic.co>
This commit is contained in:
Kibana Machine 2021-10-05 19:42:13 -04:00 committed by GitHub
parent babfa59d1e
commit 3e05797525
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 565 additions and 245 deletions

View file

@ -105,84 +105,6 @@ describe('SourceSettings', () => {
);
});
it('handles disabling synchronization', () => {
const wrapper = shallow(<SourceSettings />);
const synchronizeSwitch = wrapper.find('[data-test-subj="SynchronizeToggle"]').first();
const event = { target: { checked: false } };
synchronizeSwitch.prop('onChange')?.(event as any);
wrapper.find('[data-test-subj="SaveSyncControlsButton"]').simulate('click');
expect(updateContentSource).toHaveBeenCalledWith(fullContentSources[0].id, {
indexing: {
enabled: false,
features: {
content_extraction: { enabled: true },
thumbnails: { enabled: true },
},
},
});
});
it('handles disabling thumbnails', () => {
const wrapper = shallow(<SourceSettings />);
const thumbnailsSwitch = wrapper.find('[data-test-subj="ThumbnailsToggle"]').first();
const event = { target: { checked: false } };
thumbnailsSwitch.prop('onChange')?.(event as any);
wrapper.find('[data-test-subj="SaveSyncControlsButton"]').simulate('click');
expect(updateContentSource).toHaveBeenCalledWith(fullContentSources[0].id, {
indexing: {
enabled: true,
features: {
content_extraction: { enabled: true },
thumbnails: { enabled: false },
},
},
});
});
it('handles disabling content extraction', () => {
const wrapper = shallow(<SourceSettings />);
const contentExtractionSwitch = wrapper
.find('[data-test-subj="ContentExtractionToggle"]')
.first();
const event = { target: { checked: false } };
contentExtractionSwitch.prop('onChange')?.(event as any);
wrapper.find('[data-test-subj="SaveSyncControlsButton"]').simulate('click');
expect(updateContentSource).toHaveBeenCalledWith(fullContentSources[0].id, {
indexing: {
enabled: true,
features: {
content_extraction: { enabled: false },
thumbnails: { enabled: true },
},
},
});
});
it('disables the thumbnails switch when globally disabled', () => {
setMockValues({
...mockValues,
contentSource: {
...fullContentSources[0],
areThumbnailsConfigEnabled: false,
},
});
const wrapper = shallow(<SourceSettings />);
const synchronizeSwitch = wrapper.find('[data-test-subj="ThumbnailsToggle"]');
expect(synchronizeSwitch.prop('disabled')).toEqual(true);
});
describe('DownloadDiagnosticsButton', () => {
it('renders for org with correct href', () => {
const wrapper = shallow(<SourceSettings />);

View file

@ -17,8 +17,6 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiSpacer,
EuiSwitch,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
@ -50,12 +48,6 @@ import {
SYNC_DIAGNOSTICS_TITLE,
SYNC_DIAGNOSTICS_DESCRIPTION,
SYNC_DIAGNOSTICS_BUTTON,
SYNC_MANAGEMENT_TITLE,
SYNC_MANAGEMENT_DESCRIPTION,
SYNC_MANAGEMENT_SYNCHRONIZE_LABEL,
SYNC_MANAGEMENT_THUMBNAILS_LABEL,
SYNC_MANAGEMENT_THUMBNAILS_GLOBAL_CONFIG_LABEL,
SYNC_MANAGEMENT_CONTENT_EXTRACTION_LABEL,
} from '../constants';
import { staticSourceData } from '../source_data';
import { SourceLogic } from '../source_logic';
@ -70,22 +62,7 @@ export const SourceSettings: React.FC = () => {
const { getSourceConfigData } = useActions(AddSourceLogic);
const {
contentSource: {
name,
id,
serviceType,
custom: isCustom,
isIndexedSource,
areThumbnailsConfigEnabled,
isOauth1,
indexing: {
enabled,
features: {
contentExtraction: { enabled: contentExtractionEnabled },
thumbnails: { enabled: thumbnailsEnabled },
},
},
},
contentSource: { name, id, serviceType, isOauth1 },
buttonLoading,
} = useValues(SourceLogic);
@ -109,11 +86,6 @@ export const SourceSettings: React.FC = () => {
const hideConfirm = () => setModalVisibility(false);
const showConfig = isOrganization && !isEmpty(configuredFields);
const showSyncControls = isOrganization && isIndexedSource && !isCustom;
const [synchronizeChecked, setSynchronize] = useState(enabled);
const [thumbnailsChecked, setThumbnails] = useState(thumbnailsEnabled);
const [contentExtractionChecked, setContentExtraction] = useState(contentExtractionEnabled);
const { clientId, clientSecret, publicKey, consumerKey, baseUrl } = configuredFields || {};
@ -130,18 +102,6 @@ export const SourceSettings: React.FC = () => {
updateContentSource(id, { name: inputValue });
};
const submitSyncControls = () => {
updateContentSource(id, {
indexing: {
enabled: synchronizeChecked,
features: {
content_extraction: { enabled: contentExtractionChecked },
thumbnails: { enabled: thumbnailsChecked },
},
},
});
};
const handleSourceRemoval = () => {
/**
* The modal was just hanging while the UI waited for the server to respond.
@ -221,58 +181,6 @@ export const SourceSettings: React.FC = () => {
</EuiFormRow>
</ContentSection>
)}
{showSyncControls && (
<ContentSection title={SYNC_MANAGEMENT_TITLE} description={SYNC_MANAGEMENT_DESCRIPTION}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSwitch
checked={synchronizeChecked}
onChange={(e) => setSynchronize(e.target.checked)}
label={SYNC_MANAGEMENT_SYNCHRONIZE_LABEL}
data-test-subj="SynchronizeToggle"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSwitch
checked={thumbnailsChecked}
onChange={(e) => setThumbnails(e.target.checked)}
label={
areThumbnailsConfigEnabled
? SYNC_MANAGEMENT_THUMBNAILS_LABEL
: SYNC_MANAGEMENT_THUMBNAILS_GLOBAL_CONFIG_LABEL
}
disabled={!areThumbnailsConfigEnabled}
data-test-subj="ThumbnailsToggle"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSwitch
checked={contentExtractionChecked}
onChange={(e) => setContentExtraction(e.target.checked)}
label={SYNC_MANAGEMENT_CONTENT_EXTRACTION_LABEL}
data-test-subj="ContentExtractionToggle"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
color="primary"
onClick={submitSyncControls}
data-test-subj="SaveSyncControlsButton"
>
{SAVE_CHANGES_BUTTON}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</ContentSection>
)}
<ContentSection title={SYNC_DIAGNOSTICS_TITLE} description={SYNC_DIAGNOSTICS_DESCRIPTION}>
<EuiButton
target="_blank"

View file

@ -10,7 +10,6 @@ import React from 'react';
import {
EuiButton,
EuiComboBox,
EuiComboBoxOptionOption,
EuiDatePicker,
EuiFlexGroup,
EuiFlexItem,
@ -81,13 +80,10 @@ const syncOptions = [
},
];
const dayPickerOptions = DAYS_OF_WEEK_VALUES.reduce((options, day) => {
options.push({
label: DAYS_OF_WEEK_LABELS[day.toUpperCase() as keyof typeof DAYS_OF_WEEK_LABELS],
value: day,
});
return options;
}, [] as Array<EuiComboBoxOptionOption<string>>);
const dayPickerOptions = DAYS_OF_WEEK_VALUES.map((day) => ({
label: DAYS_OF_WEEK_LABELS[day.toUpperCase() as keyof typeof DAYS_OF_WEEK_LABELS],
value: day,
}));
export const BlockedWindowItem: React.FC<Props> = ({ blockedWindow }) => {
const handleSyncTypeChange = () => '#TODO';

View file

@ -7,6 +7,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 React from 'react';
@ -25,6 +26,7 @@ describe('BlockedWindows', () => {
};
const mockValues = {
blockedWindows: [blockedWindow],
contentSource: fullContentSources[0],
};
beforeEach(() => {

View file

@ -13,13 +13,15 @@ import { EuiButton, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui';
import { ADD_LABEL } from '../../../../constants';
import { BLOCKED_EMPTY_STATE_TITLE, BLOCKED_EMPTY_STATE_DESCRIPTION } from '../../constants';
import { SourceLogic } from '../../source_logic';
import { BlockedWindowItem } from './blocked_window_item';
import { SynchronizationLogic } from './synchronization_logic';
export const BlockedWindows: React.FC = () => {
const { blockedWindows } = useValues(SynchronizationLogic);
const { addBlockedWindow } = useActions(SynchronizationLogic);
const { contentSource } = useValues(SourceLogic);
const { blockedWindows } = useValues(SynchronizationLogic({ contentSource }));
const { addBlockedWindow } = useActions(SynchronizationLogic({ contentSource }));
const hasBlockedWindows = blockedWindows.length > 0;

View file

@ -7,6 +7,7 @@
import '../../../../../__mocks__/shallow_useeffect.mock';
import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic';
import { fullContentSources } from '../../../../__mocks__/content_sources.mock';
import React from 'react';
@ -23,7 +24,9 @@ describe('Frequency', () => {
const mockActions = {
handleSelectedTabChanged,
};
const mockValues = {};
const mockValues = {
contentSource: fullContentSources[0],
};
beforeEach(() => {
setMockActions(mockActions);

View file

@ -7,7 +7,7 @@
import React from 'react';
import { useActions } from 'kea';
import { useActions, useValues } from 'kea';
import {
EuiButton,
@ -31,6 +31,7 @@ import {
DIFFERENT_SYNC_TYPES_LINK_LABEL,
SYNC_BEST_PRACTICES_LINK_LABEL,
} from '../../constants';
import { SourceLogic } from '../../source_logic';
import { SourceLayout } from '../source_layout';
import { BlockedWindows } from './blocked_window_tab';
@ -42,7 +43,8 @@ interface FrequencyProps {
}
export const Frequency: React.FC<FrequencyProps> = ({ tabId }) => {
const { handleSelectedTabChanged } = useActions(SynchronizationLogic);
const { contentSource } = useValues(SourceLogic);
const { handleSelectedTabChanged } = useActions(SynchronizationLogic({ contentSource }));
const tabs = [
{

View file

@ -8,21 +8,24 @@
import React from 'react';
import { shallow } from 'enzyme';
import moment from 'moment';
import { EuiFieldNumber, EuiSuperSelect } from '@elastic/eui';
import { FrequencyItem } from './frequency_item';
describe('FrequencyItem', () => {
const estimate = {
duration: 'PT3D',
nextStart: '2021-09-27T21:39:24+00:00',
lastRun: '2021-09-25T21:39:24+00:00',
};
const props = {
label: 'Item',
description: 'My item',
duration: 'PT2D',
estimate: {
duration: 'PT3D',
nextStart: '2021-09-27T21:39:24+00:00',
lastRun: '2021-09-25T21:39:24+00:00',
},
estimate,
};
it('renders', () => {
@ -60,5 +63,25 @@ describe('FrequencyItem', () => {
expect(wrapper.find(EuiFieldNumber).prop('value')).toEqual(1);
expect(wrapper.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('minutes');
});
it('handles "nextStart" that is in past', () => {
const wrapper = shallow(<FrequencyItem {...props} />);
expect(
(wrapper.find('[data-test-subj="nextStartSummary"]').prop('values') as any)!.nextStartTime
).toEqual('as soon as the currently running job finishes');
});
it('handles "nextStart" that is in future', () => {
const estimateWithPastNextStart = {
...estimate,
nextStart: moment().add(2, 'days').format(),
};
const wrapper = shallow(<FrequencyItem {...props} estimate={estimateWithPastNextStart} />);
expect(
(wrapper.find('[data-test-subj="nextStartSummary"]').prop('values') as any)!.nextStartTime
).toEqual('in 2 days');
});
});
});

View file

@ -27,6 +27,8 @@ import {
} from '../../../../../shared/constants';
import { SyncEstimate } from '../../../../types';
import { NEXT_SYNC_RUNNING_MESSAGE } from '../../constants';
interface Props {
label: string;
description: string;
@ -53,6 +55,8 @@ export const FrequencyItem: React.FC<Props> = ({ label, description, duration, e
const [interval, unit] = formatDuration(duration);
const { lastRun, nextStart, duration: durationEstimate } = estimate;
const estimateDisplay = durationEstimate && moment.duration(durationEstimate).humanize();
const nextStartIsPast = moment().isAfter(nextStart);
const nextStartTime = nextStartIsPast ? NEXT_SYNC_RUNNING_MESSAGE : moment(nextStart).fromNow();
const onChange = () => '#TODO';
@ -86,6 +90,7 @@ export const FrequencyItem: React.FC<Props> = ({ label, description, duration, e
const nextStartSummary = (
<FormattedMessage
data-test-subj="nextStartSummary"
id="xpack.enterpriseSearch.workplaceSearch.contentSources.synchronization.nextStartSummary"
defaultMessage="{nextStartStrong} will begin {nextStartTime}."
values={{
@ -97,7 +102,7 @@ export const FrequencyItem: React.FC<Props> = ({ label, description, duration, e
/>
</strong>
),
nextStartTime: moment(nextStart).fromNow(),
nextStartTime,
}}
/>
);

View file

@ -0,0 +1,86 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
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 React from 'react';
import { shallow } from 'enzyme';
import { EuiSwitch } from '@elastic/eui';
import { ObjectsAndAssets } from './objects_and_assets';
describe('ObjectsAndAssets', () => {
const setThumbnailsChecked = jest.fn();
const setContentExtractionChecked = jest.fn();
const updateSyncSettings = jest.fn();
const resetSyncSettings = jest.fn();
const contentSource = fullContentSources[0];
const mockActions = {
setThumbnailsChecked,
setContentExtractionChecked,
updateSyncSettings,
resetSyncSettings,
};
const mockValues = {
dataLoading: false,
blockedWindows: [blockedWindow],
contentSource,
thumbnailsChecked: true,
contentExtractionChecked: true,
hasUnsavedObjectsAndAssetsChanges: false,
};
beforeEach(() => {
setMockActions(mockActions);
setMockValues(mockValues);
});
it('renders', () => {
const wrapper = shallow(<ObjectsAndAssets />);
expect(wrapper.find(EuiSwitch)).toHaveLength(2);
});
it('handles thumbnails switch change', () => {
const wrapper = shallow(<ObjectsAndAssets />);
wrapper
.find('[data-test-subj="ThumbnailsToggle"]')
.simulate('change', { target: { checked: false } });
expect(setThumbnailsChecked).toHaveBeenCalledWith(false);
});
it('handles content extraction switch change', () => {
const wrapper = shallow(<ObjectsAndAssets />);
wrapper
.find('[data-test-subj="ContentExtractionToggle"]')
.simulate('change', { target: { checked: false } });
expect(setContentExtractionChecked).toHaveBeenCalledWith(false);
});
it('renders correct text when areThumbnailsConfigEnabled is false', () => {
setMockValues({
...mockValues,
contentSource: {
...contentSource,
areThumbnailsConfigEnabled: false,
},
});
const wrapper = shallow(<ObjectsAndAssets />);
expect(wrapper.find('[data-test-subj="ThumbnailsToggle"]').prop('label')).toEqual(
'Sync thumbnails - disabled at global configuration level'
);
});
});

View file

@ -7,33 +7,113 @@
import React from 'react';
import { EuiHorizontalRule, EuiLink } from '@elastic/eui';
import { useActions, useValues } from 'kea';
import {
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiLink,
EuiSpacer,
EuiSwitch,
EuiText,
} from '@elastic/eui';
import { SAVE_BUTTON_LABEL } from '../../../../../shared/constants';
import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt';
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
import { NAV } from '../../../../constants';
import { NAV, RESET_BUTTON } from '../../../../constants';
import { OBJECTS_AND_ASSETS_DOCS_URL } from '../../../../routes';
import {
SYNC_MANAGEMENT_CONTENT_EXTRACTION_LABEL,
SYNC_MANAGEMENT_THUMBNAILS_LABEL,
SYNC_MANAGEMENT_THUMBNAILS_GLOBAL_CONFIG_LABEL,
SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION,
SYNC_OBJECTS_TYPES_LINK_LABEL,
SOURCE_OBJECTS_AND_ASSETS_LABEL,
SYNC_UNSAVED_CHANGES_MESSAGE,
} from '../../constants';
import { SourceLogic } from '../../source_logic';
import { SourceLayout } from '../source_layout';
import { SynchronizationLogic } from './synchronization_logic';
export const ObjectsAndAssets: React.FC = () => {
const { contentSource, dataLoading } = useValues(SourceLogic);
const { thumbnailsChecked, contentExtractionChecked, hasUnsavedObjectsAndAssetsChanges } =
useValues(SynchronizationLogic({ contentSource }));
const {
setThumbnailsChecked,
setContentExtractionChecked,
updateSyncSettings,
resetSyncSettings,
} = useActions(SynchronizationLogic({ contentSource }));
const { areThumbnailsConfigEnabled } = contentSource;
const actions = (
<EuiFlexGroup>
<EuiFlexItem>
<EuiButtonEmpty onClick={resetSyncSettings} disabled={!hasUnsavedObjectsAndAssetsChanges}>
{RESET_BUTTON}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton fill onClick={updateSyncSettings} disabled={!hasUnsavedObjectsAndAssetsChanges}>
{SAVE_BUTTON_LABEL}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
return (
<SourceLayout
pageChrome={[NAV.SYNCHRONIZATION_OBJECTS_AND_ASSETS]}
pageViewTelemetry="source_synchronization"
isLoading={false}
isLoading={dataLoading}
>
<UnsavedChangesPrompt
hasUnsavedChanges={hasUnsavedObjectsAndAssetsChanges}
messageText={SYNC_UNSAVED_CHANGES_MESSAGE}
/>
<ViewContentHeader
title={NAV.SYNCHRONIZATION_OBJECTS_AND_ASSETS}
description={SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION}
action={actions}
/>
<EuiLink href={OBJECTS_AND_ASSETS_DOCS_URL} external>
{SYNC_OBJECTS_TYPES_LINK_LABEL}
</EuiLink>
<EuiHorizontalRule />
<div>TODO</div>
<EuiText size="m">{SOURCE_OBJECTS_AND_ASSETS_LABEL}</EuiText>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSwitch
checked={thumbnailsChecked}
onChange={(e) => setThumbnailsChecked(e.target.checked)}
label={
areThumbnailsConfigEnabled
? SYNC_MANAGEMENT_THUMBNAILS_LABEL
: SYNC_MANAGEMENT_THUMBNAILS_GLOBAL_CONFIG_LABEL
}
disabled={!areThumbnailsConfigEnabled}
data-test-subj="ThumbnailsToggle"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSwitch
checked={contentExtractionChecked}
onChange={(e) => setContentExtractionChecked(e.target.checked)}
label={SYNC_MANAGEMENT_CONTENT_EXTRACTION_LABEL}
data-test-subj="ContentExtractionToggle"
/>
</EuiFlexItem>
</EuiFlexGroup>
</SourceLayout>
);
};

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { setMockValues } from '../../../../../__mocks__/kea_logic';
import { setMockValues, setMockActions } from '../../../../../__mocks__/kea_logic';
import { fullContentSources } from '../../../../__mocks__/content_sources.mock';
import React from 'react';
@ -16,8 +17,15 @@ import { EuiLink, EuiCallOut, EuiSwitch } from '@elastic/eui';
import { Synchronization } from './synchronization';
describe('Synchronization', () => {
const updateSyncEnabled = jest.fn();
const mockvalues = { contentSource: fullContentSources[0] };
beforeEach(() => {
setMockActions({ updateSyncEnabled });
setMockValues(mockvalues);
});
it('renders when config enabled', () => {
setMockValues({ contentSource: { isSyncConfigEnabled: true } });
const wrapper = shallow(<Synchronization />);
expect(wrapper.find(EuiLink)).toHaveLength(1);
@ -25,9 +33,16 @@ describe('Synchronization', () => {
});
it('renders when config disabled', () => {
setMockValues({ contentSource: { isSyncConfigEnabled: false } });
setMockValues({ contentSource: { isSyncConfigEnabled: false, indexing: { enabled: true } } });
const wrapper = shallow(<Synchronization />);
expect(wrapper.find(EuiCallOut)).toHaveLength(1);
});
it('handles EuiSwitch change event', () => {
const wrapper = shallow(<Synchronization />);
wrapper.find(EuiSwitch).simulate('change', { target: { checked: true } });
expect(updateSyncEnabled).toHaveBeenCalled();
});
});

View file

@ -7,7 +7,7 @@
import React from 'react';
import { useValues } from 'kea';
import { useActions, useValues } from 'kea';
import { EuiCallOut, EuiLink, EuiPanel, EuiSwitch, EuiSpacer, EuiText } from '@elastic/eui';
@ -25,17 +25,23 @@ import {
import { SourceLogic } from '../../source_logic';
import { SourceLayout } from '../source_layout';
export const Synchronization: React.FC = () => {
const {
contentSource: { isSyncConfigEnabled },
} = useValues(SourceLogic);
import { SynchronizationLogic } from './synchronization_logic';
const onChange = (checked: boolean) => `#TODO: ${checked}`;
export const Synchronization: React.FC = () => {
const { contentSource } = useValues(SourceLogic);
const { updateSyncEnabled } = useActions(SynchronizationLogic({ contentSource }));
const {
isSyncConfigEnabled,
indexing: { enabled },
} = contentSource;
const onChange = (checked: boolean) => updateSyncEnabled(checked);
const syncToggle = (
<EuiPanel hasBorder>
<EuiSwitch
label={SOURCE_SYNCRONIZATION_TOGGLE_LABEL}
checked
checked={enabled}
onChange={(e) => onChange(e.target.checked)}
/>
<EuiSpacer size="m" />

View file

@ -5,14 +5,22 @@
* 2.0.
*/
import { LogicMounter, mockKibanaValues } from '../../../../../__mocks__/kea_logic';
import {
LogicMounter,
mockFlashMessageHelpers,
mockHttpValues,
mockKibanaValues,
} from '../../../../../__mocks__/kea_logic';
import { fullContentSources } from '../../../../__mocks__/content_sources.mock';
import { nextTick } from '@kbn/test/jest';
const contentSource = { id: 'source123' };
import { expectedAsyncError } from '../../../../../test_helpers';
jest.mock('../../source_logic', () => ({
SourceLogic: { values: { contentSource } },
SourceLogic: { actions: { setContentSource: jest.fn() } },
}));
import { SourceLogic } from '../../source_logic';
jest.mock('../../../../app_logic', () => ({
AppLogic: { values: { isOrganization: true } },
@ -21,17 +29,23 @@ jest.mock('../../../../app_logic', () => ({
import { SynchronizationLogic, emptyBlockedWindow } from './synchronization_logic';
describe('SynchronizationLogic', () => {
const { http } = mockHttpValues;
const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers;
const { navigateToUrl } = mockKibanaValues;
const { mount } = new LogicMounter(SynchronizationLogic);
const contentSource = fullContentSources[0];
const defaultValues = {
navigatingBetweenTabs: false,
hasUnsavedObjectsAndAssetsChanges: false,
contentExtractionChecked: true,
thumbnailsChecked: true,
blockedWindows: [],
};
beforeEach(() => {
jest.clearAllMocks();
mount();
mount({}, { contentSource });
});
it('has expected default values', () => {
@ -50,6 +64,18 @@ describe('SynchronizationLogic', () => {
expect(SynchronizationLogic.values.blockedWindows).toEqual([emptyBlockedWindow]);
});
it('setThumbnailsChecked', () => {
SynchronizationLogic.actions.setThumbnailsChecked(false);
expect(SynchronizationLogic.values.thumbnailsChecked).toEqual(false);
});
it('setContentExtractionChecked', () => {
SynchronizationLogic.actions.setContentExtractionChecked(false);
expect(SynchronizationLogic.values.contentExtractionChecked).toEqual(false);
});
});
describe('listeners', () => {
@ -63,7 +89,7 @@ describe('SynchronizationLogic', () => {
await nextTick();
expect(setNavigatingBetweenTabsSpy).toHaveBeenCalledWith(true);
expect(navigateToUrl).toHaveBeenCalledWith('/sources/source123/synchronization/frequency');
expect(navigateToUrl).toHaveBeenCalledWith('/sources/123/synchronization/frequency');
});
it('calls calls correct route for "blocked_time_windows"', async () => {
@ -71,9 +97,126 @@ describe('SynchronizationLogic', () => {
await nextTick();
expect(navigateToUrl).toHaveBeenCalledWith(
'/sources/source123/synchronization/frequency/blocked_windows'
'/sources/123/synchronization/frequency/blocked_windows'
);
});
});
describe('updateSyncEnabled', () => {
it('calls API and sets values for false value', async () => {
const setContentSourceSpy = jest.spyOn(SourceLogic.actions, 'setContentSource');
const promise = Promise.resolve(contentSource);
http.patch.mockReturnValue(promise);
SynchronizationLogic.actions.updateSyncEnabled(false);
expect(http.patch).toHaveBeenCalledWith(
'/internal/workplace_search/org/sources/123/settings',
{
body: JSON.stringify({
content_source: {
indexing: { enabled: false },
},
}),
}
);
await promise;
expect(setContentSourceSpy).toHaveBeenCalledWith(contentSource);
expect(flashSuccessToast).toHaveBeenCalledWith('Source synchronization disabled.');
});
it('calls API and sets values for true value', async () => {
const promise = Promise.resolve(contentSource);
http.patch.mockReturnValue(promise);
SynchronizationLogic.actions.updateSyncEnabled(true);
expect(http.patch).toHaveBeenCalledWith(
'/internal/workplace_search/org/sources/123/settings',
{
body: JSON.stringify({
content_source: {
indexing: { enabled: true },
},
}),
}
);
await promise;
expect(flashSuccessToast).toHaveBeenCalledWith('Source synchronization enabled.');
});
it('handles error', async () => {
const error = {
response: {
error: 'this is an error',
status: 400,
},
};
const promise = Promise.reject(error);
http.patch.mockReturnValue(promise);
SynchronizationLogic.actions.updateSyncEnabled(false);
await expectedAsyncError(promise);
expect(flashAPIErrors).toHaveBeenCalledWith(error);
});
});
describe('resetSyncSettings', () => {
it('calls methods', async () => {
const setThumbnailsCheckedSpy = jest.spyOn(
SynchronizationLogic.actions,
'setThumbnailsChecked'
);
const setContentExtractionCheckedSpy = jest.spyOn(
SynchronizationLogic.actions,
'setContentExtractionChecked'
);
SynchronizationLogic.actions.resetSyncSettings();
expect(setThumbnailsCheckedSpy).toHaveBeenCalledWith(true);
expect(setContentExtractionCheckedSpy).toHaveBeenCalledWith(true);
});
});
describe('updateSyncSettings', () => {
it('calls API and sets values', async () => {
const setContentSourceSpy = jest.spyOn(SourceLogic.actions, 'setContentSource');
const promise = Promise.resolve(contentSource);
http.patch.mockReturnValue(promise);
SynchronizationLogic.actions.updateSyncSettings();
expect(http.patch).toHaveBeenCalledWith(
'/internal/workplace_search/org/sources/123/settings',
{
body: JSON.stringify({
content_source: {
indexing: {
features: {
content_extraction: { enabled: true },
thumbnails: { enabled: true },
},
},
},
}),
}
);
await promise;
expect(setContentSourceSpy).toHaveBeenCalledWith(contentSource);
expect(flashSuccessToast).toHaveBeenCalledWith('Source synchronization settings updated.');
});
it('handles error', async () => {
const error = {
response: {
error: 'this is an error',
status: 400,
},
};
const promise = Promise.reject(error);
http.patch.mockReturnValue(promise);
SynchronizationLogic.actions.updateSyncSettings();
await expectedAsyncError(promise);
expect(flashAPIErrors).toHaveBeenCalledWith(error);
});
});
});
});

View file

@ -10,6 +10,8 @@ import moment from 'moment';
export type TabId = 'source_sync_frequency' | 'blocked_time_windows';
import { flashAPIErrors, flashSuccessToast } from '../../../../../shared/flash_messages';
import { HttpLogic } from '../../../../../shared/http';
import { KibanaLogic } from '../../../../../shared/kibana';
import { AppLogic } from '../../../../app_logic';
import {
@ -19,17 +21,30 @@ import {
} from '../../../../routes';
import { BlockedWindow } from '../../../../types';
import {
SYNC_ENABLED_MESSAGE,
SYNC_DISABLED_MESSAGE,
SYNC_SETTINGS_UPDATED_MESSAGE,
} from '../../constants';
import { SourceLogic } from '../../source_logic';
interface SynchronizationActions {
setNavigatingBetweenTabs(navigatingBetweenTabs: boolean): boolean;
handleSelectedTabChanged(tabId: TabId): TabId;
addBlockedWindow(): void;
updateSyncSettings(): void;
resetSyncSettings(): void;
updateSyncEnabled(enabled: boolean): boolean;
setThumbnailsChecked(checked: boolean): boolean;
setContentExtractionChecked(checked: boolean): boolean;
}
interface SynchronizationValues {
hasUnsavedChanges: boolean;
navigatingBetweenTabs: boolean;
hasUnsavedFrequencyChanges: boolean;
hasUnsavedObjectsAndAssetsChanges: boolean;
thumbnailsChecked: boolean;
contentExtractionChecked: boolean;
blockedWindows: BlockedWindow[];
}
@ -43,12 +58,18 @@ export const emptyBlockedWindow: BlockedWindow = {
export const SynchronizationLogic = kea<
MakeLogicType<SynchronizationValues, SynchronizationActions>
>({
path: ['enterprise_search', 'workplace_search', 'synchronization_logic'],
actions: {
setNavigatingBetweenTabs: (navigatingBetweenTabs: boolean) => navigatingBetweenTabs,
handleSelectedTabChanged: (tabId: TabId) => tabId,
updateSyncEnabled: (enabled: boolean) => enabled,
setThumbnailsChecked: (checked: boolean) => checked,
setContentExtractionChecked: (checked: boolean) => checked,
updateSyncSettings: true,
resetSyncSettings: true,
addBlockedWindow: true,
},
reducers: {
reducers: ({ props }) => ({
navigatingBetweenTabs: [
false,
{
@ -61,11 +82,47 @@ export const SynchronizationLogic = kea<
addBlockedWindow: (state, _) => [...state, emptyBlockedWindow],
},
],
},
listeners: ({ actions }) => ({
thumbnailsChecked: [
props.contentSource.indexing.features.thumbnails.enabled,
{
setThumbnailsChecked: (_, thumbnailsChecked) => thumbnailsChecked,
},
],
contentExtractionChecked: [
props.contentSource.indexing.features.contentExtraction.enabled,
{
setContentExtractionChecked: (_, contentExtractionChecked) => contentExtractionChecked,
},
],
}),
selectors: ({ selectors }) => ({
hasUnsavedObjectsAndAssetsChanges: [
() => [
selectors.thumbnailsChecked,
selectors.contentExtractionChecked,
(_, props) => props.contentSource,
],
(thumbnailsChecked, contentExtractionChecked, contentSource) => {
const {
indexing: {
features: {
thumbnails: { enabled: thumbnailsEnabled },
contentExtraction: { enabled: contentExtractionEnabled },
},
},
} = contentSource;
return (
thumbnailsChecked !== thumbnailsEnabled ||
contentExtractionChecked !== contentExtractionEnabled
);
},
],
}),
listeners: ({ actions, values, props }) => ({
handleSelectedTabChanged: async (tabId, breakpoint) => {
const { isOrganization } = AppLogic.values;
const { id: sourceId } = SourceLogic.values.contentSource;
const { id: sourceId } = props.contentSource;
const path =
tabId === 'source_sync_frequency'
? getContentSourcePath(SYNC_FREQUENCY_PATH, sourceId, isOrganization)
@ -82,5 +139,51 @@ export const SynchronizationLogic = kea<
KibanaLogic.values.navigateToUrl(path);
actions.setNavigatingBetweenTabs(false);
},
updateSyncEnabled: async (enabled) => {
const { id: sourceId } = props.contentSource;
const route = `/internal/workplace_search/org/sources/${sourceId}/settings`;
const successMessage = enabled ? SYNC_ENABLED_MESSAGE : SYNC_DISABLED_MESSAGE;
try {
const response = await HttpLogic.values.http.patch(route, {
body: JSON.stringify({ content_source: { indexing: { enabled } } }),
});
SourceLogic.actions.setContentSource(response);
flashSuccessToast(successMessage);
} catch (e) {
flashAPIErrors(e);
}
},
resetSyncSettings: () => {
actions.setThumbnailsChecked(props.contentSource.indexing.features.thumbnails.enabled);
actions.setContentExtractionChecked(
props.contentSource.indexing.features.contentExtraction.enabled
);
},
updateSyncSettings: async () => {
const { id: sourceId } = props.contentSource;
const route = `/internal/workplace_search/org/sources/${sourceId}/settings`;
try {
const response = await HttpLogic.values.http.patch(route, {
body: JSON.stringify({
content_source: {
indexing: {
features: {
content_extraction: { enabled: values.contentExtractionChecked },
thumbnails: { enabled: values.thumbnailsChecked },
},
},
},
}),
});
SourceLogic.actions.setContentSource(response);
flashSuccessToast(SYNC_SETTINGS_UPDATED_MESSAGE);
} catch (e) {
flashAPIErrors(e);
}
},
}),
});

View file

@ -306,20 +306,6 @@ export const SOURCE_CONFIG_TITLE = i18n.translate(
}
);
export const SYNC_MANAGEMENT_TITLE = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementTitle',
{
defaultMessage: 'Sync management',
}
);
export const SYNC_MANAGEMENT_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementDescription',
{
defaultMessage: 'Enable and disable extraction of specific content for this source.',
}
);
export const SYNC_MANAGEMENT_SYNCHRONIZE_LABEL = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementSynchronizeLabel',
{
@ -344,7 +330,7 @@ export const SYNC_MANAGEMENT_THUMBNAILS_GLOBAL_CONFIG_LABEL = i18n.translate(
export const SYNC_MANAGEMENT_CONTENT_EXTRACTION_LABEL = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementContentExtractionLabel',
{
defaultMessage: 'Sync all text and content',
defaultMessage: 'Sync full-text from files',
}
);
@ -565,6 +551,13 @@ export const SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION = i18n.translate(
}
);
export const SOURCE_OBJECTS_AND_ASSETS_LABEL = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsLabel',
{
defaultMessage: 'Object and details to include in search results',
}
);
export const SOURCE_SYNCRONIZATION_TOGGLE_LABEL = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncronizationToggleLabel',
{
@ -711,3 +704,38 @@ export const BLOCKED_EMPTY_STATE_DESCRIPTION = i18n.translate(
defaultMessage: 'Add a blocked time window to only perform syncs at the right time.',
}
);
export const SYNC_ENABLED_MESSAGE = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.sources.syncEnabledMessage',
{
defaultMessage: 'Source synchronization enabled.',
}
);
export const SYNC_DISABLED_MESSAGE = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.sources.syncDisabledMessage',
{
defaultMessage: 'Source synchronization disabled.',
}
);
export const SYNC_SETTINGS_UPDATED_MESSAGE = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.sources.syncSettingsUpdatedMessage',
{
defaultMessage: 'Source synchronization settings updated.',
}
);
export const SYNC_UNSAVED_CHANGES_MESSAGE = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.sources.syncUnsavedChangesMessage',
{
defaultMessage: 'Your changes have not been saved. Are you sure you want to leave?',
}
);
export const NEXT_SYNC_RUNNING_MESSAGE = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.sources.nextSyncRunningMessage',
{
defaultMessage: 'as soon as the currently running job finishes',
}
);

View file

@ -58,8 +58,8 @@ describe('SourceLogic', () => {
});
describe('actions', () => {
it('onInitializeSource', () => {
SourceLogic.actions.onInitializeSource(contentSource);
it('setContentSource', () => {
SourceLogic.actions.setContentSource(contentSource);
expect(SourceLogic.values.contentSource).toEqual(contentSource);
expect(SourceLogic.values.dataLoading).toEqual(false);
@ -67,7 +67,7 @@ describe('SourceLogic', () => {
it('onUpdateSourceName', () => {
const NAME = 'foo';
SourceLogic.actions.onInitializeSource(contentSource);
SourceLogic.actions.setContentSource(contentSource);
SourceLogic.actions.onUpdateSourceName(NAME);
expect(SourceLogic.values.contentSource).toEqual({
@ -88,7 +88,7 @@ describe('SourceLogic', () => {
it('setContentFilterValue', () => {
const VALUE = 'bar';
SourceLogic.actions.setSearchResults(searchServerResponse);
SourceLogic.actions.onInitializeSource(contentSource);
SourceLogic.actions.setContentSource(contentSource);
SourceLogic.actions.setContentFilterValue(VALUE);
expect(SourceLogic.values.contentMeta).toEqual({
@ -127,7 +127,7 @@ describe('SourceLogic', () => {
describe('listeners', () => {
describe('initializeSource', () => {
it('calls API and sets values (org)', async () => {
const onInitializeSourceSpy = jest.spyOn(SourceLogic.actions, 'onInitializeSource');
const onInitializeSourceSpy = jest.spyOn(SourceLogic.actions, 'setContentSource');
const promise = Promise.resolve(contentSource);
http.get.mockReturnValue(promise);
SourceLogic.actions.initializeSource(contentSource.id);
@ -140,7 +140,7 @@ describe('SourceLogic', () => {
it('calls API and sets values (account)', async () => {
AppLogic.values.isOrganization = false;
const onInitializeSourceSpy = jest.spyOn(SourceLogic.actions, 'onInitializeSource');
const onInitializeSourceSpy = jest.spyOn(SourceLogic.actions, 'setContentSource');
const promise = Promise.resolve(contentSource);
http.get.mockReturnValue(promise);
SourceLogic.actions.initializeSource(contentSource.id);

View file

@ -23,7 +23,7 @@ import { PRIVATE_SOURCES_PATH, SOURCES_PATH, getSourcesPath } from '../../routes
import { ContentSourceFullData, Meta, DocumentSummaryItem, SourceContentItem } from '../../types';
export interface SourceActions {
onInitializeSource(contentSource: ContentSourceFullData): ContentSourceFullData;
setContentSource(contentSource: ContentSourceFullData): ContentSourceFullData;
onUpdateSourceName(name: string): string;
setSearchResults(searchResultsResponse: SearchResultsResponse): SearchResultsResponse;
initializeFederatedSummary(sourceId: string): { sourceId: string };
@ -73,7 +73,7 @@ interface SourceUpdatePayload {
export const SourceLogic = kea<MakeLogicType<SourceValues, SourceActions>>({
path: ['enterprise_search', 'workplace_search', 'source_logic'],
actions: {
onInitializeSource: (contentSource: ContentSourceFullData) => contentSource,
setContentSource: (contentSource: ContentSourceFullData) => contentSource,
onUpdateSourceName: (name: string) => name,
onUpdateSummary: (summary: object[]) => summary,
setSearchResults: (searchResultsResponse: SearchResultsResponse) => searchResultsResponse,
@ -93,7 +93,7 @@ export const SourceLogic = kea<MakeLogicType<SourceValues, SourceActions>>({
contentSource: [
{} as ContentSourceFullData,
{
onInitializeSource: (_, contentSource) => contentSource,
setContentSource: (_, contentSource) => contentSource,
onUpdateSourceName: (contentSource, name) => ({
...contentSource,
name,
@ -108,7 +108,7 @@ export const SourceLogic = kea<MakeLogicType<SourceValues, SourceActions>>({
dataLoading: [
true,
{
onInitializeSource: () => false,
setContentSource: () => false,
resetSourceState: () => true,
},
],
@ -158,7 +158,7 @@ export const SourceLogic = kea<MakeLogicType<SourceValues, SourceActions>>({
try {
const response = await HttpLogic.values.http.get(route);
actions.onInitializeSource(response);
actions.setContentSource(response);
if (response.isFederatedSource) {
actions.initializeFederatedSummary(sourceId);
}

View file

@ -10121,11 +10121,9 @@
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.unsaved.message": "表示設定は保存されていません。終了してよろしいですか?",
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.visibleFields.title": "表示フィールド",
"xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementContentExtractionLabel": "すべてのテキストとコンテンツを同期",
"xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementDescription": "このソースの特定のコンテンツの抽出を有効および無効にします。",
"xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementGlobalConfigLabel": "サムネイルを同期 - グローバル構成レベルでは無効",
"xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementSynchronizeLabel": "このソースを同期",
"xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementThumbnailsLabel": "サムネイルを同期",
"xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementTitle": "同期管理",
"xpack.enterpriseSearch.workplaceSearch.copyText": "コピー",
"xpack.enterpriseSearch.workplaceSearch.credentials.description": "クライアントで次の資格情報を使用して、認証サーバーからアクセストークンを要求します。",
"xpack.enterpriseSearch.workplaceSearch.credentials.title": "資格情報",

View file

@ -10223,11 +10223,9 @@
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.unsaved.message": "您的显示设置尚未保存。是否确定要离开?",
"xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.visibleFields.title": "可见的字段",
"xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementContentExtractionLabel": "同步所有文本和内容",
"xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementDescription": "为此源启用和禁用特定内容的提取。",
"xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementGlobalConfigLabel": "同步缩略图 - 已在全局配置级别禁用",
"xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementSynchronizeLabel": "同步此源",
"xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementThumbnailsLabel": "同步缩略图",
"xpack.enterpriseSearch.workplaceSearch.contentSources.syncManagementTitle": "同步管理",
"xpack.enterpriseSearch.workplaceSearch.copyText": "复制",
"xpack.enterpriseSearch.workplaceSearch.credentials.description": "在您的客户端中使用以下凭据从我们的身份验证服务器请求访问令牌。",
"xpack.enterpriseSearch.workplaceSearch.credentials.title": "凭据",