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 (