[Guided onboarding] Landing page updates (#149528)

## Summary
Fixes https://github.com/elastic/kibana/issues/149179

This PR updates the landing page for guided onboarding according with
the latest feedback:
- more cards on the page
- most cards redirect to different Kibana pages
- some cards activate a solution guide
- a filter to highlight cards for a specific solution
- guide progress labels and completion indicators

#### Out of scope for this PR:
- telemetry events (will be addressed in
https://github.com/elastic/kibana/issues/149273)
- turn on a solution filter based on the url params (planned for 8.8+)

#### Screenshots/recording
Light theme
<img width="1552" alt="Screenshot 2023-01-26 at 12 28 25"
src="https://user-images.githubusercontent.com/6585477/214825116-dc2f12c6-1436-4d6f-b16f-67a75c3466a5.png">


Dark theme
<img width="1535" alt="Screenshot 2023-01-26 at 12 28 51"
src="https://user-images.githubusercontent.com/6585477/214825145-ea9552e0-10ac-4e3c-bb48-d2154132d69c.png">


Progress labels and completion indicators
<img width="1402" alt="Screenshot 2023-01-26 at 12 26 54"
src="https://user-images.githubusercontent.com/6585477/214825184-63753066-20f0-4589-9970-d1a49e481f85.png">


Filter highlighting



https://user-images.githubusercontent.com/6585477/214825210-7396c1d9-3ff9-4a2f-9329-7419c5cd5802.mov





### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>
This commit is contained in:
Yulia Čech 2023-01-31 14:27:20 +01:00 committed by GitHub
parent a63404c48e
commit 486a866b42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 691 additions and 963 deletions

View file

@ -17,6 +17,6 @@ export type {
StepConfig,
StepDescriptionWithLink,
} from './src/types';
export { GuideCard, InfrastructureLinkCard } from './src/components/landing_page';
export type { GuideCardUseCase } from './src/components/landing_page';
export { GuideCards, GuideFilters } from './src/components/landing_page';
export type { GuideFilterValues } from './src/components/landing_page';
export { testGuideId, testGuideConfig } from './src/common/test_guide_config';

View file

@ -1,55 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`guide card snapshots should render use case card component for kubernetes 1`] = `
<UseCaseCard
addBasePath={[MockFunction]}
description="Monitor your Kubernetes infrastructure by consolidating your logs and metrics."
footer={
<GuideCardFooter
activateGuide={[MockFunction]}
guides={Array []}
telemetryId="kubernetes"
useCase="kubernetes"
/>
}
isDarkTheme={false}
title="Observe my Kubernetes infrastructure"
useCase="kubernetes"
/>
`;
exports[`guide card snapshots should render use case card component for search 1`] = `
<UseCaseCard
addBasePath={[MockFunction]}
description="Create a search experience for your websites, applications, workplace content, or anything in between."
footer={
<GuideCardFooter
activateGuide={[MockFunction]}
guides={Array []}
telemetryId="search"
useCase="search"
/>
}
isDarkTheme={false}
title="Search my data"
useCase="search"
/>
`;
exports[`guide card snapshots should render use case card component for siem 1`] = `
<UseCaseCard
addBasePath={[MockFunction]}
description="Investigate threats and get your SIEM up and running by installing the Elastic Defend integration."
footer={
<GuideCardFooter
activateGuide={[MockFunction]}
guides={Array []}
telemetryId="siem"
useCase="siem"
/>
}
isDarkTheme={false}
title="Protect my environment"
useCase="siem"
/>
`;

View file

@ -1,181 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`guide card footer snapshots should render the footer when the guide has been completed 1`] = `
<Fragment>
<EuiProgress
label="Completed"
labelProps={
Object {
"css": Object {
"map": undefined,
"name": "x46p4t",
"next": undefined,
"styles": "
text-align: 'left';
",
"toString": [Function],
},
}
}
max={2}
size="s"
value={1}
valueText="1/2 steps"
/>
<EuiSpacer
size="l"
/>
<EuiFlexGroup
justifyContent="center"
>
<EuiFlexItem
grow={false}
>
<EuiButton
color="primary"
data-test-subj="onboarding--guideCard--view--search"
fill={true}
isLoading={false}
onClick={[Function]}
size="m"
>
View guide
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
`;
exports[`guide card footer snapshots should render the footer when the guide has not started yet 1`] = `
<EuiFlexGroup
justifyContent="center"
>
<EuiFlexItem
grow={false}
>
<EuiButton
color="primary"
data-test-subj="onboarding--guideCard--view--search"
fill={true}
isLoading={false}
onClick={[Function]}
size="m"
>
View guide
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
`;
exports[`guide card footer snapshots should render the footer when the guide is in progress 1`] = `
<Fragment>
<EuiProgress
label="In progress"
labelProps={
Object {
"css": Object {
"map": undefined,
"name": "x46p4t",
"next": undefined,
"styles": "
text-align: 'left';
",
"toString": [Function],
},
}
}
max={2}
size="s"
value={1}
valueText="1/2 steps"
/>
<EuiSpacer
size="l"
/>
<EuiFlexGroup
justifyContent="center"
>
<EuiFlexItem
grow={false}
>
<EuiButton
color="primary"
data-test-subj="onboarding--guideCard--continue--search"
fill={true}
isLoading={false}
onClick={[Function]}
size="m"
>
Continue
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
`;
exports[`guide card footer snapshots should render the footer when the guide is ready to complete 1`] = `
<Fragment>
<EuiProgress
label="In progress"
labelProps={
Object {
"css": Object {
"map": undefined,
"name": "x46p4t",
"next": undefined,
"styles": "
text-align: 'left';
",
"toString": [Function],
},
}
}
max={2}
size="s"
value={1}
valueText="1/2 steps"
/>
<EuiSpacer
size="l"
/>
<EuiFlexGroup
justifyContent="center"
>
<EuiFlexItem
grow={false}
>
<EuiButton
color="primary"
data-test-subj="onboarding--guideCard--continue--search"
fill={true}
isLoading={false}
onClick={[Function]}
size="m"
>
Continue
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
`;
exports[`guide card footer snapshots should render the footer when the guided onboarding has not started yet 1`] = `
<EuiFlexGroup
justifyContent="center"
>
<EuiFlexItem
grow={false}
>
<EuiButton
color="primary"
data-test-subj="onboarding--guideCard--view--search"
fill={true}
isLoading={false}
onClick={[Function]}
size="m"
>
View guide
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
`;

View file

@ -0,0 +1,255 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`guide cards snapshots should render all cards 1`] = `
<EuiFlexGroup
justifyContent="center"
responsive={true}
wrap={true}
>
<EuiFlexItem
grow={false}
key="0"
>
<GuideCard
activateGuide={[MockFunction]}
activeFilter="all"
card={
Object {
"guideId": "search",
"order": 1,
"solution": "search",
"telemetryId": "guided-onboarding--search--application",
"title": "Build an application on top of Elasticsearch",
}
}
guidesState={Array []}
navigateToApp={[MockFunction]}
/>
<EuiSpacer
size="m"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
key="1"
>
<GuideCard
activateGuide={[MockFunction]}
activeFilter="all"
card={
Object {
"navigateTo": Object {
"appId": "integrations",
"path": "/browse?q=log",
},
"order": 2,
"solution": "observability",
"telemetryId": "guided-onboarding--observability--logs",
"title": "Collect and analyze my logs",
}
}
guidesState={Array []}
navigateToApp={[MockFunction]}
/>
<EuiSpacer
size="m"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
key="2"
>
<GuideCard
activateGuide={[MockFunction]}
activeFilter="all"
card={
Object {
"guideId": "siem",
"order": 3,
"solution": "security",
"telemetryId": "guided-onboarding--security--siem",
"title": "Detect threats in my data with SIEM",
}
}
guidesState={Array []}
navigateToApp={[MockFunction]}
/>
<EuiSpacer
size="m"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
key="3"
>
<GuideCard
activateGuide={[MockFunction]}
activeFilter="all"
card={
Object {
"guideId": "search",
"order": 4,
"solution": "search",
"telemetryId": "guided-onboarding--search--website",
"title": "Add search to my website",
}
}
guidesState={Array []}
navigateToApp={[MockFunction]}
/>
<EuiSpacer
size="m"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
key="4"
>
<GuideCard
activateGuide={[MockFunction]}
activeFilter="all"
card={
Object {
"navigateTo": Object {
"appId": "home",
"path": "#/tutorial/apm",
},
"order": 5,
"solution": "observability",
"telemetryId": "guided-onboarding--observability--apm",
"title": "Monitor my application performance (APM / tracing)",
}
}
guidesState={Array []}
navigateToApp={[MockFunction]}
/>
<EuiSpacer
size="m"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
key="5"
>
<GuideCard
activateGuide={[MockFunction]}
activeFilter="all"
card={
Object {
"navigateTo": Object {
"appId": "integrations",
"path": "/detail/endpoint/overview",
},
"order": 6,
"solution": "security",
"telemetryId": "guided-onboarding--security--hosts",
"title": "Secure my hosts with endpoint security",
}
}
guidesState={Array []}
navigateToApp={[MockFunction]}
/>
<EuiSpacer
size="m"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
key="6"
>
<GuideCard
activateGuide={[MockFunction]}
activeFilter="all"
card={
Object {
"guideId": "search",
"order": 7,
"solution": "search",
"telemetryId": "guided-onboarding--search--database",
"title": "Search across databases and business systems",
}
}
guidesState={Array []}
navigateToApp={[MockFunction]}
/>
<EuiSpacer
size="m"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
key="7"
>
<GuideCard
activateGuide={[MockFunction]}
activeFilter="all"
card={
Object {
"navigateTo": Object {
"appId": "integrations",
"path": "/browse/os_system",
},
"order": 8,
"solution": "observability",
"telemetryId": "guided-onboarding--observability--hosts",
"title": "Monitor my host metrics",
}
}
guidesState={Array []}
navigateToApp={[MockFunction]}
/>
<EuiSpacer
size="m"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
key="8"
>
<GuideCard
activateGuide={[MockFunction]}
activeFilter="all"
card={
Object {
"navigateTo": Object {
"appId": "integrations",
"path": "/detail/cloud_security_posture/overview",
},
"order": 9,
"solution": "security",
"telemetryId": "guided-onboarding--security--cloud",
"title": "Secure my cloud assets with posture management",
}
}
guidesState={Array []}
navigateToApp={[MockFunction]}
/>
<EuiSpacer
size="m"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
key="9"
>
<GuideCard
activateGuide={[MockFunction]}
activeFilter="all"
card={
Object {
"guideId": "kubernetes",
"order": 11,
"solution": "observability",
"telemetryId": "guided-onboarding--observability--kubernetes",
"title": "Monitor Kubernetes clusters",
}
}
guidesState={Array []}
navigateToApp={[MockFunction]}
/>
<EuiSpacer
size="m"
/>
</EuiFlexItem>
</EuiFlexGroup>
`;

View file

@ -1,30 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`observability link card snapshots should render link card for observability 1`] = `
<UseCaseCard
addBasePath={[MockFunction]}
description="Add application, infrastructure, and user data through our pre-built integrations."
footer={
<EuiFlexGroup
justifyContent="center"
>
<EuiFlexItem
grow={false}
>
<EuiButton
color="primary"
data-test-subj="onboarding--linkCard--observability"
fill={true}
onClick={[Function]}
size="m"
>
View integrations
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
}
isDarkTheme={false}
title="Observe my data"
useCase="infrastructure"
/>
`;

View file

@ -1,40 +0,0 @@
/*
* 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 { shallow } from 'enzyme';
import { GuideCard, GuideCardProps } from './guide_card';
const defaultProps: GuideCardProps = {
useCase: 'search',
guides: [],
activateGuide: jest.fn(),
isDarkTheme: false,
addBasePath: jest.fn(),
};
describe('guide card', () => {
describe('snapshots', () => {
test('should render use case card component for search', async () => {
const component = await shallow(<GuideCard {...defaultProps} useCase="search" />);
expect(component).toMatchSnapshot();
});
test('should render use case card component for kubernetes', async () => {
const component = await shallow(<GuideCard {...defaultProps} useCase="kubernetes" />);
expect(component).toMatchSnapshot();
});
test('should render use case card component for siem', async () => {
const component = await shallow(<GuideCard {...defaultProps} useCase="siem" />);
expect(component).toMatchSnapshot();
});
});
});

View file

@ -6,108 +6,115 @@
* Side Public License, v 1.
*/
import React from 'react';
import React, { useCallback, useState } from 'react';
import { css } from '@emotion/react';
import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiTextColor } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { GuideState } from '../../types';
import { GuideCardFooter } from './guide_card_footer';
import { UseCaseCard } from './use_case_card';
import { GuideCardConstants } from './guide_cards.constants';
import { GuideCardsProps } from './guide_cards';
// separate type for GuideCardUseCase that includes some of GuideIds
export type GuideCardUseCase = 'search' | 'kubernetes' | 'siem';
type GuideCardConstants = {
[key in GuideCardUseCase]: {
i18nTexts: {
title: string;
description: string;
};
// duplicate the telemetry id from the guide config to not load the config from the endpoint
// this might change if we decide to use the guide config for the cards
// see this issue https://github.com/elastic/kibana/issues/146672
telemetryId: string;
};
const cardCss = css`
position: relative;
min-height: 110px;
width: 380px;
.euiCard__content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
`;
const getProgressLabel = (guideState: GuideState | undefined): string | undefined => {
if (!guideState) {
return undefined;
}
const { steps } = guideState;
const numberSteps = steps.length;
const numberCompleteSteps = steps.filter((step) => step.status === 'complete').length;
if (numberCompleteSteps < 1 || numberCompleteSteps === numberSteps) {
return undefined;
}
return i18n.translate('guidedOnboardingPackage.gettingStarted.cards.progressLabel', {
defaultMessage: '{numberCompleteSteps} of {numberSteps} steps complete',
values: {
numberCompleteSteps,
numberSteps,
},
});
};
const constants: GuideCardConstants = {
search: {
i18nTexts: {
title: i18n.translate('guidedOnboardingPackage.gettingStarted.guideCard.search.cardTitle', {
defaultMessage: 'Search my data',
}),
description: i18n.translate(
'guidedOnboardingPackage.gettingStarted.guideCard.search.cardDescription',
{
defaultMessage:
'Create a search experience for your websites, applications, workplace content, or anything in between.',
}
),
},
telemetryId: 'search',
},
kubernetes: {
i18nTexts: {
title: i18n.translate(
'guidedOnboardingPackage.gettingStarted.guideCard.kubernetes.cardTitle',
{
defaultMessage: 'Observe my Kubernetes infrastructure',
}
),
description: i18n.translate(
'guidedOnboardingPackage.gettingStarted.guideCard.kubernetes.cardDescription',
{
defaultMessage:
'Monitor your Kubernetes infrastructure by consolidating your logs and metrics.',
}
),
},
telemetryId: 'kubernetes',
},
siem: {
i18nTexts: {
title: i18n.translate('guidedOnboardingPackage.gettingStarted.guideCard.siem.cardTitle', {
defaultMessage: 'Protect my environment',
}),
description: i18n.translate(
'guidedOnboardingPackage.gettingStarted.guideCard.siem.cardDescription',
{
defaultMessage:
'Investigate threats and get your SIEM up and running by installing the Elastic Defend integration.',
}
),
},
telemetryId: 'siem',
},
};
export interface GuideCardProps {
useCase: GuideCardUseCase;
guides: GuideState[];
activateGuide: (useCase: GuideCardUseCase, guide?: GuideState) => Promise<void>;
isDarkTheme: boolean;
addBasePath: (url: string) => string;
}
export const GuideCard = ({
useCase,
guides,
card,
guidesState,
activateGuide,
isDarkTheme,
addBasePath,
}: GuideCardProps) => {
navigateToApp,
activeFilter,
}: GuideCardsProps & { card: GuideCardConstants }) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
let guideState: GuideState | undefined;
if (card.guideId) {
guideState = guidesState.find((state) => state.guideId === card.guideId);
}
const onClick = useCallback(async () => {
setIsLoading(true);
if (card.guideId) {
await activateGuide(card.guideId, guideState);
} else if (card.navigateTo) {
await navigateToApp(card.navigateTo?.appId, {
path: card.navigateTo.path,
});
}
setIsLoading(false);
}, [activateGuide, card.guideId, card.navigateTo, guideState, navigateToApp]);
const isHighlighted = activeFilter === 'all' || activeFilter === card.solution;
const isComplete = guideState && guideState.status === 'complete';
const progress = getProgressLabel(guideState);
return (
<UseCaseCard
useCase={useCase}
title={constants[useCase].i18nTexts.title}
description={constants[useCase].i18nTexts.description}
footer={
<GuideCardFooter
guides={guides}
activateGuide={activateGuide}
useCase={useCase}
telemetryId={constants[useCase].telemetryId}
/>
<EuiCard
isDisabled={isLoading}
onClick={onClick}
css={cardCss}
display={isHighlighted ? undefined : 'transparent'}
hasBorder={!isHighlighted}
title={
<>
<EuiSpacer size="s" />
<h3 style={{ fontWeight: 600 }}>{card.title}</h3>
</>
}
titleSize="xs"
betaBadgeProps={{
label: card.solution,
}}
description={
<>
{progress && (
<EuiTextColor color="subdued">
<small>{progress}</small>
</EuiTextColor>
)}
{isComplete && (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="checkInCircleFilled" color="success" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<small>
{i18n.translate('guidedOnboardingPackage.gettingStarted.cards.completeLabel', {
defaultMessage: 'Guide complete',
})}
</small>
</EuiFlexItem>
</EuiFlexGroup>
)}
</>
}
isDarkTheme={isDarkTheme}
addBasePath={addBasePath}
/>
);
};

View file

@ -1,72 +0,0 @@
/*
* 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 { shallow } from 'enzyme';
import { GuideCardFooter, GuideCardFooterProps } from './guide_card_footer';
import { GuideState } from '../../types';
const defaultProps: GuideCardFooterProps = {
guides: [],
useCase: 'search',
telemetryId: 'search',
activateGuide: jest.fn(),
};
const searchGuideState: GuideState = {
guideId: 'search',
status: 'not_started',
steps: [
{ id: 'add_data', status: 'complete' },
{ id: 'search_experience', status: 'in_progress' },
],
isActive: true,
};
describe('guide card footer', () => {
describe('snapshots', () => {
test('should render the footer when the guided onboarding has not started yet', async () => {
const component = await shallow(<GuideCardFooter {...defaultProps} />);
expect(component).toMatchSnapshot();
});
test('should render the footer when the guide has not started yet', async () => {
const component = await shallow(
<GuideCardFooter {...defaultProps} guides={[searchGuideState]} />
);
expect(component).toMatchSnapshot();
});
test('should render the footer when the guide is in progress', async () => {
const component = await shallow(
<GuideCardFooter
{...defaultProps}
guides={[{ ...searchGuideState, status: 'in_progress' }]}
/>
);
expect(component).toMatchSnapshot();
});
test('should render the footer when the guide is ready to complete', async () => {
const component = await shallow(
<GuideCardFooter
{...defaultProps}
guides={[{ ...searchGuideState, status: 'ready_to_complete' }]}
/>
);
expect(component).toMatchSnapshot();
});
test('should render the footer when the guide has been completed', async () => {
const component = await shallow(
<GuideCardFooter {...defaultProps} guides={[{ ...searchGuideState, status: 'complete' }]} />
);
expect(component).toMatchSnapshot();
});
});
});

View file

@ -1,144 +0,0 @@
/*
* 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, { useState, useCallback } from 'react';
import { css } from '@emotion/react';
import { EuiButton, EuiProgress, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { GuideId, GuideState } from '../../types';
import type { GuideCardUseCase } from './guide_card';
const viewGuideLabel = i18n.translate(
'guidedOnboardingPackage.gettingStarted.guideCard.startGuide.buttonLabel',
{
defaultMessage: 'View guide',
}
);
const continueGuideLabel = i18n.translate(
'guidedOnboardingPackage.gettingStarted.guideCard.continueGuide.buttonLabel',
{
defaultMessage: 'Continue',
}
);
const completedLabel = i18n.translate(
'guidedOnboardingPackage.gettingStarted.guideCard.progress.completedLabel',
{
defaultMessage: 'Completed',
}
);
const inProgressLabel = i18n.translate(
'guidedOnboardingPackage.gettingStarted.guideCard.progress.inProgressLabel',
{
defaultMessage: 'In progress',
}
);
// The progress bar is rendered within EuiCard, which centers content by default
const progressBarLabelCss = css`
text-align: 'left';
`;
export interface GuideCardFooterProps {
guides: GuideState[];
useCase: GuideCardUseCase;
telemetryId: string;
activateGuide: (useCase: GuideCardUseCase, guideState?: GuideState) => Promise<void>;
}
export const GuideCardFooter = ({
guides,
useCase,
telemetryId,
activateGuide,
}: GuideCardFooterProps) => {
const guideState = guides.find((guide) => guide.guideId === (useCase as GuideId));
const [isLoading, setIsLoading] = useState<boolean>(false);
const activateGuideCallback = useCallback(async () => {
setIsLoading(true);
await activateGuide(useCase, guideState);
setIsLoading(false);
}, [activateGuide, guideState, useCase]);
const viewGuideButton = (
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiButton
isLoading={isLoading}
// Used for FS tracking
data-test-subj={`onboarding--guideCard--view--${telemetryId}`}
fill
onClick={activateGuideCallback}
>
{viewGuideLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
// guide has not started yet
if (!guideState || guideState.status === 'not_started') {
return viewGuideButton;
}
const { status, steps } = guideState;
const numberSteps = steps.length;
const numberCompleteSteps = steps.filter((step) => step.status === 'complete').length;
const stepsLabel = i18n.translate('guidedOnboardingPackage.gettingStarted.guideCard.stepsLabel', {
defaultMessage: '{progress} steps',
values: {
progress: `${numberCompleteSteps}/${numberSteps}`,
},
});
// guide is completed
if (status === 'complete') {
return (
<>
<EuiProgress
valueText={stepsLabel}
value={numberCompleteSteps}
max={numberSteps}
size="s"
label={completedLabel}
labelProps={{
css: progressBarLabelCss,
}}
/>
<EuiSpacer size="l" />
{viewGuideButton}
</>
);
}
// guide is in progress or ready to complete
return (
<>
<EuiProgress
valueText={stepsLabel}
value={numberCompleteSteps}
max={numberSteps}
size="s"
label={inProgressLabel}
labelProps={{
css: progressBarLabelCss,
}}
/>
<EuiSpacer size="l" />
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiButton
isLoading={isLoading}
// Used for FS tracking
data-test-subj={`onboarding--guideCard--continue--${telemetryId}`}
fill
onClick={activateGuideCallback}
>
{continueGuideLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};

View file

@ -0,0 +1,139 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { GuideId } from '../../..';
import { GuideCardSolutions } from './guide_cards';
export interface GuideCardConstants {
solution: GuideCardSolutions;
title: string;
// if present, guideId indicates which guide is opened when clicking the card
guideId?: GuideId;
// if present, navigateTo indicates where the user will be redirected, when clicking the card
navigateTo?: {
appId: string;
path?: string;
};
// duplicate the telemetry id from the guide config to not load the config from the endpoint
// this might change if we decide to use the guide config for the cards
// see this issue https://github.com/elastic/kibana/issues/146672
telemetryId: string;
order: number;
}
export const guideCards: GuideCardConstants[] = [
{
solution: 'search',
title: i18n.translate('guidedOnboardingPackage.gettingStarted.cards.appSearch.title', {
defaultMessage: 'Build an application on top of Elasticsearch',
}),
guideId: 'search',
telemetryId: 'guided-onboarding--search--application',
order: 1,
},
{
solution: 'search',
title: i18n.translate('guidedOnboardingPackage.gettingStarted.cards.websiteSearch.title', {
defaultMessage: 'Add search to my website',
}),
guideId: 'search',
telemetryId: 'guided-onboarding--search--website',
order: 4,
},
{
solution: 'search',
title: i18n.translate('guidedOnboardingPackage.gettingStarted.cards.databaseSearch.title', {
defaultMessage: 'Search across databases and business systems',
}),
guideId: 'search',
telemetryId: 'guided-onboarding--search--database',
order: 7,
},
{
solution: 'observability',
title: i18n.translate('guidedOnboardingPackage.gettingStarted.cards.logsObservability.title', {
defaultMessage: 'Collect and analyze my logs',
}),
navigateTo: {
appId: 'integrations',
path: '/browse?q=log',
},
telemetryId: 'guided-onboarding--observability--logs',
order: 2,
},
{
solution: 'observability',
title: i18n.translate('guidedOnboardingPackage.gettingStarted.cards.apmObservability.title', {
defaultMessage: 'Monitor my application performance (APM / tracing)',
}),
navigateTo: {
appId: 'home',
path: '#/tutorial/apm',
},
telemetryId: 'guided-onboarding--observability--apm',
order: 5,
},
{
solution: 'observability',
title: i18n.translate('guidedOnboardingPackage.gettingStarted.cards.hostsObservability.title', {
defaultMessage: 'Monitor my host metrics',
}),
navigateTo: {
appId: 'integrations',
path: '/browse/os_system',
},
telemetryId: 'guided-onboarding--observability--hosts',
order: 8,
},
{
solution: 'observability',
title: i18n.translate(
'guidedOnboardingPackage.gettingStarted.cards.kubernetesObservability.title',
{
defaultMessage: 'Monitor Kubernetes clusters',
}
),
guideId: 'kubernetes',
telemetryId: 'guided-onboarding--observability--kubernetes',
order: 11,
},
{
solution: 'security',
title: i18n.translate('guidedOnboardingPackage.gettingStarted.cards.siemSecurity.title', {
defaultMessage: 'Detect threats in my data with SIEM',
}),
guideId: 'siem',
telemetryId: 'guided-onboarding--security--siem',
order: 3,
},
{
solution: 'security',
title: i18n.translate('guidedOnboardingPackage.gettingStarted.cards.hostsSecurity.title', {
defaultMessage: 'Secure my hosts with endpoint security',
}),
navigateTo: {
appId: 'integrations',
path: '/detail/endpoint/overview',
},
telemetryId: 'guided-onboarding--security--hosts',
order: 6,
},
{
solution: 'security',
title: i18n.translate('guidedOnboardingPackage.gettingStarted.cards.cloudSecurity.title', {
defaultMessage: 'Secure my cloud assets with posture management',
}),
navigateTo: {
appId: 'integrations',
path: '/detail/cloud_security_posture/overview',
},
telemetryId: 'guided-onboarding--security--cloud',
order: 9,
},
].sort((cardA, cardB) => cardA.order - cardB.order) as GuideCardConstants[];

View file

@ -8,18 +8,20 @@
import React from 'react';
import { shallow } from 'enzyme';
import { InfrastructureLinkCard } from './infrastructure_link_card';
const defaultProps = {
import { GuideCards, GuideCardsProps } from './guide_cards';
const defaultProps: GuideCardsProps = {
activateGuide: jest.fn(),
navigateToApp: jest.fn(),
isDarkTheme: false,
addBasePath: jest.fn(),
activeFilter: 'all',
guidesState: [],
};
describe('observability link card', () => {
describe('guide cards', () => {
describe('snapshots', () => {
test('should render link card for observability', async () => {
const component = await shallow(<InfrastructureLinkCard {...defaultProps} />);
test('should render all cards', async () => {
const component = await shallow(<GuideCards {...defaultProps} />);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,39 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { ApplicationStart } from '@kbn/core-application-browser';
import { GuideId, GuideState } from '../../types';
import { GuideFilterValues } from './guide_filters';
import { guideCards } from './guide_cards.constants';
import { GuideCard } from './guide_card';
export type GuideCardSolutions = 'search' | 'observability' | 'security';
export interface GuideCardsProps {
activateGuide: (guideId: GuideId, guideState?: GuideState) => Promise<void>;
navigateToApp: ApplicationStart['navigateToApp'];
activeFilter: GuideFilterValues;
guidesState: GuideState[];
}
export const GuideCards = (props: GuideCardsProps) => {
return (
<EuiFlexGroup wrap responsive justifyContent="center">
{guideCards.map((card, index) => (
<EuiFlexItem key={index} grow={false}>
<GuideCard card={card} {...props} />
<EuiSpacer size="m" />
</EuiFlexItem>
))}
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,93 @@
/*
* 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, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { FormattedMessage } from '@kbn/i18n-react';
import { GuideCardSolutions } from './guide_cards';
const filterButtonCss = css`
border-radius: 20px !important;
min-width: 0 !important;
padding: 0 18px !important;
height: 32px !important;
&:hover {
text-decoration: none !important;
transform: none !important;
transition: none !important;
}
&:focus {
text-decoration: none;
}
`;
export type GuideFilterValues = GuideCardSolutions | 'all';
interface GuideFiltersProps {
activeFilter: GuideFilterValues;
setActiveFilter: React.Dispatch<React.SetStateAction<GuideFilterValues>>;
}
export const GuideFilters = ({ activeFilter, setActiveFilter }: GuideFiltersProps) => {
const { euiTheme } = useEuiTheme();
const activeFilterFill = css`
background: ${euiTheme.colors.darkestShade};
color: ${euiTheme.colors.lightestShade};
`;
return (
<EuiFlexGroup justifyContent="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton
onClick={() => setActiveFilter('all')}
color="text"
css={[filterButtonCss, activeFilter === 'all' && activeFilterFill]}
>
<FormattedMessage
id="guidedOnboardingPackage.gettingStarted.guideFilter.all.buttonLabel"
defaultMessage="All"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={() => setActiveFilter('search')}
color="text"
css={[filterButtonCss, activeFilter === 'search' && activeFilterFill]}
>
<FormattedMessage
id="guidedOnboardingPackage.gettingStarted.guideFilter.search.buttonLabel"
defaultMessage="Search"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={() => setActiveFilter('observability')}
color="text"
css={[filterButtonCss, activeFilter === 'observability' && activeFilterFill]}
>
<FormattedMessage
id="guidedOnboardingPackage.gettingStarted.guideFilter.observability.buttonLabel"
defaultMessage="Observability"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={() => setActiveFilter('security')}
color="text"
css={[filterButtonCss, activeFilter === 'security' && activeFilterFill]}
>
<FormattedMessage
id="guidedOnboardingPackage.gettingStarted.guideFilter.security.buttonLabel"
defaultMessage="Security"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -6,7 +6,6 @@
* Side Public License, v 1.
*/
export { GuideCard } from './guide_card';
export type { GuideCardUseCase } from './guide_card';
export { InfrastructureLinkCard } from './infrastructure_link_card';
export type { UseCase } from './use_case_card';
export { GuideCards } from './guide_cards';
export { GuideFilters } from './guide_filters';
export type { GuideFilterValues } from './guide_filters';

View file

@ -1,87 +0,0 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import type { NavigateToAppOptions } from '@kbn/core-application-browser';
import { UseCaseCard } from './use_case_card';
interface LinkCardConstants {
infrastructure: {
i18nTexts: {
title: string;
description: string;
};
};
}
const constants: LinkCardConstants = {
infrastructure: {
i18nTexts: {
title: i18n.translate(
'guidedOnboardingPackage.gettingStarted.infrastructure.linkCard.cardTitle',
{
defaultMessage: 'Observe my data',
}
),
description: i18n.translate(
'guidedOnboardingPackage.gettingStarted.infrastructure.linkCard.cardDescription',
{
defaultMessage:
'Add application, infrastructure, and user data through our pre-built integrations.',
}
),
},
},
};
export const InfrastructureLinkCard = ({
navigateToApp,
isDarkTheme,
addBasePath,
}: {
navigateToApp: (appId: string, options?: NavigateToAppOptions) => Promise<void>;
isDarkTheme: boolean;
addBasePath: (url: string) => string;
}) => {
const navigateToIntegrations = () => {
navigateToApp('integrations', {
path: '/browse/infrastructure',
});
};
const button = (
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiButton
// Used for FS tracking
data-test-subj={`onboarding--linkCard--observability`}
fill
onClick={navigateToIntegrations}
>
{i18n.translate(
'guidedOnboardingPackage.gettingStarted.infrastructure.linkCard.buttonLabel',
{
defaultMessage: 'View integrations',
}
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
return (
<UseCaseCard
useCase={'infrastructure'}
title={constants.infrastructure.i18nTexts.title}
description={constants.infrastructure.i18nTexts.description}
footer={button}
isDarkTheme={isDarkTheme}
addBasePath={addBasePath}
/>
);
};

View file

@ -1,117 +0,0 @@
/*
* 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, { ReactNode } from 'react';
import { EuiCard, EuiText, EuiImage } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { GuideCardUseCase } from './guide_card';
type UseCaseConstants = {
[key in UseCase]: {
logAltText: string;
betaBadgeLabel: string;
imageUrlPrefix: string;
};
};
const constants: UseCaseConstants = {
search: {
logAltText: i18n.translate('guidedOnboardingPackage.gettingStarted.search.iconName', {
defaultMessage: 'Enterprise Search logo',
}),
betaBadgeLabel: i18n.translate('guidedOnboardingPackage.gettingStarted.search.betaBadgeLabel', {
defaultMessage: 'search',
}),
imageUrlPrefix: '/plugins/home/assets/solution_logos/search',
},
kubernetes: {
logAltText: i18n.translate('guidedOnboardingPackage.gettingStarted.kubernetes.iconName', {
defaultMessage: 'Observability logo',
}),
betaBadgeLabel: i18n.translate(
'guidedOnboardingPackage.gettingStarted.kubernetes.betaBadgeLabel',
{
defaultMessage: 'observe',
}
),
imageUrlPrefix: '/plugins/home/assets/solution_logos/kubernetes',
},
infrastructure: {
logAltText: i18n.translate('guidedOnboardingPackage.gettingStarted.infrastructure.iconName', {
defaultMessage: 'Observability logo',
}),
betaBadgeLabel: i18n.translate(
'guidedOnboardingPackage.gettingStarted.infrastructure.betaBadgeLabel',
{
defaultMessage: 'observe',
}
),
imageUrlPrefix: '/plugins/home/assets/solution_logos/observability',
},
siem: {
logAltText: i18n.translate('guidedOnboardingPackage.gettingStarted.siem.iconName', {
defaultMessage: 'Security logo',
}),
betaBadgeLabel: i18n.translate('guidedOnboardingPackage.gettingStarted.siem.betaBadgeLabel', {
defaultMessage: 'protect',
}),
imageUrlPrefix: '/plugins/home/assets/solution_logos/security',
},
};
export type UseCase = GuideCardUseCase | 'infrastructure';
export interface UseCaseCardProps {
useCase: UseCase;
title: string;
description: string;
footer: ReactNode;
isDarkTheme: boolean;
addBasePath: (url: string) => string;
}
export const UseCaseCard = ({
useCase,
title,
description,
footer,
isDarkTheme,
addBasePath,
}: UseCaseCardProps) => {
const getImageUrl = (imageUrlPrefix: string) => {
const imagePath = `${imageUrlPrefix}${isDarkTheme ? '_dark' : ''}.png`;
return addBasePath(imagePath);
};
const titleElement = (
<EuiText textAlign="center">
<h4>
<strong>{title}</strong>
</h4>
</EuiText>
);
return (
<EuiCard
image={
<EuiImage
src={getImageUrl(constants[useCase].imageUrlPrefix)}
alt={constants[useCase].logAltText}
size={200}
margin="s"
/>
}
title={titleElement}
description={description}
footer={footer}
paddingSize="l"
betaBadgeProps={{
label: constants[useCase].betaBadgeLabel,
}}
/>
);
};

View file

@ -13,7 +13,8 @@
],
"kbn_references": [
"@kbn/i18n",
"@kbn/core-application-browser"
"@kbn/core-application-browser",
"@kbn/i18n-react"
],
"exclude": [
"target/**/*",

View file

@ -18,7 +18,7 @@ import { testGuideConfig, testGuideId } from '@kbn/guided-onboarding';
import type { PluginState } from '../../common';
import { API_BASE_PATH } from '../../common';
import { apiService } from '../services/api';
import { apiService } from '../services/api.service';
import type { GuidedOnboardingApi } from '../types';
import {
testGuideStep1ActiveState,

View file

@ -20,7 +20,7 @@ import {
import { i18n } from '@kbn/i18n';
import type { GuideState } from '@kbn/guided-onboarding';
import { NotificationsStart } from '@kbn/core/public';
import { apiService } from '../services/api';
import { apiService } from '../services/api.service';
interface QuitGuideModalProps {
closeModal: () => void;

View file

@ -27,7 +27,7 @@ import type {
GuidedOnboardingPluginStart,
} from './types';
import { GuidePanel } from './components';
import { ApiService, apiService } from './services/api';
import { ApiService, apiService } from './services/api.service';
export class GuidedOnboardingPlugin
implements Plugin<GuidedOnboardingPluginSetup, GuidedOnboardingPluginStart>

View file

@ -37,7 +37,7 @@ import {
getStepConfig,
isLastStep,
} from './helpers';
import { ConfigService } from './config_service';
import { ConfigService } from './config.service';
export class ApiService implements GuidedOnboardingApi {
private isCloudEnabled: boolean | undefined;
@ -123,6 +123,10 @@ export class ApiService implements GuidedOnboardingApi {
if (!this.client) {
throw new Error('ApiService has not be initialized.');
}
// don't send a request if a request is already in flight
if (this.isLoading$.value) {
return undefined;
}
try {
this.isLoading$.next(true);
@ -152,6 +156,10 @@ export class ApiService implements GuidedOnboardingApi {
if (!this.client) {
throw new Error('ApiService has not be initialized.');
}
// don't send a request if a request is already in flight
if (this.isLoading$.value) {
return undefined;
}
try {
this.isLoading$.next(true);
@ -474,6 +482,10 @@ export class ApiService implements GuidedOnboardingApi {
if (!this.client) {
throw new Error('ApiService has not be initialized.');
}
// don't send a request if a request is already in flight
if (this.isLoading$.value) {
return undefined;
}
this.isLoading$.next(true);
const config = await this.configService.getGuideConfig(guideId);
this.isLoading$.next(false);

View file

@ -13,7 +13,7 @@ import { testGuideConfig, testGuideId } from '@kbn/guided-onboarding';
import { firstValueFrom, Subscription } from 'rxjs';
import { API_BASE_PATH } from '../../common';
import { ApiService } from './api';
import { ApiService } from './api.service';
import {
testGuideFirstStep,
testGuideLastStep,

View file

@ -18,7 +18,7 @@ import {
wrongIntegration,
} from './api.mocks';
import { ConfigService } from './config_service';
import { ConfigService } from './config.service';
describe('GuidedOnboarding ConfigService', () => {
let configService: ConfigService;

View file

@ -36,7 +36,7 @@ exports[`getting started should render getting started component 1`] = `
textAlign="center"
>
<p>
Select a guide to help you make the most of your data.
Select an option and we'll help you get started.
</p>
</EuiText>
<EuiSpacer
@ -45,55 +45,19 @@ exports[`getting started should render getting started component 1`] = `
<EuiSpacer
size="xxl"
/>
<EuiFlexGrid
columns={4}
gutterSize="l"
>
<EuiFlexItem
key="guideCard-search"
>
<GuideCard
activateGuide={[Function]}
addBasePath={[Function]}
guides={Array []}
isDarkTheme={false}
useCase="search"
/>
</EuiFlexItem>
<EuiFlexItem
key="guideCard-kubernetes"
>
<GuideCard
activateGuide={[Function]}
addBasePath={[Function]}
guides={Array []}
isDarkTheme={false}
useCase="kubernetes"
/>
</EuiFlexItem>
<EuiFlexItem
key="linkCard-infrastructure"
>
<InfrastructureLinkCard
addBasePath={[Function]}
isDarkTheme={false}
navigateToApp={[MockFunction]}
/>
</EuiFlexItem>
<EuiFlexItem
key="guideCard-siem"
>
<GuideCard
activateGuide={[Function]}
addBasePath={[Function]}
guides={Array []}
isDarkTheme={false}
useCase="siem"
/>
</EuiFlexItem>
</EuiFlexGrid>
<EuiSpacer />
<EuiHorizontalRule />
<GuideFilters
activeFilter="all"
setActiveFilter={[Function]}
/>
<EuiSpacer
size="xxl"
/>
<GuideCards
activateGuide={[Function]}
activeFilter="all"
guidesState={Array []}
navigateToApp={[MockFunction]}
/>
<EuiSpacer />
<div
className="eui-textCenter"
@ -102,7 +66,7 @@ exports[`getting started should render getting started component 1`] = `
data-test-subj="onboarding--skipGuideLink"
onClick={[Function]}
>
Id like to do something else (skip)
Id like to do something else.
</EuiLink>
</div>
</_EuiPageSection>

View file

@ -12,8 +12,7 @@ import { act } from 'react-dom/test-utils';
import { findTestSubject, registerTestBed, TestBed } from '@kbn/test-jest-helpers';
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
import { chromeServiceMock, applicationServiceMock, httpServiceMock } from '@kbn/core/public/mocks';
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
import { ApiService } from '@kbn/guided-onboarding-plugin/public/services/api';
import { ApiService } from '@kbn/guided-onboarding-plugin/public/services/api.service';
import { GettingStarted } from './getting_started';
import { KEY_ENABLE_WELCOME } from '../home';
@ -21,8 +20,6 @@ import { KEY_ENABLE_WELCOME } from '../home';
const mockCloud = cloudMock.createSetup();
const mockChrome = chromeServiceMock.createStartContract();
const mockApplication = applicationServiceMock.createStartContract();
const mockSettingsUI = uiSettingsServiceMock.createSetupContract();
mockSettingsUI.get.mockReturnValue(false);
const mockHttp = httpServiceMock.createStartContract();
const mockApiService = new ApiService();
mockApiService.setup(mockHttp, true);
@ -33,8 +30,6 @@ jest.mock('../../kibana_services', () => ({
chrome: mockChrome,
application: mockApplication,
trackUiMetric: jest.fn(),
uiSettings: mockSettingsUI,
http: mockHttp,
guidedOnboardingService: mockApiService,
}),
}));

View file

@ -9,9 +9,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import {
EuiButton,
EuiFlexGrid,
EuiFlexItem,
EuiHorizontalRule,
EuiLink,
EuiLoadingSpinner,
EuiPageTemplate,
@ -26,9 +23,9 @@ import { useHistory } from 'react-router-dom';
import { METRIC_TYPE } from '@kbn/analytics';
import { i18n } from '@kbn/i18n';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import type { GuideState, GuideId, GuideCardUseCase } from '@kbn/guided-onboarding';
import { GuideCard, InfrastructureLinkCard } from '@kbn/guided-onboarding';
import type { GuideFilterValues, GuideId, GuideState } from '@kbn/guided-onboarding';
import { GuideCards, GuideFilters } from '@kbn/guided-onboarding';
import { getServices } from '../../kibana_services';
import { KEY_ENABLE_WELCOME } from '../home';
@ -40,18 +37,18 @@ const title = i18n.translate('home.guidedOnboarding.gettingStarted.useCaseSelect
defaultMessage: 'What would you like to do first?',
});
const subtitle = i18n.translate('home.guidedOnboarding.gettingStarted.useCaseSelectionSubtitle', {
defaultMessage: 'Select a guide to help you make the most of your data.',
defaultMessage: `Select an option and we'll help you get started.`,
});
const skipText = i18n.translate('home.guidedOnboarding.gettingStarted.skip.buttonLabel', {
defaultMessage: `Id like to do something else (skip)`,
defaultMessage: `Id like to do something else.`,
});
export const GettingStarted = () => {
const { application, trackUiMetric, chrome, guidedOnboardingService, http, uiSettings, cloud } =
getServices();
const { application, trackUiMetric, chrome, guidedOnboardingService, cloud } = getServices();
const [guidesState, setGuidesState] = useState<GuideState[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isError, setIsError] = useState<boolean>(false);
const [filter, setFilter] = useState<GuideFilterValues>('all');
const history = useHistory();
useEffect(() => {
@ -114,11 +111,10 @@ export const GettingStarted = () => {
padding: calc(${euiTheme.size.base}*3) calc(${euiTheme.size.base}*4);
`;
const isDarkTheme = uiSettings.get<boolean>('theme:darkMode');
const activateGuide = useCallback(
async (useCase: GuideCardUseCase, guideState?: GuideState) => {
async (guideId: GuideId, guideState?: GuideState) => {
try {
await guidedOnboardingService?.activateGuide(useCase as GuideId, guideState);
await guidedOnboardingService?.activateGuide(guideId, guideState);
} catch (err) {
getServices().toastNotifications.addDanger({
title: i18n.translate('home.guidedOnboarding.gettingStarted.activateGuide.errorMessage', {
@ -200,34 +196,14 @@ export const GettingStarted = () => {
</EuiText>
<EuiSpacer size="s" />
<EuiSpacer size="xxl" />
<EuiFlexGrid columns={4} gutterSize="l">
{['search', 'kubernetes', 'infrastructure', 'siem'].map((useCase) => {
if (useCase === 'infrastructure') {
return (
<EuiFlexItem key={`linkCard-${useCase}`}>
<InfrastructureLinkCard
navigateToApp={application.navigateToApp}
isDarkTheme={isDarkTheme}
addBasePath={http.basePath.prepend}
/>
</EuiFlexItem>
);
}
return (
<EuiFlexItem key={`guideCard-${useCase}`}>
<GuideCard
useCase={useCase as GuideCardUseCase}
guides={guidesState}
activateGuide={activateGuide}
isDarkTheme={isDarkTheme}
addBasePath={http.basePath.prepend}
/>
</EuiFlexItem>
);
})}
</EuiFlexGrid>
<EuiSpacer />
<EuiHorizontalRule />
<GuideFilters activeFilter={filter} setActiveFilter={setFilter} />
<EuiSpacer size="xxl" />
<GuideCards
activateGuide={activateGuide}
navigateToApp={application.navigateToApp}
activeFilter={filter}
guidesState={guidesState}
/>
<EuiSpacer />
<div className="eui-textCenter">
{/* data-test-subj used for FS tracking */}

View file

@ -23,7 +23,6 @@
"@kbn/shared-ux-page-kibana-template",
"@kbn/utility-types",
"@kbn/guided-onboarding",
"@kbn/core-ui-settings-browser-mocks",
"@kbn/ui-theme",
"@kbn/config-schema",
"@kbn/utility-types-jest",

View file

@ -3015,15 +3015,6 @@
"guidedOnboarding.quitGuideModal.modalDescription": "Vous pouvez redémarrer le guide de configuration à tout moment à partir du menu Aide.",
"guidedOnboarding.quitGuideModal.modalTitle": "Quitter ce guide ?",
"guidedOnboarding.quitGuideModal.quitButtonLabel": "Quitter le guide",
"guidedOnboardingPackage.gettingStarted.guideCard.stepsLabel": "{progress} étapes",
"guidedOnboardingPackage.gettingStarted.guideCard.continueGuide.buttonLabel": "Continuer",
"guidedOnboardingPackage.gettingStarted.guideCard.progress.completedLabel": "Terminé",
"guidedOnboardingPackage.gettingStarted.guideCard.progress.inProgressLabel": "En cours",
"guidedOnboardingPackage.gettingStarted.guideCard.search.cardDescription": "Créez une expérience de recherche pour vos sites web, vos applications, votre contenu sur le lieu de travail, etc.",
"guidedOnboardingPackage.gettingStarted.guideCard.search.cardTitle": "Rechercher dans mes données",
"guidedOnboardingPackage.gettingStarted.guideCard.startGuide.buttonLabel": "Afficher le guide",
"guidedOnboardingPackage.gettingStarted.search.betaBadgeLabel": "rechercher",
"guidedOnboardingPackage.gettingStarted.search.iconName": "Logo Entreprise Search",
"home.loadTutorials.requestFailedErrorMessage": "Échec de la requête avec le code de statut : {status}",
"home.tutorial.addDataToKibanaDescription": "En plus d'ajouter {integrationsLink}, vous pouvez essayer l'exemple de données ou charger vos propres données.",
"home.tutorial.noTutorialLabel": "Tutoriel {tutorialId} introuvable",

View file

@ -3013,15 +3013,6 @@
"guidedOnboarding.quitGuideModal.modalDescription": "[ヘルプ]メニューを使用すると、いつでもセットアップガイドを再開できます。",
"guidedOnboarding.quitGuideModal.modalTitle": "このガイドを終了しますか?",
"guidedOnboarding.quitGuideModal.quitButtonLabel": "ガイドを終了",
"guidedOnboardingPackage.gettingStarted.guideCard.stepsLabel": "{progress}ステップ",
"guidedOnboardingPackage.gettingStarted.guideCard.continueGuide.buttonLabel": "続行",
"guidedOnboardingPackage.gettingStarted.guideCard.progress.completedLabel": "完了",
"guidedOnboardingPackage.gettingStarted.guideCard.progress.inProgressLabel": "進行中",
"guidedOnboardingPackage.gettingStarted.guideCard.search.cardDescription": "Webサイト、アプリケーション、workplaceコンテンツなどに合った、検索エクスペリエンスを作成します。",
"guidedOnboardingPackage.gettingStarted.guideCard.search.cardTitle": "データを検索",
"guidedOnboardingPackage.gettingStarted.guideCard.startGuide.buttonLabel": "ガイドを表示",
"guidedOnboardingPackage.gettingStarted.search.betaBadgeLabel": "検索",
"guidedOnboardingPackage.gettingStarted.search.iconName": "エンタープライズ サーチロゴ",
"home.loadTutorials.requestFailedErrorMessage": "リクエスト失敗、ステータスコード:{status}",
"home.tutorial.addDataToKibanaDescription": "{integrationsLink}を追加するほかに、サンプルデータを試したり、独自のデータをアップロードしたりできます。",
"home.tutorial.noTutorialLabel": "チュートリアル {tutorialId} が見つかりません",

View file

@ -3017,15 +3017,6 @@
"guidedOnboarding.quitGuideModal.modalDescription": "您可以随时从“帮助”菜单重新启动设置指南。",
"guidedOnboarding.quitGuideModal.modalTitle": "退出本指南?",
"guidedOnboarding.quitGuideModal.quitButtonLabel": "退出指南",
"guidedOnboardingPackage.gettingStarted.guideCard.stepsLabel": "{progress} 步骤",
"guidedOnboardingPackage.gettingStarted.guideCard.continueGuide.buttonLabel": "继续",
"guidedOnboardingPackage.gettingStarted.guideCard.progress.completedLabel": "已完成",
"guidedOnboardingPackage.gettingStarted.guideCard.progress.inProgressLabel": "进行中",
"guidedOnboardingPackage.gettingStarted.guideCard.search.cardDescription": "为您的网站、应用程序、工作区内容或期间的任何内容创建搜索体验。",
"guidedOnboardingPackage.gettingStarted.guideCard.search.cardTitle": "搜索我的数据",
"guidedOnboardingPackage.gettingStarted.guideCard.startGuide.buttonLabel": "查看指南",
"guidedOnboardingPackage.gettingStarted.search.betaBadgeLabel": "搜索",
"guidedOnboardingPackage.gettingStarted.search.iconName": "Enterprise Search 徽标",
"home.loadTutorials.requestFailedErrorMessage": "请求失败,状态代码:{status}",
"home.tutorial.addDataToKibanaDescription": "除了添加 {integrationsLink} 以外,您还可以试用样例数据或上传自己的数据。",
"home.tutorial.noTutorialLabel": "无法找到教程 {tutorialId}",