diff --git a/src/plugins/home/public/application/components/homepage/_homepage.scss b/src/plugins/home/public/application/components/homepage/_homepage.scss index dc971440c126..25a8530c7394 100644 --- a/src/plugins/home/public/application/components/homepage/_homepage.scss +++ b/src/plugins/home/public/application/components/homepage/_homepage.scss @@ -1,3 +1,5 @@ +@import "sections/hero"; + .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. diff --git a/src/plugins/home/public/application/components/homepage/sections/hero.scss b/src/plugins/home/public/application/components/homepage/sections/hero.scss new file mode 100644 index 000000000000..3e226faeffc3 --- /dev/null +++ b/src/plugins/home/public/application/components/homepage/sections/hero.scss @@ -0,0 +1,35 @@ +.home-hero-title { + color: $euiColorPrimary; +} + +.home-hero-descriptionSection { + align-items: flex-start; +} + +.home-hero-group > * { + flex-shrink: 0; +} + +.home-getStarted-chatIcon { + padding-left: $euiSizeXS; +} + +.home-hero-illustrationContainer { + position: relative; + display: inline-block; +} + +.home-hero-illustrationButton { + z-index: 1040; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + + &:hover:not([class*="isDisabled"]), + &:active:not([class*="isDisabled"]), + &:focus:not([class*="isDisabled"]) { + transform: translate(-50%, -50%); + animation: none !important; + } +} diff --git a/src/plugins/home/public/application/components/homepage/sections/hero.tsx b/src/plugins/home/public/application/components/homepage/sections/hero.tsx new file mode 100644 index 000000000000..5a5c6b6b9e3a --- /dev/null +++ b/src/plugins/home/public/application/components/homepage/sections/hero.tsx @@ -0,0 +1,170 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiTitle, + EuiImage, + EuiLink, + EuiButtonIcon, + EuiText, + EuiSpacer, +} from '@elastic/eui'; +import illustration from '../../../../assets/illustration.svg'; +import { getServices } from '../../../opensearch_dashboards_services'; +import { renderFn } from './utils'; + +// TODO: This is hardcoded for the playground. Do not use long term +const DEFAULT_HERO_DATA = { + title: 'Try the Query Assistant', + body: + 'Automatically generate complex queries using simple conversational prompts. AI assisted summary helps you navigate and understand errors from your logs.{br}{br}You will be redirected to the AI playground where you will need to login. All the {terms} of the playground still apply.', + externalActionButton: { + text: 'Login to try', + link: 'https://ai.playground.opensearch.org/', + }, + img: { + link: 'https://www.youtube.com/watch?v=VTiJtGI2Sr4', + }, + secondaryButton: { + text: 'Learn more', + link: 'https://opensearch.org/blog/opensearch-adds-new-generative-ai-assistant-toolkit/', + }, +}; + +export const GetStartedSection: React.FC = () => { + const services = getServices(); + const addBasePath = services.http.basePath.prepend; + const heroConfig = DEFAULT_HERO_DATA; + + const description = ( + , + terms: ( + + + + ), + }} + /> + ); + + const actionButton = ( + + + + ); + + const illustrationPanel = heroConfig.img ? ( +
+ + +
+ ) : ( + + ); + + function getIllustrationImage() { + return addBasePath('/plugins/home/assets/screenshot.png'); + } + + const links = [ + { + text: i18n.translate('home.getStarted.learnMore', { + defaultMessage: heroConfig.secondaryButton.text, + }), + url: heroConfig.secondaryButton.link, + }, + ]; + + return ( + } + illustrationEle={illustrationPanel} + /> + ); +}; + +interface HeroSectionProps { + title: string; + description: React.ReactNode; + links: Array<{ + text: string; + url: string; + }>; + actionButton: React.ReactNode; + content: React.ReactNode; + illustrationEle: React.ReactNode; +} + +export const HeroSection: React.FC = ({ + title, + description, + links, + actionButton, + content, + illustrationEle, +}) => { + return ( + + {illustrationEle} + + +

{title}

+
+ {description} + + {actionButton} + + + {links.map((link) => ( + + + {link.text} + + + ))} + +
+ {content} +
+ ); +}; + +export const render = renderFn(() => { + return ; +}); + +export const heroSection = { + id: 'home:query-assist', + render, +}; diff --git a/src/plugins/home/public/assets/illustration.svg b/src/plugins/home/public/assets/illustration.svg new file mode 100644 index 000000000000..879f8deb8efa --- /dev/null +++ b/src/plugins/home/public/assets/illustration.svg @@ -0,0 +1,341 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/home/public/assets/screenshot.png b/src/plugins/home/public/assets/screenshot.png new file mode 100644 index 000000000000..60963cdb9429 Binary files /dev/null and b/src/plugins/home/public/assets/screenshot.png differ diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 75b39dadd8c0..1fafbc0e8a17 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -61,6 +61,7 @@ import { PLUGIN_ID, HOME_APP_BASE_PATH } from '../common/constants'; import { DataSourcePluginStart } from '../../data_source/public'; import { workWithDataSection } from './application/components/homepage/sections/work_with_data'; import { learnBasicsSection } from './application/components/homepage/sections/learn_basics'; +import { heroSection } from './application/components/homepage/sections/hero'; export interface HomePluginStartDependencies { data: DataPublicPluginStart; @@ -159,6 +160,7 @@ export class HomePublicPlugin sectionTypes.registerSection(workWithDataSection); sectionTypes.registerSection(learnBasicsSection); + sectionTypes.registerHeroSection(heroSection); return { featureCatalogue, diff --git a/src/plugins/home/public/services/section_type/section_type.ts b/src/plugins/home/public/services/section_type/section_type.ts index 5b57b6e0f272..a80181575769 100644 --- a/src/plugins/home/public/services/section_type/section_type.ts +++ b/src/plugins/home/public/services/section_type/section_type.ts @@ -11,6 +11,23 @@ import { DataPublicPluginStart } from '../../../../data/public'; import { SavedObjectLoader } from '../../../../saved_objects/public'; import { createSavedHomepageLoader, SavedHomepage } from '../../saved_homepage'; +// TODO: This is temporarily hardcoded for the playground +const PLAYGROUND_HOMEPAGE = { + heroes: [ + { + id: 'home:query-assist', + }, + ], + sections: [ + { + id: 'home:workWithData', + }, + { + id: 'home:learnBasics', + }, + ], +}; + // TODO: this should support error handling explicitly // TODO: this should support async rendering through a promise export type RenderFn = (element: HTMLElement) => () => void; @@ -102,42 +119,43 @@ export class SectionTypeService { const subscriptions = new Subscription(); - this.fetchHomepageData() - .then((homepage) => { - const initialHeroes = Array.isArray(homepage.heroes) ? homepage.heroes : [homepage.heroes]; - const initialSections = Array.isArray(homepage.sections) - ? homepage.sections - : [homepage.sections]; - - heroes$.next(initialHeroes.map((hero) => this.heroSections[hero.id]).filter(Boolean)); - sections$.next(initialSections.map((section) => this.sections[section.id]).filter(Boolean)); - error$.next(undefined); - - // TODO: make this debounce time configurable - const combinedSave$ = combineLatest([heroes$, sections$]).pipe(debounceTime(1000)); - - subscriptions.add( - combinedSave$.subscribe(([heroes, sections]) => { - if (heroes) { - homepage.heroes = heroes.map((hero) => ({ id: hero.id })); - } - - if (sections) { - homepage.sections = sections.map((section) => ({ id: section.id })); - } - - if (heroes || sections) { - homepage - .save({}) - .then(() => error$.next(undefined)) - .catch((e) => error$.next(e)); - } - }) - ); + // this.fetchHomepageData() + // .then((homepage) => { + const homepage = PLAYGROUND_HOMEPAGE; + const initialHeroes = Array.isArray(homepage.heroes) ? homepage.heroes : [homepage.heroes]; + const initialSections = Array.isArray(homepage.sections) + ? homepage.sections + : [homepage.sections]; + + heroes$.next(initialHeroes.map((hero) => this.heroSections[hero.id]).filter(Boolean)); + sections$.next(initialSections.map((section) => this.sections[section.id]).filter(Boolean)); + error$.next(undefined); + + // TODO: make this debounce time configurable + const combinedSave$ = combineLatest([heroes$, sections$]).pipe(debounceTime(1000)); + + subscriptions.add( + combinedSave$.subscribe(([heroes, sections]) => { + if (heroes) { + homepage.heroes = heroes.map((hero) => ({ id: hero.id })); + } + + if (sections) { + homepage.sections = sections.map((section) => ({ id: section.id })); + } + + // if (heroes || sections) { + // homepage + // .save({}) + // .then(() => error$.next(undefined)) + // .catch((e) => error$.next(e)); + // } }) - .catch((e) => { - error$.next(e); - }); + ); + // }) + // .catch((e) => { + // error$.next(e); + // }); return { heroes$: heroes$.asObservable(),