diff --git a/x-pack/plugins/fleet/common/settings/agent_policy_settings.ts b/x-pack/plugins/fleet/common/settings/agent_policy_settings.ts index f0d7a84d36d2a..ac5c57f9107c5 100644 --- a/x-pack/plugins/fleet/common/settings/agent_policy_settings.ts +++ b/x-pack/plugins/fleet/common/settings/agent_policy_settings.ts @@ -37,4 +37,93 @@ export const AGENT_POLICY_ADVANCED_SETTINGS: SettingsConfig[] = [ }, schema: z.number().int().min(0).default(0), }, + { + name: 'agent.download.timeout', + hidden: true, + title: i18n.translate('xpack.fleet.settings.agentPolicyAdvanced.downloadTimeoutTitle', { + defaultMessage: 'Agent binary download timeout', + }), + description: i18n.translate( + 'xpack.fleet.settings.agentPolicyAdvanced.downloadTimeoutDescription', + { + defaultMessage: 'Timeout for downloading the agent binary', + } + ), + learnMoreLink: + 'https://www.elastic.co/guide/en/fleet/current/enable-custom-policy-settings.html#configure-agent-download-timeout', + api_field: { + name: 'agent_download_timeout', + }, + schema: zodStringWithDurationValidation.default('2h'), + }, + { + name: 'agent.download.target_directory', + hidden: true, + api_field: { + name: 'agent_download_target_directory', + }, + title: i18n.translate( + 'xpack.fleet.settings.agentPolicyAdvanced.agentDownloadTargetDirectoryTitle', + { + defaultMessage: 'Agent binary target directory', + } + ), + description: i18n.translate( + 'xpack.fleet.settings.agentPolicyAdvanced.agentDownloadTargetDirectoryDescription', + { + defaultMessage: 'The disk path to which the agent binary will be downloaded', + } + ), + learnMoreLink: + 'https://www.elastic.co/guide/en/fleet/current/elastic-agent-standalone-download.html', + schema: z.string(), + }, + { + name: 'agent.logging.metrics.period', + hidden: true, + api_field: { + name: 'agent_logging_metrics_period', + }, + title: i18n.translate( + 'xpack.fleet.settings.agentPolicyAdvanced.agentLoggingMetricsPeriodTitle', + { + defaultMessage: 'Agent logging metrics period', + } + ), + description: i18n.translate( + 'xpack.fleet.settings.agentPolicyAdvanced.agentLoggingMetricsPeriodDescription', + { + defaultMessage: 'The frequency of agent metrics logging', + } + ), + learnMoreLink: + 'https://www.elastic.co/guide/en/fleet/current/elastic-agent-standalone-logging-config.html#elastic-agent-standalone-logging-settings', + schema: zodStringWithDurationValidation.default('30s'), + }, + { + name: 'agent.monitoring.http', + hidden: true, + api_field: { + name: 'agent_monitoring_http', + }, + title: i18n.translate('xpack.fleet.settings.agentPolicyAdvanced.agentMonitoringHttpTitle', { + defaultMessage: 'Agent HTTP monitoring', + }), + description: i18n.translate( + 'xpack.fleet.settings.agentPolicyAdvanced.agentMonitoringHttpDescription', + { + defaultMessage: 'Agent HTTP monitoring settings', + } + ), + learnMoreLink: + 'https://www.elastic.co/guide/en/fleet/current/enable-custom-policy-settings.html#override-default-monitoring-port', + schema: z + .object({ + enabled: z.boolean().describe('Enabled').default(false), + host: z.string().describe('Host').default('localhost'), + port: z.number().describe('Port').min(0).max(65353).default(6791), + 'buffer.enabled': z.boolean().describe('Buffer Enabled').default(false), + }) + .default({}), + }, ]; diff --git a/x-pack/plugins/fleet/common/settings/types.ts b/x-pack/plugins/fleet/common/settings/types.ts index 0fc8ad98f17c0..da22e15dc2609 100644 --- a/x-pack/plugins/fleet/common/settings/types.ts +++ b/x-pack/plugins/fleet/common/settings/types.ts @@ -18,4 +18,5 @@ export interface SettingsConfig { api_field: { name: string; }; + hidden?: boolean; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/form_settings/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/form_settings/index.test.tsx index 7d0cb7dbae341..2631a6527987f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/form_settings/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/form_settings/index.test.tsx @@ -101,4 +101,86 @@ describe('ConfiguredSettings', () => { ).not.toBeNull(); expect(mockUpdateAdvancedSettingsHasErrors).toHaveBeenCalledWith(true); }); + + it('should render field group', () => { + const result = render([ + { + name: 'agent.monitoring.http', + api_field: { + name: 'agent_monitoring_http', + }, + title: 'Agent HTTP monitoring', + description: 'Agent HTTP monitoring settings', + learnMoreLink: + 'https://www.elastic.co/guide/en/fleet/current/enable-custom-policy-settings.html#override-default-monitoring-port', + schema: z + .object({ + enabled: z.boolean().describe('Enabled').default(false), + host: z.string().describe('Host').default('localhost'), + port: z.number().describe('Port').min(0).max(65353).default(6791), + 'buffer.enabled': z.boolean().describe('Buffer Enabled').default(false), + }) + .default({}), + }, + ]); + + expect(result.getByText('Agent HTTP monitoring')).not.toBeNull(); + expect(result.getByText('Buffer Enabled')).not.toBeNull(); + const switches = result.getAllByRole('switch'); + expect(switches).toHaveLength(2); + expect(switches[0]).not.toBeChecked(); + expect(switches[1]).not.toBeChecked(); + const port = result.getByTestId('configuredSetting-agent.monitoring.http-port'); + expect(port).toHaveValue(6791); + const host = result.getByTestId('configuredSetting-agent.monitoring.http-host'); + expect(host).toHaveValue('localhost'); + + act(() => { + fireEvent.click(switches[0]); + }); + + expect(mockUpdateAgentPolicy).toHaveBeenCalledWith( + expect.objectContaining({ + advanced_settings: expect.objectContaining({ agent_monitoring_http: { enabled: true } }), + }) + ); + + act(() => { + fireEvent.change(port, { target: { value: '6792' } }); + }); + + expect(mockUpdateAgentPolicy).toHaveBeenCalledWith( + expect.objectContaining({ + advanced_settings: expect.objectContaining({ agent_monitoring_http: { port: 6792 } }), + }) + ); + + act(() => { + fireEvent.change(host, { target: { value: '1.2.3.4' } }); + }); + + expect(mockUpdateAgentPolicy).toHaveBeenCalledWith( + expect.objectContaining({ + advanced_settings: expect.objectContaining({ agent_monitoring_http: { host: '1.2.3.4' } }), + }) + ); + }); + + it('should not render field if hidden', () => { + const result = render([ + { + name: 'agent.limits.go_max_procs', + hidden: true, + title: 'GO_MAX_PROCS', + description: 'Description', + learnMoreLink: '', + api_field: { + name: 'agent_limits_go_max_procs', + }, + schema: z.number().int().min(0).default(0), + }, + ]); + + expect(result.queryByText('GO_MAX_PROCS')).toBeNull(); + }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/form_settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/form_settings/index.tsx index 03d66d23615ba..2836f1a2c8c38 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/form_settings/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/form_settings/index.tsx @@ -5,24 +5,24 @@ * 2.0. */ -import { z, ZodFirstPartyTypeKind } from 'zod'; -import React, { useState } from 'react'; -import { - EuiDescribedFormGroup, - EuiFieldNumber, - EuiFieldText, - EuiFormRow, - EuiLink, -} from '@elastic/eui'; +import { ZodFirstPartyTypeKind } from 'zod'; +import React from 'react'; +import { EuiFieldNumber, EuiFieldText } from '@elastic/eui'; import type { SettingsConfig } from '../../../../../common/settings/types'; -import { useAgentPolicyFormContext } from '../../sections/agent_policy/components/agent_policy_form'; + +import { SettingsFieldGroup } from './settings_field_group'; +import { getInnerType, SettingsFieldWrapper } from './settings_field_wrapper'; export const settingComponentRegistry = new Map< string, (settingsconfig: SettingsConfig) => React.ReactElement >(); +settingComponentRegistry.set(ZodFirstPartyTypeKind.ZodObject, (settingsConfig) => ( + +)); + settingComponentRegistry.set(ZodFirstPartyTypeKind.ZodNumber, (settingsConfig) => { return ( = ({ settingsConfig, typeName, renderItem }) => { - const [error, setError] = useState(''); - const agentPolicyFormContext = useAgentPolicyFormContext(); - - const fieldKey = `configuredSetting-${settingsConfig.name}`; - const defaultValue: number = - settingsConfig.schema instanceof z.ZodDefault - ? settingsConfig.schema._def.defaultValue() - : undefined; - const coercedSchema = settingsConfig.schema as z.ZodString; - - const convertValue = (value: string, type: keyof typeof ZodFirstPartyTypeKind): any => { - if (type === ZodFirstPartyTypeKind.ZodNumber) { - if (value === '') { - return 0; - } - return parseInt(value, 10); - } - return value; - }; - - const handleChange = (e: React.ChangeEvent) => { - const newValue = convertValue(e.target.value, typeName); - const validationResults = coercedSchema.safeParse(newValue); - - if (!validationResults.success) { - setError(validationResults.error.issues[0].message); - agentPolicyFormContext?.updateAdvancedSettingsHasErrors(true); - } else { - setError(''); - agentPolicyFormContext?.updateAdvancedSettingsHasErrors(false); - } - - const newAdvancedSettings = { - ...(agentPolicyFormContext?.agentPolicy.advanced_settings ?? {}), - [settingsConfig.api_field.name]: newValue, - }; - - agentPolicyFormContext?.updateAgentPolicy({ advanced_settings: newAdvancedSettings }); - }; - - const fieldValue = - agentPolicyFormContext?.agentPolicy.advanced_settings?.[settingsConfig.api_field.name] ?? - defaultValue; - - return ( - {settingsConfig.title}} - description={ - <> - {settingsConfig.description}.{' '} - - Learn more. - - - } - > - - {renderItem({ fieldValue, handleChange, isInvalid: !!error, fieldKey, coercedSchema })} - - - ); -}; - export function ConfiguredSettings({ configuredSettings, }: { @@ -137,21 +68,17 @@ export function ConfiguredSettings({ }) { return ( <> - {configuredSettings.map((configuredSetting) => { - const Component = settingComponentRegistry.get( - configuredSetting.schema instanceof z.ZodDefault - ? configuredSetting.schema._def.innerType._def.typeName === 'ZodEffects' - ? configuredSetting.schema._def.innerType._def.schema._def.typeName - : configuredSetting.schema._def.innerType._def.typeName - : configuredSetting.schema._def.typeName - ); + {configuredSettings + .filter((configuredSetting) => !configuredSetting.hidden) + .map((configuredSetting) => { + const Component = settingComponentRegistry.get(getInnerType(configuredSetting.schema)); - if (!Component) { - throw new Error(`Unknown setting type: ${configuredSetting.schema._type}}`); - } + if (!Component) { + throw new Error(`Unknown setting type: ${configuredSetting.schema._type}}`); + } - return ; - })} + return ; + })} ); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/form_settings/settings_field_group.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/form_settings/settings_field_group.tsx new file mode 100644 index 0000000000000..57d873d7a0560 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/components/form_settings/settings_field_group.tsx @@ -0,0 +1,144 @@ +/* + * 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 { z, ZodFirstPartyTypeKind } from 'zod'; +import React, { useState } from 'react'; +import { + EuiFieldNumber, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSwitch, +} from '@elastic/eui'; + +import type { SettingsConfig } from '../../../../../common/settings/types'; +import { useAgentPolicyFormContext } from '../../sections/agent_policy/components/agent_policy_form'; + +import { + convertValue, + getInnerType, + SettingsFieldWrapper, + validateSchema, +} from './settings_field_wrapper'; + +export const SettingsFieldGroup: React.FC<{ settingsConfig: SettingsConfig }> = ({ + settingsConfig, +}) => { + const [errors, setErrors] = useState<{ [key: string]: string }>({}); + const agentPolicyFormContext = useAgentPolicyFormContext(); + const shape = settingsConfig.schema._def.innerType._def.shape(); + + return ( + ( + + {Object.keys(shape).map((key) => { + const field = shape[key]; + const fieldKey = `configuredSetting-${settingsConfig.name}-${key}`; + const defaultValue: number = + field instanceof z.ZodDefault ? field._def.defaultValue() : undefined; + const coercedSchema = field as z.ZodString; + const fieldValue = + agentPolicyFormContext?.agentPolicy.advanced_settings?.[ + settingsConfig.api_field.name + ]?.[key] ?? defaultValue; + const type = getInnerType(field); + + const description = field._def.description ?? key; + + const handleChange = (e: React.ChangeEvent) => { + const newValue = convertValue(e.target.value, type); + updateFieldValue(newValue); + }; + + const updateFieldValue = (newValue: any) => { + const validationError = validateSchema(coercedSchema, newValue); + + if (validationError) { + setErrors({ ...errors, [key]: validationError }); + agentPolicyFormContext?.updateAdvancedSettingsHasErrors(true); + } else { + setErrors({ ...errors, [key]: '' }); + agentPolicyFormContext?.updateAdvancedSettingsHasErrors(false); + } + + const newApiFieldValue = { + ...agentPolicyFormContext?.agentPolicy.advanced_settings?.[ + settingsConfig.api_field.name + ], + [key]: newValue, + }; + + const newAdvancedSettings = { + ...(agentPolicyFormContext?.agentPolicy.advanced_settings ?? {}), + [settingsConfig.api_field.name]: newApiFieldValue, + }; + + agentPolicyFormContext?.updateAgentPolicy({ advanced_settings: newAdvancedSettings }); + }; + + const getFormField = () => { + switch (type) { + case ZodFirstPartyTypeKind.ZodNumber: + return ( + + ); + case ZodFirstPartyTypeKind.ZodString: + return ( + + ); + case ZodFirstPartyTypeKind.ZodBoolean: + return ( + { + updateFieldValue(e.target.checked); + }} + /> + ); + default: + return <>; + } + }; + + return ( + + + {getFormField()} + + + ); + })} + + )} + /> + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/form_settings/settings_field_wrapper.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/form_settings/settings_field_wrapper.tsx new file mode 100644 index 0000000000000..a19c8e67c1080 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/components/form_settings/settings_field_wrapper.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { z, ZodFirstPartyTypeKind } from 'zod'; +import React, { useState } from 'react'; +import { EuiDescribedFormGroup, EuiFormRow, EuiLink } from '@elastic/eui'; + +import type { SettingsConfig } from '../../../../../common/settings/types'; +import { useAgentPolicyFormContext } from '../../sections/agent_policy/components/agent_policy_form'; + +export const convertValue = (value: string, type: keyof typeof ZodFirstPartyTypeKind): any => { + if (type === ZodFirstPartyTypeKind.ZodNumber) { + if (value === '') { + return 0; + } + return parseInt(value, 10); + } + return value; +}; + +export const validateSchema = (coercedSchema: z.ZodString, newValue: any): string | undefined => { + const validationResults = coercedSchema.safeParse(newValue); + + if (!validationResults.success) { + return validationResults.error.issues[0].message; + } +}; + +export const SettingsFieldWrapper: React.FC<{ + settingsConfig: SettingsConfig; + typeName: keyof typeof ZodFirstPartyTypeKind; + renderItem: Function; +}> = ({ settingsConfig, typeName, renderItem }) => { + const [error, setError] = useState(''); + const agentPolicyFormContext = useAgentPolicyFormContext(); + + const fieldKey = `configuredSetting-${settingsConfig.name}`; + const defaultValue: number = + settingsConfig.schema instanceof z.ZodDefault + ? settingsConfig.schema._def.defaultValue() + : undefined; + const coercedSchema = settingsConfig.schema as z.ZodString; + + const handleChange = (e: React.ChangeEvent) => { + const newValue = convertValue(e.target.value, typeName); + const validationError = validateSchema(coercedSchema, newValue); + + if (validationError) { + setError(validationError); + agentPolicyFormContext?.updateAdvancedSettingsHasErrors(true); + } else { + setError(''); + agentPolicyFormContext?.updateAdvancedSettingsHasErrors(false); + } + + const newAdvancedSettings = { + ...(agentPolicyFormContext?.agentPolicy.advanced_settings ?? {}), + [settingsConfig.api_field.name]: newValue, + }; + + agentPolicyFormContext?.updateAgentPolicy({ advanced_settings: newAdvancedSettings }); + }; + + const fieldValue = + agentPolicyFormContext?.agentPolicy.advanced_settings?.[settingsConfig.api_field.name] ?? + defaultValue; + + return ( + {settingsConfig.title}} + description={ + <> + {settingsConfig.description}.{' '} + + Learn more. + + + } + > + + {renderItem({ fieldValue, handleChange, isInvalid: !!error, fieldKey, coercedSchema })} + + + ); +}; + +export const getInnerType = (schema: z.ZodType) => { + return schema instanceof z.ZodDefault + ? schema._def.innerType._def.typeName === 'ZodEffects' + ? schema._def.innerType._def.schema._def.typeName + : schema._def.innerType._def.typeName + : schema._def.typeName; +};