mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Workplace Search] Add indexing rules table (#124353)
[Workplace Search] Add indexing rules table
This commit is contained in:
parent
335d9f376a
commit
995c177629
22 changed files with 1312 additions and 106 deletions
|
@ -27,6 +27,7 @@ import './inline_editable_tables.scss';
|
|||
export interface InlineEditableTableProps<Item extends ItemWithAnID> {
|
||||
columns: Array<InlineEditableTableColumn<Item>>;
|
||||
items: Item[];
|
||||
defaultItem?: Partial<Item>;
|
||||
title: string;
|
||||
addButtonText?: string;
|
||||
canRemoveLastItem?: boolean;
|
||||
|
@ -53,6 +54,7 @@ export const InlineEditableTable = <Item extends ItemWithAnID>(
|
|||
const {
|
||||
instanceId,
|
||||
columns,
|
||||
defaultItem,
|
||||
onAdd,
|
||||
onDelete,
|
||||
onReorder,
|
||||
|
@ -67,6 +69,7 @@ export const InlineEditableTable = <Item extends ItemWithAnID>(
|
|||
props={{
|
||||
instanceId,
|
||||
columns,
|
||||
defaultItem,
|
||||
onAdd,
|
||||
onDelete,
|
||||
onReorder,
|
||||
|
@ -90,6 +93,7 @@ export const InlineEditableTableContents = <Item extends ItemWithAnID>({
|
|||
description,
|
||||
isLoading,
|
||||
lastItemWarning,
|
||||
defaultItem,
|
||||
noItemsMessage = () => null,
|
||||
uneditableItems,
|
||||
...rest
|
||||
|
|
|
@ -48,6 +48,7 @@ interface InlineEditableTableValues<Item extends ItemWithAnID> {
|
|||
export interface InlineEditableTableProps<Item extends ItemWithAnID> {
|
||||
columns: Array<InlineEditableTableColumn<Item>>;
|
||||
instanceId: string;
|
||||
defaultItem: Item;
|
||||
// TODO Because these callbacks are params, they are only set on the logic once (i.e., they are cached)
|
||||
// which makes using "useState" to back this really hard.
|
||||
onAdd(item: Item, onSuccess: () => void): void;
|
||||
|
@ -79,12 +80,15 @@ export const InlineEditableTableLogic = kea<InlineEditableTableLogicType<ItemWit
|
|||
setFieldErrors: (fieldErrors) => ({ fieldErrors }),
|
||||
setRowErrors: (rowErrors) => ({ rowErrors }),
|
||||
}),
|
||||
reducers: ({ props: { columns } }) => ({
|
||||
reducers: ({ props: { columns, defaultItem } }) => ({
|
||||
editingItemValue: [
|
||||
null,
|
||||
{
|
||||
doneEditing: () => null,
|
||||
editNewItem: () => generateEmptyItem(columns),
|
||||
editNewItem: () =>
|
||||
defaultItem
|
||||
? { ...generateEmptyItem(columns), ...defaultItem }
|
||||
: generateEmptyItem(columns),
|
||||
editExistingItem: (_, { item }) => item,
|
||||
setEditingItemValue: (_, { item }) => item,
|
||||
},
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { groups } from './groups.mock';
|
||||
|
||||
import { IndexingRule } from '../types';
|
||||
import { staticSourceData } from '../views/content_sources/source_data';
|
||||
import { mergeServerAndStaticData } from '../views/content_sources/sources_logic';
|
||||
|
||||
|
@ -45,10 +46,25 @@ export const contentSources = [
|
|||
},
|
||||
];
|
||||
|
||||
const defaultIndexingRules: IndexingRule[] = [
|
||||
{
|
||||
filterType: 'object_type',
|
||||
include: 'value',
|
||||
},
|
||||
{
|
||||
filterType: 'path_template',
|
||||
exclude: 'value',
|
||||
},
|
||||
{
|
||||
filterType: 'file_extension',
|
||||
include: 'value',
|
||||
},
|
||||
];
|
||||
|
||||
const defaultIndexing = {
|
||||
enabled: true,
|
||||
defaultAction: 'include',
|
||||
rules: [],
|
||||
rules: defaultIndexingRules,
|
||||
schedule: {
|
||||
full: 'P1D',
|
||||
incremental: 'PT2H',
|
||||
|
|
|
@ -60,10 +60,10 @@ export const NAV = {
|
|||
defaultMessage: 'Frequency',
|
||||
}
|
||||
),
|
||||
SYNCHRONIZATION_OBJECTS_AND_ASSETS: i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.nav.synchronizationObjectsAndAssets',
|
||||
SYNCHRONIZATION_ASSETS_AND_OBJECTS: i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.nav.synchronizationAssetsAndObjects',
|
||||
{
|
||||
defaultMessage: 'Objects and assets',
|
||||
defaultMessage: 'Assets and objects',
|
||||
}
|
||||
),
|
||||
DISPLAY_SETTINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.displaySettings', {
|
||||
|
|
|
@ -76,7 +76,8 @@ export const DISPLAY_SETTINGS_RESULT_DETAIL_PATH = `${SOURCE_DISPLAY_SETTINGS_PA
|
|||
|
||||
export const SYNC_FREQUENCY_PATH = `${SOURCE_SYNCHRONIZATION_PATH}/frequency`;
|
||||
export const BLOCKED_TIME_WINDOWS_PATH = `${SOURCE_SYNCHRONIZATION_PATH}/frequency/blocked_windows`;
|
||||
export const OBJECTS_AND_ASSETS_PATH = `${SOURCE_SYNCHRONIZATION_PATH}/objects_and_assets`;
|
||||
export const OLD_OBJECTS_AND_ASSETS_PATH = `${SOURCE_SYNCHRONIZATION_PATH}/objects_and_assets`;
|
||||
export const ASSETS_AND_OBJECTS_PATH = `${SOURCE_SYNCHRONIZATION_PATH}/assets_and_objects`;
|
||||
|
||||
export const ORG_SETTINGS_PATH = '/settings';
|
||||
export const ORG_SETTINGS_CUSTOMIZE_PATH = `${ORG_SETTINGS_PATH}/customize`;
|
||||
|
|
|
@ -168,6 +168,18 @@ export interface BlockedWindow {
|
|||
end: string;
|
||||
}
|
||||
|
||||
export interface IndexingRuleExclude {
|
||||
filterType: 'object_type' | 'path_template' | 'file_extension';
|
||||
exclude: string;
|
||||
}
|
||||
|
||||
export interface IndexingRuleInclude {
|
||||
filterType: 'object_type' | 'path_template' | 'file_extension';
|
||||
include: string;
|
||||
}
|
||||
|
||||
export type IndexingRule = IndexingRuleInclude | IndexingRuleExclude;
|
||||
|
||||
export interface IndexingConfig {
|
||||
enabled: boolean;
|
||||
features: {
|
||||
|
@ -178,6 +190,7 @@ export interface IndexingConfig {
|
|||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
rules: IndexingRule[];
|
||||
schedule: IndexingSchedule;
|
||||
}
|
||||
|
||||
|
|
|
@ -119,9 +119,9 @@ describe('useSourceSubNav', () => {
|
|||
href: '/sources/2/synchronization/frequency',
|
||||
},
|
||||
{
|
||||
id: 'sourceSynchronizationObjectsAndAssets',
|
||||
name: 'Objects and assets',
|
||||
href: '/sources/2/synchronization/objects_and_assets',
|
||||
id: 'sourceSynchronizationAssetsAndObjects',
|
||||
name: 'Assets and objects',
|
||||
href: '/sources/2/synchronization/assets_and_objects',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -16,19 +16,19 @@ import { shallow } from 'enzyme';
|
|||
|
||||
import { EuiSwitch } from '@elastic/eui';
|
||||
|
||||
import { ObjectsAndAssets } from './objects_and_assets';
|
||||
import { AssetsAndObjects } from './assets_and_objects';
|
||||
|
||||
describe('ObjectsAndAssets', () => {
|
||||
describe('AssetsAndObjects', () => {
|
||||
const setThumbnailsChecked = jest.fn();
|
||||
const setContentExtractionChecked = jest.fn();
|
||||
const updateObjectsAndAssetsSettings = jest.fn();
|
||||
const updateAssetsAndObjectsSettings = jest.fn();
|
||||
const resetSyncSettings = jest.fn();
|
||||
const contentSource = fullContentSources[0];
|
||||
|
||||
const mockActions = {
|
||||
setThumbnailsChecked,
|
||||
setContentExtractionChecked,
|
||||
updateObjectsAndAssetsSettings,
|
||||
updateAssetsAndObjectsSettings,
|
||||
resetSyncSettings,
|
||||
};
|
||||
const mockValues = {
|
||||
|
@ -37,7 +37,7 @@ describe('ObjectsAndAssets', () => {
|
|||
contentSource,
|
||||
thumbnailsChecked: true,
|
||||
contentExtractionChecked: true,
|
||||
hasUnsavedObjectsAndAssetsChanges: false,
|
||||
hasUnsavedAssetsAndObjectsChanges: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -46,13 +46,13 @@ describe('ObjectsAndAssets', () => {
|
|||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<ObjectsAndAssets />);
|
||||
const wrapper = shallow(<AssetsAndObjects />);
|
||||
|
||||
expect(wrapper.find(EuiSwitch)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('handles thumbnails switch change', () => {
|
||||
const wrapper = shallow(<ObjectsAndAssets />);
|
||||
const wrapper = shallow(<AssetsAndObjects />);
|
||||
wrapper
|
||||
.find('[data-test-subj="ThumbnailsToggle"]')
|
||||
.simulate('change', { target: { checked: false } });
|
||||
|
@ -61,7 +61,7 @@ describe('ObjectsAndAssets', () => {
|
|||
});
|
||||
|
||||
it('handles content extraction switch change', () => {
|
||||
const wrapper = shallow(<ObjectsAndAssets />);
|
||||
const wrapper = shallow(<AssetsAndObjects />);
|
||||
wrapper
|
||||
.find('[data-test-subj="ContentExtractionToggle"]')
|
||||
.simulate('change', { target: { checked: false } });
|
||||
|
@ -77,7 +77,7 @@ describe('ObjectsAndAssets', () => {
|
|||
areThumbnailsConfigEnabled: false,
|
||||
},
|
||||
});
|
||||
const wrapper = shallow(<ObjectsAndAssets />);
|
||||
const wrapper = shallow(<AssetsAndObjects />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="ThumbnailsToggle"]').prop('label')).toEqual(
|
||||
'Sync thumbnails - disabled at global configuration level'
|
|
@ -18,7 +18,7 @@ import {
|
|||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { SAVE_BUTTON_LABEL } from '../../../../../shared/constants';
|
||||
|
@ -27,27 +27,29 @@ import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prom
|
|||
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
|
||||
import { NAV, RESET_BUTTON } from '../../../../constants';
|
||||
import {
|
||||
LEARN_MORE_LINK,
|
||||
SYNC_MANAGEMENT_CONTENT_EXTRACTION_LABEL,
|
||||
SYNC_MANAGEMENT_THUMBNAILS_LABEL,
|
||||
SYNC_MANAGEMENT_THUMBNAILS_GLOBAL_CONFIG_LABEL,
|
||||
SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION,
|
||||
SOURCE_OBJECTS_AND_ASSETS_LABEL,
|
||||
SOURCE_ASSETS_AND_OBJECTS_DESCRIPTION,
|
||||
SOURCE_ASSETS_AND_OBJECTS_ASSETS_LABEL,
|
||||
SYNC_UNSAVED_CHANGES_MESSAGE,
|
||||
SOURCE_ASSETS_AND_OBJECTS_LEARN_MORE_LINK,
|
||||
SOURCE_ASSETS_AND_OBJECTS_OBJECTS_LABEL,
|
||||
} from '../../constants';
|
||||
import { SourceLogic } from '../../source_logic';
|
||||
import { SourceLayout } from '../source_layout';
|
||||
|
||||
import { IndexingRulesTable } from './indexing_rules_table';
|
||||
import { SynchronizationLogic } from './synchronization_logic';
|
||||
|
||||
export const ObjectsAndAssets: React.FC = () => {
|
||||
export const AssetsAndObjects: React.FC = () => {
|
||||
const { contentSource, dataLoading } = useValues(SourceLogic);
|
||||
const { thumbnailsChecked, contentExtractionChecked, hasUnsavedObjectsAndAssetsChanges } =
|
||||
const { thumbnailsChecked, contentExtractionChecked, hasUnsavedAssetsAndObjectsChanges } =
|
||||
useValues(SynchronizationLogic({ contentSource }));
|
||||
const {
|
||||
setThumbnailsChecked,
|
||||
setContentExtractionChecked,
|
||||
updateObjectsAndAssetsSettings,
|
||||
updateAssetsAndObjectsSettings,
|
||||
resetSyncSettings,
|
||||
} = useActions(SynchronizationLogic({ contentSource }));
|
||||
|
||||
|
@ -55,47 +57,43 @@ export const ObjectsAndAssets: React.FC = () => {
|
|||
|
||||
const actions = (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty onClick={resetSyncSettings} disabled={!hasUnsavedObjectsAndAssetsChanges}>
|
||||
{RESET_BUTTON}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={updateObjectsAndAssetsSettings}
|
||||
disabled={!hasUnsavedObjectsAndAssetsChanges}
|
||||
onClick={updateAssetsAndObjectsSettings}
|
||||
disabled={!hasUnsavedAssetsAndObjectsChanges}
|
||||
>
|
||||
{SAVE_BUTTON_LABEL}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty onClick={resetSyncSettings} disabled={!hasUnsavedAssetsAndObjectsChanges}>
|
||||
{RESET_BUTTON}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
return (
|
||||
<SourceLayout
|
||||
pageChrome={[NAV.SYNCHRONIZATION_OBJECTS_AND_ASSETS]}
|
||||
pageChrome={[NAV.SYNCHRONIZATION_ASSETS_AND_OBJECTS]}
|
||||
pageViewTelemetry="source_synchronization"
|
||||
isLoading={dataLoading}
|
||||
>
|
||||
<UnsavedChangesPrompt
|
||||
hasUnsavedChanges={hasUnsavedObjectsAndAssetsChanges}
|
||||
hasUnsavedChanges={hasUnsavedAssetsAndObjectsChanges}
|
||||
messageText={SYNC_UNSAVED_CHANGES_MESSAGE}
|
||||
/>
|
||||
<ViewContentHeader
|
||||
title={NAV.SYNCHRONIZATION_OBJECTS_AND_ASSETS}
|
||||
description={
|
||||
<>
|
||||
{SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION}{' '}
|
||||
<EuiLink href={docLinks.workplaceSearchSynch} external>
|
||||
{LEARN_MORE_LINK}
|
||||
</EuiLink>
|
||||
</>
|
||||
}
|
||||
action={actions}
|
||||
/>
|
||||
<ViewContentHeader title={NAV.SYNCHRONIZATION_ASSETS_AND_OBJECTS} action={actions} />
|
||||
{SOURCE_ASSETS_AND_OBJECTS_DESCRIPTION}
|
||||
<EuiSpacer />
|
||||
<EuiLink href={docLinks.workplaceSearchSynch} external>
|
||||
{SOURCE_ASSETS_AND_OBJECTS_LEARN_MORE_LINK}
|
||||
</EuiLink>
|
||||
<EuiHorizontalRule />
|
||||
<EuiText size="m">{SOURCE_OBJECTS_AND_ASSETS_LABEL}</EuiText>
|
||||
<EuiTitle size="s">
|
||||
<h3>{SOURCE_ASSETS_AND_OBJECTS_ASSETS_LABEL}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -122,6 +120,15 @@ export const ObjectsAndAssets: React.FC = () => {
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule />
|
||||
<EuiTitle size="s">
|
||||
<h3>{SOURCE_ASSETS_AND_OBJECTS_OBJECTS_LABEL}</h3>
|
||||
</EuiTitle>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<IndexingRulesTable />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</SourceLayout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
LogicMounter,
|
||||
mockFlashMessageHelpers,
|
||||
setMockActions,
|
||||
setMockValues,
|
||||
} from '../../../../../__mocks__/kea_logic';
|
||||
import { fullContentSources } from '../../../../__mocks__/content_sources.mock';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
|
||||
import { EuiFieldText, EuiSelect } from '@elastic/eui';
|
||||
|
||||
import { InlineEditableTable } from '../../../../../shared/tables/inline_editable_table';
|
||||
|
||||
import { SourceLogic } from '../../source_logic';
|
||||
|
||||
import { IndexingRulesTable } from './indexing_rules_table';
|
||||
import { SynchronizationLogic } from './synchronization_logic';
|
||||
|
||||
describe('IndexingRulesTable', () => {
|
||||
const { clearFlashMessages } = mockFlashMessageHelpers;
|
||||
const { mount: sourceMount } = new LogicMounter(SourceLogic);
|
||||
const { mount: syncMount } = new LogicMounter(SynchronizationLogic);
|
||||
|
||||
const indexingRules = [
|
||||
{ id: 0, valueType: 'exclude', filterType: 'path_template', value: 'value' },
|
||||
{ id: 1, valueType: 'include', filterType: 'file_extension', value: 'value' },
|
||||
{ id: 2, valueType: 'include', filterType: 'object_type', value: 'value 2' },
|
||||
{ id: 3, valueType: 'broken', filterType: 'not allowed', value: 'value 2' },
|
||||
];
|
||||
const contentSource = fullContentSources[0];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
sourceMount({}, {});
|
||||
setMockValues({ contentSource });
|
||||
syncMount({}, { contentSource });
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<IndexingRulesTable />);
|
||||
|
||||
expect(wrapper.find(InlineEditableTable).exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('columns', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<IndexingRulesTable />);
|
||||
});
|
||||
|
||||
const renderColumn = (index: number, ruleIndex: number) => {
|
||||
const columns = wrapper.find(InlineEditableTable).prop('columns');
|
||||
return shallow(<div>{columns[index].render(indexingRules[ruleIndex])}</div>);
|
||||
};
|
||||
|
||||
const onChange = jest.fn();
|
||||
const renderColumnInEditingMode = (index: number, ruleIndex: number) => {
|
||||
const columns = wrapper.find(InlineEditableTable).prop('columns');
|
||||
return shallow(
|
||||
<div>
|
||||
{columns[index].editingRender(indexingRules[ruleIndex], onChange, {
|
||||
isInvalid: false,
|
||||
isLoading: false,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
describe(' column', () => {
|
||||
it('shows the value type of an indexing rule', () => {
|
||||
expect(renderColumn(0, 0).html()).toContain('Exclude');
|
||||
expect(renderColumn(0, 1).html()).toContain('Include');
|
||||
expect(renderColumn(0, 3).html()).toContain('');
|
||||
});
|
||||
|
||||
it('can show the value type of an indexing rule as editable', () => {
|
||||
const column = renderColumnInEditingMode(0, 0);
|
||||
|
||||
const selectField = column.find(EuiSelect);
|
||||
expect(selectField.props()).toEqual(
|
||||
expect.objectContaining({
|
||||
value: 'exclude',
|
||||
disabled: false,
|
||||
isInvalid: false,
|
||||
options: [
|
||||
{ text: 'Include', value: 'include' },
|
||||
{ text: 'Exclude', value: 'exclude' },
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
selectField.simulate('change', { target: { value: 'include' } });
|
||||
expect(onChange).toHaveBeenCalledWith('include');
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter type column', () => {
|
||||
it('shows the filter type of an indexing rule', () => {
|
||||
expect(renderColumn(1, 0).html()).toContain('Path');
|
||||
expect(renderColumn(1, 1).html()).toContain('File');
|
||||
expect(renderColumn(1, 2).html()).toContain('Item');
|
||||
expect(renderColumn(1, 3).html()).toContain('');
|
||||
});
|
||||
|
||||
it('can show the filter type of an indexing rule as editable', () => {
|
||||
const column = renderColumnInEditingMode(1, 0);
|
||||
|
||||
const selectField = column.find(EuiSelect);
|
||||
expect(selectField.props()).toEqual(
|
||||
expect.objectContaining({
|
||||
value: 'path_template',
|
||||
disabled: false,
|
||||
isInvalid: false,
|
||||
options: [
|
||||
{ text: 'Item', value: 'object_type' },
|
||||
{ text: 'Path', value: 'path_template' },
|
||||
{ text: 'File type', value: 'file_extension' },
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
selectField.simulate('change', { target: { value: 'object_type' } });
|
||||
expect(onChange).toHaveBeenCalledWith('object_type');
|
||||
});
|
||||
});
|
||||
|
||||
describe('pattern column', () => {
|
||||
it('shows the value of an indexing rule', () => {
|
||||
expect(renderColumn(2, 0).html()).toContain('value');
|
||||
});
|
||||
|
||||
it('can show the value of a indexing rule as editable', () => {
|
||||
const column = renderColumnInEditingMode(2, 0);
|
||||
|
||||
const field = column.find(EuiFieldText);
|
||||
expect(field.props()).toEqual(
|
||||
expect.objectContaining({
|
||||
value: 'value',
|
||||
disabled: false,
|
||||
isInvalid: false,
|
||||
})
|
||||
);
|
||||
|
||||
field.simulate('change', { target: { value: 'foo' } });
|
||||
expect(onChange).toHaveBeenCalledWith('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when an indexing rule is added', () => {
|
||||
it('should update the indexing rules for the current domain, and clear flash messages', () => {
|
||||
const initAddIndexingRule = jest.fn();
|
||||
const done = jest.fn();
|
||||
setMockActions({
|
||||
initAddIndexingRule,
|
||||
});
|
||||
const wrapper = shallow(<IndexingRulesTable />);
|
||||
const table = wrapper.find(InlineEditableTable);
|
||||
|
||||
const newIndexingRule = {
|
||||
id: 2,
|
||||
value: 'new value',
|
||||
filterType: 'path_template',
|
||||
valueType: 'include',
|
||||
};
|
||||
table.prop('onAdd')(newIndexingRule, done);
|
||||
expect(initAddIndexingRule).toHaveBeenCalledWith(newIndexingRule);
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when an indexing rule is updated', () => {
|
||||
it('should update the indexing rules for the current domain, and clear flash messages', () => {
|
||||
const initSetIndexingRule = jest.fn();
|
||||
const done = jest.fn();
|
||||
setMockActions({
|
||||
initSetIndexingRule,
|
||||
});
|
||||
const wrapper = shallow(<IndexingRulesTable />);
|
||||
const table = wrapper.find(InlineEditableTable);
|
||||
|
||||
const newIndexingRule = {
|
||||
id: 2,
|
||||
value: 'new value',
|
||||
filterType: 'path_template',
|
||||
valueType: 'include',
|
||||
};
|
||||
table.prop('onUpdate')(newIndexingRule, done);
|
||||
expect(initSetIndexingRule).toHaveBeenCalledWith(newIndexingRule);
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a indexing rule is deleted', () => {
|
||||
it('should update the indexing rules for the current domain, and clear flash messages', () => {
|
||||
const deleteIndexingRule = jest.fn();
|
||||
const done = jest.fn();
|
||||
setMockActions({
|
||||
deleteIndexingRule,
|
||||
});
|
||||
const wrapper = shallow(<IndexingRulesTable />);
|
||||
const table = wrapper.find(InlineEditableTable);
|
||||
|
||||
const newIndexingRule = {
|
||||
id: 2,
|
||||
value: 'new value',
|
||||
filterType: 'path_template',
|
||||
valueType: 'include',
|
||||
};
|
||||
table.prop('onDelete')(newIndexingRule, done);
|
||||
expect(deleteIndexingRule).toHaveBeenCalledWith(newIndexingRule);
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when an indexing rule is reordered', () => {
|
||||
it('should update the indexing rules for the current domain, and clear flash messages', () => {
|
||||
const setIndexingRules = jest.fn();
|
||||
const done = jest.fn();
|
||||
setMockActions({
|
||||
setIndexingRules,
|
||||
});
|
||||
const wrapper = shallow(<IndexingRulesTable />);
|
||||
const table = wrapper.find(InlineEditableTable);
|
||||
|
||||
const newIndexingRules = [
|
||||
{
|
||||
id: 2,
|
||||
value: 'new value',
|
||||
filterType: 'path_template',
|
||||
valueType: 'include',
|
||||
},
|
||||
];
|
||||
table.prop('onReorder')!(newIndexingRules, indexingRules, done);
|
||||
expect(setIndexingRules).toHaveBeenCalledWith(newIndexingRules);
|
||||
expect(clearFlashMessages).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { docLinks } from '../../../../../shared/doc_links';
|
||||
import { clearFlashMessages } from '../../../../../shared/flash_messages';
|
||||
import { InlineEditableTable } from '../../../../../shared/tables/inline_editable_table/inline_editable_table';
|
||||
import { InlineEditableTableColumn } from '../../../../../shared/tables/inline_editable_table/types';
|
||||
|
||||
import { SourceLogic } from '../../source_logic';
|
||||
|
||||
import { EditableIndexingRule, SynchronizationLogic } from './synchronization_logic';
|
||||
|
||||
const SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_POLICY_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTablePolicyLabel',
|
||||
{
|
||||
defaultMessage: 'Policy',
|
||||
}
|
||||
);
|
||||
|
||||
const SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_PATH_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTablePathLabel',
|
||||
{
|
||||
defaultMessage: 'Path',
|
||||
}
|
||||
);
|
||||
|
||||
export const SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_ITEM_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTableItemLabel',
|
||||
{
|
||||
defaultMessage: 'Item',
|
||||
}
|
||||
);
|
||||
|
||||
export const SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_FILE_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTableFileLabel',
|
||||
{
|
||||
defaultMessage: 'File type',
|
||||
}
|
||||
);
|
||||
|
||||
export const SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_INCLUDE_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTableIncludeLabel',
|
||||
{
|
||||
defaultMessage: 'Include',
|
||||
}
|
||||
);
|
||||
|
||||
export const SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_EXCLUDE_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTableExcludeLabel',
|
||||
{
|
||||
defaultMessage: 'Exclude',
|
||||
}
|
||||
);
|
||||
|
||||
export const IndexingRulesTable: React.FC = () => {
|
||||
const { contentSource } = useValues(SourceLogic);
|
||||
const indexingRulesInstanceId = 'IndexingRulesTable';
|
||||
const { indexingRules } = useValues(
|
||||
SynchronizationLogic({ contentSource, indexingRulesInstanceId })
|
||||
);
|
||||
const { initAddIndexingRule, deleteIndexingRule, initSetIndexingRule, setIndexingRules } =
|
||||
useActions(SynchronizationLogic({ contentSource }));
|
||||
|
||||
const description = (
|
||||
<EuiText size="s" color="default">
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Include or exclude high level items, file types and (file or folder) paths to synchronize from {contentSourceName}. Everything is included by default. Each document is tested against the rules below and the first rule that matches will be applied.',
|
||||
values: { contentSourceName: contentSource.name },
|
||||
}
|
||||
)}
|
||||
<EuiSpacer />
|
||||
<EuiLink href={docLinks.workplaceSearchSynch} external>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsSyncLearnMoreLink',
|
||||
{
|
||||
defaultMessage: 'Learn more about sync rules.',
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
);
|
||||
|
||||
const valueTypeToString = (input: string): string => {
|
||||
switch (input) {
|
||||
case 'include':
|
||||
return SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_INCLUDE_LABEL;
|
||||
case 'exclude':
|
||||
return SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_EXCLUDE_LABEL;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const filterTypeToString = (input: string): string => {
|
||||
switch (input) {
|
||||
case 'object_type':
|
||||
return SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_ITEM_LABEL;
|
||||
case 'path_template':
|
||||
return SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_PATH_LABEL;
|
||||
case 'file_extension':
|
||||
return SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_FILE_LABEL;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const columns: Array<InlineEditableTableColumn<EditableIndexingRule>> = [
|
||||
{
|
||||
editingRender: (indexingRule, onChange, { isInvalid, isLoading }) => (
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
value={indexingRule.valueType}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
disabled={isLoading}
|
||||
isInvalid={isInvalid}
|
||||
options={[
|
||||
{ text: SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_INCLUDE_LABEL, value: 'include' },
|
||||
{ text: SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_EXCLUDE_LABEL, value: 'exclude' },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
render: (indexingRule) => (
|
||||
<EuiText size="s">{valueTypeToString(indexingRule.valueType)}</EuiText>
|
||||
),
|
||||
name: SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_POLICY_LABEL,
|
||||
field: 'valueType',
|
||||
},
|
||||
{
|
||||
editingRender: (indexingRule, onChange, { isInvalid, isLoading }) => (
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
value={indexingRule.filterType}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
disabled={isLoading}
|
||||
isInvalid={isInvalid}
|
||||
options={[
|
||||
{ text: SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_ITEM_LABEL, value: 'object_type' },
|
||||
{ text: SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_PATH_LABEL, value: 'path_template' },
|
||||
{ text: SOURCE_ASSETS_AND_OBJECTS_OBJECTS_TABLE_FILE_LABEL, value: 'file_extension' },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
render: (indexingRule) => (
|
||||
<EuiText size="s">{filterTypeToString(indexingRule.filterType)}</EuiText>
|
||||
),
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTableRuleLabel',
|
||||
{
|
||||
defaultMessage: 'Rule',
|
||||
}
|
||||
),
|
||||
field: 'filterType',
|
||||
},
|
||||
{
|
||||
editingRender: (indexingRule, onChange, { isInvalid, isLoading }) => (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
value={indexingRule.value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
disabled={isLoading}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
render: (indexingRule) => <EuiText size="s">{indexingRule.value}</EuiText>,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsTableValueLabel',
|
||||
{
|
||||
defaultMessage: 'Value',
|
||||
}
|
||||
),
|
||||
field: 'value',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<InlineEditableTable
|
||||
addButtonText={i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsAddRuleLabel',
|
||||
{ defaultMessage: 'Add indexing rule' }
|
||||
)}
|
||||
columns={columns}
|
||||
defaultItem={{
|
||||
valueType: 'include',
|
||||
filterType: 'object_type',
|
||||
}}
|
||||
description={description}
|
||||
instanceId={indexingRulesInstanceId}
|
||||
items={indexingRules}
|
||||
onAdd={(newRule) => {
|
||||
initAddIndexingRule(newRule);
|
||||
clearFlashMessages();
|
||||
}}
|
||||
onDelete={(rule) => {
|
||||
deleteIndexingRule(rule);
|
||||
clearFlashMessages();
|
||||
}}
|
||||
onUpdate={(rule) => {
|
||||
initSetIndexingRule(rule);
|
||||
clearFlashMessages();
|
||||
}}
|
||||
onReorder={(newIndexingRules) => {
|
||||
setIndexingRules(newIndexingRules);
|
||||
clearFlashMessages();
|
||||
}}
|
||||
title=""
|
||||
canRemoveLastItem
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -15,6 +15,11 @@ import { fullContentSources } from '../../../../__mocks__/content_sources.mock';
|
|||
|
||||
import { nextTick } from '@kbn/test-jest-helpers';
|
||||
|
||||
import {
|
||||
InlineEditableTableLogic,
|
||||
InlineEditableTableProps,
|
||||
} from '../../../../../shared/tables/inline_editable_table/inline_editable_table_logic';
|
||||
import { ItemWithAnID } from '../../../../../shared/tables/types';
|
||||
import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers';
|
||||
|
||||
jest.mock('../../source_logic', () => ({
|
||||
|
@ -30,11 +35,12 @@ import {
|
|||
SynchronizationLogic,
|
||||
emptyBlockedWindow,
|
||||
stripScheduleSeconds,
|
||||
EditableIndexingRule,
|
||||
} from './synchronization_logic';
|
||||
|
||||
describe('SynchronizationLogic', () => {
|
||||
const { http } = mockHttpValues;
|
||||
const { flashSuccessToast } = mockFlashMessageHelpers;
|
||||
const { flashSuccessToast, flashAPIErrors } = mockFlashMessageHelpers;
|
||||
const { navigateToUrl } = mockKibanaValues;
|
||||
const { mount } = new LogicMounter(SynchronizationLogic);
|
||||
const contentSource = fullContentSources[0];
|
||||
|
@ -49,12 +55,49 @@ describe('SynchronizationLogic', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const defaultIndexingRules: EditableIndexingRule[] = [
|
||||
{
|
||||
filterType: 'object_type',
|
||||
id: 0,
|
||||
value: 'value',
|
||||
valueType: 'include',
|
||||
},
|
||||
{
|
||||
filterType: 'path_template',
|
||||
id: 1,
|
||||
value: 'value',
|
||||
valueType: 'exclude',
|
||||
},
|
||||
{
|
||||
filterType: 'file_extension',
|
||||
id: 2,
|
||||
value: 'value',
|
||||
valueType: 'include',
|
||||
},
|
||||
];
|
||||
|
||||
const defaultValues = {
|
||||
navigatingBetweenTabs: false,
|
||||
hasUnsavedObjectsAndAssetsChanges: false,
|
||||
hasUnsavedAssetsAndObjectsChanges: false,
|
||||
hasUnsavedIndexingRulesChanges: false,
|
||||
hasUnsavedFrequencyChanges: false,
|
||||
contentExtractionChecked: true,
|
||||
thumbnailsChecked: true,
|
||||
indexingRules: defaultIndexingRules,
|
||||
indexingRulesForAPI: [
|
||||
{
|
||||
filter_type: 'object_type',
|
||||
include: 'value',
|
||||
},
|
||||
{
|
||||
filter_type: 'path_template',
|
||||
exclude: 'value',
|
||||
},
|
||||
{
|
||||
filter_type: 'file_extension',
|
||||
include: 'value',
|
||||
},
|
||||
],
|
||||
schedule: contentSource.indexing.schedule,
|
||||
cachedSchedule: contentSource.indexing.schedule,
|
||||
};
|
||||
|
@ -109,10 +152,17 @@ describe('SynchronizationLogic', () => {
|
|||
it('resetSyncSettings', () => {
|
||||
SynchronizationLogic.actions.setContentExtractionChecked(false);
|
||||
SynchronizationLogic.actions.setThumbnailsChecked(false);
|
||||
SynchronizationLogic.actions.addIndexingRule({
|
||||
filterType: 'file_extension',
|
||||
valueType: 'exclude',
|
||||
value: 'value',
|
||||
});
|
||||
SynchronizationLogic.actions.resetSyncSettings();
|
||||
|
||||
expect(SynchronizationLogic.values.thumbnailsChecked).toEqual(true);
|
||||
expect(SynchronizationLogic.values.contentExtractionChecked).toEqual(true);
|
||||
expect(SynchronizationLogic.values.indexingRules).toEqual(defaultIndexingRules);
|
||||
expect(SynchronizationLogic.values.hasUnsavedIndexingRulesChanges).toEqual(false);
|
||||
});
|
||||
|
||||
describe('setSyncFrequency', () => {
|
||||
|
@ -151,37 +201,133 @@ describe('SynchronizationLogic', () => {
|
|||
expect(SynchronizationLogic.values.schedule.blockedWindows).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setBlockedTimeWindow', () => {
|
||||
it('sets "jobType"', () => {
|
||||
SynchronizationLogic.actions.addBlockedWindow();
|
||||
SynchronizationLogic.actions.setBlockedTimeWindow(0, 'jobType', 'incremental');
|
||||
describe('setBlockedTimeWindow', () => {
|
||||
it('sets "jobType"', () => {
|
||||
SynchronizationLogic.actions.addBlockedWindow();
|
||||
SynchronizationLogic.actions.setBlockedTimeWindow(0, 'jobType', 'incremental');
|
||||
|
||||
expect(SynchronizationLogic.values.schedule.blockedWindows![0].jobType).toEqual(
|
||||
'incremental'
|
||||
);
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
it('sets "day"', () => {
|
||||
SynchronizationLogic.actions.addBlockedWindow();
|
||||
SynchronizationLogic.actions.setBlockedTimeWindow(0, 'day', 'tuesday');
|
||||
describe('addIndexingRule', () => {
|
||||
const indexingRule: EditableIndexingRule = {
|
||||
filterType: 'file_extension',
|
||||
valueType: 'exclude',
|
||||
value: 'value',
|
||||
id: 10,
|
||||
};
|
||||
|
||||
expect(SynchronizationLogic.values.schedule.blockedWindows![0].day).toEqual('tuesday');
|
||||
it('adds indexing rule with id 0', () => {
|
||||
SynchronizationLogic.actions.setIndexingRules([]);
|
||||
SynchronizationLogic.actions.addIndexingRule(indexingRule);
|
||||
|
||||
expect(SynchronizationLogic.values.indexingRules).toEqual([{ ...indexingRule, id: 0 }]);
|
||||
expect(SynchronizationLogic.values.hasUnsavedIndexingRulesChanges).toEqual(true);
|
||||
});
|
||||
|
||||
it('adds indexing rule with id existing length + 1', () => {
|
||||
SynchronizationLogic.actions.addIndexingRule(indexingRule);
|
||||
|
||||
expect(SynchronizationLogic.values.indexingRules).toEqual([
|
||||
...defaultValues.indexingRules,
|
||||
{ ...indexingRule, id: 3 },
|
||||
]);
|
||||
expect(SynchronizationLogic.values.hasUnsavedIndexingRulesChanges).toEqual(true);
|
||||
});
|
||||
it('adds indexing rule with unique id in case of previous deletions', () => {
|
||||
SynchronizationLogic.actions.deleteIndexingRule({ ...indexingRule, id: 1 });
|
||||
SynchronizationLogic.actions.addIndexingRule(indexingRule);
|
||||
|
||||
expect(SynchronizationLogic.values.indexingRules).toEqual([
|
||||
defaultValues.indexingRules[0],
|
||||
defaultValues.indexingRules[2],
|
||||
{ ...indexingRule, id: 3 },
|
||||
]);
|
||||
expect(SynchronizationLogic.values.hasUnsavedIndexingRulesChanges).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets "start"', () => {
|
||||
SynchronizationLogic.actions.addBlockedWindow();
|
||||
SynchronizationLogic.actions.setBlockedTimeWindow(0, 'start', '9:00:00Z');
|
||||
describe('setIndexingRule', () => {
|
||||
const indexingRule: EditableIndexingRule = {
|
||||
filterType: 'file_extension',
|
||||
valueType: 'exclude',
|
||||
value: 'value',
|
||||
id: 1,
|
||||
};
|
||||
|
||||
expect(SynchronizationLogic.values.schedule.blockedWindows![0].start).toEqual('9:00:00Z');
|
||||
it('updates indexing rule', () => {
|
||||
SynchronizationLogic.actions.setIndexingRule(indexingRule);
|
||||
|
||||
expect(SynchronizationLogic.values.indexingRules).toEqual([
|
||||
defaultValues.indexingRules[0],
|
||||
indexingRule,
|
||||
defaultValues.indexingRules[2],
|
||||
]);
|
||||
expect(SynchronizationLogic.values.hasUnsavedIndexingRulesChanges).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets "end"', () => {
|
||||
SynchronizationLogic.actions.addBlockedWindow();
|
||||
SynchronizationLogic.actions.setBlockedTimeWindow(0, 'end', '11:00:00Z');
|
||||
describe('setIndexingRules', () => {
|
||||
const indexingRule: EditableIndexingRule = {
|
||||
filterType: 'file_extension',
|
||||
valueType: 'exclude',
|
||||
value: 'value',
|
||||
id: 1,
|
||||
};
|
||||
|
||||
expect(SynchronizationLogic.values.schedule.blockedWindows![0].end).toEqual('11:00:00Z');
|
||||
it('updates indexing rules', () => {
|
||||
SynchronizationLogic.actions.setIndexingRules([indexingRule, indexingRule]);
|
||||
|
||||
expect(SynchronizationLogic.values.indexingRules).toEqual([
|
||||
{ ...indexingRule, id: 0 },
|
||||
{ ...indexingRule, id: 1 },
|
||||
]);
|
||||
expect(SynchronizationLogic.values.hasUnsavedIndexingRulesChanges).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteIndexingRule', () => {
|
||||
const indexingRule: EditableIndexingRule = {
|
||||
filterType: 'file_extension',
|
||||
valueType: 'exclude',
|
||||
value: 'value',
|
||||
id: 1,
|
||||
};
|
||||
|
||||
it('updates indexing rules', () => {
|
||||
const newIndexingRules = defaultValues.indexingRules.filter(
|
||||
(val) => val.id !== indexingRule.id
|
||||
);
|
||||
SynchronizationLogic.actions.deleteIndexingRule(indexingRule);
|
||||
expect(SynchronizationLogic.values.indexingRules).toEqual(newIndexingRules);
|
||||
|
||||
expect(SynchronizationLogic.values.hasUnsavedIndexingRulesChanges).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -209,6 +355,204 @@ describe('SynchronizationLogic', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('initAddIndexingRule', () => {
|
||||
const indexingRule: EditableIndexingRule = {
|
||||
filterType: 'file_extension',
|
||||
valueType: 'exclude',
|
||||
value: 'value',
|
||||
id: 1,
|
||||
};
|
||||
it('calls validate endpoint and continues if no errors happen', async () => {
|
||||
const addIndexingRuleSpy = jest.spyOn(SynchronizationLogic.actions, 'addIndexingRule');
|
||||
const promise = Promise.resolve({ rules: [] });
|
||||
const doneSpy = jest.spyOn(
|
||||
InlineEditableTableLogic({
|
||||
instanceId: 'IndexingRulesTable',
|
||||
} as InlineEditableTableProps<ItemWithAnID>).actions,
|
||||
'doneEditing'
|
||||
);
|
||||
http.post.mockReturnValue(promise);
|
||||
SynchronizationLogic.actions.initAddIndexingRule(indexingRule);
|
||||
|
||||
expect(http.post).toHaveBeenCalledWith(
|
||||
'/internal/workplace_search/org/sources/123/indexing_rules/validate',
|
||||
{
|
||||
body: JSON.stringify({
|
||||
rules: [
|
||||
{
|
||||
filter_type: 'file_extension',
|
||||
exclude: 'value',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
);
|
||||
await promise;
|
||||
expect(addIndexingRuleSpy).toHaveBeenCalledWith(indexingRule);
|
||||
expect(doneSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls validate endpoint and sets errors if there is an error', async () => {
|
||||
const addIndexingRuleSpy = jest.spyOn(SynchronizationLogic.actions, 'addIndexingRule');
|
||||
const promise = Promise.resolve({ rules: [{ valid: false, error: 'error' }] });
|
||||
http.post.mockReturnValue(promise);
|
||||
SynchronizationLogic.actions.initAddIndexingRule({ ...indexingRule, valueType: 'include' });
|
||||
const doneSpy = jest.spyOn(
|
||||
InlineEditableTableLogic({
|
||||
instanceId: 'IndexingRulesTable',
|
||||
} as InlineEditableTableProps<ItemWithAnID>).actions,
|
||||
'doneEditing'
|
||||
);
|
||||
|
||||
expect(http.post).toHaveBeenCalledWith(
|
||||
'/internal/workplace_search/org/sources/123/indexing_rules/validate',
|
||||
{
|
||||
body: JSON.stringify({
|
||||
rules: [
|
||||
{
|
||||
filter_type: 'file_extension',
|
||||
include: 'value',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
);
|
||||
await promise;
|
||||
expect(addIndexingRuleSpy).not.toHaveBeenCalled();
|
||||
expect(doneSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('flashes an error if the API call fails', async () => {
|
||||
const addIndexingRuleSpy = jest.spyOn(SynchronizationLogic.actions, 'addIndexingRule');
|
||||
const promise = Promise.reject('error');
|
||||
http.post.mockReturnValue(promise);
|
||||
const doneSpy = jest.spyOn(
|
||||
InlineEditableTableLogic({
|
||||
instanceId: 'IndexingRulesTable',
|
||||
} as InlineEditableTableProps<ItemWithAnID>).actions,
|
||||
'doneEditing'
|
||||
);
|
||||
SynchronizationLogic.actions.initAddIndexingRule(indexingRule);
|
||||
|
||||
expect(http.post).toHaveBeenCalledWith(
|
||||
'/internal/workplace_search/org/sources/123/indexing_rules/validate',
|
||||
{
|
||||
body: JSON.stringify({
|
||||
rules: [
|
||||
{
|
||||
filter_type: 'file_extension',
|
||||
exclude: 'value',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
);
|
||||
await nextTick();
|
||||
expect(addIndexingRuleSpy).not.toHaveBeenCalled();
|
||||
expect(doneSpy).not.toHaveBeenCalled();
|
||||
expect(flashAPIErrors).toHaveBeenCalledWith('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('initSetIndexingRule', () => {
|
||||
const indexingRule: EditableIndexingRule = {
|
||||
filterType: 'file_extension',
|
||||
valueType: 'exclude',
|
||||
value: 'value',
|
||||
id: 1,
|
||||
};
|
||||
|
||||
it('calls validate endpoint and continues if no errors happen', async () => {
|
||||
const setIndexingRuleSpy = jest.spyOn(SynchronizationLogic.actions, 'setIndexingRule');
|
||||
const promise = Promise.resolve({ rules: [] });
|
||||
http.post.mockReturnValue(promise);
|
||||
const doneSpy = jest.spyOn(
|
||||
InlineEditableTableLogic({
|
||||
instanceId: 'IndexingRulesTable',
|
||||
} as InlineEditableTableProps<ItemWithAnID>).actions,
|
||||
'doneEditing'
|
||||
);
|
||||
SynchronizationLogic.actions.initSetIndexingRule(indexingRule);
|
||||
|
||||
expect(http.post).toHaveBeenCalledWith(
|
||||
'/internal/workplace_search/org/sources/123/indexing_rules/validate',
|
||||
{
|
||||
body: JSON.stringify({
|
||||
rules: [
|
||||
{
|
||||
filter_type: 'file_extension',
|
||||
exclude: 'value',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
);
|
||||
await promise;
|
||||
expect(setIndexingRuleSpy).toHaveBeenCalledWith(indexingRule);
|
||||
expect(doneSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls validate endpoint and sets errors if there is an error', async () => {
|
||||
const setIndexingRuleSpy = jest.spyOn(SynchronizationLogic.actions, 'setIndexingRule');
|
||||
const promise = Promise.resolve({ rules: [{ valid: false, error: 'error' }] });
|
||||
http.post.mockReturnValue(promise);
|
||||
const doneSpy = jest.spyOn(
|
||||
InlineEditableTableLogic({
|
||||
instanceId: 'IndexingRulesTable',
|
||||
} as InlineEditableTableProps<ItemWithAnID>).actions,
|
||||
'doneEditing'
|
||||
);
|
||||
SynchronizationLogic.actions.initSetIndexingRule({ ...indexingRule, valueType: 'include' });
|
||||
|
||||
expect(http.post).toHaveBeenCalledWith(
|
||||
'/internal/workplace_search/org/sources/123/indexing_rules/validate',
|
||||
{
|
||||
body: JSON.stringify({
|
||||
rules: [
|
||||
{
|
||||
filter_type: 'file_extension',
|
||||
include: 'value',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
);
|
||||
await promise;
|
||||
expect(setIndexingRuleSpy).not.toHaveBeenCalled();
|
||||
expect(doneSpy).toHaveBeenCalled();
|
||||
});
|
||||
it('flashes an error if the API call fails', async () => {
|
||||
const setIndexingRuleSpy = jest.spyOn(SynchronizationLogic.actions, 'setIndexingRule');
|
||||
const promise = Promise.reject('error');
|
||||
http.post.mockReturnValue(promise);
|
||||
const doneSpy = jest.spyOn(
|
||||
InlineEditableTableLogic({
|
||||
instanceId: 'IndexingRulesTable',
|
||||
} as InlineEditableTableProps<ItemWithAnID>).actions,
|
||||
'doneEditing'
|
||||
);
|
||||
SynchronizationLogic.actions.initSetIndexingRule(indexingRule);
|
||||
|
||||
expect(http.post).toHaveBeenCalledWith(
|
||||
'/internal/workplace_search/org/sources/123/indexing_rules/validate',
|
||||
{
|
||||
body: JSON.stringify({
|
||||
rules: [
|
||||
{
|
||||
filter_type: 'file_extension',
|
||||
exclude: 'value',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
);
|
||||
await nextTick();
|
||||
expect(setIndexingRuleSpy).not.toHaveBeenCalled();
|
||||
expect(doneSpy).not.toHaveBeenCalled();
|
||||
expect(flashAPIErrors).toHaveBeenCalledWith('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSyncEnabled', () => {
|
||||
it('calls updateServerSettings method', async () => {
|
||||
const updateServerSettingsSpy = jest.spyOn(
|
||||
|
@ -225,13 +569,13 @@ describe('SynchronizationLogic', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('updateObjectsAndAssetsSettings', () => {
|
||||
describe('updateAssetsAndObjectsSettings', () => {
|
||||
it('calls updateServerSettings method', async () => {
|
||||
const updateServerSettingsSpy = jest.spyOn(
|
||||
SynchronizationLogic.actions,
|
||||
'updateServerSettings'
|
||||
);
|
||||
SynchronizationLogic.actions.updateObjectsAndAssetsSettings();
|
||||
SynchronizationLogic.actions.updateAssetsAndObjectsSettings();
|
||||
|
||||
expect(updateServerSettingsSpy).toHaveBeenCalledWith({
|
||||
content_source: {
|
||||
|
@ -240,6 +584,20 @@ describe('SynchronizationLogic', () => {
|
|||
content_extraction: { enabled: true },
|
||||
thumbnails: { enabled: true },
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
filter_type: 'object_type',
|
||||
include: 'value',
|
||||
},
|
||||
{
|
||||
filter_type: 'path_template',
|
||||
exclude: 'value',
|
||||
},
|
||||
{
|
||||
filter_type: 'file_extension',
|
||||
include: 'value',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,6 +14,11 @@ export type TabId = 'source_sync_frequency' | 'blocked_time_windows';
|
|||
import { flashAPIErrors, flashSuccessToast } from '../../../../../shared/flash_messages';
|
||||
import { HttpLogic } from '../../../../../shared/http';
|
||||
import { KibanaLogic } from '../../../../../shared/kibana';
|
||||
import {
|
||||
InlineEditableTableLogic,
|
||||
InlineEditableTableProps,
|
||||
} from '../../../../../shared/tables/inline_editable_table/inline_editable_table_logic';
|
||||
import { ItemWithAnID } from '../../../../../shared/tables/types';
|
||||
import { AppLogic } from '../../../../app_logic';
|
||||
import {
|
||||
SYNC_FREQUENCY_PATH,
|
||||
|
@ -25,9 +30,11 @@ import {
|
|||
BlockedWindow,
|
||||
DayOfWeek,
|
||||
IndexingSchedule,
|
||||
IndexingRule,
|
||||
ContentSourceFullData,
|
||||
SyncJobType,
|
||||
TimeUnit,
|
||||
IndexingRuleInclude,
|
||||
} from '../../../../types';
|
||||
|
||||
import { SYNC_SETTINGS_UPDATED_MESSAGE } from '../../constants';
|
||||
|
@ -57,6 +64,7 @@ interface ServerSyncSettingsBody {
|
|||
permissions?: string;
|
||||
blocked_windows?: ServerBlockedWindow[];
|
||||
};
|
||||
rules?: IndexingRule[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -67,7 +75,7 @@ interface SynchronizationActions {
|
|||
addBlockedWindow(): void;
|
||||
removeBlockedWindow(index: number): number;
|
||||
updateFrequencySettings(): void;
|
||||
updateObjectsAndAssetsSettings(): void;
|
||||
updateAssetsAndObjectsSettings(): void;
|
||||
resetSyncSettings(): void;
|
||||
updateSyncEnabled(enabled: boolean): boolean;
|
||||
setThumbnailsChecked(checked: boolean): boolean;
|
||||
|
@ -88,16 +96,26 @@ interface SynchronizationActions {
|
|||
setContentExtractionChecked(checked: boolean): boolean;
|
||||
setServerSchedule(schedule: IndexingSchedule): IndexingSchedule;
|
||||
updateServerSettings(body: ServerSyncSettingsBody): ServerSyncSettingsBody;
|
||||
addIndexingRule(indexingRule: EditableIndexingRuleBase): EditableIndexingRuleBase;
|
||||
initAddIndexingRule(rule: EditableIndexingRule): { rule: EditableIndexingRule };
|
||||
setIndexingRules(indexingRules: EditableIndexingRule[]): EditableIndexingRule[];
|
||||
setIndexingRule(indexingRule: EditableIndexingRule): EditableIndexingRule;
|
||||
initSetIndexingRule(indexingRule: EditableIndexingRule): { rule: EditableIndexingRule };
|
||||
deleteIndexingRule(indexingRule: EditableIndexingRule): EditableIndexingRule;
|
||||
}
|
||||
|
||||
interface SynchronizationValues {
|
||||
navigatingBetweenTabs: boolean;
|
||||
hasUnsavedIndexingRulesChanges: boolean;
|
||||
hasUnsavedFrequencyChanges: boolean;
|
||||
hasUnsavedObjectsAndAssetsChanges: boolean;
|
||||
hasUnsavedAssetsAndObjectsChanges: boolean;
|
||||
indexingRules: EditableIndexingRule[];
|
||||
thumbnailsChecked: boolean;
|
||||
contentExtractionChecked: boolean;
|
||||
cachedSchedule: IndexingSchedule;
|
||||
schedule: IndexingSchedule;
|
||||
indexingRulesForAPI: IndexingRule[];
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export const emptyBlockedWindow: BlockedWindow = {
|
||||
|
@ -111,6 +129,26 @@ type BlockedWindowMap = {
|
|||
[prop in keyof BlockedWindow]: SyncJobType | DayOfWeek | 'all' | string;
|
||||
};
|
||||
|
||||
interface EditableIndexingRuleBase {
|
||||
filterType: 'object_type' | 'path_template' | 'file_extension';
|
||||
valueType: 'include' | 'exclude';
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface EditableIndexingRule extends EditableIndexingRuleBase {
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface IndexingRuleForAPI {
|
||||
filter_type: 'object_type' | 'path_template' | 'file_extension';
|
||||
include?: string;
|
||||
exclude?: string;
|
||||
}
|
||||
|
||||
const isIncludeRule = (rule: IndexingRule): rule is IndexingRuleInclude => {
|
||||
return !!(rule as IndexingRuleInclude).include;
|
||||
};
|
||||
|
||||
export const SynchronizationLogic = kea<
|
||||
MakeLogicType<SynchronizationValues, SynchronizationActions>
|
||||
>({
|
||||
|
@ -135,11 +173,28 @@ export const SynchronizationLogic = kea<
|
|||
setServerSchedule: (schedule: IndexingSchedule) => schedule,
|
||||
removeBlockedWindow: (index: number) => index,
|
||||
updateFrequencySettings: true,
|
||||
updateObjectsAndAssetsSettings: true,
|
||||
updateAssetsAndObjectsSettings: true,
|
||||
resetSyncSettings: true,
|
||||
addBlockedWindow: true,
|
||||
addIndexingRule: (rule: EditableIndexingRuleBase) => rule,
|
||||
deleteIndexingRule: (rule: EditableIndexingRule) => rule,
|
||||
initAddIndexingRule: (rule: EditableIndexingRule) => ({ rule }),
|
||||
initSetIndexingRule: (rule: EditableIndexingRule) => ({ rule }),
|
||||
setIndexingRules: (indexingRules: EditableIndexingRule[]) => indexingRules,
|
||||
setIndexingRule: (rule: EditableIndexingRule) => rule,
|
||||
},
|
||||
reducers: ({ props }) => ({
|
||||
hasUnsavedIndexingRulesChanges: [
|
||||
false,
|
||||
{
|
||||
setIndexingRule: () => true,
|
||||
setIndexingRules: () => true,
|
||||
addIndexingRule: () => true,
|
||||
deleteIndexingRule: () => true,
|
||||
resetSyncSettings: () => false,
|
||||
updateServerSettings: () => false,
|
||||
},
|
||||
],
|
||||
navigatingBetweenTabs: [
|
||||
false,
|
||||
{
|
||||
|
@ -228,15 +283,55 @@ export const SynchronizationLogic = kea<
|
|||
},
|
||||
},
|
||||
],
|
||||
indexingRules: [
|
||||
(props.contentSource.indexing.rules as IndexingRule[]).map((rule, index) => ({
|
||||
filterType: rule.filterType,
|
||||
id: index,
|
||||
valueType: isIncludeRule(rule) ? 'include' : 'exclude',
|
||||
value: isIncludeRule(rule) ? rule.include : rule.exclude,
|
||||
})),
|
||||
{
|
||||
addIndexingRule: (indexingRules, rule) => [
|
||||
...indexingRules,
|
||||
{
|
||||
...rule,
|
||||
// make sure that we get a unique number, in case of multiple deletions and additions
|
||||
id: indexingRules.reduce(
|
||||
(prev, curr) => (curr.id >= prev ? curr.id + 1 : prev),
|
||||
indexingRules.length
|
||||
),
|
||||
},
|
||||
],
|
||||
deleteIndexingRule: (indexingRules, rule) =>
|
||||
indexingRules.filter((currentRule) => currentRule.id !== rule.id),
|
||||
resetSyncSettings: () =>
|
||||
(props.contentSource.indexing.rules as IndexingRule[]).map((rule, index) => ({
|
||||
filterType: rule.filterType,
|
||||
id: index,
|
||||
valueType: isIncludeRule(rule) ? 'include' : 'exclude',
|
||||
value: isIncludeRule(rule) ? rule.include : rule.exclude,
|
||||
})),
|
||||
setIndexingRules: (_, indexingRules) =>
|
||||
indexingRules.map((val, index) => ({ ...val, id: index })),
|
||||
setIndexingRule: (state, rule) =>
|
||||
state.map((currentRule) => (currentRule.id === rule.id ? rule : currentRule)),
|
||||
},
|
||||
],
|
||||
}),
|
||||
selectors: ({ selectors }) => ({
|
||||
hasUnsavedObjectsAndAssetsChanges: [
|
||||
hasUnsavedAssetsAndObjectsChanges: [
|
||||
() => [
|
||||
selectors.thumbnailsChecked,
|
||||
selectors.contentExtractionChecked,
|
||||
selectors.hasUnsavedIndexingRulesChanges,
|
||||
(_, props) => props.contentSource,
|
||||
],
|
||||
(thumbnailsChecked, contentExtractionChecked, contentSource) => {
|
||||
(
|
||||
thumbnailsChecked,
|
||||
contentExtractionChecked,
|
||||
hasUnsavedIndexingRulesChanges,
|
||||
contentSource
|
||||
) => {
|
||||
const {
|
||||
indexing: {
|
||||
features: {
|
||||
|
@ -248,7 +343,8 @@ export const SynchronizationLogic = kea<
|
|||
|
||||
return (
|
||||
thumbnailsChecked !== thumbnailsEnabled ||
|
||||
contentExtractionChecked !== contentExtractionEnabled
|
||||
contentExtractionChecked !== contentExtractionEnabled ||
|
||||
hasUnsavedIndexingRulesChanges
|
||||
);
|
||||
},
|
||||
],
|
||||
|
@ -256,6 +352,11 @@ export const SynchronizationLogic = kea<
|
|||
() => [selectors.cachedSchedule, selectors.schedule],
|
||||
(cachedSchedule, schedule) => !isEqual(cachedSchedule, schedule),
|
||||
],
|
||||
indexingRulesForAPI: [
|
||||
() => [selectors.indexingRules],
|
||||
(indexingRules: EditableIndexingRule[]) =>
|
||||
indexingRules.map((indexingRule) => indexingRuleToApiFormat(indexingRule)),
|
||||
],
|
||||
}),
|
||||
listeners: ({ actions, values, props }) => ({
|
||||
handleSelectedTabChanged: async (tabId, breakpoint) => {
|
||||
|
@ -277,6 +378,62 @@ export const SynchronizationLogic = kea<
|
|||
KibanaLogic.values.navigateToUrl(path);
|
||||
actions.setNavigatingBetweenTabs(false);
|
||||
},
|
||||
initAddIndexingRule: async ({ rule }) => {
|
||||
const { id: sourceId } = props.contentSource;
|
||||
const route = `/internal/workplace_search/org/sources/${sourceId}/indexing_rules/validate`;
|
||||
try {
|
||||
const response = await HttpLogic.values.http.post<{
|
||||
rules: Array<{
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
}>(route, {
|
||||
body: JSON.stringify({
|
||||
rules: [indexingRuleToApiFormat(rule)],
|
||||
}),
|
||||
});
|
||||
const error = response.rules[0]?.error;
|
||||
const tableLogic = InlineEditableTableLogic({
|
||||
instanceId: 'IndexingRulesTable',
|
||||
} as InlineEditableTableProps<ItemWithAnID>);
|
||||
if (error) {
|
||||
tableLogic.actions.setRowErrors([error]);
|
||||
} else {
|
||||
actions.addIndexingRule(rule);
|
||||
}
|
||||
tableLogic.actions.doneEditing();
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
},
|
||||
initSetIndexingRule: async ({ rule }) => {
|
||||
const { id: sourceId } = props.contentSource;
|
||||
const route = `/internal/workplace_search/org/sources/${sourceId}/indexing_rules/validate`;
|
||||
try {
|
||||
const response = await HttpLogic.values.http.post<{
|
||||
rules: Array<{
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
}>(route, {
|
||||
body: JSON.stringify({
|
||||
rules: [indexingRuleToApiFormat(rule)],
|
||||
}),
|
||||
});
|
||||
const error = response.rules[0]?.error;
|
||||
const tableLogic = InlineEditableTableLogic({
|
||||
instanceId: 'IndexingRulesTable',
|
||||
} as InlineEditableTableProps<ItemWithAnID>);
|
||||
if (error) {
|
||||
tableLogic.actions.setRowErrors([error]);
|
||||
} else {
|
||||
actions.setIndexingRule(rule);
|
||||
}
|
||||
tableLogic.actions.doneEditing();
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
},
|
||||
updateSyncEnabled: async (enabled) => {
|
||||
actions.updateServerSettings({
|
||||
content_source: {
|
||||
|
@ -284,7 +441,7 @@ export const SynchronizationLogic = kea<
|
|||
},
|
||||
});
|
||||
},
|
||||
updateObjectsAndAssetsSettings: () => {
|
||||
updateAssetsAndObjectsSettings: () => {
|
||||
actions.updateServerSettings({
|
||||
content_source: {
|
||||
indexing: {
|
||||
|
@ -292,6 +449,7 @@ export const SynchronizationLogic = kea<
|
|||
content_extraction: { enabled: values.contentExtractionChecked },
|
||||
thumbnails: { enabled: values.thumbnailsChecked },
|
||||
},
|
||||
rules: values.indexingRulesForAPI,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -360,3 +518,10 @@ const formatBlockedWindowsForServer = (
|
|||
end,
|
||||
}));
|
||||
};
|
||||
|
||||
const indexingRuleToApiFormat = (indexingRule: EditableIndexingRule): IndexingRuleForAPI => {
|
||||
const { valueType, filterType, value } = indexingRule;
|
||||
return valueType === 'include'
|
||||
? { filter_type: filterType, include: value }
|
||||
: { filter_type: filterType, exclude: value };
|
||||
};
|
||||
|
|
|
@ -10,12 +10,12 @@ import '../../../../../__mocks__/shallow_useeffect.mock';
|
|||
import { setMockValues } from '../../../../../__mocks__/kea_logic';
|
||||
|
||||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { AssetsAndObjects } from './assets_and_objects';
|
||||
import { Frequency } from './frequency';
|
||||
import { ObjectsAndAssets } from './objects_and_assets';
|
||||
import { Synchronization } from './synchronization';
|
||||
import { SynchronizationRouter } from './synchronization_router';
|
||||
|
||||
|
@ -25,9 +25,10 @@ describe('SynchronizationRouter', () => {
|
|||
const wrapper = shallow(<SynchronizationRouter />);
|
||||
|
||||
expect(wrapper.find(Synchronization)).toHaveLength(1);
|
||||
expect(wrapper.find(ObjectsAndAssets)).toHaveLength(1);
|
||||
expect(wrapper.find(AssetsAndObjects)).toHaveLength(1);
|
||||
expect(wrapper.find(Frequency)).toHaveLength(2);
|
||||
expect(wrapper.find(Switch)).toHaveLength(1);
|
||||
expect(wrapper.find(Route)).toHaveLength(4);
|
||||
expect(wrapper.find(Redirect)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,18 +6,19 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
SYNC_FREQUENCY_PATH,
|
||||
BLOCKED_TIME_WINDOWS_PATH,
|
||||
OBJECTS_AND_ASSETS_PATH,
|
||||
ASSETS_AND_OBJECTS_PATH,
|
||||
SOURCE_SYNCHRONIZATION_PATH,
|
||||
getSourcesPath,
|
||||
OLD_OBJECTS_AND_ASSETS_PATH,
|
||||
} from '../../../../routes';
|
||||
|
||||
import { AssetsAndObjects } from './assets_and_objects';
|
||||
import { Frequency } from './frequency';
|
||||
import { ObjectsAndAssets } from './objects_and_assets';
|
||||
import { Synchronization } from './synchronization';
|
||||
|
||||
export const SynchronizationRouter: React.FC = () => (
|
||||
|
@ -31,8 +32,12 @@ export const SynchronizationRouter: React.FC = () => (
|
|||
<Route exact path={getSourcesPath(BLOCKED_TIME_WINDOWS_PATH, true)}>
|
||||
<Frequency tabId={1} />
|
||||
</Route>
|
||||
<Route exact path={getSourcesPath(OBJECTS_AND_ASSETS_PATH, true)}>
|
||||
<ObjectsAndAssets />
|
||||
<Route exact path={getSourcesPath(ASSETS_AND_OBJECTS_PATH, true)}>
|
||||
<AssetsAndObjects />
|
||||
</Route>
|
||||
<Redirect
|
||||
from={getSourcesPath(OLD_OBJECTS_AND_ASSETS_PATH, true)}
|
||||
to={getSourcesPath(ASSETS_AND_OBJECTS_PATH, true)}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
|
|
|
@ -24,9 +24,9 @@ describe('useSynchronizationSubNav', () => {
|
|||
href: '/sources/1/synchronization/frequency',
|
||||
},
|
||||
{
|
||||
id: 'sourceSynchronizationObjectsAndAssets',
|
||||
name: 'Objects and assets',
|
||||
href: '/sources/1/synchronization/objects_and_assets',
|
||||
id: 'sourceSynchronizationAssetsAndObjects',
|
||||
name: 'Assets and objects',
|
||||
href: '/sources/1/synchronization/assets_and_objects',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ import { NAV } from '../../../../constants';
|
|||
import {
|
||||
getContentSourcePath,
|
||||
SYNC_FREQUENCY_PATH,
|
||||
OBJECTS_AND_ASSETS_PATH,
|
||||
ASSETS_AND_OBJECTS_PATH,
|
||||
} from '../../../../routes';
|
||||
import { SourceLogic } from '../../source_logic';
|
||||
|
||||
|
@ -35,9 +35,9 @@ export const useSynchronizationSubNav = () => {
|
|||
}),
|
||||
},
|
||||
{
|
||||
id: 'sourceSynchronizationObjectsAndAssets',
|
||||
name: NAV.SYNCHRONIZATION_OBJECTS_AND_ASSETS,
|
||||
...generateNavLink({ to: getContentSourcePath(OBJECTS_AND_ASSETS_PATH, id, true) }),
|
||||
id: 'sourceSynchronizationAssetsAndObjects',
|
||||
name: NAV.SYNCHRONIZATION_ASSETS_AND_OBJECTS,
|
||||
...generateNavLink({ to: getContentSourcePath(ASSETS_AND_OBJECTS_PATH, id, true) }),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -543,21 +543,31 @@ export const SOURCE_FREQUENCY_DESCRIPTION = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsDescription',
|
||||
export const SOURCE_ASSETS_AND_OBJECTS_DESCRIPTION = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Customize the indexing rules that determine which objects and assets are synchronized from this content source to Workplace Search.',
|
||||
'Flexibly manage the documents to be synchronized and made available for search using granular controls below.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SOURCE_OBJECTS_AND_ASSETS_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsLabel',
|
||||
export const SOURCE_ASSETS_AND_OBJECTS_LEARN_MORE_LINK = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsLearnMoreLink',
|
||||
{
|
||||
defaultMessage: 'Object and details to include in search results',
|
||||
defaultMessage: 'Learn more about sync objects types.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SOURCE_ASSETS_AND_OBJECTS_ASSETS_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsAssetsLabel',
|
||||
{ defaultMessage: 'Assets' }
|
||||
);
|
||||
|
||||
export const SOURCE_ASSETS_AND_OBJECTS_OBJECTS_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceAssetsAndObjectsObjectsLabel',
|
||||
{ defaultMessage: 'Objects' }
|
||||
);
|
||||
|
||||
export const SOURCE_SYNCHRONIZATION_TOGGLE_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.workplaceSearch.sources.sourceSynchronizationToggleLabel',
|
||||
{
|
||||
|
|
|
@ -44,6 +44,8 @@ import {
|
|||
registerOrgSourceOauthConfigurationRoute,
|
||||
registerOrgSourceSynchronizeRoute,
|
||||
registerOauthConnectorParamsRoute,
|
||||
registerAccountSourceValidateIndexingRulesRoute,
|
||||
registerOrgSourceValidateIndexingRulesRoute,
|
||||
} from './sources';
|
||||
|
||||
const mockConfig = {
|
||||
|
@ -310,6 +312,45 @@ describe('sources routes', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('POST /internal/workplace_search/account/sources/{id}/indexing_rules/validate', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockRouter = new MockRouter({
|
||||
method: 'post',
|
||||
path: '/internal/workplace_search/account/sources/{id}/indexing_rules/validate',
|
||||
});
|
||||
|
||||
registerAccountSourceValidateIndexingRulesRoute({
|
||||
...mockDependencies,
|
||||
router: mockRouter.router,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a request handler', () => {
|
||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||
path: '/ws/sources/:id/indexing_rules/validate',
|
||||
});
|
||||
});
|
||||
|
||||
describe('validates', () => {
|
||||
it('correctly', () => {
|
||||
const request = {
|
||||
body: {
|
||||
rules: [
|
||||
{
|
||||
filter_type: 'path_template',
|
||||
exclude: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
mockRouter.shouldValidate(request);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /internal/workplace_search/account/pre_sources/{id}', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
|
@ -818,6 +859,45 @@ describe('sources routes', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('POST /internal/workplace_search/org/sources/{id}/indexing_rules/validate', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockRouter = new MockRouter({
|
||||
method: 'post',
|
||||
path: '/internal/workplace_search/org/sources/{id}/indexing_rules/validate',
|
||||
});
|
||||
|
||||
registerOrgSourceValidateIndexingRulesRoute({
|
||||
...mockDependencies,
|
||||
router: mockRouter.router,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a request handler', () => {
|
||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||
path: '/ws/org/sources/:id/indexing_rules/validate',
|
||||
});
|
||||
});
|
||||
|
||||
describe('validates', () => {
|
||||
it('correctly', () => {
|
||||
const request = {
|
||||
body: {
|
||||
rules: [
|
||||
{
|
||||
filter_type: 'path_template',
|
||||
exclude: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
mockRouter.shouldValidate(request);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /internal/workplace_search/org/pre_sources/{id}', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
|
|
|
@ -96,11 +96,32 @@ const sourceSettingsSchema = schema.object({
|
|||
),
|
||||
})
|
||||
),
|
||||
rules: schema.maybe(
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
filter_type: schema.string(),
|
||||
exclude: schema.maybe(schema.string()),
|
||||
include: schema.maybe(schema.string()),
|
||||
})
|
||||
)
|
||||
),
|
||||
})
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
const validateRulesSchema = schema.object({
|
||||
rules: schema.maybe(
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
filter_type: schema.string(),
|
||||
exclude: schema.maybe(schema.string()),
|
||||
include: schema.maybe(schema.string()),
|
||||
})
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
// Account routes
|
||||
export function registerAccountSourcesRoute({
|
||||
router,
|
||||
|
@ -273,6 +294,26 @@ export function registerAccountSourceSettingsRoute({
|
|||
);
|
||||
}
|
||||
|
||||
export function registerAccountSourceValidateIndexingRulesRoute({
|
||||
router,
|
||||
enterpriseSearchRequestHandler,
|
||||
}: RouteDependencies) {
|
||||
router.post(
|
||||
{
|
||||
path: '/internal/workplace_search/account/sources/{id}/indexing_rules/validate',
|
||||
validate: {
|
||||
body: validateRulesSchema,
|
||||
params: schema.object({
|
||||
id: schema.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
enterpriseSearchRequestHandler.createRequest({
|
||||
path: '/ws/sources/:id/indexing_rules/validate',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function registerAccountPreSourceRoute({
|
||||
router,
|
||||
enterpriseSearchRequestHandler,
|
||||
|
@ -620,6 +661,26 @@ export function registerOrgSourceSettingsRoute({
|
|||
);
|
||||
}
|
||||
|
||||
export function registerOrgSourceValidateIndexingRulesRoute({
|
||||
router,
|
||||
enterpriseSearchRequestHandler,
|
||||
}: RouteDependencies) {
|
||||
router.post(
|
||||
{
|
||||
path: '/internal/workplace_search/org/sources/{id}/indexing_rules/validate',
|
||||
validate: {
|
||||
body: validateRulesSchema,
|
||||
params: schema.object({
|
||||
id: schema.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
enterpriseSearchRequestHandler.createRequest({
|
||||
path: '/ws/org/sources/:id/indexing_rules/validate',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function registerOrgPreSourceRoute({
|
||||
router,
|
||||
enterpriseSearchRequestHandler,
|
||||
|
@ -955,6 +1016,7 @@ export const registerSourcesRoutes = (dependencies: RouteDependencies) => {
|
|||
registerAccountSourceFederatedSummaryRoute(dependencies);
|
||||
registerAccountSourceReauthPrepareRoute(dependencies);
|
||||
registerAccountSourceSettingsRoute(dependencies);
|
||||
registerAccountSourceValidateIndexingRulesRoute(dependencies);
|
||||
registerAccountPreSourceRoute(dependencies);
|
||||
registerAccountPrepareSourcesRoute(dependencies);
|
||||
registerAccountSourceSearchableRoute(dependencies);
|
||||
|
@ -970,6 +1032,7 @@ export const registerSourcesRoutes = (dependencies: RouteDependencies) => {
|
|||
registerOrgSourceFederatedSummaryRoute(dependencies);
|
||||
registerOrgSourceReauthPrepareRoute(dependencies);
|
||||
registerOrgSourceSettingsRoute(dependencies);
|
||||
registerOrgSourceValidateIndexingRulesRoute(dependencies);
|
||||
registerOrgPreSourceRoute(dependencies);
|
||||
registerOrgPrepareSourcesRoute(dependencies);
|
||||
registerOrgSourceSearchableRoute(dependencies);
|
||||
|
|
|
@ -10666,7 +10666,6 @@
|
|||
"xpack.enterpriseSearch.workplaceSearch.nav.sources": "ソース",
|
||||
"xpack.enterpriseSearch.workplaceSearch.nav.synchronization": "同期",
|
||||
"xpack.enterpriseSearch.workplaceSearch.nav.synchronizationFrequency": "頻度",
|
||||
"xpack.enterpriseSearch.workplaceSearch.nav.synchronizationObjectsAndAssets": "オブジェクトとアセット",
|
||||
"xpack.enterpriseSearch.workplaceSearch.nonPlatinumOauthDescription": "Workplace Search検索APIを安全に使用するために、OAuthアプリケーションを構成します。プラチナライセンスにアップグレードして、検索APIを有効にし、OAuthアプリケーションを作成します。",
|
||||
"xpack.enterpriseSearch.workplaceSearch.nonPlatinumOauthTitle": "カスタム検索アプリケーションのOAuthを構成",
|
||||
"xpack.enterpriseSearch.workplaceSearch.oauth.description": "組織のOAuthクライアントを作成します。",
|
||||
|
@ -10920,8 +10919,6 @@
|
|||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.sharePoint": "SharePoint Online",
|
||||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.slack": "Slack",
|
||||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.zendesk": "Zendesk",
|
||||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsDescription": "このコンテンツソースからWorkplace Searchに同期されるオブジェクトとアセットを決定するインデックスルールをカスタマイズします。",
|
||||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsLabel": "検索結果に含めるオブジェクトと詳細",
|
||||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceOverviewTitle": "ソース概要",
|
||||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncConfirmMessage": "この要求を続行し、他のすべての同期を停止しますか?",
|
||||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncConfirmTitle": "新しいコンテンツ同期を開始しますか?",
|
||||
|
|
|
@ -10524,7 +10524,6 @@
|
|||
"xpack.enterpriseSearch.workplaceSearch.nav.sources": "源",
|
||||
"xpack.enterpriseSearch.workplaceSearch.nav.synchronization": "同步",
|
||||
"xpack.enterpriseSearch.workplaceSearch.nav.synchronizationFrequency": "频率",
|
||||
"xpack.enterpriseSearch.workplaceSearch.nav.synchronizationObjectsAndAssets": "对象和资产",
|
||||
"xpack.enterpriseSearch.workplaceSearch.nonPlatinumOauthDescription": "配置 OAuth 应用程序,以安全使用 Workplace Search 搜索 API。升级到白金级许可证,以启用搜索 API 并创建您的 OAuth 应用程序。",
|
||||
"xpack.enterpriseSearch.workplaceSearch.nonPlatinumOauthTitle": "正在为定制搜索应用程序配置 OAuth",
|
||||
"xpack.enterpriseSearch.workplaceSearch.oauth.description": "为您的组织创建 OAuth 客户端。",
|
||||
|
@ -10779,8 +10778,6 @@
|
|||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.sharePoint": "Sharepoint",
|
||||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.slack": "Slack",
|
||||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.zendesk": "Zendesk",
|
||||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsDescription": "定制确定将哪些对象和资产从此内容源同步到 Workplace Search 的索引规则。",
|
||||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsLabel": "要包括在搜索结果中的对象和详情",
|
||||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceOverviewTitle": "源概览",
|
||||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncConfirmMessage": "是否确定要继续处理此请求并停止所有其他同步?",
|
||||
"xpack.enterpriseSearch.workplaceSearch.sources.sourceSyncConfirmTitle": "开始新内容同步?",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue