[Unified Integrations] Create Services, Storybook, Replacements Card; add to Fleet (#113816)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Clint Andrew Hall 2021-10-05 14:29:05 -05:00 committed by GitHub
parent 2dd01c0484
commit 4f85f5e841
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 2828 additions and 37 deletions

View file

@ -8,6 +8,7 @@ const STORYBOOKS = [
'canvas',
'codeeditor',
'ci_composite',
'custom_integrations',
'url_template_editor',
'dashboard',
'dashboard_enhanced',

View file

@ -19,6 +19,7 @@
"home": "src/plugins/home",
"flot": "packages/kbn-ui-shared-deps-src/src/flot_charts",
"charts": "src/plugins/charts",
"customIntegrations": "src/plugins/custom_integrations",
"esUi": "src/plugins/es_ui_shared",
"devTools": "src/plugins/dev_tools",
"expressions": "src/plugins/expressions",

View file

@ -12,6 +12,7 @@ export const storybookAliases = {
canvas: 'x-pack/plugins/canvas/storybook',
codeeditor: 'src/plugins/kibana_react/public/code_editor/.storybook',
ci_composite: '.ci/.storybook',
custom_integrations: 'src/plugins/custom_integrations/storybook',
url_template_editor: 'src/plugins/kibana_react/public/url_template_editor/.storybook',
dashboard: 'src/plugins/dashboard/.storybook',
dashboard_enhanced: 'x-pack/plugins/dashboard_enhanced/.storybook',

View file

@ -12,5 +12,8 @@
"extraPublicDirs": [
"common"
],
"requiredPlugins": [
"presentationUtil"
],
"optionalPlugins": []
}

View 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { Suspense, ComponentType, ReactElement, Ref } from 'react';
import { EuiLoadingSpinner, EuiErrorBoundary } from '@elastic/eui';
/**
* A HOC which supplies React.Suspense with a fallback component, and a `EuiErrorBoundary` to contain errors.
* @param Component A component deferred by `React.lazy`
* @param fallback A fallback component to render while things load; default is `EuiLoadingSpinner`
*/
export const withSuspense = <P extends {}, R = {}>(
Component: ComponentType<P>,
fallback: ReactElement | null = <EuiLoadingSpinner />
) =>
React.forwardRef((props: P, ref: Ref<R>) => {
return (
<EuiErrorBoundary>
<Suspense fallback={fallback}>
<Component {...props} ref={ref} />
</Suspense>
</EuiErrorBoundary>
);
});
export const LazyReplacementCard = React.lazy(() => import('./replacement_card'));

View file

@ -0,0 +1,15 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ReplacementCard } from './replacement_card';
export { ReplacementCard, Props } from './replacement_card';
// required for dynamic import using React.lazy()
// eslint-disable-next-line import/no-default-export
export default ReplacementCard;

View 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/** @jsx jsx */
import { css, jsx } from '@emotion/react';
import {
htmlIdGenerator,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiText,
EuiAccordion,
EuiLink,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { CustomIntegration } from '../../../common';
import { usePlatformService } from '../../services';
export interface Props {
replacements: Array<Pick<CustomIntegration, 'id' | 'uiInternalPath' | 'title'>>;
}
// TODO - clintandrewhall: should use doc-links service
const URL_COMPARISON = 'https://ela.st/beats-agent-comparison';
const idGenerator = htmlIdGenerator('replacementCard');
const alsoAvailable = i18n.translate('customIntegrations.components.replacementAccordionLabel', {
defaultMessage: 'Also available in Beats',
});
const link = (
<EuiLink
href={URL_COMPARISON}
data-test-subj="customIntegrationsBeatsAgentComparisonLink"
external
>
<FormattedMessage
id="customIntegrations.components.replacementAccordion.comparisonPageLinkLabel"
defaultMessage="comparison page"
/>
</EuiLink>
);
/**
* A pure component, an accordion panel which can display information about replacements for a given EPR module.
*/
export const ReplacementCard = ({ replacements }: Props) => {
const { euiTheme } = useEuiTheme();
const { getAbsolutePath } = usePlatformService();
if (replacements.length === 0) {
return null;
}
const buttons = replacements.map((replacement) => (
<EuiFlexItem grow={false}>
<span>
<EuiButton
key={replacement.id}
href={getAbsolutePath(replacement.uiInternalPath)}
fullWidth={false}
size="s"
>
{replacement.title}
</EuiButton>
</span>
</EuiFlexItem>
));
return (
<div
css={css`
& .euiAccordion__button {
color: ${euiTheme.colors.link};
}
& .euiAccordion-isOpen .euiAccordion__childWrapper {
margin-top: ${euiTheme.size.m};
}
`}
>
<EuiAccordion id={idGenerator()} buttonContent={alsoAvailable} paddingSize="none">
<EuiPanel color="subdued" hasShadow={false} paddingSize="m">
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<EuiText size="s">
<FormattedMessage
id="customIntegrations.components.replacementAccordion.recommendationDescription"
defaultMessage="Elastic Agent Integrations are recommended, but you can also use Beats. For more
details, check out our {link}."
values={{
link,
}}
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="m">
{buttons}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiAccordion>
</div>
);
};

View file

@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { Meta } from '@storybook/react';
import { ReplacementCard as ConnectedComponent } from './replacement_card';
import { ReplacementCard as PureComponent } from './replacement_card.component';
export default {
title: 'Replacement Card',
description:
'An accordion panel which can display information about Beats alternatives to a given EPR module, (if available)',
decorators: [
(storyFn, { globals }) => (
<div
style={{
padding: 40,
backgroundColor:
globals.euiTheme === 'v8.dark' || globals.euiTheme === 'v7.dark' ? '#1D1E24' : '#FFF',
width: 350,
}}
>
{storyFn()}
</div>
),
],
} as Meta;
interface Args {
eprPackageName: string;
}
const args: Args = {
eprPackageName: 'nginx',
};
const argTypes = {
eprPackageName: {
control: {
type: 'radio',
options: ['nginx', 'okta', 'aws', 'apache'],
},
},
};
export function ReplacementCard({ eprPackageName }: Args) {
return <ConnectedComponent {...{ eprPackageName }} />;
}
ReplacementCard.args = args;
ReplacementCard.argTypes = argTypes;
export function Component() {
return (
<PureComponent
replacements={[
{ id: 'foo', title: 'Foo', uiInternalPath: '#' },
{ id: 'bar', title: 'Bar', uiInternalPath: '#' },
]}
/>
);
}

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import useAsync from 'react-use/lib/useAsync';
import { useFindService } from '../../services';
import { ReplacementCard as Component } from './replacement_card.component';
export interface Props {
eprPackageName: string;
}
/**
* A data-connected component which can query about Beats-based replacement options for a given EPR module.
*/
export const ReplacementCard = ({ eprPackageName }: Props) => {
const { findReplacementIntegrations } = useFindService();
const integrations = useAsync(async () => {
return await findReplacementIntegrations({ shipper: 'beats', eprPackageName });
}, [eprPackageName]);
const { loading, value: replacements } = integrations;
if (loading || !replacements || replacements.length === 0) {
return null;
}
return <Component {...{ replacements }} />;
};

View file

@ -13,4 +13,8 @@ import { CustomIntegrationsPlugin } from './plugin';
export function plugin() {
return new CustomIntegrationsPlugin();
}
export { CustomIntegrationsSetup, CustomIntegrationsStart } from './types';
export { withSuspense, LazyReplacementCard } from './components';
export { filterCustomIntegrations } from './services/find';

View file

@ -6,7 +6,11 @@
* Side Public License, v 1.
*/
import { CustomIntegrationsSetup } from './types';
import { pluginServices } from './services';
import { PluginServiceRegistry } from '../../presentation_util/public';
import { CustomIntegrationsSetup, CustomIntegrationsStart } from './types';
import { CustomIntegrationsServices } from './services';
import { providers } from './services/stub';
function createCustomIntegrationsSetup(): jest.Mocked<CustomIntegrationsSetup> {
const mock: jest.Mocked<CustomIntegrationsSetup> = {
@ -16,6 +20,17 @@ function createCustomIntegrationsSetup(): jest.Mocked<CustomIntegrationsSetup> {
return mock;
}
function createCustomIntegrationsStart(): jest.Mocked<CustomIntegrationsStart> {
const registry = new PluginServiceRegistry<CustomIntegrationsServices>(providers);
pluginServices.setRegistry(registry.start({}));
const ContextProvider = pluginServices.getContextProvider();
return {
ContextProvider: jest.fn(ContextProvider),
};
}
export const customIntegrationsMock = {
createSetup: createCustomIntegrationsSetup,
createStart: createCustomIntegrationsStart,
};

View file

@ -7,13 +7,20 @@
*/
import { CoreSetup, CoreStart, Plugin } from 'src/core/public';
import { CustomIntegrationsSetup, CustomIntegrationsStart } from './types';
import {
CustomIntegrationsSetup,
CustomIntegrationsStart,
CustomIntegrationsStartDependencies,
} from './types';
import {
CustomIntegration,
ROUTES_APPEND_CUSTOM_INTEGRATIONS,
ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS,
} from '../common';
import { pluginServices } from './services';
import { pluginServiceRegistry } from './services/kibana';
export class CustomIntegrationsPlugin
implements Plugin<CustomIntegrationsSetup, CustomIntegrationsStart>
{
@ -30,8 +37,14 @@ export class CustomIntegrationsPlugin
};
}
public start(core: CoreStart): CustomIntegrationsStart {
return {};
public start(
coreStart: CoreStart,
startPlugins: CustomIntegrationsStartDependencies
): CustomIntegrationsStart {
pluginServices.setRegistry(pluginServiceRegistry.start({ coreStart, startPlugins }));
return {
ContextProvider: pluginServices.getContextProvider(),
};
}
public stop() {}

View file

@ -0,0 +1,95 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { filterCustomIntegrations } from './find';
import { CustomIntegration } from '../../common';
describe('Custom Integrations Find Service', () => {
const integrations: CustomIntegration[] = [
{
id: 'foo',
title: 'Foo',
description: 'test integration',
type: 'ui_link',
uiInternalPath: '/path/to/foo',
isBeta: false,
icons: [],
categories: ['aws', 'cloud'],
shipper: 'tests',
},
{
id: 'bar',
title: 'Bar',
description: 'test integration',
type: 'ui_link',
uiInternalPath: '/path/to/bar',
isBeta: false,
icons: [],
categories: ['aws'],
shipper: 'other',
eprOverlap: 'eprValue',
},
{
id: 'bar',
title: 'Bar',
description: 'test integration',
type: 'ui_link',
uiInternalPath: '/path/to/bar',
isBeta: false,
icons: [],
categories: ['cloud'],
shipper: 'other',
eprOverlap: 'eprValue',
},
{
id: 'baz',
title: 'Baz',
description: 'test integration',
type: 'ui_link',
uiInternalPath: '/path/to/baz',
isBeta: false,
icons: [],
categories: ['cloud'],
shipper: 'tests',
eprOverlap: 'eprOtherValue',
},
];
describe('filterCustomIntegrations', () => {
test('filters on shipper', () => {
let result = filterCustomIntegrations(integrations, { shipper: 'other' });
expect(result.length).toBe(2);
result = filterCustomIntegrations(integrations, { shipper: 'tests' });
expect(result.length).toBe(2);
result = filterCustomIntegrations(integrations, { shipper: 'foobar' });
expect(result.length).toBe(0);
});
test('filters on eprOverlap', () => {
let result = filterCustomIntegrations(integrations, { eprPackageName: 'eprValue' });
expect(result.length).toBe(2);
result = filterCustomIntegrations(integrations, { eprPackageName: 'eprOtherValue' });
expect(result.length).toBe(1);
result = filterCustomIntegrations(integrations, { eprPackageName: 'otherValue' });
expect(result.length).toBe(0);
});
test('filters on categories and shipper, eprOverlap', () => {
const result = filterCustomIntegrations(integrations, {
shipper: 'other',
eprPackageName: 'eprValue',
});
expect(result.length).toBe(2);
});
});
});

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { CustomIntegration } from '../../common';
interface FindParams {
eprPackageName?: string;
shipper?: string;
}
/**
* A plugin service that finds and returns custom integrations.
*/
export interface CustomIntegrationsFindService {
findReplacementIntegrations(params?: FindParams): Promise<CustomIntegration[]>;
findAppendedIntegrations(params?: FindParams): Promise<CustomIntegration[]>;
}
/**
* Filter a set of integrations by eprPackageName, and/or shipper.
*/
export const filterCustomIntegrations = (
integrations: CustomIntegration[],
{ eprPackageName, shipper }: FindParams = {}
) => {
if (!eprPackageName && !shipper) {
return integrations;
}
let result = integrations;
if (eprPackageName) {
result = result.filter((integration) => integration.eprOverlap === eprPackageName);
}
if (shipper) {
result = result.filter((integration) => integration.shipper === shipper);
}
return result;
};

View 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PluginServices } from '../../../presentation_util/public';
import { CustomIntegrationsFindService } from './find';
import { CustomIntegrationsPlatformService } from './platform';
/**
* Services used by the custom integrations plugin.
*/
export interface CustomIntegrationsServices {
find: CustomIntegrationsFindService;
platform: CustomIntegrationsPlatformService;
}
/**
* The `PluginServices` object for the custom integrations plugin.
* @see /src/plugins/presentation_util/public/services/create/index.ts
*/
export const pluginServices = new PluginServices<CustomIntegrationsServices>();
/**
* A React hook that provides connections to the `CustomIntegrationsFindService`.
*/
export const useFindService = () => (() => pluginServices.getHooks().find.useService())();
/**
* A React hook that provides connections to the `CustomIntegrationsPlatformService`.
*/
export const usePlatformService = () => (() => pluginServices.getHooks().platform.useService())();

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
CustomIntegration,
ROUTES_APPEND_CUSTOM_INTEGRATIONS,
ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS,
} from '../../../common';
import { KibanaPluginServiceFactory } from '../../../../presentation_util/public';
import { CustomIntegrationsStartDependencies } from '../../types';
import { CustomIntegrationsFindService, filterCustomIntegrations } from '../find';
/**
* A type definition for a factory to produce the `CustomIntegrationsFindService` for use in Kibana.
* @see /src/plugins/presentation_util/public/services/create/factory.ts
*/
export type CustomIntegrationsFindServiceFactory = KibanaPluginServiceFactory<
CustomIntegrationsFindService,
CustomIntegrationsStartDependencies
>;
/**
* A factory to produce the `CustomIntegrationsFindService` for use in Kibana.
*/
export const findServiceFactory: CustomIntegrationsFindServiceFactory = ({ coreStart }) => ({
findAppendedIntegrations: async (params) => {
const integrations: CustomIntegration[] = await coreStart.http.get(
ROUTES_APPEND_CUSTOM_INTEGRATIONS
);
return filterCustomIntegrations(integrations, params);
},
findReplacementIntegrations: async (params) => {
const replacements: CustomIntegration[] = await coreStart.http.get(
ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS
);
return filterCustomIntegrations(replacements, params);
},
});

View file

@ -0,0 +1,44 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
PluginServiceProviders,
PluginServiceProvider,
PluginServiceRegistry,
KibanaPluginServiceParams,
} from '../../../../presentation_util/public';
import { CustomIntegrationsServices } from '..';
import { CustomIntegrationsStartDependencies } from '../../types';
import { findServiceFactory } from './find';
import { platformServiceFactory } from './platform';
export { findServiceFactory } from './find';
export { platformServiceFactory } from './platform';
/**
* A set of `PluginServiceProvider`s for use in Kibana.
* @see /src/plugins/presentation_util/public/services/create/provider.tsx
*/
export const pluginServiceProviders: PluginServiceProviders<
CustomIntegrationsServices,
KibanaPluginServiceParams<CustomIntegrationsStartDependencies>
> = {
find: new PluginServiceProvider(findServiceFactory),
platform: new PluginServiceProvider(platformServiceFactory),
};
/**
* A `PluginServiceRegistry` for use in Kibana.
* @see /src/plugins/presentation_util/public/services/create/registry.tsx
*/
export const pluginServiceRegistry = new PluginServiceRegistry<
CustomIntegrationsServices,
KibanaPluginServiceParams<CustomIntegrationsStartDependencies>
>(pluginServiceProviders);

View 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { KibanaPluginServiceFactory } from '../../../../presentation_util/public';
import type { CustomIntegrationsPlatformService } from '../platform';
import type { CustomIntegrationsStartDependencies } from '../../types';
/**
* A type definition for a factory to produce the `CustomIntegrationsPlatformService` for use in Kibana.
* @see /src/plugins/presentation_util/public/services/create/factory.ts
*/
export type CustomIntegrationsPlatformServiceFactory = KibanaPluginServiceFactory<
CustomIntegrationsPlatformService,
CustomIntegrationsStartDependencies
>;
/**
* A factory to produce the `CustomIntegrationsPlatformService` for use in Kibana.
*/
export const platformServiceFactory: CustomIntegrationsPlatformServiceFactory = ({
coreStart,
}) => ({
getBasePath: coreStart.http.basePath.get,
getAbsolutePath: (path: string): string => coreStart.http.basePath.prepend(`${path}`),
});

View file

@ -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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export interface CustomIntegrationsPlatformService {
getBasePath: () => string;
getAbsolutePath: (path: string) => string;
}

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
PluginServiceProviders,
PluginServiceProvider,
PluginServiceRegistry,
} from '../../../../presentation_util/public';
import { CustomIntegrationsServices } from '..';
import { findServiceFactory } from '../stub/find';
import { platformServiceFactory } from '../stub/platform';
export { findServiceFactory } from '../stub/find';
export { platformServiceFactory } from '../stub/platform';
/**
* A set of `PluginServiceProvider`s for use in Storybook.
* @see /src/plugins/presentation_util/public/services/create/provider.tsx
*/
export const providers: PluginServiceProviders<CustomIntegrationsServices> = {
find: new PluginServiceProvider(findServiceFactory),
platform: new PluginServiceProvider(platformServiceFactory),
};
/**
* A `PluginServiceRegistry` for use in Storybook.
* @see /src/plugins/presentation_util/public/services/create/registry.tsx
*/
export const registry = new PluginServiceRegistry<CustomIntegrationsServices>(providers);

View file

@ -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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PluginServiceFactory } from '../../../../presentation_util/public';
import { CustomIntegrationsFindService, filterCustomIntegrations } from '../find';
/**
* A type definition for a factory to produce the `CustomIntegrationsFindService` with stubbed output.
* @see /src/plugins/presentation_util/public/services/create/factory.ts
*/
export type CustomIntegrationsFindServiceFactory =
PluginServiceFactory<CustomIntegrationsFindService>;
/**
* A factory to produce the `CustomIntegrationsFindService` with stubbed output.
*/
export const findServiceFactory: CustomIntegrationsFindServiceFactory = () => ({
findAppendedIntegrations: async (params) => {
const { integrations } = await import('./fixtures/integrations');
return filterCustomIntegrations(integrations, params);
},
findReplacementIntegrations: async (params) => {
const { integrations } = await import('./fixtures/integrations');
return filterCustomIntegrations(integrations, params);
},
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,27 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
PluginServiceProviders,
PluginServiceProvider,
PluginServiceRegistry,
} from '../../../../presentation_util/public';
import { CustomIntegrationsServices } from '..';
import { findServiceFactory } from './find';
import { platformServiceFactory } from './platform';
export { findServiceFactory } from './find';
export { platformServiceFactory } from './platform';
export const providers: PluginServiceProviders<CustomIntegrationsServices> = {
find: new PluginServiceProvider(findServiceFactory),
platform: new PluginServiceProvider(platformServiceFactory),
};
export const registry = new PluginServiceRegistry<CustomIntegrationsServices>(providers);

View file

@ -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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PluginServiceFactory } from '../../../../presentation_util/public';
import type { CustomIntegrationsPlatformService } from '../platform';
/**
* A type definition for a factory to produce the `CustomIntegrationsPlatformService` with stubbed output.
* @see /src/plugins/presentation_util/public/services/create/factory.ts
*/
export type CustomIntegrationsPlatformServiceFactory =
PluginServiceFactory<CustomIntegrationsPlatformService>;
/**
* A factory to produce the `CustomIntegrationsPlatformService` with stubbed output.
*/
export const platformServiceFactory: CustomIntegrationsPlatformServiceFactory = () => ({
getBasePath: () => '/basePath',
getAbsolutePath: (path: string): string => `/basePath${path}`,
});

View file

@ -6,14 +6,19 @@
* Side Public License, v 1.
*/
import type { PresentationUtilPluginStart } from '../../presentation_util/public';
import { CustomIntegration } from '../common';
export interface CustomIntegrationsSetup {
getAppendCustomIntegrations: () => Promise<CustomIntegration[]>;
getReplacementCustomIntegrations: () => Promise<CustomIntegration[]>;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface CustomIntegrationsStart {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AppPluginStartDependencies {}
export interface CustomIntegrationsStart {
ContextProvider: React.FC;
}
export interface CustomIntegrationsStartDependencies {
presentationUtil: PresentationUtilPluginStart;
}

View file

@ -0,0 +1,48 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { DecoratorFn } from '@storybook/react';
import { I18nProvider } from '@kbn/i18n/react';
import { PluginServiceRegistry } from '../../presentation_util/public';
import { pluginServices } from '../public/services';
import { CustomIntegrationsServices } from '../public/services';
import { providers } from '../public/services/storybook';
import { EuiThemeProvider } from '../../kibana_react/common/eui_styled_components';
/**
* Returns a Storybook Decorator that provides both the `I18nProvider` and access to `PluginServices`
* for components rendered in Storybook.
*/
export const getCustomIntegrationsContextDecorator =
(): DecoratorFn =>
(story, { globals }) => {
const ContextProvider = getCustomIntegrationsContextProvider();
const darkMode = globals.euiTheme === 'v8.dark' || globals.euiTheme === 'v7.dark';
return (
<I18nProvider>
<EuiThemeProvider darkMode={darkMode}>
<ContextProvider>{story()}</ContextProvider>
</EuiThemeProvider>
</I18nProvider>
);
};
/**
* Prepares `PluginServices` for use in Storybook and returns a React `Context.Provider` element
* so components that access `PluginServices` can be rendered.
*/
export const getCustomIntegrationsContextProvider = () => {
const registry = new PluginServiceRegistry<CustomIntegrationsServices>(providers);
pluginServices.setRegistry(registry.start({}));
return pluginServices.getContextProvider();
};

View file

@ -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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export {
getCustomIntegrationsContextDecorator as getStorybookContextDecorator,
getCustomIntegrationsContextProvider as getStorybookContextProvider,
} from '../storybook/decorator';

View file

@ -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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { defaultConfig } from '@kbn/storybook';
module.exports = defaultConfig;

View file

@ -0,0 +1,21 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { addons } from '@storybook/addons';
import { create } from '@storybook/theming';
import { PANEL_ID } from '@storybook/addon-actions';
addons.setConfig({
theme: create({
base: 'light',
brandTitle: 'Kibana Custom Integrations Storybook',
brandUrl: 'https://github.com/elastic/kibana/tree/master/src/plugins/custom_integrations',
}),
showPanel: true.valueOf,
selectedPanel: PANEL_ID,
});

View file

@ -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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { Title, Subtitle, Description, Primary, Stories } from '@storybook/addon-docs/blocks';
import { getCustomIntegrationsContextDecorator } from './decorator';
export const decorators = [getCustomIntegrationsContextDecorator()];
export const parameters = {
docs: {
page: () => (
<>
<Title />
<Subtitle />
<Description />
<Primary />
<Stories />
</>
),
},
};

View file

@ -6,8 +6,15 @@
"declaration": true,
"declarationMap": true
},
"include": ["common/**/*", "public/**/*", "server/**/*"],
"include": [
"../../../typings/**/*",
"common/**/*",
"public/**/*",
"server/**/*",
"storybook/**/*"
],
"references": [
{ "path": "../../core/tsconfig.json" }
{ "path": "../../core/tsconfig.json" },
{ "path": "../presentation_util/tsconfig.json" }
]
}

View file

@ -8,6 +8,7 @@ yarn storybook --site apm
yarn storybook --site canvas
yarn storybook --site codeeditor
yarn storybook --site ci_composite
yarn storybook --site custom_integrations
yarn storybook --site url_template_editor
yarn storybook --site dashboard
yarn storybook --site dashboard_enhanced

View file

@ -201,13 +201,15 @@ export const IntegrationsAppContext: React.FC<{
<EuiThemeProvider darkMode={isDarkMode}>
<UIExtensionsContext.Provider value={extensions}>
<FleetStatusProvider>
<Router history={history}>
<AgentPolicyContextProvider>
<PackageInstallProvider notifications={startServices.notifications}>
{children}
</PackageInstallProvider>
</AgentPolicyContextProvider>
</Router>
<startServices.customIntegrations.ContextProvider>
<Router history={history}>
<AgentPolicyContextProvider>
<PackageInstallProvider notifications={startServices.notifications}>
{children}
</PackageInstallProvider>
</AgentPolicyContextProvider>
</Router>
</startServices.customIntegrations.ContextProvider>
</FleetStatusProvider>
</UIExtensionsContext.Provider>
</EuiThemeProvider>

View file

@ -18,6 +18,10 @@ import {
} from '@elastic/eui';
import type { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
import styled, { useTheme } from 'styled-components';
import type { EuiTheme } from '../../../../../../../../../../../src/plugins/kibana_react/common';
import type {
PackageInfo,
PackageSpecCategory,
@ -28,13 +32,21 @@ import { entries } from '../../../../../types';
import { useGetCategories } from '../../../../../hooks';
import { AssetTitleMap, DisplayedAssets, ServiceTitleMap } from '../../../constants';
import {
withSuspense,
LazyReplacementCard,
} from '../../../../../../../../../../../src/plugins/custom_integrations/public';
import { NoticeModal } from './notice_modal';
const ReplacementCard = withSuspense(LazyReplacementCard);
interface Props {
packageInfo: PackageInfo;
}
export const Details: React.FC<Props> = memo(({ packageInfo }) => {
const theme = useTheme() as EuiTheme;
const { data: categoriesData, isLoading: isLoadingCategories } = useGetCategories();
const packageCategories: string[] = useMemo(() => {
if (!isLoadingCategories && categoriesData && categoriesData.response) {
@ -163,6 +175,23 @@ export const Details: React.FC<Props> = memo(({ packageInfo }) => {
toggleNoticeModal,
]);
const Replacements = styled(EuiFlexItem)`
margin: 0;
& .euiAccordion {
padding-top: ${parseInt(theme.eui.euiSizeL, 10) * 2}px;
&::before {
content: '';
display: block;
border-top: 1px solid ${theme.eui.euiColorLightShade};
position: relative;
top: -${theme.eui.euiSizeL};
margin: 0 ${theme.eui.euiSizeXS};
}
}
`;
return (
<>
<EuiPortal>
@ -181,6 +210,9 @@ export const Details: React.FC<Props> = memo(({ packageInfo }) => {
<EuiFlexItem>
<EuiDescriptionList type="column" compressed listItems={listItems} />
</EuiFlexItem>
<Replacements>
<ReplacementCard eprPackageName={packageInfo.name} />
</Replacements>
</EuiFlexGroup>
</>
);

View file

@ -10,6 +10,7 @@ import type {
CustomIntegration,
IntegrationCategory,
} from '../../../../../src/plugins/custom_integrations/common';
import { filterCustomIntegrations } from '../../../../../src/plugins/custom_integrations/public';
// Export this as a utility to find replacements for a package (e.g. in the overview-page for an EPR package)
function findReplacementsForEprPackage(
@ -20,9 +21,7 @@ function findReplacementsForEprPackage(
if (release === 'ga') {
return [];
}
return replacements.filter((customIntegration: CustomIntegration) => {
return customIntegration.eprOverlap === packageName;
});
return filterCustomIntegrations(replacements, { eprPackageName: packageName });
}
export function useMergeEprPackagesWithReplacements(

View file

@ -26,5 +26,6 @@ export const createStartDepsMock = (): MockedFleetStartDeps => {
return {
data: dataPluginMock.createStartContract(),
navigation: navigationPluginMock.createStartContract(),
customIntegrations: customIntegrationsMock.createStart(),
};
};

View file

@ -16,8 +16,12 @@ import { i18n } from '@kbn/i18n';
import type { NavigationPublicPluginStart } from 'src/plugins/navigation/public';
import type {
CustomIntegrationsStart,
CustomIntegrationsSetup,
} from 'src/plugins/custom_integrations/public';
import { DEFAULT_APP_CATEGORIES, AppNavLinkStatus } from '../../../../src/core/public';
import type { CustomIntegrationsSetup } from '../../../../src/plugins/custom_integrations/public';
import type {
DataPublicPluginSetup,
@ -76,6 +80,7 @@ export interface FleetSetupDeps {
export interface FleetStartDeps {
data: DataPublicPluginStart;
navigation: NavigationPublicPluginStart;
customIntegrations: CustomIntegrationsStart;
}
export interface FleetStartServices extends CoreStart, FleetStartDeps {

View file

@ -13,12 +13,12 @@ import { createBrowserHistory } from 'history';
import { I18nProvider } from '@kbn/i18n/react';
import { ScopedHistory } from '../../../../../src/core/public';
import { getStorybookContextProvider } from '../../../../../src/plugins/custom_integrations/storybook';
import { IntegrationsAppContext } from '../../public/applications/integrations/app';
import type { FleetConfigType, FleetStartServices } from '../../public/plugin';
// TODO: This is a contract leak, and should be on the context, rather than a setter.
// TODO: These are contract leaks, and should be on the context, rather than a setter.
import { setHttpClient } from '../../public/hooks/use_request';
import { setCustomIntegrations } from '../../public/services/custom_integrations';
import { getApplication } from './application';
@ -36,7 +36,6 @@ import { stubbedStartServices } from './stubs';
// Expect this to grow as components that are given Stories need access to mocked services.
export const StorybookContext: React.FC<{ storyContext?: StoryContext }> = ({
children: storyChildren,
storyContext,
}) => {
const basepath = '';
const browserHistory = createBrowserHistory();
@ -56,6 +55,9 @@ export const StorybookContext: React.FC<{ storyContext?: StoryContext }> = ({
injectedMetadata: {
getInjectedVar: () => null,
},
customIntegrations: {
ContextProvider: getStorybookContextProvider(),
},
...stubbedStartServices,
};

View file

@ -10,6 +10,6 @@ import type { DecoratorFn } from '@storybook/react';
import { StorybookContext } from './context';
export const decorator: DecoratorFn = (story: Function) => {
export const decorator: DecoratorFn = (story, storybook) => {
return <StorybookContext>{story()}</StorybookContext>;
};

View file

@ -1653,12 +1653,6 @@
"data.functions.esaggs.help": "AggConfig 集約を実行します",
"data.functions.esaggs.inspector.dataRequest.description": "このリクエストはElasticsearchにクエリし、ビジュアライゼーション用のデータを取得します。",
"data.functions.esaggs.inspector.dataRequest.title": "データ",
"dataViews.indexPatternLoad.help": "インデックスパターンを読み込みます",
"dataViews.functions.indexPatternLoad.id.help": "読み込むインデックスパターンID",
"dataViews.ensureDefaultIndexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。",
"dataViews.fetchFieldErrorTitle": "インデックスパターンのフィールド取得中にエラーが発生 {title}ID{id}",
"dataViews.indexPatternLoad.error.kibanaRequest": "サーバーでこの検索を実行するには、KibanaRequest が必要です。式実行パラメーターに要求オブジェクトを渡してください。",
"dataViews.unableWriteLabel": "インデックスパターンを書き込めません。このインデックスパターンへの最新の変更を取得するには、ページを更新してください。",
"data.inspector.table..dataDescriptionTooltip": "ビジュアライゼーションの元のデータを表示",
"data.inspector.table.dataTitle": "データ",
"data.inspector.table.downloadCSVToggleButtonLabel": "CSV をダウンロード",
@ -2297,6 +2291,12 @@
"data.searchSessions.sessionService.sessionObjectFetchError": "検索セッション情報を取得できませんでした",
"data.triggers.applyFilterDescription": "Kibanaフィルターが適用されるとき。単一の値または範囲フィルターにすることができます。",
"data.triggers.applyFilterTitle": "フィルターを適用",
"dataViews.indexPatternLoad.help": "インデックスパターンを読み込みます",
"dataViews.functions.indexPatternLoad.id.help": "読み込むインデックスパターンID",
"dataViews.ensureDefaultIndexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。",
"dataViews.fetchFieldErrorTitle": "インデックスパターンのフィールド取得中にエラーが発生 {title}ID{id}",
"dataViews.indexPatternLoad.error.kibanaRequest": "サーバーでこの検索を実行するには、KibanaRequest が必要です。式実行パラメーターに要求オブジェクトを渡してください。",
"dataViews.unableWriteLabel": "インデックスパターンを書き込めません。このインデックスパターンへの最新の変更を取得するには、ページを更新してください。",
"devTools.badge.readOnly.text": "読み取り専用",
"devTools.badge.readOnly.tooltip": "を保存できませんでした",
"devTools.devToolsTitle": "開発ツール",

View file

@ -1669,12 +1669,6 @@
"data.functions.esaggs.help": "运行 AggConfig 聚合",
"data.functions.esaggs.inspector.dataRequest.description": "此请求查询 Elasticsearch以获取可视化的数据。",
"data.functions.esaggs.inspector.dataRequest.title": "数据",
"dataViews.indexPatternLoad.help": "加载索引模式",
"dataViews.functions.indexPatternLoad.id.help": "要加载的索引模式 id",
"dataViews.ensureDefaultIndexPattern.bannerLabel": "要在 Kibana 中可视化和浏览数据,必须创建索引模式,以从 Elasticsearch 中检索数据。",
"dataViews.fetchFieldErrorTitle": "提取索引模式 {title} (ID: {id}) 的字段时出错",
"dataViews.indexPatternLoad.error.kibanaRequest": "在服务器上执行此搜索时需要 Kibana 请求。请向表达式执行模式参数提供请求对象。",
"dataViews.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。",
"data.inspector.table..dataDescriptionTooltip": "查看可视化后面的数据",
"data.inspector.table.dataTitle": "数据",
"data.inspector.table.downloadCSVToggleButtonLabel": "下载 CSV",
@ -2319,6 +2313,12 @@
"data.searchSessions.sessionService.sessionObjectFetchError": "无法提取搜索会话信息",
"data.triggers.applyFilterDescription": "应用 kibana 筛选时。可能是单个值或范围筛选。",
"data.triggers.applyFilterTitle": "应用筛选",
"dataViews.indexPatternLoad.help": "加载索引模式",
"dataViews.functions.indexPatternLoad.id.help": "要加载的索引模式 id",
"dataViews.ensureDefaultIndexPattern.bannerLabel": "要在 Kibana 中可视化和浏览数据,必须创建索引模式,以从 Elasticsearch 中检索数据。",
"dataViews.fetchFieldErrorTitle": "提取索引模式 {title} (ID: {id}) 的字段时出错",
"dataViews.indexPatternLoad.error.kibanaRequest": "在服务器上执行此搜索时需要 Kibana 请求。请向表达式执行模式参数提供请求对象。",
"dataViews.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。",
"devTools.badge.readOnly.text": "只读",
"devTools.badge.readOnly.tooltip": "无法保存",
"devTools.devToolsTitle": "开发工具",