Skip to content

Commit

Permalink
[Fleet] implements new design for outputs UI (#118910)
Browse files Browse the repository at this point in the history
  • Loading branch information
nchaulet authored and dmlemeshko committed Nov 29, 2021
1 parent 8d96d32 commit 6b2838a
Show file tree
Hide file tree
Showing 36 changed files with 1,627 additions and 672 deletions.
3 changes: 3 additions & 0 deletions x-pack/plugins/fleet/common/services/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ export const outputRoutesService = {
getUpdatePath: (outputId: string) =>
OUTPUT_API_ROUTES.UPDATE_PATTERN.replace('{outputId}', outputId),
getListPath: () => OUTPUT_API_ROUTES.LIST_PATTERN,
getDeletePath: (outputId: string) =>
OUTPUT_API_ROUTES.DELETE_PATTERN.replace('{outputId}', outputId),
getCreatePath: () => OUTPUT_API_ROUTES.CREATE_PATTERN,
};

export const settingsRoutesService = {
Expand Down
18 changes: 17 additions & 1 deletion x-pack/plugins/fleet/common/types/rest_spec/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,25 @@ export interface PutOutputRequest {
outputId: string;
};
body: {
type?: 'elasticsearch';
name?: string;
hosts?: string[];
ca_sha256?: string;
config?: Record<string, any>;
config_yaml?: string;
is_default?: boolean;
is_default_monitoring?: boolean;
};
}

export interface PostOutputRequest {
body: {
id?: string;
type: 'elasticsearch';
name: string;
hosts?: string[];
ca_sha256?: string;
is_default?: boolean;
is_default_monitoring?: boolean;
config_yaml?: string;
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/*
* 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,
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiButton,
EuiForm,
EuiFormRow,
EuiFieldText,
EuiSelect,
EuiSwitch,
EuiCallOut,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import { HostsInput } from '../hosts_input';
import type { Output } from '../../../../types';
import { FLYOUT_MAX_WIDTH } from '../../constants';

import { YamlCodeEditorWithPlaceholder } from './yaml_code_editor_with_placeholder';
import { useOutputForm } from './use_output_form';

export interface EditOutputFlyoutProps {
output?: Output;
onClose: () => void;
}

export const EditOutputFlyout: React.FunctionComponent<EditOutputFlyoutProps> = ({
onClose,
output,
}) => {
const form = useOutputForm(onClose, output);
const inputs = form.inputs;

return (
<EuiFlyout maxWidth={FLYOUT_MAX_WIDTH} onClose={onClose}>
<EuiFlyoutHeader hasBorder={true}>
<EuiTitle size="m">
<h2 id="FleetEditOutputFlyoutTitle">
{!output ? (
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.createTitle"
defaultMessage="Add new output"
/>
) : (
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.editTitle"
defaultMessage="Edit output"
/>
)}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{output?.is_preconfigured && (
<>
<EuiCallOut
iconType="lock"
title={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.preconfiguredOutputCalloutTitle"
defaultMessage="This output is managed outside of Fleet"
/>
}
>
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.preconfiguredOutputCalloutDescription"
defaultMessage="Most actions related to this output are unavailable. Refer to your kibana config for more
detail."
/>
</EuiCallOut>
<EuiSpacer size="m" />
</>
)}
<EuiForm>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.nameInputLabel"
defaultMessage="Name"
/>
}
{...inputs.nameInput.formRowProps}
>
<EuiFieldText
fullWidth
{...inputs.nameInput.props}
placeholder={i18n.translate(
'xpack.fleet.settings.editOutputFlyout.nameInputPlaceholder',
{
defaultMessage: 'Specify name',
}
)}
/>
</EuiFormRow>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.typeInputLabel"
defaultMessage="Type"
/>
}
>
<EuiSelect
fullWidth
{...inputs.typeInput.props}
options={[{ value: 'elasticsearch', text: 'Elasticsearch' }]}
placeholder={i18n.translate(
'xpack.fleet.settings.editOutputFlyout.typeInputPlaceholder',
{
defaultMessage: 'Specify type',
}
)}
/>
</EuiFormRow>
<HostsInput
label={i18n.translate('xpack.fleet.settings.editOutputFlyout.hostsInputLabel', {
defaultMessage: 'Hosts',
})}
{...inputs.elasticsearchUrlInput.props}
/>
<EuiFormRow
label={i18n.translate('xpack.fleet.settings.editOutputFlyout.yamlConfigInputLabel', {
defaultMessage: 'Advanced YAML configuration',
})}
{...inputs.additionalYamlConfigInput.formRowProps}
fullWidth
>
<YamlCodeEditorWithPlaceholder
value={inputs.additionalYamlConfigInput.value}
onChange={inputs.additionalYamlConfigInput.setValue}
disabled={inputs.additionalYamlConfigInput.props.disabled}
placeholder={i18n.translate(
'xpack.fleet.settings.editOutputFlyout.yamlConfigInputPlaceholder',
{
defaultMessage:
'# YAML settings here will be added to the Elasticsearch output section of each agent policy.',
}
)}
/>
</EuiFormRow>
<EuiFormRow fullWidth {...inputs.defaultOutputInput.formRowProps}>
<EuiSwitch
{...inputs.defaultOutputInput.props}
label={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.defaultOutputSwitchLabel"
defaultMessage="Make this output the default for {boldAgentIntegrations}."
values={{
boldAgentIntegrations: (
<strong>
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.agentIntegrationsBold"
defaultMessage="agent integrations"
/>
</strong>
),
}}
/>
}
/>
</EuiFormRow>
<EuiFormRow fullWidth {...inputs.defaultMonitoringOutputInput.formRowProps}>
<EuiSwitch
{...inputs.defaultMonitoringOutputInput.props}
label={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.defaultMontoringOutputSwitchLabel"
defaultMessage="Make this output the default for {boldAgentMonitoring}."
values={{
boldAgentMonitoring: (
<strong>
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.agentMonitoringBold"
defaultMessage="agent monitoring"
/>
</strong>
),
}}
/>
}
/>
</EuiFormRow>
</EuiForm>
</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.isDisabled}
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,75 @@
/*
* 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 { validateHosts, validateYamlConfig } from './output_form_validators';

describe('Output form validation', () => {
describe('validateHosts', () => {
it('should work without any urls', () => {
const res = validateHosts([]);

expect(res).toBeUndefined();
});

it('should work with valid url', () => {
const res = validateHosts(['https://test.fr:9200']);

expect(res).toBeUndefined();
});

it('should return an error with invalid url', () => {
const res = validateHosts(['toto']);

expect(res).toEqual([{ index: 0, message: 'Invalid URL' }]);
});

it('should return an error with url with invalid port', () => {
const res = validateHosts(['https://test.fr:qwerty9200']);

expect(res).toEqual([{ index: 0, message: 'Invalid URL' }]);
});

it('should return an error with multiple invalid urls', () => {
const res = validateHosts(['toto', 'tata']);

expect(res).toEqual([
{ index: 0, message: 'Invalid URL' },
{ index: 1, message: 'Invalid URL' },
]);
});
it('should return an error with duplicate urls', () => {
const res = validateHosts(['http://test.fr', 'http://test.fr']);

expect(res).toEqual([
{ index: 0, message: 'Duplicate URL' },
{ index: 1, message: 'Duplicate URL' },
]);
});
});
describe('validateYamlConfig', () => {
it('should work with an empty yaml', () => {
const res = validateYamlConfig(``);

expect(res).toBeUndefined();
});

it('should work with valid yaml', () => {
const res = validateYamlConfig(`test: 123`);

expect(res).toBeUndefined();
});

it('should return an error with invalid yaml', () => {
const res = validateYamlConfig(`{}}`);

expect(res).toBeDefined();
if (typeof res !== 'undefined') {
expect(res[0]).toContain('Invalid YAML: ');
}
});
});
});
Loading

0 comments on commit 6b2838a

Please sign in to comment.