[Fleet] Move log level setting to settings tab (#217112)

## Summary

Closes #197707 

Moves the log level setting on an agent from the `Logs` tab, to a newly
created `Settings` tab

Before: 

![image](https://github.com/user-attachments/assets/1532f698-73e6-49dd-9dbf-037d75483e2b)

After: 

![image](https://github.com/user-attachments/assets/1239a451-dc5d-465c-a090-22bc220c8b17)


### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Identify risks

N/A

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Mason Herron 2025-04-08 07:12:49 -06:00 committed by GitHub
parent 94f210dc4c
commit f8f6e6ed0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 224 additions and 86 deletions

View file

@ -862,6 +862,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
proxiesSettings: `${FLEET_DOCS}fleet-agent-proxy-support.html`,
unprivilegedMode: `${FLEET_DOCS}elastic-agent-unprivileged.html#unprivileged-change-mode`,
httpMonitoring: `${FLEET_DOCS}agent-policy.html#change-policy-enable-agent-monitoring`,
agentLevelLogging: `${FLEET_DOCS}monitor-elastic-agent.html#change-logging-level`,
},
integrationDeveloper: {
upload: `${INTEGRATIONS_DEV_DOCS}upload-a-new-integration.html`,

View file

@ -527,6 +527,7 @@ export interface DocLinks {
proxiesSettings: string;
unprivilegedMode: string;
httpMonitoring: string;
agentLevelLogging: string;
}>;
readonly integrationDeveloper: {
upload: string;

View file

@ -18729,16 +18729,10 @@
"xpack.fleet.agentLogs.logLevelSelectText": "Niveau du log",
"xpack.fleet.agentLogs.oldAgentWarningTitle": "La vue Logs requiert Elastic Agent 7.11 ou une version ultérieure. Pour mettre à niveau un agent, accédez au menu Actions ou {downloadLink} une version plus récente.",
"xpack.fleet.agentLogs.openInDiscoverUiLinkText": "Ouvrir dans Discover",
"xpack.fleet.agentLogs.resetLogLevel.errorTitleText": "Erreur lors de la réinitialisation du niveau de logging de l'agent",
"xpack.fleet.agentLogs.resetLogLevel.successText": "Réinitialiser le niveau de logging de l'agent pour la politique",
"xpack.fleet.agentLogs.resetLogLevelLabelText": "Réinitialiser pour rétablir la politique",
"xpack.fleet.agentLogs.searchPlaceholderText": "Rechercher dans les logs…",
"xpack.fleet.agentLogs.selectLogLevel.errorTitleText": "Erreur lors de la mise à jour du niveau de logging de l'agent",
"xpack.fleet.agentLogs.selectLogLevel.successText": "Modification du niveau de logging de l'agent en \"{logLevel}\"",
"xpack.fleet.agentLogs.selectLogLevelLabelText": "Niveau de logging de l'agent",
"xpack.fleet.agentLogs.settingsLink": "paramètres",
"xpack.fleet.agentLogs.updateButtonLoadingText": "Application des modifications en cours…",
"xpack.fleet.agentLogs.updateButtonText": "Appliquer les modifications",
"xpack.fleet.agentPolicies.spaceSelectorInvalid": "{space} n'est pas un espace valide.",
"xpack.fleet.agentPolicies.spaceSelectorLabel": "Espaces",
"xpack.fleet.agentPolicy.confirmModalCalloutDescription": "Fleet a détecté que certains de vos agents utilisaient déjà les politiques d'agent sélectionnées, {policyNames}. Suite à cette action, Fleet déploie les mises à jour de tous les agents qui utilisent ces politiques.",

View file

@ -18706,16 +18706,10 @@
"xpack.fleet.agentLogs.logLevelSelectText": "ログレベル",
"xpack.fleet.agentLogs.oldAgentWarningTitle": "ログの表示には、Elastic Agent 7.11以降が必要です。エージェントをアップグレードするには、[アクション]メニューに移動するか、新しいバージョンを{downloadLink}。",
"xpack.fleet.agentLogs.openInDiscoverUiLinkText": "Discoverで開く",
"xpack.fleet.agentLogs.resetLogLevel.errorTitleText": "エージェントログレベルのリセットエラー",
"xpack.fleet.agentLogs.resetLogLevel.successText": "エージェントログレベルをポリシーにリセット",
"xpack.fleet.agentLogs.resetLogLevelLabelText": "ポリシーにリセット",
"xpack.fleet.agentLogs.searchPlaceholderText": "ログを検索…",
"xpack.fleet.agentLogs.selectLogLevel.errorTitleText": "エージェントログレベルの更新エラー",
"xpack.fleet.agentLogs.selectLogLevel.successText": "エージェントログレベルを''{logLevel}''に変更しました",
"xpack.fleet.agentLogs.selectLogLevelLabelText": "エージェントログレベル",
"xpack.fleet.agentLogs.settingsLink": "設定",
"xpack.fleet.agentLogs.updateButtonLoadingText": "変更を適用しています...",
"xpack.fleet.agentLogs.updateButtonText": "変更を適用",
"xpack.fleet.agentPolicies.spaceSelectorInvalid": "{space}は有効なスペースではありません。",
"xpack.fleet.agentPolicies.spaceSelectorLabel": "スペース",
"xpack.fleet.agentPolicy.confirmModalCalloutDescription": "選択されたエージェントポリシー\"{policyNames}\"が一部のエージェントですでに使用されていることをFleetが検出しました。このアクションの結果として、Fleetはこれらのポリシーで使用されているすべてのエージェントに更新をデプロイします。",

View file

@ -18747,16 +18747,10 @@
"xpack.fleet.agentLogs.logLevelSelectText": "日志级别",
"xpack.fleet.agentLogs.oldAgentWarningTitle": "“日志”视图需要 Elastic Agent 7.11 或更高版本。要升级代理,请前往“操作”菜单或{downloadLink}更新的版本。",
"xpack.fleet.agentLogs.openInDiscoverUiLinkText": "在 Discover 中打开",
"xpack.fleet.agentLogs.resetLogLevel.errorTitleText": "重置代理日志记录级别时出错",
"xpack.fleet.agentLogs.resetLogLevel.successText": "将代理日志记录级别重置为策略",
"xpack.fleet.agentLogs.resetLogLevelLabelText": "重置为策略",
"xpack.fleet.agentLogs.searchPlaceholderText": "搜索日志……",
"xpack.fleet.agentLogs.selectLogLevel.errorTitleText": "更新代理日志记录级别时出错",
"xpack.fleet.agentLogs.selectLogLevel.successText": "已将代理日志记录级别更改为“{logLevel}”",
"xpack.fleet.agentLogs.selectLogLevelLabelText": "代理日志记录级别",
"xpack.fleet.agentLogs.settingsLink": "设置",
"xpack.fleet.agentLogs.updateButtonLoadingText": "正在应用更改......",
"xpack.fleet.agentLogs.updateButtonText": "应用更改",
"xpack.fleet.agentPolicies.spaceSelectorInvalid": "{space} 不是有效工作区。",
"xpack.fleet.agentPolicies.spaceSelectorLabel": "工作区",
"xpack.fleet.agentPolicy.confirmModalCalloutDescription": "Fleet 检测到您的部分代理已在使用选定代理策略 {policyNames}。由于此操作Fleet 会将更新部署到使用这些策略的所有代理。",

View file

@ -147,35 +147,4 @@ describe('AgentLogsUI', () => {
const result = renderComponent();
expect(result.queryByTestId('viewInLogsBtn')).not.toBeInTheDocument();
});
it('should show log level dropdown with correct value', () => {
mockStartServices();
const result = renderComponent();
const logLevelDropdown = result.getByTestId('selectAgentLogLevel');
expect(logLevelDropdown.getElementsByTagName('option').length).toBe(4);
expect(logLevelDropdown).toHaveDisplayValue('debug');
});
it('should always show apply log level changes button', () => {
mockStartServices();
const result = renderComponent();
const applyLogLevelBtn = result.getByTestId('applyLogLevelBtn');
expect(applyLogLevelBtn).toBeInTheDocument();
expect(applyLogLevelBtn).not.toHaveAttribute('disabled');
});
it('should hide reset log level button for agents version < 8.15.0', () => {
mockStartServices();
const result = renderComponent();
const resetLogLevelBtn = result.queryByTestId('resetLogLevelBtn');
expect(resetLogLevelBtn).not.toBeInTheDocument();
});
it('should show reset log level button for agents version >= 8.15.0', () => {
mockStartServices();
const result = renderComponent({ agentVersion: '8.15.0' });
const resetLogLevelBtn = result.getByTestId('resetLogLevelBtn');
expect(resetLogLevelBtn).toBeInTheDocument();
expect(resetLogLevelBtn).not.toHaveAttribute('disabled');
});
});

View file

@ -34,7 +34,6 @@ import { DatasetFilter } from './filter_dataset';
import { LogLevelFilter } from './filter_log_level';
import { LogQueryBar } from './query_bar';
import { buildQuery } from './build_query';
import { SelectLogLevel } from './select_log_level';
import { ViewLogsButton, getFormattedRange } from './view_logs_button';
const WrapperFlexGroup = styled(EuiFlexGroup)`
@ -348,12 +347,6 @@ export const AgentLogsUI: React.FunctionComponent<AgentLogsProps> = memo(
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SelectLogLevel
agent={agent}
agentPolicyLogLevel={agentPolicy?.advanced_settings?.agent_logging_level}
/>
</EuiFlexItem>
</WrapperFlexGroup>
);
}

View file

@ -5,10 +5,20 @@
* 2.0.
*/
import React, { memo, useState, useCallback } from 'react';
import React, { memo, useState, useCallback, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSelect, EuiFormLabel, EuiButtonEmpty, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import {
EuiSelect,
EuiButtonEmpty,
EuiFlexItem,
EuiFlexGroup,
EuiTitle,
EuiText,
EuiCode,
EuiSpacer,
EuiLink,
} from '@elastic/eui';
import semverGte from 'semver/functions/gte';
import type { Agent } from '../../../../../types';
@ -19,7 +29,7 @@ import { AGENT_LOG_LEVELS, DEFAULT_LOG_LEVEL } from '../../../../../../../../com
export const SelectLogLevel: React.FC<{ agent: Agent; agentPolicyLogLevel?: string }> = memo(
({ agent, agentPolicyLogLevel = DEFAULT_LOG_LEVEL }) => {
const authz = useAuthz();
const { notifications } = useStartServices();
const { notifications, docLinks } = useStartServices();
const [isSetLevelLoading, setIsSetLevelLoading] = useState(false);
const [isResetLevelLoading, setIsResetLevelLoading] = useState(false);
const canResetLogLevel = semverGte(
@ -108,21 +118,52 @@ export const SelectLogLevel: React.FC<{ agent: Agent; agentPolicyLogLevel?: stri
send();
}, [notifications, selectedLogLevel, agent.id]);
useEffect(() => {
if (selectedLogLevel !== agentLogLevel) {
onClickApply();
}
}, [selectedLogLevel, agentLogLevel, onClickApply]);
return (
<>
<EuiFlexGroup gutterSize="m" alignItems="center">
<EuiTitle size="s">
<h2>
<FormattedMessage
id="xpack.fleet.agentLogs.selectLogLevelLabelText"
defaultMessage="Agent logging level"
/>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiFormLabel htmlFor="selectAgentLogLevel">
<EuiText>
<FormattedMessage
id="xpack.fleet.agentLogs.selectLogLevelLabelText"
defaultMessage="Agent logging level"
defaultMessage="Sets the log level for the agent. The default log level is {infoText}. {guideLink}"
values={{
infoText: <EuiCode>info</EuiCode>,
guideLink: (
<EuiLink
target="_blank"
external={true}
href={docLinks.links.fleet.agentLevelLogging}
>
<FormattedMessage
id="xpack.fleet.agentLogs.levelGuideLinkText"
defaultMessage="Learn More"
/>
</EuiLink>
),
}}
/>
</EuiFormLabel>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSelect
disabled={isSetLevelLoading || !authz.fleet.allAgents}
compressed={true}
fullWidth={true}
id="selectAgentLogLevel"
data-test-subj="selectAgentLogLevel"
value={selectedLogLevel}
@ -135,29 +176,6 @@ export const SelectLogLevel: React.FC<{ agent: Agent; agentPolicyLogLevel?: stri
}))}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
flush="both"
size="xs"
disabled={!authz.fleet.allAgents}
isLoading={isSetLevelLoading || isResetLevelLoading}
iconType="check"
onClick={onClickApply}
data-test-subj="applyLogLevelBtn"
>
{isSetLevelLoading ? (
<FormattedMessage
id="xpack.fleet.agentLogs.updateButtonLoadingText"
defaultMessage="Applying changes..."
/>
) : (
<FormattedMessage
id="xpack.fleet.agentLogs.updateButtonText"
defaultMessage="Apply changes"
/>
)}
</EuiButtonEmpty>
</EuiFlexItem>
{canResetLogLevel && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty

View file

@ -0,0 +1,122 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { useAuthz, useStartServices } from '../../../../../hooks';
import { createFleetTestRendererMock } from '../../../../../../../mock';
import { AgentSettings } from '.';
jest.mock('../../../../../hooks', () => {
return {
...jest.requireActual('../../../../../hooks'),
useLink: jest.fn(),
useStartServices: jest.fn(),
useAuthz: jest.fn(),
useDiscoverLocator: jest.fn().mockImplementation(() => {
return {
id: 'DISCOVER_APP_LOCATOR',
getRedirectUrl: jest.fn().mockResolvedValue('app/discover/logs/someview'),
};
}),
};
});
const mockUseStartServices = useStartServices as jest.Mock;
describe('AgentSettings', () => {
beforeEach(() => {
jest.mocked(useAuthz).mockReturnValue({
fleet: {
allAgents: true,
readAgents: true,
},
} as any);
});
const renderComponent = (
opts = {
agentVersion: '8.11.0',
}
) => {
const renderer = createFleetTestRendererMock();
const agent = {
id: 'agent1',
local_metadata: { elastic: { agent: { version: opts.agentVersion, log_level: 'debug' } } },
} as any;
const agentPolicy = {
id: 'policy1',
name: 'policy1',
revision: 1,
namespace: 'default',
updated_at: '2023-10-04T13:08:53.340Z',
updated_by: 'elastic',
data_streams: [],
is_managed: false,
is_default: false,
is_preconfigured: false,
} as any;
return renderer.render(<AgentSettings agent={agent} agentPolicy={agentPolicy} />);
};
const mockStartServices = (isServerlessEnabled?: boolean) => {
mockUseStartServices.mockReturnValue({
application: {},
data: {
query: {
timefilter: {
timefilter: {
calculateBounds: jest.fn().mockReturnValue({
min: '2023-10-04T13:08:53.340Z',
max: '2023-10-05T13:08:53.340Z',
}),
},
},
},
},
share: {
url: {
locators: {
get: () => ({
useUrl: () => 'https://locator.url',
}),
},
},
},
docLinks: {
links: {
fleet: {
agentLevelLogging: 'agentLevelLogging',
},
},
},
});
};
it('should show log level dropdown with correct value', () => {
mockStartServices();
const result = renderComponent();
const logLevelDropdown = result.getByTestId('selectAgentLogLevel');
expect(logLevelDropdown.getElementsByTagName('option').length).toBe(4);
expect(logLevelDropdown).toHaveDisplayValue('debug');
});
it('should hide reset log level button for agents version < 8.15.0', () => {
mockStartServices();
const result = renderComponent();
const resetLogLevelBtn = result.queryByTestId('resetLogLevelBtn');
expect(resetLogLevelBtn).not.toBeInTheDocument();
});
it('should show reset log level button for agents version >= 8.15.0', () => {
mockStartServices();
const result = renderComponent({ agentVersion: '8.15.0' });
const resetLogLevelBtn = result.getByTestId('resetLogLevelBtn');
expect(resetLogLevelBtn).toBeInTheDocument();
expect(resetLogLevelBtn).not.toHaveAttribute('disabled');
});
});

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 React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { SelectLogLevel } from '../agent_logs/select_log_level';
import type { Agent, AgentPolicy } from '../../../../../types';
interface AgentSettingsProps {
agent: Agent;
agentPolicy: AgentPolicy | undefined;
}
export const AgentSettings: React.FunctionComponent<AgentSettingsProps> = ({
agent,
agentPolicy,
}) => {
return (
<EuiFlexGroup alignItems="flexStart">
<EuiFlexItem>
<SelectLogLevel
agent={agent}
agentPolicyLogLevel={agentPolicy?.advanced_settings?.agent_logging_level}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -33,6 +33,7 @@ import {
AgentDetailsContent,
AgentDiagnosticsTab,
} from './components';
import { AgentSettings } from './components/agent_settings';
export const AgentDetailsPage: React.FunctionComponent = () => {
const {
@ -160,6 +161,14 @@ export const AgentDetailsPage: React.FunctionComponent = () => {
href: getHref('agent_details_diagnostics', { agentId, tabId: 'diagnostics' }),
isSelected: tabId === 'diagnostics',
},
{
id: 'settings',
name: i18n.translate('xpack.fleet.agentDetails.subTabs.settingsTab', {
defaultMessage: 'Settings',
}),
href: getHref('agent_details_settings', { agentId, tabId: 'settings' }),
isSelected: tabId === 'settings',
},
];
return tabs;
}, [getHref, agentId, tabId]);
@ -238,6 +247,12 @@ const AgentDetailsPageContent: React.FunctionComponent<{
return <AgentDiagnosticsTab agent={agent} />;
}}
/>
<Route
path={FLEET_ROUTING_PATHS.agent_details_settings}
render={() => {
return <AgentSettings agent={agent} agentPolicy={agentPolicy} />;
}}
/>
<Route
path={FLEET_ROUTING_PATHS.agent_details}
render={() => {

View file

@ -45,6 +45,7 @@ export type DynamicPage =
| 'agent_list'
| 'agent_details'
| 'agent_details_logs'
| 'agent_details_settings'
| 'agent_details_diagnostics'
| 'settings_edit_outputs'
| 'settings_edit_download_sources'
@ -68,6 +69,7 @@ export const FLEET_ROUTING_PATHS = {
agent_details: '/agents/:agentId/:tabId?',
agent_details_logs: '/agents/:agentId/logs',
agent_details_diagnostics: '/agents/:agentId/diagnostics',
agent_details_settings: '/agents/:agentId/settings',
policies: '/policies',
policies_list: '/policies',
policy_details: '/policies/:policyId/:tabId?',
@ -247,6 +249,7 @@ export const pagePathGetters: {
],
agent_details_logs: ({ agentId }) => [FLEET_BASE_PATH, `/agents/${agentId}/logs`],
agent_details_diagnostics: ({ agentId }) => [FLEET_BASE_PATH, `/agents/${agentId}/diagnostics`],
agent_details_settings: ({ agentId }) => [FLEET_BASE_PATH, `/agents/${agentId}/settings`],
enrollment_tokens: () => [FLEET_BASE_PATH, '/enrollment-tokens'],
uninstall_tokens: () => [FLEET_BASE_PATH, FLEET_ROUTING_PATHS.uninstall_tokens],
data_streams: () => [FLEET_BASE_PATH, '/data-streams'],