Skip to content

Commit

Permalink
[Fleet] Implement Settings new design for fleet server hosts (elastic…
Browse files Browse the repository at this point in the history
  • Loading branch information
nchaulet authored and mbondyra committed Nov 16, 2021
1 parent 0161868 commit 00df969
Show file tree
Hide file tree
Showing 17 changed files with 689 additions and 186 deletions.
Original file line number Diff line number Diff line change
@@ -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 (
<div style={{ width }}>
<Component
onClose={() => {}}
fleetServerHosts={['https://host1.fr:8220', 'https://host2-with-a-longer-name.fr:8220']}
/>
</div>
);
};

FleetServerHostsFlyout.args = args;
Original file line number Diff line number Diff line change
@@ -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<FleetServerHostsFlyoutProps> = ({
onClose,
fleetServerHosts,
}) => {
const { docLinks } = useStartServices();

const form = useFleetServerHostsForm(fleetServerHosts, onClose);

return (
<EuiFlyout maxWidth={FLYOUT_MAX_WIDTH} onClose={onClose}>
<EuiFlyoutHeader hasBorder={true}>
<EuiTitle size="m">
<h2 id="FleetPackagePolicyPreviousVersionFlyoutTitle">
<FormattedMessage
id="xpack.fleet.settings.fleetServerHostsFlyout.title"
defaultMessage="Fleet Server hosts"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiText color="subdued">
<FormattedMessage
id="xpack.fleet.settings.fleetServerHostsFlyout.description"
defaultMessage="Specify the URLs that your agents will use to connect to a Fleet Server. If multiple URLs exist, Fleet shows the first provided URL for enrollment purposes. Fleet Server uses port 8220 by default. Refer to the {link}."
values={{
link: (
<EuiLink
href={docLinks.links.fleet.settingsFleetServerHostSettings}
target="_blank"
external
>
<FormattedMessage
id="xpack.fleet.settings.fleetServerHostsFlyout.userGuideLink"
defaultMessage="Fleet and Elastic Agent Guide"
/>
</EuiLink>
),
}}
/>
</EuiText>
<HostsInput {...form.fleetServerHostsInput.props} id="fleet-server-inputs" />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={() => onClose()} flush="left">
<FormattedMessage
id="xpack.fleet.settings.fleetServerHostsFlyout.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
isLoading={form.isLoading}
isDisabled={form.isLoading}
onClick={form.submit}
>
<FormattedMessage
id="xpack.fleet.settings.fleetServerHostsFlyout.saveButton"
defaultMessage="Save and apply settings"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};
Original file line number Diff line number Diff line change
@@ -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,
};
}
Original file line number Diff line number Diff line change
@@ -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<string[]>([]);
return (
<div style={{ width }}>
<Component
id="test-host-input"
helpText={helpText}
value={value}
onChange={setValue}
label={label}
/>
</div>
);
};

HostsInput.args = args;
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
Loading

0 comments on commit 00df969

Please sign in to comment.