diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.stories.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.stories.tsx
new file mode 100644
index 0000000000000..6736b5a30d23e
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.stories.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { addParameters } from '@storybook/react';
+import React from 'react';
+
+import { FleetServerHostsFlyout as Component } from '.';
+
+addParameters({
+ docs: {
+ inlineStories: false,
+ },
+});
+export default {
+ component: Component,
+ title: 'Sections/Fleet/Settings',
+};
+
+interface Args {
+ width: number;
+}
+
+const args: Args = {
+ width: 1200,
+};
+
+export const FleetServerHostsFlyout = ({ width }: Args) => {
+ return (
+
+ {}}
+ fleetServerHosts={['https://host1.fr:8220', 'https://host2-with-a-longer-name.fr:8220']}
+ />
+
+ );
+};
+
+FleetServerHostsFlyout.args = args;
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.tsx
new file mode 100644
index 0000000000000..07593ffe3e6c5
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.tsx
@@ -0,0 +1,106 @@
+/*
+ * 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 { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutHeader,
+ EuiTitle,
+ EuiText,
+ EuiLink,
+ EuiFlyoutFooter,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiButton,
+} from '@elastic/eui';
+
+import { HostsInput } from '../hosts_input';
+import { useStartServices } from '../../../../hooks';
+
+import { useFleetServerHostsForm } from './use_fleet_server_host_form';
+
+const FLYOUT_MAX_WIDTH = 800;
+
+export interface FleetServerHostsFlyoutProps {
+ onClose: () => void;
+ fleetServerHosts: string[];
+}
+
+export const FleetServerHostsFlyout: React.FunctionComponent = ({
+ onClose,
+ fleetServerHosts,
+}) => {
+ const { docLinks } = useStartServices();
+
+ const form = useFleetServerHostsForm(fleetServerHosts, onClose);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+
+ onClose()} flush="left">
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx
new file mode 100644
index 0000000000000..f4dda2b059542
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx
@@ -0,0 +1,142 @@
+/*
+ * 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, { useCallback } from 'react';
+
+import { i18n } from '@kbn/i18n';
+
+import { sendPutSettings, useComboInput, useStartServices } from '../../../../hooks';
+import { isDiffPathProtocol } from '../../../../../../../common';
+import { useConfirmModal } from '../../hooks/use_confirm_modal';
+
+const URL_REGEX = /^(https?):\/\/[^\s$.?#].[^\s]*$/gm;
+
+function validateFleetServerHosts(value: string[]) {
+ if (value.length === 0) {
+ return [
+ {
+ message: i18n.translate('xpack.fleet.settings.fleetServerHostsEmptyError', {
+ defaultMessage: 'At least one URL is required',
+ }),
+ },
+ ];
+ }
+
+ const res: Array<{ message: string; index: number }> = [];
+ const hostIndexes: { [key: string]: number[] } = {};
+ value.forEach((val, idx) => {
+ if (!val.match(URL_REGEX)) {
+ res.push({
+ message: i18n.translate('xpack.fleet.settings.fleetServerHostsError', {
+ defaultMessage: 'Invalid URL',
+ }),
+ index: idx,
+ });
+ }
+ const curIndexes = hostIndexes[val] || [];
+ hostIndexes[val] = [...curIndexes, idx];
+ });
+
+ Object.values(hostIndexes)
+ .filter(({ length }) => length > 1)
+ .forEach((indexes) => {
+ indexes.forEach((index) =>
+ res.push({
+ message: i18n.translate('xpack.fleet.settings.fleetServerHostsDuplicateError', {
+ defaultMessage: 'Duplicate URL',
+ }),
+ index,
+ })
+ );
+ });
+
+ if (res.length) {
+ return res;
+ }
+
+ if (value.length && isDiffPathProtocol(value)) {
+ return [
+ {
+ message: i18n.translate(
+ 'xpack.fleet.settings.fleetServerHostsDifferentPathOrProtocolError',
+ {
+ defaultMessage: 'Protocol and path must be the same for each URL',
+ }
+ ),
+ },
+ ];
+ }
+}
+
+export function useFleetServerHostsForm(
+ fleetServerHostsDefaultValue: string[],
+ onSuccess: () => void
+) {
+ const [isLoading, setIsLoading] = React.useState(false);
+ const { notifications } = useStartServices();
+ const { confirm } = useConfirmModal();
+
+ const fleetServerHostsInput = useComboInput(
+ 'fleetServerHostsInput',
+ fleetServerHostsDefaultValue,
+ validateFleetServerHosts
+ );
+
+ const fleetServerHostsInputValidate = fleetServerHostsInput.validate;
+ const validate = useCallback(
+ () => fleetServerHostsInputValidate(),
+ [fleetServerHostsInputValidate]
+ );
+
+ const submit = useCallback(async () => {
+ try {
+ if (!validate) {
+ return;
+ }
+ 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;
+ }
+ setIsLoading(true);
+ const settingsResponse = await sendPutSettings({
+ fleet_server_hosts: fleetServerHostsInput.value,
+ });
+ if (settingsResponse.error) {
+ throw settingsResponse.error;
+ }
+ notifications.toasts.addSuccess(
+ i18n.translate('xpack.fleet.settings.fleetServerHostsFlyout.successToastTitle', {
+ defaultMessage: 'Settings saved',
+ })
+ );
+ setIsLoading(false);
+ onSuccess();
+ } catch (error) {
+ setIsLoading(false);
+ notifications.toasts.addError(error, {
+ title: i18n.translate('xpack.fleet.settings.fleetServerHostsFlyout.errorToastTitle', {
+ defaultMessage: 'An error happened while saving settings',
+ }),
+ });
+ }
+ }, [fleetServerHostsInput.value, validate, notifications, confirm, onSuccess]);
+
+ return {
+ isLoading,
+ 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
new file mode 100644
index 0000000000000..64ac34c52d112
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.stories.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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 { useState } from '@storybook/addons';
+import { addParameters } from '@storybook/react';
+import React from 'react';
+
+import { HostsInput as Component } from '.';
+
+addParameters({
+ options: {
+ enableShortcuts: false,
+ },
+});
+
+export default {
+ component: Component,
+ title: 'Sections/Fleet/Settings',
+};
+
+interface Args {
+ width: number;
+ label: string;
+ helpText: string;
+}
+
+const args: Args = {
+ width: 250,
+ label: 'Demo label',
+ helpText: 'Demo helpText',
+};
+
+export const HostsInput = ({ width, label, helpText }: Args) => {
+ const [value, setValue] = useState([]);
+ return (
+
+
+
+ );
+};
+
+HostsInput.args = args;
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.test.tsx
similarity index 99%
rename from x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.test.tsx
rename to x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.test.tsx
index aca3399c4af46..4d556cd2749c6 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.test.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.test.tsx
@@ -10,7 +10,7 @@ import { fireEvent, act } from '@testing-library/react';
import { createFleetTestRendererMock } from '../../../../../../mock';
-import { HostsInput } from './hosts_input';
+import { HostsInput } from '.';
function renderInput(
value = ['http://host1.com'],
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.tsx
similarity index 93%
rename from x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.tsx
rename to x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.tsx
index 30ef969aceec7..6b169a207ea73 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.tsx
@@ -29,12 +29,12 @@ import { FormattedMessage } from '@kbn/i18n/react';
import type { EuiTheme } from '../../../../../../../../../../src/plugins/kibana_react/common';
-interface Props {
+export interface HostInputProps {
id: string;
value: string[];
onChange: (newValue: string[]) => void;
- label: string;
- helpText: ReactNode;
+ label?: string;
+ helpText?: ReactNode;
errors?: Array<{ message: string; index?: number }>;
isInvalid?: boolean;
}
@@ -105,11 +105,13 @@ const SortableTextField: FunctionComponent = React.memo(
{displayErrors(errors)}
@@ -130,7 +132,7 @@ const SortableTextField: FunctionComponent = React.memo(
}
);
-export const HostsInput: FunctionComponent = ({
+export const HostsInput: FunctionComponent = ({
id,
value: valueFromProps,
onChange,
@@ -231,10 +233,12 @@ export const HostsInput: FunctionComponent = ({
<>
{displayErrors(indexedErrors[idx])}
>
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
index 6caca7209e0d2..6c52475400bdc 100644
--- 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
@@ -18,7 +18,6 @@ import {
EuiForm,
EuiFormRow,
EuiCode,
- EuiLink,
EuiPanel,
EuiTextColor,
} from '@elastic/eui';
@@ -31,16 +30,15 @@ import {
useStartServices,
useGetSettings,
useInput,
- sendPutSettings,
useDefaultOutput,
sendPutOutput,
} from '../../../../../../hooks';
-import { isDiffPathProtocol, normalizeHostsForAgents } from '../../../../../../../common';
+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';
-import { HostsInput } from './hosts_input';
const CodeEditorContainer = styled.div`
min-height: 0;
@@ -84,63 +82,6 @@ function useSettingsForm(outputId: string | undefined) {
const [isLoading, setIsloading] = React.useState(false);
const { notifications } = useStartServices();
- const fleetServerHostsInput = useComboInput('fleetServerHostsComboBox', [], (value) => {
- if (value.length === 0) {
- return [
- {
- message: i18n.translate('xpack.fleet.settings.fleetServerHostsEmptyError', {
- defaultMessage: 'At least one URL is required',
- }),
- },
- ];
- }
-
- const res: Array<{ message: string; index: number }> = [];
- const hostIndexes: { [key: string]: number[] } = {};
- value.forEach((val, idx) => {
- if (!val.match(URL_REGEX)) {
- res.push({
- message: i18n.translate('xpack.fleet.settings.fleetServerHostsError', {
- defaultMessage: 'Invalid URL',
- }),
- index: idx,
- });
- }
- const curIndexes = hostIndexes[val] || [];
- hostIndexes[val] = [...curIndexes, idx];
- });
-
- Object.values(hostIndexes)
- .filter(({ length }) => length > 1)
- .forEach((indexes) => {
- indexes.forEach((index) =>
- res.push({
- message: i18n.translate('xpack.fleet.settings.fleetServerHostsDuplicateError', {
- defaultMessage: 'Duplicate URL',
- }),
- index,
- })
- );
- });
-
- if (res.length) {
- return res;
- }
-
- if (value.length && isDiffPathProtocol(value)) {
- return [
- {
- message: i18n.translate(
- 'xpack.fleet.settings.fleetServerHostsDifferentPathOrProtocolError',
- {
- defaultMessage: 'Protocol and path must be the same for each URL',
- }
- ),
- },
- ];
- }
- });
-
const elasticsearchUrlInput = useComboInput('esHostsComboxBox', [], (value) => {
const res: Array<{ message: string; index: number }> = [];
const urlIndexes: { [key: string]: number[] } = {};
@@ -190,16 +131,15 @@ function useSettingsForm(outputId: string | undefined) {
});
const validate = useCallback(() => {
- const fleetServerHostsValid = fleetServerHostsInput.validate();
const elasticsearchUrlsValid = elasticsearchUrlInput.validate();
const additionalYamlConfigValid = additionalYamlConfigInput.validate();
- if (!fleetServerHostsValid || !elasticsearchUrlsValid || !additionalYamlConfigValid) {
+ if (!elasticsearchUrlsValid || !additionalYamlConfigValid) {
return false;
}
return true;
- }, [fleetServerHostsInput, elasticsearchUrlInput, additionalYamlConfigInput]);
+ }, [elasticsearchUrlInput, additionalYamlConfigInput]);
return {
isLoading,
@@ -217,15 +157,10 @@ function useSettingsForm(outputId: string | undefined) {
if (outputResponse.error) {
throw outputResponse.error;
}
- const settingsResponse = await sendPutSettings({
- fleet_server_hosts: fleetServerHostsInput.value,
- });
- if (settingsResponse.error) {
- throw settingsResponse.error;
- }
+
notifications.toasts.addSuccess(
i18n.translate('xpack.fleet.settings.success.message', {
- defaultMessage: 'Settings saved',
+ defaultMessage: 'Output saved',
})
);
setIsloading(false);
@@ -237,7 +172,6 @@ function useSettingsForm(outputId: string | undefined) {
}
},
inputs: {
- fleetServerHosts: fleetServerHostsInput,
elasticsearchUrl: elasticsearchUrlInput,
additionalYamlConfig: additionalYamlConfigInput,
},
@@ -245,8 +179,6 @@ function useSettingsForm(outputId: string | undefined) {
}
export const LegacySettingsForm: React.FunctionComponent = () => {
- const { docLinks } = useStartServices();
-
const settingsRequest = useGetSettings();
const settings = settingsRequest?.data?.item;
const { output } = useDefaultOutput();
@@ -277,22 +209,11 @@ export const LegacySettingsForm: React.FunctionComponent = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [output]);
- useEffect(() => {
- if (settings) {
- inputs.fleetServerHosts.setValue([...settings.fleet_server_hosts]);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [settings]);
-
const isUpdated = React.useMemo(() => {
if (!settings || !output) {
return false;
}
return (
- !isSameArrayValueWithNormalizedHosts(
- settings.fleet_server_hosts,
- inputs.fleetServerHosts.value
- ) ||
!isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value) ||
(output.config_yaml || '') !== inputs.additionalYamlConfig.value
);
@@ -319,26 +240,6 @@ export const LegacySettingsForm: React.FunctionComponent = () => {
);
}
- if (
- !isSameArrayValueWithNormalizedHosts(
- settings.fleet_server_hosts,
- inputs.fleetServerHosts.value
- )
- ) {
- tmpChanges.push(
- {
- type: 'fleet_server',
- direction: 'removed',
- urls: normalizeHosts(settings.fleet_server_hosts || []),
- },
- {
- type: 'fleet_server',
- direction: 'added',
- urls: normalizeHosts(inputs.fleetServerHosts.value),
- }
- );
- }
-
return tmpChanges;
}, [settings, inputs, output, isConfirmModalVisible]);
@@ -354,35 +255,6 @@ export const LegacySettingsForm: React.FunctionComponent = () => {
/>
-
-
-
-
- ),
- }}
- />
- }
- />
-
-
{
)}
<>
<>
-
+
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx
new file mode 100644
index 0000000000000..66a95a7952c35
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx
@@ -0,0 +1,28 @@
+/*
+ * 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 { EuiSpacer } from '@elastic/eui';
+
+import type { Settings } from '../../../../types';
+import { LegacySettingsForm } from '../legacy_settings_form';
+
+import { SettingsSection } from './settings_section';
+
+export interface SettingsPageProps {
+ settings: Settings;
+}
+
+export const SettingsPage: React.FunctionComponent = ({ settings }) => {
+ return (
+ <>
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/settings_section.stories.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/settings_section.stories.tsx
new file mode 100644
index 0000000000000..8133d5959c126
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/settings_section.stories.tsx
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { SettingsSection as Component } from './settings_section';
+
+export default {
+ component: Component,
+ title: 'Sections/Fleet/Settings',
+};
+
+interface Args {
+ width: number;
+ fleetServerHosts: string[];
+}
+
+const args: Args = {
+ width: 1200,
+ fleetServerHosts: [
+ 'https://myfleetserver:8220',
+ 'https://alongerfleetserverwithaverylongname:8220',
+ ],
+};
+
+export const SettingsSection = ({ width, fleetServerHosts }: Args) => {
+ return (
+
+
+
+ );
+};
+
+SettingsSection.args = args;
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/settings_section.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/settings_section.tsx
new file mode 100644
index 0000000000000..9aef2bb3f6380
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/settings_section.tsx
@@ -0,0 +1,76 @@
+/*
+ * 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 { EuiTitle, EuiLink, EuiText, EuiSpacer, EuiBasicTable, EuiButtonEmpty } from '@elastic/eui';
+import type { EuiBasicTableColumn } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+
+import { useLink, useStartServices } from '../../../../hooks';
+
+export interface SettingsSectionProps {
+ fleetServerHosts: string[];
+}
+
+export const SettingsSection: React.FunctionComponent = ({
+ fleetServerHosts,
+}) => {
+ const { docLinks } = useStartServices();
+ const { getHref } = useLink();
+
+ const columns = useMemo((): Array> => {
+ return [
+ {
+ render: (host: string) => host,
+ name: i18n.translate('xpack.fleet.settings.fleetServerHostUrlColumnTitle', {
+ defaultMessage: 'Host URL',
+ }),
+ },
+ ];
+ }, []);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/hooks/use_confirm_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/hooks/use_confirm_modal.tsx
new file mode 100644
index 0000000000000..b8663f8cb2977
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/hooks/use_confirm_modal.tsx
@@ -0,0 +1,97 @@
+/*
+ * 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 { EuiConfirmModal, EuiPortal } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React, { useCallback, useContext, useState } from 'react';
+
+interface ModalState {
+ title?: React.ReactNode;
+ description?: React.ReactNode;
+ onConfirm: () => void;
+ onCancel: () => void;
+}
+
+const ModalContext = React.createContext void;
+}>(null);
+
+export function useConfirmModal() {
+ const context = useContext(ModalContext);
+
+ const confirm = useCallback(
+ async (title: React.ReactNode, description: React.ReactNode) => {
+ if (context === null) {
+ throw new Error('Context need to be provided to use useConfirmModal');
+ }
+ return new Promise((resolve) => {
+ context.showModal({
+ title,
+ description,
+ onConfirm: () => resolve(true),
+ onCancel: () => resolve(false),
+ });
+ });
+ },
+ [context]
+ );
+
+ return {
+ confirm,
+ };
+}
+
+export const ConfirmModalProvider: React.FunctionComponent = ({ children }) => {
+ const [isVisible, setIsVisible] = useState(false);
+ const [modal, setModal] = useState({
+ onCancel: () => {},
+ onConfirm: () => {},
+ });
+
+ const showModal = useCallback(({ title, description, onConfirm, onCancel }) => {
+ setIsVisible(true);
+ setModal({
+ title,
+ description,
+ onConfirm: () => {
+ setIsVisible(false);
+ onConfirm();
+ },
+ onCancel: () => {
+ setIsVisible(false);
+ onCancel();
+ },
+ });
+ }, []);
+
+ return (
+
+ {isVisible && (
+
+
+ {modal.description}
+
+
+ )}
+ {children}
+
+ );
+};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx
index 6117d3249b189..212b2d9191e24 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx
@@ -5,19 +5,57 @@
* 2.0.
*/
-import React from 'react';
+import React, { useCallback } from 'react';
+import { EuiPortal } from '@elastic/eui';
+import { Router, Route, Switch, useHistory } from 'react-router-dom';
-import { useBreadcrumbs } from '../../hooks';
+import { useBreadcrumbs, useGetSettings } from '../../hooks';
+import { FLEET_ROUTING_PATHS, pagePathGetters } from '../../constants';
import { DefaultLayout } from '../../layouts';
+import { Loading } from '../../components';
-import { LegacySettingsForm } from './components/legacy_settings_form';
+import { SettingsPage } from './components/settings_page';
+import { ConfirmModalProvider } from './hooks/use_confirm_modal';
+import { FleetServerHostsFlyout } from './components/fleet_server_hosts_flyout';
export const SettingsApp = () => {
useBreadcrumbs('settings');
+ const history = useHistory();
+
+ const settings = useGetSettings();
+
+ const resendSettingsRequest = settings.resendRequest;
+
+ const onCloseCallback = useCallback(() => {
+ resendSettingsRequest();
+ history.replace(pagePathGetters.settings()[1]);
+ }, [history, resendSettingsRequest]);
+
+ if (settings.isLoading || !settings.data?.item) {
+ return (
+
+
+
+ );
+ }
return (
-
+
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/fleet/public/constants/page_paths.ts b/x-pack/plugins/fleet/public/constants/page_paths.ts
index 821c115cb1cac..39b6e4c6da075 100644
--- a/x-pack/plugins/fleet/public/constants/page_paths.ts
+++ b/x-pack/plugins/fleet/public/constants/page_paths.ts
@@ -15,7 +15,8 @@ export type StaticPage =
| 'policies_list'
| 'enrollment_tokens'
| 'data_streams'
- | 'settings';
+ | 'settings'
+ | 'settings_edit_fleet_server_hosts';
export type DynamicPage =
| 'integrations_all'
@@ -59,6 +60,7 @@ export const FLEET_ROUTING_PATHS = {
enrollment_tokens: '/enrollment-tokens',
data_streams: '/data-streams',
settings: '/settings',
+ settings_edit_fleet_server_hosts: '/settings/edit-fleet-server-hosts',
// TODO: Move this to the integrations app
add_integration_to_policy: '/integrations/:pkgkey/add-integration/:integration?',
@@ -147,5 +149,9 @@ export const pagePathGetters: {
agent_details_logs: ({ agentId }) => [FLEET_BASE_PATH, `/agents/${agentId}/logs`],
enrollment_tokens: () => [FLEET_BASE_PATH, '/enrollment-tokens'],
data_streams: () => [FLEET_BASE_PATH, '/data-streams'],
- settings: () => [FLEET_BASE_PATH, '/settings'],
+ settings: () => [FLEET_BASE_PATH, FLEET_ROUTING_PATHS.settings],
+ settings_edit_fleet_server_hosts: () => [
+ FLEET_BASE_PATH,
+ FLEET_ROUTING_PATHS.settings_edit_fleet_server_hosts,
+ ],
};
diff --git a/x-pack/plugins/fleet/public/hooks/use_input.ts b/x-pack/plugins/fleet/public/hooks/use_input.ts
index e4a517dbae9c8..908c3f4f717ca 100644
--- a/x-pack/plugins/fleet/public/hooks/use_input.ts
+++ b/x-pack/plugins/fleet/public/hooks/use_input.ts
@@ -52,7 +52,7 @@ export function useInput(defaultValue = '', validate?: (value: string) => string
export function useComboInput(
id: string,
- defaultValue = [],
+ defaultValue: string[] = [],
validate?: (value: string[]) => Array<{ message: string; index?: number }> | undefined
) {
const [value, setValue] = useState(defaultValue);
diff --git a/x-pack/plugins/fleet/storybook/context/index.tsx b/x-pack/plugins/fleet/storybook/context/index.tsx
index e6c0726e755c2..76425540c2fbc 100644
--- a/x-pack/plugins/fleet/storybook/context/index.tsx
+++ b/x-pack/plugins/fleet/storybook/context/index.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React from 'react';
+import React, { useEffect, useMemo, useCallback } from 'react';
import type { StoryContext } from '@storybook/react';
import { createBrowserHistory } from 'history';
@@ -45,37 +45,43 @@ export const StorybookContext: React.FC<{ storyContext?: StoryContext }> = ({
const browserHistory = createBrowserHistory();
const history = new ScopedHistory(browserHistory, basepath);
- const startServices: FleetStartServices = {
- ...stubbedStartServices,
- application: getApplication(),
- chrome: getChrome(),
- cloud: getCloud({ isCloudEnabled: storyContext?.args.isCloudEnabled }),
- customIntegrations: {
- ContextProvider: getStorybookContextProvider(),
- },
- docLinks: getDocLinks(),
- http: getHttp(),
- i18n: {
- Context: function I18nContext({ children }) {
- return {children};
- },
- },
- injectedMetadata: {
- getInjectedVar: () => null,
- },
- notifications: getNotifications(),
- share: getShare(),
- uiSettings: getUiSettings(),
- };
+ const isCloudEnabled = storyContext?.args.isCloudEnabled;
- setHttpClient(startServices.http);
- setCustomIntegrations({
- getAppendCustomIntegrations: async () => [],
- getReplacementCustomIntegrations: async () => {
- const { integrations } = await import('./fixtures/replacement_integrations');
- return integrations;
- },
- });
+ const startServices: FleetStartServices = useMemo(
+ () => ({
+ ...stubbedStartServices,
+ application: getApplication(),
+ chrome: getChrome(),
+ cloud: getCloud({ isCloudEnabled }),
+ customIntegrations: {
+ ContextProvider: getStorybookContextProvider(),
+ },
+ docLinks: getDocLinks(),
+ http: getHttp(),
+ i18n: {
+ Context: function I18nContext({ children }) {
+ return {children};
+ },
+ },
+ injectedMetadata: {
+ getInjectedVar: () => null,
+ },
+ notifications: getNotifications(),
+ share: getShare(),
+ uiSettings: getUiSettings(),
+ }),
+ [isCloudEnabled]
+ );
+ useEffect(() => {
+ setHttpClient(startServices.http);
+ setCustomIntegrations({
+ getAppendCustomIntegrations: async () => [],
+ getReplacementCustomIntegrations: async () => {
+ const { integrations } = await import('./fixtures/replacement_integrations');
+ return integrations;
+ },
+ });
+ }, [startServices]);
const config = {
enabled: true,
@@ -87,7 +93,7 @@ export const StorybookContext: React.FC<{ storyContext?: StoryContext }> = ({
const extensions = {};
const kibanaVersion = '1.2.3';
- const setHeaderActionMenu = () => {};
+ const setHeaderActionMenu = useCallback(() => {}, []);
return (