mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[APM] Create plugin for logs onboarding (#154728)
Closes #154733 Creates a new plugin for logs onboarding with wizard to organize steps into discrete views. #### TODO: - [x] rename plugin to observability_onboarding - [x] configure: UI and server plugin - [x] enable/disable new plugin - [x] remove the link to it from Observability nav --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Yngrid Coello <yngrid.coello@elastic.co> Co-authored-by: Yngrid Coello <yngrdyn@gmail.com>
This commit is contained in:
parent
4dda8cf995
commit
077245606b
41 changed files with 2280 additions and 0 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -94,6 +94,7 @@ pageLoadAssetSize:
|
|||
navigation: 37269
|
||||
newsfeed: 42228
|
||||
observability: 95000
|
||||
observabilityOnboarding: 19573
|
||||
observabilityShared: 21266
|
||||
osquery: 107090
|
||||
painlessLab: 179748
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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"],
|
||||
|
|
4
x-pack/plugins/observability_onboarding/.prettierrc
Normal file
4
x-pack/plugins/observability_onboarding/.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"semi": true
|
||||
}
|
3
x-pack/plugins/observability_onboarding/README.md
Normal file
3
x-pack/plugins/observability_onboarding/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Observability onboarding plugin
|
||||
|
||||
This plugin provides an onboarding framework for observability solutions: Logs and APM.
|
|
@ -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<HttpFetchOptions, 'body'> & {
|
||||
pathname: string;
|
||||
method?: string;
|
||||
body?: any;
|
||||
};
|
9
x-pack/plugins/observability_onboarding/common/index.ts
Normal file
9
x-pack/plugins/observability_onboarding/common/index.ts
Normal file
|
@ -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';
|
14
x-pack/plugins/observability_onboarding/jest.config.js
Normal file
14
x-pack/plugins/observability_onboarding/jest.config.js
Normal file
|
@ -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: ['<rootDir>/x-pack/plugins/observability_onboarding'],
|
||||
};
|
23
x-pack/plugins/observability_onboarding/kibana.jsonc
Normal file
23
x-pack/plugins/observability_onboarding/kibana.jsonc
Normal file
|
@ -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"]
|
||||
}
|
||||
}
|
|
@ -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<T>) => string) | null;
|
||||
|
||||
export interface RouteDefinition<
|
||||
T extends { [K in keyof T]?: string | undefined } = any
|
||||
> extends RouteProps {
|
||||
breadcrumb: BreadcrumbTitle<T>;
|
||||
}
|
||||
|
||||
export const onBoardingTitle = i18n.translate(
|
||||
'xpack.observability_onboarding.breadcrumbs.onboarding',
|
||||
{
|
||||
defaultMessage: 'Onboarding',
|
||||
}
|
||||
);
|
||||
|
||||
export const onboardingRoutes: RouteDefinition[] = [
|
||||
{
|
||||
exact: true,
|
||||
path: '/',
|
||||
render: () => <Redirect to="/observabilityOnboarding" />,
|
||||
breadcrumb: onBoardingTitle,
|
||||
},
|
||||
];
|
||||
|
||||
function ObservabilityOnboardingApp() {
|
||||
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
|
||||
|
||||
const { http } = useKibana<ObservabilityOnboardingPluginStartDeps>().services;
|
||||
const basePath = http.basePath.get();
|
||||
|
||||
useBreadcrumbs([
|
||||
{
|
||||
text: onBoardingTitle,
|
||||
href: basePath + '/app/observabilityOnboarding',
|
||||
},
|
||||
{
|
||||
text: i18n.translate('xpack.observability_onboarding.breadcrumbs.logs', {
|
||||
defaultMessage: 'Logs',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
theme={(outerTheme?: Theme) => ({
|
||||
...outerTheme,
|
||||
eui: darkMode ? euiDarkVars : euiLightVars,
|
||||
darkMode,
|
||||
})}
|
||||
>
|
||||
<div className={APP_WRAPPER_CLASS} data-test-subj="csmMainContainer">
|
||||
<Home />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<RedirectAppLinks
|
||||
className={APP_WRAPPER_CLASS}
|
||||
application={core.application}
|
||||
>
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
...core,
|
||||
...plugins,
|
||||
observability,
|
||||
data,
|
||||
}}
|
||||
>
|
||||
<KibanaThemeProvider
|
||||
theme$={appMountParameters.theme$}
|
||||
modify={{
|
||||
breakpoint: {
|
||||
xxl: 1600,
|
||||
xxxl: 2000,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<i18nCore.Context>
|
||||
<RouterProvider
|
||||
history={history}
|
||||
router={observabilityOnboardingRouter}
|
||||
>
|
||||
<EuiErrorBoundary>
|
||||
<ObservabilityOnboardingApp />
|
||||
</EuiErrorBoundary>
|
||||
</RouterProvider>
|
||||
</i18nCore.Context>
|
||||
</KibanaThemeProvider>
|
||||
</KibanaContextProvider>
|
||||
</RedirectAppLinks>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
<ObservabilityOnboardingAppRoot
|
||||
appMountParameters={appMountParameters}
|
||||
core={core}
|
||||
deps={deps}
|
||||
corePlugins={corePlugins}
|
||||
/>,
|
||||
element
|
||||
);
|
||||
return () => {
|
||||
corePlugins.data.search.session.clear();
|
||||
ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
||||
};
|
|
@ -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 <AnimatedTransitionsWizard />;
|
||||
}
|
||||
return <StillTransitionsWizard />;
|
||||
}
|
||||
|
||||
function StillTransitionsWizard() {
|
||||
return (
|
||||
<WizardProvider>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<WizardStep />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</WizardProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const TRANSITION_DURATION = 180;
|
||||
|
||||
function AnimatedTransitionsWizard() {
|
||||
const [transition, setTransition] = useState<TransitionState>('ready');
|
||||
const TransitionComponent = useRef<ComponentType>(() => null);
|
||||
|
||||
function onChangeStep({
|
||||
direction,
|
||||
StepComponent,
|
||||
}: {
|
||||
direction: 'back' | 'next';
|
||||
StepComponent: ComponentType;
|
||||
}) {
|
||||
setTransition(direction);
|
||||
TransitionComponent.current = StepComponent;
|
||||
setTimeout(() => {
|
||||
setTransition('ready');
|
||||
}, TRANSITION_DURATION + 10);
|
||||
}
|
||||
|
||||
return (
|
||||
<WizardProvider
|
||||
transitionDuration={TRANSITION_DURATION}
|
||||
onChangeStep={onChangeStep}
|
||||
>
|
||||
<EuiFlexGroup direction="column" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSpacer size="l" />
|
||||
<PageTitle />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ width: '50%' }}>
|
||||
<HorizontalSteps />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={1} style={{ width: '50%' }}>
|
||||
<FilmstripTransition
|
||||
duration={TRANSITION_DURATION}
|
||||
transition={transition}
|
||||
>
|
||||
<FilmstripFrame position="left">
|
||||
{
|
||||
// eslint-disable-next-line react/jsx-pascal-case
|
||||
transition === 'back' ? <TransitionComponent.current /> : null
|
||||
}
|
||||
</FilmstripFrame>
|
||||
<FilmstripFrame position="center">
|
||||
<WizardStep />
|
||||
</FilmstripFrame>
|
||||
<FilmstripFrame position="right">
|
||||
{
|
||||
// eslint-disable-next-line react/jsx-pascal-case
|
||||
transition === 'next' ? <TransitionComponent.current /> : null
|
||||
}
|
||||
</FilmstripFrame>
|
||||
</FilmstripTransition>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</WizardProvider>
|
||||
);
|
||||
}
|
|
@ -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<typeof logsType>) {
|
||||
return () => {
|
||||
if (type === logsType) {
|
||||
setLogsType(undefined);
|
||||
} else {
|
||||
setLogsType(type);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createUploadToggle(type: NonNullable<typeof uploadType>) {
|
||||
return () => {
|
||||
if (type === uploadType) {
|
||||
setUploadType(undefined);
|
||||
} else {
|
||||
setUploadType(type);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function onBack() {
|
||||
goBack();
|
||||
}
|
||||
|
||||
return (
|
||||
<StepPanel title="Choose what logs to collect">
|
||||
<StepPanelContent>
|
||||
<LogsTypeSection
|
||||
title="System logs"
|
||||
description="The quickest way to start using Elastic is to start uploading logs from your system."
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<OptionCard
|
||||
title="Stream system logs"
|
||||
iconType="document"
|
||||
onClick={createLogsTypeToggle('system')}
|
||||
isSelected={logsType === 'system'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem />
|
||||
</EuiFlexGroup>
|
||||
</LogsTypeSection>
|
||||
<EuiHorizontalRule margin="l" />
|
||||
<LogsTypeSection
|
||||
title="Custom logs"
|
||||
description="Stream custom logs files from your data sink to Elastic."
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<OptionCard
|
||||
title="Stream sys logs"
|
||||
iconType="package"
|
||||
onClick={createLogsTypeToggle('sys')}
|
||||
isSelected={logsType === 'sys'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<OptionCard
|
||||
title="Stream HTTP Endpoint logs"
|
||||
iconType="package"
|
||||
onClick={createLogsTypeToggle('http-endpoint')}
|
||||
isSelected={logsType === 'http-endpoint'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
</LogsTypeSection>
|
||||
<EuiHorizontalRule margin="l" />
|
||||
<LogsTypeSection title="Stream logs" description="Lorem ipsum">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<OptionCard
|
||||
title="Stream log file"
|
||||
iconType="document"
|
||||
onClick={createLogsTypeToggle('log-file')}
|
||||
isSelected={logsType === 'log-file'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<OptionCard
|
||||
title="Send it from my service"
|
||||
iconType="document"
|
||||
onClick={createLogsTypeToggle('service')}
|
||||
isSelected={logsType === 'service'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</LogsTypeSection>
|
||||
<EuiHorizontalRule margin="l" />
|
||||
<LogsTypeSection
|
||||
title="Log files"
|
||||
description="Upload your custom logs files to start analyzing it with Elastic."
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<OptionCard
|
||||
title="Upload log file"
|
||||
iconType="document"
|
||||
onClick={createUploadToggle('log-file')}
|
||||
isSelected={uploadType === 'log-file'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<OptionCard
|
||||
title="Get an API key"
|
||||
iconType="document"
|
||||
onClick={createUploadToggle('api-key')}
|
||||
isSelected={uploadType === 'api-key'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</LogsTypeSection>
|
||||
</StepPanelContent>
|
||||
<StepPanelFooter
|
||||
items={[
|
||||
<EuiButton color="ghost" fill onClick={onBack}>
|
||||
Back
|
||||
</EuiButton>,
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
onClick={onContinue}
|
||||
isDisabled={!(logsType && uploadType)}
|
||||
>
|
||||
Continue
|
||||
</EuiButton>,
|
||||
]}
|
||||
/>
|
||||
</StepPanel>
|
||||
);
|
||||
}
|
||||
|
||||
function LogsTypeSection({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
}: PropsWithChildren<{ title: string; description: string }>) {
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="s">
|
||||
<h3>{title}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText color="subdued">
|
||||
<p>{description}</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function OptionCard({
|
||||
title,
|
||||
iconType,
|
||||
onClick,
|
||||
isSelected,
|
||||
}: {
|
||||
title: string;
|
||||
iconType: EuiIconProps['type'];
|
||||
onClick: () => void;
|
||||
isSelected: boolean;
|
||||
}) {
|
||||
return (
|
||||
<EuiCard
|
||||
layout="horizontal"
|
||||
icon={<EuiIcon type={iconType} size="l" />}
|
||||
title={title}
|
||||
titleSize="xs"
|
||||
paddingSize="m"
|
||||
style={{ height: 56 }}
|
||||
onClick={onClick}
|
||||
hasBorder={true}
|
||||
display={isSelected ? 'primary' : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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<typeof getPath>[0]) {
|
||||
if (currentStep === stepKey) {
|
||||
return 'current';
|
||||
}
|
||||
if (previousSteps.includes(stepKey)) {
|
||||
return 'complete';
|
||||
}
|
||||
return 'incomplete';
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiStepsHorizontal
|
||||
steps={[
|
||||
{
|
||||
title: 'Name logs',
|
||||
status: getStatus('nameLogs'),
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
title: 'Configure logs',
|
||||
status: getStatus('configureLogs'),
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
title: 'Install shipper',
|
||||
status: getStatus('installElasticAgent'),
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
title: 'Import data',
|
||||
status: getStatus('importData'),
|
||||
onClick: () => {},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<StepPanel title="">
|
||||
<StepPanelContent>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut>
|
||||
<EuiFlexGroup justifyContent="flexStart" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued">
|
||||
<p>Listening for incoming logs</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSteps
|
||||
titleSize="xs"
|
||||
steps={[
|
||||
{ title: 'Ping received', status: data?.status, children: null },
|
||||
{ title: 'File found', status: 'complete', children: null },
|
||||
{
|
||||
title: 'Downloading Elastic Agent',
|
||||
status: 'loading',
|
||||
children: null,
|
||||
},
|
||||
{
|
||||
title: 'Starting Elastic Agent',
|
||||
status: 'incomplete',
|
||||
children: null,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<EuiHorizontalRule margin="l" />
|
||||
<EuiFlexGroup justifyContent="center" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="help">Need some help?</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</StepPanelContent>
|
||||
<StepPanelFooter
|
||||
items={[
|
||||
<EuiButton color="ghost" fill onClick={onBack}>
|
||||
Back
|
||||
</EuiButton>,
|
||||
<EuiButton color="primary" fill onClick={onContinue}>
|
||||
Continue
|
||||
</EuiButton>,
|
||||
]}
|
||||
/>
|
||||
</StepPanel>
|
||||
);
|
||||
}
|
|
@ -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 };
|
|
@ -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 (
|
||||
<StepPanel title="Inspect wizard">
|
||||
<StepPanelContent>
|
||||
<EuiTitle size="s">
|
||||
<h3>State</h3>
|
||||
</EuiTitle>
|
||||
<pre>{JSON.stringify(getState(), null, 4)}</pre>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="s">
|
||||
<h3>Path</h3>
|
||||
</EuiTitle>
|
||||
<pre>{JSON.stringify(getPath(), null, 4)}</pre>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="s">
|
||||
<h3>Usage</h3>
|
||||
</EuiTitle>
|
||||
<pre>{JSON.stringify(getUsage(), null, 4)}</pre>
|
||||
</StepPanelContent>
|
||||
<StepPanelFooter
|
||||
items={[
|
||||
<EuiButton color="ghost" fill onClick={goBack}>
|
||||
Back
|
||||
</EuiButton>,
|
||||
]}
|
||||
/>
|
||||
</StepPanel>
|
||||
);
|
||||
}
|
|
@ -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<keyof typeof alternativeShippers>
|
||||
) {
|
||||
return () => {
|
||||
setAlternativeShippers({
|
||||
...alternativeShippers,
|
||||
[type]: !alternativeShippers[type],
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function onBack() {
|
||||
goBack();
|
||||
}
|
||||
|
||||
return (
|
||||
<StepPanel title="Install the Elastic Agent">
|
||||
<StepPanelContent>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
legend="Choose platform"
|
||||
options={[
|
||||
{ id: 'linux-tar', label: 'Linux Tar' },
|
||||
{ id: 'macos', label: 'MacOs' },
|
||||
{ id: 'windows', label: 'Windows' },
|
||||
{ id: 'deb', label: 'DEB' },
|
||||
{ id: 'rpm', label: 'RPM' },
|
||||
]}
|
||||
type="single"
|
||||
idSelected={elasticAgentPlatform}
|
||||
onChange={(id: string) =>
|
||||
setElasticAgentPlatform(id as typeof elasticAgentPlatform)
|
||||
}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCodeBlock language="html" isCopyable>
|
||||
{PLATFORM_COMMAND[elasticAgentPlatform]}
|
||||
</EuiCodeBlock>
|
||||
<EuiHorizontalRule margin="l" />
|
||||
<LogsTypeSection title="Or select alternative shippers" description="">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<OptionCard
|
||||
title="Filebeat"
|
||||
iconType="document"
|
||||
onClick={createAlternativeShipperToggle('filebeat')}
|
||||
isSelected={alternativeShippers.filebeat}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<OptionCard
|
||||
title="fluentbit"
|
||||
iconType="package"
|
||||
onClick={createAlternativeShipperToggle('fluentbit')}
|
||||
isSelected={alternativeShippers.fluentbit}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<OptionCard
|
||||
title="Logstash"
|
||||
iconType="logstashIf"
|
||||
onClick={createAlternativeShipperToggle('logstash')}
|
||||
isSelected={alternativeShippers.logstash}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<OptionCard
|
||||
title="Fluentd"
|
||||
iconType="package"
|
||||
onClick={createAlternativeShipperToggle('fluentd')}
|
||||
isSelected={alternativeShippers.fluentd}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</LogsTypeSection>
|
||||
</StepPanelContent>
|
||||
<StepPanelFooter
|
||||
items={[
|
||||
<EuiButton color="ghost" fill onClick={onBack}>
|
||||
Back
|
||||
</EuiButton>,
|
||||
<EuiButton color="primary" fill onClick={onContinue}>
|
||||
Continue
|
||||
</EuiButton>,
|
||||
]}
|
||||
/>
|
||||
</StepPanel>
|
||||
);
|
||||
}
|
||||
|
||||
function LogsTypeSection({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
}: PropsWithChildren<{ title: string; description: string }>) {
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="s">
|
||||
<h3>{title}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText color="subdued">
|
||||
<p>{description}</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function OptionCard({
|
||||
title,
|
||||
iconType,
|
||||
onClick,
|
||||
isSelected,
|
||||
}: {
|
||||
title: string;
|
||||
iconType: EuiIconProps['type'];
|
||||
onClick: () => void;
|
||||
isSelected: boolean;
|
||||
}) {
|
||||
return (
|
||||
<EuiCard
|
||||
layout="horizontal"
|
||||
icon={<EuiIcon type={iconType} size="l" />}
|
||||
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;
|
|
@ -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 (
|
||||
<StepPanel title="Give your logs a name">
|
||||
<StepPanelContent>
|
||||
<EuiText color="subdued">
|
||||
<p>Pick a name for your logs, this will become your dataset name.</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
label="Name"
|
||||
helpText="Special characters and space are not allowed."
|
||||
>
|
||||
<EuiFieldText
|
||||
placeholder="Dataset name"
|
||||
value={datasetName}
|
||||
onChange={(event) => setDatasetName(event.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</StepPanelContent>
|
||||
<StepPanelFooter
|
||||
items={[
|
||||
<EuiButtonEmpty href="/app/observability/overview">
|
||||
Skip for now
|
||||
</EuiButtonEmpty>,
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
onClick={onContinue}
|
||||
isDisabled={!datasetName}
|
||||
>
|
||||
Save and Continue
|
||||
</EuiButton>,
|
||||
]}
|
||||
/>
|
||||
</StepPanel>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<EuiTitle size="l">
|
||||
<h1>Select your shipper</h1>
|
||||
</EuiTitle>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentStep === 'importData') {
|
||||
return (
|
||||
<EuiTitle size="l">
|
||||
<h1>Incoming logs</h1>
|
||||
</EuiTitle>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiTitle size="l">
|
||||
<h1>Collect and analyze my logs</h1>
|
||||
</EuiTitle>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexFlow: 'column nowrap',
|
||||
flexGrow: 1,
|
||||
position: 'relative',
|
||||
zIndex: 0,
|
||||
transitionTimingFunction: 'ease-out',
|
||||
transition:
|
||||
transition !== 'ready' ? `transform ${duration}ms` : undefined,
|
||||
transform:
|
||||
transition === 'ready'
|
||||
? 'translateX(0)'
|
||||
: transition === 'back'
|
||||
? 'translateX(200%)'
|
||||
: 'translateX(-200%)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilmstripFrame({
|
||||
children,
|
||||
position,
|
||||
}: PropsWithChildren<{ position: 'left' | 'center' | 'right' }>) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
// alignItems="center"
|
||||
// justifyContent="spaceAround"
|
||||
alignItems="flexStart"
|
||||
style={
|
||||
position !== 'center'
|
||||
? {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transform:
|
||||
position === 'left' ? 'translateX(-200%)' : 'translateX(200%)',
|
||||
pointerEvents: 'none',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<EuiFlexItem>{children}</EuiFlexItem>
|
||||
{/* <EuiFlexItem grow={false}>{children}</EuiFlexItem>*/}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<EuiPanel {...panelProps}>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="m">
|
||||
<h2>{title}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{children}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
interface StepPanelContentProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
export function StepPanelContent(props: StepPanelContentProps) {
|
||||
const { children } = props;
|
||||
return <EuiFlexItem>{children}</EuiFlexItem>;
|
||||
}
|
||||
|
||||
interface StepPanelFooterProps {
|
||||
children?: ReactNode;
|
||||
items?: ReactNode[];
|
||||
}
|
||||
export function StepPanelFooter(props: StepPanelFooterProps) {
|
||||
const { items = [], children } = props;
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
{children}
|
||||
{items && (
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
{items.map((itemReactNode, index) => (
|
||||
<EuiFlexItem key={index} grow={false}>
|
||||
{itemReactNode}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
|
@ -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<T, StepKey extends string> {
|
||||
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<StepKey, ComponentType>;
|
||||
}) {
|
||||
const context = createContext<WizardContext<T, StepKey>>({
|
||||
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<StepKey>(initialStep);
|
||||
const pathRef = useRef<StepKey[]>([initialStep]);
|
||||
const usageRef = useRef<ReturnType<WizardContext<T, StepKey>['getUsage']>>({
|
||||
timeSinceStart: 0,
|
||||
navEvents: [
|
||||
{ type: 'initial', step, timestamp: Date.now(), duration: 0 },
|
||||
],
|
||||
});
|
||||
const [state, setState] = useState<T>(initialState);
|
||||
return (
|
||||
<context.Provider
|
||||
value={{
|
||||
CurrentStep: steps[step],
|
||||
goToStep(stepKey: StepKey) {
|
||||
if (stepKey === step) {
|
||||
return;
|
||||
}
|
||||
pathRef.current.push(stepKey);
|
||||
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: 'progress',
|
||||
step: stepKey,
|
||||
timestamp,
|
||||
duration: 0,
|
||||
});
|
||||
if (onChangeStep) {
|
||||
onChangeStep({
|
||||
direction: 'next',
|
||||
stepKey,
|
||||
StepComponent: steps[stepKey],
|
||||
});
|
||||
}
|
||||
if (transitionDuration) {
|
||||
setTimeout(() => {
|
||||
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}
|
||||
</context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Step() {
|
||||
const { CurrentStep } = useContext(context);
|
||||
return <CurrentStep />;
|
||||
}
|
||||
|
||||
function useWizard() {
|
||||
const { CurrentStep: _, ...rest } = useContext(context);
|
||||
return rest;
|
||||
}
|
||||
|
||||
return { context, Provider, Step, useWizard };
|
||||
}
|
|
@ -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?: Data;
|
||||
status: FETCH_STATUS;
|
||||
error?: IHttpFetchError<ResponseErrorBody>;
|
||||
}
|
||||
|
||||
function getDetailsFromErrorResponse(
|
||||
error: IHttpFetchError<ResponseErrorBody>
|
||||
) {
|
||||
const message = error.body?.message ?? error.response?.statusText;
|
||||
return (
|
||||
<>
|
||||
{message} ({error.response?.status})
|
||||
<h5>
|
||||
{i18n.translate('xpack.observability_onboarding.fetcher.error.url', {
|
||||
defaultMessage: `URL`,
|
||||
})}
|
||||
</h5>
|
||||
{error.response?.url}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const createAutoAbortedClient = (
|
||||
signal: AbortSignal,
|
||||
addInspectorRequest: <Data>(result: FetcherResult<Data>) => 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<TReturn> = Exclude<TReturn, undefined> extends Promise<
|
||||
infer TResponseType
|
||||
>
|
||||
? TResponseType
|
||||
: unknown;
|
||||
|
||||
export function useFetcher<TReturn>(
|
||||
fn: (callApi: AutoAbortedObservabilityClient) => TReturn,
|
||||
fnDeps: any[],
|
||||
options: {
|
||||
preservePreviousData?: boolean;
|
||||
showToastOnError?: boolean;
|
||||
} = {}
|
||||
): FetcherResult<InferResponseType<TReturn>> & { refetch: () => void } {
|
||||
const { notifications } = useKibana();
|
||||
const { preservePreviousData = true, showToastOnError = true } = options;
|
||||
const [result, setResult] = useState<
|
||||
FetcherResult<InferResponseType<TReturn>>
|
||||
>({
|
||||
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<InferResponseType<TReturn>>);
|
||||
}
|
||||
} catch (e) {
|
||||
const err = e as Error | IHttpFetchError<ResponseErrorBody>;
|
||||
|
||||
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: (
|
||||
<div>
|
||||
<h5>
|
||||
{i18n.translate(
|
||||
'xpack.observability_onboarding.fetcher.error.status',
|
||||
{
|
||||
defaultMessage: `Error`,
|
||||
}
|
||||
)}
|
||||
</h5>
|
||||
|
||||
{errorDetails}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
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]);
|
||||
}
|
23
x-pack/plugins/observability_onboarding/public/index.ts
Normal file
23
x-pack/plugins/observability_onboarding/public/index.ts
Normal file
|
@ -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,
|
||||
};
|
99
x-pack/plugins/observability_onboarding/public/plugin.ts
Normal file
99
x-pack/plugins/observability_onboarding/public/plugin.ts
Normal file
|
@ -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<ObservabilityOnboardingConfig>();
|
||||
|
||||
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<unknown>) {
|
||||
// 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
|
||||
) {}
|
||||
}
|
|
@ -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<T = void>(
|
||||
{ http }: CoreStart | CoreSetup,
|
||||
fetchOptions: FetchOptions
|
||||
): Promise<T> {
|
||||
const {
|
||||
pathname,
|
||||
method = 'get',
|
||||
...options
|
||||
} = getFetchOptions(fetchOptions);
|
||||
|
||||
const lowercaseMethod = method.toLowerCase() as
|
||||
| 'get'
|
||||
| 'post'
|
||||
| 'put'
|
||||
| 'delete'
|
||||
| 'patch';
|
||||
|
||||
const res = await http[lowercaseMethod]<T>(pathname, options);
|
||||
|
||||
return res;
|
||||
}
|
|
@ -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<ObservabilityOnboardingClientOptions, 'signal'>
|
||||
>;
|
||||
|
||||
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<Record<string, any>>;
|
||||
};
|
||||
|
||||
const { method, pathname } = formatRequest(endpoint, params?.path);
|
||||
|
||||
return callApi(core, {
|
||||
...options,
|
||||
method,
|
||||
pathname,
|
||||
body: params?.body,
|
||||
query: params?.query,
|
||||
} as unknown as Parameters<CallApi>[1]);
|
||||
}) as ObservabilityOnboardingClient;
|
||||
}
|
38
x-pack/plugins/observability_onboarding/server/index.ts
Normal file
38
x-pack/plugins/observability_onboarding/server/index.ts
Normal file
|
@ -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<typeof configSchema>;
|
||||
|
||||
// plugin config
|
||||
export const config: PluginConfigDescriptor<ObservabilityOnboardingConfig> = {
|
||||
exposeToBrowser: {
|
||||
ui: true,
|
||||
},
|
||||
schema: configSchema,
|
||||
};
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new ObservabilityOnboardingPlugin(initializerContext);
|
||||
}
|
||||
|
||||
export type {
|
||||
ObservabilityOnboardingPluginSetup,
|
||||
ObservabilityOnboardingPluginStart,
|
||||
} from './types';
|
80
x-pack/plugins/observability_onboarding/server/plugin.ts
Normal file
80
x-pack/plugins/observability_onboarding/server/plugin.ts
Normal file
|
@ -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<ObservabilityOnboardingConfig>
|
||||
) {
|
||||
this.initContext = initContext;
|
||||
this.logger = this.initContext.logger.get();
|
||||
}
|
||||
|
||||
public setup(
|
||||
core: CoreSetup<ObservabilityOnboardingPluginStartDependencies>,
|
||||
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() {}
|
||||
}
|
|
@ -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
|
||||
>();
|
|
@ -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<ObservabilityOnboardingServerRouteRepository>;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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<ObservabilityOnboardingPluginSetupDependencies>[key];
|
||||
start: () => Promise<
|
||||
Required<ObservabilityOnboardingPluginStartDependencies>[key]
|
||||
>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ObservabilityOnboardingRouteCreateOptions {
|
||||
options: {
|
||||
tags: string[];
|
||||
};
|
||||
}
|
31
x-pack/plugins/observability_onboarding/server/types.ts
Normal file
31
x-pack/plugins/observability_onboarding/server/types.ts
Normal file
|
@ -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<{}>;
|
29
x-pack/plugins/observability_onboarding/tsconfig.json
Normal file
29
x-pack/plugins/observability_onboarding/tsconfig.json
Normal file
|
@ -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/**/*",
|
||||
]
|
||||
}
|
|
@ -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 ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue