+
+
+ ),
+ policies: (
+
+
+
+ ),
+ }}
+ />
+);
+
function validateFleetServerHosts(value: string[]) {
if (value.length === 0) {
return [
@@ -76,7 +124,7 @@ export function useFleetServerHostsForm(
fleetServerHostsDefaultValue: string[],
onSuccess: () => void
) {
- const [isLoading, setIsLoading] = React.useState(false);
+ const [isLoading, setIsLoading] = useState(false);
const { notifications } = useStartServices();
const { confirm } = useConfirmModal();
@@ -97,15 +145,11 @@ export function useFleetServerHostsForm(
if (!validate) {
return;
}
+ const { agentCount, agentPolicyCount } = await getAgentAndPolicyCount();
if (
!(await confirm(
- i18n.translate('xpack.fleet.settings.fleetServerHostsFlyout.confirmModalTitle', {
- defaultMessage: 'Save and deploy changes?',
- }),
- i18n.translate('xpack.fleet.settings.fleetServerHostsFlyout.confirmModalDescription', {
- defaultMessage:
- 'This action will update all of your agent policies and all of your agents. Are you sure you wish to continue?',
- })
+ ,
+
))
) {
return;
@@ -136,6 +180,7 @@ export function useFleetServerHostsForm(
return {
isLoading,
+ isDisabled: isLoading || !fleetServerHostsInput.hasChanged,
submit,
fleetServerHostsInput,
};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.stories.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.stories.tsx
index 64ac34c52d112..9b4674f3ce778 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.stories.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.stories.tsx
@@ -19,22 +19,24 @@ addParameters({
export default {
component: Component,
- title: 'Sections/Fleet/Settings',
+ title: 'Sections/Fleet/Settings/HostInput',
};
interface Args {
width: number;
label: string;
helpText: string;
+ disabled: boolean;
}
const args: Args = {
width: 250,
label: 'Demo label',
helpText: 'Demo helpText',
+ disabled: false,
};
-export const HostsInput = ({ width, label, helpText }: Args) => {
+export const HostsInput = ({ width, label, helpText, disabled }: Args) => {
const [value, setValue] = useState([]);
return (
@@ -44,9 +46,31 @@ export const HostsInput = ({ width, label, helpText }: Args) => {
value={value}
onChange={setValue}
label={label}
+ disabled={disabled}
/>
);
};
-
HostsInput.args = args;
+
+export const HostsInputDisabled = ({ value }: { value: string[] }) => {
+ return (
+
+ {}}
+ label={'Host input label'}
+ disabled={true}
+ />
+
+ );
+};
+
+HostsInputDisabled.args = { value: ['http://test1.fr', 'http://test2.fr'] };
+HostsInputDisabled.argTypes = {
+ value: {
+ control: { type: 'object' },
+ },
+};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.tsx
index 6b169a207ea73..e50024a2aabae 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.tsx
@@ -37,6 +37,7 @@ export interface HostInputProps {
helpText?: ReactNode;
errors?: Array<{ message: string; index?: number }>;
isInvalid?: boolean;
+ disabled?: boolean;
}
interface SortableTextFieldProps {
@@ -47,6 +48,7 @@ interface SortableTextFieldProps {
onDelete: (index: number) => void;
errors?: string[];
autoFocus?: boolean;
+ disabled?: boolean;
}
const DraggableDiv = sytled.div`
@@ -62,7 +64,7 @@ function displayErrors(errors?: string[]) {
}
const SortableTextField: FunctionComponent = React.memo(
- ({ id, index, value, onChange, onDelete, autoFocus, errors }) => {
+ ({ id, index, value, onChange, onDelete, autoFocus, errors, disabled }) => {
const onDeleteHandler = useCallback(() => {
onDelete(index);
}, [onDelete, index]);
@@ -75,6 +77,7 @@ const SortableTextField: FunctionComponent = React.memo(
spacing="m"
index={index}
draggableId={id}
+ isDragDisabled={disabled}
customDragHandle={true}
style={{
paddingLeft: 0,
@@ -109,6 +112,7 @@ const SortableTextField: FunctionComponent = React.memo(
onChange={onChange}
autoFocus={autoFocus}
isInvalid={isInvalid}
+ disabled={disabled}
placeholder={i18n.translate('xpack.fleet.hostsInput.placeholder', {
defaultMessage: 'Specify host URL',
})}
@@ -120,6 +124,7 @@ const SortableTextField: FunctionComponent = React.memo(
color="text"
onClick={onDeleteHandler}
iconType="cross"
+ disabled={disabled}
aria-label={i18n.translate('xpack.fleet.settings.deleteHostButton', {
defaultMessage: 'Delete host',
})}
@@ -140,6 +145,7 @@ export const HostsInput: FunctionComponent = ({
label,
isInvalid,
errors,
+ disabled,
}) => {
const [autoFocus, setAutoFocus] = useState(false);
const value = useMemo(() => {
@@ -214,7 +220,7 @@ export const HostsInput: FunctionComponent = ({
<>
{helpText}
-
+ {helpText && }
{rows.map((row, idx) => (
@@ -228,6 +234,7 @@ export const HostsInput: FunctionComponent = ({
value={row.value}
autoFocus={autoFocus}
errors={indexedErrors[idx]}
+ disabled={disabled}
/>
) : (
<>
@@ -239,6 +246,7 @@ export const HostsInput: FunctionComponent = ({
placeholder={i18n.translate('xpack.fleet.hostsInput.placeholder', {
defaultMessage: 'Specify host URL',
})}
+ disabled={disabled}
/>
{displayErrors(indexedErrors[idx])}
>
@@ -249,7 +257,13 @@ export const HostsInput: FunctionComponent = ({
{displayErrors(globalErrors)}
-
+
>
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/confirm_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/confirm_modal.tsx
deleted file mode 100644
index c4de3fe513214..0000000000000
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/confirm_modal.tsx
+++ /dev/null
@@ -1,188 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import {
- EuiModal,
- EuiModalHeader,
- EuiModalHeaderTitle,
- EuiModalFooter,
- EuiModalBody,
- EuiCallOut,
- EuiButton,
- EuiButtonEmpty,
- EuiBasicTable,
- EuiText,
- EuiSpacer,
-} from '@elastic/eui';
-import type { EuiBasicTableProps } from '@elastic/eui';
-
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-export interface SettingsConfirmModalProps {
- changes: Array<{
- type: 'elasticsearch' | 'fleet_server';
- direction: 'removed' | 'added';
- urls: string[];
- }>;
- onConfirm: () => void;
- onClose: () => void;
-}
-
-type Change = SettingsConfirmModalProps['changes'][0];
-
-const TABLE_COLUMNS: EuiBasicTableProps['columns'] = [
- {
- name: i18n.translate('xpack.fleet.settingsConfirmModal.fieldLabel', {
- defaultMessage: 'Field',
- }),
- field: 'label',
- render: (_, item) => getLabel(item),
- width: '180px',
- },
- {
- field: 'urls',
- name: i18n.translate('xpack.fleet.settingsConfirmModal.valueLabel', {
- defaultMessage: 'Value',
- }),
- render: (_, item) => {
- return (
-
- {item.urls.map((url) => (
- {url}
- ))}
-
- );
- },
- },
-];
-
-function getLabel(change: Change) {
- if (change.type === 'elasticsearch' && change.direction === 'removed') {
- return i18n.translate('xpack.fleet.settingsConfirmModal.elasticsearchRemovedLabel', {
- defaultMessage: 'Elasticsearch hosts (old)',
- });
- }
-
- if (change.type === 'elasticsearch' && change.direction === 'added') {
- return i18n.translate('xpack.fleet.settingsConfirmModal.elasticsearchAddedLabel', {
- defaultMessage: 'Elasticsearch hosts (new)',
- });
- }
-
- if (change.type === 'fleet_server' && change.direction === 'removed') {
- return i18n.translate('xpack.fleet.settingsConfirmModal.fleetServerRemovedLabel', {
- defaultMessage: 'Fleet Server hosts (old)',
- });
- }
-
- if (change.type === 'fleet_server' && change.direction === 'added') {
- return i18n.translate('xpack.fleet.settingsConfirmModal.fleetServerAddedLabel', {
- defaultMessage: 'Fleet Server hosts (new)',
- });
- }
-
- return i18n.translate('xpack.fleet.settingsConfirmModal.defaultChangeLabel', {
- defaultMessage: 'Unknown setting',
- });
-}
-
-export const SettingsConfirmModal = React.memo(
- ({ changes, onConfirm, onClose }) => {
- const hasESChanges = changes.some((change) => change.type === 'elasticsearch');
- const hasFleetServerChanges = changes.some((change) => change.type === 'fleet_server');
-
- return (
-
-
-
-
-
-
-
-
-
- }
- color="warning"
- iconType="alert"
- >
-
- {hasFleetServerChanges && (
-
-
-
-
- ),
- }}
- />
-
- )}
-
- {hasESChanges && (
-
-
-
-
- ),
- }}
- />
-
- )}
-
-
-
- {changes.length > 0 && (
- <>
-
-
- >
- )}
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-);
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx
deleted file mode 100644
index 6c52475400bdc..0000000000000
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx
+++ /dev/null
@@ -1,370 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useEffect, useCallback } from 'react';
-import styled from 'styled-components';
-import { i18n } from '@kbn/i18n';
-import {
- EuiPortal,
- EuiTitle,
- EuiFlexGroup,
- EuiFlexItem,
- EuiSpacer,
- EuiButton,
- EuiForm,
- EuiFormRow,
- EuiCode,
- EuiPanel,
- EuiTextColor,
-} from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiText } from '@elastic/eui';
-import { safeLoad } from 'js-yaml';
-
-import {
- useComboInput,
- useStartServices,
- useGetSettings,
- useInput,
- useDefaultOutput,
- sendPutOutput,
-} from '../../../../../../hooks';
-import { normalizeHostsForAgents } from '../../../../../../../common';
-import { CodeEditor } from '../../../../../../../../../../src/plugins/kibana_react/public';
-import { HostsInput } from '../hosts_input';
-
-import { SettingsConfirmModal } from './confirm_modal';
-import type { SettingsConfirmModalProps } from './confirm_modal';
-
-const CodeEditorContainer = styled.div`
- min-height: 0;
- position: relative;
- height: 250px;
-`;
-
-const CodeEditorPlaceholder = styled(EuiTextColor).attrs((props) => ({
- color: 'subdued',
- size: 'xs',
-}))`
- position: absolute;
- top: 0;
- left: 0;
- // Matches monaco editor
- font-family: Menlo, Monaco, 'Courier New', monospace;
- font-size: 12px;
- line-height: 21px;
- pointer-events: none;
-`;
-
-const URL_REGEX = /^(https?):\/\/[^\s$.?#].[^\s]*$/gm;
-
-function normalizeHosts(hostsInput: string[]) {
- return hostsInput.map((host) => {
- try {
- return normalizeHostsForAgents(host);
- } catch (err) {
- return host;
- }
- });
-}
-
-function isSameArrayValueWithNormalizedHosts(arrayA: string[] = [], arrayB: string[] = []) {
- const hostsA = normalizeHosts(arrayA);
- const hostsB = normalizeHosts(arrayB);
- return hostsA.length === hostsB.length && hostsA.every((val, index) => val === hostsB[index]);
-}
-
-function useSettingsForm(outputId: string | undefined) {
- const [isLoading, setIsloading] = React.useState(false);
- const { notifications } = useStartServices();
-
- const elasticsearchUrlInput = useComboInput('esHostsComboxBox', [], (value) => {
- const res: Array<{ message: string; index: number }> = [];
- const urlIndexes: { [key: string]: number[] } = {};
- value.forEach((val, idx) => {
- if (!val.match(URL_REGEX)) {
- res.push({
- message: i18n.translate('xpack.fleet.settings.elasticHostError', {
- defaultMessage: 'Invalid URL',
- }),
- index: idx,
- });
- }
- const curIndexes = urlIndexes[val] || [];
- urlIndexes[val] = [...curIndexes, idx];
- });
-
- Object.values(urlIndexes)
- .filter(({ length }) => length > 1)
- .forEach((indexes) => {
- indexes.forEach((index) =>
- res.push({
- message: i18n.translate('xpack.fleet.settings.elasticHostDuplicateError', {
- defaultMessage: 'Duplicate URL',
- }),
- index,
- })
- );
- });
-
- if (res.length) {
- return res;
- }
- });
-
- const additionalYamlConfigInput = useInput('', (value) => {
- try {
- safeLoad(value);
- return;
- } catch (error) {
- return [
- i18n.translate('xpack.fleet.settings.invalidYamlFormatErrorMessage', {
- defaultMessage: 'Invalid YAML: {reason}',
- values: { reason: error.message },
- }),
- ];
- }
- });
-
- const validate = useCallback(() => {
- const elasticsearchUrlsValid = elasticsearchUrlInput.validate();
- const additionalYamlConfigValid = additionalYamlConfigInput.validate();
-
- if (!elasticsearchUrlsValid || !additionalYamlConfigValid) {
- return false;
- }
-
- return true;
- }, [elasticsearchUrlInput, additionalYamlConfigInput]);
-
- return {
- isLoading,
- validate,
- submit: async () => {
- try {
- setIsloading(true);
- if (!outputId) {
- throw new Error('Unable to load outputs');
- }
- const outputResponse = await sendPutOutput(outputId, {
- hosts: elasticsearchUrlInput.value,
- config_yaml: additionalYamlConfigInput.value,
- });
- if (outputResponse.error) {
- throw outputResponse.error;
- }
-
- notifications.toasts.addSuccess(
- i18n.translate('xpack.fleet.settings.success.message', {
- defaultMessage: 'Output saved',
- })
- );
- setIsloading(false);
- } catch (error) {
- setIsloading(false);
- notifications.toasts.addError(error, {
- title: 'Error',
- });
- }
- },
- inputs: {
- elasticsearchUrl: elasticsearchUrlInput,
- additionalYamlConfig: additionalYamlConfigInput,
- },
- };
-}
-
-export const LegacySettingsForm: React.FunctionComponent = () => {
- const settingsRequest = useGetSettings();
- const settings = settingsRequest?.data?.item;
- const { output } = useDefaultOutput();
- const { inputs, submit, validate, isLoading } = useSettingsForm(output?.id);
-
- const [isConfirmModalVisible, setConfirmModalVisible] = React.useState(false);
-
- const onSubmit = useCallback(() => {
- if (validate()) {
- setConfirmModalVisible(true);
- }
- }, [validate, setConfirmModalVisible]);
-
- const onConfirm = useCallback(() => {
- setConfirmModalVisible(false);
- submit();
- }, [submit]);
-
- const onConfirmModalClose = useCallback(() => {
- setConfirmModalVisible(false);
- }, [setConfirmModalVisible]);
-
- useEffect(() => {
- if (output) {
- inputs.elasticsearchUrl.setValue(output.hosts || []);
- inputs.additionalYamlConfig.setValue(output.config_yaml || '');
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [output]);
-
- const isUpdated = React.useMemo(() => {
- if (!settings || !output) {
- return false;
- }
- return (
- !isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value) ||
- (output.config_yaml || '') !== inputs.additionalYamlConfig.value
- );
- }, [settings, inputs, output]);
-
- const changes = React.useMemo(() => {
- if (!settings || !output || !isConfirmModalVisible) {
- return [];
- }
-
- const tmpChanges: SettingsConfirmModalProps['changes'] = [];
- if (!isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value)) {
- tmpChanges.push(
- {
- type: 'elasticsearch',
- direction: 'removed',
- urls: normalizeHosts(output.hosts || []),
- },
- {
- type: 'elasticsearch',
- direction: 'added',
- urls: normalizeHosts(inputs.elasticsearchUrl.value),
- }
- );
- }
-
- return tmpChanges;
- }, [settings, inputs, output, isConfirmModalVisible]);
-
- const body = settings && (
-
-
- outputs,
- }}
- />
-
-
-
-
-
-
-
-
-
-
- {(!inputs.additionalYamlConfig.value || inputs.additionalYamlConfig.value === '') && (
-
- {`# YAML settings here will be added to the Elasticsearch output section of each policy`}
-
- )}
-
-
-
-
- );
-
- return (
- <>
- {isConfirmModalVisible && (
-
-
-
- )}
- <>
- <>
-
-
-
-
-
- >
- <>{body}>
- <>
-
-
-
-
- {isLoading ? (
-
- ) : (
-
- )}
-
-
-
- >
- >
- >
- );
-};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/badges.stories.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/badges.stories.tsx
new file mode 100644
index 0000000000000..fe29bd8d7192b
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/badges.stories.tsx
@@ -0,0 +1,39 @@
+/*
+ * 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 {
+ DefaultMonitoringOutputBadge as DefaultMonitoringOutputBadgeComponent,
+ DefaultOutputBadge as DefaultOutputBadgeComponent,
+ DefaultBadges as Component,
+} from './badges';
+
+export default {
+ component: Component,
+ subcomponents: {
+ DefaultMonitoringOutputBadge: DefaultMonitoringOutputBadgeComponent,
+ DefaultOutputBadge: DefaultOutputBadgeComponent,
+ },
+ title: 'Sections/Fleet/Settings/OutputsTable/DefaultBadges',
+};
+
+export const DefaultBadges = () => {
+ return (
+
+
+
+ );
+};
+
+export const DefaultMonitoringOutputBadge = () => {
+ return ;
+};
+
+export const DefaultOutputBadge = () => {
+ return ;
+};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/badges.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/badges.tsx
new file mode 100644
index 0000000000000..d9d2e87f562e7
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/badges.tsx
@@ -0,0 +1,47 @@
+/*
+ * 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, { useMemo } from 'react';
+import { EuiBadge, EuiBadgeGroup } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import type { Output } from '../../../../types';
+
+export const DefaultBadges = React.memo<{
+ output: Pick