[Observability Onboarding] Make custom cards always appear in search results (#208913)

Closes #207734

This PR:
* Changes the behavior of the search form to always include quickstart
flows in search results even when a category is not selected
* Refactors and cleans up the code a bit


https://github.com/user-attachments/assets/e5de7092-2d9f-41be-8d69-25954e5e4bff

## How to test

Make sure that the search works as expected and when clicking on the
cards it leads to the right places.
This commit is contained in:
Mykola Harmash 2025-02-05 09:47:08 +01:00 committed by GitHub
parent b1b28c3258
commit 015a4ac618
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 573 additions and 751 deletions

View file

@ -26,12 +26,13 @@ import { css } from '@emotion/react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { OnboardingFlowPackageList } from '../packages_list';
import { IntegrationCardItem } from '@kbn/fleet-plugin/public';
import { PackageListSearchForm } from '../package_list_search_form/package_list_search_form';
import { Category } from './types';
import { useCustomCardsForCategory } from './use_custom_cards_for_category';
import { useVirtualSearchResults } from './use_virtual_search_results';
import { useCustomCards } from './use_custom_cards';
import { LogoIcon, SupportedLogo } from '../shared/logo_icon';
import { ObservabilityOnboardingAppServices } from '../..';
import { PackageList } from '../package_list/package_list';
interface UseCaseOption {
id: Category;
@ -147,11 +148,25 @@ export const OnboardingFlowForm: FunctionComponent = () => {
[] // eslint-disable-line react-hooks/exhaustive-deps
);
const customCards = useCustomCardsForCategory(
createCollectionCardHandler,
searchParams.get('category') as Category | null
);
const virtualSearchResults = useVirtualSearchResults();
const featuredCardsForCategoryMap: Record<Category, string[]> = {
host: ['auto-detect-logs', 'otel-logs'],
kubernetes: ['kubernetes-quick-start', 'otel-kubernetes'],
application: ['apm-virtual', 'otel-virtual', 'synthetics-virtual'],
cloud: ['azure-logs-virtual', 'aws-logs-virtual', 'gcp-logs-virtual'],
};
const customCards = useCustomCards(createCollectionCardHandler);
const featuredCardsForCategory: IntegrationCardItem[] = customCards.filter((card) => {
const category = searchParams.get('category') as Category;
if (category === null) {
return false;
}
const cardList = featuredCardsForCategoryMap[category] ?? [];
return cardList.includes(card.id);
});
/**
* Cloud deployments have the new Firehose quick start
* flow enabled, so the ond card 'epr:awsfirehose' should
@ -272,7 +287,7 @@ export const OnboardingFlowForm: FunctionComponent = () => {
</EuiFlexGrid>
{/* Hiding element instead of not rending these elements in order to preload available packages on page load */}
<div
hidden={!searchParams.get('category') || !customCards}
hidden={featuredCardsForCategory.length === 0}
role="group"
aria-labelledby={packageListTitleId}
>
@ -310,11 +325,7 @@ export const OnboardingFlowForm: FunctionComponent = () => {
</strong>
</EuiTitle>
<EuiSpacer size="m" />
<OnboardingFlowPackageList
customCards={customCards}
flowSearch={integrationSearch}
flowCategory={searchParams.get('category')}
/>
<PackageList list={featuredCardsForCategory} />
</div>
</div>
@ -329,20 +340,12 @@ export const OnboardingFlowForm: FunctionComponent = () => {
</strong>
</EuiText>
<EuiSpacer size="m" />
<OnboardingFlowPackageList
showSearchBar={true}
<PackageListSearchForm
searchQuery={integrationSearch}
flowSearch={integrationSearch}
setSearchQuery={setIntegrationSearch}
flowCategory={searchParams.get('category')}
customCards={(customCards || [])
.filter(
// Filter out collection cards and regular integrations that show up via search anyway
(card) => card.type === 'virtual' && !card.isCollectionCard
)
.concat(virtualSearchResults)}
customCards={customCards.filter((card) => !card.isCollectionCard)}
excludePackageIdList={searchExcludePackageIdList}
joinCardLists
/>
</div>
</EuiPanel>

View file

@ -0,0 +1,411 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { EuiFlexItem } from '@elastic/eui';
import { reactRouterNavigate, useKibana } from '@kbn/kibana-react-plugin/public';
import { IntegrationCardItem } from '@kbn/fleet-plugin/public';
import { useHistory } from 'react-router-dom';
import { useLocation } from 'react-router-dom-v5-compat';
import { ObservabilityOnboardingAppServices } from '../..';
import { LogoIcon } from '../shared/logo_icon';
export function useCustomCards(
createCollectionCardHandler: (query: string) => () => void
): IntegrationCardItem[] {
const history = useHistory();
const location = useLocation();
const {
services: {
application,
http,
context: { isServerless, isCloud },
},
} = useKibana<ObservabilityOnboardingAppServices>();
const getUrlForApp = application?.getUrlForApp;
const { href: autoDetectUrl } = reactRouterNavigate(history, `/auto-detect/${location.search}`);
const { href: otelLogsUrl } = reactRouterNavigate(history, `/otel-logs/${location.search}`);
const { href: kubernetesUrl } = reactRouterNavigate(history, `/kubernetes/${location.search}`);
const { href: otelKubernetesUrl } = reactRouterNavigate(
history,
`/otel-kubernetes/${location.search}`
);
const { href: customLogsUrl } = reactRouterNavigate(history, `/customLogs/${location.search}`);
const { href: firehoseUrl } = reactRouterNavigate(history, `/firehose/${location.search}`);
const apmUrl = `${getUrlForApp?.('apm')}/${isServerless ? 'onboarding' : 'tutorial'}`;
const otelApmUrl = isServerless ? `${apmUrl}?agent=openTelemetry` : apmUrl;
const firehoseQuickstartCard: IntegrationCardItem = {
id: 'firehose-quick-start',
name: 'firehose-quick-start',
type: 'virtual',
title: i18n.translate('xpack.observability_onboarding.packageList.uploadFileTitle', {
defaultMessage: 'AWS Firehose',
}),
description: i18n.translate(
'xpack.observability_onboarding.packageList.uploadFileDescription',
{
defaultMessage: 'Collect logs and metrics from Amazon Web Services (AWS).',
}
),
categories: ['observability'],
icons: [
{
type: 'svg',
src: 'https://epr.elastic.co/package/awsfirehose/1.1.0/img/logo_firehose.svg',
},
],
url: firehoseUrl,
version: '',
integration: '',
isQuickstart: true,
};
return [
{
id: 'auto-detect-logs',
name: 'auto-detect-logs-virtual',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.autoDetectTitle',
{
defaultMessage: 'Elastic Agent: Logs & Metrics',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.autoDetectDescription',
{
defaultMessage: 'Scan your host for log files, metrics, auto-install integrations',
}
),
extraLabelsBadges: [
<ExtraLabelBadgeWrapper>
<LogoIcon logo="apple" size="m" />
</ExtraLabelBadgeWrapper>,
<ExtraLabelBadgeWrapper>
<LogoIcon logo="linux" size="m" />
</ExtraLabelBadgeWrapper>,
],
categories: ['observability'],
icons: [
{
type: 'eui',
src: 'agentApp',
},
],
url: autoDetectUrl,
version: '',
integration: '',
isQuickstart: true,
},
{
id: 'otel-logs',
name: 'custom-logs-virtual',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.logsOtelTitle',
{
defaultMessage: 'OpenTelemetry: Logs & Metrics',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.logsOtelDescription',
{
defaultMessage:
'Collect logs and host metrics with the Elastic Distro for OTel Collector',
}
),
extraLabelsBadges: [
<ExtraLabelBadgeWrapper>
<LogoIcon logo="apple" size="m" />
</ExtraLabelBadgeWrapper>,
<ExtraLabelBadgeWrapper>
<LogoIcon logo="linux" size="m" />
</ExtraLabelBadgeWrapper>,
],
categories: ['observability'],
icons: [
{
type: 'svg',
src: http?.staticAssets.getPluginAssetHref('opentelemetry.svg') ?? '',
},
],
url: otelLogsUrl,
version: '',
integration: '',
isQuickstart: true,
},
{
id: 'kubernetes-quick-start',
name: 'kubernetes-quick-start',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesTitle',
{
defaultMessage: 'Elastic Agent: Logs & Metrics',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesDescription',
{
defaultMessage: 'Collect logs and metrics from Kubernetes using Elastic Agent',
}
),
extraLabelsBadges: [
<ExtraLabelBadgeWrapper>
<LogoIcon logo="kubernetes" size="m" />
</ExtraLabelBadgeWrapper>,
],
categories: ['observability'],
icons: [
{
type: 'eui',
src: 'agentApp',
},
],
url: kubernetesUrl,
version: '',
integration: '',
isQuickstart: true,
},
{
id: 'otel-kubernetes',
name: 'otel-kubernetes-virtual',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesOtelTitle',
{
defaultMessage: 'OpenTelemetry: Full Observability',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesOtelDescription',
{
defaultMessage:
'Collect logs, traces and metrics with the Elastic Distro for OTel Collector',
}
),
extraLabelsBadges: [
<ExtraLabelBadgeWrapper>
<LogoIcon logo="kubernetes" size="m" />
</ExtraLabelBadgeWrapper>,
],
categories: ['observability'],
icons: [
{
type: 'svg',
src: http?.staticAssets.getPluginAssetHref('opentelemetry.svg') ?? '',
},
],
url: otelKubernetesUrl,
version: '',
integration: '',
isQuickstart: true,
},
{
id: 'apm-virtual',
type: 'virtual',
title: i18n.translate('xpack.observability_onboarding.useCustomCardsForCategory.apmTitle', {
defaultMessage: 'Elastic APM',
}),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.apmDescription',
{
defaultMessage: 'Collect distributed traces from your applications with Elastic APM',
}
),
name: 'apm',
categories: ['observability'],
icons: [
{
type: 'eui',
src: 'apmApp',
},
],
url: apmUrl,
version: '',
integration: '',
},
{
id: 'otel-virtual',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.apmOtelTitle',
{
defaultMessage: 'OpenTelemetry',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.apmOtelDescription',
{
defaultMessage: 'Collect distributed traces with OpenTelemetry',
}
),
name: 'otel',
categories: ['observability'],
icons: [
{
type: 'svg',
src: http?.staticAssets.getPluginAssetHref('opentelemetry.svg') ?? '',
},
],
url: otelApmUrl,
version: '',
integration: '',
},
{
id: 'synthetics-virtual',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.syntheticsTitle',
{
defaultMessage: 'Synthetic monitor',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.syntheticsDescription',
{
defaultMessage: 'Monitor endpoints, pages, and user journeys',
}
),
name: 'synthetics',
categories: ['observability'],
icons: [
{
type: 'eui',
src: 'logoUptime',
},
],
url: getUrlForApp?.('synthetics') ?? '',
version: '',
integration: '',
},
{
id: 'azure-logs-virtual',
type: 'virtual',
title: i18n.translate('xpack.observability_onboarding.useCustomCardsForCategory.azureTitle', {
defaultMessage: 'Azure',
}),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.azureDescription',
{
defaultMessage: 'Collect logs from Microsoft Azure',
}
),
name: 'azure',
categories: ['observability'],
icons: [],
url: 'https://azure.com',
version: '',
integration: '',
isCollectionCard: true,
onCardClick: createCollectionCardHandler('azure'),
},
{
id: 'aws-logs-virtual',
type: 'virtual',
title: i18n.translate('xpack.observability_onboarding.useCustomCardsForCategory.awsTitle', {
defaultMessage: 'AWS',
}),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.awsDescription',
{
defaultMessage: 'Collect logs from Amazon Web Services (AWS)',
}
),
name: 'aws',
categories: ['observability'],
icons: [],
url: 'https://aws.com',
version: '',
integration: '',
isCollectionCard: true,
onCardClick: createCollectionCardHandler('aws'),
},
{
id: 'gcp-logs-virtual',
type: 'virtual',
title: i18n.translate('xpack.observability_onboarding.useCustomCardsForCategory.gcpTitle', {
defaultMessage: 'Google Cloud Platform',
}),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.gcpDescription',
{
defaultMessage: 'Collect logs from Google Cloud Platform',
}
),
name: 'gcp',
categories: ['observability'],
icons: [],
url: '',
version: '',
integration: '',
isCollectionCard: true,
onCardClick: createCollectionCardHandler('gcp'),
},
{
id: 'upload-file-virtual',
type: 'virtual',
title: i18n.translate('xpack.observability_onboarding.packageList.uploadFileTitle', {
defaultMessage: 'Upload a file',
}),
description: i18n.translate(
'xpack.observability_onboarding.packageList.uploadFileDescription',
{
defaultMessage:
'Upload data from a CSV, TSV, JSON or other log file to Elasticsearch for analysis.',
}
),
name: 'upload-file',
categories: ['observability'],
icons: [
{
type: 'eui',
src: 'addDataApp',
},
],
url: `${getUrlForApp?.('home')}#/tutorial_directory/fileDataViz`,
version: '',
integration: '',
isCollectionCard: false,
},
{
id: 'custom-logs',
type: 'virtual',
title: 'Stream log files',
description: 'Stream any logs into Elastic in a simple way and explore their data',
name: 'custom-logs-virtual',
categories: ['observability'],
icons: [
{
type: 'eui',
src: 'filebeatApp',
},
],
url: customLogsUrl,
version: '',
integration: '',
},
/**
* The new Firehose card should only be visible on Cloud
* as Firehose integration requires additional proxy,
* which is not available for on-prem customers.
*/
...(isCloud ? [firehoseQuickstartCard] : []),
];
}
function ExtraLabelBadgeWrapper({ children }: { children: React.ReactNode }) {
return (
<EuiFlexItem grow={false} css={{ alignSelf: 'center' }}>
{children}
</EuiFlexItem>
);
}

View file

@ -1,365 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { EuiFlexItem } from '@elastic/eui';
import { reactRouterNavigate, useKibana } from '@kbn/kibana-react-plugin/public';
import { useHistory } from 'react-router-dom';
import { useLocation } from 'react-router-dom-v5-compat';
import { ObservabilityOnboardingAppServices } from '../..';
import { CustomCard } from '../packages_list/types';
import { Category } from './types';
import { LogoIcon } from '../shared/logo_icon';
export function useCustomCardsForCategory(
createCollectionCardHandler: (query: string) => () => void,
category: Category | null
): CustomCard[] | undefined {
const history = useHistory();
const location = useLocation();
const {
services: {
application,
http,
context: { isServerless },
},
} = useKibana<ObservabilityOnboardingAppServices>();
const getUrlForApp = application?.getUrlForApp;
const { href: autoDetectUrl } = reactRouterNavigate(history, `/auto-detect/${location.search}`);
const { href: otelLogsUrl } = reactRouterNavigate(history, `/otel-logs/${location.search}`);
const { href: kubernetesUrl } = reactRouterNavigate(history, `/kubernetes/${location.search}`);
const { href: otelKubernetesUrl } = reactRouterNavigate(
history,
`/otel-kubernetes/${location.search}`
);
const apmUrl = `${getUrlForApp?.('apm')}/${isServerless ? 'onboarding' : 'tutorial'}`;
const otelApmUrl = isServerless ? `${apmUrl}?agent=openTelemetry` : apmUrl;
switch (category) {
case 'host':
return [
{
id: 'auto-detect-logs',
name: 'auto-detect-logs-virtual',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.autoDetectTitle',
{
defaultMessage: 'Elastic Agent: Logs & Metrics',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.autoDetectDescription',
{
defaultMessage: 'Scan your host for log files, metrics, auto-install integrations',
}
),
extraLabelsBadges: [
<ExtraLabelBadgeWrapper>
<LogoIcon logo="apple" size="m" />
</ExtraLabelBadgeWrapper>,
<ExtraLabelBadgeWrapper>
<LogoIcon logo="linux" size="m" />
</ExtraLabelBadgeWrapper>,
],
categories: ['observability'],
icons: [
{
type: 'eui',
src: 'agentApp',
},
],
url: autoDetectUrl,
version: '',
integration: '',
isQuickstart: true,
},
{
id: 'otel-logs',
name: 'custom-logs-virtual',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.logsOtelTitle',
{
defaultMessage: 'OpenTelemetry: Logs & Metrics',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.logsOtelDescription',
{
defaultMessage:
'Collect logs and host metrics with the Elastic Distro for OTel Collector',
}
),
extraLabelsBadges: [
<ExtraLabelBadgeWrapper>
<LogoIcon logo="apple" size="m" />
</ExtraLabelBadgeWrapper>,
<ExtraLabelBadgeWrapper>
<LogoIcon logo="linux" size="m" />
</ExtraLabelBadgeWrapper>,
],
categories: ['observability'],
icons: [
{
type: 'svg',
src: http?.staticAssets.getPluginAssetHref('opentelemetry.svg') ?? '',
},
],
url: otelLogsUrl,
version: '',
integration: '',
isQuickstart: true,
},
];
case 'kubernetes':
return [
{
id: 'kubernetes-quick-start',
name: 'kubernetes-quick-start',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesTitle',
{
defaultMessage: 'Elastic Agent: Logs & Metrics',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesDescription',
{
defaultMessage: 'Collect logs and metrics from Kubernetes using Elastic Agent',
}
),
extraLabelsBadges: [
<ExtraLabelBadgeWrapper>
<LogoIcon logo="kubernetes" size="m" />
</ExtraLabelBadgeWrapper>,
],
categories: ['observability'],
icons: [
{
type: 'eui',
src: 'agentApp',
},
],
url: kubernetesUrl,
version: '',
integration: '',
isQuickstart: true,
},
{
id: 'otel-kubernetes',
name: 'otel-kubernetes-virtual',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesOtelTitle',
{
defaultMessage: 'OpenTelemetry: Full Observability',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesOtelDescription',
{
defaultMessage:
'Collect logs, traces and metrics with the Elastic Distro for OTel Collector',
}
),
extraLabelsBadges: [
<ExtraLabelBadgeWrapper>
<LogoIcon logo="kubernetes" size="m" />
</ExtraLabelBadgeWrapper>,
],
categories: ['observability'],
icons: [
{
type: 'svg',
src: http?.staticAssets.getPluginAssetHref('opentelemetry.svg') ?? '',
},
],
url: otelKubernetesUrl,
version: '',
integration: '',
isQuickstart: true,
},
];
case 'application':
return [
{
id: 'apm-virtual',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.apmTitle',
{
defaultMessage: 'Elastic APM',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.apmDescription',
{
defaultMessage: 'Collect distributed traces from your applications with Elastic APM',
}
),
name: 'apm',
categories: ['observability'],
icons: [
{
type: 'eui',
src: 'apmApp',
},
],
url: apmUrl,
version: '',
integration: '',
},
{
id: 'otel-virtual',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.apmOtelTitle',
{
defaultMessage: 'OpenTelemetry',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.apmOtelDescription',
{
defaultMessage: 'Collect distributed traces with OpenTelemetry',
}
),
name: 'otel',
categories: ['observability'],
icons: [
{
type: 'svg',
src: http?.staticAssets.getPluginAssetHref('opentelemetry.svg') ?? '',
},
],
url: otelApmUrl,
version: '',
integration: '',
},
{
id: 'synthetics-virtual',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.syntheticsTitle',
{
defaultMessage: 'Synthetic monitor',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.syntheticsDescription',
{
defaultMessage: 'Monitor endpoints, pages, and user journeys',
}
),
name: 'synthetics',
categories: ['observability'],
icons: [
{
type: 'eui',
src: 'logoUptime',
},
],
url: getUrlForApp?.('synthetics') ?? '',
version: '',
integration: '',
},
];
case 'cloud':
return [
{
id: 'azure-logs-virtual',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.azureTitle',
{
defaultMessage: 'Azure',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.azureDescription',
{
defaultMessage: 'Collect logs from Microsoft Azure',
}
),
name: 'azure',
categories: ['observability'],
icons: [],
url: 'https://azure.com',
version: '',
integration: '',
isCollectionCard: true,
onCardClick: createCollectionCardHandler('azure'),
},
{
id: 'aws-logs-virtual',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.awsTitle',
{
defaultMessage: 'AWS',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.awsDescription',
{
defaultMessage: 'Collect logs from Amazon Web Services (AWS)',
}
),
name: 'aws',
categories: ['observability'],
icons: [],
url: 'https://aws.com',
version: '',
integration: '',
isCollectionCard: true,
onCardClick: createCollectionCardHandler('aws'),
},
{
id: 'gcp-logs-virtual',
type: 'virtual',
title: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.gcpTitle',
{
defaultMessage: 'Google Cloud Platform',
}
),
description: i18n.translate(
'xpack.observability_onboarding.useCustomCardsForCategory.gcpDescription',
{
defaultMessage: 'Collect logs from Google Cloud Platform',
}
),
name: 'gcp',
categories: ['observability'],
icons: [],
url: '',
version: '',
integration: '',
isCollectionCard: true,
onCardClick: createCollectionCardHandler('gcp'),
},
];
default:
return undefined;
}
}
function ExtraLabelBadgeWrapper({ children }: { children: React.ReactNode }) {
return (
<EuiFlexItem grow={false} css={{ alignSelf: 'center' }}>
{children}
</EuiFlexItem>
);
}

View file

@ -1,103 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { useHistory } from 'react-router-dom';
import { reactRouterNavigate, useKibana } from '@kbn/kibana-react-plugin/public';
import { CustomCard } from '../packages_list/types';
import { ObservabilityOnboardingAppServices } from '../..';
export function useVirtualSearchResults(): CustomCard[] {
const {
services: {
application,
context: { isCloud },
},
} = useKibana<ObservabilityOnboardingAppServices>();
const history = useHistory();
const { href: customLogsUrl } = reactRouterNavigate(history, `/customLogs/${location.search}`);
const { href: firehoseUrl } = reactRouterNavigate(history, `/firehose/${location.search}`);
const getUrlForApp = application?.getUrlForApp;
const firehoseQuickstartCard: CustomCard = {
id: 'firehose-quick-start',
name: 'firehose-quick-start',
type: 'virtual',
title: i18n.translate('xpack.observability_onboarding.packageList.uploadFileTitle', {
defaultMessage: 'AWS Firehose',
}),
description: i18n.translate(
'xpack.observability_onboarding.packageList.uploadFileDescription',
{
defaultMessage: 'Collect logs and metrics from Amazon Web Services (AWS).',
}
),
categories: [],
icons: [
{
type: 'svg',
src: 'https://epr.elastic.co/package/awsfirehose/1.1.0/img/logo_firehose.svg',
},
],
url: firehoseUrl,
version: '',
integration: '',
isQuickstart: true,
};
return [
{
id: 'upload-file-virtual',
type: 'virtual',
title: i18n.translate('xpack.observability_onboarding.packageList.uploadFileTitle', {
defaultMessage: 'Upload a file',
}),
description: i18n.translate(
'xpack.observability_onboarding.packageList.uploadFileDescription',
{
defaultMessage:
'Upload data from a CSV, TSV, JSON or other log file to Elasticsearch for analysis.',
}
),
name: 'upload-file',
categories: [],
icons: [
{
type: 'eui',
src: 'addDataApp',
},
],
url: `${getUrlForApp?.('home')}#/tutorial_directory/fileDataViz`,
version: '',
integration: '',
isCollectionCard: false,
},
{
id: 'custom-logs',
type: 'virtual',
title: 'Stream log files',
description: 'Stream any logs into Elastic in a simple way and explore their data',
name: 'custom-logs-virtual',
categories: ['observability'],
icons: [
{
type: 'eui',
src: 'filebeatApp',
},
],
url: customLogsUrl,
version: '',
integration: '',
},
/**
* The new Firehose card should only be visible on Cloud
* as Firehose integration requires additional proxy,
* which is not available for on-prem customers.
*/
...(isCloud ? [firehoseQuickstartCard] : []),
];
}

View file

@ -0,0 +1,47 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { IntegrationCardItem } from '@kbn/fleet-plugin/public';
import React, { Suspense, lazy } from 'react';
export const LazyPackageList = lazy(async () => ({
default: await import('@kbn/fleet-plugin/public')
.then((module) => module.PackageList())
.then((pkg) => pkg.PackageListGrid),
}));
interface Props {
list: IntegrationCardItem[];
searchTerm?: string;
}
export function PackageList({ list, searchTerm = '' }: Props) {
return (
/**
* Suspense wrapper is required by PackageListGrid, but
* Onboarding does not need it because it pre-loads all
* packages beforehand with a custom loading state.
*/
<Suspense fallback={<></>}>
<LazyPackageList
list={list}
searchTerm={searchTerm}
showControls={false}
showSearchTools={false}
// we either don't need these properties (yet) or handle them upstream, but
// they are marked as required in the original API.
selectedCategory=""
setSearchTerm={() => {}}
setCategory={() => {}}
categories={[]}
setUrlandReplaceHistory={() => {}}
setUrlandPushHistory={() => {}}
showCardLabels={true}
/>
</Suspense>
);
}

View file

@ -9,120 +9,80 @@ import type { AvailablePackagesHookType, IntegrationCardItem } from '@kbn/fleet-
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiCallOut, EuiSearchBar, EuiSkeletonText } from '@elastic/eui';
import React, { useRef, Suspense, useEffect } from 'react';
import React, { useRef, useMemo } from 'react';
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
import { PackageList, fetchAvailablePackagesHook } from './lazy';
import { useIntegrationCardList } from './use_integration_card_list';
import { CustomCard } from './types';
import { useCardUrlRewrite } from './use_card_url_rewrite';
import { PackageList } from '../package_list/package_list';
interface Props {
searchQuery: string;
setSearchQuery: (searchQuery: string) => void;
/**
* A subset of either existing card names to feature, or virtual
* cards to display. The inclusion of CustomCards will override the default
* list functionality.
*/
customCards?: CustomCard[];
/**
* Override the default `observability` option.
* A list of custom items to include into the package list
*/
customCards?: IntegrationCardItem[];
selectedCategory?: string[];
showSearchBar?: boolean;
packageListRef?: React.Ref<HTMLDivElement>;
searchQuery?: string;
setSearchQuery?: React.Dispatch<React.SetStateAction<string>>;
flowCategory?: string | null;
flowSearch?: string;
/**
* When enabled, custom and integration cards are joined into a single list.
*/
joinCardLists?: boolean;
excludePackageIdList?: string[];
onLoaded?: () => void;
}
type WrapperProps = Props & {
useAvailablePackages: AvailablePackagesHookType;
};
const Loading = () => <EuiSkeletonText isLoading={true} lines={10} />;
const fetchAvailablePackagesHook = (): Promise<AvailablePackagesHookType> =>
import('@kbn/fleet-plugin/public')
.then((module) => module.AvailablePackagesHook())
.then((hook) => hook.useAvailablePackages);
const Loading = () => <EuiSkeletonText isLoading={true} lines={5} />;
const PackageListGridWrapper = ({
selectedCategory = ['observability', 'os_system'],
useAvailablePackages,
showSearchBar = false,
packageListRef,
searchQuery,
setSearchQuery,
customCards,
flowCategory,
flowSearch,
joinCardLists = false,
excludePackageIdList = [],
onLoaded,
}: WrapperProps) => {
const { filteredCards, isLoading } = useAvailablePackages({
const { filteredCards: integrationCards, isLoading } = useAvailablePackages({
prereleaseIntegrationsEnabled: false,
});
const rewriteUrl = useCardUrlRewrite({ category: flowCategory, search: searchQuery });
const list: IntegrationCardItem[] = useIntegrationCardList(
filteredCards,
selectedCategory,
excludePackageIdList,
customCards,
flowCategory,
flowSearch,
joinCardLists
);
useEffect(() => {
if (!isLoading && onLoaded !== undefined) {
onLoaded();
}
}, [isLoading, onLoaded]);
const list: IntegrationCardItem[] = useMemo(() => {
return (customCards ?? [])
.concat(integrationCards)
.filter((card) =>
card.categories.some((category) => ['observability', 'os_system'].includes(category))
)
.filter((card) => !excludePackageIdList.includes(card.id))
.map(rewriteUrl);
}, [customCards, excludePackageIdList, integrationCards, rewriteUrl]);
if (isLoading) return <Loading />;
const showPackageList = (showSearchBar && !!searchQuery) || showSearchBar === false;
return (
<Suspense fallback={<Loading />}>
<div ref={packageListRef}>
{showSearchBar && (
<EuiSearchBar
box={{
incremental: true,
}}
onChange={({ queryText, error }) => {
if (error) return;
<div ref={packageListRef}>
<EuiSearchBar
box={{
incremental: true,
}}
onChange={({ queryText, error }) => {
if (error) return;
setSearchQuery?.(queryText);
}}
query={searchQuery ?? ''}
/>
)}
{showPackageList && (
<PackageList
list={list}
searchTerm={searchQuery ?? ''}
showControls={false}
showSearchTools={false}
// we either don't need these properties (yet) or handle them upstream, but
// they are marked as required in the original API.
selectedCategory=""
setSearchTerm={() => {}}
setCategory={() => {}}
categories={[]}
setUrlandReplaceHistory={() => {}}
setUrlandPushHistory={() => {}}
showCardLabels={true}
/>
)}
</div>
</Suspense>
setSearchQuery(queryText);
}}
query={searchQuery}
/>
{searchQuery !== '' && <PackageList list={list} searchTerm={searchQuery} />}
</div>
);
};
const WithAvailablePackages = React.forwardRef(
export const PackageListSearchForm = React.forwardRef(
(props: Props, packageListRef?: React.Ref<HTMLDivElement>) => {
const ref = useRef<AvailablePackagesHookType | null>(null);
@ -176,5 +136,3 @@ const WithAvailablePackages = React.forwardRef(
);
}
);
export { WithAvailablePackages as OnboardingFlowPackageList };

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { addPathParamToUrl, toOnboardingPath } from './use_integration_card_list';
import { addPathParamToUrl, toOnboardingPath } from './use_card_url_rewrite';
describe('useIntegratrionCardList', () => {
describe('toOnboardingPath', () => {

View file

@ -0,0 +1,49 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useMemo } from 'react';
import { IntegrationCardItem } from '@kbn/fleet-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
export function toOnboardingPath({
basePath,
category,
search,
}: {
basePath?: string;
category?: string | null;
search?: string;
}): string | null {
if (typeof basePath !== 'string' && !basePath) return null;
const path = `${basePath}/app/observabilityOnboarding`;
if (!category && !search) return path;
const params = new URLSearchParams();
if (category) params.append('category', category);
if (search) params.append('search', search);
return `${path}?${params.toString()}`;
}
export function addPathParamToUrl(url: string, onboardingLink: string) {
const encoded = encodeURIComponent(onboardingLink);
if (url.indexOf('?') >= 0) {
return `${url}&observabilityOnboardingLink=${encoded}`;
}
return `${url}?observabilityOnboardingLink=${encoded}`;
}
export function useCardUrlRewrite(props: { category?: string | null; search?: string }) {
const kibana = useKibana();
const basePath = kibana.services.http?.basePath.get();
const onboardingLink = useMemo(() => toOnboardingPath({ basePath, ...props }), [basePath, props]);
return (card: IntegrationCardItem) => ({
...card,
url:
card.url.indexOf('/app/integrations') >= 0 && onboardingLink
? addPathParamToUrl(card.url, onboardingLink)
: card.url,
});
}

View file

@ -1,20 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { AvailablePackagesHookType } from '@kbn/fleet-plugin/public';
import { lazy } from 'react';
export const PackageList = lazy(async () => ({
default: await import('@kbn/fleet-plugin/public')
.then((module) => module.PackageList())
.then((pkg) => pkg.PackageListGrid),
}));
export const fetchAvailablePackagesHook = (): Promise<AvailablePackagesHookType> =>
import('@kbn/fleet-plugin/public')
.then((module) => module.AvailablePackagesHook())
.then((hook) => hook.useAvailablePackages);

View file

@ -1,19 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { IntegrationCardItem } from '@kbn/fleet-plugin/public';
export type VirtualCard = {
type: 'virtual';
} & IntegrationCardItem;
export interface FeaturedCard {
type: 'featured';
name: string;
}
export type CustomCard = FeaturedCard | VirtualCard;

View file

@ -1,139 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useMemo } from 'react';
import { IntegrationCardItem } from '@kbn/fleet-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { CustomCard } from './types';
export function toOnboardingPath({
basePath,
category,
search,
}: {
basePath?: string;
category?: string | null;
search?: string;
}): string | null {
if (typeof basePath !== 'string' && !basePath) return null;
const path = `${basePath}/app/observabilityOnboarding`;
if (!category && !search) return path;
const params = new URLSearchParams();
if (category) params.append('category', category);
if (search) params.append('search', search);
return `${path}?${params.toString()}`;
}
export function addPathParamToUrl(url: string, onboardingLink: string) {
const encoded = encodeURIComponent(onboardingLink);
if (url.indexOf('?') >= 0) {
return `${url}&observabilityOnboardingLink=${encoded}`;
}
return `${url}?observabilityOnboardingLink=${encoded}`;
}
function useCardUrlRewrite(props: { category?: string | null; search?: string }) {
const kibana = useKibana();
const basePath = kibana.services.http?.basePath.get();
const onboardingLink = useMemo(() => toOnboardingPath({ basePath, ...props }), [basePath, props]);
return (card: IntegrationCardItem) => ({
...card,
url:
card.url.indexOf('/app/integrations') >= 0 && onboardingLink
? addPathParamToUrl(card.url, onboardingLink)
: card.url,
});
}
function extractFeaturedCards(filteredCards: IntegrationCardItem[], featuredCardNames?: string[]) {
const featuredCards: Record<string, IntegrationCardItem | undefined> = {};
filteredCards.forEach((card) => {
if (featuredCardNames?.includes(card.name)) {
featuredCards[card.name] = card;
}
});
return featuredCards;
}
function formatCustomCards(
rewriteUrl: (card: IntegrationCardItem) => IntegrationCardItem,
customCards: CustomCard[],
featuredCards: Record<string, IntegrationCardItem | undefined>
) {
const cards: IntegrationCardItem[] = [];
for (const card of customCards) {
if (card.type === 'featured' && !!featuredCards[card.name]) {
cards.push(rewriteUrl(featuredCards[card.name]!));
} else if (card.type === 'virtual') {
cards.push(rewriteUrl(card));
}
}
return cards;
}
function useFilteredCards(
rewriteUrl: (card: IntegrationCardItem) => IntegrationCardItem,
integrationsList: IntegrationCardItem[],
selectedCategory: string[],
excludePackageIdList: string[],
customCards?: CustomCard[]
) {
return useMemo(() => {
const integrationCards = integrationsList
.filter((card) => !excludePackageIdList.includes(card.id))
.filter((card) => card.categories.some((category) => selectedCategory.includes(category)))
.map(rewriteUrl);
if (!customCards) {
return { featuredCards: {}, integrationCards };
}
return {
featuredCards: extractFeaturedCards(
integrationsList,
customCards.filter((c) => c.type === 'featured').map((c) => c.name)
),
integrationCards,
};
}, [integrationsList, rewriteUrl, customCards, excludePackageIdList, selectedCategory]);
}
/**
* Formats the cards to display on the integration list.
* @param integrationsList the list of cards from the integrations API.
* @param selectedCategory the card category to filter by.
* @param customCards any virtual or featured cards.
* @param fullList when true all integration cards are included.
* @returns the list of cards to display.
*/
export function useIntegrationCardList(
integrationsList: IntegrationCardItem[],
selectedCategory: string[],
excludePackageIdList: string[],
customCards?: CustomCard[],
flowCategory?: string | null,
flowSearch?: string,
fullList = false
): IntegrationCardItem[] {
const rewriteUrl = useCardUrlRewrite({ category: flowCategory, search: flowSearch });
const { featuredCards, integrationCards } = useFilteredCards(
rewriteUrl,
integrationsList,
selectedCategory,
excludePackageIdList,
customCards
);
if (customCards && customCards.length > 0) {
const formattedCustomCards = formatCustomCards(rewriteUrl, customCards, featuredCards);
if (fullList) {
return [...formattedCustomCards, ...integrationCards] as IntegrationCardItem[];
}
return formattedCustomCards;
}
return integrationCards;
}