[8.x] [Search] New search connector creation flow (#187582) (#196293)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Search] New search connector creation flow
(#187582)](https://github.com/elastic/kibana/pull/187582)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"José Luis
González","email":"joseluisgj@gmail.com"},"sourceCommit":{"committedDate":"2024-10-15T12:09:30Z","message":"[Search]
New search connector creation flow (#187582)\n\n## Summary\r\n\r\nThis
PR brings a new and dedicated search connector creation flow for\r\nES3
and
ESS.\r\n[Figma\r\nPrototype](https://www.figma.com/proto/eKQr4HYlz0v9pTofRPWIyH/Ingestion-methods-flow?page-id=411%3A158867&node-id=411-158870&viewport=3831%2C-1905%2C1.23&t=ZP9e3LtaSeJ5FMAz-9&scaling=min-zoom&content-scaling=fixed&starting-point-node-id=411%3A158870&show-proto-sidebar=1)\r\n\r\n![CleanShot
2024-07-04 at 16
27\r\n21](45e61110-f222-4bad-b24d-87ebad07ca98)\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is
usable by keyboard only (learn more\r\nabout [keyboard
accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI
touched in this PR does not create any new axe failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[ ] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[ ] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
Risk Matrix\r\n\r\nDelete this section if it is not applicable to this
PR.\r\n\r\nBefore closing this PR, invite QA, stakeholders, and other
developers to\r\nidentify risks that should be tested prior to the
change/feature\r\nrelease.\r\n\r\nWhen forming the risk matrix, consider
some of the following examples\r\nand how they may potentially impact
the change:\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes
|\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n|
Multiple Spaces&mdash;unexpected behavior in non-default Kibana
Space.\r\n| Low | High | Integration tests will verify that all features
are still\r\nsupported in non-default Kibana Space and when user
switches between\r\nspaces. |\r\n| Multiple nodes&mdash;Elasticsearch
polling might have race conditions\r\nwhen multiple Kibana nodes are
polling for the same tasks. | High | Low\r\n| Tasks are idempotent, so
executing them multiple times will not result\r\nin logical error, but
will degrade performance. To test for this case we\r\nadd plenty of unit
tests around this logic and document manual testing\r\nprocedure. |\r\n|
Code should gracefully handle cases when feature X or plugin Y
are\r\ndisabled. | Medium | High | Unit tests will verify that any
feature flag\r\nor plugin combination still results in our service
operational. |\r\n| [See more potential
risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
|\r\n\r\n\r\n### For maintainers\r\n\r\n- [ ] This was checked for
breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
Efe Gürkan YALAMAN <efeguerkan.yalaman@elastic.co>\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"63e116bb078c29c70e4e23cba1c88d0ac022801d","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:EnterpriseSearch","backport:prev-minor","v8.16.0"],"title":"[Search]
New search connector creation
flow","number":187582,"url":"https://github.com/elastic/kibana/pull/187582","mergeCommit":{"message":"[Search]
New search connector creation flow (#187582)\n\n## Summary\r\n\r\nThis
PR brings a new and dedicated search connector creation flow for\r\nES3
and
ESS.\r\n[Figma\r\nPrototype](https://www.figma.com/proto/eKQr4HYlz0v9pTofRPWIyH/Ingestion-methods-flow?page-id=411%3A158867&node-id=411-158870&viewport=3831%2C-1905%2C1.23&t=ZP9e3LtaSeJ5FMAz-9&scaling=min-zoom&content-scaling=fixed&starting-point-node-id=411%3A158870&show-proto-sidebar=1)\r\n\r\n![CleanShot
2024-07-04 at 16
27\r\n21](45e61110-f222-4bad-b24d-87ebad07ca98)\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is
usable by keyboard only (learn more\r\nabout [keyboard
accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI
touched in this PR does not create any new axe failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[ ] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[ ] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
Risk Matrix\r\n\r\nDelete this section if it is not applicable to this
PR.\r\n\r\nBefore closing this PR, invite QA, stakeholders, and other
developers to\r\nidentify risks that should be tested prior to the
change/feature\r\nrelease.\r\n\r\nWhen forming the risk matrix, consider
some of the following examples\r\nand how they may potentially impact
the change:\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes
|\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n|
Multiple Spaces&mdash;unexpected behavior in non-default Kibana
Space.\r\n| Low | High | Integration tests will verify that all features
are still\r\nsupported in non-default Kibana Space and when user
switches between\r\nspaces. |\r\n| Multiple nodes&mdash;Elasticsearch
polling might have race conditions\r\nwhen multiple Kibana nodes are
polling for the same tasks. | High | Low\r\n| Tasks are idempotent, so
executing them multiple times will not result\r\nin logical error, but
will degrade performance. To test for this case we\r\nadd plenty of unit
tests around this logic and document manual testing\r\nprocedure. |\r\n|
Code should gracefully handle cases when feature X or plugin Y
are\r\ndisabled. | Medium | High | Unit tests will verify that any
feature flag\r\nor plugin combination still results in our service
operational. |\r\n| [See more potential
risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
|\r\n\r\n\r\n### For maintainers\r\n\r\n- [ ] This was checked for
breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
Efe Gürkan YALAMAN <efeguerkan.yalaman@elastic.co>\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"63e116bb078c29c70e4e23cba1c88d0ac022801d"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/187582","number":187582,"mergeCommit":{"message":"[Search]
New search connector creation flow (#187582)\n\n## Summary\r\n\r\nThis
PR brings a new and dedicated search connector creation flow for\r\nES3
and
ESS.\r\n[Figma\r\nPrototype](https://www.figma.com/proto/eKQr4HYlz0v9pTofRPWIyH/Ingestion-methods-flow?page-id=411%3A158867&node-id=411-158870&viewport=3831%2C-1905%2C1.23&t=ZP9e3LtaSeJ5FMAz-9&scaling=min-zoom&content-scaling=fixed&starting-point-node-id=411%3A158870&show-proto-sidebar=1)\r\n\r\n![CleanShot
2024-07-04 at 16
27\r\n21](45e61110-f222-4bad-b24d-87ebad07ca98)\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is
usable by keyboard only (learn more\r\nabout [keyboard
accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI
touched in this PR does not create any new axe failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[ ] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[ ] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
Risk Matrix\r\n\r\nDelete this section if it is not applicable to this
PR.\r\n\r\nBefore closing this PR, invite QA, stakeholders, and other
developers to\r\nidentify risks that should be tested prior to the
change/feature\r\nrelease.\r\n\r\nWhen forming the risk matrix, consider
some of the following examples\r\nand how they may potentially impact
the change:\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes
|\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n|
Multiple Spaces&mdash;unexpected behavior in non-default Kibana
Space.\r\n| Low | High | Integration tests will verify that all features
are still\r\nsupported in non-default Kibana Space and when user
switches between\r\nspaces. |\r\n| Multiple nodes&mdash;Elasticsearch
polling might have race conditions\r\nwhen multiple Kibana nodes are
polling for the same tasks. | High | Low\r\n| Tasks are idempotent, so
executing them multiple times will not result\r\nin logical error, but
will degrade performance. To test for this case we\r\nadd plenty of unit
tests around this logic and document manual testing\r\nprocedure. |\r\n|
Code should gracefully handle cases when feature X or plugin Y
are\r\ndisabled. | Medium | High | Unit tests will verify that any
feature flag\r\nor plugin combination still results in our service
operational. |\r\n| [See more potential
risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
|\r\n\r\n\r\n### For maintainers\r\n\r\n- [ ] This was checked for
breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
Efe Gürkan YALAMAN <efeguerkan.yalaman@elastic.co>\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"63e116bb078c29c70e4e23cba1c88d0ac022801d"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: José Luis González <joseluisgj@gmail.com>
This commit is contained in:
Kibana Machine 2024-10-16 01:02:54 +11:00 committed by GitHub
parent 0e205fae8f
commit 529d04f2fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 2336 additions and 158 deletions

View file

@ -45,6 +45,7 @@ interface ConnectorConfigurationProps {
hasPlatinumLicense: boolean;
isLoading: boolean;
saveConfig: (configuration: Record<string, string | number | boolean | null>) => void;
saveAndSync?: (configuration: Record<string, string | number | boolean | null>) => void;
stackManagementLink?: string;
subscriptionLink?: string;
children?: React.ReactNode;
@ -90,6 +91,7 @@ export const ConnectorConfigurationComponent: FC<
hasPlatinumLicense,
isLoading,
saveConfig,
saveAndSync,
subscriptionLink,
stackManagementLink,
}) => {
@ -166,6 +168,12 @@ export const ConnectorConfigurationComponent: FC<
saveConfig(config);
setIsEditing(false);
}}
{...(saveAndSync && {
saveAndSync: (config) => {
saveAndSync(config);
setIsEditing(false);
},
})}
/>
) : (
uncategorizedDisplayList.length > 0 && (

View file

@ -36,6 +36,7 @@ interface ConnectorConfigurationForm {
isLoading: boolean;
isNative: boolean;
saveConfig: (config: Record<string, string | number | boolean | null>) => void;
saveAndSync?: (config: Record<string, string | number | boolean | null>) => void;
stackManagementHref?: string;
subscriptionLink?: string;
}
@ -60,6 +61,7 @@ export const ConnectorConfigurationForm: React.FC<ConnectorConfigurationForm> =
isLoading,
isNative,
saveConfig,
saveAndSync,
}) => {
const [localConfig, setLocalConfig] = useState<ConnectorConfiguration>(configuration);
const [configView, setConfigView] = useState<ConfigView>(
@ -167,19 +169,7 @@ export const ConnectorConfigurationForm: React.FC<ConnectorConfigurationForm> =
)}
<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>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-telemetry-id="entSearchContent-connector-configuration-cancelEdit"
@ -196,6 +186,38 @@ export const ConnectorConfigurationForm: React.FC<ConnectorConfigurationForm> =
)}
</EuiButtonEmpty>
</EuiFlexItem>
<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',
})}
</EuiButton>
</EuiFlexItem>
{saveAndSync && (
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="entSearchContent-connector-configuration-saveConfiguration"
data-telemetry-id="entSearchContent-connector-configuration-saveConfiguration"
isLoading={isLoading}
fill
onClick={() => {
saveAndSync(configViewToConfigValues(configView));
}}
>
{i18n.translate(
'searchConnectors.configurationConnector.config.submitButton.title',
{
defaultMessage: 'Save and sync',
}
)}
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFormRow>
</EuiForm>

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import dedent from 'dedent';
import {
ENTERPRISE_SEARCH_APP_ID,
ENTERPRISE_SEARCH_CONTENT_APP_ID,
@ -210,6 +212,58 @@ export const SEARCH_RELEVANCE_PLUGIN = {
SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/',
};
export const CREATE_CONNECTOR_PLUGIN = {
CLI_SNIPPET: dedent`./bin/connectors connector create
--index-name my-index
--index-language en
--from-file config.yml
`,
CONSOLE_SNIPPET: dedent`# Create an index
PUT /my-index-000001
{
"settings": {
"index": {
"number_of_shards": 3,
"number_of_replicas": 2
}
}
}
# Create an API key
POST /_security/api_key
{
"name": "my-api-key",
"expiration": "1d",
"role_descriptors":
{
"role-a": {
"cluster": ["all"],
"indices": [
{
"names": ["index-a*"],
"privileges": ["read"]
}
]
},
"role-b": {
"cluster": ["all"],
"indices": [
{
"names": ["index-b*"],
"privileges": ["all"]
}]
}
}, "metadata":
{ "application": "my-application",
"environment": {
"level": 1,
"trusted": true,
"tags": ["dev", "staging"]
}
}
}`,
};
export const LICENSED_SUPPORT_URL = 'https://support.elastic.co';
export const JSON_HEADER = {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';
interface AddConnectorValue {
@ -20,11 +20,17 @@ export interface AddConnectorApiLogicArgs {
language: string | null;
name: string;
serviceType?: string;
// Without a proper refactoring there is no good way to chain actions.
// This prop is simply passed back with the result to let listeners
// know what was the intent of the request. And call the next action
// accordingly.
uiFlags?: Record<string, boolean>;
}
export interface AddConnectorApiLogicResponse {
id: string;
indexName: string;
uiFlags?: Record<string, boolean>;
}
export const addConnector = async ({
@ -34,6 +40,7 @@ export const addConnector = async ({
isNative,
language,
serviceType,
uiFlags,
}: AddConnectorApiLogicArgs): Promise<AddConnectorApiLogicResponse> => {
const route = '/internal/enterprise_search/connectors';
@ -54,7 +61,12 @@ export const addConnector = async ({
return {
id: result.id,
indexName: result.index_name,
uiFlags,
};
};
export const AddConnectorApiLogic = createApiLogic(['add_connector_api_logic'], addConnector);
export type AddConnectorApiLogicActions = Actions<
AddConnectorApiLogicArgs,
AddConnectorApiLogicResponse
>;

View file

@ -5,13 +5,15 @@
* 2.0.
*/
import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';
export interface GenerateConfigApiArgs {
connectorId: string;
}
export type GenerateConfigApiActions = Actions<GenerateConfigApiArgs, {}>;
export const generateConnectorConfig = async ({ connectorId }: GenerateConfigApiArgs) => {
const route = `/internal/enterprise_search/connectors/${connectorId}/generate_config`;
return await HttpLogic.values.http.post(route);

View file

@ -4,23 +4,38 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';
export interface GenerateConnectorNamesApiArgs {
connectorName?: string;
connectorType?: string;
}
export interface GenerateConnectorNamesApiResponse {
apiKeyName: string;
connectorName: string;
indexName: string;
}
export const generateConnectorNames = async (
{ connectorType }: GenerateConnectorNamesApiArgs = { connectorType: 'custom' }
{ connectorType, connectorName }: GenerateConnectorNamesApiArgs = { connectorType: 'custom' }
) => {
if (connectorType === '') {
connectorType = 'custom';
}
const route = `/internal/enterprise_search/connectors/generate_connector_name`;
return await HttpLogic.values.http.post(route, {
body: JSON.stringify({ connectorType }),
body: JSON.stringify({ connectorName, connectorType }),
});
};
export const GenerateConnectorNamesApiLogic = createApiLogic(
['generate_config_api_logic'],
['generate_connector_names_api_logic'],
generateConnectorNames
);
export type GenerateConnectorNamesApiLogicActions = Actions<
GenerateConnectorNamesApiArgs,
GenerateConnectorNamesApiResponse
>;

View file

@ -12,13 +12,15 @@ import { i18n } from '@kbn/i18n';
export interface GenerateConfigButtonProps {
connectorId: string;
disabled?: boolean;
generateConfiguration: (params: { connectorId: string }) => void;
isGenerateLoading: boolean;
}
export const GenerateConfigButton: React.FC<GenerateConfigButtonProps> = ({
connectorId,
disabled,
generateConfiguration,
isGenerateLoading,
isGenerateLoading = false,
}) => {
return (
<EuiFlexGroup direction="row" gutterSize="xs" responsive={false} alignItems="center">
@ -26,6 +28,7 @@ export const GenerateConfigButton: React.FC<GenerateConfigButtonProps> = ({
<EuiButton
data-test-subj="entSearchContent-connector-configuration-generateConfigButton"
data-telemetry-id="entSearchContent-connector-configuration-generateConfigButton"
disabled={disabled}
fill
iconType="sparkles"
isLoading={isGenerateLoading}

View file

@ -36,7 +36,7 @@ import { CONNECTOR_DETAIL_PATH, SEARCH_INDEX_PATH } from '../../../routes';
export interface GeneratedConfigFieldsProps {
apiKey?: ApiKey;
connector: Connector;
generateApiKey: () => void;
generateApiKey?: () => void;
isGenerateLoading: boolean;
}
@ -93,7 +93,7 @@ export const GeneratedConfigFields: React.FC<GeneratedConfigFieldsProps> = ({
};
const onConfirm = () => {
generateApiKey();
if (generateApiKey) generateApiKey();
setIsModalVisible(false);
};
@ -222,16 +222,18 @@ export const GeneratedConfigFields: React.FC<GeneratedConfigFieldsProps> = ({
<EuiFlexItem>
<EuiCode>{apiKey?.encoded}</EuiCode>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="enterpriseSearchGeneratedConfigFieldsButton"
size="xs"
iconType="refresh"
isLoading={isGenerateLoading}
onClick={refreshButtonClick}
disabled={!connector.index_name}
/>
</EuiFlexItem>
{generateApiKey && (
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="enterpriseSearchGeneratedConfigFieldsButton"
size="xs"
iconType="refresh"
isLoading={isGenerateLoading}
onClick={refreshButtonClick}
disabled={!connector.index_name}
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButtonIcon
size="xs"
@ -245,16 +247,18 @@ export const GeneratedConfigFields: React.FC<GeneratedConfigFieldsProps> = ({
</EuiCopy>
</EuiFlexItem>
) : (
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="enterpriseSearchGeneratedConfigFieldsButton"
size="xs"
iconType="refresh"
isLoading={isGenerateLoading}
onClick={refreshButtonClick}
disabled={!connector.index_name}
/>
</EuiFlexItem>
generateApiKey && (
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="enterpriseSearchGeneratedConfigFieldsButton"
size="xs"
iconType="refresh"
isLoading={isGenerateLoading}
onClick={refreshButtonClick}
disabled={!connector.index_name}
/>
</EuiFlexItem>
)
)}
</EuiFlexGroup>
</EuiFlexItem>

View file

@ -61,6 +61,22 @@ export const ConnectorDeployment: React.FC = () => {
Record<string, { deploymentMethod: 'docker' | 'source' }>
>('search:connector-ui-options', {});
useEffect(() => {
if (connectorId && connector && connector.api_key_id) {
getApiKeyById(connector.api_key_id);
}
}, [connector, connectorId]);
const selectDeploymentMethod = (deploymentMethod: 'docker' | 'source') => {
if (connector) {
setSelectedDeploymentMethod(deploymentMethod);
setConnectorUiOptions({
...connectorUiOptions,
[connector.id]: { deploymentMethod },
});
}
};
useEffect(() => {
if (connectorUiOptions && connectorId && connectorUiOptions[connectorId]) {
setSelectedDeploymentMethod(connectorUiOptions[connectorId].deploymentMethod);
@ -68,25 +84,10 @@ export const ConnectorDeployment: React.FC = () => {
selectDeploymentMethod('docker');
}
}, [connectorUiOptions, connectorId]);
useEffect(() => {
if (connectorId && connector && connector.api_key_id) {
getApiKeyById(connector.api_key_id);
}
}, [connector, connectorId]);
if (!connector || connector.is_native) {
return <></>;
}
const selectDeploymentMethod = (deploymentMethod: 'docker' | 'source') => {
setSelectedDeploymentMethod(deploymentMethod);
setConnectorUiOptions({
...connectorUiOptions,
[connector.id]: { deploymentMethod },
});
};
const hasApiKey = !!(connector.api_key_id ?? generatedData?.apiKey);
const isWaitingForConnector = !connector.status || connector.status === ConnectorStatus.CREATED;

View file

@ -10,15 +10,12 @@ import { kea, MakeLogicType } from 'kea';
import { Connector } from '@kbn/search-connectors';
import { HttpError, Status } from '../../../../../common/types/api';
import { Actions } from '../../../shared/api_logic/create_api_logic';
import {
GenerateConfigApiArgs,
GenerateConfigApiActions,
GenerateConfigApiLogic,
} from '../../api/connector/generate_connector_config_api_logic';
import { APIKeyResponse } from '../../api/generate_api_key/generate_api_key_logic';
type GenerateConfigApiActions = Actions<GenerateConfigApiArgs, {}>;
export interface DeploymentLogicValues {
generateConfigurationError: HttpError;
generateConfigurationStatus: Status;

View file

@ -44,8 +44,8 @@ import { ConnectorStats } from './connector_stats';
import { ConnectorsLogic } from './connectors_logic';
import { ConnectorsTable } from './connectors_table';
import { CrawlerEmptyState } from './crawler_empty_state';
import { CreateConnector } from './create_connector';
import { DeleteConnectorModal } from './delete_connector_modal';
import { SelectConnector } from './select_connector/select_connector';
export const connectorsBreadcrumbs = [
i18n.translate('xpack.enterpriseSearch.content.connectors.breadcrumb', {
@ -81,7 +81,7 @@ export const Connectors: React.FC<ConnectorsProps> = ({ isCrawler }) => {
}, [searchParams.from, searchParams.size, searchQuery, isCrawler]);
return !isLoading && isEmpty && !isCrawler ? (
<SelectConnector />
<CreateConnector />
) : (
<>
<DeleteConnectorModal isCrawler={isCrawler} />

View file

@ -13,23 +13,27 @@ import {
CONNECTORS_PATH,
NEW_INDEX_SELECT_CONNECTOR_PATH,
NEW_CONNECTOR_PATH,
NEW_CONNECTOR_FLOW_PATH,
CONNECTOR_DETAIL_PATH,
} from '../../routes';
import { ConnectorDetailRouter } from '../connector_detail/connector_detail_router';
import { NewSearchIndexPage } from '../new_index/new_search_index_page';
import { Connectors } from './connectors';
import { SelectConnector } from './select_connector/select_connector';
import { CreateConnector } from './create_connector';
export const ConnectorsRouter: React.FC = () => {
return (
<Routes>
<Route path={NEW_INDEX_SELECT_CONNECTOR_PATH}>
<SelectConnector />
<CreateConnector />
</Route>
<Route path={NEW_CONNECTOR_PATH}>
<NewSearchIndexPage type="connector" />
</Route>
<Route path={NEW_CONNECTOR_FLOW_PATH}>
<CreateConnector />
</Route>
<Route path={CONNECTORS_PATH} exact>
<Connectors isCrawler={false} />
</Route>

View file

@ -0,0 +1,11 @@
<svg width="41" height="40" viewBox="0 0 41 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1064_10468)">
<path d="M17.2647 32.0743L8.42591 23.2352L11.2142 22.4883L18.0118 29.2859L27.2975 26.7978L29.7856 17.5121L22.9881 10.7146L23.7353 7.92622L32.574 16.7649L29.7124 27.4446L35.8997 33.632L35.5262 35.026L34.1319 35.3996L27.9448 29.2125L17.2647 32.0743Z" fill="#535766"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.9192 24.4199L18.8822 26.0375L14.4628 21.6181L15.7069 16.9752L5.10035 6.36864L5.47397 4.97427L6.86799 4.60074L17.4747 15.2074L22.1175 13.9634L26.5369 18.3828L24.9192 24.4199ZM23.7485 19.13L22.878 22.3786L19.6294 23.2491L17.2512 20.8709L18.1217 17.6223L21.3703 16.7518L23.7485 19.13Z" fill="#00BFB3"/>
</g>
<defs>
<clipPath id="clip0_1064_10468">
<rect width="40" height="40" fill="white" transform="translate(0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 897 B

View file

@ -0,0 +1,172 @@
/*
* 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, { useCallback, useEffect, useMemo, useState } from 'react';
import { useActions, useValues } from 'kea';
import {
EuiBadge,
EuiFlexItem,
EuiIcon,
EuiInputPopover,
EuiSelectable,
EuiSelectableOption,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { KibanaLogic } from '../../../../../shared/kibana';
import { NewConnectorLogic } from '../../../new_index/method_connector/new_connector_logic';
import { SelfManagePreference } from '../create_connector';
interface ChooseConnectorSelectableProps {
selfManaged: SelfManagePreference;
}
interface OptionData {
secondaryContent?: string;
}
export const ChooseConnectorSelectable: React.FC<ChooseConnectorSelectableProps> = ({
selfManaged,
}) => {
const { euiTheme } = useEuiTheme();
const [isOpen, setIsOpen] = useState(false);
const [selectableOptions, selectableSetOptions] = useState<
Array<EuiSelectableOption<OptionData>>
>([]);
const { connectorTypes } = useValues(KibanaLogic);
const allConnectors = useMemo(
() => connectorTypes.sort((a, b) => a.name.localeCompare(b.name)),
[connectorTypes]
);
const { selectedConnector } = useValues(NewConnectorLogic);
const { setSelectedConnector } = useActions(NewConnectorLogic);
const getInitialOptions = () => {
return allConnectors.map((connector, key) => {
const append: JSX.Element[] = [];
if (connector.isTechPreview) {
append.push(
<EuiBadge key={key + '-preview'} iconType="beaker" color="hollow">
{i18n.translate(
'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.thechPreviewBadgeLabel',
{ defaultMessage: 'Tech preview' }
)}
</EuiBadge>
);
}
if (connector.isBeta) {
append.push(
<EuiBadge key={key + '-beta'} iconType={'beta'} color="hollow">
{i18n.translate(
'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.BetaBadgeLabel',
{
defaultMessage: 'Beta',
}
)}
</EuiBadge>
);
}
if (selfManaged === 'native' && !connector.isNative) {
append.push(
<EuiBadge key={key + '-self'} iconType={'warning'} color="warning">
{i18n.translate(
'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.OnlySelfManagedBadgeLabel',
{
defaultMessage: 'Self managed',
}
)}
</EuiBadge>
);
}
return {
append,
key: key.toString(),
label: connector.name,
prepend: <EuiIcon size="l" type={connector.iconPath} />,
};
});
};
const initialOptions = getInitialOptions();
useEffect(() => {
selectableSetOptions(initialOptions);
}, [selfManaged]);
const [searchValue, setSearchValue] = useState('');
const openPopover = useCallback(() => {
setIsOpen(true);
}, []);
const closePopover = useCallback(() => {
setIsOpen(false);
}, []);
return (
<EuiFlexItem>
<EuiSelectable
aria-label={i18n.translate(
'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.euiSelectable.selectableInputPopoverLabel',
{ defaultMessage: 'Select a data source for your connector to use.' }
)}
options={selectableOptions}
onChange={(newOptions, _, changedOption) => {
selectableSetOptions(newOptions);
closePopover();
if (changedOption.checked === 'on') {
const keySelected = Number(changedOption.key);
setSelectedConnector(allConnectors[keySelected]);
setSearchValue(allConnectors[keySelected].name);
} else {
setSelectedConnector(null);
setSearchValue('');
}
}}
listProps={{
isVirtualized: true,
rowHeight: Number(euiTheme.base * 3),
showIcons: false,
}}
singleSelection
searchable
searchProps={{
fullWidth: true,
isClearable: true,
onChange: (value) => {
if (value !== selectedConnector?.name) {
setSearchValue(value);
}
},
onClick: openPopover,
onFocus: openPopover,
placeholder: i18n.translate(
'xpack.enterpriseSearch.createConnector.chooseConnectorSelectable.placeholder.text',
{ defaultMessage: 'Choose a data source' }
),
value: searchValue,
}}
>
{(list, search) => (
<EuiInputPopover
fullWidth
closePopover={closePopover}
disableFocusTrap
closeOnScroll
isOpen={isOpen}
input={search!}
panelPaddingSize="none"
>
{list}
</EuiInputPopover>
)}
</EuiSelectable>
</EuiFlexItem>
);
};

View file

@ -0,0 +1,166 @@
/*
* 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, { useState } from 'react';
import {
EuiButtonIcon,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPanel,
EuiPopover,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import connectorLogo from '../../../../../../assets/images/connector_logo_network_drive_version.svg';
const nativePopoverPanels = [
{
description: i18n.translate(
'xpack.enterpriseSearch.connectorDescriptionPopover.connectorDescriptionBadge.native.chooseADataSourceLabel',
{ defaultMessage: 'Choose a data source you would like to sync' }
),
icons: [<EuiIcon type="documents" />],
id: 'native-choose-source',
},
{
description: i18n.translate(
'xpack.enterpriseSearch.connectorDescriptionPopover.connectorDescriptionBadge.native.configureConnectorLabel',
{ defaultMessage: 'Configure your connector using our Kibana UI' }
),
icons: [<EuiIcon type={connectorLogo} />, <EuiIcon type="logoElastic" />],
id: 'native-configure-connector',
},
];
const connectorClientPopoverPanels = [
{
description: i18n.translate(
'xpack.enterpriseSearch.connectorDescriptionPopover.connectorDescriptionBadge.client.chooseADataSourceLabel',
{ defaultMessage: 'Choose a data source you would like to sync' }
),
icons: [<EuiIcon type="documents" />],
id: 'client-choose-source',
},
{
description: i18n.translate(
'xpack.enterpriseSearch.connectorDescriptionPopover.connectorDescriptionBadge.client.configureConnectorLabel',
{
defaultMessage:
'Deploy connector code on your own infrastructure by running from source or using Docker',
}
),
icons: [
<EuiIcon type={connectorLogo} />,
<EuiIcon type="sortRight" />,
<EuiIcon type="launch" />,
],
id: 'client-deploy',
},
{
description: i18n.translate(
'xpack.enterpriseSearch.connectorDescriptionPopover.connectorDescriptionBadge.client.enterDetailsLabel',
{
defaultMessage: 'Enter access and connection details for your data source',
}
),
icons: [
<EuiIcon type="documents" />,
<EuiIcon type="sortRight" />,
<EuiIcon type={connectorLogo} />,
<EuiIcon type="sortRight" />,
<EuiIcon type="logoElastic" />,
],
id: 'client-configure-connector',
},
];
export interface ConnectorDescriptionPopoverProps {
isDisabled: boolean;
isNative: boolean;
}
export const ConnectorDescriptionPopover: React.FC<ConnectorDescriptionPopoverProps> = ({
isNative,
isDisabled,
}) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const panels = isNative ? nativePopoverPanels : connectorClientPopoverPanels;
return (
<EuiPopover
anchorPosition="upCenter"
button={
<EuiButtonIcon
aria-label={i18n.translate('xpack.enterpriseSearch.createConnector.iInCircle', {
defaultMessage: 'More information',
})}
data-test-subj="enterpriseSearchConnectorDescriptionPopoverButton"
iconType="iInCircle"
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
/>
}
isOpen={isPopoverOpen}
closePopover={() => {
setIsPopoverOpen(false);
}}
>
<EuiPanel hasBorder={false} hasShadow={false}>
{isDisabled && (
<EuiFlexGroup>
<EuiFlexItem>
<EuiCallOut
title={i18n.translate(
'xpack.enterpriseSearch.createConnector.connectorDescriptionBadge.notAvailableTitle',
{
defaultMessage:
'This connector is not available as an Elastic-managed Connector',
}
)}
size="s"
iconType="warning"
color="warning"
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiSpacer size="m" />
<EuiFlexGroup>
{panels.map((panel) => {
return (
<EuiFlexItem grow={false} key={panel.id}>
<EuiFlexGroup
direction="column"
alignItems="center"
gutterSize="s"
style={{ maxWidth: 200 }}
>
<EuiFlexItem grow={false}>
<EuiFlexGroup responsive={false} gutterSize="s">
{panel.icons.map((icon, index) => (
<EuiFlexItem grow={false} key={index}>
{icon}
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" grow={false} textAlign="center">
<p>{panel.description}</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
</EuiPanel>
</EuiPopover>
);
};

View file

@ -0,0 +1,114 @@
/*
* 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, { useState } from 'react';
import {
EuiButtonIcon,
EuiContextMenuItem,
EuiContextMenuPanel,
EuiPopover,
useGeneratedHtmlId,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SelfManagePreference } from '../create_connector';
import { ManualConfigurationFlyout } from './manual_configuration_flyout';
export interface ManualConfigurationProps {
isDisabled: boolean;
selfManagePreference: SelfManagePreference;
}
export const ManualConfiguration: React.FC<ManualConfigurationProps> = ({
isDisabled,
selfManagePreference,
}) => {
const [isPopoverOpen, setPopover] = useState(false);
const splitButtonPopoverId = useGeneratedHtmlId({
prefix: 'splitButtonPopover',
});
const onButtonClick = () => {
setPopover(!isPopoverOpen);
};
const closePopover = () => {
setPopover(false);
};
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
const [flyoutContent, setFlyoutContent] = useState<'manual_config' | 'client'>();
const items = [
<EuiContextMenuItem
key="copy"
icon="wrench"
onClick={() => {
setFlyoutContent('manual_config');
setIsFlyoutVisible(true);
closePopover();
}}
>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.manageAttachedIndexContextMenuItemLabel',
{ defaultMessage: 'Manual configuration' }
)}
</EuiContextMenuItem>,
<EuiContextMenuItem
key="share"
icon="console"
onClick={() => {
setFlyoutContent('client');
setIsFlyoutVisible(true);
closePopover();
}}
>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.scheduleASyncContextMenuItemLabel',
{
defaultMessage: 'Try with CLI',
}
)}
</EuiContextMenuItem>,
];
return (
<>
<EuiPopover
id={splitButtonPopoverId}
button={
<EuiButtonIcon
data-test-subj="enterpriseSearchFinishUpStepButton"
display="fill"
disabled={isDisabled}
size="m"
iconType="boxesVertical"
aria-label={i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.euiButtonIcon.moreLabel',
{ defaultMessage: 'More' }
)}
onClick={onButtonClick}
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenuPanel items={items} />
</EuiPopover>
{isFlyoutVisible && (
<ManualConfigurationFlyout
setIsFlyoutVisible={setIsFlyoutVisible}
flyoutContent={flyoutContent}
selfManagePreference={selfManagePreference}
/>
)}
</>
);
};

View file

@ -0,0 +1,228 @@
/*
* 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,
EuiCode,
EuiCodeBlock,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiFormRow,
EuiLink,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
useGeneratedHtmlId,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { CREATE_CONNECTOR_PLUGIN } from '../../../../../../../common/constants';
import { NewConnectorLogic } from '../../../new_index/method_connector/new_connector_logic';
import { SelfManagePreference } from '../create_connector';
const CLI_LABEL = i18n.translate(
'xpack.enterpriseSearch.createConnector.manualConfiguration.cliLabel',
{
defaultMessage: 'CLI',
}
);
export interface ManualConfigurationFlyoutProps {
flyoutContent: string | undefined;
selfManagePreference: SelfManagePreference;
setIsFlyoutVisible: (value: boolean) => void;
}
export const ManualConfigurationFlyout: React.FC<ManualConfigurationFlyoutProps> = ({
flyoutContent,
selfManagePreference,
setIsFlyoutVisible,
}) => {
const simpleFlyoutTitleId = useGeneratedHtmlId({
prefix: 'simpleFlyoutTitle',
});
const { connectorName } = useValues(NewConnectorLogic);
const { setRawName, createConnector } = useActions(NewConnectorLogic);
return (
<EuiFlyout
ownFocus
onClose={() => setIsFlyoutVisible(false)}
aria-labelledby={simpleFlyoutTitleId}
size="s"
>
{flyoutContent === 'manual_config' && (
<>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2 id={simpleFlyoutTitleId}>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.manualConfiguration.h2.cliLabel',
{
defaultMessage: 'Manual configuration',
}
)}
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
<p>
<FormattedMessage
id="xpack.enterpriseSearch.createConnector.flyoutManualConfigContent.p.thisManualOptionIsLabel"
defaultMessage="This manual option is an alternative to the {generateConfig} option, here you can bring your already existing index or API key."
values={{
generateConfig: (
<b>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.manualConfiguration.generateConfigLinkLabel',
{
defaultMessage: 'Generate configuration',
}
)}
</b>
),
}}
/>
</p>
</EuiText>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiFlexItem>
<EuiPanel hasBorder>
<EuiTitle size="s">
<h3>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.manualConfiguration.connectorName',
{
defaultMessage: 'Connector',
}
)}
</h3>
</EuiTitle>
<EuiSpacer size="m" />
<EuiFormRow
fullWidth
label={i18n.translate(
'xpack.enterpriseSearch.createConnector.startStep.euiFormRow.connectorNameLabel',
{ defaultMessage: 'Connector name' }
)}
>
<EuiFieldText
data-test-subj="enterpriseSearchStartStepFieldText"
fullWidth
name="first"
value={connectorName}
onChange={(e) => {
setRawName(e.target.value);
}}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<EuiText size="xs">
<p>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.manualConfiguration.p.connectorNameDescription',
{
defaultMessage:
'You will be redirected to the connector page to configure the rest of your connector',
}
)}
</p>
</EuiText>
</EuiPanel>
</EuiFlexItem>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="enterpriseSearchFlyoutManualConfigContentCloseButton"
iconType="cross"
onClick={() => setIsFlyoutVisible(false)}
flush="left"
>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.flyoutManualConfigContent.closeButtonEmptyLabel',
{ defaultMessage: 'Close' }
)}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="enterpriseSearchFlyoutManualConfigContentSaveButton"
onClick={() => {
createConnector({
isSelfManaged: selfManagePreference === 'selfManaged',
shouldGenerateAfterCreate: false,
shouldNavigateToConnectorAfterCreate: true,
});
}}
fill
>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.flyoutManualConfigContent.saveConfigurationButtonLabel',
{ defaultMessage: 'Save configuration' }
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</>
)}
{flyoutContent === 'client' && (
<>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2 id={simpleFlyoutTitleId}>{CLI_LABEL}</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiText size="s">
<p>
<FormattedMessage
id="xpack.enterpriseSearch.createConnector.manualConfiguration.p.youCanAlsoUseLabel"
defaultMessage="You can also use the connectors {cliLink}. The following command creates a new connector attached to the {myIndex}, using configuration from your file."
values={{
cliLink: (
<EuiLink
data-test-subj="enterpriseSearchManualConfigurationConnectorsCliLink"
href="https://github.com/elastic/connectors/blob/main/docs/CLI.md"
target="_blank"
external
>
{CLI_LABEL}
</EuiLink>
),
myIndex: <EuiCode>my-index</EuiCode>,
}}
/>
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiCodeBlock language="bash" isCopyable>
{CREATE_CONNECTOR_PLUGIN.CLI_SNIPPET}
</EuiCodeBlock>
</EuiFlyoutBody>
</>
)}
</EuiFlyout>
);
};

View file

@ -0,0 +1,122 @@
/*
* 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 } from 'react';
import { useActions, useValues } from 'kea';
import {
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiSpacer,
EuiTitle,
EuiText,
EuiButton,
EuiProgress,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ConnectorConfigurationComponent, ConnectorStatus } from '@kbn/search-connectors';
import { Status } from '../../../../../../common/types/api';
import * as Constants from '../../../../shared/constants';
import { ConnectorConfigurationApiLogic } from '../../../api/connector/update_connector_configuration_api_logic';
import { ConnectorViewLogic } from '../../connector_detail/connector_view_logic';
interface ConfigurationStepProps {
setCurrentStep: Function;
title: string;
}
export const ConfigurationStep: React.FC<ConfigurationStepProps> = ({ title, setCurrentStep }) => {
const { connector } = useValues(ConnectorViewLogic);
const { updateConnectorConfiguration } = useActions(ConnectorViewLogic);
const { status } = useValues(ConnectorConfigurationApiLogic);
const isSyncing = false;
const isNextStepEnabled =
connector?.status === ConnectorStatus.CONNECTED ||
connector?.status === ConnectorStatus.CONFIGURED;
useEffect(() => {
setTimeout(() => {
window.scrollTo({
behavior: 'smooth',
top: 0,
});
}, 100);
}, []);
if (!connector) return null;
return (
<>
<EuiFlexGroup gutterSize="m" direction="column">
<EuiFlexItem>
<EuiPanel hasShadow={false} hasBorder paddingSize="l" style={{ position: 'relative' }}>
<EuiTitle size="m">
<h3>{title}</h3>
</EuiTitle>
<EuiSpacer size="m" />
<ConnectorConfigurationComponent
connector={connector}
hasPlatinumLicense
isLoading={status === Status.LOADING}
saveConfig={(config) => {
updateConnectorConfiguration({
configuration: config,
connectorId: connector.id,
});
}}
/>
<EuiSpacer size="m" />
{isSyncing && (
<EuiProgress size="xs" position="absolute" style={{ top: 'calc(100% - 2px)' }} />
)}
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel hasShadow={false} hasBorder paddingSize="l" color="plain">
<EuiText>
<h3>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.configurationStep.h4.finishUpLabel',
{
defaultMessage: 'Finish up',
}
)}
</h3>
</EuiText>
<EuiSpacer size="m" />
<EuiText color={isNextStepEnabled ? 'default' : 'subdued'} size="s">
<p>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.configurationStep.p.description',
{
defaultMessage:
'You can manually sync your data, schedule a recurring sync or manage your domains.',
}
)}
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiButton
data-test-subj="enterpriseSearchStartStepGenerateConfigurationButton"
onClick={() => setCurrentStep('finish')}
fill
>
{Constants.NEXT_BUTTON_LABEL}
</EuiButton>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};

View file

@ -0,0 +1,265 @@
/*
* 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 { css } from '@emotion/react';
import { useActions, useValues } from 'kea';
import {
EuiBadge,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiIcon,
EuiLink,
EuiPanel,
EuiSpacer,
EuiSteps,
EuiSuperSelect,
EuiText,
useEuiTheme,
} from '@elastic/eui';
import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps';
import { i18n } from '@kbn/i18n';
import { AddConnectorApiLogic } from '../../../api/connector/add_connector_api_logic';
import { EnterpriseSearchContentPageTemplate } from '../../layout';
import { NewConnectorLogic } from '../../new_index/method_connector/new_connector_logic';
import { errorToText } from '../../new_index/utils/error_to_text';
import { connectorsBreadcrumbs } from '../connectors';
import { generateStepState } from '../utils/generate_step_state';
import connectorsBackgroundImage from './assets/connector_logos_comp.png';
import { ConfigurationStep } from './configuration_step';
import { DeploymentStep } from './deployment_step';
import { FinishUpStep } from './finish_up_step';
import { StartStep } from './start_step';
export type ConnectorCreationSteps = 'start' | 'deployment' | 'configure' | 'finish';
export type SelfManagePreference = 'native' | 'selfManaged';
export const CreateConnector: React.FC = () => {
const { error } = useValues(AddConnectorApiLogic);
const { euiTheme } = useEuiTheme();
const [selfManagePreference, setSelfManagePreference] = useState<SelfManagePreference>('native');
const { selectedConnector, currentStep } = useValues(NewConnectorLogic);
const { setCurrentStep } = useActions(NewConnectorLogic);
const stepStates = generateStepState(currentStep);
useEffect(() => {
// TODO: separate this to ability and preference
if (!selectedConnector?.isNative || !selfManagePreference) {
setSelfManagePreference('selfManaged');
} else {
setSelfManagePreference('native');
}
}, [selectedConnector]);
const getSteps = (selfManaged: boolean): EuiContainedStepProps[] => {
return [
{
children: null,
status: stepStates.start,
title: i18n.translate('xpack.enterpriseSearch.createConnector.startStep.startLabel', {
defaultMessage: 'Start',
}),
},
...(selfManaged
? [
{
children: null,
status: stepStates.deployment,
title: i18n.translate(
'xpack.enterpriseSearch.createConnector.deploymentStep.deploymentLabel',
{ defaultMessage: 'Deployment' }
),
},
]
: []),
{
children: null,
status: stepStates.configure,
title: i18n.translate(
'xpack.enterpriseSearch.createConnector.configurationStep.configurationLabel',
{ defaultMessage: 'Configuration' }
),
},
{
children: null,
status: stepStates.finish,
title: i18n.translate('xpack.enterpriseSearch.createConnector.finishUpStep.finishUpLabel', {
defaultMessage: 'Finish up',
}),
},
];
};
const stepContent: Record<'start' | 'deployment' | 'configure' | 'finish', React.ReactNode> = {
configure: (
<ConfigurationStep
title={i18n.translate(
'xpack.enterpriseSearch.createConnector.configurationStep.configurationLabel',
{ defaultMessage: 'Configuration' }
)}
setCurrentStep={setCurrentStep}
/>
),
deployment: <DeploymentStep setCurrentStep={setCurrentStep} />,
finish: (
<FinishUpStep
title={i18n.translate('xpack.enterpriseSearch.createConnector.finishUpStep.finishUpLabel', {
defaultMessage: 'Finish up',
})}
/>
),
start: (
<StartStep
title={i18n.translate('xpack.enterpriseSearch.createConnector.startStep.startLabel', {
defaultMessage: 'Start',
})}
selfManagePreference={selfManagePreference}
setCurrentStep={setCurrentStep}
onSelfManagePreferenceChange={(preference) => {
setSelfManagePreference(preference);
}}
error={errorToText(error)}
/>
),
};
return (
<EnterpriseSearchContentPageTemplate
pageChrome={[
...connectorsBreadcrumbs,
i18n.translate('xpack.enterpriseSearch.createConnector..breadcrumb', {
defaultMessage: 'New connector',
}),
]}
pageViewTelemetry="create_connector"
isLoading={false}
pageHeader={{
description: i18n.translate('xpack.enterpriseSearch.createConnector.description', {
defaultMessage: 'Extract, transform, index and sync data from a third-party data source.',
}),
pageTitle: i18n.translate('xpack.enterpriseSearch.createConnector..title', {
defaultMessage: 'Create a connector',
}),
}}
>
<EuiFlexGroup gutterSize="m">
{/* Col 1 */}
<EuiFlexItem grow={2}>
<EuiPanel
hasShadow={false}
hasBorder
color="subdued"
paddingSize="l"
css={css`
${currentStep === 'start'
? `background-image: url(${connectorsBackgroundImage});`
: ''}
background-size: contain;
background-repeat: no-repeat;
background-position: bottom center;
min-height: 550px;
border: 1px solid ${euiTheme.colors.lightShade};
`}
>
<EuiSteps
titleSize="xxs"
steps={getSteps(selfManagePreference === 'selfManaged')}
css={() => css`
.euiStep__content {
padding-block-end: ${euiTheme.size.xs};
}
`}
/>
<EuiSpacer size="xl" />
{selectedConnector?.docsUrl && selectedConnector?.docsUrl !== '' && (
<>
<EuiText size="s">
<p>
<EuiLink
external
data-test-subj="enterpriseSearchCreateConnectorConnectorDocsLink"
href={selectedConnector?.docsUrl}
target="_blank"
>
{'Elastic '}
{selectedConnector?.name}
{i18n.translate(
'xpack.enterpriseSearch.createConnector.connectorDocsLinkLabel',
{ defaultMessage: ' connector reference' }
)}
</EuiLink>
</p>
</EuiText>
<EuiSpacer size="s" />
</>
)}
{currentStep !== 'start' && (
<>
<EuiFormRow
label={i18n.translate(
'xpack.enterpriseSearch.createConnector.euiFormRow.connectorLabel',
{ defaultMessage: 'Connector' }
)}
>
<EuiSuperSelect
readOnly
valueOfSelected="item1"
options={[
{
inputDisplay: (
<>
<EuiIcon
size="l"
type={selectedConnector?.iconPath ?? ''}
css={css`
margin-right: ${euiTheme.size.m};
`}
/>
{selectedConnector?.name}
</>
),
value: 'item1',
},
]}
/>
</EuiFormRow>
<EuiSpacer size="s" />
<EuiBadge color="hollow">
{selfManagePreference
? i18n.translate(
'xpack.enterpriseSearch.createConnector.badgeType.selfManaged',
{
defaultMessage: 'Self managed',
}
)
: i18n.translate(
'xpack.enterpriseSearch.createConnector.badgeType.ElasticManaged',
{
defaultMessage: 'Elastic managed',
}
)}
</EuiBadge>
</>
)}
</EuiPanel>
</EuiFlexItem>
{/* Col 2 */}
<EuiFlexItem grow={7}>{stepContent[currentStep]}</EuiFlexItem>
</EuiFlexGroup>
</EnterpriseSearchContentPageTemplate>
);
};

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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect } from 'react';
import { useValues } from 'kea';
import { EuiFlexItem, EuiPanel, EuiSpacer, EuiText, EuiButton, EuiFlexGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ConnectorStatus } from '@kbn/search-connectors';
import * as Constants from '../../../../shared/constants';
import { ConnectorViewLogic } from '../../connector_detail/connector_view_logic';
import { ConnectorDeployment } from '../../connector_detail/deployment';
interface DeploymentStepProps {
setCurrentStep: Function;
}
export const DeploymentStep: React.FC<DeploymentStepProps> = ({ setCurrentStep }) => {
const { connector } = useValues(ConnectorViewLogic);
const isNextStepEnabled =
connector && !(!connector.status || connector.status === ConnectorStatus.CREATED);
useEffect(() => {
setTimeout(() => {
window.scrollTo({
behavior: 'smooth',
top: 0,
});
}, 100);
}, []);
return (
<EuiFlexGroup gutterSize="m" direction="column">
<ConnectorDeployment />
<EuiFlexItem>
<EuiPanel
hasShadow={false}
hasBorder
paddingSize="l"
color={isNextStepEnabled ? 'plain' : 'subdued'}
>
<EuiText color={isNextStepEnabled ? 'default' : 'subdued'}>
<h3>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.DeploymentStep.Configuration.title',
{
defaultMessage: 'Configuration',
}
)}
</h3>
</EuiText>
<EuiSpacer size="m" />
<EuiText color={isNextStepEnabled ? 'default' : 'subdued'} size="s">
<p>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.DeploymentStep.Configuration.description',
{
defaultMessage: 'Now configure your Elastic crawler and sync the data.',
}
)}
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiButton
data-test-subj="enterpriseSearchStartStepGenerateConfigurationButton"
onClick={() => setCurrentStep('configure')}
fill
disabled={!isNextStepEnabled}
>
{Constants.NEXT_BUTTON_LABEL}
</EuiButton>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,348 @@
/*
* 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 { css } from '@emotion/react';
import { useActions, useValues } from 'kea';
import {
EuiButton,
EuiCard,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiIcon,
EuiPanel,
EuiSpacer,
EuiTitle,
useEuiTheme,
EuiProgress,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { APPLICATIONS_PLUGIN } from '../../../../../../common/constants';
import { KibanaDeps } from '../../../../../../common/types';
import { PLAYGROUND_PATH } from '../../../../applications/routes';
import { generateEncodedPath } from '../../../../shared/encode_path_params';
import { KibanaLogic } from '../../../../shared/kibana';
import { CONNECTOR_DETAIL_TAB_PATH } from '../../../routes';
import { ConnectorDetailTabId } from '../../connector_detail/connector_detail';
import { ConnectorViewLogic } from '../../connector_detail/connector_view_logic';
import { IndexViewLogic } from '../../search_index/index_view_logic';
import { SyncsLogic } from '../../shared/header_actions/syncs_logic';
import connectorLogo from './assets/connector_logo.svg';
interface FinishUpStepProps {
title: string;
}
export const FinishUpStep: React.FC<FinishUpStepProps> = ({ title }) => {
const { euiTheme } = useEuiTheme();
const {
services: { discover },
} = useKibana<KibanaDeps>();
const [showNext, setShowNext] = useState(false);
const { isWaitingForSync, isSyncing: isSyncingProp } = useValues(IndexViewLogic);
const { connector } = useValues(ConnectorViewLogic);
const { startSync } = useActions(SyncsLogic);
const isSyncing = isWaitingForSync || isSyncingProp;
useEffect(() => {
setTimeout(() => {
window.scrollTo({
behavior: 'smooth',
top: 0,
});
}, 100);
}, []);
return (
<>
<EuiFlexGroup gutterSize="m" direction="column">
<EuiFlexItem>
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
<EuiTitle size="m">
<h3>{title}</h3>
</EuiTitle>
<EuiSpacer size="xl" />
{isSyncing && (
<>
<EuiFlexGroup gutterSize="m">
<EuiFlexItem>
<EuiText size="xs">
{i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.syncingDataTextLabel',
{
defaultMessage: 'Syncing data',
}
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
<EuiProgress
size="s"
color="success"
onClick={() => {
setShowNext(true);
}}
/>
<EuiSpacer size="xl" />
</>
)}
<EuiFlexItem>
<EuiFlexGroup gutterSize="m">
<EuiFlexItem>
<EuiCard
icon={<EuiIcon size="xxl" type="machineLearningApp" />}
titleSize="s"
title={i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.chatWithYourDataLabel',
{ defaultMessage: 'Chat with your data' }
)}
description={i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.chatWithYourDataDescriptionl',
{
defaultMessage:
'Combine your data with the power of LLMs for retrieval augmented generation (RAG)',
}
)}
footer={
showNext ? (
<EuiButton
data-test-subj="enterpriseSearchFinishUpStepStartSearchPlaygroundButton"
aria-label={i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.euiButton.startSearchPlaygroundLabel',
{ defaultMessage: 'Start Search Playground' }
)}
onClick={() => {
if (connector) {
KibanaLogic.values.navigateToUrl(
`${APPLICATIONS_PLUGIN.URL}${PLAYGROUND_PATH}?default-index=${connector.index_name}`,
{ shouldNotCreateHref: true }
);
}
}}
>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.startSearchPlaygroundButtonLabel',
{ defaultMessage: 'Start Search Playground' }
)}
</EuiButton>
) : (
<EuiButton
data-test-subj="enterpriseSearchFinishUpStepButton"
color="warning"
iconSide="left"
iconType="refresh"
isLoading={isSyncing}
aria-label={i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.euiButton.firstSyncDataLabel',
{ defaultMessage: 'First sync data' }
)}
onClick={() => {
startSync(connector);
setShowNext(true);
}}
>
{isSyncing ? 'Syncing data' : 'First sync data'}
</EuiButton>
)
}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
icon={<EuiIcon size="xxl" type="discoverApp" />}
titleSize="s"
title={i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.exploreYourDataLabel',
{ defaultMessage: 'Explore your data' }
)}
description={i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.exploreYourDataDescription',
{
defaultMessage:
'See your connector documents or make a data view to explore them',
}
)}
footer={
showNext ? (
<EuiButton
data-test-subj="enterpriseSearchFinishUpStepViewInDiscoverButton"
aria-label={i18n.translate(
'xpack.enterpriseSearch.finishUpStep.euiButton.viewInDiscoverLabel',
{ defaultMessage: 'View in Discover' }
)}
onClick={() => {
discover.locator?.navigate({
dataViewSpec: {
title: connector?.name,
},
indexPattern: connector?.index_name,
title: connector?.name,
});
}}
>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.viewInDiscoverButtonLabel',
{ defaultMessage: 'View in Discover' }
)}
</EuiButton>
) : (
<EuiButton
data-test-subj="enterpriseSearchFinishUpStepButton"
color="warning"
iconSide="left"
iconType="refresh"
isLoading={isSyncing}
aria-label={i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.euiButton.firstSyncDataLabel',
{ defaultMessage: 'First sync data' }
)}
onClick={() => {
startSync(connector);
setShowNext(true);
}}
>
{isSyncing ? 'Syncing data' : 'First sync data'}
</EuiButton>
)
}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
icon={<EuiIcon size="xxl" type={connectorLogo} />}
titleSize="s"
title={i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.manageYourConnectorLabel',
{ defaultMessage: 'Manage your connector' }
)}
description={i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.manageYourConnectorDescription',
{
defaultMessage:
'Now you can manage your connector, schedule a sync and much more',
}
)}
footer={
<EuiFlexGroup responsive={false} gutterSize="xs" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="enterpriseSearchFinishUpStepManageConnectorButton"
size="m"
fill
onClick={() => {
if (connector) {
KibanaLogic.values.navigateToUrl(
generateEncodedPath(CONNECTOR_DETAIL_TAB_PATH, {
connectorId: connector.id,
tabId: ConnectorDetailTabId.CONFIGURATION,
})
);
}
}}
>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.manageConnectorButtonLabel',
{ defaultMessage: 'Manage connector' }
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiHorizontalRule margin="xl" />
<EuiTitle size="s">
<h3>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.h3.queryYourDataLabel',
{
defaultMessage: 'Query your data',
}
)}
</h3>
</EuiTitle>
<EuiSpacer size="l" />
<EuiFlexGroup gutterSize="m">
<EuiFlexItem>
<EuiCard
layout="horizontal"
icon={
<EuiIcon
css={() => css`
margin-top: ${euiTheme.size.xs};
`}
size="m"
type="visVega"
/>
}
title={i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.queryWithLanguageClientsLabel',
{ defaultMessage: 'Query with language clients' }
)}
titleSize="xs"
description={i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.queryWithLanguageClientsLDescription',
{
defaultMessage:
'Use your favorite language client to query your data in your app',
}
)}
onClick={() => {}}
display="subdued"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
layout="horizontal"
icon={
<EuiIcon
css={() => css`
margin-top: ${euiTheme.size.xs};
`}
size="m"
type="console"
/>
}
title={i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.devToolsLabel',
{ defaultMessage: 'Dev tools' }
)}
titleSize="xs"
description={i18n.translate(
'xpack.enterpriseSearch.createConnector.finishUpStep.euiCard.devToolsDescription',
{
defaultMessage:
'Tools for interacting with your data, such as console, profiler, Grok debugger and more',
}
)}
onClick={() => {}}
display="subdued"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};

View file

@ -0,0 +1,8 @@
/*
* 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 { CreateConnector } from './create_connector';

View file

@ -0,0 +1,340 @@
/*
* 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, { ChangeEvent } from 'react';
import { useActions, useValues } from 'kea';
import {
EuiButton,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiPanel,
EuiRadio,
EuiSpacer,
EuiText,
EuiTitle,
useGeneratedHtmlId,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import * as Constants from '../../../../shared/constants';
import { GeneratedConfigFields } from '../../connector_detail/components/generated_config_fields';
import { ConnectorViewLogic } from '../../connector_detail/connector_view_logic';
import { NewConnectorLogic } from '../../new_index/method_connector/new_connector_logic';
import { ChooseConnectorSelectable } from './components/choose_connector_selectable';
import { ConnectorDescriptionPopover } from './components/connector_description_popover';
import { ManualConfiguration } from './components/manual_configuration';
import { SelfManagePreference } from './create_connector';
interface StartStepProps {
error?: string | React.ReactNode;
onSelfManagePreferenceChange(preference: SelfManagePreference): void;
selfManagePreference: SelfManagePreference;
setCurrentStep: Function;
title: string;
}
export const StartStep: React.FC<StartStepProps> = ({
title,
selfManagePreference,
setCurrentStep,
onSelfManagePreferenceChange,
error,
}) => {
const elasticManagedRadioButtonId = useGeneratedHtmlId({ prefix: 'elasticManagedRadioButton' });
const selfManagedRadioButtonId = useGeneratedHtmlId({ prefix: 'selfManagedRadioButton' });
const {
rawName,
canConfigureConnector,
selectedConnector,
generatedConfigData,
isGenerateLoading,
isCreateLoading,
} = useValues(NewConnectorLogic);
const { setRawName, createConnector, generateConnectorName } = useActions(NewConnectorLogic);
const { connector } = useValues(ConnectorViewLogic);
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
setRawName(e.target.value);
};
return (
<EuiForm component="form" id="enterprise-search-create-connector">
<EuiFlexGroup gutterSize="m" direction="column">
<EuiFlexItem>
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
<EuiTitle size="m">
<h3>{title}</h3>
</EuiTitle>
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
fullWidth
label={i18n.translate(
'xpack.enterpriseSearch.createConnector.startStep.euiFormRow.connectorLabel',
{ defaultMessage: 'Connector' }
)}
>
<ChooseConnectorSelectable selfManaged={selfManagePreference} />
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
fullWidth
isInvalid={!!error}
label={i18n.translate(
'xpack.enterpriseSearch.createConnector.startStep.euiFormRow.connectorNameLabel',
{ defaultMessage: 'Connector name' }
)}
>
<EuiFieldText
data-test-subj="enterpriseSearchStartStepFieldText"
fullWidth
name="first"
value={rawName}
onChange={handleNameChange}
onBlur={() => {
if (selectedConnector) {
generateConnectorName({
connectorName: rawName,
connectorType: selectedConnector.serviceType,
});
}
}}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiFlexItem>
<EuiFormRow
fullWidth
label={i18n.translate(
'xpack.enterpriseSearch.createConnector.startStep.euiFormRow.descriptionLabel',
{ defaultMessage: 'Description' }
)}
>
<EuiFieldText
data-test-subj="enterpriseSearchStartStepFieldText"
fullWidth
name="first"
/>
</EuiFormRow>
</EuiFlexItem>
</EuiPanel>
</EuiFlexItem>
{/* Set up */}
<EuiFlexItem>
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
<EuiTitle size="s">
<h4>
{i18n.translate('xpack.enterpriseSearch.createConnector.startStep.h4.setUpLabel', {
defaultMessage: 'Set up',
})}
</h4>
</EuiTitle>
<EuiSpacer size="m" />
<EuiText size="s">
<p>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.startStep.p.whereDoYouWantLabel',
{
defaultMessage:
'Where do you want to store the connector and how do you want to manage it?',
}
)}
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiRadio
id={elasticManagedRadioButtonId}
label={i18n.translate(
'xpack.enterpriseSearch.createConnector.startStep.euiRadio.elasticManagedLabel',
{ defaultMessage: 'Elastic managed' }
)}
checked={selfManagePreference === 'native'}
disabled={selectedConnector?.isNative === false}
onChange={() => onSelfManagePreferenceChange('native')}
name="setUp"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ConnectorDescriptionPopover
isDisabled={selectedConnector?.isNative === false}
isNative
/>
</EuiFlexItem>
&nbsp; &nbsp;
<EuiFlexItem grow={false}>
<EuiRadio
id={selfManagedRadioButtonId}
label={i18n.translate(
'xpack.enterpriseSearch.createConnector.startStep.euiRadio.selfManagedLabel',
{ defaultMessage: 'Self managed' }
)}
checked={selfManagePreference === 'selfManaged'}
onChange={() => onSelfManagePreferenceChange('selfManaged')}
name="setUp"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ConnectorDescriptionPopover isDisabled={false} isNative={false} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
{selfManagePreference === 'selfManaged' ? (
<EuiFlexItem>
<EuiPanel
hasShadow={false}
hasBorder
paddingSize="l"
color={selectedConnector?.name ? 'plain' : 'subdued'}
>
<EuiText color={selectedConnector?.name ? 'default' : 'subdued'}>
<h3>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.startStep.h4.deploymentLabel',
{
defaultMessage: 'Deployment',
}
)}
</h3>
</EuiText>
<EuiSpacer size="m" />
<EuiText color={selectedConnector?.name ? 'default' : 'subdued'} size="s">
<p>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.startStep.p.youWillStartTheLabel',
{
defaultMessage:
'You will start the process of creating a new index, API key, and a Web Crawler Connector ID manually. Optionally you can bring your own configuration as well.',
}
)}
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiButton
data-test-subj="enterpriseSearchStartStepNextButton"
onClick={() => {
if (selectedConnector && selectedConnector.name) {
createConnector({
isSelfManaged: true,
});
setCurrentStep('deployment');
}
}}
fill
disabled={!canConfigureConnector}
isLoading={isCreateLoading || isGenerateLoading}
>
{Constants.NEXT_BUTTON_LABEL}
</EuiButton>
</EuiPanel>
</EuiFlexItem>
) : (
<EuiFlexItem>
<EuiPanel
color={selectedConnector?.name ? 'plain' : 'subdued'}
hasShadow={false}
hasBorder
paddingSize="l"
>
<EuiText color={selectedConnector?.name ? 'default' : 'subdued'}>
<h3>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.startStep.h4.configureIndexAndAPILabel',
{
defaultMessage: 'Configure index and API key',
}
)}
</h3>
</EuiText>
<EuiSpacer size="m" />
<EuiText color={selectedConnector?.name ? 'default' : 'subdued'} size="s">
<p>
{i18n.translate(
'xpack.enterpriseSearch.createConnector.startStep.p.thisProcessWillCreateLabel',
{
defaultMessage:
'This process will create a new index, API key, and a Connector ID. Optionally you can bring your own configuration as well.',
}
)}
</p>
</EuiText>
<EuiSpacer size="m" />
{generatedConfigData && connector ? (
<>
<GeneratedConfigFields
apiKey={{
api_key: generatedConfigData.apiKey.api_key,
encoded: generatedConfigData.apiKey.encoded,
id: generatedConfigData.apiKey.id,
name: generatedConfigData.apiKey.name,
}}
connector={connector}
isGenerateLoading={false}
/>
<EuiSpacer size="m" />
<EuiButton
data-test-subj="enterpriseSearchStartStepGenerateConfigurationButton"
fill
onClick={() => setCurrentStep('configure')}
>
{Constants.NEXT_BUTTON_LABEL}
</EuiButton>
</>
) : (
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="entSearchContent-connector-configuration-generateConfigButton"
data-telemetry-id="entSearchContent-connector-configuration-generateConfigButton"
disabled={!canConfigureConnector}
fill
iconType="sparkles"
isLoading={isGenerateLoading || isCreateLoading}
onClick={() => {
createConnector({
isSelfManaged: false,
});
}}
>
{i18n.translate(
'xpack.enterpriseSearch.content.connector_detail.configurationConnector.steps.generateApiKey.button.label',
{
defaultMessage: 'Generate configuration',
}
)}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ManualConfiguration
isDisabled={isGenerateLoading || isCreateLoading || !canConfigureConnector}
selfManagePreference={selfManagePreference}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiPanel>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiForm>
);
};

View file

@ -0,0 +1,29 @@
/*
* 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 { EuiStepStatus } from '@elastic/eui';
type Steps = 'start' | 'configure' | 'deployment' | 'finish';
export const generateStepState = (currentStep: Steps): { [key in Steps]: EuiStepStatus } => {
return {
configure:
currentStep === 'start' || currentStep === 'deployment'
? 'incomplete'
: currentStep === 'configure'
? 'current'
: 'complete',
deployment:
currentStep === 'deployment'
? 'current'
: currentStep === 'finish' || currentStep === 'configure'
? 'complete'
: 'incomplete',
finish: currentStep === 'finish' ? 'current' : 'incomplete',
start: currentStep === 'start' ? 'current' : 'complete',
};
};

View file

@ -7,65 +7,214 @@
import { kea, MakeLogicType } from 'kea';
import { Connector } from '@kbn/search-connectors';
import { ConnectorDefinition } from '@kbn/search-connectors-plugin/public';
import { Status } from '../../../../../../common/types/api';
import { Actions } from '../../../../shared/api_logic/create_api_logic';
import { generateEncodedPath } from '../../../../shared/encode_path_params';
import { KibanaLogic } from '../../../../shared/kibana';
import {
AddConnectorApiLogic,
AddConnectorApiLogicActions,
AddConnectorApiLogicArgs,
AddConnectorApiLogicResponse,
} from '../../../api/connector/add_connector_api_logic';
import {
IndexExistsApiLogic,
IndexExistsApiParams,
IndexExistsApiResponse,
} from '../../../api/index/index_exists_api_logic';
GenerateConfigApiActions,
GenerateConfigApiLogic,
} from '../../../api/connector/generate_connector_config_api_logic';
import {
GenerateConnectorNamesApiLogic,
GenerateConnectorNamesApiLogicActions,
GenerateConnectorNamesApiResponse,
} from '../../../api/connector/generate_connector_names_api_logic';
import { APIKeyResponse } from '../../../api/generate_api_key/generate_api_key_logic';
import { isValidIndexName } from '../../../utils/validate_index_name';
import { UNIVERSAL_LANGUAGE_VALUE } from '../constants';
import { LanguageForOptimization } from '../types';
import { getLanguageForOptimization } from '../utils';
import { CONNECTOR_DETAIL_TAB_PATH } from '../../../routes';
import {
ConnectorViewActions,
ConnectorViewLogic,
} from '../../connector_detail/connector_view_logic';
import { ConnectorCreationSteps } from '../../connectors/create_connector/create_connector';
import { SearchIndexTabId } from '../../search_index/search_index';
export interface NewConnectorValues {
data: IndexExistsApiResponse;
fullIndexName: string;
fullIndexNameExists: boolean;
fullIndexNameIsValid: boolean;
language: LanguageForOptimization;
languageSelectValue: string;
canConfigureConnector: boolean;
connectorId: string;
connectorName: string;
createConnectorApiStatus: Status;
currentStep: ConnectorCreationSteps;
generateConfigurationStatus: Status;
generatedConfigData:
| {
apiKey: APIKeyResponse['apiKey'];
connectorId: Connector['id'];
indexName: string;
}
| undefined;
generatedNameData: GenerateConnectorNamesApiResponse | undefined;
isCreateLoading: boolean;
isGenerateLoading: boolean;
rawName: string;
selectedConnector: ConnectorDefinition | null;
shouldGenerateConfigAfterCreate: boolean;
}
type NewConnectorActions = Pick<
Actions<IndexExistsApiParams, IndexExistsApiResponse>,
'makeRequest'
> & {
type NewConnectorActions = {
generateConnectorName: GenerateConnectorNamesApiLogicActions['makeRequest'];
} & {
configurationGenerated: GenerateConfigApiActions['apiSuccess'];
generateConfiguration: GenerateConfigApiActions['makeRequest'];
} & {
connectorCreated: Actions<AddConnectorApiLogicArgs, AddConnectorApiLogicResponse>['apiSuccess'];
setLanguageSelectValue(language: string): { language: string };
createConnector: ({
isSelfManaged,
shouldGenerateAfterCreate,
shouldNavigateToConnectorAfterCreate,
}: {
isSelfManaged: boolean;
shouldGenerateAfterCreate?: boolean;
shouldNavigateToConnectorAfterCreate?: boolean;
}) => {
isSelfManaged: boolean;
shouldGenerateAfterCreate?: boolean;
shouldNavigateToConnectorAfterCreate?: boolean;
};
createConnectorApi: AddConnectorApiLogicActions['makeRequest'];
fetchConnector: ConnectorViewActions['fetchConnector'];
setCurrentStep(step: ConnectorCreationSteps): { step: ConnectorCreationSteps };
setRawName(rawName: string): { rawName: string };
setSelectedConnector(connector: ConnectorDefinition | null): {
connector: ConnectorDefinition | null;
};
};
export const NewConnectorLogic = kea<MakeLogicType<NewConnectorValues, NewConnectorActions>>({
actions: {
setLanguageSelectValue: (language) => ({ language }),
createConnector: ({
isSelfManaged,
shouldGenerateAfterCreate,
shouldNavigateToConnectorAfterCreate,
}) => ({
isSelfManaged,
shouldGenerateAfterCreate,
shouldNavigateToConnectorAfterCreate,
}),
setCurrentStep: (step) => ({ step }),
setRawName: (rawName) => ({ rawName }),
setSelectedConnector: (connector) => ({ connector }),
},
connect: {
actions: [
GenerateConnectorNamesApiLogic,
['makeRequest as generateConnectorName', 'apiSuccess as connectorNameGenerated'],
AddConnectorApiLogic,
['apiSuccess as connectorCreated'],
IndexExistsApiLogic,
['makeRequest'],
['makeRequest as createConnectorApi', 'apiSuccess as connectorCreated'],
GenerateConfigApiLogic,
['makeRequest as generateConfiguration', 'apiSuccess as configurationGenerated'],
ConnectorViewLogic,
['fetchConnector'],
],
values: [
GenerateConnectorNamesApiLogic,
['data as generatedNameData'],
GenerateConfigApiLogic,
['data as generatedConfigData', 'status as generateConfigurationStatus'],
AddConnectorApiLogic,
['status as createConnectorApiStatus'],
],
values: [IndexExistsApiLogic, ['data']],
},
path: ['enterprise_search', 'content', 'new_search_index'],
listeners: ({ actions, values }) => ({
connectorCreated: ({ id, uiFlags }) => {
if (uiFlags?.shouldNavigateToConnectorAfterCreate) {
KibanaLogic.values.navigateToUrl(
generateEncodedPath(CONNECTOR_DETAIL_TAB_PATH, {
connectorId: id,
tabId: SearchIndexTabId.CONFIGURATION,
})
);
} else {
actions.fetchConnector({ connectorId: id });
if (!uiFlags || uiFlags.shouldGenerateAfterCreate) {
actions.generateConfiguration({ connectorId: id });
}
}
},
connectorNameGenerated: ({ connectorName }) => {
if (!values.rawName) {
actions.setRawName(connectorName);
}
},
createConnector: ({
isSelfManaged,
shouldGenerateAfterCreate = true,
shouldNavigateToConnectorAfterCreate = false,
}) => {
if (
!values.rawName &&
values.selectedConnector &&
values.connectorName &&
values.generatedNameData
) {
// name is generated, use everything generated
actions.createConnectorApi({
deleteExistingConnector: false,
indexName: values.connectorName,
isNative: !values.selectedConnector.isNative ? false : !isSelfManaged,
language: null,
name: values.generatedNameData.connectorName,
serviceType: values.selectedConnector.serviceType,
uiFlags: {
shouldGenerateAfterCreate,
shouldNavigateToConnectorAfterCreate,
},
});
} else {
if (values.generatedNameData && values.selectedConnector) {
actions.createConnectorApi({
deleteExistingConnector: false,
indexName: values.generatedNameData.indexName,
isNative: !values.selectedConnector.isNative ? false : !isSelfManaged,
language: null,
name: values.connectorName,
serviceType: values.selectedConnector?.serviceType,
uiFlags: {
shouldGenerateAfterCreate,
shouldNavigateToConnectorAfterCreate,
},
});
}
}
},
setSelectedConnector: ({ connector }) => {
if (connector) {
actions.generateConnectorName({
connectorName: values.rawName,
connectorType: connector.serviceType,
});
}
},
}),
path: ['enterprise_search', 'content', 'new_search_connector'],
reducers: {
languageSelectValue: [
UNIVERSAL_LANGUAGE_VALUE,
connectorId: [
'',
{
// @ts-expect-error upgrade typescript v5.1.6
setLanguageSelectValue: (_, { language }) => language ?? null,
connectorCreated: (
_: NewConnectorValues['connectorId'],
{ id }: { id: NewConnectorValues['connectorId'] }
) => id,
},
],
currentStep: [
'start',
{
setCurrentStep: (
_: NewConnectorValues['currentStep'],
{ step }: { step: NewConnectorValues['currentStep'] }
) => step,
},
],
rawName: [
@ -75,21 +224,34 @@ export const NewConnectorLogic = kea<MakeLogicType<NewConnectorValues, NewConnec
setRawName: (_, { rawName }) => rawName,
},
],
selectedConnector: [
null,
{
setSelectedConnector: (
_: NewConnectorValues['selectedConnector'],
{ connector }: { connector: NewConnectorValues['selectedConnector'] }
) => connector,
},
],
},
selectors: ({ selectors }) => ({
fullIndexName: [() => [selectors.rawName], (name: string) => name],
fullIndexNameExists: [
() => [selectors.data, selectors.fullIndexName],
(data: IndexExistsApiResponse | undefined, fullIndexName: string) =>
data?.exists === true && data.indexName === fullIndexName,
canConfigureConnector: [
() => [selectors.connectorName, selectors.selectedConnector],
(connectorName: string, selectedConnector: NewConnectorValues['selectedConnector']) =>
(connectorName && selectedConnector?.name) ?? false,
],
fullIndexNameIsValid: [
() => [selectors.fullIndexName],
(fullIndexName) => isValidIndexName(fullIndexName),
connectorName: [
() => [selectors.rawName, selectors.generatedNameData],
(name: string, generatedName: NewConnectorValues['generatedNameData']) =>
name ? name : generatedName?.connectorName ?? '',
],
language: [
() => [selectors.languageSelectValue],
(languageSelectValue) => getLanguageForOptimization(languageSelectValue),
isCreateLoading: [
() => [selectors.createConnectorApiStatus],
(status) => status === Status.LOADING,
],
isGenerateLoading: [
() => [selectors.generateConfigurationStatus],
(status) => status === Status.LOADING,
],
}),
});

View file

@ -54,44 +54,17 @@ export const NewConnectorTemplate: React.FC<Props> = ({
type,
isBeta,
}) => {
const { fullIndexName, fullIndexNameExists, fullIndexNameIsValid, rawName } =
useValues(NewConnectorLogic);
const { connectorName, rawName } = useValues(NewConnectorLogic);
const { setRawName } = useActions(NewConnectorLogic);
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
setRawName(e.target.value);
if (onNameChange) {
onNameChange(fullIndexName);
onNameChange(connectorName);
}
};
const formInvalid = !!error || fullIndexNameExists || !fullIndexNameIsValid;
const formError = () => {
if (fullIndexNameExists) {
return i18n.translate(
'xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.alreadyExists.error',
{
defaultMessage: 'A connector with the name {connectorName} already exists',
values: {
connectorName: fullIndexName,
},
}
);
}
if (!fullIndexNameIsValid) {
return i18n.translate(
'xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.isInvalid.error',
{
defaultMessage: '{connectorName} is an invalid connector name',
values: {
connectorName: fullIndexName,
},
}
);
}
return error;
};
const formInvalid = !!error;
return (
<>
@ -100,7 +73,7 @@ export const NewConnectorTemplate: React.FC<Props> = ({
id="enterprise-search-create-connector"
onSubmit={(event) => {
event.preventDefault();
onSubmit(fullIndexName);
onSubmit(connectorName);
}}
>
<EuiFlexGroup direction="column">
@ -131,10 +104,10 @@ export const NewConnectorTemplate: React.FC<Props> = ({
}
)}
isInvalid={formInvalid}
error={formError()}
fullWidth
>
<EuiFieldText
data-test-subj="enterpriseSearchNewConnectorTemplateFieldText"
data-telemetry-id={`entSearchContent-${type}-newConnector-editName`}
placeholder={i18n.translate(
'xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputPlaceholder',
@ -167,7 +140,11 @@ export const NewConnectorTemplate: React.FC<Props> = ({
<EuiFlexGroup direction="column" gutterSize="xs">
{type === INGESTION_METHOD_IDS.CONNECTOR && (
<EuiFlexItem grow={false}>
<EuiLink target="_blank" href={docLinks.connectors}>
<EuiLink
data-test-subj="enterpriseSearchNewConnectorTemplateLearnMoreAboutConnectorsLink"
target="_blank"
href={docLinks.connectors}
>
{i18n.translate(
'xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.learnMoreConnectors.linkText',
{
@ -182,6 +159,7 @@ export const NewConnectorTemplate: React.FC<Props> = ({
<EuiFlexGroup direction="row" alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="enterpriseSearchNewConnectorTemplateButton"
data-telemetry-id={`entSearchContent-${type}-newConnector-goBack`}
isDisabled={buttonLoading}
onClick={() => history.back()}

View file

@ -21,6 +21,7 @@ export const NEW_ES_INDEX_PATH = `${NEW_INDEX_PATH}/elasticsearch`;
export const NEW_DIRECT_UPLOAD_PATH = `${NEW_INDEX_PATH}/upload`;
export const NEW_INDEX_SELECT_CONNECTOR_PATH = `${CONNECTORS_PATH}/select_connector`;
export const NEW_CONNECTOR_PATH = `${CONNECTORS_PATH}/new_connector`;
export const NEW_CONNECTOR_FLOW_PATH = `${CONNECTORS_PATH}/new_connector_flow`;
export const NEW_CRAWLER_PATH = `${CRAWLERS_PATH}/new_crawler`;
export const NEW_INDEX_SELECT_CONNECTOR_NATIVE_PATH = `${CONNECTORS_PATH}/select_connector?filter=native`;
export const NEW_INDEX_SELECT_CONNECTOR_CLIENTS_PATH = `${CONNECTORS_PATH}/select_connector?filter=connector_clients`;

View file

@ -49,6 +49,10 @@ export const BACK_BUTTON_LABEL = i18n.translate('xpack.enterpriseSearch.actions.
defaultMessage: 'Back',
});
export const NEXT_BUTTON_LABEL = i18n.translate('xpack.enterpriseSearch.actions.nextButtonLabel', {
defaultMessage: 'Next',
});
export const CLOSE_BUTTON_LABEL = i18n.translate(
'xpack.enterpriseSearch.actions.closeButtonLabel',
{ defaultMessage: 'Close' }

View file

@ -16,24 +16,51 @@ import { indexOrAliasExists } from '../indices/exists_index';
export const generateConnectorName = async (
client: IScopedClusterClient,
connectorType: string
connectorType: string,
userConnectorName?: string
): Promise<{ apiKeyName: string; connectorName: string; indexName: string }> => {
const prefix = toAlphanumeric(connectorType);
if (!prefix || prefix.length === 0) {
throw new Error('Connector type is required');
throw new Error('Connector type or connectorName is required');
}
for (let i = 0; i < 20; i++) {
const connectorName = `${prefix}-${uuidv4().split('-')[1]}`;
const indexName = `connector-${connectorName}`;
const result = await indexOrAliasExists(client, indexName);
if (!result) {
if (userConnectorName) {
let indexName = `connector-${userConnectorName}`;
const resultSameName = await indexOrAliasExists(client, indexName);
// index with same name doesn't exist
if (!resultSameName) {
return {
apiKeyName: indexName,
connectorName,
apiKeyName: userConnectorName,
connectorName: userConnectorName,
indexName,
};
}
// if the index name already exists, we will generate until it doesn't for 20 times
for (let i = 0; i < 20; i++) {
indexName = `connector-${userConnectorName}-${uuidv4().split('-')[1].slice(0, 4)}`;
const result = await indexOrAliasExists(client, indexName);
if (!result) {
return {
apiKeyName: indexName,
connectorName: userConnectorName,
indexName,
};
}
}
} else {
for (let i = 0; i < 20; i++) {
const connectorName = `${prefix}-${uuidv4().split('-')[1].slice(0, 4)}`;
const indexName = `connector-${connectorName}`;
const result = await indexOrAliasExists(client, indexName);
if (!result) {
return {
apiKeyName: indexName,
connectorName,
indexName,
};
}
}
}
throw new Error(ErrorCode.GENERATE_INDEX_NAME_ERROR);
};

View file

@ -6,7 +6,6 @@
*/
import { schema } from '@kbn/config-schema';
import { ElasticsearchErrorDetails } from '@kbn/es-errors';
import { i18n } from '@kbn/i18n';
@ -841,15 +840,20 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
path: '/internal/enterprise_search/connectors/generate_connector_name',
validate: {
body: schema.object({
connectorName: schema.maybe(schema.string()),
connectorType: schema.string(),
}),
},
},
elasticsearchErrorHandler(log, async (context, request, response) => {
const { client } = (await context.core).elasticsearch;
const { connectorType } = request.body;
const { connectorType, connectorName } = request.body;
try {
const generatedNames = await generateConnectorName(client, connectorType ?? 'custom');
const generatedNames = await generateConnectorName(
client,
connectorType ?? 'custom',
connectorName
);
return response.ok({
body: generatedNames,
headers: { 'content-type': 'application/json' },

View file

@ -16858,10 +16858,8 @@
"xpack.enterpriseSearch.content.new_index.genericTitle": "Nouvel index de recherche",
"xpack.enterpriseSearch.content.new_index.successToast.title": "Lindex a bien été créé",
"xpack.enterpriseSearch.content.new_web_crawler.breadcrumbs": "Nouveau robot d'indexation",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.alreadyExists.error": "Un connecteur nommé {connectorName} existe déjà",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.createIndex.buttonText": "Créer un connecteur",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.formTitle": "Créer un connecteur",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.isInvalid.error": "{connectorName} est un nom de connecteur non valide",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.learnMoreConnectors.linkText": "En savoir plus sur les connecteurs",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputHelpText.lineTwo": "Les noms doivent être en minuscules et ne peuvent pas contenir d'espaces ni de caractères spéciaux.",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputLabel": "Nom du connecteur",

View file

@ -16604,10 +16604,8 @@
"xpack.enterpriseSearch.content.new_index.genericTitle": "新しい検索インデックス",
"xpack.enterpriseSearch.content.new_index.successToast.title": "インデックスが正常に作成されました",
"xpack.enterpriseSearch.content.new_web_crawler.breadcrumbs": "新しいWebクローラー",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.alreadyExists.error": "名前\"{connectorName}\"のコネクターはすでに存在しています",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.createIndex.buttonText": "コネクターを作成",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.formTitle": "コネクターを作成する",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.isInvalid.error": "{connectorName}は無効なコネクター名です",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.learnMoreConnectors.linkText": "コネクターの詳細",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputHelpText.lineTwo": "名前は小文字で入力してください。スペースや特殊文字は使用できません。",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputLabel": "コネクター名",

View file

@ -16633,10 +16633,8 @@
"xpack.enterpriseSearch.content.new_index.genericTitle": "新搜索索引",
"xpack.enterpriseSearch.content.new_index.successToast.title": "已成功创建索引",
"xpack.enterpriseSearch.content.new_web_crawler.breadcrumbs": "新网络爬虫",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.alreadyExists.error": "名为 {connectorName} 的连接器已存在",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.createIndex.buttonText": "创建连接器",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.formTitle": "创建连接器",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.isInvalid.error": "{connectorName} 为无效的连接器名称",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.learnMoreConnectors.linkText": "详细了解连接器",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputHelpText.lineTwo": "名称应为小写,并且不能包含空格或特殊字符。",
"xpack.enterpriseSearch.content.newConnector.newConnectorTemplate.nameInputLabel": "连接器名称",