diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.styles.ts b/src/plugins/guided_onboarding/public/components/guide_panel.styles.ts index 121bafb23e8ee..fb636b706d93c 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.styles.ts +++ b/src/plugins/guided_onboarding/public/components/guide_panel.styles.ts @@ -6,8 +6,15 @@ * Side Public License, v 1. */ -import { EuiThemeComputed } from '@elastic/eui'; +import { + euiCanAnimate, + euiFlyoutSlideInRight, + euiYScrollWithShadows, + logicalCSS, + logicalCSSWithFallback, +} from '@elastic/eui'; import { css } from '@emotion/react'; +import { UseEuiTheme } from '@elastic/eui/src/services/theme/hooks'; import panelBgTop from '../../assets/panel_bg_top.svg'; import panelBgTopDark from '../../assets/panel_bg_top_dark.svg'; import panelBgBottom from '../../assets/panel_bg_bottom.svg'; @@ -21,54 +28,98 @@ import panelBgBottomDark from '../../assets/panel_bg_bottom_dark.svg'; * See https://github.com/elastic/eui/issues/6241 for more details */ export const getGuidePanelStyles = ({ - euiTheme, + euiThemeContext, isDarkTheme, }: { - euiTheme: EuiThemeComputed; + euiThemeContext: UseEuiTheme; isDarkTheme: boolean; -}) => ({ - setupButton: css` - margin-right: ${euiTheme.size.m}; - `, - wellDoneAnimatedPrompt: css` - text-align: left; - `, - flyoutOverrides: { - flyoutHeader: css` - background: url(${isDarkTheme ? panelBgTopDark : panelBgTop}) top right no-repeat; - `, - flyoutContainer: css` - top: 55px !important; - // Unsetting bottom and height default values to create auto height - bottom: unset !important; +}) => { + const euiTheme = euiThemeContext.euiTheme; + const flyoutContainerBase = css` + position: fixed; + height: 100%; + max-height: 76vh; + max-inline-size: 480px; + max-block-size: auto; + inset-inline-end: 0; + inset-block-start: ${euiTheme.size.xxxl}; + ${euiCanAnimate} { + animation: ${euiFlyoutSlideInRight} ${euiTheme.animation.normal} + ${euiTheme.animation.resistance}; + } + @media (min-width: ${euiTheme.breakpoint.m}px) { right: calc(${euiTheme.size.s} + 128px); // Accounting for margin on button - border-radius: 6px; - animation: euiModal 350ms cubic-bezier(0.34, 1.61, 0.7, 1); - box-shadow: none; - max-height: 76vh; - @media (max-width: ${euiTheme.breakpoint.s}px) { - right: 25px !important; - } - `, - flyoutBody: css` - overflow: auto; - .euiFlyoutBody__overflowContent { - width: 480px; - padding-top: 10px; - @media (max-width: ${euiTheme.breakpoint.s}px) { - width: 100%; - } - } - `, - flyoutFooter: css` - border-radius: 0 0 6px 6px; - background: url(${isDarkTheme ? panelBgBottomDark : panelBgBottom}) 0 7px no-repeat; - padding: 24px 30px; - height: 125px; + }) + `; + + return { + setupButton: css` + margin-right: ${euiTheme.size.m}; `, - flyoutFooterLink: css` - color: ${euiTheme.colors.darkShade}; - font-weight: 400; + wellDoneAnimatedPrompt: css` + text-align: left; `, - }, -}); + flyoutOverrides: { + flyoutContainer: css` + ${flyoutContainerBase}; + background: ${euiTheme.colors.emptyShade} url(${isDarkTheme ? panelBgTopDark : panelBgTop}) + top right no-repeat; + padding: 0; + `, + flyoutContainerError: css` + ${flyoutContainerBase}; + padding: 24px; + `, + flyoutHeader: css` + flex-grow: 0; + padding: 16px 16px 0; + `, + flyoutHeaderError: css` + flex-grow: 0; + padding: 8px 0 0; + `, + flyoutContentWrapper: css` + display: flex; + block-size: 100%; + justify-content: space-between; + flex-direction: column; + `, + flyoutCloseButtonIcon: css` + position: absolute; + inset-block-start: ${euiTheme.size.base}; + inset-inline-end: ${euiTheme.size.base}; + `, + flyoutBodyWrapper: css` + ${logicalCSS('height', '100%')} + ${logicalCSSWithFallback('overflow-y', 'hidden')} + flex-grow: 1; + `, + flyoutBody: css` + ${euiYScrollWithShadows(euiThemeContext, { + side: 'end', + })} + padding: 16px 10px 0 16px; + `, + flyoutBodyError: css` + height: 600px; + `, + flyoutStepsWrapper: css` + > li { + list-style-type: none; + } + margin-inline-start: 0 !important; + `, + flyoutFooter: css` + border-radius: 0 0 6px 6px; + background: url(${isDarkTheme ? panelBgBottomDark : panelBgBottom}) 0 36px no-repeat; + padding: 24px 30px; + height: 125px; + flex-grow: 0; + `, + flyoutFooterLink: css` + color: ${euiTheme.colors.darkShade}; + font-weight: 400; + `, + }, + }; +}; diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.tsx index e450eaedd85cd..2d3ef51489081 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.tsx @@ -7,26 +7,7 @@ */ import React, { useState, useEffect, useCallback } from 'react'; -import { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiFlyoutFooter, - EuiButton, - EuiText, - EuiProgress, - EuiHorizontalRule, - EuiSpacer, - htmlIdGenerator, - EuiButtonEmpty, - EuiTitle, - EuiLink, - EuiFlexGroup, - EuiFlexItem, - useEuiTheme, - EuiEmptyPrompt, - EuiImage, -} from '@elastic/eui'; +import { useEuiTheme } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -38,13 +19,11 @@ import type { GuidedOnboardingApi } from '../types'; import type { PluginState } from '../../common'; -import { GuideStep } from './guide_panel_step'; import { QuitGuideModal } from './quit_guide_modal'; import { getGuidePanelStyles } from './guide_panel.styles'; import { GuideButton } from './guide_button'; -import wellDoneAnimatedGif from '../../assets/well_done_animated.gif'; -import wellDoneAnimatedDarkGif from '../../assets/well_done_animated_dark.gif'; +import { GuidePanelFlyout } from './guide_panel_flyout'; interface GuidePanelProps { api: GuidedOnboardingApi; @@ -65,43 +44,9 @@ const getProgress = (state?: GuideState): number => { return 0; }; -const errorSection = ( - - {i18n.translate('guidedOnboarding.dropdownPanel.errorSectionTitle', { - defaultMessage: 'Unable to load the guide', - })} - - } - body={ - <> - - {i18n.translate('guidedOnboarding.dropdownPanel.errorSectionDescription', { - defaultMessage: `Wait a moment and try again. If the problem persists, contact your administrator.`, - })} - - - window.location.reload()} - iconType="refresh" - color="danger" - > - {i18n.translate('guidedOnboarding.dropdownPanel.errorSectionReloadButton', { - defaultMessage: 'Reload', - })} - - - } - /> -); - export const GuidePanel = ({ api, application, notifications, uiSettings }: GuidePanelProps) => { - const { euiTheme } = useEuiTheme(); + const euiThemeContext = useEuiTheme(); + const euiTheme = euiThemeContext.euiTheme; const [isGuideOpen, setIsGuideOpen] = useState(false); const [isQuitGuideModalOpen, setIsQuitGuideModalOpen] = useState(false); const [pluginState, setPluginState] = useState(undefined); @@ -109,7 +54,7 @@ export const GuidePanel = ({ api, application, notifications, uiSettings }: Guid const [isLoading, setIsLoading] = useState(false); const isDarkTheme = uiSettings.get('theme:darkMode'); - const styles = getGuidePanelStyles({ euiTheme, isDarkTheme }); + const styles = getGuidePanelStyles({ euiThemeContext, isDarkTheme }); const toggleGuide = () => { setIsGuideOpen((prevIsGuideOpen) => !prevIsGuideOpen); @@ -224,23 +169,7 @@ export const GuidePanel = ({ api, application, notifications, uiSettings }: Guid const stepsCompleted = getProgress(pluginState?.activeGuide); const isGuideReadyToComplete = pluginState?.activeGuide?.status === 'ready_to_complete'; - const getImageUrl = () => { - return isDarkTheme ? wellDoneAnimatedDarkGif : wellDoneAnimatedGif; - }; - const backToGuidesButton = ( - - {i18n.translate('guidedOnboarding.dropdownPanel.backToGuidesLink', { - defaultMessage: 'Back to guides', - })} - - ); return ( <>
@@ -254,228 +183,22 @@ export const GuidePanel = ({ api, application, notifications, uiSettings }: Guid />
- {isGuideOpen && ( - - {guideConfig && pluginState && pluginState.status !== 'error' ? ( - <> - - {backToGuidesButton} - -

- {isGuideReadyToComplete - ? i18n.translate('guidedOnboarding.dropdownPanel.completeGuideFlyoutTitle', { - defaultMessage: 'Well done!', - }) - : guideConfig.title} -

-
- - - -
- - -
- {isGuideReadyToComplete && ( - <> - - - - - )} - - -

- {isGuideReadyToComplete - ? i18n.translate( - 'guidedOnboarding.dropdownPanel.completeGuideFlyoutDescription', - { - defaultMessage: `You've completed the Elastic {guideName} guide. Feel free to come back to the Guides for more onboarding help or a refresher.`, - values: { - guideName: guideConfig.guideName, - }, - } - ) - : guideConfig.description} -

-
- - {guideConfig.docs && ( - <> - - - - {guideConfig.docs.text} - - - - )} - - {/* Progress bar should only show after the first step has been complete */} - {stepsCompleted > 0 && ( - <> - - - - - - )} - - - - {guideConfig?.steps.map((step, index) => { - const accordionId = htmlIdGenerator(`accordion${index}`)(); - const stepState = pluginState?.activeGuide?.steps[index]; - - if (stepState) { - return ( - handleStepButtonClick(stepState, step)} - key={accordionId} - telemetryGuideId={guideConfig!.telemetryId} - /> - ); - } - })} - - {isGuideReadyToComplete && ( - - - completeGuide(guideConfig.completedGuideRedirectLocation)} - fill - // data-test-subj used for FS tracking and testing - data-test-subj={`onboarding--completeGuideButton--${ - guideConfig!.telemetryId - }`} - > - {i18n.translate('guidedOnboarding.dropdownPanel.elasticButtonLabel', { - defaultMessage: 'Continue using Elastic', - })} - - - - )} -
-
- - - - - - {i18n.translate('guidedOnboarding.dropdownPanel.footer.support', { - defaultMessage: 'Need help?', - })} - - - - - | - - - - - {i18n.translate('guidedOnboarding.dropdownPanel.footer.feedback', { - defaultMessage: 'Give feedback', - })} - - - - - | - - - - - {i18n.translate( - 'guidedOnboarding.dropdownPanel.footer.exitGuideButtonLabel', - { - defaultMessage: 'Quit guide', - } - )} - - - - - - ) : ( - - {backToGuidesButton} - {errorSection} - - )} -
- )} + {isQuitGuideModalOpen && ( ; + guideConfig?: GuideConfig; + isDarkTheme: boolean; + stepsCompleted: number; + isGuideReadyToComplete: boolean; + pluginState?: PluginState; + handleStepButtonClick: ( + stepState: GuideStepType, + step: StepConfig + ) => Promise<{ pluginState: PluginState } | undefined>; + isLoading: boolean; + completeGuide: ( + completedGuideRedirectLocation: GuideConfig['completedGuideRedirectLocation'] + ) => Promise; +}) => { + const docsLink = () => { + if (!guideConfig || !guideConfig.docs) { + return null; + } + + return ( + <> + + + + {guideConfig.docs.text} + + + + ); + }; + + if (!guideConfig || !pluginState || (pluginState && pluginState.status === 'error')) { + return ( + + {i18n.translate('guidedOnboarding.dropdownPanel.errorSectionTitle', { + defaultMessage: 'Unable to load the guide', + })} + + } + body={ + <> + + {i18n.translate('guidedOnboarding.dropdownPanel.errorSectionDescription', { + defaultMessage: `Wait a moment and try again. If the problem persists, contact your administrator.`, + })} + + + window.location.reload()} + iconType="refresh" + color="danger" + > + {i18n.translate('guidedOnboarding.dropdownPanel.errorSectionReloadButton', { + defaultMessage: 'Reload', + })} + + + } + /> + ); + } + + if (isGuideReadyToComplete) { + return ( + <> + + + + + +

+ {i18n.translate('guidedOnboarding.dropdownPanel.completeGuideFlyoutDescription', { + defaultMessage: `You've completed the Elastic {guideName} guide. Feel free to come back to the Guides for more onboarding help or a refresher.`, + values: { + guideName: guideConfig.guideName, + }, + })} +

+
+ + {docsLink()} + + + + + + completeGuide(guideConfig.completedGuideRedirectLocation)} + fill + // data-test-subj used for FS tracking and testing + data-test-subj={`onboarding--completeGuideButton--${guideConfig!.telemetryId}`} + > + {i18n.translate('guidedOnboarding.dropdownPanel.elasticButtonLabel', { + defaultMessage: 'Continue using Elastic', + })} + + + + + ); + } + + return ( + <> +
+ +

{guideConfig.description}

+
+ + {docsLink()} + + +
+ + ); +}; diff --git a/src/plugins/guided_onboarding/public/components/guide_panel_flyout/guide_panel_flyout_footer.tsx b/src/plugins/guided_onboarding/public/components/guide_panel_flyout/guide_panel_flyout_footer.tsx new file mode 100644 index 0000000000000..badae4517e989 --- /dev/null +++ b/src/plugins/guided_onboarding/public/components/guide_panel_flyout/guide_panel_flyout_footer.tsx @@ -0,0 +1,87 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText, EuiThemeComputed } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { getGuidePanelStyles } from '../guide_panel.styles'; + +export const GuidePanelFlyoutFooter = ({ + styles, + euiTheme, + openQuitGuideModal, +}: { + styles: ReturnType; + euiTheme: EuiThemeComputed; + openQuitGuideModal: () => void; +}) => { + return ( +
+ + + + {i18n.translate('guidedOnboarding.dropdownPanel.footer.support', { + defaultMessage: 'Need help?', + })} + + + + + | + + + + + {i18n.translate('guidedOnboarding.dropdownPanel.footer.feedback', { + defaultMessage: 'Give feedback', + })} + + + + + | + + + + + {i18n.translate('guidedOnboarding.dropdownPanel.footer.exitGuideButtonLabel', { + defaultMessage: 'Quit guide', + })} + + + +
+ ); +}; diff --git a/src/plugins/guided_onboarding/public/components/guide_panel_flyout/guide_panel_flyout_header.tsx b/src/plugins/guided_onboarding/public/components/guide_panel_flyout/guide_panel_flyout_header.tsx new file mode 100644 index 0000000000000..b57b305e3331e --- /dev/null +++ b/src/plugins/guided_onboarding/public/components/guide_panel_flyout/guide_panel_flyout_header.tsx @@ -0,0 +1,94 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { ReactElement } from 'react'; +import { EuiButtonIcon, EuiHorizontalRule, EuiSpacer, EuiTitle, keys } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { GuideConfig } from '@kbn/guided-onboarding'; +import { getGuidePanelStyles } from '../guide_panel.styles'; + +export const GuidePanelFlyoutHeader = ({ + styles, + titleId, + toggleGuide, + hasError, + isGuideReadyToComplete, + guideConfig, + backButton, +}: { + styles: ReturnType; + titleId: string; + toggleGuide: () => void; + hasError: boolean; + isGuideReadyToComplete: boolean; + guideConfig?: GuideConfig; + backButton: ReactElement; +}) => { + /** + * ESC key closes CustomFlyout + */ + const onKeyDown = (event: any) => { + if (event.key === keys.ESCAPE) { + event.preventDefault(); + event.stopPropagation(); + toggleGuide(); + } + }; + + const getTitle = () => { + if (isGuideReadyToComplete) { + return i18n.translate('guidedOnboarding.dropdownPanel.completeGuideFlyoutTitle', { + defaultMessage: 'Well done!', + }); + } + + return guideConfig ? guideConfig.title : ''; + }; + + const closeIcon = ( + + ); + + if (hasError) { + return ( +
+ {backButton} + {closeIcon} +
+ ); + } + + return ( +
+ + + {backButton} + + + + +

+ {getTitle()} +

+
+ + {closeIcon} + + +
+ ); +}; diff --git a/src/plugins/guided_onboarding/public/components/guide_panel_flyout/guide_progress.tsx b/src/plugins/guided_onboarding/public/components/guide_panel_flyout/guide_progress.tsx new file mode 100644 index 0000000000000..c139a6a66dbed --- /dev/null +++ b/src/plugins/guided_onboarding/public/components/guide_panel_flyout/guide_progress.tsx @@ -0,0 +1,94 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiHorizontalRule, EuiProgress, EuiSpacer, htmlIdGenerator } from '@elastic/eui'; +import type { GuideConfig, GuideStep as GuideStepType, StepConfig } from '@kbn/guided-onboarding'; +import { i18n } from '@kbn/i18n'; +import { GuideStep } from '../guide_panel_step'; +import type { PluginState } from '../../../common'; +import { getGuidePanelStyles } from '../guide_panel.styles'; + +export const GuideProgress = ({ + guideConfig, + styles, + pluginState, + isLoading, + stepsCompleted, + isGuideReadyToComplete, + handleStepButtonClick, +}: { + guideConfig: GuideConfig; + styles: ReturnType; + pluginState: PluginState; + isLoading: boolean; + stepsCompleted: number; + isGuideReadyToComplete: boolean; + handleStepButtonClick: (stepState: GuideStepType, step: StepConfig) => void; +}) => { + const { flyoutStepsWrapper } = styles.flyoutOverrides; + + return ( + <> + {/* Progress bar should only show after the first step has been complete */} + {stepsCompleted > 0 && ( + <> + + + + + + )} + + + +
    + {guideConfig?.steps.map((step, index) => { + const accordionId = htmlIdGenerator(`accordion${index}`)(); + const stepState = pluginState?.activeGuide?.steps[index]; + + if (stepState) { + return ( +
  1. + handleStepButtonClick(stepState, step)} + telemetryGuideId={guideConfig!.telemetryId} + /> +
  2. + ); + } + })} +
+ + ); +}; diff --git a/src/plugins/guided_onboarding/public/components/guide_panel_flyout/index.tsx b/src/plugins/guided_onboarding/public/components/guide_panel_flyout/index.tsx new file mode 100644 index 0000000000000..05ff95a035a47 --- /dev/null +++ b/src/plugins/guided_onboarding/public/components/guide_panel_flyout/index.tsx @@ -0,0 +1,140 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiButtonEmpty, + EuiPanel, + EuiPortal, + EuiOverlayMask, + EuiFocusTrap, + EuiThemeComputed, +} from '@elastic/eui'; +import { GuideConfig, GuideStep as GuideStepType, StepConfig } from '@kbn/guided-onboarding'; +import { i18n } from '@kbn/i18n'; +import { GuidePanelFlyoutHeader } from './guide_panel_flyout_header'; +import { GuidePanelFlyoutBody } from './guide_panel_flyout_body'; +import type { PluginState } from '../../../common'; +import { GuidePanelFlyoutFooter } from './guide_panel_flyout_footer'; +import { getGuidePanelStyles } from '../guide_panel.styles'; + +export const GuidePanelFlyout = ({ + isOpen, + isDarkTheme, + toggleGuide, + isGuideReadyToComplete, + guideConfig, + styles, + navigateToLandingPage, + stepsCompleted, + pluginState, + handleStepButtonClick, + isLoading, + euiTheme, + openQuitGuideModal, + completeGuide, +}: { + isOpen: boolean; + isDarkTheme: boolean; + toggleGuide: () => void; + isGuideReadyToComplete: boolean; + guideConfig?: GuideConfig; + styles: ReturnType; + navigateToLandingPage: () => void; + stepsCompleted: number; + pluginState?: PluginState; + handleStepButtonClick: ( + stepState: GuideStepType, + step: StepConfig + ) => Promise<{ pluginState: PluginState } | undefined>; + isLoading: boolean; + euiTheme: EuiThemeComputed; + openQuitGuideModal: () => void; + completeGuide: ( + completedGuideRedirectLocation: GuideConfig['completedGuideRedirectLocation'] + ) => Promise; +}) => { + if (!isOpen) { + return null; + } + + const guidePanelFlyoutTitleId = 'onboarding-guide'; + const backToGuidesButton = ( + + {i18n.translate('guidedOnboarding.dropdownPanel.backToGuidesLink', { + defaultMessage: 'Back to guides', + })} + + ); + + const hasError = !guideConfig || !pluginState || (pluginState && pluginState.status === 'error'); + const { + flyoutContentWrapper, + flyoutBody, + flyoutBodyWrapper, + flyoutContainerError, + flyoutContainer, + } = styles.flyoutOverrides; + + return ( + + + + +
+ + +
+
+ +
+
+ + {!hasError && ( + + )} +
+
+
+
+
+ ); +};