From d374d7a85f81ed894e419ad402eb0d4ffc745575 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 10 Nov 2021 12:46:17 -0500 Subject: [PATCH 1/5] [Fleet] Settings new design --- .../index.stories.tsx | 39 +++++ .../fleet_server_hosts_flyout/index.tsx | 106 ++++++++++++ .../use_fleet_server_host_form.tsx | 158 ++++++++++++++++++ .../components/hosts_input/index.stories.tsx | 45 +++++ .../index.test.tsx} | 2 +- .../hosts_input.tsx => hosts_input/index.tsx} | 16 +- .../components/legacy_settings_form/index.tsx | 144 +--------------- .../components/settings_page/index.tsx | 28 ++++ .../settings_section.stories.tsx | 38 +++++ .../settings_page/settings_section.tsx | 76 +++++++++ .../settings/hooks/use_confirm_modal.tsx | 98 +++++++++++ .../fleet/sections/settings/index.tsx | 46 ++++- .../fleet/public/constants/page_paths.ts | 10 +- .../plugins/fleet/public/hooks/use_input.ts | 2 +- 14 files changed, 658 insertions(+), 150 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.stories.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.stories.tsx rename x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/{legacy_settings_form/hosts_input.test.tsx => hosts_input/index.test.tsx} (99%) rename x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/{legacy_settings_form/hosts_input.tsx => hosts_input/index.tsx} (93%) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/settings_section.stories.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/settings_section.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/settings/hooks/use_confirm_modal.tsx 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..02f8abc711092 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.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 { 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 ( +
+ {}} /> +
+ ); +}; + +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..5f7c497369b23 --- /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,158 @@ +/* + * 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 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 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.confirmModalTitle', { + 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..b39fb8e1902ea --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.stories.tsx @@ -0,0 +1,45 @@ +/* + * 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 React from 'react'; + +import { HostsInput as Component } from '.'; + +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..306f81930eb29 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/hooks/use_confirm_modal.tsx @@ -0,0 +1,98 @@ +/* + * 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); + + if (context === null) { + throw new Error('Context need to be provided to use useConfirmModal'); + } + + const confirm = useCallback( + async (title: React.ReactNode, description: React.ReactNode) => { + 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, setIsVisble] = useState(false); + const [modal, setModal] = useState({ + onCancel: () => {}, + onConfirm: () => {}, + }); + + const showModal = useCallback(({ title, description, onConfirm, onCancel }) => { + setIsVisble(true); + setModal({ + title, + description, + onConfirm: () => { + setIsVisble(false); + onConfirm(); + }, + onCancel: () => { + setIsVisble(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); From cf5cd1ec638fe8a3618931ac1312e5630cc56878 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 11 Nov 2021 15:17:48 -0500 Subject: [PATCH 2/5] fix i18n --- .../fleet_server_hosts_flyout/use_fleet_server_host_form.tsx | 2 +- x-pack/plugins/translations/translations/ja-JP.json | 2 -- x-pack/plugins/translations/translations/zh-CN.json | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) 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 index 5f7c497369b23..f63b8bc58181a 100644 --- 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 @@ -118,7 +118,7 @@ export function useFleetServerHostsForm( i18n.translate('xpack.fleet.settings.fleetServerHostsFlyout.confirmModalTitle', { defaultMessage: 'Save and deploy changes?', }), - i18n.translate('xpack.fleet.settings.fleetServerHostsFlyout.confirmModalTitle', { + 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?', }) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f728ec13847f9..6a26e7dbfcea9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11059,8 +11059,6 @@ "xpack.fleet.settings.fleetServerHostsDifferentPathOrProtocolError": "各URLのプロトコルとパスは同じでなければなりません", "xpack.fleet.settings.fleetServerHostsEmptyError": "1つ以上のURLが必要です。", "xpack.fleet.settings.fleetServerHostsError": "無効なURL", - "xpack.fleet.settings.fleetServerHostsHelpTect": "エージェントがFleetサーバーに接続するために使用するURLを指定します。複数のURLが存在する場合、Fleetは登録目的で最初に指定されたURLを表示します。Fleetサーバーはデフォルトで8220番ポートを使用します。{link}を参照してください。", - "xpack.fleet.settings.fleetServerHostsLabel": "Fleetサーバーホスト", "xpack.fleet.settings.flyoutTitle": "Fleet 設定", "xpack.fleet.settings.globalOutputDescription": "これらの設定はグローバルにすべてのエージェントポリシーの{outputs}セクションに適用され、すべての登録されたエージェントに影響します。", "xpack.fleet.settings.invalidYamlFormatErrorMessage": "無効なYAML形式:{reason}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 33927d4ffbb0b..a3c7a1f54df5b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11172,8 +11172,6 @@ "xpack.fleet.settings.fleetServerHostsDifferentPathOrProtocolError": "对于每个 URL,协议和路径必须相同", "xpack.fleet.settings.fleetServerHostsEmptyError": "至少需要一个 URL", "xpack.fleet.settings.fleetServerHostsError": "URL 无效", - "xpack.fleet.settings.fleetServerHostsHelpTect": "指定代理用于连接 Fleet 服务器的 URL。如果多个 URL 存在,Fleet 显示提供的第一个 URL 用于注册。Fleet 服务器默认使用端口 8220。请参阅 {link}。", - "xpack.fleet.settings.fleetServerHostsLabel": "Fleet 服务器主机", "xpack.fleet.settings.flyoutTitle": "Fleet 设置", "xpack.fleet.settings.globalOutputDescription": "这些设置将全局应用到所有代理策略的 {outputs} 部分并影响所有注册的代理。", "xpack.fleet.settings.invalidYamlFormatErrorMessage": "YAML 无效:{reason}", From 02cd376582f6b4a8c8ae3df0cb4529f144b7a1fb Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 15 Nov 2021 08:40:56 -0500 Subject: [PATCH 3/5] Fix tests --- .../fleet_server_hosts_flyout/index.stories.tsx | 5 ++++- .../use_fleet_server_host_form.tsx | 16 ---------------- .../settings/hooks/use_confirm_modal.tsx | 7 +++---- 3 files changed, 7 insertions(+), 21 deletions(-) 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 index 02f8abc711092..6736b5a30d23e 100644 --- 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 @@ -31,7 +31,10 @@ const args: Args = { export const FleetServerHostsFlyout = ({ width }: Args) => { return (
- {}} /> + {}} + fleetServerHosts={['https://host1.fr:8220', 'https://host2-with-a-longer-name.fr:8220']} + />
); }; 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 index f63b8bc58181a..add9e8b287ab7 100644 --- 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 @@ -15,22 +15,6 @@ import { useConfirmModal } from '../../hooks/use_confirm_modal'; 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 validateFleetServerHosts(value: string[]) { if (value.length === 0) { 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 index 306f81930eb29..4ceeef475f0d7 100644 --- 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 @@ -23,12 +23,11 @@ const ModalContext = React.createContext { + if (context === null) { + throw new Error('Context need to be provided to use useConfirmModal'); + } return new Promise((resolve) => { context.showModal({ title, From ad2016bd65cf2b473413bb706a5b95d9d4c1f086 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 15 Nov 2021 10:53:33 -0500 Subject: [PATCH 4/5] Fix storybook rerendering --- .../components/hosts_input/index.stories.tsx | 7 ++ .../plugins/fleet/storybook/context/index.tsx | 70 ++++++++++--------- 2 files changed, 45 insertions(+), 32 deletions(-) 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 b39fb8e1902ea..64ac34c52d112 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 @@ -6,10 +6,17 @@ */ 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', 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 ( Date: Mon, 15 Nov 2021 10:59:45 -0500 Subject: [PATCH 5/5] corret typos --- .../use_fleet_server_host_form.tsx | 8 ++++---- .../fleet/sections/settings/hooks/use_confirm_modal.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) 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 index add9e8b287ab7..f4dda2b059542 100644 --- 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 @@ -76,7 +76,7 @@ export function useFleetServerHostsForm( fleetServerHostsDefaultValue: string[], onSuccess: () => void ) { - const [isLoading, setIsloading] = React.useState(false); + const [isLoading, setIsLoading] = React.useState(false); const { notifications } = useStartServices(); const { confirm } = useConfirmModal(); @@ -110,7 +110,7 @@ export function useFleetServerHostsForm( ) { return; } - setIsloading(true); + setIsLoading(true); const settingsResponse = await sendPutSettings({ fleet_server_hosts: fleetServerHostsInput.value, }); @@ -122,10 +122,10 @@ export function useFleetServerHostsForm( defaultMessage: 'Settings saved', }) ); - setIsloading(false); + setIsLoading(false); onSuccess(); } catch (error) { - setIsloading(false); + setIsLoading(false); notifications.toasts.addError(error, { title: i18n.translate('xpack.fleet.settings.fleetServerHostsFlyout.errorToastTitle', { defaultMessage: 'An error happened while saving settings', 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 index 4ceeef475f0d7..b8663f8cb2977 100644 --- 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 @@ -46,23 +46,23 @@ export function useConfirmModal() { } export const ConfirmModalProvider: React.FunctionComponent = ({ children }) => { - const [isVisible, setIsVisble] = useState(false); + const [isVisible, setIsVisible] = useState(false); const [modal, setModal] = useState({ onCancel: () => {}, onConfirm: () => {}, }); const showModal = useCallback(({ title, description, onConfirm, onCancel }) => { - setIsVisble(true); + setIsVisible(true); setModal({ title, description, onConfirm: () => { - setIsVisble(false); + setIsVisible(false); onConfirm(); }, onCancel: () => { - setIsVisble(false); + setIsVisible(false); onCancel(); }, });