mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Observability Onboarding] Export package list from fleet (#179301)
## Summary
Resolves https://github.com/elastic/kibana/issues/178945.
Resolves #178947.
Resolves https://github.com/elastic/kibana/issues/178952.
~Undergoing the work to export package list in a sharable way while
minimally impacting the existing implementation used by the Fleet team.~
### Update
Updated to provide the flows for all three paths of the form. Synthetics
and APM still link to their respective apps. Logs icons link to the
embedded flows.
#### Things to test
- Check that the integration cards behave in a manner you'd expect.
There's a lot of custom card creation happening and it's possible I have
made a mistake in some linking.
- Icons: there are a few icons that don't correspond to the design. I
wasn't able to find the correct `src` for the colored-in APM logo, for
example, and I'm not sure where the one logs icon lives. I've used the
`logoFilebeat` icon in its place as it's quite similar and is heavily
used in the integrations list.
- For the searchable integrations list, I was unsure if I should re-hide
it when the user changes their radio selection in the top-level
question. Right now it'll just stay as-is with their original query.
- The `Collections` feature, make sure collection buttons properly apply
the search query.
~This now works with the exported package list grid from Fleet. I've
introduced a real package list in place of the dummy grid that we had
before. I also added a searchable grid below.~
~Surely there are some things to iron out still. I also still consider
the code rough, so if you should choose to review it you will likely
find areas you want to see improved.~

---
**Note:** I will likely extract the Fleet-specific pieces of this to a
separate patch that we can merge to Kibana `main` to make it easier for
the Fleet team to review the changes. We can then merge those upstream
and pull them into our feature branch.
^^ this is still true, before we merged this I'll likely pull out the
Fleet-specific modifications we need and have the Fleet team review them
separately. We can still merge this to the feature branch in the
meantime and resolve it later.
---------
Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Thom Heymann <190132+thomheymann@users.noreply.github.com>
This commit is contained in:
parent
01e2379929
commit
a73de7319c
9 changed files with 590 additions and 57 deletions
|
@ -6,21 +6,21 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiPageTemplate,
|
||||
EuiSpacer,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Route, Routes } from '@kbn/shared-ux-router';
|
||||
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
|
||||
import { useNavigate, useLocation } from 'react-router-dom-v5-compat';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPageTemplate,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import backgroundImageUrl from './header/background.svg';
|
||||
import { Footer } from './footer/footer';
|
||||
import { OnboardingFlowForm } from './onboarding_flow_form/onboarding_flow_form';
|
||||
|
@ -28,12 +28,14 @@ import { Header } from './header/header';
|
|||
import { SystemLogsPanel } from './quickstart_flows/system_logs';
|
||||
import { CustomLogsPanel } from './quickstart_flows/custom_logs';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export function ExperimentalOnboardingFlow() {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{/* Test buttons to be removed once integrations selector has been implemented */}
|
||||
<EuiPageTemplate.Section grow={false} color="accent" restrictWidth>
|
||||
<EuiFlexGroup>
|
||||
|
@ -104,7 +106,7 @@ export function ExperimentalOnboardingFlow() {
|
|||
<Footer />
|
||||
<EuiSpacer size="xl" />
|
||||
</EuiPageTemplate.Section>
|
||||
</>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,27 +6,29 @@
|
|||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { FunctionComponent } from 'react';
|
||||
import {
|
||||
EuiAvatar,
|
||||
EuiCheckableCard,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiFlexGrid,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiCard,
|
||||
EuiIcon,
|
||||
EuiAvatar,
|
||||
useEuiTheme,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { OnboardingFlowPackageList } from '../packages_list';
|
||||
import { useCustomMargin } from '../shared/use_custom_margin';
|
||||
import { Category } from './types';
|
||||
import { useCustomCardsForCategory } from './use_custom_cards_for_category';
|
||||
|
||||
interface UseCaseOption {
|
||||
id: string;
|
||||
id: Category;
|
||||
label: string;
|
||||
description: React.ReactNode;
|
||||
}
|
||||
|
@ -77,11 +79,31 @@ export const OnboardingFlowForm: FunctionComponent = () => {
|
|||
},
|
||||
];
|
||||
|
||||
const customMargin = useCustomMargin();
|
||||
const radioGroupId = useGeneratedHtmlId({ prefix: 'onboardingCategory' });
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const packageListSearchBarRef = React.useRef<null | HTMLInputElement>(null);
|
||||
const [integrationSearch, setIntegrationSearch] = useState('');
|
||||
|
||||
const createCollectionCardHandler = useCallback(
|
||||
(query: string) => () => {
|
||||
setIntegrationSearch(query);
|
||||
if (packageListSearchBarRef.current) {
|
||||
packageListSearchBarRef.current.focus();
|
||||
packageListSearchBarRef.current.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
block: 'center',
|
||||
});
|
||||
}
|
||||
},
|
||||
[setIntegrationSearch]
|
||||
);
|
||||
|
||||
const customCards = useCustomCardsForCategory(
|
||||
createCollectionCardHandler,
|
||||
searchParams.get('category') as Category | null
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder>
|
||||
|
@ -96,12 +118,8 @@ export const OnboardingFlowForm: FunctionComponent = () => {
|
|||
)}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup
|
||||
css={{ margin: `calc(${euiTheme.size.xxl} / 2)` }}
|
||||
gutterSize="m"
|
||||
direction="column"
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<EuiFlexGroup css={customMargin} gutterSize="m" direction="column">
|
||||
{options.map((option) => (
|
||||
<EuiFlexItem key={option.id}>
|
||||
<EuiCheckableCard
|
||||
id={`${radioGroupId}_${option.id}`}
|
||||
|
@ -137,32 +155,22 @@ export const OnboardingFlowForm: FunctionComponent = () => {
|
|||
/>
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
{/* Mock integrations grid */}
|
||||
<EuiFlexGrid columns={3} css={{ margin: 20 }}>
|
||||
{new Array(6).fill(null).map((_, index) => (
|
||||
<EuiCard
|
||||
key={index}
|
||||
layout="horizontal"
|
||||
title={searchParams.get('category')!}
|
||||
titleSize="xs"
|
||||
description={searchParams.get('category')!}
|
||||
icon={<EuiIcon type="logoObservability" size="l" />}
|
||||
betaBadgeProps={
|
||||
index === 0
|
||||
? {
|
||||
label: 'Quick Start',
|
||||
color: 'accent',
|
||||
size: 's',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
hasBorder
|
||||
css={{
|
||||
borderColor: index === 0 ? '#ba3d76' : undefined,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</EuiFlexGrid>
|
||||
{Array.isArray(customCards) && (
|
||||
<OnboardingFlowPackageList customCards={customCards} />
|
||||
)}
|
||||
|
||||
<EuiText css={customMargin} size="s" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.observability_onboarding.experimentalOnboardingFlow.form.searchPromptText"
|
||||
defaultMessage="Not seeing yours? Search through our 130 ways of ingesting data:"
|
||||
/>
|
||||
</EuiText>
|
||||
<OnboardingFlowPackageList
|
||||
showSearchBar={true}
|
||||
searchQuery={integrationSearch}
|
||||
setSearchQuery={setIntegrationSearch}
|
||||
ref={packageListSearchBarRef}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</EuiPanel>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type Category = 'apm' | 'infra' | 'logs';
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* 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 {
|
||||
reactRouterNavigate,
|
||||
useKibana,
|
||||
} from '@kbn/kibana-react-plugin/public';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
import { CustomCard, FeaturedCard } from '../packages_list/types';
|
||||
import { Category } from './types';
|
||||
|
||||
function toFeaturedCard(name: string): FeaturedCard {
|
||||
return { type: 'featured', name };
|
||||
}
|
||||
|
||||
export function useCustomCardsForCategory(
|
||||
createCollectionCardHandler: (query: string) => () => void,
|
||||
category: Category | null
|
||||
): CustomCard[] | undefined {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const getUrlForApp = useKibana()?.services.application?.getUrlForApp;
|
||||
|
||||
const { href: systemLogsUrl } = reactRouterNavigate(
|
||||
history,
|
||||
`/systemLogs/${location.search}`
|
||||
);
|
||||
const { href: customLogsUrl } = reactRouterNavigate(
|
||||
history,
|
||||
`/customLogs/${location.search}`
|
||||
);
|
||||
|
||||
switch (category) {
|
||||
case 'apm':
|
||||
return [
|
||||
{
|
||||
id: 'apm-generated',
|
||||
type: 'generated',
|
||||
title: 'Elastic APM',
|
||||
description:
|
||||
'Collect distributed traces from your applications with Elastic APM',
|
||||
name: 'apm',
|
||||
categories: ['observability'],
|
||||
icons: [
|
||||
{
|
||||
type: 'eui',
|
||||
src: 'apmApp',
|
||||
},
|
||||
],
|
||||
url: getUrlForApp?.('apm') ?? '',
|
||||
version: '',
|
||||
integration: '',
|
||||
},
|
||||
{
|
||||
id: 'synthetics-generated',
|
||||
type: 'generated',
|
||||
title: 'Synthetic monitor',
|
||||
description: 'Monitor endpoints, pages, and user journeys',
|
||||
name: 'synthetics',
|
||||
categories: ['observability'],
|
||||
icons: [
|
||||
{
|
||||
type: 'eui',
|
||||
src: 'logoUptime',
|
||||
},
|
||||
],
|
||||
url: getUrlForApp?.('synthetics') ?? '',
|
||||
version: '',
|
||||
integration: '',
|
||||
},
|
||||
];
|
||||
case 'infra':
|
||||
return [
|
||||
toFeaturedCard('kubernetes'),
|
||||
toFeaturedCard('prometheus'),
|
||||
toFeaturedCard('docker'),
|
||||
{
|
||||
id: 'azure-generated',
|
||||
type: 'generated',
|
||||
title: 'Azure',
|
||||
description: 'Collect logs and metrics from Microsoft Azure',
|
||||
name: 'azure',
|
||||
categories: ['observability'],
|
||||
icons: [],
|
||||
url: 'https://azure.com',
|
||||
version: '',
|
||||
integration: '',
|
||||
isCollectionCard: true,
|
||||
onCardClick: createCollectionCardHandler('azure'),
|
||||
},
|
||||
{
|
||||
id: 'aws-generated',
|
||||
type: 'generated',
|
||||
title: 'AWS',
|
||||
description:
|
||||
'Collect logs and metrics from Amazon Web Services (AWS)',
|
||||
name: 'aws',
|
||||
categories: ['observability'],
|
||||
icons: [],
|
||||
url: 'https://aws.com',
|
||||
version: '',
|
||||
integration: '',
|
||||
isCollectionCard: true,
|
||||
onCardClick: createCollectionCardHandler('aws'),
|
||||
},
|
||||
{
|
||||
id: 'gcp-generated',
|
||||
type: 'generated',
|
||||
title: 'Google Cloud Platform',
|
||||
description: 'Collect logs and metrics from Google Cloud Platform',
|
||||
name: 'gcp',
|
||||
categories: ['observability'],
|
||||
icons: [],
|
||||
url: '',
|
||||
version: '',
|
||||
integration: '',
|
||||
isCollectionCard: true,
|
||||
onCardClick: createCollectionCardHandler('gcp'),
|
||||
},
|
||||
];
|
||||
case 'logs':
|
||||
return [
|
||||
{
|
||||
id: 'system-logs',
|
||||
type: 'generated',
|
||||
title: 'Stream host system logs',
|
||||
description:
|
||||
'The quickest path to onboard log data from your own machine or server',
|
||||
name: 'system-logs-generated',
|
||||
categories: ['observability'],
|
||||
icons: [
|
||||
{
|
||||
type: 'svg',
|
||||
src: '/XXXXXXXXXXXX/plugins/home/assets/logos/system.svg',
|
||||
},
|
||||
],
|
||||
url: systemLogsUrl,
|
||||
version: '',
|
||||
integration: '',
|
||||
},
|
||||
{
|
||||
id: 'logs-logs',
|
||||
type: 'generated',
|
||||
title: 'Stream log files',
|
||||
description:
|
||||
'Stream any logs into Elastic in a simple way and explore their data',
|
||||
name: 'logs-logs-generated',
|
||||
categories: ['observability'],
|
||||
icons: [
|
||||
{
|
||||
type: 'eui',
|
||||
src: 'filebeatApp',
|
||||
},
|
||||
],
|
||||
url: customLogsUrl,
|
||||
version: '',
|
||||
integration: '',
|
||||
},
|
||||
toFeaturedCard('nginx'),
|
||||
{
|
||||
id: 'azure-logs-generated',
|
||||
type: 'generated',
|
||||
title: 'Azure',
|
||||
description: 'Collect logs from Microsoft Azure',
|
||||
name: 'azure',
|
||||
categories: ['observability'],
|
||||
icons: [],
|
||||
url: 'https://azure.com',
|
||||
version: '',
|
||||
integration: '',
|
||||
isCollectionCard: true,
|
||||
onCardClick: createCollectionCardHandler('azure'),
|
||||
},
|
||||
{
|
||||
id: 'aws-logs-generated',
|
||||
type: 'generated',
|
||||
title: 'AWS',
|
||||
description: '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-generated',
|
||||
type: 'generated',
|
||||
title: 'Google Cloud Platform',
|
||||
description: 'Collect logs from Google Cloud Platform',
|
||||
name: 'gcp',
|
||||
categories: ['observability'],
|
||||
icons: [],
|
||||
url: '',
|
||||
version: '',
|
||||
integration: '',
|
||||
isCollectionCard: true,
|
||||
onCardClick: createCollectionCardHandler('gcp'),
|
||||
},
|
||||
];
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* 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,
|
||||
IntegrationCardItem,
|
||||
} from '@kbn/fleet-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiSearchBar,
|
||||
EuiSkeletonText,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { useRef, Suspense, useState } from 'react';
|
||||
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
|
||||
import { PackageList, fetchAvailablePackagesHook } from './lazy';
|
||||
import { useIntegrationCardList } from './use_integration_card_list';
|
||||
import { useCustomMargin } from '../shared/use_custom_margin';
|
||||
import { CustomCard } from './types';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* A subset of either existing card names to feature, or generated
|
||||
* cards to display. The inclusion of CustomCards will override the default
|
||||
* list functionality.
|
||||
*/
|
||||
customCards?: CustomCard[];
|
||||
/**
|
||||
* Override the default `observability` option.
|
||||
*/
|
||||
selectedCategory?: string;
|
||||
showSearchBar?: boolean;
|
||||
searchBarRef?: React.Ref<HTMLInputElement>;
|
||||
searchQuery?: string;
|
||||
setSearchQuery?: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
type WrapperProps = Props & {
|
||||
useAvailablePackages: AvailablePackagesHookType;
|
||||
};
|
||||
|
||||
const Loading = () => <EuiSkeletonText isLoading={true} lines={10} />;
|
||||
|
||||
const PackageListGridWrapper = ({
|
||||
selectedCategory = 'observability',
|
||||
useAvailablePackages,
|
||||
showSearchBar = false,
|
||||
searchBarRef,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
customCards,
|
||||
}: WrapperProps) => {
|
||||
const [isInitialHidden, setIsInitialHidden] = useState(showSearchBar);
|
||||
const customMargin = useCustomMargin();
|
||||
const { filteredCards, isLoading } = useAvailablePackages({
|
||||
prereleaseIntegrationsEnabled: false,
|
||||
});
|
||||
|
||||
const list: IntegrationCardItem[] = useIntegrationCardList(
|
||||
filteredCards,
|
||||
selectedCategory,
|
||||
customCards
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isInitialHidden && searchQuery) {
|
||||
setIsInitialHidden(false);
|
||||
}
|
||||
}, [searchQuery, isInitialHidden]);
|
||||
|
||||
if (!isInitialHidden && isLoading) return <Loading />;
|
||||
|
||||
const showPackageList =
|
||||
(showSearchBar && !isInitialHidden) || showSearchBar === false;
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<div css={customMargin}>
|
||||
{showSearchBar && (
|
||||
<div
|
||||
css={css`
|
||||
max-width: 600px;
|
||||
`}
|
||||
>
|
||||
<EuiSearchBar
|
||||
box={{
|
||||
incremental: true,
|
||||
inputRef: (ref: any) => {
|
||||
(
|
||||
searchBarRef as React.MutableRefObject<HTMLInputElement>
|
||||
).current = ref;
|
||||
},
|
||||
}}
|
||||
onChange={(arg) => {
|
||||
if (setSearchQuery) {
|
||||
setSearchQuery(arg.queryText);
|
||||
}
|
||||
setIsInitialHidden(false);
|
||||
}}
|
||||
query={searchQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{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={selectedCategory}
|
||||
setSearchTerm={() => {}}
|
||||
setCategory={() => {}}
|
||||
categories={[]}
|
||||
setUrlandReplaceHistory={() => {}}
|
||||
setUrlandPushHistory={() => {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
const WithAvailablePackages = React.forwardRef(
|
||||
(props: Props, searchBarRef?: React.Ref<HTMLInputElement>) => {
|
||||
const ref = useRef<AvailablePackagesHookType | null>(null);
|
||||
|
||||
const {
|
||||
error: errorLoading,
|
||||
retry: retryAsyncLoad,
|
||||
loading: asyncLoading,
|
||||
} = useAsyncRetry(async () => {
|
||||
ref.current = await fetchAvailablePackagesHook();
|
||||
});
|
||||
|
||||
if (errorLoading)
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.observability_onboarding.asyncLoadFailureCallout.title',
|
||||
{
|
||||
defaultMessage: 'Loading failure',
|
||||
}
|
||||
)}
|
||||
color="warning"
|
||||
iconType="cross"
|
||||
size="m"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.observability_onboarding.asyncLoadFailureCallout.copy"
|
||||
defaultMessage="Some required elements failed to load."
|
||||
/>
|
||||
</p>
|
||||
<EuiButton
|
||||
color="warning"
|
||||
data-test-subj="xpack.observability_onboarding.asyncLoadFailureCallout.button"
|
||||
onClick={() => {
|
||||
if (!asyncLoading) retryAsyncLoad();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.observability_onboarding.asyncLoadFailureCallout.buttonContent"
|
||||
defaultMessage="Retry"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
);
|
||||
|
||||
if (asyncLoading || ref.current === null) return <Loading />;
|
||||
|
||||
return (
|
||||
<PackageListGridWrapper
|
||||
{...props}
|
||||
useAvailablePackages={ref.current}
|
||||
searchBarRef={searchBarRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export { WithAvailablePackages as OnboardingFlowPackageList };
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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);
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 GeneratedCard = {
|
||||
type: 'generated';
|
||||
} & IntegrationCardItem;
|
||||
|
||||
export interface FeaturedCard {
|
||||
type: 'featured';
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type CustomCard = FeaturedCard | GeneratedCard;
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { CustomCard } from './types';
|
||||
|
||||
const QUICKSTART_FLOWS = ['kubernetes', 'nginx', 'system-logs-generated'];
|
||||
|
||||
const toCustomCard = (card: IntegrationCardItem) => ({
|
||||
...card,
|
||||
isQuickstart: QUICKSTART_FLOWS.includes(card.name),
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function useIntegrationCardList(
|
||||
filteredCards: IntegrationCardItem[],
|
||||
selectedCategory = 'observability',
|
||||
customCards?: CustomCard[]
|
||||
): IntegrationCardItem[] {
|
||||
const featuredCards = useMemo(() => {
|
||||
if (!customCards) return {};
|
||||
return extractFeaturedCards(
|
||||
filteredCards,
|
||||
customCards.filter((c) => c.type === 'featured').map((c) => c.name)
|
||||
);
|
||||
}, [filteredCards, customCards]);
|
||||
|
||||
if (customCards && customCards.length > 0) {
|
||||
return customCards
|
||||
.map((c) => {
|
||||
if (c.type === 'featured') {
|
||||
return !!featuredCards[c.name]
|
||||
? toCustomCard(featuredCards[c.name]!)
|
||||
: null;
|
||||
}
|
||||
return toCustomCard(c);
|
||||
})
|
||||
.filter((c) => c) as IntegrationCardItem[];
|
||||
}
|
||||
return filteredCards
|
||||
.filter((card) => card.categories.includes(selectedCategory))
|
||||
.map(toCustomCard);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { useEuiTheme } from '@elastic/eui';
|
||||
|
||||
export function useCustomMargin() {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return { margin: `calc(${euiTheme.size.xxl} / 2)` };
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue