[Search] Add Legacy App Search Gated Form (#189816)

## Summary

This PR adds Gated form and logic when user visits App Search , when
`kibana_uis_enabled == false` and the `role_type` is `owner`. The user
will not be able to able to access any other App Search routes other
than Engines Overview page until this form is submitted.

Also - removes App Search from the search providers so it will no longer
show up in the results.

(note - any migrated clusters will automatically allow access without
the gate on the backend - that is, it will already have set
`kibana_uis_enabled=true`)

Also updates a couple of links and CTA for Workplace Search to be
consistent with new App Search ones.

**Screen Recording:**


https://github.com/user-attachments/assets/8e3d73b3-52d4-4952-bb1e-49219c8cdaed

### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Mark J. Hoy 2024-08-20 13:18:09 -04:00 committed by GitHub
parent f98ef4c04a
commit 845d275d3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1099 additions and 40 deletions

View file

@ -41,6 +41,7 @@ export const DEFAULT_INITIAL_APP_DATA = {
},
appSearch: {
accountId: 'some-id-string',
kibanaUIsEnabled: true,
onboardingComplete: true,
role: {
id: 'account_id:somestring|user_oid:somestring',

View file

@ -7,6 +7,7 @@
export interface Account {
accountId: string;
kibanaUIsEnabled: boolean;
onboardingComplete: boolean;
role: {
id: string;

View file

@ -30,10 +30,12 @@ describe('AppLogic', () => {
},
account: {
accountId: 'some-id-string',
kibanaUIsEnabled: true,
onboardingComplete: true,
role: DEFAULT_INITIAL_APP_DATA.appSearch.role,
},
myRole: {},
showGateForm: false,
};
it('sets values from props', () => {
@ -48,6 +50,7 @@ describe('AppLogic', () => {
},
account: {
accountId: 'some-id-string',
kibanaUIsEnabled: true,
onboardingComplete: true,
role: DEFAULT_INITIAL_APP_DATA.appSearch.role,
},
@ -60,6 +63,7 @@ describe('AppLogic', () => {
canViewAccountCredentials: true,
// Truncated for brevity - see utils/role/index.test.ts for full output
}),
showGateForm: false,
});
});

View file

@ -16,10 +16,12 @@ import { ConfiguredLimits, Account, Role } from './types';
import { getRoleAbilities } from './utils/role';
interface AppValues {
configuredLimits: ConfiguredLimits;
account: Account;
showGateForm: boolean;
configuredLimits: ConfiguredLimits;
myRole: Role;
}
interface AppActions {
setOnboardingComplete(): boolean;
}
@ -41,6 +43,10 @@ export const AppLogic = kea<MakeLogicType<AppValues, AppActions, Required<Initia
},
],
configuredLimits: [props.configuredLimits.appSearch, {}],
showGateForm: [
props.appSearch.kibanaUIsEnabled === false && props.appSearch.role.roleType === 'owner',
{},
],
}),
selectors: {
myRole: [

View file

@ -0,0 +1,613 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { useActions, useValues } from 'kea';
import {
EuiButton,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormLabel,
EuiFormRow,
EuiIcon,
EuiLink,
EuiPanel,
EuiSelect,
EuiSpacer,
EuiSuperSelect,
EuiText,
EuiTextArea,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { docLinks } from '../../../shared/doc_links';
import { AppSearchGateLogic } from './app_search_gate_logic';
const featuresList = {
webCrawler: {
actionLabel: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.webCrawler.featureButtonLabel',
{
defaultMessage: 'Try Open Crawler',
}
),
actionLink: 'https://github.com/elastic/crawler?tab=readme-ov-file#setup',
addOnLearnMoreLabel: undefined,
addOnLearnMoreUrl: undefined,
description: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.webCrawler.featureDescription',
{
defaultMessage: 'Ingest web content into Elasticsearch using a web crawler',
}
),
id: 'webCrawler',
learnMore: 'https://github.com/elastic/crawler#readme ',
panelText: i18n.translate('xpack.enterpriseSearch.appSearch.gateForm.webCrawler.panelText', {
defaultMessage:
'Did you know the new self-managed Elastic open crawler is now available? You can keep your web content in sync with your search-optimized indices!',
}),
title: i18n.translate('xpack.enterpriseSearch.appSearch.gateForm.webCrawler.featureName', {
defaultMessage: 'Web crawler',
}),
},
analyticsAndLogs: {
actionLabel: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.analyticsAndLogs.featureButtonLabel',
{
defaultMessage: 'Add search analytics',
}
),
actionLink:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/behavioral-analytics-event.html',
addOnLearnMoreLabel: undefined,
addOnLearnMoreUrl: undefined,
description: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.analyticsAndLogs.featureDescription',
{
defaultMessage: 'Add and view analytics and logs for your search application',
}
),
id: 'analyticsAndLogs',
learnMore:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/behavioral-analytics-overview.html',
panelText: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.analyticsAndLogs.panelText',
{
defaultMessage:
"You can track and analyze users' searching and clicking behavior with Behavioral Analytics. Instrument your website or application to track relevant user actions.",
}
),
title: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.analyticsAndLogs.featureName',
{
defaultMessage: 'Search analytics and logs',
}
),
},
synonyms: {
actionLabel: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.synonyms.featureButtonLabel',
{
defaultMessage: 'Search with synonyms',
}
),
actionLink:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/synonyms-apis.html',
addOnLearnMoreLabel: undefined,
addOnLearnMoreUrl: undefined,
description: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.synonyms.featureDescription',
{
defaultMessage: 'Perform search with synonym based query expansion',
}
),
id: 'synonyms',
learnMore:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/search-with-synonyms.html',
panelText: i18n.translate('xpack.enterpriseSearch.appSearch.gateForm.synonyms.panelText', {
defaultMessage:
'Use the Elasticsearch Synonyms APIs to easily create and manage synonym sets.',
}),
title: i18n.translate('xpack.enterpriseSearch.appSearch.gateForm.synonyms.featureName', {
defaultMessage: 'Search with synonyms',
}),
},
relevanceTuning: {
actionLabel: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.relevanceTuning.featureButtonLabel',
{
defaultMessage: 'Tune search relevancy',
}
),
actionLink:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/search-with-elasticsearch.html',
addOnLearnMoreLabel: undefined,
addOnLearnMoreUrl: undefined,
description: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.relevanceTuning.featureDescription',
{
defaultMessage: 'Tune the relevancy of your results using ranking and boosting methods',
}
),
id: 'relevanceTuning',
learnMore:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-boosting-query.html',
panelText: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.relevanceTuning.panelText',
{
defaultMessage: "Elasticsearch's query DSL provides an in-depth set of relevance tools.",
}
),
title: i18n.translate('xpack.enterpriseSearch.appSearch.gateForm.relevanceTuning.featureName', {
defaultMessage: 'Relevance tuning',
}),
},
curations: {
actionLabel: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.curations.featureButtonLabel',
{
defaultMessage: 'Use query rules',
}
),
actionLink:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/search-using-query-rules.html',
addOnLearnMoreLabel: undefined,
addOnLearnMoreUrl: undefined,
description: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.curations.featureDescription',
{
defaultMessage: 'Curate and pin results for specific queries',
}
),
id: 'curations',
learnMore: 'https://www.elastic.co/blog/introducing-query-rules-elasticsearch-8-10',
panelText: i18n.translate('xpack.enterpriseSearch.appSearch.gateForm.curations.panelText', {
defaultMessage:
'Query rules provide a more robust set of tools to customize your search results for queries that match specific criteria and metadata.',
}),
title: i18n.translate('xpack.enterpriseSearch.appSearch.gateForm.curations.featureName', {
defaultMessage: 'Curate results',
}),
},
searchManagementUis: {
actionLabel: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.searchManagementUis.featureButtonLabel',
{
defaultMessage: 'Build a search experience with Search UI',
}
),
actionLink: 'https://www.elastic.co/docs/current/search-ui/overview',
addOnLearnMoreLabel: undefined,
addOnLearnMoreUrl: undefined,
description: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.searchManagementUis.featureDescription',
{
defaultMessage:
'Use graphical user interfaces (GUIs) to manage your search application experience',
}
),
id: 'searchManagementUis',
learnMore: 'https://www.elastic.co/docs/current/search-ui/tutorials/elasticsearch',
panelText: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.searchManagementUis.panelText',
{
defaultMessage:
'Search UI provides the components needed to build a modern search experience.',
}
),
title: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.searchManagementUis.featureName',
{
defaultMessage: 'Search and management UIs',
}
),
},
credentials: {
actionLabel: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.credentials.featureButtonLabel',
{
defaultMessage: 'Secure with Elasticsearch',
}
),
actionLink:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/document-level-security.html',
addOnLearnMoreLabel: undefined,
addOnLearnMoreUrl: undefined,
description: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.credentials.featureDescription',
{
defaultMessage:
'Manage your users and roles, and credentials for accessing your search endpoints',
}
),
id: 'credentials',
learnMore: 'https://www.elastic.co/search-labs/blog/dls-internal-knowledge-search',
panelText: i18n.translate('xpack.enterpriseSearch.appSearch.gateForm.credentials.panelText', {
defaultMessage:
'Elasticsearch provides a comprehensive set of security features, including document-level security and role-based access control.',
}),
title: i18n.translate('xpack.enterpriseSearch.appSearch.gateForm.credentials.featureName', {
defaultMessage: 'Credentials and roles',
}),
},
};
interface FeatureOption {
id: string;
title: string;
description: string;
learnMore: string | undefined;
actionLabel: string;
actionLink: string;
panelText: string;
addOnLearnMoreLabel?: string;
addOnLearnMoreUrl?: string;
}
const getFeature = (id: string): FeatureOption | undefined => {
switch (id) {
case featuresList.webCrawler.id:
return featuresList.webCrawler;
case featuresList.analyticsAndLogs.id:
return featuresList.analyticsAndLogs;
case featuresList.synonyms.id:
return featuresList.synonyms;
case featuresList.relevanceTuning.id:
return featuresList.relevanceTuning;
case featuresList.curations.id:
return featuresList.curations;
case featuresList.searchManagementUis.id:
return featuresList.searchManagementUis;
case featuresList.credentials.id:
return featuresList.credentials;
default:
return undefined;
}
};
interface FeatureOptionsSelection {
dropdownDisplay: React.ReactNode;
inputDisplay: string;
value: string;
}
const getOptionsFeaturesList = (): FeatureOptionsSelection[] => {
const baseTranslatePrefix = 'xpack.enterpriseSearch.appSearch.gateForm.superSelect';
const featureList = Object.keys(featuresList).map((featureKey): FeatureOptionsSelection => {
const feature = getFeature(featureKey);
if (!feature) {
return {
dropdownDisplay: <></>,
inputDisplay: '',
value: '',
};
}
return {
dropdownDisplay: (
<>
<strong>{feature.title}</strong>
<EuiText size="s" color="subdued">
<p>{feature.description}</p>
</EuiText>
</>
),
inputDisplay: feature.title,
value: feature.id,
};
});
featureList.push({
dropdownDisplay: (
<>
<strong>
{i18n.translate(`${baseTranslatePrefix}.other.title`, {
defaultMessage: 'Other',
})}
</strong>
<EuiText size="s" color="subdued">
<p>
{i18n.translate(`${baseTranslatePrefix}.other.description`, {
defaultMessage: 'Another feature not listed here',
})}
</p>
</EuiText>
</>
),
inputDisplay: i18n.translate(`${baseTranslatePrefix}.other.inputDisplay`, {
defaultMessage: 'Other',
}),
value: 'other',
});
return featureList;
};
const participateInUXLabsChoice = {
no: { choice: 'no', value: false },
yes: { choice: 'yes', value: true },
};
const EducationPanel: React.FC<{ featureContent: string }> = ({ featureContent }) => {
const feature = getFeature(featureContent);
const { setFeaturesOther } = useActions(AppSearchGateLogic);
if (feature) {
return (
<EuiPanel hasShadow={false}>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiIcon type="logoElastic" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem>
<EuiText>
<h5>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.educationalPanel.title',
{
defaultMessage: 'Elasticsearch native equivalent',
}
)}
</h5>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
<p>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.educationalPanel.subTitle',
{
defaultMessage: 'Based on your selection we recommend:',
}
)}
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiCallOut title={feature.title} color="success" iconType="checkInCircleFilled">
<p>{feature.panelText}</p>
<EuiFlexGroup gutterSize="m" wrap alignItems="baseline">
{feature.actionLink !== undefined && feature.actionLabel !== undefined && (
<EuiFlexItem grow={false}>
<EuiButton
href={feature.actionLink}
target="_blank"
iconType="sortRight"
iconSide="right"
>
{feature.actionLabel}
</EuiButton>
</EuiFlexItem>
)}
{feature.learnMore !== undefined && (
<EuiFlexItem grow={false}>
<EuiLink href={feature.learnMore} target="_blank">
{i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.educationalPanel.learnMore',
{
defaultMessage: 'Learn more',
}
)}
</EuiLink>
</EuiFlexItem>
)}
{feature.addOnLearnMoreLabel !== undefined &&
feature.addOnLearnMoreUrl !== undefined && (
<EuiFlexItem grow={false}>
<EuiLink type="button" href={feature.addOnLearnMoreUrl} target="_blank" external>
<EuiSpacer />
{feature.addOnLearnMoreLabel}
</EuiLink>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiCallOut>
</EuiPanel>
);
} else {
return (
<>
<EuiSpacer />
<EuiFormRow
label={i18n.translate('xpack.enterpriseSearch.appSearch.gateForm.featureOther.Label', {
defaultMessage: "Can you explain what other feature(s) you're looking for?",
})}
>
<EuiTextArea
onChange={(e) => {
setFeaturesOther(e.target.value);
}}
/>
</EuiFormRow>
</>
);
}
};
export const AppSearchGate: React.FC = () => {
const { feature, participateInUXLabs } = useValues(AppSearchGateLogic);
const { formSubmitRequest, setAdditionalFeedback, setParticipateInUXLabs, setFeature } =
useActions(AppSearchGateLogic);
const options = getOptionsFeaturesList();
return (
<EuiPanel hasShadow={false}>
<EuiForm component="form" fullWidth>
<EuiFormLabel>
{i18n.translate('xpack.enterpriseSearch.appSearch.gateForm.features.Label', {
defaultMessage: 'What App Search feature are you looking to use?',
})}
</EuiFormLabel>
<EuiSpacer size="xs" />
<EuiSuperSelect
options={options}
valueOfSelected={feature}
placeholder={i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.features.selectOption',
{
defaultMessage: 'Select an option',
}
)}
onChange={(value) => setFeature(value)}
itemLayoutAlign="top"
hasDividers
fullWidth
/>
{feature && <EducationPanel featureContent={feature} />}
<EuiSpacer />
<EuiFormRow
label={i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.additionalFeedback.Label',
{
defaultMessage: 'Would you like to share any additional feedback?',
}
)}
labelAppend={i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.additionalFeedback.optionalLabel',
{
defaultMessage: 'Optional',
}
)}
>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiTextArea
onChange={(e) => {
setAdditionalFeedback(e.target.value);
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.gateForm.additionalFeedback.description"
defaultMessage=" By submitting feedback you acknowledge that you've read and agree to our {termsOfService}, and that Elastic may {contact} about our related products and services,
using the details you provide above. See {privacyStatementLink} for more
details or to opt-out at any time."
values={{
contact: (
<EuiLink href={docLinks.workplaceSearchGatedFormDataUse}>
<FormattedMessage
id="xpack.enterpriseSearch.workplaceSearch.gateForm.additionalFeedback.contact"
defaultMessage="contact you"
/>
</EuiLink>
),
privacyStatementLink: (
<EuiLink href={docLinks.workplaceSearchGatedFormPrivacyStatement}>
<FormattedMessage
id="xpack.enterpriseSearch.workplaceSearch.gateForm.additionalFeedback.readDataPrivacyStatementLink"
defaultMessage="Elastics Privacy Statement"
/>
</EuiLink>
),
termsOfService: (
<EuiLink href={docLinks.workplaceSearchGatedFormTermsOfService}>
<FormattedMessage
id="xpack.enterpriseSearch.workplaceSearch.gateForm.additionalFeedback.readTermsOfService"
defaultMessage="Terms of Service"
/>
</EuiLink>
),
}}
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
<EuiSpacer />
<EuiFormRow
labelAppend={i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.participateUxLab.optionalLabel',
{
defaultMessage: 'Optional',
}
)}
label={i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.participateUxLab.Label',
{
defaultMessage: 'Join our user research studies to improve Elasticsearch?',
}
)}
>
<EuiSelect
hasNoInitialSelection
options={[
{
text: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.participateUxLab.Label.Yes',
{
defaultMessage: 'Yes',
}
),
value: participateInUXLabsChoice.yes.choice,
},
{
text: i18n.translate(
'xpack.enterpriseSearch.appSearch.gateForm.participateUxLab.Label.No',
{
defaultMessage: 'No',
}
),
value: participateInUXLabsChoice.no.choice,
},
]}
onChange={(e) =>
setParticipateInUXLabs(
e.target.value === participateInUXLabsChoice.yes.choice
? participateInUXLabsChoice.yes.value
: participateInUXLabsChoice.no.value
)
}
value={
participateInUXLabs !== null
? participateInUXLabs
? participateInUXLabsChoice.yes.choice
: participateInUXLabsChoice.no.choice
: undefined
}
/>
</EuiFormRow>
<EuiSpacer />
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
isDisabled={!feature ?? false}
type="submit"
fill
onClick={() => formSubmitRequest()}
>
{i18n.translate('xpack.enterpriseSearch.appSearch.gateForm.submit', {
defaultMessage: 'Submit',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
</EuiPanel>
);
};

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { mockHttpValues } from '../../../__mocks__/kea_logic';
import { nextTick } from '@kbn/test-jest-helpers';
import { sendAppSearchGatedFormData } from './app_search_gate_api_logic';
describe('AppSearchGatedFormApiLogic', () => {
const { http } = mockHttpValues;
beforeEach(() => {
jest.clearAllMocks();
});
describe('sendAppSearchGatedFormData', () => {
it('calls correct api', async () => {
const asFormData = {
additionalFeedback: 'my-test-additional-data',
feature: 'Web Crawler',
featuresOther: null,
participateInUXLabs: null,
};
const promise = Promise.resolve();
http.put.mockReturnValue(promise);
sendAppSearchGatedFormData(asFormData);
await nextTick();
expect(http.post).toHaveBeenCalledWith('/internal/app_search/as_gate', {
body: '{"as_gate_data":{"additional_feedback":"my-test-additional-data","feature":"Web Crawler"}}',
});
});
});
});

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';
export interface AppSearchGatedFormDataApiLogicArguments {
additionalFeedback: string | null;
feature: string;
featuresOther: string | null;
participateInUXLabs: boolean | null;
}
export interface AppSearchGatedFormDataApiLogicResponse {
created: string;
}
export const sendAppSearchGatedFormData = async ({
feature,
featuresOther,
additionalFeedback,
participateInUXLabs,
}: AppSearchGatedFormDataApiLogicArguments): Promise<AppSearchGatedFormDataApiLogicResponse> => {
return await HttpLogic.values.http.post<AppSearchGatedFormDataApiLogicResponse>(
'/internal/app_search/as_gate',
{
body: JSON.stringify({
as_gate_data: {
additional_feedback: additionalFeedback != null ? additionalFeedback : undefined,
feature,
features_other: featuresOther != null ? featuresOther : undefined,
participate_in_ux_labs: participateInUXLabs != null ? participateInUXLabs : undefined,
},
}),
}
);
};
export type AppSearchGatedFormDataApiLogicActions = Actions<
AppSearchGatedFormDataApiLogicArguments,
AppSearchGatedFormDataApiLogicResponse
>;
export const UpdateAppSearchGatedFormDataApiLogic = createApiLogic(
['app_search', 'send_app_search_gatedForm_data_api_logic'],
sendAppSearchGatedFormData
);

View file

@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { LogicMounter } from '../../../__mocks__/kea_logic';
import { UpdateAppSearchGatedFormDataApiLogic } from './app_search_gate_api_logic';
import { AppSearchGateLogic } from './app_search_gate_logic';
const DEFAULT_VALUES = {
additionalFeedback: null,
feature: '',
featuresOther: null,
participateInUXLabs: null,
};
describe('Gated form data', () => {
const { mount: apiLogicMount } = new LogicMounter(UpdateAppSearchGatedFormDataApiLogic);
const { mount } = new LogicMounter(AppSearchGateLogic);
beforeEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
apiLogicMount();
mount();
});
it('has expected default values', () => {
expect(AppSearchGateLogic.values).toEqual(DEFAULT_VALUES);
});
describe('listeners', () => {
it('make Request with only feature, participateInUXLabs response ', () => {
jest.spyOn(AppSearchGateLogic.actions, 'submitGatedFormDataRequest');
AppSearchGateLogic.actions.setFeature('Web Crawler');
AppSearchGateLogic.actions.setParticipateInUXLabs(false);
AppSearchGateLogic.actions.formSubmitRequest();
expect(AppSearchGateLogic.actions.submitGatedFormDataRequest).toHaveBeenCalledTimes(1);
expect(AppSearchGateLogic.actions.submitGatedFormDataRequest).toHaveBeenCalledWith({
additionalFeedback: null,
feature: 'Web Crawler',
featuresOther: null,
participateInUXLabs: false,
});
});
it('when no feature selected, formSubmitRequest is not called', () => {
jest.spyOn(AppSearchGateLogic.actions, 'submitGatedFormDataRequest');
AppSearchGateLogic.actions.formSubmitRequest();
expect(AppSearchGateLogic.actions.submitGatedFormDataRequest).toHaveBeenCalledTimes(0);
});
});
});

View file

@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { kea, MakeLogicType } from 'kea';
import {
AppSearchGatedFormDataApiLogicActions,
UpdateAppSearchGatedFormDataApiLogic,
} from './app_search_gate_api_logic';
interface AppSearchGateValues {
additionalFeedback: string | null;
feature: string;
featuresOther: string | null;
participateInUXLabs: boolean | null;
}
interface AppSearchGateActions {
formSubmitRequest: () => void;
setAdditionalFeedback(additionalFeedback: string): { additionalFeedback: string };
setFeature(feature: string): { feature: string };
setFeaturesOther(featuresOther: string): { featuresOther: string };
setParticipateInUXLabs(participateInUXLabs: boolean): {
participateInUXLabs: boolean;
};
submitGatedFormDataRequest: AppSearchGatedFormDataApiLogicActions['makeRequest'];
}
export const AppSearchGateLogic = kea<MakeLogicType<AppSearchGateValues, AppSearchGateActions>>({
actions: {
formSubmitRequest: true,
setAdditionalFeedback: (additionalFeedback) => ({ additionalFeedback }),
setFeature: (feature) => ({ feature }),
setFeaturesOther: (featuresOther) => ({ featuresOther }),
setParticipateInUXLabs: (participateInUXLabs) => ({ participateInUXLabs }),
},
connect: {
actions: [UpdateAppSearchGatedFormDataApiLogic, ['makeRequest as submitGatedFormDataRequest']],
},
listeners: ({ actions, values }) => ({
formSubmitRequest: () => {
if (values.feature) {
actions.submitGatedFormDataRequest({
additionalFeedback: values?.additionalFeedback ? values?.additionalFeedback : null,
feature: values.feature,
featuresOther: values?.featuresOther ? values?.featuresOther : null,
participateInUXLabs: values?.participateInUXLabs,
});
}
},
}),
path: ['enterprise_search', 'app_search', 'gate_form'],
reducers: ({}) => ({
additionalFeedback: [
null,
{
setAdditionalFeedback: (
_: AppSearchGateValues['additionalFeedback'],
{ additionalFeedback }: { additionalFeedback: AppSearchGateValues['additionalFeedback'] }
): AppSearchGateValues['additionalFeedback'] => additionalFeedback ?? null,
},
],
feature: [
'',
{
setFeature: (
_: AppSearchGateValues['feature'],
{ feature }: { feature: AppSearchGateValues['feature'] }
): AppSearchGateValues['feature'] => feature ?? '',
},
],
featuresOther: [
null,
{
setFeaturesOther: (
_: AppSearchGateValues['featuresOther'],
{ featuresOther }: { featuresOther: AppSearchGateValues['featuresOther'] }
): AppSearchGateValues['featuresOther'] => featuresOther ?? null,
},
],
participateInUXLabs: [
null,
{
setParticipateInUXLabs: (
_: AppSearchGateValues['participateInUXLabs'],
{
participateInUXLabs,
}: { participateInUXLabs: AppSearchGateValues['participateInUXLabs'] }
): AppSearchGateValues['participateInUXLabs'] => participateInUXLabs ?? null,
},
],
}),
});

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { ENTERPRISE_SEARCH_CONTENT_PLUGIN } from '../../../../../common/constants';
import {
EnterpriseSearchPageTemplateWrapper,
PageTemplateProps,
useEnterpriseSearchNav,
} from '../../../shared/layout';
import { SendAppSearchTelemetry } from '../../../shared/telemetry';
import { AppSearchGate } from './app_search_gate';
export const AppSearchGatePage: React.FC<PageTemplateProps> = ({ isLoading }) => {
return (
<EnterpriseSearchPageTemplateWrapper
restrictWidth
pageHeader={{
description: (
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.gateForm.description"
defaultMessage="The standalone App Search product remains available in maintenance mode, but is not recommended for new search experiences. We recommend using native Elasticsearch tools, which offer flexibility and composability, and include exciting new search features. To help you choose the tools best suited for your use case, weve created this recommendation wizard. Select the features you need, and we'll point you to corresponding Elasticsearch features. If you still want to use the standalone App Search product, you can do so after submitting the form."
/>
),
pageTitle: i18n.translate('xpack.enterpriseSearch.appSearch.gateForm.title', {
defaultMessage: 'Before you begin...',
}),
}}
solutionNav={{
items: useEnterpriseSearchNav(),
name: ENTERPRISE_SEARCH_CONTENT_PLUGIN.NAME,
}}
isLoading={isLoading}
hideEmbeddedConsole
>
<SendAppSearchTelemetry action="viewed" metric="App Search Gate form" />
<AppSearchGate />
</EnterpriseSearchPageTemplateWrapper>
);
};

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 { AppSearchGate } from './app_search_gate';

View file

@ -14,6 +14,8 @@ import { shallow, ShallowWrapper } from 'enzyme';
import { rerender } from '../../../test_helpers';
import { AppSearchGatePage } from '../app_search_gate/app_search_gated_form_page';
import { EnginesTable } from './components/tables/engines_table';
import { MetaEnginesTable } from './components/tables/meta_engines_table';
@ -21,6 +23,12 @@ import { EnginesOverview } from '.';
describe('EnginesOverview', () => {
const values = {
account: {
kibanaUIsEnabled: true,
role: {
roleType: 'owner',
},
},
dataLoading: false,
engines: [],
enginesMeta: {
@ -46,7 +54,9 @@ describe('EnginesOverview', () => {
// MetaEnginesTableLogic
expandedSourceEngines: {},
conflictingEnginesSets: {},
showGateForm: false,
};
const actions = {
loadEngines: jest.fn(),
loadMetaEngines: jest.fn(),
@ -78,10 +88,23 @@ describe('EnginesOverview', () => {
setMockValues(valuesWithEngines);
});
it('renders and calls the engines API', () => {
it('does not render overview page when kibanaUIsEnabled is false', () => {
setMockValues({
...values,
showGateForm: true,
});
const wrapper = shallow(<EnginesOverview />);
expect(wrapper.find(AppSearchGatePage)).toHaveLength(1);
expect(wrapper.find(EnginesTable)).toHaveLength(0);
expect(actions.loadEngines).toHaveBeenCalled();
});
it('renders and calls the engines API kibanaUIsEnabled is true', () => {
const wrapper = shallow(<EnginesOverview />);
expect(wrapper.find(EnginesTable)).toHaveLength(1);
expect(wrapper.find(AppSearchGatePage)).toHaveLength(0);
expect(actions.loadEngines).toHaveBeenCalled();
});

View file

@ -16,6 +16,7 @@ import { convertMetaToPagination, handlePageChange } from '../../../shared/table
import { AppLogic } from '../../app_logic';
import { EngineIcon, MetaEngineIcon } from '../../icons';
import { ENGINE_CREATION_PATH, META_ENGINE_CREATION_PATH } from '../../routes';
import { AppSearchGatePage } from '../app_search_gate/app_search_gated_form_page';
import { DataPanel } from '../data_panel';
import { AppSearchPageTemplate } from '../layout';
@ -23,6 +24,7 @@ import { EmptyState, EmptyMetaEnginesState } from './components';
import { AuditLogsModal } from './components/audit_logs_modal/audit_logs_modal';
import { EnginesTable } from './components/tables/engines_table';
import { MetaEnginesTable } from './components/tables/meta_engines_table';
import {
ENGINES_OVERVIEW_TITLE,
CREATE_AN_ENGINE_BUTTON_LABEL,
@ -34,6 +36,7 @@ import { EnginesLogic } from './engines_logic';
export const EnginesOverview: React.FC = () => {
const {
showGateForm,
myRole: { canManageEngines, canManageMetaEngines },
} = useValues(AppLogic);
@ -58,7 +61,7 @@ export const EnginesOverview: React.FC = () => {
loadMetaEngines();
}, [metaEnginesMeta.page.current]);
return (
return !showGateForm ? (
<AppSearchPageTemplate
pageViewTelemetry="engines_overview"
pageChrome={[ENGINES_TITLE]}
@ -131,5 +134,7 @@ export const EnginesOverview: React.FC = () => {
</DataPanel>
<AuditLogsModal />
</AppSearchPageTemplate>
) : (
<AppSearchGatePage isLoading={dataLoading} />
);
};

View file

@ -79,6 +79,21 @@ describe('AppSearchUnconfigured', () => {
});
});
describe('AppSearchConfigured showGateForm is true', () => {
let wrapper: ShallowWrapper;
const renderHeaderActions = jest.fn();
beforeAll(() => {
setMockValues({ showGateForm: true, myRole: {}, renderHeaderActions });
wrapper = shallow(<AppSearchConfigured {...DEFAULT_INITIAL_APP_DATA} />);
});
it('renders engine overview only when showGateForm is true', () => {
expect(wrapper.find(EnginesOverview)).toHaveLength(1);
expect(wrapper.find(EngineRouter)).toHaveLength(0);
});
});
describe('AppSearchConfigured', () => {
let wrapper: ShallowWrapper;
const renderHeaderActions = jest.fn();

View file

@ -93,6 +93,7 @@ export const AppSearchUnconfigured: React.FC = () => {
export const AppSearchConfigured: React.FC<Required<InitialAppData>> = (props) => {
const {
showGateForm,
myRole: {
canManageEngines,
canManageMetaEngines,
@ -107,7 +108,7 @@ export const AppSearchConfigured: React.FC<Required<InitialAppData>> = (props) =
renderHeaderActions(KibanaHeaderActions);
}, []);
return (
return !showGateForm ? (
<Routes>
{process.env.NODE_ENV === 'development' && (
<Route path={LIBRARY_PATH}>
@ -152,5 +153,17 @@ export const AppSearchConfigured: React.FC<Required<InitialAppData>> = (props) =
<NotFound />
</Route>
</Routes>
) : (
<Routes>
<Route exact path={ROOT_PATH}>
<Redirect to={ENGINES_PATH} />
</Route>
<Route exact path={ENGINES_PATH}>
<EnginesOverview />
</Route>
<Route>
<NotFound />
</Route>
</Routes>
);
};

View file

@ -55,10 +55,11 @@ const featuresList = {
actionLabel: i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.gateForm.analytics.action.Label',
{
defaultMessage: 'Start with Behavioral Analytics',
defaultMessage: 'Add Search Analytics',
}
),
actionLink: './analytics ',
actionLink:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/behavioral-analytics-event.html',
addOnLearnMoreLabel: undefined,
addOnLearnMoreUrl: undefined,
description: i18n.translate(
@ -69,7 +70,8 @@ const featuresList = {
}
),
id: 'Analytics',
learnMore: 'https://www.elastic.co/guide/en/enterprise-search/current/analytics-overview.html',
learnMore:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/behavioral-analytics-overview.html',
title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.gateForm.analytics.featureName', {
defaultMessage: 'Use Behavioral Analytics',
}),
@ -153,22 +155,22 @@ const featuresList = {
actionLabel: i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.gateForm.searchApplication.action.Label',
{
defaultMessage: 'Create a Search Application',
defaultMessage: 'Build a search experience with Search UI',
}
),
actionLink: './applications/search_applications',
actionLink: 'https://www.elastic.co/docs/current/search-ui/overview',
addOnLearnMoreLabel: i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.gateForm.searchApplication.addOn.learnMoreLabel',
{
defaultMessage: 'Search UI',
}
),
addOnLearnMoreUrl: 'https://www.elastic.co/guide/en/enterprise-search/current/search-ui.html ',
addOnLearnMoreUrl: 'https://www.elastic.co/docs/current/search-ui/tutorials/elasticsearch',
description: i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.gateForm.searchApplication.featureDescription',
{
defaultMessage:
'Did you know you can restrict access to documents in your Elasticsearch indices according to user and group permissions? Return only authorized search results for users with Elastics document level security. ',
'Search-powered applications that leverage the full power of Elasticsearch! Build a unified search using Search Applications or integrate directly with your existing UI with Search UI.',
}
),
id: 'Search Application',
@ -241,7 +243,7 @@ const EducationPanel: React.FC<{ featureContent: string }> = ({ featureContent }
{i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.gateForm.educationalPanel.subTitle',
{
defaultMessage: 'Based on your selection we recommend you',
defaultMessage: 'Based on your selection we recommend:',
}
)}
</p>

View file

@ -80,6 +80,7 @@ describe('callEnterpriseSearchConfigAPI', () => {
app_search: {
account: {
id: 'some-id-string',
kibana_uis_enabled: true,
onboarding_complete: true,
},
role: {
@ -99,7 +100,7 @@ describe('callEnterpriseSearchConfigAPI', () => {
organization: {
name: 'ACME Donuts',
default_org_name: 'My Organization',
kibanaUIsEnabled: false,
kibana_uis_enabled: false,
},
account: {
id: 'some-id-string',
@ -174,6 +175,7 @@ describe('callEnterpriseSearchConfigAPI', () => {
},
appSearch: {
accountId: undefined,
kibanaUIsEnabled: false,
onboardingComplete: false,
role: {
id: undefined,

View file

@ -139,6 +139,7 @@ export const callEnterpriseSearchConfigAPI = async ({
},
appSearch: {
accountId: data?.current_user?.app_search?.account?.id,
kibanaUIsEnabled: data?.current_user?.app_search?.account?.kibana_uis_enabled || false,
onboardingComplete: !!data?.current_user?.app_search?.account?.onboarding_complete,
role: {
id: data?.current_user?.app_search?.role?.id,

View file

@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
import { registerAppSearchGatedFormRoute } from './app_search_gated_form';
describe('Overview route with kibana_uis_enabled ', () => {
describe('POST /internal/app_search/as_gate', () => {
let mockRouter: MockRouter;
beforeEach(() => {
jest.clearAllMocks();
mockRouter = new MockRouter({
method: 'post',
path: '/internal/app_search/as_gate',
});
registerAppSearchGatedFormRoute({
...mockDependencies,
router: mockRouter.router,
});
});
it('creates a request handler', () => {
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
path: '/api/ent/v2/internal/as_gate',
});
});
describe('validates', () => {
it('correctly', () => {
const request = {
body: {
as_gate_data: {
additional_feedback: '',
feature: 'Selected feature',
features_other: '',
participate_in_ux_labs: true,
},
},
};
mockRouter.shouldValidate(request);
});
it('throws error unexpected values in body', () => {
const request = {
body: {
foo: 'bar',
},
};
mockRouter.shouldThrow(request);
});
});
});
});

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { RouteDependencies } from '../../plugin';
export function registerAppSearchGatedFormRoute({
router,
enterpriseSearchRequestHandler,
}: RouteDependencies) {
router.post(
{
path: '/internal/app_search/as_gate',
validate: {
body: schema.object({
as_gate_data: schema.object({
additional_feedback: schema.maybe(schema.string()),
feature: schema.string(),
features_other: schema.maybe(schema.string()),
participate_in_ux_labs: schema.maybe(schema.boolean()),
}),
}),
},
},
enterpriseSearchRequestHandler.createRequest({
path: '/api/ent/v2/internal/as_gate',
})
);
}

View file

@ -11,6 +11,7 @@ import { registerCrawlerExtractionRulesRoutes } from '../enterprise_search/crawl
import { registerSearchRelevanceSuggestionsRoutes } from './adaptive_relevance';
import { registerAnalyticsRoutes } from './analytics';
import { registerApiLogsRoutes } from './api_logs';
import { registerAppSearchGatedFormRoute } from './app_search_gated_form';
import { registerCrawlerRoutes } from './crawler';
import { registerCrawlerCrawlRulesRoutes } from './crawler_crawl_rules';
import { registerCrawlerEntryPointRoutes } from './crawler_entry_points';
@ -54,4 +55,5 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => {
registerCrawlerExtractionRulesRoutes(dependencies);
registerCrawlerSitemapRoutes(dependencies);
registerSearchRelevanceSuggestionsRoutes(dependencies);
registerAppSearchGatedFormRoute(dependencies);
};

View file

@ -8,7 +8,7 @@
import { NEVER } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { APP_SEARCH_PLUGIN, ENTERPRISE_SEARCH_CONTENT_PLUGIN } from '../../common/constants';
import { ENTERPRISE_SEARCH_CONTENT_PLUGIN } from '../../common/constants';
import { getSearchResultProvider } from './search_result_provider';
@ -96,18 +96,6 @@ describe('Enterprise Search search provider', () => {
},
};
const appSearchResult = {
icon: 'logoEnterpriseSearch',
id: 'app_search',
score: 100,
title: 'App Search',
type: 'Search',
url: {
path: `${APP_SEARCH_PLUGIN.URL}`,
prependBasePath: true,
},
};
const searchResultProvider = getSearchResultProvider(
{
hasConnectors: true,
@ -268,7 +256,7 @@ describe('Enterprise Search search provider', () => {
});
});
});
it('returns results for legacy app search', () => {
it('does not return results for legacy app search', () => {
const searchProvider = getSearchResultProvider(
{
canDeployEntSearch: true,
@ -287,7 +275,7 @@ describe('Enterprise Search search provider', () => {
{} as any
)
).toBe('(a|)', {
a: [appSearchResult],
a: [],
});
});
});

View file

@ -16,7 +16,6 @@ import { ConfigType } from '..';
import {
ENTERPRISE_SEARCH_CONNECTOR_CRAWLER_SERVICE_TYPE,
ENTERPRISE_SEARCH_CONTENT_PLUGIN,
APP_SEARCH_PLUGIN,
AI_SEARCH_PLUGIN,
} from '../../common/constants';
@ -103,14 +102,6 @@ export function getSearchResultProvider(
...(config.hasConnectors ? connectorTypes : []),
...(config.canDeployEntSearch
? [
{
keywords: ['app', 'search', 'engines'],
name: i18n.translate('xpack.enterpriseSearch.searchProvider.appSearch.name', {
defaultMessage: 'App Search',
}),
serviceType: 'app_search',
url: APP_SEARCH_PLUGIN.URL,
},
{
keywords: ['esre', 'search'],
name: i18n.translate('xpack.enterpriseSearch.searchProvider.aiSearch.name', {

View file

@ -18079,7 +18079,6 @@
"xpack.enterpriseSearch.searchNav.mngt": "Gestion de la Suite",
"xpack.enterpriseSearch.searchNav.relevance": "Pertinence",
"xpack.enterpriseSearch.searchProvider.aiSearch.name": "Intelligence artificielle de recherche",
"xpack.enterpriseSearch.searchProvider.appSearch.name": "App Search",
"xpack.enterpriseSearch.searchProvider.type.name": "Recherche",
"xpack.enterpriseSearch.searchProvider.webCrawler.name": "Robot d'indexation d'Elastic",
"xpack.enterpriseSearch.selectConnector.badgeOnClick.ariaLabel": "Cliquer pour ouvrir la fenêtre contextuelle d'explication du connecteur",

View file

@ -18065,7 +18065,6 @@
"xpack.enterpriseSearch.searchNav.mngt": "スタック管理",
"xpack.enterpriseSearch.searchNav.relevance": "<b>関連性</b>",
"xpack.enterpriseSearch.searchProvider.aiSearch.name": "検索AI",
"xpack.enterpriseSearch.searchProvider.appSearch.name": "App Search",
"xpack.enterpriseSearch.searchProvider.type.name": "検索",
"xpack.enterpriseSearch.searchProvider.webCrawler.name": "Elastic Webクローラー",
"xpack.enterpriseSearch.selectConnector.badgeOnClick.ariaLabel": "クリックすると、コネクター説明ポップオーバーが開きます",

View file

@ -18091,7 +18091,6 @@
"xpack.enterpriseSearch.searchNav.mngt": "Stack Management",
"xpack.enterpriseSearch.searchNav.relevance": "相关性",
"xpack.enterpriseSearch.searchProvider.aiSearch.name": "搜索 AI",
"xpack.enterpriseSearch.searchProvider.appSearch.name": "App Search",
"xpack.enterpriseSearch.searchProvider.type.name": "搜索",
"xpack.enterpriseSearch.searchProvider.webCrawler.name": "Elastic 网络爬虫",
"xpack.enterpriseSearch.selectConnector.badgeOnClick.ariaLabel": "单击以打开连接器说明弹出框",