diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 690698e02a142..e06b53da0ab57 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -716,6 +716,7 @@ packages/kbn-search-connectors @elastic/search-kibana x-pack/plugins/search_connectors @elastic/search-kibana packages/kbn-search-errors @elastic/kibana-data-discovery examples/search_examples @elastic/kibana-data-discovery +x-pack/plugins/search_homepage @elastic/search-kibana packages/kbn-search-index-documents @elastic/search-kibana x-pack/plugins/search_inference_endpoints @elastic/search-kibana x-pack/plugins/search_notebooks @elastic/search-kibana diff --git a/config/serverless.es.yml b/config/serverless.es.yml index 531fa5520c0ae..a170cf569b54d 100644 --- a/config/serverless.es.yml +++ b/config/serverless.es.yml @@ -69,3 +69,6 @@ xpack.searchInferenceEndpoints.ui.enabled: false # Search Notebooks xpack.search.notebooks.catalog.url: https://elastic-enterprise-search.s3.us-east-2.amazonaws.com/serverless/catalog.json + +# Search Homepage +xpack.search.homepage.ui.enabled: true diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index f78070b9bfa49..fc15206bd284d 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -789,6 +789,10 @@ It uses Chromium and Puppeteer underneath to run the browser in headless mode. |This plugin contains common assets and endpoints for the use of connectors in Kibana. Primarily used by the enterprise_search and serverless_search plugins. +|{kib-repo}blob/{branch}/x-pack/plugins/search_homepage/README.mdx[searchHomepage] +|The Search Homepage is a shared homepage for elasticsearch users. + + |{kib-repo}blob/{branch}/x-pack/plugins/search_inference_endpoints/README.md[searchInferenceEndpoints] |The Inference Endpoints is a tool used to manage inference endpoints diff --git a/package.json b/package.json index 48abd4ddd30b7..efd020e91df66 100644 --- a/package.json +++ b/package.json @@ -727,6 +727,7 @@ "@kbn/search-connectors-plugin": "link:x-pack/plugins/search_connectors", "@kbn/search-errors": "link:packages/kbn-search-errors", "@kbn/search-examples-plugin": "link:examples/search_examples", + "@kbn/search-homepage": "link:x-pack/plugins/search_homepage", "@kbn/search-index-documents": "link:packages/kbn-search-index-documents", "@kbn/search-inference-endpoints": "link:x-pack/plugins/search_inference_endpoints", "@kbn/search-notebooks": "link:x-pack/plugins/search_notebooks", diff --git a/packages/deeplinks/search/constants.ts b/packages/deeplinks/search/constants.ts index 36d31d22dfe21..3fdcc78bb68a1 100644 --- a/packages/deeplinks/search/constants.ts +++ b/packages/deeplinks/search/constants.ts @@ -17,3 +17,4 @@ export const SERVERLESS_ES_APP_ID = 'serverlessElasticsearch'; export const SERVERLESS_ES_CONNECTORS_ID = 'serverlessConnectors'; export const SERVERLESS_ES_SEARCH_PLAYGROUND_ID = 'searchPlayground'; export const SERVERLESS_ES_SEARCH_INFERENCE_ENDPOINTS_ID = 'searchInferenceEndpoints'; +export const SEARCH_HOMEPAGE = 'searchHomepage'; diff --git a/packages/deeplinks/search/deep_links.ts b/packages/deeplinks/search/deep_links.ts index 8eeceff8f8ca2..f004d1b2c9dd6 100644 --- a/packages/deeplinks/search/deep_links.ts +++ b/packages/deeplinks/search/deep_links.ts @@ -17,6 +17,7 @@ import { ENTERPRISE_SEARCH_WORKPLACESEARCH_APP_ID, SERVERLESS_ES_SEARCH_PLAYGROUND_ID, SERVERLESS_ES_SEARCH_INFERENCE_ENDPOINTS_ID, + SEARCH_HOMEPAGE, } from './constants'; export type EnterpriseSearchApp = typeof ENTERPRISE_SEARCH_APP_ID; @@ -29,6 +30,7 @@ export type ServerlessSearchApp = typeof SERVERLESS_ES_APP_ID; export type ConnectorsId = typeof SERVERLESS_ES_CONNECTORS_ID; export type SearchPlaygroundId = typeof SERVERLESS_ES_SEARCH_PLAYGROUND_ID; export type SearchInferenceEndpointsId = typeof SERVERLESS_ES_SEARCH_INFERENCE_ENDPOINTS_ID; +export type SearchHomepage = typeof SEARCH_HOMEPAGE; export type ContentLinkId = 'searchIndices' | 'connectors' | 'webCrawlers'; @@ -47,6 +49,7 @@ export type DeepLinkId = | ConnectorsId | SearchPlaygroundId | SearchInferenceEndpointsId + | SearchHomepage | `${EnterpriseSearchContentApp}:${ContentLinkId}` | `${EnterpriseSearchApplicationsApp}:${ApplicationsLinkId}` | `${EnterpriseSearchAppsearchApp}:${AppsearchLinkId}`; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 336cfd2c6b93d..c3ffc507387b8 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -132,6 +132,7 @@ pageLoadAssetSize: screenshotMode: 17856 screenshotting: 22870 searchConnectors: 30000 + searchHomepage: 19831 searchInferenceEndpoints: 20470 searchNotebooks: 18942 searchPlayground: 19325 diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 31e0a44b9e823..19eeef57ba62b 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -313,8 +313,9 @@ export default function ({ getService }: PluginFunctionalProviderContext) { // 'xpack.reporting.poll.jobsRefresh.intervalErrorMultiplier (number)', 'xpack.rollup.ui.enabled (boolean)', 'xpack.saved_object_tagging.cache_refresh_interval (duration)', - 'xpack.searchPlayground.ui.enabled (boolean)', + 'xpack.search.homepage.ui.enabled (boolean)', 'xpack.searchInferenceEndpoints.ui.enabled (boolean)', + 'xpack.searchPlayground.ui.enabled (boolean)', 'xpack.security.loginAssistanceMessage (string)', 'xpack.security.sameSiteCookies (alternatives)', 'xpack.security.showInsecureClusterWarning (boolean)', diff --git a/tsconfig.base.json b/tsconfig.base.json index ea18fe456af85..0f8d9a11563e0 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1426,6 +1426,8 @@ "@kbn/search-errors/*": ["packages/kbn-search-errors/*"], "@kbn/search-examples-plugin": ["examples/search_examples"], "@kbn/search-examples-plugin/*": ["examples/search_examples/*"], + "@kbn/search-homepage": ["x-pack/plugins/search_homepage"], + "@kbn/search-homepage/*": ["x-pack/plugins/search_homepage/*"], "@kbn/search-index-documents": ["packages/kbn-search-index-documents"], "@kbn/search-index-documents/*": ["packages/kbn-search-index-documents/*"], "@kbn/search-inference-endpoints": ["x-pack/plugins/search_inference_endpoints"], diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 2943af3cf46d5..1f224ca164e52 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -92,6 +92,7 @@ "xpack.rollupJobs": ["plugins/rollup"], "xpack.runtimeFields": "plugins/runtime_fields", "xpack.screenshotting": "plugins/screenshotting", + "xpack.searchHomepage": "plugins/search_homepage", "xpack.searchNotebooks": "plugins/search_notebooks", "xpack.searchPlayground": "plugins/search_playground", "xpack.searchInferenceEndpoints": "plugins/search_inference_endpoints", diff --git a/x-pack/plugins/enterprise_search/kibana.jsonc b/x-pack/plugins/enterprise_search/kibana.jsonc index 3855fd3d9e1f9..0dc2562ff2fe3 100644 --- a/x-pack/plugins/enterprise_search/kibana.jsonc +++ b/x-pack/plugins/enterprise_search/kibana.jsonc @@ -30,6 +30,7 @@ "guidedOnboarding", "console", "searchConnectors", + "searchHomepage", "searchPlayground", "searchInferenceEndpoints", "embeddable", diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts index cca5523ded681..5f4774be15b96 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts @@ -45,6 +45,7 @@ export const mockKibanaValues = { history: mockHistory, indexMappingComponent: null, isCloud: false, + isSearchHomepageEnabled: false, isSidebarEnabled: true, lens: { EmbeddableComponent: jest.fn(), @@ -64,6 +65,7 @@ export const mockKibanaValues = { hasWebCrawler: true, }, renderHeaderActions: jest.fn(), + searchHomepage: null, searchInferenceEndpoints: null, searchPlayground: searchPlaygroundMock.createStart(), security: securityMock.createStart(), diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index ea45e121470e2..98d6677c35fc1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -117,6 +117,7 @@ export const renderApp = ( guidedOnboarding, history, indexMappingComponent, + isSearchHomepageEnabled: plugins.searchHomepage?.isHomepageFeatureEnabled() ?? false, isSidebarEnabled, lens, ml, @@ -127,6 +128,7 @@ export const renderApp = ( params.setHeaderActionMenu( HeaderActions ? renderHeaderActions.bind(null, HeaderActions, store, params) : undefined ), + searchHomepage: plugins.searchHomepage, searchPlayground: plugins.searchPlayground, searchInferenceEndpoints: plugins.searchInferenceEndpoints, security, diff --git a/x-pack/plugins/enterprise_search/public/applications/search_homepage/components/layout/page_template.test.tsx b/x-pack/plugins/enterprise_search/public/applications/search_homepage/components/layout/page_template.test.tsx new file mode 100644 index 0000000000000..c44cc39c5eb1d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/search_homepage/components/layout/page_template.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { TestHelper } from '../../../test_helpers/test_utils.test_helper'; + +import { SearchHomepagePageTemplate } from './page_template'; + +describe('SearchHomepagePageTemplate', () => { + beforeAll(() => { + TestHelper.prepare(); + }); + + it('renders as expected', async () => { + const { container } = TestHelper.render( + +
Test
+
+ ); + + expect(container.querySelector('.kbnSolutionNav__title')).toHaveTextContent('Search'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/search_homepage/components/layout/page_template.tsx b/x-pack/plugins/enterprise_search/public/applications/search_homepage/components/layout/page_template.tsx new file mode 100644 index 0000000000000..76f2e6e526239 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/search_homepage/components/layout/page_template.tsx @@ -0,0 +1,37 @@ +/* + * 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 { SEARCH_PRODUCT_NAME } from '../../../../../common/constants'; +import { SetSearchChrome } from '../../../shared/kibana_chrome'; +import { EnterpriseSearchPageTemplateWrapper, PageTemplateProps } from '../../../shared/layout'; +import { useEnterpriseSearchNav } from '../../../shared/layout'; +import { SendEnterpriseSearchTelemetry } from '../../../shared/telemetry'; + +export const SearchHomepagePageTemplate: React.FC = ({ + children, + pageChrome, + pageViewTelemetry, + ...pageTemplateProps +}) => { + return ( + } + > + {pageViewTelemetry && ( + + )} + {children} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/search_homepage/components/search_homepage.tsx b/x-pack/plugins/enterprise_search/public/applications/search_homepage/components/search_homepage.tsx new file mode 100644 index 0000000000000..a605010fcb00d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/search_homepage/components/search_homepage.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { KibanaLogic } from '../../shared/kibana'; +import { SetSearchChrome } from '../../shared/kibana_chrome'; + +import { SearchHomepagePageTemplate } from './layout/page_template'; + +export const SearchHomepagePage = () => { + const { isSearchHomepageEnabled, searchHomepage } = useValues(KibanaLogic); + + if (!isSearchHomepageEnabled || !searchHomepage) { + return null; + } + + return ( + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/search_homepage/index.tsx b/x-pack/plugins/enterprise_search/public/applications/search_homepage/index.tsx new file mode 100644 index 0000000000000..43963f21d3b5d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/search_homepage/index.tsx @@ -0,0 +1,42 @@ +/* + * 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 { Routes, Route } from '@kbn/shared-ux-router'; + +import { isVersionMismatch } from '../../../common/is_version_mismatch'; +import type { InitialAppData } from '../../../common/types'; +import { VersionMismatchPage } from '../shared/version_mismatch'; + +import { SearchHomepagePage } from './components/search_homepage'; + +export const SearchHomepage: React.FC = (props) => { + const { enterpriseSearchVersion, kibanaVersion } = props; + const incompatibleVersions = isVersionMismatch(enterpriseSearchVersion, kibanaVersion); + + const showView = () => { + if (incompatibleVersions) { + return ( + + ); + } + + return ; + }; + + return ( + + + {showView()} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/search_homepage/jest.config.js b/x-pack/plugins/enterprise_search/public/applications/search_homepage/jest.config.js new file mode 100644 index 0000000000000..c18a3561afb65 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/search_homepage/jest.config.js @@ -0,0 +1,26 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: ['/x-pack/plugins/enterprise_search/public/applications/search_homepage'], + collectCoverage: true, + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/enterprise_search/public/applications/**/*.{ts,tsx}', + '!/x-pack/plugins/enterprise_search/public/*.ts', + '!/x-pack/plugins/enterprise_search/server/*.ts', + '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/enterprise_search/public/applications/search_homepage', + modulePathIgnorePatterns: [ + '/x-pack/plugins/enterprise_search/public/applications/app_search/cypress', + '/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress', + ], +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index da18e9e8bb44f..4920b25cffd75 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -29,6 +29,7 @@ import { LensPublicStart } from '@kbn/lens-plugin/public'; import { MlPluginStart } from '@kbn/ml-plugin/public'; import { ELASTICSEARCH_URL_PLACEHOLDER } from '@kbn/search-api-panels/constants'; import { ConnectorDefinition } from '@kbn/search-connectors-plugin/public'; +import type { SearchHomepagePluginStart } from '@kbn/search-homepage/public'; import { SearchInferenceEndpointsPluginStart } from '@kbn/search-inference-endpoints/public'; import { SearchPlaygroundPluginStart } from '@kbn/search-playground/public'; import { AuthenticatedUser, SecurityPluginStart } from '@kbn/security-plugin/public'; @@ -58,6 +59,7 @@ export interface KibanaLogicProps { guidedOnboarding?: GuidedOnboardingPluginStart; history: ScopedHistory; indexMappingComponent?: React.FC; + isSearchHomepageEnabled: boolean; isSidebarEnabled: boolean; lens?: LensPublicStart; ml?: MlPluginStart; @@ -65,6 +67,7 @@ export interface KibanaLogicProps { productAccess: ProductAccess; productFeatures: ProductFeatures; renderHeaderActions(HeaderActions?: FC): void; + searchHomepage?: SearchHomepagePluginStart; searchPlayground?: SearchPlaygroundPluginStart; searchInferenceEndpoints?: SearchInferenceEndpointsPluginStart; security?: SecurityPluginStart; @@ -91,6 +94,7 @@ export interface KibanaValues { history: ScopedHistory; indexMappingComponent: React.FC | null; isCloud: boolean; + isSearchHomepageEnabled: boolean; isSidebarEnabled: boolean; lens: LensPublicStart | null; ml: MlPluginStart | null; @@ -98,6 +102,7 @@ export interface KibanaValues { productAccess: ProductAccess; productFeatures: ProductFeatures; renderHeaderActions(HeaderActions?: FC): void; + searchHomepage: SearchHomepagePluginStart | null; searchPlayground: SearchPlaygroundPluginStart | null; searchInferenceEndpoints: SearchInferenceEndpointsPluginStart | null; security: SecurityPluginStart | null; @@ -129,6 +134,7 @@ export const KibanaLogic = kea>({ guidedOnboarding: [props.guidedOnboarding || null, {}], history: [props.history, {}], indexMappingComponent: [props.indexMappingComponent || null, {}], + isSearchHomepageEnabled: [props.isSearchHomepageEnabled, {}], isSidebarEnabled: [props.isSidebarEnabled, {}], lens: [props.lens || null, {}], ml: [props.ml || null, {}], @@ -143,6 +149,7 @@ export const KibanaLogic = kea>({ productAccess: [props.productAccess, {}], productFeatures: [props.productFeatures, {}], renderHeaderActions: [props.renderHeaderActions, {}], + searchHomepage: [props.searchHomepage || null, {}], searchPlayground: [props.searchPlayground || null, {}], searchInferenceEndpoints: [props.searchInferenceEndpoints || null, {}], security: [props.security || null, {}], diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/breadcrumbs_home.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/breadcrumbs_home.ts new file mode 100644 index 0000000000000..34e9e12c88b76 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/breadcrumbs_home.ts @@ -0,0 +1,18 @@ +/* + * 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 { ENTERPRISE_SEARCH_OVERVIEW_PLUGIN } from '../../../../common/constants'; + +/** + * HACK for base homepage URL, this can be removed and updated to a static + * URL when Search Homepage is no longer feature flagged. + */ +const breadCrumbHome = { url: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.URL }; +export const getHomeURL = () => breadCrumbHome.url; +export const setBreadcrumbHomeUrl = (url: string) => { + breadCrumbHome.url = url; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts index ac3e6d7a6437d..5798a48680d1f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts @@ -15,7 +15,6 @@ import { APP_SEARCH_PLUGIN, ENTERPRISE_SEARCH_CONTENT_PLUGIN, INFERENCE_ENDPOINTS_PLUGIN, - ENTERPRISE_SEARCH_OVERVIEW_PLUGIN, ENTERPRISE_SEARCH_PRODUCT_NAME, AI_SEARCH_PLUGIN, SEARCH_EXPERIENCES_PLUGIN, @@ -29,6 +28,8 @@ import { HttpLogic } from '../http'; import { KibanaLogic } from '../kibana'; import { letBrowserHandleEvent, createHref } from '../react_router_helpers'; +import { getHomeURL } from './breadcrumbs_home'; + /** * Types */ @@ -107,7 +108,7 @@ export const useSearchBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => useEuiBreadcrumbs([ { text: SEARCH_PRODUCT_NAME, - path: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.URL, + path: getHomeURL(), shouldNotCreateHref: true, }, ...breadcrumbs, @@ -117,7 +118,7 @@ export const useEnterpriseSearchBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => useEuiBreadcrumbs([ { text: ENTERPRISE_SEARCH_PRODUCT_NAME, - path: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.URL, + path: getHomeURL(), shouldNotCreateHref: true, }, ...breadcrumbs, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx index bd53ed235d4cb..77454581c61e7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx @@ -49,7 +49,8 @@ import { generateNavLink } from './nav_link_helpers'; * @returns The Enterprise Search navigation items */ export const useEnterpriseSearchNav = (alwaysReturn = false) => { - const { isSidebarEnabled, productAccess } = useValues(KibanaLogic); + const { isSearchHomepageEnabled, searchHomepage, isSidebarEnabled, productAccess } = + useValues(KibanaLogic); const indicesNavItems = useIndicesNav(); if (!isSidebarEnabled && !alwaysReturn) return undefined; @@ -66,7 +67,10 @@ export const useEnterpriseSearchNav = (alwaysReturn = false) => { ...generateNavLink({ shouldNotCreateHref: true, shouldShowActiveForSubroutes: true, - to: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.URL, + to: + isSearchHomepageEnabled && searchHomepage + ? searchHomepage.app.appRoute + : ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.URL, }), }, { diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/test_utils.test_helper.tsx b/x-pack/plugins/enterprise_search/public/applications/test_helpers/test_utils.test_helper.tsx index 5601c006a4433..0050165b8be50 100644 --- a/x-pack/plugins/enterprise_search/public/applications/test_helpers/test_utils.test_helper.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/test_utils.test_helper.tsx @@ -62,6 +62,7 @@ export const mockKibanaProps: KibanaLogicProps = { indexMappingComponent: () => { return <>; }, + isSearchHomepageEnabled: false, isSidebarEnabled: true, lens: { EmbeddableComponent: jest.fn(), @@ -84,6 +85,7 @@ export const mockKibanaProps: KibanaLogicProps = { hasWebCrawler: true, }, renderHeaderActions: jest.fn(), + searchHomepage: undefined, searchPlayground: searchPlaygroundMock.createStart(), security: securityMock.createStart(), setBreadcrumbs: jest.fn(), @@ -114,7 +116,7 @@ interface TestHelper { defaultMockValues: typeof DEFAULT_VALUES; mountLogic: (logicFile: LogicFile, props?: object) => void; prepare: (options?: PrepareOptions) => void; - render: (children: JSX.Element) => void; + render: (children: JSX.Element) => ReturnType; } export const TestHelper: TestHelper = { @@ -147,7 +149,7 @@ export const TestHelper: TestHelper = { TestHelper.actionsToRun.forEach((action) => { action(); }); - testingLibraryRender( + return testingLibraryRender( {children} diff --git a/x-pack/plugins/enterprise_search/public/navigation_tree.ts b/x-pack/plugins/enterprise_search/public/navigation_tree.ts index dc079aec90688..051cfaa6779af 100644 --- a/x-pack/plugins/enterprise_search/public/navigation_tree.ts +++ b/x-pack/plugins/enterprise_search/public/navigation_tree.ts @@ -67,12 +67,14 @@ const euiItemTypeToNodeDefinition = ({ export const getNavigationTreeDefinition = ({ dynamicItems$, + isSearchHomepageEnabled, }: { dynamicItems$: Observable; + isSearchHomepageEnabled: boolean; }): AddSolutionNavigationArg => { return { dataTestSubj: 'searchSideNav', - homePage: 'enterpriseSearch', + homePage: isSearchHomepageEnabled ? 'searchHomepage' : 'enterpriseSearch', icon, id: 'es', navigationTree$: dynamicItems$.pipe( @@ -84,7 +86,7 @@ export const getNavigationTreeDefinition = ({ breadcrumbStatus: 'hidden', children: [ { - link: 'enterpriseSearch', + link: isSearchHomepageEnabled ? 'searchHomepage' : 'enterpriseSearch', }, { getIsActive: ({ pathNameSerialized, prepend }) => { diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 9c03fb623fa3d..280de2f04356b 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -32,6 +32,10 @@ import { MlPluginStart } from '@kbn/ml-plugin/public'; import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; import { ELASTICSEARCH_URL_PLACEHOLDER } from '@kbn/search-api-panels/constants'; import { SearchConnectorsPluginStart } from '@kbn/search-connectors-plugin/public'; +import type { + SearchHomepagePluginSetup, + SearchHomepagePluginStart, +} from '@kbn/search-homepage/public'; import { SearchInferenceEndpointsPluginStart } from '@kbn/search-inference-endpoints/public'; import { SearchPlaygroundPluginStart } from '@kbn/search-playground/public'; import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public'; @@ -67,6 +71,7 @@ import { import { INFERENCE_ENDPOINTS_PATH } from './applications/enterprise_search_relevance/routes'; import { docLinks } from './applications/shared/doc_links'; +import { setBreadcrumbHomeUrl } from './applications/shared/kibana_chrome/breadcrumbs_home'; import type { DynamicSideNavItems } from './navigation_tree'; export interface ClientData extends InitialAppData { @@ -80,6 +85,7 @@ export type EnterpriseSearchPublicStart = ReturnType { - const kibanaDeps = await this.getKibanaDeps(core, params, cloud); - const { chrome, http } = kibanaDeps.core; - chrome.docTitle.change(ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.NAME); + if (useSearchHomepage) { + const { app } = plugins.searchHomepage!; + core.application.register({ + ...app, + category: DEFAULT_APP_CATEGORIES.enterpriseSearch, + euiIconType: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.LOGO, + visibleIn: ['home', 'kibanaOverview', 'globalSearch', 'sideNav'], + mount: async (params: AppMountParameters) => { + const kibanaDeps = await this.getKibanaDeps(core, params, cloud); + const { chrome, http } = kibanaDeps.core; + chrome.docTitle.change(app.title); - await this.getInitialData(http); - const pluginData = this.getPluginData(); + await this.getInitialData(http); + const pluginData = this.getPluginData(); - const { renderApp } = await import('./applications'); - const { EnterpriseSearchOverview } = await import( - './applications/enterprise_search_overview' - ); + const { renderApp } = await import('./applications'); + const { SearchHomepage } = await import('./applications/search_homepage'); - return renderApp(EnterpriseSearchOverview, kibanaDeps, pluginData); - }, - title: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.NAV_TITLE, - visibleIn: ['home', 'kibanaOverview', 'globalSearch', 'sideNav'], - }); + return renderApp(SearchHomepage, kibanaDeps, pluginData); + }, + }); + setBreadcrumbHomeUrl(app.appRoute); + } else { + core.application.register({ + appRoute: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.URL, + category: DEFAULT_APP_CATEGORIES.enterpriseSearch, + euiIconType: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.LOGO, + id: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.ID, + mount: async (params: AppMountParameters) => { + const kibanaDeps = await this.getKibanaDeps(core, params, cloud); + const { chrome, http } = kibanaDeps.core; + chrome.docTitle.change(ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.NAME); + + await this.getInitialData(http); + const pluginData = this.getPluginData(); + + const { renderApp } = await import('./applications'); + const { EnterpriseSearchOverview } = await import( + './applications/enterprise_search_overview' + ); + + return renderApp(EnterpriseSearchOverview, kibanaDeps, pluginData); + }, + title: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.NAV_TITLE, + visibleIn: ['home', 'kibanaOverview', 'globalSearch', 'sideNav'], + }); + } core.application.register({ appRoute: ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL, @@ -512,14 +545,27 @@ export class EnterpriseSearchPlugin implements Plugin { } if (plugins.home) { - plugins.home.featureCatalogue.registerSolution({ - description: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.DESCRIPTION, - icon: 'logoEnterpriseSearch', - id: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.ID, - order: 100, - path: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.URL, - title: SEARCH_PRODUCT_NAME, - }); + if (useSearchHomepage) { + const { searchHomepage } = plugins; + + plugins.home.featureCatalogue.registerSolution({ + description: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.DESCRIPTION, + icon: 'logoEnterpriseSearch', + id: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.ID, + order: 100, + path: searchHomepage!.app.appRoute, + title: SEARCH_PRODUCT_NAME, + }); + } else { + plugins.home.featureCatalogue.registerSolution({ + description: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.DESCRIPTION, + icon: 'logoEnterpriseSearch', + id: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.ID, + order: 100, + path: ENTERPRISE_SEARCH_OVERVIEW_PLUGIN.URL, + title: SEARCH_PRODUCT_NAME, + }); + } plugins.home.featureCatalogue.register({ category: 'data', @@ -587,7 +633,10 @@ export class EnterpriseSearchPlugin implements Plugin { import('./navigation_tree').then(({ getNavigationTreeDefinition }) => { return plugins.navigation.addSolutionNavigation( - getNavigationTreeDefinition({ dynamicItems$: this.sideNavDynamicItems$ }) + getNavigationTreeDefinition({ + dynamicItems$: this.sideNavDynamicItems$, + isSearchHomepageEnabled: plugins.searchHomepage?.isHomepageFeatureEnabled() ?? false, + }) ); }); diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json index 3a66b8350be4f..a6bb797de5eb1 100644 --- a/x-pack/plugins/enterprise_search/tsconfig.json +++ b/x-pack/plugins/enterprise_search/tsconfig.json @@ -79,6 +79,7 @@ "@kbn/cloud", "@kbn/try-in-console", "@kbn/core-chrome-browser", - "@kbn/navigation-plugin" + "@kbn/navigation-plugin", + "@kbn/search-homepage" ] } diff --git a/x-pack/plugins/search_homepage/README.mdx b/x-pack/plugins/search_homepage/README.mdx new file mode 100644 index 0000000000000..00ba4f491c607 --- /dev/null +++ b/x-pack/plugins/search_homepage/README.mdx @@ -0,0 +1,3 @@ +# Search Homepage + +The Search Homepage is a shared homepage for elasticsearch users. diff --git a/x-pack/plugins/search_homepage/common/index.ts b/x-pack/plugins/search_homepage/common/index.ts new file mode 100644 index 0000000000000..d93d5c49f8cc5 --- /dev/null +++ b/x-pack/plugins/search_homepage/common/index.ts @@ -0,0 +1,14 @@ +/* + * 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 const PLUGIN_ID = 'searchHomepage'; +export const PLUGIN_NAME = 'searchHomepage'; + +/** + * UI Setting id for the Search Homepage feature flag + */ +export const HOMEPAGE_FEATURE_FLAG_ID = 'searchHomepage:homepageEnabled'; diff --git a/x-pack/plugins/search_homepage/jest.config.js b/x-pack/plugins/search_homepage/jest.config.js new file mode 100644 index 0000000000000..65cd8f1e34252 --- /dev/null +++ b/x-pack/plugins/search_homepage/jest.config.js @@ -0,0 +1,15 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/search_homepage'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/search_homepage', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/search_homepage/{public,server}/**/*.{ts,tsx}'], +}; diff --git a/x-pack/plugins/search_homepage/kibana.jsonc b/x-pack/plugins/search_homepage/kibana.jsonc new file mode 100644 index 0000000000000..0e345ab0d330a --- /dev/null +++ b/x-pack/plugins/search_homepage/kibana.jsonc @@ -0,0 +1,26 @@ +{ + "type": "plugin", + "id": "@kbn/search-homepage", + "owner": "@elastic/search-kibana", + "plugin": { + "id": "searchHomepage", + "server": true, + "browser": true, + "configPath": [ + "xpack", + "search", + "homepage" + ], + "requiredPlugins": [ + "share", + ], + "optionalPlugins": [ + "cloud", + "console", + "usageCollection", + ], + "requiredBundles": [ + "kibanaReact" + ] + } +} diff --git a/x-pack/plugins/search_homepage/public/application.tsx b/x-pack/plugins/search_homepage/public/application.tsx new file mode 100644 index 0000000000000..4af256de498ed --- /dev/null +++ b/x-pack/plugins/search_homepage/public/application.tsx @@ -0,0 +1,37 @@ +/* + * 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 ReactDOM from 'react-dom'; +import { CoreStart } from '@kbn/core/public'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { I18nProvider } from '@kbn/i18n-react'; +import { Router } from '@kbn/shared-ux-router'; +import { SearchHomepageAppPluginStartDependencies } from './types'; +import { HomepageRouter } from './router'; + +export const renderApp = async ( + core: CoreStart, + services: SearchHomepageAppPluginStartDependencies, + element: HTMLElement +) => { + ReactDOM.render( + + + + + + + + + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/x-pack/plugins/search_homepage/public/components/search_homepage.tsx b/x-pack/plugins/search_homepage/public/components/search_homepage.tsx new file mode 100644 index 0000000000000..7af02cbcf3275 --- /dev/null +++ b/x-pack/plugins/search_homepage/public/components/search_homepage.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiPageTemplate } from '@elastic/eui'; + +import { useKibana } from '../hooks/use_kibana'; +import { SearchHomepageBody } from './search_homepage_body'; +import { SearchHomepageHeader } from './search_homepage_header'; + +export const SearchHomepagePage = () => { + const { + services: { console: consolePlugin }, + } = useKibana(); + + const embeddableConsole = useMemo( + () => (consolePlugin?.EmbeddableConsole ? : null), + [consolePlugin] + ); + + return ( + + + + {embeddableConsole} + + ); +}; diff --git a/x-pack/plugins/search_homepage/public/components/search_homepage_body.tsx b/x-pack/plugins/search_homepage/public/components/search_homepage_body.tsx new file mode 100644 index 0000000000000..808393594e7d8 --- /dev/null +++ b/x-pack/plugins/search_homepage/public/components/search_homepage_body.tsx @@ -0,0 +1,24 @@ +/* + * 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 { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; + +export const SearchHomepageBody = () => ( + +
+ +); diff --git a/x-pack/plugins/search_homepage/public/components/search_homepage_header.tsx b/x-pack/plugins/search_homepage/public/components/search_homepage_header.tsx new file mode 100644 index 0000000000000..941655d67cdab --- /dev/null +++ b/x-pack/plugins/search_homepage/public/components/search_homepage_header.tsx @@ -0,0 +1,26 @@ +/* + * 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 { EuiPageTemplate, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +export const SearchHomepageHeader = () => ( + +

+ +

+ + } + data-test-subj="search-homepage-header" + rightSideItems={[]} + /> +); diff --git a/x-pack/plugins/search_homepage/public/components/stack_app.tsx b/x-pack/plugins/search_homepage/public/components/stack_app.tsx new file mode 100644 index 0000000000000..ca18ac7112c09 --- /dev/null +++ b/x-pack/plugins/search_homepage/public/components/stack_app.tsx @@ -0,0 +1,19 @@ +/* + * 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 { SearchHomepageBody } from './search_homepage_body'; +import { SearchHomepageHeader } from './search_homepage_header'; + +export const App: React.FC = () => { + return ( + <> + + + + ); +}; diff --git a/x-pack/plugins/search_homepage/public/embeddable.tsx b/x-pack/plugins/search_homepage/public/embeddable.tsx new file mode 100644 index 0000000000000..ee69062ea3fe5 --- /dev/null +++ b/x-pack/plugins/search_homepage/public/embeddable.tsx @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { dynamic } from '@kbn/shared-ux-utility'; + +export const SearchHomepage = dynamic(async () => ({ + default: (await import('./components/stack_app')).App, +})); diff --git a/x-pack/plugins/search_homepage/public/feature_flags.ts b/x-pack/plugins/search_homepage/public/feature_flags.ts new file mode 100644 index 0000000000000..bea65a8e1548f --- /dev/null +++ b/x-pack/plugins/search_homepage/public/feature_flags.ts @@ -0,0 +1,13 @@ +/* + * 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 { IUiSettingsClient } from '@kbn/core/public'; +import { HOMEPAGE_FEATURE_FLAG_ID } from '../common'; + +export function isHomepageEnabled(uiSettings: IUiSettingsClient): boolean { + return uiSettings.get(HOMEPAGE_FEATURE_FLAG_ID, false); +} diff --git a/x-pack/plugins/search_homepage/public/hooks/use_kibana.ts b/x-pack/plugins/search_homepage/public/hooks/use_kibana.ts new file mode 100644 index 0000000000000..b22c7b4ed9d7f --- /dev/null +++ b/x-pack/plugins/search_homepage/public/hooks/use_kibana.ts @@ -0,0 +1,11 @@ +/* + * 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 { useKibana as _useKibana } from '@kbn/kibana-react-plugin/public'; +import { SearchHomepageServicesContext } from '../types'; + +export const useKibana = () => _useKibana(); diff --git a/x-pack/plugins/search_homepage/public/index.ts b/x-pack/plugins/search_homepage/public/index.ts new file mode 100644 index 0000000000000..b5133bb506406 --- /dev/null +++ b/x-pack/plugins/search_homepage/public/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializerContext } from '@kbn/core/public'; + +import { SearchHomepagePlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new SearchHomepagePlugin(initializerContext); +} + +export type { + SearchHomepagePluginSetup, + SearchHomepagePluginStart, + SearchHomepageAppInfo, +} from './types'; diff --git a/x-pack/plugins/search_homepage/public/plugin.ts b/x-pack/plugins/search_homepage/public/plugin.ts new file mode 100644 index 0000000000000..ebb3ef8ed822c --- /dev/null +++ b/x-pack/plugins/search_homepage/public/plugin.ts @@ -0,0 +1,80 @@ +/* + * 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 { + CoreSetup, + Plugin, + CoreStart, + AppMountParameters, + PluginInitializerContext, +} from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import { PLUGIN_ID } from '../common'; + +import { SearchHomepage } from './embeddable'; +import { isHomepageEnabled } from './feature_flags'; +import { + SearchHomepageConfigType, + SearchHomepagePluginSetup, + SearchHomepagePluginStart, + SearchHomepageAppPluginStartDependencies, + SearchHomepageAppInfo, +} from './types'; + +const appInfo: SearchHomepageAppInfo = { + id: PLUGIN_ID, + appRoute: '/app/elasticsearch/home', + title: i18n.translate('xpack.searchHomepage.appTitle', { defaultMessage: 'Home' }), +}; + +export class SearchHomepagePlugin + implements Plugin +{ + private readonly config: SearchHomepageConfigType; + constructor(initializerContext: PluginInitializerContext) { + this.config = initializerContext.config.get(); + } + + public setup( + core: CoreSetup + ) { + const result: SearchHomepagePluginSetup = { + app: appInfo, + isHomepageFeatureEnabled() { + return isHomepageEnabled(core.uiSettings); + }, + }; + if (!this.config.ui?.enabled) return result; + if (!isHomepageEnabled(core.uiSettings)) return result; + + core.application.register({ + ...result.app, + async mount({ element, history }: AppMountParameters) { + const { renderApp } = await import('./application'); + const [coreStart, depsStart] = await core.getStartServices(); + const startDeps: SearchHomepageAppPluginStartDependencies = { + ...depsStart, + history, + }; + + return renderApp(coreStart, startDeps, element); + }, + }); + + return result; + } + + public start(core: CoreStart) { + return { + app: appInfo, + isHomepageFeatureEnabled() { + return isHomepageEnabled(core.uiSettings); + }, + SearchHomepage, + }; + } +} diff --git a/x-pack/plugins/search_homepage/public/router.tsx b/x-pack/plugins/search_homepage/public/router.tsx new file mode 100644 index 0000000000000..e4db94ebde4ae --- /dev/null +++ b/x-pack/plugins/search_homepage/public/router.tsx @@ -0,0 +1,19 @@ +/* + * 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 { Route, Routes } from '@kbn/shared-ux-router'; + +import { SearchHomepagePage } from './components/search_homepage'; + +export const HomepageRouter = () => ( + + + + + +); diff --git a/x-pack/plugins/search_homepage/public/types.ts b/x-pack/plugins/search_homepage/public/types.ts new file mode 100644 index 0000000000000..de5283abfc61d --- /dev/null +++ b/x-pack/plugins/search_homepage/public/types.ts @@ -0,0 +1,74 @@ +/* + * 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 type { ComponentProps, FC } from 'react'; +import type { CloudSetup } from '@kbn/cloud-plugin/public'; +import type { ConsolePluginStart } from '@kbn/console-plugin/public'; +import type { AppMountParameters, HttpStart } from '@kbn/core/public'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; +import type { App } from './components/stack_app'; + +export interface SearchHomepageConfigType { + ui: { + enabled: boolean; + }; +} + +export interface SearchHomepageAppInfo { + appRoute: string; + id: string; + title: string; +} + +export interface SearchHomepagePluginSetup { + /** + * Search Homepage shared information for the Kibana application. + * Used to ensure the stack and serverless apps have the same route + * and deep links. + */ + app: SearchHomepageAppInfo; + /** + * Checks if the Search Homepage feature flag is currently enabled. + * @returns true if Search Homepage feature is enabled + */ + isHomepageFeatureEnabled: () => boolean; +} + +export interface SearchHomepagePluginStart { + /** + * Search Homepage shared information for the Kibana application. + * Used to ensure the stack and serverless apps have the same route + * and deep links. + */ + app: SearchHomepageAppInfo; + /** + * Checks if the Search Homepage feature flag is currently enabled. + * @returns true if Search Homepage feature is enabled + */ + isHomepageFeatureEnabled: () => boolean; + /** + * SearchHomepage shared component, used to render the search homepage in + * the Stack search plugin + */ + SearchHomepage: FC>; +} + +export interface SearchHomepageAppPluginStartDependencies { + history: AppMountParameters['history']; + usageCollection?: UsageCollectionStart; + share: SharePluginStart; + console?: ConsolePluginStart; +} + +export interface SearchHomepageServicesContext { + http: HttpStart; + share: SharePluginStart; + cloud?: CloudSetup; + usageCollection?: UsageCollectionStart; + console?: ConsolePluginStart; +} diff --git a/x-pack/plugins/search_homepage/server/config.ts b/x-pack/plugins/search_homepage/server/config.ts new file mode 100644 index 0000000000000..3e068a719f046 --- /dev/null +++ b/x-pack/plugins/search_homepage/server/config.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from '@kbn/core/server'; + +export * from './types'; + +const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + }), +}); + +export type SearchHomepageConfig = TypeOf; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: configSchema, +}; diff --git a/x-pack/plugins/search_homepage/server/index.ts b/x-pack/plugins/search_homepage/server/index.ts new file mode 100644 index 0000000000000..864af85c0a2fb --- /dev/null +++ b/x-pack/plugins/search_homepage/server/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializerContext } from '@kbn/core/server'; + +export { config } from './config'; + +export async function plugin(initializerContext: PluginInitializerContext) { + const { SearchHomepagePlugin } = await import('./plugin'); + return new SearchHomepagePlugin(initializerContext); +} + +export type { SearchHomepagePluginSetup, SearchHomepagePluginStart } from './types'; diff --git a/x-pack/plugins/search_homepage/server/plugin.ts b/x-pack/plugins/search_homepage/server/plugin.ts new file mode 100644 index 0000000000000..f446ba4e41fd3 --- /dev/null +++ b/x-pack/plugins/search_homepage/server/plugin.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server'; + +import { SearchHomepagePluginSetup, SearchHomepagePluginStart } from './types'; + +export class SearchHomepagePlugin + implements Plugin +{ + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup<{}, SearchHomepagePluginStart>) { + this.logger.debug('searchHomepage: Setup'); + return {}; + } + + public start(core: CoreStart) { + return {}; + } +} diff --git a/x-pack/plugins/search_homepage/server/types.ts b/x-pack/plugins/search_homepage/server/types.ts new file mode 100644 index 0000000000000..c4e5d46959422 --- /dev/null +++ b/x-pack/plugins/search_homepage/server/types.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SearchHomepagePluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SearchHomepagePluginStart {} diff --git a/x-pack/plugins/search_homepage/tsconfig.json b/x-pack/plugins/search_homepage/tsconfig.json new file mode 100644 index 0000000000000..9f084b32f7d3b --- /dev/null +++ b/x-pack/plugins/search_homepage/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + }, + "include": [ + "__mocks__/**/*", + "common/**/*", + "public/**/*", + "server/**/*", + ], + "kbn_references": [ + "@kbn/core", + "@kbn/react-kibana-context-render", + "@kbn/kibana-react-plugin", + "@kbn/i18n-react", + "@kbn/shared-ux-router", + "@kbn/shared-ux-page-kibana-template", + "@kbn/shared-ux-utility", + "@kbn/i18n", + "@kbn/cloud-plugin", + "@kbn/console-plugin", + "@kbn/share-plugin", + "@kbn/usage-collection-plugin", + "@kbn/config-schema", + ], + "exclude": [ + "target/**/*", + ] +} diff --git a/x-pack/plugins/serverless_search/kibana.jsonc b/x-pack/plugins/serverless_search/kibana.jsonc index 3a98a87c032a4..04002ee897cf4 100644 --- a/x-pack/plugins/serverless_search/kibana.jsonc +++ b/x-pack/plugins/serverless_search/kibana.jsonc @@ -29,6 +29,7 @@ "optionalPlugins": [ "indexManagement", "searchConnectors", + "searchHomepage", "searchInferenceEndpoints", "usageCollection" ], diff --git a/x-pack/plugins/serverless_search/public/navigation_tree.ts b/x-pack/plugins/serverless_search/public/navigation_tree.ts index d21eee8de9c19..f5618e8b83e05 100644 --- a/x-pack/plugins/serverless_search/public/navigation_tree.ts +++ b/x-pack/plugins/serverless_search/public/navigation_tree.ts @@ -9,7 +9,7 @@ import type { NavigationTreeDefinition } from '@kbn/core-chrome-browser'; import { i18n } from '@kbn/i18n'; import { CONNECTORS_LABEL } from '../common/i18n_string'; -export const navigationTree: NavigationTreeDefinition = { +export const navigationTree = (useSearchHomepage: boolean = false): NavigationTreeDefinition => ({ body: [ { type: 'navGroup', @@ -25,7 +25,7 @@ export const navigationTree: NavigationTreeDefinition = { title: i18n.translate('xpack.serverlessSearch.nav.home', { defaultMessage: 'Home', }), - link: 'serverlessElasticsearch', + link: useSearchHomepage ? 'searchHomepage' : 'serverlessElasticsearch', spaceBefore: 'm', }, { @@ -149,4 +149,4 @@ export const navigationTree: NavigationTreeDefinition = { ], }, ], -}; +}); diff --git a/x-pack/plugins/serverless_search/public/plugin.ts b/x-pack/plugins/serverless_search/public/plugin.ts index d9019f911444a..e72e1a4575079 100644 --- a/x-pack/plugins/serverless_search/public/plugin.ts +++ b/x-pack/plugins/serverless_search/public/plugin.ts @@ -43,6 +43,9 @@ export class ServerlessSearchPlugin core: CoreSetup, setupDeps: ServerlessSearchPluginSetupDependencies ): ServerlessSearchPluginSetup { + const { searchHomepage } = setupDeps; + const useSearchHomepage = searchHomepage && searchHomepage.isHomepageFeatureEnabled(); + const queryClient = new QueryClient({ mutationCache: new MutationCache({ onError: (error) => { @@ -69,6 +72,24 @@ export class ServerlessSearchPlugin }, }), }); + if (useSearchHomepage) { + core.application.register({ + id: 'serverlessHomeRedirect', + title: i18n.translate('xpack.serverlessSearch.app.home.title', { + defaultMessage: 'Home', + }), + appRoute: '/app/elasticsearch', + euiIconType: 'logoElastic', + category: DEFAULT_APP_CATEGORIES.enterpriseSearch, + visibleIn: [], + async mount({}: AppMountParameters) { + const [coreStart] = await core.getStartServices(); + coreStart.application.navigateToApp('searchHomepage'); + return () => {}; + }, + }); + } + core.application.register({ id: 'serverlessElasticsearch', title: i18n.translate('xpack.serverlessSearch.app.elasticsearch.title', { @@ -76,7 +97,7 @@ export class ServerlessSearchPlugin }), euiIconType: 'logoElastic', category: DEFAULT_APP_CATEGORIES.enterpriseSearch, - appRoute: '/app/elasticsearch', + appRoute: useSearchHomepage ? '/app/elasticsearch/getting_started' : '/app/elasticsearch', async mount({ element, history }: AppMountParameters) { const { renderApp } = await import('./application/elasticsearch'); const [coreStart, services] = await core.getStartServices(); @@ -121,10 +142,12 @@ export class ServerlessSearchPlugin core: CoreStart, services: ServerlessSearchPluginStartDependencies ): ServerlessSearchPluginStart { - const { serverless, management, indexManagement, security } = services; - serverless.setProjectHome('/app/elasticsearch'); + const { serverless, management, indexManagement, security, searchHomepage } = services; + const useSearchHomepage = searchHomepage && searchHomepage.isHomepageFeatureEnabled(); + + serverless.setProjectHome(useSearchHomepage ? '/app/elasticsearch/home' : '/app/elasticsearch'); - const navigationTree$ = of(navigationTree); + const navigationTree$ = of(navigationTree(searchHomepage?.isHomepageFeatureEnabled() ?? false)); serverless.initNavigation('search', navigationTree$, { dataTestSubj: 'svlSearchSideNav' }); const extendCardNavDefinitions = serverless.getNavigationCards( diff --git a/x-pack/plugins/serverless_search/public/types.ts b/x-pack/plugins/serverless_search/public/types.ts index e4eab5fbfd61d..d3011210c524f 100644 --- a/x-pack/plugins/serverless_search/public/types.ts +++ b/x-pack/plugins/serverless_search/public/types.ts @@ -5,16 +5,20 @@ * 2.0. */ -import { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; -import { ConsolePluginStart } from '@kbn/console-plugin/public'; -import { SearchPlaygroundPluginStart } from '@kbn/search-playground/public'; -import { SearchInferenceEndpointsPluginStart } from '@kbn/search-inference-endpoints/public'; -import { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public'; -import { SecurityPluginStart } from '@kbn/security-plugin/public'; -import { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; -import { SharePluginStart } from '@kbn/share-plugin/public'; -import { IndexManagementPluginStart } from '@kbn/index-management-plugin/public'; +import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; +import type { ConsolePluginStart } from '@kbn/console-plugin/public'; +import type { SearchInferenceEndpointsPluginStart } from '@kbn/search-inference-endpoints/public'; +import type { SearchPlaygroundPluginStart } from '@kbn/search-playground/public'; +import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public'; +import type { SecurityPluginStart } from '@kbn/security-plugin/public'; +import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import type { IndexManagementPluginStart } from '@kbn/index-management-plugin/public'; import type { DiscoverSetup } from '@kbn/discover-plugin/public'; +import type { + SearchHomepagePluginSetup, + SearchHomepagePluginStart, +} from '@kbn/search-homepage/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ServerlessSearchPluginSetup {} @@ -27,6 +31,7 @@ export interface ServerlessSearchPluginSetupDependencies { management: ManagementSetup; serverless: ServerlessPluginSetup; discover: DiscoverSetup; + searchHomepage?: SearchHomepagePluginSetup; } export interface ServerlessSearchPluginStartDependencies { @@ -39,4 +44,5 @@ export interface ServerlessSearchPluginStartDependencies { serverless: ServerlessPluginStart; share: SharePluginStart; indexManagement?: IndexManagementPluginStart; + searchHomepage?: SearchHomepagePluginStart; } diff --git a/x-pack/plugins/serverless_search/tsconfig.json b/x-pack/plugins/serverless_search/tsconfig.json index 8cf2dec121d46..418dcb5fc6f5c 100644 --- a/x-pack/plugins/serverless_search/tsconfig.json +++ b/x-pack/plugins/serverless_search/tsconfig.json @@ -50,5 +50,6 @@ "@kbn/react-kibana-context-render", "@kbn/search-playground", "@kbn/search-inference-endpoints", + "@kbn/search-homepage", ] } diff --git a/x-pack/test_serverless/functional/page_objects/index.ts b/x-pack/test_serverless/functional/page_objects/index.ts index f1604d48508e2..94e02f9c5e455 100644 --- a/x-pack/test_serverless/functional/page_objects/index.ts +++ b/x-pack/test_serverless/functional/page_objects/index.ts @@ -21,6 +21,7 @@ import { SvlRuleDetailsPageProvider } from './svl_rule_details_ui_page'; import { SvlSearchConnectorsPageProvider } from './svl_search_connectors_page'; import { SvlManagementPageProvider } from './svl_management_page'; import { SvlIngestPipelines } from './svl_ingest_pipelines'; +import { SvlSearchHomePageProvider } from './svl_search_homepage'; export const pageObjects = { ...xpackFunctionalPageObjects, @@ -38,4 +39,5 @@ export const pageObjects = { svlRuleDetailsUI: SvlRuleDetailsPageProvider, svlManagementPage: SvlManagementPageProvider, svlIngestPipelines: SvlIngestPipelines, + svlSearchHomePage: SvlSearchHomePageProvider, }; diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_homepage.ts b/x-pack/test_serverless/functional/page_objects/svl_search_homepage.ts new file mode 100644 index 0000000000000..eeb1b6de731f9 --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/svl_search_homepage.ts @@ -0,0 +1,26 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function SvlSearchHomePageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + + return { + async expectToBeOnHomepage() { + expect(await browser.getCurrentUrl()).contain('/app/elasticsearch/home'); + }, + async expectToNotBeOnHomepage() { + expect(await browser.getCurrentUrl()).not.contain('/app/elasticsearch/home'); + }, + async expectHomepageHeader() { + await testSubjects.existOrFail('search-homepage-header', { timeout: 2000 }); + }, + }; +} diff --git a/x-pack/test_serverless/functional/services/index.ts b/x-pack/test_serverless/functional/services/index.ts index a1112232377cd..c63a16b4402f1 100644 --- a/x-pack/test_serverless/functional/services/index.ts +++ b/x-pack/test_serverless/functional/services/index.ts @@ -16,6 +16,7 @@ import { SvlCommonScreenshotsProvider } from './svl_common_screenshots'; import { SvlCasesServiceProvider } from '../../api_integration/services/svl_cases'; import { MachineLearningProvider } from './ml'; import { LogsSynthtraceProvider } from './log'; +import { UISettingsServiceProvider } from './ui_settings'; export const services = { // deployment agnostic FTR services @@ -30,6 +31,7 @@ export const services = { svlCommonScreenshots: SvlCommonScreenshotsProvider, svlCases: SvlCasesServiceProvider, svlMl: MachineLearningProvider, + uiSettings: UISettingsServiceProvider, // log services svlLogsSynthtraceClient: LogsSynthtraceProvider, }; diff --git a/x-pack/test_serverless/functional/services/ui_settings.ts b/x-pack/test_serverless/functional/services/ui_settings.ts new file mode 100644 index 0000000000000..337930790489d --- /dev/null +++ b/x-pack/test_serverless/functional/services/ui_settings.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; +import { RoleCredentials } from '../../shared/services'; + +export function UISettingsServiceProvider({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + return { + async setUiSetting(role: RoleCredentials, settingId: string, value: unknown) { + await supertestWithoutAuth + .post(`/internal/kibana/settings/${settingId}`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(role.apiKeyHeader) + .send({ value }) + .expect(200); + }, + async deleteUISetting(role: RoleCredentials, settingId: string) { + await supertestWithoutAuth + .delete(`/internal/kibana/settings/${settingId}`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(role.apiKeyHeader) + .expect(200); + }, + }; +} diff --git a/x-pack/test_serverless/functional/test_suites/search/index.ts b/x-pack/test_serverless/functional/test_suites/search/index.ts index 455acc0404429..8c3cfd83e04e9 100644 --- a/x-pack/test_serverless/functional/test_suites/search/index.ts +++ b/x-pack/test_serverless/functional/test_suites/search/index.ts @@ -23,5 +23,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./playground_overview')); loadTestFile(require.resolve('./ml')); + loadTestFile(require.resolve('./search_homepage')); }); } diff --git a/x-pack/test_serverless/functional/test_suites/search/search_homepage.ts b/x-pack/test_serverless/functional/test_suites/search/search_homepage.ts new file mode 100644 index 0000000000000..3138e0e7f0242 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/search_homepage.ts @@ -0,0 +1,58 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; +import { RoleCredentials } from '../../../shared/services'; + +import { testHasEmbeddedConsole } from './embedded_console'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['svlCommonPage', 'svlCommonNavigation', 'svlSearchHomePage']); + const svlUserManager = getService('svlUserManager'); + const uiSettings = getService('uiSettings'); + let roleAuthc: RoleCredentials; + + const HOMEPAGE_FF_UI_SETTING = 'searchHomepage:homepageEnabled'; + describe('Search Homepage', function () { + this.tags('skipMKI'); + before(async () => { + roleAuthc = await svlUserManager.createApiKeyForRole('admin'); + // Enable Homepage Feature Flag + await uiSettings.setUiSetting(roleAuthc, HOMEPAGE_FF_UI_SETTING, true); + + await pageObjects.svlCommonPage.login(); + }); + + after(async () => { + if (!roleAuthc) return; + + // Disable Homepage Feature Flag + await uiSettings.deleteUISetting(roleAuthc, HOMEPAGE_FF_UI_SETTING); + + await pageObjects.svlCommonPage.forceLogout(); + }); + + it('has search homepage with Home sidenav', async () => { + pageObjects.svlSearchHomePage.expectToBeOnHomepage(); + pageObjects.svlSearchHomePage.expectHomepageHeader(); + // Navigate to another page + await pageObjects.svlCommonNavigation.sidenav.clickLink({ + deepLinkId: 'serverlessConnectors', + }); + pageObjects.svlSearchHomePage.expectToNotBeOnHomepage(); + // Click Home in Side nav + await pageObjects.svlCommonNavigation.sidenav.clickLink({ + deepLinkId: 'searchHomepage', + }); + pageObjects.svlSearchHomePage.expectToBeOnHomepage(); + }); + + it('has embedded dev console', async () => { + testHasEmbeddedConsole(pageObjects); + }); + }); +} diff --git a/yarn.lock b/yarn.lock index 64ba753c57695..d1dec3cae024a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6056,6 +6056,10 @@ version "0.0.0" uid "" +"@kbn/search-homepage@link:x-pack/plugins/search_homepage": + version "0.0.0" + uid "" + "@kbn/search-index-documents@link:packages/kbn-search-index-documents": version "0.0.0" uid ""