mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Enterprise Search] Add editable filter rules to connectors (#145170)
This commit is contained in:
parent
5ad2a36305
commit
3e0448e69a
32 changed files with 1787 additions and 164 deletions
|
@ -103,11 +103,17 @@ export enum TriggerMethod {
|
|||
SCHEDULED = 'scheduled',
|
||||
}
|
||||
|
||||
export enum FeatureName {
|
||||
FILTERING_ADVANCED_CONFIG = 'filtering_advanced_config',
|
||||
FILTERING_RULES = 'filtering_rules',
|
||||
}
|
||||
|
||||
export interface Connector {
|
||||
api_key_id: string | null;
|
||||
configuration: ConnectorConfiguration;
|
||||
description: string | null;
|
||||
error: string | null;
|
||||
features: Record<FeatureName, boolean | null> | null;
|
||||
filtering: FilteringConfig[];
|
||||
id: string;
|
||||
index_name: string;
|
||||
|
|
12
x-pack/plugins/enterprise_search/common/types/util_types.ts
Normal file
12
x-pack/plugins/enterprise_search/common/types/util_types.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface OptimisticConcurrency<T> {
|
||||
primaryTerm: number | undefined;
|
||||
seqNo: number | undefined;
|
||||
value: T;
|
||||
}
|
|
@ -35,6 +35,7 @@ export const indices: ElasticsearchIndexWithIngestion[] = [
|
|||
configuration: { foo: { label: 'bar', value: 'barbar' } },
|
||||
description: null,
|
||||
error: null,
|
||||
features: null,
|
||||
filtering: [
|
||||
{
|
||||
active: {
|
||||
|
@ -120,6 +121,7 @@ export const indices: ElasticsearchIndexWithIngestion[] = [
|
|||
configuration: { foo: { label: 'bar', value: 'barbar' } },
|
||||
description: null,
|
||||
error: null,
|
||||
features: null,
|
||||
filtering: [
|
||||
{
|
||||
active: {
|
||||
|
|
|
@ -45,6 +45,7 @@ export const connectorIndex: ConnectorViewIndex = {
|
|||
configuration: { foo: { label: 'bar', value: 'barbar' } },
|
||||
description: null,
|
||||
error: null,
|
||||
features: null,
|
||||
filtering: [
|
||||
{
|
||||
active: {
|
||||
|
@ -134,6 +135,7 @@ export const crawlerIndex: CrawlerViewIndex = {
|
|||
configuration: { foo: { label: 'bar', value: 'barbar' } },
|
||||
description: null,
|
||||
error: null,
|
||||
features: null,
|
||||
filtering: [
|
||||
{
|
||||
active: {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { FilteringRule, FilteringRules } from '../../../../../common/types/connectors';
|
||||
import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
|
||||
import { HttpLogic } from '../../../shared/http';
|
||||
|
||||
export interface PutConnectorFilteringArgs {
|
||||
advancedSnippet: string;
|
||||
connectorId: string;
|
||||
filteringRules: FilteringRule[];
|
||||
}
|
||||
|
||||
export type PutConnectorFilteringResponse = FilteringRules;
|
||||
|
||||
export const putConnectorFiltering = async ({
|
||||
advancedSnippet,
|
||||
connectorId,
|
||||
filteringRules,
|
||||
}: PutConnectorFilteringArgs) => {
|
||||
const route = `/internal/enterprise_search/connectors/${connectorId}/filtering`;
|
||||
|
||||
return await HttpLogic.values.http.put(route, {
|
||||
body: JSON.stringify({
|
||||
advanced_snippet: advancedSnippet,
|
||||
filtering_rules: filteringRules,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export const ConnectorFilteringApiLogic = createApiLogic(
|
||||
['content', 'connector_filtering_api_logic'],
|
||||
putConnectorFiltering
|
||||
);
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { FilteringRule, FilteringRules } from '../../../../../common/types/connectors';
|
||||
import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
|
||||
import { HttpLogic } from '../../../shared/http';
|
||||
|
||||
export interface PutConnectorFilteringDraftArgs {
|
||||
advancedSnippet: string;
|
||||
connectorId: string;
|
||||
filteringRules: FilteringRule[];
|
||||
}
|
||||
|
||||
export type PutConnectorFilteringDraftResponse = FilteringRules;
|
||||
|
||||
export const putConnectorFilteringDraft = async ({
|
||||
advancedSnippet,
|
||||
connectorId,
|
||||
filteringRules,
|
||||
}: PutConnectorFilteringDraftArgs) => {
|
||||
const route = `/internal/enterprise_search/connectors/${connectorId}/filtering/draft`;
|
||||
|
||||
return await HttpLogic.values.http.put(route, {
|
||||
body: JSON.stringify({
|
||||
advanced_snippet: advancedSnippet,
|
||||
filtering_rules: filteringRules,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export const ConnectorFilteringDraftApiLogic = createApiLogic(
|
||||
['content', 'connector_filtering_draft_api_logic'],
|
||||
putConnectorFilteringDraft
|
||||
);
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CodeEditor } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import { ConnectorFilteringLogic } from './connector_filtering_logic';
|
||||
|
||||
export const AdvancedFilteringRules: React.FC = () => {
|
||||
const { hasJsonValidationError: hasError, localAdvancedSnippet } =
|
||||
useValues(ConnectorFilteringLogic);
|
||||
const { setLocalAdvancedSnippet } = useActions(ConnectorFilteringLogic);
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.enterpriseSearch.content.indices.connector.filtering.advancedRules.title',
|
||||
{
|
||||
defaultMessage: 'Advanced filtering rules',
|
||||
}
|
||||
)}
|
||||
isInvalid={hasError}
|
||||
error={
|
||||
hasError
|
||||
? i18n.translate(
|
||||
'xpack.enterpriseSearch.content.indices.connector.filtering.advancedRules.error',
|
||||
{
|
||||
defaultMessage: 'JSON format is invalid',
|
||||
}
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
<CodeEditor
|
||||
isCopyable
|
||||
languageId="json"
|
||||
options={{
|
||||
detectIndentation: true,
|
||||
lineNumbers: 'on',
|
||||
tabSize: 2,
|
||||
}}
|
||||
value={localAdvancedSnippet}
|
||||
onChange={(value) => {
|
||||
setLocalAdvancedSnippet(value);
|
||||
}}
|
||||
height="250px"
|
||||
width="100%"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiCodeBlock,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { FilteringRulesTable } from '../../../shared/filtering_rules_table/filtering_rules_table';
|
||||
import { IndexViewLogic } from '../../index_view_logic';
|
||||
|
||||
import { ConnectorFilteringLogic } from './connector_filtering_logic';
|
||||
import { EditFilteringFlyout } from './edit_filtering_flyout';
|
||||
import { FilteringStateCallouts } from './filtering_callouts';
|
||||
|
||||
export const ConnectorFiltering: React.FC = () => {
|
||||
const { indexName } = useValues(IndexViewLogic);
|
||||
const { applyDraft, setLocalFilteringRules, setLocalAdvancedSnippet, setIsEditing } =
|
||||
useActions(ConnectorFilteringLogic);
|
||||
const { advancedSnippet, draftState, filteringRules, hasDraft, isEditing } =
|
||||
useValues(ConnectorFilteringLogic);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isEditing && (
|
||||
<EditFilteringFlyout
|
||||
revertLocalFilteringRules={() => setLocalFilteringRules(filteringRules)}
|
||||
revertLocalAdvancedFiltering={() => setLocalAdvancedSnippet(advancedSnippet)}
|
||||
setIsEditing={setIsEditing}
|
||||
/>
|
||||
)}
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup direction="column">
|
||||
{hasDraft && (
|
||||
<EuiFlexItem>
|
||||
<FilteringStateCallouts
|
||||
applyDraft={applyDraft}
|
||||
editDraft={() => setIsEditing(true)}
|
||||
state={draftState}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
{i18n.translate('xpack.enterpriseSearch.index.connector.filtering.title', {
|
||||
defaultMessage: 'Sync filters ',
|
||||
})}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
{i18n.translate('xpack.enterpriseSearch.index.connector.filtering.description', {
|
||||
defaultMessage: `Include or exclude high level items, file types and (file or folder) paths to
|
||||
synchronize from {indexName}. Everything is included by default. Each document is
|
||||
tested against the reules below and the first rule that matches will be applied.`,
|
||||
values: {
|
||||
indexName,
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
<EuiLink href="TODOTODOTODOTODO" external>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.syncFiltersLabel',
|
||||
{
|
||||
defaultMessage: 'Learn more about sync rules',
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-telemetry-id="entSearchContent-connector-filtering-editRules-editDraftRules"
|
||||
color="primary"
|
||||
onClick={() => setIsEditing(!isEditing)}
|
||||
>
|
||||
{hasDraft
|
||||
? i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.editFilterRulesTitle',
|
||||
{
|
||||
defaultMessage: 'Edit filter rules',
|
||||
}
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.draftNewFilterRulesTitle',
|
||||
{
|
||||
defaultMessage: 'Draft new filter rules',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel color="plain" hasShadow={false} hasBorder>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.connector.filtering.basicFiltersTitle',
|
||||
{
|
||||
defaultMessage: 'Basic filters',
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.connector.filtering.basicFiltersDescription',
|
||||
{
|
||||
defaultMessage: 'These filters apply to documents in post-processing.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<FilteringRulesTable filteringRules={filteringRules} showOrder />
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
{!!advancedSnippet && (
|
||||
<EuiFlexItem>
|
||||
<EuiPanel color="plain" hasShadow={false} hasBorder>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.connector.filtering.advancedFiltersTitle',
|
||||
{
|
||||
defaultMessage: 'Advanced filters',
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.connector.filtering.advancedFiltersDescription',
|
||||
{
|
||||
defaultMessage: 'These filters apply to documents at the data source.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<EuiLink external href="TODOTODOTODODOTO">
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.connector.filtering.advancedFiltersLinkTitle',
|
||||
{
|
||||
defaultMessage: 'Learn more about advanced sync rules.',
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiCodeBlock isCopyable language="json">
|
||||
{advancedSnippet}
|
||||
</EuiCodeBlock>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiForm } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt';
|
||||
|
||||
import { ConnectorFilteringLogic } from './connector_filtering_logic';
|
||||
|
||||
export const ConnectorFilteringForm: React.FC = ({ children }) => {
|
||||
const { saveDraftFilteringRules, setIsEditing } = useActions(ConnectorFilteringLogic);
|
||||
const { hasJsonValidationError, isEditing, isLoading } = useValues(ConnectorFilteringLogic);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<UnsavedChangesPrompt
|
||||
hasUnsavedChanges={isEditing}
|
||||
messageText={i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.unsavedChanges',
|
||||
{
|
||||
defaultMessage: 'Your changes have not been saved. Are you sure you want to leave?',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<EuiFlexItem>
|
||||
<EuiForm>{children}</EuiForm>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
{isEditing && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-telemetry-id="entSearchContent-connector-filtering-editRules-cancelEditing"
|
||||
onClick={() => {
|
||||
setIsEditing(!isEditing);
|
||||
}}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.cancelEditingFilteringDraft',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-telemetry-id="entSearchContent-connector-filtering-editRules-saveAndValidate"
|
||||
disabled={hasJsonValidationError}
|
||||
isLoading={isLoading}
|
||||
onClick={saveDraftFilteringRules}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.validateDraftTitle',
|
||||
{
|
||||
defaultMessage: 'Save and validate draft',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,343 @@
|
|||
/*
|
||||
* 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 { isEqual } from 'lodash';
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { Status } from '../../../../../../../common/types/api';
|
||||
|
||||
import {
|
||||
FilteringConfig,
|
||||
FilteringPolicy,
|
||||
FilteringRule,
|
||||
FilteringRuleRule,
|
||||
FilteringValidationState,
|
||||
} from '../../../../../../../common/types/connectors';
|
||||
import { Actions } from '../../../../../shared/api_logic/create_api_logic';
|
||||
import {
|
||||
flashAPIErrors,
|
||||
flashSuccessToast,
|
||||
clearFlashMessages,
|
||||
} from '../../../../../shared/flash_messages';
|
||||
import {
|
||||
ConnectorFilteringApiLogic,
|
||||
PutConnectorFilteringArgs,
|
||||
PutConnectorFilteringResponse,
|
||||
} from '../../../../api/connector/update_connector_filtering_api_logic';
|
||||
import {
|
||||
ConnectorFilteringDraftApiLogic,
|
||||
PutConnectorFilteringDraftArgs,
|
||||
PutConnectorFilteringDraftResponse,
|
||||
} from '../../../../api/connector/update_connector_filtering_draft_api_logic';
|
||||
import {
|
||||
FetchIndexApiLogic,
|
||||
FetchIndexApiParams,
|
||||
FetchIndexApiResponse,
|
||||
} from '../../../../api/index/fetch_index_api_logic';
|
||||
import { isConnectorIndex } from '../../../../utils/indices';
|
||||
|
||||
type ConnectorFilteringActions = Pick<
|
||||
Actions<PutConnectorFilteringArgs, PutConnectorFilteringResponse>,
|
||||
'apiError' | 'apiSuccess' | 'makeRequest'
|
||||
> & {
|
||||
addFilteringRule(filteringRule: FilteringRule): FilteringRule;
|
||||
applyDraft: () => void;
|
||||
deleteFilteringRule(filteringRule: FilteringRule): FilteringRule;
|
||||
draftApiError: Actions<
|
||||
PutConnectorFilteringDraftArgs,
|
||||
PutConnectorFilteringDraftResponse
|
||||
>['apiError'];
|
||||
draftApiSuccess: Actions<
|
||||
PutConnectorFilteringDraftArgs,
|
||||
PutConnectorFilteringDraftResponse
|
||||
>['apiSuccess'];
|
||||
draftMakeRequest: Actions<
|
||||
PutConnectorFilteringDraftArgs,
|
||||
PutConnectorFilteringDraftResponse
|
||||
>['makeRequest'];
|
||||
fetchIndexApiSuccess: Actions<FetchIndexApiParams, FetchIndexApiResponse>['apiSuccess'];
|
||||
reorderFilteringRules(filteringRules: FilteringRule[]): FilteringRule[];
|
||||
revertLocalAdvancedFiltering: () => void;
|
||||
revertLocalFilteringRules: () => void;
|
||||
saveDraftFilteringRules: () => void;
|
||||
setFilteringConfig(filtering: FilteringConfig | null): FilteringConfig | null;
|
||||
setHasJsonValidationError(hasJsonValidationError: boolean): { hasJsonValidationError: boolean };
|
||||
setIsEditing(isEditing: boolean): { isEditing: boolean };
|
||||
setLocalAdvancedSnippet(advancedSnippet: string): { advancedSnippet: string };
|
||||
setLocalFilteringRules(filteringRules: FilteringRule[]): FilteringRule[];
|
||||
updateFilteringRule(filteringRule: FilteringRule): FilteringRule;
|
||||
};
|
||||
|
||||
interface ConnectorFilteringValues {
|
||||
advancedSnippet: string;
|
||||
draftState: FilteringValidationState;
|
||||
editableFilteringRules: FilteringRule[];
|
||||
filteringConfig: FilteringConfig | null;
|
||||
filteringRules: FilteringRule[];
|
||||
hasDraft: boolean;
|
||||
hasJsonValidationError: boolean;
|
||||
index: FetchIndexApiResponse;
|
||||
isEditing: boolean;
|
||||
isLoading: boolean;
|
||||
jsonValidationError: boolean;
|
||||
lastFilteringRule: FilteringRule;
|
||||
localAdvancedSnippet: string;
|
||||
localFilteringRules: FilteringRule[];
|
||||
status: Status;
|
||||
}
|
||||
|
||||
export const ConnectorFilteringLogic = kea<
|
||||
MakeLogicType<ConnectorFilteringValues, ConnectorFilteringActions>
|
||||
>({
|
||||
actions: {
|
||||
addFilteringRule: (filteringRule) => filteringRule,
|
||||
applyDraft: true,
|
||||
deleteFilteringRule: (filteringRule) => filteringRule,
|
||||
reorderFilteringRules: (filteringRules) => filteringRules,
|
||||
revertLocalAdvancedFiltering: true,
|
||||
revertLocalFilteringRules: true,
|
||||
saveDraftFilteringRules: true,
|
||||
setFilteringConfig: (filteringConfig) => filteringConfig,
|
||||
setIsEditing: (isEditing: boolean) => ({
|
||||
isEditing,
|
||||
}),
|
||||
|
||||
setLocalAdvancedSnippet: (advancedSnippet) => ({
|
||||
advancedSnippet,
|
||||
}),
|
||||
setLocalFilteringRules: (filteringRules) => filteringRules,
|
||||
updateFilteringRule: (filteringRule) => filteringRule,
|
||||
},
|
||||
connect: {
|
||||
actions: [
|
||||
ConnectorFilteringApiLogic,
|
||||
['apiError', 'apiSuccess', 'makeRequest'],
|
||||
ConnectorFilteringDraftApiLogic,
|
||||
[
|
||||
'apiError as draftApiError',
|
||||
'apiSuccess as draftApiSuccess',
|
||||
'makeRequest as draftMakeRequest',
|
||||
],
|
||||
FetchIndexApiLogic,
|
||||
['apiSuccess as fetchIndexApiSuccess'],
|
||||
],
|
||||
values: [ConnectorFilteringApiLogic, ['status'], FetchIndexApiLogic, ['data as index']],
|
||||
},
|
||||
events: ({ actions, values }) => ({
|
||||
afterMount: () =>
|
||||
actions.setFilteringConfig(
|
||||
isConnectorIndex(values.index) ? values.index.connector.filtering[0] : null
|
||||
),
|
||||
}),
|
||||
listeners: ({ actions, values }) => ({
|
||||
apiError: (error) => flashAPIErrors(error),
|
||||
apiSuccess: () => {
|
||||
flashSuccessToast(
|
||||
i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.connector.filtering.successToastRules.title',
|
||||
{ defaultMessage: 'Filtering rules updated' }
|
||||
)
|
||||
);
|
||||
},
|
||||
applyDraft: () => {
|
||||
if (isConnectorIndex(values.index)) {
|
||||
actions.makeRequest({
|
||||
advancedSnippet: values.localAdvancedSnippet ?? '',
|
||||
connectorId: values.index.connector.id,
|
||||
filteringRules: values.localFilteringRules ?? [],
|
||||
});
|
||||
}
|
||||
},
|
||||
draftApiError: (error) => flashAPIErrors(error),
|
||||
draftApiSuccess: () => {
|
||||
flashSuccessToast(
|
||||
i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.connector.filtering.successToastDraft.title',
|
||||
{ defaultMessage: 'Draft saved' }
|
||||
)
|
||||
);
|
||||
},
|
||||
draftMakeRequest: () => clearFlashMessages(),
|
||||
fetchIndexApiSuccess: (index) => {
|
||||
if (
|
||||
!values.isEditing &&
|
||||
isConnectorIndex(index) &&
|
||||
!isEqual(values.filteringConfig, index.connector.filtering[0])
|
||||
) {
|
||||
actions.setFilteringConfig(index.connector.filtering[0]);
|
||||
}
|
||||
},
|
||||
makeRequest: () => clearFlashMessages(),
|
||||
saveDraftFilteringRules: () => {
|
||||
if (isConnectorIndex(values.index)) {
|
||||
actions.draftMakeRequest({
|
||||
advancedSnippet: values.localAdvancedSnippet ?? '',
|
||||
connectorId: values.index.connector.id,
|
||||
filteringRules: values.localFilteringRules ?? [],
|
||||
});
|
||||
}
|
||||
},
|
||||
setIsEditing: (isEditing) => {
|
||||
if (isEditing && values.filteringConfig) {
|
||||
actions.setLocalFilteringRules(
|
||||
values.hasDraft ? values.filteringConfig.draft.rules : values.filteringConfig.active.rules
|
||||
);
|
||||
actions.setLocalAdvancedSnippet(
|
||||
values.hasDraft
|
||||
? JSON.stringify(
|
||||
values.filteringConfig.draft.advanced_snippet.value ?? {},
|
||||
undefined,
|
||||
2
|
||||
)
|
||||
: JSON.stringify(
|
||||
values.filteringConfig.active.advanced_snippet.value ?? {},
|
||||
undefined,
|
||||
2
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
path: ['enterprise_search', 'content', 'connector_filtering'],
|
||||
reducers: () => ({
|
||||
filteringConfig: [
|
||||
null,
|
||||
{
|
||||
apiSuccess: (filteringConfig, filteringRules) =>
|
||||
filteringConfig
|
||||
? {
|
||||
...filteringConfig,
|
||||
active: filteringRules,
|
||||
}
|
||||
: null,
|
||||
draftApiSuccess: (filteringConfig, filteringRules) =>
|
||||
filteringConfig
|
||||
? {
|
||||
...filteringConfig,
|
||||
draft: filteringRules,
|
||||
}
|
||||
: null,
|
||||
setFilteringConfig: (_, filteringConfig) => filteringConfig,
|
||||
},
|
||||
],
|
||||
isEditing: [
|
||||
false,
|
||||
{
|
||||
draftApiError: () => false,
|
||||
draftApiSuccess: () => false,
|
||||
setIsEditing: (_, { isEditing }) => isEditing,
|
||||
},
|
||||
],
|
||||
localAdvancedSnippet: [
|
||||
'',
|
||||
{
|
||||
setLocalAdvancedSnippet: (_, { advancedSnippet }) => advancedSnippet,
|
||||
},
|
||||
],
|
||||
localFilteringRules: [
|
||||
[],
|
||||
{
|
||||
addFilteringRule: (filteringRules, filteringRule) => {
|
||||
const newFilteringRules = filteringRules.length
|
||||
? [
|
||||
...filteringRules.slice(0, filteringRules.length - 1),
|
||||
filteringRule,
|
||||
filteringRules[filteringRules.length - 1],
|
||||
]
|
||||
: [
|
||||
filteringRule,
|
||||
{
|
||||
created_at: new Date().toISOString(),
|
||||
field: '_',
|
||||
id: uuidv4(),
|
||||
order: 0,
|
||||
policy: FilteringPolicy.INCLUDE,
|
||||
rule: FilteringRuleRule.EQUALS,
|
||||
updated_at: new Date().toISOString(),
|
||||
value: '.*',
|
||||
},
|
||||
];
|
||||
return newFilteringRules.map((rule, index) => ({ ...rule, order: index }));
|
||||
},
|
||||
deleteFilteringRule: (filteringRules, filteringRule) =>
|
||||
filteringRules.filter((rule) => rule.id !== filteringRule.id),
|
||||
reorderFilteringRules: (filteringRules, newFilteringRules) => {
|
||||
const lastItem = filteringRules.length
|
||||
? filteringRules[filteringRules.length - 1]
|
||||
: {
|
||||
created_at: new Date().toISOString(),
|
||||
field: '_',
|
||||
id: uuidv4(),
|
||||
order: 0,
|
||||
policy: FilteringPolicy.INCLUDE,
|
||||
rule: FilteringRuleRule.EQUALS,
|
||||
updated_at: new Date().toISOString(),
|
||||
value: '.*',
|
||||
};
|
||||
return [...newFilteringRules, lastItem].map((rule, index) => ({ ...rule, order: index }));
|
||||
},
|
||||
setLocalFilteringRules: (_, filteringRules) => filteringRules,
|
||||
updateFilteringRule: (filteringRules, filteringRule) =>
|
||||
filteringRules.map((rule) => (rule.id === filteringRule.id ? filteringRule : rule)),
|
||||
},
|
||||
],
|
||||
}),
|
||||
selectors: ({ selectors }) => ({
|
||||
advancedSnippet: [
|
||||
() => [selectors.filteringConfig],
|
||||
(filteringConfig: FilteringConfig | null) =>
|
||||
filteringConfig?.active.advanced_snippet.value
|
||||
? JSON.stringify(filteringConfig?.active.advanced_snippet.value, undefined, 2)
|
||||
: '',
|
||||
],
|
||||
draftErrors: [
|
||||
() => [selectors.filteringConfig],
|
||||
(filteringConfig: FilteringConfig | null) => filteringConfig?.draft.validation.errors ?? [],
|
||||
],
|
||||
draftState: [
|
||||
() => [selectors.filteringConfig],
|
||||
(filteringConfig: FilteringConfig | null) =>
|
||||
filteringConfig?.draft.validation.state ?? FilteringValidationState.VALID,
|
||||
],
|
||||
editableFilteringRules: [
|
||||
() => [selectors.localFilteringRules],
|
||||
(filteringRules: FilteringRule[] | null) => {
|
||||
return filteringRules?.length ? filteringRules.slice(0, filteringRules.length - 1) : [];
|
||||
},
|
||||
],
|
||||
filteringRules: [
|
||||
() => [selectors.filteringConfig],
|
||||
(filteringConfig: FilteringConfig | null) => filteringConfig?.active.rules ?? [],
|
||||
],
|
||||
hasDraft: [
|
||||
() => [selectors.filteringConfig],
|
||||
(filteringConfig: FilteringConfig | null) =>
|
||||
!isEqual(
|
||||
filteringConfig?.active.advanced_snippet.value,
|
||||
filteringConfig?.draft.advanced_snippet.value
|
||||
) || !isEqual(filteringConfig?.draft.rules, filteringConfig?.active.rules),
|
||||
],
|
||||
hasJsonValidationError: [
|
||||
() => [selectors.localAdvancedSnippet],
|
||||
(advancedSnippet: string) => {
|
||||
if (!advancedSnippet) return false;
|
||||
try {
|
||||
JSON.parse(advancedSnippet);
|
||||
return false;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
],
|
||||
isLoading: [() => [selectors.status], (status: Status) => status === Status.LOADING],
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlyout,
|
||||
EuiFlyoutHeader,
|
||||
EuiTitle,
|
||||
EuiFlyoutBody,
|
||||
EuiText,
|
||||
EuiTabbedContent,
|
||||
EuiTabbedContentTab,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { AdvancedFilteringRules } from './advanced_filtering_rules';
|
||||
import { EditFilteringTab } from './edit_filtering_tab';
|
||||
import { FilteringRulesTable } from './editable_filtering_rules_table';
|
||||
|
||||
interface EditFilteringFlyoutProps {
|
||||
revertLocalAdvancedFiltering: () => void;
|
||||
revertLocalFilteringRules: () => void;
|
||||
setIsEditing: (value: boolean) => void;
|
||||
}
|
||||
|
||||
enum FilteringTabs {
|
||||
BASIC = 'basic',
|
||||
ADVANCED = 'advanced',
|
||||
}
|
||||
|
||||
export const EditFilteringFlyout: React.FC<EditFilteringFlyoutProps> = ({
|
||||
revertLocalFilteringRules,
|
||||
revertLocalAdvancedFiltering,
|
||||
setIsEditing,
|
||||
}) => {
|
||||
const tabs: EuiTabbedContentTab[] = [
|
||||
{
|
||||
content: (
|
||||
<EditFilteringTab revertAction={revertLocalFilteringRules}>
|
||||
<FilteringRulesTable />
|
||||
</EditFilteringTab>
|
||||
),
|
||||
id: FilteringTabs.BASIC,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.connector.filtering.basicTabTitle',
|
||||
{
|
||||
defaultMessage: 'Basic filters',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
content: (
|
||||
<EditFilteringTab revertAction={revertLocalAdvancedFiltering}>
|
||||
<AdvancedFilteringRules />
|
||||
</EditFilteringTab>
|
||||
),
|
||||
id: FilteringTabs.ADVANCED,
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.connector.filtering.advancedTabTitle',
|
||||
{
|
||||
defaultMessage: 'Advanced filters',
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiFlyout
|
||||
ownFocus
|
||||
onClose={() => setIsEditing(false)}
|
||||
aria-labelledby="filteringFlyout"
|
||||
size="l"
|
||||
>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h2 id="filteringFlyout">
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.connector.filtering.flyout.title',
|
||||
{
|
||||
defaultMessage: 'Draft rules',
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiText size="s">
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.connector.filtering.flyout.description',
|
||||
{
|
||||
defaultMessage: 'Plan and edit filters here before applying them to the next sync.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiTabbedContent tabs={tabs} />
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { ConnectorFilteringForm } from './connector_filtering_form';
|
||||
|
||||
export const EditFilteringTab: React.FC<{ revertAction: () => void }> = ({
|
||||
children,
|
||||
revertAction,
|
||||
}) => {
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-telemetry-id="entSearchContent-connector-filtering-editRules-revert"
|
||||
onClick={revertAction}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.connector.filtering.flyout.revertButtonTitle',
|
||||
{
|
||||
defaultMessage: 'Revert to active rules',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<ConnectorFilteringForm>{children}</ConnectorFilteringForm>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* 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 { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import {
|
||||
EuiCode,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
FilteringPolicy,
|
||||
FilteringRule,
|
||||
FilteringRuleRule,
|
||||
} from '../../../../../../../common/types/connectors';
|
||||
|
||||
import { InlineEditableTable } from '../../../../../shared/tables/inline_editable_table/inline_editable_table';
|
||||
import {
|
||||
InlineEditableTableLogic,
|
||||
InlineEditableTableProps,
|
||||
} from '../../../../../shared/tables/inline_editable_table/inline_editable_table_logic';
|
||||
import {
|
||||
FormErrors,
|
||||
InlineEditableTableColumn,
|
||||
} from '../../../../../shared/tables/inline_editable_table/types';
|
||||
import { ItemWithAnID } from '../../../../../shared/tables/types';
|
||||
|
||||
import {
|
||||
filteringPolicyToText,
|
||||
filteringRuleToText,
|
||||
} from '../../../../utils/filtering_rule_helpers';
|
||||
|
||||
import { IndexViewLogic } from '../../index_view_logic';
|
||||
|
||||
import { ConnectorFilteringLogic } from './connector_filtering_logic';
|
||||
|
||||
const instanceId = 'FilteringRulesTable';
|
||||
|
||||
function validateItem(filteringRule: FilteringRule): FormErrors {
|
||||
if (filteringRule.rule === FilteringRuleRule.REGEX) {
|
||||
try {
|
||||
new RegExp(filteringRule.value);
|
||||
return {};
|
||||
} catch {
|
||||
return {
|
||||
value: i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.connector.filteringRules.regExError',
|
||||
{ defaultMessage: 'Value should be a regular expression' }
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export const FilteringRulesTable: React.FC = () => {
|
||||
const { editableFilteringRules } = useValues(ConnectorFilteringLogic);
|
||||
const { indexName } = useValues(IndexViewLogic);
|
||||
const { addFilteringRule, deleteFilteringRule, reorderFilteringRules, updateFilteringRule } =
|
||||
useActions(ConnectorFilteringLogic);
|
||||
|
||||
const description = (
|
||||
<EuiText size="s" color="default">
|
||||
{i18n.translate('xpack.enterpriseSearch.content.index.connector.filteringRules.description', {
|
||||
defaultMessage:
|
||||
'Add an indexing rule to customize what data is synchronized from {indexName}. Everything is included by default, and documents are validated against the configured set of indexing rules starting from the top listed down.',
|
||||
values: { indexName },
|
||||
})}
|
||||
<EuiSpacer />
|
||||
<EuiLink href={'TODOTODOTODO'} external>
|
||||
{i18n.translate('xpack.enterpriseSearch.content.index.connector.filteringRules.link', {
|
||||
defaultMessage: 'Learn more about customizing your index rules.',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
);
|
||||
|
||||
const columns: Array<InlineEditableTableColumn<FilteringRule>> = [
|
||||
{
|
||||
editingRender: (filteringRule, onChange) => (
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
value={filteringRule.policy}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
options={[
|
||||
{
|
||||
text: filteringPolicyToText(FilteringPolicy.INCLUDE),
|
||||
value: FilteringPolicy.INCLUDE,
|
||||
},
|
||||
{
|
||||
text: filteringPolicyToText(FilteringPolicy.EXCLUDE),
|
||||
value: FilteringPolicy.EXCLUDE,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
field: 'policy',
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.basicTable.policyTitle',
|
||||
{ defaultMessage: 'Policy' }
|
||||
),
|
||||
render: (indexingRule) => (
|
||||
<EuiText size="s">{filteringPolicyToText(indexingRule.policy)}</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
editingRender: (filteringRule, onChange) => (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
value={filteringRule.field}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
field: 'field',
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.basicTable.fieldTitle',
|
||||
{ defaultMessage: 'Field' }
|
||||
),
|
||||
render: (indexingRule) => (
|
||||
<EuiText size="s">
|
||||
<EuiCode>{indexingRule.field}</EuiCode>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
editingRender: (filteringRule, onChange) => (
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
value={filteringRule.rule}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
options={Object.values(FilteringRuleRule).map((rule) => ({
|
||||
text: filteringRuleToText(rule),
|
||||
value: rule,
|
||||
}))}
|
||||
/>
|
||||
),
|
||||
field: 'rule',
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.basicTable.ruleTitle',
|
||||
{ defaultMessage: 'Rule' }
|
||||
),
|
||||
render: (filteringRule) => (
|
||||
<EuiText size="s">{filteringRuleToText(filteringRule.rule)}</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
editingRender: (filteringRule, onChange) => (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
value={filteringRule.value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
field: 'value',
|
||||
name: i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.basicTable.valueTitle',
|
||||
{
|
||||
defaultMessage: 'Value',
|
||||
}
|
||||
),
|
||||
render: (indexingRule) => (
|
||||
<EuiText size="s">
|
||||
<EuiCode>{indexingRule.value}</EuiCode>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<InlineEditableTable
|
||||
addButtonText={i18n.translate(
|
||||
'xpack.enterpriseSearch.content.index.connector.filtering.table.addRuleLabel',
|
||||
{ defaultMessage: 'Add filter rule' }
|
||||
)}
|
||||
columns={columns}
|
||||
defaultItem={{
|
||||
policy: FilteringPolicy.INCLUDE,
|
||||
rule: FilteringRuleRule.EQUALS,
|
||||
value: '',
|
||||
}}
|
||||
description={description}
|
||||
instanceId={instanceId}
|
||||
items={editableFilteringRules}
|
||||
onAdd={(rule) => {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const newRule = {
|
||||
...rule,
|
||||
created_at: now,
|
||||
// crypto.randomUUID isn't widely enough available in browsers yet
|
||||
id: uuidv4(),
|
||||
updated_at: now,
|
||||
};
|
||||
addFilteringRule(newRule);
|
||||
InlineEditableTableLogic({
|
||||
instanceId,
|
||||
} as InlineEditableTableProps<ItemWithAnID>).actions.doneEditing();
|
||||
}}
|
||||
onDelete={deleteFilteringRule}
|
||||
onUpdate={updateFilteringRule}
|
||||
onReorder={reorderFilteringRules}
|
||||
title=""
|
||||
validateItem={validateItem}
|
||||
bottomRows={[
|
||||
<EuiText size="s">
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.sources.filteringRulesTable.includeEverythingMessage',
|
||||
{
|
||||
defaultMessage: 'Include everything else from this source',
|
||||
}
|
||||
)}
|
||||
</EuiText>,
|
||||
]}
|
||||
canRemoveLastItem
|
||||
emptyPropertyAllowed
|
||||
showRowIndex
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { FilteringValidationState } from '../../../../../../../common/types/connectors';
|
||||
|
||||
interface FilteringStatusCalloutsProps {
|
||||
applyDraft: () => void;
|
||||
editDraft: () => void;
|
||||
state: FilteringValidationState;
|
||||
}
|
||||
|
||||
export const FilteringStateCallouts: React.FC<FilteringStatusCalloutsProps> = ({
|
||||
applyDraft,
|
||||
editDraft,
|
||||
state,
|
||||
}) => {
|
||||
switch (state) {
|
||||
case FilteringValidationState.EDITED:
|
||||
return (
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
title={
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.validatingTitle',
|
||||
{
|
||||
defaultMessage: 'Draft sync rules are validating',
|
||||
}
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.validatingDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Draft rules need to be validated before they can take effect. This may take a few minutes.',
|
||||
}
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<span>
|
||||
<EuiButton
|
||||
data-telemetry-id="entSearchContent-connector-filtering-validatingCallout-editRules"
|
||||
onClick={editDraft}
|
||||
color="warning"
|
||||
fill
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.validatingCallout.editDraftRulesTitle',
|
||||
{
|
||||
defaultMessage: 'Edit draft rules',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiCallOut>
|
||||
);
|
||||
case FilteringValidationState.INVALID:
|
||||
return (
|
||||
<EuiCallOut
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
title={i18n.translate('xpack.enterpriseSearch.index.connector.filtering.invalidTitle', {
|
||||
defaultMessage: 'Draft sync rules are invalid',
|
||||
})}
|
||||
>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.invalidDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Draft rules did not validate. Edit the draft rules before they can take effect.',
|
||||
}
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<span>
|
||||
<EuiButton
|
||||
data-telemetry-id="entSearchContent-connector-filtering-errorCallout-editRules"
|
||||
onClick={editDraft}
|
||||
color="danger"
|
||||
fill
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.errorCallout.editDraftRulesTitle',
|
||||
{
|
||||
defaultMessage: 'Edit draft rules',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiCallOut>
|
||||
);
|
||||
case FilteringValidationState.VALID:
|
||||
return (
|
||||
<EuiCallOut
|
||||
color="success"
|
||||
iconType="check"
|
||||
title={i18n.translate('xpack.enterpriseSearch.index.connector.filtering.validatedTitle', {
|
||||
defaultMessage: 'Draft sync rules validated',
|
||||
})}
|
||||
>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.validatedDescription',
|
||||
{
|
||||
defaultMessage: 'Apply draft rules to take effect on the next sync.',
|
||||
}
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
<span>
|
||||
<EuiButton
|
||||
data-telemetry-id="entSearchContent-connector-filtering-successCallout-applyRules"
|
||||
onClick={applyDraft}
|
||||
color="success"
|
||||
fill
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.successCallout.applyDraftRulesTitle',
|
||||
{
|
||||
defaultMessage: 'Apply draft rules',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<span>
|
||||
<EuiButton
|
||||
data-telemetry-id="entSearchContent-connector-filtering-successCallout-editRules"
|
||||
onClick={editDraft}
|
||||
color="success"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.index.connector.filtering.errorCallout.successEditDraftRulesTitle',
|
||||
{
|
||||
defaultMessage: 'Edit draft rules',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiCallOut>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
|
@ -34,6 +34,9 @@ const DEFAULT_VALUES = {
|
|||
connectorId: null,
|
||||
fetchIndexApiData: undefined,
|
||||
fetchIndexApiStatus: Status.LOADING,
|
||||
hasAdvancedFilteringFeature: false,
|
||||
hasBasicFilteringFeature: false,
|
||||
hasFilteringFeature: false,
|
||||
index: undefined,
|
||||
indexData: null,
|
||||
indexName: 'index-name',
|
||||
|
|
|
@ -12,6 +12,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { Status } from '../../../../../common/types/api';
|
||||
import {
|
||||
Connector,
|
||||
FeatureName,
|
||||
IngestPipelineParams,
|
||||
SyncStatus,
|
||||
} from '../../../../../common/types/connectors';
|
||||
|
@ -70,6 +71,9 @@ export interface IndexViewValues {
|
|||
connectorId: string | null;
|
||||
fetchIndexApiData: typeof CachedFetchIndexApiLogic.values.fetchIndexApiData;
|
||||
fetchIndexApiStatus: Status;
|
||||
hasAdvancedFilteringFeature: boolean;
|
||||
hasBasicFilteringFeature: boolean;
|
||||
hasFilteringFeature: boolean;
|
||||
index: ElasticsearchViewIndex | undefined;
|
||||
indexData: typeof CachedFetchIndexApiLogic.values.indexData;
|
||||
indexName: string;
|
||||
|
@ -203,6 +207,20 @@ export const IndexViewLogic = kea<MakeLogicType<IndexViewValues, IndexViewAction
|
|||
() => [selectors.indexData],
|
||||
(index) => (isConnectorViewIndex(index) ? index.connector.id : null),
|
||||
],
|
||||
hasAdvancedFilteringFeature: [
|
||||
() => [selectors.connector],
|
||||
(connector?: Connector) =>
|
||||
connector?.features ? connector.features[FeatureName.FILTERING_ADVANCED_CONFIG] : false,
|
||||
],
|
||||
hasBasicFilteringFeature: [
|
||||
() => [selectors.connector],
|
||||
(connector?: Connector) =>
|
||||
connector?.features ? connector.features[FeatureName.FILTERING_RULES] : false,
|
||||
],
|
||||
hasFilteringFeature: [
|
||||
() => [selectors.hasAdvancedFilteringFeature, selectors.hasBasicFilteringFeature],
|
||||
(advancedFeature: boolean, basicFeature: boolean) => advancedFeature || basicFeature,
|
||||
],
|
||||
index: [
|
||||
() => [selectors.indexData],
|
||||
(data: IndexViewValues['indexData']) => (data ? indexToViewIndex(data) : undefined),
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
SEARCH_INDEX_SELECT_CONNECTOR_PATH,
|
||||
SEARCH_INDEX_TAB_PATH,
|
||||
} from '../../routes';
|
||||
|
||||
import { isConnectorIndex, isCrawlerIndex } from '../../utils/indices';
|
||||
import { EnterpriseSearchContentPageTemplate } from '../layout/page_template';
|
||||
|
||||
|
@ -32,6 +33,7 @@ import { baseBreadcrumbs } from '../search_indices';
|
|||
import { getHeaderActions } from './components/header_actions/header_actions';
|
||||
import { ConnectorConfiguration } from './connector/connector_configuration';
|
||||
import { ConnectorSchedulingComponent } from './connector/connector_scheduling';
|
||||
import { ConnectorFiltering } from './connector/filtering/connector_filtering';
|
||||
import { AutomaticCrawlScheduler } from './crawler/automatic_crawl_scheduler/automatic_crawl_scheduler';
|
||||
import { CrawlCustomSettingsFlyout } from './crawler/crawl_custom_settings_flyout/crawl_custom_settings_flyout';
|
||||
import { SearchIndexDomainManagement } from './crawler/domain_management/domain_management';
|
||||
|
@ -50,13 +52,14 @@ export enum SearchIndexTabId {
|
|||
PIPELINES = 'pipelines',
|
||||
// connector indices
|
||||
CONFIGURATION = 'configuration',
|
||||
FILTERS = 'filters',
|
||||
SCHEDULING = 'scheduling',
|
||||
// crawler indices
|
||||
DOMAIN_MANAGEMENT = 'domain_management',
|
||||
}
|
||||
|
||||
export const SearchIndex: React.FC = () => {
|
||||
const { index, isInitialLoading } = useValues(IndexViewLogic);
|
||||
const { hasFilteringFeature, index, isInitialLoading } = useValues(IndexViewLogic);
|
||||
|
||||
const { tabId = SearchIndexTabId.OVERVIEW } = useParams<{
|
||||
tabId?: string;
|
||||
|
@ -122,6 +125,17 @@ export const SearchIndex: React.FC = () => {
|
|||
defaultMessage: 'Configuration',
|
||||
}),
|
||||
},
|
||||
...(hasFilteringFeature
|
||||
? [
|
||||
{
|
||||
content: <ConnectorFiltering />,
|
||||
id: SearchIndexTabId.FILTERS,
|
||||
name: i18n.translate('xpack.enterpriseSearch.content.searchIndex.filtersTabLabel', {
|
||||
defaultMessage: 'Filters',
|
||||
}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
content: <ConnectorSchedulingComponent />,
|
||||
id: SearchIndexTabId.SCHEDULING,
|
||||
|
|
|
@ -5,35 +5,9 @@ exports[`FilteringPanel renders 1`] = `
|
|||
<FlyoutPanel
|
||||
title="Filtering"
|
||||
>
|
||||
<EuiBasicTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "policy",
|
||||
"name": "Pipeline setting",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"field": "rule",
|
||||
"name": "Rule",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"field": "value",
|
||||
"name": "Value",
|
||||
"render": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
items={Array []}
|
||||
noItemsMessage={
|
||||
<EuiI18n
|
||||
default="No items found"
|
||||
token="euiBasicTable.noItemsMessage"
|
||||
/>
|
||||
}
|
||||
responsive={true}
|
||||
tableLayout="fixed"
|
||||
<FilteringRulesTable
|
||||
filteringRules={Array []}
|
||||
showOrder={false}
|
||||
/>
|
||||
</FlyoutPanel>
|
||||
</Fragment>
|
||||
|
@ -44,34 +18,9 @@ exports[`FilteringPanel renders advanced snippet 1`] = `
|
|||
<FlyoutPanel
|
||||
title="Filtering"
|
||||
>
|
||||
<EuiBasicTable
|
||||
columns={
|
||||
<FilteringRulesTable
|
||||
filteringRules={
|
||||
Array [
|
||||
Object {
|
||||
"field": "policy",
|
||||
"name": "Pipeline setting",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"field": "rule",
|
||||
"name": "Rule",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"field": "value",
|
||||
"name": "Value",
|
||||
"render": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"order": 0,
|
||||
"policy": "include",
|
||||
"rule": "equals",
|
||||
"value": "THIS VALUE",
|
||||
},
|
||||
Object {
|
||||
"order": 1,
|
||||
"policy": "exclude",
|
||||
|
@ -85,9 +34,9 @@ exports[`FilteringPanel renders advanced snippet 1`] = `
|
|||
"value": "THIS VALUE",
|
||||
},
|
||||
Object {
|
||||
"order": 4,
|
||||
"policy": "exclude",
|
||||
"rule": "<",
|
||||
"order": 0,
|
||||
"policy": "include",
|
||||
"rule": "equals",
|
||||
"value": "THIS VALUE",
|
||||
},
|
||||
Object {
|
||||
|
@ -96,16 +45,15 @@ exports[`FilteringPanel renders advanced snippet 1`] = `
|
|||
"rule": ">",
|
||||
"value": "THIS VALUE",
|
||||
},
|
||||
Object {
|
||||
"order": 4,
|
||||
"policy": "exclude",
|
||||
"rule": "<",
|
||||
"value": "THIS VALUE",
|
||||
},
|
||||
]
|
||||
}
|
||||
noItemsMessage={
|
||||
<EuiI18n
|
||||
default="No items found"
|
||||
token="euiBasicTable.noItemsMessage"
|
||||
/>
|
||||
}
|
||||
responsive={true}
|
||||
tableLayout="fixed"
|
||||
showOrder={false}
|
||||
/>
|
||||
</FlyoutPanel>
|
||||
<EuiSpacer />
|
||||
|
@ -134,34 +82,9 @@ exports[`FilteringPanel renders filtering rules list 1`] = `
|
|||
<FlyoutPanel
|
||||
title="Filtering"
|
||||
>
|
||||
<EuiBasicTable
|
||||
columns={
|
||||
<FilteringRulesTable
|
||||
filteringRules={
|
||||
Array [
|
||||
Object {
|
||||
"field": "policy",
|
||||
"name": "Pipeline setting",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"field": "rule",
|
||||
"name": "Rule",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"field": "value",
|
||||
"name": "Value",
|
||||
"render": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"order": 0,
|
||||
"policy": "include",
|
||||
"rule": "equals",
|
||||
"value": "THIS VALUE",
|
||||
},
|
||||
Object {
|
||||
"order": 1,
|
||||
"policy": "exclude",
|
||||
|
@ -175,9 +98,9 @@ exports[`FilteringPanel renders filtering rules list 1`] = `
|
|||
"value": "THIS VALUE",
|
||||
},
|
||||
Object {
|
||||
"order": 4,
|
||||
"policy": "exclude",
|
||||
"rule": "<",
|
||||
"order": 0,
|
||||
"policy": "include",
|
||||
"rule": "equals",
|
||||
"value": "THIS VALUE",
|
||||
},
|
||||
Object {
|
||||
|
@ -186,16 +109,15 @@ exports[`FilteringPanel renders filtering rules list 1`] = `
|
|||
"rule": ">",
|
||||
"value": "THIS VALUE",
|
||||
},
|
||||
Object {
|
||||
"order": 4,
|
||||
"policy": "exclude",
|
||||
"rule": "<",
|
||||
"value": "THIS VALUE",
|
||||
},
|
||||
]
|
||||
}
|
||||
noItemsMessage={
|
||||
<EuiI18n
|
||||
default="No items found"
|
||||
token="euiBasicTable.noItemsMessage"
|
||||
/>
|
||||
}
|
||||
responsive={true}
|
||||
tableLayout="fixed"
|
||||
showOrder={false}
|
||||
/>
|
||||
</FlyoutPanel>
|
||||
</Fragment>
|
||||
|
|
|
@ -7,25 +7,13 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiCode,
|
||||
EuiCodeBlock,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { EuiCodeBlock, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
FilteringPolicy,
|
||||
FilteringRule,
|
||||
FilteringRuleRule,
|
||||
FilteringRules,
|
||||
} from '../../../../../../common/types/connectors';
|
||||
import { FilteringRule, FilteringRules } from '../../../../../../common/types/connectors';
|
||||
|
||||
import { filteringRuleToText, filteringPolicyToText } from '../../../utils/filtering_rule_helpers';
|
||||
import { FilteringRulesTable } from '../../shared/filtering_rules_table/filtering_rules_table';
|
||||
|
||||
import { FlyoutPanel } from './flyout_panel';
|
||||
|
||||
|
@ -38,29 +26,6 @@ export const FilteringPanel: React.FC<FilteringPanelProps> = ({
|
|||
advancedSnippet,
|
||||
filteringRules,
|
||||
}) => {
|
||||
const columns: Array<EuiBasicTableColumn<FilteringRule>> = [
|
||||
{
|
||||
field: 'policy',
|
||||
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.filtering.policy', {
|
||||
defaultMessage: 'Pipeline setting',
|
||||
}),
|
||||
render: (policy: FilteringPolicy) => filteringPolicyToText(policy),
|
||||
},
|
||||
{
|
||||
field: 'rule',
|
||||
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.filtering.rule', {
|
||||
defaultMessage: 'Rule',
|
||||
}),
|
||||
render: (rule: FilteringRuleRule) => filteringRuleToText(rule),
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.filtering.value', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
render: (value: string) => <EuiCode>{value}</EuiCode>,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<FlyoutPanel
|
||||
|
@ -68,10 +33,7 @@ export const FilteringPanel: React.FC<FilteringPanelProps> = ({
|
|||
defaultMessage: 'Filtering',
|
||||
})}
|
||||
>
|
||||
<EuiBasicTable
|
||||
columns={columns}
|
||||
items={filteringRules.sort(({ order }, { order: secondOrder }) => order - secondOrder)}
|
||||
/>
|
||||
<FilteringRulesTable filteringRules={filteringRules} showOrder={false} />
|
||||
</FlyoutPanel>
|
||||
{!!advancedSnippet?.value ? (
|
||||
<>
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 { EuiBasicTable, EuiBasicTableColumn, EuiCode } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
FilteringRule,
|
||||
FilteringPolicy,
|
||||
FilteringRuleRule,
|
||||
} from '../../../../../../common/types/connectors';
|
||||
|
||||
import { filteringPolicyToText, filteringRuleToText } from '../../../utils/filtering_rule_helpers';
|
||||
|
||||
interface FilteringRulesTableProps {
|
||||
filteringRules: FilteringRule[];
|
||||
showOrder: boolean;
|
||||
}
|
||||
|
||||
export const FilteringRulesTable: React.FC<FilteringRulesTableProps> = ({
|
||||
showOrder,
|
||||
filteringRules,
|
||||
}) => {
|
||||
const columns: Array<EuiBasicTableColumn<FilteringRule>> = [
|
||||
...(showOrder
|
||||
? [
|
||||
{
|
||||
field: 'order',
|
||||
name: i18n.translate('xpack.enterpriseSearch.content.index.filtering.priority', {
|
||||
defaultMessage: 'Rule priority',
|
||||
}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
field: 'policy',
|
||||
name: i18n.translate('xpack.enterpriseSearch.content.index.filtering.policy', {
|
||||
defaultMessage: 'Policy',
|
||||
}),
|
||||
render: (policy: FilteringPolicy) => filteringPolicyToText(policy),
|
||||
},
|
||||
{
|
||||
field: 'field',
|
||||
name: i18n.translate('xpack.enterpriseSearch.content.index.filtering.field', {
|
||||
defaultMessage: 'field',
|
||||
}),
|
||||
render: (value: string) => <EuiCode>{value}</EuiCode>,
|
||||
},
|
||||
{
|
||||
field: 'rule',
|
||||
name: i18n.translate('xpack.enterpriseSearch.content.index.filtering.rule', {
|
||||
defaultMessage: 'Rule',
|
||||
}),
|
||||
render: (rule: FilteringRuleRule) => filteringRuleToText(rule),
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
name: i18n.translate('xpack.enterpriseSearch.content.index.filtering.value', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
render: (value: string) => <EuiCode>{value}</EuiCode>,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<EuiBasicTable
|
||||
columns={columns}
|
||||
items={filteringRules.sort(({ order }, { order: secondOrder }) => order - secondOrder)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -24,6 +24,7 @@ import { InlineEditableTableLogic } from './inline_editable_table_logic';
|
|||
|
||||
interface ActionColumnProps<Item extends ItemWithAnID> {
|
||||
displayedItems: Item[];
|
||||
emptyPropertyAllowed?: boolean;
|
||||
isActivelyEditing: (i: Item) => boolean;
|
||||
isLoading?: boolean;
|
||||
item: Item;
|
||||
|
@ -33,6 +34,7 @@ interface ActionColumnProps<Item extends ItemWithAnID> {
|
|||
}
|
||||
|
||||
export const ActionColumn = <Item extends ItemWithAnID>({
|
||||
emptyPropertyAllowed = false,
|
||||
displayedItems,
|
||||
isActivelyEditing,
|
||||
isLoading = false,
|
||||
|
@ -61,7 +63,11 @@ export const ActionColumn = <Item extends ItemWithAnID>({
|
|||
color="primary"
|
||||
iconType="checkInCircleFilled"
|
||||
onClick={isEditingUnsavedItem ? saveNewItem : saveExistingItem}
|
||||
disabled={isLoading || isInvalid || doesEditingItemValueContainEmptyProperty}
|
||||
disabled={
|
||||
isLoading ||
|
||||
isInvalid ||
|
||||
(doesEditingItemValueContainEmptyProperty && !emptyPropertyAllowed)
|
||||
}
|
||||
>
|
||||
{SAVE_BUTTON_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
|
|
|
@ -81,6 +81,7 @@ describe('getUpdatedColumns', () => {
|
|||
expect(actionColumn.props()).toEqual({
|
||||
isActivelyEditing: expect.any(Function),
|
||||
displayedItems,
|
||||
emptyPropertyAllowed: false,
|
||||
isLoading: false,
|
||||
canRemoveLastItem,
|
||||
lastItemWarning,
|
||||
|
|
|
@ -17,6 +17,7 @@ import { InlineEditableTableColumn } from './types';
|
|||
interface GetUpdatedColumnProps<Item extends ItemWithAnID> {
|
||||
columns: Array<InlineEditableTableColumn<Item>>;
|
||||
displayedItems: Item[];
|
||||
emptyPropertyAllowed?: boolean;
|
||||
isActivelyEditing: (item: Item) => boolean;
|
||||
canRemoveLastItem?: boolean;
|
||||
isLoading?: boolean;
|
||||
|
@ -27,6 +28,7 @@ interface GetUpdatedColumnProps<Item extends ItemWithAnID> {
|
|||
export const getUpdatedColumns = <Item extends ItemWithAnID>({
|
||||
columns,
|
||||
displayedItems,
|
||||
emptyPropertyAllowed = false,
|
||||
isActivelyEditing,
|
||||
canRemoveLastItem,
|
||||
isLoading = false,
|
||||
|
@ -52,6 +54,7 @@ export const getUpdatedColumns = <Item extends ItemWithAnID>({
|
|||
render: (item: Item) => (
|
||||
<ActionColumn
|
||||
displayedItems={displayedItems}
|
||||
emptyPropertyAllowed={emptyPropertyAllowed ?? false}
|
||||
isActivelyEditing={isActivelyEditing}
|
||||
isLoading={isLoading}
|
||||
canRemoveLastItem={canRemoveLastItem}
|
||||
|
|
|
@ -28,6 +28,7 @@ export interface InlineEditableTableProps<Item extends ItemWithAnID> {
|
|||
columns: Array<InlineEditableTableColumn<Item>>;
|
||||
items: Item[];
|
||||
defaultItem?: Partial<Item>;
|
||||
emptyPropertyAllowed?: boolean;
|
||||
title: string;
|
||||
addButtonText?: string;
|
||||
canRemoveLastItem?: boolean;
|
||||
|
@ -87,6 +88,7 @@ export const InlineEditableTable = <Item extends ItemWithAnID>(
|
|||
|
||||
export const InlineEditableTableContents = <Item extends ItemWithAnID>({
|
||||
columns,
|
||||
emptyPropertyAllowed,
|
||||
items,
|
||||
title,
|
||||
addButtonText,
|
||||
|
@ -117,6 +119,7 @@ export const InlineEditableTableContents = <Item extends ItemWithAnID>({
|
|||
const updatedColumns = getUpdatedColumns({
|
||||
columns,
|
||||
displayedItems,
|
||||
emptyPropertyAllowed,
|
||||
isActivelyEditing,
|
||||
canRemoveLastItem,
|
||||
isLoading,
|
||||
|
|
|
@ -88,6 +88,7 @@ describe('addConnector lib function', () => {
|
|||
configuration: {},
|
||||
description: null,
|
||||
error: null,
|
||||
features: null,
|
||||
filtering: [
|
||||
{
|
||||
active: {
|
||||
|
@ -270,6 +271,7 @@ describe('addConnector lib function', () => {
|
|||
configuration: {},
|
||||
description: null,
|
||||
error: null,
|
||||
features: null,
|
||||
filtering: [
|
||||
{
|
||||
active: {
|
||||
|
@ -374,6 +376,7 @@ describe('addConnector lib function', () => {
|
|||
configuration: {},
|
||||
description: null,
|
||||
error: null,
|
||||
features: null,
|
||||
filtering: [
|
||||
{
|
||||
active: {
|
||||
|
|
|
@ -95,6 +95,7 @@ export const addConnector = async (
|
|||
configuration: {},
|
||||
description: null,
|
||||
error: null,
|
||||
features: null,
|
||||
filtering: [
|
||||
{
|
||||
active: {
|
||||
|
|
|
@ -29,11 +29,17 @@ describe('fetchConnectors lib', () => {
|
|||
describe('fetch connector by id', () => {
|
||||
it('should fetch connector by id', async () => {
|
||||
mockClient.asCurrentUser.get.mockImplementationOnce(() =>
|
||||
Promise.resolve({ _id: 'connectorId', _source: { source: 'source' } })
|
||||
Promise.resolve({
|
||||
_id: 'connectorId',
|
||||
_primary_term: 'primaryTerm',
|
||||
_seq_no: 5,
|
||||
_source: { source: 'source' },
|
||||
})
|
||||
);
|
||||
await expect(fetchConnectorById(mockClient as any, 'id')).resolves.toEqual({
|
||||
id: 'connectorId',
|
||||
source: 'source',
|
||||
primaryTerm: 'primaryTerm',
|
||||
seqNo: 5,
|
||||
value: { id: 'connectorId', source: 'source' },
|
||||
});
|
||||
expect(mockClient.asCurrentUser.get).toHaveBeenCalledWith({
|
||||
id: 'id',
|
||||
|
|
|
@ -10,6 +10,7 @@ import { IScopedClusterClient } from '@kbn/core/server';
|
|||
|
||||
import { CONNECTORS_INDEX } from '../..';
|
||||
import { Connector, ConnectorDocument } from '../../../common/types/connectors';
|
||||
import { OptimisticConcurrency } from '../../../common/types/util_types';
|
||||
import { setupConnectorsIndices } from '../../index_management/setup_indices';
|
||||
|
||||
import { isIndexNotFoundException } from '../../utils/identify_exceptions';
|
||||
|
@ -18,14 +19,18 @@ import { fetchAll } from '../fetch_all';
|
|||
export const fetchConnectorById = async (
|
||||
client: IScopedClusterClient,
|
||||
connectorId: string
|
||||
): Promise<Connector | undefined> => {
|
||||
): Promise<OptimisticConcurrency<Connector> | undefined> => {
|
||||
try {
|
||||
const connectorResult = await client.asCurrentUser.get<ConnectorDocument>({
|
||||
id: connectorId,
|
||||
index: CONNECTORS_INDEX,
|
||||
});
|
||||
return connectorResult._source
|
||||
? { ...connectorResult._source, id: connectorResult._id }
|
||||
? {
|
||||
primaryTerm: connectorResult._primary_term,
|
||||
seqNo: connectorResult._seq_no,
|
||||
value: { ...connectorResult._source, id: connectorResult._id },
|
||||
}
|
||||
: undefined;
|
||||
} catch (error) {
|
||||
if (isIndexNotFoundException(error)) {
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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-elasticsearch-server';
|
||||
|
||||
import { CONNECTORS_INDEX } from '../..';
|
||||
import {
|
||||
Connector,
|
||||
FilteringRule,
|
||||
FilteringRules,
|
||||
FilteringValidationState,
|
||||
} from '../../../common/types/connectors';
|
||||
|
||||
import { fetchConnectorById } from './fetch_connectors';
|
||||
|
||||
export const updateFiltering = async (
|
||||
client: IScopedClusterClient,
|
||||
connectorId: string,
|
||||
{
|
||||
advancedSnippet,
|
||||
filteringRules,
|
||||
}: {
|
||||
advancedSnippet: string;
|
||||
filteringRules: FilteringRule[];
|
||||
}
|
||||
): Promise<FilteringRules | undefined> => {
|
||||
const now = new Date().toISOString();
|
||||
const parsedAdvancedSnippet: Record<string, unknown> = advancedSnippet
|
||||
? JSON.parse(advancedSnippet)
|
||||
: {};
|
||||
const parsedFilteringRules = filteringRules.map((filteringRule) => ({
|
||||
...filteringRule,
|
||||
created_at: filteringRule.created_at ? filteringRule.created_at : now,
|
||||
updated_at: now,
|
||||
}));
|
||||
const connectorResult = await fetchConnectorById(client, connectorId);
|
||||
if (!connectorResult) {
|
||||
throw new Error(`Could not find connector with id ${connectorId}`);
|
||||
}
|
||||
const { value: connector, seqNo, primaryTerm } = connectorResult;
|
||||
const active: FilteringRules = {
|
||||
advanced_snippet: {
|
||||
created_at: connector.filtering[0].active.advanced_snippet.created_at || now,
|
||||
updated_at: now,
|
||||
value: parsedAdvancedSnippet,
|
||||
},
|
||||
rules: parsedFilteringRules,
|
||||
validation: {
|
||||
errors: [],
|
||||
state: FilteringValidationState.VALID,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await client.asCurrentUser.update<Connector>({
|
||||
doc: { ...connector, filtering: [{ ...connector.filtering[0], active, draft: active }] },
|
||||
id: connectorId,
|
||||
if_primary_term: primaryTerm,
|
||||
if_seq_no: seqNo,
|
||||
index: CONNECTORS_INDEX,
|
||||
refresh: true,
|
||||
});
|
||||
|
||||
return result.result === 'updated' ? active : undefined;
|
||||
};
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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-elasticsearch-server';
|
||||
|
||||
import { CONNECTORS_INDEX } from '../..';
|
||||
import {
|
||||
Connector,
|
||||
FilteringRule,
|
||||
FilteringRules,
|
||||
FilteringValidationState,
|
||||
} from '../../../common/types/connectors';
|
||||
|
||||
import { fetchConnectorById } from './fetch_connectors';
|
||||
|
||||
export const updateFilteringDraft = async (
|
||||
client: IScopedClusterClient,
|
||||
connectorId: string,
|
||||
{
|
||||
advancedSnippet,
|
||||
filteringRules,
|
||||
}: {
|
||||
advancedSnippet: string;
|
||||
filteringRules: FilteringRule[];
|
||||
}
|
||||
): Promise<FilteringRules | undefined> => {
|
||||
const now = new Date().toISOString();
|
||||
const parsedAdvancedSnippet: Record<string, unknown> = advancedSnippet
|
||||
? JSON.parse(advancedSnippet)
|
||||
: {};
|
||||
const parsedFilteringRules = filteringRules.map((filteringRule) => ({
|
||||
...filteringRule,
|
||||
created_at: filteringRule.created_at ? filteringRule.created_at : now,
|
||||
updated_at: now,
|
||||
}));
|
||||
const draft: FilteringRules = {
|
||||
advanced_snippet: {
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
value: parsedAdvancedSnippet,
|
||||
},
|
||||
rules: parsedFilteringRules,
|
||||
validation: {
|
||||
errors: [],
|
||||
state: FilteringValidationState.EDITED,
|
||||
},
|
||||
};
|
||||
const connectorResult = await fetchConnectorById(client, connectorId);
|
||||
if (!connectorResult) {
|
||||
throw new Error(`Could not find connector with id ${connectorId}`);
|
||||
}
|
||||
const { value: connector, seqNo, primaryTerm } = connectorResult;
|
||||
|
||||
const result = await client.asCurrentUser.update<Connector>({
|
||||
doc: { ...connector, filtering: [{ ...connector.filtering[0], draft }] },
|
||||
id: connectorId,
|
||||
if_primary_term: primaryTerm,
|
||||
if_seq_no: seqNo,
|
||||
index: CONNECTORS_INDEX,
|
||||
refresh: true,
|
||||
});
|
||||
|
||||
return result.result === 'updated' ? draft : undefined;
|
||||
};
|
|
@ -9,12 +9,19 @@ import { schema } from '@kbn/config-schema';
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { ConnectorStatus } from '../../../common/types/connectors';
|
||||
import {
|
||||
ConnectorStatus,
|
||||
FilteringPolicy,
|
||||
FilteringRule,
|
||||
FilteringRuleRule,
|
||||
} from '../../../common/types/connectors';
|
||||
|
||||
import { ErrorCode } from '../../../common/types/error_codes';
|
||||
import { addConnector } from '../../lib/connectors/add_connector';
|
||||
import { fetchSyncJobsByConnectorId } from '../../lib/connectors/fetch_sync_jobs';
|
||||
import { cancelSyncs } from '../../lib/connectors/post_cancel_syncs';
|
||||
import { updateFiltering } from '../../lib/connectors/put_update_filtering';
|
||||
import { updateFilteringDraft } from '../../lib/connectors/put_update_filtering_draft';
|
||||
import { startConnectorSync } from '../../lib/connectors/start_sync';
|
||||
import { updateConnectorConfiguration } from '../../lib/connectors/update_connector_configuration';
|
||||
import { updateConnectorNameAndDescription } from '../../lib/connectors/update_connector_name_and_description';
|
||||
|
@ -28,6 +35,7 @@ import { updateConnectorPipeline } from '../../lib/pipelines/update_pipeline';
|
|||
import { RouteDependencies } from '../../plugin';
|
||||
import { createError } from '../../utils/create_error';
|
||||
import { elasticsearchErrorHandler } from '../../utils/elasticsearch_error_handler';
|
||||
import { validateEnum } from '../../utils/validate_enum';
|
||||
|
||||
export function registerConnectorRoutes({ router, log }: RouteDependencies) {
|
||||
router.post(
|
||||
|
@ -127,12 +135,12 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
|
|||
{
|
||||
path: '/internal/enterprise_search/connectors/{connectorId}/start_sync',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
connectorId: schema.string(),
|
||||
}),
|
||||
body: schema.object({
|
||||
nextSyncConfig: schema.maybe(schema.string()),
|
||||
}),
|
||||
params: schema.object({
|
||||
connectorId: schema.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
elasticsearchErrorHandler(log, async (context, request, response) => {
|
||||
|
@ -225,10 +233,10 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
|
|||
{
|
||||
path: '/internal/enterprise_search/connectors/{connectorId}/service_type',
|
||||
validate: {
|
||||
body: schema.object({ serviceType: schema.string() }),
|
||||
params: schema.object({
|
||||
connectorId: schema.string(),
|
||||
}),
|
||||
body: schema.object({ serviceType: schema.string() }),
|
||||
},
|
||||
},
|
||||
elasticsearchErrorHandler(log, async (context, request, response) => {
|
||||
|
@ -246,10 +254,10 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
|
|||
{
|
||||
path: '/internal/enterprise_search/connectors/{connectorId}/status',
|
||||
validate: {
|
||||
body: schema.object({ status: schema.string() }),
|
||||
params: schema.object({
|
||||
connectorId: schema.string(),
|
||||
}),
|
||||
body: schema.object({ status: schema.string() }),
|
||||
},
|
||||
},
|
||||
elasticsearchErrorHandler(log, async (context, request, response) => {
|
||||
|
@ -267,13 +275,13 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
|
|||
{
|
||||
path: '/internal/enterprise_search/connectors/{connectorId}/name_and_description',
|
||||
validate: {
|
||||
body: schema.object({
|
||||
description: schema.nullable(schema.string()),
|
||||
name: schema.string(),
|
||||
}),
|
||||
params: schema.object({
|
||||
connectorId: schema.string(),
|
||||
}),
|
||||
body: schema.object({
|
||||
name: schema.string(),
|
||||
description: schema.nullable(schema.string()),
|
||||
}),
|
||||
},
|
||||
},
|
||||
elasticsearchErrorHandler(log, async (context, request, response) => {
|
||||
|
@ -286,4 +294,88 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
|
|||
return response.ok({ body: result });
|
||||
})
|
||||
);
|
||||
|
||||
router.put(
|
||||
{
|
||||
path: '/internal/enterprise_search/connectors/{connectorId}/filtering/draft',
|
||||
validate: {
|
||||
body: schema.object({
|
||||
advanced_snippet: schema.string(),
|
||||
filtering_rules: schema.arrayOf(
|
||||
schema.object({
|
||||
created_at: schema.string(),
|
||||
field: schema.string(),
|
||||
id: schema.string(),
|
||||
order: schema.number(),
|
||||
policy: schema.string({
|
||||
validate: validateEnum(FilteringPolicy, 'policy'),
|
||||
}),
|
||||
rule: schema.string({
|
||||
validate: validateEnum(FilteringRuleRule, 'rule'),
|
||||
}),
|
||||
updated_at: schema.string(),
|
||||
value: schema.string(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
params: schema.object({
|
||||
connectorId: schema.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
elasticsearchErrorHandler(log, async (context, request, response) => {
|
||||
const { client } = (await context.core).elasticsearch;
|
||||
const { connectorId } = request.params;
|
||||
const { advanced_snippet, filtering_rules } = request.body;
|
||||
const result = await updateFilteringDraft(client, connectorId, {
|
||||
advancedSnippet: advanced_snippet,
|
||||
// Have to cast here because our API schema validator doesn't know how to deal with enums
|
||||
// We're relying on the schema in the validator above to flag if something goes wrong
|
||||
filteringRules: filtering_rules as FilteringRule[],
|
||||
});
|
||||
return result ? response.ok({ body: result }) : response.conflict();
|
||||
})
|
||||
);
|
||||
|
||||
router.put(
|
||||
{
|
||||
path: '/internal/enterprise_search/connectors/{connectorId}/filtering',
|
||||
validate: {
|
||||
body: schema.object({
|
||||
advanced_snippet: schema.string(),
|
||||
filtering_rules: schema.arrayOf(
|
||||
schema.object({
|
||||
created_at: schema.string(),
|
||||
field: schema.string(),
|
||||
id: schema.string(),
|
||||
order: schema.number(),
|
||||
policy: schema.string({
|
||||
validate: validateEnum(FilteringPolicy, 'policy'),
|
||||
}),
|
||||
rule: schema.string({
|
||||
validate: validateEnum(FilteringRuleRule, 'rule'),
|
||||
}),
|
||||
updated_at: schema.string(),
|
||||
value: schema.string(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
params: schema.object({
|
||||
connectorId: schema.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
elasticsearchErrorHandler(log, async (context, request, response) => {
|
||||
const { client } = (await context.core).elasticsearch;
|
||||
const { connectorId } = request.params;
|
||||
const { advanced_snippet, filtering_rules } = request.body;
|
||||
const result = await updateFiltering(client, connectorId, {
|
||||
advancedSnippet: advanced_snippet,
|
||||
// Have to cast here because our API schema validator doesn't know how to deal with enums
|
||||
// We're relying on the schema in the validator above to flag if something goes wrong
|
||||
filteringRules: filtering_rules as FilteringRule[],
|
||||
});
|
||||
return result ? response.ok({ body: result }) : response.conflict();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param enumToValidateAgainst enum to validate against
|
||||
* @param fieldName name of field, used for diagnostic messaging
|
||||
* @returns A validator usable in our API schema validator to validate that values adhere to the enum they're supposed to have
|
||||
*/
|
||||
|
||||
export function validateEnum(
|
||||
enumToValidateAgainst: Record<string | number, string | number>,
|
||||
fieldName: string
|
||||
) {
|
||||
return (value: string) => {
|
||||
if (!Object.values(enumToValidateAgainst).includes(value)) {
|
||||
return i18n.translate('xpack.enterpriseSearch.server.utils.invalidEnumValue', {
|
||||
defaultMessage: 'Illegal value {value} for field {fieldName}',
|
||||
values: {
|
||||
fieldName,
|
||||
value,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue