mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Enterprise Search] Add sync now button for connectors (#136941)
This commit is contained in:
parent
0b8b66f73f
commit
b12d58cd55
26 changed files with 1017 additions and 273 deletions
|
@ -38,14 +38,19 @@ export interface ConnectorIndex extends ElasticsearchIndex {
|
|||
export interface CrawlerIndex extends ElasticsearchIndex {
|
||||
crawler: Crawler;
|
||||
}
|
||||
export interface ElasticsearchIndexWithIngestion extends ElasticsearchIndex {
|
||||
connector?: Connector;
|
||||
crawler?: Crawler;
|
||||
export interface ConnectorIndex extends ElasticsearchIndex {
|
||||
connector: Connector;
|
||||
}
|
||||
export interface ElasticsearchIndexWithPrivileges extends ElasticsearchIndex {
|
||||
alias: boolean;
|
||||
privileges: {
|
||||
read: boolean;
|
||||
manage: boolean;
|
||||
read: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CrawlerIndex extends ElasticsearchIndex {
|
||||
crawler: Crawler;
|
||||
}
|
||||
|
||||
export type ElasticsearchIndexWithIngestion = ElasticsearchIndex | ConnectorIndex | CrawlerIndex;
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 { SyncStatus, ConnectorStatus } from '../../../../common/types/connectors';
|
||||
|
||||
import {
|
||||
ApiViewIndex,
|
||||
ConnectorViewIndex,
|
||||
CrawlerViewIndex,
|
||||
IngestionMethod,
|
||||
IngestionStatus,
|
||||
} from '../types';
|
||||
|
||||
export const apiIndex: ApiViewIndex = {
|
||||
ingestionMethod: IngestionMethod.API,
|
||||
ingestionStatus: IngestionStatus.CONNECTED,
|
||||
lastUpdated: null,
|
||||
name: 'api',
|
||||
total: {
|
||||
docs: {
|
||||
count: 1,
|
||||
deleted: 0,
|
||||
},
|
||||
store: { size_in_bytes: '8024' },
|
||||
},
|
||||
};
|
||||
export const connectorIndex: ConnectorViewIndex = {
|
||||
connector: {
|
||||
api_key_id: null,
|
||||
configuration: {},
|
||||
id: '2',
|
||||
index_name: 'connector',
|
||||
last_seen: null,
|
||||
last_sync_error: null,
|
||||
last_sync_status: SyncStatus.COMPLETED,
|
||||
last_synced: null,
|
||||
scheduling: {
|
||||
enabled: false,
|
||||
interval: '',
|
||||
},
|
||||
service_type: null,
|
||||
status: ConnectorStatus.CONFIGURED,
|
||||
sync_now: false,
|
||||
},
|
||||
ingestionMethod: IngestionMethod.CONNECTOR,
|
||||
ingestionStatus: IngestionStatus.INCOMPLETE,
|
||||
lastUpdated: 'never',
|
||||
name: 'connector',
|
||||
total: {
|
||||
docs: {
|
||||
count: 1,
|
||||
deleted: 0,
|
||||
},
|
||||
store: { size_in_bytes: '8024' },
|
||||
},
|
||||
};
|
||||
export const crawlerIndex: CrawlerViewIndex = {
|
||||
crawler: {
|
||||
id: '3',
|
||||
index_name: 'crawler',
|
||||
},
|
||||
ingestionMethod: IngestionMethod.CRAWLER,
|
||||
ingestionStatus: IngestionStatus.INCOMPLETE,
|
||||
lastUpdated: null,
|
||||
name: 'crawler',
|
||||
total: {
|
||||
docs: {
|
||||
count: 1,
|
||||
deleted: 0,
|
||||
},
|
||||
store: { size_in_bytes: '8024' },
|
||||
},
|
||||
};
|
||||
|
||||
export const elasticsearchViewIndices = [apiIndex, connectorIndex, crawlerIndex];
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { mockHttpValues } from '../../../__mocks__/kea_logic';
|
||||
|
||||
import { nextTick } from '@kbn/test-jest-helpers';
|
||||
|
||||
import { startSync } from './start_sync_api_logic';
|
||||
|
||||
describe('startSync', () => {
|
||||
const { http } = mockHttpValues;
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('generateApiKey', () => {
|
||||
it('calls correct api', async () => {
|
||||
const promise = Promise.resolve('result');
|
||||
http.post.mockReturnValue(promise);
|
||||
const result = startSync({ connectorId: 'connectorId' });
|
||||
await nextTick();
|
||||
expect(http.post).toHaveBeenCalledWith(
|
||||
'/internal/enterprise_search/connectors/connectorId/start_sync'
|
||||
);
|
||||
await expect(result).resolves.toEqual('result');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { createApiLogic } from '../../../shared/api_logic/create_api_logic';
|
||||
import { HttpLogic } from '../../../shared/http';
|
||||
|
||||
export interface StartSyncArgs {
|
||||
connectorId: string;
|
||||
}
|
||||
|
||||
export const startSync = async ({ connectorId }: StartSyncArgs) => {
|
||||
const route = `/internal/enterprise_search/connectors/${connectorId}/start_sync`;
|
||||
return await HttpLogic.values.http.post(route);
|
||||
};
|
||||
|
||||
export const StartSyncApiLogic = createApiLogic(['start_sync_api_logic'], startSync);
|
|
@ -15,6 +15,8 @@ import {
|
|||
EuiContextMenuPanel,
|
||||
EuiContextMenuItem,
|
||||
EuiText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -23,62 +25,82 @@ import { APP_SEARCH_PLUGIN } from '../../../../../../../common/constants';
|
|||
import { ENGINE_CREATION_PATH } from '../../../../../app_search/routes';
|
||||
import { KibanaLogic } from '../../../../../shared/kibana';
|
||||
|
||||
import { IngestionMethod } from '../../../../types';
|
||||
import { IndexViewLogic } from '../../index_view_logic';
|
||||
|
||||
import { HeaderActionsLogic } from './header_actions.logic';
|
||||
import { SyncButton } from './sync_button';
|
||||
|
||||
const SearchEnginesPopover: React.FC = () => {
|
||||
const { isSearchEnginesPopoverOpen } = useValues(HeaderActionsLogic);
|
||||
const { toggleSearchEnginesPopover } = useActions(HeaderActionsLogic);
|
||||
const { ingestionMethod } = useValues(IndexViewLogic);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
isOpen={isSearchEnginesPopoverOpen}
|
||||
closePopover={toggleSearchEnginesPopover}
|
||||
button={
|
||||
<EuiButton iconSide="right" iconType="arrowDown" onClick={toggleSearchEnginesPopover}>
|
||||
{i18n.translate('xpack.enterpriseSearch.content.index.searchEngines.label', {
|
||||
defaultMessage: 'Search Engines',
|
||||
})}
|
||||
</EuiButton>
|
||||
}
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
size="s"
|
||||
items={[
|
||||
<EuiContextMenuItem
|
||||
icon="eye"
|
||||
onClick={() => {
|
||||
KibanaLogic.values.navigateToUrl(APP_SEARCH_PLUGIN.URL, {
|
||||
shouldNotCreateHref: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<EuiText>
|
||||
<p>
|
||||
{i18n.translate('xpack.enterpriseSearch.content.index.searchEngines.viewEngines', {
|
||||
defaultMessage: 'View App Search engines',
|
||||
})}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
icon="plusInCircle"
|
||||
onClick={() => {
|
||||
KibanaLogic.values.navigateToUrl(APP_SEARCH_PLUGIN.URL + ENGINE_CREATION_PATH, {
|
||||
shouldNotCreateHref: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<EuiText>
|
||||
<p>
|
||||
{i18n.translate('xpack.enterpriseSearch.content.index.searchEngines.createEngine', {
|
||||
defaultMessage: 'Create a new App Search engine',
|
||||
})}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiContextMenuItem>,
|
||||
]}
|
||||
/>
|
||||
</EuiPopover>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiPopover
|
||||
isOpen={isSearchEnginesPopoverOpen}
|
||||
closePopover={toggleSearchEnginesPopover}
|
||||
button={
|
||||
<EuiButton iconSide="right" iconType="arrowDown" onClick={toggleSearchEnginesPopover}>
|
||||
{i18n.translate('xpack.enterpriseSearch.content.index.searchEngines.label', {
|
||||
defaultMessage: 'Search Engines',
|
||||
})}
|
||||
</EuiButton>
|
||||
}
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
size="s"
|
||||
items={[
|
||||
<EuiContextMenuItem
|
||||
icon="eye"
|
||||
onClick={() => {
|
||||
KibanaLogic.values.navigateToUrl(APP_SEARCH_PLUGIN.URL, {
|
||||
shouldNotCreateHref: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<EuiText>
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.searchEngines.viewEngines',
|
||||
{
|
||||
defaultMessage: 'View App Search engines',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
icon="plusInCircle"
|
||||
onClick={() => {
|
||||
KibanaLogic.values.navigateToUrl(APP_SEARCH_PLUGIN.URL + ENGINE_CREATION_PATH, {
|
||||
shouldNotCreateHref: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<EuiText>
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.searchEngines.createEngine',
|
||||
{
|
||||
defaultMessage: 'Create a new App Search engine',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiContextMenuItem>,
|
||||
]}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
{ingestionMethod === IngestionMethod.CONNECTOR && (
|
||||
<EuiFlexItem>
|
||||
<SyncButton />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { useValues, useActions } from 'kea';
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { IngestionStatus } from '../../../../types';
|
||||
import { IndexViewLogic } from '../../index_view_logic';
|
||||
|
||||
export const SyncButton: React.FC = () => {
|
||||
const { ingestionStatus, isSyncing, isWaitingForSync } = useValues(IndexViewLogic);
|
||||
const { startSync } = useActions(IndexViewLogic);
|
||||
|
||||
const getSyncButtonText = () => {
|
||||
if (isWaitingForSync) {
|
||||
return i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.syncButton.waitingForSync.label',
|
||||
{
|
||||
defaultMessage: 'Waiting for sync',
|
||||
}
|
||||
);
|
||||
}
|
||||
if (isSyncing && ingestionStatus !== IngestionStatus.ERROR) {
|
||||
return i18n.translate('xpack.enterpriseSearch.content.index.syncButton.syncing.label', {
|
||||
defaultMessage: 'Syncing',
|
||||
});
|
||||
}
|
||||
return i18n.translate('xpack.enterpriseSearch.content.index.syncButton.label', {
|
||||
defaultMessage: 'Sync',
|
||||
});
|
||||
};
|
||||
return (
|
||||
<EuiButton
|
||||
onClick={startSync}
|
||||
fill
|
||||
disabled={ingestionStatus === IngestionStatus.INCOMPLETE}
|
||||
isLoading={
|
||||
// If there's an error, the ingestion status may not be accurate and we may need to be able to trigger a sync
|
||||
(isSyncing && !(ingestionStatus === IngestionStatus.ERROR)) || isWaitingForSync
|
||||
}
|
||||
>
|
||||
{getSyncButtonText()}
|
||||
</EuiButton>
|
||||
);
|
||||
};
|
|
@ -28,6 +28,7 @@ import { EuiButtonTo } from '../../../../shared/react_router_helpers';
|
|||
import { GenerateConnectorApiKeyApiLogic } from '../../../api/connector_package/generate_connector_api_key_api_logic';
|
||||
import { FetchIndexApiLogic } from '../../../api/index/fetch_index_api_logic';
|
||||
import { SEARCH_INDEX_TAB_PATH } from '../../../routes';
|
||||
import { isConnectorIndex } from '../../../utils/indices';
|
||||
import { ApiKey } from '../../api_key/api_key';
|
||||
|
||||
import { IndexNameLogic } from '../index_name_logic';
|
||||
|
@ -41,22 +42,22 @@ export const ConnectorConfiguration: React.FC = () => {
|
|||
const { data: apiKeyData } = useValues(GenerateConnectorApiKeyApiLogic);
|
||||
const { data: indexData } = useValues(FetchIndexApiLogic);
|
||||
const { indexName } = useValues(IndexNameLogic);
|
||||
const indexId = indexData?.connector?.id ?? '';
|
||||
if (!isConnectorIndex(indexData)) {
|
||||
return <></>;
|
||||
}
|
||||
const indexId = indexData.connector.id ?? '';
|
||||
|
||||
const hasApiKey = !!(indexData?.connector?.api_key_id ?? apiKeyData);
|
||||
const hasApiKey = !!(indexData.connector.api_key_id ?? apiKeyData);
|
||||
|
||||
const ConnectorConfig: React.FC = () =>
|
||||
indexData?.connector ? (
|
||||
<ConnectorConfigurationConfig
|
||||
apiKey={apiKeyData?.encoded}
|
||||
configuration={indexData.connector.configuration}
|
||||
connectorId={indexData.connector.id}
|
||||
indexId={indexId}
|
||||
indexName={indexName}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
const ConnectorConfig: React.FC = () => (
|
||||
<ConnectorConfigurationConfig
|
||||
apiKey={apiKeyData?.encoded}
|
||||
configuration={indexData.connector.configuration}
|
||||
connectorId={indexData.connector.id}
|
||||
indexId={indexId}
|
||||
indexName={indexName}
|
||||
/>
|
||||
);
|
||||
|
||||
const ScheduleStep: React.FC = () => (
|
||||
<EuiFlexGroup direction="column">
|
||||
|
@ -145,7 +146,7 @@ export const ConnectorConfiguration: React.FC = () => {
|
|||
children: (
|
||||
<ApiKeyConfig
|
||||
indexName={indexName}
|
||||
hasApiKey={!!indexData?.connector?.api_key_id}
|
||||
hasApiKey={!!indexData.connector.api_key_id}
|
||||
/>
|
||||
),
|
||||
status: hasApiKey ? 'complete' : 'incomplete',
|
||||
|
@ -160,7 +161,7 @@ export const ConnectorConfiguration: React.FC = () => {
|
|||
{
|
||||
children: <ConnectorPackage />,
|
||||
status:
|
||||
!indexData?.connector?.status ||
|
||||
!indexData.connector.status ||
|
||||
indexData.connector.status === ConnectorStatus.CREATED
|
||||
? 'incomplete'
|
||||
: 'complete',
|
||||
|
@ -175,7 +176,6 @@ export const ConnectorConfiguration: React.FC = () => {
|
|||
{
|
||||
children: <ConnectorConfig />,
|
||||
status:
|
||||
indexData?.connector?.status &&
|
||||
indexData.connector.status === ConnectorStatus.CONNECTED
|
||||
? 'complete'
|
||||
: 'incomplete',
|
||||
|
@ -189,7 +189,7 @@ export const ConnectorConfiguration: React.FC = () => {
|
|||
},
|
||||
{
|
||||
children: <ScheduleStep />,
|
||||
status: indexData?.connector?.scheduling.enabled ? 'complete' : 'incomplete',
|
||||
status: indexData.connector.scheduling.enabled ? 'complete' : 'incomplete',
|
||||
title: i18n.translate(
|
||||
'xpack.enterpriseSearch.content.indices.configurationConnector.steps.schedule.title',
|
||||
{
|
||||
|
|
|
@ -68,7 +68,7 @@ export const ConnectorConfigurationConfig: React.FC<ConnectorConfigurationConfig
|
|||
api_key: "${apiKey}"
|
||||
`
|
||||
: ''
|
||||
}connector_package_id: "${connectorId}"
|
||||
}connector_id: "${connectorId}"
|
||||
`}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
|
|
|
@ -19,14 +19,18 @@ import {
|
|||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CronEditor, Frequency } from '@kbn/es-ui-shared-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { Status } from '../../../../../../common/types/api';
|
||||
import { ConnectorIndex } from '../../../../../../common/types/indices';
|
||||
import { UnsavedChangesPrompt } from '../../../../shared/unsaved_changes_prompt';
|
||||
import { UpdateConnectorSchedulingApiLogic } from '../../../api/connector_package/update_connector_scheduling_api_logic';
|
||||
import { FetchIndexApiLogic } from '../../../api/index/fetch_index_api_logic';
|
||||
|
||||
import { isConnectorIndex } from '../../../utils/indices';
|
||||
|
||||
import { ConnectorSchedulingLogic } from './connector_scheduling_logic';
|
||||
|
||||
export const ConnectorSchedulingComponent: React.FC = () => {
|
||||
|
@ -36,7 +40,8 @@ export const ConnectorSchedulingComponent: React.FC = () => {
|
|||
const { hasChanges } = useValues(ConnectorSchedulingLogic);
|
||||
const { setHasChanges } = useActions(ConnectorSchedulingLogic);
|
||||
|
||||
const schedulingInput = data?.connector?.scheduling;
|
||||
// Need to do this ugly casting because we can't check this after the below typecheck, because useState can't be used after an if
|
||||
const schedulingInput = (data as ConnectorIndex)?.connector?.scheduling;
|
||||
const [scheduling, setScheduling] = useState(schedulingInput);
|
||||
const [fieldToPreferredValueMap, setFieldToPreferredValueMap] = useState({});
|
||||
const [simpleCron, setSimpleCron] = useState<{
|
||||
|
@ -47,7 +52,11 @@ export const ConnectorSchedulingComponent: React.FC = () => {
|
|||
frequency: schedulingInput?.interval ? cronToFrequency(schedulingInput.interval) : 'HOUR',
|
||||
});
|
||||
|
||||
const editor = scheduling && (
|
||||
if (!isConnectorIndex(data)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const editor = (
|
||||
<CronEditor
|
||||
fieldToPreferredValueMap={fieldToPreferredValueMap}
|
||||
cronExpression={simpleCron.expression}
|
||||
|
@ -68,7 +77,7 @@ export const ConnectorSchedulingComponent: React.FC = () => {
|
|||
/>
|
||||
);
|
||||
|
||||
return scheduling ? (
|
||||
return (
|
||||
<>
|
||||
<UnsavedChangesPrompt
|
||||
hasUnsavedChanges={hasChanges}
|
||||
|
@ -150,8 +159,6 @@ export const ConnectorSchedulingComponent: React.FC = () => {
|
|||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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 } from '../../../__mocks__/kea_logic';
|
||||
|
||||
import { apiIndex, connectorIndex } from '../../__mocks__/view_index.mock';
|
||||
|
||||
import { nextTick } from '@kbn/test-jest-helpers';
|
||||
|
||||
import { HttpError } from '../../../../../common/types/api';
|
||||
|
||||
import { SyncStatus } from '../../../../../common/types/connectors';
|
||||
import { StartSyncApiLogic } from '../../api/connector_package/start_sync_api_logic';
|
||||
import { FetchIndexApiLogic } from '../../api/index/fetch_index_api_logic';
|
||||
|
||||
import { IngestionMethod, IngestionStatus } from '../../types';
|
||||
|
||||
import { indexToViewIndex } from '../../utils/indices';
|
||||
|
||||
import { IndexViewLogic } from './index_view_logic';
|
||||
|
||||
const DEFAULT_VALUES = {
|
||||
data: undefined,
|
||||
index: undefined,
|
||||
ingestionMethod: IngestionMethod.API,
|
||||
ingestionStatus: IngestionStatus.CONNECTED,
|
||||
isSyncing: false,
|
||||
isWaitingForSync: false,
|
||||
lastUpdated: null,
|
||||
localSyncNowValue: false,
|
||||
syncStatus: undefined,
|
||||
};
|
||||
|
||||
const CONNECTOR_VALUES = {
|
||||
...DEFAULT_VALUES,
|
||||
data: connectorIndex,
|
||||
index: indexToViewIndex(connectorIndex),
|
||||
ingestionMethod: IngestionMethod.CONNECTOR,
|
||||
ingestionStatus: IngestionStatus.INCOMPLETE,
|
||||
lastUpdated: 'never',
|
||||
};
|
||||
|
||||
describe('IndexViewLogic', () => {
|
||||
const { mount: apiLogicMount } = new LogicMounter(StartSyncApiLogic);
|
||||
const { mount: fetchIndexMount } = new LogicMounter(FetchIndexApiLogic);
|
||||
const { mount } = new LogicMounter(IndexViewLogic);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useRealTimers();
|
||||
apiLogicMount();
|
||||
fetchIndexMount();
|
||||
mount();
|
||||
});
|
||||
|
||||
it('has expected default values', () => {
|
||||
expect(IndexViewLogic.values).toEqual(DEFAULT_VALUES);
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
describe('fetchIndex.apiSuccess', () => {
|
||||
it('should update values', () => {
|
||||
FetchIndexApiLogic.actions.apiSuccess({
|
||||
...connectorIndex,
|
||||
connector: { ...connectorIndex.connector!, sync_now: true },
|
||||
});
|
||||
expect(IndexViewLogic.values).toEqual({
|
||||
...CONNECTOR_VALUES,
|
||||
data: {
|
||||
...CONNECTOR_VALUES.data,
|
||||
connector: { ...CONNECTOR_VALUES.data.connector, sync_now: true },
|
||||
},
|
||||
index: {
|
||||
...CONNECTOR_VALUES.index,
|
||||
connector: { ...CONNECTOR_VALUES.index.connector, sync_now: true },
|
||||
},
|
||||
isWaitingForSync: true,
|
||||
syncStatus: SyncStatus.COMPLETED,
|
||||
});
|
||||
});
|
||||
it('should update values with no connector', () => {
|
||||
FetchIndexApiLogic.actions.apiSuccess(apiIndex);
|
||||
expect(IndexViewLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
data: apiIndex,
|
||||
index: apiIndex,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('startSync', () => {
|
||||
it('should call makeRequest', async () => {
|
||||
FetchIndexApiLogic.actions.apiSuccess(connectorIndex);
|
||||
IndexViewLogic.actions.makeRequest = jest.fn();
|
||||
IndexViewLogic.actions.startSync();
|
||||
await nextTick();
|
||||
expect(IndexViewLogic.actions.makeRequest).toHaveBeenCalledWith({
|
||||
connectorId: '2',
|
||||
});
|
||||
expect(IndexViewLogic.values).toEqual({
|
||||
...CONNECTOR_VALUES,
|
||||
syncStatus: SyncStatus.COMPLETED,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('apiSuccess', () => {
|
||||
it('should set localSyncNow to true', async () => {
|
||||
FetchIndexApiLogic.actions.apiSuccess(connectorIndex);
|
||||
StartSyncApiLogic.actions.apiSuccess({});
|
||||
expect(IndexViewLogic.values).toEqual({
|
||||
...CONNECTOR_VALUES,
|
||||
isWaitingForSync: true,
|
||||
localSyncNowValue: true,
|
||||
syncStatus: SyncStatus.COMPLETED,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listeners', () => {
|
||||
it('calls clearFlashMessages on makeRequest', () => {
|
||||
IndexViewLogic.actions.makeRequest({ connectorId: 'connectorId' });
|
||||
expect(mockFlashMessageHelpers.clearFlashMessages).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('calls flashAPIErrors on apiError', () => {
|
||||
IndexViewLogic.actions.apiError({} as HttpError);
|
||||
expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledTimes(1);
|
||||
expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledWith({});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 { kea, MakeLogicType } from 'kea';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { SyncStatus } from '../../../../../common/types/connectors';
|
||||
import { Actions } from '../../../shared/api_logic/create_api_logic';
|
||||
import {
|
||||
flashAPIErrors,
|
||||
clearFlashMessages,
|
||||
flashSuccessToast,
|
||||
} from '../../../shared/flash_messages';
|
||||
import { StartSyncApiLogic, StartSyncArgs } from '../../api/connector_package/start_sync_api_logic';
|
||||
import {
|
||||
FetchIndexApiLogic,
|
||||
FetchIndexApiParams,
|
||||
FetchIndexApiResponse,
|
||||
} from '../../api/index/fetch_index_api_logic';
|
||||
import { ElasticsearchViewIndex, IngestionMethod, IngestionStatus } from '../../types';
|
||||
import {
|
||||
getIngestionMethod,
|
||||
getIngestionStatus,
|
||||
getLastUpdated,
|
||||
indexToViewIndex,
|
||||
isConnectorIndex,
|
||||
} from '../../utils/indices';
|
||||
|
||||
export type IndicesActions = Pick<
|
||||
Actions<StartSyncArgs, {}>,
|
||||
'makeRequest' | 'apiSuccess' | 'apiError'
|
||||
> & {
|
||||
fetchIndexSuccess: Actions<FetchIndexApiParams, FetchIndexApiResponse>['apiSuccess'];
|
||||
startSync(): void;
|
||||
};
|
||||
export interface IndicesValues {
|
||||
data: typeof FetchIndexApiLogic.values.data;
|
||||
index: ElasticsearchViewIndex | undefined;
|
||||
ingestionMethod: IngestionMethod;
|
||||
ingestionStatus: IngestionStatus;
|
||||
isSyncing: boolean;
|
||||
isWaitingForSync: boolean;
|
||||
lastUpdated: string | null;
|
||||
localSyncNowValue: boolean; // holds local value after update so UI updates correctly
|
||||
syncStatus: SyncStatus;
|
||||
}
|
||||
|
||||
export const IndexViewLogic = kea<MakeLogicType<IndicesValues, IndicesActions>>({
|
||||
actions: {
|
||||
startSync: true,
|
||||
},
|
||||
connect: {
|
||||
actions: [StartSyncApiLogic, ['makeRequest', 'apiSuccess', 'apiError']],
|
||||
values: [FetchIndexApiLogic, ['data']],
|
||||
},
|
||||
listeners: ({ actions, values }) => ({
|
||||
apiError: (e) => flashAPIErrors(e),
|
||||
apiSuccess: () => {
|
||||
flashSuccessToast(
|
||||
i18n.translate('xpack.enterpriseSearch.content.searchIndex.index.syncSuccess.message', {
|
||||
defaultMessage: 'Successfully scheduled a sync, waiting for a connector to pick it up',
|
||||
})
|
||||
);
|
||||
},
|
||||
makeRequest: () => clearFlashMessages(),
|
||||
startSync: () => {
|
||||
if (isConnectorIndex(values.data)) {
|
||||
actions.makeRequest({ connectorId: values.data?.connector?.id });
|
||||
}
|
||||
},
|
||||
}),
|
||||
path: ['enterprise_search', 'content', 'view_index_logic'],
|
||||
reducers: {
|
||||
localSyncNowValue: [
|
||||
false,
|
||||
{
|
||||
apiSuccess: () => true,
|
||||
fetchIndexSuccess: (_, index) =>
|
||||
isConnectorIndex(index) ? index.connector.sync_now : false,
|
||||
},
|
||||
],
|
||||
},
|
||||
selectors: ({ selectors }) => ({
|
||||
index: [() => [selectors.data], (data) => (data ? indexToViewIndex(data) : undefined)],
|
||||
ingestionMethod: [() => [selectors.data], (data) => getIngestionMethod(data)],
|
||||
ingestionStatus: [() => [selectors.data], (data) => getIngestionStatus(data)],
|
||||
isSyncing: [
|
||||
() => [selectors.syncStatus],
|
||||
(syncStatus) => syncStatus === SyncStatus.IN_PROGRESS,
|
||||
],
|
||||
isWaitingForSync: [
|
||||
() => [selectors.data, selectors.localSyncNowValue],
|
||||
(data, localSyncNowValue) => data?.connector?.sync_now || localSyncNowValue,
|
||||
],
|
||||
lastUpdated: [() => [selectors.data], (data) => getLastUpdated(data)],
|
||||
syncStatus: [() => [selectors.data], (data) => data?.connector?.last_sync_status],
|
||||
}),
|
||||
});
|
|
@ -13,6 +13,8 @@ import { EuiSpacer } from '@elastic/eui';
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { isApiIndex, isConnectorIndex, isCrawlerIndex } from '../../utils/indices';
|
||||
|
||||
import { CrawlDetailsFlyout } from './crawler/crawl_details_flyout/crawl_details_flyout';
|
||||
import { CrawlRequestsPanel } from './crawler/crawl_requests_panel/crawl_requests_panel';
|
||||
import { CrawlerTotalStats } from './crawler_total_stats';
|
||||
|
@ -23,19 +25,15 @@ import { TotalStats } from './total_stats';
|
|||
export const SearchIndexOverview: React.FC = () => {
|
||||
const { indexData } = useValues(OverviewLogic);
|
||||
|
||||
const isCrawler = typeof indexData?.crawler !== 'undefined';
|
||||
const isConnector = typeof indexData?.connector !== 'undefined';
|
||||
const isApi = !(isCrawler || isConnector);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
{isCrawler ? (
|
||||
{isCrawlerIndex(indexData) ? (
|
||||
<CrawlerTotalStats />
|
||||
) : (
|
||||
<TotalStats
|
||||
ingestionType={
|
||||
isConnector
|
||||
isConnectorIndex(indexData)
|
||||
? i18n.translate(
|
||||
'xpack.enterpriseSearch.content.searchIndex.totalStats.connectorIngestionMethodLabel',
|
||||
{
|
||||
|
@ -51,13 +49,13 @@ export const SearchIndexOverview: React.FC = () => {
|
|||
}
|
||||
/>
|
||||
)}
|
||||
{isApi && (
|
||||
{isApiIndex(indexData) && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<GenerateApiKeyPanel />
|
||||
</>
|
||||
)}
|
||||
{isCrawler && (
|
||||
{isCrawlerIndex(indexData) && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<CrawlRequestsPanel />
|
||||
|
|
|
@ -20,6 +20,7 @@ import { generateEncodedPath } from '../../../shared/encode_path_params';
|
|||
import { KibanaLogic } from '../../../shared/kibana';
|
||||
import { FetchIndexApiLogic } from '../../api/index/fetch_index_api_logic';
|
||||
import { SEARCH_INDEX_PATH, SEARCH_INDEX_TAB_PATH } from '../../routes';
|
||||
import { isConnectorIndex, isCrawlerIndex } from '../../utils/indices';
|
||||
import { EnterpriseSearchContentPageTemplate } from '../layout/page_template';
|
||||
|
||||
import { baseBreadcrumbs } from '../search_indices';
|
||||
|
@ -125,8 +126,8 @@ export const SearchIndex: React.FC = () => {
|
|||
|
||||
const tabs: EuiTabbedContentTab[] = [
|
||||
...ALL_INDICES_TABS,
|
||||
...(indexData?.connector ? CONNECTOR_TABS : []),
|
||||
...(indexData?.crawler ? CRAWLER_TABS : []),
|
||||
...(isConnectorIndex(indexData) ? CONNECTOR_TABS : []),
|
||||
...(isCrawlerIndex(indexData) ? CRAWLER_TABS : []),
|
||||
];
|
||||
|
||||
const selectedTab = tabs.find((tab) => tab.id === tabId);
|
||||
|
@ -151,14 +152,14 @@ export const SearchIndex: React.FC = () => {
|
|||
pageTitle: indexName,
|
||||
rightSideItems: [
|
||||
...headerActions,
|
||||
...(indexData?.crawler ? [<CrawlerStatusIndicator />] : []),
|
||||
...(isCrawlerIndex(indexData) ? [<CrawlerStatusIndicator />] : []),
|
||||
],
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{isCalloutVisible && <IndexCreatedCallout indexName={indexName} />}
|
||||
<EuiTabbedContent tabs={tabs} selectedTab={selectedTab} onTabClick={onTabClick} />
|
||||
{indexData?.crawler && <CrawlCustomSettingsFlyout />}
|
||||
{isCrawlerIndex(indexData) && <CrawlCustomSettingsFlyout />}
|
||||
</>
|
||||
</EnterpriseSearchContentPageTemplate>
|
||||
);
|
||||
|
|
|
@ -9,6 +9,8 @@ import { LogicMounter, mockFlashMessageHelpers } from '../../../__mocks__/kea_lo
|
|||
|
||||
import { indices } from '../../__mocks__/search_indices.mock';
|
||||
|
||||
import { connectorIndex, elasticsearchViewIndices } from '../../__mocks__/view_index.mock';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
import { nextTick } from '@kbn/test-jest-helpers';
|
||||
|
@ -20,7 +22,9 @@ import { DEFAULT_META } from '../../../shared/constants';
|
|||
|
||||
import { FetchIndicesAPILogic } from '../../api/index/fetch_indices_api_logic';
|
||||
|
||||
import { IndicesLogic, IngestionMethod, IngestionStatus, ViewSearchIndex } from './indices_logic';
|
||||
import { IngestionStatus } from '../../types';
|
||||
|
||||
import { IndicesLogic } from './indices_logic';
|
||||
|
||||
const DEFAULT_VALUES = {
|
||||
data: undefined,
|
||||
|
@ -31,69 +35,6 @@ const DEFAULT_VALUES = {
|
|||
status: Status.IDLE,
|
||||
};
|
||||
|
||||
const apiIndex: ViewSearchIndex = {
|
||||
ingestionMethod: IngestionMethod.API,
|
||||
ingestionStatus: IngestionStatus.CONNECTED,
|
||||
lastUpdated: null,
|
||||
name: 'api',
|
||||
total: {
|
||||
docs: {
|
||||
count: 1,
|
||||
deleted: 0,
|
||||
},
|
||||
store: { size_in_bytes: '8024' },
|
||||
},
|
||||
};
|
||||
const connectorIndex: ViewSearchIndex = {
|
||||
connector: {
|
||||
api_key_id: null,
|
||||
configuration: {},
|
||||
id: '2',
|
||||
index_name: 'connector',
|
||||
last_seen: null,
|
||||
last_sync_error: null,
|
||||
last_sync_status: SyncStatus.COMPLETED,
|
||||
last_synced: null,
|
||||
scheduling: {
|
||||
enabled: false,
|
||||
interval: '',
|
||||
},
|
||||
service_type: null,
|
||||
status: ConnectorStatus.CONFIGURED,
|
||||
sync_now: false,
|
||||
},
|
||||
ingestionMethod: IngestionMethod.CONNECTOR,
|
||||
ingestionStatus: IngestionStatus.INCOMPLETE,
|
||||
lastUpdated: 'never',
|
||||
name: 'connector',
|
||||
total: {
|
||||
docs: {
|
||||
count: 1,
|
||||
deleted: 0,
|
||||
},
|
||||
store: { size_in_bytes: '8024' },
|
||||
},
|
||||
};
|
||||
const crawlerIndex: ViewSearchIndex = {
|
||||
crawler: {
|
||||
id: '3',
|
||||
index_name: 'crawler',
|
||||
},
|
||||
ingestionMethod: IngestionMethod.CRAWLER,
|
||||
ingestionStatus: IngestionStatus.INCOMPLETE,
|
||||
lastUpdated: null,
|
||||
name: 'crawler',
|
||||
total: {
|
||||
docs: {
|
||||
count: 1,
|
||||
deleted: 0,
|
||||
},
|
||||
store: { size_in_bytes: '8024' },
|
||||
},
|
||||
};
|
||||
|
||||
const viewSearchIndices = [apiIndex, connectorIndex, crawlerIndex];
|
||||
|
||||
describe('IndicesLogic', () => {
|
||||
const { mount: apiLogicMount } = new LogicMounter(FetchIndicesAPILogic);
|
||||
const { mount } = new LogicMounter(IndicesLogic);
|
||||
|
@ -150,7 +91,7 @@ describe('IndicesLogic', () => {
|
|||
meta: newMeta,
|
||||
},
|
||||
hasNoIndices: false,
|
||||
indices: viewSearchIndices,
|
||||
indices: elasticsearchViewIndices,
|
||||
isLoading: false,
|
||||
meta: newMeta,
|
||||
status: Status.SUCCESS,
|
||||
|
@ -275,19 +216,19 @@ describe('IndicesLogic', () => {
|
|||
it('updates when apiSuccess listener triggered', () => {
|
||||
expect(IndicesLogic.values).toEqual(DEFAULT_VALUES);
|
||||
IndicesLogic.actions.apiSuccess({
|
||||
indices: viewSearchIndices,
|
||||
indices: elasticsearchViewIndices,
|
||||
isInitialRequest: true,
|
||||
meta: DEFAULT_META,
|
||||
});
|
||||
|
||||
expect(IndicesLogic.values).toEqual({
|
||||
data: {
|
||||
indices: viewSearchIndices,
|
||||
indices: elasticsearchViewIndices,
|
||||
isInitialRequest: true,
|
||||
meta: DEFAULT_META,
|
||||
},
|
||||
hasNoIndices: false,
|
||||
indices: viewSearchIndices,
|
||||
indices: elasticsearchViewIndices,
|
||||
isLoading: false,
|
||||
meta: DEFAULT_META,
|
||||
status: Status.SUCCESS,
|
||||
|
@ -300,9 +241,9 @@ describe('IndicesLogic', () => {
|
|||
IndicesLogic.actions.apiSuccess({
|
||||
indices: [
|
||||
{
|
||||
...indices[1],
|
||||
...connectorIndex,
|
||||
connector: {
|
||||
...indices[1].connector!,
|
||||
...connectorIndex.connector!,
|
||||
last_seen: lastSeen,
|
||||
status: ConnectorStatus.CONNECTED,
|
||||
},
|
||||
|
@ -316,9 +257,9 @@ describe('IndicesLogic', () => {
|
|||
data: {
|
||||
indices: [
|
||||
{
|
||||
...indices[1],
|
||||
...connectorIndex,
|
||||
connector: {
|
||||
...indices[1].connector!,
|
||||
...connectorIndex.connector!,
|
||||
last_seen: lastSeen,
|
||||
status: ConnectorStatus.CONNECTED,
|
||||
},
|
||||
|
@ -349,8 +290,8 @@ describe('IndicesLogic', () => {
|
|||
IndicesLogic.actions.apiSuccess({
|
||||
indices: [
|
||||
{
|
||||
...indices[1],
|
||||
connector: { ...indices[1].connector!, status: ConnectorStatus.CONNECTED },
|
||||
...connectorIndex,
|
||||
connector: { ...connectorIndex.connector, status: ConnectorStatus.CONNECTED },
|
||||
},
|
||||
],
|
||||
isInitialRequest: true,
|
||||
|
@ -361,8 +302,8 @@ describe('IndicesLogic', () => {
|
|||
data: {
|
||||
indices: [
|
||||
{
|
||||
...indices[1],
|
||||
connector: { ...indices[1].connector!, status: ConnectorStatus.CONNECTED },
|
||||
...connectorIndex,
|
||||
connector: { ...connectorIndex.connector, status: ConnectorStatus.CONNECTED },
|
||||
},
|
||||
],
|
||||
isInitialRequest: true,
|
||||
|
@ -389,8 +330,8 @@ describe('IndicesLogic', () => {
|
|||
IndicesLogic.actions.apiSuccess({
|
||||
indices: [
|
||||
{
|
||||
...indices[1],
|
||||
connector: { ...indices[1].connector!, status: ConnectorStatus.ERROR },
|
||||
...connectorIndex,
|
||||
connector: { ...connectorIndex.connector!, status: ConnectorStatus.ERROR },
|
||||
},
|
||||
],
|
||||
isInitialRequest: true,
|
||||
|
@ -401,8 +342,8 @@ describe('IndicesLogic', () => {
|
|||
data: {
|
||||
indices: [
|
||||
{
|
||||
...indices[1],
|
||||
connector: { ...indices[1].connector!, status: ConnectorStatus.ERROR },
|
||||
...connectorIndex,
|
||||
connector: { ...connectorIndex.connector!, status: ConnectorStatus.ERROR },
|
||||
},
|
||||
],
|
||||
isInitialRequest: true,
|
||||
|
@ -426,9 +367,9 @@ describe('IndicesLogic', () => {
|
|||
IndicesLogic.actions.apiSuccess({
|
||||
indices: [
|
||||
{
|
||||
...indices[1],
|
||||
...connectorIndex,
|
||||
connector: {
|
||||
...indices[1].connector!,
|
||||
...connectorIndex.connector!,
|
||||
last_sync_status: SyncStatus.ERROR,
|
||||
status: ConnectorStatus.CONNECTED,
|
||||
},
|
||||
|
@ -442,9 +383,9 @@ describe('IndicesLogic', () => {
|
|||
data: {
|
||||
indices: [
|
||||
{
|
||||
...indices[1],
|
||||
...connectorIndex,
|
||||
connector: {
|
||||
...indices[1].connector!,
|
||||
...connectorIndex.connector!,
|
||||
last_sync_status: SyncStatus.ERROR,
|
||||
status: ConnectorStatus.CONNECTED,
|
||||
},
|
||||
|
|
|
@ -6,72 +6,16 @@
|
|||
*/
|
||||
|
||||
import { kea, MakeLogicType } from 'kea';
|
||||
import moment from 'moment';
|
||||
|
||||
import { Meta } from '../../../../../common/types';
|
||||
import { HttpError, Status } from '../../../../../common/types/api';
|
||||
import { ConnectorStatus, SyncStatus } from '../../../../../common/types/connectors';
|
||||
import { ElasticsearchIndexWithIngestion } from '../../../../../common/types/indices';
|
||||
import { DEFAULT_META } from '../../../shared/constants';
|
||||
import { flashAPIErrors, clearFlashMessages } from '../../../shared/flash_messages';
|
||||
import { updateMetaPageIndex } from '../../../shared/table_pagination';
|
||||
import { FetchIndicesAPILogic } from '../../api/index/fetch_indices_api_logic';
|
||||
|
||||
export const enum IngestionMethod {
|
||||
CONNECTOR,
|
||||
CRAWLER,
|
||||
API,
|
||||
}
|
||||
|
||||
export const enum IngestionStatus {
|
||||
CONNECTED,
|
||||
ERROR,
|
||||
SYNC_ERROR,
|
||||
INCOMPLETE,
|
||||
}
|
||||
|
||||
export interface ViewSearchIndex extends ElasticsearchIndexWithIngestion {
|
||||
ingestionMethod: IngestionMethod;
|
||||
ingestionStatus: IngestionStatus;
|
||||
lastUpdated: Date | 'never' | null;
|
||||
}
|
||||
|
||||
function getIngestionMethod(index?: ElasticsearchIndexWithIngestion): IngestionMethod {
|
||||
if (index?.connector) {
|
||||
return IngestionMethod.CONNECTOR;
|
||||
}
|
||||
if (index?.crawler) {
|
||||
return IngestionMethod.CRAWLER;
|
||||
}
|
||||
return IngestionMethod.API;
|
||||
}
|
||||
|
||||
function getIngestionStatus(
|
||||
index: ElasticsearchIndexWithIngestion,
|
||||
ingestionMethod: IngestionMethod
|
||||
): IngestionStatus {
|
||||
if (ingestionMethod === IngestionMethod.API) {
|
||||
return IngestionStatus.CONNECTED;
|
||||
}
|
||||
if (ingestionMethod === IngestionMethod.CONNECTOR) {
|
||||
if (
|
||||
index.connector?.last_seen &&
|
||||
moment(index.connector.last_seen).isBefore(moment().subtract(30, 'minutes'))
|
||||
) {
|
||||
return IngestionStatus.ERROR;
|
||||
}
|
||||
if (index.connector?.last_sync_status === SyncStatus.ERROR) {
|
||||
return IngestionStatus.SYNC_ERROR;
|
||||
}
|
||||
if (index.connector?.status === ConnectorStatus.CONNECTED) {
|
||||
return IngestionStatus.CONNECTED;
|
||||
}
|
||||
if (index.connector?.status === ConnectorStatus.ERROR) {
|
||||
return IngestionStatus.ERROR;
|
||||
}
|
||||
}
|
||||
return IngestionStatus.INCOMPLETE;
|
||||
}
|
||||
import { ElasticsearchViewIndex } from '../../types';
|
||||
import { indexToViewIndex } from '../../utils/indices';
|
||||
|
||||
export interface IndicesActions {
|
||||
apiError(error: HttpError): HttpError;
|
||||
|
@ -103,7 +47,7 @@ export interface IndicesActions {
|
|||
export interface IndicesValues {
|
||||
data: typeof FetchIndicesAPILogic.values.data;
|
||||
hasNoIndices: boolean;
|
||||
indices: ViewSearchIndex[];
|
||||
indices: ElasticsearchViewIndex[];
|
||||
isLoading: boolean;
|
||||
meta: Meta;
|
||||
status: typeof FetchIndicesAPILogic.values.status;
|
||||
|
@ -149,15 +93,7 @@ export const IndicesLogic = kea<MakeLogicType<IndicesValues, IndicesActions>>({
|
|||
],
|
||||
indices: [
|
||||
() => [selectors.data],
|
||||
(data) =>
|
||||
data?.indices
|
||||
? data.indices.map((index: ElasticsearchIndexWithIngestion) => ({
|
||||
...index,
|
||||
ingestionMethod: getIngestionMethod(index),
|
||||
ingestionStatus: getIngestionStatus(index, getIngestionMethod(index)),
|
||||
lastUpdated: index.connector ? index.connector.last_synced ?? 'never' : null,
|
||||
}))
|
||||
: [],
|
||||
(data) => (data?.indices ? data.indices.map(indexToViewIndex) : []),
|
||||
],
|
||||
isLoading: [
|
||||
() => [selectors.status],
|
||||
|
|
|
@ -24,8 +24,8 @@ import { Meta } from '../../../../../common/types';
|
|||
import { EuiLinkTo, EuiButtonIconTo } from '../../../shared/react_router_helpers';
|
||||
import { convertMetaToPagination } from '../../../shared/table_pagination';
|
||||
import { SEARCH_INDEX_PATH } from '../../routes';
|
||||
|
||||
import { ViewSearchIndex, IngestionMethod, IngestionStatus } from './indices_logic';
|
||||
import { ElasticsearchViewIndex, IngestionMethod, IngestionStatus } from '../../types';
|
||||
import { ingestionMethodToText } from '../../utils/indices';
|
||||
|
||||
const healthColorsMap = {
|
||||
green: 'success',
|
||||
|
@ -34,29 +34,7 @@ const healthColorsMap = {
|
|||
yellow: 'warning',
|
||||
};
|
||||
|
||||
function ingestionMethodToText(ingestionMethod: IngestionMethod) {
|
||||
if (ingestionMethod === IngestionMethod.CONNECTOR) {
|
||||
return i18n.translate(
|
||||
'xpack.enterpriseSearch.content.searchIndices.ingestionMethod.connector.label',
|
||||
{
|
||||
defaultMessage: 'Connector',
|
||||
}
|
||||
);
|
||||
}
|
||||
if (ingestionMethod === IngestionMethod.CRAWLER) {
|
||||
return i18n.translate(
|
||||
'xpack.enterpriseSearch.content.searchIndices.ingestionMethod.crawler.label',
|
||||
{
|
||||
defaultMessage: 'Crawler',
|
||||
}
|
||||
);
|
||||
}
|
||||
return i18n.translate('xpack.enterpriseSearch.content.searchIndices.ingestionMethod.api.label', {
|
||||
defaultMessage: 'API',
|
||||
});
|
||||
}
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<ViewSearchIndex>> = [
|
||||
const columns: Array<EuiBasicTableColumn<ElasticsearchViewIndex>> = [
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.name.columnTitle', {
|
||||
|
@ -207,10 +185,10 @@ const columns: Array<EuiBasicTableColumn<ViewSearchIndex>> = [
|
|||
];
|
||||
|
||||
interface IndicesTableProps {
|
||||
indices: ViewSearchIndex[];
|
||||
indices: ElasticsearchViewIndex[];
|
||||
isLoading: boolean;
|
||||
meta: Meta;
|
||||
onChange: (criteria: CriteriaWithPagination<ViewSearchIndex>) => void;
|
||||
onChange: (criteria: CriteriaWithPagination<ElasticsearchViewIndex>) => void;
|
||||
}
|
||||
|
||||
export const IndicesTable: React.FC<IndicesTableProps> = ({
|
||||
|
|
|
@ -5,6 +5,35 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConnectorIndex, CrawlerIndex, ElasticsearchIndex } from '../../../common/types/indices';
|
||||
|
||||
export interface Crawler {
|
||||
domains: [];
|
||||
}
|
||||
|
||||
export const enum IngestionMethod {
|
||||
CONNECTOR,
|
||||
CRAWLER,
|
||||
API,
|
||||
}
|
||||
|
||||
export const enum IngestionStatus {
|
||||
CONNECTED,
|
||||
ERROR,
|
||||
SYNC_ERROR,
|
||||
INCOMPLETE,
|
||||
}
|
||||
|
||||
interface ElasticsearchViewIndexExtension {
|
||||
ingestionMethod: IngestionMethod;
|
||||
ingestionStatus: IngestionStatus;
|
||||
lastUpdated: string | 'never' | null; // date string
|
||||
}
|
||||
|
||||
export type ConnectorViewIndex = ConnectorIndex & ElasticsearchViewIndexExtension;
|
||||
|
||||
export type CrawlerViewIndex = CrawlerIndex & ElasticsearchViewIndexExtension;
|
||||
|
||||
export type ApiViewIndex = ElasticsearchIndex & ElasticsearchViewIndexExtension;
|
||||
|
||||
export type ElasticsearchViewIndex = CrawlerViewIndex | ConnectorViewIndex | ApiViewIndex;
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* 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 { connectorIndex, crawlerIndex, apiIndex } from '../__mocks__/view_index.mock';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
import { ConnectorStatus, SyncStatus } from '../../../../common/types/connectors';
|
||||
import { IngestionMethod, IngestionStatus } from '../types';
|
||||
|
||||
import {
|
||||
getIngestionMethod,
|
||||
getIngestionStatus,
|
||||
getLastUpdated,
|
||||
indexToViewIndex,
|
||||
} from './indices';
|
||||
|
||||
describe('Indices util functions', () => {
|
||||
describe('getIngestionMethod', () => {
|
||||
it('should return correct ingestion method for connector', () => {
|
||||
expect(getIngestionMethod(connectorIndex)).toEqual(IngestionMethod.CONNECTOR);
|
||||
});
|
||||
it('should return correct ingestion method for crawler', () => {
|
||||
expect(getIngestionMethod(crawlerIndex)).toEqual(IngestionMethod.CRAWLER);
|
||||
});
|
||||
it('should return correct ingestion method for API', () => {
|
||||
expect(getIngestionMethod(apiIndex)).toEqual(IngestionMethod.API);
|
||||
});
|
||||
it('should return API for undefined index', () => {
|
||||
expect(getIngestionMethod(undefined)).toEqual(IngestionMethod.API);
|
||||
});
|
||||
});
|
||||
describe('getIngestionStatus', () => {
|
||||
it('should return connected for API', () => {
|
||||
expect(getIngestionStatus(apiIndex)).toEqual(IngestionStatus.CONNECTED);
|
||||
});
|
||||
it('should return connected for undefined', () => {
|
||||
expect(getIngestionStatus(undefined)).toEqual(IngestionStatus.CONNECTED);
|
||||
});
|
||||
it('should return incomplete for incomplete connector', () => {
|
||||
expect(getIngestionStatus(connectorIndex)).toEqual(IngestionStatus.INCOMPLETE);
|
||||
});
|
||||
it('should return connected for complete connector', () => {
|
||||
expect(
|
||||
getIngestionStatus({
|
||||
...connectorIndex,
|
||||
connector: { ...connectorIndex.connector, status: ConnectorStatus.CONNECTED },
|
||||
})
|
||||
).toEqual(IngestionStatus.CONNECTED);
|
||||
});
|
||||
it('should return error for connector that last checked in more than 30 minutes ago', () => {
|
||||
const lastSeen = moment().subtract(31, 'minutes').format();
|
||||
expect(
|
||||
getIngestionStatus({
|
||||
...connectorIndex,
|
||||
connector: {
|
||||
...connectorIndex.connector,
|
||||
last_seen: lastSeen,
|
||||
status: ConnectorStatus.CONNECTED,
|
||||
},
|
||||
})
|
||||
).toEqual(IngestionStatus.ERROR);
|
||||
});
|
||||
it('should return sync error for complete connector with sync error', () => {
|
||||
expect(
|
||||
getIngestionStatus({
|
||||
...connectorIndex,
|
||||
connector: {
|
||||
...connectorIndex.connector,
|
||||
last_sync_status: SyncStatus.ERROR,
|
||||
status: ConnectorStatus.NEEDS_CONFIGURATION,
|
||||
},
|
||||
})
|
||||
).toEqual(IngestionStatus.SYNC_ERROR);
|
||||
});
|
||||
it('should return error for connector with error', () => {
|
||||
expect(
|
||||
getIngestionStatus({
|
||||
...connectorIndex,
|
||||
connector: {
|
||||
...connectorIndex.connector,
|
||||
last_sync_status: SyncStatus.COMPLETED,
|
||||
status: ConnectorStatus.ERROR,
|
||||
},
|
||||
})
|
||||
).toEqual(IngestionStatus.ERROR);
|
||||
});
|
||||
});
|
||||
describe('getLastUpdated', () => {
|
||||
it('should return never for connector with no last updated time', () => {
|
||||
expect(getLastUpdated(connectorIndex)).toEqual('never');
|
||||
});
|
||||
it('should return last_synced for connector with no last updated time', () => {
|
||||
expect(
|
||||
getLastUpdated({
|
||||
...connectorIndex,
|
||||
connector: { ...connectorIndex.connector, last_synced: 'last_synced' },
|
||||
})
|
||||
).toEqual('last_synced');
|
||||
});
|
||||
it('should return null for api', () => {
|
||||
expect(getLastUpdated(apiIndex)).toEqual(null);
|
||||
});
|
||||
});
|
||||
describe('indexToViewIndex', () => {
|
||||
it('should apply above transformations to viewIndex', () => {
|
||||
expect(indexToViewIndex(connectorIndex)).toEqual({
|
||||
...connectorIndex,
|
||||
ingestionMethod: getIngestionMethod(connectorIndex),
|
||||
ingestionStatus: getIngestionStatus(connectorIndex),
|
||||
lastUpdated: getLastUpdated(connectorIndex),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { SyncStatus, ConnectorStatus } from '../../../../common/types/connectors';
|
||||
import {
|
||||
ConnectorIndex,
|
||||
CrawlerIndex,
|
||||
ElasticsearchIndexWithIngestion,
|
||||
} from '../../../../common/types/indices';
|
||||
|
||||
import {
|
||||
ApiViewIndex,
|
||||
ConnectorViewIndex,
|
||||
CrawlerViewIndex,
|
||||
ElasticsearchViewIndex,
|
||||
IngestionMethod,
|
||||
IngestionStatus,
|
||||
} from '../types';
|
||||
|
||||
export function isConnectorIndex(
|
||||
index: ElasticsearchIndexWithIngestion | undefined
|
||||
): index is ConnectorIndex {
|
||||
return !!(index as ConnectorIndex)?.connector;
|
||||
}
|
||||
|
||||
export function isCrawlerIndex(
|
||||
index: ElasticsearchIndexWithIngestion | undefined
|
||||
): index is CrawlerIndex {
|
||||
return !!(index as CrawlerIndex)?.crawler;
|
||||
}
|
||||
|
||||
export function isApiIndex(index: ElasticsearchIndexWithIngestion | undefined): boolean {
|
||||
if (!index) {
|
||||
return false;
|
||||
}
|
||||
return !isConnectorIndex(index) && !isCrawlerIndex(index);
|
||||
}
|
||||
|
||||
export function isConnectorViewIndex(index: ElasticsearchViewIndex): index is ConnectorViewIndex {
|
||||
return !!(index as ConnectorViewIndex)?.connector;
|
||||
}
|
||||
|
||||
export function isCrawlerViewIndex(index: ElasticsearchViewIndex): index is CrawlerViewIndex {
|
||||
return !!(index as CrawlerViewIndex)?.crawler;
|
||||
}
|
||||
|
||||
export function isApiViewIndex(index: ElasticsearchViewIndex): index is ApiViewIndex {
|
||||
return !!index && !isConnectorViewIndex(index) && !isCrawlerViewIndex(index);
|
||||
}
|
||||
|
||||
export function getIngestionMethod(index?: ElasticsearchIndexWithIngestion): IngestionMethod {
|
||||
if (!index) return IngestionMethod.API;
|
||||
if (isConnectorIndex(index)) {
|
||||
return IngestionMethod.CONNECTOR;
|
||||
}
|
||||
if (isCrawlerIndex(index)) {
|
||||
return IngestionMethod.CRAWLER;
|
||||
}
|
||||
return IngestionMethod.API;
|
||||
}
|
||||
|
||||
export function getIngestionStatus(index?: ElasticsearchIndexWithIngestion): IngestionStatus {
|
||||
if (!index || isApiIndex(index)) {
|
||||
return IngestionStatus.CONNECTED;
|
||||
}
|
||||
if (isConnectorIndex(index)) {
|
||||
if (
|
||||
index.connector.last_seen &&
|
||||
moment(index.connector.last_seen).isBefore(moment().subtract(30, 'minutes'))
|
||||
) {
|
||||
return IngestionStatus.ERROR;
|
||||
}
|
||||
if (index.connector.last_sync_status === SyncStatus.ERROR) {
|
||||
return IngestionStatus.SYNC_ERROR;
|
||||
}
|
||||
if (index.connector.status === ConnectorStatus.CONNECTED) {
|
||||
return IngestionStatus.CONNECTED;
|
||||
}
|
||||
if (index.connector.status === ConnectorStatus.ERROR) {
|
||||
return IngestionStatus.ERROR;
|
||||
}
|
||||
}
|
||||
return IngestionStatus.INCOMPLETE;
|
||||
}
|
||||
|
||||
export function getLastUpdated(index?: ElasticsearchIndexWithIngestion): string | null {
|
||||
return isConnectorIndex(index) ? index.connector.last_synced ?? 'never' : null;
|
||||
}
|
||||
|
||||
export function indexToViewIndex(index: ConnectorIndex): ConnectorViewIndex;
|
||||
export function indexToViewIndex(index: CrawlerIndex): CrawlerViewIndex;
|
||||
export function indexToViewIndex(index: ElasticsearchIndexWithIngestion): ApiViewIndex {
|
||||
const extraFields = {
|
||||
ingestionMethod: getIngestionMethod(index),
|
||||
ingestionStatus: getIngestionStatus(index),
|
||||
lastUpdated: getLastUpdated(index),
|
||||
};
|
||||
if (isConnectorIndex(index)) {
|
||||
const connectorResult: ConnectorViewIndex = { ...index, ...extraFields };
|
||||
return connectorResult;
|
||||
}
|
||||
if (isCrawlerIndex(index)) {
|
||||
const crawlerResult: CrawlerViewIndex = { ...index, ...extraFields };
|
||||
return crawlerResult;
|
||||
}
|
||||
const apiResult: ApiViewIndex = { ...index, ...extraFields };
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
export function ingestionMethodToText(ingestionMethod: IngestionMethod) {
|
||||
if (ingestionMethod === IngestionMethod.CONNECTOR) {
|
||||
return i18n.translate(
|
||||
'xpack.enterpriseSearch.content.searchIndices.ingestionMethod.connector',
|
||||
{
|
||||
defaultMessage: 'Connector',
|
||||
}
|
||||
);
|
||||
}
|
||||
if (ingestionMethod === IngestionMethod.CRAWLER) {
|
||||
return i18n.translate('xpack.enterpriseSearch.content.searchIndices.ingestionMethod.crawler', {
|
||||
defaultMessage: 'Crawler',
|
||||
});
|
||||
}
|
||||
return i18n.translate('xpack.enterpriseSearch.content.searchIndices.ingestionMethod.api', {
|
||||
defaultMessage: 'API',
|
||||
});
|
||||
}
|
|
@ -72,7 +72,7 @@ describe('Setup Indices', () => {
|
|||
},
|
||||
indexed_document_count: { type: 'integer' },
|
||||
status: {
|
||||
type: 'object',
|
||||
type: 'keyword',
|
||||
},
|
||||
worker_hostname: { type: 'keyword' },
|
||||
},
|
||||
|
|
|
@ -78,7 +78,7 @@ const indices: IndexDefinition[] = [
|
|||
},
|
||||
indexed_document_count: { type: 'integer' },
|
||||
status: {
|
||||
type: 'object',
|
||||
type: 'keyword',
|
||||
},
|
||||
worker_hostname: { type: 'keyword' },
|
||||
},
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 { IScopedClusterClient } from '@kbn/core/server';
|
||||
|
||||
import { CONNECTORS_INDEX } from '../..';
|
||||
import { ErrorCode } from '../../../common/types/error_codes';
|
||||
|
||||
import { startConnectorSync } from './start_sync';
|
||||
|
||||
describe('addConnector lib function', () => {
|
||||
const mockClient = {
|
||||
asCurrentUser: {
|
||||
get: jest.fn(),
|
||||
index: jest.fn(),
|
||||
indices: {
|
||||
refresh: jest.fn(),
|
||||
},
|
||||
},
|
||||
asInternalUser: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should start a sync', async () => {
|
||||
mockClient.asCurrentUser.get.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
_source: {
|
||||
api_key_id: null,
|
||||
configuration: {},
|
||||
created_at: null,
|
||||
index_name: 'index_name',
|
||||
last_seen: null,
|
||||
last_sync_error: null,
|
||||
last_sync_status: null,
|
||||
last_synced: null,
|
||||
scheduling: { enabled: true, interval: '1 2 3 4 5' },
|
||||
service_type: null,
|
||||
status: 'not connected',
|
||||
sync_now: false,
|
||||
},
|
||||
index: CONNECTORS_INDEX,
|
||||
});
|
||||
});
|
||||
mockClient.asCurrentUser.index.mockImplementation(() => ({ _id: 'fakeId' }));
|
||||
|
||||
await expect(
|
||||
startConnectorSync(mockClient as unknown as IScopedClusterClient, 'connectorId')
|
||||
).resolves.toEqual({ _id: 'fakeId' });
|
||||
expect(mockClient.asCurrentUser.index).toHaveBeenCalledWith({
|
||||
document: {
|
||||
api_key_id: null,
|
||||
configuration: {},
|
||||
created_at: null,
|
||||
index_name: 'index_name',
|
||||
last_seen: null,
|
||||
last_sync_error: null,
|
||||
last_sync_status: null,
|
||||
last_synced: null,
|
||||
scheduling: { enabled: true, interval: '1 2 3 4 5' },
|
||||
service_type: null,
|
||||
status: 'not connected',
|
||||
sync_now: true,
|
||||
},
|
||||
id: 'connectorId',
|
||||
index: CONNECTORS_INDEX,
|
||||
});
|
||||
expect(mockClient.asCurrentUser.indices.refresh).toHaveBeenCalledWith({
|
||||
index: CONNECTORS_INDEX,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not create index if there is no connector', async () => {
|
||||
mockClient.asCurrentUser.get.mockImplementationOnce(() => {
|
||||
return Promise.resolve({});
|
||||
});
|
||||
await expect(
|
||||
startConnectorSync(mockClient as unknown as IScopedClusterClient, 'connectorId')
|
||||
).rejects.toEqual(new Error(ErrorCode.RESOURCE_NOT_FOUND));
|
||||
expect(mockClient.asCurrentUser.index).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { IScopedClusterClient } from '@kbn/core/server';
|
||||
|
||||
import { CONNECTORS_INDEX } from '../..';
|
||||
|
||||
import { ConnectorDocument } from '../../../common/types/connectors';
|
||||
import { ErrorCode } from '../../../common/types/error_codes';
|
||||
|
||||
export const startConnectorSync = async (client: IScopedClusterClient, connectorId: string) => {
|
||||
const connectorResult = await client.asCurrentUser.get<ConnectorDocument>({
|
||||
id: connectorId,
|
||||
index: CONNECTORS_INDEX,
|
||||
});
|
||||
const connector = connectorResult._source;
|
||||
if (connector) {
|
||||
const result = await client.asCurrentUser.index<ConnectorDocument>({
|
||||
document: { ...connector, sync_now: true },
|
||||
id: connectorId,
|
||||
index: CONNECTORS_INDEX,
|
||||
});
|
||||
await client.asCurrentUser.indices.refresh({ index: CONNECTORS_INDEX });
|
||||
return result;
|
||||
} else {
|
||||
throw new Error(ErrorCode.RESOURCE_NOT_FOUND);
|
||||
}
|
||||
};
|
|
@ -16,6 +16,9 @@ describe('addConnector lib function', () => {
|
|||
asCurrentUser: {
|
||||
get: jest.fn(),
|
||||
index: jest.fn(),
|
||||
indices: {
|
||||
refresh: jest.fn(),
|
||||
},
|
||||
},
|
||||
asInternalUser: {},
|
||||
};
|
||||
|
@ -36,7 +39,7 @@ describe('addConnector lib function', () => {
|
|||
last_sync_error: null,
|
||||
last_sync_status: null,
|
||||
last_synced: null,
|
||||
scheduling: { enabled: true, interval: '1 2 3 4 5' },
|
||||
scheduling: { enabled: false, interval: '* * * * *' },
|
||||
service_type: null,
|
||||
status: 'not connected',
|
||||
sync_now: false,
|
||||
|
@ -70,9 +73,12 @@ describe('addConnector lib function', () => {
|
|||
id: 'connectorId',
|
||||
index: CONNECTORS_INDEX,
|
||||
});
|
||||
expect(mockClient.asCurrentUser.indices.refresh).toHaveBeenCalledWith({
|
||||
index: CONNECTORS_INDEX,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not create index if there is no connector', async () => {
|
||||
it('should not index document if there is no connector', async () => {
|
||||
mockClient.asCurrentUser.get.mockImplementationOnce(() => {
|
||||
return Promise.resolve({});
|
||||
});
|
||||
|
|
|
@ -23,11 +23,13 @@ export const updateConnectorScheduling = async (
|
|||
});
|
||||
const connector = connectorResult._source;
|
||||
if (connector) {
|
||||
return await client.asCurrentUser.index<ConnectorDocument>({
|
||||
const result = await client.asCurrentUser.index<ConnectorDocument>({
|
||||
document: { ...connector, scheduling },
|
||||
id: connectorId,
|
||||
index: CONNECTORS_INDEX,
|
||||
});
|
||||
await client.asCurrentUser.indices.refresh({ index: CONNECTORS_INDEX });
|
||||
return result;
|
||||
} else {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.enterpriseSearch.server.connectors.scheduling.error', {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
import { ErrorCode } from '../../../common/types/error_codes';
|
||||
import { addConnector } from '../../lib/connectors/add_connector';
|
||||
import { startConnectorSync } from '../../lib/connectors/start_sync';
|
||||
import { updateConnectorConfiguration } from '../../lib/connectors/update_connector_configuration';
|
||||
import { updateConnectorScheduling } from '../../lib/connectors/update_connector_scheduling';
|
||||
|
||||
|
@ -124,4 +125,28 @@ export function registerConnectorRoutes({ router }: RouteDependencies) {
|
|||
}
|
||||
}
|
||||
);
|
||||
router.post(
|
||||
{
|
||||
path: '/internal/enterprise_search/connectors/{connectorId}/start_sync',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
connectorId: schema.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const { client } = (await context.core).elasticsearch;
|
||||
try {
|
||||
await startConnectorSync(client, request.params.connectorId);
|
||||
return response.ok();
|
||||
} catch (error) {
|
||||
return response.customError({
|
||||
body: i18n.translate('xpack.enterpriseSearch.server.routes.updateConnector.error', {
|
||||
defaultMessage: 'Error fetching data from Enterprise Search',
|
||||
}),
|
||||
statusCode: 502,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue