[Ingest] Agent config settings UI (#64854)

* Remove duplicate donut chart, move used donut chart closer to usage, clean up import paths

* Change delete agent config to only single delete and delete all its associated data sources

* Initial pass at settings tab

* Reuse existing create agent config form instead

* Fix delete api

* Prevent nav drawer from hiding bottom bar (save action area) content

* Remove delete config functionality from list page

* Prevent API from deleting config with agents enrolled

* Fix namespace populating in form

* Display confirmation modal to deploy to agents if agents are detected

* Adjust confirm delete copy

* Fix i18n checks

* Fix type check

* Fix it again

* De-dupe confirm modal

* Fix i18n

* Update agent config info after saving

* Adjust skip unassign from agent config option schema in delete datasource method
This commit is contained in:
Jen Huang 2020-04-30 11:12:22 -07:00 committed by GitHub
parent fc2f2446eb
commit 8a304623d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 680 additions and 744 deletions

View file

@ -49,16 +49,16 @@ export interface UpdateAgentConfigResponse {
success: boolean;
}
export interface DeleteAgentConfigsRequest {
export interface DeleteAgentConfigRequest {
body: {
agentConfigIds: string[];
agentConfigId: string;
};
}
export type DeleteAgentConfigsResponse = Array<{
export interface DeleteAgentConfigResponse {
id: string;
success: boolean;
}>;
}
export interface GetFullAgentConfigRequest {
params: {

View file

@ -18,8 +18,8 @@ import {
CreateAgentConfigResponse,
UpdateAgentConfigRequest,
UpdateAgentConfigResponse,
DeleteAgentConfigsRequest,
DeleteAgentConfigsResponse,
DeleteAgentConfigRequest,
DeleteAgentConfigResponse,
} from '../../types';
export const useGetAgentConfigs = (query: HttpFetchQuery = {}) => {
@ -75,8 +75,8 @@ export const sendUpdateAgentConfig = (
});
};
export const sendDeleteAgentConfigs = (body: DeleteAgentConfigsRequest['body']) => {
return sendRequest<DeleteAgentConfigsResponse>({
export const sendDeleteAgentConfig = (body: DeleteAgentConfigRequest['body']) => {
return sendRequest<DeleteAgentConfigResponse>({
path: agentConfigRouteService.getDeletePath(),
method: 'post',
body: JSON.stringify(body),

View file

@ -0,0 +1,14 @@
@import '@elastic/eui/src/components/header/variables';
@import '@elastic/eui/src/components/nav_drawer/variables';
/**
* 1. Hack EUI so the bottom bar doesn't obscure the nav drawer flyout.
*/
.ingestManager__bottomBar {
z-index: 0; /* 1 */
left: $euiNavDrawerWidthCollapsed;
}
.ingestManager__bottomBar-isNavDrawerLocked {
left: $euiNavDrawerWidthExpanded;
}

View file

@ -23,6 +23,7 @@ import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp, DataStreamApp
import { CoreContext, DepsContext, ConfigContext, setHttpClient, useConfig } from './hooks';
import { PackageInstallProvider } from './sections/epm/hooks';
import { sendSetup } from './hooks/use_request/setup';
import './index.scss';
export interface ProtectedRouteProps extends RouteProps {
isAllowed?: boolean;

View file

@ -5,117 +5,92 @@
*/
import React, { Fragment, useRef, useState } from 'react';
import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
import { EuiConfirmModal, EuiOverlayMask, EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants';
import { sendDeleteAgentConfigs, useCore, sendRequest } from '../../../hooks';
import { sendDeleteAgentConfig, useCore, useConfig, sendRequest } from '../../../hooks';
interface Props {
children: (deleteAgentConfigs: deleteAgentConfigs) => React.ReactElement;
children: (deleteAgentConfig: DeleteAgentConfig) => React.ReactElement;
}
export type deleteAgentConfigs = (agentConfigs: string[], onSuccess?: OnSuccessCallback) => void;
export type DeleteAgentConfig = (agentConfig: string, onSuccess?: OnSuccessCallback) => void;
type OnSuccessCallback = (agentConfigsUnenrolled: string[]) => void;
type OnSuccessCallback = (agentConfigDeleted: string) => void;
export const AgentConfigDeleteProvider: React.FunctionComponent<Props> = ({ children }) => {
const { notifications } = useCore();
const [agentConfigs, setAgentConfigs] = useState<string[]>([]);
const {
fleet: { enabled: isFleetEnabled },
} = useConfig();
const [agentConfig, setAgentConfig] = useState<string>();
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [isLoadingAgentsCount, setIsLoadingAgentsCount] = useState<boolean>(false);
const [agentsCount, setAgentsCount] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(false);
const onSuccessCallback = useRef<OnSuccessCallback | null>(null);
const deleteAgentConfigsPrompt: deleteAgentConfigs = (
agentConfigsToDelete,
const deleteAgentConfigPrompt: DeleteAgentConfig = (
agentConfigToDelete,
onSuccess = () => undefined
) => {
if (
agentConfigsToDelete === undefined ||
(Array.isArray(agentConfigsToDelete) && agentConfigsToDelete.length === 0)
) {
throw new Error('No agent configs specified for deletion');
if (!agentConfigToDelete) {
throw new Error('No agent config specified for deletion');
}
setIsModalOpen(true);
setAgentConfigs(agentConfigsToDelete);
fetchAgentsCount(agentConfigsToDelete);
setAgentConfig(agentConfigToDelete);
fetchAgentsCount(agentConfigToDelete);
onSuccessCallback.current = onSuccess;
};
const closeModal = () => {
setAgentConfigs([]);
setAgentConfig(undefined);
setIsLoading(false);
setIsLoadingAgentsCount(false);
setIsModalOpen(false);
};
const deleteAgentConfigs = async () => {
const deleteAgentConfig = async () => {
setIsLoading(true);
try {
const { data } = await sendDeleteAgentConfigs({
agentConfigIds: agentConfigs,
const { data } = await sendDeleteAgentConfig({
agentConfigId: agentConfig!,
});
const successfulResults = data?.filter(result => result.success) || [];
const failedResults = data?.filter(result => !result.success) || [];
if (successfulResults.length) {
const hasMultipleSuccesses = successfulResults.length > 1;
const successMessage = hasMultipleSuccesses
? i18n.translate(
'xpack.ingestManager.deleteAgentConfigs.successMultipleNotificationTitle',
{
defaultMessage: 'Deleted {count} agent configs',
values: { count: successfulResults.length },
}
)
: i18n.translate(
'xpack.ingestManager.deleteAgentConfigs.successSingleNotificationTitle',
{
defaultMessage: "Deleted agent config '{id}'",
values: { id: successfulResults[0].id },
}
);
notifications.toasts.addSuccess(successMessage);
if (data?.success) {
notifications.toasts.addSuccess(
i18n.translate('xpack.ingestManager.deleteAgentConfig.successSingleNotificationTitle', {
defaultMessage: "Deleted agent config '{id}'",
values: { id: agentConfig },
})
);
if (onSuccessCallback.current) {
onSuccessCallback.current(agentConfig!);
}
}
if (failedResults.length) {
const hasMultipleFailures = failedResults.length > 1;
const failureMessage = hasMultipleFailures
? i18n.translate(
'xpack.ingestManager.deleteAgentConfigs.failureMultipleNotificationTitle',
{
defaultMessage: 'Error deleting {count} agent configs',
values: { count: failedResults.length },
}
)
: i18n.translate(
'xpack.ingestManager.deleteAgentConfigs.failureSingleNotificationTitle',
{
defaultMessage: "Error deleting agent config '{id}'",
values: { id: failedResults[0].id },
}
);
notifications.toasts.addDanger(failureMessage);
}
if (onSuccessCallback.current) {
onSuccessCallback.current(successfulResults.map(result => result.id));
if (!data?.success) {
notifications.toasts.addDanger(
i18n.translate('xpack.ingestManager.deleteAgentConfig.failureSingleNotificationTitle', {
defaultMessage: "Error deleting agent config '{id}'",
values: { id: agentConfig },
})
);
}
} catch (e) {
notifications.toasts.addDanger(
i18n.translate('xpack.ingestManager.deleteAgentConfigs.fatalErrorNotificationTitle', {
defaultMessage: 'Error deleting agent configs',
i18n.translate('xpack.ingestManager.deleteAgentConfig.fatalErrorNotificationTitle', {
defaultMessage: 'Error deleting agent config',
})
);
}
closeModal();
};
const fetchAgentsCount = async (agentConfigsToCheck: string[]) => {
if (isLoadingAgentsCount) {
const fetchAgentsCount = async (agentConfigToCheck: string) => {
if (!isFleetEnabled || isLoadingAgentsCount) {
return;
}
setIsLoadingAgentsCount(true);
@ -123,7 +98,7 @@ export const AgentConfigDeleteProvider: React.FunctionComponent<Props> = ({ chil
path: `/api/ingest_manager/fleet/agents`,
method: 'get',
query: {
kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id : (${agentConfigsToCheck.join(' or ')})`,
kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id : ${agentConfigToCheck}`,
},
});
setAgentsCount(data?.total || 0);
@ -140,68 +115,61 @@ export const AgentConfigDeleteProvider: React.FunctionComponent<Props> = ({ chil
<EuiConfirmModal
title={
<FormattedMessage
id="xpack.ingestManager.deleteAgentConfigs.confirmModal.deleteMultipleTitle"
defaultMessage="Delete {count, plural, one {this agent config} other {# agent configs}}?"
values={{ count: agentConfigs.length }}
id="xpack.ingestManager.deleteAgentConfig.confirmModal.deleteConfigTitle"
defaultMessage="Delete this agent configuration?"
/>
}
onCancel={closeModal}
onConfirm={deleteAgentConfigs}
onConfirm={deleteAgentConfig}
cancelButtonText={
<FormattedMessage
id="xpack.ingestManager.deleteAgentConfigs.confirmModal.cancelButtonLabel"
id="xpack.ingestManager.deleteAgentConfig.confirmModal.cancelButtonLabel"
defaultMessage="Cancel"
/>
}
confirmButtonText={
isLoading || isLoadingAgentsCount ? (
<FormattedMessage
id="xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingButtonLabel"
id="xpack.ingestManager.deleteAgentConfig.confirmModal.loadingButtonLabel"
defaultMessage="Loading…"
/>
) : agentsCount ? (
<FormattedMessage
id="xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmAndReassignButtonLabel"
defaultMessage="Delete {agentConfigsCount, plural, one {agent config} other {agent configs}} and unenroll {agentsCount, plural, one {agent} other {agents}}"
values={{
agentsCount,
agentConfigsCount: agentConfigs.length,
}}
/>
) : (
<FormattedMessage
id="xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmButtonLabel"
defaultMessage="Delete {agentConfigsCount, plural, one {agent config} other {agent configs}}"
values={{
agentConfigsCount: agentConfigs.length,
}}
id="xpack.ingestManager.deleteAgentConfig.confirmModal.confirmButtonLabel"
defaultMessage="Delete configuration"
/>
)
}
buttonColor="danger"
confirmButtonDisabled={isLoading || isLoadingAgentsCount}
confirmButtonDisabled={isLoading || isLoadingAgentsCount || !!agentsCount}
>
{isLoadingAgentsCount ? (
<FormattedMessage
id="xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingAgentsCountMessage"
id="xpack.ingestManager.deleteAgentConfig.confirmModal.loadingAgentsCountMessage"
defaultMessage="Checking amount of affected agents…"
/>
) : agentsCount ? (
<FormattedMessage
id="xpack.ingestManager.deleteAgentConfigs.confirmModal.affectedAgentsMessage"
defaultMessage="{agentsCount, plural, one {# agent is} other {# agents are}} assigned {agentConfigsCount, plural, one {to this agent config} other {across these agentConfigs}}. {agentsCount, plural, one {This agent} other {These agents}} will be unenrolled."
values={{
agentsCount,
agentConfigsCount: agentConfigs.length,
}}
/>
<EuiCallOut
color="danger"
title={i18n.translate(
'xpack.ingestManager.deleteAgentConfig.confirmModal.affectedAgentsTitle',
{
defaultMessage: 'Configuration in use',
}
)}
>
<FormattedMessage
id="xpack.ingestManager.deleteAgentConfig.confirmModal.affectedAgentsMessage"
defaultMessage="{agentsCount, plural, one {# agent is} other {# agents are}} assigned to this agent configuration. Unassign these agents before deleting this configuration."
values={{
agentsCount,
}}
/>
</EuiCallOut>
) : (
<FormattedMessage
id="xpack.ingestManager.deleteAgentConfigs.confirmModal.noAffectedAgentsMessage"
defaultMessage="There are no agents assigned to {agentConfigsCount, plural, one {this agent config} other {these agentConfigs}}."
values={{
agentConfigsCount: agentConfigs.length,
}}
id="xpack.ingestManager.deleteAgentConfig.confirmModal.irreversibleMessage"
defaultMessage="This action cannot be undone."
/>
)}
</EuiConfirmModal>
@ -211,7 +179,7 @@ export const AgentConfigDeleteProvider: React.FunctionComponent<Props> = ({ chil
return (
<Fragment>
{children(deleteAgentConfigsPrompt)}
{children(deleteAgentConfigPrompt)}
{renderModal()}
</Fragment>
);

View file

@ -8,8 +8,7 @@ import React, { useMemo, useState } from 'react';
import {
EuiAccordion,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiDescribedFormGroup,
EuiForm,
EuiFormRow,
EuiHorizontalRule,
@ -19,11 +18,13 @@ import {
EuiComboBox,
EuiIconTip,
EuiCheckboxGroup,
EuiButton,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import { NewAgentConfig } from '../../../types';
import { NewAgentConfig, AgentConfig } from '../../../types';
import { AgentConfigDeleteProvider } from './config_delete_provider';
interface ValidationResults {
[key: string]: JSX.Element[];
@ -36,7 +37,7 @@ const StyledEuiAccordion = styled(EuiAccordion)`
`;
export const agentConfigFormValidation = (
agentConfig: Partial<NewAgentConfig>
agentConfig: Partial<NewAgentConfig | AgentConfig>
): ValidationResults => {
const errors: ValidationResults = {};
@ -53,11 +54,13 @@ export const agentConfigFormValidation = (
};
interface Props {
agentConfig: Partial<NewAgentConfig>;
updateAgentConfig: (u: Partial<NewAgentConfig>) => void;
agentConfig: Partial<NewAgentConfig | AgentConfig>;
updateAgentConfig: (u: Partial<NewAgentConfig | AgentConfig>) => void;
withSysMonitoring: boolean;
updateSysMonitoring: (newValue: boolean) => void;
validation: ValidationResults;
isEditing?: boolean;
onDelete?: () => void;
}
export const AgentConfigForm: React.FunctionComponent<Props> = ({
@ -66,9 +69,11 @@ export const AgentConfigForm: React.FunctionComponent<Props> = ({
withSysMonitoring,
updateSysMonitoring,
validation,
isEditing = false,
onDelete = () => {},
}) => {
const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({});
const [showNamespace, setShowNamespace] = useState<boolean>(false);
const [showNamespace, setShowNamespace] = useState<boolean>(!!agentConfig.namespace);
const fields: Array<{
name: 'name' | 'description' | 'namespace';
label: JSX.Element;
@ -105,209 +110,281 @@ export const AgentConfigForm: React.FunctionComponent<Props> = ({
];
}, []);
return (
<EuiForm>
{fields.map(({ name, label, placeholder }) => {
return (
<EuiFormRow
fullWidth
key={name}
label={label}
error={touchedFields[name] && validation[name] ? validation[name] : null}
isInvalid={Boolean(touchedFields[name] && validation[name])}
>
<EuiFieldText
fullWidth
value={agentConfig[name]}
onChange={e => updateAgentConfig({ [name]: e.target.value })}
isInvalid={Boolean(touchedFields[name] && validation[name])}
onBlur={() => setTouchedFields({ ...touchedFields, [name]: true })}
placeholder={placeholder}
/>
</EuiFormRow>
);
})}
const generalSettingsWrapper = (children: JSX.Element[]) => (
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.ingestManager.configForm.generalSettingsGroupTitle"
defaultMessage="General settings"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.ingestManager.configForm.generalSettingsGroupDescription"
defaultMessage="Choose a name and description for your agent configuration."
/>
}
>
{children}
</EuiDescribedFormGroup>
);
const generalFields = fields.map(({ name, label, placeholder }) => {
return (
<EuiFormRow
label={
<EuiText size="xs" color="subdued">
fullWidth
key={name}
label={label}
error={touchedFields[name] && validation[name] ? validation[name] : null}
isInvalid={Boolean(touchedFields[name] && validation[name])}
>
<EuiFieldText
fullWidth
value={agentConfig[name]}
onChange={e => updateAgentConfig({ [name]: e.target.value })}
isInvalid={Boolean(touchedFields[name] && validation[name])}
onBlur={() => setTouchedFields({ ...touchedFields, [name]: true })}
placeholder={placeholder}
/>
</EuiFormRow>
);
});
const advancedOptionsContent = (
<>
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.ingestManager.agentConfigForm.systemMonitoringFieldLabel"
defaultMessage="Optional"
id="xpack.ingestManager.agentConfigForm.namespaceFieldLabel"
defaultMessage="Default namespace"
/>
</EuiText>
</h4>
}
description={
<FormattedMessage
id="xpack.ingestManager.agentConfigForm.namespaceFieldDescription"
defaultMessage="Apply a default namespace to data sources that use this configuration. Data sources can specify their own namespaces."
/>
}
>
<EuiSwitch
showLabel={true}
label={
<>
<FormattedMessage
id="xpack.ingestManager.agentConfigForm.systemMonitoringText"
defaultMessage="Collect system metrics"
/>{' '}
<EuiIconTip
content={i18n.translate(
'xpack.ingestManager.agentConfigForm.systemMonitoringTooltipText',
{
defaultMessage:
'Enable this option to bootstrap your configuration with a data source that collects system metrics and information.',
}
)}
position="right"
type="iInCircle"
/>
</>
<FormattedMessage
id="xpack.ingestManager.agentConfigForm.namespaceUseDefaultsFieldLabel"
defaultMessage="Use default namespace"
/>
}
checked={withSysMonitoring}
checked={showNamespace}
onChange={() => {
updateSysMonitoring(!withSysMonitoring);
setShowNamespace(!showNamespace);
if (showNamespace) {
updateAgentConfig({ namespace: '' });
}
}}
/>
</EuiFormRow>
<EuiHorizontalRule />
<EuiSpacer size="xs" />
<StyledEuiAccordion
id="advancedOptions"
buttonContent={
{showNamespace && (
<>
<EuiSpacer size="m" />
<EuiFormRow
fullWidth
error={touchedFields.namespace && validation.namespace ? validation.namespace : null}
isInvalid={Boolean(touchedFields.namespace && validation.namespace)}
>
<EuiComboBox
fullWidth
singleSelection
noSuggestions
selectedOptions={agentConfig.namespace ? [{ label: agentConfig.namespace }] : []}
onCreateOption={(value: string) => {
updateAgentConfig({ namespace: value });
}}
onChange={selectedOptions => {
updateAgentConfig({
namespace: (selectedOptions.length ? selectedOptions[0] : '') as string,
});
}}
isInvalid={Boolean(touchedFields.namespace && validation.namespace)}
onBlur={() => setTouchedFields({ ...touchedFields, namespace: true })}
/>
</EuiFormRow>
</>
)}
</EuiDescribedFormGroup>
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.ingestManager.agentConfigForm.monitoringLabel"
defaultMessage="Agent monitoring"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.ingestManager.agentConfigForm.advancedOptionsToggleLabel"
defaultMessage="Advanced options"
id="xpack.ingestManager.agentConfigForm.monitoringDescription"
defaultMessage="Collect data about your agents for debugging and tracking performance."
/>
}
buttonClassName="ingest-active-button"
>
<EuiSpacer size="l" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiText>
<h4>
<FormattedMessage
id="xpack.ingestManager.agentConfigForm.namespaceFieldLabel"
defaultMessage="Default namespace"
/>
</h4>
</EuiText>
<EuiSpacer size="m" />
<EuiText size="s">
<EuiCheckboxGroup
options={[
{
id: 'logs',
label: i18n.translate(
'xpack.ingestManager.agentConfigForm.monitoringLogsFieldLabel',
{ defaultMessage: 'Collect agent logs' }
),
},
{
id: 'metrics',
label: i18n.translate(
'xpack.ingestManager.agentConfigForm.monitoringMetricsFieldLabel',
{ defaultMessage: 'Collect agent metrics' }
),
},
]}
idToSelectedMap={(agentConfig.monitoring_enabled || []).reduce(
(acc: { logs: boolean; metrics: boolean }, key) => {
acc[key] = true;
return acc;
},
{ logs: false, metrics: false }
)}
onChange={id => {
if (id !== 'logs' && id !== 'metrics') {
return;
}
const hasLogs =
agentConfig.monitoring_enabled && agentConfig.monitoring_enabled.indexOf(id) >= 0;
const previousValues = agentConfig.monitoring_enabled || [];
updateAgentConfig({
monitoring_enabled: hasLogs
? previousValues.filter(type => type !== id)
: [...previousValues, id],
});
}}
/>
</EuiDescribedFormGroup>
{isEditing && 'id' in agentConfig ? (
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.ingestManager.agentConfigForm.namespaceFieldDescription"
defaultMessage="Apply a default namespace to data sources that use this configuration. Data sources can specify their own namespaces."
id="xpack.ingestManager.configForm.deleteConfigGroupTitle"
defaultMessage="Delete configuration"
/>
</h4>
}
description={
<>
<FormattedMessage
id="xpack.ingestManager.configForm.deleteConfigGroupDescription"
defaultMessage="Existing data will not be deleted."
/>
<EuiSpacer size="s" />
<AgentConfigDeleteProvider>
{deleteAgentConfigPrompt => {
return (
<EuiButton
color="danger"
disabled={Boolean(agentConfig.is_default)}
onClick={() => deleteAgentConfigPrompt(agentConfig.id!, onDelete)}
>
<FormattedMessage
id="xpack.ingestManager.configForm.deleteConfigActionText"
defaultMessage="Delete configuration"
/>
</EuiButton>
);
}}
</AgentConfigDeleteProvider>
{agentConfig.is_default ? (
<>
<EuiSpacer size="xs" />
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.ingestManager.configForm.unableToDeleteDefaultConfigText"
defaultMessage="Default configuration cannot be deleted"
/>
</EuiText>
</>
) : null}
</>
}
/>
) : null}
</>
);
return (
<EuiForm>
{!isEditing ? generalFields : generalSettingsWrapper(generalFields)}
{!isEditing ? (
<EuiFormRow
label={
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.ingestManager.agentConfigForm.systemMonitoringFieldLabel"
defaultMessage="Optional"
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiSwitch
showLabel={true}
label={
<FormattedMessage
id="xpack.ingestManager.agentConfigForm.namespaceUseDefaultsFieldLabel"
defaultMessage="Use default namespace"
/>
}
checked={showNamespace}
onChange={() => {
setShowNamespace(!showNamespace);
if (showNamespace) {
updateAgentConfig({ namespace: '' });
}
}}
/>
{showNamespace && (
}
>
<EuiSwitch
showLabel={true}
label={
<>
<EuiSpacer size="m" />
<EuiFormRow
fullWidth
error={
touchedFields.namespace && validation.namespace ? validation.namespace : null
}
isInvalid={Boolean(touchedFields.namespace && validation.namespace)}
>
<EuiComboBox
fullWidth
singleSelection
noSuggestions
selectedOptions={
agentConfig.namespace ? [{ label: agentConfig.namespace }] : []
}
onCreateOption={(value: string) => {
updateAgentConfig({ namespace: value });
}}
onChange={selectedOptions => {
updateAgentConfig({
namespace: (selectedOptions.length ? selectedOptions[0] : '') as string,
});
}}
isInvalid={Boolean(touchedFields.namespace && validation.namespace)}
onBlur={() => setTouchedFields({ ...touchedFields, namespace: true })}
/>
</EuiFormRow>
</>
)}
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiText>
<h4>
<FormattedMessage
id="xpack.ingestManager.agentConfigForm.monitoringLabel"
defaultMessage="Agent monitoring"
id="xpack.ingestManager.agentConfigForm.systemMonitoringText"
defaultMessage="Collect system metrics"
/>{' '}
<EuiIconTip
content={i18n.translate(
'xpack.ingestManager.agentConfigForm.systemMonitoringTooltipText',
{
defaultMessage:
'Enable this option to bootstrap your configuration with a data source that collects system metrics and information.',
}
)}
position="right"
type="iInCircle"
/>
</h4>
</EuiText>
<EuiSpacer size="m" />
<EuiText size="s">
</>
}
checked={withSysMonitoring}
onChange={() => {
updateSysMonitoring(!withSysMonitoring);
}}
/>
</EuiFormRow>
) : null}
{!isEditing ? (
<>
<EuiHorizontalRule />
<EuiSpacer size="xs" />
<StyledEuiAccordion
id="advancedOptions"
buttonContent={
<FormattedMessage
id="xpack.ingestManager.agentConfigForm.monitoringDescription"
defaultMessage="Collect data about your agents for debugging and tracking performance."
id="xpack.ingestManager.agentConfigForm.advancedOptionsToggleLabel"
defaultMessage="Advanced options"
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiCheckboxGroup
options={[
{
id: 'logs',
label: i18n.translate(
'xpack.ingestManager.agentConfigForm.monitoringLogsFieldLabel',
{ defaultMessage: 'Collect agent logs' }
),
},
{
id: 'metrics',
label: i18n.translate(
'xpack.ingestManager.agentConfigForm.monitoringMetricsFieldLabel',
{ defaultMessage: 'Collect agent metrics' }
),
},
]}
idToSelectedMap={(agentConfig.monitoring_enabled || []).reduce(
(acc: { logs: boolean; metrics: boolean }, key) => {
acc[key] = true;
return acc;
},
{ logs: false, metrics: false }
)}
onChange={id => {
if (id !== 'logs' && id !== 'metrics') {
return;
}
const hasLogs =
agentConfig.monitoring_enabled && agentConfig.monitoring_enabled.indexOf(id) >= 0;
const previousValues = agentConfig.monitoring_enabled || [];
updateAgentConfig({
monitoring_enabled: hasLogs
? previousValues.filter(type => type !== id)
: [...previousValues, id],
});
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
</StyledEuiAccordion>
}
buttonClassName="ingest-active-button"
>
<EuiSpacer size="l" />
{advancedOptionsContent}
</StyledEuiAccordion>
</>
) : (
advancedOptionsContent
)}
</EuiForm>
);
};

View file

@ -8,9 +8,9 @@ import React from 'react';
import { EuiCallOut, EuiOverlayMask, EuiConfirmModal, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { AgentConfig } from '../../../../types';
import { AgentConfig } from '../../../types';
export const ConfirmCreateDatasourceModal: React.FunctionComponent<{
export const ConfirmDeployConfigModal: React.FunctionComponent<{
onConfirm: () => void;
onCancel: () => void;
agentCount: number;
@ -21,7 +21,7 @@ export const ConfirmCreateDatasourceModal: React.FunctionComponent<{
<EuiConfirmModal
title={
<FormattedMessage
id="xpack.ingestManager.createDatasource.confirmModalTitle"
id="xpack.ingestManager.agentConfig.confirmModalTitle"
defaultMessage="Save and deploy changes"
/>
}
@ -29,13 +29,13 @@ export const ConfirmCreateDatasourceModal: React.FunctionComponent<{
onConfirm={onConfirm}
cancelButtonText={
<FormattedMessage
id="xpack.ingestManager.deleteApiKeys.confirmModal.cancelButtonLabel"
id="xpack.ingestManager.agentConfig.confirmModalCancelButtonLabel"
defaultMessage="Cancel"
/>
}
confirmButtonText={
<FormattedMessage
id="xpack.ingestManager.createDatasource.confirmModalConfirmButtonLabel"
id="xpack.ingestManager.agentConfig.confirmModalConfirmButtonLabel"
defaultMessage="Save and deploy changes"
/>
}
@ -43,7 +43,7 @@ export const ConfirmCreateDatasourceModal: React.FunctionComponent<{
>
<EuiCallOut
iconType="iInCircle"
title={i18n.translate('xpack.ingestManager.createDatasource.confirmModalCalloutTitle', {
title={i18n.translate('xpack.ingestManager.agentConfig.confirmModalCalloutTitle', {
defaultMessage:
'This action will update {agentCount, plural, one {# agent} other {# agents}}',
values: {
@ -52,7 +52,7 @@ export const ConfirmCreateDatasourceModal: React.FunctionComponent<{
})}
>
<FormattedMessage
id="xpack.ingestManager.createDatasource.confirmModalCalloutDescription"
id="xpack.ingestManager.agentConfig.confirmModalCalloutDescription"
defaultMessage="Fleet has detected that the selected agent configuration, {configName}, is already in use by
some of your agents. As a result of this action, Fleet will deploy updates to all agents
that use this configuration."
@ -63,7 +63,7 @@ export const ConfirmCreateDatasourceModal: React.FunctionComponent<{
</EuiCallOut>
<EuiSpacer size="l" />
<FormattedMessage
id="xpack.ingestManager.createDatasource.confirmModalDescription"
id="xpack.ingestManager.agentConfig.confirmModalDescription"
defaultMessage="This action can not be undone. Are you sure you wish to continue?"
/>
</EuiConfirmModal>

View file

@ -7,3 +7,4 @@
export { AgentConfigForm, agentConfigFormValidation } from './config_form';
export { AgentConfigDeleteProvider } from './config_delete_provider';
export { LinkedAgentCount } from './linked_agent_count';
export { ConfirmDeployConfigModal } from './confirm_deploy_modal';

View file

@ -8,7 +8,7 @@ import React, { memo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiLink } from '@elastic/eui';
import { useLink } from '../../../hooks';
import { FLEET_AGENTS_PATH } from '../../../constants';
import { FLEET_AGENTS_PATH, AGENT_SAVED_OBJECT_TYPE } from '../../../constants';
export const LinkedAgentCount = memo<{ count: number; agentConfigId: string }>(
({ count, agentConfigId }) => {
@ -21,7 +21,7 @@ export const LinkedAgentCount = memo<{ count: number; agentConfigId: string }>(
/>
);
return count > 0 ? (
<EuiLink href={`${FLEET_URI}?kuery=agents.config_id : ${agentConfigId}`}>
<EuiLink href={`${FLEET_URI}?kuery=${AGENT_SAVED_OBJECT_TYPE}.config_id : ${agentConfigId}`}>
{displayValue}
</EuiLink>
) : (

View file

@ -5,5 +5,4 @@
*/
export { CreateDatasourcePageLayout } from './layout';
export { DatasourceInputPanel } from './datasource_input_panel';
export { ConfirmCreateDatasourceModal } from './confirm_modal';
export { DatasourceInputVarField } from './datasource_input_var_field';

View file

@ -27,7 +27,8 @@ import {
sendGetAgentStatus,
} from '../../../hooks';
import { useLinks as useEPMLinks } from '../../epm/hooks';
import { CreateDatasourcePageLayout, ConfirmCreateDatasourceModal } from './components';
import { ConfirmDeployConfigModal } from '../components';
import { CreateDatasourcePageLayout } from './components';
import { CreateDatasourceFrom, DatasourceFormState } from './types';
import { DatasourceValidationResults, validateDatasource, validationHasErrors } from './services';
import { StepSelectPackage } from './step_select_package';
@ -36,7 +37,10 @@ import { StepConfigureDatasource } from './step_configure_datasource';
import { StepDefineDatasource } from './step_define_datasource';
export const CreateDatasourcePage: React.FunctionComponent = () => {
const { notifications } = useCore();
const {
notifications,
chrome: { getIsNavDrawerLocked$ },
} = useCore();
const {
fleet: { enabled: isFleetEnabled },
} = useConfig();
@ -45,6 +49,15 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
} = useRouteMatch();
const history = useHistory();
const from: CreateDatasourceFrom = configId ? 'config' : 'package';
const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false);
useEffect(() => {
const subscription = getIsNavDrawerLocked$().subscribe((newIsNavDrawerLocked: boolean) => {
setIsNavDrawerLocked(newIsNavDrawerLocked);
});
return () => subscription.unsubscribe();
});
// Agent config and package info states
const [agentConfig, setAgentConfig] = useState<AgentConfig>();
@ -269,7 +282,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
return (
<CreateDatasourcePageLayout {...layoutProps}>
{formState === 'CONFIRM' && agentConfig && (
<ConfirmCreateDatasourceModal
<ConfirmDeployConfigModal
agentCount={agentCount}
agentConfig={agentConfig}
onConfirm={onSubmit}
@ -278,7 +291,14 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
)}
<EuiSteps steps={steps} />
<EuiSpacer size="l" />
<EuiBottomBar css={{ zIndex: 5 }} paddingSize="s">
<EuiBottomBar
css={{ zIndex: 5 }}
className={
isNavDrawerLocked
? 'ingestManager__bottomBar-isNavDrawerLocked'
: 'ingestManager__bottomBar'
}
>
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty color="ghost" href={cancelUrl}>

View file

@ -1,94 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import { EuiFieldText, EuiForm, EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { AgentConfig } from '../../../../types';
interface ValidationResults {
[key: string]: JSX.Element[];
}
export const configFormValidation = (config: Partial<AgentConfig>): ValidationResults => {
const errors: ValidationResults = {};
if (!config.name?.trim()) {
errors.name = [
<FormattedMessage
id="xpack.ingestManager.configForm.nameRequiredErrorMessage"
defaultMessage="Config name is required"
/>,
];
}
return errors;
};
interface Props {
config: Partial<AgentConfig>;
updateConfig: (u: Partial<AgentConfig>) => void;
validation: ValidationResults;
}
export const ConfigForm: React.FunctionComponent<Props> = ({
config,
updateConfig,
validation,
}) => {
const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({});
const fields: Array<{ name: 'name' | 'description' | 'namespace'; label: JSX.Element }> = [
{
name: 'name',
label: (
<FormattedMessage
id="xpack.ingestManager.configForm.nameFieldLabel"
defaultMessage="Name"
/>
),
},
{
name: 'description',
label: (
<FormattedMessage
id="xpack.ingestManager.configForm.descriptionFieldLabel"
defaultMessage="Description"
/>
),
},
{
name: 'namespace',
label: (
<FormattedMessage
id="xpack.ingestManager.configForm.namespaceFieldLabel"
defaultMessage="Namespace"
/>
),
},
];
return (
<EuiForm>
{fields.map(({ name, label }) => {
return (
<EuiFormRow
key={name}
label={label}
error={touchedFields[name] && validation[name] ? validation[name] : null}
isInvalid={Boolean(touchedFields[name] && validation[name])}
>
<EuiFieldText
value={config[name]}
onChange={e => updateConfig({ [name]: e.target.value })}
isInvalid={Boolean(touchedFields[name] && validation[name])}
onBlur={() => setTouchedFields({ ...touchedFields, [name]: true })}
/>
</EuiFormRow>
);
})}
</EuiForm>
);
};

View file

@ -1,65 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect, useRef } from 'react';
import d3 from 'd3';
import { EuiFlexItem } from '@elastic/eui';
interface DonutChartProps {
data: {
[key: string]: number;
};
height: number;
width: number;
}
export const DonutChart = ({ height, width, data }: DonutChartProps) => {
const chartElement = useRef<SVGSVGElement | null>(null);
useEffect(() => {
if (chartElement.current !== null) {
// we must remove any existing paths before painting
d3.selectAll('g').remove();
const svgElement = d3
.select(chartElement.current)
.append('g')
.attr('transform', `translate(${width / 2}, ${height / 2})`);
const color = d3.scale
.ordinal()
// @ts-ignore
.domain(data)
.range(['#017D73', '#98A2B3', '#BD271E']);
const pieGenerator = d3.layout
.pie()
.value(({ value }: any) => value)
// these start/end angles will reverse the direction of the pie,
// which matches our design
.startAngle(2 * Math.PI)
.endAngle(0);
svgElement
.selectAll('g')
// @ts-ignore
.data(pieGenerator(d3.entries(data)))
.enter()
.append('path')
.attr(
'd',
// @ts-ignore attr does not expect a param of type Arc<Arc> but it behaves as desired
d3.svg
.arc()
.innerRadius(width * 0.28)
.outerRadius(Math.min(width, height) / 2 - 10)
)
.attr('fill', (d: any) => color(d.data.key));
}
}, [data, height, width]);
return (
<EuiFlexItem grow={false}>
<svg ref={chartElement} width={width} height={height} />
</EuiFlexItem>
);
};

View file

@ -1,135 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import {
EuiFlyout,
EuiFlyoutHeader,
EuiTitle,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiButton,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { useCore, sendRequest } from '../../../../hooks';
import { agentConfigRouteService } from '../../../../services';
import { AgentConfig } from '../../../../types';
import { ConfigForm, configFormValidation } from './config_form';
interface Props {
agentConfig: AgentConfig;
onClose: () => void;
}
export const EditConfigFlyout: React.FunctionComponent<Props> = ({
agentConfig: originalAgentConfig,
onClose,
}) => {
const { notifications } = useCore();
const [config, setConfig] = useState<Partial<AgentConfig>>({
name: originalAgentConfig.name,
description: originalAgentConfig.description,
});
const [isLoading, setIsLoading] = useState<boolean>(false);
const updateConfig = (updatedFields: Partial<AgentConfig>) => {
setConfig({
...config,
...updatedFields,
});
};
const validation = configFormValidation(config);
const header = (
<EuiFlyoutHeader hasBorder aria-labelledby="FleetEditConfigFlyoutTitle">
<EuiTitle size="m">
<h2 id="FleetEditConfigFlyoutTitle">
<FormattedMessage
id="xpack.ingestManager.editConfig.flyoutTitle"
defaultMessage="Edit config"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
);
const body = (
<EuiFlyoutBody>
<ConfigForm config={config} updateConfig={updateConfig} validation={validation} />
</EuiFlyoutBody>
);
const footer = (
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
<FormattedMessage
id="xpack.ingestManager.editConfig.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
isLoading={isLoading}
disabled={isLoading || Object.keys(validation).length > 0}
onClick={async () => {
setIsLoading(true);
try {
const { error } = await sendRequest({
path: agentConfigRouteService.getUpdatePath(originalAgentConfig.id),
method: 'put',
body: JSON.stringify(config),
});
if (!error) {
notifications.toasts.addSuccess(
i18n.translate('xpack.ingestManager.editConfig.successNotificationTitle', {
defaultMessage: "Agent config '{name}' updated",
values: { name: config.name },
})
);
} else {
notifications.toasts.addDanger(
error
? error.message
: i18n.translate('xpack.ingestManager.editConfig.errorNotificationTitle', {
defaultMessage: 'Unable to update agent config',
})
);
}
} catch (e) {
notifications.toasts.addDanger(
i18n.translate('xpack.ingestManager.editConfig.errorNotificationTitle', {
defaultMessage: 'Unable to update agent config',
})
);
}
setIsLoading(false);
onClose();
}}
>
<FormattedMessage
id="xpack.ingestManager.editConfig.submitButtonLabel"
defaultMessage="Update"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
);
return (
<EuiFlyout onClose={onClose} size="m" maxWidth={400}>
{header}
{body}
{footer}
</EuiFlyout>
);
};

View file

@ -4,5 +4,3 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { DatasourcesTable } from './datasources/datasources_table';
export { DonutChart } from './donut_chart';
export { EditConfigFlyout } from './edit_config';

View file

@ -0,0 +1,216 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { memo, useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import styled from 'styled-components';
import { EuiBottomBar, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { AGENT_CONFIG_PATH } from '../../../../../constants';
import { AgentConfig } from '../../../../../types';
import {
useCore,
useCapabilities,
sendUpdateAgentConfig,
useConfig,
sendGetAgentStatus,
} from '../../../../../hooks';
import {
AgentConfigForm,
agentConfigFormValidation,
ConfirmDeployConfigModal,
} from '../../../components';
import { useConfigRefresh } from '../../hooks';
const FormWrapper = styled.div`
max-width: 800px;
margin-right: auto;
margin-left: auto;
`;
export const ConfigSettingsView = memo<{ config: AgentConfig }>(
({ config: originalAgentConfig }) => {
const {
notifications,
chrome: { getIsNavDrawerLocked$ },
} = useCore();
const {
fleet: { enabled: isFleetEnabled },
} = useConfig();
const history = useHistory();
const hasWriteCapabilites = useCapabilities().write;
const refreshConfig = useConfigRefresh();
const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false);
const [agentConfig, setAgentConfig] = useState<AgentConfig>({
...originalAgentConfig,
});
const [isLoading, setIsLoading] = useState<boolean>(false);
const [hasChanges, setHasChanges] = useState<boolean>(false);
const [agentCount, setAgentCount] = useState<number>(0);
const [withSysMonitoring, setWithSysMonitoring] = useState<boolean>(true);
const validation = agentConfigFormValidation(agentConfig);
useEffect(() => {
const subscription = getIsNavDrawerLocked$().subscribe((newIsNavDrawerLocked: boolean) => {
setIsNavDrawerLocked(newIsNavDrawerLocked);
});
return () => subscription.unsubscribe();
});
const updateAgentConfig = (updatedFields: Partial<AgentConfig>) => {
setAgentConfig({
...agentConfig,
...updatedFields,
});
setHasChanges(true);
};
const submitUpdateAgentConfig = async () => {
setIsLoading(true);
try {
const { name, description, namespace, monitoring_enabled } = agentConfig;
const { data, error } = await sendUpdateAgentConfig(agentConfig.id, {
name,
description,
namespace,
monitoring_enabled,
});
if (data?.success) {
notifications.toasts.addSuccess(
i18n.translate('xpack.ingestManager.editAgentConfig.successNotificationTitle', {
defaultMessage: "Successfully updated '{name}' settings",
values: { name: agentConfig.name },
})
);
refreshConfig();
setHasChanges(false);
} else {
notifications.toasts.addDanger(
error
? error.message
: i18n.translate('xpack.ingestManager.editAgentConfig.errorNotificationTitle', {
defaultMessage: 'Unable to update agent config',
})
);
}
} catch (e) {
notifications.toasts.addDanger(
i18n.translate('xpack.ingestManager.editAgentConfig.errorNotificationTitle', {
defaultMessage: 'Unable to update agent config',
})
);
}
setIsLoading(false);
};
const onSubmit = async () => {
// Retrieve agent count if fleet is enabled
if (isFleetEnabled) {
setIsLoading(true);
const { data } = await sendGetAgentStatus({ configId: agentConfig.id });
if (data?.results.total) {
setAgentCount(data.results.total);
} else {
await submitUpdateAgentConfig();
}
} else {
await submitUpdateAgentConfig();
}
};
return (
<FormWrapper>
{agentCount ? (
<ConfirmDeployConfigModal
agentCount={agentCount}
agentConfig={agentConfig}
onConfirm={() => {
setAgentCount(0);
submitUpdateAgentConfig();
}}
onCancel={() => {
setAgentCount(0);
setIsLoading(false);
}}
/>
) : null}
<AgentConfigForm
agentConfig={agentConfig}
updateAgentConfig={updateAgentConfig}
withSysMonitoring={withSysMonitoring}
updateSysMonitoring={newValue => setWithSysMonitoring(newValue)}
validation={validation}
isEditing={true}
onDelete={() => {
history.push(AGENT_CONFIG_PATH);
}}
/>
{hasChanges ? (
<EuiBottomBar
css={{ zIndex: 5 }}
className={
isNavDrawerLocked
? 'ingestManager__bottomBar-isNavDrawerLocked'
: 'ingestManager__bottomBar'
}
>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem>
<FormattedMessage
id="xpack.ingestManager.editAgentConfig.unsavedChangesText"
defaultMessage="You have unsaved changes"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
color="ghost"
onClick={() => {
setAgentConfig({ ...originalAgentConfig });
setHasChanges(false);
}}
>
<FormattedMessage
id="xpack.ingestManager.editAgentConfig.cancelButtonText"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={onSubmit}
isLoading={isLoading}
isDisabled={
!hasWriteCapabilites || isLoading || Object.keys(validation).length > 0
}
iconType="save"
color="primary"
fill
>
{isLoading ? (
<FormattedMessage
id="xpack.ingestManager.editAgentConfig.savingButtonText"
defaultMessage="Saving…"
/>
) : (
<FormattedMessage
id="xpack.ingestManager.editAgentConfig.saveButtonText"
defaultMessage="Save changes"
/>
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiBottomBar>
) : null}
</FormWrapper>
);
}
);

View file

@ -15,7 +15,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { AgentConfig } from '../../../../../../../../common/types/models';
import { AgentConfig } from '../../../../../types';
import {
useGetOneAgentConfigFull,
useGetEnrollmentAPIKeys,

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { useGetAgentStatus, AgentStatusRefreshContext } from './use_agent_status';
export { ConfigRefreshContext } from './use_config';
export { ConfigRefreshContext, useConfigRefresh } from './use_config';

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, memo, useCallback, useMemo, useState } from 'react';
import React, { Fragment, memo, useMemo, useState } from 'react';
import { Redirect, useRouteMatch, Switch, Route } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedDate } from '@kbn/i18n/react';
@ -26,12 +26,12 @@ import { useGetOneAgentConfig } from '../../../hooks';
import { Loading } from '../../../components';
import { WithHeaderLayout } from '../../../layouts';
import { ConfigRefreshContext, useGetAgentStatus, AgentStatusRefreshContext } from './hooks';
import { EditConfigFlyout } from './components';
import { LinkedAgentCount } from '../components';
import { useAgentConfigLink } from './hooks/use_details_uri';
import { DETAILS_ROUTER_PATH, DETAILS_ROUTER_SUB_PATH } from './constants';
import { ConfigDatasourcesView } from './components/datasources';
import { ConfigYamlView } from './components/yaml';
import { ConfigSettingsView } from './components/settings';
const Divider = styled.div`
width: 0;
@ -70,14 +70,6 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => {
const configDetailsYamlLink = useAgentConfigLink('details-yaml', { configId });
const configDetailsSettingsLink = useAgentConfigLink('details-settings', { configId });
// Flyout states
const [isEditConfigFlyoutOpen, setIsEditConfigFlyoutOpen] = useState<boolean>(false);
const refreshData = useCallback(() => {
refreshAgentConfig();
refreshAgentStatus();
}, [refreshAgentConfig, refreshAgentStatus]);
const headerLeftContent = useMemo(
() => (
<React.Fragment>
@ -196,7 +188,7 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => {
return [
{
id: 'datasources',
name: i18n.translate('xpack.ingestManager.configDetails.subTabs.datasouces', {
name: i18n.translate('xpack.ingestManager.configDetails.subTabs.datasourcesTabText', {
defaultMessage: 'Data sources',
}),
href: configDetailsLink,
@ -204,15 +196,15 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => {
},
{
id: 'yaml',
name: i18n.translate('xpack.ingestManager.configDetails.subTabs.yamlFile', {
defaultMessage: 'YAML File',
name: i18n.translate('xpack.ingestManager.configDetails.subTabs.yamlTabText', {
defaultMessage: 'YAML',
}),
href: configDetailsYamlLink,
isSelected: tabId === 'yaml',
},
{
id: 'settings',
name: i18n.translate('xpack.ingestManager.configDetails.subTabs.settings', {
name: i18n.translate('xpack.ingestManager.configDetails.subTabs.settingsTabText', {
defaultMessage: 'Settings',
}),
href: configDetailsSettingsLink,
@ -269,16 +261,6 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => {
rightColumn={headerRightContent}
tabs={(headerTabs as unknown) as EuiTabProps[]}
>
{isEditConfigFlyoutOpen ? (
<EditConfigFlyout
onClose={() => {
setIsEditConfigFlyoutOpen(false);
refreshData();
}}
agentConfig={agentConfig}
/>
) : null}
<Switch>
<Route
path={`${DETAILS_ROUTER_PATH}/yaml`}
@ -289,8 +271,7 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => {
<Route
path={`${DETAILS_ROUTER_PATH}/settings`}
render={() => {
// TODO: Settings implementation tracked via: https://github.com/elastic/kibana/issues/57959
return <div>Settings placeholder</div>;
return <ConfigSettingsView config={agentConfig} />;
}}
/>
<Route

View file

@ -29,10 +29,8 @@ import {
sendGetPackageInfoByKey,
} from '../../../hooks';
import { Loading, Error } from '../../../components';
import {
CreateDatasourcePageLayout,
ConfirmCreateDatasourceModal,
} from '../create_datasource_page/components';
import { ConfirmDeployConfigModal } from '../components';
import { CreateDatasourcePageLayout } from '../create_datasource_page/components';
import {
DatasourceValidationResults,
validateDatasource,
@ -243,7 +241,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => {
) : (
<>
{formState === 'CONFIRM' && (
<ConfirmCreateDatasourceModal
<ConfirmDeployConfigModal
agentCount={agentCount}
agentConfig={agentConfig}
onConfirm={onSubmit}

View file

@ -1,6 +0,0 @@
.fleet__agentList__table .euiTableFooterCell {
.euiTableCellContent,
.euiTableCellContent__text {
overflow: visible;
}
}

View file

@ -19,12 +19,12 @@ import {
} from '@elastic/eui';
import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab';
import { useRouteMatch } from 'react-router-dom';
import { DonutChart } from '../agent_list_page/components/donut_chart';
import { useGetAgentStatus } from '../../agent_config/details_page/hooks';
import { useCapabilities, useLink, useGetAgentConfigs } from '../../../hooks';
import { WithHeaderLayout } from '../../../layouts';
import { FLEET_ENROLLMENT_TOKENS_PATH, FLEET_AGENTS_PATH } from '../../../constants';
import { useCapabilities, useLink, useGetAgentConfigs } from '../../../hooks';
import { useGetAgentStatus } from '../../agent_config/details_page/hooks';
import { AgentEnrollmentFlyout } from '../agent_list_page/components';
import { DonutChart } from './donut_chart';
const REFRESH_INTERVAL_MS = 5000;

View file

@ -28,8 +28,8 @@ export {
CreateAgentConfigResponse,
UpdateAgentConfigRequest,
UpdateAgentConfigResponse,
DeleteAgentConfigsRequest,
DeleteAgentConfigsResponse,
DeleteAgentConfigRequest,
DeleteAgentConfigResponse,
// API schemas - Datasource
CreateDatasourceRequest,
CreateDatasourceResponse,

View file

@ -14,7 +14,7 @@ import {
GetOneAgentConfigRequestSchema,
CreateAgentConfigRequestSchema,
UpdateAgentConfigRequestSchema,
DeleteAgentConfigsRequestSchema,
DeleteAgentConfigRequestSchema,
GetFullAgentConfigRequestSchema,
AgentConfig,
DefaultPackages,
@ -26,7 +26,7 @@ import {
GetOneAgentConfigResponse,
CreateAgentConfigResponse,
UpdateAgentConfigResponse,
DeleteAgentConfigsResponse,
DeleteAgentConfigResponse,
GetFullAgentConfigResponse,
} from '../../../common';
@ -49,7 +49,7 @@ export const getAgentConfigsHandler: RequestHandler<
items,
(agentConfig: GetAgentConfigsResponseItem) =>
listAgents(soClient, {
showInactive: true,
showInactive: false,
perPage: 0,
page: 1,
kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id:${agentConfig.id}`,
@ -179,13 +179,13 @@ export const updateAgentConfigHandler: RequestHandler<
export const deleteAgentConfigsHandler: RequestHandler<
unknown,
unknown,
TypeOf<typeof DeleteAgentConfigsRequestSchema.body>
TypeOf<typeof DeleteAgentConfigRequestSchema.body>
> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
try {
const body: DeleteAgentConfigsResponse = await agentConfigService.delete(
const body: DeleteAgentConfigResponse = await agentConfigService.delete(
soClient,
request.body.agentConfigIds
request.body.agentConfigId
);
return response.ok({
body,

View file

@ -10,7 +10,7 @@ import {
GetOneAgentConfigRequestSchema,
CreateAgentConfigRequestSchema,
UpdateAgentConfigRequestSchema,
DeleteAgentConfigsRequestSchema,
DeleteAgentConfigRequestSchema,
GetFullAgentConfigRequestSchema,
} from '../../types';
import {
@ -67,7 +67,7 @@ export const registerRoutes = (router: IRouter) => {
router.post(
{
path: AGENT_CONFIG_API_ROUTES.DELETE_PATTERN,
validate: DeleteAgentConfigsRequestSchema,
validate: DeleteAgentConfigRequestSchema,
options: { tags: [`access:${PLUGIN_ID}-all`] },
},
deleteAgentConfigsHandler

View file

@ -6,7 +6,11 @@
import { uniq } from 'lodash';
import { SavedObjectsClientContract } from 'src/core/server';
import { AuthenticatedUser } from '../../../security/server';
import { DEFAULT_AGENT_CONFIG, AGENT_CONFIG_SAVED_OBJECT_TYPE } from '../constants';
import {
DEFAULT_AGENT_CONFIG,
AGENT_CONFIG_SAVED_OBJECT_TYPE,
AGENT_SAVED_OBJECT_TYPE,
} from '../constants';
import {
Datasource,
NewAgentConfig,
@ -15,7 +19,8 @@ import {
AgentConfigStatus,
ListWithKuery,
} from '../types';
import { DeleteAgentConfigsResponse, storedDatasourceToAgentDatasource } from '../../common';
import { DeleteAgentConfigResponse, storedDatasourceToAgentDatasource } from '../../common';
import { listAgents } from './agents';
import { datasourceService } from './datasource';
import { outputService } from './output';
import { agentConfigUpdateEventHandler } from './agent_config_update';
@ -256,32 +261,40 @@ class AgentConfigService {
public async delete(
soClient: SavedObjectsClientContract,
ids: string[]
): Promise<DeleteAgentConfigsResponse> {
const result: DeleteAgentConfigsResponse = [];
const defaultConfigId = await this.getDefaultAgentConfigId(soClient);
id: string
): Promise<DeleteAgentConfigResponse> {
const config = await this.get(soClient, id, false);
if (!config) {
throw new Error('Agent configuration not found');
}
if (ids.includes(defaultConfigId)) {
const defaultConfigId = await this.getDefaultAgentConfigId(soClient);
if (id === defaultConfigId) {
throw new Error('The default agent configuration cannot be deleted');
}
for (const id of ids) {
try {
await soClient.delete(SAVED_OBJECT_TYPE, id);
await this.triggerAgentConfigUpdatedEvent(soClient, 'deleted', id);
result.push({
id,
success: true,
});
} catch (e) {
result.push({
id,
success: false,
});
}
const { total } = await listAgents(soClient, {
showInactive: false,
perPage: 0,
page: 1,
kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id:${id}`,
});
if (total > 0) {
throw new Error('Cannot delete agent config that is assigned to agent(s)');
}
return result;
if (config.datasources && config.datasources.length) {
await datasourceService.delete(soClient, config.datasources as string[], {
skipUnassignFromAgentConfigs: true,
});
}
await soClient.delete(SAVED_OBJECT_TYPE, id);
await this.triggerAgentConfigUpdatedEvent(soClient, 'deleted', id);
return {
id,
success: true,
};
}
public async getFullConfig(

View file

@ -145,7 +145,7 @@ class DatasourceService {
public async delete(
soClient: SavedObjectsClientContract,
ids: string[],
options?: { user?: AuthenticatedUser }
options?: { user?: AuthenticatedUser; skipUnassignFromAgentConfigs?: boolean }
): Promise<DeleteDatasourcesResponse> {
const result: DeleteDatasourcesResponse = [];
@ -155,14 +155,16 @@ class DatasourceService {
if (!oldDatasource) {
throw new Error('Datasource not found');
}
await agentConfigService.unassignDatasources(
soClient,
oldDatasource.config_id,
[oldDatasource.id],
{
user: options?.user,
}
);
if (!options?.skipUnassignFromAgentConfigs) {
await agentConfigService.unassignDatasources(
soClient,
oldDatasource.config_id,
[oldDatasource.id],
{
user: options?.user,
}
);
}
await soClient.delete(SAVED_OBJECT_TYPE, id);
result.push({
id,

View file

@ -29,9 +29,9 @@ export const UpdateAgentConfigRequestSchema = {
body: NewAgentConfigSchema,
};
export const DeleteAgentConfigsRequestSchema = {
export const DeleteAgentConfigRequestSchema = {
body: schema.object({
agentConfigIds: schema.arrayOf(schema.string()),
agentConfigId: schema.string(),
}),
};

View file

@ -8317,9 +8317,6 @@
"xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "名前空間",
"xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "パッケージ",
"xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "ストリーム",
"xpack.ingestManager.configDetails.subTabs.datasouces": "データソース",
"xpack.ingestManager.configDetails.subTabs.settings": "設定",
"xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML ファイル",
"xpack.ingestManager.configDetails.summary.datasources": "データソース",
"xpack.ingestManager.configDetails.summary.lastUpdated": "最終更新日:",
"xpack.ingestManager.configDetails.summary.revision": "リビジョン",
@ -8329,10 +8326,6 @@
"xpack.ingestManager.configDetailsDatasources.createFirstButtonText": "データソースを作成",
"xpack.ingestManager.configDetailsDatasources.createFirstMessage": "この構成にはデータソースはまだありません。",
"xpack.ingestManager.configDetailsDatasources.createFirstTitle": "初めてのデーソースを作成する",
"xpack.ingestManager.configForm.descriptionFieldLabel": "説明",
"xpack.ingestManager.configForm.nameFieldLabel": "名前",
"xpack.ingestManager.configForm.nameRequiredErrorMessage": "構成名が必要です",
"xpack.ingestManager.configForm.namespaceFieldLabel": "名前空間",
"xpack.ingestManager.createAgentConfig.cancelButtonLabel": "キャンセル",
"xpack.ingestManager.createAgentConfig.errorNotificationTitle": "エージェント構成を作成できません",
"xpack.ingestManager.createAgentConfig.flyoutTitle": "エージェント構成を作成",
@ -8364,20 +8357,6 @@
"xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingPackagesTitle": "パッケージの読み込みエラー",
"xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingSelectedPackageTitle": "選択したパッケージの読み込みエラー",
"xpack.ingestManager.createDatasource.stepSelectPackage.filterPackagesInputPlaceholder": "パッケージの検索",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {# エージェントを} other {# エージェントを}}{agentConfigsCount, plural, one {このエージェント構成に} other {これらのエージェント構成に}}割り当てました。 {agentsCount, plural, one {このエージェント} other {これらのエージェント}}の登録が解除されます。",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.cancelButtonLabel": "キャンセル",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmAndReassignButtonLabel": "{agentConfigsCount, plural, one {エージェント構成} other {エージェント構成}} and unenroll {agentsCount, plural, one {エージェント} other {エージェント}} を削除",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmButtonLabel": "{agentConfigsCount, plural, one {エージェント構成} other {エージェント構成}}を削除",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.deleteMultipleTitle": "{count, plural, one {this agent config} other {# agent configs}} を削除しますか?",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingAgentsCountMessage": "影響があるエージェントの数を確認中...",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingButtonLabel": "読み込み中...",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.noAffectedAgentsMessage": "{agentConfigsCount, plural, one {this agent config} other {these agentConfigs}}に割り当てられたエージェントはありません。",
"xpack.ingestManager.deleteAgentConfigs.failureMultipleNotificationTitle": "{count} 件のエージェント構成の削除エラー",
"xpack.ingestManager.deleteAgentConfigs.failureSingleNotificationTitle": "エージェント構成「{id}」の削除エラー",
"xpack.ingestManager.deleteAgentConfigs.fatalErrorNotificationTitle": "エージェント構成の削除エラー",
"xpack.ingestManager.deleteAgentConfigs.successMultipleNotificationTitle": "{count} 件のエージェント構成を削除しました",
"xpack.ingestManager.deleteAgentConfigs.successSingleNotificationTitle": "エージェント構成「{id}」を削除しました",
"xpack.ingestManager.deleteApiKeys.confirmModal.cancelButtonLabel": "キャンセル",
"xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsMessage": "{agentConfigName} が一部のエージェントで既に使用されていることをフリートが検出しました。",
"xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsTitle": "このアクションは {agentsCount} {agentsCount, plural, one {# エージェント} other {# エージェント}}に影響します",
"xpack.ingestManager.deleteDatasource.confirmModal.cancelButtonLabel": "キャンセル",
@ -8393,11 +8372,6 @@
"xpack.ingestManager.deleteDatasource.successSingleNotificationTitle": "データソース「{id}」を削除しました",
"xpack.ingestManager.disabledSecurityDescription": "Elastic Fleet を使用するには、Kibana と Elasticsearch でセキュリティを有効にする必要があります。",
"xpack.ingestManager.disabledSecurityTitle": "セキュリティが有効ではありません",
"xpack.ingestManager.editConfig.cancelButtonLabel": "キャンセル",
"xpack.ingestManager.editConfig.errorNotificationTitle": "エージェント構成を作成できません",
"xpack.ingestManager.editConfig.flyoutTitle": "構成を編集",
"xpack.ingestManager.editConfig.submitButtonLabel": "更新",
"xpack.ingestManager.editConfig.successNotificationTitle": "エージェント構成「{name}」を更新しました",
"xpack.ingestManager.enrollmentApiKeyForm.namePlaceholder": "名前を選択",
"xpack.ingestManager.enrollmentApiKeyList.createNewButton": "新規キーを作成",
"xpack.ingestManager.enrollmentApiKeyList.useExistingsButton": "既存のキーを使用",

View file

@ -8320,9 +8320,6 @@
"xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "命名空间",
"xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "软件包",
"xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "流计数",
"xpack.ingestManager.configDetails.subTabs.datasouces": "数据源",
"xpack.ingestManager.configDetails.subTabs.settings": "设置",
"xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML 文件",
"xpack.ingestManager.configDetails.summary.datasources": "数据源",
"xpack.ingestManager.configDetails.summary.lastUpdated": "最后更新时间",
"xpack.ingestManager.configDetails.summary.revision": "修订",
@ -8332,10 +8329,6 @@
"xpack.ingestManager.configDetailsDatasources.createFirstButtonText": "创建数据源",
"xpack.ingestManager.configDetailsDatasources.createFirstMessage": "此配置尚未有任何数据源。",
"xpack.ingestManager.configDetailsDatasources.createFirstTitle": "创建您的首个数据源",
"xpack.ingestManager.configForm.descriptionFieldLabel": "描述",
"xpack.ingestManager.configForm.nameFieldLabel": "名称",
"xpack.ingestManager.configForm.nameRequiredErrorMessage": "配置名称必填",
"xpack.ingestManager.configForm.namespaceFieldLabel": "命名空间",
"xpack.ingestManager.createAgentConfig.cancelButtonLabel": "取消",
"xpack.ingestManager.createAgentConfig.errorNotificationTitle": "无法创建代理配置",
"xpack.ingestManager.createAgentConfig.flyoutTitle": "创建代理配置",
@ -8367,20 +8360,6 @@
"xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingPackagesTitle": "加载软件包时出错",
"xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingSelectedPackageTitle": "加载选定软件包时出错",
"xpack.ingestManager.createDatasource.stepSelectPackage.filterPackagesInputPlaceholder": "搜索软件包",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {# 个代理} other {# 个代理}}已分配{agentConfigsCount, plural, one {给此代理配置} other {给这些代理配置}}。将取消注册{agentsCount, plural, one {此代理} other {这些代理}}。",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.cancelButtonLabel": "取消",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmAndReassignButtonLabel": "删除{agentConfigsCount, plural, one {代理配置} other {代理配置}}并取消注册{agentsCount, plural, one {代理} other {代理}}",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmButtonLabel": "删除{agentConfigsCount, plural, one {代理配置} other {代理配置}}",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.deleteMultipleTitle": "删除{count, plural, one {此代理配置} other {# 个代理配置}}",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingAgentsCountMessage": "正在检查受影响代理数量……",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingButtonLabel": "正在加载……",
"xpack.ingestManager.deleteAgentConfigs.confirmModal.noAffectedAgentsMessage": "没有代理分配给{agentConfigsCount, plural, one {此代理配置} other {这些代理配置}}。",
"xpack.ingestManager.deleteAgentConfigs.failureMultipleNotificationTitle": "删除 {count} 个代理配置时出错",
"xpack.ingestManager.deleteAgentConfigs.failureSingleNotificationTitle": "删除代理配置“{id}”时出错",
"xpack.ingestManager.deleteAgentConfigs.fatalErrorNotificationTitle": "删除代理配置时出错",
"xpack.ingestManager.deleteAgentConfigs.successMultipleNotificationTitle": "已删除 {count} 个代理配置",
"xpack.ingestManager.deleteAgentConfigs.successSingleNotificationTitle": "已删除代理配置“{id}”",
"xpack.ingestManager.deleteApiKeys.confirmModal.cancelButtonLabel": "取消",
"xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsMessage": "Fleet 已检测到 {agentConfigName} 已由您的部分代理使用。",
"xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsTitle": "此操作将影响 {agentsCount} 个 {agentsCount, plural, one {代理} other {代理}}。",
"xpack.ingestManager.deleteDatasource.confirmModal.cancelButtonLabel": "取消",
@ -8396,11 +8375,6 @@
"xpack.ingestManager.deleteDatasource.successSingleNotificationTitle": "已删除数据源“{id}”",
"xpack.ingestManager.disabledSecurityDescription": "必须在 Kibana 和 Elasticsearch 启用安全性,才能使用 Elastic Fleet。",
"xpack.ingestManager.disabledSecurityTitle": "安全性未启用",
"xpack.ingestManager.editConfig.cancelButtonLabel": "取消",
"xpack.ingestManager.editConfig.errorNotificationTitle": "无法更新代理配置",
"xpack.ingestManager.editConfig.flyoutTitle": "编辑配置",
"xpack.ingestManager.editConfig.submitButtonLabel": "更新",
"xpack.ingestManager.editConfig.successNotificationTitle": "代理配置“{name}”已更新",
"xpack.ingestManager.enrollmentApiKeyForm.namePlaceholder": "选择名称",
"xpack.ingestManager.enrollmentApiKeyList.createNewButton": "创建新密钥",
"xpack.ingestManager.enrollmentApiKeyList.useExistingsButton": "使用现有密钥",