mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[GenAI][Integrations] UI for the custom integration creation with AI (#186304)
## Summary
This ticket is the initial implementation for the UI side for the
AI-driven custom integration creation.
This PR only contains the implementation of the UI, due to the tight
timing it will not include tests, everything will be tested manually for
8.15 FF. We'll implement the tests later.
#### Enable Feature
The new integration assistant plugin is disabled by default, to enable
it:
```
xpack.integration_assistant.enabled: true
```
#### Complete tasks
- [x] New integration button on the /integrations page
- [x] New integration "landing" page with buttons to upload zip and
assistant
- [x] Upload zip page to install integration
- [x] Integration assistant:
- [x] Connector selection step
- [x] Integration details step
- [x] Data stream step
- [x] Review and install
#### Follow-ups (will be implemented in separate PRs)
- [ ] Add RBAC
- [ ] Add telemetry
- [ ] Documentation
- [ ] Add license/productType controls
- [ ] Add links to the create integration page
- [ ] Improve package name retrieval:
https://github.com/elastic/kibana/issues/185932
- [ ] Add time estimation on the generation stage
- [ ] Add support for multi-valuated "input type"
- [ ] Enable Langsmith tracing using AI assistant settings
#### Demo
b04c21c6
-09cf-49bb-be8f-bf4b9d3feb8e
## Files by Code Owner
### elastic/docs
* packages/kbn-doc-links/src/get_doc_links.ts
* packages/kbn-doc-links/src/types.ts
### elastic/fleet
* x-pack/plugins/fleet/kibana.jsonc
*
x-pack/plugins/fleet/public/applications/integrations/hooks/use_breadcrumbs.tsx
*
x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx
*
x-pack/plugins/fleet/public/applications/integrations/sections/epm/index.tsx
*
x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/create/index.tsx
* x-pack/plugins/fleet/public/components/header.tsx
* x-pack/plugins/fleet/public/constants/page_paths.ts
* x-pack/plugins/fleet/public/plugin.ts
* x-pack/plugins/fleet/tsconfig.json
### elastic/kibana-core
* x-pack/plugins/fleet/kibana.jsonc
* x-pack/plugins/integration_assistant/kibana.jsonc
### elastic/kibana-operations
* packages/kbn-optimizer/limits.yml
### elastic/security-solution
* x-pack/plugins/integration_assistant/**/*
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
114b58290d
commit
9b3775e6b5
126 changed files with 4287 additions and 178 deletions
|
@ -1017,6 +1017,7 @@ module.exports = {
|
|||
'import/no-nodejs-modules': 'error',
|
||||
'no-duplicate-imports': 'off',
|
||||
'@typescript-eslint/no-duplicate-imports': 'error',
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
|
|
|
@ -28,6 +28,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
|
|||
const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`;
|
||||
const KIBANA_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/`;
|
||||
const FLEET_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/fleet/${DOC_LINK_VERSION}/`;
|
||||
const INTEGRATIONS_DEV_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/integrations-developer/${DOC_LINK_VERSION}/`;
|
||||
const PLUGIN_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`;
|
||||
const OBSERVABILITY_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/`;
|
||||
const APM_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/apm/`;
|
||||
|
@ -865,6 +866,9 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
|
|||
roleAndPrivileges: `${FLEET_DOCS}fleet-roles-and-privileges.html`,
|
||||
proxiesSettings: `${FLEET_DOCS}fleet-agent-proxy-support.html`,
|
||||
},
|
||||
integrationDeveloper: {
|
||||
upload: `${INTEGRATIONS_DEV_DOCS}upload-a-new-integration.html`,
|
||||
},
|
||||
ecs: {
|
||||
guide: `${ELASTIC_WEBSITE_URL}guide/en/ecs/${ECS_VERSION}/index.html`,
|
||||
dataStreams: `${ELASTIC_WEBSITE_URL}guide/en/ecs/${ECS_VERSION}/ecs-data_stream.html`,
|
||||
|
|
|
@ -555,6 +555,9 @@ export interface DocLinks {
|
|||
roleAndPrivileges: string;
|
||||
proxiesSettings: string;
|
||||
}>;
|
||||
readonly integrationDeveloper: {
|
||||
upload: string;
|
||||
};
|
||||
readonly ecs: {
|
||||
readonly guide: string;
|
||||
readonly dataStreams: string;
|
||||
|
|
|
@ -82,6 +82,7 @@ pageLoadAssetSize:
|
|||
ingestPipelines: 58003
|
||||
inputControlVis: 172675
|
||||
inspector: 148711
|
||||
integrationAssistant: 19524
|
||||
interactiveSetup: 80000
|
||||
investigate: 17970
|
||||
kibanaOverview: 56279
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
"xpack.logsShared": "plugins/observability_solution/logs_shared",
|
||||
"xpack.fleet": "plugins/fleet",
|
||||
"xpack.ingestPipelines": "plugins/ingest_pipelines",
|
||||
"xpack.integrationAssistant": "plugins/integration_assistant",
|
||||
"xpack.investigate": "plugins/observability_solution/investigate",
|
||||
"xpack.kubernetesSecurity": "plugins/kubernetes_security",
|
||||
"xpack.lens": "plugins/lens",
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
"ingestPipelines",
|
||||
"spaces",
|
||||
"guidedOnboarding",
|
||||
"integrationAssistant",
|
||||
],
|
||||
"requiredBundles": [
|
||||
"kibanaReact",
|
||||
|
|
|
@ -42,6 +42,14 @@ const breadcrumbGetters: {
|
|||
}),
|
||||
},
|
||||
],
|
||||
integration_create: () => [
|
||||
BASE_BREADCRUMB,
|
||||
{
|
||||
text: i18n.translate('xpack.fleet.breadcrumbs.createIntegrationPageTitle', {
|
||||
defaultMessage: 'Create integration',
|
||||
}),
|
||||
},
|
||||
],
|
||||
integration_details_overview: ({ pkgTitle }) => [BASE_BREADCRUMB, { text: pkgTitle }],
|
||||
integration_policy_edit: ({ pkgTitle, pkgkey, policyName }) => [
|
||||
BASE_BREADCRUMB,
|
||||
|
|
|
@ -8,7 +8,7 @@ import React, { memo } from 'react';
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiNotificationBadge } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { useLink } from '../../../hooks';
|
||||
import { useLink, useStartServices } from '../../../hooks';
|
||||
import type { Section } from '../sections';
|
||||
|
||||
import { WithHeaderLayout } from '.';
|
||||
|
@ -21,6 +21,7 @@ interface Props {
|
|||
|
||||
export const DefaultLayout: React.FC<Props> = memo(
|
||||
({ section, children, notificationsBySection }) => {
|
||||
const { integrationAssistant } = useStartServices();
|
||||
const { getHref } = useLink();
|
||||
const tabs = [
|
||||
{
|
||||
|
@ -45,6 +46,8 @@ export const DefaultLayout: React.FC<Props> = memo(
|
|||
},
|
||||
];
|
||||
|
||||
const CreateIntegrationCardButton = integrationAssistant?.CreateIntegrationCardButton;
|
||||
|
||||
return (
|
||||
<WithHeaderLayout
|
||||
leftColumn={
|
||||
|
@ -70,8 +73,18 @@ export const DefaultLayout: React.FC<Props> = memo(
|
|||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
rightColumnGrow={false}
|
||||
rightColumn={
|
||||
CreateIntegrationCardButton ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<CreateIntegrationCardButton href={getHref('integration_create')} />
|
||||
</EuiFlexItem>
|
||||
) : undefined
|
||||
}
|
||||
tabs={tabs.map((tab) => {
|
||||
const notificationCount = notificationsBySection?.[tab.section];
|
||||
return {
|
||||
|
|
|
@ -11,14 +11,16 @@ import { Routes, Route } from '@kbn/shared-ux-router';
|
|||
import { EuiSkeletonText } from '@elastic/eui';
|
||||
|
||||
import { INTEGRATIONS_ROUTING_PATHS } from '../../constants';
|
||||
import { IntegrationsStateContextProvider, useBreadcrumbs } from '../../hooks';
|
||||
import { IntegrationsStateContextProvider, useBreadcrumbs, useStartServices } from '../../hooks';
|
||||
|
||||
import { EPMHomePage } from './screens/home';
|
||||
import { Detail } from './screens/detail';
|
||||
import { Policy } from './screens/policy';
|
||||
import { CreateIntegration } from './screens/create';
|
||||
import { CustomLanguagesOverview } from './screens/detail/custom_languages_overview';
|
||||
|
||||
export const EPMApp: React.FunctionComponent = () => {
|
||||
const { integrationAssistant } = useStartServices();
|
||||
useBreadcrumbs('integrations');
|
||||
|
||||
return (
|
||||
|
@ -38,6 +40,11 @@ export const EPMApp: React.FunctionComponent = () => {
|
|||
</React.Suspense>
|
||||
</IntegrationsStateContextProvider>
|
||||
</Route>
|
||||
{integrationAssistant && (
|
||||
<Route path={INTEGRATIONS_ROUTING_PATHS.integrations_create}>
|
||||
<CreateIntegration />
|
||||
</Route>
|
||||
)}
|
||||
<Route path={INTEGRATIONS_ROUTING_PATHS.integrations}>
|
||||
<EPMHomePage />
|
||||
</Route>
|
||||
|
|
|
@ -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 React, { useMemo } from 'react';
|
||||
|
||||
import { useStartServices, useBreadcrumbs } from '../../../../hooks';
|
||||
|
||||
export const CreateIntegration = React.memo(() => {
|
||||
const { integrationAssistant } = useStartServices();
|
||||
useBreadcrumbs('integration_create');
|
||||
|
||||
const CreateIntegrationAssistant = useMemo(
|
||||
() => integrationAssistant?.CreateIntegration,
|
||||
[integrationAssistant]
|
||||
);
|
||||
|
||||
return CreateIntegrationAssistant ? <CreateIntegrationAssistant /> : null;
|
||||
});
|
|
@ -83,7 +83,6 @@ export const Header: React.FC<HeaderProps> = ({
|
|||
<EuiFlexGroup>
|
||||
{tabs ? (
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<Tabs className={tabsClassName}>
|
||||
{tabs.map((props, index) => (
|
||||
<EuiTab {...(props as EuiTabProps)} key={`${props.id}-${index}`}>
|
||||
|
|
|
@ -11,6 +11,7 @@ export type StaticPage =
|
|||
| 'base'
|
||||
| 'overview'
|
||||
| 'integrations'
|
||||
| 'integration_create'
|
||||
| 'policies'
|
||||
| 'policies_list'
|
||||
| 'enrollment_tokens'
|
||||
|
@ -97,6 +98,7 @@ export const INTEGRATIONS_ROUTING_PATHS = {
|
|||
integrations_all: '/browse/:category?/:subcategory?',
|
||||
integrations_installed: '/installed/:category?',
|
||||
integrations_installed_updates_available: '/installed/updates_available/:category?',
|
||||
integrations_create: '/create',
|
||||
integration_details: '/detail/:pkgkey/:panel?',
|
||||
integration_details_overview: '/detail/:pkgkey/overview',
|
||||
integration_details_policies: '/detail/:pkgkey/policies',
|
||||
|
@ -152,6 +154,7 @@ export const pagePathGetters: {
|
|||
const queryParams = query ? `?${INTEGRATIONS_SEARCH_QUERYPARAM}=${query}` : ``;
|
||||
return [INTEGRATIONS_BASE_PATH, `/installed/updates_available${categoryPath}${queryParams}`];
|
||||
},
|
||||
integration_create: () => [INTEGRATIONS_BASE_PATH, `/create`],
|
||||
integration_details_overview: ({ pkgkey, integration }) => [
|
||||
INTEGRATIONS_BASE_PATH,
|
||||
`/detail/${pkgkey}/overview${integration ? `?integration=${integration}` : ''}`,
|
||||
|
|
|
@ -18,6 +18,7 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
|
||||
import type { IntegrationAssistantPluginStart } from '@kbn/integration-assistant-plugin/public';
|
||||
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
|
||||
|
||||
import type {
|
||||
|
@ -130,6 +131,7 @@ export interface FleetStartDeps {
|
|||
navigation: NavigationPublicPluginStart;
|
||||
customIntegrations: CustomIntegrationsStart;
|
||||
share: SharePluginStart;
|
||||
integrationAssistant?: IntegrationAssistantPluginStart;
|
||||
cloud?: CloudStart;
|
||||
usageCollection?: UsageCollectionStart;
|
||||
guidedOnboarding?: GuidedOnboardingPluginStart;
|
||||
|
@ -139,6 +141,7 @@ export interface FleetStartServices extends CoreStart, Exclude<FleetStartDeps, '
|
|||
storage: Storage;
|
||||
share: SharePluginStart;
|
||||
dashboard: DashboardStart;
|
||||
integrationAssistant?: IntegrationAssistantPluginStart;
|
||||
cloud?: CloudSetup & CloudStart;
|
||||
discover?: DiscoverStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
|
|
|
@ -110,5 +110,6 @@
|
|||
"@kbn/fields-metadata-plugin",
|
||||
"@kbn/test-jest-helpers",
|
||||
"@kbn/core-saved-objects-utils-server",
|
||||
"@kbn/integration-assistant-plugin",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -181,7 +181,7 @@ export const categorizationTestState = {
|
|||
exAnswer: 'testanswer',
|
||||
lastExecutedChain: 'testchain',
|
||||
packageName: 'testpackage',
|
||||
dataStreamName: 'testdatastream',
|
||||
dataStreamName: 'testDataStream',
|
||||
errors: { test: 'testerror' },
|
||||
pipelineResults: [{ test: 'testresult' }],
|
||||
finalized: false,
|
||||
|
|
|
@ -443,6 +443,6 @@ export const ecsTestState = {
|
|||
rawSamples: ['{"test1": "test1"}'],
|
||||
samples: ['{ "test1": "test1" }'],
|
||||
packageName: 'testpackage',
|
||||
dataStreamName: 'testdatastream',
|
||||
dataStreamName: 'testDataStream',
|
||||
formattedSamples: '{"test1": "test1"}',
|
||||
};
|
||||
|
|
|
@ -163,7 +163,7 @@ export const relatedTestState = {
|
|||
ecs: 'testtypes',
|
||||
exAnswer: 'testanswer',
|
||||
packageName: 'testpackage',
|
||||
dataStreamName: 'testdatastream',
|
||||
dataStreamName: 'testDataStream',
|
||||
errors: { test: 'testerror' },
|
||||
pipelineResults: [{ test: 'testresult' }],
|
||||
finalized: false,
|
||||
|
|
|
@ -19,15 +19,15 @@ paths:
|
|||
type: object
|
||||
required:
|
||||
- packageName
|
||||
- datastreamName
|
||||
- dataStreamName
|
||||
- rawSamples
|
||||
- currentPipeline
|
||||
- connectorId
|
||||
properties:
|
||||
packageName:
|
||||
$ref: "../model/common_attributes.schema.yaml#/components/schemas/PackageName"
|
||||
datastreamName:
|
||||
$ref: "../model/common_attributes.schema.yaml#/components/schemas/DatastreamName"
|
||||
dataStreamName:
|
||||
$ref: "../model/common_attributes.schema.yaml#/components/schemas/DataStreamName"
|
||||
rawSamples:
|
||||
$ref: "../model/common_attributes.schema.yaml#/components/schemas/RawSamples"
|
||||
currentPipeline:
|
||||
|
|
|
@ -9,7 +9,7 @@ import { z } from 'zod';
|
|||
|
||||
import {
|
||||
Connector,
|
||||
DatastreamName,
|
||||
DataStreamName,
|
||||
PackageName,
|
||||
Pipeline,
|
||||
RawSamples,
|
||||
|
@ -19,7 +19,7 @@ import { CategorizationAPIResponse } from '../model/response_schemas';
|
|||
export type CategorizationRequestBody = z.infer<typeof CategorizationRequestBody>;
|
||||
export const CategorizationRequestBody = z.object({
|
||||
packageName: PackageName,
|
||||
datastreamName: DatastreamName,
|
||||
dataStreamName: DataStreamName,
|
||||
rawSamples: RawSamples,
|
||||
currentPipeline: Pipeline,
|
||||
connectorId: Connector,
|
||||
|
|
|
@ -19,14 +19,14 @@ paths:
|
|||
type: object
|
||||
required:
|
||||
- packageName
|
||||
- datastreamName
|
||||
- dataStreamName
|
||||
- rawSamples
|
||||
- connectorId
|
||||
properties:
|
||||
packageName:
|
||||
$ref: "../model/common_attributes.schema.yaml#/components/schemas/PackageName"
|
||||
datastreamName:
|
||||
$ref: "../model/common_attributes.schema.yaml#/components/schemas/DatastreamName"
|
||||
dataStreamName:
|
||||
$ref: "../model/common_attributes.schema.yaml#/components/schemas/DataStreamName"
|
||||
rawSamples:
|
||||
$ref: "../model/common_attributes.schema.yaml#/components/schemas/RawSamples"
|
||||
mapping:
|
||||
|
|
|
@ -9,7 +9,7 @@ import { z } from 'zod';
|
|||
|
||||
import {
|
||||
Connector,
|
||||
DatastreamName,
|
||||
DataStreamName,
|
||||
Mapping,
|
||||
PackageName,
|
||||
RawSamples,
|
||||
|
@ -19,7 +19,7 @@ import { EcsMappingAPIResponse } from '../model/response_schemas';
|
|||
export type EcsMappingRequestBody = z.infer<typeof EcsMappingRequestBody>;
|
||||
export const EcsMappingRequestBody = z.object({
|
||||
packageName: PackageName,
|
||||
datastreamName: DatastreamName,
|
||||
dataStreamName: DataStreamName,
|
||||
rawSamples: RawSamples,
|
||||
mapping: Mapping.optional(),
|
||||
connectorId: Connector,
|
||||
|
|
|
@ -11,10 +11,10 @@ components:
|
|||
minLength: 1
|
||||
description: Package name for the integration to be built.
|
||||
|
||||
DatastreamName:
|
||||
DataStreamName:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Datastream name for the integration to be built.
|
||||
description: DataStream name for the integration to be built.
|
||||
|
||||
RawSamples:
|
||||
type: array
|
||||
|
@ -64,7 +64,7 @@ components:
|
|||
|
||||
InputType:
|
||||
type: string
|
||||
description: The input type for the datastream to pull logs from.
|
||||
description: The input type for the dataStream to pull logs from.
|
||||
enum:
|
||||
- aws_cloudwatch
|
||||
- aws_s3
|
||||
|
@ -80,9 +80,9 @@ components:
|
|||
- tcp
|
||||
- udp
|
||||
|
||||
Datastream:
|
||||
DataStream:
|
||||
type: object
|
||||
description: The datastream object.
|
||||
description: The dataStream object.
|
||||
required:
|
||||
- name
|
||||
- title
|
||||
|
@ -94,27 +94,27 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The name of the datastream.
|
||||
description: The name of the dataStream.
|
||||
title:
|
||||
type: string
|
||||
description: The title of the datastream.
|
||||
description: The title of the dataStream.
|
||||
description:
|
||||
type: string
|
||||
description: The description of the datastream.
|
||||
description: The description of the dataStream.
|
||||
inputTypes:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/InputType"
|
||||
description: The input types of the datastream.
|
||||
description: The input types of the dataStream.
|
||||
rawSamples:
|
||||
$ref: "#/components/schemas/RawSamples"
|
||||
description: The raw samples of the datastream.
|
||||
description: The raw samples of the dataStream.
|
||||
pipeline:
|
||||
$ref: "#/components/schemas/Pipeline"
|
||||
description: The pipeline of the datastream.
|
||||
description: The pipeline of the dataStream.
|
||||
docs:
|
||||
$ref: "#/components/schemas/Docs"
|
||||
description: The documents of the datastream.
|
||||
description: The documents of the dataStream.
|
||||
|
||||
Integration:
|
||||
type: object
|
||||
|
@ -137,8 +137,8 @@ components:
|
|||
dataStreams:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Datastream"
|
||||
description: The datastreams of the integration.
|
||||
$ref: "#/components/schemas/DataStream"
|
||||
description: The dataStreams of the integration.
|
||||
logo:
|
||||
type: string
|
||||
description: The logo of the integration.
|
||||
|
|
|
@ -16,10 +16,10 @@ export type PackageName = z.infer<typeof PackageName>;
|
|||
export const PackageName = z.string().min(1);
|
||||
|
||||
/**
|
||||
* Datastream name for the integration to be built.
|
||||
* DataStream name for the integration to be built.
|
||||
*/
|
||||
export type DatastreamName = z.infer<typeof DatastreamName>;
|
||||
export const DatastreamName = z.string().min(1);
|
||||
export type DataStreamName = z.infer<typeof DataStreamName>;
|
||||
export const DataStreamName = z.string().min(1);
|
||||
|
||||
/**
|
||||
* String array containing the json raw samples that are used for ecs mapping.
|
||||
|
@ -31,7 +31,7 @@ export const RawSamples = z.array(z.string());
|
|||
* mapping object to ECS Mapping Request.
|
||||
*/
|
||||
export type Mapping = z.infer<typeof Mapping>;
|
||||
export const Mapping = z.object({});
|
||||
export const Mapping = z.object({}).passthrough();
|
||||
|
||||
/**
|
||||
* LLM Connector to be used in each API request.
|
||||
|
@ -43,7 +43,7 @@ export const Connector = z.string();
|
|||
* An array of processed documents.
|
||||
*/
|
||||
export type Docs = z.infer<typeof Docs>;
|
||||
export const Docs = z.array(z.object({}));
|
||||
export const Docs = z.array(z.object({}).passthrough());
|
||||
|
||||
/**
|
||||
* The pipeline object.
|
||||
|
@ -73,7 +73,7 @@ export const Pipeline = z.object({
|
|||
});
|
||||
|
||||
/**
|
||||
* The input type for the datastream to pull logs from.
|
||||
* The input type for the dataStream to pull logs from.
|
||||
*/
|
||||
export type InputType = z.infer<typeof InputType>;
|
||||
export const InputType = z.enum([
|
||||
|
@ -95,36 +95,36 @@ export type InputTypeEnum = typeof InputType.enum;
|
|||
export const InputTypeEnum = InputType.enum;
|
||||
|
||||
/**
|
||||
* The datastream object.
|
||||
* The dataStream object.
|
||||
*/
|
||||
export type Datastream = z.infer<typeof Datastream>;
|
||||
export const Datastream = z.object({
|
||||
export type DataStream = z.infer<typeof DataStream>;
|
||||
export const DataStream = z.object({
|
||||
/**
|
||||
* The name of the datastream.
|
||||
* The name of the dataStream.
|
||||
*/
|
||||
name: z.string(),
|
||||
/**
|
||||
* The title of the datastream.
|
||||
* The title of the dataStream.
|
||||
*/
|
||||
title: z.string(),
|
||||
/**
|
||||
* The description of the datastream.
|
||||
* The description of the dataStream.
|
||||
*/
|
||||
description: z.string(),
|
||||
/**
|
||||
* The input types of the datastream.
|
||||
* The input types of the dataStream.
|
||||
*/
|
||||
inputTypes: z.array(InputType),
|
||||
/**
|
||||
* The raw samples of the datastream.
|
||||
* The raw samples of the dataStream.
|
||||
*/
|
||||
rawSamples: RawSamples,
|
||||
/**
|
||||
* The pipeline of the datastream.
|
||||
* The pipeline of the dataStream.
|
||||
*/
|
||||
pipeline: Pipeline,
|
||||
/**
|
||||
* The documents of the datastream.
|
||||
* The documents of the dataStream.
|
||||
*/
|
||||
docs: Docs,
|
||||
});
|
||||
|
@ -147,9 +147,9 @@ export const Integration = z.object({
|
|||
*/
|
||||
description: z.string(),
|
||||
/**
|
||||
* The datastreams of the integration.
|
||||
* The dataStreams of the integration.
|
||||
*/
|
||||
dataStreams: z.array(Datastream),
|
||||
dataStreams: z.array(DataStream),
|
||||
/**
|
||||
* The logo of the integration.
|
||||
*/
|
||||
|
|
|
@ -19,15 +19,15 @@ paths:
|
|||
type: object
|
||||
required:
|
||||
- packageName
|
||||
- datastreamName
|
||||
- dataStreamName
|
||||
- rawSamples
|
||||
- currentPipeline
|
||||
- connectorId
|
||||
properties:
|
||||
packageName:
|
||||
$ref: "../model/common_attributes.schema.yaml#/components/schemas/PackageName"
|
||||
datastreamName:
|
||||
$ref: "../model/common_attributes.schema.yaml#/components/schemas/DatastreamName"
|
||||
dataStreamName:
|
||||
$ref: "../model/common_attributes.schema.yaml#/components/schemas/DataStreamName"
|
||||
rawSamples:
|
||||
$ref: "../model/common_attributes.schema.yaml#/components/schemas/RawSamples"
|
||||
currentPipeline:
|
||||
|
|
|
@ -9,7 +9,7 @@ import { z } from 'zod';
|
|||
|
||||
import {
|
||||
Connector,
|
||||
DatastreamName,
|
||||
DataStreamName,
|
||||
PackageName,
|
||||
Pipeline,
|
||||
RawSamples,
|
||||
|
@ -19,7 +19,7 @@ import { RelatedAPIResponse } from '../model/response_schemas';
|
|||
export type RelatedRequestBody = z.infer<typeof RelatedRequestBody>;
|
||||
export const RelatedRequestBody = z.object({
|
||||
packageName: PackageName,
|
||||
datastreamName: DatastreamName,
|
||||
dataStreamName: DataStreamName,
|
||||
rawSamples: RawSamples,
|
||||
currentPipeline: Pipeline,
|
||||
connectorId: Connector,
|
||||
|
|
|
@ -13,8 +13,10 @@ export const INTEGRATION_ASSISTANT_APP_ROUTE = '/app/integration_assistant';
|
|||
|
||||
// Server API Routes
|
||||
export const INTEGRATION_ASSISTANT_BASE_PATH = '/api/integration_assistant';
|
||||
|
||||
export const ECS_GRAPH_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/ecs`;
|
||||
export const CATEGORIZATION_GRAPH_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/categorization`;
|
||||
export const RELATED_GRAPH_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/related`;
|
||||
export const CHECK_PIPELINE_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/pipeline`;
|
||||
export const INTEGRATION_BUILDER_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/build`;
|
||||
export const TEST_PIPELINE_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/pipeline`;
|
||||
export const FLEET_PACKAGES_PATH = `/api/fleet/epm/packages`;
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { BuildIntegrationRequestBody } from './api/build_integration/build_integration';
|
||||
export {
|
||||
CategorizationRequestBody,
|
||||
|
@ -17,7 +16,13 @@ export {
|
|||
export { EcsMappingRequestBody, EcsMappingResponse } from './api/ecs/ecs_route';
|
||||
export { RelatedRequestBody, RelatedResponse } from './api/related/related_route';
|
||||
|
||||
export type { Datastream, InputType, Integration, Pipeline } from './api/model/common_attributes';
|
||||
export type {
|
||||
DataStream,
|
||||
InputType,
|
||||
Integration,
|
||||
Pipeline,
|
||||
Docs,
|
||||
} from './api/model/common_attributes';
|
||||
export type { ESProcessorItem } from './api/model/processor_attributes';
|
||||
|
||||
export {
|
||||
|
@ -28,5 +33,5 @@ export {
|
|||
INTEGRATION_BUILDER_PATH,
|
||||
PLUGIN_ID,
|
||||
RELATED_GRAPH_PATH,
|
||||
TEST_PIPELINE_PATH,
|
||||
CHECK_PIPELINE_PATH,
|
||||
} from './constants';
|
||||
|
|
|
@ -12,10 +12,10 @@ module.exports = {
|
|||
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/x-pack/plugins/integration_assistant',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/integration_assistant/{common,server}/**/*.{ts,tsx}',
|
||||
'<rootDir>/x-pack/plugins/integration_assistant/{common,server,public}/**/*.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/integration_assistant/{__jest__}/**/*',
|
||||
'!<rootDir>/x-pack/plugins/integration_assistant/*.test.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/integration_assistant/*.config.ts',
|
||||
'!<rootDir>/x-pack/plugins/integration_assistant/**/*.test.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/integration_assistant/**/*.config.ts',
|
||||
],
|
||||
setupFiles: ['jest-canvas-mock'],
|
||||
};
|
||||
|
|
|
@ -2,14 +2,20 @@
|
|||
"type": "plugin",
|
||||
"id": "@kbn/integration-assistant-plugin",
|
||||
"owner": "@elastic/security-solution",
|
||||
"description": "A simple example of how to use core's routing services test",
|
||||
"description": "Plugin implementing the Integration Assistant API and UI",
|
||||
"plugin": {
|
||||
"id": "integrationAssistant",
|
||||
"server": true,
|
||||
"browser": false,
|
||||
"configPath": ["xpack", "integration_assistant"],
|
||||
"requiredPlugins": ["actions", "licensing", "management", "features", "share", "fileUpload"],
|
||||
"optionalPlugins": ["security", "usageCollection", "console"],
|
||||
"extraPublicDirs": ["common"]
|
||||
"browser": true,
|
||||
"configPath": [
|
||||
"xpack",
|
||||
"integration_assistant"
|
||||
],
|
||||
"requiredPlugins": [
|
||||
"kibanaReact",
|
||||
"triggersActionsUi",
|
||||
"actions",
|
||||
"stackConnectors",
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import React from 'react';
|
||||
import { useKibana } from '../hooks/use_kibana';
|
||||
|
||||
const bottomBarCss = css`
|
||||
animation: none !important; // disable the animation to prevent the footer from flickering
|
||||
`;
|
||||
const containerCss = css`
|
||||
min-height: 40px;
|
||||
`;
|
||||
const contentCss = css`
|
||||
width: 100%;
|
||||
max-width: 730px;
|
||||
`;
|
||||
|
||||
interface ButtonsFooterProps {
|
||||
cancelButtonText?: React.ReactNode;
|
||||
nextButtonText?: React.ReactNode;
|
||||
backButtonText?: React.ReactNode;
|
||||
onNext?: () => void;
|
||||
onBack?: () => void;
|
||||
hideCancel?: boolean;
|
||||
isNextDisabled?: boolean;
|
||||
}
|
||||
export const ButtonsFooter = React.memo<ButtonsFooterProps>(
|
||||
({
|
||||
cancelButtonText,
|
||||
nextButtonText,
|
||||
backButtonText,
|
||||
onNext,
|
||||
onBack,
|
||||
hideCancel = false,
|
||||
isNextDisabled = false,
|
||||
}) => {
|
||||
const integrationsUrl = useKibana().services.application.getUrlForApp('integrations');
|
||||
return (
|
||||
<KibanaPageTemplate.BottomBar paddingSize="s" position="sticky" css={bottomBarCss}>
|
||||
<EuiFlexGroup direction="column" alignItems="center" css={containerCss}>
|
||||
<EuiFlexItem css={contentCss}>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
justifyContent="spaceBetween"
|
||||
alignItems="center"
|
||||
gutterSize="l"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
{!hideCancel && (
|
||||
<EuiLink href={integrationsUrl} color="text">
|
||||
{cancelButtonText || (
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.footer.cancel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
)}
|
||||
</EuiLink>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
justifyContent="flexEnd"
|
||||
alignItems="center"
|
||||
gutterSize="l"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{onBack && (
|
||||
<EuiLink onClick={onBack} color="text">
|
||||
{backButtonText || (
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.footer.back"
|
||||
defaultMessage="Back"
|
||||
/>
|
||||
)}
|
||||
</EuiLink>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{onNext && (
|
||||
<EuiButton fill color="primary" onClick={onNext} isDisabled={isNextDisabled}>
|
||||
{nextButtonText || (
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.footer.next"
|
||||
defaultMessage="Next"
|
||||
/>
|
||||
)}
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</KibanaPageTemplate.BottomBar>
|
||||
);
|
||||
}
|
||||
);
|
||||
ButtonsFooter.displayName = 'ButtonsFooter';
|
|
@ -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 { EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer } from '@elastic/eui';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { css } from '@emotion/react';
|
||||
import integrationsImage from '../images/integrations_light.svg';
|
||||
|
||||
const headerCss = css`
|
||||
> div {
|
||||
padding-block: 0;
|
||||
}
|
||||
`;
|
||||
const imageCss = css`
|
||||
width: 318px;
|
||||
height: 183px;
|
||||
object-fit: cover;
|
||||
object-position: center top;
|
||||
`;
|
||||
|
||||
export const IntegrationImageHeader = React.memo(() => {
|
||||
return (
|
||||
<KibanaPageTemplate.Header css={headerCss}>
|
||||
<EuiFlexGroup direction="column" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiImage alt="create integration background" src={integrationsImage} css={imageCss} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</KibanaPageTemplate.Header>
|
||||
);
|
||||
});
|
||||
IntegrationImageHeader.displayName = 'IntegrationImageHeader';
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
const contentCss = css`
|
||||
width: 100%;
|
||||
max-width: 47em;
|
||||
`;
|
||||
const titleCss = css`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export interface SectionTitleProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
export const SectionTitle = React.memo<SectionTitleProps>(({ title, description }) => {
|
||||
return (
|
||||
<EuiFlexGroup direction="column" alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem css={contentCss}>
|
||||
<EuiTitle size="l">
|
||||
<h1 css={titleCss}>{title}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{description && (
|
||||
<EuiFlexItem css={contentCss}>
|
||||
<EuiText size="s" textAlign="center" color="subdued">
|
||||
{description}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
});
|
||||
SectionTitle.displayName = 'SectionTitle';
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { type PropsWithChildren } from 'react';
|
||||
import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
const contentCss = css`
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
`;
|
||||
const titleCss = css`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export type SectionWrapperProps = PropsWithChildren<{
|
||||
title: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
}>;
|
||||
export const SectionWrapper = React.memo<SectionWrapperProps>(({ children, title, subtitle }) => (
|
||||
<>
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiFlexGroup direction="column" alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem css={contentCss}>
|
||||
<EuiFlexGroup direction="column" alignItems="center" justifyContent="center" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="l">
|
||||
<h1 css={titleCss}>{title}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{subtitle && (
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" textAlign="center" color="subdued">
|
||||
{subtitle}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
{children}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
));
|
||||
SectionWrapper.displayName = 'SectionWrapper';
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { SuccessSection } from './success_section';
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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, { useMemo, type PropsWithChildren } from 'react';
|
||||
|
||||
import { EuiButton, EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { SectionWrapper } from '../section_wrapper';
|
||||
|
||||
export type SectionWrapperProps = PropsWithChildren<{
|
||||
integrationName: string;
|
||||
}>;
|
||||
|
||||
export const SuccessSection = React.memo<SectionWrapperProps>(({ integrationName, children }) => {
|
||||
const getUrlForApp = useKibana().services.application?.getUrlForApp;
|
||||
|
||||
const { installIntegrationUrl, viewIntegrationUrl } = useMemo(() => {
|
||||
if (!getUrlForApp) {
|
||||
return { installIntegrationUrl: '', viewIntegrationUrl: '' };
|
||||
}
|
||||
return {
|
||||
installIntegrationUrl: getUrlForApp?.('fleet', {
|
||||
path: `/integrations/${integrationName}/add-integration`,
|
||||
}),
|
||||
viewIntegrationUrl: getUrlForApp?.('integrations', {
|
||||
path: `/detail/${integrationName}`,
|
||||
}),
|
||||
};
|
||||
}, [integrationName, getUrlForApp]);
|
||||
|
||||
return (
|
||||
<SectionWrapper title={i18n.SUCCESS_TITLE} subtitle={i18n.SUCCESS_DESCRIPTION}>
|
||||
<EuiFlexGroup direction="row" gutterSize="l" alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
paddingSize="l"
|
||||
titleSize="xs"
|
||||
icon={<EuiIcon type="launch" size="l" />}
|
||||
title={i18n.ADD_TO_AGENT_TITLE}
|
||||
description={i18n.ADD_TO_AGENT_DESCRIPTION}
|
||||
footer={<EuiButton href={installIntegrationUrl}>{i18n.ADD_TO_AGENT_BUTTON}</EuiButton>}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
paddingSize="l"
|
||||
titleSize="xs"
|
||||
icon={<EuiIcon type="eye" size="l" />}
|
||||
title={i18n.VIEW_INTEGRATION_TITLE}
|
||||
description={i18n.VIEW_INTEGRATION_DESCRIPTION}
|
||||
footer={<EuiButton href={viewIntegrationUrl}>{i18n.VIEW_INTEGRATION_BUTTON}</EuiButton>}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{children}
|
||||
</SectionWrapper>
|
||||
);
|
||||
});
|
||||
SuccessSection.displayName = 'SuccessSection';
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export const SUCCESS_TITLE = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationSuccess.title',
|
||||
{
|
||||
defaultMessage: 'Success',
|
||||
}
|
||||
);
|
||||
|
||||
export const SUCCESS_DESCRIPTION = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationSuccess.description',
|
||||
{
|
||||
defaultMessage: 'Your integration is successfully created.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_TO_AGENT_TITLE = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationSuccess.addToAgent.title',
|
||||
{
|
||||
defaultMessage: 'Add to an agent',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_TO_AGENT_DESCRIPTION = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationSuccess.addToAgent.description',
|
||||
{
|
||||
defaultMessage: 'Add your new integration to an agent to start collecting data',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_TO_AGENT_BUTTON = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationSuccess.addToAgent.button',
|
||||
{
|
||||
defaultMessage: 'Add to an agent',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_INTEGRATION_TITLE = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationSuccess.viewIntegration.title',
|
||||
{
|
||||
defaultMessage: 'View integration',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_INTEGRATION_DESCRIPTION = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationSuccess.viewIntegration.description',
|
||||
{
|
||||
defaultMessage: 'See detailed information about your new custom integration',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_INTEGRATION_BUTTON = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationSuccess.viewIntegration.button',
|
||||
{
|
||||
defaultMessage: 'View Integration',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 enum Page {
|
||||
landing = 'landing',
|
||||
upload = 'upload',
|
||||
assistant = 'assistant',
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { useKibana as _useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import type { CreateIntegrationServices } from '../../components/create_integration/types';
|
||||
|
||||
export const useKibana = () => _useKibana<CreateIntegrationServices>();
|
|
@ -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 { useCallback } from 'react';
|
||||
import { Page } from '../constants';
|
||||
import { useKibana } from './use_kibana';
|
||||
|
||||
export { Page }; // re-export for convenience
|
||||
|
||||
const getPathFromPage = (page: Page): string =>
|
||||
page === Page.landing ? '/create' : `/create/${page}`;
|
||||
|
||||
export const useNavigate = () => {
|
||||
const { navigateToApp } = useKibana().services.application;
|
||||
const navigateToPage = useCallback(
|
||||
(page: Page) => {
|
||||
navigateToApp('integrations', { path: getPathFromPage(page) });
|
||||
},
|
||||
[navigateToApp]
|
||||
);
|
||||
return navigateToPage;
|
||||
};
|
||||
|
||||
export const usePageUrl = (page: Page) => {
|
||||
const { getUrlForApp } = useKibana().services.application;
|
||||
return getUrlForApp('integrations', { path: getPathFromPage(page) });
|
||||
};
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 89 KiB |
116
x-pack/plugins/integration_assistant/public/common/lib/api.ts
Normal file
116
x-pack/plugins/integration_assistant/public/common/lib/api.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from '@kbn/core-http-browser';
|
||||
import type {
|
||||
EcsMappingRequestBody,
|
||||
EcsMappingResponse,
|
||||
CategorizationRequestBody,
|
||||
CategorizationResponse,
|
||||
RelatedRequestBody,
|
||||
RelatedResponse,
|
||||
CheckPipelineRequestBody,
|
||||
CheckPipelineResponse,
|
||||
BuildIntegrationRequestBody,
|
||||
} from '../../../common';
|
||||
import {
|
||||
INTEGRATION_BUILDER_PATH,
|
||||
ECS_GRAPH_PATH,
|
||||
CATEGORIZATION_GRAPH_PATH,
|
||||
RELATED_GRAPH_PATH,
|
||||
CHECK_PIPELINE_PATH,
|
||||
} from '../../../common';
|
||||
import { FLEET_PACKAGES_PATH } from '../../../common/constants';
|
||||
|
||||
export interface EpmPackageResponse {
|
||||
response: [{ id: string; name: string }];
|
||||
}
|
||||
|
||||
const defaultHeaders = {
|
||||
'Elastic-Api-Version': '1',
|
||||
};
|
||||
const fleetDefaultHeaders = {
|
||||
'Elastic-Api-Version': '2023-10-31',
|
||||
};
|
||||
|
||||
export interface RequestDeps {
|
||||
http: HttpSetup;
|
||||
abortSignal: AbortSignal;
|
||||
}
|
||||
|
||||
export const runEcsGraph = async (
|
||||
body: EcsMappingRequestBody,
|
||||
{ http, abortSignal }: RequestDeps
|
||||
): Promise<EcsMappingResponse> =>
|
||||
http.post<EcsMappingResponse>(ECS_GRAPH_PATH, {
|
||||
headers: defaultHeaders,
|
||||
body: JSON.stringify(body),
|
||||
signal: abortSignal,
|
||||
});
|
||||
|
||||
export const runCategorizationGraph = async (
|
||||
body: CategorizationRequestBody,
|
||||
{ http, abortSignal }: RequestDeps
|
||||
): Promise<CategorizationResponse> =>
|
||||
http.post<CategorizationResponse>(CATEGORIZATION_GRAPH_PATH, {
|
||||
headers: defaultHeaders,
|
||||
body: JSON.stringify(body),
|
||||
signal: abortSignal,
|
||||
});
|
||||
|
||||
export const runRelatedGraph = async (
|
||||
body: RelatedRequestBody,
|
||||
{ http, abortSignal }: RequestDeps
|
||||
): Promise<RelatedResponse> =>
|
||||
http.post<RelatedResponse>(RELATED_GRAPH_PATH, {
|
||||
headers: defaultHeaders,
|
||||
body: JSON.stringify(body),
|
||||
signal: abortSignal,
|
||||
});
|
||||
|
||||
export const runCheckPipelineResults = async (
|
||||
body: CheckPipelineRequestBody,
|
||||
{ http, abortSignal }: RequestDeps
|
||||
): Promise<CheckPipelineResponse> =>
|
||||
http.post<CheckPipelineResponse>(CHECK_PIPELINE_PATH, {
|
||||
headers: defaultHeaders,
|
||||
body: JSON.stringify(body),
|
||||
signal: abortSignal,
|
||||
});
|
||||
|
||||
export const runBuildIntegration = async (
|
||||
body: BuildIntegrationRequestBody,
|
||||
{ http, abortSignal }: RequestDeps
|
||||
): Promise<Blob> =>
|
||||
http.post<Blob>(INTEGRATION_BUILDER_PATH, {
|
||||
headers: defaultHeaders,
|
||||
body: JSON.stringify(body),
|
||||
signal: abortSignal,
|
||||
});
|
||||
|
||||
export const runInstallPackage = async (
|
||||
zipFile: Blob,
|
||||
{ http, abortSignal }: RequestDeps
|
||||
): Promise<EpmPackageResponse> =>
|
||||
http.post<EpmPackageResponse>(FLEET_PACKAGES_PATH, {
|
||||
headers: {
|
||||
...fleetDefaultHeaders,
|
||||
Accept: 'application/zip',
|
||||
'Content-Type': 'application/zip',
|
||||
},
|
||||
body: zipFile,
|
||||
signal: abortSignal,
|
||||
});
|
||||
|
||||
export const getInstalledPackages = async ({
|
||||
http,
|
||||
abortSignal,
|
||||
}: RequestDeps): Promise<EpmPackageResponse> =>
|
||||
http.get<EpmPackageResponse>(FLEET_PACKAGES_PATH, {
|
||||
headers: fleetDefaultHeaders,
|
||||
signal: abortSignal,
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { EpmPackageResponse } from './api';
|
||||
|
||||
/**
|
||||
* This is a hacky way to get the integration name from the response.
|
||||
* Since the integration name is not returned in the response we have to parse it from the ingest pipeline name.
|
||||
* TODO: Return the package name from the fleet API: https://github.com/elastic/kibana/issues/185932
|
||||
*/
|
||||
export const getIntegrationNameFromResponse = (response: EpmPackageResponse) => {
|
||||
const ingestPipelineName = response.response?.[0]?.id;
|
||||
if (ingestPipelineName) {
|
||||
const match = ingestPipelineName.match(/^.*-([a-z\d_]+)\..*-([\d\.]+)$/);
|
||||
const integrationName = match?.at(1);
|
||||
const version = match?.at(2);
|
||||
if (integrationName && version) {
|
||||
return `${integrationName}-${version}`;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Switch } from 'react-router-dom';
|
||||
import { Route } from '@kbn/shared-ux-router';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { CreateIntegrationServices } from './types';
|
||||
import { CreateIntegrationLanding } from './create_integration_landing';
|
||||
import { CreateIntegrationUpload } from './create_integration_upload';
|
||||
import { CreateIntegrationAssistant } from './create_integration_assistant';
|
||||
|
||||
interface CreateIntegrationProps {
|
||||
services: CreateIntegrationServices;
|
||||
}
|
||||
export const CreateIntegration = React.memo<CreateIntegrationProps>(({ services }) => (
|
||||
<KibanaContextProvider services={services}>
|
||||
<Switch>
|
||||
<Route path={'/create/assistant'} component={CreateIntegrationAssistant} />
|
||||
<Route path={'/create/upload'} component={CreateIntegrationUpload} />
|
||||
<Route path={'/create'} component={CreateIntegrationLanding} />
|
||||
</Switch>
|
||||
</KibanaContextProvider>
|
||||
));
|
||||
CreateIntegration.displayName = 'CreateIntegration';
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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, { useReducer, useMemo, useCallback } from 'react';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { Header } from './header';
|
||||
import { Footer } from './footer';
|
||||
import { ConnectorStep, isConnectorStepReady } from './steps/connector_step';
|
||||
import { IntegrationStep, isIntegrationStepReady } from './steps/integration_step';
|
||||
import { DataStreamStep, isDataStreamStepReady } from './steps/data_stream_step';
|
||||
import { ReviewStep, isReviewStepReady } from './steps/review_step';
|
||||
import { DeployStep } from './steps/deploy_step';
|
||||
import { reducer, initialState, ActionsProvider, type Actions } from './state';
|
||||
|
||||
export const CreateIntegrationAssistant = React.memo(() => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
const actions = useMemo<Actions>(
|
||||
() => ({
|
||||
setStep: (payload) => {
|
||||
dispatch({ type: 'SET_STEP', payload });
|
||||
},
|
||||
setConnectorId: (payload) => {
|
||||
dispatch({ type: 'SET_CONNECTOR_ID', payload });
|
||||
},
|
||||
setIntegrationSettings: (payload) => {
|
||||
dispatch({ type: 'SET_INTEGRATION_SETTINGS', payload });
|
||||
},
|
||||
setIsGenerating: (payload) => {
|
||||
dispatch({ type: 'SET_IS_GENERATING', payload });
|
||||
},
|
||||
setResult: (payload) => {
|
||||
dispatch({ type: 'SET_GENERATED_RESULT', payload });
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const isNextStepEnabled = useMemo(() => {
|
||||
if (state.step === 1) {
|
||||
return isConnectorStepReady(state);
|
||||
} else if (state.step === 2) {
|
||||
return isIntegrationStepReady(state);
|
||||
} else if (state.step === 3) {
|
||||
return isDataStreamStepReady(state);
|
||||
} else if (state.step === 4) {
|
||||
return isReviewStepReady(state);
|
||||
}
|
||||
return false;
|
||||
}, [state]);
|
||||
|
||||
const onGenerate = useCallback(() => actions.setIsGenerating(true), [actions]);
|
||||
|
||||
return (
|
||||
<ActionsProvider value={actions}>
|
||||
<KibanaPageTemplate>
|
||||
<Header currentStep={state.step} isGenerating={state.isGenerating} />
|
||||
<KibanaPageTemplate.Section grow paddingSize="l">
|
||||
{state.step === 1 && <ConnectorStep connectorId={state.connectorId} />}
|
||||
{state.step === 2 && <IntegrationStep integrationSettings={state.integrationSettings} />}
|
||||
{state.step === 3 && (
|
||||
<DataStreamStep
|
||||
integrationSettings={state.integrationSettings}
|
||||
connectorId={state.connectorId}
|
||||
isGenerating={state.isGenerating}
|
||||
/>
|
||||
)}
|
||||
{state.step === 4 && (
|
||||
<ReviewStep
|
||||
integrationSettings={state.integrationSettings}
|
||||
connectorId={state.connectorId}
|
||||
isGenerating={state.isGenerating}
|
||||
result={state.result}
|
||||
/>
|
||||
)}
|
||||
{state.step === 5 && (
|
||||
<DeployStep
|
||||
integrationSettings={state.integrationSettings}
|
||||
result={state.result}
|
||||
connectorId={state.connectorId}
|
||||
/>
|
||||
)}
|
||||
</KibanaPageTemplate.Section>
|
||||
<Footer
|
||||
currentStep={state.step}
|
||||
onGenerate={onGenerate}
|
||||
isGenerating={state.isGenerating}
|
||||
isNextStepEnabled={isNextStepEnabled}
|
||||
/>
|
||||
</KibanaPageTemplate>
|
||||
</ActionsProvider>
|
||||
);
|
||||
});
|
||||
CreateIntegrationAssistant.displayName = 'CreateIntegrationAssistant';
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { ButtonsFooter } from '../../../../common/components/buttons_footer';
|
||||
import { useNavigate, Page } from '../../../../common/hooks/use_navigate';
|
||||
import { useActions, type State } from '../state';
|
||||
import * as i18n from './translations';
|
||||
|
||||
// Generation button for Step 3
|
||||
const AnalyzeButtonText = React.memo<{ isGenerating: boolean }>(({ isGenerating }) => {
|
||||
if (!isGenerating) {
|
||||
return <>{i18n.ANALYZE_LOGS}</>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<EuiLoadingSpinner size="s" />
|
||||
{i18n.LOADING}
|
||||
</>
|
||||
);
|
||||
});
|
||||
AnalyzeButtonText.displayName = 'AnalyzeButtonText';
|
||||
|
||||
interface FooterProps {
|
||||
currentStep: State['step'];
|
||||
isGenerating: State['isGenerating'];
|
||||
onGenerate: () => void;
|
||||
isNextStepEnabled?: boolean;
|
||||
}
|
||||
|
||||
export const Footer = React.memo<FooterProps>(
|
||||
({ currentStep, onGenerate, isGenerating, isNextStepEnabled = false }) => {
|
||||
const { setStep } = useActions();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onBack = useCallback(() => {
|
||||
if (currentStep === 1) {
|
||||
navigate(Page.landing);
|
||||
} else {
|
||||
setStep(currentStep - 1);
|
||||
}
|
||||
}, [currentStep, navigate, setStep]);
|
||||
|
||||
const onNext = useCallback(() => {
|
||||
if (currentStep === 3) {
|
||||
onGenerate();
|
||||
} else {
|
||||
setStep(currentStep + 1);
|
||||
}
|
||||
}, [currentStep, onGenerate, setStep]);
|
||||
|
||||
const nextButtonText = useMemo(() => {
|
||||
if (currentStep === 3) {
|
||||
return <AnalyzeButtonText isGenerating={isGenerating} />;
|
||||
}
|
||||
if (currentStep === 4) {
|
||||
return i18n.ADD_TO_ELASTIC;
|
||||
}
|
||||
}, [currentStep, isGenerating]);
|
||||
|
||||
if (currentStep === 5) {
|
||||
return <ButtonsFooter cancelButtonText={i18n.CLOSE} />;
|
||||
}
|
||||
return (
|
||||
<ButtonsFooter
|
||||
isNextDisabled={!isNextStepEnabled || isGenerating}
|
||||
onBack={onBack}
|
||||
onNext={onNext}
|
||||
nextButtonText={nextButtonText}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Footer.displayName = 'Footer';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { Footer } from './footer';
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export const ANALYZE_LOGS = i18n.translate('xpack.integrationAssistant.bottomBar.analyzeLogs', {
|
||||
defaultMessage: 'Analyze logs',
|
||||
});
|
||||
|
||||
export const LOADING = i18n.translate('xpack.integrationAssistant.bottomBar.loading', {
|
||||
defaultMessage: 'Loading',
|
||||
});
|
||||
|
||||
export const ADD_TO_ELASTIC = i18n.translate('xpack.integrationAssistant.bottomBar.addToElastic', {
|
||||
defaultMessage: 'Add to Elastic',
|
||||
});
|
||||
|
||||
export const CLOSE = i18n.translate('xpack.integrationAssistant.bottomBar.close', {
|
||||
defaultMessage: 'Close',
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiAvatar,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { AssistantAvatar } from '@kbn/elastic-assistant';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import React from 'react';
|
||||
import { useActions } from '../state';
|
||||
import { Steps } from './steps';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const useAvatarCss = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return css`
|
||||
border: 1px solid ${euiTheme.colors.lightShade};
|
||||
padding: ${euiTheme.size.xs};
|
||||
`;
|
||||
};
|
||||
|
||||
const contentCss = css`
|
||||
width: 100%;
|
||||
max-width: 730px;
|
||||
`;
|
||||
|
||||
interface HeaderProps {
|
||||
currentStep: number;
|
||||
isGenerating: boolean;
|
||||
}
|
||||
export const Header = React.memo<HeaderProps>(({ currentStep, isGenerating }) => {
|
||||
const { setStep } = useActions();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const avatarCss = useAvatarCss();
|
||||
return (
|
||||
<KibanaPageTemplate.Header>
|
||||
<EuiFlexGroup direction="column" alignItems="center">
|
||||
<EuiFlexItem css={contentCss}>
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
justifyContent="center"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiAvatar
|
||||
name={i18n.ASSISTANT_AVATAR}
|
||||
size="m"
|
||||
iconType={AssistantAvatar}
|
||||
color={euiTheme.colors.emptyShade}
|
||||
css={avatarCss}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h1>
|
||||
<span>{i18n.TITLE}</span>
|
||||
</h1>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<Steps currentStep={currentStep} setStep={setStep} isGenerating={isGenerating} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</KibanaPageTemplate.Header>
|
||||
);
|
||||
});
|
||||
Header.displayName = 'Header';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { Header } from './header';
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 { EuiStepsHorizontal, type EuiStepStatus, type EuiStepsHorizontalProps } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
interface StepsProps {
|
||||
currentStep: number;
|
||||
setStep: (step: number) => void;
|
||||
isGenerating: boolean;
|
||||
}
|
||||
|
||||
const STEP_CONNECTOR = i18n.translate('xpack.integrationAssistant.step.connector', {
|
||||
defaultMessage: 'Connector',
|
||||
});
|
||||
|
||||
const STEP_INTEGRATION = i18n.translate('xpack.integrationAssistant.step.integration', {
|
||||
defaultMessage: 'Integration',
|
||||
});
|
||||
|
||||
const STEP_DATA_STREAM = i18n.translate('xpack.integrationAssistant.step.dataStream', {
|
||||
defaultMessage: 'Data stream',
|
||||
});
|
||||
|
||||
const STEP_REVIEW = i18n.translate('xpack.integrationAssistant.step.review', {
|
||||
defaultMessage: 'Review',
|
||||
});
|
||||
|
||||
const getStepStatus = (step: number, currentStep: number, loading?: boolean): EuiStepStatus => {
|
||||
if (step === currentStep) {
|
||||
return loading ? 'loading' : 'current';
|
||||
} else if (step < currentStep) {
|
||||
return 'complete';
|
||||
} else {
|
||||
return 'disabled';
|
||||
}
|
||||
};
|
||||
|
||||
const getStepOnClick = (step: number, currentStep: number, setStep: (step: number) => void) => {
|
||||
if (step < currentStep) {
|
||||
return () => setStep(step);
|
||||
}
|
||||
return () => {};
|
||||
};
|
||||
|
||||
export const Steps = React.memo<StepsProps>(({ currentStep, setStep, isGenerating }) => {
|
||||
const steps = useMemo<EuiStepsHorizontalProps['steps']>(() => {
|
||||
return [
|
||||
{
|
||||
title: STEP_CONNECTOR,
|
||||
status: getStepStatus(1, currentStep),
|
||||
onClick: getStepOnClick(1, currentStep, setStep),
|
||||
},
|
||||
{
|
||||
title: STEP_INTEGRATION,
|
||||
status: getStepStatus(2, currentStep),
|
||||
onClick: getStepOnClick(2, currentStep, setStep),
|
||||
},
|
||||
{
|
||||
title: STEP_DATA_STREAM,
|
||||
status: getStepStatus(3, currentStep, isGenerating),
|
||||
onClick: getStepOnClick(3, currentStep, setStep),
|
||||
},
|
||||
{
|
||||
title: STEP_REVIEW,
|
||||
status: getStepStatus(4, currentStep, isGenerating),
|
||||
onClick: getStepOnClick(4, currentStep, setStep),
|
||||
},
|
||||
];
|
||||
}, [currentStep, setStep, isGenerating]);
|
||||
|
||||
return <EuiStepsHorizontal steps={steps} size="s" />;
|
||||
});
|
||||
Steps.displayName = 'Steps';
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const TITLE = i18n.translate('xpack.integrationAssistant.pages.header.title', {
|
||||
defaultMessage: 'New integration',
|
||||
});
|
||||
|
||||
export const ASSISTANT_AVATAR = i18n.translate(
|
||||
'xpack.integrationAssistant.pages.header.avatarTitle',
|
||||
{
|
||||
defaultMessage: 'Powered by generative AI',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { CreateIntegrationAssistant } from './create_integration_assistant';
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 { createContext, useContext } from 'react';
|
||||
import type { Pipeline, Docs } from '../../../../common';
|
||||
import type { AIConnector, IntegrationSettings } from './types';
|
||||
|
||||
export interface State {
|
||||
step: number;
|
||||
connectorId?: AIConnector['id'];
|
||||
integrationSettings?: IntegrationSettings;
|
||||
isGenerating: boolean;
|
||||
result?: {
|
||||
pipeline: Pipeline;
|
||||
docs: Docs;
|
||||
};
|
||||
}
|
||||
|
||||
export const initialState: State = {
|
||||
step: 1,
|
||||
connectorId: undefined,
|
||||
integrationSettings: undefined,
|
||||
isGenerating: false,
|
||||
result: undefined,
|
||||
};
|
||||
|
||||
type Action =
|
||||
| { type: 'SET_STEP'; payload: State['step'] }
|
||||
| { type: 'SET_CONNECTOR_ID'; payload: State['connectorId'] }
|
||||
| { type: 'SET_INTEGRATION_SETTINGS'; payload: State['integrationSettings'] }
|
||||
| { type: 'SET_IS_GENERATING'; payload: State['isGenerating'] }
|
||||
| { type: 'SET_GENERATED_RESULT'; payload: State['result'] };
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'SET_STEP':
|
||||
return {
|
||||
...state,
|
||||
step: action.payload,
|
||||
isGenerating: false,
|
||||
...(action.payload < state.step && { result: undefined }), // reset the result when we go back
|
||||
};
|
||||
case 'SET_CONNECTOR_ID':
|
||||
return { ...state, connectorId: action.payload };
|
||||
case 'SET_INTEGRATION_SETTINGS':
|
||||
return { ...state, integrationSettings: action.payload };
|
||||
case 'SET_IS_GENERATING':
|
||||
return { ...state, isGenerating: action.payload };
|
||||
case 'SET_GENERATED_RESULT':
|
||||
return { ...state, result: action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Actions {
|
||||
setStep: (payload: State['step']) => void;
|
||||
setConnectorId: (payload: State['connectorId']) => void;
|
||||
setIntegrationSettings: (payload: State['integrationSettings']) => void;
|
||||
setIsGenerating: (payload: State['isGenerating']) => void;
|
||||
setResult: (payload: State['result']) => void;
|
||||
}
|
||||
|
||||
const ActionsContext = createContext<Actions | undefined>(undefined);
|
||||
export const ActionsProvider = ActionsContext.Provider;
|
||||
export const useActions = () => {
|
||||
const actions = useContext(ActionsContext);
|
||||
if (!actions) {
|
||||
throw new Error('useActions must be used within a ActionsProvider');
|
||||
}
|
||||
return actions;
|
||||
};
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 { useEuiTheme, EuiBadge, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiRadio } from '@elastic/eui';
|
||||
import { noop } from 'lodash/fp';
|
||||
import { css } from '@emotion/react';
|
||||
import { useKibana } from '../../../../../common/hooks/use_kibana';
|
||||
import type { AIConnector } from '../../types';
|
||||
import { useActions } from '../../state';
|
||||
|
||||
const useRowCss = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return css`
|
||||
&.euiPanel:hover,
|
||||
&.euiPanel:focus {
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
&.euiPanel:hover {
|
||||
background-color: ${euiTheme.colors.lightestShade};
|
||||
}
|
||||
.euiRadio {
|
||||
color: ${euiTheme.colors.primaryText};
|
||||
label.euiRadio__label {
|
||||
padding-left: ${euiTheme.size.xl} !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
interface ConnectorSelectorProps {
|
||||
connectors: AIConnector[];
|
||||
selectedConnectorId: string | undefined;
|
||||
}
|
||||
export const ConnectorSelector = React.memo<ConnectorSelectorProps>(
|
||||
({ connectors, selectedConnectorId }) => {
|
||||
const {
|
||||
triggersActionsUi: { actionTypeRegistry },
|
||||
} = useKibana().services;
|
||||
const { setConnectorId } = useActions();
|
||||
const rowCss = useRowCss();
|
||||
return (
|
||||
<>
|
||||
{connectors.map((connector) => (
|
||||
<EuiFlexItem key={connector.id}>
|
||||
<EuiPanel
|
||||
key={connector.id}
|
||||
onClick={() => setConnectorId(connector.id)}
|
||||
hasShadow={false}
|
||||
hasBorder
|
||||
paddingSize="l"
|
||||
css={rowCss}
|
||||
>
|
||||
<EuiFlexGroup direction="row" alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiRadio
|
||||
label={connector.name}
|
||||
id={connector.id}
|
||||
checked={selectedConnectorId === connector.id}
|
||||
onChange={noop}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="hollow">
|
||||
{actionTypeRegistry.get(connector.actionTypeId).actionTypeTitle}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
ConnectorSelector.displayName = 'ConnectorSelector';
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
useEuiTheme,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiListGroup,
|
||||
EuiIcon,
|
||||
EuiPanel,
|
||||
EuiLoadingSpinner,
|
||||
EuiText,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import {
|
||||
ConnectorAddModal,
|
||||
type ActionConnector,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public/common/constants';
|
||||
import { useLoadActionTypes } from '@kbn/elastic-assistant/impl/connectorland/use_load_action_types';
|
||||
import type { ActionType } from '@kbn/actions-plugin/common';
|
||||
import { useKibana } from '../../../../../common/hooks/use_kibana';
|
||||
|
||||
const usePanelCss = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return css`
|
||||
&.euiPanel:hover {
|
||||
background-color: ${euiTheme.colors.lightestShade};
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
interface ConnectorSetupProps {
|
||||
onConnectorSaved?: (savedAction: ActionConnector) => void;
|
||||
onClose?: () => void;
|
||||
actionTypeIds?: string[];
|
||||
compressed?: boolean;
|
||||
}
|
||||
export const ConnectorSetup = React.memo<ConnectorSetupProps>(
|
||||
({ onConnectorSaved, onClose, actionTypeIds, compressed = false }) => {
|
||||
const panelCss = usePanelCss();
|
||||
const {
|
||||
http,
|
||||
triggersActionsUi: { actionTypeRegistry },
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const [selectedActionType, setSelectedActionType] = useState<ActionType | null>(null);
|
||||
|
||||
const onModalClose = useCallback(() => {
|
||||
setSelectedActionType(null);
|
||||
onClose?.();
|
||||
}, [onClose]);
|
||||
|
||||
const { data } = useLoadActionTypes({ http, toasts });
|
||||
|
||||
const actionTypes = useMemo(() => {
|
||||
if (actionTypeIds && data) {
|
||||
return data.filter((actionType) => actionTypeIds.includes(actionType.id));
|
||||
}
|
||||
return data;
|
||||
}, [data, actionTypeIds]);
|
||||
|
||||
if (!actionTypes) {
|
||||
return <EuiLoadingSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{compressed ? (
|
||||
<EuiListGroup
|
||||
flush
|
||||
listItems={actionTypes.map((actionType) => ({
|
||||
id: actionType.id,
|
||||
label: actionType.name,
|
||||
size: 's',
|
||||
icon: (
|
||||
<EuiIcon
|
||||
size="l"
|
||||
color="text"
|
||||
type={actionTypeRegistry.get(actionType.id).iconClass}
|
||||
/>
|
||||
),
|
||||
isDisabled: !actionType.enabled,
|
||||
onClick: () => setSelectedActionType(actionType),
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{actionTypes?.map((actionType: ActionType) => (
|
||||
<EuiFlexItem data-test-subj="action-option" key={actionType.id}>
|
||||
<EuiLink onClick={() => setSelectedActionType(actionType)}>
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="l" css={panelCss}>
|
||||
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon
|
||||
size="xl"
|
||||
color="text"
|
||||
type={actionTypeRegistry.get(actionType.id).iconClass}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">{actionType.name}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
|
||||
{selectedActionType && (
|
||||
<ConnectorAddModal
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
actionType={selectedActionType}
|
||||
onClose={onModalClose}
|
||||
postSaveEventHandler={onConnectorSaved}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
ConnectorSetup.displayName = 'ConnectorSetup';
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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, { useCallback, useEffect, useState } from 'react';
|
||||
import { useLoadConnectors } from '@kbn/elastic-assistant';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiPopover, EuiLink } from '@elastic/eui';
|
||||
import { useKibana } from '../../../../../common/hooks/use_kibana';
|
||||
import { StepContentWrapper } from '../step_content_wrapper';
|
||||
import { ConnectorSelector } from './connector_selector';
|
||||
import { ConnectorSetup } from './connector_setup';
|
||||
import type { AIConnector } from '../../types';
|
||||
import { useActions } from '../../state';
|
||||
import * as i18n from './translations';
|
||||
|
||||
/**
|
||||
* List of allowed action type IDs for the integrations assistant.
|
||||
* Replace by ['.bedrock', '.gen-ai'] to allow OpenAI connectors.
|
||||
*/
|
||||
const AllowedActionTypeIds = ['.bedrock'];
|
||||
|
||||
interface ConnectorStepProps {
|
||||
connectorId: string | undefined;
|
||||
}
|
||||
export const ConnectorStep = React.memo<ConnectorStepProps>(({ connectorId }) => {
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const { setConnectorId } = useActions();
|
||||
const [connectors, setConnectors] = useState<AIConnector[]>();
|
||||
const {
|
||||
isLoading,
|
||||
data: aiConnectors,
|
||||
refetch: refetchConnectors,
|
||||
} = useLoadConnectors({ http, toasts });
|
||||
|
||||
useEffect(() => {
|
||||
if (aiConnectors != null) {
|
||||
// filter out connectors, this is temporary until we add support for more models
|
||||
const filteredAiConnectors = aiConnectors.filter(({ actionTypeId }) =>
|
||||
AllowedActionTypeIds.includes(actionTypeId)
|
||||
);
|
||||
setConnectors(filteredAiConnectors);
|
||||
if (filteredAiConnectors && filteredAiConnectors.length === 1) {
|
||||
// pre-select the connector if there is only one
|
||||
setConnectorId(filteredAiConnectors[0].id);
|
||||
}
|
||||
}
|
||||
}, [aiConnectors, setConnectorId]);
|
||||
|
||||
const onConnectorSaved = useCallback(() => refetchConnectors(), [refetchConnectors]);
|
||||
|
||||
const hasConnectors = !isLoading && connectors?.length;
|
||||
|
||||
return (
|
||||
<StepContentWrapper
|
||||
title={i18n.TITLE}
|
||||
subtitle={i18n.DESCRIPTION}
|
||||
right={hasConnectors ? <CreateConnectorPopover onConnectorSaved={onConnectorSaved} /> : null}
|
||||
>
|
||||
<EuiFlexGroup direction="column" alignItems="stretch">
|
||||
<EuiFlexItem>
|
||||
{isLoading ? (
|
||||
<EuiLoadingSpinner />
|
||||
) : (
|
||||
<>
|
||||
{hasConnectors ? (
|
||||
<EuiFlexGroup alignItems="stretch" direction="column" gutterSize="s">
|
||||
<ConnectorSelector connectors={connectors} selectedConnectorId={connectorId} />
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<ConnectorSetup
|
||||
actionTypeIds={AllowedActionTypeIds}
|
||||
onConnectorSaved={onConnectorSaved}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</StepContentWrapper>
|
||||
);
|
||||
});
|
||||
ConnectorStep.displayName = 'ConnectorStep';
|
||||
|
||||
interface CreateConnectorPopoverProps {
|
||||
onConnectorSaved: () => void;
|
||||
}
|
||||
const CreateConnectorPopover = React.memo<CreateConnectorPopoverProps>(({ onConnectorSaved }) => {
|
||||
const [isOpen, setIsPopoverOpen] = useState(false);
|
||||
const openPopover = useCallback(() => setIsPopoverOpen(true), []);
|
||||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
|
||||
const onConnectorSavedAndClose = useCallback(() => {
|
||||
onConnectorSaved();
|
||||
closePopover();
|
||||
}, [onConnectorSaved, closePopover]);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={<EuiLink onClick={openPopover}>{i18n.CREATE_CONNECTOR}</EuiLink>}
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopover}
|
||||
>
|
||||
<EuiFlexGroup alignItems="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ConnectorSetup
|
||||
actionTypeIds={AllowedActionTypeIds}
|
||||
onConnectorSaved={onConnectorSavedAndClose}
|
||||
onClose={closePopover}
|
||||
compressed
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopover>
|
||||
);
|
||||
});
|
||||
CreateConnectorPopover.displayName = 'CreateConnectorPopover';
|
|
@ -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 { ConnectorStep } from './connector_step';
|
||||
export * from './is_step_ready';
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { State } from '../../state';
|
||||
|
||||
export const isConnectorStepReady = ({ connectorId }: State) => connectorId != null;
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export const TITLE = i18n.translate('xpack.integrationAssistant.steps.connector.title', {
|
||||
defaultMessage: 'Choose your AI connector',
|
||||
});
|
||||
|
||||
export const DESCRIPTION = i18n.translate(
|
||||
'xpack.integrationAssistant.steps.connector.description',
|
||||
{
|
||||
defaultMessage: 'Select an AI connector to help you create your custom integration',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_CONNECTOR = i18n.translate(
|
||||
'xpack.integrationAssistant.steps.connector.createConnectorLabel',
|
||||
{
|
||||
defaultMessage: 'Create new connector',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,230 @@
|
|||
/*
|
||||
* 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, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiPanel,
|
||||
EuiSelect,
|
||||
} from '@elastic/eui';
|
||||
import type { InputType } from '../../../../../../common';
|
||||
import { useActions, type State } from '../../state';
|
||||
import type { IntegrationSettings } from '../../types';
|
||||
import { StepContentWrapper } from '../step_content_wrapper';
|
||||
import { SampleLogsInput } from './sample_logs_input';
|
||||
import type { OnComplete } from './generation_modal';
|
||||
import { GenerationModal } from './generation_modal';
|
||||
import { useLoadPackageNames } from './use_load_package_names';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const InputTypeOptions: Array<{ value: InputType; text: string }> = [
|
||||
{ value: 'aws_cloudwatch', text: 'AWS Cloudwatch' },
|
||||
{ value: 'aws_s3', text: 'AWS S3' },
|
||||
{ value: 'azure_blob_storage', text: 'Azure Blob Storage' },
|
||||
{ value: 'azure_eventhub', text: 'Azure Event Hub' },
|
||||
{ value: 'cloudfoundry', text: 'Cloud Foundry' },
|
||||
{ value: 'filestream', text: 'File Stream' },
|
||||
{ value: 'gcp_pubsub', text: 'GCP Pub/Sub' },
|
||||
{ value: 'gcs', text: 'Google Cloud Storage' },
|
||||
{ value: 'http_endpoint', text: 'HTTP Endpoint' },
|
||||
{ value: 'journald', text: 'Journald' },
|
||||
{ value: 'kafka', text: 'Kafka' },
|
||||
{ value: 'tcp', text: 'TCP' },
|
||||
{ value: 'udp', text: 'UDP' },
|
||||
];
|
||||
|
||||
const isValidName = (name: string) => /^[a-z0-9_]+$/.test(name);
|
||||
const getNameFromTitle = (title: string) => title.toLowerCase().replaceAll(/[^a-z0-9]/g, '_');
|
||||
|
||||
interface DataStreamStepProps {
|
||||
integrationSettings: State['integrationSettings'];
|
||||
connectorId: State['connectorId'];
|
||||
isGenerating: State['isGenerating'];
|
||||
}
|
||||
export const DataStreamStep = React.memo<DataStreamStepProps>(
|
||||
({ integrationSettings, connectorId, isGenerating }) => {
|
||||
const { setIntegrationSettings, setIsGenerating, setStep, setResult } = useActions();
|
||||
const { isLoading: isLoadingPackageNames, packageNames } = useLoadPackageNames(); // this is used to avoid duplicate names
|
||||
|
||||
const [name, setName] = useState<string>(integrationSettings?.name ?? '');
|
||||
const [dataStreamName, setDataStreamName] = useState<string>(
|
||||
integrationSettings?.dataStreamName ?? ''
|
||||
);
|
||||
const [invalidFields, setInvalidFields] = useState({ name: false, dataStreamName: false });
|
||||
|
||||
const setIntegrationValues = useCallback(
|
||||
(settings: Partial<IntegrationSettings>) =>
|
||||
setIntegrationSettings({ ...integrationSettings, ...settings }),
|
||||
[integrationSettings, setIntegrationSettings]
|
||||
);
|
||||
|
||||
const onChange = useMemo(() => {
|
||||
return {
|
||||
name: (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const nextName = e.target.value;
|
||||
setName(nextName);
|
||||
if (!isValidName(nextName) || packageNames?.has(nextName)) {
|
||||
setInvalidFields((current) => ({ ...current, name: true }));
|
||||
setIntegrationValues({ name: undefined });
|
||||
} else {
|
||||
setInvalidFields((current) => ({ ...current, name: false }));
|
||||
setIntegrationValues({ name: nextName });
|
||||
}
|
||||
},
|
||||
dataStreamName: (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const nextDataStreamName = e.target.value;
|
||||
setDataStreamName(nextDataStreamName);
|
||||
if (!isValidName(nextDataStreamName)) {
|
||||
setInvalidFields((current) => ({ ...current, dataStreamName: true }));
|
||||
setIntegrationValues({ dataStreamName: undefined });
|
||||
} else {
|
||||
setInvalidFields((current) => ({ ...current, dataStreamName: false }));
|
||||
setIntegrationValues({ dataStreamName: nextDataStreamName });
|
||||
}
|
||||
},
|
||||
// inputs without validation
|
||||
dataStreamTitle: (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setIntegrationValues({ dataStreamTitle: e.target.value }),
|
||||
dataStreamDescription: (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setIntegrationValues({ dataStreamDescription: e.target.value }),
|
||||
inputType: (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setIntegrationValues({ inputType: e.target.value as InputType });
|
||||
},
|
||||
};
|
||||
}, [setIntegrationValues, setInvalidFields, packageNames]);
|
||||
|
||||
useEffect(() => {
|
||||
// Pre-populates the name from the title set in the previous step.
|
||||
// Only executed once when the packageNames are loaded
|
||||
if (packageNames != null && integrationSettings?.name == null && integrationSettings?.title) {
|
||||
const generatedName = getNameFromTitle(integrationSettings.title);
|
||||
if (!packageNames.has(generatedName)) {
|
||||
setName(generatedName);
|
||||
setIntegrationValues({ name: generatedName });
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [packageNames]);
|
||||
|
||||
const onGenerationCompleted = useCallback<OnComplete>(
|
||||
(result: State['result']) => {
|
||||
if (result) {
|
||||
setResult(result);
|
||||
setIsGenerating(false);
|
||||
setStep(4);
|
||||
}
|
||||
},
|
||||
[setResult, setIsGenerating, setStep]
|
||||
);
|
||||
const onGenerationClosed = useCallback(() => {
|
||||
setIsGenerating(false); // aborts generation
|
||||
}, [setIsGenerating]);
|
||||
|
||||
const nameInputError = useMemo(() => {
|
||||
if (packageNames && name && packageNames.has(name)) {
|
||||
return i18n.NAME_ALREADY_EXISTS_ERROR;
|
||||
}
|
||||
}, [packageNames, name]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
<EuiFlexItem>
|
||||
<StepContentWrapper
|
||||
title={i18n.INTEGRATION_NAME_TITLE}
|
||||
subtitle={i18n.INTEGRATION_NAME_DESCRIPTION}
|
||||
>
|
||||
<EuiPanel hasShadow={false} hasBorder>
|
||||
<EuiForm component="form" fullWidth>
|
||||
<EuiFormRow
|
||||
label={i18n.INTEGRATION_NAME_LABEL}
|
||||
helpText={
|
||||
!nameInputError && !invalidFields.name ? i18n.NO_SPACES_HELP : undefined
|
||||
}
|
||||
isInvalid={!!nameInputError || invalidFields.name}
|
||||
error={[nameInputError ?? i18n.NO_SPACES_HELP]}
|
||||
>
|
||||
<EuiFieldText
|
||||
name="name"
|
||||
value={name}
|
||||
onChange={onChange.name}
|
||||
isInvalid={invalidFields.name}
|
||||
isLoading={isLoadingPackageNames}
|
||||
disabled={isLoadingPackageNames}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</EuiPanel>
|
||||
</StepContentWrapper>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<StepContentWrapper
|
||||
title={i18n.DATA_STREAM_TITLE}
|
||||
subtitle={i18n.DATA_STREAM_DESCRIPTION}
|
||||
>
|
||||
<EuiPanel hasShadow={false} hasBorder>
|
||||
<EuiForm component="form" fullWidth>
|
||||
<EuiFormRow label={i18n.DATA_STREAM_TITLE_LABEL}>
|
||||
<EuiFieldText
|
||||
name="dataStreamTitle"
|
||||
value={integrationSettings?.dataStreamTitle ?? ''}
|
||||
onChange={onChange.dataStreamTitle}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow label={i18n.DATA_STREAM_DESCRIPTION_LABEL}>
|
||||
<EuiFieldText
|
||||
name="dataStreamDescription"
|
||||
value={integrationSettings?.dataStreamDescription ?? ''}
|
||||
onChange={onChange.dataStreamDescription}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label={i18n.DATA_STREAM_NAME_LABEL}
|
||||
helpText={!invalidFields.dataStreamName ? i18n.NO_SPACES_HELP : undefined}
|
||||
isInvalid={invalidFields.dataStreamName}
|
||||
error={[i18n.NO_SPACES_HELP]}
|
||||
>
|
||||
<EuiFieldText
|
||||
name="dataStreamName"
|
||||
value={dataStreamName}
|
||||
onChange={onChange.dataStreamName}
|
||||
isInvalid={invalidFields.dataStreamName}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow label={i18n.DATA_COLLECTION_METHOD_LABEL}>
|
||||
<EuiSelect
|
||||
name="dataCollectionMethod"
|
||||
options={InputTypeOptions}
|
||||
value={integrationSettings?.inputType ?? ''}
|
||||
onChange={onChange.inputType}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<SampleLogsInput
|
||||
integrationSettings={integrationSettings}
|
||||
setIntegrationSettings={setIntegrationSettings}
|
||||
/>
|
||||
</EuiForm>
|
||||
</EuiPanel>
|
||||
</StepContentWrapper>
|
||||
{isGenerating && (
|
||||
<GenerationModal
|
||||
integrationSettings={integrationSettings}
|
||||
connectorId={connectorId}
|
||||
onComplete={onGenerationCompleted}
|
||||
onClose={onGenerationClosed}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
DataStreamStep.displayName = 'DataStreamStep';
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiLoadingSpinner,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiProgress,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import type {
|
||||
CategorizationRequestBody,
|
||||
EcsMappingRequestBody,
|
||||
RelatedRequestBody,
|
||||
} from '../../../../../../common';
|
||||
import {
|
||||
runCategorizationGraph,
|
||||
runEcsGraph,
|
||||
runRelatedGraph,
|
||||
} from '../../../../../common/lib/api';
|
||||
import { useKibana } from '../../../../../common/hooks/use_kibana';
|
||||
import type { State } from '../../state';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export type OnComplete = (result: State['result']) => void;
|
||||
|
||||
const ProgressOrder = ['ecs', 'categorization', 'related'];
|
||||
type ProgressItem = typeof ProgressOrder[number];
|
||||
|
||||
const progressText: Record<ProgressItem, string> = {
|
||||
ecs: i18n.PROGRESS_ECS_MAPPING,
|
||||
categorization: i18n.PROGRESS_CATEGORIZATION,
|
||||
related: i18n.PROGRESS_RELATED_GRAPH,
|
||||
};
|
||||
|
||||
interface UseGenerationProps {
|
||||
integrationSettings: State['integrationSettings'];
|
||||
connectorId: State['connectorId'];
|
||||
onComplete: OnComplete;
|
||||
}
|
||||
export const useGeneration = ({
|
||||
integrationSettings,
|
||||
connectorId,
|
||||
onComplete,
|
||||
}: UseGenerationProps) => {
|
||||
const { http, notifications } = useKibana().services;
|
||||
const [progress, setProgress] = useState<ProgressItem>();
|
||||
const [error, setError] = useState<null | string>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (http == null || integrationSettings == null || notifications?.toasts == null) {
|
||||
return;
|
||||
}
|
||||
const abortController = new AbortController();
|
||||
const deps = { http, abortSignal: abortController.signal };
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const ecsRequest: EcsMappingRequestBody = {
|
||||
packageName: integrationSettings.name ?? '',
|
||||
dataStreamName: integrationSettings.dataStreamName ?? '',
|
||||
rawSamples: integrationSettings.logsSampleParsed ?? [],
|
||||
connectorId: connectorId ?? '',
|
||||
};
|
||||
|
||||
setProgress('ecs');
|
||||
const ecsGraphResult = await runEcsGraph(ecsRequest, deps);
|
||||
if (abortController.signal.aborted) return;
|
||||
if (isEmpty(ecsGraphResult?.results)) {
|
||||
setError('No results from ECS graph');
|
||||
return;
|
||||
}
|
||||
const categorizationRequest: CategorizationRequestBody = {
|
||||
...ecsRequest,
|
||||
currentPipeline: ecsGraphResult.results.pipeline,
|
||||
};
|
||||
|
||||
setProgress('categorization');
|
||||
const categorizationResult = await runCategorizationGraph(categorizationRequest, deps);
|
||||
if (abortController.signal.aborted) return;
|
||||
const relatedRequest: RelatedRequestBody = {
|
||||
...categorizationRequest,
|
||||
currentPipeline: categorizationResult.results.pipeline,
|
||||
};
|
||||
|
||||
setProgress('related');
|
||||
const relatedGraphResult = await runRelatedGraph(relatedRequest, deps);
|
||||
if (abortController.signal.aborted) return;
|
||||
if (!isEmpty(relatedGraphResult?.results)) {
|
||||
onComplete(relatedGraphResult.results);
|
||||
}
|
||||
} catch (e) {
|
||||
if (abortController.signal.aborted) return;
|
||||
setError(`Error: ${e.body.message}`);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [onComplete, setProgress, connectorId, http, integrationSettings, notifications?.toasts]);
|
||||
|
||||
return {
|
||||
progress,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
const useModalCss = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return {
|
||||
headerCss: css`
|
||||
justify-content: center;
|
||||
margin-top: ${euiTheme.size.m};
|
||||
`,
|
||||
bodyCss: css`
|
||||
padding: ${euiTheme.size.xxxxl};
|
||||
min-width: 600px;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
interface GenerationModalProps {
|
||||
integrationSettings: State['integrationSettings'];
|
||||
connectorId: State['connectorId'];
|
||||
onComplete: OnComplete;
|
||||
onClose: () => void;
|
||||
}
|
||||
export const GenerationModal = React.memo<GenerationModalProps>(
|
||||
({ integrationSettings, connectorId, onComplete, onClose }) => {
|
||||
const { headerCss, bodyCss } = useModalCss();
|
||||
const { progress, error } = useGeneration({
|
||||
integrationSettings,
|
||||
connectorId,
|
||||
onComplete,
|
||||
});
|
||||
|
||||
const progressValue = useMemo<number>(
|
||||
() => (progress ? ProgressOrder.indexOf(progress) + 1 : 0),
|
||||
[progress]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiModal onClose={onClose}>
|
||||
<EuiModalHeader css={headerCss}>
|
||||
<EuiModalHeaderTitle>{i18n.ANALYZING}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody css={bodyCss}>
|
||||
<EuiFlexGroup direction="column" gutterSize="l" justifyContent="center">
|
||||
{progress && (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{!error && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="s" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color="subdued">
|
||||
{progressText[progress]}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiProgress value={progressValue} max={4} color="primary" size="m" />
|
||||
</EuiFlexItem>
|
||||
{error && (
|
||||
<EuiFlexItem>
|
||||
<EuiText color="danger" size="xs">
|
||||
{error}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiSpacer size="xl" />
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
);
|
||||
}
|
||||
);
|
||||
GenerationModal.displayName = 'GenerationModal';
|
|
@ -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 { DataStreamStep } from './data_stream_step';
|
||||
export * from './is_step_ready';
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { State } from '../../state';
|
||||
|
||||
export const isDataStreamStepReady = ({ integrationSettings }: State) =>
|
||||
Boolean(
|
||||
integrationSettings?.name &&
|
||||
integrationSettings?.dataStreamTitle &&
|
||||
integrationSettings?.dataStreamDescription &&
|
||||
integrationSettings?.dataStreamName &&
|
||||
integrationSettings?.logsSampleParsed
|
||||
);
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* 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, { useCallback, useState } from 'react';
|
||||
import { EuiCallOut, EuiFilePicker, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { isPlainObject } from 'lodash/fp';
|
||||
import type { IntegrationSettings } from '../../types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const MaxLogsSampleRows = 10;
|
||||
|
||||
/**
|
||||
* Parse the logs sample file content (json or ndjson) and return the parsed logs sample
|
||||
*/
|
||||
const parseLogsContent = (
|
||||
fileContent: string | undefined,
|
||||
fileType: string
|
||||
): { error?: string; isTruncated?: boolean; logsSampleParsed?: string[] } => {
|
||||
if (fileContent == null) {
|
||||
return { error: i18n.LOGS_SAMPLE_ERROR.CAN_NOT_READ };
|
||||
}
|
||||
let parsedContent;
|
||||
try {
|
||||
if (fileType === 'application/json') {
|
||||
parsedContent = JSON.parse(fileContent);
|
||||
} else if (fileType === 'application/x-ndjson') {
|
||||
parsedContent = fileContent
|
||||
.split('\n')
|
||||
.filter((line) => line.trim() !== '')
|
||||
.map((line) => JSON.parse(line));
|
||||
}
|
||||
} catch (_) {
|
||||
return { error: i18n.LOGS_SAMPLE_ERROR.FORMAT(fileType) };
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsedContent)) {
|
||||
return { error: i18n.LOGS_SAMPLE_ERROR.NOT_ARRAY };
|
||||
}
|
||||
if (parsedContent.length === 0) {
|
||||
return { error: i18n.LOGS_SAMPLE_ERROR.EMPTY };
|
||||
}
|
||||
|
||||
let isTruncated = false;
|
||||
if (parsedContent.length > MaxLogsSampleRows) {
|
||||
parsedContent = parsedContent.slice(0, MaxLogsSampleRows);
|
||||
isTruncated = true;
|
||||
}
|
||||
|
||||
if (parsedContent.some((log) => !isPlainObject(log))) {
|
||||
return { error: i18n.LOGS_SAMPLE_ERROR.NOT_OBJECT };
|
||||
}
|
||||
|
||||
const logsSampleParsed = parsedContent.map((log) => JSON.stringify(log));
|
||||
return { isTruncated, logsSampleParsed };
|
||||
};
|
||||
|
||||
interface SampleLogsInputProps {
|
||||
integrationSettings: IntegrationSettings | undefined;
|
||||
setIntegrationSettings: (param: IntegrationSettings) => void;
|
||||
}
|
||||
export const SampleLogsInput = React.memo<SampleLogsInputProps>(
|
||||
({ integrationSettings, setIntegrationSettings }) => {
|
||||
const { notifications } = useKibana().services;
|
||||
const [isParsing, setIsParsing] = useState(false);
|
||||
const [sampleFileError, setSampleFileError] = useState<string>();
|
||||
|
||||
const onChangeLogsSample = useCallback(
|
||||
(files: FileList | null) => {
|
||||
const logsSampleFile = files?.[0];
|
||||
if (logsSampleFile == null) {
|
||||
setSampleFileError(undefined);
|
||||
setIntegrationSettings({ ...integrationSettings, logsSampleParsed: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
const fileContent = e.target?.result as string | undefined; // We can safely cast to string since we call `readAsText` to load the file.
|
||||
const { error, isTruncated, logsSampleParsed } = parseLogsContent(
|
||||
fileContent,
|
||||
logsSampleFile.type
|
||||
);
|
||||
setIsParsing(false);
|
||||
setSampleFileError(error);
|
||||
if (error) {
|
||||
setIntegrationSettings({ ...integrationSettings, logsSampleParsed: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTruncated) {
|
||||
notifications?.toasts.addInfo(i18n.LOGS_SAMPLE_TRUNCATED(MaxLogsSampleRows));
|
||||
}
|
||||
|
||||
setIntegrationSettings({
|
||||
...integrationSettings,
|
||||
logsSampleParsed,
|
||||
});
|
||||
};
|
||||
setIsParsing(true);
|
||||
reader.readAsText(logsSampleFile);
|
||||
},
|
||||
[integrationSettings, setIntegrationSettings, notifications?.toasts, setIsParsing]
|
||||
);
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.LOGS_SAMPLE_LABEL}
|
||||
helpText={
|
||||
<EuiText color="danger" size="xs">
|
||||
{sampleFileError}
|
||||
</EuiText>
|
||||
}
|
||||
isInvalid={sampleFileError != null}
|
||||
>
|
||||
<>
|
||||
<EuiCallOut iconType="iInCircle" color="warning">
|
||||
{i18n.LOGS_SAMPLE_WARNING}
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFilePicker
|
||||
id="logsSampleFilePicker"
|
||||
initialPromptText={
|
||||
<>
|
||||
<EuiText size="s" textAlign="center">
|
||||
{i18n.LOGS_SAMPLE_DESCRIPTION}
|
||||
</EuiText>
|
||||
<EuiText size="xs" color="subdued" textAlign="center">
|
||||
{i18n.LOGS_SAMPLE_DESCRIPTION_2}
|
||||
</EuiText>
|
||||
</>
|
||||
}
|
||||
onChange={onChangeLogsSample}
|
||||
display="large"
|
||||
aria-label="Upload logs sample file"
|
||||
accept="application/json,application/x-ndjson"
|
||||
isLoading={isParsing}
|
||||
/>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
);
|
||||
SampleLogsInput.displayName = 'SampleLogsInput';
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export const INTEGRATION_NAME_TITLE = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.integrationNameTitle',
|
||||
{
|
||||
defaultMessage: 'Define package name',
|
||||
}
|
||||
);
|
||||
export const INTEGRATION_NAME_DESCRIPTION = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.integrationNameDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
"The package name is used to refer to the integration in Elastic's ingest pipeline",
|
||||
}
|
||||
);
|
||||
export const DATA_STREAM_TITLE = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.dataStreamTitle',
|
||||
{
|
||||
defaultMessage: 'Define data stream and upload logs',
|
||||
}
|
||||
);
|
||||
export const DATA_STREAM_DESCRIPTION = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.dataStreamDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Logs are analyzed to automatically map ECS fields and help create the ingestion pipeline',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATION_NAME_LABEL = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.integrationName.label',
|
||||
{
|
||||
defaultMessage: 'Integration package name',
|
||||
}
|
||||
);
|
||||
export const NO_SPACES_HELP = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.noSpacesHelpText',
|
||||
{
|
||||
defaultMessage: 'Name can only contain lowercase letters, numbers, and underscore (_)',
|
||||
}
|
||||
);
|
||||
export const PACKAGE_NAMES_FETCH_ERROR = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.packageNamesFetchError',
|
||||
{
|
||||
defaultMessage: 'Error fetching package names',
|
||||
}
|
||||
);
|
||||
export const NAME_ALREADY_EXISTS_ERROR = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.nameAlreadyExistsError',
|
||||
{
|
||||
defaultMessage: 'This integration name is already in use. Please choose a different name.',
|
||||
}
|
||||
);
|
||||
|
||||
export const DATA_STREAM_TITLE_LABEL = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.dataStreamTitle.label',
|
||||
{
|
||||
defaultMessage: 'Data stream title',
|
||||
}
|
||||
);
|
||||
|
||||
export const DATA_STREAM_DESCRIPTION_LABEL = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.dataStreamDescription.label',
|
||||
{
|
||||
defaultMessage: 'Data stream description',
|
||||
}
|
||||
);
|
||||
|
||||
export const DATA_STREAM_NAME_LABEL = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.dataStreamName.label',
|
||||
{
|
||||
defaultMessage: 'Data stream name',
|
||||
}
|
||||
);
|
||||
|
||||
export const DATA_COLLECTION_METHOD_LABEL = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.dataCollectionMethod.label',
|
||||
{
|
||||
defaultMessage: 'Data collection method',
|
||||
}
|
||||
);
|
||||
|
||||
export const LOGS_SAMPLE_LABEL = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.logsSample.label',
|
||||
{
|
||||
defaultMessage: 'Logs',
|
||||
}
|
||||
);
|
||||
|
||||
export const LOGS_SAMPLE_WARNING = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.logsSample.warning',
|
||||
{
|
||||
defaultMessage:
|
||||
'Please note that this data will be analyzed by a third-party AI tool. Ensure that you comply with privacy and security guidelines when selecting data.',
|
||||
}
|
||||
);
|
||||
|
||||
export const LOGS_SAMPLE_DESCRIPTION = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.logsSample.description',
|
||||
{
|
||||
defaultMessage: 'Drag and drop a file or Browse files.',
|
||||
}
|
||||
);
|
||||
export const LOGS_SAMPLE_DESCRIPTION_2 = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.logsSample.description2',
|
||||
{
|
||||
defaultMessage: 'JSON/NDJSON format',
|
||||
}
|
||||
);
|
||||
export const LOGS_SAMPLE_TRUNCATED = (maxRows: number) =>
|
||||
i18n.translate('xpack.integrationAssistant.step.dataStream.logsSample.truncatedWarning', {
|
||||
values: { maxRows },
|
||||
defaultMessage: `The logs sample has been truncated to {maxRows} rows.`,
|
||||
});
|
||||
export const LOGS_SAMPLE_ERROR = {
|
||||
CAN_NOT_READ: i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.logsSample.errorCanNotRead',
|
||||
{
|
||||
defaultMessage: 'Failed to read the logs sample file',
|
||||
}
|
||||
),
|
||||
FORMAT: (fileType: string) =>
|
||||
i18n.translate('xpack.integrationAssistant.step.dataStream.logsSample.errorFormat', {
|
||||
values: { fileType },
|
||||
defaultMessage: 'The logs sample file has not a valid {fileType} format',
|
||||
}),
|
||||
NOT_ARRAY: i18n.translate('xpack.integrationAssistant.step.dataStream.logsSample.errorNotArray', {
|
||||
defaultMessage: 'The logs sample file is not an array',
|
||||
}),
|
||||
EMPTY: i18n.translate('xpack.integrationAssistant.step.dataStream.logsSample.errorEmpty', {
|
||||
defaultMessage: 'The logs sample file is empty',
|
||||
}),
|
||||
NOT_OBJECT: i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.logsSample.errorNotObject',
|
||||
{
|
||||
defaultMessage: 'The logs sample file contains non-object entries',
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
export const ANALYZING = i18n.translate('xpack.integrationAssistant.step.dataStream.analyzing', {
|
||||
defaultMessage: 'Analyzing',
|
||||
});
|
||||
export const PROGRESS_ECS_MAPPING = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.progress.ecsMapping',
|
||||
{
|
||||
defaultMessage: 'Mapping ECS fields',
|
||||
}
|
||||
);
|
||||
export const PROGRESS_CATEGORIZATION = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.progress.categorization',
|
||||
{
|
||||
defaultMessage: 'Adding categorization',
|
||||
}
|
||||
);
|
||||
export const PROGRESS_RELATED_GRAPH = i18n.translate(
|
||||
'xpack.integrationAssistant.step.dataStream.progress.relatedGraph',
|
||||
{
|
||||
defaultMessage: 'Generating related fields',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import { useKibana } from '../../../../../common/hooks/use_kibana';
|
||||
import { getInstalledPackages } from '../../../../../common/lib/api';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const useLoadPackageNames = () => {
|
||||
const { http, notifications } = useKibana().services;
|
||||
const [packageNames, setPackageNames] = useState<Set<string>>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
const deps = { http, abortSignal: abortController.signal };
|
||||
(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const packagesResponse = await getInstalledPackages(deps);
|
||||
if (abortController.signal.aborted) return;
|
||||
if (!packagesResponse?.response?.length) {
|
||||
throw Error('No packages found');
|
||||
}
|
||||
setPackageNames(new Set(packagesResponse.response.map((pkg) => pkg.name)));
|
||||
} catch (e) {
|
||||
if (!abortController.signal.aborted) {
|
||||
notifications?.toasts.addError(e, {
|
||||
title: i18n.PACKAGE_NAMES_FETCH_ERROR,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [http, notifications]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
packageNames,
|
||||
};
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
/* geoToken icon svg base64 encoded */
|
||||
export const defaultLogoEncoded =
|
||||
'PHN2ZyB3aWR0aD0iMzEiIGhlaWdodD0iMzEiIHZpZXdCb3g9IjAgMCAzMSAzMSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzk4XzcwMDApIj4KPHJlY3Qgd2lkdGg9IjMxIiBoZWlnaHQ9IjMxIiByeD0iMyIgZmlsbD0id2hpdGUiLz4KPHJlY3Qgb3BhY2l0eT0iMC4xIiB3aWR0aD0iMzEiIGhlaWdodD0iMzEiIHJ4PSIzIiBmaWxsPSIjRDZCRjU3Ii8+CjxyZWN0IG9wYWNpdHk9IjAuMyIgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCIgcng9IjIuNSIgc3Ryb2tlPSIjRDZCRjU3Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTUuNSA1LjgxMjVDMTguNjY5MiA1LjgxMjUgMjEuNDgzIDcuMzM0MzUgMjMuMjUwNCA5LjY4NzEyTDIzLjI1IDkuNjg3NUMyNC40NjczIDExLjMwNzkgMjUuMTg3NSAxMy4zMTk4IDI1LjE4NzUgMTUuNUMyNS4xODc1IDE3LjY4MDIgMjQuNDY3MyAxOS42OTIxIDIzLjI1MTkgMjEuMzEwOUwyMy4yNSAyMS4zMTI1QzIxLjQ4MTUgMjMuNjY2NSAxOC42Njg0IDI1LjE4NzUgMTUuNSAyNS4xODc1QzEyLjMzMTYgMjUuMTg3NSA5LjUxODU0IDIzLjY2NjUgNy43NTEwNCAyMS4zMTQ4TDcuNzUgMjEuMzEyNUM2LjUzMzI1IDE5LjY5MzcgNS44MTI1IDE3LjY4MSA1LjgxMjUgMTUuNUM1LjgxMjUgMTAuMTQ5NyAxMC4xNDk3IDUuODEyNSAxNS41IDUuODEyNVpNMTcuMzM2NSAyMS4zMTM1SDEzLjY2MzVDMTQuMjAwNSAyMi41MjQ2IDE0Ljg2OTUgMjMuMjUgMTUuNSAyMy4yNUMxNi4xMzA1IDIzLjI1IDE2Ljc5OTUgMjIuNTI0NiAxNy4zMzY1IDIxLjMxMzVaTTExLjYyNTIgMjEuMzEzOUwxMC4zNzU4IDIxLjMxNDRDMTAuOTA2NSAyMS43ODI0IDExLjUwMTcgMjIuMTc4OSAxMi4xNDY0IDIyLjQ4ODhDMTEuOTU3IDIyLjEyNjUgMTEuNzgyOCAyMS43MzM0IDExLjYyNTIgMjEuMzEzOVpNMjAuNjI0MiAyMS4zMTQ0TDE5LjM3NDggMjEuMzEzOUMxOS4yMTcyIDIxLjczMzQgMTkuMDQzIDIyLjEyNjUgMTguODU0IDIyLjQ4OTJDMTkuNDk4MyAyMi4xNzg5IDIwLjA5MzUgMjEuNzgyNCAyMC42MjQyIDIxLjMxNDRaTTEwLjY4MDIgMTYuNDY5M0w3LjgxMDE0IDE2LjQ3MDJDNy45NDEwOCAxNy41MTg3IDguMjgxNDUgMTguNTAyIDguNzg4MDYgMTkuMzc3MUwxMS4wNTk3IDE5LjM3NjdDMTAuODYxOCAxOC40NzEzIDEwLjczMTEgMTcuNDkzOCAxMC42ODAyIDE2LjQ2OTNaTTE4LjM4MDUgMTYuNDcxSDEyLjYxOTVDMTIuNjc1NSAxNy41MjQ2IDEyLjgyMDYgMTguNTA1NyAxMy4wMjczIDE5LjM3NkgxNy45NzI3QzE4LjE3OTQgMTguNTA1NyAxOC4zMjQ1IDE3LjUyNDYgMTguMzgwNSAxNi40NzFaTTIzLjE4OTkgMTYuNDcwMkwyMC4zMTk4IDE2LjQ2OTNDMjAuMjY4OSAxNy40OTM4IDIwLjEzODIgMTguNDcxMyAxOS45NDAzIDE5LjM3NjdMMjIuMjExOSAxOS4zNzcxQzIyLjcxODUgMTguNTAyIDIzLjA1ODkgMTcuNTE4NyAyMy4xODk5IDE2LjQ3MDJaTTExLjA1OTIgMTEuNjI1M0w4Ljc4Njk1IDExLjYyNDhDOC4yODA2NCAxMi40OTk5IDcuOTQwNTcgMTMuNDgzMyA3LjgwOTkgMTQuNTMxN0wxMC42ODAxIDE0LjUzMjZDMTAuNzMwOSAxMy41MDgyIDEwLjg2MTUgMTIuNTMwNiAxMS4wNTkyIDExLjYyNTNaTTE3Ljk3MzIgMTEuNjI2SDEzLjAyNjhDMTIuODIwMiAxMi40OTYzIDEyLjY3NTMgMTMuNDc3NCAxMi42MTk0IDE0LjUzMUgxOC4zODA2QzE4LjMyNDcgMTMuNDc3NCAxOC4xNzk4IDEyLjQ5NjMgMTcuOTczMiAxMS42MjZaTTIyLjIxMzEgMTEuNjI0OEwxOS45NDA4IDExLjYyNTNDMjAuMTM4NSAxMi41MzA2IDIwLjI2OTEgMTMuNTA4MiAyMC4zMTk5IDE0LjUzMjZMMjMuMTkwMSAxNC41MzE3QzIzLjA1OTQgMTMuNDgzMyAyMi43MTk0IDEyLjQ5OTkgMjIuMjEzMSAxMS42MjQ4Wk0xMi4xNDYgOC41MTA3NUwxMS45MDYyIDguNjMxODZDMTEuMzUyNiA4LjkyMjEyIDEwLjgzODQgOS4yNzczNCAxMC4zNzM4IDkuNjg3MzlMMTEuNjI0NCA5LjY4ODA0QzExLjc4MjIgOS4yNjc4IDExLjk1NjcgOC44NzQwNiAxMi4xNDYgOC41MTA3NVpNMTUuNSA3Ljc1QzE0Ljg2OTEgNy43NSAxNC4xOTk3IDguNDc2MjEgMTMuNjYyNiA5LjY4ODQ4SDE3LjMzNzRDMTYuODAwMyA4LjQ3NjIxIDE2LjEzMDkgNy43NSAxNS41IDcuNzVaTTE4Ljg1MzYgOC41MTExN0wxOC45MjUgOC42NDk5QzE5LjA4NzEgOC45NzM5NyAxOS4yMzc3IDkuMzIwODkgMTkuMzc1NiA5LjY4ODA0TDIwLjYyNjIgOS42ODczOUMyMC4wOTUgOS4yMTg2MSAxOS40OTkxIDguODIxNDkgMTguODUzNiA4LjUxMTE3WiIgZmlsbD0iIzgwNzIzNCIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzk4XzcwMDAiPgo8cmVjdCB3aWR0aD0iMzEiIGhlaWdodD0iMzEiIHJ4PSIzIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=';
|
|
@ -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 React, { useCallback } from 'react';
|
||||
import {
|
||||
EuiLink,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
// @ts-expect-error untyped library
|
||||
import { saveAs } from '@elastic/filesaver';
|
||||
import { SectionWrapper } from '../../../../../common/components/section_wrapper';
|
||||
import type { State } from '../../state';
|
||||
import { SuccessSection } from '../../../../../common/components/success_section';
|
||||
import { useDeployIntegration } from './use_deploy_integration';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface DeployStepProps {
|
||||
integrationSettings: State['integrationSettings'];
|
||||
result: State['result'];
|
||||
connectorId: State['connectorId'];
|
||||
}
|
||||
|
||||
export const DeployStep = React.memo<DeployStepProps>(
|
||||
({ integrationSettings, result, connectorId }) => {
|
||||
const { isLoading, error, integrationFile, integrationName } = useDeployIntegration({
|
||||
integrationSettings,
|
||||
result,
|
||||
connectorId,
|
||||
});
|
||||
|
||||
const onSaveZip = useCallback(() => {
|
||||
saveAs(integrationFile, `${integrationName ?? 'custom_integration'}.zip`);
|
||||
}, [integrationFile, integrationName]);
|
||||
|
||||
if (isLoading || error) {
|
||||
return (
|
||||
<SectionWrapper title={i18n.DEPLOYING}>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup direction="row" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{isLoading && <EuiLoadingSpinner size="xl" />}
|
||||
{error && (
|
||||
<EuiText color="danger" size="s">
|
||||
{error}
|
||||
</EuiText>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</SectionWrapper>
|
||||
);
|
||||
}
|
||||
if (integrationName) {
|
||||
return (
|
||||
<SuccessSection integrationName={integrationName}>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
|
||||
<EuiFlexGroup direction="row" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row" alignItems="flexStart" justifyContent="flexStart">
|
||||
<EuiFlexItem grow={false} css={{ marginTop: '3px' }}>
|
||||
<EuiIcon type="download" color="primary" size="m" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h4>{i18n.DOWNLOAD_ZIP_TITLE}</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" size="s">
|
||||
{i18n.DOWNLOAD_ZIP_DESCRIPTION}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink onClick={onSaveZip}>{i18n.DOWNLOAD_ZIP_LINK}</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</SuccessSection>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
DeployStep.displayName = 'DeployStep';
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { DeployStep } from './deploy_step';
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export const DEPLOYING = i18n.translate('xpack.integrationAssistant.step.deploy.loadingTitle', {
|
||||
defaultMessage: 'Deploying',
|
||||
});
|
||||
|
||||
export const DOWNLOAD_ZIP_TITLE = i18n.translate(
|
||||
'xpack.integrationAssistant.step.deploy.downloadZip.title',
|
||||
{
|
||||
defaultMessage: 'Download .zip package',
|
||||
}
|
||||
);
|
||||
export const DOWNLOAD_ZIP_DESCRIPTION = i18n.translate(
|
||||
'xpack.integrationAssistant.step.deploy.downloadZip.description',
|
||||
{
|
||||
defaultMessage: 'Download your integration package to reuse in other deployments. ',
|
||||
}
|
||||
);
|
||||
|
||||
export const DOWNLOAD_ZIP_LINK = i18n.translate(
|
||||
'xpack.integrationAssistant.step.deploy.downloadZip.link',
|
||||
{
|
||||
defaultMessage: 'Download package',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 { useState, useEffect } from 'react';
|
||||
import { useKibana } from '../../../../../common/hooks/use_kibana';
|
||||
import type { BuildIntegrationRequestBody } from '../../../../../../common';
|
||||
import type { State } from '../../state';
|
||||
import { runBuildIntegration, runInstallPackage } from '../../../../../common/lib/api';
|
||||
import { defaultLogoEncoded } from '../default_logo';
|
||||
import { getIntegrationNameFromResponse } from '../../../../../common/lib/api_parsers';
|
||||
|
||||
interface PipelineGenerationProps {
|
||||
integrationSettings: State['integrationSettings'];
|
||||
result: State['result'];
|
||||
connectorId: State['connectorId'];
|
||||
}
|
||||
|
||||
export type ProgressItem = 'build' | 'install';
|
||||
|
||||
export const useDeployIntegration = ({ integrationSettings, result }: PipelineGenerationProps) => {
|
||||
const { http, notifications } = useKibana().services;
|
||||
const [integrationFile, setIntegrationFile] = useState<Blob | null>(null);
|
||||
const [integrationName, setIntegrationName] = useState<string>();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<null | string>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
http == null ||
|
||||
integrationSettings == null ||
|
||||
notifications?.toasts == null ||
|
||||
result?.pipeline == null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const abortController = new AbortController();
|
||||
const deps = { http, abortSignal: abortController.signal };
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const parameters: BuildIntegrationRequestBody = {
|
||||
integration: {
|
||||
title: integrationSettings.title ?? '',
|
||||
description: integrationSettings.description ?? '',
|
||||
name: integrationSettings.name ?? '',
|
||||
logo: integrationSettings.logo ?? defaultLogoEncoded,
|
||||
dataStreams: [
|
||||
{
|
||||
title: integrationSettings.dataStreamTitle ?? '',
|
||||
description: integrationSettings.dataStreamDescription ?? '',
|
||||
name: integrationSettings.dataStreamName ?? '',
|
||||
inputTypes: integrationSettings.inputType ? [integrationSettings.inputType] : [],
|
||||
rawSamples: integrationSettings.logsSampleParsed ?? [],
|
||||
docs: result.docs ?? [],
|
||||
pipeline: result.pipeline,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const zippedIntegration = await runBuildIntegration(parameters, deps);
|
||||
if (abortController.signal.aborted) return;
|
||||
setIntegrationFile(zippedIntegration);
|
||||
|
||||
const installResult = await runInstallPackage(zippedIntegration, deps);
|
||||
if (abortController.signal.aborted) return;
|
||||
|
||||
const integrationNameFromResponse = getIntegrationNameFromResponse(installResult);
|
||||
if (integrationNameFromResponse) {
|
||||
setIntegrationName(integrationNameFromResponse);
|
||||
} else {
|
||||
throw new Error('Integration name not found in response');
|
||||
}
|
||||
} catch (e) {
|
||||
if (abortController.signal.aborted) return;
|
||||
setError(`Error: ${e.body?.message ?? e.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [
|
||||
setIntegrationFile,
|
||||
http,
|
||||
integrationSettings,
|
||||
notifications?.toasts,
|
||||
result?.docs,
|
||||
result?.pipeline,
|
||||
]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
integrationFile,
|
||||
integrationName,
|
||||
error,
|
||||
};
|
||||
};
|
|
@ -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 { IntegrationStep } from './integration_step';
|
||||
export * from './is_step_ready';
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiFilePicker,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTextArea,
|
||||
useEuiBackgroundColor,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import type { IntegrationSettings } from '../../types';
|
||||
import { StepContentWrapper } from '../step_content_wrapper';
|
||||
import { PackageCardPreview } from './package_card_preview';
|
||||
import { useActions } from '../../state';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const MaxLogoSize = 1048576; // One megabyte
|
||||
|
||||
const useLayoutStyles = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const subduedBgCss = useEuiBackgroundColor('subdued');
|
||||
return {
|
||||
left: css`
|
||||
padding: ${euiTheme.size.l};
|
||||
}
|
||||
`,
|
||||
right: css`
|
||||
padding: ${euiTheme.size.l};
|
||||
background: ${subduedBgCss};
|
||||
width: 45%;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
interface IntegrationStepProps {
|
||||
integrationSettings: IntegrationSettings | undefined;
|
||||
}
|
||||
|
||||
export const IntegrationStep = React.memo<IntegrationStepProps>(({ integrationSettings }) => {
|
||||
const styles = useLayoutStyles();
|
||||
const { setIntegrationSettings } = useActions();
|
||||
const [logoError, setLogoError] = React.useState<string>();
|
||||
|
||||
const setIntegrationValues = useCallback(
|
||||
(settings: Partial<IntegrationSettings>) =>
|
||||
setIntegrationSettings({ ...integrationSettings, ...settings }),
|
||||
[integrationSettings, setIntegrationSettings]
|
||||
);
|
||||
|
||||
const onChange = useMemo(() => {
|
||||
return {
|
||||
title: (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setIntegrationValues({ title: e.target.value }),
|
||||
description: (e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setIntegrationValues({ description: e.target.value }),
|
||||
logo: (files: FileList | null) => {
|
||||
setLogoError(undefined);
|
||||
const logoFile = files?.[0];
|
||||
if (!logoFile) {
|
||||
setIntegrationValues({ logo: undefined });
|
||||
return;
|
||||
}
|
||||
if (logoFile.size > MaxLogoSize) {
|
||||
setLogoError(`${logoFile.name} is too large, maximum size is 1Mb.`);
|
||||
return;
|
||||
}
|
||||
logoFile
|
||||
.arrayBuffer()
|
||||
.then((fileBuffer) => {
|
||||
const encodedLogo = window.btoa(String.fromCharCode(...new Uint8Array(fileBuffer)));
|
||||
setIntegrationValues({ logo: encodedLogo });
|
||||
})
|
||||
.catch((e) => {
|
||||
setLogoError(i18n.LOGO_ERROR);
|
||||
});
|
||||
},
|
||||
};
|
||||
}, [setIntegrationValues]);
|
||||
|
||||
return (
|
||||
<StepContentWrapper title={i18n.TITLE} subtitle={i18n.DESCRIPTION}>
|
||||
<EuiPanel paddingSize="none" hasShadow={false} hasBorder>
|
||||
<EuiFlexGroup direction="row" gutterSize="none">
|
||||
<EuiFlexItem css={styles.left}>
|
||||
<EuiForm component="form" fullWidth>
|
||||
<EuiFormRow label={i18n.TITLE_LABEL}>
|
||||
<EuiFieldText
|
||||
name="title"
|
||||
value={integrationSettings?.title ?? ''}
|
||||
onChange={onChange.title}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow label={i18n.DESCRIPTION_LABEL}>
|
||||
<EuiTextArea
|
||||
name="description"
|
||||
value={integrationSettings?.description ?? ''}
|
||||
onChange={onChange.description}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow label={i18n.LOGO_LABEL}>
|
||||
<>
|
||||
<EuiFilePicker
|
||||
id="logsSampleFilePicker"
|
||||
initialPromptText={i18n.LOGO_DESCRIPTION}
|
||||
onChange={onChange.logo}
|
||||
display="large"
|
||||
aria-label="Upload an svg logo image"
|
||||
accept="image/svg+xml"
|
||||
isInvalid={logoError != null}
|
||||
/>
|
||||
<EuiSpacer size="xs" />
|
||||
{logoError && (
|
||||
<EuiText color="danger" size="xs">
|
||||
{logoError}
|
||||
</EuiText>
|
||||
)}
|
||||
</>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} css={styles.right}>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<PackageCardPreview integrationSettings={integrationSettings} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</StepContentWrapper>
|
||||
);
|
||||
});
|
||||
IntegrationStep.displayName = 'IntegrationStep';
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { State } from '../../state';
|
||||
|
||||
export const isIntegrationStepReady = ({ integrationSettings }: State) =>
|
||||
Boolean(integrationSettings?.title && integrationSettings?.description);
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { useEuiTheme, EuiCard, EuiIcon } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { defaultLogoEncoded } from '../default_logo';
|
||||
import type { IntegrationSettings } from '../../types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const useCardCss = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return css`
|
||||
margin-top: calc(
|
||||
${euiTheme.size.m} + 7px
|
||||
); // To align with title input that has a margin-top of 4px to the label
|
||||
|
||||
min-height: 127px;
|
||||
|
||||
[class*='euiCard__content'] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
[class*='euiCard__description'] {
|
||||
flex-grow: 1;
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
interface PackageCardPreviewProps {
|
||||
integrationSettings: IntegrationSettings | undefined;
|
||||
}
|
||||
|
||||
export const PackageCardPreview = React.memo<PackageCardPreviewProps>(({ integrationSettings }) => {
|
||||
const cardCss = useCardCss();
|
||||
return (
|
||||
<EuiCard
|
||||
css={cardCss}
|
||||
data-test-subj="package-card-preview"
|
||||
layout="horizontal"
|
||||
title={integrationSettings?.title ?? ''}
|
||||
description={integrationSettings?.description ?? ''}
|
||||
titleSize="xs"
|
||||
hasBorder
|
||||
icon={
|
||||
<EuiIcon
|
||||
size={'xl'}
|
||||
type={`data:image/svg+xml;base64,${integrationSettings?.logo ?? defaultLogoEncoded}`}
|
||||
/>
|
||||
}
|
||||
betaBadgeProps={{
|
||||
label: i18n.PREVIEW,
|
||||
tooltipContent: i18n.PREVIEW_TOOLTIP,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
PackageCardPreview.displayName = 'PackageCardPreview';
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export const TITLE = i18n.translate('xpack.integrationAssistant.step.integration.title', {
|
||||
defaultMessage: 'Integration details',
|
||||
});
|
||||
export const DESCRIPTION = i18n.translate(
|
||||
'xpack.integrationAssistant.step.integration.description',
|
||||
{
|
||||
defaultMessage: 'Name your integration, give it a description, and (optional) add a logo',
|
||||
}
|
||||
);
|
||||
|
||||
export const TITLE_LABEL = i18n.translate(
|
||||
'xpack.integrationAssistant.step.integration.integrationTitle',
|
||||
{
|
||||
defaultMessage: 'Title',
|
||||
}
|
||||
);
|
||||
export const DESCRIPTION_LABEL = i18n.translate(
|
||||
'xpack.integrationAssistant.step.integration.integrationDescription',
|
||||
{
|
||||
defaultMessage: 'Description',
|
||||
}
|
||||
);
|
||||
export const LOGO_LABEL = i18n.translate('xpack.integrationAssistant.step.integration.logo.label', {
|
||||
defaultMessage: 'Logo (optional)',
|
||||
});
|
||||
|
||||
export const LOGO_DESCRIPTION = i18n.translate(
|
||||
'xpack.integrationAssistant.step.integration.logo.description',
|
||||
{
|
||||
defaultMessage: 'Drag and drop a .svg file or Browse files',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVIEW = i18n.translate('xpack.integrationAssistant.step.integration.preview', {
|
||||
defaultMessage: 'Preview',
|
||||
});
|
||||
|
||||
export const PREVIEW_TOOLTIP = i18n.translate(
|
||||
'xpack.integrationAssistant.step.integration.previewTooltip',
|
||||
{
|
||||
defaultMessage: 'This is a preview of the integration card for the integrations catalog',
|
||||
}
|
||||
);
|
||||
|
||||
export const LOGO_ERROR = i18n.translate('xpack.integrationAssistant.step.integration.logo.error', {
|
||||
defaultMessage: 'Error processing logo file',
|
||||
});
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { EcsFlat } from '@elastic/ecs';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiInMemoryTable,
|
||||
EuiToken,
|
||||
EuiToolTip,
|
||||
type EuiBasicTableColumn,
|
||||
type EuiSearchBarProps,
|
||||
} from '@elastic/eui';
|
||||
|
||||
interface FieldObject {
|
||||
type: string;
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const getIconFromType = (type: string | null | undefined) => {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return 'tokenString';
|
||||
case 'keyword':
|
||||
return 'tokenKeyword';
|
||||
case 'number':
|
||||
case 'long':
|
||||
return 'tokenNumber';
|
||||
case 'date':
|
||||
return 'tokenDate';
|
||||
case 'ip':
|
||||
case 'geo_point':
|
||||
return 'tokenGeo';
|
||||
case 'object':
|
||||
return 'tokenQuestionInCircle';
|
||||
case 'float':
|
||||
return 'tokenNumber';
|
||||
default:
|
||||
return 'tokenQuestionInCircle';
|
||||
}
|
||||
};
|
||||
|
||||
const tooltipAnchorProps = { css: { display: 'flex' } };
|
||||
const columns: Array<EuiBasicTableColumn<FieldObject>> = [
|
||||
{
|
||||
field: 'name',
|
||||
name: 'Name',
|
||||
sortable: true,
|
||||
render: (name: string, { type }) => {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
{type ? (
|
||||
<EuiToolTip content={type} anchorProps={tooltipAnchorProps}>
|
||||
<EuiToken
|
||||
data-test-subj={`field-${name}-icon`}
|
||||
iconType={getIconFromType(type ?? null)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<EuiIcon type="questionInCircle" />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>{name}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
name: 'Value',
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
|
||||
const search: EuiSearchBarProps = {
|
||||
box: {
|
||||
incremental: true,
|
||||
schema: true,
|
||||
},
|
||||
};
|
||||
|
||||
const flattenDocument = (document?: object): FieldObject[] => {
|
||||
if (!document) {
|
||||
return [];
|
||||
}
|
||||
const fields: FieldObject[] = [];
|
||||
const flatten = (object: object, prefix = '') => {
|
||||
Object.entries(object).forEach(([key, value]) => {
|
||||
const name = `${prefix}${key}` as keyof typeof EcsFlat;
|
||||
if (!Array.isArray(value) && typeof value === 'object' && value !== null) {
|
||||
flatten(value, `${name}.`);
|
||||
} else {
|
||||
fields.push({ name, value, type: EcsFlat[name]?.type });
|
||||
}
|
||||
});
|
||||
};
|
||||
flatten(document);
|
||||
return fields;
|
||||
};
|
||||
|
||||
interface FieldsTableProps {
|
||||
documents?: object[];
|
||||
}
|
||||
export const FieldsTable = React.memo<FieldsTableProps>(({ documents = [] }) => {
|
||||
const fields = useMemo(() => flattenDocument(documents[0]), [documents]);
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
items={fields}
|
||||
columns={columns}
|
||||
search={search}
|
||||
pagination={true}
|
||||
sorting={true}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FieldsTable.displayName = 'FieldsTable';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
export { ReviewStep } from './review_step';
|
||||
export * from './is_step_ready';
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { State } from '../../state';
|
||||
|
||||
export const isReviewStepReady = ({ isGenerating, result }: State) =>
|
||||
isGenerating === false && result != null;
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { CodeEditor } from '@kbn/code-editor';
|
||||
import { XJsonLang } from '@kbn/monaco';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { Pipeline } from '../../../../../../common';
|
||||
import type { State } from '../../state';
|
||||
import { FieldsTable } from './fields_table';
|
||||
import { StepContentWrapper } from '../step_content_wrapper';
|
||||
import { useCheckPipeline } from './use_check_pipeline';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const flyoutBodyCss = css`
|
||||
height: 100%;
|
||||
.euiFlyoutBody__overflowContent {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
interface ReviewStepProps {
|
||||
integrationSettings: State['integrationSettings'];
|
||||
connectorId: State['connectorId'];
|
||||
result: State['result'];
|
||||
isGenerating: State['isGenerating'];
|
||||
}
|
||||
export const ReviewStep = React.memo<ReviewStepProps>(
|
||||
({ integrationSettings, connectorId, isGenerating, result }) => {
|
||||
const [customPipeline, setCustomPipeline] = useState<Pipeline>();
|
||||
const { error: checkPipelineError } = useCheckPipeline({
|
||||
customPipeline,
|
||||
integrationSettings,
|
||||
connectorId,
|
||||
});
|
||||
|
||||
const [isPipelineEditionVisible, setIsPipelineEditionVisible] = useState(false);
|
||||
const [updatedPipeline, setUpdatedPipeline] = useState<string>();
|
||||
|
||||
const changeCustomPipeline = useCallback((value: string) => {
|
||||
setUpdatedPipeline(value);
|
||||
}, []);
|
||||
|
||||
const saveCustomPipeline = useCallback(() => {
|
||||
if (updatedPipeline) {
|
||||
try {
|
||||
const pipeline = JSON.parse(updatedPipeline);
|
||||
setCustomPipeline(pipeline);
|
||||
} catch (_) {
|
||||
return; // Syntax errors are already displayed in the code editor
|
||||
}
|
||||
}
|
||||
setIsPipelineEditionVisible(false);
|
||||
}, [updatedPipeline]);
|
||||
|
||||
return (
|
||||
<StepContentWrapper
|
||||
title={i18n.TITLE}
|
||||
subtitle={i18n.DESCRIPTION}
|
||||
right={
|
||||
<EuiButton onClick={() => setIsPipelineEditionVisible(true)}>
|
||||
{i18n.EDIT_PIPELINE_BUTTON}
|
||||
</EuiButton>
|
||||
}
|
||||
>
|
||||
<EuiPanel hasShadow={false} hasBorder>
|
||||
{isGenerating ? (
|
||||
<EuiLoadingSpinner size="l" />
|
||||
) : (
|
||||
<>
|
||||
{checkPipelineError && (
|
||||
<EuiText color="danger" size="s">
|
||||
{checkPipelineError}
|
||||
</EuiText>
|
||||
)}
|
||||
<FieldsTable documents={result?.docs} />
|
||||
</>
|
||||
)}
|
||||
{isPipelineEditionVisible && (
|
||||
<EuiFlyout onClose={() => setIsPipelineEditionVisible(false)}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h2>{i18n.INGEST_PIPELINE_TITLE}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody css={flyoutBodyCss}>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
wrap={false}
|
||||
responsive={false}
|
||||
css={{ height: '100%' }}
|
||||
>
|
||||
<EuiFlexItem grow={true} data-test-subj="inspectorRequestCodeViewerContainer">
|
||||
<CodeEditor
|
||||
languageId={XJsonLang.ID}
|
||||
value={JSON.stringify(result?.pipeline, null, 2)}
|
||||
onChange={changeCustomPipeline}
|
||||
width="100%"
|
||||
height="100%"
|
||||
options={{
|
||||
fontSize: 12,
|
||||
minimap: { enabled: true },
|
||||
folding: true,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
wrappingIndent: 'indent',
|
||||
automaticLayout: true,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup direction="row" gutterSize="none" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill onClick={saveCustomPipeline}>
|
||||
{i18n.SAVE_BUTTON}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</StepContentWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
ReviewStep.displayName = 'ReviewStep';
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export const TITLE = i18n.translate('xpack.integrationAssistant.step.review.title', {
|
||||
defaultMessage: 'Review results',
|
||||
});
|
||||
export const DESCRIPTION = i18n.translate('xpack.integrationAssistant.step.review.description', {
|
||||
defaultMessage:
|
||||
'Review all the fields and their values in your integration. Make any necessary adjustments to ensure accuracy.',
|
||||
});
|
||||
|
||||
export const EDIT_PIPELINE_BUTTON = i18n.translate(
|
||||
'xpack.integrationAssistant.step.review.editPipeline',
|
||||
{
|
||||
defaultMessage: 'Edit pipeline',
|
||||
}
|
||||
);
|
||||
export const INGEST_PIPELINE_TITLE = i18n.translate(
|
||||
'xpack.integrationAssistant.step.review.ingestPipelineTitle',
|
||||
{
|
||||
defaultMessage: 'Ingest pipeline',
|
||||
}
|
||||
);
|
||||
|
||||
export const SAVE_BUTTON = i18n.translate('xpack.integrationAssistant.step.review.save', {
|
||||
defaultMessage: 'Save',
|
||||
});
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { CheckPipelineRequestBody, Pipeline } from '../../../../../../common';
|
||||
import { useActions, type State } from '../../state';
|
||||
import { runCheckPipelineResults } from '../../../../../common/lib/api';
|
||||
|
||||
interface CheckPipelineProps {
|
||||
integrationSettings: State['integrationSettings'];
|
||||
connectorId: State['connectorId'];
|
||||
customPipeline: Pipeline | undefined;
|
||||
}
|
||||
|
||||
export const useCheckPipeline = ({
|
||||
integrationSettings,
|
||||
connectorId,
|
||||
customPipeline,
|
||||
}: CheckPipelineProps) => {
|
||||
const { http, notifications } = useKibana().services;
|
||||
const { setIsGenerating, setResult } = useActions();
|
||||
const [error, setError] = useState<null | string>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
customPipeline == null ||
|
||||
http == null ||
|
||||
integrationSettings == null ||
|
||||
notifications?.toasts == null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const abortController = new AbortController();
|
||||
const deps = { http, abortSignal: abortController.signal };
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const parameters: CheckPipelineRequestBody = {
|
||||
pipeline: customPipeline,
|
||||
rawSamples: integrationSettings.logsSampleParsed ?? [],
|
||||
};
|
||||
setIsGenerating(true);
|
||||
|
||||
const ecsGraphResult = await runCheckPipelineResults(parameters, deps);
|
||||
if (abortController.signal.aborted) return;
|
||||
if (isEmpty(ecsGraphResult?.pipelineResults) || ecsGraphResult?.errors?.length) {
|
||||
setError('No results for the pipeline');
|
||||
return;
|
||||
}
|
||||
setResult({
|
||||
pipeline: customPipeline,
|
||||
docs: ecsGraphResult.pipelineResults,
|
||||
});
|
||||
} catch (e) {
|
||||
if (abortController.signal.aborted) return;
|
||||
setError(`Error: ${e.body.message}`);
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [
|
||||
setIsGenerating,
|
||||
connectorId,
|
||||
http,
|
||||
integrationSettings,
|
||||
notifications?.toasts,
|
||||
setResult,
|
||||
customPipeline,
|
||||
]);
|
||||
|
||||
return { error };
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { type PropsWithChildren } from 'react';
|
||||
import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
const contentCss = css`
|
||||
width: 100%;
|
||||
max-width: 730px;
|
||||
`;
|
||||
|
||||
export type StepContentWrapperProps = PropsWithChildren<{
|
||||
title: React.ReactNode;
|
||||
subtitle: React.ReactNode;
|
||||
right?: React.ReactNode;
|
||||
}>;
|
||||
|
||||
export const StepContentWrapper = React.memo<StepContentWrapperProps>(
|
||||
({ children, title, subtitle, right }) => (
|
||||
<>
|
||||
<EuiFlexGroup direction="column" alignItems="center">
|
||||
<EuiFlexItem css={contentCss}>
|
||||
<EuiFlexGroup direction="row" justifyContent="spaceBetween" alignItems="flexEnd">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
alignItems="flexStart"
|
||||
justifyContent="flexStart"
|
||||
gutterSize="xs"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h1>{title}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" color="subdued">
|
||||
{subtitle}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{right && <EuiFlexItem grow={false}>{right}</EuiFlexItem>}
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
{children}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)
|
||||
);
|
||||
StepContentWrapper.displayName = 'StepContentWrapper';
|
|
@ -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 type { InputType } from '../../../../common';
|
||||
|
||||
// TODO: find a better home for this type
|
||||
export type { AIConnector } from '@kbn/elastic-assistant/impl/connectorland/connector_selector';
|
||||
|
||||
export interface IntegrationSettings {
|
||||
title?: string;
|
||||
description?: string;
|
||||
logo?: string;
|
||||
name?: string;
|
||||
dataStreamTitle?: string;
|
||||
dataStreamDescription?: string;
|
||||
dataStreamName?: string;
|
||||
inputType?: InputType;
|
||||
logsSampleParsed?: string[];
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiCard,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { AssistantAvatar } from '@kbn/elastic-assistant';
|
||||
import { css } from '@emotion/react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { IntegrationImageHeader } from '../../../common/components/integration_image_header';
|
||||
import { ButtonsFooter } from '../../../common/components/buttons_footer';
|
||||
import { SectionWrapper } from '../../../common/components/section_wrapper';
|
||||
import { useNavigate, Page } from '../../../common/hooks/use_navigate';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const useAssistantCardCss = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return css`
|
||||
/* compensate for EuiCard children margin-block-start */
|
||||
margin-block-start: calc(${euiTheme.size.s} * -2);
|
||||
`;
|
||||
};
|
||||
|
||||
export const CreateIntegrationLanding = React.memo(() => {
|
||||
const navigate = useNavigate();
|
||||
const assistantCardCss = useAssistantCardCss();
|
||||
return (
|
||||
<KibanaPageTemplate>
|
||||
<IntegrationImageHeader />
|
||||
<KibanaPageTemplate.Section grow>
|
||||
<SectionWrapper title={i18n.LANDING_TITLE} subtitle={i18n.LANDING_DESCRIPTION}>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="l"
|
||||
alignItems="center"
|
||||
justifyContent="flexStart"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiCard
|
||||
display="plain"
|
||||
hasBorder={true}
|
||||
paddingSize="l"
|
||||
title={''} // title shown inside the child component
|
||||
betaBadgeProps={{
|
||||
label: i18n.TECH_PREVIEW,
|
||||
tooltipContent: i18n.TECH_PREVIEW_TOOLTIP,
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="l"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
css={assistantCardCss}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssistantAvatar />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
alignItems="flexStart"
|
||||
justifyContent="flexStart"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{i18n.ASSISTANT_TITLE}</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" color="subdued" textAlign="left">
|
||||
{i18n.ASSISTANT_DESCRIPTION}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton onClick={() => navigate(Page.assistant)}>
|
||||
{i18n.ASSISTANT_BUTTON}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiCard>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
justifyContent="flexStart"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="package" size="l" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.createIntegrationLanding.uploadPackageDescription"
|
||||
defaultMessage="If you have an existing integration package, {link}"
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink onClick={() => navigate(Page.upload)}>
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.createIntegrationLanding.uploadPackageLink"
|
||||
defaultMessage="upload it as a .zip"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</SectionWrapper>
|
||||
</KibanaPageTemplate.Section>
|
||||
<ButtonsFooter />
|
||||
</KibanaPageTemplate>
|
||||
);
|
||||
});
|
||||
CreateIntegrationLanding.displayName = 'CreateIntegrationLanding';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { CreateIntegrationLanding } from './create_integration_landing';
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const LANDING_TITLE = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationLanding.title',
|
||||
{
|
||||
defaultMessage: 'Create new integration',
|
||||
}
|
||||
);
|
||||
|
||||
export const LANDING_DESCRIPTION = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationLanding.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Start an AI-driven process to build your integration step-by-step, or upload a .zip package of a previously created integration',
|
||||
}
|
||||
);
|
||||
|
||||
export const PACKAGE_UPLOAD_TITLE = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationLanding.packageUpload.title',
|
||||
{
|
||||
defaultMessage: 'Package upload',
|
||||
}
|
||||
);
|
||||
|
||||
export const PACKAGE_UPLOAD_DESCRIPTION = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationLanding.packageUpload.description',
|
||||
{
|
||||
defaultMessage: 'Use this option if you have an existing integration package in a .zip file',
|
||||
}
|
||||
);
|
||||
|
||||
export const PACKAGE_UPLOAD_BUTTON = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationLanding.packageUpload.button',
|
||||
{
|
||||
defaultMessage: 'Upload .zip',
|
||||
}
|
||||
);
|
||||
|
||||
export const ASSISTANT_TITLE = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationLanding.assistant.title',
|
||||
{
|
||||
defaultMessage: 'Create custom integration',
|
||||
}
|
||||
);
|
||||
|
||||
export const ASSISTANT_DESCRIPTION = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationLanding.assistant.description',
|
||||
{
|
||||
defaultMessage: 'Use our AI-driven process to build your custom integration',
|
||||
}
|
||||
);
|
||||
|
||||
export const ASSISTANT_BUTTON = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationLanding.assistant.button',
|
||||
{
|
||||
defaultMessage: 'Create Integration',
|
||||
}
|
||||
);
|
||||
|
||||
export const TECH_PREVIEW = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationLanding.assistant.techPreviewBadge',
|
||||
{
|
||||
defaultMessage: 'Technical preview',
|
||||
}
|
||||
);
|
||||
|
||||
export const TECH_PREVIEW_TOOLTIP = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationLanding.assistant.techPreviewTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'This functionality is in technical preview and is subject to change. Please use with caution in production environments.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* 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, { useCallback, useState } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFilePicker, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { SuccessSection } from '../../../common/components/success_section';
|
||||
import { SectionWrapper } from '../../../common/components/section_wrapper';
|
||||
import { ButtonsFooter } from '../../../common/components/buttons_footer';
|
||||
import { IntegrationImageHeader } from '../../../common/components/integration_image_header';
|
||||
import { runInstallPackage, type RequestDeps } from '../../../common/lib/api';
|
||||
import { getIntegrationNameFromResponse } from '../../../common/lib/api_parsers';
|
||||
import { useNavigate, Page } from '../../../common/hooks/use_navigate';
|
||||
import { DocsLinkSubtitle } from './docs_link_subtitle';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const CreateIntegrationUpload = React.memo(() => {
|
||||
const navigate = useNavigate();
|
||||
const { http } = useKibana().services;
|
||||
const [file, setFile] = useState<Blob>();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const [integrationName, setIntegrationName] = useState<string>();
|
||||
|
||||
const onBack = useCallback(() => {
|
||||
navigate(Page.landing);
|
||||
}, [navigate]);
|
||||
|
||||
const onChangeFile = useCallback((files: FileList | null) => {
|
||||
setFile(files?.[0]);
|
||||
setError(undefined);
|
||||
}, []);
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
if (http == null || file == null) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
const abortController = new AbortController();
|
||||
(async () => {
|
||||
try {
|
||||
const deps: RequestDeps = { http, abortSignal: abortController.signal };
|
||||
const response = await runInstallPackage(file, deps);
|
||||
|
||||
const integrationNameFromResponse = getIntegrationNameFromResponse(response);
|
||||
if (integrationNameFromResponse) {
|
||||
setIntegrationName(integrationNameFromResponse);
|
||||
} else {
|
||||
throw new Error('Integration name not found in response');
|
||||
}
|
||||
} catch (e) {
|
||||
if (!abortController.signal.aborted) {
|
||||
setError(`${i18n.UPLOAD_ERROR}: ${e.body.message}`);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [file, http, setIntegrationName, setError]);
|
||||
|
||||
return (
|
||||
<KibanaPageTemplate>
|
||||
<IntegrationImageHeader />
|
||||
{integrationName ? (
|
||||
<>
|
||||
<KibanaPageTemplate.Section grow>
|
||||
<SuccessSection integrationName={integrationName} />
|
||||
</KibanaPageTemplate.Section>
|
||||
<ButtonsFooter cancelButtonText={i18n.CLOSE_BUTTON} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<KibanaPageTemplate.Section grow>
|
||||
<SectionWrapper title={i18n.UPLOAD_TITLE} subtitle={<DocsLinkSubtitle />}>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gutterSize="xl"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiFilePicker
|
||||
id="logsSampleFilePicker"
|
||||
initialPromptText={i18n.UPLOAD_INPUT_TEXT}
|
||||
onChange={onChangeFile}
|
||||
display="large"
|
||||
aria-label="Upload .zip file"
|
||||
accept="application/zip"
|
||||
isLoading={isLoading}
|
||||
fullWidth
|
||||
isInvalid={error != null}
|
||||
/>
|
||||
<EuiSpacer size="xs" />
|
||||
{error && (
|
||||
<EuiText color="danger" size="xs">
|
||||
{error}
|
||||
</EuiText>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</SectionWrapper>
|
||||
</KibanaPageTemplate.Section>
|
||||
<ButtonsFooter
|
||||
isNextDisabled={file == null}
|
||||
nextButtonText={i18n.INSTALL_BUTTON}
|
||||
onBack={onBack}
|
||||
onNext={onConfirm}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</KibanaPageTemplate>
|
||||
);
|
||||
});
|
||||
CreateIntegrationUpload.displayName = 'CreateIntegrationUpload';
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiText, EuiLink } from '@elastic/eui';
|
||||
|
||||
export const DocsLinkSubtitle = React.memo(() => {
|
||||
const { docLinks } = useKibana().services;
|
||||
return (
|
||||
<EuiText size="s" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.createIntegrationUpload.uploadHelpText"
|
||||
defaultMessage="For more information, refer to {link}"
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink href={docLinks?.links.integrationDeveloper.upload} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.createIntegrationUpload.documentation"
|
||||
defaultMessage="Upload an Integration"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
);
|
||||
});
|
||||
DocsLinkSubtitle.displayName = 'DocsLinkSubtitle';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { CreateIntegrationUpload } from './create_integration_upload';
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export const UPLOAD_TITLE = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationUpload.title',
|
||||
{
|
||||
defaultMessage: 'Upload integration package',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPLOAD_INPUT_TEXT = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationUpload.inputText',
|
||||
{
|
||||
defaultMessage: 'Drag and drop a .zip file or Browse files',
|
||||
}
|
||||
);
|
||||
|
||||
export const INSTALL_BUTTON = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationUpload.install',
|
||||
{
|
||||
defaultMessage: 'Add to Elastic',
|
||||
}
|
||||
);
|
||||
|
||||
export const CLOSE_BUTTON = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationUpload.close',
|
||||
{
|
||||
defaultMessage: 'Close',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPLOAD_ERROR = i18n.translate(
|
||||
'xpack.integrationAssistant.createIntegrationUpload.error',
|
||||
{
|
||||
defaultMessage: 'Error installing package',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React, { Suspense } from 'react';
|
||||
import type { CreateIntegrationServices } from './types';
|
||||
|
||||
const CreateIntegration = React.lazy(() =>
|
||||
import('./create_integration').then((module) => ({
|
||||
default: module.CreateIntegration,
|
||||
}))
|
||||
);
|
||||
|
||||
export const getCreateIntegrationLazy = (services: CreateIntegrationServices) =>
|
||||
React.memo(function CreateIntegrationLazy() {
|
||||
return (
|
||||
<Suspense fallback={<EuiLoadingSpinner size="l" />}>
|
||||
<CreateIntegration services={services} />
|
||||
</Suspense>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { CoreStart } from '@kbn/core/public';
|
||||
import type { IntegrationAssistantPluginStartDependencies } from '../../types';
|
||||
|
||||
export type CreateIntegrationServices = CoreStart & IntegrationAssistantPluginStartDependencies;
|
||||
|
||||
export type CreateIntegrationComponent = React.ComponentType;
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiLink,
|
||||
EuiPanel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiImage,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { css } from '@emotion/react';
|
||||
import integrationsImage from '../../common/images/integrations_light.svg';
|
||||
|
||||
const useStyles = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return {
|
||||
image: css`
|
||||
width: 160px;
|
||||
height: 155px;
|
||||
object-fit: cover;
|
||||
object-position: left center;
|
||||
`,
|
||||
container: css`
|
||||
height: 135px;
|
||||
`,
|
||||
textContainer: css`
|
||||
height: 100%;
|
||||
padding: ${euiTheme.size.l} 0 ${euiTheme.size.l} ${euiTheme.size.l};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
export interface CreateIntegrationCardButtonProps {
|
||||
href: string;
|
||||
}
|
||||
export const CreateIntegrationCardButton = React.memo<CreateIntegrationCardButtonProps>(
|
||||
({ href }) => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="none">
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="none"
|
||||
css={styles.container}
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
justifyContent="spaceBetween"
|
||||
css={styles.textContainer}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.createIntegrationTitle"
|
||||
defaultMessage="Can't find an Integration?"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.createIntegrationDescription"
|
||||
defaultMessage="Create a custom one to fit your requirements"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink color="primary" href={href}>
|
||||
<EuiFlexGroup justifyContent="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="plusInCircle" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.createIntegrationButton"
|
||||
defaultMessage="Create new integration"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiImage
|
||||
alt="create integration background"
|
||||
src={integrationsImage}
|
||||
css={styles.image}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
);
|
||||
CreateIntegrationCardButton.displayName = 'CreateIntegrationCardButton';
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React, { Suspense } from 'react';
|
||||
import type { CreateIntegrationCardButtonProps } from './create_integration_card_button';
|
||||
|
||||
const CreateIntegrationCardButton = React.lazy(() =>
|
||||
import('./create_integration_card_button').then((module) => ({
|
||||
default: module.CreateIntegrationCardButton,
|
||||
}))
|
||||
);
|
||||
|
||||
export const getCreateIntegrationCardButtonLazy = () =>
|
||||
React.memo(function CreateIntegrationCardButtonLazy(props: CreateIntegrationCardButtonProps) {
|
||||
return (
|
||||
<Suspense fallback={<EuiLoadingSpinner size="l" />}>
|
||||
<CreateIntegrationCardButton {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { CreateIntegrationCardButtonProps } from './create_integration_card_button';
|
||||
|
||||
export type CreateIntegrationCardButtonComponent =
|
||||
React.ComponentType<CreateIntegrationCardButtonProps>;
|
13
x-pack/plugins/integration_assistant/public/index.ts
Normal file
13
x-pack/plugins/integration_assistant/public/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IntegrationAssistantPlugin } from './plugin';
|
||||
|
||||
export function plugin() {
|
||||
return new IntegrationAssistantPlugin();
|
||||
}
|
||||
export type { IntegrationAssistantPluginSetup, IntegrationAssistantPluginStart } from './types';
|
36
x-pack/plugins/integration_assistant/public/plugin.ts
Normal file
36
x-pack/plugins/integration_assistant/public/plugin.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { CoreStart, Plugin, CoreSetup } from '@kbn/core/public';
|
||||
import type {
|
||||
IntegrationAssistantPluginSetup,
|
||||
IntegrationAssistantPluginStart,
|
||||
IntegrationAssistantPluginStartDependencies,
|
||||
} from './types';
|
||||
import { getCreateIntegrationLazy } from './components/create_integration';
|
||||
import { getCreateIntegrationCardButtonLazy } from './components/create_integration_card_button';
|
||||
|
||||
export class IntegrationAssistantPlugin
|
||||
implements Plugin<IntegrationAssistantPluginSetup, IntegrationAssistantPluginStart>
|
||||
{
|
||||
public setup(_: CoreSetup): IntegrationAssistantPluginSetup {
|
||||
return {};
|
||||
}
|
||||
|
||||
public start(
|
||||
core: CoreStart,
|
||||
dependencies: IntegrationAssistantPluginStartDependencies
|
||||
): IntegrationAssistantPluginStart {
|
||||
const services = { ...core, ...dependencies };
|
||||
return {
|
||||
CreateIntegration: getCreateIntegrationLazy(services),
|
||||
CreateIntegrationCardButton: getCreateIntegrationCardButtonLazy(),
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
31
x-pack/plugins/integration_assistant/public/types.ts
Normal file
31
x-pack/plugins/integration_assistant/public/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 type { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import type {
|
||||
TriggersAndActionsUIPublicPluginSetup,
|
||||
TriggersAndActionsUIPublicPluginStart,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { CreateIntegrationComponent } from './components/create_integration/types';
|
||||
import type { CreateIntegrationCardButtonComponent } from './components/create_integration_card_button/types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface IntegrationAssistantPluginSetup {}
|
||||
|
||||
export interface IntegrationAssistantPluginStart {
|
||||
CreateIntegration: CreateIntegrationComponent;
|
||||
CreateIntegrationCardButton: CreateIntegrationCardButtonComponent;
|
||||
}
|
||||
|
||||
export interface IntegrationAssistantPluginSetupDependencies {
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
|
||||
spaces: SpacesPluginSetup;
|
||||
}
|
||||
|
||||
export interface IntegrationAssistantPluginStartDependencies {
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
spaces: SpacesPluginStart;
|
||||
}
|
|
@ -9,10 +9,10 @@ import AdmZip from 'adm-zip';
|
|||
import nunjucks from 'nunjucks';
|
||||
import { tmpdir } from 'os';
|
||||
import { join as joinPath } from 'path';
|
||||
import type { Datastream, Integration } from '../../common';
|
||||
import type { DataStream, Integration } from '../../common';
|
||||
import { copySync, createSync, ensureDirSync, generateUniqueId } from '../util';
|
||||
import { createAgentInput } from './agent';
|
||||
import { createDatastream } from './data_stream';
|
||||
import { createDataStream } from './data_stream';
|
||||
import { createFieldMapping } from './fields';
|
||||
import { createPipeline } from './pipeline';
|
||||
|
||||
|
@ -26,27 +26,30 @@ export async function buildPackage(integration: Integration): Promise<Buffer> {
|
|||
});
|
||||
|
||||
const tmpDir = joinPath(tmpdir(), `integration-assistant-${generateUniqueId()}`);
|
||||
const packageDir = createDirectories(tmpDir, integration);
|
||||
const packageDirectoryName = `${integration.name}-0.1.0`;
|
||||
const packageDir = createDirectories(tmpDir, integration, packageDirectoryName);
|
||||
const dataStreamsDir = joinPath(packageDir, 'data_stream');
|
||||
|
||||
for (const dataStream of integration.dataStreams) {
|
||||
const dataStreamName = dataStream.name;
|
||||
const specificDataStreamDir = joinPath(dataStreamsDir, dataStreamName);
|
||||
|
||||
createDatastream(integration.name, specificDataStreamDir, dataStream);
|
||||
createDataStream(integration.name, specificDataStreamDir, dataStream);
|
||||
createAgentInput(specificDataStreamDir, dataStream.inputTypes);
|
||||
createPipeline(specificDataStreamDir, dataStream.pipeline);
|
||||
createFieldMapping(integration.name, dataStreamName, specificDataStreamDir, dataStream.docs);
|
||||
}
|
||||
|
||||
const tmpPackageDir = joinPath(tmpDir, `${integration.name}-0.1.0`);
|
||||
|
||||
const zipBuffer = await createZipArchive(tmpPackageDir);
|
||||
const zipBuffer = await createZipArchive(tmpDir, packageDirectoryName);
|
||||
return zipBuffer;
|
||||
}
|
||||
|
||||
function createDirectories(tmpDir: string, integration: Integration): string {
|
||||
const packageDir = joinPath(tmpDir, `${integration.name}-0.1.0`);
|
||||
function createDirectories(
|
||||
tmpDir: string,
|
||||
integration: Integration,
|
||||
packageDirectoryName: string
|
||||
): string {
|
||||
const packageDir = joinPath(tmpDir, packageDirectoryName);
|
||||
ensureDirSync(tmpDir);
|
||||
ensureDirSync(packageDir);
|
||||
createPackage(packageDir, integration);
|
||||
|
@ -95,7 +98,7 @@ function createChangelog(packageDir: string): void {
|
|||
function createReadme(packageDir: string, integration: Integration) {
|
||||
const readmeDirPath = joinPath(packageDir, '_dev/build/docs/');
|
||||
ensureDirSync(readmeDirPath);
|
||||
const readmeTemplate = nunjucks.render('readme.md.njk', {
|
||||
const readmeTemplate = nunjucks.render('package_readme.md.njk', {
|
||||
package_name: integration.name,
|
||||
data_streams: integration.dataStreams,
|
||||
});
|
||||
|
@ -103,9 +106,10 @@ function createReadme(packageDir: string, integration: Integration) {
|
|||
createSync(joinPath(readmeDirPath, 'README.md'), readmeTemplate);
|
||||
}
|
||||
|
||||
async function createZipArchive(tmpPackageDir: string): Promise<Buffer> {
|
||||
async function createZipArchive(tmpDir: string, packageDirectoryName: string): Promise<Buffer> {
|
||||
const tmpPackageDir = joinPath(tmpDir, packageDirectoryName);
|
||||
const zip = new AdmZip();
|
||||
zip.addLocalFolder(tmpPackageDir);
|
||||
zip.addLocalFolder(tmpPackageDir, packageDirectoryName);
|
||||
const buffer = zip.toBuffer();
|
||||
return buffer;
|
||||
}
|
||||
|
@ -113,7 +117,7 @@ async function createZipArchive(tmpPackageDir: string): Promise<Buffer> {
|
|||
function createPackageManifest(packageDir: string, integration: Integration): void {
|
||||
const uniqueInputs: { [key: string]: { type: string; title: string; description: string } } = {};
|
||||
|
||||
integration.dataStreams.forEach((dataStream: Datastream) => {
|
||||
integration.dataStreams.forEach((dataStream: DataStream) => {
|
||||
dataStream.inputTypes.forEach((inputType: string) => {
|
||||
if (!uniqueInputs[inputType]) {
|
||||
uniqueInputs[inputType] = {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue