diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ac389064e2986..96dea6833c4b3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -479,6 +479,7 @@ x-pack/plugins/notifications @elastic/appex-sharedux packages/kbn-object-versioning @elastic/appex-sharedux x-pack/packages/observability/alert_details @elastic/actionable-observability x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops +x-pack/plugins/observability_onboarding @elastic/apm-ui x-pack/plugins/observability @elastic/actionable-observability x-pack/plugins/observability_shared @elastic/actionable-observability x-pack/test/security_api_integration/plugins/oidc_provider @elastic/kibana-security diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 1755f65193276..6b534f45b4f2d 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -641,6 +641,10 @@ Elastic. |This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI. +|{kib-repo}blob/{branch}/x-pack/plugins/observability_onboarding/README.md[observabilityOnboarding] +|This plugin provides an onboarding framework for observability solutions: Logs and APM. + + |{kib-repo}blob/{branch}/x-pack/plugins/observability_shared/README.md[observabilityShared] |A plugin that contains components and utilities shared by all Observability plugins. diff --git a/package.json b/package.json index 06bec180ca93b..3881ce124ea6a 100644 --- a/package.json +++ b/package.json @@ -494,6 +494,7 @@ "@kbn/object-versioning": "link:packages/kbn-object-versioning", "@kbn/observability-alert-details": "link:x-pack/packages/observability/alert_details", "@kbn/observability-fixtures-plugin": "link:x-pack/test/cases_api_integration/common/plugins/observability", + "@kbn/observability-onboarding-plugin": "link:x-pack/plugins/observability_onboarding", "@kbn/observability-plugin": "link:x-pack/plugins/observability", "@kbn/observability-shared-plugin": "link:x-pack/plugins/observability_shared", "@kbn/oidc-provider-plugin": "link:x-pack/test/security_api_integration/plugins/oidc_provider", diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 93d5507c53a1e..8b3a5b5356bc5 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -94,6 +94,7 @@ pageLoadAssetSize: navigation: 37269 newsfeed: 42228 observability: 95000 + observabilityOnboarding: 19573 observabilityShared: 21266 osquery: 107090 painlessLab: 179748 diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index ad66d01098665..7d624e7d1c94f 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -236,6 +236,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.observability.unsafe.alertDetails.metrics.enabled (boolean)', 'xpack.observability.unsafe.alertDetails.logs.enabled (boolean)', 'xpack.observability.unsafe.alertDetails.uptime.enabled (boolean)', + 'xpack.observability_onboarding.ui.enabled (boolean)', ]; // We don't assert that actualExposedConfigKeys and expectedExposedConfigKeys are equal, because test failure messages with large // arrays are hard to grok. Instead, we take the difference between the two arrays and assert them separately, that way it's diff --git a/tsconfig.base.json b/tsconfig.base.json index 929f3e791ad1e..e47345e2844c4 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -952,6 +952,8 @@ "@kbn/observability-alert-details/*": ["x-pack/packages/observability/alert_details/*"], "@kbn/observability-fixtures-plugin": ["x-pack/test/cases_api_integration/common/plugins/observability"], "@kbn/observability-fixtures-plugin/*": ["x-pack/test/cases_api_integration/common/plugins/observability/*"], + "@kbn/observability-onboarding-plugin": ["x-pack/plugins/observability_onboarding"], + "@kbn/observability-onboarding-plugin/*": ["x-pack/plugins/observability_onboarding/*"], "@kbn/observability-plugin": ["x-pack/plugins/observability"], "@kbn/observability-plugin/*": ["x-pack/plugins/observability/*"], "@kbn/observability-shared-plugin": ["x-pack/plugins/observability_shared"], diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index ff4718c2c7f3a..8071085e9cc8b 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -50,6 +50,7 @@ "xpack.monitoring": ["plugins/monitoring"], "xpack.observability": "plugins/observability", "xpack.observabilityShared": "plugins/observability_shared", + "xpack.observability_onboarding": "plugins/observability_onboarding", "xpack.osquery": ["plugins/osquery"], "xpack.painlessLab": "plugins/painless_lab", "xpack.profiling": ["plugins/profiling"], diff --git a/x-pack/plugins/observability_onboarding/.prettierrc b/x-pack/plugins/observability_onboarding/.prettierrc new file mode 100644 index 0000000000000..650cb880f6f5a --- /dev/null +++ b/x-pack/plugins/observability_onboarding/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "semi": true +} diff --git a/x-pack/plugins/observability_onboarding/README.md b/x-pack/plugins/observability_onboarding/README.md new file mode 100644 index 0000000000000..f416ae6748dcc --- /dev/null +++ b/x-pack/plugins/observability_onboarding/README.md @@ -0,0 +1,3 @@ +# Observability onboarding plugin + +This plugin provides an onboarding framework for observability solutions: Logs and APM. diff --git a/x-pack/plugins/observability_onboarding/common/fetch_options.ts b/x-pack/plugins/observability_onboarding/common/fetch_options.ts new file mode 100644 index 0000000000000..3a72a72762dee --- /dev/null +++ b/x-pack/plugins/observability_onboarding/common/fetch_options.ts @@ -0,0 +1,14 @@ +/* + * 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 { HttpFetchOptions } from '@kbn/core/public'; + +export type FetchOptions = Omit & { + pathname: string; + method?: string; + body?: any; +}; diff --git a/x-pack/plugins/observability_onboarding/common/index.ts b/x-pack/plugins/observability_onboarding/common/index.ts new file mode 100644 index 0000000000000..6c03dd09627aa --- /dev/null +++ b/x-pack/plugins/observability_onboarding/common/index.ts @@ -0,0 +1,9 @@ +/* + * 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 const PLUGIN_ID = 'observabilityOnboarding'; +export const PLUGIN_NAME = 'observabilityOnboarding'; diff --git a/x-pack/plugins/observability_onboarding/jest.config.js b/x-pack/plugins/observability_onboarding/jest.config.js new file mode 100644 index 0000000000000..66a2f768ca0ff --- /dev/null +++ b/x-pack/plugins/observability_onboarding/jest.config.js @@ -0,0 +1,14 @@ +/* + * 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. + */ + +const path = require('path'); + +module.exports = { + preset: '@kbn/test', + rootDir: path.resolve(__dirname, '../../..'), + roots: ['/x-pack/plugins/observability_onboarding'], +}; diff --git a/x-pack/plugins/observability_onboarding/kibana.jsonc b/x-pack/plugins/observability_onboarding/kibana.jsonc new file mode 100644 index 0000000000000..673bc0f78b120 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/kibana.jsonc @@ -0,0 +1,23 @@ +{ + "type": "plugin", + "id": "@kbn/observability-onboarding-plugin", + "owner": "@elastic/apm-ui", + "plugin": { + "id": "observabilityOnboarding", + "server": true, + "browser": true, + "configPath": ["xpack", "observability_onboarding"], + "requiredPlugins": [ + "data", + "observability", + ], + "optionalPlugins": [ + "cloud", + "usageCollection", + ], + "requiredBundles": [ + "kibanaReact" + ], + "extraPublicDirs": ["common"] + } +} diff --git a/x-pack/plugins/observability_onboarding/public/application/app.tsx b/x-pack/plugins/observability_onboarding/public/application/app.tsx new file mode 100644 index 0000000000000..28d96eda8df26 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/application/app.tsx @@ -0,0 +1,179 @@ +/* + * 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 { EuiErrorBoundary } from '@elastic/eui'; +import { Theme, ThemeProvider } from '@emotion/react'; +import { + APP_WRAPPER_CLASS, + AppMountParameters, + CoreStart, +} from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import { + KibanaContextProvider, + KibanaThemeProvider, + RedirectAppLinks, + useKibana, + useUiSetting$, +} from '@kbn/kibana-react-plugin/public'; +import { useBreadcrumbs } from '@kbn/observability-plugin/public'; +import { RouterProvider, createRouter } from '@kbn/typed-react-router-config'; +import { euiDarkVars, euiLightVars } from '@kbn/ui-theme'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Redirect, RouteComponentProps, RouteProps } from 'react-router-dom'; +import { Home } from '../components/app/home'; +import { + ObservabilityOnboardingPluginSetupDeps, + ObservabilityOnboardingPluginStartDeps, +} from '../plugin'; + +export type BreadcrumbTitle< + T extends { [K in keyof T]?: string | undefined } = {} +> = string | ((props: RouteComponentProps) => string) | null; + +export interface RouteDefinition< + T extends { [K in keyof T]?: string | undefined } = any +> extends RouteProps { + breadcrumb: BreadcrumbTitle; +} + +export const onBoardingTitle = i18n.translate( + 'xpack.observability_onboarding.breadcrumbs.onboarding', + { + defaultMessage: 'Onboarding', + } +); + +export const onboardingRoutes: RouteDefinition[] = [ + { + exact: true, + path: '/', + render: () => , + breadcrumb: onBoardingTitle, + }, +]; + +function ObservabilityOnboardingApp() { + const [darkMode] = useUiSetting$('theme:darkMode'); + + const { http } = useKibana().services; + const basePath = http.basePath.get(); + + useBreadcrumbs([ + { + text: onBoardingTitle, + href: basePath + '/app/observabilityOnboarding', + }, + { + text: i18n.translate('xpack.observability_onboarding.breadcrumbs.logs', { + defaultMessage: 'Logs', + }), + }, + ]); + + return ( + ({ + ...outerTheme, + eui: darkMode ? euiDarkVars : euiLightVars, + darkMode, + })} + > +
+ +
+
+ ); +} + +export const observabilityOnboardingRouter = createRouter({}); + +export function ObservabilityOnboardingAppRoot({ + appMountParameters, + core, + deps, + corePlugins: { observability, data }, +}: { + appMountParameters: AppMountParameters; + core: CoreStart; + deps: ObservabilityOnboardingPluginSetupDeps; + corePlugins: ObservabilityOnboardingPluginStartDeps; +}) { + const { history } = appMountParameters; + const i18nCore = core.i18n; + const plugins = { ...deps }; + + return ( + + + + + + + + + + + + + + ); +} + +/** + * This module is rendered asynchronously in the Kibana platform. + */ + +export const renderApp = ({ + core, + deps, + appMountParameters, + corePlugins, +}: { + core: CoreStart; + deps: ObservabilityOnboardingPluginSetupDeps; + appMountParameters: AppMountParameters; + corePlugins: ObservabilityOnboardingPluginStartDeps; +}) => { + const { element } = appMountParameters; + + ReactDOM.render( + , + element + ); + return () => { + corePlugins.data.search.session.clear(); + ReactDOM.unmountComponentAtNode(element); + }; +}; diff --git a/x-pack/plugins/observability_onboarding/public/components/app/home/index.tsx b/x-pack/plugins/observability_onboarding/public/components/app/home/index.tsx new file mode 100644 index 0000000000000..4b4b3dfdbf3af --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/app/home/index.tsx @@ -0,0 +1,99 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import React, { ComponentType, useRef, useState } from 'react'; +import { + FilmstripFrame, + FilmstripTransition, + TransitionState, +} from '../../shared/filmstrip_transition'; +import { + Provider as WizardProvider, + Step as WizardStep, +} from './logs_onboarding_wizard'; +import { HorizontalSteps } from './logs_onboarding_wizard/horizontal_steps'; +import { PageTitle } from './logs_onboarding_wizard/page_title'; + +export function Home({ animated = true }: { animated?: boolean }) { + if (animated) { + return ; + } + return ; +} + +function StillTransitionsWizard() { + return ( + + + + + + + + ); +} + +const TRANSITION_DURATION = 180; + +function AnimatedTransitionsWizard() { + const [transition, setTransition] = useState('ready'); + const TransitionComponent = useRef(() => null); + + function onChangeStep({ + direction, + StepComponent, + }: { + direction: 'back' | 'next'; + StepComponent: ComponentType; + }) { + setTransition(direction); + TransitionComponent.current = StepComponent; + setTimeout(() => { + setTransition('ready'); + }, TRANSITION_DURATION + 10); + } + + return ( + + + + + + + + + + + + + { + // eslint-disable-next-line react/jsx-pascal-case + transition === 'back' ? : null + } + + + + + + { + // eslint-disable-next-line react/jsx-pascal-case + transition === 'next' ? : null + } + + + + + + ); +} diff --git a/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/configure_logs.tsx b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/configure_logs.tsx new file mode 100644 index 0000000000000..6c01111d5f43d --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/configure_logs.tsx @@ -0,0 +1,218 @@ +/* + * 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, { PropsWithChildren, useState } from 'react'; +import { + EuiTitle, + EuiText, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, + EuiCard, + EuiIcon, + EuiIconProps, +} from '@elastic/eui'; +import { + StepPanel, + StepPanelContent, + StepPanelFooter, +} from '../../../shared/step_panel'; +import { useWizard } from '.'; + +export function ConfigureLogs() { + const { goToStep, goBack, getState, setState } = useWizard(); + const wizardState = getState(); + const [logsType, setLogsType] = useState(wizardState.logsType); + const [uploadType, setUploadType] = useState(wizardState.uploadType); + + function onContinue() { + if (logsType && uploadType) { + setState({ ...getState(), logsType, uploadType }); + goToStep('installElasticAgent'); + } + } + + function createLogsTypeToggle(type: NonNullable) { + return () => { + if (type === logsType) { + setLogsType(undefined); + } else { + setLogsType(type); + } + }; + } + + function createUploadToggle(type: NonNullable) { + return () => { + if (type === uploadType) { + setUploadType(undefined); + } else { + setUploadType(type); + } + }; + } + + function onBack() { + goBack(); + } + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Back + , + + Continue + , + ]} + /> + + ); +} + +function LogsTypeSection({ + title, + description, + children, +}: PropsWithChildren<{ title: string; description: string }>) { + return ( + <> + +

{title}

+
+ + +

{description}

+
+ + {children} + + ); +} + +function OptionCard({ + title, + iconType, + onClick, + isSelected, +}: { + title: string; + iconType: EuiIconProps['type']; + onClick: () => void; + isSelected: boolean; +}) { + return ( + } + title={title} + titleSize="xs" + paddingSize="m" + style={{ height: 56 }} + onClick={onClick} + hasBorder={true} + display={isSelected ? 'primary' : undefined} + /> + ); +} diff --git a/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/horizontal_steps.tsx b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/horizontal_steps.tsx new file mode 100644 index 0000000000000..789f09bb7d574 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/horizontal_steps.tsx @@ -0,0 +1,52 @@ +/* + * 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 from 'react'; +import { EuiStepsHorizontal } from '@elastic/eui'; +import { useWizard } from '.'; + +export function HorizontalSteps() { + const { getPath } = useWizard(); + const [currentStep, ...previousSteps] = getPath().reverse(); + + function getStatus(stepKey: ReturnType[0]) { + if (currentStep === stepKey) { + return 'current'; + } + if (previousSteps.includes(stepKey)) { + return 'complete'; + } + return 'incomplete'; + } + + return ( + {}, + }, + { + title: 'Configure logs', + status: getStatus('configureLogs'), + onClick: () => {}, + }, + { + title: 'Install shipper', + status: getStatus('installElasticAgent'), + onClick: () => {}, + }, + { + title: 'Import data', + status: getStatus('importData'), + onClick: () => {}, + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/import_data.tsx b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/import_data.tsx new file mode 100644 index 0000000000000..7b64ca81ccaee --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/import_data.tsx @@ -0,0 +1,105 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLoadingSpinner, + EuiSpacer, + EuiSteps, + EuiText, +} from '@elastic/eui'; +import React from 'react'; +import { useWizard } from '.'; +import { useFetcher } from '../../../../hooks/use_fetcher'; +import { + StepPanel, + StepPanelContent, + StepPanelFooter, +} from '../../../shared/step_panel'; + +export function ImportData() { + const { goToStep, goBack } = useWizard(); + + const { data } = useFetcher((callApi) => { + return callApi('GET /internal/observability_onboarding/get_status'); + }, []); + + function onContinue() { + goToStep('inspect'); + } + + function onBack() { + goBack(); + } + + return ( + + + +

+ It might take a few minutes for the data to get to Elasticsearch. If + you're not seeing any, try generating some to verify. If + you're having trouble connecting, check out the troubleshooting + guide. +

+
+ + + + + + + + +

Listening for incoming logs

+
+
+
+
+ + + + + + Need some help? + + +
+ + Back + , + + Continue + , + ]} + /> +
+ ); +} diff --git a/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/index.tsx b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/index.tsx new file mode 100644 index 0000000000000..d7f3eeca3da46 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/index.tsx @@ -0,0 +1,58 @@ +/* + * 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 { NameLogs } from './name_logs'; +import { ConfigureLogs } from './configure_logs'; +import { InstallElasticAgent } from './install_elastic_agent'; +import { createWizardContext } from '../../../../context/create_wizard_context'; +import { ImportData } from './import_data'; +import { Inspect } from './inspect'; + +interface WizardState { + datasetName: string; + logsType?: + | 'system' + | 'sys' + | 'http-endpoint' + | 'opentelemetry' + | 'amazon-firehose' + | 'log-file' + | 'service'; + uploadType?: 'log-file' | 'api-key'; + elasticAgentPlatform: 'linux-tar' | 'macos' | 'windows' | 'deb' | 'rpm'; + alternativeShippers: { + filebeat: boolean; + fluentbit: boolean; + logstash: boolean; + fluentd: boolean; + }; +} + +const initialState: WizardState = { + datasetName: '', + elasticAgentPlatform: 'linux-tar', + alternativeShippers: { + filebeat: false, + fluentbit: false, + logstash: false, + fluentd: false, + }, +}; + +const { Provider, Step, useWizard } = createWizardContext({ + initialState, + initialStep: 'nameLogs', + steps: { + nameLogs: NameLogs, + configureLogs: ConfigureLogs, + installElasticAgent: InstallElasticAgent, + importData: ImportData, + inspect: Inspect, + }, +}); + +export { Provider, Step, useWizard }; diff --git a/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/inspect.tsx b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/inspect.tsx new file mode 100644 index 0000000000000..94b0040984edd --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/inspect.tsx @@ -0,0 +1,46 @@ +/* + * 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 from 'react'; +import { EuiButton, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { + StepPanel, + StepPanelContent, + StepPanelFooter, +} from '../../../shared/step_panel'; +import { useWizard } from '.'; + +export function Inspect() { + const { goBack, getState, getPath, getUsage } = useWizard(); + return ( + + + +

State

+
+
{JSON.stringify(getState(), null, 4)}
+ + +

Path

+
+
{JSON.stringify(getPath(), null, 4)}
+ + +

Usage

+
+
{JSON.stringify(getUsage(), null, 4)}
+
+ + Back + , + ]} + /> +
+ ); +} diff --git a/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/install_elastic_agent.tsx b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/install_elastic_agent.tsx new file mode 100644 index 0000000000000..f75ecee1d3992 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/install_elastic_agent.tsx @@ -0,0 +1,199 @@ +/* + * 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, { PropsWithChildren, useState } from 'react'; +import { + EuiTitle, + EuiText, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, + EuiCard, + EuiIcon, + EuiIconProps, + EuiButtonGroup, + EuiCodeBlock, +} from '@elastic/eui'; +import { + StepPanel, + StepPanelContent, + StepPanelFooter, +} from '../../../shared/step_panel'; +import { useWizard } from '.'; + +export function InstallElasticAgent() { + const { goToStep, goBack, getState, setState } = useWizard(); + const wizardState = getState(); + const [elasticAgentPlatform, setElasticAgentPlatform] = useState( + wizardState.elasticAgentPlatform + ); + const [alternativeShippers, setAlternativeShippers] = useState( + wizardState.alternativeShippers + ); + + function onContinue() { + setState({ ...getState(), elasticAgentPlatform, alternativeShippers }); + goToStep('importData'); + } + + function createAlternativeShipperToggle( + type: NonNullable + ) { + return () => { + setAlternativeShippers({ + ...alternativeShippers, + [type]: !alternativeShippers[type], + }); + }; + } + + function onBack() { + goBack(); + } + + return ( + + + +

+ Select a platform and run the command to install, enroll, and start + the Elastic Agent. Do this for each host. For other platforms, see + our downloads page. Review host requirements and other installation + options. +

+
+ + + setElasticAgentPlatform(id as typeof elasticAgentPlatform) + } + /> + + + {PLATFORM_COMMAND[elasticAgentPlatform]} + + + + + + + + + + + + + + + + + + + + + +
+ + Back + , + + Continue + , + ]} + /> +
+ ); +} + +function LogsTypeSection({ + title, + description, + children, +}: PropsWithChildren<{ title: string; description: string }>) { + return ( + <> + +

{title}

+
+ + +

{description}

+
+ + {children} + + ); +} + +function OptionCard({ + title, + iconType, + onClick, + isSelected, +}: { + title: string; + iconType: EuiIconProps['type']; + onClick: () => void; + isSelected: boolean; +}) { + return ( + } + title={title} + titleSize="xs" + paddingSize="m" + style={{ height: 56 }} + onClick={onClick} + hasBorder={true} + display={isSelected ? 'primary' : undefined} + /> + ); +} + +const PLATFORM_COMMAND = { + 'linux-tar': `curl -O https://elastic.co/agent-setup.sh && sudo bash agent-setup.sh -- service.name=my-service --url=https://elasticsearch:8220 --enrollment-token=SRSc2ozWUItWXNuWE5oZzdERFU6anJtY0FIzhSRGlzeTJYcUF5UklfUQ==`, + macos: `curl -O https://elastic.co/agent-setup.sh && sudo bash agent-setup.sh -- service.name=my-service --url=https://elasticsearch:8220 --enrollment-token=SRSc2ozWUItWXNuWE5oZzdERFU6anJtY0FIzhSRGlzeTJYcUF5UklfUQ==`, + windows: `curl -O https://elastic.co/agent-setup.sh && sudo bash agent-setup.sh -- service.name=my-service --url=https://elasticsearch:8220 --enrollment-token=SRSc2ozWUItWXNuWE5oZzdERFU6anJtY0FIzhSRGlzeTJYcUF5UklfUQ==`, + deb: `curl -O https://elastic.co/agent-setup.sh && sudo bash agent-setup.sh -- service.name=my-service --url=https://elasticsearch:8220 --enrollment-token=SRSc2ozWUItWXNuWE5oZzdERFU6anJtY0FIzhSRGlzeTJYcUF5UklfUQ==`, + rpm: `curl -O https://elastic.co/agent-setup.sh && sudo bash agent-setup.sh -- service.name=my-service --url=https://elasticsearch:8220 --enrollment-token=SRSc2ozWUItWXNuWE5oZzdERFU6anJtY0FIzhSRGlzeTJYcUF5UklfUQ==`, +} as const; diff --git a/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/name_logs.tsx b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/name_logs.tsx new file mode 100644 index 0000000000000..253ce03282a5e --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/name_logs.tsx @@ -0,0 +1,72 @@ +/* + * 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, { useState } from 'react'; +import { + EuiText, + EuiButton, + EuiButtonEmpty, + EuiForm, + EuiFormRow, + EuiFieldText, + EuiSpacer, +} from '@elastic/eui'; +import { + StepPanel, + StepPanelContent, + StepPanelFooter, +} from '../../../shared/step_panel'; +import { useWizard } from '.'; + +export function NameLogs() { + const { goToStep, getState, setState } = useWizard(); + const wizardState = getState(); + const [datasetName, setDatasetName] = useState(wizardState.datasetName); + + function onContinue() { + setState({ ...getState(), datasetName }); + goToStep('configureLogs'); + } + + return ( + + + +

Pick a name for your logs, this will become your dataset name.

+
+ + + + setDatasetName(event.target.value)} + /> + + +
+ + Skip for now + , + + Save and Continue + , + ]} + /> +
+ ); +} diff --git a/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/page_title.tsx b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/page_title.tsx new file mode 100644 index 0000000000000..dde2797c1cf23 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/app/home/logs_onboarding_wizard/page_title.tsx @@ -0,0 +1,37 @@ +/* + * 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 from 'react'; +import { EuiTitle } from '@elastic/eui'; +import { useWizard } from '.'; + +export function PageTitle() { + const { getPath } = useWizard(); + const [currentStep] = getPath().reverse(); + + if (currentStep === 'installElasticAgent') { + return ( + +

Select your shipper

+
+ ); + } + + if (currentStep === 'importData') { + return ( + +

Incoming logs

+
+ ); + } + + return ( + +

Collect and analyze my logs

+
+ ); +} diff --git a/x-pack/plugins/observability_onboarding/public/components/shared/filmstrip_transition.tsx b/x-pack/plugins/observability_onboarding/public/components/shared/filmstrip_transition.tsx new file mode 100644 index 0000000000000..df877c2856733 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/shared/filmstrip_transition.tsx @@ -0,0 +1,68 @@ +/* + * 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, { PropsWithChildren } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +export type TransitionState = 'ready' | 'back' | 'next'; + +export function FilmstripTransition({ + children, + duration, + transition, +}: PropsWithChildren<{ duration: number; transition: TransitionState }>) { + return ( +
+ {children} +
+ ); +} + +export function FilmstripFrame({ + children, + position, +}: PropsWithChildren<{ position: 'left' | 'center' | 'right' }>) { + return ( + + {children} + {/* {children}*/} + + ); +} diff --git a/x-pack/plugins/observability_onboarding/public/components/shared/step_panel.tsx b/x-pack/plugins/observability_onboarding/public/components/shared/step_panel.tsx new file mode 100644 index 0000000000000..bd3c2bfc13e3c --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/shared/step_panel.tsx @@ -0,0 +1,68 @@ +/* + * 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, { ReactNode } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiPanelProps, + EuiTitle, +} from '@elastic/eui'; + +interface StepPanelProps { + title: string; + panelProps?: EuiPanelProps; + children?: ReactNode; +} + +export function StepPanel(props: StepPanelProps) { + const { title, children } = props; + const panelProps = props.panelProps ?? null; + return ( + + + + +

{title}

+
+
+ {children} +
+
+ ); +} + +interface StepPanelContentProps { + children?: ReactNode; +} +export function StepPanelContent(props: StepPanelContentProps) { + const { children } = props; + return {children}; +} + +interface StepPanelFooterProps { + children?: ReactNode; + items?: ReactNode[]; +} +export function StepPanelFooter(props: StepPanelFooterProps) { + const { items = [], children } = props; + return ( + + {children} + {items && ( + + {items.map((itemReactNode, index) => ( + + {itemReactNode} + + ))} + + )} + + ); +} diff --git a/x-pack/plugins/observability_onboarding/public/context/create_wizard_context.tsx b/x-pack/plugins/observability_onboarding/public/context/create_wizard_context.tsx new file mode 100644 index 0000000000000..7e7937ebc7e14 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/context/create_wizard_context.tsx @@ -0,0 +1,180 @@ +/* + * 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, { + ComponentType, + ReactNode, + createContext, + useContext, + useState, + useRef, +} from 'react'; + +interface WizardContext { + CurrentStep: ComponentType; + goToStep: (step: StepKey) => void; + goBack: () => void; + getState: () => T; + setState: (state: T) => void; + getPath: () => StepKey[]; + getUsage: () => { + timeSinceStart: number; + navEvents: Array<{ + type: string; + step: StepKey; + timestamp: number; + duration: number; + }>; + }; +} + +export function createWizardContext< + T, + StepKey extends string, + InitialStepKey extends StepKey +>({ + initialState, + initialStep, + steps, +}: { + initialState: T; + initialStep: InitialStepKey; + steps: Record; +}) { + const context = createContext>({ + CurrentStep: () => null, + goToStep: () => {}, + goBack: () => {}, + getState: () => initialState, + setState: () => {}, + getPath: () => [], + getUsage: () => ({ timeSinceStart: 0, navEvents: [] }), + }); + + function Provider({ + children, + onChangeStep, + transitionDuration, + }: { + children?: ReactNode; + onChangeStep?: (stepChangeEvent: { + direction: 'back' | 'next'; + stepKey: StepKey; + StepComponent: ComponentType; + }) => void; + transitionDuration?: number; + }) { + const [step, setStep] = useState(initialStep); + const pathRef = useRef([initialStep]); + const usageRef = useRef['getUsage']>>({ + timeSinceStart: 0, + navEvents: [ + { type: 'initial', step, timestamp: Date.now(), duration: 0 }, + ], + }); + const [state, setState] = useState(initialState); + return ( + { + setStep(stepKey); + }, transitionDuration); + } else { + setStep(stepKey); + } + }, + goBack() { + if (step === initialStep) { + return; + } + const path = pathRef.current; + path.pop(); + const lastStep = path[path.length - 1]; + const navEvents = usageRef.current.navEvents; + const currentNavEvent = navEvents[navEvents.length - 1]; + const timestamp = Date.now(); + currentNavEvent.duration = timestamp - currentNavEvent.timestamp; + usageRef.current.navEvents.push({ + type: 'back', + step: lastStep, + timestamp, + duration: 0, + }); + if (onChangeStep) { + onChangeStep({ + direction: 'back', + stepKey: lastStep, + StepComponent: steps[lastStep], + }); + } + if (transitionDuration) { + setTimeout(() => { + setStep(lastStep); + }, transitionDuration); + } else { + setStep(lastStep); + } + }, + getState: () => state as T, + setState: (_state: T) => { + setState(_state); + }, + getPath: () => [...pathRef.current], + getUsage: () => { + const currentTime = Date.now(); + const navEvents = usageRef.current.navEvents; + const firstNavEvent = navEvents[0]; + const lastNavEvent = navEvents[navEvents.length - 1]; + lastNavEvent.duration = currentTime - lastNavEvent.timestamp; + return { + timeSinceStart: currentTime - firstNavEvent.timestamp, + navEvents, + }; + }, + }} + > + {children} + + ); + } + + function Step() { + const { CurrentStep } = useContext(context); + return ; + } + + function useWizard() { + const { CurrentStep: _, ...rest } = useContext(context); + return rest; + } + + return { context, Provider, Step, useWizard }; +} diff --git a/x-pack/plugins/observability_onboarding/public/hooks/use_fetcher.tsx b/x-pack/plugins/observability_onboarding/public/hooks/use_fetcher.tsx new file mode 100644 index 0000000000000..5e96225144d34 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/hooks/use_fetcher.tsx @@ -0,0 +1,209 @@ +/* + * 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, { useEffect, useMemo, useState } from 'react'; +import type { + IHttpFetchError, + ResponseErrorBody, +} from '@kbn/core-http-browser'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useInspectorContext } from '@kbn/observability-plugin/public'; +import { + AutoAbortedObservabilityClient, + callObservabilityOnboardingApi, +} from '../services/rest/create_call_api'; + +export enum FETCH_STATUS { + LOADING = 'loading', + SUCCESS = 'success', + FAILURE = 'failure', + NOT_INITIATED = 'not_initiated', +} + +export const isPending = (fetchStatus: FETCH_STATUS) => + fetchStatus === FETCH_STATUS.LOADING || + fetchStatus === FETCH_STATUS.NOT_INITIATED; + +export interface FetcherResult { + data?: Data; + status: FETCH_STATUS; + error?: IHttpFetchError; +} + +function getDetailsFromErrorResponse( + error: IHttpFetchError +) { + const message = error.body?.message ?? error.response?.statusText; + return ( + <> + {message} ({error.response?.status}) +
+ {i18n.translate('xpack.observability_onboarding.fetcher.error.url', { + defaultMessage: `URL`, + })} +
+ {error.response?.url} + + ); +} + +const createAutoAbortedClient = ( + signal: AbortSignal, + addInspectorRequest: (result: FetcherResult) => void +): AutoAbortedObservabilityClient => { + return ((endpoint, options) => { + return callObservabilityOnboardingApi(endpoint, { + ...options, + signal, + } as any) + .catch((err) => { + addInspectorRequest({ + status: FETCH_STATUS.FAILURE, + data: err.body?.attributes, + }); + throw err; + }) + .then((response) => { + addInspectorRequest({ + data: response, + status: FETCH_STATUS.SUCCESS, + }); + return response; + }); + }) as AutoAbortedObservabilityClient; +}; + +// fetcher functions can return undefined OR a promise. Previously we had a more simple type +// but it led to issues when using object destructuring with default values +type InferResponseType = Exclude extends Promise< + infer TResponseType +> + ? TResponseType + : unknown; + +export function useFetcher( + fn: (callApi: AutoAbortedObservabilityClient) => TReturn, + fnDeps: any[], + options: { + preservePreviousData?: boolean; + showToastOnError?: boolean; + } = {} +): FetcherResult> & { refetch: () => void } { + const { notifications } = useKibana(); + const { preservePreviousData = true, showToastOnError = true } = options; + const [result, setResult] = useState< + FetcherResult> + >({ + data: undefined, + status: FETCH_STATUS.NOT_INITIATED, + }); + const [counter, setCounter] = useState(0); + const { addInspectorRequest } = useInspectorContext(); + + useEffect(() => { + let controller: AbortController = new AbortController(); + + async function doFetch() { + controller.abort(); + + controller = new AbortController(); + + const signal = controller.signal; + + const promise = fn(createAutoAbortedClient(signal, addInspectorRequest)); + // if `fn` doesn't return a promise it is a signal that data fetching was not initiated. + // This can happen if the data fetching is conditional (based on certain inputs). + // In these cases it is not desirable to invoke the global loading spinner, or change the status to success + if (!promise) { + return; + } + + setResult((prevResult) => ({ + data: preservePreviousData ? prevResult.data : undefined, // preserve data from previous state while loading next state + status: FETCH_STATUS.LOADING, + error: undefined, + })); + + try { + const data = await promise; + // when http fetches are aborted, the promise will be rejected + // and this code is never reached. For async operations that are + // not cancellable, we need to check whether the signal was + // aborted before updating the result. + if (!signal.aborted) { + setResult({ + data, + status: FETCH_STATUS.SUCCESS, + error: undefined, + } as FetcherResult>); + } + } catch (e) { + const err = e as Error | IHttpFetchError; + + if (!signal.aborted) { + const errorDetails = + 'response' in err ? getDetailsFromErrorResponse(err) : err.message; + + if (showToastOnError) { + notifications.toasts.danger({ + title: i18n.translate( + 'xpack.observability_onboarding.fetcher.error.title', + { + defaultMessage: `Error while fetching resource`, + } + ), + + body: ( +
+
+ {i18n.translate( + 'xpack.observability_onboarding.fetcher.error.status', + { + defaultMessage: `Error`, + } + )} +
+ + {errorDetails} +
+ ), + }); + } + setResult({ + data: undefined, + status: FETCH_STATUS.FAILURE, + error: e, + }); + } + } + } + + doFetch(); + + return () => { + controller.abort(); + }; + /* eslint-disable react-hooks/exhaustive-deps */ + }, [ + counter, + preservePreviousData, + showToastOnError, + ...fnDeps, + /* eslint-enable react-hooks/exhaustive-deps */ + ]); + + return useMemo(() => { + return { + ...result, + refetch: () => { + // this will invalidate the deps to `useEffect` and will result in a new request + setCounter((count) => count + 1); + }, + }; + }, [result]); +} diff --git a/x-pack/plugins/observability_onboarding/public/index.ts b/x-pack/plugins/observability_onboarding/public/index.ts new file mode 100644 index 0000000000000..e703c46392e6d --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; +import { + ObservabilityOnboardingPlugin, + ObservabilityOnboardingPluginSetup, + ObservabilityOnboardingPluginStart, +} from './plugin'; + +export const plugin: PluginInitializer< + ObservabilityOnboardingPluginSetup, + ObservabilityOnboardingPluginStart +> = (ctx: PluginInitializerContext) => new ObservabilityOnboardingPlugin(ctx); + +export type { + ObservabilityOnboardingPluginSetup, + ObservabilityOnboardingPluginStart, +}; diff --git a/x-pack/plugins/observability_onboarding/public/plugin.ts b/x-pack/plugins/observability_onboarding/public/plugin.ts new file mode 100644 index 0000000000000..35c22b7548705 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/plugin.ts @@ -0,0 +1,99 @@ +/* + * 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 { + ObservabilityPublicSetup, + ObservabilityPublicStart, +} from '@kbn/observability-plugin/public'; +import { + HttpStart, + AppMountParameters, + CoreSetup, + CoreStart, + DEFAULT_APP_CATEGORIES, + Plugin, + PluginInitializerContext, + AppNavLinkStatus, +} from '@kbn/core/public'; +import { + DataPublicPluginSetup, + DataPublicPluginStart, +} from '@kbn/data-plugin/public'; +import type { ObservabilityOnboardingConfig } from '../server'; + +export type ObservabilityOnboardingPluginSetup = void; +export type ObservabilityOnboardingPluginStart = void; + +export interface ObservabilityOnboardingPluginSetupDeps { + data: DataPublicPluginSetup; + observability: ObservabilityPublicSetup; +} + +export interface ObservabilityOnboardingPluginStartDeps { + http: HttpStart; + data: DataPublicPluginStart; + observability: ObservabilityPublicStart; +} + +export class ObservabilityOnboardingPlugin + implements + Plugin< + ObservabilityOnboardingPluginSetup, + ObservabilityOnboardingPluginStart + > +{ + constructor(private ctx: PluginInitializerContext) {} + + public setup( + core: CoreSetup, + plugins: ObservabilityOnboardingPluginSetupDeps + ) { + const { + ui: { enabled: isObservabilityOnboardingUiEnabled }, + } = this.ctx.config.get(); + + const pluginSetupDeps = plugins; + + // set xpack.observability_onboarding.ui.enabled: true + // and go to /app/observabilityOnboarding + if (isObservabilityOnboardingUiEnabled) { + core.application.register({ + navLinkStatus: AppNavLinkStatus.hidden, + id: 'observabilityOnboarding', + title: 'Observability Onboarding', + order: 8500, + euiIconType: 'logoObservability', + category: DEFAULT_APP_CATEGORIES.observability, + keywords: [], + async mount(appMountParameters: AppMountParameters) { + // Load application bundle and Get start service + const [{ renderApp }, [coreStart, corePlugins]] = await Promise.all([ + import('./application/app'), + core.getStartServices(), + ]); + + const { createCallApi } = await import( + './services/rest/create_call_api' + ); + + createCallApi(core); + + return renderApp({ + core: coreStart, + deps: pluginSetupDeps, + appMountParameters, + corePlugins: corePlugins as ObservabilityOnboardingPluginStartDeps, + }); + }, + }); + } + } + public start( + core: CoreStart, + plugins: ObservabilityOnboardingPluginStartDeps + ) {} +} diff --git a/x-pack/plugins/observability_onboarding/public/services/rest/call_api.ts b/x-pack/plugins/observability_onboarding/public/services/rest/call_api.ts new file mode 100644 index 0000000000000..12249c35b7f50 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/services/rest/call_api.ts @@ -0,0 +1,45 @@ +/* + * 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 { CoreSetup, CoreStart } from '@kbn/core/public'; +import { FetchOptions } from '../../../common/fetch_options'; + +function getFetchOptions(fetchOptions: FetchOptions) { + const { body, ...rest } = fetchOptions; + + return { + ...rest, + ...(body !== undefined ? { body: JSON.stringify(body) } : {}), + query: { + ...fetchOptions.query, + }, + }; +} + +export type CallApi = typeof callApi; + +export async function callApi( + { http }: CoreStart | CoreSetup, + fetchOptions: FetchOptions +): Promise { + const { + pathname, + method = 'get', + ...options + } = getFetchOptions(fetchOptions); + + const lowercaseMethod = method.toLowerCase() as + | 'get' + | 'post' + | 'put' + | 'delete' + | 'patch'; + + const res = await http[lowercaseMethod](pathname, options); + + return res; +} diff --git a/x-pack/plugins/observability_onboarding/public/services/rest/create_call_api.ts b/x-pack/plugins/observability_onboarding/public/services/rest/create_call_api.ts new file mode 100644 index 0000000000000..e8c39fa783dd2 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/services/rest/create_call_api.ts @@ -0,0 +1,55 @@ +/* + * 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 { CoreSetup, CoreStart } from '@kbn/core/public'; +import type { RouteRepositoryClient } from '@kbn/server-route-repository'; +import { formatRequest } from '@kbn/server-route-repository'; +import { FetchOptions } from '../../../common/fetch_options'; +import type { ObservabilityOnboardingServerRouteRepository } from '../../../server/routes'; +import { CallApi, callApi } from './call_api'; + +export type ObservabilityOnboardingClientOptions = Omit< + FetchOptions, + 'query' | 'body' | 'pathname' | 'signal' +> & { + signal: AbortSignal | null; +}; + +export type ObservabilityOnboardingClient = RouteRepositoryClient< + ObservabilityOnboardingServerRouteRepository, + ObservabilityOnboardingClientOptions +>; + +export type AutoAbortedObservabilityClient = RouteRepositoryClient< + ObservabilityOnboardingServerRouteRepository, + Omit +>; + +export let callObservabilityOnboardingApi: ObservabilityOnboardingClient = + () => { + throw new Error( + 'callObservabilityOnboardingApi has to be initialized before used. Call createCallApi first.' + ); + }; + +export function createCallApi(core: CoreStart | CoreSetup) { + callObservabilityOnboardingApi = ((endpoint, options) => { + const { params } = options as unknown as { + params?: Partial>; + }; + + const { method, pathname } = formatRequest(endpoint, params?.path); + + return callApi(core, { + ...options, + method, + pathname, + body: params?.body, + query: params?.query, + } as unknown as Parameters[1]); + }) as ObservabilityOnboardingClient; +} diff --git a/x-pack/plugins/observability_onboarding/server/index.ts b/x-pack/plugins/observability_onboarding/server/index.ts new file mode 100644 index 0000000000000..7a6b9b9587b53 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/server/index.ts @@ -0,0 +1,38 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; +import { + PluginConfigDescriptor, + PluginInitializerContext, +} from '@kbn/core/server'; +import { ObservabilityOnboardingPlugin } from './plugin'; + +const configSchema = schema.object({ + ui: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + }), +}); + +export type ObservabilityOnboardingConfig = TypeOf; + +// plugin config +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: configSchema, +}; + +export function plugin(initializerContext: PluginInitializerContext) { + return new ObservabilityOnboardingPlugin(initializerContext); +} + +export type { + ObservabilityOnboardingPluginSetup, + ObservabilityOnboardingPluginStart, +} from './types'; diff --git a/x-pack/plugins/observability_onboarding/server/plugin.ts b/x-pack/plugins/observability_onboarding/server/plugin.ts new file mode 100644 index 0000000000000..3045ad66c869e --- /dev/null +++ b/x-pack/plugins/observability_onboarding/server/plugin.ts @@ -0,0 +1,80 @@ +/* + * 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 { + CoreSetup, + CoreStart, + Logger, + Plugin, + PluginInitializerContext, +} from '@kbn/core/server'; +import { mapValues } from 'lodash'; +import { getObservabilityOnboardingServerRouteRepository } from './routes'; +import { registerRoutes } from './routes/register_routes'; +import { ObservabilityOnboardingRouteHandlerResources } from './routes/types'; +import { + ObservabilityOnboardingPluginSetup, + ObservabilityOnboardingPluginSetupDependencies, + ObservabilityOnboardingPluginStart, + ObservabilityOnboardingPluginStartDependencies, +} from './types'; +import { ObservabilityOnboardingConfig } from '.'; + +export class ObservabilityOnboardingPlugin + implements + Plugin< + ObservabilityOnboardingPluginSetup, + ObservabilityOnboardingPluginStart, + ObservabilityOnboardingPluginSetupDependencies, + ObservabilityOnboardingPluginStartDependencies + > +{ + private readonly logger: Logger; + constructor( + private readonly initContext: PluginInitializerContext + ) { + this.initContext = initContext; + this.logger = this.initContext.logger.get(); + } + + public setup( + core: CoreSetup, + plugins: ObservabilityOnboardingPluginSetupDependencies + ) { + this.logger.debug('observability_onboarding: Setup'); + + const resourcePlugins = mapValues(plugins, (value, key) => { + return { + setup: value, + start: () => + core.getStartServices().then((services) => { + const [, pluginsStartContracts] = services; + return pluginsStartContracts[ + key as keyof ObservabilityOnboardingPluginStartDependencies + ]; + }), + }; + }) as ObservabilityOnboardingRouteHandlerResources['plugins']; + + registerRoutes({ + core, + logger: this.logger, + repository: getObservabilityOnboardingServerRouteRepository(), + plugins: resourcePlugins, + }); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('observability_onboarding: Started'); + + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/observability_onboarding/server/routes/create_observability_onboarding_server_route.ts b/x-pack/plugins/observability_onboarding/server/routes/create_observability_onboarding_server_route.ts new file mode 100644 index 0000000000000..47c3d026bc665 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/server/routes/create_observability_onboarding_server_route.ts @@ -0,0 +1,17 @@ +/* + * 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 { createServerRouteFactory } from '@kbn/server-route-repository'; +import { + ObservabilityOnboardingRouteCreateOptions, + ObservabilityOnboardingRouteHandlerResources, +} from './types'; + +export const createObservabilityOnboardingServerRoute = + createServerRouteFactory< + ObservabilityOnboardingRouteHandlerResources, + ObservabilityOnboardingRouteCreateOptions + >(); diff --git a/x-pack/plugins/observability_onboarding/server/routes/index.ts b/x-pack/plugins/observability_onboarding/server/routes/index.ts new file mode 100644 index 0000000000000..6a1067465787c --- /dev/null +++ b/x-pack/plugins/observability_onboarding/server/routes/index.ts @@ -0,0 +1,31 @@ +/* + * 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 { + EndpointOf, + ServerRouteRepository, +} from '@kbn/server-route-repository'; +import { statusRouteRepository } from './status/route'; + +function getTypedObservabilityOnboardingServerRouteRepository() { + const repository = { + ...statusRouteRepository, + }; + + return repository; +} + +export const getObservabilityOnboardingServerRouteRepository = + (): ServerRouteRepository => { + return getTypedObservabilityOnboardingServerRouteRepository(); + }; + +export type ObservabilityOnboardingServerRouteRepository = ReturnType< + typeof getTypedObservabilityOnboardingServerRouteRepository +>; + +export type APIEndpoint = + EndpointOf; diff --git a/x-pack/plugins/observability_onboarding/server/routes/register_routes.ts b/x-pack/plugins/observability_onboarding/server/routes/register_routes.ts new file mode 100644 index 0000000000000..83000c1eaeec4 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/server/routes/register_routes.ts @@ -0,0 +1,103 @@ +/* + * 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 { errors } from '@elastic/elasticsearch'; +import Boom from '@hapi/boom'; +import { CoreSetup, Logger, RouteRegistrar } from '@kbn/core/server'; +import { + ServerRouteRepository, + decodeRequestParams, + parseEndpoint, + routeValidationObject, +} from '@kbn/server-route-repository'; +import * as t from 'io-ts'; +import { ObservabilityOnboardingRequestHandlerContext } from '../types'; +import { ObservabilityOnboardingRouteHandlerResources } from './types'; + +interface RegisterRoutes { + core: CoreSetup; + repository: ServerRouteRepository; + logger: Logger; + plugins: ObservabilityOnboardingRouteHandlerResources['plugins']; +} + +export function registerRoutes({ + repository, + core, + logger, + plugins, +}: RegisterRoutes) { + const routes = Object.values(repository); + + const router = core.http.createRouter(); + + routes.forEach((route) => { + const { endpoint, options, handler, params } = route; + const { pathname, method } = parseEndpoint(endpoint); + + ( + router[method] as RouteRegistrar< + typeof method, + ObservabilityOnboardingRequestHandlerContext + > + )( + { + path: pathname, + validate: routeValidationObject, + options, + }, + async (context, request, response) => { + try { + const decodedParams = decodeRequestParams( + { + params: request.params, + body: request.body, + query: request.query, + }, + params ?? t.strict({}) + ); + + const data = (await handler({ + context, + request, + logger, + params: decodedParams, + plugins, + })) as any; + + if (data === undefined) { + return response.noContent(); + } + + return response.ok({ body: data }); + } catch (error) { + if (Boom.isBoom(error)) { + logger.error(error.output.payload.message); + return response.customError({ + statusCode: error.output.statusCode, + body: { message: error.output.payload.message }, + }); + } + + logger.error(error); + const opts = { + statusCode: 500, + body: { + message: error.message, + }, + }; + + if (error instanceof errors.RequestAbortedError) { + opts.statusCode = 499; + opts.body.message = 'Client closed request'; + } + + return response.customError(opts); + } + } + ); + }); +} diff --git a/x-pack/plugins/observability_onboarding/server/routes/status/route.ts b/x-pack/plugins/observability_onboarding/server/routes/status/route.ts new file mode 100644 index 0000000000000..438bbe7e8b6a5 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/server/routes/status/route.ts @@ -0,0 +1,22 @@ +/* + * 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 { createObservabilityOnboardingServerRoute } from '../create_observability_onboarding_server_route'; + +const statusRoute = createObservabilityOnboardingServerRoute({ + endpoint: 'GET /internal/observability_onboarding/get_status', + options: { + tags: [], + }, + async handler(resources): Promise<{ status: 'incomplete' | 'complete' }> { + return { status: 'complete' }; + }, +}); + +export const statusRouteRepository = { + ...statusRoute, +}; diff --git a/x-pack/plugins/observability_onboarding/server/routes/types.ts b/x-pack/plugins/observability_onboarding/server/routes/types.ts new file mode 100644 index 0000000000000..c8f1c0dc99560 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/server/routes/types.ts @@ -0,0 +1,35 @@ +/* + * 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 { KibanaRequest, Logger } from '@kbn/core/server'; +import { ObservabilityOnboardingServerRouteRepository } from '.'; +import { + ObservabilityOnboardingPluginSetupDependencies, + ObservabilityOnboardingPluginStartDependencies, + ObservabilityOnboardingRequestHandlerContext, +} from '../types'; + +export type { ObservabilityOnboardingServerRouteRepository }; + +export interface ObservabilityOnboardingRouteHandlerResources { + context: ObservabilityOnboardingRequestHandlerContext; + logger: Logger; + request: KibanaRequest; + plugins: { + [key in keyof ObservabilityOnboardingPluginSetupDependencies]: { + setup: Required[key]; + start: () => Promise< + Required[key] + >; + }; + }; +} + +export interface ObservabilityOnboardingRouteCreateOptions { + options: { + tags: string[]; + }; +} diff --git a/x-pack/plugins/observability_onboarding/server/types.ts b/x-pack/plugins/observability_onboarding/server/types.ts new file mode 100644 index 0000000000000..99e7157178ff1 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/server/types.ts @@ -0,0 +1,31 @@ +/* + * 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 { CustomRequestHandlerContext } from '@kbn/core/server'; +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '@kbn/data-plugin/server'; +import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server'; + +export interface ObservabilityOnboardingPluginSetupDependencies { + data: DataPluginSetup; + observability: ObservabilityPluginSetup; +} + +export interface ObservabilityOnboardingPluginStartDependencies { + data: DataPluginStart; + observability: undefined; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ObservabilityOnboardingPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ObservabilityOnboardingPluginStart {} + +export type ObservabilityOnboardingRequestHandlerContext = + CustomRequestHandlerContext<{}>; diff --git a/x-pack/plugins/observability_onboarding/tsconfig.json b/x-pack/plugins/observability_onboarding/tsconfig.json new file mode 100644 index 0000000000000..4534db3d6d37a --- /dev/null +++ b/x-pack/plugins/observability_onboarding/tsconfig.json @@ -0,0 +1,29 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + }, + "include": [ + "../../../typings/**/*", + "common/**/*", + "public/**/*", + "typings/**/*", + "public/**/*.json", + "server/**/*" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/data-plugin", + "@kbn/kibana-react-plugin", + "@kbn/observability-plugin", + "@kbn/i18n", + "@kbn/core-http-browser", + "@kbn/ui-theme", + "@kbn/typed-react-router-config", + "@kbn/server-route-repository", + "@kbn/config-schema", + ], + "exclude": [ + "target/**/*", + ] +} diff --git a/yarn.lock b/yarn.lock index bc57b5fc7d283..f74b53547eec6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4641,6 +4641,10 @@ version "0.0.0" uid "" +"@kbn/observability-onboarding-plugin@link:x-pack/plugins/observability_onboarding": + version "0.0.0" + uid "" + "@kbn/observability-plugin@link:x-pack/plugins/observability": version "0.0.0" uid ""