[Fleet] Support browsing granular integrations (#99866) (#100707)

* Manual cherry pick of work to support integration tiles and package-level vars

* Fix types

* Remove registry input group typings

* Show integration-specific readme, title, and icon in package details page

* Revert unnecessary changes

* Add package-level `vars` field to package policy SO mappings

* Fix types

* Fix test

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Jen Huang <its.jenetic@gmail.com>
This commit is contained in:
Kibana Machine 2021-05-26 14:55:24 -04:00 committed by GitHub
parent a89dd49316
commit 112e174c31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 684 additions and 355 deletions

View file

@ -16,3 +16,4 @@ export { isValidNamespace } from './is_valid_namespace';
export { isDiffPathProtocol } from './is_diff_path_protocol';
export { LicenseService } from './license';
export { isAgentUpgradeable } from './is_agent_upgradeable';
export { doesPackageHaveIntegrations } from './packages_with_integrations';

View file

@ -40,6 +40,21 @@ const getStreamsForInputType = (
return streams;
};
// Reduces registry var def into config object entry
const varsReducer = (
configObject: PackagePolicyConfigRecord,
registryVar: RegistryVarsEntry
): PackagePolicyConfigRecord => {
const configEntry: PackagePolicyConfigRecordEntry = {
value: !registryVar.default && registryVar.multi ? [] : registryVar.default,
};
if (registryVar.type) {
configEntry.type = registryVar.type;
}
configObject![registryVar.name] = configEntry;
return configObject;
};
/*
* This service creates a package policy inputs definition from defaults provided in package info
*/
@ -58,21 +73,6 @@ export const packageToPackagePolicyInputs = (
if (packagePolicyTemplate?.inputs?.length) {
// Map each package package policy input to agent policy package policy input
packagePolicyTemplate.inputs.forEach((packageInput) => {
// Reduces registry var def into config object entry
const varsReducer = (
configObject: PackagePolicyConfigRecord,
registryVar: RegistryVarsEntry
): PackagePolicyConfigRecord => {
const configEntry: PackagePolicyConfigRecordEntry = {
value: !registryVar.default && registryVar.multi ? [] : registryVar.default,
};
if (registryVar.type) {
configEntry.type = registryVar.type;
}
configObject![registryVar.name] = configEntry;
return configObject;
};
// Map each package input stream into package policy input stream
const streams: NewPackagePolicyInputStream[] = getStreamsForInputType(
packageInput.type,
@ -121,7 +121,7 @@ export const packageToPackagePolicy = (
packagePolicyName?: string,
description?: string
): NewPackagePolicy => {
return {
const packagePolicy: NewPackagePolicy = {
name: packagePolicyName || `${packageInfo.name}-1`,
namespace,
description,
@ -135,4 +135,10 @@ export const packageToPackagePolicy = (
output_id: outputId,
inputs: packageToPackagePolicyInputs(packageInfo),
};
if (packageInfo.vars?.length) {
packagePolicy.vars = packageInfo.vars.reduce(varsReducer, {});
}
return packagePolicy;
};

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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { PackageInfo, PackageListItem } from '../types';
export const doesPackageHaveIntegrations = (pkgInfo: PackageInfo | PackageListItem) => {
return (pkgInfo.policy_templates || []).length > 1;
};

View file

@ -19,7 +19,12 @@ import type {
} from '../../constants';
import type { ValueOf } from '../../types';
import type { PackageSpecManifest, PackageSpecScreenshot } from './package_spec';
import type {
PackageSpecManifest,
PackageSpecIcon,
PackageSpecScreenshot,
PackageSpecCategory,
} from './package_spec';
export type InstallationStatus = typeof installationStatuses;
@ -118,19 +123,20 @@ interface RegistryOverridePropertyValue {
}
export type RegistryRelease = PackageSpecManifest['release'];
export interface RegistryImage {
src: string;
export interface RegistryImage extends PackageSpecIcon {
path: string;
title?: string;
size?: string;
type?: string;
}
export enum RegistryPolicyTemplateKeys {
name = 'name',
title = 'title',
description = 'description',
icons = 'icons',
screenshots = 'screenshots',
categories = 'categories',
data_streams = 'data_streams',
inputs = 'inputs',
readme = 'readme',
multiple = 'multiple',
}
@ -138,7 +144,12 @@ export interface RegistryPolicyTemplate {
[RegistryPolicyTemplateKeys.name]: string;
[RegistryPolicyTemplateKeys.title]: string;
[RegistryPolicyTemplateKeys.description]: string;
[RegistryPolicyTemplateKeys.icons]?: RegistryImage[];
[RegistryPolicyTemplateKeys.screenshots]?: RegistryImage[];
[RegistryPolicyTemplateKeys.categories]?: Array<PackageSpecCategory | undefined>;
[RegistryPolicyTemplateKeys.data_streams]?: string[];
[RegistryPolicyTemplateKeys.inputs]?: RegistryInput[];
[RegistryPolicyTemplateKeys.readme]?: string;
[RegistryPolicyTemplateKeys.multiple]?: boolean;
}
@ -148,15 +159,19 @@ export enum RegistryInputKeys {
description = 'description',
template_path = 'template_path',
condition = 'condition',
input_group = 'input_group',
vars = 'vars',
}
export type RegistryInputGroup = 'logs' | 'metrics';
export interface RegistryInput {
[RegistryInputKeys.type]: string;
[RegistryInputKeys.title]: string;
[RegistryInputKeys.description]: string;
[RegistryInputKeys.template_path]?: string;
[RegistryInputKeys.condition]?: string;
[RegistryInputKeys.input_group]?: RegistryInputGroup;
[RegistryInputKeys.vars]?: RegistryVarsEntry[];
}
@ -273,7 +288,7 @@ export interface RegistryDataStream {
[RegistryDataStreamKeys.streams]?: RegistryStream[];
[RegistryDataStreamKeys.package]: string;
[RegistryDataStreamKeys.path]: string;
[RegistryDataStreamKeys.ingest_pipeline]: string;
[RegistryDataStreamKeys.ingest_pipeline]?: string;
[RegistryDataStreamKeys.elasticsearch]?: RegistryElasticsearch;
[RegistryDataStreamKeys.dataset_is_prefix]?: boolean;
}
@ -307,7 +322,7 @@ export interface RegistryVarsEntry {
[RegistryVarsEntryKeys.required]?: boolean;
[RegistryVarsEntryKeys.show_user]?: boolean;
[RegistryVarsEntryKeys.multi]?: boolean;
[RegistryVarsEntryKeys.default]?: string | string[];
[RegistryVarsEntryKeys.default]?: string | string[] | boolean;
[RegistryVarsEntryKeys.os]?: {
[key: string]: {
default: string | string[];
@ -329,8 +344,11 @@ type Merge<FirstType, SecondType> = Omit<FirstType, Extract<keyof FirstType, key
// Managers public HTTP response types
export type PackageList = PackageListItem[];
export type PackageListItem = Installable<RegistrySearchResult> & {
integration?: string;
id: string;
};
export type PackageListItem = Installable<RegistrySearchResult>;
export type PackagesGroupedByStatus = Record<ValueOf<InstallationStatus>, PackageList>;
export type PackageInfo =
| Installable<Merge<RegistryPackage, EpmPackageAdditions>>

View file

@ -58,6 +58,7 @@ export interface NewPackagePolicy {
output_id: string;
package?: PackagePolicyPackage;
inputs: NewPackagePolicyInput[];
vars?: PackagePolicyConfigRecord;
}
export interface UpdatePackagePolicy extends NewPackagePolicy {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { RegistryPolicyTemplate } from './epm';
import type { RegistryPolicyTemplate, RegistryVarsEntry } from './epm';
// Based on https://github.com/elastic/package-spec/blob/master/versions/1/manifest.spec.yml#L8
export interface PackageSpecManifest {
@ -22,6 +22,7 @@ export interface PackageSpecManifest {
icons?: PackageSpecIcon[];
screenshots?: PackageSpecScreenshot[];
policy_templates?: RegistryPolicyTemplate[];
vars?: RegistryVarsEntry[];
owner: { github: string };
}

View file

@ -8,8 +8,7 @@
import type {
AssetReference,
CategorySummaryList,
Installable,
RegistrySearchResult,
PackageList,
PackageInfo,
PackageUsageStats,
InstallType,
@ -18,6 +17,7 @@ import type {
export interface GetCategoriesRequest {
query: {
experimental?: boolean;
include_policy_templates?: boolean;
};
}
@ -33,7 +33,7 @@ export interface GetPackagesRequest {
}
export interface GetPackagesResponse {
response: Array<Installable<RegistrySearchResult>>;
response: PackageList;
}
export interface GetLimitedPackagesResponse {

View file

@ -14,7 +14,7 @@ import { usePackageIconType } from '../hooks';
export const PackageIcon: React.FunctionComponent<
UsePackageIconType & Omit<EuiIconProps, 'type'>
> = ({ packageName, version, icons, tryApi, ...euiIconProps }) => {
const iconType = usePackageIconType({ packageName, version, icons, tryApi });
> = ({ packageName, integrationName, version, icons, tryApi, ...euiIconProps }) => {
const iconType = usePackageIconType({ packageName, integrationName, version, icons, tryApi });
return <EuiIcon size="s" type={iconType} {...euiIconProps} />;
};

View file

@ -56,7 +56,7 @@ export const PAGE_ROUTING_PATHS = {
policy_details: '/policies/:policyId/:tabId?',
policy_details_settings: '/policies/:policyId/settings',
add_integration_from_policy: '/policies/:policyId/add-integration',
add_integration_to_policy: '/integrations/:pkgkey/add-integration',
add_integration_to_policy: '/integrations/:pkgkey/add-integration/:integration?',
edit_integration: '/policies/:policyId/edit-integration/:packagePolicyId',
fleet: '/fleet',
fleet_agent_list: '/fleet/agents',
@ -77,17 +77,22 @@ export const pagePathGetters: {
integrations: () => '/integrations',
integrations_all: () => '/integrations',
integrations_installed: () => '/integrations/installed',
integration_details_overview: ({ pkgkey }) => `/integrations/detail/${pkgkey}/overview`,
integration_details_policies: ({ pkgkey }) => `/integrations/detail/${pkgkey}/policies`,
integration_details_settings: ({ pkgkey }) => `/integrations/detail/${pkgkey}/settings`,
integration_details_custom: ({ pkgkey }) => `/integrations/detail/${pkgkey}/custom`,
integration_details_overview: ({ pkgkey, integration }) =>
`/integrations/detail/${pkgkey}/overview${integration ? `?integration=${integration}` : ''}`,
integration_details_policies: ({ pkgkey, integration }) =>
`/integrations/detail/${pkgkey}/policies${integration ? `?integration=${integration}` : ''}`,
integration_details_settings: ({ pkgkey, integration }) =>
`/integrations/detail/${pkgkey}/settings${integration ? `?integration=${integration}` : ''}`,
integration_details_custom: ({ pkgkey, integration }) =>
`/integrations/detail/${pkgkey}/custom${integration ? `?integration=${integration}` : ''}`,
integration_policy_edit: ({ packagePolicyId }) =>
`/integrations/edit-integration/${packagePolicyId}`,
policies: () => '/policies',
policies_list: () => '/policies',
policy_details: ({ policyId, tabId }) => `/policies/${policyId}${tabId ? `/${tabId}` : ''}`,
add_integration_from_policy: ({ policyId }) => `/policies/${policyId}/add-integration`,
add_integration_to_policy: ({ pkgkey }) => `/integrations/${pkgkey}/add-integration`,
add_integration_to_policy: ({ pkgkey, integration }) =>
`/integrations/${pkgkey}/add-integration${integration ? `/${integration}` : ''}`,
edit_integration: ({ policyId, packagePolicyId }) =>
`/policies/${policyId}/edit-integration/${packagePolicyId}`,
fleet: () => '/fleet',

View file

@ -16,7 +16,8 @@ import { sendGetPackageInfoByKey } from './index';
type Package = PackageInfo | PackageListItem;
export interface UsePackageIconType {
packageName: Package['name'];
packageName: string;
integrationName?: string;
version: Package['version'];
icons?: Package['icons'];
tryApi?: boolean; // should it call API to try to find missing icons?
@ -26,6 +27,7 @@ const CACHED_ICONS = new Map<string, string>();
export const usePackageIconType = ({
packageName,
integrationName,
version,
icons: paramIcons,
tryApi = false,
@ -33,13 +35,13 @@ export const usePackageIconType = ({
const { toPackageImage } = useLinks();
const [iconList, setIconList] = useState<UsePackageIconType['icons']>();
const [iconType, setIconType] = useState<string>(''); // FIXME: use `empty` icon during initialization - see: https://github.com/elastic/kibana/issues/60622
const pkgKey = `${packageName}-${version}`;
const cacheKey = `${packageName}-${version}${integrationName ? `-${integrationName}` : ''}`;
// Generates an icon path or Eui Icon name based on an icon list from the package
// or by using the package name against logo icons from Eui
useEffect(() => {
if (CACHED_ICONS.has(pkgKey)) {
setIconType(CACHED_ICONS.get(pkgKey) || '');
if (CACHED_ICONS.has(cacheKey)) {
setIconType(CACHED_ICONS.get(cacheKey) || '');
return;
}
const svgIcons = (paramIcons || iconList)?.filter(
@ -48,29 +50,29 @@ export const usePackageIconType = ({
const localIconSrc =
Array.isArray(svgIcons) && toPackageImage(svgIcons[0], packageName, version);
if (localIconSrc) {
CACHED_ICONS.set(pkgKey, localIconSrc);
setIconType(CACHED_ICONS.get(pkgKey) || '');
CACHED_ICONS.set(cacheKey, localIconSrc);
setIconType(CACHED_ICONS.get(cacheKey) || '');
return;
}
const euiLogoIcon = ICON_TYPES.find((key) => key.toLowerCase() === `logo${packageName}`);
if (euiLogoIcon) {
CACHED_ICONS.set(pkgKey, euiLogoIcon);
CACHED_ICONS.set(cacheKey, euiLogoIcon);
setIconType(euiLogoIcon);
return;
}
if (tryApi && !paramIcons && !iconList) {
sendGetPackageInfoByKey(pkgKey)
sendGetPackageInfoByKey(cacheKey)
.catch((error) => undefined) // Ignore API errors
.then((res) => {
CACHED_ICONS.delete(pkgKey);
CACHED_ICONS.delete(cacheKey);
setIconList(res?.data?.response?.icons);
});
}
CACHED_ICONS.set(pkgKey, 'package');
CACHED_ICONS.set(cacheKey, 'package');
setIconType('package');
}, [paramIcons, pkgKey, toPackageImage, iconList, packageName, iconType, tryApi, version]);
}, [paramIcons, cacheKey, toPackageImage, iconList, packageName, iconType, tryApi, version]);
return iconType;
};

View file

@ -333,6 +333,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
packagePolicy={packagePolicy}
updatePackagePolicy={updatePackagePolicy}
validationResults={validationResults!}
submitAttempted={formState === 'INVALID'}
/>
{/* Only show the out-of-box configuration step if a UI extension is NOT registered */}

View file

@ -7,6 +7,7 @@
import { i18n } from '@kbn/i18n';
import { safeLoad } from 'js-yaml';
import { keyBy } from 'lodash';
import { getFlattenedObject, isValidNamespace } from '../../../../services';
import type {
@ -32,12 +33,12 @@ export type PackagePolicyInputValidationResults = PackagePolicyConfigValidationR
streams?: Record<PackagePolicyInputStream['id'], PackagePolicyConfigValidationResults>;
};
export interface PackagePolicyValidationResults {
export type PackagePolicyValidationResults = {
name: Errors;
description: Errors;
namespace: Errors;
inputs: Record<PackagePolicyInput['type'], PackagePolicyInputValidationResults> | null;
}
} & PackagePolicyConfigValidationResults;
/*
* Returns validation information for a given package policy and package info
@ -67,6 +68,16 @@ export const validatePackagePolicy = (
validationResults.namespace = [namespaceValidation.error];
}
// Validate package-level vars
const packageVarsByName = keyBy(packageInfo.vars || [], 'name');
const packageVars = Object.entries(packagePolicy.vars || {});
if (packageVars.length) {
validationResults.vars = packageVars.reduce((results, [name, varEntry]) => {
results[name] = validatePackagePolicyConfig(varEntry, packageVarsByName[name]);
return results;
}, {} as ValidationEntry);
}
if (
!packageInfo.policy_templates ||
packageInfo.policy_templates.length === 0 ||

View file

@ -25,7 +25,6 @@ import { Loading } from '../../../components';
import type { PackagePolicyValidationResults } from './services';
import { PackagePolicyInputPanel } from './components';
import type { CreatePackagePolicyFrom } from './types';
const findStreamsForInputType = (
inputType: string,
@ -50,22 +49,12 @@ const findStreamsForInputType = (
};
export const StepConfigurePackagePolicy: React.FunctionComponent<{
from?: CreatePackagePolicyFrom;
packageInfo: PackageInfo;
packagePolicy: NewPackagePolicy;
packagePolicyId?: string;
updatePackagePolicy: (fields: Partial<NewPackagePolicy>) => void;
validationResults: PackagePolicyValidationResults;
submitAttempted: boolean;
}> = ({
from = 'policy',
packageInfo,
packagePolicy,
packagePolicyId,
updatePackagePolicy,
validationResults,
submitAttempted,
}) => {
}> = ({ packageInfo, packagePolicy, updatePackagePolicy, validationResults, submitAttempted }) => {
// Configure inputs (and their streams)
// Assume packages only export one config template for now
const renderConfigureInputs = () =>

View file

@ -5,14 +5,13 @@
* 2.0.
*/
import React, { useEffect, useState } from 'react';
import React, { memo, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFormRow,
EuiFieldText,
EuiButtonEmpty,
EuiSpacer,
EuiText,
EuiComboBox,
EuiDescribedFormGroup,
@ -21,180 +20,145 @@ import {
EuiLink,
} from '@elastic/eui';
import type { AgentPolicy, PackageInfo, PackagePolicy, NewPackagePolicy } from '../../../types';
import { packageToPackagePolicyInputs } from '../../../services';
import type {
AgentPolicy,
PackageInfo,
PackagePolicy,
NewPackagePolicy,
RegistryVarsEntry,
} from '../../../types';
import { packageToPackagePolicy } from '../../../services';
import { Loading } from '../../../components';
import { pkgKeyFromPackageInfo } from '../../../services/pkg_key_from_package_info';
import { isAdvancedVar } from './services';
import type { PackagePolicyValidationResults } from './services';
import { PackagePolicyInputVarField } from './components';
export const StepDefinePackagePolicy: React.FunctionComponent<{
agentPolicy: AgentPolicy;
packageInfo: PackageInfo;
packagePolicy: NewPackagePolicy;
integration?: string;
updatePackagePolicy: (fields: Partial<NewPackagePolicy>) => void;
validationResults: PackagePolicyValidationResults;
}> = ({ agentPolicy, packageInfo, packagePolicy, updatePackagePolicy, validationResults }) => {
// Form show/hide states
const [isShowingAdvanced, setIsShowingAdvanced] = useState<boolean>(false);
// Update package policy's package and agent policy info
useEffect(() => {
const pkg = packagePolicy.package;
const currentPkgKey = pkg ? pkgKeyFromPackageInfo(pkg) : '';
const pkgKey = pkgKeyFromPackageInfo(packageInfo);
// If package has changed, create shell package policy with input&stream values based on package info
if (currentPkgKey !== pkgKey) {
// Existing package policies on the agent policy using the package name, retrieve highest number appended to package policy name
const pkgPoliciesNamePattern = new RegExp(`${packageInfo.name}-(\\d+)`);
const pkgPoliciesWithMatchingNames = (agentPolicy.package_policies as PackagePolicy[])
.filter((ds) => Boolean(ds.name.match(pkgPoliciesNamePattern)))
.map((ds) => parseInt(ds.name.match(pkgPoliciesNamePattern)![1], 10))
.sort((a, b) => a - b);
updatePackagePolicy({
// FIXME: Improve package policies name uniqueness - https://github.com/elastic/kibana/issues/72948
name: `${packageInfo.name}-${
pkgPoliciesWithMatchingNames.length
? pkgPoliciesWithMatchingNames[pkgPoliciesWithMatchingNames.length - 1] + 1
: 1
}`,
package: {
name: packageInfo.name,
title: packageInfo.title,
version: packageInfo.version,
},
inputs: packageToPackagePolicyInputs(packageInfo),
});
}
// If agent policy has changed, update package policy's agent policy ID and namespace
if (packagePolicy.policy_id !== agentPolicy.id) {
updatePackagePolicy({
policy_id: agentPolicy.id,
namespace: agentPolicy.namespace,
});
}
}, [
packagePolicy.package,
packagePolicy.policy_id,
submitAttempted: boolean;
}> = memo(
({
agentPolicy,
packageInfo,
packagePolicy,
integration,
updatePackagePolicy,
]);
validationResults,
submitAttempted,
}) => {
// Form show/hide states
const [isShowingAdvanced, setIsShowingAdvanced] = useState<boolean>(false);
return validationResults ? (
<EuiDescribedFormGroup
title={
<h4>
// Package-level vars
const requiredVars: RegistryVarsEntry[] = [];
const advancedVars: RegistryVarsEntry[] = [];
if (packageInfo.vars) {
packageInfo.vars.forEach((varDef) => {
if (isAdvancedVar(varDef)) {
advancedVars.push(varDef);
} else {
requiredVars.push(varDef);
}
});
}
// Update package policy's package and agent policy info
useEffect(() => {
const pkg = packagePolicy.package;
const currentPkgKey = pkg ? pkgKeyFromPackageInfo(pkg) : '';
const pkgKey = pkgKeyFromPackageInfo(packageInfo);
// If package has changed, create shell package policy with input&stream values based on package info
if (currentPkgKey !== pkgKey) {
// Existing package policies on the agent policy using the package name, retrieve highest number appended to package policy name
const pkgPoliciesNamePattern = new RegExp(`${packageInfo.name}-(\\d+)`);
const pkgPoliciesWithMatchingNames = (agentPolicy.package_policies as PackagePolicy[])
.filter((ds) => Boolean(ds.name.match(pkgPoliciesNamePattern)))
.map((ds) => parseInt(ds.name.match(pkgPoliciesNamePattern)![1], 10))
.sort((a, b) => a - b);
updatePackagePolicy(
packageToPackagePolicy(
packageInfo,
agentPolicy.id,
packagePolicy.output_id,
packagePolicy.namespace,
`${packageInfo.name}-${
pkgPoliciesWithMatchingNames.length
? pkgPoliciesWithMatchingNames[pkgPoliciesWithMatchingNames.length - 1] + 1
: 1
}`,
packagePolicy.description
)
);
}
// If agent policy has changed, update package policy's agent policy ID and namespace
if (packagePolicy.policy_id !== agentPolicy.id) {
updatePackagePolicy({
policy_id: agentPolicy.id,
namespace: agentPolicy.namespace,
});
}
}, [packagePolicy, agentPolicy, packageInfo, updatePackagePolicy, integration]);
return validationResults ? (
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.integrationSettingsSectionTitle"
defaultMessage="Integration settings"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.integrationSettingsSectionTitle"
defaultMessage="Integration settings"
id="xpack.fleet.createPackagePolicy.stepConfigure.integrationSettingsSectionDescription"
defaultMessage="Choose a name and description to help identify how this integration will be used."
/>
</h4>
}
description={
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.integrationSettingsSectionDescription"
defaultMessage="Choose a name and description to help identify how this integration will be used."
/>
}
>
<>
{/* Name */}
<EuiFormRow
isInvalid={!!validationResults.name}
error={validationResults.name}
label={
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyNameInputLabel"
defaultMessage="Integration name"
/>
}
>
<EuiFieldText
value={packagePolicy.name}
onChange={(e) =>
updatePackagePolicy({
name: e.target.value,
})
}
data-test-subj="packagePolicyNameInput"
/>
</EuiFormRow>
{/* Description */}
<EuiFormRow
label={
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyDescriptionInputLabel"
defaultMessage="Description"
/>
}
labelAppend={
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.inputVarFieldOptionalLabel"
defaultMessage="Optional"
/>
</EuiText>
}
isInvalid={!!validationResults.description}
error={validationResults.description}
>
<EuiFieldText
value={packagePolicy.description}
onChange={(e) =>
updatePackagePolicy({
description: e.target.value,
})
}
data-test-subj="packagePolicyDescriptionInput"
/>
</EuiFormRow>
<EuiSpacer size="m" />
{/* Advanced options toggle */}
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
iconType={isShowingAdvanced ? 'arrowDown' : 'arrowRight'}
onClick={() => setIsShowingAdvanced(!isShowingAdvanced)}
flush="left"
>
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.advancedOptionsToggleLinkText"
defaultMessage="Advanced options"
/>
</EuiButtonEmpty>
</EuiFlexItem>
{!isShowingAdvanced && !!validationResults.namespace ? (
<EuiFlexItem grow={false}>
<EuiText color="danger" size="s">
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.errorCountText"
defaultMessage="{count, plural, one {# error} other {# errors}}"
values={{ count: 1 }}
/>
</EuiText>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
{/* Advanced options content */}
{/* Todo: Populate list of existing namespaces */}
{isShowingAdvanced ? (
<>
<EuiSpacer size="m" />
}
>
<EuiFlexGroup direction="column" gutterSize="m">
{/* Name */}
<EuiFlexItem>
<EuiFormRow
isInvalid={!!validationResults.namespace}
error={validationResults.namespace}
isInvalid={!!validationResults.name}
error={validationResults.name}
label={
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyNamespaceInputLabel"
defaultMessage="Namespace"
id="xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyNameInputLabel"
defaultMessage="Integration name"
/>
}
>
<EuiFieldText
value={packagePolicy.name}
onChange={(e) =>
updatePackagePolicy({
name: e.target.value,
})
}
data-test-subj="packagePolicyNameInput"
/>
</EuiFormRow>
</EuiFlexItem>
{/* Description */}
<EuiFlexItem>
<EuiFormRow
label={
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyDescriptionInputLabel"
defaultMessage="Description"
/>
}
helpText={
@ -216,30 +180,156 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{
}}
/>
}
labelAppend={
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.inputVarFieldOptionalLabel"
defaultMessage="Optional"
/>
</EuiText>
}
isInvalid={!!validationResults.description}
error={validationResults.description}
>
<EuiComboBox
noSuggestions
singleSelection={true}
selectedOptions={
packagePolicy.namespace ? [{ label: packagePolicy.namespace }] : []
<EuiFieldText
value={packagePolicy.description}
onChange={(e) =>
updatePackagePolicy({
description: e.target.value,
})
}
onCreateOption={(newNamespace: string) => {
updatePackagePolicy({
namespace: newNamespace,
});
}}
onChange={(newNamespaces: Array<{ label: string }>) => {
updatePackagePolicy({
namespace: newNamespaces.length ? newNamespaces[0].label : '',
});
}}
data-test-subj="packagePolicyDescriptionInput"
/>
</EuiFormRow>
</>
) : null}
</>
</EuiDescribedFormGroup>
) : (
<Loading />
);
};
</EuiFlexItem>
{/* Required vars */}
{requiredVars.map((varDef) => {
const { name: varName, type: varType } = varDef;
if (!packagePolicy.vars || !packagePolicy.vars[varName]) return null;
const value = packagePolicy.vars[varName].value;
return (
<EuiFlexItem key={varName}>
<PackagePolicyInputVarField
varDef={varDef}
value={value}
onChange={(newValue: any) => {
updatePackagePolicy({
vars: {
...packagePolicy.vars,
[varName]: {
type: varType,
value: newValue,
},
},
});
}}
errors={validationResults.vars![varName]}
forceShowErrors={submitAttempted}
/>
</EuiFlexItem>
);
})}
{/* Advanced options toggle */}
<EuiFlexItem>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
iconType={isShowingAdvanced ? 'arrowDown' : 'arrowRight'}
onClick={() => setIsShowingAdvanced(!isShowingAdvanced)}
flush="left"
>
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.advancedOptionsToggleLinkText"
defaultMessage="Advanced options"
/>
</EuiButtonEmpty>
</EuiFlexItem>
{!isShowingAdvanced && !!validationResults.namespace ? (
<EuiFlexItem grow={false}>
<EuiText color="danger" size="s">
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.errorCountText"
defaultMessage="{count, plural, one {# error} other {# errors}}"
values={{ count: 1 }}
/>
</EuiText>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiFlexItem>
{/* Advanced options content */}
{/* Todo: Populate list of existing namespaces */}
{isShowingAdvanced ? (
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<EuiFormRow
isInvalid={!!validationResults.namespace}
error={validationResults.namespace}
label={
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyNamespaceInputLabel"
defaultMessage="Namespace"
/>
}
>
<EuiComboBox
noSuggestions
singleSelection={true}
selectedOptions={
packagePolicy.namespace ? [{ label: packagePolicy.namespace }] : []
}
onCreateOption={(newNamespace: string) => {
updatePackagePolicy({
namespace: newNamespace,
});
}}
onChange={(newNamespaces: Array<{ label: string }>) => {
updatePackagePolicy({
namespace: newNamespaces.length ? newNamespaces[0].label : '',
});
}}
/>
</EuiFormRow>
</EuiFlexItem>
{/* Advanced vars */}
{advancedVars.map((varDef) => {
const { name: varName, type: varType } = varDef;
if (!packagePolicy.vars || !packagePolicy.vars[varName]) return null;
const value = packagePolicy.vars![varName].value;
return (
<EuiFlexItem key={varName}>
<PackagePolicyInputVarField
varDef={varDef}
value={value}
onChange={(newValue: any) => {
updatePackagePolicy({
vars: {
...packagePolicy.vars,
[varName]: {
type: varType,
value: newValue,
},
},
});
}}
errors={validationResults.vars![varName]}
forceShowErrors={submitAttempted}
/>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiDescribedFormGroup>
) : (
<Loading />
);
}
);

View file

@ -351,15 +351,14 @@ export const EditPackagePolicyForm = memo<{
packagePolicy={packagePolicy}
updatePackagePolicy={updatePackagePolicy}
validationResults={validationResults!}
submitAttempted={formState === 'INVALID'}
/>
{/* Only show the out-of-box configuration step if a UI extension is NOT registered */}
{!ExtensionView && (
<StepConfigurePackagePolicy
from={'edit'}
packageInfo={packageInfo}
packagePolicy={packagePolicy}
packagePolicyId={packagePolicyId}
updatePackagePolicy={updatePackagePolicy}
validationResults={validationResults!}
submitAttempted={formState === 'INVALID'}
@ -386,7 +385,6 @@ export const EditPackagePolicyForm = memo<{
packagePolicy,
updatePackagePolicy,
validationResults,
packagePolicyId,
formState,
originalPackagePolicy,
ExtensionView,

View file

@ -9,13 +9,13 @@ import React from 'react';
import styled from 'styled-components';
import { EuiCard } from '@elastic/eui';
import type { PackageInfo, PackageListItem } from '../../../types';
import type { PackageListItem } from '../../../types';
import { useLink } from '../../../hooks';
import { PackageIcon } from '../../../components/package_icon';
import { RELEASE_BADGE_LABEL, RELEASE_BADGE_DESCRIPTION } from './release_badge';
type PackageCardProps = PackageListItem | PackageInfo;
type PackageCardProps = PackageListItem;
// adding the `href` causes EuiCard to use a `a` instead of a `button`
// `a` tags use `euiLinkColor` which results in blueish Badge text
@ -31,6 +31,7 @@ export function PackageCard({
release,
status,
icons,
integration,
...restProps
}: PackageCardProps) {
const { getHref } = useLink();
@ -44,8 +45,19 @@ export function PackageCard({
<Card
title={title || ''}
description={description}
icon={<PackageIcon icons={icons} packageName={name} version={version} size="xl" />}
href={getHref('integration_details_overview', { pkgkey: `${name}-${urlVersion}` })}
icon={
<PackageIcon
icons={icons}
packageName={name}
integrationName={integration}
version={version}
size="xl"
/>
}
href={getHref('integration_details_overview', {
pkgkey: `${name}-${urlVersion}`,
...(integration ? { integration } : {}),
})}
betaBadgeLabel={release && release !== 'ga' ? RELEASE_BADGE_LABEL[release] : undefined}
betaBadgeTooltipContent={
release && release !== 'ga' ? RELEASE_BADGE_DESCRIPTION[release] : undefined

View file

@ -25,7 +25,6 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { Loading } from '../../../components';
import type { PackageList } from '../../../types';
import { useLocalSearch, searchIdField } from '../hooks';
import { pkgKeyFromPackageInfo } from '../../../services/pkg_key_from_package_info';
import { PackageCard } from './package_card';
@ -153,11 +152,13 @@ function GridColumn({ list, showMissingIntegrationMessage = false }: GridColumnP
return (
<EuiFlexGrid gutterSize="l" columns={3}>
{list.length ? (
list.map((item) => (
<EuiFlexItem key={pkgKeyFromPackageInfo(item)}>
<PackageCard {...item} />
</EuiFlexItem>
))
list.map((item) => {
return (
<EuiFlexItem key={item.id}>
<PackageCard {...item} />
</EuiFlexItem>
);
})
) : (
<EuiFlexItem grow={3}>
<EuiText>

View file

@ -8,11 +8,10 @@
import { Search as LocalSearch } from 'js-search';
import { useEffect, useRef } from 'react';
import type { PackageList, PackageListItem } from '../../../types';
import type { PackageList } from '../../../types';
export type SearchField = keyof PackageListItem;
export const searchIdField: SearchField = 'name';
export const fieldsToSearch: SearchField[] = ['description', 'name', 'title'];
export const searchIdField = 'id';
export const fieldsToSearch = ['description', 'name', 'title'];
export function useLocalSearch(packageList: PackageList) {
const localSearchRef = useRef<LocalSearch | null>(null);

View file

@ -37,10 +37,11 @@ const Panel = styled(EuiPanel)`
export function IconPanel({
packageName,
integrationName,
version,
icons,
}: Pick<UsePackageIconType, 'packageName' | 'version' | 'icons'>) {
const iconType = usePackageIconType({ packageName, version, icons });
}: Pick<UsePackageIconType, 'packageName' | 'integrationName' | 'version' | 'icons'>) {
const iconType = usePackageIconType({ packageName, integrationName, version, icons });
return (
<PanelWrapper>

View file

@ -74,7 +74,9 @@ export function Detail() {
const { getHref, getPath } = useLink();
const hasWriteCapabilites = useCapabilities().write;
const history = useHistory();
const location = useLocation();
const { pathname, search, hash } = useLocation();
const queryParams = useMemo(() => new URLSearchParams(search), [search]);
const integration = useMemo(() => queryParams.get('integration'), [queryParams]);
// Package info state
const [packageInfo, setPackageInfo] = useState<PackageInfo | null>(null);
@ -120,6 +122,16 @@ export function Detail() {
}
}, [packageInfoData, setPackageInstallStatus, setPackageInfo]);
const integrationInfo = useMemo(
() =>
integration
? packageInfo?.policy_templates?.find(
(policyTemplate) => policyTemplate.name === integration
)
: undefined,
[integration, packageInfo]
);
const headerLeftContent = useMemo(
() => (
<EuiFlexGroup direction="column" gutterSize="m">
@ -147,8 +159,9 @@ export function Detail() {
) : (
<IconPanel
packageName={packageInfo.name}
integrationName={integrationInfo?.name}
version={packageInfo.version}
icons={packageInfo.icons}
icons={integrationInfo?.icons || packageInfo.icons}
/>
)}
</EuiFlexItem>
@ -157,7 +170,7 @@ export function Detail() {
<FlexItemWithMinWidth grow={false}>
<EuiText>
{/* Render space in place of package name while package info loads to prevent layout from jumping around */}
<h1>{packageInfo?.title || '\u00A0'}</h1>
<h1>{integrationInfo?.title || packageInfo?.title || '\u00A0'}</h1>
</EuiText>
</FlexItemWithMinWidth>
{packageInfo?.release && packageInfo.release !== 'ga' ? (
@ -174,7 +187,7 @@ export function Detail() {
</EuiFlexItem>
</EuiFlexGroup>
),
[getHref, isLoading, packageInfo]
[getHref, integrationInfo, isLoading, packageInfo]
);
const handleAddIntegrationPolicyClick = useCallback<ReactEventHandler>(
@ -184,9 +197,9 @@ export function Detail() {
// The object below, given to `createHref` is explicitly accessing keys of `location` in order
// to ensure that dependencies to this `useCallback` is set correctly (because `location` is mutable)
const currentPath = history.createHref({
pathname: location.pathname,
search: location.search,
hash: location.hash,
pathname,
search,
hash,
});
const redirectToPath: CreatePackagePolicyRouteState['onSaveNavigateTo'] &
CreatePackagePolicyRouteState['onCancelNavigateTo'] = [
@ -204,11 +217,12 @@ export function Detail() {
history.push({
pathname: getPath('add_integration_to_policy', {
pkgkey,
...(integration ? { integration } : {}),
}),
state: redirectBackRouteState,
});
},
[getPath, history, location.hash, location.pathname, location.search, pkgkey]
[getPath, history, hash, pathname, search, pkgkey, integration]
);
const headerRightContent = useMemo(
@ -255,6 +269,7 @@ export function Detail() {
iconType="plusInCircle"
href={getHref('add_integration_to_policy', {
pkgkey,
...(integration ? { integration } : {}),
})}
onClick={handleAddIntegrationPolicyClick}
data-test-subj="addIntegrationPolicyButton"
@ -263,7 +278,7 @@ export function Detail() {
id="xpack.fleet.epm.addPackagePolicyButtonText"
defaultMessage="Add {packageName}"
values={{
packageName: packageInfo.title,
packageName: integrationInfo?.title || packageInfo.title,
}}
/>
</EuiButton>
@ -290,6 +305,8 @@ export function Detail() {
getHref,
handleAddIntegrationPolicyClick,
hasWriteCapabilites,
integration,
integrationInfo,
packageInfo,
packageInstallStatus,
pkgkey,
@ -316,6 +333,7 @@ export function Detail() {
'data-test-subj': `tab-overview`,
href: getHref('integration_details_overview', {
pkgkey: packageInfoKey,
...(integration ? { integration } : {}),
}),
},
];
@ -333,6 +351,7 @@ export function Detail() {
'data-test-subj': `tab-policies`,
href: getHref('integration_details_policies', {
pkgkey: packageInfoKey,
...(integration ? { integration } : {}),
}),
});
}
@ -349,6 +368,7 @@ export function Detail() {
'data-test-subj': `tab-settings`,
href: getHref('integration_details_settings', {
pkgkey: packageInfoKey,
...(integration ? { integration } : {}),
}),
});
@ -365,12 +385,13 @@ export function Detail() {
'data-test-subj': `tab-custom`,
href: getHref('integration_details_custom', {
pkgkey: packageInfoKey,
...(integration ? { integration } : {}),
}),
});
}
return tabs;
}, [getHref, packageInfo, panel, showCustomTab, packageInstallStatus]);
}, [packageInfo, panel, getHref, integration, packageInstallStatus, showCustomTab]);
return (
<WithHeaderLayout
@ -380,7 +401,7 @@ export function Detail() {
tabs={headerTabs}
tabsClassName="fleet__epm__shiftNavTabs"
>
{packageInfo ? <Breadcrumbs packageTitle={packageInfo.title} /> : null}
{integrationInfo ? <Breadcrumbs packageTitle={integrationInfo.title} /> : null}
{packageInfoError ? (
<Error
title={
@ -396,7 +417,7 @@ export function Detail() {
) : (
<Switch>
<Route path={PAGE_ROUTING_PATHS.integration_details_overview}>
<OverviewPage packageInfo={packageInfo} />
<OverviewPage packageInfo={packageInfo} integrationInfo={integrationInfo} />
</Route>
<Route path={PAGE_ROUTING_PATHS.integration_details_settings}>
<SettingsPage packageInfo={packageInfo} />

View file

@ -4,11 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo } from 'react';
import React, { memo, useMemo } from 'react';
import styled from 'styled-components';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import type { PackageInfo } from '../../../../../types';
import type { PackageInfo, RegistryPolicyTemplate } from '../../../../../types';
import { Screenshots } from './screenshots';
import { Readme } from './readme';
@ -16,6 +16,7 @@ import { Details } from './details';
interface Props {
packageInfo: PackageInfo;
integrationInfo?: RegistryPolicyTemplate;
}
const LeftColumn = styled(EuiFlexItem)`
@ -25,14 +26,19 @@ const LeftColumn = styled(EuiFlexItem)`
}
`;
export const OverviewPage: React.FC<Props> = memo(({ packageInfo }: Props) => {
export const OverviewPage: React.FC<Props> = memo(({ packageInfo, integrationInfo }) => {
const screenshots = useMemo(() => integrationInfo?.screenshots || packageInfo.screenshots || [], [
integrationInfo,
packageInfo.screenshots,
]);
return (
<EuiFlexGroup alignItems="flexStart">
<LeftColumn grow={2} />
<EuiFlexItem grow={9} className="eui-textBreakWord">
{packageInfo.readme ? (
<Readme
readmePath={packageInfo.readme}
readmePath={integrationInfo?.readme || packageInfo.readme}
packageName={packageInfo.name}
version={packageInfo.version}
/>
@ -40,10 +46,10 @@ export const OverviewPage: React.FC<Props> = memo(({ packageInfo }: Props) => {
</EuiFlexItem>
<EuiFlexItem grow={3}>
<EuiFlexGroup direction="column" gutterSize="l" alignItems="flexStart">
{packageInfo.screenshots && packageInfo.screenshots.length ? (
{screenshots.length ? (
<EuiFlexItem>
<Screenshots
images={packageInfo.screenshots}
images={screenshots}
packageName={packageInfo.name}
version={packageInfo.version}
/>

View file

@ -5,22 +5,24 @@
* 2.0.
*/
import React, { useState } from 'react';
import React, { memo, useState, useMemo } from 'react';
import { useRouteMatch, Switch, Route, useLocation, useHistory } from 'react-router-dom';
import semverLt from 'semver/functions/lt';
import type { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab';
import { i18n } from '@kbn/i18n';
import { installationStatuses } from '../../../../../../../common/constants';
import { PAGE_ROUTING_PATHS } from '../../../../constants';
import { useLink, useGetCategories, useGetPackages, useBreadcrumbs } from '../../../../hooks';
import { doesPackageHaveIntegrations } from '../../../../services';
import { WithHeaderLayout } from '../../../../layouts';
import type { CategorySummaryItem } from '../../../../types';
import type { CategorySummaryItem, PackageList } from '../../../../types';
import { PackageListGrid } from '../../components/package_list_grid';
import { CategoryFacets } from './category_facets';
import { HeroCopy, HeroImage } from './header';
export function EPMHomePage() {
export const EPMHomePage: React.FC = memo(() => {
const {
params: { tabId },
} = useRouteMatch<{ tabId?: string }>();
@ -61,51 +63,94 @@ export function EPMHomePage() {
</Switch>
</WithHeaderLayout>
);
}
});
function InstalledPackages() {
// Packages can export multiple integrations, aka `policy_templates`
// In the case where packages ship >1 `policy_templates`, we flatten out the
// list of packages by bringing all integrations to top-level so that
// each integration is displayed as its own tile
const packageListToIntegrationsList = (packages: PackageList): PackageList => {
return packages.reduce((acc: PackageList, pkg) => {
const { policy_templates: policyTemplates = [], ...restOfPackage } = pkg;
return [
...acc,
restOfPackage,
...(doesPackageHaveIntegrations(pkg)
? policyTemplates.map((integration) => {
const { name, title, description, icons } = integration;
return {
...restOfPackage,
id: `${restOfPackage}-${name}`,
integration: name,
title,
description,
icons: icons || restOfPackage.icons,
};
})
: []),
];
}, []);
};
const InstalledPackages: React.FC = memo(() => {
useBreadcrumbs('integrations_installed');
const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages({
experimental: true,
});
const [selectedCategory, setSelectedCategory] = useState('');
const title = i18n.translate('xpack.fleet.epmList.installedTitle', {
defaultMessage: 'Installed integrations',
});
const allInstalledPackages =
allPackages && allPackages.response
? allPackages.response.filter((pkg) => pkg.status === installationStatuses.Installed)
: [];
const updatablePackages = allInstalledPackages.filter(
(item) => 'savedObject' in item && item.version > item.savedObject.attributes.version
const allInstalledPackages = useMemo(
() =>
(allPackages?.response || []).filter((pkg) => pkg.status === installationStatuses.Installed),
[allPackages?.response]
);
const categories = [
{
id: '',
title: i18n.translate('xpack.fleet.epmList.allFilterLinkText', {
defaultMessage: 'All',
}),
count: allInstalledPackages.length,
},
{
id: 'updates_available',
title: i18n.translate('xpack.fleet.epmList.updatesAvailableFilterLinkText', {
defaultMessage: 'Updates available',
}),
count: updatablePackages.length,
},
];
const updatablePackages = useMemo(
() =>
allInstalledPackages.filter(
(item) =>
'savedObject' in item && semverLt(item.savedObject.attributes.version, item.version)
),
[allInstalledPackages]
);
const controls = (
<CategoryFacets
categories={categories}
selectedCategory={selectedCategory}
onCategoryChange={({ id }: CategorySummaryItem) => setSelectedCategory(id)}
/>
const title = useMemo(
() =>
i18n.translate('xpack.fleet.epmList.installedTitle', {
defaultMessage: 'Installed integrations',
}),
[]
);
const categories = useMemo(
() => [
{
id: '',
title: i18n.translate('xpack.fleet.epmList.allFilterLinkText', {
defaultMessage: 'All',
}),
count: allInstalledPackages.length,
},
{
id: 'updates_available',
title: i18n.translate('xpack.fleet.epmList.updatesAvailableFilterLinkText', {
defaultMessage: 'Updates available',
}),
count: updatablePackages.length,
},
],
[allInstalledPackages.length, updatablePackages.length]
);
const controls = useMemo(
() => (
<CategoryFacets
categories={categories}
selectedCategory={selectedCategory}
onCategoryChange={({ id }: CategorySummaryItem) => setSelectedCategory(id)}
/>
),
[categories, selectedCategory]
);
return (
@ -116,9 +161,9 @@ function InstalledPackages() {
list={selectedCategory === 'updates_available' ? updatablePackages : allInstalledPackages}
/>
);
}
});
function AvailablePackages() {
const AvailablePackages: React.FC = memo(() => {
useBreadcrumbs('integrations_all');
const history = useHistory();
const queryParams = new URLSearchParams(useLocation().search);
@ -128,24 +173,36 @@ function AvailablePackages() {
const { data: categoryPackagesRes, isLoading: isLoadingCategoryPackages } = useGetPackages({
category: selectedCategory,
});
const { data: categoriesRes, isLoading: isLoadingCategories } = useGetCategories();
const packages =
categoryPackagesRes && categoryPackagesRes.response ? categoryPackagesRes.response : [];
const title = i18n.translate('xpack.fleet.epmList.allTitle', {
defaultMessage: 'Browse by category',
const { data: categoriesRes, isLoading: isLoadingCategories } = useGetCategories({
include_policy_templates: true,
});
const packages = useMemo(
() => packageListToIntegrationsList(categoryPackagesRes?.response || []),
[categoryPackagesRes]
);
const categories = [
{
id: '',
title: i18n.translate('xpack.fleet.epmList.allPackagesFilterLinkText', {
defaultMessage: 'All',
const title = useMemo(
() =>
i18n.translate('xpack.fleet.epmList.allTitle', {
defaultMessage: 'Browse by category',
}),
count: allPackagesRes?.response?.length || 0,
},
...(categoriesRes ? categoriesRes.response : []),
];
[]
);
const categories = useMemo(
() => [
{
id: '',
title: i18n.translate('xpack.fleet.epmList.allPackagesFilterLinkText', {
defaultMessage: 'All',
}),
count: allPackagesRes?.response?.length || 0,
},
...(categoriesRes ? categoriesRes.response : []),
],
[allPackagesRes?.response?.length, categoriesRes]
);
const controls = categories ? (
<CategoryFacets
isLoading={isLoadingCategories || isLoadingAllPackages}
@ -171,4 +228,4 @@ function AvailablePackages() {
showMissingIntegrationMessage
/>
);
}
});

View file

@ -20,6 +20,7 @@ export {
outputRoutesService,
settingsRoutesService,
appRoutesService,
packageToPackagePolicy,
packageToPackagePolicyInputs,
storedPackagePoliciesToAgentInputs,
fullAgentPolicyToYaml,
@ -28,4 +29,5 @@ export {
isValidNamespace,
LicenseService,
isAgentUpgradeable,
doesPackageHaveIntegrations,
} from '../../../../common';

View file

@ -24,7 +24,9 @@ jest.mock('../../services/package_policy', (): {
} => {
return {
packagePolicyService: {
compilePackagePolicyInputs: jest.fn((packageInfo, dataInputs) => Promise.resolve(dataInputs)),
compilePackagePolicyInputs: jest.fn((packageInfo, vars, dataInputs) =>
Promise.resolve(dataInputs)
),
buildPackagePolicyFromPackage: jest.fn(),
bulkCreate: jest.fn(),
create: jest.fn((soClient, esClient, newData) =>

View file

@ -229,6 +229,7 @@ const getSavedObjectTypes = (
version: { type: 'keyword' },
},
},
vars: { type: 'flattened' },
inputs: {
type: 'nested',
enabled: false,

View file

@ -121,7 +121,7 @@ test('getPipelineNameForInstallation gets correct name', () => {
const packageVersion = '1.0.1';
const pipelineRefName = 'pipeline-json';
const pipelineEntryNameForInstallation = getPipelineNameForInstallation({
pipelineName: dataStream.ingest_pipeline,
pipelineName: dataStream.ingest_pipeline!,
dataStream,
packageVersion,
});

View file

@ -18,6 +18,7 @@ import type {
ArchivePackage,
RegistryPackage,
EpmPackageAdditions,
GetCategoriesRequest,
} from '../../../../common/types';
import type { Installation, PackageInfo } from '../../../types';
import { IngestManagerError } from '../../../errors';
@ -35,7 +36,7 @@ function nameAsTitle(name: string) {
return name.charAt(0).toUpperCase() + name.substr(1).toLowerCase();
}
export async function getCategories(options: Registry.CategoriesParams) {
export async function getCategories(options: GetCategoriesRequest['query']) {
return Registry.fetchCategories(options);
}
@ -47,7 +48,7 @@ export async function getPackages(
const { savedObjectsClient, experimental, category } = options;
const registryItems = await Registry.fetchList({ category, experimental }).then((items) => {
return items.map((item) =>
Object.assign({}, item, { title: item.title || nameAsTitle(item.name) })
Object.assign({}, item, { title: item.title || nameAsTitle(item.name) }, { id: item.name })
);
});
// get the installed packages

View file

@ -20,6 +20,7 @@ import type {
RegistryPackage,
RegistrySearchResults,
RegistrySearchResult,
GetCategoriesRequest,
} from '../../../types';
import {
getArchiveFilelist,
@ -45,10 +46,6 @@ export interface SearchParams {
experimental?: boolean;
}
export interface CategoriesParams {
experimental?: boolean;
}
/**
* Extract the package name and package version from a string.
*
@ -150,13 +147,18 @@ function setKibanaVersion(url: URL) {
}
}
export async function fetchCategories(params?: CategoriesParams): Promise<CategorySummaryList> {
export async function fetchCategories(
params?: GetCategoriesRequest['query']
): Promise<CategorySummaryList> {
const registryUrl = getRegistryUrl();
const url = new URL(`${registryUrl}/categories`);
if (params) {
if (params.experimental) {
url.searchParams.set('experimental', params.experimental.toString());
}
if (params.include_policy_templates) {
url.searchParams.set('include_policy_templates', params.include_policy_templates.toString());
}
}
setKibanaVersion(url);

View file

@ -34,6 +34,12 @@ paths:
{{#each paths}}
- {{this}}
{{/each}}
{{#if hosts}}
hosts:
{{#each hosts}}
- {{this}}
{{/each}}
{{/if}}
`),
},
];
@ -118,6 +124,7 @@ describe('Package policy service', () => {
},
],
} as unknown) as PackageInfo,
{},
[
{
type: 'log',
@ -180,6 +187,7 @@ describe('Package policy service', () => {
},
],
} as unknown) as PackageInfo,
{},
[
{
type: 'log',
@ -231,6 +239,7 @@ describe('Package policy service', () => {
},
],
} as unknown) as PackageInfo,
{},
[
{
type: 'log',
@ -276,6 +285,74 @@ describe('Package policy service', () => {
]);
});
it('should work with config variables at the package level', async () => {
const inputs = await packagePolicyService.compilePackagePolicyInputs(
({
data_streams: [
{
dataset: 'package.dataset1',
type: 'logs',
streams: [{ input: 'log', template_path: 'some_template_path.yml' }],
path: 'dataset1',
},
],
policy_templates: [
{
inputs: [{ type: 'log' }],
},
],
} as unknown) as PackageInfo,
{
hosts: {
value: ['localhost'],
},
},
[
{
type: 'log',
enabled: true,
vars: {
paths: {
value: ['/var/log/set.log'],
},
},
streams: [
{
id: 'datastream01',
data_stream: { dataset: 'package.dataset1', type: 'logs' },
enabled: true,
},
],
},
]
);
expect(inputs).toEqual([
{
type: 'log',
enabled: true,
vars: {
paths: {
value: ['/var/log/set.log'],
},
},
streams: [
{
id: 'datastream01',
data_stream: { dataset: 'package.dataset1', type: 'logs' },
enabled: true,
compiled_stream: {
metricset: ['dataset1'],
paths: ['/var/log/set.log'],
type: 'log',
hosts: ['localhost'],
},
},
],
},
]);
});
it('should work with an input with a template and no streams', async () => {
const inputs = await packagePolicyService.compilePackagePolicyInputs(
({
@ -286,6 +363,7 @@ describe('Package policy service', () => {
},
],
} as unknown) as PackageInfo,
{},
[
{
type: 'log',
@ -334,6 +412,7 @@ describe('Package policy service', () => {
},
],
} as unknown) as PackageInfo,
{},
[
{
type: 'log',
@ -380,6 +459,7 @@ describe('Package policy service', () => {
compiled_stream: {
metricset: ['dataset1'],
paths: ['/var/log/set.log'],
hosts: ['localhost'],
type: 'log',
},
},
@ -397,6 +477,7 @@ describe('Package policy service', () => {
},
],
} as unknown) as PackageInfo,
{},
[]
);
@ -412,6 +493,7 @@ describe('Package policy service', () => {
},
],
} as unknown) as PackageInfo,
{},
[]
);

View file

@ -128,7 +128,7 @@ class PackagePolicyService {
}
}
inputs = await this.compilePackagePolicyInputs(pkgInfo, inputs);
inputs = await this.compilePackagePolicyInputs(pkgInfo, packagePolicy.vars || {}, inputs);
}
const isoDate = new Date().toISOString();
@ -356,7 +356,7 @@ class PackagePolicyService {
pkgVersion: packagePolicy.package.version,
});
inputs = await this.compilePackagePolicyInputs(pkgInfo, inputs);
inputs = await this.compilePackagePolicyInputs(pkgInfo, packagePolicy.vars || {}, inputs);
}
await soClient.update<PackagePolicySOAttributes>(
@ -432,7 +432,7 @@ class PackagePolicyService {
): Promise<NewPackagePolicy | undefined> {
const pkgInstall = await getInstallation({ savedObjectsClient: soClient, pkgName });
if (pkgInstall) {
const [pkgInfo, defaultOutputId] = await Promise.all([
const [packageInfo, defaultOutputId] = await Promise.all([
getPackageInfo({
savedObjectsClient: soClient,
pkgName: pkgInstall.name,
@ -440,23 +440,24 @@ class PackagePolicyService {
}),
outputService.getDefaultOutputId(soClient),
]);
if (pkgInfo) {
if (packageInfo) {
if (!defaultOutputId) {
throw new Error('Default output is not set');
}
return packageToPackagePolicy(pkgInfo, '', defaultOutputId);
return packageToPackagePolicy(packageInfo, '', defaultOutputId);
}
}
}
public async compilePackagePolicyInputs(
pkgInfo: PackageInfo,
vars: PackagePolicy['vars'],
inputs: PackagePolicyInput[]
): Promise<PackagePolicyInput[]> {
const registryPkgInfo = await Registry.fetchInfo(pkgInfo.name, pkgInfo.version);
const inputsPromises = inputs.map(async (input) => {
const compiledInput = await _compilePackagePolicyInput(registryPkgInfo, pkgInfo, input);
const compiledStreams = await _compilePackageStreams(registryPkgInfo, pkgInfo, input);
const compiledInput = await _compilePackagePolicyInput(registryPkgInfo, pkgInfo, vars, input);
const compiledStreams = await _compilePackageStreams(registryPkgInfo, pkgInfo, vars, input);
return {
...input,
compiled_input: compiledInput,
@ -506,6 +507,7 @@ function assignStreamIdToInput(packagePolicyId: string, input: NewPackagePolicyI
async function _compilePackagePolicyInput(
registryPkgInfo: RegistryPackage,
pkgInfo: PackageInfo,
vars: PackagePolicy['vars'],
input: PackagePolicyInput
) {
if ((!input.enabled || !pkgInfo.policy_templates?.[0]?.inputs?.length) ?? 0 > 0) {
@ -531,8 +533,8 @@ async function _compilePackagePolicyInput(
}
return compileTemplate(
// Populate template variables from input vars
Object.assign({}, input.vars),
// Populate template variables from package- and input-level vars
Object.assign({}, vars, input.vars),
pkgInputTemplate.buffer.toString()
);
}
@ -540,10 +542,11 @@ async function _compilePackagePolicyInput(
async function _compilePackageStreams(
registryPkgInfo: RegistryPackage,
pkgInfo: PackageInfo,
vars: PackagePolicy['vars'],
input: PackagePolicyInput
) {
const streamsPromises = input.streams.map((stream) =>
_compilePackageStream(registryPkgInfo, pkgInfo, input, stream)
_compilePackageStream(registryPkgInfo, pkgInfo, vars, input, stream)
);
return await Promise.all(streamsPromises);
@ -552,6 +555,7 @@ async function _compilePackageStreams(
async function _compilePackageStream(
registryPkgInfo: RegistryPackage,
pkgInfo: PackageInfo,
vars: PackagePolicy['vars'],
input: PackagePolicyInput,
stream: PackagePolicyInputStream
) {
@ -600,8 +604,8 @@ async function _compilePackageStream(
}
const yaml = compileTemplate(
// Populate template variables from input vars and stream vars
Object.assign({}, input.vars, stream.vars),
// Populate template variables from package-, input-, and stream-level vars
Object.assign({}, vars, input.vars, stream.vars),
pkgStreamTemplate.buffer.toString()
);

View file

@ -71,6 +71,7 @@ export {
InstallType,
InstallSource,
InstallResult,
GetCategoriesRequest,
DataType,
dataTypes,
// Fleet Server types

View file

@ -77,6 +77,7 @@ const PackagePolicyBaseSchema = {
),
})
),
vars: schema.maybe(ConfigRecordSchema),
};
export const NewPackagePolicySchema = schema.object({

View file

@ -10,6 +10,7 @@ import { schema } from '@kbn/config-schema';
export const GetCategoriesRequestSchema = {
query: schema.object({
experimental: schema.maybe(schema.boolean()),
include_policy_templates: schema.maybe(schema.boolean()),
}),
};

View file

@ -1302,6 +1302,7 @@ export class EndpointDocGenerator extends BaseDataGenerator {
*/
public generateEpmPackage(): GetPackagesResponse['response'][0] {
return {
id: this.seededUUIDv4(),
name: 'endpoint',
title: 'Elastic Endpoint',
version: '0.5.0',