Skip to content

Commit

Permalink
[Observability Onboarding] Auto-detect flow (elastic#186106)
Browse files Browse the repository at this point in the history
Resolves elastic#183362

## Summary

Implements auto-detect based onboarding flow.
The new flow can only be accessed behind a hidden URL:
`/app/observabilityOnboarding/auto-detect?category=logs`

## Screenshots

### Kibana

<img width="1243" alt="Screenshot 2024-06-12 at 16 05 54"
src="https://github.com/elastic/kibana/assets/190132/ab4d01a4-431b-425e-acc1-80cb7adadb35">

### Terminal

<img width="1188" alt="Screenshot 2024-06-12 at 16 07 32"
src="https://github.com/elastic/kibana/assets/190132/e5924156-d2c5-487d-af2f-163c3d114e93">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
  • Loading branch information
3 people authored Jun 24, 2024
1 parent 8bd7124 commit f015066
Show file tree
Hide file tree
Showing 29 changed files with 1,443 additions and 265 deletions.
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
{
"type": "plugin",
"id": "@kbn/observability-onboarding-plugin",
"owner": ["@elastic/obs-ux-logs-team", "@elastic/obs-ux-onboarding-team"],
"owner": [
"@elastic/obs-ux-logs-team",
"@elastic/obs-ux-onboarding-team"
],
"plugin": {
"id": "observabilityOnboarding",
"server": true,
"browser": true,
"configPath": ["xpack", "observability_onboarding"],
"configPath": [
"xpack",
"observability_onboarding"
],
"requiredPlugins": [
"data",
"observability",
"observabilityShared",
"discover",
"share",
"fleet"
"fleet",
"security"
],
"optionalPlugins": [
"cloud",
"cloudExperiments",
"usageCollection"
],
"requiredBundles": [
"kibanaReact"
],
"optionalPlugins": ["cloud", "cloudExperiments", "usageCollection"],
"requiredBundles": ["kibanaReact"],
"extraPublicDirs": ["common"]
"extraPublicDirs": [
"common"
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ObservabilityOnboardingHeaderActionMenu } from './shared/header_action_
import {
ObservabilityOnboardingPluginSetupDeps,
ObservabilityOnboardingPluginStartDeps,
ObservabilityOnboardingContextValue,
} from '../plugin';
import { ObservabilityOnboardingFlow } from './observability_onboarding_flow';

Expand All @@ -40,14 +41,13 @@ export const breadcrumbsApp = {
export function ObservabilityOnboardingAppRoot({
appMountParameters,
core,
deps,
corePlugins: { observability, data },
corePlugins,
config,
}: {
appMountParameters: AppMountParameters;
} & RenderAppProps) {
const { history, setHeaderActionMenu, theme$ } = appMountParameters;
const plugins = { ...deps };
const services: ObservabilityOnboardingContextValue = { ...core, ...corePlugins, config };

const renderFeedbackLinkAsPortal = !config.serverless.enabled;

Expand All @@ -63,15 +63,7 @@ export function ObservabilityOnboardingAppRoot({
application: core.application,
}}
>
<KibanaContextProvider
services={{
...core,
...plugins,
observability,
data,
config,
}}
>
<KibanaContextProvider services={services}>
<KibanaThemeProvider
theme={{ theme$ }}
modify={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { OnboardingFlowForm } from './onboarding_flow_form/onboarding_flow_form'
import { Header } from './header/header';
import { SystemLogsPanel } from './quickstart_flows/system_logs';
import { CustomLogsPanel } from './quickstart_flows/custom_logs';
import { AutoDetectPanel } from './quickstart_flows/auto_detect';
import { BackButton } from './shared/back_button';

const queryClient = new QueryClient();
Expand Down Expand Up @@ -52,6 +53,10 @@ export function ObservabilityOnboardingFlow() {
</EuiPageTemplate.Section>
<EuiPageTemplate.Section paddingSize="xl" color="subdued" restrictWidth>
<Routes>
<Route path="/auto-detect">
<BackButton />
<AutoDetectPanel />
</Route>
<Route path="/systemLogs">
<BackButton />
<SystemLogsPanel />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
/*
* 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 React, { type FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiPanel,
EuiSteps,
EuiCodeBlock,
EuiSpacer,
EuiSkeletonText,
EuiBadge,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiImage,
EuiSkeletonRectangle,
useGeneratedHtmlId,
} from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
type SingleDatasetLocatorParams,
SINGLE_DATASET_LOCATOR_ID,
} from '@kbn/deeplinks-observability/locators';
import { type DashboardLocatorParams } from '@kbn/dashboard-plugin/public';
import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
import { getAutoDetectCommand } from './get_auto_detect_command';
import { useOnboardingFlow } from './use_onboarding_flow';
import { ProgressIndicator } from '../shared/progress_indicator';
import { AccordionWithIcon } from '../shared/accordion_with_icon';
import { type ObservabilityOnboardingContextValue } from '../../../plugin';
import { EmptyPrompt } from '../shared/empty_prompt';
import { CopyToClipboardButton } from '../shared/copy_to_clipboard_button';
import { LocatorButtonEmpty } from '../shared/locator_button_empty';

export const AutoDetectPanel: FunctionComponent = () => {
const {
services: { http },
} = useKibana<ObservabilityOnboardingContextValue>();
const { status, data, error, refetch, installedIntegrations } = useOnboardingFlow();
const command = data ? getAutoDetectCommand(data) : undefined;
const accordionId = useGeneratedHtmlId({ prefix: 'accordion' });

if (error) {
return <EmptyPrompt error={error} onRetryClick={refetch} />;
}

const registryIntegrations = installedIntegrations.filter(
(integration) => integration.installSource === 'registry'
);
const customIntegrations = installedIntegrations.filter(
(integration) => integration.installSource === 'custom'
);

return (
<EuiPanel hasBorder paddingSize="xl">
<EuiSteps
steps={[
{
title: i18n.translate(
'xpack.observability_onboarding.autoDetectPanel.runTheCommandOnLabel',
{ defaultMessage: 'Run the command on your host' }
),
status: status === 'notStarted' ? 'current' : 'complete',
children: command ? (
<>
<EuiText>
<p>
{i18n.translate(
'xpack.observability_onboarding.autoDetectPanel.p.wellScanYourHostLabel',
{
defaultMessage: "We'll scan your host for logs and metrics, including:",
}
)}
</p>
</EuiText>
<EuiSpacer size="s" />
<EuiFlexGroup gutterSize="s">
{['Apache', 'Docker', 'Nginx', 'System', 'Custom .log files'].map((item) => (
<EuiFlexItem key={item} grow={false}>
<EuiBadge color="hollow">{item}</EuiBadge>
</EuiFlexItem>
))}
</EuiFlexGroup>
<EuiSpacer />
{/* Bash syntax highlighting only highlights a few random numbers (badly) so it looks less messy to go with plain text */}
<EuiCodeBlock paddingSize="m" language="text">
{command}
</EuiCodeBlock>
<EuiSpacer />
<CopyToClipboardButton textToCopy={command} fill={status === 'notStarted'} />
</>
) : (
<EuiSkeletonText lines={6} />
),
},
{
title: i18n.translate(
'xpack.observability_onboarding.autoDetectPanel.visualizeYourDataLabel',
{ defaultMessage: 'Visualize your data' }
),
status:
status === 'dataReceived'
? 'complete'
: status === 'awaitingData' || status === 'inProgress'
? 'current'
: 'incomplete',
children: (
<>
{status === 'dataReceived' ? (
<ProgressIndicator
iconType="cheer"
title={i18n.translate(
'xpack.observability_onboarding.autoDetectPanel.yourDataIsReadyToExploreLabel',
{ defaultMessage: 'Your data is ready to explore!' }
)}
isLoading={false}
/>
) : status === 'awaitingData' ? (
<ProgressIndicator
title={i18n.translate(
'xpack.observability_onboarding.autoDetectPanel.installingElasticAgentFlexItemLabel',
{ defaultMessage: 'Waiting for data to arrive...' }
)}
/>
) : status === 'inProgress' ? (
<ProgressIndicator
title={i18n.translate(
'xpack.observability_onboarding.autoDetectPanel.lookingForLogFilesFlexItemLabel',
{ defaultMessage: 'Waiting for installation to complete...' }
)}
/>
) : null}
{(status === 'awaitingData' || status === 'dataReceived') &&
installedIntegrations.length > 0 ? (
<>
<EuiSpacer />
{registryIntegrations.map((integration) => (
<AccordionWithIcon
key={integration.pkgName}
id={`${accordionId}_${integration.pkgName}`}
iconType="desktop"
title={i18n.translate(
'xpack.observability_onboarding.autoDetectPanel.h3.getStartedWithNginxLabel',
{
defaultMessage: 'Get started with {title} logs',
values: { title: integration.title },
}
)}
isDisabled={status !== 'dataReceived'}
initialIsOpen
>
<EuiFlexGroup responsive={false}>
<EuiFlexItem grow={false}>
{status === 'dataReceived' ? (
<EuiImage
src={http.staticAssets.getPluginAssetHref('charts_screen.svg')}
width={162}
height={117}
alt=""
hasShadow
/>
) : (
<EuiSkeletonRectangle width={162} height={117} />
)}
</EuiFlexItem>
<EuiFlexItem>
<ul>
{integration.kibanaAssets
.filter((asset) => asset.type === 'dashboard')
.map((dashboard) => (
<li key={dashboard.id}>
<LocatorButtonEmpty<DashboardLocatorParams>
locator={DASHBOARD_APP_LOCATOR}
params={{ dashboardId: dashboard.id }}
target="_blank"
iconType="dashboardApp"
isDisabled={status !== 'dataReceived'}
flush="left"
size="s"
>
{dashboard.attributes.title}
</LocatorButtonEmpty>
</li>
))}
</ul>
</EuiFlexItem>
</EuiFlexGroup>
</AccordionWithIcon>
))}
{customIntegrations.length > 0 && (
<AccordionWithIcon
id={`${accordionId}_custom`}
iconType="documents"
title={i18n.translate(
'xpack.observability_onboarding.autoDetectPanel.h3.getStartedWithlogLabel',
{ defaultMessage: 'Get started with custom .log files' }
)}
isDisabled={status !== 'dataReceived'}
initialIsOpen
>
<ul>
{customIntegrations.map((integration) =>
integration.dataStreams.map((datastream) => (
<li key={`${integration.pkgName}/${datastream.dataset}`}>
<LocatorButtonEmpty<SingleDatasetLocatorParams>
locator={SINGLE_DATASET_LOCATOR_ID}
params={{
integration: integration.pkgName,
dataset: datastream.dataset,
}}
target="_blank"
iconType="document"
isDisabled={status !== 'dataReceived'}
flush="left"
size="s"
>
{integration.pkgName}
</LocatorButtonEmpty>
</li>
))
)}
</ul>
</AccordionWithIcon>
)}
</>
) : null}
</>
),
},
]}
/>
</EuiPanel>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 { flatten, zip } from 'lodash';
import { useOnboardingFlow } from './use_onboarding_flow';

export function getAutoDetectCommand(
options: NonNullable<ReturnType<typeof useOnboardingFlow>['data']>
) {
const scriptName = 'auto_detect.sh';
return oneLine`
curl ${options.scriptDownloadUrl} -so ${scriptName} &&
sudo bash ${scriptName}
--id=${options.onboardingFlow.id}
--kibana-url=${options.kibanaUrl}
--install-key=${options.installApiKey}
--ingest-key=${options.ingestApiKey}
--ea-version=${options.elasticAgentVersion}
`;
}
function oneLine(parts: TemplateStringsArray, ...args: string[]) {
const str = flatten(zip(parts, args)).join('');
return str.replace(/\s+/g, ' ').trim();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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 { InstallIntegrationsStepPayload } from '../../../../server/routes/types';
import type { ObservabilityOnboardingFlow } from '../../../../server/saved_objects/observability_onboarding_status';
import type { InstalledIntegration } from '../../../../server/routes/types';

export function getInstalledIntegrations(
data: Pick<ObservabilityOnboardingFlow, 'progress'> | undefined
): InstalledIntegration[] {
return (data?.progress['install-integrations']?.payload as InstallIntegrationsStepPayload) ?? [];
}
Loading

0 comments on commit f015066

Please sign in to comment.