diff --git a/.github/workflows/cypress_workflow.yml b/.github/workflows/cypress_workflow.yml index 14975e23b17a..565daf40603c 100644 --- a/.github/workflows/cypress_workflow.yml +++ b/.github/workflows/cypress_workflow.yml @@ -28,7 +28,7 @@ on: env: TEST_REPO: ${{ inputs.test_repo != '' && inputs.test_repo || 'opensearch-project/opensearch-dashboards-functional-test' }} - TEST_BRANCH: "${{ inputs.test_branch != '' && inputs.test_branch || github.base_ref }}" + TEST_BRANCH: "${{ 'feature/new-home' || inputs.test_branch != '' && inputs.test_branch || github.base_ref }}" FTR_PATH: 'ftr' START_CMD: 'node ../scripts/opensearch_dashboards --dev --no-base-path --no-watch --savedObjects.maxImportPayloadBytes=10485760 --server.maxPayloadBytes=1759977 --logging.json=false --data.search.aggs.shardDelay.enabled=true' OPENSEARCH_SNAPSHOT_CMD: 'node ../scripts/opensearch snapshot -E cluster.routing.allocation.disk.threshold_enabled=false' diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ca74788d8fd..0ff26e9ca74d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Discover] Added customizable pagination options based on Discover UI settings [#5610](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5610) - [Chrome] Introduce registerCollapsibleNavHeader to allow plugins to customize the rendering of nav menu header ([#5244](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5244)) - [Custom Branding] Relative URL should be allowed for logos ([#5572](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5572)) +- Implement new homepage ([#5613](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5613)) - [Discover] Enhanced the data source selector with added sorting functionality ([#5609](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/5609)) ### 🐛 Bug Fixes diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 9c6c040433b4..d1535f4d4f03 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -38,6 +38,9 @@ # Set the value to true to disable the new theme introduction modal #home.disableNewThemeModal: false +# Set the value to true to make the next homepage the default. Both homepages will be available regardless of this setting. +#home.preferNextHomePage: false + # Setting for an optimized healthcheck that only uses the local OpenSearch node to do Dashboards healthcheck. # This settings should be used for large clusters or for clusters with ingest heavy nodes. # It allows Dashboards to only healthcheck using the local OpenSearch node rather than fan out requests across all nodes. diff --git a/src/plugins/home/config.ts b/src/plugins/home/config.ts index 399c92174c6a..31ad716b07a9 100644 --- a/src/plugins/home/config.ts +++ b/src/plugins/home/config.ts @@ -33,6 +33,7 @@ import { schema, TypeOf } from '@osd/config-schema'; export const configSchema = schema.object({ disableWelcomeScreen: schema.boolean({ defaultValue: false }), disableNewThemeModal: schema.boolean({ defaultValue: false }), + preferNextHomePage: schema.boolean({ defaultValue: false }), }); export type ConfigSchema = TypeOf; diff --git a/src/plugins/home/opensearch_dashboards.json b/src/plugins/home/opensearch_dashboards.json index 40351c0dd83c..7dc3adf0d2c7 100644 --- a/src/plugins/home/opensearch_dashboards.json +++ b/src/plugins/home/opensearch_dashboards.json @@ -3,9 +3,7 @@ "version": "opensearchDashboards", "server": true, "ui": true, - "requiredPlugins": ["data", "urlForwarding"], + "requiredPlugins": ["data", "urlForwarding", "savedObjects"], "optionalPlugins": ["usageCollection", "telemetry", "dataSource"], - "requiredBundles": [ - "opensearchDashboardsReact", "dataSourceManagement" - ] + "requiredBundles": ["opensearchDashboardsReact", "dataSourceManagement"] } diff --git a/src/plugins/home/public/application/components/_index.scss b/src/plugins/home/public/application/components/_index.scss index 2c66b7bbdd31..8370781134ea 100644 --- a/src/plugins/home/public/application/components/_index.scss +++ b/src/plugins/home/public/application/components/_index.scss @@ -12,3 +12,4 @@ @import "synopsis"; @import "welcome"; @import "tutorial/tutorial"; +@import "homepage/homepage"; diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index 4e8dd16dd998..aa9108c42217 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -31,7 +31,8 @@ import React from 'react'; import { I18nProvider } from '@osd/i18n/react'; import PropTypes from 'prop-types'; -import { Home } from './home'; +import { Home } from './legacy/home'; +import { Homepage } from './homepage'; import { FeatureDirectory } from './feature_directory'; import { TutorialDirectory } from './tutorial_directory'; import { Tutorial } from './tutorial/tutorial'; @@ -56,6 +57,7 @@ export function HomeApp({ directories, solutions }) { addBasePath, environmentService, telemetry, + homeConfig, } = getServices(); const environment = environmentService.getEnvironment(); const isCloudEnabled = environment.cloud; @@ -83,6 +85,20 @@ export function HomeApp({ directories, solutions }) { ); }; + const legacyHome = ( + + ); + + const homepage = ; + return ( @@ -92,17 +108,25 @@ export function HomeApp({ directories, solutions }) { - - - + {homeConfig.preferNextHomePage ? ( + <> + + {legacyHome} + + + {homepage} + + + ) : ( + <> + + {homepage} + + + {legacyHome} + + + )} diff --git a/src/plugins/home/public/application/components/homepage/_homepage.scss b/src/plugins/home/public/application/components/homepage/_homepage.scss new file mode 100644 index 000000000000..dc971440c126 --- /dev/null +++ b/src/plugins/home/public/application/components/homepage/_homepage.scss @@ -0,0 +1,9 @@ +.home-homepage-pageBody { + // This is needed to make sure the page body is not wider than the page. + // This is otherwise not possible with the props on EuiPageTemplate. + padding: 0 $euiSizeL; +} + +.home-homepage-body--fill { + min-height: $euiSize * 50; +} diff --git a/src/plugins/home/public/application/components/homepage/footer.tsx b/src/plugins/home/public/application/components/homepage/footer.tsx new file mode 100644 index 000000000000..41e7c3123f37 --- /dev/null +++ b/src/plugins/home/public/application/components/homepage/footer.tsx @@ -0,0 +1,105 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { HOME_APP_BASE_PATH } from '../../../../common/constants'; +import { + RedirectAppLinks, + useOpenSearchDashboards, + useUiSetting$, +} from '../../../../../opensearch_dashboards_react/public'; + +export const Footer: React.FC = () => { + const [defaultRoute, setDefaultRoute] = useUiSetting$('defaultRoute'); + const { + services: { + application, + notifications: { toasts }, + }, + } = useOpenSearchDashboards(); + + const getUrlForApp = application.getUrlForApp; + const { show, save } = application.capabilities.advancedSettings ?? {}; + + const isAdvancedSettingsEnabled = show && save; + + const defaultRouteButton = + defaultRoute === HOME_APP_BASE_PATH ? ( + + + + + + ) : ( + { + setDefaultRoute(HOME_APP_BASE_PATH); + toasts.addSuccess({ + title: i18n.translate('home.footer.changeDefaultRouteSuccessToast', { + defaultMessage: 'Landing page updated', + }), + }); + }} + size="xs" + > + + + ); + + return ( + + {isAdvancedSettingsEnabled && {defaultRouteButton}} + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/plugins/home/public/application/components/homepage/hero_section.tsx b/src/plugins/home/public/application/components/homepage/hero_section.tsx new file mode 100644 index 000000000000..5e8348c664b8 --- /dev/null +++ b/src/plugins/home/public/application/components/homepage/hero_section.tsx @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { FC } from 'react'; +import { EuiPanel } from '@elastic/eui'; +import { RenderFn } from '../../../services/section_type/section_type'; +import { LazyRender } from './lazy_render'; + +interface Props { + render: RenderFn; +} + +export const HeroSection: FC = ({ render }) => { + return ( + + + + ); +}; diff --git a/src/plugins/home/public/application/components/homepage/homepage.tsx b/src/plugins/home/public/application/components/homepage/homepage.tsx new file mode 100644 index 000000000000..aca9e9d9374b --- /dev/null +++ b/src/plugins/home/public/application/components/homepage/homepage.tsx @@ -0,0 +1,278 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect } from 'react'; +import { useMount } from 'react-use'; +import { Subscription } from 'rxjs'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +import { + EuiPageTemplate, + EuiButtonEmpty, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiIcon, +} from '@elastic/eui'; +import { + HeroSection as HeroSectionType, + Section as SectionType, +} from '../../../services/section_type/section_type'; +import { getServices } from '../../opensearch_dashboards_services'; +import { HeroSection } from './hero_section'; +import { Section } from './section'; +import { Footer } from './footer'; +import { Welcome } from '../welcome'; + +const KEY_ENABLE_WELCOME = 'home:welcome:show'; + +const useHomepage = () => { + const { sectionTypes } = getServices(); + const [heroes, setHeroes] = useState(); + const [sections, setSections] = useState(); + const [error, setError] = useState(); + + useEffect(() => { + const homepage = sectionTypes.getHomepage(); + + const subscriptions = new Subscription(); + subscriptions.add(homepage.heroes$.subscribe(setHeroes)); + subscriptions.add(homepage.sections$.subscribe(setSections)); + subscriptions.add(homepage.error$.subscribe(setError)); + + return () => { + subscriptions.unsubscribe(); + homepage.cleanup(); + }; + }, [sectionTypes]); + + const isLoading = !heroes && !sections; + + return { heroes, sections, error, isLoading }; +}; + +const useShowWelcome = () => { + const [isLoading, setIsLoading] = useState(true); + const [isNewInstance, setIsNewInstance] = useState(false); + const { homeConfig, savedObjectsClient } = getServices(); + + const [isWelcomeEnabled, setIsWelcomeEnabled] = useState( + !(homeConfig.disableWelcomeScreen || localStorage.getItem(KEY_ENABLE_WELCOME) === 'false') + ); + + useEffect(() => { + if (!isWelcomeEnabled) { + setIsLoading(false); + return; + } + + const timeout = setTimeout(() => { + setIsLoading(false); + setIsWelcomeEnabled(false); + }, 500); + + savedObjectsClient + .find({ + type: 'index-pattern', + fields: ['title'], + search: `*`, + searchFields: ['title'], + perPage: 1, + }) + .then((resp) => { + setIsLoading(false); + setIsNewInstance(resp.total === 0); + + clearTimeout(timeout); + }); + + return () => { + clearTimeout(timeout); + }; + }, [isWelcomeEnabled, savedObjectsClient]); + + return { + isLoading, + showWelcome: isWelcomeEnabled && isNewInstance, + onSkip: () => { + localStorage.setItem(KEY_ENABLE_WELCOME, 'false'); + setIsWelcomeEnabled(false); + }, + }; +}; + +const Content = () => { + const { heroes, sections, error, isLoading: isHomepageLoading } = useHomepage(); + const { + chrome: { logos }, + getBasePath, + injectedMetadata: { getBranding }, + telemetry, + toastNotifications, + } = getServices(); + const { isLoading: isWelcomeLoading, showWelcome, onSkip } = useShowWelcome(); + + useEffect(() => { + if (!error) { + return; + } + + toastNotifications.addDanger({ + title: i18n.translate('home.loadingError.title', { + defaultMessage: 'Error loading homepage', + }), + text: i18n.translate('home.loadingError.description', { + defaultMessage: + 'There was an error loading the homepage. Please refresh the page to try again.', + }), + }); + + // TODO: added a toast, but is there a better way to surface this error? + // eslint-disable-next-line no-console + console.error(error); + }, [toastNotifications, error]); + + if (error) { + return ( + + + + + + + + + ); + } + + if (isHomepageLoading || isWelcomeLoading) { + return ( + + + + + + + + + ); + } + + if (showWelcome) { + return ( + + ); + } + + const hero = heroes?.[0]; + + return ( + <> + {hero && } + {sections?.map(({ render, title, description, links }, i) => ( +
+ ))} + +