mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Implement a11y improvements (#152126)
Fixes: #143878 This PR updates the guided onboarding panel to utilise a custom flyout created based on the [example accessible custom flyout](https://eui.elastic.co/pr_6247/#/utilities/portal#a-custom-flyout)
This commit is contained in:
parent
ab7c879c23
commit
b686e0fa99
7 changed files with 717 additions and 344 deletions
|
@ -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;
|
||||
}
|
||||
})
|
||||
`;
|
||||
|
||||
return {
|
||||
setupButton: css`
|
||||
margin-right: ${euiTheme.size.m};
|
||||
`,
|
||||
flyoutBody: css`
|
||||
overflow: auto;
|
||||
.euiFlyoutBody__overflowContent {
|
||||
width: 480px;
|
||||
padding-top: 10px;
|
||||
@media (max-width: ${euiTheme.breakpoint.s}px) {
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
}
|
||||
`,
|
||||
flyoutFooter: css`
|
||||
border-radius: 0 0 6px 6px;
|
||||
background: url(${isDarkTheme ? panelBgBottomDark : panelBgBottom}) 0 7px no-repeat;
|
||||
padding: 24px 30px;
|
||||
height: 125px;
|
||||
`,
|
||||
flyoutFooterLink: css`
|
||||
color: ${euiTheme.colors.darkShade};
|
||||
font-weight: 400;
|
||||
`,
|
||||
},
|
||||
});
|
||||
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;
|
||||
`,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 = (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="guideErrorSection"
|
||||
iconType="alert"
|
||||
color="danger"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('guidedOnboarding.dropdownPanel.errorSectionTitle', {
|
||||
defaultMessage: 'Unable to load the guide',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<EuiText color="subdued">
|
||||
{i18n.translate('guidedOnboarding.dropdownPanel.errorSectionDescription', {
|
||||
defaultMessage: `Wait a moment and try again. If the problem persists, contact your administrator.`,
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
<EuiButton
|
||||
iconSide="right"
|
||||
onClick={() => window.location.reload()}
|
||||
iconType="refresh"
|
||||
color="danger"
|
||||
>
|
||||
{i18n.translate('guidedOnboarding.dropdownPanel.errorSectionReloadButton', {
|
||||
defaultMessage: 'Reload',
|
||||
})}
|
||||
</EuiButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
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<PluginState | undefined>(undefined);
|
||||
|
@ -109,7 +54,7 @@ export const GuidePanel = ({ api, application, notifications, uiSettings }: Guid
|
|||
const [isLoading, setIsLoading] = useState<boolean>(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 = (
|
||||
<EuiButtonEmpty
|
||||
onClick={navigateToLandingPage}
|
||||
iconSide="left"
|
||||
iconType="arrowLeft"
|
||||
flush="left"
|
||||
color="text"
|
||||
>
|
||||
{i18n.translate('guidedOnboarding.dropdownPanel.backToGuidesLink', {
|
||||
defaultMessage: 'Back to guides',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div css={styles.setupButton}>
|
||||
|
@ -254,228 +183,22 @@ export const GuidePanel = ({ api, application, notifications, uiSettings }: Guid
|
|||
/>
|
||||
</div>
|
||||
|
||||
{isGuideOpen && (
|
||||
<EuiFlyout
|
||||
ownFocus
|
||||
onClose={toggleGuide}
|
||||
aria-labelledby="onboarding-guide"
|
||||
css={styles.flyoutOverrides.flyoutContainer}
|
||||
maskProps={{ headerZindexLocation: 'above' }}
|
||||
data-test-subj="guidePanel"
|
||||
maxWidth={480}
|
||||
>
|
||||
{guideConfig && pluginState && pluginState.status !== 'error' ? (
|
||||
<>
|
||||
<EuiFlyoutHeader css={styles.flyoutOverrides.flyoutHeader}>
|
||||
{backToGuidesButton}
|
||||
<EuiTitle size="m">
|
||||
<h2 data-test-subj="guideTitle">
|
||||
{isGuideReadyToComplete
|
||||
? i18n.translate('guidedOnboarding.dropdownPanel.completeGuideFlyoutTitle', {
|
||||
defaultMessage: 'Well done!',
|
||||
})
|
||||
: guideConfig.title}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
<EuiHorizontalRule margin="s" />
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody css={styles.flyoutOverrides.flyoutBody}>
|
||||
<div>
|
||||
{isGuideReadyToComplete && (
|
||||
<>
|
||||
<EuiImage
|
||||
size="fullWidth"
|
||||
src={getImageUrl()}
|
||||
alt={i18n.translate('guidedOnboarding.dropdownPanel.wellDoneAnimatedGif', {
|
||||
defaultMessage: `Guide completed animated gif`,
|
||||
})}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
|
||||
<EuiText size="m">
|
||||
<p data-test-subj="guideDescription">
|
||||
{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}
|
||||
</p>
|
||||
</EuiText>
|
||||
|
||||
{guideConfig.docs && (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiText size="m">
|
||||
<EuiLink external target="_blank" href={guideConfig.docs.url}>
|
||||
{guideConfig.docs.text}
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Progress bar should only show after the first step has been complete */}
|
||||
{stepsCompleted > 0 && (
|
||||
<>
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiProgress
|
||||
data-test-subj="guideProgress"
|
||||
label={
|
||||
isGuideReadyToComplete
|
||||
? i18n.translate('guidedOnboarding.dropdownPanel.completedLabel', {
|
||||
defaultMessage: 'Completed',
|
||||
})
|
||||
: i18n.translate('guidedOnboarding.dropdownPanel.progressLabel', {
|
||||
defaultMessage: 'Progress',
|
||||
})
|
||||
}
|
||||
value={stepsCompleted}
|
||||
valueText={i18n.translate(
|
||||
'guidedOnboarding.dropdownPanel.progressValueLabel',
|
||||
{
|
||||
defaultMessage: '{stepCount} steps',
|
||||
values: {
|
||||
stepCount: `${stepsCompleted} / ${guideConfig.steps.length}`,
|
||||
},
|
||||
}
|
||||
)}
|
||||
max={guideConfig.steps.length}
|
||||
size="l"
|
||||
/>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<EuiHorizontalRule />
|
||||
|
||||
{guideConfig?.steps.map((step, index) => {
|
||||
const accordionId = htmlIdGenerator(`accordion${index}`)();
|
||||
const stepState = pluginState?.activeGuide?.steps[index];
|
||||
|
||||
if (stepState) {
|
||||
return (
|
||||
<GuideStep
|
||||
isLoading={isLoading}
|
||||
accordionId={accordionId}
|
||||
stepStatus={stepState.status}
|
||||
stepConfig={step}
|
||||
stepNumber={index + 1}
|
||||
handleButtonClick={() => handleStepButtonClick(stepState, step)}
|
||||
key={accordionId}
|
||||
telemetryGuideId={guideConfig!.telemetryId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
{isGuideReadyToComplete && (
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
isLoading={isLoading}
|
||||
onClick={() => 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',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</div>
|
||||
</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter css={styles.flyoutOverrides.flyoutFooter}>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
wrap
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="questionInCircle"
|
||||
iconSide="right"
|
||||
href="https://cloud.elastic.co/support "
|
||||
target="_blank"
|
||||
css={styles.flyoutOverrides.flyoutFooterLink}
|
||||
iconSize="m"
|
||||
>
|
||||
{i18n.translate('guidedOnboarding.dropdownPanel.footer.support', {
|
||||
defaultMessage: 'Need help?',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color={euiTheme.colors.disabled}>
|
||||
|
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="faceHappy"
|
||||
iconSide="right"
|
||||
href="https://www.elastic.co/kibana/feedback"
|
||||
target="_blank"
|
||||
css={styles.flyoutOverrides.flyoutFooterLink}
|
||||
iconSize="s"
|
||||
>
|
||||
{i18n.translate('guidedOnboarding.dropdownPanel.footer.feedback', {
|
||||
defaultMessage: 'Give feedback',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color={euiTheme.colors.disabled}>
|
||||
|
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="exit"
|
||||
iconSide="right"
|
||||
onClick={openQuitGuideModal}
|
||||
data-test-subj="quitGuideButton"
|
||||
css={styles.flyoutOverrides.flyoutFooterLink}
|
||||
iconSize="s"
|
||||
>
|
||||
{i18n.translate(
|
||||
'guidedOnboarding.dropdownPanel.footer.exitGuideButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Quit guide',
|
||||
}
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</>
|
||||
) : (
|
||||
<EuiFlyoutBody>
|
||||
{backToGuidesButton}
|
||||
{errorSection}
|
||||
</EuiFlyoutBody>
|
||||
)}
|
||||
</EuiFlyout>
|
||||
)}
|
||||
<GuidePanelFlyout
|
||||
isOpen={isGuideOpen}
|
||||
isLoading={isLoading}
|
||||
styles={styles}
|
||||
toggleGuide={toggleGuide}
|
||||
isDarkTheme={isDarkTheme}
|
||||
stepsCompleted={stepsCompleted}
|
||||
isGuideReadyToComplete={isGuideReadyToComplete}
|
||||
guideConfig={guideConfig}
|
||||
navigateToLandingPage={navigateToLandingPage}
|
||||
pluginState={pluginState}
|
||||
handleStepButtonClick={handleStepButtonClick}
|
||||
openQuitGuideModal={openQuitGuideModal}
|
||||
euiTheme={euiTheme}
|
||||
completeGuide={completeGuide}
|
||||
/>
|
||||
|
||||
{isQuitGuideModalOpen && (
|
||||
<QuitGuideModal
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiImage,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { GuideConfig, GuideStep as GuideStepType, StepConfig } from '@kbn/guided-onboarding';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import wellDoneAnimatedDarkGif from '../../../assets/well_done_animated_dark.gif';
|
||||
import { PluginState } from '../../../common';
|
||||
import { GuideProgress } from './guide_progress';
|
||||
import wellDoneAnimatedGif from '../../../assets/well_done_animated.gif';
|
||||
import { getGuidePanelStyles } from '../guide_panel.styles';
|
||||
|
||||
export const GuidePanelFlyoutBody = ({
|
||||
styles,
|
||||
guideConfig,
|
||||
isDarkTheme,
|
||||
stepsCompleted,
|
||||
isGuideReadyToComplete,
|
||||
pluginState,
|
||||
handleStepButtonClick,
|
||||
isLoading,
|
||||
completeGuide,
|
||||
}: {
|
||||
styles: ReturnType<typeof getGuidePanelStyles>;
|
||||
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<void>;
|
||||
}) => {
|
||||
const docsLink = () => {
|
||||
if (!guideConfig || !guideConfig.docs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiText size="m">
|
||||
<EuiLink external target="_blank" href={guideConfig.docs.url}>
|
||||
{guideConfig.docs.text}
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
if (!guideConfig || !pluginState || (pluginState && pluginState.status === 'error')) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="guideErrorSection"
|
||||
iconType="alert"
|
||||
color="danger"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('guidedOnboarding.dropdownPanel.errorSectionTitle', {
|
||||
defaultMessage: 'Unable to load the guide',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<EuiText color="subdued">
|
||||
{i18n.translate('guidedOnboarding.dropdownPanel.errorSectionDescription', {
|
||||
defaultMessage: `Wait a moment and try again. If the problem persists, contact your administrator.`,
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
<EuiButton
|
||||
iconSide="right"
|
||||
onClick={() => window.location.reload()}
|
||||
iconType="refresh"
|
||||
color="danger"
|
||||
>
|
||||
{i18n.translate('guidedOnboarding.dropdownPanel.errorSectionReloadButton', {
|
||||
defaultMessage: 'Reload',
|
||||
})}
|
||||
</EuiButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isGuideReadyToComplete) {
|
||||
return (
|
||||
<>
|
||||
<EuiImage
|
||||
size="fullWidth"
|
||||
src={isDarkTheme ? wellDoneAnimatedDarkGif : wellDoneAnimatedGif}
|
||||
alt={i18n.translate('guidedOnboarding.dropdownPanel.wellDoneAnimatedGif', {
|
||||
defaultMessage: `Guide completed animated gif`,
|
||||
})}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiText size="m">
|
||||
<p data-test-subj="guideDescription">
|
||||
{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,
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
</EuiText>
|
||||
|
||||
{docsLink()}
|
||||
|
||||
<GuideProgress
|
||||
guideConfig={guideConfig}
|
||||
styles={styles}
|
||||
pluginState={pluginState}
|
||||
isLoading={isLoading}
|
||||
handleStepButtonClick={handleStepButtonClick}
|
||||
isGuideReadyToComplete={isGuideReadyToComplete}
|
||||
stepsCompleted={stepsCompleted}
|
||||
/>
|
||||
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
isLoading={isLoading}
|
||||
onClick={() => 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',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<EuiText size="m">
|
||||
<p data-test-subj="guideDescription">{guideConfig.description}</p>
|
||||
</EuiText>
|
||||
|
||||
{docsLink()}
|
||||
|
||||
<GuideProgress
|
||||
guideConfig={guideConfig}
|
||||
styles={styles}
|
||||
pluginState={pluginState}
|
||||
isLoading={isLoading}
|
||||
handleStepButtonClick={handleStepButtonClick}
|
||||
isGuideReadyToComplete={isGuideReadyToComplete}
|
||||
stepsCompleted={stepsCompleted}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<typeof getGuidePanelStyles>;
|
||||
euiTheme: EuiThemeComputed;
|
||||
openQuitGuideModal: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div css={styles.flyoutOverrides.flyoutFooter}>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
wrap
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="questionInCircle"
|
||||
iconSide="right"
|
||||
href="https://cloud.elastic.co/support "
|
||||
target="_blank"
|
||||
css={styles.flyoutOverrides.flyoutFooterLink}
|
||||
iconSize="m"
|
||||
>
|
||||
{i18n.translate('guidedOnboarding.dropdownPanel.footer.support', {
|
||||
defaultMessage: 'Need help?',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color={euiTheme.colors.disabled}>
|
||||
|
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="faceHappy"
|
||||
iconSide="right"
|
||||
href="https://www.elastic.co/kibana/feedback"
|
||||
target="_blank"
|
||||
css={styles.flyoutOverrides.flyoutFooterLink}
|
||||
iconSize="s"
|
||||
>
|
||||
{i18n.translate('guidedOnboarding.dropdownPanel.footer.feedback', {
|
||||
defaultMessage: 'Give feedback',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color={euiTheme.colors.disabled}>
|
||||
|
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="exit"
|
||||
iconSide="right"
|
||||
onClick={openQuitGuideModal}
|
||||
data-test-subj="quitGuideButton"
|
||||
css={styles.flyoutOverrides.flyoutFooterLink}
|
||||
iconSize="s"
|
||||
>
|
||||
{i18n.translate('guidedOnboarding.dropdownPanel.footer.exitGuideButtonLabel', {
|
||||
defaultMessage: 'Quit guide',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -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<typeof getGuidePanelStyles>;
|
||||
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 = (
|
||||
<EuiButtonIcon
|
||||
iconType="cross"
|
||||
aria-label={i18n.translate('guidedOnboarding.dropdownPanel.closeButton.ariaLabel', {
|
||||
defaultMessage: 'Close modal',
|
||||
})}
|
||||
onClick={toggleGuide}
|
||||
onKeyDown={onKeyDown}
|
||||
color="text"
|
||||
css={styles.flyoutOverrides.flyoutCloseButtonIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
if (hasError) {
|
||||
return (
|
||||
<div css={styles.flyoutOverrides.flyoutHeaderError}>
|
||||
{backButton}
|
||||
{closeIcon}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div css={styles.flyoutOverrides.flyoutHeader}>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{backButton}
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiTitle size="m">
|
||||
<h2 id={titleId} data-test-subj="guideTitle">
|
||||
{getTitle()}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
||||
{closeIcon}
|
||||
|
||||
<EuiHorizontalRule />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -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<typeof getGuidePanelStyles>;
|
||||
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 && (
|
||||
<>
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiProgress
|
||||
data-test-subj="guideProgress"
|
||||
label={
|
||||
isGuideReadyToComplete
|
||||
? i18n.translate('guidedOnboarding.dropdownPanel.completedLabel', {
|
||||
defaultMessage: 'Completed',
|
||||
})
|
||||
: i18n.translate('guidedOnboarding.dropdownPanel.progressLabel', {
|
||||
defaultMessage: 'Progress',
|
||||
})
|
||||
}
|
||||
value={stepsCompleted}
|
||||
valueText={i18n.translate('guidedOnboarding.dropdownPanel.progressValueLabel', {
|
||||
defaultMessage: '{stepCount} steps',
|
||||
values: {
|
||||
stepCount: `${stepsCompleted} / ${guideConfig.steps.length}`,
|
||||
},
|
||||
})}
|
||||
max={guideConfig.steps.length}
|
||||
size="l"
|
||||
/>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<EuiHorizontalRule />
|
||||
|
||||
<ol css={flyoutStepsWrapper}>
|
||||
{guideConfig?.steps.map((step, index) => {
|
||||
const accordionId = htmlIdGenerator(`accordion${index}`)();
|
||||
const stepState = pluginState?.activeGuide?.steps[index];
|
||||
|
||||
if (stepState) {
|
||||
return (
|
||||
<li key={accordionId}>
|
||||
<GuideStep
|
||||
isLoading={isLoading}
|
||||
accordionId={accordionId}
|
||||
stepStatus={stepState.status}
|
||||
stepConfig={step}
|
||||
stepNumber={index + 1}
|
||||
handleButtonClick={() => handleStepButtonClick(stepState, step)}
|
||||
telemetryGuideId={guideConfig!.telemetryId}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</ol>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<typeof getGuidePanelStyles>;
|
||||
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<void>;
|
||||
}) => {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const guidePanelFlyoutTitleId = 'onboarding-guide';
|
||||
const backToGuidesButton = (
|
||||
<EuiButtonEmpty
|
||||
onClick={navigateToLandingPage}
|
||||
iconSide="left"
|
||||
iconType="arrowLeft"
|
||||
flush="left"
|
||||
color="text"
|
||||
>
|
||||
{i18n.translate('guidedOnboarding.dropdownPanel.backToGuidesLink', {
|
||||
defaultMessage: 'Back to guides',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
const hasError = !guideConfig || !pluginState || (pluginState && pluginState.status === 'error');
|
||||
const {
|
||||
flyoutContentWrapper,
|
||||
flyoutBody,
|
||||
flyoutBodyWrapper,
|
||||
flyoutContainerError,
|
||||
flyoutContainer,
|
||||
} = styles.flyoutOverrides;
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiOverlayMask>
|
||||
<EuiFocusTrap onClickOutside={toggleGuide}>
|
||||
<EuiPanel
|
||||
data-test-subj="guidePanel"
|
||||
aria-labelledby={guidePanelFlyoutTitleId}
|
||||
role="dialog"
|
||||
css={hasError ? flyoutContainerError : flyoutContainer}
|
||||
>
|
||||
<div css={flyoutContentWrapper}>
|
||||
<GuidePanelFlyoutHeader
|
||||
styles={styles}
|
||||
titleId={guidePanelFlyoutTitleId}
|
||||
toggleGuide={toggleGuide}
|
||||
guideConfig={guideConfig}
|
||||
isGuideReadyToComplete={isGuideReadyToComplete}
|
||||
backButton={backToGuidesButton}
|
||||
hasError={hasError}
|
||||
/>
|
||||
|
||||
<div css={flyoutBodyWrapper}>
|
||||
<div css={flyoutBody}>
|
||||
<GuidePanelFlyoutBody
|
||||
styles={styles}
|
||||
guideConfig={guideConfig}
|
||||
pluginState={pluginState}
|
||||
handleStepButtonClick={handleStepButtonClick}
|
||||
isLoading={isLoading}
|
||||
isDarkTheme={isDarkTheme}
|
||||
stepsCompleted={stepsCompleted}
|
||||
isGuideReadyToComplete={isGuideReadyToComplete}
|
||||
completeGuide={completeGuide}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!hasError && (
|
||||
<GuidePanelFlyoutFooter
|
||||
styles={styles}
|
||||
euiTheme={euiTheme}
|
||||
openQuitGuideModal={openQuitGuideModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</EuiPanel>
|
||||
</EuiFocusTrap>
|
||||
</EuiOverlayMask>
|
||||
</EuiPortal>
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue