Search serverless connector config (#169139)

## Summary

This adds the ability to edit connector configuration in Serverless, and
moves connector configuration to a shared package.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Sander Philipse 2023-10-24 18:47:28 +02:00 committed by GitHub
parent ea6b251ee8
commit 1f1c4553db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
94 changed files with 2540 additions and 2570 deletions

View file

@ -0,0 +1,196 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { createContext, useEffect, useState } from 'react';
import {
EuiButton,
EuiCallOut,
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { sortAndFilterConnectorConfiguration } from '../../utils/connector_configuration_utils';
import { Connector, ConnectorConfigProperties, ConnectorStatus, FeatureName } from '../..';
import { ConnectorConfigurationForm } from './connector_configuration_form';
function entryToDisplaylistItem(entry: ConfigEntryView): { description: string; title: string } {
return {
description: entry.sensitive && !!entry.value ? '********' : String(entry.value) || '--',
title: entry.label,
};
}
interface ConnectorConfigurationProps {
connector: Connector;
hasPlatinumLicense: boolean;
isLoading: boolean;
saveConfig: (configuration: Record<string, string | number | boolean | null>) => void;
stackManagementLink?: string;
subscriptionLink?: string;
}
interface ConfigEntry extends ConnectorConfigProperties {
key: string;
}
export interface ConfigEntryView extends ConfigEntry {
isValid: boolean;
validationErrors: string[];
}
export interface CategoryEntry {
configEntries: ConfigEntryView[];
key: string;
label: string;
order: number;
}
export interface ConfigView {
advancedConfigurations: ConfigEntryView[];
categories: CategoryEntry[];
unCategorizedItems: ConfigEntryView[];
}
export const LicenseContext = createContext<{
hasPlatinumLicense: boolean;
stackManagementLink?: string;
subscriptionLink?: string;
}>({
hasPlatinumLicense: false,
subscriptionLink: undefined,
stackManagementLink: undefined,
});
export const ConnectorConfigurationComponent: React.FC<ConnectorConfigurationProps> = ({
children,
connector,
hasPlatinumLicense,
isLoading,
saveConfig,
subscriptionLink,
stackManagementLink,
}) => {
const {
configuration,
error,
status: connectorStatus,
is_native: isNative,
features,
} = connector;
const hasDocumentLevelSecurity = Boolean(
features?.[FeatureName.DOCUMENT_LEVEL_SECURITY]?.enabled
);
const [isEditing, setIsEditing] = useState(false);
useEffect(() => {
setIsEditing(false);
}, [configuration]);
useEffect(() => {
if (
Object.keys(configuration || {}).length > 0 &&
(connectorStatus === ConnectorStatus.CREATED ||
connectorStatus === ConnectorStatus.NEEDS_CONFIGURATION)
) {
// Only start in edit mode if we haven't configured yet
// Necessary to prevent a race condition between saving config and getting updated connector
setIsEditing(true);
}
}, [configuration, connectorStatus]);
const configView = sortAndFilterConnectorConfiguration(configuration, isNative);
const uncategorizedDisplayList = configView.unCategorizedItems.map(entryToDisplaylistItem);
return (
<LicenseContext.Provider value={{ hasPlatinumLicense, stackManagementLink, subscriptionLink }}>
<EuiFlexGroup direction="column">
{children && <EuiFlexItem>{children}</EuiFlexItem>}
<EuiFlexItem>
{isEditing ? (
<ConnectorConfigurationForm
cancelEditing={() => setIsEditing(false)}
configuration={configuration}
hasDocumentLevelSecurity={hasDocumentLevelSecurity}
isLoading={isLoading}
isNative={isNative}
saveConfig={(config) => {
saveConfig(config);
setIsEditing(false);
}}
/>
) : (
uncategorizedDisplayList.length > 0 && (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiDescriptionList
listItems={uncategorizedDisplayList}
className="eui-textBreakWord"
/>
</EuiFlexItem>
{configView.categories.length > 0 &&
configView.categories.map((category) => (
<EuiFlexGroup direction="column" key={category.key}>
<EuiFlexItem>
<EuiTitle size="s">
<h3>{category.label}</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiDescriptionList
listItems={category.configEntries.map(entryToDisplaylistItem)}
className="eui-textBreakWord"
/>
</EuiFlexItem>
</EuiFlexGroup>
))}
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="entSearchContent-connector-configuration-editConfiguration"
data-telemetry-id="entSearchContent-connector-overview-configuration-editConfiguration"
onClick={() => setIsEditing(!isEditing)}
>
{i18n.translate(
'searchConnectors.configurationConnector.config.editButton.title',
{
defaultMessage: 'Edit configuration',
}
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
)
)}
</EuiFlexItem>
{!!error && (
<EuiFlexItem>
<EuiCallOut
color="danger"
title={i18n.translate('searchConnectors.configurationConnector.config.error.title', {
defaultMessage: 'Connector error',
})}
>
<EuiText size="s">{error}</EuiText>
</EuiCallOut>
</EuiFlexItem>
)}
</EuiFlexGroup>
</LicenseContext.Provider>
);
};

View file

@ -1,13 +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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState } from 'react';
import { useActions, useValues } from 'kea';
import React, { useContext, useState } from 'react';
import {
EuiAccordion,
@ -25,35 +24,39 @@ import {
import { i18n } from '@kbn/i18n';
import { DisplayType } from '@kbn/search-connectors';
import { DisplayType } from '../..';
import { Status } from '../../../../../../common/types/api';
import { LicensingLogic } from '../../../../shared/licensing';
import { ConnectorConfigurationApiLogic } from '../../../api/connector/update_connector_configuration_api_logic';
import { PlatinumLicensePopover } from '../../shared/platinum_license_popover/platinum_license_popover';
import { ConnectorConfigurationLogic, ConfigEntryView } from './connector_configuration_logic';
import { DocumentLevelSecurityPanel } from './document_level_security/document_level_security_panel';
import { ensureBooleanType, ensureStringType } from './utils/connector_configuration_utils';
import { ConfigEntryView, LicenseContext } from './connector_configuration';
import { DocumentLevelSecurityPanel } from './document_level_security_panel';
import {
ensureBooleanType,
ensureCorrectTyping,
ensureStringType,
} from '../../utils/connector_configuration_utils';
import { PlatinumLicensePopover } from './platinum_license_popover';
interface ConnectorConfigurationFieldProps {
configEntry: ConfigEntryView;
isLoading: boolean;
setConfigValue: (value: number | string | boolean | null) => void;
}
export const ConnectorConfigurationField: React.FC<ConnectorConfigurationFieldProps> = ({
configEntry,
isLoading,
setConfigValue,
}) => {
const { status } = useValues(ConnectorConfigurationApiLogic);
const { setLocalConfigEntry } = useActions(ConnectorConfigurationLogic);
const { hasPlatinumLicense } = useValues(LicensingLogic);
const { hasPlatinumLicense, stackManagementLink, subscriptionLink } = useContext(LicenseContext);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const validateAndSetConfigValue = (value: number | string | boolean) => {
setConfigValue(ensureCorrectTyping(configEntry.type, value));
};
const {
key,
display,
is_valid: isValid,
isValid,
label,
options,
required,
@ -67,22 +70,22 @@ export const ConnectorConfigurationField: React.FC<ConnectorConfigurationFieldPr
case DisplayType.DROPDOWN:
return options.length > 3 ? (
<EuiSelect
disabled={status === Status.LOADING}
disabled={isLoading}
options={options.map((option) => ({ text: option.label, value: option.value }))}
required={required}
value={ensureStringType(value)}
onChange={(event) => {
setLocalConfigEntry({ ...configEntry, value: event.target.value });
validateAndSetConfigValue(event.target.value);
}}
/>
) : (
<EuiRadioGroup
disabled={status === Status.LOADING}
disabled={isLoading}
idSelected={ensureStringType(value)}
name="radio group"
options={options.map((option) => ({ id: option.value, label: option.label }))}
onChange={(id) => {
setLocalConfigEntry({ ...configEntry, value: id });
validateAndSetConfigValue(id);
}}
/>
);
@ -90,12 +93,12 @@ export const ConnectorConfigurationField: React.FC<ConnectorConfigurationFieldPr
case DisplayType.NUMERIC:
return (
<EuiFieldText
disabled={status === Status.LOADING}
disabled={isLoading}
required={required}
value={ensureStringType(value)}
isInvalid={!isValid}
onChange={(event) => {
setLocalConfigEntry({ ...configEntry, value: event.target.value });
validateAndSetConfigValue(event.target.value);
}}
placeholder={placeholder}
/>
@ -104,12 +107,12 @@ export const ConnectorConfigurationField: React.FC<ConnectorConfigurationFieldPr
case DisplayType.TEXTAREA:
const textarea = (
<EuiTextArea
disabled={status === Status.LOADING}
disabled={isLoading}
placeholder={placeholder}
required={required}
value={ensureStringType(value) || undefined} // ensures placeholder shows up when value is empty string
onChange={(event) => {
setLocalConfigEntry({ ...configEntry, value: event.target.value });
validateAndSetConfigValue(event.target.value);
}}
/>
);
@ -147,10 +150,10 @@ export const ConnectorConfigurationField: React.FC<ConnectorConfigurationFieldPr
<EuiFlexItem grow={false}>
<EuiSwitch
checked={ensureBooleanType(value)}
disabled={status === Status.LOADING || !hasPlatinumLicense}
disabled={isLoading || !hasPlatinumLicense}
label={<p>{label}</p>}
onChange={(event) => {
setLocalConfigEntry({ ...configEntry, value: event.target.checked });
validateAndSetConfigValue(event.target.checked);
}}
/>
</EuiFlexItem>
@ -160,7 +163,7 @@ export const ConnectorConfigurationField: React.FC<ConnectorConfigurationFieldPr
button={
<EuiButtonIcon
aria-label={i18n.translate(
'xpack.enterpriseSearch.content.newIndex.selectConnector.openPopoverLabel',
'searchConnectors.configuration.openPopoverLabel',
{
defaultMessage: 'Open licensing popover',
}
@ -171,6 +174,8 @@ export const ConnectorConfigurationField: React.FC<ConnectorConfigurationFieldPr
}
closePopover={() => setIsPopoverOpen(false)}
isPopoverOpen={isPopoverOpen}
stackManagementHref={stackManagementLink}
subscriptionLink={subscriptionLink}
/>
</EuiFlexItem>
)}
@ -182,7 +187,7 @@ export const ConnectorConfigurationField: React.FC<ConnectorConfigurationFieldPr
return (
<EuiSwitch
checked={ensureBooleanType(value)}
disabled={status === Status.LOADING}
disabled={isLoading}
label={
tooltip ? (
<EuiFlexGroup gutterSize="xs">
@ -198,7 +203,7 @@ export const ConnectorConfigurationField: React.FC<ConnectorConfigurationFieldPr
)
}
onChange={(event) => {
setLocalConfigEntry({ ...configEntry, value: event.target.checked });
validateAndSetConfigValue(event.target.checked);
}}
/>
);
@ -206,22 +211,22 @@ export const ConnectorConfigurationField: React.FC<ConnectorConfigurationFieldPr
default:
return sensitive ? (
<EuiFieldPassword
disabled={status === Status.LOADING}
disabled={isLoading}
required={required}
type="dual"
value={ensureStringType(value)}
onChange={(event) => {
setLocalConfigEntry({ ...configEntry, value: event.target.value });
validateAndSetConfigValue(event.target.value);
}}
/>
) : (
<EuiFieldText
disabled={status === Status.LOADING}
disabled={isLoading}
placeholder={placeholder}
required={required}
value={ensureStringType(value)}
onChange={(event) => {
setLocalConfigEntry({ ...configEntry, value: event.target.value });
validateAndSetConfigValue(event.target.value);
}}
/>
);

View file

@ -0,0 +1,182 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useEffect, useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiSpacer,
EuiPanel,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isCategoryEntry } from '../../utils';
import { sortAndFilterConnectorConfiguration } from '../../utils/connector_configuration_utils';
import { ConnectorConfiguration } from '../../types';
import { ConfigView } from './connector_configuration';
import { ConnectorConfigurationFormItems } from './connector_configuration_form_items';
interface ConnectorConfigurationForm {
cancelEditing: () => void;
configuration: ConnectorConfiguration;
hasDocumentLevelSecurity: boolean;
isLoading: boolean;
isNative: boolean;
saveConfig: (config: Record<string, string | number | boolean | null>) => void;
stackManagementHref?: string;
subscriptionLink?: string;
}
function configViewToConfigValues(
configView: ConfigView
): Record<string, string | number | boolean | null> {
const result: Record<string, string | number | boolean | null> = {};
for (const { key, value } of configView.advancedConfigurations) {
result[key] = value;
}
for (const { key, value } of configView.unCategorizedItems) {
result[key] = value;
}
return result;
}
export const ConnectorConfigurationForm: React.FC<ConnectorConfigurationForm> = ({
cancelEditing,
configuration,
hasDocumentLevelSecurity,
isLoading,
isNative,
saveConfig,
}) => {
const [localConfig, setLocalConfig] = useState<ConnectorConfiguration>(configuration);
const [configView, setConfigView] = useState<ConfigView>(
sortAndFilterConnectorConfiguration(configuration, isNative)
);
useEffect(() => {
setConfigView(sortAndFilterConnectorConfiguration(localConfig, isNative));
}, [localConfig, isNative]);
useEffect(() => {
setLocalConfig(configuration);
}, [configuration]);
return (
<EuiForm
onSubmit={(event) => {
event.preventDefault();
saveConfig(configViewToConfigValues(configView));
}}
component="form"
>
<ConnectorConfigurationFormItems
isLoading={isLoading}
items={configView.unCategorizedItems}
hasDocumentLevelSecurityEnabled={hasDocumentLevelSecurity}
setConfigEntry={(key, value) => {
const entry = localConfig[key];
if (entry && !isCategoryEntry(entry)) {
const newConfiguration: ConnectorConfiguration = {
...localConfig,
[key]: { ...entry, value },
};
setLocalConfig(newConfiguration);
}
}}
/>
{configView.categories.map((category, index) => (
<React.Fragment key={index}>
<EuiSpacer />
<EuiTitle size="s">
<h3>{category.label}</h3>
</EuiTitle>
<EuiSpacer />
<ConnectorConfigurationFormItems
isLoading={isLoading}
items={category.configEntries}
hasDocumentLevelSecurityEnabled={hasDocumentLevelSecurity}
setConfigEntry={(key, value) => {
const categories = configView.categories;
categories[index] = { ...categories[index], [key]: value };
setConfigView({
...configView,
categories,
});
}}
/>
</React.Fragment>
))}
{configView.advancedConfigurations.length > 0 && (
<React.Fragment>
<EuiSpacer />
<EuiTitle size="xs">
<h4>
{i18n.translate(
'searchConnectors.configurationConnector.config.advancedConfigurations.title',
{ defaultMessage: 'Advanced Configurations' }
)}
</h4>
</EuiTitle>
<EuiPanel color="subdued">
<ConnectorConfigurationFormItems
isLoading={isLoading}
items={configView.advancedConfigurations}
hasDocumentLevelSecurityEnabled={hasDocumentLevelSecurity}
setConfigEntry={(key, value) => {
setConfigView({
...configView,
advancedConfigurations: { ...configView.advancedConfigurations, [key]: value },
});
}}
/>
</EuiPanel>
</React.Fragment>
)}
<EuiSpacer />
<EuiFormRow>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="entSearchContent-connector-configuration-saveConfiguration"
data-telemetry-id="entSearchContent-connector-configuration-saveConfiguration"
type="submit"
isLoading={isLoading}
>
{i18n.translate('searchConnectors.configurationConnector.config.submitButton.title', {
defaultMessage: 'Save configuration',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-telemetry-id="entSearchContent-connector-configuration-cancelEdit"
isDisabled={isLoading}
onClick={() => {
cancelEditing();
}}
>
{i18n.translate(
'searchConnectors.configurationConnector.config.cancelEditingButton.title',
{
defaultMessage: 'Cancel',
}
)}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
</EuiForm>
);
};

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
@ -11,19 +12,23 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIcon, EuiPanel, EuiToolTip }
import { i18n } from '@kbn/i18n';
import { DisplayType } from '@kbn/search-connectors';
import { DisplayType } from '../..';
import { ConfigEntryView } from './connector_configuration';
import { ConnectorConfigurationField } from './connector_configuration_field';
import { ConfigEntryView } from './connector_configuration_logic';
interface ConnectorConfigurationFormItemsProps {
hasDocumentLevelSecurityEnabled: boolean;
isLoading: boolean;
items: ConfigEntryView[];
setConfigEntry: (key: string, value: string | number | boolean | null) => void;
}
export const ConnectorConfigurationFormItems: React.FC<ConnectorConfigurationFormItemsProps> = ({
isLoading,
items,
hasDocumentLevelSecurityEnabled,
setConfigEntry,
}) => {
return (
<EuiFlexGroup direction="column">
@ -33,11 +38,11 @@ export const ConnectorConfigurationFormItems: React.FC<ConnectorConfigurationFor
depends_on: dependencies,
key,
display,
is_valid: isValid,
isValid,
label,
sensitive,
tooltip,
validation_errors: validationErrors,
validationErrors,
} = configEntry;
if (key === 'use_document_level_security' && !hasDocumentLevelSecurityEnabled) {
@ -45,13 +50,10 @@ export const ConnectorConfigurationFormItems: React.FC<ConnectorConfigurationFor
}
const helpText = defaultValue
? i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.config.defaultValue',
{
defaultMessage: 'If left empty, the default value {defaultValue} will be used.',
values: { defaultValue },
}
)
? i18n.translate('searchConnectors.configurationConnector.config.defaultValue', {
defaultMessage: 'If left empty, the default value {defaultValue} will be used.',
values: { defaultValue },
})
: '';
// toggle and sensitive textarea labels go next to the element, not in the row
const rowLabel =
@ -82,7 +84,13 @@ export const ConnectorConfigurationFormItems: React.FC<ConnectorConfigurationFor
isInvalid={!isValid}
data-test-subj={`entSearchContent-connector-configuration-formrow-${key}`}
>
<ConnectorConfigurationField configEntry={configEntry} />
<ConnectorConfigurationField
configEntry={configEntry}
isLoading={isLoading}
setConfigValue={(value) => {
setConfigEntry(configEntry.key, value);
}}
/>
</EuiFormRow>
</EuiToolTip>
</EuiPanel>
@ -99,7 +107,13 @@ export const ConnectorConfigurationFormItems: React.FC<ConnectorConfigurationFor
isInvalid={!isValid}
data-test-subj={`entSearchContent-connector-configuration-formrow-${key}`}
>
<ConnectorConfigurationField configEntry={configEntry} />
<ConnectorConfigurationField
configEntry={configEntry}
isLoading={isLoading}
setConfigValue={(value) => {
setConfigEntry(configEntry.key, value);
}}
/>
</EuiFormRow>
</EuiToolTip>
</EuiFlexItem>

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
@ -24,7 +25,7 @@ export const DocumentLevelSecurityPanel: React.FC<DocumentLevelSecurityPanelProp
<EuiTitle>
<h4>
{i18n.translate(
'xpack.enterpriseSearch.connector.documentLevelSecurity.enablePanel.heading',
'searchConnectors.connector.documentLevelSecurity.enablePanel.heading',
{ defaultMessage: 'Document Level Security' }
)}
</h4>
@ -33,7 +34,7 @@ export const DocumentLevelSecurityPanel: React.FC<DocumentLevelSecurityPanelProp
<EuiText size="s">
<p>
{i18n.translate(
'xpack.enterpriseSearch.connector.documentLevelSecurity.enablePanel.description',
'searchConnectors.connector.documentLevelSecurity.enablePanel.description',
{
defaultMessage:
'Enables you to control which documents users can access, based on their permissions. This ensures search results only return relevant, authorized information for users, based on their roles.',

View file

@ -0,0 +1,9 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './connector_configuration';

View file

@ -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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { css } from '@emotion/react';
import {
EuiPopover,
EuiPopoverTitle,
EuiText,
EuiPopoverFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiPopoverProps,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
interface PlatinumLicensePopoverProps {
button: EuiPopoverProps['button'];
closePopover: () => void;
isPopoverOpen: boolean;
stackManagementHref?: string;
subscriptionLink?: string;
}
export const PlatinumLicensePopover: React.FC<PlatinumLicensePopoverProps> = ({
button,
isPopoverOpen,
closePopover,
stackManagementHref,
subscriptionLink,
}) => {
const { euiTheme } = useEuiTheme();
return (
<EuiPopover button={button} isOpen={isPopoverOpen} closePopover={closePopover}>
<EuiPopoverTitle>
{i18n.translate('searchConnectors.connectors.upgradeTitle', {
defaultMessage: 'Upgrade to Elastic Platinum',
})}
</EuiPopoverTitle>
<EuiText
grow={false}
size="s"
css={css`
max-width: calc(${euiTheme.size.xl} * 10);
`}
>
<p>
{i18n.translate('searchConnectors.connectors.upgradeDescription', {
defaultMessage:
'To use this connector, you must update your license to Platinum or start a 30-day free trial.',
})}
</p>
</EuiText>
<EuiPopoverFooter>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
{subscriptionLink && (
<EuiFlexItem grow={false}>
<EuiButton iconType="popout" target="_blank" href={subscriptionLink}>
{i18n.translate('searchConnectors.connectors.subscriptionLabel', {
defaultMessage: 'Subscription plans',
})}
</EuiButton>
</EuiFlexItem>
)}
{stackManagementHref && (
<EuiFlexItem grow={false}>
<EuiButton iconType="wrench" iconSide="right" href={stackManagementHref}>
{i18n.translate('searchConnectors.manageLicenseButtonLabel', {
defaultMessage: 'Manage license',
})}
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiPopoverFooter>
</EuiPopover>
);
};

View file

@ -0,0 +1,10 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './configuration';
export * from './sync_jobs';

View file

@ -10,7 +10,7 @@ exports[`SyncCalloutsPanel renders 1`] = `
>
<FormattedMessage
defaultMessage="Completed at {date}"
id="xpack.enterpriseSearch.content.syncJobs.flyout.completedDescription"
id="searchConnectors.syncJobs.flyout.completedDescription"
values={
Object {
"date": <FormattedDateTime
@ -29,7 +29,7 @@ exports[`SyncCalloutsPanel renders 1`] = `
>
<FormattedMessage
defaultMessage="Started at {date}"
id="xpack.enterpriseSearch.content.syncJobs.flyout.startedAtDescription"
id="searchConnectors.syncJobs.flyout.startedAtDescription"
values={
Object {
"date": <FormattedDateTime
@ -53,7 +53,7 @@ exports[`SyncCalloutsPanel renders canceled job 1`] = `
>
<FormattedMessage
defaultMessage="Completed at {date}"
id="xpack.enterpriseSearch.content.syncJobs.flyout.completedDescription"
id="searchConnectors.syncJobs.flyout.completedDescription"
values={
Object {
"date": <FormattedDateTime
@ -79,7 +79,7 @@ exports[`SyncCalloutsPanel renders canceled job 1`] = `
>
<FormattedMessage
defaultMessage="Started at {date}"
id="xpack.enterpriseSearch.content.syncJobs.flyout.startedAtDescription"
id="searchConnectors.syncJobs.flyout.startedAtDescription"
values={
Object {
"date": <FormattedDateTime
@ -103,7 +103,7 @@ exports[`SyncCalloutsPanel renders different trigger method 1`] = `
>
<FormattedMessage
defaultMessage="Completed at {date}"
id="xpack.enterpriseSearch.content.syncJobs.flyout.completedDescription"
id="searchConnectors.syncJobs.flyout.completedDescription"
values={
Object {
"date": <FormattedDateTime
@ -131,7 +131,7 @@ exports[`SyncCalloutsPanel renders different trigger method 1`] = `
>
<FormattedMessage
defaultMessage="Started at {date}"
id="xpack.enterpriseSearch.content.syncJobs.flyout.startedAtDescription"
id="searchConnectors.syncJobs.flyout.startedAtDescription"
values={
Object {
"date": <FormattedDateTime
@ -155,7 +155,7 @@ exports[`SyncCalloutsPanel renders error job 1`] = `
>
<FormattedMessage
defaultMessage="Completed at {date}"
id="xpack.enterpriseSearch.content.syncJobs.flyout.completedDescription"
id="searchConnectors.syncJobs.flyout.completedDescription"
values={
Object {
"date": <FormattedDateTime
@ -183,7 +183,7 @@ exports[`SyncCalloutsPanel renders error job 1`] = `
>
<FormattedMessage
defaultMessage="Started at {date}"
id="xpack.enterpriseSearch.content.syncJobs.flyout.startedAtDescription"
id="searchConnectors.syncJobs.flyout.startedAtDescription"
values={
Object {
"date": <FormattedDateTime
@ -207,7 +207,7 @@ exports[`SyncCalloutsPanel renders in progress job 1`] = `
>
<FormattedMessage
defaultMessage="Completed at {date}"
id="xpack.enterpriseSearch.content.syncJobs.flyout.completedDescription"
id="searchConnectors.syncJobs.flyout.completedDescription"
values={
Object {
"date": <FormattedDateTime
@ -235,7 +235,7 @@ exports[`SyncCalloutsPanel renders in progress job 1`] = `
>
<FormattedMessage
defaultMessage="Started at {date}"
id="xpack.enterpriseSearch.content.syncJobs.flyout.startedAtDescription"
id="searchConnectors.syncJobs.flyout.startedAtDescription"
values={
Object {
"date": <FormattedDateTime

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
@ -24,49 +25,43 @@ export const SyncJobDocumentsPanel: React.FC<SyncJobDocumentsPanelProps> = (sync
const columns: Array<EuiBasicTableColumn<SyncJobDocumentsPanelProps>> = [
{
field: 'added',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.documents.added', {
name: i18n.translate('searchConnectors.index.syncJobs.documents.added', {
defaultMessage: 'Added',
}),
},
{
field: 'removed',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.documents.removed', {
name: i18n.translate('searchConnectors.index.syncJobs.documents.removed', {
defaultMessage: 'Removed',
}),
},
{
field: 'total',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.documents.total', {
name: i18n.translate('searchConnectors.index.syncJobs.documents.total', {
defaultMessage: 'Total',
}),
},
{
field: 'volume',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.documents.volume', {
name: i18n.translate('searchConnectors.index.syncJobs.documents.volume', {
defaultMessage: 'Volume',
}),
render: (volume: number) =>
volume < 1
? i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.documents.volume.lessThanOneMBLabel',
{
defaultMessage: 'Less than 1mb',
}
)
: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.documents.volume.aboutLabel',
{
defaultMessage: 'About {volume}',
values: {
volume: new ByteSizeValue(volume * 1024 * 1024).toString(),
},
}
),
? i18n.translate('searchConnectors.index.syncJobs.documents.volume.lessThanOneMBLabel', {
defaultMessage: 'Less than 1mb',
})
: i18n.translate('searchConnectors.index.syncJobs.documents.volume.aboutLabel', {
defaultMessage: 'About {volume}',
values: {
volume: new ByteSizeValue(volume * 1024 * 1024).toString(),
},
}),
},
];
return (
<FlyoutPanel
title={i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.documents.title', {
title={i18n.translate('searchConnectors.index.syncJobs.documents.title', {
defaultMessage: 'Documents',
})}
>

View file

@ -1,15 +1,16 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { TriggerMethod } from '@kbn/search-connectors';
import { TriggerMethod } from '../..';
import { SyncJobEventsPanel } from './events_panel';

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
@ -13,9 +14,8 @@ import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { TriggerMethod } from '@kbn/search-connectors';
import { FormattedDateTime } from '../../../../shared/formatted_date_time';
import { FormattedDateTime } from '../../utils/formatted_date_time';
import { TriggerMethod } from '../..';
import { FlyoutPanel } from './flyout_panel';
@ -48,45 +48,40 @@ export const SyncJobEventsPanel: React.FC<SyncJobsEventPanelProps> = ({
date: syncRequestedAt,
title:
triggerMethod === TriggerMethod.ON_DEMAND
? i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.events.syncRequestedManually',
{ defaultMessage: 'Sync requested manually' }
)
: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.events.syncRequestedScheduled',
{ defaultMessage: 'Sync requested by schedule' }
),
? i18n.translate('searchConnectors.index.syncJobs.events.syncRequestedManually', {
defaultMessage: 'Sync requested manually',
})
: i18n.translate('searchConnectors.index.syncJobs.events.syncRequestedScheduled', {
defaultMessage: 'Sync requested by schedule',
}),
},
{
date: syncStarted,
title: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.events.syncStarted', {
title: i18n.translate('searchConnectors.index.syncJobs.events.syncStarted', {
defaultMessage: 'Sync started',
}),
},
{
date: lastUpdated,
title: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.events.lastUpdated', {
title: i18n.translate('searchConnectors.index.syncJobs.events.lastUpdated', {
defaultMessage: 'Last updated',
}),
},
{
date: completed,
title: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.events.completed', {
title: i18n.translate('searchConnectors.index.syncJobs.events.completed', {
defaultMessage: 'Completed',
}),
},
{
date: cancelationRequestedAt,
title: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.events.cancelationRequested',
{
defaultMessage: 'Cancelation requested',
}
),
title: i18n.translate('searchConnectors.index.syncJobs.events.cancelationRequested', {
defaultMessage: 'Cancelation requested',
}),
},
{
date: canceledAt,
title: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.events.canceled', {
title: i18n.translate('searchConnectors.index.syncJobs.events.canceled', {
defaultMessage: 'Canceled',
}),
},
@ -97,14 +92,14 @@ export const SyncJobEventsPanel: React.FC<SyncJobsEventPanelProps> = ({
const columns: Array<EuiBasicTableColumn<SyncJobEvent>> = [
{
field: 'title',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.events.state', {
name: i18n.translate('searchConnectors.index.syncJobs.events.state', {
defaultMessage: 'State',
}),
width: '50%',
},
{
field: 'date',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.events.time', {
name: i18n.translate('searchConnectors.index.syncJobs.events.time', {
defaultMessage: 'Time',
}),
render: (date: string) => <FormattedDateTime date={new Date(date)} />,
@ -113,7 +108,7 @@ export const SyncJobEventsPanel: React.FC<SyncJobsEventPanelProps> = ({
];
return (
<FlyoutPanel
title={i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.events.title', {
title={i18n.translate('searchConnectors.index.syncJobs.events.title', {
defaultMessage: 'Events',
})}
>

View file

@ -1,15 +1,16 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { FilteringPolicy, FilteringRule, FilteringRuleRule } from '@kbn/search-connectors';
import { FilteringRule } from '../..';
import { FilteringPanel } from './filtering_panel';
@ -17,32 +18,32 @@ describe('FilteringPanel', () => {
const filteringRules = [
{
order: 1,
policy: FilteringPolicy.EXCLUDE,
rule: FilteringRuleRule.CONTAINS,
policy: 'exclude',
rule: 'contains',
value: 'THIS VALUE',
},
{
order: 2,
policy: FilteringPolicy.EXCLUDE,
rule: FilteringRuleRule.ENDS_WITH,
policy: 'exclude',
rule: 'ends_with',
value: 'THIS VALUE',
},
{
order: 0,
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.EQUALS,
policy: 'include',
rule: 'equals',
value: 'THIS VALUE',
},
{
order: 5,
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.GT,
policy: 'include',
rule: '>',
value: 'THIS VALUE',
},
{
order: 4,
policy: FilteringPolicy.EXCLUDE,
rule: FilteringRuleRule.LT,
policy: 'exclude',
rule: '<',
value: 'THIS VALUE',
},
] as FilteringRule[];

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
@ -11,9 +12,9 @@ import { EuiCodeBlock, EuiPanel, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FilteringRule, FilteringRules } from '@kbn/search-connectors';
import { FilteringRule, FilteringRules } from '../..';
import { FilteringRulesTable } from '../../shared/filtering_rules_table/filtering_rules_table';
import { FilteringRulesTable } from './filtering_rules_table';
import { FlyoutPanel } from './flyout_panel';
@ -29,7 +30,7 @@ export const FilteringPanel: React.FC<FilteringPanelProps> = ({
return (
<>
<FlyoutPanel
title={i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.syncRulesTitle', {
title={i18n.translate('searchConnectors.index.syncJobs.syncRulesTitle', {
defaultMessage: 'Sync rules',
})}
>
@ -39,12 +40,9 @@ export const FilteringPanel: React.FC<FilteringPanelProps> = ({
<>
<EuiSpacer />
<FlyoutPanel
title={i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.syncRulesAdvancedTitle',
{
defaultMessage: 'Advanced sync rules',
}
)}
title={i18n.translate('searchConnectors.index.syncJobs.syncRulesAdvancedTitle', {
defaultMessage: 'Advanced sync rules',
})}
>
<EuiPanel hasShadow={false}>
<EuiCodeBlock transparentBackground language="json">

View file

@ -0,0 +1,73 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiBasicTable, EuiBasicTableColumn, EuiCode } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { filteringPolicyToText, filteringRuleToText } from '../../utils/filtering_rule_helpers';
import { FilteringRule, FilteringPolicy, FilteringRuleRule } from '../..';
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('searchConnectors.index.filtering.priority', {
defaultMessage: 'Rule priority',
}),
},
]
: []),
{
field: 'policy',
name: i18n.translate('searchConnectors.index.filtering.policy', {
defaultMessage: 'Policy',
}),
render: (policy: FilteringPolicy) => filteringPolicyToText(policy),
},
{
field: 'field',
name: i18n.translate('searchConnectors.index.filtering.field', {
defaultMessage: 'field',
}),
render: (value: string) => <EuiCode>{value}</EuiCode>,
},
{
field: 'rule',
name: i18n.translate('searchConnectors.index.filtering.rule', {
defaultMessage: 'Rule',
}),
render: (rule: FilteringRuleRule) => filteringRuleToText(rule),
},
{
field: 'value',
name: i18n.translate('searchConnectors.index.filtering.value', {
defaultMessage: 'Value',
}),
render: (value: string) => <EuiCode>{value}</EuiCode>,
},
];
return (
<EuiBasicTable
columns={columns}
items={filteringRules.sort(({ order }, { order: secondOrder }) => order - secondOrder)}
/>
);
};

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';

View file

@ -0,0 +1,9 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './sync_jobs_table';

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
@ -11,7 +12,7 @@ import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { IngestPipelineParams } from '@kbn/search-connectors';
import { IngestPipelineParams } from '../..';
import { FlyoutPanel } from './flyout_panel';
@ -22,56 +23,47 @@ interface PipelinePanelProps {
export const PipelinePanel: React.FC<PipelinePanelProps> = ({ pipeline }) => {
const items: Array<{ setting: string; value: string | boolean }> = [
{
setting: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.pipeline.name', {
setting: i18n.translate('searchConnectors.index.syncJobs.pipeline.name', {
defaultMessage: 'Pipeline name',
}),
value: pipeline.name,
},
{
setting: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.pipeline.extractBinaryContent',
{
defaultMessage: 'Extract binary content',
}
),
setting: i18n.translate('searchConnectors.index.syncJobs.pipeline.extractBinaryContent', {
defaultMessage: 'Extract binary content',
}),
value: pipeline.extract_binary_content,
},
{
setting: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.pipeline.reduceWhitespace',
{
defaultMessage: 'Reduce whitespace',
}
),
setting: i18n.translate('searchConnectors.index.syncJobs.pipeline.reduceWhitespace', {
defaultMessage: 'Reduce whitespace',
}),
value: pipeline.reduce_whitespace,
},
{
setting: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.pipeline.runMlInference',
{
defaultMessage: 'Machine learning inference',
}
),
setting: i18n.translate('searchConnectors.index.syncJobs.pipeline.runMlInference', {
defaultMessage: 'Machine learning inference',
}),
value: pipeline.run_ml_inference,
},
];
const columns: Array<EuiBasicTableColumn<{ setting: string; value: string | boolean }>> = [
{
field: 'setting',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.pipeline.setting', {
name: i18n.translate('searchConnectors.index.syncJobs.pipeline.setting', {
defaultMessage: 'Pipeline setting',
}),
},
{
field: 'value',
name: i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.documents.value', {
name: i18n.translate('searchConnectors.index.syncJobs.documents.value', {
defaultMessage: 'Value',
}),
},
];
return (
<FlyoutPanel
title={i18n.translate('xpack.enterpriseSearch.content.index.syncJobs.pipeline.title', {
title={i18n.translate('searchConnectors.index.syncJobs.pipeline.title', {
defaultMessage: 'Pipeline',
})}
>

View file

@ -0,0 +1,83 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { SyncJobType, SyncStatus, TriggerMethod } from '../..';
import { SyncJobCallouts } from './sync_callouts';
describe('SyncCalloutsPanel', () => {
const syncJob = {
cancelation_requested_at: null,
canceled_at: null,
completed_at: '2022-09-05T15:59:39.816+00:00',
connector: {
configuration: {},
filtering: null,
id: 'we2284IBjobuR2-lAuXh',
index_name: 'indexName',
language: '',
pipeline: null,
service_type: '',
},
created_at: '2022-09-05T14:59:39.816+00:00',
deleted_document_count: 20,
error: null,
id: 'id',
indexed_document_count: 50,
indexed_document_volume: 40,
job_type: SyncJobType.FULL,
last_seen: '2022-09-05T15:59:39.816+00:00',
metadata: {},
started_at: '2022-09-05T14:59:39.816+00:00',
status: SyncStatus.COMPLETED,
total_document_count: null,
trigger_method: TriggerMethod.ON_DEMAND,
worker_hostname: 'hostname_fake',
};
it('renders', () => {
const wrapper = shallow(<SyncJobCallouts syncJob={syncJob} />);
expect(wrapper).toMatchSnapshot();
});
it('renders error job', () => {
const wrapper = shallow(<SyncJobCallouts syncJob={{ ...syncJob, status: SyncStatus.ERROR }} />);
expect(wrapper).toMatchSnapshot();
});
it('renders canceled job', () => {
const wrapper = shallow(
<SyncJobCallouts syncJob={{ ...syncJob, status: SyncStatus.CANCELED }} />
);
expect(wrapper).toMatchSnapshot();
});
it('renders in progress job', () => {
const wrapper = shallow(
<SyncJobCallouts syncJob={{ ...syncJob, status: SyncStatus.IN_PROGRESS }} />
);
expect(wrapper).toMatchSnapshot();
});
it('renders different trigger method', () => {
const wrapper = shallow(
<SyncJobCallouts
syncJob={{
...syncJob,
status: SyncStatus.IN_PROGRESS,
trigger_method: TriggerMethod.SCHEDULED,
}}
/>
);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
@ -13,16 +14,12 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { SyncStatus, TriggerMethod } from '@kbn/search-connectors';
import { FormattedDateTime } from '../../../../shared/formatted_date_time';
import { durationToText } from '../../../utils/duration_to_text';
import { SyncJobView } from './sync_jobs_view_logic';
import { durationToText, getSyncJobDuration } from '../../utils/duration_to_text';
import { FormattedDateTime } from '../../utils/formatted_date_time';
import { ConnectorSyncJob, SyncStatus, TriggerMethod } from '../..';
interface SyncJobCalloutsProps {
syncJob: SyncJobView;
syncJob: ConnectorSyncJob;
}
export const SyncJobCallouts: React.FC<SyncJobCalloutsProps> = ({ syncJob }) => {
@ -33,12 +30,12 @@ export const SyncJobCallouts: React.FC<SyncJobCalloutsProps> = ({ syncJob }) =>
<EuiCallOut
color="success"
iconType="check"
title={i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.completedTitle', {
title={i18n.translate('searchConnectors.syncJobs.flyout.completedTitle', {
defaultMessage: 'Sync complete',
})}
>
<FormattedMessage
id="xpack.enterpriseSearch.content.syncJobs.flyout.completedDescription"
id="searchConnectors.syncJobs.flyout.completedDescription"
defaultMessage="Completed at {date}"
values={{
date: <FormattedDateTime date={new Date(syncJob.completed_at)} />,
@ -52,11 +49,11 @@ export const SyncJobCallouts: React.FC<SyncJobCalloutsProps> = ({ syncJob }) =>
<EuiCallOut
color="danger"
iconType="cross"
title={i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.failureTitle', {
title={i18n.translate('searchConnectors.syncJobs.flyout.failureTitle', {
defaultMessage: 'Sync failure',
})}
>
{i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.failureDescription', {
{i18n.translate('searchConnectors.syncJobs.flyout.failureDescription', {
defaultMessage: 'Sync failure: {error}.',
values: {
error: syncJob.error,
@ -70,13 +67,13 @@ export const SyncJobCallouts: React.FC<SyncJobCalloutsProps> = ({ syncJob }) =>
<EuiCallOut
color="danger"
iconType="cross"
title={i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.canceledTitle', {
title={i18n.translate('searchConnectors.syncJobs.flyout.canceledTitle', {
defaultMessage: 'Sync canceled',
})}
>
{!!syncJob.canceled_at && (
<FormattedMessage
id="xpack.enterpriseSearch.content.syncJobs.flyout.canceledDescription"
id="searchConnectors.syncJobs.flyout.canceledDescription"
defaultMessage="Sync canceled at {date}"
values={{
date: <FormattedDateTime date={new Date(syncJob.canceled_at)} />,
@ -91,22 +88,16 @@ export const SyncJobCallouts: React.FC<SyncJobCalloutsProps> = ({ syncJob }) =>
<EuiCallOut
color="warning"
iconType="clock"
title={i18n.translate(
'xpack.enterpriseSearch.content.syncJobs.flyout.inProgressTitle',
{
defaultMessage: 'In progress',
}
)}
title={i18n.translate('searchConnectors.syncJobs.flyout.inProgressTitle', {
defaultMessage: 'In progress',
})}
>
{i18n.translate(
'xpack.enterpriseSearch.content.syncJobs.flyout.inProgressDescription',
{
defaultMessage: 'Sync has been running for {duration}.',
values: {
duration: durationToText(syncJob.duration),
},
}
)}
{i18n.translate('searchConnectors.syncJobs.flyout.inProgressDescription', {
defaultMessage: 'Sync has been running for {duration}.',
values: {
duration: durationToText(getSyncJobDuration(syncJob)),
},
})}
</EuiCallOut>
</EuiFlexItem>
)}
@ -117,22 +108,16 @@ export const SyncJobCallouts: React.FC<SyncJobCalloutsProps> = ({ syncJob }) =>
iconType="iInCircle"
title={
syncJob.trigger_method === TriggerMethod.ON_DEMAND
? i18n.translate(
'xpack.enterpriseSearch.content.syncJobs.flyout.syncStartedManually',
{
defaultMessage: 'Sync started manually',
}
)
: i18n.translate(
'xpack.enterpriseSearch.content.syncJobs.flyout.syncStartedScheduled',
{
defaultMessage: 'Sync started by schedule',
}
)
? i18n.translate('searchConnectors.syncJobs.flyout.syncStartedManually', {
defaultMessage: 'Sync started manually',
})
: i18n.translate('searchConnectors.syncJobs.flyout.syncStartedScheduled', {
defaultMessage: 'Sync started by schedule',
})
}
>
<FormattedMessage
id="xpack.enterpriseSearch.content.syncJobs.flyout.startedAtDescription"
id="searchConnectors.syncJobs.flyout.startedAtDescription"
defaultMessage="Started at {date}"
values={{
date: <FormattedDateTime date={new Date(syncJob.started_at)} />,

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
@ -19,17 +20,17 @@ import {
import { i18n } from '@kbn/i18n';
import { ConnectorSyncJob } from '../../types';
import { SyncJobDocumentsPanel } from './documents_panel';
import { SyncJobEventsPanel } from './events_panel';
import { FilteringPanel } from './filtering_panel';
import { FlyoutPanel } from './flyout_panel';
import { PipelinePanel } from './pipeline_panel';
import { SyncJobCallouts } from './sync_callouts';
import { SyncJobView } from './sync_jobs_view_logic';
interface SyncJobFlyoutProps {
onClose: () => void;
syncJob?: SyncJobView;
syncJob?: ConnectorSyncJob;
}
export const SyncJobFlyout: React.FC<SyncJobFlyoutProps> = ({ onClose, syncJob }) => {
@ -44,7 +45,7 @@ export const SyncJobFlyout: React.FC<SyncJobFlyoutProps> = ({ onClose, syncJob }
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>
{i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.title', {
{i18n.translate('searchConnectors.syncJobs.flyout.title', {
defaultMessage: 'Event log',
})}
</h2>
@ -55,7 +56,7 @@ export const SyncJobFlyout: React.FC<SyncJobFlyoutProps> = ({ onClose, syncJob }
<SyncJobCallouts syncJob={syncJob} />
<EuiFlexItem>
<FlyoutPanel
title={i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.sync', {
title={i18n.translate('searchConnectors.syncJobs.flyout.sync', {
defaultMessage: 'Sync',
})}
>
@ -63,7 +64,7 @@ export const SyncJobFlyout: React.FC<SyncJobFlyoutProps> = ({ onClose, syncJob }
columns={[
{
field: 'id',
name: i18n.translate('xpack.enterpriseSearch.content.syncJobs.flyout.sync.id', {
name: i18n.translate('searchConnectors.syncJobs.flyout.sync.id', {
defaultMessage: 'ID',
}),
},

View file

@ -0,0 +1,151 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState } from 'react';
import {
CriteriaWithPagination,
EuiBadge,
EuiBasicTable,
EuiBasicTableColumn,
Pagination,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ConnectorSyncJob, SyncJobType, SyncStatus } from '../..';
import { syncJobTypeToText, syncStatusToColor, syncStatusToText } from '../..';
import { durationToText, getSyncJobDuration } from '../../utils/duration_to_text';
import { FormattedDateTime } from '../../utils/formatted_date_time';
import { SyncJobFlyout } from './sync_job_flyout';
interface SyncJobHistoryTableProps {
isLoading?: boolean;
onPaginate: (criteria: CriteriaWithPagination<ConnectorSyncJob>) => void;
pagination: Pagination;
syncJobs: ConnectorSyncJob[];
type: 'content' | 'access_control';
}
export const SyncJobsTable: React.FC<SyncJobHistoryTableProps> = ({
isLoading,
onPaginate,
pagination,
syncJobs,
type,
}) => {
const [selectedSyncJob, setSelectedSyncJob] = useState<ConnectorSyncJob | undefined>(undefined);
const columns: Array<EuiBasicTableColumn<ConnectorSyncJob>> = [
{
field: 'completed_at',
name: i18n.translate('searchConnectors.syncJobs.lastSync.columnTitle', {
defaultMessage: 'Last sync',
}),
render: (lastSync: string) =>
lastSync ? <FormattedDateTime date={new Date(lastSync)} /> : '--',
sortable: true,
truncateText: false,
},
{
name: i18n.translate('searchConnectors.syncJobs.syncDuration.columnTitle', {
defaultMessage: 'Sync duration',
}),
render: (syncJob: ConnectorSyncJob) => durationToText(getSyncJobDuration(syncJob)),
truncateText: false,
},
...(type === 'content'
? [
{
field: 'indexed_document_count',
name: i18n.translate('searchConnectors.searchIndices.addedDocs.columnTitle', {
defaultMessage: 'Docs added',
}),
sortable: true,
truncateText: true,
},
{
field: 'deleted_document_count',
name: i18n.translate('searchConnectors.searchIndices.deletedDocs.columnTitle', {
defaultMessage: 'Docs deleted',
}),
sortable: true,
truncateText: true,
},
{
field: 'job_type',
name: i18n.translate('searchConnectors.searchIndices.syncJobType.columnTitle', {
defaultMessage: 'Content sync type',
}),
render: (syncType: SyncJobType) => {
const syncJobTypeText = syncJobTypeToText(syncType);
if (syncJobTypeText.length === 0) return null;
return <EuiBadge color="hollow">{syncJobTypeText}</EuiBadge>;
},
sortable: true,
truncateText: true,
},
]
: []),
...(type === 'access_control'
? [
{
field: 'indexed_document_count',
name: i18n.translate('searchConnectors.searchIndices.identitySync.columnTitle', {
defaultMessage: 'Identities synced',
}),
sortable: true,
truncateText: true,
},
]
: []),
{
field: 'status',
name: i18n.translate('searchConnectors.searchIndices.syncStatus.columnTitle', {
defaultMessage: 'Status',
}),
render: (syncStatus: SyncStatus) => (
<EuiBadge color={syncStatusToColor(syncStatus)}>{syncStatusToText(syncStatus)}</EuiBadge>
),
truncateText: true,
},
{
actions: [
{
description: i18n.translate('searchConnectors.index.syncJobs.actions.viewJob.title', {
defaultMessage: 'View this sync job',
}),
icon: 'eye',
isPrimary: false,
name: i18n.translate('searchConnectors.index.syncJobs.actions.viewJob.caption', {
defaultMessage: 'View this sync job',
}),
onClick: (job) => setSelectedSyncJob(job),
type: 'icon',
},
],
},
];
return (
<>
{Boolean(selectedSyncJob) && (
<SyncJobFlyout onClose={() => setSelectedSyncJob(undefined)} syncJob={selectedSyncJob} />
)}
<EuiBasicTable
data-test-subj={`entSearchContent-index-${type}-syncJobs-table`}
items={syncJobs}
columns={columns}
hasActions
onChange={onPaginate}
pagination={pagination}
tableLayout="fixed"
loading={isLoading}
/>
</>
);
};

View file

@ -13,6 +13,7 @@ export const CURRENT_CONNECTORS_JOB_INDEX = '.elastic-connectors-sync-jobs-v1';
export const CONNECTORS_VERSION = 1;
export const CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX = '.search-acl-filter-';
export * from './components';
export * from './connectors';
export * from './lib';
export * from './types';

View file

@ -11,8 +11,6 @@ import {
ConnectorConfiguration,
ConnectorDocument,
ConnectorStatus,
FilteringPolicy,
FilteringRuleRule,
FilteringValidationState,
IngestPipelineParams,
} from '../types/connectors';
@ -59,8 +57,8 @@ export function createConnectorDocument({
field: '_',
id: 'DEFAULT',
order: 0,
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.REGEX,
policy: 'include',
rule: 'regex',
updated_at: currentTimestamp,
value: '.*',
},
@ -83,8 +81,8 @@ export function createConnectorDocument({
field: '_',
id: 'DEFAULT',
order: 0,
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.REGEX,
policy: 'include',
rule: 'regex',
updated_at: currentTimestamp,
value: '.*',
},

View file

@ -17,6 +17,7 @@ export * from './update_filtering_draft';
export * from './update_native';
export * from './start_sync';
export * from './update_connector_configuration';
export * from './update_connector_index_name';
export * from './update_connector_name_and_description';
export * from './update_connector_scheduling';
export * from './update_connector_service_type';

View file

@ -30,7 +30,7 @@ export const updateConnectorConfiguration = async (
connector.status === ConnectorStatus.CREATED
? ConnectorStatus.CONFIGURED
: connector.status;
const updatedConfig = Object.keys(connector.configuration)
const updatedConfig: ConnectorConfiguration = Object.keys(connector.configuration)
.map((key) => {
const configEntry = connector.configuration[key];
return isConfigEntry(configEntry)

View file

@ -0,0 +1,34 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { WriteResponseBase } from '@elastic/elasticsearch/lib/api/types';
import { ElasticsearchClient } from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
import { CONNECTORS_INDEX, fetchConnectorByIndexName } from '..';
export const updateConnectorIndexName = async (
client: ElasticsearchClient,
connectorId: string,
indexName: string
): Promise<WriteResponseBase> => {
const connectorResult = await fetchConnectorByIndexName(client, indexName);
if (connectorResult) {
throw new Error(
i18n.translate('searchConnectors.server.connectors.indexName.error', {
defaultMessage:
'This index has already been registered to connector {connectorId}. Please delete that connector or select a different index name.',
values: { connectorId },
})
);
}
return await client.update({
index: CONNECTORS_INDEX,
doc: { index_name: indexName },
id: connectorId,
});
};

View file

@ -19,5 +19,7 @@
"@kbn/i18n",
"@kbn/core",
"@kbn/core-elasticsearch-server",
"@kbn/config-schema",
"@kbn/i18n-react",
]
}

View file

@ -111,20 +111,26 @@ export interface IngestPipelineParams {
run_ml_inference: boolean;
}
export enum FilteringPolicy {
EXCLUDE = 'exclude',
INCLUDE = 'include',
}
export type FilteringPolicy = 'exclude' | 'include';
export enum FilteringRuleRule {
CONTAINS = 'contains',
ENDS_WITH = 'ends_with',
EQUALS = 'equals',
GT = '>',
LT = '<',
REGEX = 'regex',
STARTS_WITH = 'starts_with',
}
export type FilteringRuleRule =
| 'contains'
| 'ends_with'
| 'equals'
| '>'
| '<'
| 'regex'
| 'starts_with';
export const FilteringRuleRuleValues: FilteringRuleRule[] = [
'contains',
'ends_with',
'equals',
'>',
'<',
'regex',
'starts_with',
];
export interface FilteringRule {
created_at: string;

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
@ -12,13 +13,13 @@ import {
Dependency,
FieldType,
isConfigEntry,
} from '@kbn/search-connectors';
} from '..';
import { isCategoryEntry } from '../../../../../../../common/connectors/is_category_entry';
import { ConfigEntryView, ConfigView } from '../components/configuration/connector_configuration';
import { isNotNullish } from '../../../../../../../common/utils/is_not_nullish';
import { isCategoryEntry } from './is_category_entry';
import type { ConfigEntryView, ConfigView } from '../connector_configuration_logic';
import { isNotNullish } from './is_not_nullish';
export type ConnectorConfigEntry = ConnectorConfigProperties & { key: string };
@ -120,20 +121,17 @@ export const filterSortValidateEntries = (
if (configEntry.type === FieldType.INTEGER && !validIntInput(configEntry.value)) {
validationErrors.push(
i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.config.invalidInteger',
{
defaultMessage: '{label} must be an integer.',
values: { label },
}
)
i18n.translate('searchConnectors.config.invalidInteger', {
defaultMessage: '{label} must be an integer.',
values: { label },
})
);
}
return {
...configEntry,
is_valid: validationErrors.length <= 0,
validation_errors: validationErrors,
isValid: validationErrors.length <= 0,
validationErrors,
};
});
};
@ -153,6 +151,13 @@ export const sortAndFilterConnectorConfiguration = (
config: ConnectorConfiguration,
isNative: boolean
): ConfigView => {
if (!config) {
return {
advancedConfigurations: [],
categories: [],
unCategorizedItems: [],
};
}
// This casting is ugly but makes all of the iteration below work for TypeScript
// extract_full_html is only defined for crawlers, who don't use this config screen
// we explicitly filter it out as well

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import moment from 'moment';

View file

@ -0,0 +1,27 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import moment from 'moment';
import { ConnectorSyncJob } from '../types';
export function getSyncJobDuration(syncJob: ConnectorSyncJob): moment.Duration | undefined {
return syncJob.started_at
? moment.duration(moment(syncJob.completed_at || new Date()).diff(moment(syncJob.started_at)))
: undefined;
}
export function durationToText(input?: moment.Duration): string {
if (input) {
const hours = input.hours();
const minutes = input.minutes();
const seconds = input.seconds();
return `${hours}h ${minutes}m ${seconds}s`;
} else {
return '--';
}
}

View file

@ -0,0 +1,52 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { FilteringPolicy, FilteringRuleRule } from '..';
const filteringRuleStringMap: Record<FilteringRuleRule, string> = {
contains: i18n.translate('searchConnectors.content.filteringRules.rules.contains', {
defaultMessage: 'Contains',
}),
ends_with: i18n.translate('searchConnectors.content.filteringRules.rules.endsWith', {
defaultMessage: 'Ends with',
}),
equals: i18n.translate('searchConnectors.content.filteringRules.rules.equals', {
defaultMessage: 'Equals',
}),
['>']: i18n.translate('searchConnectors.content.filteringRules.rules.greaterThan', {
defaultMessage: 'Greater than',
}),
['<']: i18n.translate('searchConnectors.content.filteringRules.rules.lessThan', {
defaultMessage: 'Less than',
}),
regex: i18n.translate('searchConnectors.content.filteringRules.rules.regEx', {
defaultMessage: 'Regular expression',
}),
starts_with: i18n.translate('searchConnectors.content.filteringRules.rules.startsWith', {
defaultMessage: 'Starts with',
}),
};
export function filteringRuleToText(filteringRule: FilteringRuleRule): string {
return filteringRuleStringMap[filteringRule];
}
const filteringPolicyStringMap: Record<FilteringPolicy, string> = {
exclude: i18n.translate('searchConnectors.content.filteringRules.policy.exclude', {
defaultMessage: 'Exclude',
}),
include: i18n.translate('searchConnectors.content.filteringRules.policy.include', {
defaultMessage: 'Include',
}),
};
export function filteringPolicyToText(filteringPolicy: FilteringPolicy): string {
return filteringPolicyStringMap[filteringPolicy];
}

View file

@ -0,0 +1,28 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { FormattedDate, FormattedTime } from '@kbn/i18n-react';
interface Props {
date: Date;
hideTime?: boolean;
}
export const FormattedDateTime: React.FC<Props> = ({ date, hideTime = false }) => (
<>
<FormattedDate value={date} year="numeric" month="short" day="numeric" />
{!hideTime && (
<>
{' '}
<FormattedTime value={date} />
</>
)}
</>
);

View file

@ -6,5 +6,7 @@
* Side Public License, v 1.
*/
export * from './filtering_rule_helpers';
export * from './is_category_entry';
export * from './page_to_pagination';
export * from './sync_status_to_text';

View file

@ -0,0 +1,17 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export function pageToPagination(page: { from: number; size: number; total: number }) {
// Prevent divide-by-zero-error
const pageIndex = page.size ? Math.trunc(page.from / page.size) : 0;
return {
pageIndex,
pageSize: page.size,
totalItemCount: page.total,
};
}

View file

@ -9,8 +9,6 @@ import {
ConnectorStatus,
DisplayType,
FieldType,
FilteringPolicy,
FilteringRuleRule,
FilteringValidationState,
SyncStatus,
} from '@kbn/search-connectors';
@ -78,8 +76,8 @@ export const indices: ElasticsearchIndexWithIngestion[] = [
field: '_',
id: 'DEFAULT',
order: 0,
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.REGEX,
policy: 'include',
rule: 'regex',
updated_at: expect.any(String),
value: '.*',
},
@ -102,8 +100,8 @@ export const indices: ElasticsearchIndexWithIngestion[] = [
field: '_',
id: 'DEFAULT',
order: 0,
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.REGEX,
policy: 'include',
rule: 'regex',
updated_at: expect.any(String),
value: '.*',
},
@ -204,8 +202,8 @@ export const indices: ElasticsearchIndexWithIngestion[] = [
field: '_',
id: 'DEFAULT',
order: 0,
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.REGEX,
policy: 'include',
rule: 'regex',
updated_at: expect.any(String),
value: '.*',
},
@ -228,8 +226,8 @@ export const indices: ElasticsearchIndexWithIngestion[] = [
field: '_',
id: 'DEFAULT',
order: 0,
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.REGEX,
policy: 'include',
rule: 'regex',
updated_at: expect.any(String),
value: '.*',
},

View file

@ -9,8 +9,6 @@ import {
ConnectorStatus,
DisplayType,
FieldType,
FilteringPolicy,
FilteringRuleRule,
FilteringValidationState,
SyncStatus,
} from '@kbn/search-connectors';
@ -87,8 +85,8 @@ export const connectorIndex: ConnectorViewIndex = {
field: '_',
id: 'DEFAULT',
order: 0,
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.REGEX,
policy: 'include',
rule: 'regex',
updated_at: expect.any(String),
value: '.*',
},
@ -111,8 +109,8 @@ export const connectorIndex: ConnectorViewIndex = {
field: '_',
id: 'DEFAULT',
order: 0,
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.REGEX,
policy: 'include',
rule: 'regex',
updated_at: expect.any(String),
value: '.*',
},
@ -217,8 +215,8 @@ export const crawlerIndex: CrawlerViewIndex = {
field: '_',
id: 'DEFAULT',
order: 0,
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.REGEX,
policy: 'include',
rule: 'regex',
updated_at: expect.any(String),
value: '.*',
},
@ -241,8 +239,8 @@ export const crawlerIndex: CrawlerViewIndex = {
field: '_',
id: 'DEFAULT',
order: 0,
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.REGEX,
policy: 'include',
rule: 'regex',
updated_at: expect.any(String),
value: '.*',
},

View file

@ -27,7 +27,6 @@ import { KibanaLogic } from '../../../../../shared/kibana';
import { CancelSyncsApiLogic } from '../../../../api/connector/cancel_syncs_api_logic';
import { IngestionStatus } from '../../../../types';
import { CancelSyncsLogic } from '../../connector/cancel_syncs_logic';
import { ConnectorConfigurationLogic } from '../../connector/connector_configuration_logic';
import { IndexViewLogic } from '../../index_view_logic';
export const SyncsContextMenu: React.FC = () => {
@ -44,7 +43,7 @@ export const SyncsContextMenu: React.FC = () => {
const { cancelSyncs } = useActions(CancelSyncsLogic);
const { status } = useValues(CancelSyncsApiLogic);
const { startSync, startIncrementalSync, startAccessControlSync } = useActions(IndexViewLogic);
const { configState } = useValues(ConnectorConfigurationLogic);
const { connector } = useValues(IndexViewLogic);
const [isPopoverOpen, setPopover] = useState(false);
const togglePopover = () => setPopover(!isPopoverOpen);
@ -126,9 +125,10 @@ export const SyncsContextMenu: React.FC = () => {
'entSearchContent-${ingestionMethod}-header-sync-more-accessControlSync',
'data-test-subj':
'entSearchContent-${ingestionMethod}-header-sync-more-accessControlSync',
disabled:
disabled: Boolean(
ingestionStatus === IngestionStatus.INCOMPLETE ||
!configState.use_document_level_security?.value,
connector?.configuration.use_document_level_security?.value
),
icon: 'play',
name: i18n.translate('xpack.enterpriseSearch.index.header.more.accessControlSync', {
defaultMessage: 'Access Control',

View file

@ -26,15 +26,20 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { ConnectorStatus } from '@kbn/search-connectors';
import { ConnectorConfigurationComponent, ConnectorStatus } from '@kbn/search-connectors';
import { Status } from '../../../../../../common/types/api';
import { BetaConnectorCallout } from '../../../../shared/beta/beta_connector_callout';
import { useCloudDetails } from '../../../../shared/cloud_details/cloud_details';
import { docLinks } from '../../../../shared/doc_links';
import { generateEncodedPath } from '../../../../shared/encode_path_params';
import { HttpLogic } from '../../../../shared/http/http_logic';
import { LicensingLogic } from '../../../../shared/licensing';
import { EuiButtonTo, EuiLinkTo } from '../../../../shared/react_router_helpers';
import { GenerateConnectorApiKeyApiLogic } from '../../../api/connector/generate_connector_api_key_api_logic';
import { ConnectorConfigurationApiLogic } from '../../../api/connector/update_connector_configuration_api_logic';
import { SEARCH_INDEX_TAB_PATH } from '../../../routes';
import { isConnectorIndex } from '../../../utils/indices';
@ -45,7 +50,6 @@ import { IndexViewLogic } from '../index_view_logic';
import { SearchIndexTabId } from '../search_index';
import { ApiKeyConfig } from './api_key_configuration';
import { ConnectorConfigurationConfig } from './connector_configuration_config';
import { ConnectorNameAndDescription } from './connector_name_and_description/connector_name_and_description';
import { BETA_CONNECTORS, CONNECTORS, getConnectorTemplate } from './constants';
import { NativeConnectorConfiguration } from './native_connector_configuration/native_connector_configuration';
@ -56,6 +60,10 @@ export const ConnectorConfiguration: React.FC = () => {
const { indexName } = useValues(IndexNameLogic);
const { recheckIndex } = useActions(IndexViewLogic);
const cloudContext = useCloudDetails();
const { hasPlatinumLicense } = useValues(LicensingLogic);
const { status } = useValues(ConnectorConfigurationApiLogic);
const { makeRequest } = useActions(ConnectorConfigurationApiLogic);
const { http } = useValues(HttpLogic);
if (!isConnectorIndex(index)) {
return <></>;
@ -190,7 +198,22 @@ export const ConnectorConfiguration: React.FC = () => {
},
{
children: (
<ConnectorConfigurationConfig>
<ConnectorConfigurationComponent
connector={index.connector}
hasPlatinumLicense={hasPlatinumLicense}
isLoading={status === Status.LOADING}
saveConfig={(configuration) =>
makeRequest({
configuration,
connectorId: index.connector.id,
indexName: index.name,
})
}
subscriptionLink={docLinks.licenseManagement}
stackManagementLink={http.basePath.prepend(
'/app/management/stack/license_management'
)}
>
{!index.connector.status ||
index.connector.status === ConnectorStatus.CREATED ? (
<EuiCallOut
@ -238,7 +261,7 @@ export const ConnectorConfiguration: React.FC = () => {
)}
/>
)}
</ConnectorConfigurationConfig>
</ConnectorConfigurationComponent>
),
status:
index.connector.status === ConnectorStatus.CONNECTED

View file

@ -1,113 +0,0 @@
/*
* 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,
EuiCallOut,
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { IndexViewLogic } from '../index_view_logic';
import { ConnectorConfigurationForm } from './connector_configuration_form';
import { ConfigEntryView, ConnectorConfigurationLogic } from './connector_configuration_logic';
function entryToDisplaylistItem(entry: ConfigEntryView): { description: string; title: string } {
return {
description: entry.sensitive && !!entry.value ? '********' : String(entry.value) || '--',
title: entry.label,
};
}
export const ConnectorConfigurationConfig: React.FC = ({ children }) => {
const { connectorError } = useValues(IndexViewLogic);
const { configView, isEditing } = useValues(ConnectorConfigurationLogic);
const { setIsEditing } = useActions(ConnectorConfigurationLogic);
const uncategorizedDisplayList = configView.unCategorizedItems.map(entryToDisplaylistItem);
return (
<EuiFlexGroup direction="column">
{children && <EuiFlexItem>{children}</EuiFlexItem>}
<EuiFlexItem>
{isEditing ? (
<ConnectorConfigurationForm />
) : (
uncategorizedDisplayList.length > 0 && (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiDescriptionList
listItems={uncategorizedDisplayList}
className="eui-textBreakWord"
/>
</EuiFlexItem>
{configView.categories.length > 0 &&
configView.categories.map((category) => (
<EuiFlexGroup direction="column" key={category.key}>
<EuiFlexItem>
<EuiTitle size="s">
<h3>{category.label}</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiDescriptionList
listItems={category.configEntries.map(entryToDisplaylistItem)}
className="eui-textBreakWord"
/>
</EuiFlexItem>
</EuiFlexGroup>
))}
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="entSearchContent-connector-configuration-editConfiguration"
data-telemetry-id="entSearchContent-connector-overview-configuration-editConfiguration"
onClick={() => setIsEditing(!isEditing)}
>
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.config.editButton.title',
{
defaultMessage: 'Edit configuration',
}
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
)
)}
</EuiFlexItem>
{!!connectorError && (
<EuiFlexItem>
<EuiCallOut
color="danger"
title={i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.config.error.title',
{
defaultMessage: 'Connector error',
}
)}
>
<EuiText size="s">{connectorError}</EuiText>
</EuiCallOut>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};

View file

@ -1,124 +0,0 @@
/*
* 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,
EuiFormRow,
EuiSpacer,
EuiPanel,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Status } from '../../../../../../common/types/api';
import { KibanaLogic } from '../../../../shared/kibana';
import { ConnectorConfigurationApiLogic } from '../../../api/connector/update_connector_configuration_api_logic';
import { ConnectorConfigurationFormItems } from './connector_configuration_form_items';
import { ConnectorConfigurationLogic } from './connector_configuration_logic';
export const ConnectorConfigurationForm = () => {
const { productFeatures } = useValues(KibanaLogic);
const { status } = useValues(ConnectorConfigurationApiLogic);
const { localConfigView } = useValues(ConnectorConfigurationLogic);
const { saveConfig, setIsEditing } = useActions(ConnectorConfigurationLogic);
return (
<EuiForm
onSubmit={(event) => {
event.preventDefault();
saveConfig();
}}
component="form"
>
<ConnectorConfigurationFormItems
items={localConfigView.unCategorizedItems}
hasDocumentLevelSecurityEnabled={productFeatures.hasDocumentLevelSecurityEnabled}
/>
{localConfigView.categories.map((category, index) => (
<React.Fragment key={index}>
<EuiSpacer />
<EuiTitle size="s">
<h3>{category.label}</h3>
</EuiTitle>
<EuiSpacer />
<ConnectorConfigurationFormItems
items={category.configEntries}
hasDocumentLevelSecurityEnabled={productFeatures.hasDocumentLevelSecurityEnabled}
/>
</React.Fragment>
))}
{localConfigView.advancedConfigurations.length > 0 && (
<React.Fragment>
<EuiSpacer />
<EuiTitle size="xs">
<h4>
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.config.advancedConfigurations.title',
{ defaultMessage: 'Advanced Configurations' }
)}
</h4>
</EuiTitle>
<EuiPanel color="subdued">
<ConnectorConfigurationFormItems
items={localConfigView.advancedConfigurations}
hasDocumentLevelSecurityEnabled={productFeatures.hasDocumentLevelSecurityEnabled}
/>
</EuiPanel>
</React.Fragment>
)}
<EuiSpacer />
<EuiFormRow>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="entSearchContent-connector-configuration-saveConfiguration"
data-telemetry-id="entSearchContent-connector-configuration-saveConfiguration"
type="submit"
isLoading={status === Status.LOADING}
>
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.config.submitButton.title',
{
defaultMessage: 'Save configuration',
}
)}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-telemetry-id="entSearchContent-connector-configuration-cancelEdit"
isDisabled={status === Status.LOADING}
onClick={() => {
setIsEditing(false);
}}
>
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.config.cancelEditingButton.title',
{
defaultMessage: 'Cancel',
}
)}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
</EuiForm>
);
};

View file

@ -1,231 +0,0 @@
/*
* 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 {
ConnectorConfigProperties,
ConnectorConfiguration,
ConnectorStatus,
} from '@kbn/search-connectors';
import { isCategoryEntry } from '../../../../../../common/connectors/is_category_entry';
import { isNotNullish } from '../../../../../../common/utils/is_not_nullish';
import {
ConnectorConfigurationApiLogic,
PostConnectorConfigurationActions,
} from '../../../api/connector/update_connector_configuration_api_logic';
import {
CachedFetchIndexApiLogic,
CachedFetchIndexApiLogicActions,
} from '../../../api/index/cached_fetch_index_api_logic';
import { FetchIndexApiResponse } from '../../../api/index/fetch_index_api_logic';
import { isConnectorIndex } from '../../../utils/indices';
import {
ensureCorrectTyping,
sortAndFilterConnectorConfiguration,
} from './utils/connector_configuration_utils';
type ConnectorConfigurationActions = Pick<
PostConnectorConfigurationActions,
'apiSuccess' | 'makeRequest'
> & {
fetchIndexApiSuccess: CachedFetchIndexApiLogicActions['apiSuccess'];
saveConfig: () => void;
setConfigState(configState: ConnectorConfiguration): {
configState: ConnectorConfiguration;
};
setIsEditing(isEditing: boolean): { isEditing: boolean };
setLocalConfigEntry(configEntry: ConfigEntry): ConfigEntry;
setLocalConfigState(configState: ConnectorConfiguration): {
configState: ConnectorConfiguration;
};
setShouldStartInEditMode(shouldStartInEditMode: boolean): { shouldStartInEditMode: boolean };
};
interface ConnectorConfigurationValues {
configState: ConnectorConfiguration;
configView: ConfigView;
index: FetchIndexApiResponse;
isEditing: boolean;
localConfigState: ConnectorConfiguration;
localConfigView: ConfigView;
shouldStartInEditMode: boolean;
}
interface ConfigEntry extends ConnectorConfigProperties {
key: string;
}
export interface ConfigEntryView extends ConfigEntry {
is_valid: boolean;
validation_errors: string[];
}
export interface CategoryEntry {
configEntries: ConfigEntryView[];
key: string;
label: string;
order: number;
}
export interface ConfigView {
advancedConfigurations: ConfigEntryView[];
categories: CategoryEntry[];
unCategorizedItems: ConfigEntryView[];
}
export const ConnectorConfigurationLogic = kea<
MakeLogicType<ConnectorConfigurationValues, ConnectorConfigurationActions>
>({
actions: {
saveConfig: true,
setConfigState: (configState: ConnectorConfiguration) => ({ configState }),
setIsEditing: (isEditing: boolean) => ({
isEditing,
}),
setLocalConfigEntry: (configEntry: ConfigEntry) => ({ ...configEntry }),
setLocalConfigState: (configState: ConnectorConfiguration) => ({ configState }),
setShouldStartInEditMode: (shouldStartInEditMode: boolean) => ({ shouldStartInEditMode }),
},
connect: {
actions: [
ConnectorConfigurationApiLogic,
['apiSuccess', 'makeRequest'],
CachedFetchIndexApiLogic,
['apiSuccess as fetchIndexApiSuccess'],
],
values: [CachedFetchIndexApiLogic, ['indexData as index']],
},
events: ({ actions, values }) => ({
afterMount: () => {
actions.setConfigState(
isConnectorIndex(values.index) ? values.index.connector.configuration : {}
);
if (
isConnectorIndex(values.index) &&
(values.index.connector.status === ConnectorStatus.CREATED ||
values.index.connector.status === ConnectorStatus.NEEDS_CONFIGURATION)
) {
// Only start in edit mode if we haven't configured yet
// Necessary to prevent a race condition between saving config and getting updated connector
actions.setShouldStartInEditMode(true);
}
},
}),
listeners: ({ actions, values }) => ({
apiSuccess: ({ indexName }) => {
CachedFetchIndexApiLogic.actions.makeRequest({ indexName });
},
fetchIndexApiSuccess: (index) => {
if (!values.isEditing && isConnectorIndex(index)) {
actions.setConfigState(index.connector.configuration);
}
if (
!values.isEditing &&
values.shouldStartInEditMode &&
isConnectorIndex(index) &&
index.connector.status === ConnectorStatus.NEEDS_CONFIGURATION &&
index.connector.configuration &&
Object.entries(index.connector.configuration).length > 0
) {
actions.setIsEditing(true);
}
},
saveConfig: () => {
if (isConnectorIndex(values.index)) {
actions.makeRequest({
configuration: Object.keys(values.localConfigState)
.map((key) => {
const entry = values.localConfigState[key];
if (isCategoryEntry(entry) || !entry) {
return null;
}
return { key, value: entry.value ?? '' };
})
.filter(isNotNullish)
.reduce((prev: Record<string, string | number | boolean | null>, { key, value }) => {
prev[key] = value;
return prev;
}, {}),
connectorId: values.index.connector.id,
indexName: values.index.connector.index_name ?? '',
});
}
},
setIsEditing: (isEditing) => {
if (isEditing) {
actions.setLocalConfigState(values.configState);
}
},
}),
path: ['enterprise_search', 'content', 'connector_configuration'],
reducers: () => ({
configState: [
{},
{
apiSuccess: (_, { configuration }) => configuration,
setConfigState: (_, { configState }) => configState,
},
],
isEditing: [
false,
{
apiSuccess: () => false,
setIsEditing: (_, { isEditing }) => isEditing,
},
],
localConfigState: [
{},
{
setLocalConfigEntry: (
configState,
{ key, display, type, validations, value, ...configEntry }
) => ({
...configState,
[key]: {
...configEntry,
display,
type,
validations: validations ?? [],
value: display ? ensureCorrectTyping(type, value) : value, // only check type if field had a specified eui element
},
}),
setLocalConfigState: (_, { configState }) => configState,
},
],
shouldStartInEditMode: [
false,
{
apiSuccess: () => false,
setShouldStartInEditMode: (_, { shouldStartInEditMode }) => shouldStartInEditMode,
},
],
}),
selectors: ({ selectors }) => ({
configView: [
() => [selectors.configState, selectors.index],
(configState: ConnectorConfiguration, index: FetchIndexApiResponse) =>
sortAndFilterConnectorConfiguration(
configState,
isConnectorIndex(index) ? index.connector.is_native : false
),
],
localConfigView: [
() => [selectors.localConfigState, selectors.index],
(configState: ConnectorConfiguration, index: FetchIndexApiResponse) =>
sortAndFilterConnectorConfiguration(
configState,
isConnectorIndex(index) ? index.connector.is_native : false
),
],
}),
});

View file

@ -1,88 +0,0 @@
/*
* 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 { EuiCallOut, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
export const ConnectorConfigurationConfig: React.FC = () => {
return (
<ConnectorConfigurationConfig>
<EuiText size="s">
<FormattedMessage
id="xpack.enterpriseSearch.content.indices.configurationConnector.config.description.firstParagraph"
defaultMessage="Now that your connector is deployed, enhance the connector client for your custom data source. There are several {link} you can customize with your own additional implementation logic."
values={{
link: (
<EuiLink
href="https://github.com/elastic/connectors-python/tree/main/connectors/sources"
target="_blank"
>
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.config.connectorClientLink',
{ defaultMessage: 'connectors' }
)}
</EuiLink>
),
}}
/>
<EuiSpacer />
<p>
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.config.description.secondParagraph',
{
defaultMessage:
'While the connector clients in the repository are built in Ruby, theres no technical limitation to only use Ruby. Build a connector client with the technology that works best for your skillset.',
}
)}
</p>
<FormattedMessage
id="xpack.enterpriseSearch.content.indices.configurationConnector.config.description.thirdParagraph"
defaultMessage="If you need help, you can always open an {issuesLink} in the repository or ask a question in our {discussLink} forum."
values={{
discussLink: (
<EuiLink href="https://discuss.elastic.co/c/enterprise-search/84" target="_blank">
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.config.discussLink',
{ defaultMessage: 'Discuss' }
)}
</EuiLink>
),
issuesLink: (
<EuiLink href="https://github.com/elastic/connectors-python/issues" target="_blank">
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.config.issuesLink',
{ defaultMessage: 'issue' }
)}
</EuiLink>
),
}}
/>
<EuiSpacer />
<EuiCallOut
iconType="warning"
color="warning"
title={i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.config.warning.title',
{ defaultMessage: 'This connector is tied to your Elastic index' }
)}
>
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.warning.description',
{
defaultMessage:
'If you sync at least one document before youve finalized your connector client, you will have to recreate your search index.',
}
)}
</EuiCallOut>
</EuiText>
</ConnectorConfigurationConfig>
);
};

View file

@ -122,6 +122,7 @@ export const NativeConnectorConfiguration: React.FC = () => {
{
children: (
<NativeConnectorConfigurationConfig
connector={index.connector}
nativeConnector={nativeConnector}
status={index.connector.status}
/>

View file

@ -7,27 +7,55 @@
import React from 'react';
import { useActions, useValues } from 'kea';
import { EuiSpacer, EuiLink, EuiText, EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ConnectorStatus } from '@kbn/search-connectors';
import { Connector, ConnectorStatus } from '@kbn/search-connectors';
import { ConnectorConfigurationComponent } from '@kbn/search-connectors/components/configuration/connector_configuration';
import { Status } from '../../../../../../../common/types/api';
import { docLinks } from '../../../../../shared/doc_links';
import { HttpLogic } from '../../../../../shared/http';
import { LicensingLogic } from '../../../../../shared/licensing';
import { ConnectorConfigurationConfig } from '../connector_configuration_config';
import { ConnectorConfigurationApiLogic } from '../../../../api/connector/update_connector_configuration_api_logic';
import { IndexNameLogic } from '../../index_name_logic';
import { ConnectorDefinition } from '../types';
interface NativeConnectorConfigurationConfigProps {
connector: Connector;
nativeConnector: ConnectorDefinition;
status: ConnectorStatus;
}
export const NativeConnectorConfigurationConfig: React.FC<
NativeConnectorConfigurationConfigProps
> = ({ nativeConnector, status }) => {
> = ({ connector, nativeConnector, status }) => {
const { hasPlatinumLicense } = useValues(LicensingLogic);
const { indexName } = useValues(IndexNameLogic);
const { status: updateStatus } = useValues(ConnectorConfigurationApiLogic);
const { makeRequest } = useActions(ConnectorConfigurationApiLogic);
const { http } = useValues(HttpLogic);
return (
<ConnectorConfigurationConfig>
<ConnectorConfigurationComponent
connector={connector}
hasPlatinumLicense={hasPlatinumLicense}
isLoading={updateStatus === Status.LOADING}
saveConfig={(configuration) =>
makeRequest({
configuration,
connectorId: connector.id,
indexName,
})
}
subscriptionLink={docLinks.licenseManagement}
stackManagementLink={http.basePath.prepend('/app/management/stack/license_management')}
>
<EuiText size="s">
{i18n.translate(
'xpack.enterpriseSearch.content.indices.configurationConnector.nativeConnector.config.encryptionWarningMessage',
@ -82,6 +110,6 @@ export const NativeConnectorConfigurationConfig: React.FC<
/>
</>
)}
</ConnectorConfigurationConfig>
</ConnectorConfigurationComponent>
);
};

View file

@ -11,9 +11,7 @@ import { isEqual } from 'lodash';
import {
FilteringConfig,
FilteringPolicy,
FilteringRule,
FilteringRuleRule,
FilteringValidation,
FilteringValidationState,
} from '@kbn/search-connectors';
@ -90,15 +88,15 @@ interface ConnectorFilteringValues {
status: Status;
}
function createDefaultRule(order: number) {
function createDefaultRule(order: number): FilteringRule {
const now = new Date().toISOString();
return {
created_at: now,
field: '_',
id: 'DEFAULT',
order,
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.REGEX,
policy: 'include',
rule: 'regex',
updated_at: now,
value: '.*',
};
@ -237,7 +235,7 @@ export const ConnectorFilteringLogic = kea<
[],
{
addFilteringRule: (filteringRules, filteringRule) => {
const newFilteringRules = filteringRules.length
const newFilteringRules: FilteringRule[] = filteringRules.length
? [
...filteringRules.slice(0, filteringRules.length - 1),
filteringRule,

View file

@ -23,7 +23,12 @@ import {
import { i18n } from '@kbn/i18n';
import { FilteringPolicy, FilteringRule, FilteringRuleRule } from '@kbn/search-connectors';
import {
filteringPolicyToText,
filteringRuleToText,
FilteringRule,
FilteringRuleRuleValues,
} from '@kbn/search-connectors';
import { docLinks } from '../../../../../shared/doc_links';
@ -38,11 +43,6 @@ import {
} 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';
@ -50,7 +50,7 @@ import { ConnectorFilteringLogic } from './connector_filtering_logic';
const instanceId = 'FilteringRulesTable';
function validateItem(filteringRule: FilteringRule): FormErrors {
if (filteringRule.rule === FilteringRuleRule.REGEX) {
if (filteringRule.rule === 'regex') {
try {
new RegExp(filteringRule.value);
return {};
@ -97,12 +97,12 @@ export const SyncRulesTable: React.FC = () => {
onChange={(e) => onChange(e.target.value)}
options={[
{
text: filteringPolicyToText(FilteringPolicy.INCLUDE),
value: FilteringPolicy.INCLUDE,
text: filteringPolicyToText('include'),
value: 'include',
},
{
text: filteringPolicyToText(FilteringPolicy.EXCLUDE),
value: FilteringPolicy.EXCLUDE,
text: filteringPolicyToText('exclude'),
value: 'exclude',
},
]}
/>
@ -140,7 +140,7 @@ export const SyncRulesTable: React.FC = () => {
fullWidth
value={filteringRule.rule}
onChange={(e) => onChange(e.target.value)}
options={Object.values(FilteringRuleRule).map((rule) => ({
options={Object.values(FilteringRuleRuleValues).map((rule) => ({
text: filteringRuleToText(rule),
value: rule,
}))}
@ -184,8 +184,8 @@ export const SyncRulesTable: React.FC = () => {
)}
columns={columns}
defaultItem={{
policy: FilteringPolicy.INCLUDE,
rule: FilteringRuleRule.EQUALS,
policy: 'include',
rule: 'equals',
value: '',
}}
description={description}

View file

@ -25,6 +25,10 @@ import {
StartIncrementalSyncArgs,
} from '../../api/connector/start_incremental_sync_api_logic';
import { StartSyncApiLogic, StartSyncArgs } from '../../api/connector/start_sync_api_logic';
import {
ConnectorConfigurationApiLogic,
PostConnectorConfigurationActions,
} from '../../api/connector/update_connector_configuration_api_logic';
import {
CachedFetchIndexApiLogic,
CachedFetchIndexApiLogicActions,
@ -69,6 +73,7 @@ export interface IndexViewActions {
startSync(): void;
stopFetchIndexPoll(): CachedFetchIndexApiLogicActions['stopPolling'];
stopFetchIndexPoll(): void;
updateConfigurationApiSuccess: PostConnectorConfigurationActions['apiSuccess'];
}
export interface IndexViewValues {
@ -124,6 +129,8 @@ export const IndexViewLogic = kea<MakeLogicType<IndexViewValues, IndexViewAction
'startPolling as startFetchIndexPoll',
'stopPolling as stopFetchIndexPoll',
],
ConnectorConfigurationApiLogic,
['apiSuccess as updateConfigurationApiSuccess'],
StartSyncApiLogic,
['apiSuccess as startSyncApiSuccess', 'makeRequest as makeStartSyncRequest'],
StartIncrementalSyncApiLogic,
@ -204,6 +211,14 @@ export const IndexViewLogic = kea<MakeLogicType<IndexViewValues, IndexViewAction
actions.makeStartSyncRequest({ connectorId: values.fetchIndexApiData.connector.id });
}
},
updateConfigurationApiSuccess: ({ configuration }) => {
if (isConnectorIndex(values.fetchIndexApiData)) {
actions.fetchIndexApiSuccess({
...values.fetchIndexApiData,
connector: { ...values.fetchIndexApiData.connector, configuration },
});
}
},
}),
path: ['enterprise_search', 'content', 'index_view_logic'],
reducers: {

View file

@ -1,58 +0,0 @@
/*
* 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 { syncJobView } from '../../../__mocks__/sync_job.mock';
import React from 'react';
import { shallow } from 'enzyme';
import { SyncStatus, TriggerMethod } from '@kbn/search-connectors';
import { SyncJobCallouts } from './sync_callouts';
describe('SyncCalloutsPanel', () => {
it('renders', () => {
const wrapper = shallow(<SyncJobCallouts syncJob={syncJobView} />);
expect(wrapper).toMatchSnapshot();
});
it('renders error job', () => {
const wrapper = shallow(
<SyncJobCallouts syncJob={{ ...syncJobView, status: SyncStatus.ERROR }} />
);
expect(wrapper).toMatchSnapshot();
});
it('renders canceled job', () => {
const wrapper = shallow(
<SyncJobCallouts syncJob={{ ...syncJobView, status: SyncStatus.CANCELED }} />
);
expect(wrapper).toMatchSnapshot();
});
it('renders in progress job', () => {
const wrapper = shallow(
<SyncJobCallouts syncJob={{ ...syncJobView, status: SyncStatus.IN_PROGRESS }} />
);
expect(wrapper).toMatchSnapshot();
});
it('renders different trigger method', () => {
const wrapper = shallow(
<SyncJobCallouts
syncJob={{
...syncJobView,
status: SyncStatus.IN_PROGRESS,
trigger_method: TriggerMethod.SCHEDULED,
}}
/>
);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -5,19 +5,22 @@
* 2.0.
*/
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useValues } from 'kea';
import { type } from 'io-ts';
import { useActions, useValues } from 'kea';
import { EuiButtonGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SyncJobsTable } from '@kbn/search-connectors';
import { KibanaLogic } from '../../../../shared/kibana';
import { IndexViewLogic } from '../index_view_logic';
import { SyncJobsHistoryTable } from './sync_jobs_history_table';
import { SyncJobsViewLogic } from './sync_jobs_view_logic';
export const SyncJobs: React.FC = () => {
const { hasDocumentLevelSecurityFeature } = useValues(IndexViewLogic);
@ -25,6 +28,19 @@ export const SyncJobs: React.FC = () => {
const [selectedSyncJobCategory, setSelectedSyncJobCategory] = useState<string>('content');
const shouldShowAccessSyncs =
productFeatures.hasDocumentLevelSecurityEnabled && hasDocumentLevelSecurityFeature;
const { connectorId, syncJobsPagination: pagination, syncJobs } = useValues(SyncJobsViewLogic);
const { fetchSyncJobs } = useActions(SyncJobsViewLogic);
useEffect(() => {
if (connectorId) {
fetchSyncJobs({
connectorId,
from: pagination.pageIndex * (pagination.pageSize || 0),
size: pagination.pageSize ?? 10,
type: selectedSyncJobCategory as 'access_control' | 'content',
});
}
}, [connectorId, selectedSyncJobCategory, type]);
return (
<>
@ -62,9 +78,37 @@ export const SyncJobs: React.FC = () => {
/>
)}
{selectedSyncJobCategory === 'content' ? (
<SyncJobsHistoryTable type="content" />
<SyncJobsTable
onPaginate={({ page: { index, size } }) => {
if (connectorId) {
fetchSyncJobs({
connectorId,
from: index * size,
size,
type: selectedSyncJobCategory,
});
}
}}
pagination={pagination}
syncJobs={syncJobs}
type="content"
/>
) : (
<SyncJobsHistoryTable type="access_control" />
<SyncJobsTable
onPaginate={({ page: { index, size } }) => {
if (connectorId) {
fetchSyncJobs({
connectorId,
from: index * size,
size,
type: 'access_control',
});
}
}}
pagination={pagination}
syncJobs={syncJobs}
type="access_control"
/>
)}
</>
);

View file

@ -1,179 +0,0 @@
/*
* 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, { useEffect, useState } from 'react';
import { useActions, useValues } from 'kea';
import { EuiBadge, EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SyncJobType, SyncStatus } from '@kbn/search-connectors';
import { syncJobTypeToText, syncStatusToColor, syncStatusToText } from '@kbn/search-connectors';
import { FormattedDateTime } from '../../../../shared/formatted_date_time';
import { pageToPagination } from '../../../../shared/pagination/page_to_pagination';
import { durationToText } from '../../../utils/duration_to_text';
import { IndexViewLogic } from '../index_view_logic';
import { SyncJobFlyout } from './sync_job_flyout';
import { SyncJobsViewLogic, SyncJobView } from './sync_jobs_view_logic';
interface SyncJobHistoryTableProps {
type: 'content' | 'access_control';
}
export const SyncJobsHistoryTable: React.FC<SyncJobHistoryTableProps> = ({ type }) => {
const { connectorId } = useValues(IndexViewLogic);
const { fetchSyncJobs } = useActions(SyncJobsViewLogic);
const { syncJobs, syncJobsLoading, syncJobsPagination } = useValues(SyncJobsViewLogic);
const [syncJobFlyout, setSyncJobFlyout] = useState<SyncJobView | undefined>(undefined);
const columns: Array<EuiBasicTableColumn<SyncJobView>> = [
{
field: 'lastSync',
name: i18n.translate('xpack.enterpriseSearch.content.syncJobs.lastSync.columnTitle', {
defaultMessage: 'Last sync',
}),
render: (lastSync: string) => <FormattedDateTime date={new Date(lastSync)} />,
sortable: true,
truncateText: true,
},
{
field: 'duration',
name: i18n.translate('xpack.enterpriseSearch.content.syncJobs.syncDuration.columnTitle', {
defaultMessage: 'Sync duration',
}),
render: (duration: moment.Duration) => durationToText(duration),
sortable: true,
truncateText: true,
},
...(type === 'content'
? [
{
field: 'indexed_document_count',
name: i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.addedDocs.columnTitle',
{
defaultMessage: 'Docs added',
}
),
sortable: true,
truncateText: true,
},
{
field: 'deleted_document_count',
name: i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.deletedDocs.columnTitle',
{
defaultMessage: 'Docs deleted',
}
),
sortable: true,
truncateText: true,
},
{
field: 'job_type',
name: i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.syncJobType.columnTitle',
{
defaultMessage: 'Content sync type',
}
),
render: (syncType: SyncJobType) => {
const syncJobTypeText = syncJobTypeToText(syncType);
if (syncJobTypeText.length === 0) return null;
return <EuiBadge color="hollow">{syncJobTypeText}</EuiBadge>;
},
sortable: true,
truncateText: true,
},
]
: []),
...(type === 'access_control'
? [
{
field: 'indexed_document_count',
name: i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.identitySync.columnTitle',
{
defaultMessage: 'Identities synced',
}
),
sortable: true,
truncateText: true,
},
]
: []),
{
field: 'status',
name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.syncStatus.columnTitle', {
defaultMessage: 'Status',
}),
render: (syncStatus: SyncStatus) => (
<EuiBadge color={syncStatusToColor(syncStatus)}>{syncStatusToText(syncStatus)}</EuiBadge>
),
truncateText: true,
},
{
actions: [
{
description: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.actions.viewJob.title',
{
defaultMessage: 'View this sync job',
}
),
icon: 'eye',
isPrimary: false,
name: i18n.translate(
'xpack.enterpriseSearch.content.index.syncJobs.actions.viewJob.caption',
{
defaultMessage: 'View this sync job',
}
),
onClick: (job) => setSyncJobFlyout(job),
type: 'icon',
},
],
},
];
useEffect(() => {
if (connectorId) {
fetchSyncJobs({
connectorId,
from: syncJobsPagination.from ?? 0,
size: syncJobsPagination.size ?? 10,
type,
});
}
}, [connectorId, type]);
return (
<>
<SyncJobFlyout onClose={() => setSyncJobFlyout(undefined)} syncJob={syncJobFlyout} />
<EuiBasicTable
data-test-subj={`entSearchContent-index-${type}-syncJobs-table`}
items={syncJobs}
columns={columns}
hasActions
onChange={({ page: { index, size } }: { page: { index: number; size: number } }) => {
if (connectorId) {
fetchSyncJobs({ connectorId, from: index * size, size, type });
}
}}
pagination={pageToPagination(syncJobsPagination)}
tableLayout="fixed"
loading={syncJobsLoading}
/>
</>
);
};

View file

@ -28,10 +28,9 @@ const DEFAULT_VALUES = {
syncJobsData: undefined,
syncJobsLoading: true,
syncJobsPagination: {
from: 0,
has_more_hits_than_total: false,
size: 10,
total: 0,
pageIndex: 0,
pageSize: 10,
totalItemCount: 0,
},
syncJobsStatus: Status.IDLE,
};
@ -116,10 +115,9 @@ describe('SyncJobsViewLogic', () => {
},
syncJobsLoading: false,
syncJobsPagination: {
from: 40,
has_more_hits_than_total: false,
size: 20,
total: 50,
pageIndex: 2,
pageSize: 20,
totalItemCount: 50,
},
syncJobsStatus: Status.SUCCESS,
});
@ -176,10 +174,9 @@ describe('SyncJobsViewLogic', () => {
},
syncJobsLoading: false,
syncJobsPagination: {
from: 40,
has_more_hits_than_total: false,
size: 20,
total: 50,
pageIndex: 2,
pageSize: 20,
totalItemCount: 50,
},
syncJobsStatus: Status.SUCCESS,
});

View file

@ -9,11 +9,12 @@ import { kea, MakeLogicType } from 'kea';
import moment from 'moment';
import { ConnectorSyncJob } from '@kbn/search-connectors';
import { Pagination } from '@elastic/eui';
import { ConnectorSyncJob, pageToPagination } from '@kbn/search-connectors';
import { Status } from '../../../../../../common/types/api';
import { Page, Paginate } from '../../../../../../common/types/pagination';
import { Paginate } from '../../../../../../common/types/pagination';
import { Actions } from '../../../../shared/api_logic/create_api_logic';
import {
FetchSyncJobsApiLogic,
@ -38,7 +39,7 @@ export interface IndexViewValues {
syncJobs: SyncJobView[];
syncJobsData: Paginate<ConnectorSyncJob> | null;
syncJobsLoading: boolean;
syncJobsPagination: Page;
syncJobsPagination: Pagination;
syncJobsStatus: Status;
}
@ -84,14 +85,13 @@ export const SyncJobsViewLogic = kea<MakeLogicType<IndexViewValues, IndexViewAct
],
syncJobsPagination: [
() => [selectors.syncJobsData],
(data?: Paginate<ConnectorSyncJob>) =>
(data?: Paginate<ConnectorSyncJob>): Pagination =>
data
? data._meta.page
? pageToPagination(data._meta.page)
: {
from: 0,
has_more_hits_than_total: false,
size: 10,
total: 0,
pageIndex: 0,
pageSize: 10,
totalItemCount: 0,
},
],
}),

View file

@ -11,9 +11,13 @@ import { EuiBasicTable, EuiBasicTableColumn, EuiCode } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FilteringRule, FilteringPolicy, FilteringRuleRule } from '@kbn/search-connectors';
import { filteringPolicyToText, filteringRuleToText } from '../../../utils/filtering_rule_helpers';
import {
filteringPolicyToText,
filteringRuleToText,
FilteringRule,
FilteringPolicy,
FilteringRuleRule,
} from '@kbn/search-connectors';
interface FilteringRulesTableProps {
filteringRules: FilteringRule[];

View file

@ -1,19 +0,0 @@
/*
* 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';
export function durationToText(input?: moment.Duration): string {
if (input) {
const hours = input.hours();
const minutes = input.minutes();
const seconds = input.seconds();
return `${hours}h ${minutes}m ${seconds}s`;
} else {
return '--';
}
}

View file

@ -1,78 +0,0 @@
/*
* 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';
import { FilteringPolicy, FilteringRuleRule } from '@kbn/search-connectors';
const filteringRuleStringMap: Record<FilteringRuleRule, string> = {
[FilteringRuleRule.CONTAINS]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.rules.contains',
{
defaultMessage: 'Contains',
}
),
[FilteringRuleRule.ENDS_WITH]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.rules.endsWith',
{
defaultMessage: 'Ends with',
}
),
[FilteringRuleRule.EQUALS]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.rules.equals',
{
defaultMessage: 'Equals',
}
),
[FilteringRuleRule.GT]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.rules.greaterThan',
{
defaultMessage: 'Greater than',
}
),
[FilteringRuleRule.LT]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.rules.lessThan',
{
defaultMessage: 'Less than',
}
),
[FilteringRuleRule.REGEX]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.rules.regEx',
{
defaultMessage: 'Regular expression',
}
),
[FilteringRuleRule.STARTS_WITH]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.rules.startsWith',
{
defaultMessage: 'Starts with',
}
),
};
export function filteringRuleToText(filteringRule: FilteringRuleRule): string {
return filteringRuleStringMap[filteringRule];
}
const filteringPolicyStringMap: Record<FilteringPolicy, string> = {
[FilteringPolicy.EXCLUDE]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.policy.exclude',
{
defaultMessage: 'Exclude',
}
),
[FilteringPolicy.INCLUDE]: i18n.translate(
'xpack.enterpriseSearch.content.filteringRules.policy.include',
{
defaultMessage: 'Include',
}
),
};
export function filteringPolicyToText(filteringPolicy: FilteringPolicy): string {
return filteringPolicyStringMap[filteringPolicy];
}

View file

@ -19,13 +19,7 @@ import {
updateFilteringDraft,
} from '@kbn/search-connectors';
import {
ConnectorStatus,
FilteringPolicy,
FilteringRule,
FilteringRuleRule,
SyncJobType,
} from '@kbn/search-connectors';
import { ConnectorStatus, FilteringRule, SyncJobType } from '@kbn/search-connectors';
import { cancelSyncs } from '@kbn/search-connectors/lib/cancel_syncs';
import { ErrorCode } from '../../../common/types/error_codes';
@ -39,7 +33,6 @@ import { RouteDependencies } from '../../plugin';
import { createError } from '../../utils/create_error';
import { elasticsearchErrorHandler } from '../../utils/elasticsearch_error_handler';
import { isAccessControlDisabledException } from '../../utils/identify_exceptions';
import { validateEnum } from '../../utils/validate_enum';
export function registerConnectorRoutes({ router, log }: RouteDependencies) {
router.post(
@ -391,12 +384,8 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
field: schema.string(),
id: schema.string(),
order: schema.number(),
policy: schema.string({
validate: validateEnum(FilteringPolicy, 'policy'),
}),
rule: schema.string({
validate: validateEnum(FilteringRuleRule, 'rule'),
}),
policy: schema.string(),
rule: schema.string(),
updated_at: schema.string(),
value: schema.string(),
})
@ -433,12 +422,8 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
field: schema.string(),
id: schema.string(),
order: schema.number(),
policy: schema.string({
validate: validateEnum(FilteringPolicy, 'policy'),
}),
rule: schema.string({
validate: validateEnum(FilteringRuleRule, 'rule'),
}),
policy: schema.string(),
rule: schema.string(),
updated_at: schema.string(),
value: schema.string(),
})

View file

@ -23,6 +23,10 @@ export const SAVE_LABEL: string = i18n.translate('xpack.serverlessSearch.save',
defaultMessage: 'Save',
});
export const UPDATE_LABEL: string = i18n.translate('xpack.serverlessSearch.update', {
defaultMessage: 'Update',
});
export const BACK_LABEL: string = i18n.translate('xpack.serverlessSearch.back', {
defaultMessage: 'Back',
});
@ -74,3 +78,14 @@ export const COPY_CONNECTOR_ID_LABEL = i18n.translate(
defaultMessage: 'Copy connector id',
}
);
export const OVERVIEW_LABEL = i18n.translate('xpack.serverlessSearch.connectors.overviewLabel', {
defaultMessage: 'Overview',
});
export const CONFIGURATION_LABEL = i18n.translate(
'xpack.serverlessSearch.connectors.configurationLabel',
{
defaultMessage: 'Configuration',
}
);

View file

@ -27,8 +27,8 @@ import React, { useState } from 'react';
import { useKibanaServices } from '../../hooks/use_kibana';
import { MANAGEMENT_API_KEYS } from '../../../../common/routes';
import { CreateApiKeyFlyout } from './create_api_key_flyout';
import { CreateApiKeyResponse } from './types';
import './api_key.scss';
import { CreateApiKeyResponse } from '../../hooks/api/use_create_api_key';
export const ApiKeyPanel = ({ setClientApiKey }: { setClientApiKey: (value: string) => void }) => {
const { http, user } = useKibanaServices();

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { css } from '@emotion/react';
import {
@ -29,7 +29,6 @@ import {
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useMutation } from '@tanstack/react-query';
import {
CANCEL_LABEL,
@ -38,14 +37,11 @@ import {
INVALID_JSON_ERROR,
REQUIRED_LABEL,
} from '../../../../common/i18n_string';
import { CreateAPIKeyArgs } from '../../../../common/types';
import { useKibanaServices } from '../../hooks/use_kibana';
import { CREATE_API_KEY_PATH } from '../../../../common/routes';
import { isApiError } from '../../../utils/api';
import { BasicSetupForm, DEFAULT_EXPIRES_VALUE } from './basic_setup_form';
import { MetadataForm } from './metadata_form';
import { SecurityPrivilegesForm } from './security_privileges_form';
import { CreateApiKeyResponse } from './types';
import { CreateApiKeyResponse, useCreateApiKey } from '../../hooks/api/use_create_api_key';
const DEFAULT_ROLE_DESCRIPTORS = `{
"serverless_search": {
@ -84,7 +80,6 @@ export const CreateApiKeyFlyout: React.FC<CreateApiKeyFlyoutProps> = ({
setApiKey,
}) => {
const { euiTheme } = useEuiTheme();
const { http } = useKibanaServices();
const [name, setName] = useState('');
const [expires, setExpires] = useState<string | null>(DEFAULT_EXPIRES_VALUE);
const [roleDescriptors, setRoleDescriptors] = useState(DEFAULT_ROLE_DESCRIPTORS);
@ -142,18 +137,15 @@ export const CreateApiKeyFlyout: React.FC<CreateApiKeyFlyoutProps> = ({
});
};
const { isLoading, isError, error, mutate } = useMutation({
mutationFn: async (input: CreateAPIKeyArgs) => {
const result = await http.post<CreateApiKeyResponse>(CREATE_API_KEY_PATH, {
body: JSON.stringify(input),
});
return result;
},
onSuccess: (apiKey) => {
setApiKey(apiKey);
const { data, isLoading, isError, isSuccess, error, mutate } = useCreateApiKey();
useEffect(() => {
if (isSuccess) {
setApiKey(data);
onClose();
},
}
});
const createError = parseCreateError(error);
return (
<EuiFlyout

View file

@ -1,15 +0,0 @@
/*
* 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 CreateApiKeyResponse {
id: string;
name: string;
expiration?: number;
api_key: string;
encoded?: string;
beats_logstash_format: string;
}

View file

@ -0,0 +1,100 @@
/*
* 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 {
EuiPanel,
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
EuiBadge,
EuiSpacer,
EuiText,
EuiCode,
EuiButton,
EuiCodeBlock,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Connector, CONNECTORS_INDEX } from '@kbn/search-connectors';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { OPTIONAL_LABEL } from '../../../../../common/i18n_string';
import { useCreateApiKey } from '../../../hooks/api/use_create_api_key';
interface ApiKeyPanelProps {
connector: Connector;
}
export const ApiKeyPanel: React.FC<ApiKeyPanelProps> = ({ connector }) => {
const { data, isLoading, mutate } = useCreateApiKey();
return (
<EuiPanel hasBorder>
<EuiFlexGroup direction="row" justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h3>
{i18n.translate('xpack.serverlessSearch.connectors.config.apiKeyTitle', {
defaultMessage: 'Prepare an API key',
})}
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge>{OPTIONAL_LABEL}</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiText color="subdued">
<FormattedMessage
id="xpack.serverlessSearch.connectors.config.apiKeyDescription"
defaultMessage="You can limit the connector's API key to only have access to the above index. Once created, use this key to set the {apiKey} variable in your {config} file."
values={{
apiKey: <EuiCode>api_key</EuiCode>,
config: <EuiCode>config.yml</EuiCode>,
}}
/>
</EuiText>
<EuiSpacer />
<span>
<EuiButton
isDisabled={!connector.index_name}
isLoading={isLoading}
iconType="plusInCircle"
color="primary"
onClick={() => {
if (connector.index_name) {
mutate({
name: `${connector.index_name}-connector`,
role_descriptors: {
[`${connector.index_name}-connector-role`]: {
cluster: ['monitor'],
index: [
{
names: [
connector.index_name,
connector.index_name.replace(
/^(?:search-)?(.*)$/,
'.search-acl-filter-$1'
),
`${CONNECTORS_INDEX}*`,
],
privileges: ['all'],
},
],
},
},
});
}
}}
>
{i18n.translate('xpack.serverlessSearch.connectors.config.createApikeyLabel', {
defaultMessage: 'New API key',
})}
</EuiButton>
</span>
<EuiSpacer />
{Boolean(data) && <EuiCodeBlock isCopyable>{data?.encoded}</EuiCodeBlock>}
</EuiPanel>
);
};

View file

@ -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 { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Connector, ConnectorConfigurationComponent } from '@kbn/search-connectors';
import { useQueryClient } from '@tanstack/react-query';
import React, { useEffect } from 'react';
import { useConnector } from '../../../hooks/api/use_connector';
import { useEditConnectorConfiguration } from '../../../hooks/api/use_connector_configuration';
interface ConnectorConfigFieldsProps {
connector: Connector;
}
export const ConnectorConfigFields: React.FC<ConnectorConfigFieldsProps> = ({ connector }) => {
const { data, isLoading, isSuccess, mutate, reset } = useEditConnectorConfiguration(connector.id);
const { queryKey } = useConnector(connector.id);
const queryClient = useQueryClient();
useEffect(() => {
if (isSuccess) {
queryClient.setQueryData(queryKey, { connector: { ...connector, configuration: data } });
queryClient.invalidateQueries(queryKey);
reset();
}
}, [data, isSuccess, connector, queryClient, queryKey, reset]);
return (
<EuiFlexGroup direction="column" alignItems="center" justifyContent="center">
<EuiFlexItem>
<EuiTitle size="s">
<h2>
{i18n.translate('xpack.serverlessSearch.connectors.config.connectorConfigTitle', {
defaultMessage: 'Configure your connector',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued">
{i18n.translate('xpack.serverlessSearch.connectors.config.connectorConfigDescription', {
defaultMessage:
'Your connector is set up. Now you can enter access details for your data source. This ensures the connector can find content and is authorized to access it.',
})}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<ConnectorConfigurationComponent
connector={connector}
hasPlatinumLicense={false}
isLoading={isLoading}
saveConfig={mutate}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,51 @@
/*
* 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 { EuiPanel, EuiSpacer } from '@elastic/eui';
import { Connector, ConnectorConfigurationComponent } from '@kbn/search-connectors';
import { useQueryClient } from '@tanstack/react-query';
import React, { useEffect } from 'react';
import { useConnector } from '../../../hooks/api/use_connector';
import { useEditConnectorConfiguration } from '../../../hooks/api/use_connector_configuration';
import { ConnectorIndexnamePanel } from './connector_index_name_panel';
interface ConnectorConfigurationPanels {
connector: Connector;
}
export const ConnectorConfigurationPanels: React.FC<ConnectorConfigurationPanels> = ({
connector,
}) => {
const { data, isLoading, isSuccess, mutate, reset } = useEditConnectorConfiguration(connector.id);
const { queryKey } = useConnector(connector.id);
const queryClient = useQueryClient();
useEffect(() => {
if (isSuccess) {
queryClient.setQueryData(queryKey, { connector: { ...connector, configuration: data } });
queryClient.invalidateQueries(queryKey);
reset();
}
}, [data, isSuccess, connector, queryClient, queryKey, reset]);
return (
<>
<EuiPanel hasBorder>
<ConnectorConfigurationComponent
connector={connector}
hasPlatinumLicense={false}
isLoading={isLoading}
saveConfig={mutate}
/>
<EuiSpacer />
</EuiPanel>
<EuiSpacer />
<EuiPanel hasBorder>
<ConnectorIndexnamePanel connector={connector} />
</EuiPanel>
</>
);
};

View file

@ -0,0 +1,154 @@
/*
* 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, { useEffect, useState } from 'react';
import {
Connector,
ConnectorStatus,
pageToPagination,
SyncJobsTable,
} from '@kbn/search-connectors';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiStepsHorizontal,
EuiStepsHorizontalProps,
EuiTabbedContent,
EuiTabbedContentTab,
Pagination,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { CONFIGURATION_LABEL, OVERVIEW_LABEL } from '../../../../../common/i18n_string';
import { ConnectorLinkElasticsearch } from './connector_link';
import { ConnectorConfigFields } from './connector_config_fields';
import { ConnectorIndexName } from './connector_index_name';
import { useSyncJobs } from '../../../hooks/api/use_sync_jobs';
import { ConnectorConfigurationPanels } from './connector_config_panels';
interface ConnectorConfigurationProps {
connector: Connector;
}
type ConnectorConfigurationStep = 'link' | 'configure' | 'connect' | 'connected';
export const ConnectorConfiguration: React.FC<ConnectorConfigurationProps> = ({ connector }) => {
const [currentStep, setCurrentStep] = useState<ConnectorConfigurationStep>('link');
useEffect(() => {
const step =
connector.status === ConnectorStatus.CREATED
? 'link'
: connector.status === ConnectorStatus.NEEDS_CONFIGURATION
? 'configure'
: connector.status === ConnectorStatus.CONFIGURED
? 'connect'
: 'connected';
setCurrentStep(step);
}, [connector.status, setCurrentStep]);
const steps: EuiStepsHorizontalProps['steps'] = [
{
title: i18n.translate('xpack.serverlessSearch.connectors.config.linkToElasticTitle', {
defaultMessage: 'Link to Elasticsearch',
}),
status:
currentStep === 'link'
? 'current'
: connector.status === ConnectorStatus.CREATED
? 'incomplete'
: 'complete',
onClick: () => setCurrentStep('link'),
size: 's',
},
{
title: i18n.translate('xpack.serverlessSearch.connectors.config.configureTitle', {
defaultMessage: 'Configure',
}),
status:
currentStep === 'configure'
? 'current'
: connector.status === ConnectorStatus.CONFIGURED ||
connector.status === ConnectorStatus.CONNECTED
? 'complete'
: 'incomplete',
onClick: () => setCurrentStep('configure'),
size: 's',
},
{
title: i18n.translate('xpack.serverlessSearch.connectors.config.connectTitle', {
defaultMessage: 'Connect Index',
}),
status:
currentStep === 'connect'
? 'current'
: connector.status === ConnectorStatus.CONNECTED && connector.index_name
? 'complete'
: 'incomplete',
onClick: () => setCurrentStep('connect'),
size: 's',
},
];
const [pagination, setPagination] = useState<Omit<Pagination, 'totalItemCount'>>({
pageIndex: 0,
pageSize: 20,
});
const { data: syncJobsData, isLoading: syncJobsLoading } = useSyncJobs(connector.id, pagination);
const tabs: EuiTabbedContentTab[] = [
{
content: (
<>
<EuiSpacer />
<SyncJobsTable
isLoading={syncJobsLoading}
onPaginate={({ page }) => setPagination({ pageIndex: page.index, pageSize: page.size })}
pagination={
syncJobsData
? pageToPagination(syncJobsData?._meta.page)
: { pageIndex: 0, pageSize: 20, totalItemCount: 0 }
}
syncJobs={syncJobsData?.data || []}
type="content"
/>
</>
),
id: 'overview',
name: OVERVIEW_LABEL,
},
{
content: (
<>
<EuiSpacer />
<ConnectorConfigurationPanels connector={connector} />
</>
),
id: 'configuration',
name: CONFIGURATION_LABEL,
},
];
return currentStep === 'connected' ? (
<EuiTabbedContent tabs={tabs} />
) : (
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiStepsHorizontal size="s" steps={steps} />
</EuiFlexItem>
<EuiFlexItem>
{currentStep === 'link' && (
<ConnectorLinkElasticsearch
connectorId={connector.id}
serviceType={connector.service_type || ''}
status={connector.status}
/>
)}
{currentStep === 'configure' && <ConnectorConfigFields connector={connector} />}
{currentStep === 'connect' && <ConnectorIndexName connector={connector} />}
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,126 @@
/*
* 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Connector } from '@kbn/search-connectors';
import React, { useEffect, useState } from 'react';
import { useQueryClient, useMutation } from '@tanstack/react-query';
import { isValidIndexName } from '../../../../utils/validate_index_name';
import { SAVE_LABEL } from '../../../../../common/i18n_string';
import { useConnector } from '../../../hooks/api/use_connector';
import { useKibanaServices } from '../../../hooks/use_kibana';
import { ApiKeyPanel } from './api_key_panel';
import { ConnectorIndexNameForm } from './connector_index_name_form';
interface ConnectorIndexNameProps {
connector: Connector;
}
export const ConnectorIndexName: React.FC<ConnectorIndexNameProps> = ({ connector }) => {
const { http, notifications } = useKibanaServices();
const queryClient = useQueryClient();
const { queryKey } = useConnector(connector.id);
const { data, error, isLoading, isSuccess, mutate, reset } = useMutation({
mutationFn: async ({ inputName, sync }: { inputName: string | null; sync?: boolean }) => {
if (inputName && inputName !== connector.index_name) {
const body = { index_name: inputName };
await http.post(`/internal/serverless_search/connectors/${connector.id}/index_name`, {
body: JSON.stringify(body),
});
}
if (sync) {
await http.post(`/internal/serverless_search/connectors/${connector.id}/sync`);
}
return inputName;
},
});
useEffect(() => {
if (isSuccess) {
queryClient.setQueryData(queryKey, { connector: { ...connector, index_name: data } });
queryClient.invalidateQueries(queryKey);
reset();
}
}, [data, isSuccess, connector, queryClient, queryKey, reset]);
useEffect(() => {
if (error) {
notifications.toasts.addError(error as Error, {
title: i18n.translate('xpack.serverlessSearch.connectors.config.connectorIndexNameError', {
defaultMessage: 'Error updating index name',
}),
});
}
}, [error, notifications]);
const [newIndexName, setNewIndexname] = useState(connector.index_name);
return (
<>
<EuiFlexGroup direction="column" alignItems="center" justifyContent="center">
<EuiFlexItem>
<EuiTitle size="s">
<h2>
{i18n.translate('xpack.serverlessSearch.connectors.config.connectorIndexNameTitle', {
defaultMessage: 'Link index',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued">
{i18n.translate(
'xpack.serverlessSearch.connectors.config.connectorIndexNameDescription',
{
defaultMessage:
'Pick an index where your documents will be synced, or create a new one for this connector.',
}
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<ConnectorIndexNameForm
indexName={newIndexName || ''}
onChange={(name) => setNewIndexname(name)}
/>
<EuiSpacer />
<ApiKeyPanel connector={connector} />
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<span>
<EuiButton
color="primary"
isDisabled={!isValidIndexName(newIndexName)}
isLoading={isLoading}
onClick={() => mutate({ inputName: newIndexName, sync: false })}
>
{SAVE_LABEL}
</EuiButton>
</span>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<span>
<EuiButton
color="primary"
disabled={!isValidIndexName(newIndexName)}
fill
isLoading={isLoading}
onClick={() => mutate({ inputName: newIndexName, sync: true })}
>
{i18n.translate('xpack.serverlessSearch.connectors.config.saveSyncLabel', {
defaultMessage: 'Save and sync',
})}
</EuiButton>
</span>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};

View file

@ -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 { EuiForm, EuiFormRow, EuiComboBox } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useState } from 'react';
import { isValidIndexName } from '../../../../utils/validate_index_name';
import { useIndexNameSearch } from '../../../hooks/api/use_index_name_search';
interface ConnectorIndexNameFormProps {
indexName: string;
isDisabled?: boolean;
onChange: (output: string) => void;
}
export const ConnectorIndexNameForm: React.FC<ConnectorIndexNameFormProps> = ({
indexName,
onChange,
isDisabled,
}) => {
const [query, setQuery] = useState('');
const { data: indexNames, isLoading: isLoadingIndices, refetch } = useIndexNameSearch(query);
useEffect(() => {
refetch();
}, [query, refetch]);
const [newIndexName, setNewIndexName] = useState(indexName);
useEffect(() => {
onChange(newIndexName);
}, [newIndexName, onChange]);
return (
<EuiForm fullWidth>
<EuiFormRow
error={
!isValidIndexName(newIndexName || '')
? i18n.translate('xpack.serverlessSearch.connectors.indexNameErrorText', {
defaultMessage:
'Names should be lowercase and cannot contain spaces or special characters.',
})
: undefined
}
isInvalid={!!newIndexName && !isValidIndexName(newIndexName)}
label={i18n.translate('xpack.serverlessSearch.connectors.config.indexNameLabel', {
defaultMessage: 'Create or select an index',
})}
fullWidth
helpText={i18n.translate('xpack.serverlessSearch.connectors.indexNameInputHelpText', {
defaultMessage:
'Names should be lowercase and cannot contain spaces or special characters.',
})}
>
<EuiComboBox
async
isClearable={false}
customOptionText={i18n.translate(
'xpack.serverlessSearch.connectors.config.createIndexLabel',
{
defaultMessage: 'The connector will create the index {searchValue}',
values: { searchValue: '{searchValue}' },
}
)}
isDisabled={isDisabled}
isLoading={isLoadingIndices}
onChange={(values) => {
if (values[0].value) {
setNewIndexName(values[0].value);
}
}}
onCreateOption={(value) => setNewIndexName(value)}
onSearchChange={(value) => setQuery(value)}
options={(indexNames?.index_names || []).map((name) => ({
label: name,
value: name,
}))}
selectedOptions={newIndexName ? [{ label: newIndexName, value: newIndexName }] : []}
singleSelection
/>
</EuiFormRow>
</EuiForm>
);
};

View file

@ -0,0 +1,84 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Connector } from '@kbn/search-connectors';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useEffect, useState } from 'react';
import { isValidIndexName } from '../../../../utils/validate_index_name';
import { UPDATE_LABEL } from '../../../../../common/i18n_string';
import { useConnector } from '../../../hooks/api/use_connector';
import { useKibanaServices } from '../../../hooks/use_kibana';
import { ConnectorIndexNameForm } from './connector_index_name_form';
interface ConnectorIndexNamePanelProps {
connector: Connector;
}
export const ConnectorIndexnamePanel: React.FC<ConnectorIndexNamePanelProps> = ({ connector }) => {
const { http, notifications } = useKibanaServices();
const { data, error, isLoading, isSuccess, mutate, reset } = useMutation({
mutationFn: async (inputName: string) => {
if (inputName && inputName !== connector.index_name) {
const body = { index_name: inputName };
await http.post(`/internal/serverless_search/connectors/${connector.id}/index_name`, {
body: JSON.stringify(body),
});
}
return inputName;
},
});
const queryClient = useQueryClient();
const { queryKey } = useConnector(connector.id);
useEffect(() => {
if (isSuccess) {
queryClient.setQueryData(queryKey, { connector: { ...connector, index_name: data } });
queryClient.invalidateQueries(queryKey);
reset();
}
}, [data, isSuccess, connector, queryClient, queryKey, reset]);
useEffect(() => {
if (error) {
notifications.toasts.addError(error as Error, {
title: i18n.translate('xpack.serverlessSearch.connectors.config.connectorIndexNameError', {
defaultMessage: 'Error updating index name',
}),
});
}
}, [error, notifications]);
const [newIndexName, setNewIndexName] = useState(connector.index_name || '');
return (
<>
<ConnectorIndexNameForm
isDisabled={isLoading}
indexName={newIndexName}
onChange={(name) => setNewIndexName(name)}
/>
<EuiSpacer />
<EuiFlexGroup alignItems="flexEnd" direction="row">
<EuiFlexItem>
<span>
<EuiButton
color="primary"
disabled={!isValidIndexName(newIndexName)}
fill
isLoading={isLoading}
onClick={() => mutate(newIndexName)}
>
{UPDATE_LABEL}
</EuiButton>
</span>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};

View file

@ -0,0 +1,131 @@
/*
* 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 {
EuiButton,
EuiCallOut,
EuiCode,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ConnectorStatus } from '@kbn/search-connectors';
import React from 'react';
import { docLinks } from '../../../../../common/doc_links';
import { useAssetBasePath } from '../../../hooks/use_asset_base_path';
interface ConnectorLinkElasticsearchProps {
connectorId: string;
serviceType: string;
status: ConnectorStatus;
}
export const ConnectorLinkElasticsearch: React.FC<ConnectorLinkElasticsearchProps> = ({
connectorId,
serviceType,
status,
}) => {
const assetBasePath = useAssetBasePath();
return (
<EuiFlexGroup direction="column" alignItems="center" justifyContent="center">
<EuiFlexItem>
<EuiTitle size="s">
<h2>
{i18n.translate('xpack.serverlessSearch.connectors.config.link.linkToElasticTitle', {
defaultMessage: 'Link your connector to Elasticsearch',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued">
{i18n.translate('xpack.serverlessSearch.connectors.config.linkToElasticDescription', {
defaultMessage:
'You need to run the connector code on your own infrastructure and link it to your Elasticsearch instance. You have two options:',
})}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="row" alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<span>
<EuiButton iconType={`${assetBasePath}/docker.svg`} href={docLinks.connectors} fill>
{i18n.translate('xpack.serverlessSearch.connectors.runWithDockerLink', {
defaultMessage: 'Run with Docker',
})}
</EuiButton>
</span>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<span>
<EuiButton
iconType={`${assetBasePath}/github_white.svg`}
href="https://github.com/elastic/connectors-python"
fill
>
{i18n.translate('xpack.serverlessSearch.connectors.runFromSourceLink', {
defaultMessage: 'Run from source',
})}
</EuiButton>
</span>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexItem>
<EuiPanel hasBorder>
<EuiTitle size="xs">
<h3>
{i18n.translate('xpack.serverlessSearch.connectors.variablesTitle', {
defaultMessage: 'Variables for your ',
})}
<EuiCode>connectors-python/config.yml</EuiCode>
</h3>
</EuiTitle>
<EuiSpacer />
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiText size="s">
<strong>connector_id</strong>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiCode>{connectorId}</EuiCode>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiText size="s">
<strong>service_type</strong>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{Boolean(serviceType) && <EuiCode>{serviceType}</EuiCode>}
</EuiFlexItem>
</EuiFlexGroup>
{status === ConnectorStatus.CREATED && (
<>
<EuiSpacer />
<EuiCallOut
title={i18n.translate('xpack.serverlessSearch.connectors.waitingForConnection', {
defaultMessage: 'Waiting for connection',
})}
color="warning"
iconType="iInCircle"
/>
</>
)}
</EuiPanel>
</EuiFlexItem>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -11,12 +11,11 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiPageTemplate,
EuiPanel,
EuiPopover,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { useQuery } from '@tanstack/react-query';
import { Connector } from '@kbn/search-connectors';
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
@ -31,6 +30,8 @@ import { EditName } from './edit_name';
import { EditServiceType } from './edit_service_type';
import { EditDescription } from './edit_description';
import { DeleteConnectorModal } from './delete_connector_modal';
import { ConnectorConfiguration } from './connector_config/connector_configuration';
import { useConnector } from '../../hooks/api/use_connector';
export const EditConnector: React.FC = () => {
const [deleteModalIsOpen, setDeleteModalIsOpen] = useState(false);
@ -41,14 +42,9 @@ export const EditConnector: React.FC = () => {
useEffect(() => setDeleteModalIsOpen(false), [id, setDeleteModalIsOpen]);
const {
application: { navigateToUrl },
http,
} = useKibanaServices();
const { data, isLoading, refetch } = useQuery({
queryKey: [`fetchConnector${id}`],
queryFn: () =>
http.fetch<{ connector: Connector }>(`/internal/serverless_search/connector/${id}`),
});
const { data, isLoading, refetch } = useConnector(id);
if (isLoading) {
<EuiPageTemplate offset={0} grow restrictWidth data-test-subj="svlSearchEditConnectorsPage">
@ -91,7 +87,7 @@ export const EditConnector: React.FC = () => {
return (
<EuiPageTemplate offset={0} grow restrictWidth data-test-subj="svlSearchEditConnectorsPage">
<EuiPageTemplate.Section grow={false}>
<EuiPageTemplate.Section grow={false} color="subdued">
<EuiText size="s">{CONNECTOR_LABEL}</EuiText>
<EuiFlexGroup direction="row" justifyContent="spaceBetween">
<EuiFlexItem>
@ -154,7 +150,7 @@ export const EditConnector: React.FC = () => {
</EuiPageTemplate.Section>
<EuiPageTemplate.Section>
<EuiFlexGroup direction="row">
<EuiFlexItem>
<EuiFlexItem grow={1}>
<EditServiceType
connectorId={id}
serviceType={connector.service_type ?? ''}
@ -167,6 +163,11 @@ export const EditConnector: React.FC = () => {
onSuccess={refetch}
/>
</EuiFlexItem>
<EuiFlexItem grow={2}>
<EuiPanel hasBorder hasShadow={false}>
<ConnectorConfiguration connector={connector} />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageTemplate.Section>
</EuiPageTemplate>

View file

@ -16,7 +16,7 @@ export const EDIT_CONNECTOR_PATH = `${BASE_CONNECTORS_PATH}/:id`;
export const ConnectorsRouter: React.FC = () => {
return (
<Routes>
<Route path={'/:id'}>
<Route exact path={'/:id'}>
<EditConnector />
</Route>
<Route exact path="/">

View file

@ -0,0 +1,21 @@
/*
* 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 { Connector } from '@kbn/search-connectors';
import { useQuery } from '@tanstack/react-query';
import { useKibanaServices } from '../use_kibana';
export const useConnector = (id: string) => {
const { http } = useKibanaServices();
const queryKey = ['fetchConnector', id];
const result = useQuery({
queryKey,
queryFn: () =>
http.fetch<{ connector: Connector }>(`/internal/serverless_search/connector/${id}`),
});
return { queryKey, ...result };
};

View file

@ -0,0 +1,25 @@
/*
* 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 { useMutation } from '@tanstack/react-query';
import { useKibanaServices } from '../use_kibana';
export const useEditConnectorConfiguration = (connectorId: string) => {
const { http } = useKibanaServices();
return useMutation({
mutationFn: async (configuration: Record<string, string | number | boolean | null>) => {
const body = { configuration };
const result = await http.post(
`/internal/serverless_search/connectors/${connectorId}/configuration`,
{
body: JSON.stringify(body),
}
);
return result;
},
});
};

View file

@ -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 { useMutation } from '@tanstack/react-query';
import { CREATE_API_KEY_PATH } from '../../../../common/routes';
import { CreateAPIKeyArgs } from '../../../../common/types';
import { useKibanaServices } from '../use_kibana';
export interface CreateApiKeyResponse {
id: string;
name: string;
expiration?: number;
api_key: string;
encoded?: string;
beats_logstash_format: string;
}
export const useCreateApiKey = () => {
const { http } = useKibanaServices();
return useMutation({
mutationFn: async (input: CreateAPIKeyArgs) => {
const result = await http.post<CreateApiKeyResponse>(CREATE_API_KEY_PATH, {
body: JSON.stringify(input),
});
return result;
},
});
};

View file

@ -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 { useQuery } from '@tanstack/react-query';
import { useKibanaServices } from '../use_kibana';
export const useIndexNameSearch = (query: string) => {
const { http } = useKibanaServices();
return useQuery({
queryKey: ['fetchIndexNames', query],
queryFn: async () =>
http.fetch<{ index_names: string[] }>('/internal/serverless_search/index_names', {
query: { query },
}),
});
};

View file

@ -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 { Pagination } from '@elastic/eui';
import { ConnectorSyncJob, Paginate } from '@kbn/search-connectors';
import { useQuery } from '@tanstack/react-query';
import { useKibanaServices } from '../use_kibana';
export const useSyncJobs = (
connectorId: string,
pagination: Omit<Pagination, 'totalItemCount'>
) => {
const { http } = useKibanaServices();
return useQuery({
keepPreviousData: true,
queryKey: ['fetchSyncJobs', pagination],
queryFn: async () =>
http.fetch<Paginate<ConnectorSyncJob>>(
`/internal/serverless_search/connectors/${connectorId}/sync_jobs`,
{
query: {
from: pagination.pageIndex * (pagination.pageSize || 10),
size: pagination.pageSize || 10,
},
}
),
});
};

View file

@ -0,0 +1,4 @@
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.04961 5.554H10.7016V4.0675H9.05011V5.554H9.04961ZM7.09711 5.554H8.74911V4.0675H7.09711V5.554ZM5.14461 5.554H6.79661V4.0675H5.14511V5.554H5.14461ZM3.19211 5.554H4.84511V4.0675H3.19211V5.554ZM1.24011 5.554H2.89211V4.0675H1.24011V5.554ZM3.19211 3.77H4.84511V2.284H3.19211V3.77ZM5.14461 3.77H6.79661V2.284H5.14511V3.77H5.14461ZM7.09711 3.77H8.74911V2.284H7.09711V3.77ZM7.09711 1.9865H8.74911V0.5H7.09711V1.9865ZM15.6666 4.6875C15.3056 4.4485 14.4766 4.361 13.8386 4.48C13.7566 3.885 13.4216 3.3695 12.8126 2.9035L12.4626 2.672L12.2286 3.019C11.9296 3.4655 11.7801 4.084 11.8286 4.6775C11.8511 4.8865 11.9201 5.2595 12.1371 5.5875C11.9206 5.703 11.4921 5.862 10.9271 5.8515H0.062113L0.040613 5.975C-0.061387 6.5715 -0.059387 8.432 1.16061 9.862C2.08911 10.949 3.48011 11.5 5.29511 11.5C9.23011 11.5 12.1416 9.707 13.5051 6.448C14.0416 6.458 15.1956 6.451 15.7886 5.3295C15.8041 5.304 15.8396 5.2365 15.9436 5.0245L16.0001 4.9075L15.6666 4.6875Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 0C3.58203 0 0 3.67167 0 8.2002C0 11.8238 2.292 14.8969 5.47119 15.981C5.87109 16.0561 6.01709 15.8028 6.01709 15.5866C6.01709 15.3914 6.00977 14.7447 6.00586 14.0601C3.78125 14.5555 3.31103 13.0931 3.31103 13.0931C2.94678 12.1461 2.42284 11.8939 2.42284 11.8939C1.6958 11.3854 2.478 11.3954 2.478 11.3954C3.28122 11.4524 3.70409 12.2402 3.70409 12.2402C4.41797 13.4935 5.57714 13.1311 6.03222 12.9209C6.10494 12.3924 6.31197 12.03 6.54003 11.8258C4.76416 11.6186 2.896 10.9149 2.896 7.77276C2.896 6.87686 3.20802 6.14615 3.71875 5.57207C3.6372 5.36386 3.36181 4.52952 3.79784 3.4009C3.79784 3.4009 4.46875 3.18069 5.99803 4.24175C6.63572 4.05907 7.31981 3.96898 7.99998 3.96597C8.67967 3.96898 9.36423 4.06006 10.0029 4.24274C11.5293 3.18068 12.2012 3.4019 12.2012 3.4019C12.6387 4.53152 12.3633 5.36485 12.2812 5.57207C12.7939 6.14615 13.1035 6.87688 13.1035 7.77276C13.1035 10.9229 11.2324 11.6166 9.4502 11.8198C9.7383 12.0741 9.99317 12.5726 9.99317 13.3373C9.99317 14.4334 9.98242 15.3173 9.98242 15.5876C9.98242 15.8058 10.1279 16.0611 10.5332 15.981C13.71 14.8949 16 11.8218 16 8.2002C16 3.67167 12.418 0 8 0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,23 @@
/*
* 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.
*/
// see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html for the current rules
export function isValidIndexName(name: string | undefined | null): boolean {
if (!name) return false;
const byteLength = encodeURI(name).split(/%(?:u[0-9A-F]{2})?[0-9A-F]{2}|./).length - 1;
const reg = new RegExp('[\\\\/:*?"<>|\\s,#]+');
const indexPatternInvalid =
byteLength > 255 || // name can't be greater than 255 bytes
name !== name.toLowerCase() || // name should be lowercase
name === '.' ||
name === '..' || // name can't be . or ..
name.match(/^[-_+]/) !== null || // name can't start with these chars
name.match(reg) !== null; // name can't contain these chars
return !indexPatternInvalid;
}

View file

@ -12,7 +12,10 @@ import {
deleteConnectorById,
fetchConnectorById,
fetchConnectors,
fetchSyncJobsByConnectorId,
startConnectorSync,
updateConnectorConfiguration,
updateConnectorIndexName,
updateConnectorNameAndDescription,
updateConnectorServiceType,
} from '@kbn/search-connectors';
@ -170,6 +173,38 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) =>
}
);
router.post(
{
path: '/internal/serverless_search/connectors/{connectorId}/index_name',
validate: {
body: schema.object({
index_name: schema.string(),
}),
params: schema.object({
connectorId: schema.string(),
}),
},
},
async (context, request, response) => {
const { client } = (await context.core).elasticsearch;
try {
const result = await updateConnectorIndexName(
client.asCurrentUser,
request.params.connectorId,
request.body.index_name
);
return response.ok({
body: {
result,
},
headers: { 'content-type': 'application/json' },
});
} catch (e) {
return response.conflict({ body: e });
}
}
);
router.post(
{
path: '/internal/serverless_search/connectors/{connectorId}/service_type',
@ -244,9 +279,60 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) =>
);
return response.ok({
body: {
result,
},
body: result,
headers: { 'content-type': 'application/json' },
});
}
);
router.post(
{
path: '/internal/serverless_search/connectors/{connectorId}/sync',
validate: {
params: schema.object({
connectorId: schema.string(),
}),
},
},
async (context, request, response) => {
const { client } = (await context.core).elasticsearch;
const result = await startConnectorSync(client.asCurrentUser, {
connectorId: request.params.connectorId,
});
return response.ok({
body: result,
headers: { 'content-type': 'application/json' },
});
}
);
router.get(
{
path: '/internal/serverless_search/connectors/{connectorId}/sync_jobs',
validate: {
params: schema.object({
connectorId: schema.string(),
}),
query: schema.object({
from: schema.maybe(schema.number()),
size: schema.maybe(schema.number()),
type: schema.maybe(schema.string()),
}),
},
},
async (context, request, response) => {
const { client } = (await context.core).elasticsearch;
const result = await fetchSyncJobsByConnectorId(
client.asCurrentUser,
request.params.connectorId,
request.query.from || 0,
request.query.size || 20,
(request.query.type as 'content' | 'access_control' | 'all' | undefined) || 'all'
);
return response.ok({
body: result,
headers: { 'content-type': 'application/json' },
});
}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { IndicesIndexState } from '@elastic/elasticsearch/lib/api/types';
import { schema } from '@kbn/config-schema';
import { fetchIndices } from '../lib/indices/fetch_indices';
@ -44,4 +45,41 @@ export const registerIndicesRoutes = ({ router, security }: RouteDependencies) =
});
}
);
router.get(
{
path: '/internal/serverless_search/index_names',
validate: {
query: schema.object({
query: schema.maybe(schema.string()),
}),
},
},
async (context, request, response) => {
const client = (await context.core).elasticsearch.client.asCurrentUser;
const result = await client.indices.get({
expand_wildcards: 'open',
index: request.query.query ? `*${request.query.query}*` : '*',
});
return response.ok({
body: {
index_names: Object.keys(result || {}).filter(
(indexName) => !isHidden(result[indexName]) && !isClosed(result[indexName])
),
},
headers: { 'content-type': 'application/json' },
});
}
);
};
function isHidden(index?: IndicesIndexState): boolean {
return index?.settings?.index?.hidden === true || index?.settings?.index?.hidden === 'true';
}
function isClosed(index?: IndicesIndexState): boolean {
return (
index?.settings?.index?.verified_before_close === true ||
index?.settings?.index?.verified_before_close === 'true'
);
}

View file

@ -12037,13 +12037,8 @@
"xpack.enterpriseSearch.content.index.connector.syncRules.flyout.errorTitle": "{ids} {idsLength, plural, one {règle} many {règles} other {règles}} de synchronisation {idsLength, plural, one {est} many {sont} other {sont}} non valide(s).",
"xpack.enterpriseSearch.content.index.pipelines.copyCustomizeCallout.description": "Votre index utilise notre pipeline d'ingestion par défaut {defaultPipeline}. Copiez ce pipeline dans une configuration spécifique à l'index pour déverrouiller la possibilité de créer des pipelines d'ingestion et d'inférence personnalisés.",
"xpack.enterpriseSearch.content.index.pipelines.ingestFlyout.modalBodyAPIText": "{apiIndex} Les modifications apportées aux paramètres ci-dessous sont uniquement fournies à titre indicatif. Ces paramètres ne seront pas conservés dans votre index ou pipeline.",
"xpack.enterpriseSearch.content.index.syncJobs.documents.volume.aboutLabel": "À propos de {volume}",
"xpack.enterpriseSearch.content.indices.callout.text": "Vos index Elasticsearch sont maintenant au premier plan d'Enterprise Search. Vous pouvez créer des index et lancer directement des expériences de recherche avec ces index. Pour en savoir plus sur l'utilisation des index de Elasticsearch dans Enterprise Search {docLink}",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.description": "D'abord, générez une clé d'API Elasticsearch. Cette clé {apiKeyName} permet d'activer les autorisations de lecture et d'écriture du connecteur pour qu'il puisse indexer les documents dans l'index {indexName} créé. Enregistrez cette clé en lieu sûr, car vous en aurez besoin pour configurer votre connecteur.",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.defaultValue": "Si cette option est laissée vide, la valeur par défaut {defaultValue} sera utilisée.",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.description.firstParagraph": "Maintenant que votre connecteur est déployé, améliorez le client du connecteur pour votre source de données personnalisée. Il y en a plusieurs {link} que vous pouvez personnaliser avec votre propre logique d'implémentation supplémentaire.",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.description.thirdParagraph": "Si vous avez besoin d'aide, vous pouvez toujours ouvrir un {issuesLink} dans le référentiel ou bien poser une question dans notre forum {discussLink}.",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.invalidInteger": "{label} doit être un nombre entier.",
"xpack.enterpriseSearch.content.indices.configurationConnector.connectorPackage.connectorConnected": "Votre connecteur {name} est connecté à Enterprise Search.",
"xpack.enterpriseSearch.content.indices.configurationConnector.connectorPackage.description.secondParagraph": "Le référentiel de connecteurs contient plusieurs {link}. Utilisez notre framework de développement accéléré avec des sources de données personnalisées.",
"xpack.enterpriseSearch.content.indices.configurationConnector.connectorPackage.description.thirdParagraph": "Dans cette étape, vous devez cloner ou dupliquer le référentiel, puis copier la clé d'API et l'ID de connecteur générés au {link} associé. L'ID de connecteur identifie ce connecteur auprès d'Enterprise Search. Le type de service détermine pour quel type de source de données le connecteur est configuré.",
@ -12084,11 +12079,6 @@
"xpack.enterpriseSearch.content.searchIndices.searchIndices.onlySearchOptimized.tooltipContent": "Les index optimisés pour la recherche sont précédés du préfixe {code}. Ils sont gérés par des mécanismes d'ingestion tels que des robots d'indexation, des connecteurs ou des API d'ingestion.",
"xpack.enterpriseSearch.content.settings.description": "Ces paramètres s'appliquent à tous les nouveaux index Elasticsearch créés par des mécanismes d'ingestion Enterprise Search. Pour les index basés sur l'ingestion d'API, n'oubliez pas d'inclure le pipeline lorsque vous ingérez des documents. Ces fonctionnalités sont alimentées par {link}.",
"xpack.enterpriseSearch.content.shared.result.header.metadata.icon.ariaLabel": "Métadonnées pour le document : {id}",
"xpack.enterpriseSearch.content.syncJobs.flyout.canceledDescription": "Synchronisation annulée à {date}",
"xpack.enterpriseSearch.content.syncJobs.flyout.completedDescription": "Terminé à {date}",
"xpack.enterpriseSearch.content.syncJobs.flyout.failureDescription": "Échec de la synchronisation : {error}.",
"xpack.enterpriseSearch.content.syncJobs.flyout.inProgressDescription": "La synchronisation est en cours depuis {duration}.",
"xpack.enterpriseSearch.content.syncJobs.flyout.startedAtDescription": "Démarré à {date}",
"xpack.enterpriseSearch.crawler.action.deleteDomain.confirmationPopupMessage": "Voulez-vous vraiment supprimer le domaine \"{domainUrl}\" et tous ses paramètres ?",
"xpack.enterpriseSearch.crawler.addDomainForm.entryPointLabel": "Le point d'entrée du robot d'indexation a été défini sur {entryPointValue}",
"xpack.enterpriseSearch.crawler.authenticationPanel.emptyPrompt.description": "Cliquer sur {addAuthenticationButtonLabel} afin de fournir les informations d'identification nécessaires pour indexer le contenu protégé",
@ -13165,8 +13155,6 @@
"xpack.enterpriseSearch.betaLabel": "Bêta",
"xpack.enterpriseSearch.connector.connectorTypePanel.title": "Type de connecteur",
"xpack.enterpriseSearch.connector.connectorTypePanel.unknown.label": "Inconnu",
"xpack.enterpriseSearch.connector.documentLevelSecurity.enablePanel.description": "Vous permet de contrôler les documents auxquels peuvent accéder les utilisateurs, selon leurs autorisations. Cela permet de vous assurer que les résultats de recherche ne renvoient que des informations pertinentes et autorisées pour les utilisateurs, selon leurs rôles.",
"xpack.enterpriseSearch.connector.documentLevelSecurity.enablePanel.heading": "Sécurité au niveau du document",
"xpack.enterpriseSearch.connector.ingestionStatus.title": "Statut de l'ingestion",
"xpack.enterpriseSearch.content,overview.documentExample.clientLibraries.label": "Bibliothèques de clients",
"xpack.enterpriseSearch.content.analytics.api.generateAnalyticsApiKeyModal.apiKeyWarning": "Elastic ne stocke pas les clés dAPI. Une fois la clé générée, vous ne pourrez la visualiser qu'une seule fois. Veillez à l'enregistrer dans un endroit sûr. Si vous n'y avez plus accès, vous devrez générer une nouvelle clé dAPI à partir de cet écran.",
@ -13210,15 +13198,6 @@
"xpack.enterpriseSearch.content.crawler.extractionRulesTable.emptyMessageTitle": "Il n'existe aucune règle d'extraction de contenu",
"xpack.enterpriseSearch.content.crawler.siteMaps": "Plans de site",
"xpack.enterpriseSearch.content.description": "Enterprise Search offre un certain nombre de moyens de rendre vos données facilement interrogeables. Vous pouvez choisir entre le robot d'indexation, les indices Elasticsearch, l'API, les téléchargements directs ou les connecteurs tiers.",
"xpack.enterpriseSearch.content.filteringRules.policy.exclude": "Exclure",
"xpack.enterpriseSearch.content.filteringRules.policy.include": "Inclure",
"xpack.enterpriseSearch.content.filteringRules.rules.contains": "Contient",
"xpack.enterpriseSearch.content.filteringRules.rules.endsWith": "Se termine par",
"xpack.enterpriseSearch.content.filteringRules.rules.equals": "Est égal à",
"xpack.enterpriseSearch.content.filteringRules.rules.greaterThan": "Supérieur à",
"xpack.enterpriseSearch.content.filteringRules.rules.lessThan": "Inférieur à",
"xpack.enterpriseSearch.content.filteringRules.rules.regEx": "Expression régulière",
"xpack.enterpriseSearch.content.filteringRules.rules.startsWith": "Commence par",
"xpack.enterpriseSearch.content.index.connector.filtering.successToastRules.title": "Règles de synchronisation mises à jour",
"xpack.enterpriseSearch.content.index.connector.filteringRules.regExError": "La valeur doit être une expression régulière",
"xpack.enterpriseSearch.content.index.connector.syncRules.advancedFiltersDescription": "Ces règles s'appliquent avant l'obtention des données auprès de la source de données.",
@ -13285,48 +13264,12 @@
"xpack.enterpriseSearch.content.index.syncButton.label": "Sync",
"xpack.enterpriseSearch.content.index.syncButton.syncing.label": "Synchronisation en cours",
"xpack.enterpriseSearch.content.index.syncButton.waitingForSync.label": "En attente de la synchronisation",
"xpack.enterpriseSearch.content.index.syncJobs.actions.viewJob.caption": "Afficher cette tâche de synchronisation",
"xpack.enterpriseSearch.content.index.syncJobs.actions.viewJob.title": "Afficher cette tâche de synchronisation",
"xpack.enterpriseSearch.content.index.syncJobs.documents.added": "Ajouté",
"xpack.enterpriseSearch.content.index.syncJobs.documents.removed": "Retiré",
"xpack.enterpriseSearch.content.index.syncJobs.documents.title": "Documents",
"xpack.enterpriseSearch.content.index.syncJobs.documents.total": "Total",
"xpack.enterpriseSearch.content.index.syncJobs.documents.value": "Valeur",
"xpack.enterpriseSearch.content.index.syncJobs.documents.volume": "Volume",
"xpack.enterpriseSearch.content.index.syncJobs.documents.volume.lessThanOneMBLabel": "Inférieur à 1 Mo",
"xpack.enterpriseSearch.content.index.syncJobs.events.cancelationRequested": "Annulation demandée",
"xpack.enterpriseSearch.content.index.syncJobs.events.canceled": "Annulé",
"xpack.enterpriseSearch.content.index.syncJobs.events.completed": "Terminé",
"xpack.enterpriseSearch.content.index.syncJobs.events.lastUpdated": "Dernière mise à jour",
"xpack.enterpriseSearch.content.index.syncJobs.events.state": "État",
"xpack.enterpriseSearch.content.index.syncJobs.events.syncRequestedManually": "Synchronisation demandée manuellement",
"xpack.enterpriseSearch.content.index.syncJobs.events.syncRequestedScheduled": "Synchronisation demandée par planification",
"xpack.enterpriseSearch.content.index.syncJobs.events.syncStarted": "Synchronisation démarrée",
"xpack.enterpriseSearch.content.index.syncJobs.events.time": "Heure",
"xpack.enterpriseSearch.content.index.syncJobs.events.title": "Événements",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.extractBinaryContent": "Extraire le contenu binaire",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.name": "Nom du pipeline",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.reduceWhitespace": "Réduire l'espace",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.runMlInference": "Inférence de Machine Learning",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.setting": "Paramètre de pipeline",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.title": "Pipeline",
"xpack.enterpriseSearch.content.index.syncJobs.syncRulesAdvancedTitle": "Règles de synchronisation avancées",
"xpack.enterpriseSearch.content.index.syncJobs.syncRulesTitle": "Règles de synchronisation",
"xpack.enterpriseSearch.content.indices.callout.docLink": "lire la documentation",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.button.label": "Générer une clé API",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.confirmModal.cancelButton.label": "Annuler",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.confirmModal.confirmButton.label": "Générer une clé API",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.confirmModal.description": "La génération d'une nouvelle clé dAPI invalidera la clé précédente. Êtes-vous sûr de vouloir générer une nouvelle clé dAPI ? Cette action ne peut pas être annulée.",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.confirmModal.title": "Générer une clé d'API Elasticsearch",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.cancelEditingButton.title": "Annuler",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.connectorClientLink": "connecteurs",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.description.secondParagraph": "Bien que les clients connecteurs dans le référentiel soient créés dans Ruby, il n'y a aucune limitation technique à n'utiliser que Ruby. Créez un client connecteur avec la technologie qui convient le mieux à vos compétences.",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.discussLink": "Discuter",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.editButton.title": "Modifier la configuration",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.error.title": "Erreur de connecteur",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.issuesLink": "problème",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.submitButton.title": "Enregistrer la configuration",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.warning.title": "Ce connecteur est lié à votre index Elastic",
"xpack.enterpriseSearch.content.indices.configurationConnector.configuration.successToast.title": "Configuration mise à jour",
"xpack.enterpriseSearch.content.indices.configurationConnector.connectorPackage.clientExamplesLink": "exemples de clients connecteurs",
"xpack.enterpriseSearch.content.indices.configurationConnector.connectorPackage.configurationFileLink": "fichier de configuration",
@ -13372,7 +13315,6 @@
"xpack.enterpriseSearch.content.indices.configurationConnector.support.readme.label": "Fichier readme du connecteur",
"xpack.enterpriseSearch.content.indices.configurationConnector.support.title": "Support technique et documentation",
"xpack.enterpriseSearch.content.indices.configurationConnector.support.viewDocumentation.label": "Afficher la documentation",
"xpack.enterpriseSearch.content.indices.configurationConnector.warning.description": "Si vous synchronisez au moins un document avant d'avoir finalisé votre client connecteur, vous devrez recréer votre index de recherche.",
"xpack.enterpriseSearch.content.indices.connector.syncRules.advancedRules.error": "Le format JSON n'est pas valide",
"xpack.enterpriseSearch.content.indices.connector.syncRules.advancedRules.title": "Règles avancées",
"xpack.enterpriseSearch.content.indices.connectorScheduling.accordion.accessControlSync.description": "Planifiez des synchronisations de contrôles daccès pour garder les mappings dautorisation à jour.",
@ -13727,9 +13669,7 @@
"xpack.enterpriseSearch.content.searchIndices.actions.columnTitle": "Actions",
"xpack.enterpriseSearch.content.searchIndices.actions.deleteIndex.title": "Supprimer cet index",
"xpack.enterpriseSearch.content.searchIndices.actions.viewIndex.title": "Afficher cet index",
"xpack.enterpriseSearch.content.searchIndices.addedDocs.columnTitle": "Documents ajoutés",
"xpack.enterpriseSearch.content.searchIndices.create.buttonTitle": "Créer un nouvel index",
"xpack.enterpriseSearch.content.searchIndices.deletedDocs.columnTitle": "Documents supprimés",
"xpack.enterpriseSearch.content.searchIndices.deleteModal.cancelButton.title": "Annuler",
"xpack.enterpriseSearch.content.searchIndices.deleteModal.closeButton.title": "Fermer",
"xpack.enterpriseSearch.content.searchIndices.deleteModal.confirmButton.title": "Supprimer l'index",
@ -13738,7 +13678,6 @@
"xpack.enterpriseSearch.content.searchIndices.deleteModal.syncsWarning.title": "Synchronisations en cours",
"xpack.enterpriseSearch.content.searchIndices.docsCount.columnTitle": "Nombre de documents",
"xpack.enterpriseSearch.content.searchIndices.health.columnTitle": "Intégrité des index",
"xpack.enterpriseSearch.content.searchIndices.identitySync.columnTitle": "Identités synchronisées",
"xpack.enterpriseSearch.content.searchIndices.ingestionMethod.api": "API",
"xpack.enterpriseSearch.content.searchIndices.ingestionMethod.columnTitle": "Méthodes d'ingestion",
"xpack.enterpriseSearch.content.searchIndices.ingestionMethod.connector": "Connecteur",
@ -13769,8 +13708,6 @@
"xpack.enterpriseSearch.content.searchIndices.searchIndices.searchBar.placeHolder": "Filtrer les index Elasticsearch",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.stepsTitle": "Créer de belles expériences de recherche avec Enterprise Search",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.tableTitle": "Index disponibles",
"xpack.enterpriseSearch.content.searchIndices.syncJobType.columnTitle": "Type de synchronisation de contenu",
"xpack.enterpriseSearch.content.searchIndices.syncStatus.columnTitle": "Statut",
"xpack.enterpriseSearch.content.settings.breadcrumb": "Paramètres",
"xpack.enterpriseSearch.content.settings.contactExtraction.label": "Extraction du contenu",
"xpack.enterpriseSearch.content.settings.contentExtraction.description": "Extrayez du contenu interrogeable de fichiers binaires, tels que des PDF et des documents Word.",
@ -13805,21 +13742,10 @@
"xpack.enterpriseSearch.content.supportedLanguages.spanishLabel": "Espagnol",
"xpack.enterpriseSearch.content.supportedLanguages.thaiLabel": "Thaï",
"xpack.enterpriseSearch.content.supportedLanguages.universalLabel": "Universel",
"xpack.enterpriseSearch.content.syncJobs.flyout.canceledTitle": "Synchronisation annulée",
"xpack.enterpriseSearch.content.syncJobs.flyout.completedTitle": "Synchronisation terminée",
"xpack.enterpriseSearch.content.syncJobs.flyout.failureTitle": "Échec de la synchronisation",
"xpack.enterpriseSearch.content.syncJobs.flyout.inProgressTitle": "En cours",
"xpack.enterpriseSearch.content.syncJobs.flyout.sync": "Sync",
"xpack.enterpriseSearch.content.syncJobs.flyout.sync.id": "ID",
"xpack.enterpriseSearch.content.syncJobs.flyout.syncStartedManually": "Synchronisation démarrée manuellement",
"xpack.enterpriseSearch.content.syncJobs.flyout.syncStartedScheduled": "Synchronisation démarrée par planification",
"xpack.enterpriseSearch.content.syncJobs.flyout.title": "Log d'événements",
"xpack.enterpriseSearch.content.syncJobs.lastSync.columnTitle": "Dernière synchronisation",
"xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.accessControl.label": "Synchronisations de contrôle d'accès",
"xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.content.label": "Synchronisations de contenu",
"xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.legend": "Sélectionnez le type de tâche de synchronisation à afficher.",
"xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.name": "Type de tâche de synchronisation",
"xpack.enterpriseSearch.content.syncJobs.syncDuration.columnTitle": "Durée de synchronisation",
"xpack.enterpriseSearch.crawler.addDomainFlyout.description": "Vous pouvez ajouter plusieurs domaines au robot d'indexation de cet index. Ajoutez un autre domaine ici et modifiez les points d'entrée et les règles d'indexation à partir de la page \"Gérer\".",
"xpack.enterpriseSearch.crawler.addDomainFlyout.openButtonLabel": "Ajouter un domaine",
"xpack.enterpriseSearch.crawler.addDomainFlyout.title": "Ajouter un nouveau domaine",

View file

@ -12051,13 +12051,8 @@
"xpack.enterpriseSearch.content.index.connector.syncRules.flyout.errorTitle": "同期{idsLength, plural, other {ルール}}{ids}{idsLength, plural, other {あります}}無効です。",
"xpack.enterpriseSearch.content.index.pipelines.copyCustomizeCallout.description": "インデックスはデフォルトインジェストパイプライン\"{defaultPipeline}\"を使用しています。パイプラインをインデックス固有の構成にコピーし、カスタムインジェストと推論パイプラインを作成できるようにします。",
"xpack.enterpriseSearch.content.index.pipelines.ingestFlyout.modalBodyAPIText": "{apiIndex}以下の設定に行われた変更は参照専用です。これらの設定は、インデックスまたはパイプラインまで永続しません。",
"xpack.enterpriseSearch.content.index.syncJobs.documents.volume.aboutLabel": "{volume}の概要",
"xpack.enterpriseSearch.content.indices.callout.text": "Elasticsearchインデックスは、現在、エンタープライズ サーチの中心です。直接そのインデックスを使用して、新しいインデックスを作成し、検索エクスペリエンスを構築できます。エンタープライズ サーチでのElasticsearchの使用方法の詳細については、{docLink}をご覧ください",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.description": "まず、Elasticsearch APIキーを生成します。この{apiKeyName}は、コネクターがドキュメントを作成された{indexName}インデックスにインデックスするための読み書き権限を有効にします。キーは安全な場所に保管してください。コネクターを構成するときに必要になります。",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.defaultValue": "空にする場合は、デフォルト値の{defaultValue}が使用されます。",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.description.firstParagraph": "コネクターがデプロイされたので、カスタムデータソースのコネクタークライアントを強化します。独自の追加の実装ロジックを使用してカスタマイズ可能な複数の{link}があります。",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.description.thirdParagraph": "サポートが必要な場合は、いつでもリポジトリで{issuesLink}をオープンするか、{discussLink}フォーラムで質問することができます。",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.invalidInteger": "{label}は整数でなければなりません。",
"xpack.enterpriseSearch.content.indices.configurationConnector.connectorPackage.connectorConnected": "コネクター{name}は、正常にエンタープライズ サーチに接続されました。",
"xpack.enterpriseSearch.content.indices.configurationConnector.connectorPackage.description.secondParagraph": "コネクターリポジトリには複数の{link}が含まれています。カスタムデータソースに対して高速開発のフレームワークを使用します。",
"xpack.enterpriseSearch.content.indices.configurationConnector.connectorPackage.description.thirdParagraph": "このステップでは、リポジトリを複製またはフォークし、生成されたAPIキーとコネクターIDを、関連付けられた{link}にコピーする必要があります。コネクターIDは、エンタープライズ サーチに対するこのコネクターを特定します。サービスタイプは、コネクターが構成されているデータソースのタイプを決定します。",
@ -12098,11 +12093,6 @@
"xpack.enterpriseSearch.content.searchIndices.searchIndices.onlySearchOptimized.tooltipContent": "検索用に最適化されたインデックスには、{code}がプレフィックスとして付けられます。これらは、クローラー、コネクター、インジェストAPIなどのインジェストメカニズムによって管理されます。",
"xpack.enterpriseSearch.content.settings.description": "これらの設定は、エンタープライズ サーチインジェストメカニズムで作成されたすべての新しいElasticsearchインデックスに適用されます。APIインジェストベースのインデックスの場合は、ドキュメントをインジェストするときに、必ずパイプラインを含めてください。これらの機能は{link}によって実現されます。",
"xpack.enterpriseSearch.content.shared.result.header.metadata.icon.ariaLabel": "ドキュメント{id}のメタデータ",
"xpack.enterpriseSearch.content.syncJobs.flyout.canceledDescription": "{date}に同期がキャンセルされました",
"xpack.enterpriseSearch.content.syncJobs.flyout.completedDescription": "完了日時:{date}",
"xpack.enterpriseSearch.content.syncJobs.flyout.failureDescription": "同期失敗:{error}",
"xpack.enterpriseSearch.content.syncJobs.flyout.inProgressDescription": "同期は{duration}実行中です。",
"xpack.enterpriseSearch.content.syncJobs.flyout.startedAtDescription": "{date}に開始しました",
"xpack.enterpriseSearch.crawler.action.deleteDomain.confirmationPopupMessage": "ドメイン\"{domainUrl}\"とすべての設定を削除しますか?",
"xpack.enterpriseSearch.crawler.addDomainForm.entryPointLabel": "Webクローラーエントリポイントが{entryPointValue}として設定されました",
"xpack.enterpriseSearch.crawler.authenticationPanel.emptyPrompt.description": "{addAuthenticationButtonLabel}をクリックすると、保護されたコンテンツのクローリングに必要な資格情報を提供します",
@ -13179,8 +13169,6 @@
"xpack.enterpriseSearch.betaLabel": "ベータ",
"xpack.enterpriseSearch.connector.connectorTypePanel.title": "コネクタータイプ",
"xpack.enterpriseSearch.connector.connectorTypePanel.unknown.label": "不明",
"xpack.enterpriseSearch.connector.documentLevelSecurity.enablePanel.description": "権限に基づいて、ユーザーがアクセスできるドキュメントを制御できます。これにより、検索結果は、ユーザーの役割に基づき、関連性のある許可された情報のみを返すようになります。",
"xpack.enterpriseSearch.connector.documentLevelSecurity.enablePanel.heading": "ドキュメントレベルのセキュリティ",
"xpack.enterpriseSearch.connector.ingestionStatus.title": "インジェスチョンステータス",
"xpack.enterpriseSearch.content,overview.documentExample.clientLibraries.label": "クライアントライブラリ",
"xpack.enterpriseSearch.content.analytics.api.generateAnalyticsApiKeyModal.apiKeyWarning": "ElasticはAPIキーを保存しません。生成後は、1回だけキーを表示できます。必ず安全に保管してください。アクセスできなくなった場合は、この画面から新しいAPIキーを生成する必要があります。",
@ -13224,15 +13212,6 @@
"xpack.enterpriseSearch.content.crawler.extractionRulesTable.emptyMessageTitle": "コンテンツ抽出ルールがありません",
"xpack.enterpriseSearch.content.crawler.siteMaps": "サイトマップ",
"xpack.enterpriseSearch.content.description": "エンタープライズ サーチでは、さまざまな方法で簡単にデータを検索可能にできます。Webクローラー、Elasticsearchインデックス、API、直接アップロード、サードパーティコネクターから選択します。",
"xpack.enterpriseSearch.content.filteringRules.policy.exclude": "除外",
"xpack.enterpriseSearch.content.filteringRules.policy.include": "含める",
"xpack.enterpriseSearch.content.filteringRules.rules.contains": "を含む",
"xpack.enterpriseSearch.content.filteringRules.rules.endsWith": "で終了",
"xpack.enterpriseSearch.content.filteringRules.rules.equals": "一致する",
"xpack.enterpriseSearch.content.filteringRules.rules.greaterThan": "より大きい",
"xpack.enterpriseSearch.content.filteringRules.rules.lessThan": "より小さい",
"xpack.enterpriseSearch.content.filteringRules.rules.regEx": "正規表現",
"xpack.enterpriseSearch.content.filteringRules.rules.startsWith": "で始まる",
"xpack.enterpriseSearch.content.index.connector.filtering.successToastRules.title": "同期ルールが更新されました",
"xpack.enterpriseSearch.content.index.connector.filteringRules.regExError": "値は正規表現にしてください",
"xpack.enterpriseSearch.content.index.connector.syncRules.advancedFiltersDescription": "これらのルールは、データがデータソースから取得される前に適用されます。",
@ -13299,48 +13278,12 @@
"xpack.enterpriseSearch.content.index.syncButton.label": "同期",
"xpack.enterpriseSearch.content.index.syncButton.syncing.label": "同期中",
"xpack.enterpriseSearch.content.index.syncButton.waitingForSync.label": "同期を待機しています",
"xpack.enterpriseSearch.content.index.syncJobs.actions.viewJob.caption": "この同期ジョブを表示",
"xpack.enterpriseSearch.content.index.syncJobs.actions.viewJob.title": "この同期ジョブを表示",
"xpack.enterpriseSearch.content.index.syncJobs.documents.added": "追加",
"xpack.enterpriseSearch.content.index.syncJobs.documents.removed": "削除しました",
"xpack.enterpriseSearch.content.index.syncJobs.documents.title": "ドキュメント",
"xpack.enterpriseSearch.content.index.syncJobs.documents.total": "合計",
"xpack.enterpriseSearch.content.index.syncJobs.documents.value": "値",
"xpack.enterpriseSearch.content.index.syncJobs.documents.volume": "量",
"xpack.enterpriseSearch.content.index.syncJobs.documents.volume.lessThanOneMBLabel": "1mb未満",
"xpack.enterpriseSearch.content.index.syncJobs.events.cancelationRequested": "キャンセルがリクエストされました",
"xpack.enterpriseSearch.content.index.syncJobs.events.canceled": "キャンセル",
"xpack.enterpriseSearch.content.index.syncJobs.events.completed": "完了",
"xpack.enterpriseSearch.content.index.syncJobs.events.lastUpdated": "最終更新",
"xpack.enterpriseSearch.content.index.syncJobs.events.state": "ステータス",
"xpack.enterpriseSearch.content.index.syncJobs.events.syncRequestedManually": "同期が手動でリクエストされました",
"xpack.enterpriseSearch.content.index.syncJobs.events.syncRequestedScheduled": "同期がスケジュールでリクエストされました",
"xpack.enterpriseSearch.content.index.syncJobs.events.syncStarted": "同期が開始しました",
"xpack.enterpriseSearch.content.index.syncJobs.events.time": "時間",
"xpack.enterpriseSearch.content.index.syncJobs.events.title": "イベント",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.extractBinaryContent": "バイナリコンテンツを抽出",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.name": "パイプライン名",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.reduceWhitespace": "空白の削除",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.runMlInference": "機械学習推論",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.setting": "パイプライン設定",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.title": "パイプライン",
"xpack.enterpriseSearch.content.index.syncJobs.syncRulesAdvancedTitle": "詳細同期ルール",
"xpack.enterpriseSearch.content.index.syncJobs.syncRulesTitle": "同期ルール",
"xpack.enterpriseSearch.content.indices.callout.docLink": "ドキュメントを読む",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.button.label": "APIキーを生成",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.confirmModal.cancelButton.label": "キャンセル",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.confirmModal.confirmButton.label": "APIキーを生成",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.confirmModal.description": "新しいAPIキーを生成すると、前のキーが無効になります。新しいAPIキーを生成しますかこの操作は元に戻せません。",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.confirmModal.title": "Elasticsearch APIキーを生成",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.cancelEditingButton.title": "キャンセル",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.connectorClientLink": "コネクター",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.description.secondParagraph": "リポジトリのコネクタークライアントはRubyで構築されていますが、技術的な制限事項はないため、Ruby以外も使用できます。ご自身のスキルセットに最適な技術でコネクタークライアントを構築してください。",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.discussLink": "ディスカッション",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.editButton.title": "構成の編集",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.error.title": "コネクターエラー",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.issuesLink": "問題",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.submitButton.title": "構成を保存",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.warning.title": "このコネクターは、Elasticインデックスに関連付けられています",
"xpack.enterpriseSearch.content.indices.configurationConnector.configuration.successToast.title": "構成が更新されました",
"xpack.enterpriseSearch.content.indices.configurationConnector.connectorPackage.clientExamplesLink": "コネクタークライアントの例",
"xpack.enterpriseSearch.content.indices.configurationConnector.connectorPackage.configurationFileLink": "構成ファイル",
@ -13386,7 +13329,6 @@
"xpack.enterpriseSearch.content.indices.configurationConnector.support.readme.label": "コネクターReadme",
"xpack.enterpriseSearch.content.indices.configurationConnector.support.title": "サポートとドキュメント",
"xpack.enterpriseSearch.content.indices.configurationConnector.support.viewDocumentation.label": "ドキュメンテーションを表示",
"xpack.enterpriseSearch.content.indices.configurationConnector.warning.description": "コネクタークライアントを確定する前に、1つ以上のドキュメントを同期した場合、検索インデックスを再作成する必要があります。",
"xpack.enterpriseSearch.content.indices.connector.syncRules.advancedRules.error": "JSON形式が無効です",
"xpack.enterpriseSearch.content.indices.connector.syncRules.advancedRules.title": "詳細ルール",
"xpack.enterpriseSearch.content.indices.connectorScheduling.accordion.accessControlSync.description": "アクセス制御同期をスケジュールし、権限マッピングを常に最新の状態に保ちます。",
@ -13741,9 +13683,7 @@
"xpack.enterpriseSearch.content.searchIndices.actions.columnTitle": "アクション",
"xpack.enterpriseSearch.content.searchIndices.actions.deleteIndex.title": "このインデックスを削除",
"xpack.enterpriseSearch.content.searchIndices.actions.viewIndex.title": "このインデックスを表示",
"xpack.enterpriseSearch.content.searchIndices.addedDocs.columnTitle": "ドキュメントが追加されました",
"xpack.enterpriseSearch.content.searchIndices.create.buttonTitle": "新しいインデックスを作成",
"xpack.enterpriseSearch.content.searchIndices.deletedDocs.columnTitle": "ドキュメントが削除されました",
"xpack.enterpriseSearch.content.searchIndices.deleteModal.cancelButton.title": "キャンセル",
"xpack.enterpriseSearch.content.searchIndices.deleteModal.closeButton.title": "閉じる",
"xpack.enterpriseSearch.content.searchIndices.deleteModal.confirmButton.title": "インデックスの削除",
@ -13752,7 +13692,6 @@
"xpack.enterpriseSearch.content.searchIndices.deleteModal.syncsWarning.title": "同期実行中",
"xpack.enterpriseSearch.content.searchIndices.docsCount.columnTitle": "ドキュメント数",
"xpack.enterpriseSearch.content.searchIndices.health.columnTitle": "インデックス正常性",
"xpack.enterpriseSearch.content.searchIndices.identitySync.columnTitle": "IDが同期されました",
"xpack.enterpriseSearch.content.searchIndices.ingestionMethod.api": "API",
"xpack.enterpriseSearch.content.searchIndices.ingestionMethod.columnTitle": "インジェスチョン方法",
"xpack.enterpriseSearch.content.searchIndices.ingestionMethod.connector": "コネクター",
@ -13783,8 +13722,6 @@
"xpack.enterpriseSearch.content.searchIndices.searchIndices.searchBar.placeHolder": "Elasticsearchインデックスをフィルター",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.stepsTitle": "エンタープライズ サーチで構築する優れた検索エクスペリエンス",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.tableTitle": "利用可能なインデックス",
"xpack.enterpriseSearch.content.searchIndices.syncJobType.columnTitle": "コンテンツ同期タイプ",
"xpack.enterpriseSearch.content.searchIndices.syncStatus.columnTitle": "ステータス",
"xpack.enterpriseSearch.content.settings.breadcrumb": "設定",
"xpack.enterpriseSearch.content.settings.contactExtraction.label": "コンテンツ抽出",
"xpack.enterpriseSearch.content.settings.contentExtraction.description": "PDFやWordドキュメントなどの検索可能なコンテンツのみをバイナリファイルから抽出します。",
@ -13819,21 +13756,10 @@
"xpack.enterpriseSearch.content.supportedLanguages.spanishLabel": "スペイン語",
"xpack.enterpriseSearch.content.supportedLanguages.thaiLabel": "タイ語",
"xpack.enterpriseSearch.content.supportedLanguages.universalLabel": "ユニバーサル",
"xpack.enterpriseSearch.content.syncJobs.flyout.canceledTitle": "同期がキャンセルされました",
"xpack.enterpriseSearch.content.syncJobs.flyout.completedTitle": "同期完了",
"xpack.enterpriseSearch.content.syncJobs.flyout.failureTitle": "同期失敗",
"xpack.enterpriseSearch.content.syncJobs.flyout.inProgressTitle": "進行中",
"xpack.enterpriseSearch.content.syncJobs.flyout.sync": "同期",
"xpack.enterpriseSearch.content.syncJobs.flyout.sync.id": "ID",
"xpack.enterpriseSearch.content.syncJobs.flyout.syncStartedManually": "同期は手動で開始しました",
"xpack.enterpriseSearch.content.syncJobs.flyout.syncStartedScheduled": "同期はスケジュールで開始しました",
"xpack.enterpriseSearch.content.syncJobs.flyout.title": "イベントログ",
"xpack.enterpriseSearch.content.syncJobs.lastSync.columnTitle": "前回の同期",
"xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.accessControl.label": "アクセス制御同期",
"xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.content.label": "コンテンツ同期",
"xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.legend": "表示する同期ジョブタイプを選択します。",
"xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.name": "同期ジョブタイプ",
"xpack.enterpriseSearch.content.syncJobs.syncDuration.columnTitle": "同期時間",
"xpack.enterpriseSearch.crawler.addDomainFlyout.description": "複数のドメインをこのインデックスのWebクローラーに追加できます。ここで別のドメインを追加して、[管理]ページからエントリポイントとクロールルールを変更します。",
"xpack.enterpriseSearch.crawler.addDomainFlyout.openButtonLabel": "ドメインを追加",
"xpack.enterpriseSearch.crawler.addDomainFlyout.title": "新しいドメインを追加",

View file

@ -12051,13 +12051,8 @@
"xpack.enterpriseSearch.content.index.connector.syncRules.flyout.errorTitle": "同步{idsLength, plural, other {规则}} {ids}{idsLength, plural, other {有}}无效。",
"xpack.enterpriseSearch.content.index.pipelines.copyCustomizeCallout.description": "您的索引正使用默认采集管道 {defaultPipeline}。将该管道复制到特定于索引的配置中,以解锁创建定制采集和推理管道的功能。",
"xpack.enterpriseSearch.content.index.pipelines.ingestFlyout.modalBodyAPIText": "{apiIndex}对以下设置所做的更改仅供参考。这些设置不会持续用于您的索引或管道。",
"xpack.enterpriseSearch.content.index.syncJobs.documents.volume.aboutLabel": "关于 {volume}",
"xpack.enterpriseSearch.content.indices.callout.text": "您的 Elasticsearch 索引如今在 Enterprise Search 中位于前排和中心位置。您可以创建新索引,直接通过它们构建搜索体验。有关如何在 Enterprise Search 中使用 Elasticsearch 索引的详情,{docLink}",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.description": "首先,生成一个 Elasticsearch API 密钥。此 {apiKeyName} 密钥将为连接器启用读取和写入权限,以便将文档索引到已创建的 {indexName} 索引。请将该密钥保存到安全位置,因为您需要它来配置连接器。",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.defaultValue": "如果留空,将使用默认值 {defaultValue}。",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.description.firstParagraph": "部署连接器后,请为您的定制数据源增强连接器客户端。您可以通过自己的其他实施逻辑定制几个 {link}。",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.description.thirdParagraph": "如果需要帮助,您始终可以在存储库中打开 {issuesLink},或在我们的 {discussLink} 论坛中提出问题。",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.invalidInteger": "{label} 必须为整数。",
"xpack.enterpriseSearch.content.indices.configurationConnector.connectorPackage.connectorConnected": "您的连接器 {name} 已成功连接到 Enterprise Search。",
"xpack.enterpriseSearch.content.indices.configurationConnector.connectorPackage.description.secondParagraph": "连接器存储库包含几个 {link}。使用我们的框架针对定制数据源进行加速开发。",
"xpack.enterpriseSearch.content.indices.configurationConnector.connectorPackage.description.thirdParagraph": "在此步骤中,您需要克隆或分叉存储库,然后将生成的 API 密钥和连接器 ID 复制到关联的 {link}。连接器 ID 会将此连接器标识到 Enterprise Search。此服务类型将决定要将连接器配置用于哪些类型的数据源。",
@ -12098,11 +12093,6 @@
"xpack.enterpriseSearch.content.searchIndices.searchIndices.onlySearchOptimized.tooltipContent": "搜索优化索引以 {code} 为前缀。它们由网络爬虫、连接器或采集 API 等采集机制进行管理。",
"xpack.enterpriseSearch.content.settings.description": "这些设置适用于由 Enterprise Search 采集机制创建的所有新 Elasticsearch 索引。对于基于 API 采集的索引,在采集文档时请记得包括管道。这些功能由 {link} 提供支持。",
"xpack.enterpriseSearch.content.shared.result.header.metadata.icon.ariaLabel": "以下文档的元数据:{id}",
"xpack.enterpriseSearch.content.syncJobs.flyout.canceledDescription": "同步已于 {date}取消",
"xpack.enterpriseSearch.content.syncJobs.flyout.completedDescription": "已于 {date} 完成",
"xpack.enterpriseSearch.content.syncJobs.flyout.failureDescription": "同步失败:{error}。",
"xpack.enterpriseSearch.content.syncJobs.flyout.inProgressDescription": "同步已运行 {duration}。",
"xpack.enterpriseSearch.content.syncJobs.flyout.startedAtDescription": "已于 {date}启动",
"xpack.enterpriseSearch.crawler.action.deleteDomain.confirmationPopupMessage": "确定要移除域“{domainUrl}”及其所有设置?",
"xpack.enterpriseSearch.crawler.addDomainForm.entryPointLabel": "网络爬虫入口点已设置为 {entryPointValue}",
"xpack.enterpriseSearch.crawler.authenticationPanel.emptyPrompt.description": "单击 {addAuthenticationButtonLabel} 以提供爬网受保护内容所需的凭据",
@ -13179,8 +13169,6 @@
"xpack.enterpriseSearch.betaLabel": "公测版",
"xpack.enterpriseSearch.connector.connectorTypePanel.title": "连接器类型",
"xpack.enterpriseSearch.connector.connectorTypePanel.unknown.label": "未知",
"xpack.enterpriseSearch.connector.documentLevelSecurity.enablePanel.description": "允许您基于用户的权限,控制他们可以访问的文档。这确保了搜索结果将基于用户角色,仅为其返回相关授权信息。",
"xpack.enterpriseSearch.connector.documentLevelSecurity.enablePanel.heading": "文档级别安全性",
"xpack.enterpriseSearch.connector.ingestionStatus.title": "采集状态",
"xpack.enterpriseSearch.content,overview.documentExample.clientLibraries.label": "客户端库",
"xpack.enterpriseSearch.content.analytics.api.generateAnalyticsApiKeyModal.apiKeyWarning": "Elastic 不会存储 API 密钥。一旦生成,您只能查看密钥一次。请确保将其保存在某个安全位置。如果失去它的访问权限,您需要从此屏幕生成新的 API 密钥。",
@ -13224,15 +13212,6 @@
"xpack.enterpriseSearch.content.crawler.extractionRulesTable.emptyMessageTitle": "没有内容提取规则",
"xpack.enterpriseSearch.content.crawler.siteMaps": "站点地图",
"xpack.enterpriseSearch.content.description": "Enterprise Search 提供了各种方法以便您轻松搜索数据。从网络爬虫、Elasticsearch 索引、API、直接上传或第三方连接器中选择。",
"xpack.enterpriseSearch.content.filteringRules.policy.exclude": "排除",
"xpack.enterpriseSearch.content.filteringRules.policy.include": "包括",
"xpack.enterpriseSearch.content.filteringRules.rules.contains": "Contains",
"xpack.enterpriseSearch.content.filteringRules.rules.endsWith": "结束于",
"xpack.enterpriseSearch.content.filteringRules.rules.equals": "等于",
"xpack.enterpriseSearch.content.filteringRules.rules.greaterThan": "大于",
"xpack.enterpriseSearch.content.filteringRules.rules.lessThan": "小于",
"xpack.enterpriseSearch.content.filteringRules.rules.regEx": "正则表达式",
"xpack.enterpriseSearch.content.filteringRules.rules.startsWith": "开头为",
"xpack.enterpriseSearch.content.index.connector.filtering.successToastRules.title": "同步规则已更新",
"xpack.enterpriseSearch.content.index.connector.filteringRules.regExError": "值应为正则表达式",
"xpack.enterpriseSearch.content.index.connector.syncRules.advancedFiltersDescription": "从数据源获取数据之前,这些规则适用。",
@ -13299,48 +13278,12 @@
"xpack.enterpriseSearch.content.index.syncButton.label": "同步",
"xpack.enterpriseSearch.content.index.syncButton.syncing.label": "正在同步",
"xpack.enterpriseSearch.content.index.syncButton.waitingForSync.label": "等待同步",
"xpack.enterpriseSearch.content.index.syncJobs.actions.viewJob.caption": "查看此同步作业",
"xpack.enterpriseSearch.content.index.syncJobs.actions.viewJob.title": "查看此同步作业",
"xpack.enterpriseSearch.content.index.syncJobs.documents.added": "已添加",
"xpack.enterpriseSearch.content.index.syncJobs.documents.removed": "已移除",
"xpack.enterpriseSearch.content.index.syncJobs.documents.title": "文档",
"xpack.enterpriseSearch.content.index.syncJobs.documents.total": "合计",
"xpack.enterpriseSearch.content.index.syncJobs.documents.value": "值",
"xpack.enterpriseSearch.content.index.syncJobs.documents.volume": "卷",
"xpack.enterpriseSearch.content.index.syncJobs.documents.volume.lessThanOneMBLabel": "小于 1mb",
"xpack.enterpriseSearch.content.index.syncJobs.events.cancelationRequested": "已请求取消",
"xpack.enterpriseSearch.content.index.syncJobs.events.canceled": "已取消",
"xpack.enterpriseSearch.content.index.syncJobs.events.completed": "已完成",
"xpack.enterpriseSearch.content.index.syncJobs.events.lastUpdated": "上次更新时间",
"xpack.enterpriseSearch.content.index.syncJobs.events.state": "状态",
"xpack.enterpriseSearch.content.index.syncJobs.events.syncRequestedManually": "已手动请求同步",
"xpack.enterpriseSearch.content.index.syncJobs.events.syncRequestedScheduled": "已按计划请求同步",
"xpack.enterpriseSearch.content.index.syncJobs.events.syncStarted": "已启动同步",
"xpack.enterpriseSearch.content.index.syncJobs.events.time": "时间",
"xpack.enterpriseSearch.content.index.syncJobs.events.title": "事件",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.extractBinaryContent": "提取二进制内容",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.name": "管道名称",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.reduceWhitespace": "减少空白",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.runMlInference": "Machine Learning 推理",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.setting": "管道设置",
"xpack.enterpriseSearch.content.index.syncJobs.pipeline.title": "管道",
"xpack.enterpriseSearch.content.index.syncJobs.syncRulesAdvancedTitle": "高级同步规则",
"xpack.enterpriseSearch.content.index.syncJobs.syncRulesTitle": "同步规则",
"xpack.enterpriseSearch.content.indices.callout.docLink": "阅读文档",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.button.label": "生成 API 密钥",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.confirmModal.cancelButton.label": "取消",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.confirmModal.confirmButton.label": "生成 API 密钥",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.confirmModal.description": "生成新的 API 密钥将使之前的密钥失效。是否确定要生成新的 API 密钥?此操作无法撤消。",
"xpack.enterpriseSearch.content.indices.configurationConnector.apiKey.confirmModal.title": "生成 Elasticsearch API 密钥",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.cancelEditingButton.title": "取消",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.connectorClientLink": "连接器",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.description.secondParagraph": "虽然存储库中的连接器客户端是在 Ruby 中构建的,但仅使用 Ruby 并不存在技术限制。通过最适合您技能集的技术来构建连接器客户端。",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.discussLink": "讨论",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.editButton.title": "编辑配置",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.error.title": "连接器错误",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.issuesLink": "问题",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.submitButton.title": "保存配置",
"xpack.enterpriseSearch.content.indices.configurationConnector.config.warning.title": "此连接器将绑定到您的 Elastic 索引",
"xpack.enterpriseSearch.content.indices.configurationConnector.configuration.successToast.title": "已更新配置",
"xpack.enterpriseSearch.content.indices.configurationConnector.connectorPackage.clientExamplesLink": "连接器客户端示例",
"xpack.enterpriseSearch.content.indices.configurationConnector.connectorPackage.configurationFileLink": "配置文件",
@ -13386,7 +13329,6 @@
"xpack.enterpriseSearch.content.indices.configurationConnector.support.readme.label": "连接器自述文件",
"xpack.enterpriseSearch.content.indices.configurationConnector.support.title": "支持和文档",
"xpack.enterpriseSearch.content.indices.configurationConnector.support.viewDocumentation.label": "查看文档",
"xpack.enterpriseSearch.content.indices.configurationConnector.warning.description": "如果您在完成连接器客户端之前至少同步一个文档,您必须重新创建搜索索引。",
"xpack.enterpriseSearch.content.indices.connector.syncRules.advancedRules.error": "JSON 格式无效",
"xpack.enterpriseSearch.content.indices.connector.syncRules.advancedRules.title": "高级规则",
"xpack.enterpriseSearch.content.indices.connectorScheduling.accordion.accessControlSync.description": "计划访问控制同步以使权限映射保持最新。",
@ -13741,9 +13683,7 @@
"xpack.enterpriseSearch.content.searchIndices.actions.columnTitle": "操作",
"xpack.enterpriseSearch.content.searchIndices.actions.deleteIndex.title": "删除此索引",
"xpack.enterpriseSearch.content.searchIndices.actions.viewIndex.title": "查看此索引",
"xpack.enterpriseSearch.content.searchIndices.addedDocs.columnTitle": "已添加文档",
"xpack.enterpriseSearch.content.searchIndices.create.buttonTitle": "创建新索引",
"xpack.enterpriseSearch.content.searchIndices.deletedDocs.columnTitle": "文档已删除",
"xpack.enterpriseSearch.content.searchIndices.deleteModal.cancelButton.title": "取消",
"xpack.enterpriseSearch.content.searchIndices.deleteModal.closeButton.title": "关闭",
"xpack.enterpriseSearch.content.searchIndices.deleteModal.confirmButton.title": "删除索引",
@ -13752,7 +13692,6 @@
"xpack.enterpriseSearch.content.searchIndices.deleteModal.syncsWarning.title": "进行中的同步",
"xpack.enterpriseSearch.content.searchIndices.docsCount.columnTitle": "文档计数",
"xpack.enterpriseSearch.content.searchIndices.health.columnTitle": "索引运行状况",
"xpack.enterpriseSearch.content.searchIndices.identitySync.columnTitle": "身份已同步",
"xpack.enterpriseSearch.content.searchIndices.ingestionMethod.api": "API",
"xpack.enterpriseSearch.content.searchIndices.ingestionMethod.columnTitle": "采集方法",
"xpack.enterpriseSearch.content.searchIndices.ingestionMethod.connector": "连接器",
@ -13783,8 +13722,6 @@
"xpack.enterpriseSearch.content.searchIndices.searchIndices.searchBar.placeHolder": "筛选 Elasticsearch 索引",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.stepsTitle": "通过 Enterprise Search 构建出色的搜索体验",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.tableTitle": "可用索引",
"xpack.enterpriseSearch.content.searchIndices.syncJobType.columnTitle": "内容同步类型",
"xpack.enterpriseSearch.content.searchIndices.syncStatus.columnTitle": "状态",
"xpack.enterpriseSearch.content.settings.breadcrumb": "设置",
"xpack.enterpriseSearch.content.settings.contactExtraction.label": "内容提取",
"xpack.enterpriseSearch.content.settings.contentExtraction.description": "从二进制文件(如 PDF 和 Word 文档)提取可搜索内容。",
@ -13819,21 +13756,10 @@
"xpack.enterpriseSearch.content.supportedLanguages.spanishLabel": "西班牙语",
"xpack.enterpriseSearch.content.supportedLanguages.thaiLabel": "泰语",
"xpack.enterpriseSearch.content.supportedLanguages.universalLabel": "通用",
"xpack.enterpriseSearch.content.syncJobs.flyout.canceledTitle": "同步已取消",
"xpack.enterpriseSearch.content.syncJobs.flyout.completedTitle": "同步已完成",
"xpack.enterpriseSearch.content.syncJobs.flyout.failureTitle": "同步失败",
"xpack.enterpriseSearch.content.syncJobs.flyout.inProgressTitle": "进行中",
"xpack.enterpriseSearch.content.syncJobs.flyout.sync": "同步",
"xpack.enterpriseSearch.content.syncJobs.flyout.sync.id": "ID",
"xpack.enterpriseSearch.content.syncJobs.flyout.syncStartedManually": "已手动启动同步",
"xpack.enterpriseSearch.content.syncJobs.flyout.syncStartedScheduled": "已按计划启动同步",
"xpack.enterpriseSearch.content.syncJobs.flyout.title": "事件日志",
"xpack.enterpriseSearch.content.syncJobs.lastSync.columnTitle": "上次同步",
"xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.accessControl.label": "访问控制同步",
"xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.content.label": "内容同步",
"xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.legend": "选择要显示的同步作业类型。",
"xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.name": "同步作业类型",
"xpack.enterpriseSearch.content.syncJobs.syncDuration.columnTitle": "同步持续时间",
"xpack.enterpriseSearch.crawler.addDomainFlyout.description": "可以将多个域添加到此索引的网络爬虫。在此添加其他域并从“管理”页面修改入口点和爬网规则。",
"xpack.enterpriseSearch.crawler.addDomainFlyout.openButtonLabel": "添加域",
"xpack.enterpriseSearch.crawler.addDomainFlyout.title": "添加新域",