-
Notifications
You must be signed in to change notification settings - Fork 8.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Observability Onboarding] Auto-detect flow #186106
Changes from 21 commits
63e1321
ee37a72
d30158c
6524770
3c36774
4fb7baf
22fa187
4bd2fc5
09a92e9
37dff1a
b15fbdc
cc18fed
1703e14
6c76c59
3c58df5
884fda5
0d7b6ca
d48d2f8
6537d89
6fdafa7
60b21ff
a085986
17c73f7
8464513
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
@@ -0,0 +1,51 @@ | ||
/* | ||
* 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 { | ||
EuiAccordion, | ||
EuiIcon, | ||
EuiTitle, | ||
EuiFlexGroup, | ||
EuiFlexItem, | ||
type EuiAccordionProps, | ||
} from '@elastic/eui'; | ||
|
||
interface AccordionWithIconProps | ||
extends Omit<EuiAccordionProps, 'buttonContent' | 'buttonProps' | 'borders' | 'paddingSize'> { | ||
title: string; | ||
iconType: string; | ||
} | ||
export const AccordionWithIcon: FunctionComponent<AccordionWithIconProps> = ({ | ||
title, | ||
iconType, | ||
children, | ||
...rest | ||
}) => { | ||
return ( | ||
<EuiAccordion | ||
{...rest} | ||
buttonContent={ | ||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false} css={{ marginLeft: 8 }}> | ||
<EuiFlexItem grow={false}> | ||
<EuiIcon type={iconType} size="l" /> | ||
</EuiFlexItem> | ||
<EuiFlexItem> | ||
<EuiTitle size="xs" css={rest.isDisabled ? { color: 'inherit' } : undefined}> | ||
<h3>{title}</h3> | ||
</EuiTitle> | ||
</EuiFlexItem> | ||
</EuiFlexGroup> | ||
} | ||
buttonProps={{ paddingSize: 'l' }} | ||
borders="horizontal" | ||
paddingSize="none" | ||
> | ||
<div css={{ paddingLeft: 36, paddingBottom: 24 }}>{children}</div> | ||
</EuiAccordion> | ||
); | ||
}; |
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 './progress_indicator'; | ||
import { AccordionWithIcon } from './accordion_with_icon'; | ||
import { type ObservabilityOnboardingContextValue } from '../../../plugin'; | ||
import { EmptyPrompt } from './empty_prompt'; | ||
import { CopyToClipboardButton } from './copy_to_clipboard_button'; | ||
import { LocatorButtonEmpty } from './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 {pkgName} logs', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we use the title of the integration instead of the technical name? |
||
values: { pkgName: integration.pkgName }, | ||
} | ||
)} | ||
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) => ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeh, we need a way of establishing which dashboard is the main one we want to present. Usually there's an "overview" dashboard that can be considered the best entry point so we might be able to do a naive pattern matching. Ideally there would also be a way of determining whether a dashboard is relevant in the first place (e.g. whether it contains any data) but I'm not sure this can be easily done currently. I think this panel needs another design iteration where we look at what information is returned from fleet for each of the supported integrations and then how we want to present that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense, could you prepare that discussion for our next sync? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree we should have some logic on which dashboard should be the one we want to show and group the rest on the link to see all of them. As mentioned, usually there is an Overview. In this case we'll need to evaluate host vs system metrics overview dashboards. |
||
<li key={dashboard.id}> | ||
<LocatorButtonEmpty<DashboardLocatorParams> | ||
locator={DASHBOARD_APP_LOCATOR} | ||
params={{ dashboardId: dashboard.id }} | ||
target="_blank" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's great opening in a new tab! However, when using it I didn't trust it because there's no indication this will happen, should we append a suitable icon to the end? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adds some visual noise as well though, so not 100% sure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately, buttons can only have one icon. It think it would be too confusing otherwise. We could change the icon to an external link icon on the right hand side but then we would have to remove the current icons There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's discuss with @sileschristian once he's back - no strong opinion on it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This makes sense. Let's do this. Dashboard icon is not bringing that much of value. Also, we are missing a copy that we usually we had before the link that gives better context. |
||
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> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: better to have
isDisabled
as a de-structured prop as you use it directlyThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But then I would also have to manually pass it into accordion. Leaving it inside
rest
means it will get spread with the rest of the props that are just pass through props from theEuiAccordion
.