[APM] Fleet: APM Integration settings (fleet editor) redesign (#106535)

* initial commig

* adding forms

* moving settings_form

* moving settings_form

* renaming

* fixing import

* adding error message per settings

* fixing onchange bug

* adding isValid to onchange func

* fixing TS issue

* fixing default value

* validating form

* refactoring to validate form

* renaming to vars

* refactoring

* adding apm integration settings

* fixing some stff

* refactoring name

* adding translations

* refactoring

* fixing test

* adding unit test

* flats settings

* refactoring cloud policy constant

* removing fleet exported function

* fixing TS issue

* fixing bug

* fixing ts

* addressing PR comments

* addressing PR comments

* addressing PR comments
This commit is contained in:
Cauê Marcondes 2021-07-30 16:18:48 -04:00 committed by GitHub
parent b9acd3ce74
commit f50843dc52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1358 additions and 9 deletions

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const POLICY_ELASTIC_AGENT_ON_CLOUD = 'policy-elastic-agent-on-cloud';

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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { APMPolicyForm } from '.';
import {
PackagePolicyVars,
NewPackagePolicy,
PackagePolicyCreateExtensionComponentProps,
} from './typings';
interface Props {
newPolicy: NewPackagePolicy;
onChange: PackagePolicyCreateExtensionComponentProps['onChange'];
}
export function CreateAPMPolicyForm({ newPolicy, onChange }: Props) {
const [firstInput, ...restInputs] = newPolicy?.inputs;
const vars = firstInput?.vars;
function handleChange(newVars: PackagePolicyVars, isValid: boolean) {
onChange({
isValid,
updatedPolicy: {
...newPolicy,
inputs: [{ ...firstInput, vars: newVars }, ...restInputs],
},
});
}
return (
<APMPolicyForm vars={vars} onChange={handleChange} isCloudPolicy={false} />
);
}

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { APMPolicyForm } from '.';
import {
NewPackagePolicy,
PackagePolicy,
PackagePolicyEditExtensionComponentProps,
PackagePolicyVars,
} from './typings';
import { POLICY_ELASTIC_AGENT_ON_CLOUD } from '../../../../common/fleet';
interface Props {
policy: PackagePolicy;
newPolicy: NewPackagePolicy;
onChange: PackagePolicyEditExtensionComponentProps['onChange'];
}
export function EditAPMPolicyForm({ newPolicy, onChange }: Props) {
const [firstInput, ...restInputs] = newPolicy?.inputs;
const vars = firstInput?.vars;
function handleChange(newVars: PackagePolicyVars, isValid: boolean) {
onChange({
isValid,
updatedPolicy: {
inputs: [{ ...firstInput, vars: newVars }, ...restInputs],
},
});
}
return (
<APMPolicyForm
vars={vars}
onChange={handleChange}
isCloudPolicy={newPolicy.policy_id === POLICY_ELASTIC_AGENT_ON_CLOUD}
/>
);
}

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiSpacer } from '@elastic/eui';
import React from 'react';
import { OnFormChangeFn, PackagePolicyVars } from './typings';
import { APMSettingsForm } from './settings/apm_settings';
import { RUMSettingsForm } from './settings/rum_settings';
import { TLSSettingsForm } from './settings/tls_settings';
interface Props {
onChange: OnFormChangeFn;
vars?: PackagePolicyVars;
isCloudPolicy: boolean;
}
export function APMPolicyForm({ vars = {}, isCloudPolicy, onChange }: Props) {
return (
<>
<APMSettingsForm
vars={vars}
onChange={onChange}
isCloudPolicy={isCloudPolicy}
/>
<EuiSpacer />
<RUMSettingsForm vars={vars} onChange={onChange} />
<EuiSpacer />
<TLSSettingsForm vars={vars} onChange={onChange} />
</>
);
}

View file

@ -0,0 +1,306 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import { getDurationRt } from '../../../../../common/agent_configuration/runtime_types/duration_rt';
import { getIntegerRt } from '../../../../../common/agent_configuration/runtime_types/integer_rt';
import { OnFormChangeFn, PackagePolicyVars } from '../typings';
import { SettingsForm } from './settings_form';
import { SettingDefinition } from './typings';
import {
isSettingsFormValid,
mergeNewVars,
OPTIONAL_LABEL,
REQUIRED_LABEL,
} from './utils';
interface Props {
vars: PackagePolicyVars;
onChange: OnFormChangeFn;
isCloudPolicy: boolean;
}
function getApmSettings(isCloudPolicy: boolean): SettingDefinition[] {
return [
{
type: 'text',
key: 'host',
labelAppend: REQUIRED_LABEL,
readOnly: isCloudPolicy,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.hostLabel',
{ defaultMessage: 'Host' }
),
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.hostTitle',
{ defaultMessage: 'Server configuration' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.hostDescription',
{
defaultMessage:
'Choose a name and description to help identify how this integration will be used.',
}
),
required: true,
},
{
type: 'text',
key: 'url',
labelAppend: REQUIRED_LABEL,
readOnly: isCloudPolicy,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.urlLabel',
{
defaultMessage: 'URL',
}
),
required: true,
},
{
type: 'text',
key: 'secret_token',
readOnly: isCloudPolicy,
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.secretTokenLabel',
{ defaultMessage: 'Secret token' }
),
},
{
type: 'boolean',
key: 'api_key_enabled',
labelAppend: OPTIONAL_LABEL,
placeholder: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.apiKeyAuthenticationPlaceholder',
{ defaultMessage: 'API key for agent authentication' }
),
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.apiKeyAuthenticationHelpText',
{
defaultMessage:
'Enable API Key auth between APM Server and APM Agents.',
}
),
},
{
type: 'advanced_settings',
settings: [
{
key: 'max_header_bytes',
type: 'integer',
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.maxHeaderBytesLabel',
{ defaultMessage: "Maximum size of a request's header (bytes)" }
),
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.maxHeaderBytesTitle',
{ defaultMessage: 'Limits' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.maxHeaderBytesDescription',
{
defaultMessage:
'Set limits on request headers sizes and timing configurations.',
}
),
validation: getIntegerRt({ min: 1 }),
},
{
key: 'idle_timeout',
type: 'text',
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.idleTimeoutLabel',
{
defaultMessage:
'Idle time before underlying connection is closed',
}
),
validation: getDurationRt({ min: '1ms' }),
},
{
key: 'read_timeout',
type: 'text',
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.readTimeoutLabel',
{ defaultMessage: 'Maximum duration for reading an entire request' }
),
validation: getDurationRt({ min: '1ms' }),
},
{
key: 'shutdown_timeout',
type: 'text',
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.shutdownTimeoutLabel',
{
defaultMessage:
'Maximum duration before releasing resources when shutting down',
}
),
validation: getDurationRt({ min: '1ms' }),
},
{
key: 'write_timeout',
type: 'text',
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.writeTimeoutLabel',
{ defaultMessage: 'Maximum duration for writing a response' }
),
validation: getDurationRt({ min: '1ms' }),
},
{
key: 'max_event_bytes',
type: 'integer',
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.maxEventBytesLabel',
{ defaultMessage: 'Maximum size per event (bytes)' }
),
validation: getIntegerRt({ min: 1 }),
},
{
key: 'max_connections',
type: 'integer',
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.maxConnectionsLabel',
{ defaultMessage: 'Simultaneously accepted connections' }
),
validation: getIntegerRt({ min: 1 }),
},
{
key: 'response_headers',
type: 'area',
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.responseHeadersLabel',
{ defaultMessage: 'Custom HTTP headers added to HTTP responses' }
),
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.responseHeadersHelpText',
{ defaultMessage: 'Might be used for security policy compliance.' }
),
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.responseHeadersTitle',
{ defaultMessage: 'Custom headers' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.responseHeadersDescription',
{
defaultMessage:
'Set limits on request headers sizes and timing configurations.',
}
),
},
{
key: 'api_key_limit',
type: 'integer',
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.apiKeyLimitLabel',
{ defaultMessage: 'Number of keys' }
),
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.apiKeyLimitHelpText',
{ defaultMessage: 'Might be used for security policy compliance.' }
),
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.apiKeyLimitTitle',
{
defaultMessage:
'Maximum number of API keys of Agent authentication',
}
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.apiKeyLimitDescription',
{
defaultMessage:
'Restrict number of unique API keys per minute, used for auth between aPM Agents and Server.',
}
),
validation: getIntegerRt({ min: 1 }),
},
{
key: 'capture_personal_data',
type: 'boolean',
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.capturePersonalDataTitle',
{ defaultMessage: 'Capture personal data' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.capturePersonalDataDescription',
{ defaultMessage: 'Capture personal data such as IP or User Agent' }
),
},
{
key: 'default_service_environment',
type: 'text',
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.defaultServiceEnvironmentLabel',
{ defaultMessage: 'Default Service Environment' }
),
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.defaultServiceEnvironmentTitle',
{ defaultMessage: 'Service configuration' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.defaultServiceEnvironmentDescription',
{
defaultMessage:
'Default service environment to record in events which have no service environment defined.',
}
),
},
{
key: 'expvar_enabled',
type: 'boolean',
labelAppend: OPTIONAL_LABEL,
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.expvarEnabledTitle',
{ defaultMessage: 'Enable APM Server Golang expvar support' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.expvarEnabledDescription',
{ defaultMessage: 'Exposed under /debug/vars' }
),
},
],
},
];
}
export function APMSettingsForm({ vars, onChange, isCloudPolicy }: Props) {
const apmSettings = useMemo(() => {
return getApmSettings(isCloudPolicy);
}, [isCloudPolicy]);
return (
<SettingsForm
title={i18n.translate(
'xpack.apm.fleet_integration.settings.apm.settings.title',
{ defaultMessage: 'General' }
)}
subtitle={i18n.translate(
'xpack.apm.fleet_integration.settings.apm.settings.subtitle',
{ defaultMessage: 'Settings for the APM integration.' }
)}
settings={apmSettings}
vars={vars}
onChange={(key, value) => {
const newVars = mergeNewVars(vars, key, value);
onChange(newVars, isSettingsFormValid(apmSettings, newVars));
}}
/>
);
}

View file

@ -0,0 +1,113 @@
/*
* 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 {
EuiFieldNumber,
EuiFieldText,
EuiIcon,
EuiSwitch,
EuiTextArea,
EuiComboBox,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { FormRowOnChange } from './settings_form';
import { SettingDefinition } from './typings';
interface Props {
setting: SettingDefinition;
value?: any;
onChange: FormRowOnChange;
}
const ENABLED_LABEL = i18n.translate(
'xpack.apm.fleet_integration.settings.enabledLabel',
{ defaultMessage: 'Enabled' }
);
const DISABLED_LABEL = i18n.translate(
'xpack.apm.fleet_integration.settings.disabledLabel',
{ defaultMessage: 'Disabled' }
);
export function FormRowSetting({ setting, value, onChange }: Props) {
switch (setting.type) {
case 'boolean': {
return (
<EuiSwitch
label={
setting.placeholder || (value ? ENABLED_LABEL : DISABLED_LABEL)
}
checked={value}
onChange={(e) => {
onChange(setting.key, e.target.checked);
}}
/>
);
}
case 'duration':
case 'text': {
return (
<EuiFieldText
readOnly={setting.readOnly}
value={value}
prepend={setting.readOnly ? <EuiIcon type="lock" /> : undefined}
onChange={(e) => {
onChange(setting.key, e.target.value);
}}
/>
);
}
case 'area': {
return (
<EuiTextArea
value={value}
onChange={(e) => {
onChange(setting.key, e.target.value);
}}
/>
);
}
case 'bytes':
case 'integer': {
return (
<EuiFieldNumber
value={value}
onChange={(e) => {
onChange(setting.key, e.target.value);
}}
/>
);
}
case 'combo': {
const comboOptions = Array.isArray(value)
? value.map((label) => ({ label }))
: [];
return (
<EuiComboBox
placeholder={i18n.translate(
'xpack.apm.fleet_integration.settings.selectOrCreateOptions',
{ defaultMessage: 'Select or create options' }
)}
options={comboOptions}
selectedOptions={comboOptions}
onChange={(option) => {
onChange(
setting.key,
option.map(({ label }) => label)
);
}}
onCreateOption={(newOption) => {
onChange(setting.key, [...value, newOption]);
}}
isClearable={true}
/>
);
}
default:
throw new Error(`Unknown type "${setting.type}"`);
}
}

View file

@ -0,0 +1,197 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { getIntegerRt } from '../../../../../common/agent_configuration/runtime_types/integer_rt';
import { OnFormChangeFn, PackagePolicyVars } from '../typings';
import { SettingsForm } from './settings_form';
import { SettingDefinition } from './typings';
import { isSettingsFormValid, mergeNewVars, OPTIONAL_LABEL } from './utils';
const ENABLE_RUM_KEY = 'enable_rum';
const rumSettings: SettingDefinition[] = [
{
key: ENABLE_RUM_KEY,
type: 'boolean',
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.enableRumTitle',
{ defaultMessage: 'Enable RUM' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.enableRumDescription',
{ defaultMessage: 'Enable Real User Monitoring (RUM)' }
),
settings: [
{
key: 'rum_allow_headers',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowHeaderLabel',
{ defaultMessage: 'Allowed origin headers' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowHeaderHelpText',
{
defaultMessage: 'Allowed Origin headers to be sent by User Agents.',
}
),
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowHeaderTitle',
{ defaultMessage: 'Custom headers' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowHeaderDescription',
{ defaultMessage: 'Configure authentication for the agent' }
),
},
{
key: 'rum_allow_origins',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowOriginsLabel',
{ defaultMessage: 'Access-Control-Allow-Headers' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowOriginsHelpText',
{
defaultMessage:
'Supported Access-Control-Allow-Headers in addition to "Content-Type", "Content-Encoding" and "Accept".',
}
),
},
{
key: 'rum_response_headers',
type: 'area',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumResponseHeadersLabel',
{ defaultMessage: 'Custom HTTP response headers' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumResponseHeadersHelpText',
{
defaultMessage:
'Added to RUM responses, e.g. for security policy compliance.',
}
),
},
{
type: 'advanced_settings',
settings: [
{
key: 'rum_event_rate_limit',
type: 'integer',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumEventRateLimitLabel',
{ defaultMessage: 'Rate limit events per IP' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumEventRateLimitHelpText',
{
defaultMessage:
'Maximum number of events allowed per IP per second.',
}
),
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumEventRateLimitTitle',
{ defaultMessage: 'Limits' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumEventRateLimitDescription',
{ defaultMessage: 'Configure authentication for the agent' }
),
validation: getIntegerRt({ min: 1 }),
},
{
key: 'rum_event_rate_lru_size',
type: 'integer',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumEventRateLRUSizeLabel',
{ defaultMessage: 'Rate limit cache size' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumEventRateLRUSizeHelpText',
{
defaultMessage:
'Number of unique IPs to be cached for the rate limiter.',
}
),
validation: getIntegerRt({ min: 1 }),
},
{
key: 'rum_library_pattern',
type: 'text',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumLibraryPatternLabel',
{ defaultMessage: 'Library Frame Pattern' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumLibraryPatternHelpText',
{
defaultMessage:
"Identify library frames by matching a stacktrace frame's file_name and abs_path against this regexp.",
}
),
},
{
key: 'rum_allow_service_names',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowServiceNamesLabel',
{ defaultMessage: 'Allowed service names' }
),
labelAppend: OPTIONAL_LABEL,
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowServiceNamesTitle',
{ defaultMessage: 'Allowed service names' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowServiceNamesDescription',
{ defaultMessage: 'Configure authentication for the agent' }
),
},
],
},
],
},
];
interface Props {
vars: PackagePolicyVars;
onChange: OnFormChangeFn;
}
export function RUMSettingsForm({ vars, onChange }: Props) {
return (
<SettingsForm
title={i18n.translate(
'xpack.apm.fleet_integration.settings.rum.settings.title',
{ defaultMessage: 'Real User Monitoring' }
)}
subtitle={i18n.translate(
'xpack.apm.fleet_integration.settings.rum.settings.subtitle',
{ defaultMessage: 'Manage the configuration of the RUM JS agent.' }
)}
settings={rumSettings}
vars={vars}
onChange={(key, value) => {
const newVars = mergeNewVars(vars, key, value);
onChange(
newVars,
// only validates RUM when its flag is enabled
!newVars[ENABLE_RUM_KEY].value ||
isSettingsFormValid(rumSettings, newVars)
);
}}
/>
);
}

View file

@ -0,0 +1,158 @@
/*
* 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 {
EuiButtonEmpty,
EuiDescribedFormGroup,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiHorizontalRule,
EuiPanel,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { PackagePolicyVars } from '../typings';
import { FormRowSetting } from './form_row_setting';
import { SettingDefinition } from './typings';
import { validateSettingValue } from './utils';
export type FormRowOnChange = (key: string, value: any) => void;
function FormRow({
initialSetting,
vars,
onChange,
}: {
initialSetting: SettingDefinition;
vars?: PackagePolicyVars;
onChange: FormRowOnChange;
}) {
function getSettingFormRow(setting: SettingDefinition) {
if (setting.type === 'advanced_settings') {
return (
<AdvancedOptions>
{setting.settings.map((advancedSetting) =>
getSettingFormRow(advancedSetting)
)}
</AdvancedOptions>
);
} else {
const { key } = setting;
const value = vars?.[key]?.value;
const { isValid, message } = validateSettingValue(setting, value);
return (
<React.Fragment key={key}>
<EuiDescribedFormGroup
title={<h3>{setting.rowTitle}</h3>}
description={setting.rowDescription}
>
<EuiFormRow
label={setting.label}
isInvalid={!isValid}
error={isValid ? undefined : message}
helpText={<EuiText size="xs">{setting.helpText}</EuiText>}
labelAppend={
<EuiText size="xs" color="subdued">
{setting.labelAppend}
</EuiText>
}
>
<FormRowSetting
setting={setting}
onChange={onChange}
value={value}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
{setting.settings &&
value &&
setting.settings.map((childSettings) =>
getSettingFormRow(childSettings)
)}
</React.Fragment>
);
}
}
return getSettingFormRow(initialSetting);
}
interface Props {
title: string;
subtitle: string;
settings: SettingDefinition[];
vars?: PackagePolicyVars;
onChange: FormRowOnChange;
}
export function SettingsForm({
title,
subtitle,
settings,
vars,
onChange,
}: Props) {
return (
<EuiPanel>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiTitle size="s">
<h3>{title}</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s" color="subdued">
{subtitle}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="s" />
{settings.map((setting) => {
return FormRow({
initialSetting: setting,
vars,
onChange,
});
})}
</EuiPanel>
);
}
function AdvancedOptions({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<EuiFlexGroup>
<EuiFlexItem />
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType={isOpen ? 'arrowDown' : 'arrowRight'}
onClick={() => {
setIsOpen((state) => !state);
}}
>
{i18n.translate(
'xpack.apm.fleet_integration.settings.advancedOptionsLavel',
{ defaultMessage: 'Advanced options' }
)}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
{isOpen && (
<>
<EuiHorizontalRule />
{children}
</>
)}
</>
);
}

View file

@ -0,0 +1,117 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { OnFormChangeFn, PackagePolicyVars } from '../typings';
import { SettingsForm } from './settings_form';
import { SettingDefinition } from './typings';
import {
isSettingsFormValid,
mergeNewVars,
OPTIONAL_LABEL,
REQUIRED_LABEL,
} from './utils';
const TLS_ENABLED_KEY = 'tls_enabled';
const tlsSettings: SettingDefinition[] = [
{
key: TLS_ENABLED_KEY,
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsEnabledTitle',
{ defaultMessage: 'Enable TLS' }
),
type: 'boolean',
settings: [
{
key: 'tls_certificate',
type: 'text',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsCertificateLabel',
{ defaultMessage: 'File path to server certificate' }
),
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsCertificateTitle',
{ defaultMessage: 'TLS certificate' }
),
labelAppend: REQUIRED_LABEL,
required: true,
},
{
key: 'tls_key',
type: 'text',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsKeyLabel',
{ defaultMessage: 'File path to server certificate key' }
),
labelAppend: REQUIRED_LABEL,
required: true,
},
{
key: 'tls_supported_protocols',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsSupportedProtocolsLabel',
{ defaultMessage: 'Supported protocol versions' }
),
labelAppend: OPTIONAL_LABEL,
},
{
key: 'tls_cipher_suites',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsCipherSuitesLabel',
{ defaultMessage: 'Cipher suites for TLS connections' }
),
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsCipherSuitesHelpText',
{ defaultMessage: 'Not configurable for TLS 1.3.' }
),
labelAppend: OPTIONAL_LABEL,
},
{
key: 'tls_curve_types',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsCurveTypesLabel',
{ defaultMessage: 'Curve types for ECDHE based cipher suites' }
),
labelAppend: OPTIONAL_LABEL,
},
],
},
];
interface Props {
vars: PackagePolicyVars;
onChange: OnFormChangeFn;
}
export function TLSSettingsForm({ vars, onChange }: Props) {
return (
<SettingsForm
title={i18n.translate(
'xpack.apm.fleet_integration.settings.tls.settings.title',
{ defaultMessage: 'TLS Settings' }
)}
subtitle={i18n.translate(
'xpack.apm.fleet_integration.settings.tls.settings.subtitle',
{ defaultMessage: 'Settings for TLS certification.' }
)}
settings={tlsSettings}
vars={vars}
onChange={(key, value) => {
const newVars = mergeNewVars(vars, key, value);
onChange(
newVars,
// only validates TLS when its flag is enabled
!newVars[TLS_ENABLED_KEY].value ||
isSettingsFormValid(tlsSettings, newVars)
);
}}
/>
);
}

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
export type SettingValidation = t.Type<any, string, unknown>;
interface AdvancedSettings {
type: 'advanced_settings';
settings: SettingDefinition[];
}
export interface Setting {
type:
| 'text'
| 'combo'
| 'area'
| 'boolean'
| 'integer'
| 'bytes'
| 'duration';
key: string;
rowTitle?: string;
rowDescription?: string;
label?: string;
helpText?: string;
placeholder?: string;
labelAppend?: string;
settings?: SettingDefinition[];
validation?: SettingValidation;
required?: boolean;
readOnly?: boolean;
}
export type SettingDefinition = Setting | AdvancedSettings;

View file

@ -0,0 +1,148 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getDurationRt } from '../../../../../common/agent_configuration/runtime_types/duration_rt';
import { PackagePolicyVars } from '../typings';
import { SettingDefinition } from './typings';
import {
mergeNewVars,
isSettingsFormValid,
validateSettingValue,
} from './utils';
describe('settings utils', () => {
describe('validateSettingValue', () => {
it('returns invalid when setting is required and value is empty', () => {
const setting: SettingDefinition = {
key: 'foo',
type: 'text',
required: true,
};
expect(validateSettingValue(setting, undefined)).toEqual({
isValid: false,
message: 'Required field',
});
});
it('returns valid when setting is NOT required and value is empty', () => {
const setting: SettingDefinition = {
key: 'foo',
type: 'text',
};
expect(validateSettingValue(setting, undefined)).toEqual({
isValid: true,
message: '',
});
});
it('returns valid when setting does not have a validation property', () => {
const setting: SettingDefinition = {
key: 'foo',
type: 'text',
};
expect(validateSettingValue(setting, 'foo')).toEqual({
isValid: true,
message: '',
});
});
it('returns valid after validating value', () => {
const setting: SettingDefinition = {
key: 'foo',
type: 'text',
validation: getDurationRt({ min: '1ms' }),
};
expect(validateSettingValue(setting, '2ms')).toEqual({
isValid: true,
message: 'No errors!',
});
});
it('returns invalid after validating value', () => {
const setting: SettingDefinition = {
key: 'foo',
type: 'text',
validation: getDurationRt({ min: '1ms' }),
};
expect(validateSettingValue(setting, 'foo')).toEqual({
isValid: false,
message: 'Must be greater than 1ms',
});
});
});
describe('isSettingsFormValid', () => {
const settings: SettingDefinition[] = [
{ key: 'foo', type: 'text', required: true },
{
key: 'bar',
type: 'text',
settings: [{ type: 'text', key: 'bar_1', required: true }],
},
{ key: 'baz', type: 'text', validation: getDurationRt({ min: '1ms' }) },
{
type: 'advanced_settings',
settings: [
{
type: 'text',
key: 'advanced_1',
required: true,
settings: [
{
type: 'text',
key: 'advanced_1_1',
validation: getDurationRt({ min: '1ms' }),
settings: [
{
type: 'text',
key: 'advanced_1_1_1',
required: true,
validation: getDurationRt({ min: '1ms' }),
},
],
},
],
},
],
},
];
it('returns false when form is invalid', () => {
const vars: PackagePolicyVars = {
foo: { value: undefined, type: 'text' },
bar: { value: undefined, type: 'text' },
baz: { value: 'baz', type: 'text' },
advanced_1: { value: undefined, type: 'text' },
advanced_1_1: { value: '1', type: 'text' },
advanced_1_1_1: { value: undefined, type: 'text' },
};
expect(isSettingsFormValid(settings, vars)).toBeFalsy();
});
it('returns true when form is valid', () => {
const vars: PackagePolicyVars = {
foo: { value: 'foo', type: 'text' },
bar: { value: undefined, type: 'text' },
bar_1: { value: 'bar_1' },
baz: { value: '1ms', type: 'text' },
advanced_1: { value: 'advanced_1', type: 'text' },
advanced_1_1: { value: undefined, type: 'text' },
advanced_1_1_1: { value: '1s', type: 'text' },
};
expect(isSettingsFormValid(settings, vars)).toBeTruthy();
});
});
describe('mergeNewVars', () => {
it('updates key value', () => {
const vars: PackagePolicyVars = {
foo: { value: 'foo', type: 'text' },
bar: { value: undefined, type: 'text' },
baz: { value: '1ms', type: 'text' },
qux: { value: undefined, type: 'text' },
};
const newVars = mergeNewVars(vars, 'qux', 'qux');
expect(newVars).toEqual({
foo: { value: 'foo', type: 'text' },
bar: { value: undefined, type: 'text' },
baz: { value: '1ms', type: 'text' },
qux: { value: 'qux', type: 'text' },
});
});
});
});

View file

@ -0,0 +1,77 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { isRight } from 'fp-ts/lib/Either';
import { PathReporter } from 'io-ts/lib/PathReporter';
import { isEmpty } from 'lodash';
import { PackagePolicyVars } from '../typings';
import { SettingDefinition, Setting } from './typings';
export const REQUIRED_LABEL = i18n.translate(
'xpack.apm.fleet_integration.settings.requiredLabel',
{ defaultMessage: 'Required' }
);
export const OPTIONAL_LABEL = i18n.translate(
'xpack.apm.fleet_integration.settings.optionalLabel',
{ defaultMessage: 'Optional' }
);
const REQUIRED_FIELD = i18n.translate(
'xpack.apm.fleet_integration.settings.requiredFieldLabel',
{ defaultMessage: 'Required field' }
);
export function mergeNewVars(
oldVars: PackagePolicyVars,
key: string,
value?: any
): PackagePolicyVars {
return { ...oldVars, [key]: { ...oldVars[key], value } };
}
export function isSettingsFormValid(
parentSettings: SettingDefinition[],
vars: PackagePolicyVars
) {
function isSettingsValid(settings: SettingDefinition[]): boolean {
return !settings
.map((setting) => {
if (setting.type === 'advanced_settings') {
return isSettingsValid(setting.settings);
}
if (setting.settings) {
return isSettingsValid(setting.settings);
}
const { isValid } = validateSettingValue(
setting,
vars[setting.key]?.value
);
return isValid;
})
.flat()
.some((isValid) => !isValid);
}
return isSettingsValid(parentSettings);
}
export function validateSettingValue(setting: Setting, value?: any) {
if (isEmpty(value)) {
return {
isValid: !setting.required,
message: setting.required ? REQUIRED_FIELD : '',
};
}
if (setting.validation) {
const result = setting.validation.decode(String(value));
const message = PathReporter.report(result)[0];
const isValid = isRight(result);
return { isValid, message };
}
return { isValid: true, message: '' };
}

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { PackagePolicyConfigRecordEntry } from '../../../../../fleet/common';
export {
PackagePolicyCreateExtensionComponentProps,
PackagePolicyEditExtensionComponentProps,
} from '../../../../../fleet/public';
export {
NewPackagePolicy,
PackagePolicy,
PackagePolicyConfigRecordEntry,
} from '../../../../../fleet/common';
export type PackagePolicyVars = Record<string, PackagePolicyConfigRecordEntry>;
export type OnFormChangeFn = (
newVars: PackagePolicyVars,
isValid: boolean
) => void;

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { lazy } from 'react';
import { PackagePolicyCreateExtensionComponent } from '../../../../fleet/public';
export const getLazyAPMPolicyCreateExtension = () => {
return lazy<PackagePolicyCreateExtensionComponent>(async () => {
const { CreateAPMPolicyForm } = await import(
'./apm_policy_form/create_apm_policy_form'
);
return { default: CreateAPMPolicyForm };
});
};

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { lazy } from 'react';
import { PackagePolicyEditExtensionComponent } from '../../../../fleet/public';
export const getLazyAPMPolicyEditExtension = () => {
return lazy<PackagePolicyEditExtensionComponent>(async () => {
const { EditAPMPolicyForm } = await import(
'./apm_policy_form/edit_apm_policy_form'
);
return { default: EditAPMPolicyForm };
});
};

View file

@ -48,6 +48,8 @@ import {
getApmEnrollmentFlyoutData,
LazyApmCustomAssetsExtension,
} from './components/fleet_integration';
import { getLazyAPMPolicyCreateExtension } from './components/fleet_integration/lazy_apm_policy_create_extension';
import { getLazyAPMPolicyEditExtension } from './components/fleet_integration/lazy_apm_policy_edit_extension';
export type ApmPluginSetup = ReturnType<ApmPlugin['setup']>;
@ -332,6 +334,18 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
view: 'package-detail-assets',
Component: LazyApmCustomAssetsExtension,
});
fleet.registerExtension({
package: 'apm',
view: 'package-policy-create',
Component: getLazyAPMPolicyCreateExtension(),
});
fleet.registerExtension({
package: 'apm',
view: 'package-policy-edit',
Component: getLazyAPMPolicyEditExtension(),
});
}
}
}

View file

@ -8,8 +8,9 @@
import { Story } from '@storybook/react';
import { HttpStart } from 'kibana/public';
import React from 'react';
import { POLICY_ELASTIC_AGENT_ON_CLOUD } from '../../../common/fleet';
import TutorialConfigAgent from './';
import { APIReturnType } from '../..//services/rest/createCallApmApi';
import { APIReturnType } from '../../services/rest/createCallApmApi';
export type APIResponseType = APIReturnType<'GET /api/apm/fleet/agents'>;
@ -22,7 +23,7 @@ interface Args {
}
const policyElasticAgentOnCloudAgent: APIResponseType['fleetAgents'][0] = {
id: 'policy-elastic-agent-on-cloud',
id: POLICY_ELASTIC_AGENT_ON_CLOUD,
name: 'Elastic Cloud agent policy',
apmServerUrl: 'apm_cloud_url',
secretToken: 'apm_cloud_token',

View file

@ -5,10 +5,9 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { POLICY_ELASTIC_AGENT_ON_CLOUD } from '../../../common/fleet';
import { APIResponseType } from './';
const POLICY_ELASTIC_AGENT_ON_CLOUD = 'policy-elastic-agent-on-cloud';
const DEFAULT_STANDALONE_CONFIG_LABEL = i18n.translate(
'xpack.apm.tutorial.agent_config.defaultStandaloneConfig',
{ defaultMessage: 'Default Standalone configuration' }

View file

@ -5,11 +5,9 @@
* 2.0.
*/
import { POLICY_ELASTIC_AGENT_ON_CLOUD } from '../../../common/fleet';
import { APMPluginSetupDependencies } from '../../types';
import {
POLICY_ELASTIC_AGENT_ON_CLOUD,
APM_PACKAGE_NAME,
} from './get_cloud_apm_package_policy';
import { APM_PACKAGE_NAME } from './get_cloud_apm_package_policy';
interface GetApmPackagePolicyDefinitionOptions {
apmServerSchema: Record<string, any>;

View file

@ -9,8 +9,8 @@ import { SavedObjectsClientContract } from 'kibana/server';
import { Maybe } from '../../../typings/common';
import { AgentPolicy, PackagePolicy } from '../../../../fleet/common';
import { APMPluginStartDependencies } from '../../types';
import { POLICY_ELASTIC_AGENT_ON_CLOUD } from '../../../common/fleet';
export const POLICY_ELASTIC_AGENT_ON_CLOUD = 'policy-elastic-agent-on-cloud';
export const APM_PACKAGE_NAME = 'apm';
export async function getCloudAgentPolicy({